turbo-editors/turbo-corepublic Fork 0
28d59854361aeda8541d853093e732126f3d7bff
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

widgets_test.go · 322 lines · 8.8 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 17h ago1package ui
2
3import (
4 "strings"
5 "testing"
6
7 "github.com/gdamore/tcell/v2"
8
9 "codeberg.org/turbo-editors/turbo-core/theme"
10)
11
12// stubContent is a widget that records what it was given, so a container's
13// routing can be checked without a real editor behind it.
14type stubContent struct {
15 Box
16 keys int
17 clicks int
18 drawnAt Rect
19 consume bool
20}
21
22func (s *stubContent) Draw(p *Painter, _ *theme.Theme) {
23 s.drawnAt = p.Size()
24 p.Fill(p.Size(), '·', tcell.StyleDefault)
25}
26
27func (s *stubContent) HandleKey(*tcell.EventKey) bool {
28 s.keys++
29 return s.consume
30}
31
32func (s *stubContent) HandleMouse(*tcell.EventMouse) bool {
33 s.clicks++
34 return s.consume
35}
36
37func TestSplitHotKey(t *testing.T) {
38 tests := []struct {
39 label string
40 wantText string
41 wantKey rune
42 wantIndex int
43 }{
44 {"~F~ile", "File", 'f', 0},
45 {"E~x~it", "Exit", 'x', 1},
46 {"Save ~A~s…", "Save As…", 'a', 5},
47 {"No hot key", "No hot key", 0, -1},
48 {"~", "", 0, -1},
49 }
50
51 for _, tc := range tests {
52 t.Run(tc.label, func(t *testing.T) {
53 text, key, index := SplitHotKey(tc.label)
54 if text != tc.wantText || key != tc.wantKey || index != tc.wantIndex {
55 t.Errorf("SplitHotKey(%q) = (%q, %q, %d), want (%q, %q, %d)",
56 tc.label, text, key, index, tc.wantText, tc.wantKey, tc.wantIndex)
57 }
58 })
59 }
60}
61
62func TestLabelWidthIgnoresTheMarkers(t *testing.T) {
63 if got := LabelWidth("~F~ile"); got != 4 {
64 t.Errorf("LabelWidth(\"~F~ile\") = %d, want 4", got)
65 }
66}
67
68func TestMatchesHotKeyIgnoresCase(t *testing.T) {
69 if !MatchesHotKey("~F~ile", 'F') || !MatchesHotKey("~F~ile", 'f') {
70 t.Error("a hot key must match in either case")
71 }
72 if MatchesHotKey("~F~ile", 'x') || MatchesHotKey("File", 'f') {
73 t.Error("a hot key matched something it should not")
74 }
75}
76
77func TestDrawFrameDrawsOnlyTheEdges(t *testing.T) {
78 screen, root := newTestScreen(t, 8, 4)
79
80 DrawFrame(root, Rect{X: 0, Y: 0, W: 6, H: 3}, FrameDouble, tcell.StyleDefault)
81 screen.Show()
82
83 lines := screenLines(t, screen)
84 want := []string{"╔════╗ ", "║ ║ ", "╚════╝ ", " "}
85 for i, w := range want {
86 if lines[i] != w {
87 t.Errorf("row %d = %q, want %q", i, lines[i], w)
88 }
89 }
90}
91
92func TestDrawFrameNeedsRoomForItsCorners(t *testing.T) {
93 screen, root := newTestScreen(t, 4, 2)
94
95 DrawFrame(root, Rect{X: 0, Y: 0, W: 1, H: 5}, FrameSingle, tcell.StyleDefault)
96 screen.Show()
97
98 for _, line := range screenLines(t, screen) {
99 if strings.TrimSpace(line) != "" {
100 t.Errorf("a frame too narrow to have corners drew something: %q", line)
101 }
102 }
103}
104
105func TestThumbOffsetStaysOnTheTrack(t *testing.T) {
106 tests := []struct {
107 name string
108 position, span, total, track int
109 want int
110 }{
111 {"at the top", 0, 10, 100, 8, 0},
112 {"at the bottom", 90, 10, 100, 8, 7},
113 {"nothing to scroll", 0, 100, 10, 8, 0},
114 {"past the end", 999, 10, 100, 8, 7},
115 {"a track too short", 5, 10, 100, 1, 0},
116 }
117
118 for _, tc := range tests {
119 t.Run(tc.name, func(t *testing.T) {
120 got := thumbOffset(tc.position, tc.span, tc.total, tc.track)
121 if got != tc.want {
122 t.Errorf("thumbOffset() = %d, want %d", got, tc.want)
123 }
124 })
125 }
126}
127
128func TestWindowDrawsAFrameATitleAndItsContent(t *testing.T) {
129 screen, root := newTestScreen(t, 30, 8)
130 content := &stubContent{}
131 w := NewWindow("main.go", content)
132 w.SetBounds(Rect{X: 0, Y: 0, W: 20, H: 5})
133 w.SetActive(true)
134 w.SetNumber(1)
135
136 w.Draw(root, testTheme(t))
137 screen.Show()
138
139 lines := screenLines(t, screen)
140 if !strings.HasPrefix(lines[0], "╔") {
141 t.Errorf("an active window's frame is %q, want a double line", lines[0])
142 }
143 if !strings.Contains(lines[0], "main.go") {
144 t.Errorf("the title bar is %q, want it to show the title", lines[0])
145 }
146 if !strings.Contains(lines[0], closeBoxLabel) {
147 t.Errorf("the title bar is %q, want a close box", lines[0])
148 }
149 if !strings.Contains(lines[0], "1") {
150 t.Errorf("the title bar is %q, want the window number", lines[0])
151 }
152 if got := content.drawnAt; got != (Rect{W: 18, H: 3}) {
153 t.Errorf("the content was drawn in %+v, want the interior of the frame", got)
154 }
155}
156
157func TestAnInactiveWindowGetsASingleFrame(t *testing.T) {
158 screen, root := newTestScreen(t, 20, 5)
159 w := NewWindow("x", nil)
160 w.SetBounds(Rect{X: 0, Y: 0, W: 10, H: 4})
161
162 w.Draw(root, testTheme(t))
163 screen.Show()
164
165 if got := screenLines(t, screen)[0]; !strings.HasPrefix(got, "┌") {
166 t.Errorf("an inactive window's frame is %q, want a single line", got)
167 }
168}
169
170func TestWindowPassesInteriorClicksToItsContent(t *testing.T) {
171 content := &stubContent{consume: true}
172 w := NewWindow("t", content)
173 w.SetBounds(Rect{X: 0, Y: 0, W: 20, H: 6})
174
175 if !w.HandleMouse(clickEvent(5, 3)) {
176 t.Fatal("a click inside the window was not handled")
177 }
178 if content.clicks != 1 {
179 t.Errorf("the content saw %d clicks, want 1", content.clicks)
180 }
181}
182
183func TestAClickOutsideTheWindowIsNotItsBusiness(t *testing.T) {
184 content := &stubContent{}
185 w := NewWindow("t", content)
186 w.SetBounds(Rect{X: 10, Y: 10, W: 5, H: 5})
187
188 if w.HandleMouse(clickEvent(0, 0)) {
189 t.Error("the window claimed a click that landed elsewhere")
190 }
191 if content.clicks != 0 {
192 t.Error("the content was given a click from outside the window")
193 }
194}
195
196func TestClickingTheCloseBoxAsksToClose(t *testing.T) {
197 asked := 0
198 w := NewWindow("t", nil)
199 w.SetBounds(Rect{X: 0, Y: 0, W: 20, H: 6})
200 w.OnClose = func() bool { asked++; return true }
201
202 w.HandleMouse(clickEvent(3, 0)) // the middle of "[■]" at column 2
203
204 if asked != 1 {
205 t.Errorf("OnClose was called %d times, want 1", asked)
206 }
207}
208
209func TestDraggingTheTitleBarMovesTheWindow(t *testing.T) {
210 w := NewWindow("t", nil)
211 w.SetBounds(Rect{X: 2, Y: 2, W: 20, H: 6})
212
213 w.HandleMouse(clickEvent(10, 2)) // press on the title bar
214 w.HandleMouse(tcell.NewEventMouse(14, 5, tcell.Button1, tcell.ModNone)) // drag
215
216 if got := w.Bounds(); got != (Rect{X: 6, Y: 5, W: 20, H: 6}) {
217 t.Errorf("Bounds() = %+v, want the window moved by (4, 3)", got)
218 }
219 if !w.Dragging() {
220 t.Error("Dragging() = false while the button is still down")
221 }
222
223 w.HandleMouse(releaseEvent(14, 5))
224 if w.Dragging() {
225 t.Error("Dragging() = true after the button came back up")
226 }
227}
228
229func TestDraggingTheCornerResizesTheWindow(t *testing.T) {
230 w := NewWindow("t", nil)
231 w.SetBounds(Rect{X: 0, Y: 0, W: 20, H: 6})
232
233 w.HandleMouse(clickEvent(19, 5)) // the bottom-right corner
234 w.HandleMouse(tcell.NewEventMouse(25, 9, tcell.Button1, tcell.ModNone))
235
236 if got := w.Bounds(); got != (Rect{X: 0, Y: 0, W: 26, H: 10}) {
237 t.Errorf("Bounds() = %+v, want it grown by (6, 4)", got)
238 }
239}
240
241func TestAWindowCannotBeResizedBelowItsMinimum(t *testing.T) {
242 w := NewWindow("t", nil)
243 w.SetBounds(Rect{X: 0, Y: 0, W: 20, H: 6})
244
245 w.HandleMouse(clickEvent(19, 5))
246 w.HandleMouse(tcell.NewEventMouse(0, 0, tcell.Button1, tcell.ModNone))
247
248 got := w.Bounds()
249 if got.W != MinWindowWidth || got.H != MinWindowHeight {
250 t.Errorf("Bounds() = %+v, want it clamped to the minimum size", got)
251 }
252}
253
254func TestSettingWindowBoundsLaysTheContentOut(t *testing.T) {
255 content := &stubContent{}
256 w := NewWindow("t", content)
257
258 w.SetBounds(Rect{X: 3, Y: 4, W: 20, H: 10})
259
260 if got := content.Bounds(); got != (Rect{X: 4, Y: 5, W: 18, H: 8}) {
261 t.Errorf("the content is at %+v, want the interior of the frame", got)
262 }
263}
264
265func TestTheCloseBoxIsThreeColumnsWide(t *testing.T) {
266 // The block characters in "[■]" and "[▬]" are three bytes but one column,
267 // so a width measured in bytes would make a box swallow its neighbours.
268 for _, label := range []string{closeBoxLabel, maximizeBoxLabel, restoreBoxLabel} {
269 if got := len([]rune(label)); got != boxWidth {
270 t.Fatalf("%q is %d columns, but boxWidth says %d", label, got, boxWidth)
271 }
272 }
273
274 closed := 0
275 w := NewWindow("t", nil)
276 w.SetBounds(Rect{X: 0, Y: 0, W: 20, H: 6})
277 w.OnClose = func() bool { closed++; return true }
278
279 for _, x := range []int{2, 3, 4} {
280 w.HandleMouse(clickEvent(x, 0))
281 }
282 if closed != 3 {
283 t.Errorf("clicking the three columns of the close box asked to close %d times, want 3", closed)
284 }
285
286 w.HandleMouse(clickEvent(5, 0)) // just past it: this starts a move instead
287 if closed != 3 {
288 t.Error("a click past the close box was taken as a close")
289 }
290}
291
292func TestAWindowTellsItsContentWhetherItIsActive(t *testing.T) {
293 content := &focusableStub{}
294 w := NewWindow("t", content)
295
296 w.SetActive(true)
297 if !content.Focused() {
298 t.Error("an active window did not give its content the focus")
299 }
300
301 w.SetActive(false)
302 if content.Focused() {
303 t.Error("an inactive window left the focus with its content")
304 }
305}
306
307func TestAWindowWhoseContentCannotFocusStillWorks(t *testing.T) {
308 w := NewWindow("t", &stubContent{}) // embeds Box, not FocusBox
309
310 w.SetActive(true) // must not panic
311
312 if !w.Active() {
313 t.Error("Active() = false after SetActive(true)")
314 }
315}
316
317// focusableStub is a content widget that can hold the focus.
318type focusableStub struct {
319 FocusBox
320}
321
322func (f *focusableStub) Draw(*Painter, *theme.Theme) {}