turbo-editors/turbo-corepublic Fork 0
v0.9.0
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.

dialog_test.go · 465 lines · 13.0 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 20h ago1package ui
2
3import (
4 "strings"
5 "testing"
6
7 "github.com/gdamore/tcell/v2"
8)
9
10// testDialog builds a dialog with a label, a field and two buttons, laid out
11// as the file dialogs actually are.
12func testDialog() (*Dialog, *InputLine, *Button, *Button) {
13 d := NewDialog("Open a File")
14 d.SetBounds(Rect{X: 10, Y: 5, W: 40, H: 8})
15
16 label := NewLabel("Name:")
17 label.SetBounds(d.Layout(2, 2, 10, 1))
18 field := NewInputLine("main.go")
19 field.SetBounds(d.Layout(2, 3, 34, 1))
20 ok := NewButton("~O~K", func() { d.Close(ResultOK) })
21 ok.Default = true
22 ok.SetBounds(d.Layout(10, 6, ButtonWidth("~O~K"), 1))
23 cancel := NewButton("~C~ancel", func() { d.Close(ResultCancel) })
24 cancel.SetBounds(d.Layout(22, 6, ButtonWidth("~C~ancel"), 1))
25
26 d.Add(label)
27 d.Add(field)
28 d.Add(ok)
29 d.Add(cancel)
30 return d, field, ok, cancel
31}
32
33func TestADialogStartsOpenWithTheFirstFocusableControlFocused(t *testing.T) {
34 d, field, _, _ := testDialog()
35
36 if d.Done() {
37 t.Error("Done() = true on a new dialog")
38 }
39 if d.Result() != ResultNone {
40 t.Errorf("Result() = %v, want ResultNone", d.Result())
41 }
42 if d.Focused() != Focusable(field) {
43 t.Error("the focus is not on the field; the label should have been skipped")
44 }
45 if !field.Focused() {
46 t.Error("the focused control was not told it has the focus")
47 }
48}
49
50func TestTabWalksTheFocusRingSkippingLabels(t *testing.T) {
51 d, field, ok, cancel := testDialog()
52
53 d.HandleKey(keyEvent(tcell.KeyTab, 0, tcell.ModNone))
54 if d.Focused() != Focusable(ok) {
55 t.Error("Tab did not move the focus to the OK button")
56 }
57
58 d.HandleKey(keyEvent(tcell.KeyTab, 0, tcell.ModNone))
59 if d.Focused() != Focusable(cancel) {
60 t.Error("Tab did not move on to Cancel")
61 }
62
63 d.HandleKey(keyEvent(tcell.KeyTab, 0, tcell.ModNone))
64 if d.Focused() != Focusable(field) {
65 t.Error("Tab did not wrap round to the field, skipping the label")
66 }
67
68 d.HandleKey(keyEvent(tcell.KeyBacktab, 0, tcell.ModNone))
69 if d.Focused() != Focusable(cancel) {
70 t.Error("Shift-Tab did not walk the ring backwards")
71 }
72}
73
74func TestEscapeCancelsTheDialog(t *testing.T) {
75 d, _, _, _ := testDialog()
76
77 d.HandleKey(keyEvent(tcell.KeyEscape, 0, tcell.ModNone))
78
79 if !d.Done() || d.Result() != ResultCancel {
80 t.Errorf("Result() = %v, want ResultCancel", d.Result())
81 }
82}
83
84func TestEnterPressesTheDefaultButtonFromAnywhere(t *testing.T) {
85 d, _, _, _ := testDialog()
86 // The focus is on the input line, which does nothing with Enter.
87
88 d.HandleKey(keyEvent(tcell.KeyEnter, 0, tcell.ModNone))
89
90 if d.Result() != ResultOK {
91 t.Errorf("Result() = %v, want the default button to have been pressed", d.Result())
92 }
93}
94
95func TestAltLetterPressesTheMatchingButton(t *testing.T) {
96 d, _, _, _ := testDialog()
97
98 d.HandleKey(keyEvent(tcell.KeyRune, 'c', tcell.ModAlt))
99
100 if d.Result() != ResultCancel {
101 t.Errorf("Result() = %v, want Alt-C to have pressed Cancel", d.Result())
102 }
103}
104
105func TestTypingReachesTheFocusedField(t *testing.T) {
106 d, field, _, _ := testDialog()
107
108 d.HandleKey(runeEvent('!'))
109
110 if got := field.Text(); got != "main.go!" {
111 t.Errorf("the field holds %q, want the character to have reached it", got)
112 }
113}
114
115func TestADialogSwallowsEveryKey(t *testing.T) {
116 d, _, _, _ := testDialog()
117
118 // A modal dialog must not let anything through to the editor behind it,
119 // even a key none of its controls wants.
120 if !d.HandleKey(keyEvent(tcell.KeyF9, 0, tcell.ModNone)) {
121 t.Error("a key escaped the modal dialog")
122 }
123}
124
125func TestADialogSwallowsClicksThatMissIt(t *testing.T) {
126 d, _, _, _ := testDialog()
127
128 if !d.HandleMouse(clickEvent(0, 0)) {
129 t.Error("a click outside the dialog was passed through to what is behind it")
130 }
131}
132
133func TestClickingAControlFocusesIt(t *testing.T) {
134 d, _, _, cancel := testDialog()
135
136 bounds := cancel.Bounds()
137 d.HandleMouse(clickEvent(bounds.X+1, bounds.Y))
138
139 if d.Result() != ResultCancel {
140 t.Errorf("Result() = %v, want the clicked button to have been pressed", d.Result())
141 }
142}
143
144func TestOnKeyIsOfferedTheKeyFirst(t *testing.T) {
145 d, field, _, _ := testDialog()
146 seen := 0
147 d.OnKey = func(ev *tcell.EventKey) bool {
148 seen++
149 return ev.Key() == tcell.KeyF1
150 }
151
152 if !d.HandleKey(keyEvent(tcell.KeyF1, 0, tcell.ModNone)) {
153 t.Error("OnKey claimed the key but the dialog reported it unhandled")
154 }
155 d.HandleKey(runeEvent('z'))
156
157 if seen != 2 {
158 t.Errorf("OnKey saw %d keys, want 2", seen)
159 }
160 if got := field.Text(); got != "main.goz" {
161 t.Errorf("the field holds %q, want the key OnKey declined to have reached it", got)
162 }
163}
164
165func TestSetFocusRefusesAControlThatCannotTakeIt(t *testing.T) {
166 d, field, _, _ := testDialog()
167
168 d.SetFocus(0) // the label
169 if d.Focused() != Focusable(field) {
170 t.Error("the focus moved onto a label")
171 }
172
173 d.SetFocus(99)
174 if d.Focused() != Focusable(field) {
175 t.Error("the focus moved onto an index out of range")
176 }
177}
178
179func TestADialogDrawsItsFrameTitleAndControls(t *testing.T) {
180 screen, root := newTestScreen(t, 60, 16)
181 d, _, _, _ := testDialog()
182
183 d.Draw(root, testTheme(t))
184 screen.Show()
185
186 lines := screenLines(t, screen)
187 if !strings.Contains(lines[5], "Open a File") {
188 t.Errorf("row 5 = %q, want the dialog's title", lines[5])
189 }
190 if !strings.Contains(lines[5], "╔") {
191 t.Errorf("row 5 = %q, want a double frame", lines[5])
192 }
193 if !strings.Contains(lines[7], "Name:") {
194 t.Errorf("row 7 = %q, want the label", lines[7])
195 }
196 if !strings.Contains(lines[8], "main.go") {
197 t.Errorf("row 8 = %q, want the field's content", lines[8])
198 }
199 if !strings.Contains(lines[11], "OK") || !strings.Contains(lines[11], "Cancel") {
200 t.Errorf("row 11 = %q, want both buttons", lines[11])
201 }
202}
203
204func TestAnEmptyDialogHandlesKeysWithoutPanicking(t *testing.T) {
205 d := NewDialog("Empty")
206 d.SetBounds(Rect{X: 0, Y: 0, W: 20, H: 5})
207
208 d.HandleKey(keyEvent(tcell.KeyTab, 0, tcell.ModNone))
209 d.HandleKey(keyEvent(tcell.KeyEnter, 0, tcell.ModNone))
210
211 if d.Focused() != nil {
212 t.Error("Focused() returned a control from an empty dialog")
213 }
214}
215
216func TestListBoxSelectionAndScrolling(t *testing.T) {
217 items := make([]string, 30)
218 for i := range items {
219 items[i] = string(rune('a' + i%26))
220 }
221 l := NewListBox(items)
222 l.SetBounds(Rect{X: 0, Y: 0, W: 10, H: 5})
223 l.SetFocused(true)
224
225 l.HandleKey(keyEvent(tcell.KeyDown, 0, tcell.ModNone))
226 if l.Selected() != 1 {
227 t.Errorf("Selected() = %d, want 1", l.Selected())
228 }
229
230 l.HandleKey(keyEvent(tcell.KeyEnd, 0, tcell.ModNone))
231 if l.Selected() != 29 {
232 t.Errorf("Selected() = %d, want the last item", l.Selected())
233 }
234
235 l.HandleKey(keyEvent(tcell.KeyPgUp, 0, tcell.ModNone))
236 if l.Selected() != 24 {
237 t.Errorf("Selected() = %d, want a page back", l.Selected())
238 }
239
240 l.HandleKey(keyEvent(tcell.KeyHome, 0, tcell.ModNone))
241 if l.Selected() != 0 {
242 t.Errorf("Selected() = %d, want the first item", l.Selected())
243 }
244}
245
246func TestListBoxClampsTheSelectionToTheList(t *testing.T) {
247 l := NewListBox([]string{"one", "two"})
248 l.SetFocused(true)
249
250 l.Select(99)
251 if l.Selected() != 1 {
252 t.Errorf("Selected() = %d, want the last item", l.Selected())
253 }
254 l.Select(-5)
255 if l.Selected() != 0 {
256 t.Errorf("Selected() = %d, want the first item", l.Selected())
257 }
258}
259
260func TestAnEmptyListBoxHasNoSelection(t *testing.T) {
261 l := NewListBox(nil)
262 l.SetFocused(true)
263
264 if got := l.Selected(); got != -1 {
265 t.Errorf("Selected() = %d, want -1", got)
266 }
267 if got := l.SelectedItem(); got != "" {
268 t.Errorf("SelectedItem() = %q, want empty", got)
269 }
270 l.HandleKey(keyEvent(tcell.KeyDown, 0, tcell.ModNone)) // must not panic
271 l.Choose()
272}
273
274func TestListBoxReportsSelectionsAndChoices(t *testing.T) {
275 var selected, chosen []int
276 l := NewListBox([]string{"a", "b", "c"})
277 l.SetBounds(Rect{X: 0, Y: 0, W: 10, H: 3})
278 l.SetFocused(true)
279 l.OnSelect = func(i int) { selected = append(selected, i) }
280 l.OnChoose = func(i int) { chosen = append(chosen, i) }
281
282 l.HandleKey(keyEvent(tcell.KeyDown, 0, tcell.ModNone))
283 l.HandleKey(keyEvent(tcell.KeyEnter, 0, tcell.ModNone))
284
285 if len(selected) != 1 || selected[0] != 1 {
286 t.Errorf("OnSelect saw %v, want [1]", selected)
287 }
288 if len(chosen) != 1 || chosen[0] != 1 {
289 t.Errorf("OnChoose saw %v, want [1]", chosen)
290 }
291}
292
293func TestSetItemsResetsTheView(t *testing.T) {
294 l := NewListBox([]string{"a", "b", "c"})
295 l.SetBounds(Rect{X: 0, Y: 0, W: 10, H: 2})
296 l.SetFocused(true)
297 l.Select(2)
298
299 l.SetItems([]string{"x", "y"})
300
301 if l.Selected() != 0 {
302 t.Errorf("Selected() = %d, want the first of the new items", l.Selected())
303 }
304 if l.top != 0 {
305 t.Errorf("the view is scrolled to %d, want the top", l.top)
306 }
307}
308
309func TestListBoxMouseSelectsThenChooses(t *testing.T) {
310 chosen := 0
311 l := NewListBox([]string{"a", "b", "c"})
312 l.SetBounds(Rect{X: 0, Y: 2, W: 10, H: 3})
313 l.SetFocused(true)
314 l.OnChoose = func(int) { chosen++ }
315
316 l.HandleMouse(clickEvent(1, 3)) // the second line
317 if l.Selected() != 1 {
318 t.Fatalf("Selected() = %d, want 1", l.Selected())
319 }
320 if chosen != 0 {
321 t.Error("a first click on a line opened it; it should only select")
322 }
323
324 l.HandleMouse(clickEvent(1, 3)) // the same line again
325 if chosen != 1 {
326 t.Error("a second click on the selected line did not open it")
327 }
328}
329
330func TestListBoxWheelScrolls(t *testing.T) {
331 l := NewListBox([]string{"a", "b", "c"})
332 l.SetBounds(Rect{X: 0, Y: 0, W: 10, H: 3})
333
334 l.HandleMouse(tcell.NewEventMouse(1, 1, tcell.WheelDown, tcell.ModNone))
335 if l.Selected() != 1 {
336 t.Errorf("Selected() = %d after a wheel-down, want 1", l.Selected())
337 }
338
339 l.HandleMouse(tcell.NewEventMouse(1, 1, tcell.WheelUp, tcell.ModNone))
340 if l.Selected() != 0 {
341 t.Errorf("Selected() = %d after a wheel-up, want 0", l.Selected())
342 }
343}
344
345func TestListBoxDrawsItsItemsAndScrollBar(t *testing.T) {
346 screen, root := newTestScreen(t, 20, 6)
347 l := NewListBox([]string{"alpha", "beta", "gamma", "delta", "epsilon", "zeta"})
348 l.SetBounds(Rect{X: 0, Y: 0, W: 12, H: 4})
349 l.SetFocused(true)
350
351 l.Draw(root, testTheme(t))
352 screen.Show()
353
354 lines := screenLines(t, screen)
355 if !strings.HasPrefix(lines[0], "alpha") {
356 t.Errorf("row 0 = %q, want the first item", lines[0])
357 }
358 if !strings.Contains(lines[0], "▲") {
359 t.Errorf("row 0 = %q, want the scroll bar's up arrow at the right", lines[0])
360 }
361 if !strings.Contains(lines[3], "▼") {
362 t.Errorf("row 3 = %q, want the scroll bar's down arrow", lines[3])
363 }
364}
365
366func TestListBoxScrollsToKeepTheSelectionVisible(t *testing.T) {
367 l := NewListBox([]string{"a", "b", "c", "d", "e", "f", "g", "h"})
368 l.SetBounds(Rect{X: 0, Y: 0, W: 10, H: 3})
369 l.SetFocused(true)
370
371 l.Select(7)
372 l.scrollToSelection()
373
374 if l.top != 5 {
375 t.Errorf("the view is scrolled to %d, want 5 so the last item shows", l.top)
376 }
377}
378
379func TestTheArrowsReachAListBoxBeforeTheyMoveTheFocus(t *testing.T) {
380 // A dialog that grabbed the arrows for its focus ring would leave every
381 // list in it — the theme picker, the window list — unusable by keyboard.
382 d := NewDialog("Theme")
383 d.SetBounds(Rect{X: 0, Y: 0, W: 40, H: 12})
384
385 list := NewListBox([]string{"one", "two", "three"})
386 list.SetBounds(d.Layout(2, 2, 30, 5))
387 ok := NewButton("~O~K", func() { d.Close(ResultOK) })
388 ok.SetBounds(d.Layout(2, 9, ButtonWidth("~O~K"), 1))
389 d.Add(list)
390 d.Add(ok)
391
392 d.HandleKey(keyEvent(tcell.KeyDown, 0, tcell.ModNone))
393
394 if got := list.Selected(); got != 1 {
395 t.Errorf("Selected() = %d, want the arrow to have walked the list", got)
396 }
397 if d.Focused() != Focusable(list) {
398 t.Error("the arrow moved the focus off the list instead of walking it")
399 }
400}
401
402func TestTheArrowsStillWalkTheRingWhenTheFocusedControlDeclinesThem(t *testing.T) {
403 d := NewDialog("Confirm")
404 d.SetBounds(Rect{X: 0, Y: 0, W: 40, H: 8})
405
406 yes := NewButton("~Y~es", nil)
407 yes.SetBounds(d.Layout(2, 5, ButtonWidth("~Y~es"), 1))
408 no := NewButton("~N~o", nil)
409 no.SetBounds(d.Layout(12, 5, ButtonWidth("~N~o"), 1))
410 d.Add(yes)
411 d.Add(no)
412
413 d.HandleKey(keyEvent(tcell.KeyDown, 0, tcell.ModNone))
414
415 if d.Focused() != Focusable(no) {
416 t.Error("the arrow did not move the focus between buttons, which have no use for it")
417 }
418}
419
420func TestMovingADialogTakesItsControlsWithIt(t *testing.T) {
421 // Controls are placed in screen coordinates when the dialog is built, so
422 // moving the frame on its own would leave every one of them behind.
423 d, field, ok, _ := testDialog()
424 fieldOffset := field.Bounds().X - d.Bounds().X
425 buttonOffset := ok.Bounds().Y - d.Bounds().Y
426
427 d.MoveTo(30, 12)
428
429 if got := d.Bounds(); got.X != 30 || got.Y != 12 {
430 t.Fatalf("Bounds() = %+v, want the dialog at (30, 12)", got)
431 }
432 if got := field.Bounds().X - d.Bounds().X; got != fieldOffset {
433 t.Errorf("the field is %d from the frame, want %d", got, fieldOffset)
434 }
435 if got := ok.Bounds().Y - d.Bounds().Y; got != buttonOffset {
436 t.Errorf("the button is %d from the frame, want %d", got, buttonOffset)
437 }
438}
439
440func TestMovingADialogNowhereChangesNothing(t *testing.T) {
441 d, field, _, _ := testDialog()
442 before := field.Bounds()
443
444 d.MoveTo(d.Bounds().X, d.Bounds().Y)
445
446 if got := field.Bounds(); got != before {
447 t.Errorf("the field moved to %+v from %+v", got, before)
448 }
449}
450
451func TestCenterInPutsADialogBackInTheMiddle(t *testing.T) {
452 d, field, _, _ := testDialog()
453 fieldOffset := field.Bounds().X - d.Bounds().X
454 screen := Rect{W: 120, H: 40}
455
456 d.CenterIn(screen)
457
458 want := d.Bounds().CenteredIn(screen)
459 if got := d.Bounds(); got.X != want.X || got.Y != want.Y {
460 t.Errorf("Bounds() = %+v, want it centred at %+v", got, want)
461 }
462 if got := field.Bounds().X - d.Bounds().X; got != fieldOffset {
463 t.Errorf("the field is %d from the frame, want %d", got, fieldOffset)
464 }
465}