1//go:build linux
2
3package ui
4
5import (
6 "errors"
7 "fmt"
8 "testing"
9
10 "github.com/bnema/nefergui/internal/layout"
11 "github.com/bnema/nefergui/internal/presentation/session"
12 "github.com/bnema/nefergui/internal/text"
13 "github.com/stretchr/testify/mock"
14)
15
16type testModel struct {
17 clicks int
18 rects []Rect
19}
20
21func 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
31func 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
41func 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
52func 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
66func 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 // Nothing changed: no rebuild result, so no Draw (the mock has no further expectation).
83 if ok, err := r.Render(&out, &m, testView); ok || err != nil {
84 t.Fatalf("unchanged view: ok=%v err=%v", ok, err)
85 }
86}
87
88func TestRendererKeepsFrameWhenNoBufferFree(t *testing.T) {
89 r, tg := newTestRenderer(t)
90 var m testModel
91 var out Output
92 tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once()
93 r.Resize(100, 50, 1)
94 tg.EXPECT().Draw(mock.Anything, mock.Anything, mock.Anything).Return(false, nil).Once()
95 tg.EXPECT().Waiting().Return(true).Once()
96 if ok, err := r.Render(&out, &m, testView); ok || err != nil {
97 t.Fatalf("no buffer: ok=%v err=%v", ok, err)
98 }
99 if !r.Pending() {
100 t.Fatal("a built frame blocked on the GPU must report Pending")
101 }
102 drawOK(tg, nil).Once()
103 if ok, err := r.Render(&out, &m, testView); !ok || err != nil {
104 t.Fatalf("retry: ok=%v err=%v", ok, err)
105 }
106}
107
108func TestRendererDrawError(t *testing.T) {
109 r, tg := newTestRenderer(t)
110 var m testModel
111 var out Output
112 tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once()
113 r.Resize(100, 50, 1)
114 boom := errors.New("boom")
115 tg.EXPECT().Draw(mock.Anything, mock.Anything, mock.Anything).Return(false, boom).Once()
116 if ok, err := r.Render(&out, &m, testView); ok || !errors.Is(err, boom) {
117 t.Fatalf("ok=%v err=%v", ok, err)
118 }
119}
120
121func TestRendererResizeReachesTargetInPhysicalPixels(t *testing.T) {
122 r, tg := newTestRenderer(t)
123 tg.EXPECT().Resize(int32(150), int32(75)).Return().Once()
124 r.Resize(100, 50, 1.5)
125 r.Resize(100, 50, 1.5) // unchanged: no second call
126 r.Resize(0, 50, 1) // invalid: ignored
127 r.Resize(10, 10, 0) // invalid: ignored
128}
129
130func TestRendererInputActivatesButtonAndRequestsRedraw(t *testing.T) {
131 r, tg := newTestRenderer(t)
132 var m testModel
133 var out Output
134 tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once()
135 r.Resize(200, 100, 1)
136 drawOK(tg, nil).Maybe()
137 for i := 0; i < 3; i++ {
138 if _, err := r.Render(&out, &m, testView); err != nil {
139 t.Fatal(err)
140 }
141 }
142 if r.Input(&Input{Kind: InputPointerMotion, X: 5, Y: 5}) != true {
143 t.Fatal("hover over a button needs a redraw")
144 }
145 r.Input(&Input{Kind: InputPointerPress, X: 5, Y: 5, Button: 0x110, Pressed: true})
146 if !r.Input(&Input{Kind: InputPointerRelease, X: 5, Y: 5, Button: 0x110}) {
147 t.Fatal("release did not request redraw")
148 }
149 for i := 0; i < 3; i++ {
150 if _, err := r.Render(&out, &m, testView); err != nil {
151 t.Fatal(err)
152 }
153 }
154 if m.clicks != 1 {
155 t.Fatalf("clicks=%d", m.clicks)
156 }
157 // Keyboard: Tab focuses, Return activates, text comes from Text.
158 r.Input(&Input{Kind: InputPointerLeave})
159 r.Input(&Input{Kind: InputKey, Keysym: 0xff09, Pressed: true})
160 r.Input(&Input{Kind: InputKey, Keysym: 0xff0d, Pressed: true})
161 for i := 0; i < 3; i++ {
162 if _, err := r.Render(&out, &m, testView); err != nil {
163 t.Fatal(err)
164 }
165 }
166 if m.clicks != 2 {
167 t.Fatalf("Return did not activate the focused button: clicks=%d", m.clicks)
168 }
169 if r.Input(&Input{Kind: 99}) || r.Input(nil) {
170 t.Fatal("unknown input requested a redraw")
171 }
172 if r.Input(&Input{Kind: InputPointerAxis}) {
173 t.Fatal("zero scroll requested a redraw")
174 }
175}
176
177func TestRendererTextInputUsesTextBytes(t *testing.T) {
178 value := ""
179 view := func(f *Frame, v *string) { f.Root().Input("name", v, Key("name"), Inline("width:100px;height:20px")) }
180 r, settle := settledRenderer(t, &value, view)
181 r.Input(&Input{Kind: InputPointerPress, X: 5, Y: 5, Button: 0x110, Pressed: true})
182 r.Input(&Input{Kind: InputPointerRelease, X: 5, Y: 5, Button: 0x110})
183 settle()
184 text := []byte("é")
185 r.Input(&Input{Kind: InputKey, Keysym: 0xe9, Pressed: true, Text: text})
186 text[0] = 0 // the caller may reuse the buffer after the call
187 settle()
188 if value != "é" {
189 t.Fatalf("value=%q", value)
190 }
191}
192
193func TestRendererOutputCarriesCursorAndInputRects(t *testing.T) {
194 r, tg := newTestRenderer(t)
195 m := testModel{rects: []Rect{{1, 2, 30, 20}}}
196 var out Output
197 tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once()
198 r.Resize(200, 100, 1)
199 drawOK(tg, nil).Maybe()
200 r.Input(&Input{Kind: InputPointerMotion, X: 5, Y: 5}) // over the button: pointer cursor
201 if ok, err := r.Render(&out, &m, testView); !ok || err != nil {
202 t.Fatalf("ok=%v err=%v", ok, err)
203 }
204 if out.Cursor != CursorPointer || !out.InputRectsChanged || len(out.InputRects) != 1 || out.InputRects[0] != (Rect{1, 2, 30, 20}) {
205 t.Fatalf("output: cursor=%v changed=%v rects=%v", out.Cursor, out.InputRectsChanged, out.InputRects)
206 }
207 // An unchanged region is not reported again.
208 r.Invalidate()
209 ok, err := r.Render(&out, &m, testView)
210 if err != nil {
211 t.Fatal(err)
212 }
213 if ok && out.InputRectsChanged && len(out.InputRects) != 0 {
214 t.Fatalf("region reported twice: %+v", out)
215 }
216 // nil restores the whole surface: changed with no rects.
217 m.rects = nil
218 var nilView = func(f *Frame, _ *testModel) { f.Root(); _ = f.SetInputRects(nil) }
219 r.Invalidate()
220 if ok, err = r.Render(&out, &m, nilView); err != nil || !ok || !out.InputRectsChanged || out.InputRects != nil {
221 t.Fatalf("nil region: ok=%v err=%v %+v", ok, err, out)
222 }
223}
224
225func TestRendererForwardsReleasedAndClose(t *testing.T) {
226 r, tg := newTestRenderer(t)
227 tg.EXPECT().Released(uint64(3)).Return(nil).Once()
228 if err := r.Released(3); err != nil {
229 t.Fatal(err)
230 }
231 tg.EXPECT().Close().Return(nil).Once()
232 if err := r.Close(); err != nil {
233 t.Fatal(err)
234 }
235 // After Close nothing reaches the target (the mock has no expectation).
236 r.Resize(10, 10, 1)
237 if r.Input(&Input{Kind: InputPointerMotion}) {
238 t.Fatal("Input after Close requested a redraw")
239 }
240 if err := r.Released(3); err == nil {
241 t.Fatal("Released after Close succeeded")
242 }
243 if err := r.Close(); err != nil { // second Close is a no-op
244 t.Fatal(err)
245 }
246 var out Output
247 var m testModel
248 if _, err := r.Render(&out, &m, testView); err == nil {
249 t.Fatal("Render after Close succeeded")
250 }
251}
252
253func TestTextIntAndMasked(t *testing.T) {
254 r := newRuntime()
255 r.Build(func(f *Frame) {
256 root := f.Root()
257 root.TextInt("Count: ", -42)
258 root.Masked(3)
259 root.Masked(0)
260 root.Masked(300)
261 })
262 c := r.committed.children
263 if len(c) != 4 || c[0].text != "Count: -42" || c[1].text != "•••" || c[2].text != "" || len([]rune(c[3].text)) != 300 {
264 t.Fatalf("texts: %q %q %q", c[0].text, c[1].text, c[2].text)
265 }
266}
267
268// settledRenderer returns a 200x100 renderer for view and a settle function
269// that renders until queued input has been applied.
270func settledRenderer[T any](t *testing.T, model *T, view func(*Frame, *T)) (*Renderer, func()) {
271 t.Helper()
272 r, tg := newTestRenderer(t)
273 tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once()
274 r.Resize(200, 100, 1)
275 drawOK(tg, nil).Maybe()
276 var out Output
277 settle := func() {
278 for i := 0; i < 3; i++ {
279 if _, err := r.Render(&out, model, view); err != nil {
280 t.Fatal(err)
281 }
282 }
283 }
284 settle()
285 return r, settle
286}
287
288func TestModifierBits(t *testing.T) {
289 // The values match neferclient's, so applications copy the bits as is.
290 if ModShift != 1 || ModCtrl != 2 || ModAlt != 4 || ModSuper != 8 || ModCapsLock != 16 || ModNumLock != 32 {
291 t.Fatal("modifier bit values changed")
292 }
293}
294
295// Caps Lock and Num Lock must not change shortcuts, selection or activation.
296func TestRendererLockModifiersDoNotChangeInput(t *testing.T) {
297 inputView := func(f *Frame, v *string) { f.Root().Input("name", v, Key("name"), Inline("width:100px;height:20px")) }
298 key := func(r *Renderer, keysym uint32, text string, mods Modifiers) {
299 in := &Input{Kind: InputKey, Keysym: keysym, Pressed: true, Modifiers: mods}
300 if text != "" {
301 in.Text = []byte(text)
302 }
303 r.Input(in)
304 }
305 click := func(r *Renderer, x float64, mods Modifiers) {
306 r.Input(&Input{Kind: InputPointerPress, X: x, Y: 5, Button: 0x110, Pressed: true, Modifiers: mods})
307 r.Input(&Input{Kind: InputPointerRelease, X: x, Y: 5, Button: 0x110, Modifiers: mods})
308 }
309 for _, lock := range []Modifiers{0, ModCapsLock, ModNumLock, ModCapsLock | ModNumLock} {
310 t.Run(fmt.Sprintf("lock=%#x", uint8(lock)), func(t *testing.T) {
311 t.Run("CtrlA and ShiftLeft", func(t *testing.T) {
312 value := "hello"
313 r, settle := settledRenderer(t, &value, inputView)
314 click(r, 5, lock)
315 settle()
316 key(r, 'a', "a", ModCtrl|lock) // select all
317 settle()
318 key(r, 'X', "X", lock)
319 settle()
320 if value != "X" {
321 t.Fatalf("Ctrl+A then text: value=%q, want X", value)
322 }
323 key(r, 0xff51, "", ModShift|lock) // extend selection left
324 settle()
325 key(r, 'y', "y", lock)
326 settle()
327 if value != "y" {
328 t.Fatalf("Shift+Left then text: value=%q, want y", value)
329 }
330 })
331 t.Run("ShiftClick", func(t *testing.T) {
332 value := "hello"
333 r, settle := settledRenderer(t, &value, inputView)
334 click(r, 1, lock)
335 settle()
336 click(r, 95, ModShift|lock) // select to the end
337 settle()
338 key(r, 'Z', "Z", lock)
339 settle()
340 if value != "Z" {
341 t.Fatalf("Shift+click then text: value=%q, want Z", value)
342 }
343 })
344 t.Run("TabReturn", func(t *testing.T) {
345 m := testModel{}
346 r, settle := settledRenderer(t, &m, testView)
347 key(r, 0xff09, "", lock)
348 key(r, 0xff0d, "", lock)
349 settle()
350 if m.clicks != 1 {
351 t.Fatalf("Tab+Return clicks=%d, want 1", m.clicks)
352 }
353 })
354 })
355 }
356}