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
102// TestCommittedLayoutSurvivesNextBuild guards the layout arena rotation: the
103// view reads the committed layout while the next one is built, and a frame
104// that does not commit must leave it intact.
105func TestCommittedLayoutSurvivesNextBuild(t *testing.T) {
106 r := newRuntime()
107 view := func(label string) func(*Frame) {
108 return func(f *Frame) { f.Root().Box(Key(label), Inline("width:10px;height:10px;background:#fff")) }
109 }
110 // Warm both arenas: a fresh one allocates detached results, which would
111 // hide an overwrite.
112 for _, label := range []string{"w1", "w2", "a"} {
113 r.Redraw()
114 if !r.Build(view(label)) {
115 t.Fatalf("warm-up build %q skipped", label)
116 }
117 }
118 snapshot := func() (string, int) {
119 leaf := r.output.Tree
120 for len(leaf.Children) > 0 {
121 leaf = leaf.Children[0]
122 }
123 return leaf.ID, len(r.output.Display)
124 }
125 id, cmds := snapshot()
126 // Building the next frame must not write into the previous output, which
127 // the view, hit-testing and the renderer read until the commit.
128 prevTree, prevDisplay := r.output.Tree, r.output.Display
129 first := prevDisplay[0].ID
130 r.Redraw()
131 r.Build(view("b"))
132 leaf := prevTree
133 for len(leaf.Children) > 0 {
134 leaf = leaf.Children[0]
135 }
136 if leaf.ID != id || len(prevDisplay) != cmds || prevDisplay[0].ID != first {
137 t.Fatalf("next build overwrote the previous layout: leaf %q (want %q), %d commands (want %d), first %q (want %q)", leaf.ID, id, len(prevDisplay), cmds, prevDisplay[0].ID, first)
138 }
139 idB, _ := snapshot()
140 if idB == id {
141 t.Fatalf("second build not committed: %q", idB)
142 }
143 // A panicking view does not commit: the output stays readable and intact.
144 func() {
145 defer func() { _ = recover() }()
146 r.Redraw()
147 r.Build(func(f *Frame) { view("c")(f); panic("view failure") })
148 }()
149 if got, _ := snapshot(); got != idB {
150 t.Fatalf("panicked frame replaced the output: %q, want %q", got, idB)
151 }
152 // A failed layout writes into the free arena but commits nothing.
153 r.Redraw()
154 if r.Build(func(f *Frame) {
155 n := f.Root()
156 for range 300 { // deeper than layout allows
157 n = n.Box()
158 }
159 }) {
160 t.Fatal("too deep layout committed")
161 }
162 if got, _ := snapshot(); got != idB {
163 t.Fatalf("failed layout replaced the output: %q, want %q", got, idB)
164 }
165 // Rotation still alternates after both failures: each build keeps the
166 // previously committed layout intact.
167 for _, label := range []string{"d", "e"} {
168 prevTree, prevDisplay, before := r.output.Tree, r.output.Display, idB
169 first, cmds := prevDisplay[0].ID, len(prevDisplay)
170 r.Redraw()
171 if !r.Build(view(label)) {
172 t.Fatalf("build %q skipped", label)
173 }
174 leaf := prevTree
175 for len(leaf.Children) > 0 {
176 leaf = leaf.Children[0]
177 }
178 if leaf.ID != before || len(prevDisplay) != cmds || prevDisplay[0].ID != first {
179 t.Fatalf("build %q overwrote the previous layout: leaf %q (want %q), first command %q (want %q)", label, leaf.ID, before, prevDisplay[0].ID, first)
180 }
181 idB, _ = snapshot()
182 }
183}
184
185func TestConcurrentBuildDefersCommit(t *testing.T) {
186 r := newRuntime()
187 entered := make(chan struct{})
188 release := make(chan struct{})
189 firstDone := make(chan bool, 1)
190 secondDone := make(chan bool, 1)
191 go func() { firstDone <- r.Build(func(f *Frame) { f.Root().Box(Key("first")); close(entered); <-release }) }()
192 waitEntered(t, entered)
193 go func() {
194 secondDone <- r.Build(func(f *Frame) { t.Error("busy view executed"); f.Root().Box(Key("second")) })
195 }()
196 select {
197 case built := <-secondDone:
198 if built {
199 t.Fatal("busy build committed")
200 }
201 case <-time.After(time.Second):
202 t.Fatal("concurrent Build blocked")
203 }
204 if r.Target(0) != nil {
205 t.Fatal("uncommitted tree visible")
206 }
207 close(release)
208 select {
209 case built := <-firstDone:
210 if !built {
211 t.Fatal("first build skipped")
212 }
213 case <-time.After(time.Second):
214 t.Fatal("first build did not finish")
215 }
216 if id := r.Target(0); id == nil || id.key != "first" {
217 t.Fatalf("commit out of order: %+v", id)
218 }
219 ctx, cancel := context.WithTimeout(context.Background(), time.Second)
220 defer cancel()
221 if err := r.Wait(ctx); err != nil {
222 t.Fatalf("lost wake: %v", err)
223 }
224 if !r.Build(func(f *Frame) { f.Root().Box(Key("second")) }) {
225 t.Fatal("redraw skipped")
226 }
227 if id := r.Target(0); id == nil || id.key != "second" {
228 t.Fatalf("follow-up commit missing: %+v", id)
229 }
230}
231
232func TestReentrantBuildSchedulesQueuedEvents(t *testing.T) {
233 r := newRuntime()
234 r.Build(func(f *Frame) { f.Root().Button("target", Key("target")) })
235 target := r.Target(0)
236 r.Redraw()
237 finished := make(chan bool, 1)
238 go func() {
239 finished <- r.Build(func(f *Frame) {
240 f.Root().Button("target", Key("target"))
241 r.Queue(inputEvent{Target: target, Kind: "activate"})
242 if r.Build(func(*Frame) { t.Error("reentrant view executed") }) {
243 t.Error("reentrant build committed")
244 }
245 })
246 }()
247 select {
248 case built := <-finished:
249 if !built {
250 t.Fatal("outer build skipped")
251 }
252 case <-time.After(time.Second):
253 t.Fatal("reentrant Build deadlocked")
254 }
255 ctx, cancel := context.WithTimeout(context.Background(), time.Second)
256 defer cancel()
257 if err := r.Wait(ctx); err != nil {
258 t.Fatalf("follow-up wake lost: %v", err)
259 }
260 if !r.Build(func(f *Frame) {
261 if !f.Root().Button("target", Key("target")).Activated() {
262 t.Error("queued event not delivered")
263 }
264 }) {
265 t.Fatal("follow-up skipped")
266 }
267 assertOneSettleFrame(t, r, func(f *Frame) { f.Root().Button("target", Key("target")) })
268}
269
270// assertOneSettleFrame checks that an event batch schedules exactly one
271// event-free follow-up build, and that this build schedules nothing more.
272func assertOneSettleFrame(t *testing.T, r *runtime, view func(*Frame)) {
273 t.Helper()
274 settled := false
275 if !r.Build(func(f *Frame) {
276 settled = true
277 if len(f.events) != 0 {
278 t.Error("settle frame carried events")
279 }
280 view(f)
281 }) || !settled {
282 t.Fatal("no settle frame after events")
283 }
284 if r.Build(func(*Frame) { t.Error("extra view executed") }) {
285 t.Fatal("extra redraw after settle frame")
286 }
287}
288
289func TestQueueDuringConstruction(t *testing.T) {
290 r := newRuntime()
291 r.Build(func(f *Frame) { f.Root().Button("target", Key("target")) })
292 target := r.Target(0)
293 r.Queue(inputEvent{Target: target, Kind: "activate"})
294 entered := make(chan struct{})
295 release := make(chan struct{})
296 result := make(chan bool, 1)
297 go func() {
298 result <- r.Build(func(f *Frame) {
299 root := f.Root()
300 if !root.Button("target", Key("target")).Activated() {
301 t.Error("first batch missing")
302 }
303 close(entered)
304 <-release
305 })
306 }()
307 waitEntered(t, entered)
308 r.Queue(inputEvent{Target: target, Kind: "activate"})
309 r.Redraw()
310 close(release)
311 if !<-result {
312 t.Fatal("first batch skipped")
313 }
314 ctx, cancel := context.WithTimeout(context.Background(), time.Second)
315 defer cancel()
316 if err := r.Wait(ctx); err != nil {
317 t.Fatalf("lost wake during build: %v", err)
318 }
319 if !r.Build(func(f *Frame) {
320 if !f.Root().Button("target", Key("target")).Activated() {
321 t.Error("event queued during construction lost")
322 }
323 }) {
324 t.Fatal("second batch skipped")
325 }
326 assertOneSettleFrame(t, r, func(f *Frame) { f.Root().Button("target", Key("target")) })
327}
328
329func TestCheckboxElementAndActivation(t *testing.T) {
330 r := newRuntime()
331 checked := false
332 build := func(f *Frame) ChangeEvent { root := f.Root(); return root.Checkbox("accept", &checked, Key("accept")) }
333 r.Build(func(f *Frame) {
334 if build(f).Changed() {
335 t.Fatal("spurious change")
336 }
337 })
338 if e := r.committed.children[0]; e.typ != "checkbox" || e.role != "checkbox" || e.text != "accept" {
339 t.Fatalf("wrong semantics: %+v", e)
340 }
341 r.Queue(inputEvent{Target: r.Target(0), Kind: "activate"})
342 r.Build(func(f *Frame) {
343 ev := build(f)
344 changed := ev.Changed()
345 if !changed || ev.Changed() != changed || !checked {
346 t.Fatal("checkbox activation missing")
347 }
348 })
349 if e := r.committed.children[0]; e.typ != "checkbox" || e.role != "checkbox" {
350 t.Fatalf("wrong semantics after activation: %+v", e)
351 }
352 r.Queue(inputEvent{Target: r.Target(0), Kind: "activate"})
353 r.Build(func(f *Frame) { f.Root().Checkbox("accept", &checked, Key("accept"), Disabled(true)) })
354 if !checked {
355 t.Fatal("disabled checkbox mutated model")
356 }
357}
358
359func TestHeadingLevel(t *testing.T) {
360 r := newRuntime()
361 r.Build(func(f *Frame) { f.Root().Header().Heading("title", Level(1), Class("logo")) })
362 e := r.committed.children[0].children[0]
363 if e.typ != "heading" || e.role != "heading" || e.level != 1 || e.text != "title" || len(e.classes) != 1 || e.classes[0] != "logo" {
364 t.Fatalf("heading metadata: %+v", e)
365 }
366 defer func() {
367 if recover() == nil {
368 t.Error("invalid level accepted")
369 }
370 }()
371 Level(7)
372}