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.

app_test.go · 942 lines · 24.0 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 18h ago1package app
2
3import (
4 "os"
5 "path/filepath"
6 "strings"
7 "testing"
8
9 "github.com/gdamore/tcell/v2"
10
11 "codeberg.org/turbo-editors/turbo-core/buffer"
12 "codeberg.org/turbo-editors/turbo-core/editor"
13 "codeberg.org/turbo-editors/turbo-core/theme"
14 "codeberg.org/turbo-editors/turbo-core/ui"
15 "codeberg.org/turbo-editors/turbo-core/version"
16)
17
18// newTestApp returns an editor drawing on a simulated terminal.
19//
20// The whole application is exercised through it — layout, routing, dialogs,
21// drawing — with only the terminal itself replaced.
22func newTestApp(t *testing.T) (*App, tcell.SimulationScreen) {
23 t.Helper()
24
25 screen := tcell.NewSimulationScreen("UTF-8")
26 if err := screen.Init(); err != nil {
27 t.Fatalf("initialising the simulation screen: %v", err)
28 }
29 t.Cleanup(screen.Fini)
30 screen.SetSize(80, 24)
31
32 t.Setenv(testProfile().ThemeDirEnvVar(), t.TempDir()) // only the embedded themes
33 t.Setenv(testProfile().SnippetDirEnvVar(), t.TempDir())
34 a := New(screen, "turbo-classic", testProfile())
35 a.layout()
36 return a, screen
37}
38
39// press sends a key through the whole routing chain, exactly as the event loop
40// would.
41func press(a *App, key tcell.Key, r rune, mods tcell.ModMask) {
42 a.handle(tcell.NewEventKey(key, r, mods))
43}
44
45// typeText sends a run of printable characters.
46func typeText(a *App, text string) {
47 for _, r := range text {
48 press(a, tcell.KeyRune, r, tcell.ModNone)
49 }
50}
51
52// click sends a left-button press at a screen position.
53func click(a *App, x, y int) {
54 a.handle(tcell.NewEventMouse(x, y, tcell.Button1, tcell.ModNone))
55}
56
57// render draws the app and returns what the screen shows, one string per row.
58func render(t *testing.T, a *App, screen tcell.SimulationScreen) []string {
59 t.Helper()
60
61 a.layout()
62 a.draw()
63
64 cells, width, height := screen.GetContents()
65 lines := make([]string, height)
66 for y := range height {
67 var row strings.Builder
68 for x := range width {
69 runes := cells[y*width+x].Runes
70 if len(runes) == 0 {
71 row.WriteRune(' ')
72 continue
73 }
74 row.WriteRune(runes[0])
75 }
76 lines[y] = row.String()
77 }
78 return lines
79}
80
81// activeBuffer returns the buffer of the front window.
82func activeBuffer(t *testing.T, a *App) *buffer.Buffer {
83 t.Helper()
84 view := a.activeView()
85 if view == nil {
86 t.Fatal("no window is open")
87 }
88 return view.Buffer()
89}
90
91func TestANewEditorHasNoWindows(t *testing.T) {
92 a, _ := newTestApp(t)
93
94 if a.Desktop().Count() != 0 {
95 t.Errorf("Count() = %d, want 0", a.Desktop().Count())
96 }
97 if a.activeView() != nil {
98 t.Error("activeView() returned a view with no window open")
99 }
100}
101
102func TestNewFileOpensAnUntitledWindow(t *testing.T) {
103 a, _ := newTestApp(t)
104
105 a.NewFile()
106
107 if a.Desktop().Count() != 1 {
108 t.Fatalf("Count() = %d, want 1", a.Desktop().Count())
109 }
110 if got := a.Desktop().Active().Title(); got != untitledName {
111 t.Errorf("the window is called %q, want %q", got, untitledName)
112 }
113}
114
115func TestOpenReadsAFile(t *testing.T) {
116 a, _ := newTestApp(t)
117 path := filepath.Join(t.TempDir(), "main.go")
118 writeTestFile(t, path, "package main\n")
119
120 a.Open(path)
121
122 if a.Desktop().Count() != 1 {
123 t.Fatalf("Count() = %d, want 1", a.Desktop().Count())
124 }
125 if got := activeBuffer(t, a).Text(); got != "package main\n" {
126 t.Errorf("the buffer holds %q", got)
127 }
128 if got := a.Desktop().Active().Title(); got != "main.go" {
129 t.Errorf("the window is called %q, want main.go", got)
130 }
131}
132
133func TestOpeningTheSameFileTwiceRaisesTheExistingWindow(t *testing.T) {
134 a, _ := newTestApp(t)
135 path := filepath.Join(t.TempDir(), "main.go")
136 writeTestFile(t, path, "package main\n")
137
138 a.Open(path)
139 a.NewFile()
140 a.Open(path)
141
142 if a.Desktop().Count() != 2 {
143 t.Errorf("Count() = %d, want 2 — the file must not open twice", a.Desktop().Count())
144 }
145 if got := a.Desktop().Active().Title(); got != "main.go" {
146 t.Errorf("the front window is %q, want the existing main.go raised", got)
147 }
148}
149
150func TestOpeningAnUnreadableFileReportsIt(t *testing.T) {
151 a, _ := newTestApp(t)
152
153 a.Open(t.TempDir()) // a directory, not a file
154
155 if a.Modals() != 1 {
156 t.Fatalf("Modals() = %d, want a message box", a.Modals())
157 }
158 if got := a.TopModal().Title(); got != "Cannot open" {
159 t.Errorf("the box is titled %q", got)
160 }
161}
162
163func TestTypingReachesTheFrontWindow(t *testing.T) {
164 a, _ := newTestApp(t)
165 a.NewFile()
166
167 typeText(a, "package main")
168
169 if got := activeBuffer(t, a).Text(); got != "package main" {
170 t.Errorf("the buffer holds %q", got)
171 }
172}
173
174func TestEditingStarsTheWindowTitle(t *testing.T) {
175 a, _ := newTestApp(t)
176 path := filepath.Join(t.TempDir(), "main.go")
177 writeTestFile(t, path, "package main\n")
178 a.Open(path)
179
180 typeText(a, "x")
181
182 if got := a.Desktop().Active().Title(); got != "main.go *" {
183 t.Errorf("the window is called %q, want a star for unsaved changes", got)
184 }
185}
186
187func TestSaveWritesTheFileAndClearsTheStar(t *testing.T) {
188 a, _ := newTestApp(t)
189 path := filepath.Join(t.TempDir(), "main.go")
190 writeTestFile(t, path, "package main\n")
191 a.Open(path)
192 typeText(a, "// ")
193
194 press(a, tcell.KeyF2, 0, tcell.ModNone)
195
196 if got := readTestFile(t, path); got != "// package main\n" {
197 t.Errorf("the file holds %q", got)
198 }
199 if got := a.Desktop().Active().Title(); got != "main.go" {
200 t.Errorf("the window is called %q, want the star gone", got)
201 }
202 if !strings.Contains(a.StatusBar().Message(), "Saved") {
203 t.Errorf("the status bar says %q, want it to confirm the save", a.StatusBar().Message())
204 }
205}
206
207func TestSavingAnUntitledFileAsksForAName(t *testing.T) {
208 a, _ := newTestApp(t)
209 a.NewFile()
210
211 press(a, tcell.KeyF2, 0, tcell.ModNone)
212
213 if a.Modals() != 1 {
214 t.Fatalf("Modals() = %d, want the Save as dialog", a.Modals())
215 }
216 if !strings.HasPrefix(a.TopModal().Title(), "Save as") {
217 t.Errorf("the dialog is titled %q", a.TopModal().Title())
218 }
219}
220
221func TestF3OpensTheFileDialog(t *testing.T) {
222 a, _ := newTestApp(t)
223
224 press(a, tcell.KeyF3, 0, tcell.ModNone)
225
226 if a.Modals() != 1 {
227 t.Fatalf("Modals() = %d, want the Open dialog", a.Modals())
228 }
229 if !strings.HasPrefix(a.TopModal().Title(), "Open") {
230 t.Errorf("the dialog is titled %q", a.TopModal().Title())
231 }
232}
233
234func TestEscapeClosesADialogWithoutDoingAnything(t *testing.T) {
235 a, _ := newTestApp(t)
236
237 press(a, tcell.KeyF3, 0, tcell.ModNone)
238 press(a, tcell.KeyEscape, 0, tcell.ModNone)
239
240 if a.Modals() != 0 {
241 t.Errorf("Modals() = %d, want the dialog gone", a.Modals())
242 }
243 if a.Desktop().Count() != 0 {
244 t.Error("a cancelled Open dialog opened a window anyway")
245 }
246}
247
248func TestOpeningAFileThroughTheDialog(t *testing.T) {
249 a, _ := newTestApp(t)
250 directory := t.TempDir()
251 writeTestFile(t, filepath.Join(directory, "hello.go"), "package hello\n")
252
253 dialog := NewFileDialog("Open", directory, a.screenRect())
254 a.pushModal(dialog.Dialog(), func(result ui.Result) {
255 if result == ui.ResultOK {
256 a.Open(dialog.Path())
257 }
258 })
259 typeText(a, "hello.go")
260 press(a, tcell.KeyEnter, 0, tcell.ModNone)
261
262 if a.Desktop().Count() != 1 {
263 t.Fatalf("Count() = %d, want the file opened", a.Desktop().Count())
264 }
265 if got := activeBuffer(t, a).Text(); got != "package hello\n" {
266 t.Errorf("the buffer holds %q", got)
267 }
268}
269
270func TestAModalSwallowsTypingMeantForTheEditor(t *testing.T) {
271 a, _ := newTestApp(t)
272 a.NewFile()
273
274 press(a, tcell.KeyF3, 0, tcell.ModNone)
275 typeText(a, "zzz")
276
277 if got := activeBuffer(t, a).Text(); got != "" {
278 t.Errorf("the buffer holds %q, want the typing to have gone to the dialog", got)
279 }
280}
281
282func TestClosingAModifiedFileAsksFirst(t *testing.T) {
283 a, _ := newTestApp(t)
284 a.NewFile()
285 typeText(a, "x")
286
287 press(a, tcell.KeyCtrlW, 0, tcell.ModNone)
288
289 if a.Modals() != 1 {
290 t.Fatalf("Modals() = %d, want a confirmation", a.Modals())
291 }
292 if a.Desktop().Count() != 1 {
293 t.Error("the window was closed before the question was answered")
294 }
295}
296
297func TestAnsweringNoClosesWithoutSaving(t *testing.T) {
298 a, _ := newTestApp(t)
299 a.NewFile()
300 typeText(a, "x")
301 press(a, tcell.KeyCtrlW, 0, tcell.ModNone)
302
303 press(a, tcell.KeyRune, 'n', tcell.ModAlt) // the No button
304
305 if a.Modals() != 0 {
306 t.Errorf("Modals() = %d, want the question answered", a.Modals())
307 }
308 if a.Desktop().Count() != 0 {
309 t.Error("the window is still open after answering No")
310 }
311}
312
313func TestClosingAnUnmodifiedFileAsksNothing(t *testing.T) {
314 a, _ := newTestApp(t)
315 a.NewFile()
316
317 press(a, tcell.KeyCtrlW, 0, tcell.ModNone)
318
319 if a.Modals() != 0 {
320 t.Errorf("Modals() = %d, want no question for an untouched file", a.Modals())
321 }
322 if a.Desktop().Count() != 0 {
323 t.Error("the window is still open")
324 }
325}
326
327func TestQuitAsksAboutUnsavedWork(t *testing.T) {
328 a, _ := newTestApp(t)
329 a.NewFile()
330 typeText(a, "x")
331
332 press(a, tcell.KeyRune, 'x', tcell.ModAlt)
333
334 if a.Quitting() {
335 t.Error("the editor quit with unsaved changes and no question")
336 }
337 if a.Modals() != 1 {
338 t.Fatalf("Modals() = %d, want a confirmation", a.Modals())
339 }
340}
341
342func TestQuitLeavesStraightAwayWithNothingToSave(t *testing.T) {
343 a, _ := newTestApp(t)
344 a.NewFile()
345
346 press(a, tcell.KeyRune, 'x', tcell.ModAlt)
347
348 if !a.Quitting() {
349 t.Error("the editor did not quit although nothing was unsaved")
350 }
351}
352
353func TestUndoAndRedoThroughTheMenu(t *testing.T) {
354 a, _ := newTestApp(t)
355 a.NewFile()
356 typeText(a, "abc")
357
358 a.Undo()
359 if got := activeBuffer(t, a).Text(); got != "" {
360 t.Errorf("the buffer holds %q after undo, want it empty", got)
361 }
362
363 a.Redo()
364 if got := activeBuffer(t, a).Text(); got != "abc" {
365 t.Errorf("the buffer holds %q after redo", got)
366 }
367}
368
369func TestClipboardIsSharedBetweenWindows(t *testing.T) {
370 a, _ := newTestApp(t)
371 a.NewFile()
372 typeText(a, "shared")
373 a.SelectAll()
374 a.Copy()
375
376 a.NewFile()
377 a.Paste()
378
379 if got := activeBuffer(t, a).Text(); got != "shared" {
380 t.Errorf("the second window holds %q, want the copied text", got)
381 }
382}
383
384func TestFindSelectsTheMatch(t *testing.T) {
385 a, _ := newTestApp(t)
386 a.NewFile()
387 typeText(a, "one two three")
388 activeBuffer(t, a).MoveBufferStart()
389
390 a.lastSearch, a.lastMatchCase = "two", true
391 a.FindNext()
392
393 if got := activeBuffer(t, a).SelectedText(); got != "two" {
394 t.Errorf("SelectedText() = %q, want the match selected", got)
395 }
396}
397
398func TestFindNextMovesOffTheCurrentMatch(t *testing.T) {
399 a, _ := newTestApp(t)
400 a.NewFile()
401 typeText(a, "x y x y x")
402 activeBuffer(t, a).MoveBufferStart()
403 a.lastSearch, a.lastMatchCase = "x", true
404
405 a.FindNext()
406 first := activeBuffer(t, a).Cursor()
407 a.FindNext()
408 second := activeBuffer(t, a).Cursor()
409
410 if first == second {
411 t.Error("Find next found the same match twice")
412 }
413}
414
415func TestFindReportsWhenThereIsNoMatch(t *testing.T) {
416 a, _ := newTestApp(t)
417 a.NewFile()
418 typeText(a, "abc")
419 a.lastSearch = "zzz"
420
421 a.FindNext()
422
423 if !strings.Contains(a.StatusBar().Message(), "not found") {
424 t.Errorf("the status bar says %q, want it to report the failure", a.StatusBar().Message())
425 }
426}
427
428func TestGoToLine(t *testing.T) {
429 a, _ := newTestApp(t)
430 a.NewFile()
431 activeBuffer(t, a).SetText(strings.Repeat("line\n", 40))
432
433 press(a, tcell.KeyCtrlG, 0, tcell.ModNone)
434 typeText(a, "25")
435 press(a, tcell.KeyEnter, 0, tcell.ModNone)
436
437 if got := activeBuffer(t, a).Cursor().Line; got != 24 {
438 t.Errorf("Cursor().Line = %d, want 24", got)
439 }
440}
441
442func TestGoToLineRejectsNonsense(t *testing.T) {
443 a, _ := newTestApp(t)
444 a.NewFile()
445
446 press(a, tcell.KeyCtrlG, 0, tcell.ModNone)
447 typeText(a, "banana")
448 press(a, tcell.KeyEnter, 0, tcell.ModNone)
449
450 if !strings.Contains(a.StatusBar().Message(), "Not a line number") {
451 t.Errorf("the status bar says %q", a.StatusBar().Message())
452 }
453}
454
455func TestWindowNumbersSelectWindows(t *testing.T) {
456 a, _ := newTestApp(t)
457 a.NewFile()
458 first := a.Desktop().Active()
459 a.NewFile()
460
461 press(a, tcell.KeyRune, '1', tcell.ModAlt)
462
463 if a.Desktop().Active() != first {
464 t.Error("Alt-1 did not bring the first window forward")
465 }
466}
467
468func TestAltZeroListsTheWindows(t *testing.T) {
469 a, _ := newTestApp(t)
470 a.NewFile()
471 a.NewFile()
472
473 press(a, tcell.KeyRune, '0', tcell.ModAlt)
474
475 if a.Modals() != 1 {
476 t.Fatalf("Modals() = %d, want the window list", a.Modals())
477 }
478 if got := a.TopModal().Title(); got != "Windows" {
479 t.Errorf("the dialog is titled %q", got)
480 }
481}
482
483func TestF6CyclesWindows(t *testing.T) {
484 a, _ := newTestApp(t)
485 a.NewFile()
486 first := a.Desktop().Active()
487 a.NewFile()
488
489 press(a, tcell.KeyF6, 0, tcell.ModNone)
490
491 if a.Desktop().Active() != first {
492 t.Error("F6 did not cycle to the window behind")
493 }
494}
495
496func TestChangingTheTheme(t *testing.T) {
497 a, _ := newTestApp(t)
498 before := a.Theme()
499
500 a.setTheme("turbo-dark")
501
502 if a.Theme() == before {
503 t.Error("the theme did not change")
504 }
505 if got := a.ThemeName(); got != "turbo-dark" {
506 t.Errorf("ThemeName() = %q", got)
507 }
508}
509
510func TestAnUnknownThemeFallsBackToTheDefault(t *testing.T) {
511 a, _ := newTestApp(t)
512
513 a.setTheme("no-such-theme")
514
515 if got := a.ThemeName(); got != "turbo-classic" {
516 t.Errorf("ThemeName() = %q, want the default", got)
517 }
518}
519
520func TestToggleLineNumbers(t *testing.T) {
521 a, _ := newTestApp(t)
522 a.NewFile()
523 view := a.activeView()
524
525 a.ToggleLineNumbers()
526
527 if view.LineNumbers() {
528 t.Error("the gutter is still shown after toggling it off")
529 }
530}
531
532func TestF10OpensTheMenuAndEscapeClosesIt(t *testing.T) {
533 a, _ := newTestApp(t)
534
535 press(a, tcell.KeyF10, 0, tcell.ModNone)
536 if !a.MenuBar().Open() {
537 t.Fatal("F10 did not open the menu")
538 }
539
540 press(a, tcell.KeyEscape, 0, tcell.ModNone)
541 if a.MenuBar().Open() {
542 t.Error("Escape did not close the menu")
543 }
544}
545
546func TestAMenuItemRunsItsAction(t *testing.T) {
547 a, _ := newTestApp(t)
548
549 press(a, tcell.KeyRune, 'f', tcell.ModAlt) // open File
550 press(a, tcell.KeyRune, 'n', tcell.ModNone) // New
551
552 if a.Desktop().Count() != 1 {
553 t.Errorf("Count() = %d, want the File ▸ New item to have opened a window", a.Desktop().Count())
554 }
555 if a.MenuBar().Open() {
556 t.Error("the menu stayed open after an item ran")
557 }
558}
559
560func TestItemsThatNeedAFileAreGreyedOutWithoutOne(t *testing.T) {
561 a, _ := newTestApp(t)
562
563 if a.hasWindow() {
564 t.Fatal("hasWindow() = true with no window open")
565 }
566 a.NewFile()
567 if !a.hasWindow() {
568 t.Error("hasWindow() = false with a window open")
569 }
570}
571
572func TestTheScreenShowsTheFurniture(t *testing.T) {
573 a, screen := newTestApp(t)
574 path := filepath.Join(t.TempDir(), "main.go")
575 writeTestFile(t, path, "package main\n\nfunc main() {}\n")
576 a.Open(path)
577
578 lines := render(t, a, screen)
579
580 if !strings.Contains(lines[0], "File") || !strings.Contains(lines[0], "Help") {
581 t.Errorf("the menu bar is %q", lines[0])
582 }
583 if !strings.Contains(lines[1], "main.go") {
584 t.Errorf("row 1 is %q, want the window's title bar", lines[1])
585 }
586 if !strings.Contains(lines[2], "package main") {
587 t.Errorf("row 2 is %q, want the file's first line", lines[2])
588 }
589 if !strings.Contains(lines[23], "F2 Save") {
590 t.Errorf("the status bar is %q", lines[23])
591 }
592}
593
594func TestTheStatusBarShowsTheCursorPosition(t *testing.T) {
595 a, screen := newTestApp(t)
596 a.NewFile()
597 activeBuffer(t, a).SetText("one\ntwo\nthree")
598 activeBuffer(t, a).SetCursor(buffer.Position{Line: 2, Col: 3})
599
600 lines := render(t, a, screen)
601
602 if !strings.Contains(lines[23], "3:4") {
603 t.Errorf("the status bar is %q, want the cursor at 3:4", lines[23])
604 }
605}
606
607func TestAResizeIsHandledWithoutPanicking(t *testing.T) {
608 a, screen := newTestApp(t)
609 a.NewFile()
610
611 screen.SetSize(40, 12)
612 a.handle(tcell.NewEventResize(40, 12))
613 render(t, a, screen)
614
615 if got := a.desktopRect(); got.W != 40 || got.H != 10 {
616 t.Errorf("the desktop is %+v, want it to fill the smaller screen", got)
617 }
618}
619
620func TestAVerySmallScreenDoesNotPanic(t *testing.T) {
621 a, screen := newTestApp(t)
622 a.NewFile()
623
624 screen.SetSize(4, 2)
625 a.handle(tcell.NewEventResize(4, 2))
626 render(t, a, screen)
627}
628
629func TestClickingTheMenuBarOpensAMenu(t *testing.T) {
630 a, screen := newTestApp(t)
631 render(t, a, screen)
632
633 click(a, 2, 0)
634
635 if !a.MenuBar().Open() {
636 t.Error("clicking the menu bar did not open a menu")
637 }
638}
639
640func TestClickingAStatusHintRunsIt(t *testing.T) {
641 a, screen := newTestApp(t)
642 render(t, a, screen)
643
644 click(a, 2, 23) // "F1 Describe"
645
646 if a.Modals() != 1 {
647 t.Errorf("Modals() = %d, want the keyboard help that F1 opens with no file", a.Modals())
648 }
649}
650
651func TestWindowTitleForABufferWithNoPath(t *testing.T) {
652 if got := windowTitle(buffer.New()); got != untitledName {
653 t.Errorf("windowTitle() = %q, want %q", got, untitledName)
654 }
655}
656
657func TestSaveAsChangesTheWindowTitleAndTurnsColouringOn(t *testing.T) {
658 a, _ := newTestApp(t)
659 a.NewFile()
660 typeText(a, "package main")
661 path := filepath.Join(t.TempDir(), "renamed.go")
662
663 a.save(a.activeView(), path)
664
665 if got := a.Desktop().Active().Title(); got != "renamed.go" {
666 t.Errorf("the window is called %q", got)
667 }
668 if got := readTestFile(t, path); got != "package main" {
669 t.Errorf("the file holds %q", got)
670 }
671}
672
673func TestTheAboutBoxNamesTheEditorAndTheTheme(t *testing.T) {
674 a, _ := newTestApp(t)
675
676 a.ShowAbout()
677
678 if a.Modals() != 1 {
679 t.Fatalf("Modals() = %d, want the About box", a.Modals())
680 }
681}
682
683func TestTheAboutTextCarriesEveryFactTheBuildRecorded(t *testing.T) {
684 got := aboutText(testProfile().Name, testProfile().Language, version.Info{
685 Number: "0.2.0",
686 Commit: "88a4c38",
687 Built: "2026-08-31T18:04:05Z",
688 }, "turbo-dark")
689
690 for _, want := range []string{
691 testProfile().Name + " 0.2.0",
692 "Commit: 88a4c38",
693 "Built: 2026-08-31 18:04 UTC",
694 "Theme: turbo-dark",
695 } {
696 if !strings.Contains(got, want) {
697 t.Errorf("the About box never says %q:\n%s", want, got)
698 }
699 }
700}
701
702func TestTheAboutTextNamesTheLanguageTheEditorIsFor(t *testing.T) {
703 // This box is drawn by the library, and the library is not for Go. It said
704 // "an editor for Go" in every editor built on it, so Turbo Rust's About box
705 // named the wrong language.
706 got := aboutText("Turbo Rust", "Rust", version.Info{Number: "0.2.0"}, "turbo-dark")
707
708 if !strings.Contains(got, "A Turbo C-style editor for Rust,") {
709 t.Errorf("the About box does not say what the editor is for:\n%s", got)
710 }
711 if strings.Contains(got, "editor for Go") {
712 t.Errorf("the About box names Go in an editor for Rust:\n%s", got)
713 }
714 // It is written in Go whatever it edits, and that line stays.
715 if !strings.Contains(got, "written in Go.") {
716 t.Errorf("the About box lost the language it is implemented in:\n%s", got)
717 }
718}
719
720func TestTheAboutTextLeavesOutWhatTheBuildDidNotRecord(t *testing.T) {
721 // A binary from `go install …@v0.2.0` knows its version and nothing else.
722 // A blank "Commit:" line would say only that the editor failed to fill it.
723 got := aboutText(testProfile().Name, testProfile().Language, version.Info{Number: "0.2.0"}, "turbo-classic")
724
725 for _, unwanted := range []string{"Commit", "Built"} {
726 if strings.Contains(got, unwanted) {
727 t.Errorf("the About box mentions %q with nothing to put after it:\n%s", unwanted, got)
728 }
729 }
730 if !strings.Contains(got, "Theme: turbo-classic") {
731 t.Errorf("the About box lost its theme line:\n%s", got)
732 }
733}
734
735func TestTheAboutTextNeverShowsAHardCodedVersion(t *testing.T) {
736 // The whole point of internal/version: the number comes from the build, so
737 // a release cannot ship an About box still naming the previous one.
738 got := aboutText(testProfile().Name, testProfile().Language, version.Current(), "turbo-dark")
739
740 if strings.Contains(got, testProfile().Name+" 0.1.0\n") {
741 t.Errorf("the About box named a version no build reported:\n%s", got)
742 }
743}
744
745func TestWindowLayoutCommands(t *testing.T) {
746 a, _ := newTestApp(t)
747 a.NewFile()
748 a.NewFile()
749 a.NewFile()
750
751 a.TileWindows()
752 windows := a.Desktop().Windows()
753 for i, first := range windows {
754 for _, second := range windows[i+1:] {
755 if !first.Bounds().Intersect(second.Bounds()).IsEmpty() {
756 t.Error("tiled windows overlap")
757 }
758 }
759 }
760
761 a.CascadeWindows()
762 a.MaximizeWindow()
763 if got := a.Desktop().Active().Bounds(); got != a.desktopRect() {
764 t.Errorf("the maximised window is %+v, want the whole desktop", got)
765 }
766}
767
768func TestViewAndWindowStayPaired(t *testing.T) {
769 a, _ := newTestApp(t)
770 a.NewFile()
771 view := a.activeView()
772
773 if got := a.windowOf(view); got != a.Desktop().Active() {
774 t.Error("windowOf did not find the view's own window")
775 }
776 if a.windowOf(editor.NewView(buffer.New(), &editor.Clipboard{})) != nil {
777 t.Error("windowOf found a window for a view that is in none")
778 }
779}
780
781func writeTestFile(t *testing.T, path, content string) {
782 t.Helper()
783 if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
784 t.Fatalf("writing %s: %v", path, err)
785 }
786}
787
788func readTestFile(t *testing.T, path string) string {
789 t.Helper()
790 data, err := os.ReadFile(path)
791 if err != nil {
792 t.Fatalf("reading %s: %v", path, err)
793 }
794 return string(data)
795}
796
797func TestOnlyTheActiveWindowShowsACursor(t *testing.T) {
798 // Two painted cursor blocks would mean neither of them means anything.
799 a, _ := newTestApp(t)
800 a.NewFile()
801 behind := a.activeView()
802 a.NewFile()
803 front := a.activeView()
804
805 if behind == front {
806 t.Fatal("the two windows share a view")
807 }
808 if behind.Focused() {
809 t.Error("the window behind still holds the focus")
810 }
811 if !front.Focused() {
812 t.Error("the front window does not hold the focus")
813 }
814
815 a.NextWindow()
816
817 if !behind.Focused() || front.Focused() {
818 t.Error("the focus did not follow the window that came forward")
819 }
820}
821
822func TestResizingTheTerminalResizesTheWindow(t *testing.T) {
823 a, screen := newTestApp(t)
824 a.NewFile()
825 render(t, a, screen)
826
827 before := a.Desktop().Active().Bounds()
828 marginRight := 80 - before.Right()
829 marginBottom := 24 - before.Bottom()
830
831 screen.SetSize(120, 40)
832 a.handle(tcell.NewEventResize(120, 40))
833 render(t, a, screen)
834
835 after := a.Desktop().Active().Bounds()
836 if after.W <= before.W || after.H <= before.H {
837 t.Fatalf("the window is %+v, want it grown from %+v", after, before)
838 }
839 // It kept the same distance from the terminal's far edges, which is what
840 // "the window still fills the terminal" means.
841 if got := 120 - after.Right(); got != marginRight {
842 t.Errorf("the window ends %d cells from the right edge, want %d", got, marginRight)
843 }
844 if got := 40 - after.Bottom(); got != marginBottom {
845 t.Errorf("the window ends %d rows from the bottom edge, want %d", got, marginBottom)
846 }
847}
848
849func TestResizingTheTerminalSmallerKeepsTheWindowInside(t *testing.T) {
850 a, screen := newTestApp(t)
851 a.NewFile()
852 render(t, a, screen)
853
854 screen.SetSize(40, 12)
855 a.handle(tcell.NewEventResize(40, 12))
856 render(t, a, screen)
857
858 got := a.Desktop().Active().Bounds()
859 if got.Right() > 40 || got.Bottom() > 11 {
860 t.Errorf("the window is %+v, want it inside a 40x12 terminal", got)
861 }
862}
863
864func TestTheEditorFollowsItsWindowWhenTheTerminalIsResized(t *testing.T) {
865 a, screen := newTestApp(t)
866 a.NewFile()
867 activeBuffer(t, a).SetText(strings.Repeat("line\n", 60))
868 render(t, a, screen)
869
870 before := a.activeView().VisibleLines()
871
872 screen.SetSize(120, 40)
873 a.handle(tcell.NewEventResize(120, 40))
874 render(t, a, screen)
875
876 if after := a.activeView().VisibleLines(); after <= before {
877 t.Errorf("the editor shows %d lines, want more than the %d it showed before", after, before)
878 }
879}
880
881func TestResizingRecentresAnOpenDialog(t *testing.T) {
882 a, screen := newTestApp(t)
883 press(a, tcell.KeyF3, 0, tcell.ModNone)
884 if a.Modals() != 1 {
885 t.Fatal("the Open dialog did not appear")
886 }
887
888 screen.SetSize(120, 40)
889 a.handle(tcell.NewEventResize(120, 40))
890
891 dialog := a.TopModal()
892 want := dialog.Bounds().CenteredIn(a.screenRect())
893 if got := dialog.Bounds(); got.X != want.X || got.Y != want.Y {
894 t.Errorf("the dialog is at %+v, want it recentred at %+v", got, want)
895 }
896}
897
898func TestResizingDismissesTheCompletionPopup(t *testing.T) {
899 // It is anchored to a cursor that has just moved, so the only honest thing
900 // to do with it is put it away.
901 a, screen := newTestApp(t)
902 a.NewFile()
903 a.Completion().Show(sampleItems(), "", 10, 5, a.screenRect())
904
905 screen.SetSize(120, 40)
906 a.handle(tcell.NewEventResize(120, 40))
907
908 if a.Completion().Visible() {
909 t.Error("the popup survived a resize")
910 }
911}
912
913func TestTheTerminalCursorTakesItsColourFromTheTheme(t *testing.T) {
914 // A terminal draws its cursor *over* the cell, in whatever colour the user
915 // configured for some other palette. Painting the cell underneath is not
916 // enough; the theme has to name the cursor's own colour.
917 for _, name := range []string{"turbo-classic", "turbo-dark", "borland-light"} {
918 t.Run(name, func(t *testing.T) {
919 th, err := theme.Load(name, "")
920 if err != nil {
921 t.Fatalf("Load() error = %v", err)
922 }
923
924 _, want, _ := th.Style(theme.KeyEditorCursor).Decompose()
925
926 if got := cursorColor(th); got != want {
927 t.Errorf("cursorColor() = %v, want the cursor style's background %v", got, want)
928 }
929 })
930 }
931}
932
933func TestChangingTheThemeChangesTheCursorColour(t *testing.T) {
934 a, _ := newTestApp(t)
935 before := cursorColor(a.Theme())
936
937 a.setTheme("turbo-dark")
938
939 if cursorColor(a.Theme()) == before {
940 t.Error("the cursor colour did not follow the theme")
941 }
942}