Skip to content

Commit 6971cd7

Browse files
committed
Add test cases documenting Counter rerendering issue
Created unit tests that demonstrate the architectural issue with nested components and channel listeners: - TestCounterChannelListener: Shows channel updates work when listener is manually started - TestCounterChannelMultipleValues: Verifies multiple updates process correctly - TestCounterCreationProblem: Demonstrates the core issue - Counter instances created inline in Render() never have BindApp called, so channel listeners are never started Root cause: VarDecls in component bodies are generated inside Render() closures, causing channels and components to be recreated on every render instead of persisting. This leads to goroutine leaks and the counter not updating in e2e tests. Required fix: Hoist stateful VarDecls (channels, make() calls, component instances) to component struct fields so they persist across renders.
1 parent b8352ab commit 6971cd7

1 file changed

Lines changed: 120 additions & 0 deletions

File tree

examples/counter/counter_test.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package main
2+
3+
import (
4+
"testing"
5+
"time"
6+
)
7+
8+
// TestCounterChannelListener tests that the channel listener updates the counter value
9+
func TestCounterChannelListener(t *testing.T) {
10+
// Create a counter with a channel
11+
counterChan := make(chan int, 10)
12+
counter := NewCounter(WithCounterChannel(counterChan))
13+
14+
// Manually start the listener (simulating what BindApp does)
15+
updateCalled := false
16+
go func() {
17+
for val := range counter.CounterChannel {
18+
counter.currentCounterChannel = val
19+
updateCalled = true
20+
}
21+
}()
22+
23+
// Give the goroutine time to start
24+
time.Sleep(10 * time.Millisecond)
25+
26+
// Send a value to the channel
27+
counterChan <- 42
28+
29+
// Give the goroutine time to process
30+
time.Sleep(10 * time.Millisecond)
31+
32+
// Verify the counter value was updated
33+
if counter.currentCounterChannel != 42 {
34+
t.Errorf("Expected currentCounterChannel to be 42, got %d", counter.currentCounterChannel)
35+
}
36+
37+
if !updateCalled {
38+
t.Error("Expected update to be triggered when channel receives value")
39+
}
40+
41+
close(counterChan)
42+
}
43+
44+
// TestCounterChannelMultipleValues tests that multiple values update the counter correctly
45+
func TestCounterChannelMultipleValues(t *testing.T) {
46+
counterChan := make(chan int, 10)
47+
counter := NewCounter(WithCounterChannel(counterChan))
48+
49+
updateCount := 0
50+
go func() {
51+
for val := range counter.CounterChannel {
52+
counter.currentCounterChannel = val
53+
updateCount++
54+
}
55+
}()
56+
57+
time.Sleep(10 * time.Millisecond)
58+
59+
// Send multiple values
60+
values := []int{10, 25, 100}
61+
for _, v := range values {
62+
counterChan <- v
63+
time.Sleep(10 * time.Millisecond)
64+
65+
if counter.currentCounterChannel != v {
66+
t.Errorf("Expected currentCounterChannel to be %d, got %d", v, counter.currentCounterChannel)
67+
}
68+
}
69+
70+
// Verify all values were processed
71+
if updateCount != len(values) {
72+
t.Errorf("Expected %d updates, got %d", len(values), updateCount)
73+
}
74+
75+
close(counterChan)
76+
}
77+
78+
// TestCounterWithoutChannel tests that counter works without a channel
79+
func TestCounterWithoutChannel(t *testing.T) {
80+
counter := NewCounter()
81+
82+
// Should have zero value
83+
if counter.currentCounterChannel != 0 {
84+
t.Errorf("Expected currentCounterChannel to be 0, got %d", counter.currentCounterChannel)
85+
}
86+
87+
// Counter should be created successfully without channel
88+
if counter.CounterChannel != nil {
89+
t.Error("Expected CounterChannel to be nil when not provided")
90+
}
91+
}
92+
93+
// TestCounterCreationProblem demonstrates the issue with inline Counter creation
94+
func TestCounterCreationProblem(t *testing.T) {
95+
t.Log("This test demonstrates the problem with how Counter is created in App.Render()")
96+
97+
// This is what happens in App.Render() - a new Counter is created each time
98+
createCounterInline := func() *Counter {
99+
counterChan := make(chan int, 10)
100+
counter := NewCounter(WithCounterChannel(counterChan))
101+
// BindApp is NEVER called, so the listener is never started
102+
return counter
103+
}
104+
105+
counter1 := createCounterInline()
106+
107+
// Send a value to the channel
108+
if counter1.CounterChannel != nil {
109+
counter1.CounterChannel <- 42
110+
time.Sleep(10 * time.Millisecond)
111+
112+
// The value will NOT be updated because BindApp was never called
113+
if counter1.currentCounterChannel == 42 {
114+
t.Error("Counter updated without BindApp being called - this shouldn't happen")
115+
} else {
116+
t.Log("CONFIRMED: Counter does NOT update when BindApp is not called")
117+
t.Log("This is why the e2e tests are failing!")
118+
}
119+
}
120+
}

0 commit comments

Comments
 (0)