detached 2543591

test: restore SetInputRects and Node.Rect tests through Renderer output

commit
25435917524179bf6f7a4207517db84e0cdbd06b
author
brice · · 2026-10-01 17:52 UTC
parent
8e05969
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +133 -0
A internal/ui/inputrects_test.go +133 -0
@@ -0,0 +1,133 @@
1+//go:build linux
2+
3+package ui
4+
5+import (
6+ "math"
7+ "strconv"
8+ "testing"
9+
10+ "github.com/bnema/nefergui/internal/layout"
11+ "github.com/stretchr/testify/mock"
12+)
13+
14+func TestFrameSetInputRects(t *testing.T) {
15+ r, tg := newTestRenderer(t)
16+ tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once()
17+ drawOK(tg, nil).Maybe()
18+ r.Resize(200, 100, 1)
19+ var out Output
20+ var setErr error
21+ rects := []Rect{{1, 2, 3, 4}, {5, 6, 7, 8}}
22+ render := func(view func(*Frame, *testModel)) bool {
23+ t.Helper()
24+ r.Invalidate()
25+ ok, err := r.Render(&out, &testModel{}, view)
26+ if err != nil {
27+ t.Fatal(err)
28+ }
29+ return ok
30+ }
31+ if !render(func(f *Frame, _ *testModel) { f.Root(); setErr = f.SetInputRects(rects) }) || setErr != nil {
32+ t.Fatalf("render or set: %v", setErr)
33+ }
34+ rects[0].Width = 99 // caller reuse must not affect the staged copy
35+ if !out.InputRectsChanged || len(out.InputRects) != 2 || out.InputRects[0] != (Rect{1, 2, 3, 4}) {
36+ t.Fatalf("applied %+v", out.InputRects)
37+ }
38+ // An unchanged region is not reported again.
39+ if render(func(f *Frame, _ *testModel) { f.Root(); _ = f.SetInputRects([]Rect{{1, 2, 3, 4}, {5, 6, 7, 8}}) }) && out.InputRectsChanged {
40+ t.Fatalf("restaged %+v", out.InputRects)
41+ }
42+ // A frame that never calls SetInputRects stages nothing new.
43+ if render(func(f *Frame, _ *testModel) { f.Root() }) && out.InputRectsChanged {
44+ t.Fatal("unchanged frame restaged the region")
45+ }
46+ // nil restores the full surface; empty non-nil is click-through.
47+ if !render(func(f *Frame, _ *testModel) { f.Root(); _ = f.SetInputRects(nil) }) || !out.InputRectsChanged || out.InputRects != nil {
48+ t.Fatalf("nil region: %+v", out)
49+ }
50+ if !render(func(f *Frame, _ *testModel) { f.Root(); _ = f.SetInputRects([]Rect{}) }) || !out.InputRectsChanged || out.InputRects == nil || len(out.InputRects) != 0 {
51+ t.Fatalf("empty region: %+v", out)
52+ }
53+ // Invalid rects are rejected in the frame call and stage nothing.
54+ for _, bad := range []Rect{{0, 0, 0, 5}, {0, 0, 5, -1}, {math.MaxInt32, 0, 5, 5}} {
55+ render(func(f *Frame, _ *testModel) { f.Root(); setErr = f.SetInputRects([]Rect{bad}) })
56+ if setErr == nil || r.rt.inputStaged {
57+ t.Fatalf("accepted %+v", bad)
58+ }
59+ }
60+ // Steady-state staging does not allocate once buffers exist.
61+ rt := newRuntime()
62+ rt.Build(func(f *Frame) {
63+ f.Root()
64+ _ = f.SetInputRects(rects)
65+ if n := testing.AllocsPerRun(50, func() { _ = f.SetInputRects(rects) }); n != 0 {
66+ t.Fatalf("SetInputRects allocs %v", n)
67+ }
68+ })
69+}
70+
71+func TestNodeRectAndFrameSize(t *testing.T) {
72+ r := newRuntime()
73+ r.width, r.height = 400, 300
74+ var w, h float64
75+ r.Build(func(f *Frame) {
76+ w, h = f.Size()
77+ stack := f.Root().Stack()
78+ stack.Box(Key("a")).Rect(10, 20, 30, 40)
79+ stack.Box(Key("b")).Rect(50.5, 60.25, 70, 80)
80+ })
81+ if w != 400 || h != 300 {
82+ t.Fatalf("size %v x %v", w, h)
83+ }
84+ kids := r.output.Tree.Children[0].Children
85+ if len(kids) != 2 {
86+ t.Fatalf("children %d", len(kids))
87+ }
88+ if kids[0].Border != (layout.Rect{X: 10, Y: 20, W: 30, H: 40}) || kids[1].Border != (layout.Rect{X: 50.5, Y: 60.25, W: 70, H: 80}) {
89+ t.Fatalf("rects %+v %+v", kids[0].Border, kids[1].Border)
90+ }
91+ // Hit testing follows the rect geometry.
92+ if id := layout.Hit(r.output.Tree, 12, 22); id != kids[0].ID {
93+ t.Fatalf("hit %q want %q", id, kids[0].ID)
94+ }
95+}
96+
97+func TestNodeRectOutsideStackIsIgnored(t *testing.T) {
98+ r := newRuntime()
99+ r.width, r.height = 200, 100
100+ r.Build(func(f *Frame) {
101+ col := f.Root().Column()
102+ col.Box(Key("a"), Inline("height:10px")).Rect(50, 50, 5, 5)
103+ col.Box(Key("b"), Inline("height:10px"))
104+ })
105+ kids := r.output.Tree.Children[0].Children
106+ if kids[0].Border.X != 0 || kids[0].Border.H != 10 || kids[1].Border.Y != 10 {
107+ t.Fatalf("Rect changed normal flow: %+v %+v", kids[0].Border, kids[1].Border)
108+ }
109+}
110+
111+// The absolute geometry path adds no allocations over the same tree without it.
112+func TestNodeRectAddsNoAllocations(t *testing.T) {
113+ build := func(withRect bool) func() {
114+ r := newRuntime()
115+ r.width, r.height = 640, 480
116+ view := func(f *Frame) {
117+ st := f.Root().Stack()
118+ for i := 0; i < 32; i++ {
119+ b := st.Box(Key(strconv.Itoa(i)), Inline("background:#fff"))
120+ if withRect {
121+ b.Rect(float64(i), float64(i), 20, 20)
122+ }
123+ }
124+ }
125+ r.Build(view)
126+ return func() { r.Redraw(); r.Build(view) }
127+ }
128+ plain := testing.AllocsPerRun(20, build(false))
129+ rect := testing.AllocsPerRun(20, build(true))
130+ if rect > plain {
131+ t.Fatalf("Rect added allocations: %v vs %v", rect, plain)
132+ }
133+}
@@ -0,0 +1,133 @@
1+//go:build linux
2+
3+package ui
4+
5+import (
6+ "math"
7+ "strconv"
8+ "testing"
9+
10+ "github.com/bnema/nefergui/internal/layout"
11+ "github.com/stretchr/testify/mock"
12+)
13+
14+func TestFrameSetInputRects(t *testing.T) {
15+ r, tg := newTestRenderer(t)
16+ tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once()
17+ drawOK(tg, nil).Maybe()
18+ r.Resize(200, 100, 1)
19+ var out Output
20+ var setErr error
21+ rects := []Rect{{1, 2, 3, 4}, {5, 6, 7, 8}}
22+ render := func(view func(*Frame, *testModel)) bool {
23+ t.Helper()
24+ r.Invalidate()
25+ ok, err := r.Render(&out, &testModel{}, view)
26+ if err != nil {
27+ t.Fatal(err)
28+ }
29+ return ok
30+ }
31+ if !render(func(f *Frame, _ *testModel) { f.Root(); setErr = f.SetInputRects(rects) }) || setErr != nil {
32+ t.Fatalf("render or set: %v", setErr)
33+ }
34+ rects[0].Width = 99 // caller reuse must not affect the staged copy
35+ if !out.InputRectsChanged || len(out.InputRects) != 2 || out.InputRects[0] != (Rect{1, 2, 3, 4}) {
36+ t.Fatalf("applied %+v", out.InputRects)
37+ }
38+ // An unchanged region is not reported again.
39+ if render(func(f *Frame, _ *testModel) { f.Root(); _ = f.SetInputRects([]Rect{{1, 2, 3, 4}, {5, 6, 7, 8}}) }) && out.InputRectsChanged {
40+ t.Fatalf("restaged %+v", out.InputRects)
41+ }
42+ // A frame that never calls SetInputRects stages nothing new.
43+ if render(func(f *Frame, _ *testModel) { f.Root() }) && out.InputRectsChanged {
44+ t.Fatal("unchanged frame restaged the region")
45+ }
46+ // nil restores the full surface; empty non-nil is click-through.
47+ if !render(func(f *Frame, _ *testModel) { f.Root(); _ = f.SetInputRects(nil) }) || !out.InputRectsChanged || out.InputRects != nil {
48+ t.Fatalf("nil region: %+v", out)
49+ }
50+ if !render(func(f *Frame, _ *testModel) { f.Root(); _ = f.SetInputRects([]Rect{}) }) || !out.InputRectsChanged || out.InputRects == nil || len(out.InputRects) != 0 {
51+ t.Fatalf("empty region: %+v", out)
52+ }
53+ // Invalid rects are rejected in the frame call and stage nothing.
54+ for _, bad := range []Rect{{0, 0, 0, 5}, {0, 0, 5, -1}, {math.MaxInt32, 0, 5, 5}} {
55+ render(func(f *Frame, _ *testModel) { f.Root(); setErr = f.SetInputRects([]Rect{bad}) })
56+ if setErr == nil || r.rt.inputStaged {
57+ t.Fatalf("accepted %+v", bad)
58+ }
59+ }
60+ // Steady-state staging does not allocate once buffers exist.
61+ rt := newRuntime()
62+ rt.Build(func(f *Frame) {
63+ f.Root()
64+ _ = f.SetInputRects(rects)
65+ if n := testing.AllocsPerRun(50, func() { _ = f.SetInputRects(rects) }); n != 0 {
66+ t.Fatalf("SetInputRects allocs %v", n)
67+ }
68+ })
69+}
70+
71+func TestNodeRectAndFrameSize(t *testing.T) {
72+ r := newRuntime()
73+ r.width, r.height = 400, 300
74+ var w, h float64
75+ r.Build(func(f *Frame) {
76+ w, h = f.Size()
77+ stack := f.Root().Stack()
78+ stack.Box(Key("a")).Rect(10, 20, 30, 40)
79+ stack.Box(Key("b")).Rect(50.5, 60.25, 70, 80)
80+ })
81+ if w != 400 || h != 300 {
82+ t.Fatalf("size %v x %v", w, h)
83+ }
84+ kids := r.output.Tree.Children[0].Children
85+ if len(kids) != 2 {
86+ t.Fatalf("children %d", len(kids))
87+ }
88+ if kids[0].Border != (layout.Rect{X: 10, Y: 20, W: 30, H: 40}) || kids[1].Border != (layout.Rect{X: 50.5, Y: 60.25, W: 70, H: 80}) {
89+ t.Fatalf("rects %+v %+v", kids[0].Border, kids[1].Border)
90+ }
91+ // Hit testing follows the rect geometry.
92+ if id := layout.Hit(r.output.Tree, 12, 22); id != kids[0].ID {
93+ t.Fatalf("hit %q want %q", id, kids[0].ID)
94+ }
95+}
96+
97+func TestNodeRectOutsideStackIsIgnored(t *testing.T) {
98+ r := newRuntime()
99+ r.width, r.height = 200, 100
100+ r.Build(func(f *Frame) {
101+ col := f.Root().Column()
102+ col.Box(Key("a"), Inline("height:10px")).Rect(50, 50, 5, 5)
103+ col.Box(Key("b"), Inline("height:10px"))
104+ })
105+ kids := r.output.Tree.Children[0].Children
106+ if kids[0].Border.X != 0 || kids[0].Border.H != 10 || kids[1].Border.Y != 10 {
107+ t.Fatalf("Rect changed normal flow: %+v %+v", kids[0].Border, kids[1].Border)
108+ }
109+}
110+
111+// The absolute geometry path adds no allocations over the same tree without it.
112+func TestNodeRectAddsNoAllocations(t *testing.T) {
113+ build := func(withRect bool) func() {
114+ r := newRuntime()
115+ r.width, r.height = 640, 480
116+ view := func(f *Frame) {
117+ st := f.Root().Stack()
118+ for i := 0; i < 32; i++ {
119+ b := st.Box(Key(strconv.Itoa(i)), Inline("background:#fff"))
120+ if withRect {
121+ b.Rect(float64(i), float64(i), 20, 20)
122+ }
123+ }
124+ }
125+ r.Build(view)
126+ return func() { r.Redraw(); r.Build(view) }
127+ }
128+ plain := testing.AllocsPerRun(20, build(false))
129+ rect := testing.AllocsPerRun(20, build(true))
130+ if rect > plain {
131+ t.Fatalf("Rect added allocations: %v vs %v", rect, plain)
132+ }
133+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 12

↑↓ moveenter openesc close