turbo-editors/turbo-corepublic Fork 0
v1.0.1
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.

dialogs_test.go · 496 lines · 14.9 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 17h ago1package app
2
3import (
4 "os"
5 "path/filepath"
6 "strings"
7 "testing"
8
9 "github.com/gdamore/tcell/v2"
10
📦 Turbo Core f3ade8d k33g 10h ago11 "rickub.com/turbo-editors/turbo-core/ui"
🛟 Updated. 28d5985 k33g 17h ago12)
13
14// testDirectory builds a directory holding the given entries; a name ending in
15// a slash becomes a subdirectory.
16func testDirectory(t *testing.T, names ...string) string {
17 t.Helper()
18
19 root := t.TempDir()
20 for _, name := range names {
21 path := filepath.Join(root, strings.TrimSuffix(name, "/"))
22 var err error
23 if strings.HasSuffix(name, "/") {
24 err = os.Mkdir(path, 0o755)
25 } else {
26 err = os.WriteFile(path, nil, 0o644)
27 }
28 if err != nil {
29 t.Fatalf("creating %s: %v", name, err)
30 }
31 }
32 return root
33}
34
35func TestTheFileDialogListsDirectoriesFirst(t *testing.T) {
36 root := testDirectory(t, "zeta.go", "alpha/", "beta.go", ".hidden")
37
38 dialog := NewFileDialog("Open", root, ui.Rect{W: 80, H: 24})
39
40 want := []string{".." + string(filepath.Separator), "alpha" + string(filepath.Separator), "beta.go", "zeta.go"}
41 got := dialog.list.Items()
42 if len(got) != len(want) {
43 t.Fatalf("the list holds %v, want %v", got, want)
44 }
45 for i, name := range want {
46 if got[i] != name {
47 t.Errorf("entry %d is %q, want %q", i, got[i], name)
48 }
49 }
50}
51
52func TestTheFileDialogHidesDotFiles(t *testing.T) {
53 root := testDirectory(t, ".git/", ".env", "main.go")
54
55 dialog := NewFileDialog("Open", root, ui.Rect{W: 80, H: 24})
56
57 for _, entry := range dialog.list.Items() {
58 if strings.HasPrefix(entry, ".") && entry != ".."+string(filepath.Separator) {
59 t.Errorf("the list shows %q, want dot entries hidden", entry)
60 }
61 }
62}
63
64func TestTheFileDialogOpensBesideTheGivenFile(t *testing.T) {
65 root := testDirectory(t, "main.go")
66 path := filepath.Join(root, "main.go")
67
68 dialog := NewFileDialog("Save as", path, ui.Rect{W: 80, H: 24})
69
70 if dialog.directory != root {
71 t.Errorf("the dialog opened in %q, want %q", dialog.directory, root)
72 }
73 if got := dialog.name.Text(); got != "main.go" {
74 t.Errorf("the name field holds %q, want the file's name", got)
75 }
76 if got := dialog.Path(); got != path {
77 t.Errorf("Path() = %q, want %q", got, path)
78 }
79}
80
81func TestTheFileDialogOpensInADirectoryGivenDirectly(t *testing.T) {
82 root := testDirectory(t, "main.go")
83
84 dialog := NewFileDialog("Open", root, ui.Rect{W: 80, H: 24})
85
86 if dialog.directory != root {
87 t.Errorf("the dialog opened in %q, want %q", dialog.directory, root)
88 }
89 if got := dialog.name.Text(); got != "" {
90 t.Errorf("the name field holds %q, want it empty", got)
91 }
92}
93
94func TestTheFileDialogPathJoinsTheDirectoryAndTheName(t *testing.T) {
95 root := testDirectory(t)
96 dialog := NewFileDialog("Save as", root, ui.Rect{W: 80, H: 24})
97
98 dialog.name.SetText("new.go")
99
100 if got := dialog.Path(); got != filepath.Join(root, "new.go") {
101 t.Errorf("Path() = %q", got)
102 }
103}
104
105func TestTheFileDialogAcceptsAnAbsolutePath(t *testing.T) {
106 dialog := NewFileDialog("Open", testDirectory(t), ui.Rect{W: 80, H: 24})
107
108 dialog.name.SetText("/etc/hosts")
109
110 if got := dialog.Path(); got != filepath.Clean("/etc/hosts") {
111 t.Errorf("Path() = %q, want the absolute path used as it is", got)
112 }
113}
114
115func TestAnEmptyNameGivesNoPath(t *testing.T) {
116 dialog := NewFileDialog("Open", testDirectory(t), ui.Rect{W: 80, H: 24})
117
118 dialog.name.SetText(" ")
119
120 if got := dialog.Path(); got != "" {
121 t.Errorf("Path() = %q, want nothing for a blank name", got)
122 }
123}
124
125func TestChoosingADirectoryBrowsesIntoIt(t *testing.T) {
126 root := testDirectory(t, "sub/")
127 dialog := NewFileDialog("Open", root, ui.Rect{W: 80, H: 24})
128
129 dialog.list.Select(1) // "sub/"
130 dialog.choose()
131
132 if dialog.directory != filepath.Join(root, "sub") {
133 t.Errorf("the dialog is in %q, want it inside sub", dialog.directory)
134 }
135 if dialog.Dialog().Done() {
136 t.Error("browsing into a directory closed the dialog")
137 }
138}
139
140func TestChoosingAFileClosesTheDialog(t *testing.T) {
141 root := testDirectory(t, "main.go")
142 dialog := NewFileDialog("Open", root, ui.Rect{W: 80, H: 24})
143
144 dialog.list.Select(1) // "main.go"
145 dialog.choose()
146
147 if !dialog.Dialog().Done() {
148 t.Fatal("choosing a file did not close the dialog")
149 }
150 if got := dialog.Path(); got != filepath.Join(root, "main.go") {
151 t.Errorf("Path() = %q", got)
152 }
153}
154
155func TestConfirmingADirectoryNameBrowsesRatherThanClosing(t *testing.T) {
156 root := testDirectory(t, "sub/")
157 dialog := NewFileDialog("Save as", root, ui.Rect{W: 80, H: 24})
158
159 dialog.name.SetText("sub")
160 dialog.confirm()
161
162 if dialog.Dialog().Done() {
163 t.Error("the dialog closed on a directory name")
164 }
165 if dialog.directory != filepath.Join(root, "sub") {
166 t.Errorf("the dialog is in %q, want it inside sub", dialog.directory)
167 }
168}
169
170func TestAnUnreadableDirectoryStillOffersTheParent(t *testing.T) {
171 dialog := NewFileDialog("Open", filepath.Join(t.TempDir(), "does-not-exist"), ui.Rect{W: 80, H: 24})
172
173 items := dialog.list.Items()
174
175 if len(items) != 1 || items[0] != ".."+string(filepath.Separator) {
176 t.Errorf("the list holds %v, want just the parent entry", items)
177 }
178}
179
180func TestTheDialogTitleShowsTheDirectory(t *testing.T) {
181 root := testDirectory(t)
182
183 dialog := NewFileDialog("Open", root, ui.Rect{W: 80, H: 24})
184
185 if !strings.HasPrefix(dialog.Dialog().Title(), "Open") {
186 t.Errorf("the title is %q, want it to start with the dialog's name", dialog.Dialog().Title())
187 }
188 if !strings.Contains(dialog.Dialog().Title(), filepath.Base(root)) {
189 t.Errorf("the title is %q, want the directory in it", dialog.Dialog().Title())
190 }
191}
192
193func TestShortenPath(t *testing.T) {
194 tests := []struct {
195 name string
196 path string
197 width int
198 want string
199 }{
200 {"already short enough", "/tmp/x", 20, "/tmp/x"},
201 {"trimmed from the left", "/a/very/long/path/indeed", 10, "…th/indeed"},
202 {"a width too small to trim to", "/a/b/c", 2, "/a/b/c"},
203 }
204
205 for _, tc := range tests {
206 t.Run(tc.name, func(t *testing.T) {
207 got := shortenPath(tc.path, tc.width)
208 if len([]rune(got)) > max(tc.width, len([]rune(tc.path))) {
209 t.Errorf("shortenPath() = %q, which is longer than %d", got, tc.width)
210 }
211 if tc.want != "" && got != tc.want {
212 t.Errorf("shortenPath() = %q, want %q", got, tc.want)
213 }
214 })
215 }
216}
217
218func TestTheConfirmDialogOffersThreeAnswers(t *testing.T) {
219 dialog := NewConfirmDialog("Close", "Save changes?", ui.Rect{W: 80, H: 24})
220
221 if got := len(dialog.Controls()); got != 4 {
222 t.Errorf("the dialog holds %d controls, want a label and three buttons", got)
223 }
224
225 dialog.HandleKey(tcell.NewEventKey(tcell.KeyRune, 'n', tcell.ModAlt))
226 if got := dialog.Result(); got != ui.ResultNo {
227 t.Errorf("Result() = %v, want ResultNo", got)
228 }
229}
230
231func TestTheMessageDialogShowsEveryLine(t *testing.T) {
232 dialog := NewMessageDialog("Help", "first\nsecond\nthird", ui.Rect{W: 80, H: 24})
233
234 if got := len(dialog.Controls()); got != 4 {
235 t.Errorf("the dialog holds %d controls, want three labels and a button", got)
236 }
237 if got := dialog.Bounds().H; got != 9 {
238 t.Errorf("the dialog is %d rows tall, want it to grow with the message", got)
239 }
240}
241
242func TestThePromptDialogReturnsWhatWasTyped(t *testing.T) {
243 dialog, field := NewPromptDialog("Go to line", "Line:", "12", ui.Rect{W: 80, H: 24})
244
245 dialog.HandleKey(tcell.NewEventKey(tcell.KeyRune, '3', tcell.ModNone))
246
247 if got := field.Text(); got != "123" {
248 t.Errorf("the field holds %q, want %q", got, "123")
249 }
250}
251
252func TestTheFindDialogCarriesItsOptions(t *testing.T) {
253 dialog := NewFindDialog("func", true, ui.Rect{W: 80, H: 24})
254
255 if got := dialog.Needle(); got != "func" {
256 t.Errorf("Needle() = %q", got)
257 }
258 if !dialog.MatchCase() {
259 t.Error("MatchCase() = false, want the option carried over")
260 }
261}
262
263func TestTheChoiceDialogStartsOnTheCurrentItem(t *testing.T) {
264 _, list := NewChoiceDialog("Theme", []string{"a", "b", "c"}, 2, ui.Rect{W: 80, H: 24})
265
266 if got := list.Selected(); got != 2 {
267 t.Errorf("Selected() = %d, want 2", got)
268 }
269}
270
271func TestChoosingFromAChoiceDialogClosesIt(t *testing.T) {
272 dialog, list := NewChoiceDialog("Theme", []string{"a", "b"}, 0, ui.Rect{W: 80, H: 24})
273
274 list.Choose()
275
276 if !dialog.Done() || dialog.Result() != ui.ResultOK {
277 t.Errorf("Result() = %v, want the dialog accepted", dialog.Result())
278 }
279}
280
281func TestSelectingAFileInTheListFillsTheNameField(t *testing.T) {
282 root := testDirectory(t, "main.go")
283 dialog := NewFileDialog("Open", root, ui.Rect{W: 80, H: 24})
284
285 dialog.list.Select(1) // "main.go"
286
287 if got := dialog.name.Text(); got != "main.go" {
288 t.Errorf("the name field holds %q, want the highlighted entry", got)
289 }
290}
291
292func TestSelectingADirectoryFillsTheFieldWithoutItsSlash(t *testing.T) {
293 root := testDirectory(t, "sub/")
294 dialog := NewFileDialog("Open", root, ui.Rect{W: 80, H: 24})
295
296 dialog.list.Select(1) // "sub/"
297
298 if got := dialog.name.Text(); got != "sub" {
299 t.Errorf("the name field holds %q, want sub without the separator", got)
300 }
301}
302
303func TestSelectingTheParentEntryClearsTheNameField(t *testing.T) {
304 // ".." is not a name anyone would type into a field labelled "Name:".
305 root := testDirectory(t, "main.go")
306 dialog := NewFileDialog("Open", root, ui.Rect{W: 80, H: 24})
307 dialog.list.Select(1)
308
309 dialog.list.Select(0) // "../"
310
311 if got := dialog.name.Text(); got != "" {
312 t.Errorf("the name field holds %q, want it cleared", got)
313 }
314}
315
316func TestOKActsOnTheHighlightedFile(t *testing.T) {
317 // The bug this covers: OK did nothing at all on a freshly opened dialog,
318 // because highlighting a file never reached the name field and Path() was
319 // therefore empty.
320 root := testDirectory(t, "main.go")
321 dialog := NewFileDialog("Open", root, ui.Rect{W: 80, H: 24})
322
323 dialog.list.Select(1) // "main.go"
324 dialog.confirm() // what the OK button does
325
326 if !dialog.Dialog().Done() {
327 t.Fatal("OK did nothing with a file highlighted in the list")
328 }
329 if got := dialog.Path(); got != filepath.Join(root, "main.go") {
330 t.Errorf("Path() = %q, want the highlighted file", got)
331 }
332}
333
334func TestOKOnAnUntouchedDialogBrowsesUpRatherThanDoingNothing(t *testing.T) {
335 // Nothing has been highlighted, so the field is empty and the highlight is
336 // still on the parent entry. OK must act on that rather than appear dead.
337 root := testDirectory(t, "main.go")
338 dialog := NewFileDialog("Open", root, ui.Rect{W: 80, H: 24})
339
340 dialog.confirm()
341
342 if dialog.Dialog().Done() {
343 t.Fatal("OK closed the dialog with no file chosen")
344 }
345 if dialog.directory != filepath.Dir(root) {
346 t.Errorf("the dialog is in %q, want the parent of %q", dialog.directory, root)
347 }
348}
349
350func TestOKOnAHighlightedDirectoryBrowsesIntoIt(t *testing.T) {
351 root := testDirectory(t, "sub/")
352 dialog := NewFileDialog("Open", root, ui.Rect{W: 80, H: 24})
353
354 dialog.list.Select(1) // "sub/"
355 dialog.confirm()
356
357 if dialog.Dialog().Done() {
358 t.Error("OK on a directory closed the dialog instead of browsing into it")
359 }
360 if dialog.directory != filepath.Join(root, "sub") {
361 t.Errorf("the dialog is in %q, want it inside sub", dialog.directory)
362 }
363}
364
365func TestBrowsingIntoADirectoryClearsTheNameField(t *testing.T) {
366 // Refilling the list moves the highlight back to the parent entry, and the
367 // field follows it — the name from the directory just left must not linger.
368 root := testDirectory(t, "sub/")
369 dialog := NewFileDialog("Open", root, ui.Rect{W: 80, H: 24})
370 dialog.list.Select(1)
371
372 dialog.confirm() // browses into sub
373
374 if got := dialog.name.Text(); got != "" {
375 t.Errorf("the name field still holds %q after entering a directory", got)
376 }
377}
378
379func TestATypedNameWinsOverTheHighlight(t *testing.T) {
380 // Someone who typed a name meant that name, whatever the list is showing.
381 root := testDirectory(t, "main.go", "other.go")
382 dialog := NewFileDialog("Open", root, ui.Rect{W: 80, H: 24})
383 dialog.list.Select(1) // "main.go", which fills the field
384
385 dialog.name.SetText("typed.go")
386 dialog.confirm()
387
388 if got := dialog.Path(); got != filepath.Join(root, "typed.go") {
389 t.Errorf("Path() = %q, want the name that was typed", got)
390 }
391}
392
393func TestSaveAsKeepsTheNameItOpenedWith(t *testing.T) {
394 // Building the dialog must not let the list's initial highlight overwrite
395 // the file name Save As put in the field.
396 root := testDirectory(t, "other.go")
397 dialog := NewFileDialog("Save as", filepath.Join(root, "main.go"), ui.Rect{W: 80, H: 24})
398
399 if got := dialog.name.Text(); got != "main.go" {
400 t.Errorf("the name field holds %q, want main.go", got)
401 }
402}
403
404func TestOKWithAnEmptyDirectoryListingDoesNotCrash(t *testing.T) {
405 root := t.TempDir()
406 dialog := NewFileDialog("Open", root, ui.Rect{W: 80, H: 24})
407
408 dialog.confirm() // only "../" is listed
409
410 if dialog.directory != filepath.Dir(root) {
411 t.Errorf("the dialog is in %q, want the parent", dialog.directory)
412 }
413}
414
415// findOnScreen returns the screen position of a piece of text, so a test can
416// click it the way a user would rather than calling the action behind it.
417//
418// The column is counted in runes. strings.Index would give a byte offset, and
419// a drawn row is full of ░ and ║ at three bytes each — clicking a byte offset
420// lands about thirty columns to the right of the thing you meant.
421func findOnScreen(t *testing.T, lines []string, text string) (x, y int) {
422 t.Helper()
423
424 for row, line := range lines {
425 at := strings.Index(line, text)
426 if at < 0 {
427 continue
428 }
429 return len([]rune(line[:at])), row
430 }
431 t.Fatalf("%q is not on the screen:\n%s", text, strings.Join(lines, "\n"))
432 return 0, 0
433}
434
435func TestClickingOKInTheOpenDialogOpensTheHighlightedFile(t *testing.T) {
436 a, screen := newTestApp(t)
437 root := testDirectory(t, "main.go")
438 t.Chdir(root)
439
440 a.OpenFile()
441 // The dialog opens with the focus on the Name field, so the first Down
442 // moves it to the list and the second highlights the first real entry.
443 press(a, tcell.KeyDown, 0, tcell.ModNone)
444 press(a, tcell.KeyDown, 0, tcell.ModNone)
445
446 lines := render(t, a, screen)
447 x, y := findOnScreen(t, lines, "OK")
448 click(a, x, y)
449
450 if a.Modals() != 0 {
451 t.Fatalf("Modals() = %d; clicking OK left the dialog up", a.Modals())
452 }
453 if a.Desktop().Count() != 1 {
454 t.Fatalf("Count() = %d, want the file opened", a.Desktop().Count())
455 }
456 if got := a.Desktop().Active().Title(); got != "main.go" {
457 t.Errorf("the window is called %q, want main.go", got)
458 }
459}
460
461func TestTabbingToOKAndPressingEnterOpensTheHighlightedFile(t *testing.T) {
462 a, _ := newTestApp(t)
463 root := testDirectory(t, "main.go")
464 t.Chdir(root)
465
466 a.OpenFile()
467 press(a, tcell.KeyDown, 0, tcell.ModNone) // focus the list
468 press(a, tcell.KeyDown, 0, tcell.ModNone) // highlight main.go
469 press(a, tcell.KeyTab, 0, tcell.ModNone) // move the focus off the list
470 press(a, tcell.KeyEnter, 0, tcell.ModNone)
471
472 if a.Modals() != 0 {
473 t.Fatalf("Modals() = %d; the dialog is still up", a.Modals())
474 }
475 if a.Desktop().Count() != 1 {
476 t.Fatalf("Count() = %d, want the file opened", a.Desktop().Count())
477 }
478 if got := a.Desktop().Active().Title(); got != "main.go" {
479 t.Errorf("the window is called %q, want main.go", got)
480 }
481}
482
483func TestPressingAltOInTheOpenDialogOpensTheHighlightedFile(t *testing.T) {
484 a, _ := newTestApp(t)
485 root := testDirectory(t, "main.go")
486 t.Chdir(root)
487
488 a.OpenFile()
489 press(a, tcell.KeyDown, 0, tcell.ModNone) // focus the list
490 press(a, tcell.KeyDown, 0, tcell.ModNone) // highlight main.go
491 press(a, tcell.KeyRune, 'o', tcell.ModAlt) // the OK button's hot key
492
493 if a.Desktop().Count() != 1 {
494 t.Fatalf("Count() = %d, want the file opened", a.Desktop().Count())
495 }
496}