1//go:build linux
2
3package ui
4
5import (
6 "errors"
7 "fmt"
8 "math"
9 "strings"
10 "testing"
11
12 "github.com/bnema/nefergui/internal/css"
13 "github.com/bnema/nefergui/internal/layout"
14 "github.com/bnema/nefergui/internal/presentation/session"
15 "github.com/bnema/nefergui/internal/text"
16 "github.com/stretchr/testify/mock"
17)
18
19type testModel struct {
20 clicks int
21 rects []Rect
22}
23
24func testView(f *Frame, m *testModel) {
25 root := f.Root()
26 if root.Button("Go", Key("go"), Inline("width:60px;height:20px")).Activated() {
27 m.clicks++
28 }
29 if m.rects != nil {
30 _ = f.SetInputRects(m.rects)
31 }
32}
33
34func newTestRenderer(t *testing.T) (*Renderer, *mocktarget) {
35 t.Helper()
36 tg := newMocktarget(t)
37 r, err := newRenderer(RendererConfig{}, tg, text.DirectorySource("../../testdata/fonts"))
38 if err != nil {
39 t.Fatal(err)
40 }
41 return r, tg
42}
43
44func drawOK(tg *mocktarget, fill func(*session.Output)) *mocktarget_Draw_Call {
45 return tg.EXPECT().Draw(mock.Anything, mock.Anything, mock.Anything).
46 RunAndReturn(func(_ []layout.Command, _ float64, out *session.Output) (bool, error) {
47 *out = session.Output{Buffer: 1, Width: 100, Height: 50}
48 if fill != nil {
49 fill(out)
50 }
51 return true, nil
52 })
53}
54
55func TestKeysymNames(t *testing.T) {
56 for sym, want := range map[uint32]string{
57 0x20: "space", 0xff09: "tab", 0xfe20: "iso_left_tab", 0xff51: "left", 0xff53: "right",
58 0xff52: "up", 0xff54: "down", 0xff50: "home", 0xff57: "end", 0xff55: "page_up",
59 0xff56: "page_down", 0xff08: "backspace", 0xffff: "delete", 'a': "a", 'A': "a",
60 'c': "c", 'x': "x", 'v': "v", 0xff0d: "return", 0xff8d: "kp_enter", 0xff1b: "escape",
61 'z': "", 0xffe1: "",
62 } {
63 if got := keysymName(sym); got != want {
64 t.Errorf("keysym %#x: got %q want %q", sym, got, want)
65 }
66 }
67}
68
69func TestRendererRenderBeforeResizeAndWhenUnchanged(t *testing.T) {
70 r, tg := newTestRenderer(t)
71 var m testModel
72 var out Output
73 if ok, err := r.Render(&out, &m, testView); ok || err != nil {
74 t.Fatalf("before Resize: ok=%v err=%v", ok, err)
75 }
76 tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once()
77 r.Resize(100, 50, 2)
78 drawOK(tg, nil).Once()
79 if ok, err := r.Render(&out, &m, testView); !ok || err != nil {
80 t.Fatalf("first render: ok=%v err=%v", ok, err)
81 }
82 if out.Buffer != 1 || out.Width != 100 || len(out.Damage) != 1 || out.Damage[0] != (Rect{Width: 100, Height: 50}) {
83 t.Fatalf("output: %+v", out)
84 }
85 // Nothing changed: no rebuild result, so no Draw (the mock has no further expectation).
86 if ok, err := r.Render(&out, &m, testView); ok || err != nil {
87 t.Fatalf("unchanged view: ok=%v err=%v", ok, err)
88 }
89}
90
91func TestRendererKeepsFrameWhenNoBufferFree(t *testing.T) {
92 r, tg := newTestRenderer(t)
93 var m testModel
94 var out Output
95 tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once()
96 r.Resize(100, 50, 1)
97 tg.EXPECT().Draw(mock.Anything, mock.Anything, mock.Anything).Return(false, nil).Once()
98 tg.EXPECT().Waiting().Return(true).Once()
99 if ok, err := r.Render(&out, &m, testView); ok || err != nil {
100 t.Fatalf("no buffer: ok=%v err=%v", ok, err)
101 }
102 if !r.Pending() {
103 t.Fatal("a built frame blocked on the GPU must report Pending")
104 }
105 drawOK(tg, nil).Once()
106 if ok, err := r.Render(&out, &m, testView); !ok || err != nil {
107 t.Fatalf("retry: ok=%v err=%v", ok, err)
108 }
109}
110
111func TestRendererDrawError(t *testing.T) {
112 r, tg := newTestRenderer(t)
113 var m testModel
114 var out Output
115 tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once()
116 r.Resize(100, 50, 1)
117 boom := errors.New("boom")
118 tg.EXPECT().Draw(mock.Anything, mock.Anything, mock.Anything).Return(false, boom).Once()
119 if ok, err := r.Render(&out, &m, testView); ok || !errors.Is(err, boom) {
120 t.Fatalf("ok=%v err=%v", ok, err)
121 }
122}
123
124func TestRendererResizeReachesTargetInPhysicalPixels(t *testing.T) {
125 r, tg := newTestRenderer(t)
126 tg.EXPECT().Resize(int32(150), int32(75)).Return().Once()
127 r.Resize(100, 50, 1.5)
128 r.Resize(100, 50, 1.5) // unchanged: no second call
129 r.Resize(0, 50, 1) // invalid: ignored
130 r.Resize(10, 10, 0) // invalid: ignored
131}
132
133func TestRendererInputActivatesButtonAndRequestsRedraw(t *testing.T) {
134 r, tg := newTestRenderer(t)
135 var m testModel
136 var out Output
137 tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once()
138 r.Resize(200, 100, 1)
139 drawOK(tg, nil).Maybe()
140 for i := 0; i < 3; i++ {
141 if _, err := r.Render(&out, &m, testView); err != nil {
142 t.Fatal(err)
143 }
144 }
145 if r.Input(&Input{Kind: InputPointerMotion, X: 5, Y: 5}) != true {
146 t.Fatal("hover over a button needs a redraw")
147 }
148 r.Input(&Input{Kind: InputPointerPress, X: 5, Y: 5, Button: 0x110, Pressed: true})
149 if !r.Input(&Input{Kind: InputPointerRelease, X: 5, Y: 5, Button: 0x110}) {
150 t.Fatal("release did not request redraw")
151 }
152 for i := 0; i < 3; i++ {
153 if _, err := r.Render(&out, &m, testView); err != nil {
154 t.Fatal(err)
155 }
156 }
157 if m.clicks != 1 {
158 t.Fatalf("clicks=%d", m.clicks)
159 }
160 // Keyboard: Tab focuses, Return activates, text comes from Text.
161 r.Input(&Input{Kind: InputPointerLeave})
162 r.Input(&Input{Kind: InputKey, Keysym: 0xff09, Pressed: true})
163 r.Input(&Input{Kind: InputKey, Keysym: 0xff0d, Pressed: true})
164 for i := 0; i < 3; i++ {
165 if _, err := r.Render(&out, &m, testView); err != nil {
166 t.Fatal(err)
167 }
168 }
169 if m.clicks != 2 {
170 t.Fatalf("Return did not activate the focused button: clicks=%d", m.clicks)
171 }
172 if r.Input(&Input{Kind: 99}) || r.Input(nil) {
173 t.Fatal("unknown input requested a redraw")
174 }
175 if r.Input(&Input{Kind: InputPointerAxis}) {
176 t.Fatal("zero scroll requested a redraw")
177 }
178}
179
180func TestRendererTextInputUsesTextBytes(t *testing.T) {
181 value := ""
182 view := func(f *Frame, v *string) { f.Root().Input("name", v, Key("name"), Inline("width:100px;height:20px")) }
183 r, settle := settledRenderer(t, &value, view)
184 r.Input(&Input{Kind: InputPointerPress, X: 5, Y: 5, Button: 0x110, Pressed: true})
185 r.Input(&Input{Kind: InputPointerRelease, X: 5, Y: 5, Button: 0x110})
186 settle()
187 text := []byte("é")
188 r.Input(&Input{Kind: InputKey, Keysym: 0xe9, Pressed: true, Text: text})
189 text[0] = 0 // the caller may reuse the buffer after the call
190 settle()
191 if value != "é" {
192 t.Fatalf("value=%q", value)
193 }
194}
195
196func TestRendererOutputCarriesCursorAndInputRects(t *testing.T) {
197 r, tg := newTestRenderer(t)
198 m := testModel{rects: []Rect{{1, 2, 30, 20}}}
199 var out Output
200 tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once()
201 r.Resize(200, 100, 1)
202 drawOK(tg, nil).Maybe()
203 r.Input(&Input{Kind: InputPointerMotion, X: 5, Y: 5}) // over the button: pointer cursor
204 if ok, err := r.Render(&out, &m, testView); !ok || err != nil {
205 t.Fatalf("ok=%v err=%v", ok, err)
206 }
207 if out.Cursor != CursorPointer || !out.InputRectsChanged || len(out.InputRects) != 1 || out.InputRects[0] != (Rect{1, 2, 30, 20}) {
208 t.Fatalf("output: cursor=%v changed=%v rects=%v", out.Cursor, out.InputRectsChanged, out.InputRects)
209 }
210 // An unchanged region is not reported again.
211 r.Invalidate()
212 ok, err := r.Render(&out, &m, testView)
213 if err != nil {
214 t.Fatal(err)
215 }
216 if ok && out.InputRectsChanged && len(out.InputRects) != 0 {
217 t.Fatalf("region reported twice: %+v", out)
218 }
219 // nil restores the whole surface: changed with no rects.
220 m.rects = nil
221 var nilView = func(f *Frame, _ *testModel) { f.Root(); _ = f.SetInputRects(nil) }
222 r.Invalidate()
223 if ok, err = r.Render(&out, &m, nilView); err != nil || !ok || !out.InputRectsChanged || out.InputRects != nil {
224 t.Fatalf("nil region: ok=%v err=%v %+v", ok, err, out)
225 }
226}
227
228func TestRendererForwardsReleasedAndClose(t *testing.T) {
229 r, tg := newTestRenderer(t)
230 tg.EXPECT().Released(uint64(3)).Return(nil).Once()
231 if err := r.Released(3); err != nil {
232 t.Fatal(err)
233 }
234 tg.EXPECT().Close().Return(nil).Once()
235 if err := r.Close(); err != nil {
236 t.Fatal(err)
237 }
238 // After Close nothing reaches the target (the mock has no expectation).
239 r.Resize(10, 10, 1)
240 if r.Input(&Input{Kind: InputPointerMotion}) {
241 t.Fatal("Input after Close requested a redraw")
242 }
243 if err := r.Released(3); err == nil {
244 t.Fatal("Released after Close succeeded")
245 }
246 if err := r.Close(); err != nil { // second Close is a no-op
247 t.Fatal(err)
248 }
249 var out Output
250 var m testModel
251 if _, err := r.Render(&out, &m, testView); err == nil {
252 t.Fatal("Render after Close succeeded")
253 }
254}
255
256func TestTextIntAndMasked(t *testing.T) {
257 r := newRuntime()
258 r.Build(func(f *Frame) {
259 root := f.Root()
260 root.TextInt("Count: ", -42)
261 root.Masked(3)
262 root.Masked(0)
263 root.Masked(300)
264 })
265 c := r.committed.children
266 if len(c) != 4 || c[0].text != "Count: -42" || c[1].text != "•••" || c[2].text != "" || len([]rune(c[3].text)) != 300 {
267 t.Fatalf("texts: %q %q %q", c[0].text, c[1].text, c[2].text)
268 }
269}
270
271// settledRenderer returns a 200x100 renderer for view and a settle function
272// that renders until queued input has been applied.
273func settledRenderer[T any](t *testing.T, model *T, view func(*Frame, *T)) (*Renderer, func()) {
274 t.Helper()
275 r, tg := newTestRenderer(t)
276 tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once()
277 r.Resize(200, 100, 1)
278 drawOK(tg, nil).Maybe()
279 var out Output
280 settle := func() {
281 for i := 0; i < 3; i++ {
282 if _, err := r.Render(&out, model, view); err != nil {
283 t.Fatal(err)
284 }
285 }
286 }
287 settle()
288 return r, settle
289}
290
291func TestModifierBits(t *testing.T) {
292 // The values match neferclient's, so applications copy the bits as is.
293 if ModShift != 1 || ModCtrl != 2 || ModAlt != 4 || ModSuper != 8 || ModCapsLock != 16 || ModNumLock != 32 {
294 t.Fatal("modifier bit values changed")
295 }
296}
297
298// Caps Lock and Num Lock must not change shortcuts, selection or activation.
299func TestRendererLockModifiersDoNotChangeInput(t *testing.T) {
300 inputView := func(f *Frame, v *string) { f.Root().Input("name", v, Key("name"), Inline("width:100px;height:20px")) }
301 key := func(r *Renderer, keysym uint32, text string, mods Modifiers) {
302 in := &Input{Kind: InputKey, Keysym: keysym, Pressed: true, Modifiers: mods}
303 if text != "" {
304 in.Text = []byte(text)
305 }
306 r.Input(in)
307 }
308 click := func(r *Renderer, x float64, mods Modifiers) {
309 r.Input(&Input{Kind: InputPointerPress, X: x, Y: 5, Button: 0x110, Pressed: true, Modifiers: mods})
310 r.Input(&Input{Kind: InputPointerRelease, X: x, Y: 5, Button: 0x110, Modifiers: mods})
311 }
312 for _, lock := range []Modifiers{0, ModCapsLock, ModNumLock, ModCapsLock | ModNumLock} {
313 t.Run(fmt.Sprintf("lock=%#x", uint8(lock)), func(t *testing.T) {
314 t.Run("CtrlA and ShiftLeft", func(t *testing.T) {
315 value := "hello"
316 r, settle := settledRenderer(t, &value, inputView)
317 click(r, 5, lock)
318 settle()
319 key(r, 'a', "a", ModCtrl|lock) // select all
320 settle()
321 key(r, 'X', "X", lock)
322 settle()
323 if value != "X" {
324 t.Fatalf("Ctrl+A then text: value=%q, want X", value)
325 }
326 key(r, 0xff51, "", ModShift|lock) // extend selection left
327 settle()
328 key(r, 'y', "y", lock)
329 settle()
330 if value != "y" {
331 t.Fatalf("Shift+Left then text: value=%q, want y", value)
332 }
333 })
334 t.Run("ShiftClick", func(t *testing.T) {
335 value := "hello"
336 r, settle := settledRenderer(t, &value, inputView)
337 click(r, 1, lock)
338 settle()
339 click(r, 95, ModShift|lock) // select to the end
340 settle()
341 key(r, 'Z', "Z", lock)
342 settle()
343 if value != "Z" {
344 t.Fatalf("Shift+click then text: value=%q, want Z", value)
345 }
346 })
347 t.Run("TabReturn", func(t *testing.T) {
348 m := testModel{}
349 r, settle := settledRenderer(t, &m, testView)
350 key(r, 0xff09, "", lock)
351 key(r, 0xff0d, "", lock)
352 settle()
353 if m.clicks != 1 {
354 t.Fatalf("Tab+Return clicks=%d, want 1", m.clicks)
355 }
356 })
357 })
358 }
359}
360
361type measureModel struct{ title, body string }
362
363func measureView(f *Frame, m *measureModel) {
364 root := f.Root(Inline("padding:4px"))
365 root.Text(m.title)
366 root.Text(m.body)
367}
368
369func TestRendererMeasureNaturalSizeBeforeResize(t *testing.T) {
370 r, _ := newTestRenderer(t) // the mock target has no expectations: any target call fails
371 m := measureModel{title: "Title", body: "short"}
372 w, h, err := r.Measure(&m, measureView, 400)
373 if err != nil {
374 t.Fatal(err)
375 }
376 if w <= 8 || w >= 400 || h <= 8 || w != float64(int(w)) || h != float64(int(h)) {
377 t.Fatalf("size %g x %g", w, h)
378 }
379 single := h
380 m.body = "alpha beta gamma delta epsilon zeta eta theta iota kappa lambda mu nu xi omicron pi rho sigma tau"
381 w, h, err = r.Measure(&m, measureView, 100)
382 if err != nil {
383 t.Fatal(err)
384 }
385 if w != 100 || h <= single+10 {
386 t.Fatalf("wrapped size %g x %g, one-line height %g", w, h, single)
387 }
388}
389
390func TestRendererMeasureValidation(t *testing.T) {
391 r, _ := newTestRenderer(t)
392 m := measureModel{}
393 for _, bad := range []float64{0, -1, math.NaN(), math.Inf(1)} {
394 if _, _, err := r.Measure(&m, measureView, bad); err == nil {
395 t.Errorf("maxWidth %v accepted", bad)
396 }
397 }
398 if _, _, err := r.Measure[measureModel](nil, measureView, 100); err == nil {
399 t.Error("nil model accepted")
400 }
401 if _, _, err := r.Measure(&m, nil, 100); err == nil {
402 t.Error("nil view accepted")
403 }
404 if w, h, err := r.Measure(&m, func(*Frame, *measureModel) {}, 100); err != nil || w != 0 || h != 0 {
405 t.Errorf("view without root: %g x %g %v", w, h, err)
406 }
407 tg := r.target.(*mocktarget)
408 tg.EXPECT().Close().Return(nil).Once()
409 _ = r.Close()
410 if _, _, err := r.Measure(&m, measureView, 100); err == nil {
411 t.Error("closed renderer accepted")
412 }
413}
414
415func TestRendererMeasureChangesNoState(t *testing.T) {
416 r, tg := newTestRenderer(t)
417 type model struct {
418 clicks int
419 text string
420 }
421 m := model{text: "edit me"}
422 view := func(f *Frame, m *model) {
423 root := f.Root()
424 if root.Button("Go", Key("go"), Inline("width:60px;height:20px")).Activated() {
425 m.clicks++
426 }
427 root.Input("field", &m.text, Key("field"))
428 }
429 var out Output
430 tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once()
431 r.Resize(200, 100, 1)
432 drawOK(tg, nil).Maybe()
433 render := func() {
434 t.Helper()
435 for i := 0; i < 3; i++ {
436 if _, err := r.Render(&out, &m, view); err != nil {
437 t.Fatal(err)
438 }
439 }
440 }
441 render()
442 r.Input(&Input{Kind: InputPointerMotion, X: 5, Y: 5})
443 r.Input(&Input{Kind: InputKey, Keysym: 0xff09, Pressed: true}) // Tab: focus the button
444 render()
445 draws := len(tg.Calls)
446 rt := r.rt
447 if rt.state.hover == nil || rt.state.focus == nil {
448 t.Fatalf("setup: hover=%v focus=%v", rt.state.hover, rt.state.focus)
449 }
450 // Input queued but not yet built must survive Measure untouched.
451 r.Input(&Input{Kind: InputPointerPress, X: 5, Y: 5, Button: 0x110, Pressed: true})
452 r.Input(&Input{Kind: InputPointerRelease, X: 5, Y: 5, Button: 0x110})
453 before := struct {
454 state interactionState
455 committed *element
456 generation uint64
457 edits, pending, wake int
458 styles any
459 engine any
460 }{rt.state, rt.committed, rt.generation, len(rt.edits), len(rt.pending), len(rt.wake), rt.styles, rt.textEngine}
461 if before.pending == 0 || before.wake == 0 {
462 t.Fatalf("setup: pending=%d wake=%d", before.pending, before.wake)
463 }
464
465 if _, _, err := r.Measure(&m, view, 300); err != nil {
466 t.Fatal(err)
467 }
468
469 if rt.state.hover != before.state.hover || rt.state.focus != before.state.focus || rt.state.x != before.state.x || rt.state.y != before.state.y || rt.state.inside != before.state.inside || rt.state.down != before.state.down {
470 t.Fatalf("interaction state changed: %+v -> %+v", before.state, rt.state)
471 }
472 if rt.committed != before.committed || rt.generation != before.generation {
473 t.Fatal("committed tree or generation changed")
474 }
475 if len(rt.edits) != before.edits || rt.styles != before.styles || rt.textEngine != before.engine {
476 t.Fatal("runtime parts changed")
477 }
478 if len(rt.pending) != before.pending || len(rt.wake) != before.wake {
479 t.Fatalf("pending %d -> %d, wake %d -> %d", before.pending, len(rt.pending), before.wake, len(rt.wake))
480 }
481 if m.clicks != 0 || m.text != "edit me" {
482 t.Fatalf("model changed: %+v", m)
483 }
484 if len(tg.Calls) != draws {
485 t.Fatalf("target called by Measure: %d -> %d calls", draws, len(tg.Calls))
486 }
487 render()
488 if m.clicks != 1 {
489 t.Fatalf("the click queued before Measure was lost: clicks=%d", m.clicks)
490 }
491 if rt.state.hover == nil || rt.state.focus == nil {
492 t.Fatalf("hover/focus lost after Render: %+v", rt.state)
493 }
494}
495
496const measureLong = "alpha beta gamma delta epsilon zeta eta theta iota kappa lambda mu nu xi omicron pi rho sigma tau upsilon phi chi psi omega"
497
498func TestRendererMeasureRootMargins(t *testing.T) {
499 r, _ := newTestRenderer(t)
500 m := measureModel{body: measureLong}
501 view := func(f *Frame, m *measureModel) {
502 root := f.Root(Inline("margin:20px;padding:3px"))
503 root.Text(m.body)
504 }
505 const maxWidth = 300
506 w, h, err := r.Measure(&m, view, maxWidth)
507 if err != nil {
508 t.Fatal(err)
509 }
510 // The widest wrapped line plus padding and margins: close to, never over, the limit.
511 if w > maxWidth || w < maxWidth-60 {
512 t.Fatalf("width %g, want within 60px below %d", w, maxWidth)
513 }
514 // Lay the same view out at the measured size, as a surface would.
515 rt := newRuntime()
516 rt.styles, rt.textEngine = r.rt.styles, r.rt.textEngine
517 rt.width, rt.height = w, h
518 rt.redraw = true
519 if !rt.Build(func(f *Frame) { view(f, &m) }) {
520 t.Fatal("build failed")
521 }
522 tree := rt.output.Tree
523 if tree == nil {
524 t.Fatal("no layout")
525 }
526 right, bottom := tree.Border.X+tree.Border.W+tree.Margin.Right, tree.Border.Y+tree.Border.H+tree.Margin.Bottom
527 if right > w+0.01 || bottom > h+0.01 || bottom < h-1 {
528 t.Fatalf("laid-out margin box %g x %g does not fit the measured %g x %g", right, bottom, w, h)
529 }
530 if tree.Border.X != 20 || tree.Border.Y != 20 {
531 t.Fatalf("margins not applied: %+v", tree.Border)
532 }
533 if len(tree.Children) != 1 || len(tree.Children[0].Lines) < 2 {
534 t.Fatalf("text did not wrap at the measured width: %+v", tree.Children)
535 }
536 // Without the margin, the same text must be shorter than with it: the
537 // margin narrows the wrap width by 40px and adds 40px of height.
538 _, plain, err := r.Measure(&m, func(f *Frame, m *measureModel) { f.Root(Inline("padding:3px")).Text(m.body) }, maxWidth)
539 if err != nil {
540 t.Fatal(err)
541 }
542 if h < plain+40 {
543 t.Fatalf("height %g with margins, %g without: margins missing", h, plain)
544 }
545}
546
547func TestRendererMeasurePercentHeightsAreIndefinite(t *testing.T) {
548 r, _ := newTestRenderer(t)
549 m := measureModel{title: "Title", body: "body"}
550 build := func(rootStyle string) func(*Frame, *measureModel) {
551 return func(f *Frame, m *measureModel) {
552 root := f.Root(Inline(rootStyle))
553 root.Text(m.title)
554 root.Text(m.body)
555 }
556 }
557 _, auto, err := r.Measure(&m, build("padding:4px"), 200)
558 if err != nil {
559 t.Fatal(err)
560 }
561 if auto <= 8 || auto > 200 {
562 t.Fatalf("setup: content height %g", auto)
563 }
564 for _, style := range []string{
565 "padding:4px;height:100%",
566 "padding:4px;min-height:50%",
567 "padding:4px;max-height:50%",
568 "padding:4px;height:100%;min-height:50%;max-height:100%",
569 } {
570 _, h, err := r.Measure(&m, build(style), 200)
571 if err != nil {
572 t.Fatalf("%s: %v", style, err)
573 }
574 if h != auto {
575 t.Errorf("%q: height %g, want the content height %g", style, h, auto)
576 }
577 }
578 // Pixel heights still apply.
579 if _, h, err := r.Measure(&m, build("height:123px"), 200); err != nil || h != 123 {
580 t.Errorf("height:123px gave %g, %v", h, err)
581 }
582 var seen float64
583 _, _, err = r.Measure(&m, func(f *Frame, m *measureModel) {
584 _, seen = f.Size()
585 f.Root().Text(m.title)
586 }, 200)
587 if err != nil || seen < 1<<20 {
588 t.Errorf("Frame.Size height %g (err %v), want unbounded", seen, err)
589 }
590}
591
592func TestRendererMeasureLayoutError(t *testing.T) {
593 r, _ := newTestRenderer(t)
594 m := measureModel{}
595 flexStackWithRect := func(f *Frame, _ *measureModel) {
596 root := f.Root()
597 stack := root.Stack(Inline("display:flex"))
598 stack.Box().Rect(0, 0, 10, 10)
599 }
600 _, _, err := r.Measure(&m, flexStackWithRect, 100)
601 if err == nil || !strings.HasPrefix(err.Error(), "nefergui: measure: ") || !strings.Contains(err.Error(), "Rect") {
602 t.Fatalf("flex Stack with a Rect child: %v", err)
603 }
604 deep := func(f *Frame, _ *measureModel) {
605 n := f.Root()
606 for i := 0; i < 300; i++ {
607 n = n.Box()
608 }
609 n.Text("x")
610 }
611 _, _, err = r.Measure(&m, deep, 100)
612 if !errors.Is(err, layout.ErrDepth) || !strings.HasPrefix(err.Error(), "nefergui: measure: ") {
613 t.Fatalf("deep tree: %v, want a wrapped ErrDepth", err)
614 }
615 // The Renderer stays usable.
616 if _, _, err := r.Measure(&m, measureView, 100); err != nil {
617 t.Fatalf("after errors: %v", err)
618 }
619}
620
621func TestRendererMeasureDoesNotAgeSharedCaches(t *testing.T) {
622 r, _ := newTestRenderer(t)
623 m := measureModel{title: "one", body: "two"}
624 view := func(f *Frame, m *measureModel) {
625 root := f.Root(Class("rendered"))
626 root.Text(m.title)
627 root.Text(m.body)
628 }
629 other := func(f *Frame, m *measureModel) {
630 f.Root(Class("other")).Text("unrelated " + m.title)
631 }
632 rt := r.rt
633 build := func() *css.Computed {
634 t.Helper()
635 rt.Redraw()
636 if !rt.Build(func(f *Frame) { view(f, &m) }) {
637 t.Fatal("build failed")
638 }
639 return rt.committed.computed
640 }
641 first := build()
642 if first == nil {
643 t.Fatal("no computed style")
644 }
645 // Each Build that called EndFrame would age the entries out: after three
646 // builds of another view the "rendered" style would be recomputed.
647 for i := 0; i < 3; i++ {
648 if _, _, err := r.Measure(&m, other, 200); err != nil {
649 t.Fatal(err)
650 }
651 }
652 if again := build(); again != first {
653 t.Fatal("Measure aged the shared style cache: the next Render recomputed the root style")
654 }
655}