1package ui
2
3import (
4 "context"
5 "testing"
6 "time"
7)
8
9func waitEntered(t *testing.T, entered <-chan struct{}) {
10 t.Helper()
11 select {
12 case <-entered:
13 case <-time.After(time.Second):
14 t.Fatal("view did not start")
15 }
16}
17
18func TestIdentityAndEvents(t *testing.T) {
19 r := newRuntime()
20 order := []string{"a", "b"}
21 var received []string
22 view := func(f *Frame) {
23 root := f.Root()
24 for _, key := range order {
25 if root.Button(key, Key(key)).Activated() {
26 received = append(received, key)
27 }
28 }
29 }
30 r.Build(view)
31 id := r.Target(1)
32 order = []string{"b", "a"}
33 r.Queue(inputEvent{Target: id, Kind: "activate"})
34 r.Queue(inputEvent{Target: id, Kind: "activate"})
35 if err := r.Wait(context.Background()); err != nil {
36 t.Fatal(err)
37 }
38 builds := 0
39 r.Build(func(f *Frame) { builds++; view(f) })
40 if builds != 1 || len(received) != 1 || received[0] != "b" {
41 t.Fatalf("batch: %v builds %d target=%+v first=%+v", received, builds, id, r.Target(0))
42 }
43 r.Redraw()
44 r.Build(view)
45 if len(received) != 1 {
46 t.Fatalf("expired event: %v", received)
47 }
48}
49func TestImplicitIdentity(t *testing.T) {
50 r := newRuntime()
51 r.Build(func(f *Frame) { f.Root().Box().Text("x") })
52 first := r.Target(0)
53 r.Redraw()
54 r.Build(func(f *Frame) { f.Root().Box().Text("y") })
55 if !first.same(r.Target(0)) {
56 t.Fatal("position/type identity changed")
57 }
58 r.Redraw()
59 r.Build(func(f *Frame) {
60 f.Root().Row()
61 if debugDiagnostics && len(f.Diagnostics()) != 1 {
62 t.Fatalf("missing unkeyed change diagnostic: %v", f.Diagnostics())
63 }
64 })
65 if first.same(r.Target(0)) {
66 t.Fatal("type omitted from identity")
67 }
68}
69func TestStaleAndDuplicates(t *testing.T) {
70 r := newRuntime()
71 var old Node
72 r.Build(func(f *Frame) {
73 old = f.Root()
74 old.Box(Key("dup"))
75 old.Box(Key("dup"))
76 if debugDiagnostics && len(f.Diagnostics()) != 1 {
77 t.Fatal(f.Diagnostics())
78 }
79 })
80 if old.valid() {
81 t.Fatal("stale node accepted")
82 }
83 if debugDiagnostics {
84 defer func() {
85 if recover() == nil {
86 t.Error("missing stale panic")
87 }
88 }()
89 old.Box()
90 }
91}
92func TestWake(t *testing.T) {
93 r := newRuntime()
94 ctx, cancel := context.WithTimeout(context.Background(), time.Second)
95 defer cancel()
96 r.Redraw()
97 if err := r.Wait(ctx); err != nil {
98 t.Fatal(err)
99 }
100}
101
102func TestConcurrentBuildDefersCommit(t *testing.T) {
103 r := newRuntime()
104 entered := make(chan struct{})
105 release := make(chan struct{})
106 firstDone := make(chan bool, 1)
107 secondDone := make(chan bool, 1)
108 go func() { firstDone <- r.Build(func(f *Frame) { f.Root().Box(Key("first")); close(entered); <-release }) }()
109 waitEntered(t, entered)
110 go func() {
111 secondDone <- r.Build(func(f *Frame) { t.Error("busy view executed"); f.Root().Box(Key("second")) })
112 }()
113 select {
114 case built := <-secondDone:
115 if built {
116 t.Fatal("busy build committed")
117 }
118 case <-time.After(time.Second):
119 t.Fatal("concurrent Build blocked")
120 }
121 if r.Target(0) != nil {
122 t.Fatal("uncommitted tree visible")
123 }
124 close(release)
125 select {
126 case built := <-firstDone:
127 if !built {
128 t.Fatal("first build skipped")
129 }
130 case <-time.After(time.Second):
131 t.Fatal("first build did not finish")
132 }
133 if id := r.Target(0); id == nil || id.key != "first" {
134 t.Fatalf("commit out of order: %+v", id)
135 }
136 ctx, cancel := context.WithTimeout(context.Background(), time.Second)
137 defer cancel()
138 if err := r.Wait(ctx); err != nil {
139 t.Fatalf("lost wake: %v", err)
140 }
141 if !r.Build(func(f *Frame) { f.Root().Box(Key("second")) }) {
142 t.Fatal("redraw skipped")
143 }
144 if id := r.Target(0); id == nil || id.key != "second" {
145 t.Fatalf("follow-up commit missing: %+v", id)
146 }
147}
148
149func TestReentrantBuildSchedulesQueuedEvents(t *testing.T) {
150 r := newRuntime()
151 r.Build(func(f *Frame) { f.Root().Button("target", Key("target")) })
152 target := r.Target(0)
153 r.Redraw()
154 finished := make(chan bool, 1)
155 go func() {
156 finished <- r.Build(func(f *Frame) {
157 f.Root().Button("target", Key("target"))
158 r.Queue(inputEvent{Target: target, Kind: "activate"})
159 if r.Build(func(*Frame) { t.Error("reentrant view executed") }) {
160 t.Error("reentrant build committed")
161 }
162 })
163 }()
164 select {
165 case built := <-finished:
166 if !built {
167 t.Fatal("outer build skipped")
168 }
169 case <-time.After(time.Second):
170 t.Fatal("reentrant Build deadlocked")
171 }
172 ctx, cancel := context.WithTimeout(context.Background(), time.Second)
173 defer cancel()
174 if err := r.Wait(ctx); err != nil {
175 t.Fatalf("follow-up wake lost: %v", err)
176 }
177 if !r.Build(func(f *Frame) {
178 if !f.Root().Button("target", Key("target")).Activated() {
179 t.Error("queued event not delivered")
180 }
181 }) {
182 t.Fatal("follow-up skipped")
183 }
184 assertOneSettleFrame(t, r, func(f *Frame) { f.Root().Button("target", Key("target")) })
185}
186
187// assertOneSettleFrame checks that an event batch schedules exactly one
188// event-free follow-up build, and that this build schedules nothing more.
189func assertOneSettleFrame(t *testing.T, r *runtime, view func(*Frame)) {
190 t.Helper()
191 settled := false
192 if !r.Build(func(f *Frame) {
193 settled = true
194 if len(f.events) != 0 {
195 t.Error("settle frame carried events")
196 }
197 view(f)
198 }) || !settled {
199 t.Fatal("no settle frame after events")
200 }
201 if r.Build(func(*Frame) { t.Error("extra view executed") }) {
202 t.Fatal("extra redraw after settle frame")
203 }
204}
205
206func TestQueueDuringConstruction(t *testing.T) {
207 r := newRuntime()
208 r.Build(func(f *Frame) { f.Root().Button("target", Key("target")) })
209 target := r.Target(0)
210 r.Queue(inputEvent{Target: target, Kind: "activate"})
211 entered := make(chan struct{})
212 release := make(chan struct{})
213 result := make(chan bool, 1)
214 go func() {
215 result <- r.Build(func(f *Frame) {
216 root := f.Root()
217 if !root.Button("target", Key("target")).Activated() {
218 t.Error("first batch missing")
219 }
220 close(entered)
221 <-release
222 })
223 }()
224 waitEntered(t, entered)
225 r.Queue(inputEvent{Target: target, Kind: "activate"})
226 r.Redraw()
227 close(release)
228 if !<-result {
229 t.Fatal("first batch skipped")
230 }
231 ctx, cancel := context.WithTimeout(context.Background(), time.Second)
232 defer cancel()
233 if err := r.Wait(ctx); err != nil {
234 t.Fatalf("lost wake during build: %v", err)
235 }
236 if !r.Build(func(f *Frame) {
237 if !f.Root().Button("target", Key("target")).Activated() {
238 t.Error("event queued during construction lost")
239 }
240 }) {
241 t.Fatal("second batch skipped")
242 }
243 assertOneSettleFrame(t, r, func(f *Frame) { f.Root().Button("target", Key("target")) })
244}
245
246func TestCheckboxElementAndActivation(t *testing.T) {
247 r := newRuntime()
248 checked := false
249 build := func(f *Frame) ChangeEvent { root := f.Root(); return root.Checkbox("accept", &checked, Key("accept")) }
250 r.Build(func(f *Frame) {
251 if build(f).Changed() {
252 t.Fatal("spurious change")
253 }
254 })
255 if e := r.committed.children[0]; e.typ != "checkbox" || e.role != "checkbox" || e.text != "accept" {
256 t.Fatalf("wrong semantics: %+v", e)
257 }
258 r.Queue(inputEvent{Target: r.Target(0), Kind: "activate"})
259 r.Build(func(f *Frame) {
260 ev := build(f)
261 changed := ev.Changed()
262 if !changed || ev.Changed() != changed || !checked {
263 t.Fatal("checkbox activation missing")
264 }
265 })
266 if e := r.committed.children[0]; e.typ != "checkbox" || e.role != "checkbox" {
267 t.Fatalf("wrong semantics after activation: %+v", e)
268 }
269 r.Queue(inputEvent{Target: r.Target(0), Kind: "activate"})
270 r.Build(func(f *Frame) { f.Root().Checkbox("accept", &checked, Key("accept"), Disabled(true)) })
271 if !checked {
272 t.Fatal("disabled checkbox mutated model")
273 }
274}
275
276func TestHeadingLevel(t *testing.T) {
277 r := newRuntime()
278 r.Build(func(f *Frame) { f.Root().Header().Heading("title", Level(1), Class("logo")) })
279 e := r.committed.children[0].children[0]
280 if e.typ != "heading" || e.role != "heading" || e.level != 1 || e.text != "title" || len(e.classes) != 1 || e.classes[0] != "logo" {
281 t.Fatalf("heading metadata: %+v", e)
282 }
283 defer func() {
284 if recover() == nil {
285 t.Error("invalid level accepted")
286 }
287 }()
288 Level(7)
289}