1//go:build linux
2
3package ui
4
5import (
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
15type testModel struct {
16 clicks int
17 rects []Rect
18}
19
20func testView(f *Frame, m *testModel) {
21 root := f.Root()
22 if root.Button("Go", Key("go"), Inline("width:60px;height:20px")).Activated() {
23 m.clicks++
24 }
25 if m.rects != nil {
26 _ = f.SetInputRects(m.rects)
27 }
28}
29
30func newTestRenderer(t *testing.T) (*Renderer, *mocktarget) {
31 t.Helper()
32 tg := newMocktarget(t)
33 r, err := newRenderer(RendererConfig{}, tg, text.DirectorySource("../../testdata/fonts"))
34 if err != nil {
35 t.Fatal(err)
36 }
37 return r, tg
38}
39
40func drawOK(tg *mocktarget, fill func(*session.Output)) *mocktarget_Draw_Call {
41 return tg.EXPECT().Draw(mock.Anything, mock.Anything, mock.Anything).
42 RunAndReturn(func(_ []layout.Command, _ float64, out *session.Output) (bool, error) {
43 *out = session.Output{Buffer: 1, Width: 100, Height: 50}
44 if fill != nil {
45 fill(out)
46 }
47 return true, nil
48 })
49}
50
51func TestKeysymNames(t *testing.T) {
52 for sym, want := range map[uint32]string{
53 0x20: "space", 0xff09: "tab", 0xfe20: "iso_left_tab", 0xff51: "left", 0xff53: "right",
54 0xff52: "up", 0xff54: "down", 0xff50: "home", 0xff57: "end", 0xff55: "page_up",
55 0xff56: "page_down", 0xff08: "backspace", 0xffff: "delete", 'a': "a", 'A': "a",
56 'c': "c", 'x': "x", 'v': "v", 0xff0d: "return", 0xff8d: "kp_enter", 0xff1b: "escape",
57 'z': "", 0xffe1: "",
58 } {
59 if got := keysymName(sym); got != want {
60 t.Errorf("keysym %#x: got %q want %q", sym, got, want)
61 }
62 }
63}
64
65func TestRendererRenderBeforeResizeAndWhenUnchanged(t *testing.T) {
66 r, tg := newTestRenderer(t)
67 var m testModel
68 var out Output
69 if ok, err := r.Render(&out, &m, testView); ok || err != nil {
70 t.Fatalf("before Resize: ok=%v err=%v", ok, err)
71 }
72 tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once()
73 r.Resize(100, 50, 2)
74 drawOK(tg, nil).Once()
75 if ok, err := r.Render(&out, &m, testView); !ok || err != nil {
76 t.Fatalf("first render: ok=%v err=%v", ok, err)
77 }
78 if out.Buffer != 1 || out.Width != 100 || len(out.Damage) != 1 || out.Damage[0] != (Rect{Width: 100, Height: 50}) {
79 t.Fatalf("output: %+v", out)
80 }
81 // Nothing changed: no rebuild result, so no Draw (the mock has no further expectation).
82 if ok, err := r.Render(&out, &m, testView); ok || err != nil {
83 t.Fatalf("unchanged view: ok=%v err=%v", ok, err)
84 }
85}
86
87func TestRendererKeepsFrameWhenNoBufferFree(t *testing.T) {
88 r, tg := newTestRenderer(t)
89 var m testModel
90 var out Output
91 tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once()
92 r.Resize(100, 50, 1)
93 tg.EXPECT().Draw(mock.Anything, mock.Anything, mock.Anything).Return(false, nil).Once()
94 tg.EXPECT().Waiting().Return(true).Once()
95 if ok, err := r.Render(&out, &m, testView); ok || err != nil {
96 t.Fatalf("no buffer: ok=%v err=%v", ok, err)
97 }
98 if !r.Pending() {
99 t.Fatal("a built frame blocked on the GPU must report Pending")
100 }
101 drawOK(tg, nil).Once()
102 if ok, err := r.Render(&out, &m, testView); !ok || err != nil {
103 t.Fatalf("retry: ok=%v err=%v", ok, err)
104 }
105}
106
107func TestRendererDrawError(t *testing.T) {
108 r, tg := newTestRenderer(t)
109 var m testModel
110 var out Output
111 tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once()
112 r.Resize(100, 50, 1)
113 boom := errors.New("boom")
114 tg.EXPECT().Draw(mock.Anything, mock.Anything, mock.Anything).Return(false, boom).Once()
115 if ok, err := r.Render(&out, &m, testView); ok || !errors.Is(err, boom) {
116 t.Fatalf("ok=%v err=%v", ok, err)
117 }
118}
119
120func TestRendererResizeReachesTargetInPhysicalPixels(t *testing.T) {
121 r, tg := newTestRenderer(t)
122 tg.EXPECT().Resize(int32(150), int32(75)).Return().Once()
123 r.Resize(100, 50, 1.5)
124 r.Resize(100, 50, 1.5) // unchanged: no second call
125 r.Resize(0, 50, 1) // invalid: ignored
126 r.Resize(10, 10, 0) // invalid: ignored
127}
128
129func TestRendererInputActivatesButtonAndRequestsRedraw(t *testing.T) {
130 r, tg := newTestRenderer(t)
131 var m testModel
132 var out Output
133 tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once()
134 r.Resize(200, 100, 1)
135 drawOK(tg, nil).Maybe()
136 for i := 0; i < 3; i++ {
137 if _, err := r.Render(&out, &m, testView); err != nil {
138 t.Fatal(err)
139 }
140 }
141 if r.Input(&Input{Kind: InputPointerMotion, X: 5, Y: 5}) != true {
142 t.Fatal("hover over a button needs a redraw")
143 }
144 r.Input(&Input{Kind: InputPointerPress, X: 5, Y: 5, Button: 0x110, Pressed: true})
145 if !r.Input(&Input{Kind: InputPointerRelease, X: 5, Y: 5, Button: 0x110}) {
146 t.Fatal("release did not request redraw")
147 }
148 for i := 0; i < 3; i++ {
149 if _, err := r.Render(&out, &m, testView); err != nil {
150 t.Fatal(err)
151 }
152 }
153 if m.clicks != 1 {
154 t.Fatalf("clicks=%d", m.clicks)
155 }
156 // Keyboard: Tab focuses, Return activates, text comes from Text.
157 r.Input(&Input{Kind: InputPointerLeave})
158 r.Input(&Input{Kind: InputKey, Keysym: 0xff09, Pressed: true})
159 r.Input(&Input{Kind: InputKey, Keysym: 0xff0d, Pressed: true})
160 for i := 0; i < 3; i++ {
161 if _, err := r.Render(&out, &m, testView); err != nil {
162 t.Fatal(err)
163 }
164 }
165 if m.clicks != 2 {
166 t.Fatalf("Return did not activate the focused button: clicks=%d", m.clicks)
167 }
168 if r.Input(&Input{Kind: 99}) || r.Input(nil) {
169 t.Fatal("unknown input requested a redraw")
170 }
171 if r.Input(&Input{Kind: InputPointerAxis}) {
172 t.Fatal("zero scroll requested a redraw")
173 }
174}
175
176func TestRendererTextInputUsesTextBytes(t *testing.T) {
177 r, tg := newTestRenderer(t)
178 value := ""
179 var out Output
180 view := func(f *Frame, v *string) { f.Root().Input("name", v, Key("name"), Inline("width:100px;height:20px")) }
181 tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once()
182 r.Resize(200, 100, 1)
183 drawOK(tg, nil).Maybe()
184 settle := func() {
185 for i := 0; i < 3; i++ {
186 if _, err := r.Render(&out, &value, view); err != nil {
187 t.Fatal(err)
188 }
189 }
190 }
191 settle()
192 r.Input(&Input{Kind: InputPointerPress, X: 5, Y: 5, Button: 0x110, Pressed: true})
193 r.Input(&Input{Kind: InputPointerRelease, X: 5, Y: 5, Button: 0x110})
194 settle()
195 text := []byte("é")
196 r.Input(&Input{Kind: InputKey, Keysym: 0xe9, Pressed: true, Text: text})
197 text[0] = 0 // the caller may reuse the buffer after the call
198 settle()
199 if value != "é" {
200 t.Fatalf("value=%q", value)
201 }
202}
203
204func TestRendererOutputCarriesCursorAndInputRects(t *testing.T) {
205 r, tg := newTestRenderer(t)
206 m := testModel{rects: []Rect{{1, 2, 30, 20}}}
207 var out Output
208 tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once()
209 r.Resize(200, 100, 1)
210 drawOK(tg, nil).Maybe()
211 r.Input(&Input{Kind: InputPointerMotion, X: 5, Y: 5}) // over the button: pointer cursor
212 if ok, err := r.Render(&out, &m, testView); !ok || err != nil {
213 t.Fatalf("ok=%v err=%v", ok, err)
214 }
215 if out.Cursor != CursorPointer || !out.InputRectsChanged || len(out.InputRects) != 1 || out.InputRects[0] != (Rect{1, 2, 30, 20}) {
216 t.Fatalf("output: cursor=%v changed=%v rects=%v", out.Cursor, out.InputRectsChanged, out.InputRects)
217 }
218 // An unchanged region is not reported again.
219 r.Invalidate()
220 ok, err := r.Render(&out, &m, testView)
221 if err != nil {
222 t.Fatal(err)
223 }
224 if ok && out.InputRectsChanged && len(out.InputRects) != 0 {
225 t.Fatalf("region reported twice: %+v", out)
226 }
227 // nil restores the whole surface: changed with no rects.
228 m.rects = nil
229 var nilView = func(f *Frame, _ *testModel) { f.Root(); _ = f.SetInputRects(nil) }
230 r.Invalidate()
231 if ok, err = r.Render(&out, &m, nilView); err != nil || !ok || !out.InputRectsChanged || out.InputRects != nil {
232 t.Fatalf("nil region: ok=%v err=%v %+v", ok, err, out)
233 }
234}
235
236func TestRendererForwardsReleasedAndClose(t *testing.T) {
237 r, tg := newTestRenderer(t)
238 tg.EXPECT().Released(uint64(3)).Return(nil).Once()
239 if err := r.Released(3); err != nil {
240 t.Fatal(err)
241 }
242 tg.EXPECT().Close().Return(nil).Once()
243 if err := r.Close(); err != nil {
244 t.Fatal(err)
245 }
246 // After Close nothing reaches the target (the mock has no expectation).
247 r.Resize(10, 10, 1)
248 if r.Input(&Input{Kind: InputPointerMotion}) {
249 t.Fatal("Input after Close requested a redraw")
250 }
251 if err := r.Released(3); err == nil {
252 t.Fatal("Released after Close succeeded")
253 }
254 if err := r.Close(); err != nil { // second Close is a no-op
255 t.Fatal(err)
256 }
257 var out Output
258 var m testModel
259 if _, err := r.Render(&out, &m, testView); err == nil {
260 t.Fatal("Render after Close succeeded")
261 }
262}
263
264func TestTextIntAndMasked(t *testing.T) {
265 r := newRuntime()
266 r.Build(func(f *Frame) {
267 root := f.Root()
268 root.TextInt("Count: ", -42)
269 root.Masked(3)
270 root.Masked(0)
271 root.Masked(300)
272 })
273 c := r.committed.children
274 if len(c) != 4 || c[0].text != "Count: -42" || c[1].text != "•••" || c[2].text != "" || len([]rune(c[3].text)) != 300 {
275 t.Fatalf("texts: %q %q %q", c[0].text, c[1].text, c[2].text)
276 }
277}