detached 62a7058

test(ui): cover Renderer with a Mockery GPU seam

Adds the testify dependency and a same-package mock of the unexported
target interface.
commit
62a7058d7bdad91adcc2051cbeaa165c12a72792
author
brice · · 2026-10-01 16:31 UTC
parent
008e36d
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
6 files · +588 -12
A .mockery.yml +16 -0
@@ -0,0 +1,16 @@
1+all: false
2+formatter: goimports
3+force-file-write: true
4+structname: '{{.Mock}}{{.InterfaceName}}'
5+template: testify
6+template-schema: '{{.Template}}.schema.json'
7+require-template-schema-exists: true
8+packages:
9+ # Unexported GPU seam of Renderer: the mock lives next to the code, in a test file.
10+ github.com/bnema/nefergui/internal/ui:
11+ config:
12+ dir: internal/ui
13+ pkgname: ui
14+ filename: '{{.InterfaceName}}_mock_test.go'
15+ interfaces:
16+ target:
@@ -0,0 +1,16 @@
1+all: false
2+formatter: goimports
3+force-file-write: true
4+structname: '{{.Mock}}{{.InterfaceName}}'
5+template: testify
6+template-schema: '{{.Template}}.schema.json'
7+require-template-schema-exists: true
8+packages:
9+ # Unexported GPU seam of Renderer: the mock lives next to the code, in a test file.
10+ github.com/bnema/nefergui/internal/ui:
11+ config:
12+ dir: internal/ui
13+ pkgname: ui
14+ filename: '{{.InterfaceName}}_mock_test.go'
15+ interfaces:
16+ target:
M go.mod +6 -1
@@ -8,8 +8,13 @@ require (
88 github.com/bnema/purego-xkbcommon v0.1.0
99 github.com/bnema/wlturbo v0.5.0
1010 github.com/go-text/typesetting v0.3.5
11+ github.com/stretchr/testify v1.12.1
1112 golang.org/x/image v0.46.0
1213 golang.org/x/sys v0.48.0
1314 )
1415
15-require golang.org/x/text v0.42.0 // indirect
16+require (
17+ github.com/stretchr/objx v0.5.3 // indirect
18+ go.yaml.in/yaml/v3 v3.0.5 // indirect
19+ golang.org/x/text v0.42.0 // indirect
20+)
@@ -8,8 +8,13 @@ require (
8 github.com/bnema/purego-xkbcommon v0.1.08 github.com/bnema/purego-xkbcommon v0.1.0
9 github.com/bnema/wlturbo v0.5.09 github.com/bnema/wlturbo v0.5.0
10 github.com/go-text/typesetting v0.3.510 github.com/go-text/typesetting v0.3.5
11+ github.com/stretchr/testify v1.12.1
11 golang.org/x/image v0.46.012 golang.org/x/image v0.46.0
12 golang.org/x/sys v0.48.013 golang.org/x/sys v0.48.0
13 )14 )
14 15
15-require golang.org/x/text v0.42.0 // indirect16+require (
17+ github.com/stretchr/objx v0.5.3 // indirect
18+ go.yaml.in/yaml/v3 v3.0.5 // indirect
19+ golang.org/x/text v0.42.0 // indirect
20+)
M go.sum +6 -0
@@ -10,6 +10,12 @@ github.com/go-text/typesetting v0.3.5 h1:XZPUooClHY0Vf/rFyUyuPRNEkawARaFzLMQcXLS
1010 github.com/go-text/typesetting v0.3.5/go.mod h1:XZO1hD+nQVyvVa5IicQk7FsCa4PFQaJ2soWAP1f//68=
1111 github.com/go-text/typesetting-utils v0.0.0-20260419141703-4ffe8874dabc h1:8FGo2It5K75XkavhTiCKExUfVaVDS1feBnLCru5qeoY=
1212 github.com/go-text/typesetting-utils v0.0.0-20260419141703-4ffe8874dabc/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o=
13+github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
14+github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
15+github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
16+github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
17+go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
18+go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
1319 golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ=
1420 golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew=
1521 golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
@@ -10,6 +10,12 @@ github.com/go-text/typesetting v0.3.5 h1:XZPUooClHY0Vf/rFyUyuPRNEkawARaFzLMQcXLS
10 github.com/go-text/typesetting v0.3.5/go.mod h1:XZO1hD+nQVyvVa5IicQk7FsCa4PFQaJ2soWAP1f//68=10 github.com/go-text/typesetting v0.3.5/go.mod h1:XZO1hD+nQVyvVa5IicQk7FsCa4PFQaJ2soWAP1f//68=
11 github.com/go-text/typesetting-utils v0.0.0-20260419141703-4ffe8874dabc h1:8FGo2It5K75XkavhTiCKExUfVaVDS1feBnLCru5qeoY=11 github.com/go-text/typesetting-utils v0.0.0-20260419141703-4ffe8874dabc h1:8FGo2It5K75XkavhTiCKExUfVaVDS1feBnLCru5qeoY=
12 github.com/go-text/typesetting-utils v0.0.0-20260419141703-4ffe8874dabc/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o=12 github.com/go-text/typesetting-utils v0.0.0-20260419141703-4ffe8874dabc/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o=
13+github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
14+github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
15+github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
16+github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
17+go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
18+go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
13 golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ=19 golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ=
14 golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew=20 golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew=
15 golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=21 golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
M internal/ui/renderer.go +24 -11
@@ -8,6 +8,7 @@ import (
88 "fmt"
99 "math"
1010 "os"
11+ "slices"
1112
1213 "github.com/bnema/nefergui/internal/css"
1314 "github.com/bnema/nefergui/internal/edit"
@@ -112,6 +113,11 @@ type Renderer struct {
112113 scale float64
113114 pw, ph int32
114115 closed bool
116+
117+ // Last input region handed out, to report only changes.
118+ region []Rect
119+ regionNil bool
120+ haveRegion bool
115121 }
116122
117123 // asyncClipboard lets an AsyncClipboard satisfy the editor's Clipboard port;
@@ -281,18 +287,25 @@ func (r *Renderer) export(out *Output) {
281287 Damage: damage, Cursor: cursorOf(r.rt.cursor()), InputRects: rects[:0],
282288 }
283289 rt := r.rt
284- if rt.inputStaged {
285- out.InputRectsChanged = true
286- if rt.inputNil {
287- out.InputRects = nil
288- } else {
289- if out.InputRects == nil {
290- out.InputRects = []Rect{}
291- }
292- out.InputRects = append(out.InputRects, rt.inputRects...)
293- }
294- rt.inputStaged = false
290+ if !rt.inputStaged {
291+ return
292+ }
293+ rt.inputStaged = false
294+ // A region equal to the last reported one costs nothing.
295+ if r.haveRegion && r.regionNil == rt.inputNil && slices.Equal(r.region, rt.inputRects) {
296+ return
297+ }
298+ r.haveRegion, r.regionNil = true, rt.inputNil
299+ r.region = append(r.region[:0], rt.inputRects...)
300+ out.InputRectsChanged = true
301+ if rt.inputNil {
302+ out.InputRects = nil
303+ return
304+ }
305+ if out.InputRects == nil {
306+ out.InputRects = []Rect{}
295307 }
308+ out.InputRects = append(out.InputRects, rt.inputRects...)
296309 }
297310
298311 func cursorOf(k css.Keyword) Cursor {
@@ -8,6 +8,7 @@ import (
8 "fmt"8 "fmt"
9 "math"9 "math"
10 "os"10 "os"
11+ "slices"
11 12
12 "github.com/bnema/nefergui/internal/css"13 "github.com/bnema/nefergui/internal/css"
13 "github.com/bnema/nefergui/internal/edit"14 "github.com/bnema/nefergui/internal/edit"
@@ -112,6 +113,11 @@ type Renderer struct {
112 scale float64113 scale float64
113 pw, ph int32114 pw, ph int32
114 closed bool115 closed bool
116+
117+ // Last input region handed out, to report only changes.
118+ region []Rect
119+ regionNil bool
120+ haveRegion bool
115 }121 }
116 122
117 // asyncClipboard lets an AsyncClipboard satisfy the editor's Clipboard port;123 // asyncClipboard lets an AsyncClipboard satisfy the editor's Clipboard port;
@@ -281,18 +287,25 @@ func (r *Renderer) export(out *Output) {
281 Damage: damage, Cursor: cursorOf(r.rt.cursor()), InputRects: rects[:0],287 Damage: damage, Cursor: cursorOf(r.rt.cursor()), InputRects: rects[:0],
282 }288 }
283 rt := r.rt289 rt := r.rt
284- if rt.inputStaged {290+ if !rt.inputStaged {
285- out.InputRectsChanged = true291+ return
286- if rt.inputNil {292+ }
287- out.InputRects = nil293+ rt.inputStaged = false
288- } else {294+ // A region equal to the last reported one costs nothing.
289- if out.InputRects == nil {295+ if r.haveRegion && r.regionNil == rt.inputNil && slices.Equal(r.region, rt.inputRects) {
290- out.InputRects = []Rect{}296+ return
291- }297+ }
292- out.InputRects = append(out.InputRects, rt.inputRects...)298+ r.haveRegion, r.regionNil = true, rt.inputNil
293- }299+ r.region = append(r.region[:0], rt.inputRects...)
294- rt.inputStaged = false300+ out.InputRectsChanged = true
301+ if rt.inputNil {
302+ out.InputRects = nil
303+ return
304+ }
305+ if out.InputRects == nil {
306+ out.InputRects = []Rect{}
295 }307 }
308+ out.InputRects = append(out.InputRects, rt.inputRects...)
296 }309 }
297 310
298 func cursorOf(k css.Keyword) Cursor {311 func cursorOf(k css.Keyword) Cursor {
A internal/ui/renderer_test.go +276 -0
@@ -0,0 +1,276 @@
1+//go:build linux
2+
3+package ui
4+
5+import (
6+ "errors"
7+ "testing"
8+
9+ "github.com/bnema/nefergui/internal/layout"
10+ "github.com/bnema/nefergui/internal/presentation/session"
11+ "github.com/bnema/nefergui/internal/text"
12+ "github.com/stretchr/testify/mock"
13+)
14+
15+type testModel struct {
16+ clicks int
17+ rects []Rect
18+ inline string
19+}
20+
21+func testView(f *Frame, m *testModel) {
22+ root := f.Root()
23+ if root.Button("Go", Key("go"), Inline("width:60px;height:20px")).Activated() {
24+ m.clicks++
25+ }
26+ if m.rects != nil {
27+ _ = f.SetInputRects(m.rects)
28+ }
29+}
30+
31+func newTestRenderer(t *testing.T) (*Renderer, *mocktarget) {
32+ t.Helper()
33+ tg := newMocktarget(t)
34+ r, err := newRenderer(RendererConfig{}, tg, text.DirectorySource("../../testdata/fonts"))
35+ if err != nil {
36+ t.Fatal(err)
37+ }
38+ return r, tg
39+}
40+
41+func drawOK(tg *mocktarget, fill func(*session.Output)) *mocktarget_Draw_Call {
42+ return tg.EXPECT().Draw(mock.Anything, mock.Anything, mock.Anything).
43+ RunAndReturn(func(_ []layout.Command, _ float64, out *session.Output) (bool, error) {
44+ *out = session.Output{Buffer: 1, Width: 100, Height: 50}
45+ if fill != nil {
46+ fill(out)
47+ }
48+ return true, nil
49+ })
50+}
51+
52+func TestKeysymNames(t *testing.T) {
53+ for sym, want := range map[uint32]string{
54+ 0x20: "space", 0xff09: "tab", 0xfe20: "iso_left_tab", 0xff51: "left", 0xff53: "right",
55+ 0xff52: "up", 0xff54: "down", 0xff50: "home", 0xff57: "end", 0xff55: "page_up",
56+ 0xff56: "page_down", 0xff08: "backspace", 0xffff: "delete", 'a': "a", 'A': "a",
57+ 'c': "c", 'x': "x", 'v': "v", 0xff0d: "return", 0xff8d: "kp_enter", 0xff1b: "escape",
58+ 'z': "", 0xffe1: "",
59+ } {
60+ if got := keysymName(sym); got != want {
61+ t.Errorf("keysym %#x: got %q want %q", sym, got, want)
62+ }
63+ }
64+}
65+
66+func TestRendererRenderBeforeResizeAndWhenUnchanged(t *testing.T) {
67+ r, tg := newTestRenderer(t)
68+ var m testModel
69+ var out Output
70+ if ok, err := r.Render(&out, &m, testView); ok || err != nil {
71+ t.Fatalf("before Resize: ok=%v err=%v", ok, err)
72+ }
73+ tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once()
74+ r.Resize(100, 50, 2)
75+ drawOK(tg, nil).Once()
76+ if ok, err := r.Render(&out, &m, testView); !ok || err != nil {
77+ t.Fatalf("first render: ok=%v err=%v", ok, err)
78+ }
79+ if out.Buffer != 1 || out.Width != 100 || len(out.Damage) != 1 || out.Damage[0] != (Rect{Width: 100, Height: 50}) {
80+ t.Fatalf("output: %+v", out)
81+ }
82+ // The follow-up frame a Build requests after settling still draws once; after that nothing changes.
83+ for i := 0; i < 3; i++ {
84+ ok, err := r.Render(&out, &m, testView)
85+ if err != nil {
86+ t.Fatal(err)
87+ }
88+ if ok {
89+ drawOK(tg, nil).Maybe()
90+ }
91+ }
92+ // tg asserts no Draw beyond the allowed ones when unchanged.
93+ if ok, _ := r.Render(&out, &m, testView); ok {
94+ t.Fatal("unchanged view drew")
95+ }
96+}
97+
98+func TestRendererKeepsFrameWhenNoBufferFree(t *testing.T) {
99+ r, tg := newTestRenderer(t)
100+ var m testModel
101+ var out Output
102+ tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once()
103+ r.Resize(100, 50, 1)
104+ tg.EXPECT().Draw(mock.Anything, mock.Anything, mock.Anything).Return(false, nil).Once()
105+ if ok, err := r.Render(&out, &m, testView); ok || err != nil {
106+ t.Fatalf("no buffer: ok=%v err=%v", ok, err)
107+ }
108+ drawOK(tg, nil).Once()
109+ if ok, err := r.Render(&out, &m, testView); !ok || err != nil {
110+ t.Fatalf("retry: ok=%v err=%v", ok, err)
111+ }
112+}
113+
114+func TestRendererDrawError(t *testing.T) {
115+ r, tg := newTestRenderer(t)
116+ var m testModel
117+ var out Output
118+ tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once()
119+ r.Resize(100, 50, 1)
120+ boom := errors.New("boom")
121+ tg.EXPECT().Draw(mock.Anything, mock.Anything, mock.Anything).Return(false, boom).Once()
122+ if ok, err := r.Render(&out, &m, testView); ok || !errors.Is(err, boom) {
123+ t.Fatalf("ok=%v err=%v", ok, err)
124+ }
125+}
126+
127+func TestRendererResizeReachesTargetInPhysicalPixels(t *testing.T) {
128+ r, tg := newTestRenderer(t)
129+ tg.EXPECT().Resize(int32(150), int32(75)).Return().Once()
130+ r.Resize(100, 50, 1.5)
131+ r.Resize(100, 50, 1.5) // unchanged: no second call
132+ r.Resize(0, 50, 1) // invalid: ignored
133+ r.Resize(10, 10, 0) // invalid: ignored
134+}
135+
136+func TestRendererInputActivatesButtonAndRequestsRedraw(t *testing.T) {
137+ r, tg := newTestRenderer(t)
138+ var m testModel
139+ var out Output
140+ tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once()
141+ r.Resize(200, 100, 1)
142+ drawOK(tg, nil).Maybe()
143+ for i := 0; i < 3; i++ {
144+ if _, err := r.Render(&out, &m, testView); err != nil {
145+ t.Fatal(err)
146+ }
147+ }
148+ if r.Input(&Input{Kind: InputPointerMotion, X: 5, Y: 5}) != true {
149+ t.Fatal("hover over a button needs a redraw")
150+ }
151+ r.Input(&Input{Kind: InputPointerPress, X: 5, Y: 5, Button: 0x110, Pressed: true})
152+ if !r.Input(&Input{Kind: InputPointerRelease, X: 5, Y: 5, Button: 0x110}) {
153+ t.Fatal("release did not request redraw")
154+ }
155+ for i := 0; i < 3; i++ {
156+ if _, err := r.Render(&out, &m, testView); err != nil {
157+ t.Fatal(err)
158+ }
159+ }
160+ if m.clicks != 1 {
161+ t.Fatalf("clicks=%d", m.clicks)
162+ }
163+ // Keyboard: Tab focuses, Return activates, text comes from Text.
164+ r.Input(&Input{Kind: InputPointerLeave})
165+ r.Input(&Input{Kind: InputKey, Keysym: 0xff09, Pressed: true})
166+ r.Input(&Input{Kind: InputKey, Keysym: 0xff0d, Pressed: true})
167+ for i := 0; i < 3; i++ {
168+ if _, err := r.Render(&out, &m, testView); err != nil {
169+ t.Fatal(err)
170+ }
171+ }
172+ if m.clicks != 2 {
173+ t.Fatalf("Return did not activate the focused button: clicks=%d", m.clicks)
174+ }
175+ if r.Input(&Input{Kind: 99}) || r.Input(nil) {
176+ t.Fatal("unknown input requested a redraw")
177+ }
178+ if r.Input(&Input{Kind: InputPointerAxis}) {
179+ t.Fatal("zero scroll requested a redraw")
180+ }
181+}
182+
183+func TestRendererTextInputUsesTextBytes(t *testing.T) {
184+ r, tg := newTestRenderer(t)
185+ value := ""
186+ var out Output
187+ view := func(f *Frame, v *string) { f.Root().Input("name", v, Key("name"), Inline("width:100px;height:20px")) }
188+ tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once()
189+ r.Resize(200, 100, 1)
190+ drawOK(tg, nil).Maybe()
191+ settle := func() {
192+ for i := 0; i < 3; i++ {
193+ if _, err := r.Render(&out, &value, view); err != nil {
194+ t.Fatal(err)
195+ }
196+ }
197+ }
198+ settle()
199+ r.Input(&Input{Kind: InputPointerPress, X: 5, Y: 5, Button: 0x110, Pressed: true})
200+ r.Input(&Input{Kind: InputPointerRelease, X: 5, Y: 5, Button: 0x110})
201+ settle()
202+ text := []byte("é")
203+ r.Input(&Input{Kind: InputKey, Keysym: 0xe9, Pressed: true, Text: text})
204+ text[0] = 0 // the caller may reuse the buffer after the call
205+ settle()
206+ if value != "é" {
207+ t.Fatalf("value=%q", value)
208+ }
209+}
210+
211+func TestRendererOutputCarriesCursorAndInputRects(t *testing.T) {
212+ r, tg := newTestRenderer(t)
213+ m := testModel{rects: []Rect{{1, 2, 30, 20}}}
214+ var out Output
215+ tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once()
216+ r.Resize(200, 100, 1)
217+ drawOK(tg, nil).Maybe()
218+ r.Input(&Input{Kind: InputPointerMotion, X: 5, Y: 5}) // over the button: pointer cursor
219+ if ok, err := r.Render(&out, &m, testView); !ok || err != nil {
220+ t.Fatalf("ok=%v err=%v", ok, err)
221+ }
222+ if out.Cursor != CursorPointer || !out.InputRectsChanged || len(out.InputRects) != 1 || out.InputRects[0] != (Rect{1, 2, 30, 20}) {
223+ t.Fatalf("output: cursor=%v changed=%v rects=%v", out.Cursor, out.InputRectsChanged, out.InputRects)
224+ }
225+ // An unchanged region is not reported again.
226+ r.Invalidate()
227+ ok, err := r.Render(&out, &m, testView)
228+ if err != nil {
229+ t.Fatal(err)
230+ }
231+ if ok && out.InputRectsChanged && len(out.InputRects) != 0 {
232+ t.Fatalf("region reported twice: %+v", out)
233+ }
234+ // nil restores the whole surface: changed with no rects.
235+ m.rects = nil
236+ var nilView = func(f *Frame, _ *testModel) { f.Root(); _ = f.SetInputRects(nil) }
237+ r.Invalidate()
238+ if ok, err = r.Render(&out, &m, nilView); err != nil || !ok || !out.InputRectsChanged || out.InputRects != nil {
239+ t.Fatalf("nil region: ok=%v err=%v %+v", ok, err, out)
240+ }
241+}
242+
243+func TestRendererForwardsReleasedAndClose(t *testing.T) {
244+ r, tg := newTestRenderer(t)
245+ tg.EXPECT().Released(uint64(3)).Return(nil).Once()
246+ if err := r.Released(3); err != nil {
247+ t.Fatal(err)
248+ }
249+ tg.EXPECT().Close().Return(nil).Once()
250+ if err := r.Close(); err != nil {
251+ t.Fatal(err)
252+ }
253+ if err := r.Close(); err != nil { // second Close is a no-op
254+ t.Fatal(err)
255+ }
256+ var out Output
257+ var m testModel
258+ if _, err := r.Render(&out, &m, testView); err == nil {
259+ t.Fatal("Render after Close succeeded")
260+ }
261+}
262+
263+func TestTextIntAndMasked(t *testing.T) {
264+ r := newRuntime()
265+ r.Build(func(f *Frame) {
266+ root := f.Root()
267+ root.TextInt("Count: ", -42)
268+ root.Masked(3)
269+ root.Masked(0)
270+ root.Masked(300)
271+ })
272+ c := r.committed.children
273+ if len(c) != 4 || c[0].text != "Count: -42" || c[1].text != "•••" || c[2].text != "" || len([]rune(c[3].text)) != 300 {
274+ t.Fatalf("texts: %q %q %q", c[0].text, c[1].text, c[2].text)
275+ }
276+}
@@ -0,0 +1,276 @@
1+//go:build linux
2+
3+package ui
4+
5+import (
6+ "errors"
7+ "testing"
8+
9+ "github.com/bnema/nefergui/internal/layout"
10+ "github.com/bnema/nefergui/internal/presentation/session"
11+ "github.com/bnema/nefergui/internal/text"
12+ "github.com/stretchr/testify/mock"
13+)
14+
15+type testModel struct {
16+ clicks int
17+ rects []Rect
18+ inline string
19+}
20+
21+func testView(f *Frame, m *testModel) {
22+ root := f.Root()
23+ if root.Button("Go", Key("go"), Inline("width:60px;height:20px")).Activated() {
24+ m.clicks++
25+ }
26+ if m.rects != nil {
27+ _ = f.SetInputRects(m.rects)
28+ }
29+}
30+
31+func newTestRenderer(t *testing.T) (*Renderer, *mocktarget) {
32+ t.Helper()
33+ tg := newMocktarget(t)
34+ r, err := newRenderer(RendererConfig{}, tg, text.DirectorySource("../../testdata/fonts"))
35+ if err != nil {
36+ t.Fatal(err)
37+ }
38+ return r, tg
39+}
40+
41+func drawOK(tg *mocktarget, fill func(*session.Output)) *mocktarget_Draw_Call {
42+ return tg.EXPECT().Draw(mock.Anything, mock.Anything, mock.Anything).
43+ RunAndReturn(func(_ []layout.Command, _ float64, out *session.Output) (bool, error) {
44+ *out = session.Output{Buffer: 1, Width: 100, Height: 50}
45+ if fill != nil {
46+ fill(out)
47+ }
48+ return true, nil
49+ })
50+}
51+
52+func TestKeysymNames(t *testing.T) {
53+ for sym, want := range map[uint32]string{
54+ 0x20: "space", 0xff09: "tab", 0xfe20: "iso_left_tab", 0xff51: "left", 0xff53: "right",
55+ 0xff52: "up", 0xff54: "down", 0xff50: "home", 0xff57: "end", 0xff55: "page_up",
56+ 0xff56: "page_down", 0xff08: "backspace", 0xffff: "delete", 'a': "a", 'A': "a",
57+ 'c': "c", 'x': "x", 'v': "v", 0xff0d: "return", 0xff8d: "kp_enter", 0xff1b: "escape",
58+ 'z': "", 0xffe1: "",
59+ } {
60+ if got := keysymName(sym); got != want {
61+ t.Errorf("keysym %#x: got %q want %q", sym, got, want)
62+ }
63+ }
64+}
65+
66+func TestRendererRenderBeforeResizeAndWhenUnchanged(t *testing.T) {
67+ r, tg := newTestRenderer(t)
68+ var m testModel
69+ var out Output
70+ if ok, err := r.Render(&out, &m, testView); ok || err != nil {
71+ t.Fatalf("before Resize: ok=%v err=%v", ok, err)
72+ }
73+ tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once()
74+ r.Resize(100, 50, 2)
75+ drawOK(tg, nil).Once()
76+ if ok, err := r.Render(&out, &m, testView); !ok || err != nil {
77+ t.Fatalf("first render: ok=%v err=%v", ok, err)
78+ }
79+ if out.Buffer != 1 || out.Width != 100 || len(out.Damage) != 1 || out.Damage[0] != (Rect{Width: 100, Height: 50}) {
80+ t.Fatalf("output: %+v", out)
81+ }
82+ // The follow-up frame a Build requests after settling still draws once; after that nothing changes.
83+ for i := 0; i < 3; i++ {
84+ ok, err := r.Render(&out, &m, testView)
85+ if err != nil {
86+ t.Fatal(err)
87+ }
88+ if ok {
89+ drawOK(tg, nil).Maybe()
90+ }
91+ }
92+ // tg asserts no Draw beyond the allowed ones when unchanged.
93+ if ok, _ := r.Render(&out, &m, testView); ok {
94+ t.Fatal("unchanged view drew")
95+ }
96+}
97+
98+func TestRendererKeepsFrameWhenNoBufferFree(t *testing.T) {
99+ r, tg := newTestRenderer(t)
100+ var m testModel
101+ var out Output
102+ tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once()
103+ r.Resize(100, 50, 1)
104+ tg.EXPECT().Draw(mock.Anything, mock.Anything, mock.Anything).Return(false, nil).Once()
105+ if ok, err := r.Render(&out, &m, testView); ok || err != nil {
106+ t.Fatalf("no buffer: ok=%v err=%v", ok, err)
107+ }
108+ drawOK(tg, nil).Once()
109+ if ok, err := r.Render(&out, &m, testView); !ok || err != nil {
110+ t.Fatalf("retry: ok=%v err=%v", ok, err)
111+ }
112+}
113+
114+func TestRendererDrawError(t *testing.T) {
115+ r, tg := newTestRenderer(t)
116+ var m testModel
117+ var out Output
118+ tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once()
119+ r.Resize(100, 50, 1)
120+ boom := errors.New("boom")
121+ tg.EXPECT().Draw(mock.Anything, mock.Anything, mock.Anything).Return(false, boom).Once()
122+ if ok, err := r.Render(&out, &m, testView); ok || !errors.Is(err, boom) {
123+ t.Fatalf("ok=%v err=%v", ok, err)
124+ }
125+}
126+
127+func TestRendererResizeReachesTargetInPhysicalPixels(t *testing.T) {
128+ r, tg := newTestRenderer(t)
129+ tg.EXPECT().Resize(int32(150), int32(75)).Return().Once()
130+ r.Resize(100, 50, 1.5)
131+ r.Resize(100, 50, 1.5) // unchanged: no second call
132+ r.Resize(0, 50, 1) // invalid: ignored
133+ r.Resize(10, 10, 0) // invalid: ignored
134+}
135+
136+func TestRendererInputActivatesButtonAndRequestsRedraw(t *testing.T) {
137+ r, tg := newTestRenderer(t)
138+ var m testModel
139+ var out Output
140+ tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once()
141+ r.Resize(200, 100, 1)
142+ drawOK(tg, nil).Maybe()
143+ for i := 0; i < 3; i++ {
144+ if _, err := r.Render(&out, &m, testView); err != nil {
145+ t.Fatal(err)
146+ }
147+ }
148+ if r.Input(&Input{Kind: InputPointerMotion, X: 5, Y: 5}) != true {
149+ t.Fatal("hover over a button needs a redraw")
150+ }
151+ r.Input(&Input{Kind: InputPointerPress, X: 5, Y: 5, Button: 0x110, Pressed: true})
152+ if !r.Input(&Input{Kind: InputPointerRelease, X: 5, Y: 5, Button: 0x110}) {
153+ t.Fatal("release did not request redraw")
154+ }
155+ for i := 0; i < 3; i++ {
156+ if _, err := r.Render(&out, &m, testView); err != nil {
157+ t.Fatal(err)
158+ }
159+ }
160+ if m.clicks != 1 {
161+ t.Fatalf("clicks=%d", m.clicks)
162+ }
163+ // Keyboard: Tab focuses, Return activates, text comes from Text.
164+ r.Input(&Input{Kind: InputPointerLeave})
165+ r.Input(&Input{Kind: InputKey, Keysym: 0xff09, Pressed: true})
166+ r.Input(&Input{Kind: InputKey, Keysym: 0xff0d, Pressed: true})
167+ for i := 0; i < 3; i++ {
168+ if _, err := r.Render(&out, &m, testView); err != nil {
169+ t.Fatal(err)
170+ }
171+ }
172+ if m.clicks != 2 {
173+ t.Fatalf("Return did not activate the focused button: clicks=%d", m.clicks)
174+ }
175+ if r.Input(&Input{Kind: 99}) || r.Input(nil) {
176+ t.Fatal("unknown input requested a redraw")
177+ }
178+ if r.Input(&Input{Kind: InputPointerAxis}) {
179+ t.Fatal("zero scroll requested a redraw")
180+ }
181+}
182+
183+func TestRendererTextInputUsesTextBytes(t *testing.T) {
184+ r, tg := newTestRenderer(t)
185+ value := ""
186+ var out Output
187+ view := func(f *Frame, v *string) { f.Root().Input("name", v, Key("name"), Inline("width:100px;height:20px")) }
188+ tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once()
189+ r.Resize(200, 100, 1)
190+ drawOK(tg, nil).Maybe()
191+ settle := func() {
192+ for i := 0; i < 3; i++ {
193+ if _, err := r.Render(&out, &value, view); err != nil {
194+ t.Fatal(err)
195+ }
196+ }
197+ }
198+ settle()
199+ r.Input(&Input{Kind: InputPointerPress, X: 5, Y: 5, Button: 0x110, Pressed: true})
200+ r.Input(&Input{Kind: InputPointerRelease, X: 5, Y: 5, Button: 0x110})
201+ settle()
202+ text := []byte("é")
203+ r.Input(&Input{Kind: InputKey, Keysym: 0xe9, Pressed: true, Text: text})
204+ text[0] = 0 // the caller may reuse the buffer after the call
205+ settle()
206+ if value != "é" {
207+ t.Fatalf("value=%q", value)
208+ }
209+}
210+
211+func TestRendererOutputCarriesCursorAndInputRects(t *testing.T) {
212+ r, tg := newTestRenderer(t)
213+ m := testModel{rects: []Rect{{1, 2, 30, 20}}}
214+ var out Output
215+ tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once()
216+ r.Resize(200, 100, 1)
217+ drawOK(tg, nil).Maybe()
218+ r.Input(&Input{Kind: InputPointerMotion, X: 5, Y: 5}) // over the button: pointer cursor
219+ if ok, err := r.Render(&out, &m, testView); !ok || err != nil {
220+ t.Fatalf("ok=%v err=%v", ok, err)
221+ }
222+ if out.Cursor != CursorPointer || !out.InputRectsChanged || len(out.InputRects) != 1 || out.InputRects[0] != (Rect{1, 2, 30, 20}) {
223+ t.Fatalf("output: cursor=%v changed=%v rects=%v", out.Cursor, out.InputRectsChanged, out.InputRects)
224+ }
225+ // An unchanged region is not reported again.
226+ r.Invalidate()
227+ ok, err := r.Render(&out, &m, testView)
228+ if err != nil {
229+ t.Fatal(err)
230+ }
231+ if ok && out.InputRectsChanged && len(out.InputRects) != 0 {
232+ t.Fatalf("region reported twice: %+v", out)
233+ }
234+ // nil restores the whole surface: changed with no rects.
235+ m.rects = nil
236+ var nilView = func(f *Frame, _ *testModel) { f.Root(); _ = f.SetInputRects(nil) }
237+ r.Invalidate()
238+ if ok, err = r.Render(&out, &m, nilView); err != nil || !ok || !out.InputRectsChanged || out.InputRects != nil {
239+ t.Fatalf("nil region: ok=%v err=%v %+v", ok, err, out)
240+ }
241+}
242+
243+func TestRendererForwardsReleasedAndClose(t *testing.T) {
244+ r, tg := newTestRenderer(t)
245+ tg.EXPECT().Released(uint64(3)).Return(nil).Once()
246+ if err := r.Released(3); err != nil {
247+ t.Fatal(err)
248+ }
249+ tg.EXPECT().Close().Return(nil).Once()
250+ if err := r.Close(); err != nil {
251+ t.Fatal(err)
252+ }
253+ if err := r.Close(); err != nil { // second Close is a no-op
254+ t.Fatal(err)
255+ }
256+ var out Output
257+ var m testModel
258+ if _, err := r.Render(&out, &m, testView); err == nil {
259+ t.Fatal("Render after Close succeeded")
260+ }
261+}
262+
263+func TestTextIntAndMasked(t *testing.T) {
264+ r := newRuntime()
265+ r.Build(func(f *Frame) {
266+ root := f.Root()
267+ root.TextInt("Count: ", -42)
268+ root.Masked(3)
269+ root.Masked(0)
270+ root.Masked(300)
271+ })
272+ c := r.committed.children
273+ if len(c) != 4 || c[0].text != "Count: -42" || c[1].text != "•••" || c[2].text != "" || len([]rune(c[3].text)) != 300 {
274+ t.Fatalf("texts: %q %q %q", c[0].text, c[1].text, c[2].text)
275+ }
276+}
A internal/ui/target_mock_test.go +260 -0
@@ -0,0 +1,260 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package ui
6+
7+import (
8+ "github.com/bnema/nefergui/internal/layout"
9+ "github.com/bnema/nefergui/internal/presentation/session"
10+ mock "github.com/stretchr/testify/mock"
11+)
12+
13+// newMocktarget creates a new instance of mocktarget. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
14+// The first argument is typically a *testing.T value.
15+func newMocktarget(t interface {
16+ mock.TestingT
17+ Cleanup(func())
18+}) *mocktarget {
19+ if helper, ok := t.(interface{ Helper() }); ok {
20+ helper.Helper()
21+ }
22+
23+ mock := &mocktarget{}
24+ mock.Mock.Test(t)
25+
26+ t.Cleanup(func() {
27+ if helper, ok := t.(interface{ Helper() }); ok {
28+ helper.Helper()
29+ }
30+ mock.AssertExpectations(t)
31+ })
32+
33+ return mock
34+}
35+
36+// mocktarget is an autogenerated mock type for the target type
37+type mocktarget struct {
38+ mock.Mock
39+}
40+
41+type mocktarget_Expecter struct {
42+ mock *mock.Mock
43+}
44+
45+func (_m *mocktarget) EXPECT() *mocktarget_Expecter {
46+ return &mocktarget_Expecter{mock: &_m.Mock}
47+}
48+
49+// Close provides a mock function for the type mocktarget
50+func (_mock *mocktarget) Close() error {
51+ ret := _mock.Called()
52+
53+ if len(ret) == 0 {
54+ panic("no return value specified for Close")
55+ }
56+
57+ var r0 error
58+ if returnFunc, ok := ret.Get(0).(func() error); ok {
59+ r0 = returnFunc()
60+ } else {
61+ r0 = ret.Error(0)
62+ }
63+ return r0
64+}
65+
66+// mocktarget_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
67+type mocktarget_Close_Call struct {
68+ *mock.Call
69+}
70+
71+// Close is a helper method to define mock.On call
72+func (_e *mocktarget_Expecter) Close() *mocktarget_Close_Call {
73+ return &mocktarget_Close_Call{Call: _e.mock.On("Close")}
74+}
75+
76+func (_c *mocktarget_Close_Call) Run(run func()) *mocktarget_Close_Call {
77+ _c.Call.Run(func(args mock.Arguments) {
78+ run()
79+ })
80+ return _c
81+}
82+
83+func (_c *mocktarget_Close_Call) Return(err error) *mocktarget_Close_Call {
84+ _c.Call.Return(err)
85+ return _c
86+}
87+
88+func (_c *mocktarget_Close_Call) RunAndReturn(run func() error) *mocktarget_Close_Call {
89+ _c.Call.Return(run)
90+ return _c
91+}
92+
93+// Draw provides a mock function for the type mocktarget
94+func (_mock *mocktarget) Draw(display []layout.Command, scale float64, out *session.Output) (bool, error) {
95+ ret := _mock.Called(display, scale, out)
96+
97+ if len(ret) == 0 {
98+ panic("no return value specified for Draw")
99+ }
100+
101+ var r0 bool
102+ var r1 error
103+ if returnFunc, ok := ret.Get(0).(func([]layout.Command, float64, *session.Output) (bool, error)); ok {
104+ return returnFunc(display, scale, out)
105+ }
106+ if returnFunc, ok := ret.Get(0).(func([]layout.Command, float64, *session.Output) bool); ok {
107+ r0 = returnFunc(display, scale, out)
108+ } else {
109+ r0 = ret.Get(0).(bool)
110+ }
111+ if returnFunc, ok := ret.Get(1).(func([]layout.Command, float64, *session.Output) error); ok {
112+ r1 = returnFunc(display, scale, out)
113+ } else {
114+ r1 = ret.Error(1)
115+ }
116+ return r0, r1
117+}
118+
119+// mocktarget_Draw_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Draw'
120+type mocktarget_Draw_Call struct {
121+ *mock.Call
122+}
123+
124+// Draw is a helper method to define mock.On call
125+// - display []layout.Command
126+// - scale float64
127+// - out *session.Output
128+func (_e *mocktarget_Expecter) Draw(display any, scale any, out any) *mocktarget_Draw_Call {
129+ return &mocktarget_Draw_Call{Call: _e.mock.On("Draw", display, scale, out)}
130+}
131+
132+func (_c *mocktarget_Draw_Call) Run(run func(display []layout.Command, scale float64, out *session.Output)) *mocktarget_Draw_Call {
133+ _c.Call.Run(func(args mock.Arguments) {
134+ var arg0 []layout.Command
135+ if args[0] != nil {
136+ arg0 = args[0].([]layout.Command)
137+ }
138+ var arg1 float64
139+ if args[1] != nil {
140+ arg1 = args[1].(float64)
141+ }
142+ var arg2 *session.Output
143+ if args[2] != nil {
144+ arg2 = args[2].(*session.Output)
145+ }
146+ run(
147+ arg0,
148+ arg1,
149+ arg2,
150+ )
151+ })
152+ return _c
153+}
154+
155+func (_c *mocktarget_Draw_Call) Return(b bool, err error) *mocktarget_Draw_Call {
156+ _c.Call.Return(b, err)
157+ return _c
158+}
159+
160+func (_c *mocktarget_Draw_Call) RunAndReturn(run func(display []layout.Command, scale float64, out *session.Output) (bool, error)) *mocktarget_Draw_Call {
161+ _c.Call.Return(run)
162+ return _c
163+}
164+
165+// Released provides a mock function for the type mocktarget
166+func (_mock *mocktarget) Released(buffer uint64) error {
167+ ret := _mock.Called(buffer)
168+
169+ if len(ret) == 0 {
170+ panic("no return value specified for Released")
171+ }
172+
173+ var r0 error
174+ if returnFunc, ok := ret.Get(0).(func(uint64) error); ok {
175+ r0 = returnFunc(buffer)
176+ } else {
177+ r0 = ret.Error(0)
178+ }
179+ return r0
180+}
181+
182+// mocktarget_Released_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Released'
183+type mocktarget_Released_Call struct {
184+ *mock.Call
185+}
186+
187+// Released is a helper method to define mock.On call
188+// - buffer uint64
189+func (_e *mocktarget_Expecter) Released(buffer any) *mocktarget_Released_Call {
190+ return &mocktarget_Released_Call{Call: _e.mock.On("Released", buffer)}
191+}
192+
193+func (_c *mocktarget_Released_Call) Run(run func(buffer uint64)) *mocktarget_Released_Call {
194+ _c.Call.Run(func(args mock.Arguments) {
195+ var arg0 uint64
196+ if args[0] != nil {
197+ arg0 = args[0].(uint64)
198+ }
199+ run(
200+ arg0,
201+ )
202+ })
203+ return _c
204+}
205+
206+func (_c *mocktarget_Released_Call) Return(err error) *mocktarget_Released_Call {
207+ _c.Call.Return(err)
208+ return _c
209+}
210+
211+func (_c *mocktarget_Released_Call) RunAndReturn(run func(buffer uint64) error) *mocktarget_Released_Call {
212+ _c.Call.Return(run)
213+ return _c
214+}
215+
216+// Resize provides a mock function for the type mocktarget
217+func (_mock *mocktarget) Resize(width int32, height int32) {
218+ _mock.Called(width, height)
219+ return
220+}
221+
222+// mocktarget_Resize_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Resize'
223+type mocktarget_Resize_Call struct {
224+ *mock.Call
225+}
226+
227+// Resize is a helper method to define mock.On call
228+// - width int32
229+// - height int32
230+func (_e *mocktarget_Expecter) Resize(width any, height any) *mocktarget_Resize_Call {
231+ return &mocktarget_Resize_Call{Call: _e.mock.On("Resize", width, height)}
232+}
233+
234+func (_c *mocktarget_Resize_Call) Run(run func(width int32, height int32)) *mocktarget_Resize_Call {
235+ _c.Call.Run(func(args mock.Arguments) {
236+ var arg0 int32
237+ if args[0] != nil {
238+ arg0 = args[0].(int32)
239+ }
240+ var arg1 int32
241+ if args[1] != nil {
242+ arg1 = args[1].(int32)
243+ }
244+ run(
245+ arg0,
246+ arg1,
247+ )
248+ })
249+ return _c
250+}
251+
252+func (_c *mocktarget_Resize_Call) Return() *mocktarget_Resize_Call {
253+ _c.Call.Return()
254+ return _c
255+}
256+
257+func (_c *mocktarget_Resize_Call) RunAndReturn(run func(width int32, height int32)) *mocktarget_Resize_Call {
258+ _c.Run(run)
259+ return _c
260+}
@@ -0,0 +1,260 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package ui
6+
7+import (
8+ "github.com/bnema/nefergui/internal/layout"
9+ "github.com/bnema/nefergui/internal/presentation/session"
10+ mock "github.com/stretchr/testify/mock"
11+)
12+
13+// newMocktarget creates a new instance of mocktarget. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
14+// The first argument is typically a *testing.T value.
15+func newMocktarget(t interface {
16+ mock.TestingT
17+ Cleanup(func())
18+}) *mocktarget {
19+ if helper, ok := t.(interface{ Helper() }); ok {
20+ helper.Helper()
21+ }
22+
23+ mock := &mocktarget{}
24+ mock.Mock.Test(t)
25+
26+ t.Cleanup(func() {
27+ if helper, ok := t.(interface{ Helper() }); ok {
28+ helper.Helper()
29+ }
30+ mock.AssertExpectations(t)
31+ })
32+
33+ return mock
34+}
35+
36+// mocktarget is an autogenerated mock type for the target type
37+type mocktarget struct {
38+ mock.Mock
39+}
40+
41+type mocktarget_Expecter struct {
42+ mock *mock.Mock
43+}
44+
45+func (_m *mocktarget) EXPECT() *mocktarget_Expecter {
46+ return &mocktarget_Expecter{mock: &_m.Mock}
47+}
48+
49+// Close provides a mock function for the type mocktarget
50+func (_mock *mocktarget) Close() error {
51+ ret := _mock.Called()
52+
53+ if len(ret) == 0 {
54+ panic("no return value specified for Close")
55+ }
56+
57+ var r0 error
58+ if returnFunc, ok := ret.Get(0).(func() error); ok {
59+ r0 = returnFunc()
60+ } else {
61+ r0 = ret.Error(0)
62+ }
63+ return r0
64+}
65+
66+// mocktarget_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
67+type mocktarget_Close_Call struct {
68+ *mock.Call
69+}
70+
71+// Close is a helper method to define mock.On call
72+func (_e *mocktarget_Expecter) Close() *mocktarget_Close_Call {
73+ return &mocktarget_Close_Call{Call: _e.mock.On("Close")}
74+}
75+
76+func (_c *mocktarget_Close_Call) Run(run func()) *mocktarget_Close_Call {
77+ _c.Call.Run(func(args mock.Arguments) {
78+ run()
79+ })
80+ return _c
81+}
82+
83+func (_c *mocktarget_Close_Call) Return(err error) *mocktarget_Close_Call {
84+ _c.Call.Return(err)
85+ return _c
86+}
87+
88+func (_c *mocktarget_Close_Call) RunAndReturn(run func() error) *mocktarget_Close_Call {
89+ _c.Call.Return(run)
90+ return _c
91+}
92+
93+// Draw provides a mock function for the type mocktarget
94+func (_mock *mocktarget) Draw(display []layout.Command, scale float64, out *session.Output) (bool, error) {
95+ ret := _mock.Called(display, scale, out)
96+
97+ if len(ret) == 0 {
98+ panic("no return value specified for Draw")
99+ }
100+
101+ var r0 bool
102+ var r1 error
103+ if returnFunc, ok := ret.Get(0).(func([]layout.Command, float64, *session.Output) (bool, error)); ok {
104+ return returnFunc(display, scale, out)
105+ }
106+ if returnFunc, ok := ret.Get(0).(func([]layout.Command, float64, *session.Output) bool); ok {
107+ r0 = returnFunc(display, scale, out)
108+ } else {
109+ r0 = ret.Get(0).(bool)
110+ }
111+ if returnFunc, ok := ret.Get(1).(func([]layout.Command, float64, *session.Output) error); ok {
112+ r1 = returnFunc(display, scale, out)
113+ } else {
114+ r1 = ret.Error(1)
115+ }
116+ return r0, r1
117+}
118+
119+// mocktarget_Draw_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Draw'
120+type mocktarget_Draw_Call struct {
121+ *mock.Call
122+}
123+
124+// Draw is a helper method to define mock.On call
125+// - display []layout.Command
126+// - scale float64
127+// - out *session.Output
128+func (_e *mocktarget_Expecter) Draw(display any, scale any, out any) *mocktarget_Draw_Call {
129+ return &mocktarget_Draw_Call{Call: _e.mock.On("Draw", display, scale, out)}
130+}
131+
132+func (_c *mocktarget_Draw_Call) Run(run func(display []layout.Command, scale float64, out *session.Output)) *mocktarget_Draw_Call {
133+ _c.Call.Run(func(args mock.Arguments) {
134+ var arg0 []layout.Command
135+ if args[0] != nil {
136+ arg0 = args[0].([]layout.Command)
137+ }
138+ var arg1 float64
139+ if args[1] != nil {
140+ arg1 = args[1].(float64)
141+ }
142+ var arg2 *session.Output
143+ if args[2] != nil {
144+ arg2 = args[2].(*session.Output)
145+ }
146+ run(
147+ arg0,
148+ arg1,
149+ arg2,
150+ )
151+ })
152+ return _c
153+}
154+
155+func (_c *mocktarget_Draw_Call) Return(b bool, err error) *mocktarget_Draw_Call {
156+ _c.Call.Return(b, err)
157+ return _c
158+}
159+
160+func (_c *mocktarget_Draw_Call) RunAndReturn(run func(display []layout.Command, scale float64, out *session.Output) (bool, error)) *mocktarget_Draw_Call {
161+ _c.Call.Return(run)
162+ return _c
163+}
164+
165+// Released provides a mock function for the type mocktarget
166+func (_mock *mocktarget) Released(buffer uint64) error {
167+ ret := _mock.Called(buffer)
168+
169+ if len(ret) == 0 {
170+ panic("no return value specified for Released")
171+ }
172+
173+ var r0 error
174+ if returnFunc, ok := ret.Get(0).(func(uint64) error); ok {
175+ r0 = returnFunc(buffer)
176+ } else {
177+ r0 = ret.Error(0)
178+ }
179+ return r0
180+}
181+
182+// mocktarget_Released_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Released'
183+type mocktarget_Released_Call struct {
184+ *mock.Call
185+}
186+
187+// Released is a helper method to define mock.On call
188+// - buffer uint64
189+func (_e *mocktarget_Expecter) Released(buffer any) *mocktarget_Released_Call {
190+ return &mocktarget_Released_Call{Call: _e.mock.On("Released", buffer)}
191+}
192+
193+func (_c *mocktarget_Released_Call) Run(run func(buffer uint64)) *mocktarget_Released_Call {
194+ _c.Call.Run(func(args mock.Arguments) {
195+ var arg0 uint64
196+ if args[0] != nil {
197+ arg0 = args[0].(uint64)
198+ }
199+ run(
200+ arg0,
201+ )
202+ })
203+ return _c
204+}
205+
206+func (_c *mocktarget_Released_Call) Return(err error) *mocktarget_Released_Call {
207+ _c.Call.Return(err)
208+ return _c
209+}
210+
211+func (_c *mocktarget_Released_Call) RunAndReturn(run func(buffer uint64) error) *mocktarget_Released_Call {
212+ _c.Call.Return(run)
213+ return _c
214+}
215+
216+// Resize provides a mock function for the type mocktarget
217+func (_mock *mocktarget) Resize(width int32, height int32) {
218+ _mock.Called(width, height)
219+ return
220+}
221+
222+// mocktarget_Resize_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Resize'
223+type mocktarget_Resize_Call struct {
224+ *mock.Call
225+}
226+
227+// Resize is a helper method to define mock.On call
228+// - width int32
229+// - height int32
230+func (_e *mocktarget_Expecter) Resize(width any, height any) *mocktarget_Resize_Call {
231+ return &mocktarget_Resize_Call{Call: _e.mock.On("Resize", width, height)}
232+}
233+
234+func (_c *mocktarget_Resize_Call) Run(run func(width int32, height int32)) *mocktarget_Resize_Call {
235+ _c.Call.Run(func(args mock.Arguments) {
236+ var arg0 int32
237+ if args[0] != nil {
238+ arg0 = args[0].(int32)
239+ }
240+ var arg1 int32
241+ if args[1] != nil {
242+ arg1 = args[1].(int32)
243+ }
244+ run(
245+ arg0,
246+ arg1,
247+ )
248+ })
249+ return _c
250+}
251+
252+func (_c *mocktarget_Resize_Call) Return() *mocktarget_Resize_Call {
253+ _c.Call.Return()
254+ return _c
255+}
256+
257+func (_c *mocktarget_Resize_Call) RunAndReturn(run func(width int32, height int32)) *mocktarget_Resize_Call {
258+ _c.Run(run)
259+ return _c
260+}

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