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.

screen_test.go · 697 lines · 16.3 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 21h ago1package terminal
2
3import (
4 "strings"
5 "testing"
6
7 "github.com/gdamore/tcell/v2"
8)
9
10// writeString puts a run of characters onto a screen, as a program would.
11func writeString(s *Screen, text string) {
12 for _, r := range text {
13 s.WriteRune(r)
14 }
15}
16
17// wantLines fails the test unless the screen's rows read as expected. Rows
18// beyond the ones given are not checked.
19func wantLines(t *testing.T, s *Screen, lines ...string) {
20 t.Helper()
21 for row, want := range lines {
22 if got := s.LineText(row); got != want {
23 t.Errorf("row %d = %q, want %q", row, got, want)
24 }
25 }
26}
27
28func TestANewScreenIsBlankWithTheCursorAtTheTopLeft(t *testing.T) {
29 s := NewScreen(20, 5)
30
31 width, height := s.Size()
32 if width != 20 || height != 5 {
33 t.Errorf("Size() = %dx%d, want 20x5", width, height)
34 }
35 if got := s.Cursor(); got != (Cursor{}) {
36 t.Errorf("Cursor() = %+v, want the top left", got)
37 }
38 if !s.CursorVisible() {
39 t.Error("CursorVisible() = false on a new screen")
40 }
41 for row := range 5 {
42 if got := s.LineText(row); got != "" {
43 t.Errorf("row %d = %q, want it blank", row, got)
44 }
45 }
46}
47
48func TestASizeBelowOneIsRaisedToOne(t *testing.T) {
49 s := NewScreen(0, -3)
50
51 if width, height := s.Size(); width != 1 || height != 1 {
52 t.Errorf("Size() = %dx%d, want 1x1 — the cursor must have somewhere to be", width, height)
53 }
54 s.WriteRune('x') // must not panic
55}
56
57func TestWritingAdvancesTheCursor(t *testing.T) {
58 s := NewScreen(20, 3)
59
60 writeString(s, "hello")
61
62 wantLines(t, s, "hello")
63 if got := s.Cursor(); got != (Cursor{Row: 0, Col: 5}) {
64 t.Errorf("Cursor() = %+v, want column 5", got)
65 }
66}
67
68func TestTheCursorStopsInTheLastColumnUntilTheNextCharacter(t *testing.T) {
69 // A character written in the last column must not scroll the screen by
70 // itself. Only the character after it wraps.
71 s := NewScreen(3, 2)
72
73 writeString(s, "abc")
74
75 if got := s.Cursor(); got != (Cursor{Row: 0, Col: 2}) {
76 t.Fatalf("Cursor() = %+v, want it parked in the last column", got)
77 }
78 wantLines(t, s, "abc", "")
79
80 s.WriteRune('d')
81 wantLines(t, s, "abc", "d")
82}
83
84func TestWrappingCanBeTurnedOff(t *testing.T) {
85 s := NewScreen(3, 2)
86 s.SetAutoWrap(false)
87
88 writeString(s, "abcdef")
89
90 wantLines(t, s, "abf", "")
91 if got := s.Cursor(); got != (Cursor{Row: 0, Col: 2}) {
92 t.Errorf("Cursor() = %+v, want it held in the last column", got)
93 }
94}
95
96func TestMoveToClampsToTheScreen(t *testing.T) {
97 s := NewScreen(10, 4)
98
99 tests := []struct {
100 name string
101 row, col int
102 want Cursor
103 }{
104 {"inside", 2, 3, Cursor{Row: 2, Col: 3}},
105 {"past the bottom", 99, 0, Cursor{Row: 3, Col: 0}},
106 {"past the right", 0, 99, Cursor{Row: 0, Col: 9}},
107 {"negative", -5, -5, Cursor{}},
108 }
109
110 for _, tc := range tests {
111 t.Run(tc.name, func(t *testing.T) {
112 s.MoveTo(tc.row, tc.col)
113 if got := s.Cursor(); got != tc.want {
114 t.Errorf("MoveTo(%d, %d) gave %+v, want %+v", tc.row, tc.col, got, tc.want)
115 }
116 })
117 }
118}
119
120func TestCarriageReturnAndLineFeed(t *testing.T) {
121 s := NewScreen(10, 3)
122 writeString(s, "one")
123
124 s.CarriageReturn()
125 if got := s.Cursor().Col; got != 0 {
126 t.Errorf("after a carriage return the cursor is at column %d, want 0", got)
127 }
128
129 s.LineFeed()
130 if got := s.Cursor(); got != (Cursor{Row: 1, Col: 0}) {
131 t.Errorf("Cursor() = %+v, want the start of the next line", got)
132 }
133}
134
135func TestALineFeedOnTheLastRowScrolls(t *testing.T) {
136 s := NewScreen(10, 3)
137 for _, text := range []string{"one", "two", "three"} {
138 writeString(s, text)
139 s.CarriageReturn()
140 s.LineFeed()
141 }
142
143 wantLines(t, s, "two", "three", "")
144 if got := s.ScrollbackLen(); got != 1 {
145 t.Errorf("ScrollbackLen() = %d, want the scrolled-off line kept", got)
146 }
147}
148
149func TestReverseLineFeedScrollsDownAtTheTop(t *testing.T) {
150 s := NewScreen(10, 3)
151 writeString(s, "one")
152 s.MoveTo(0, 0)
153
154 s.ReverseLineFeed()
155
156 wantLines(t, s, "", "one")
157 if got := s.Cursor().Row; got != 0 {
158 t.Errorf("Cursor().Row = %d, want it held at the top", got)
159 }
160}
161
162func TestBackspaceMovesWithoutErasing(t *testing.T) {
163 s := NewScreen(10, 2)
164 writeString(s, "abc")
165
166 s.Backspace()
167
168 if got := s.Cursor().Col; got != 2 {
169 t.Errorf("Cursor().Col = %d, want 2", got)
170 }
171 wantLines(t, s, "abc")
172
173 s.MoveTo(0, 0)
174 s.Backspace()
175 if got := s.Cursor().Col; got != 0 {
176 t.Errorf("Cursor().Col = %d, want it held at the first column", got)
177 }
178}
179
180func TestTabMovesToTheNextStop(t *testing.T) {
181 s := NewScreen(30, 2)
182
183 s.Tab()
184 if got := s.Cursor().Col; got != TabWidth {
185 t.Errorf("Cursor().Col = %d, want %d", got, TabWidth)
186 }
187
188 s.MoveTo(0, 3)
189 s.Tab()
190 if got := s.Cursor().Col; got != TabWidth {
191 t.Errorf("Cursor().Col = %d, want the next stop at %d", got, TabWidth)
192 }
193}
194
195func TestTabStopsAtTheLastColumn(t *testing.T) {
196 s := NewScreen(10, 2)
197 s.MoveTo(0, 9)
198
199 s.Tab()
200
201 if got := s.Cursor().Col; got != 9 {
202 t.Errorf("Cursor().Col = %d, want it held in the last column", got)
203 }
204}
205
206func TestSaveAndRestoreCursorCarryTheStyle(t *testing.T) {
207 s := NewScreen(10, 4)
208 s.MoveTo(2, 3)
209 s.SetStyle(tcell.StyleDefault.Bold(true))
210
211 s.SaveCursor()
212 s.MoveTo(0, 0)
213 s.SetStyle(tcell.StyleDefault)
214 s.RestoreCursor()
215
216 if got := s.Cursor(); got != (Cursor{Row: 2, Col: 3}) {
217 t.Errorf("Cursor() = %+v, want what was saved", got)
218 }
219 if _, _, attrs := s.Style().Decompose(); attrs&tcell.AttrBold == 0 {
220 t.Error("the saved style was not restored")
221 }
222}
223
224func TestEraseDisplay(t *testing.T) {
225 fill := func() *Screen {
226 s := NewScreen(5, 3)
227 for row, text := range []string{"aaaaa", "bbbbb", "ccccc"} {
228 s.MoveTo(row, 0)
229 writeString(s, text)
230 }
231 s.MoveTo(1, 2)
232 return s
233 }
234
235 tests := []struct {
236 name string
237 mode EraseMode
238 lines []string
239 }{
240 {"to the end", EraseToEnd, []string{"aaaaa", "bb", ""}},
241 {"to the start", EraseToStart, []string{"", " bb", "ccccc"}},
242 {"all of it", EraseAll, []string{"", "", ""}},
243 }
244
245 for _, tc := range tests {
246 t.Run(tc.name, func(t *testing.T) {
247 s := fill()
248 s.EraseDisplay(tc.mode)
249 wantLines(t, s, tc.lines...)
250 if got := s.Cursor(); got != (Cursor{Row: 1, Col: 2}) {
251 t.Errorf("Cursor() = %+v, want it left where it was", got)
252 }
253 })
254 }
255}
256
257func TestEraseLine(t *testing.T) {
258 tests := []struct {
259 name string
260 mode EraseMode
261 want string
262 }{
263 {"to the end", EraseToEnd, "ab"},
264 {"to the start", EraseToStart, " de"},
265 {"all of it", EraseAll, ""},
266 }
267
268 for _, tc := range tests {
269 t.Run(tc.name, func(t *testing.T) {
270 s := NewScreen(5, 2)
271 writeString(s, "abcde")
272 s.MoveTo(0, 2)
273
274 s.EraseLine(tc.mode)
275
276 wantLines(t, s, tc.want)
277 })
278 }
279}
280
281func TestEraseCharsBlanksInPlace(t *testing.T) {
282 s := NewScreen(6, 2)
283 writeString(s, "abcdef")
284 s.MoveTo(0, 1)
285
286 s.EraseChars(3)
287
288 wantLines(t, s, "a ef")
289}
290
291func TestInsertAndDeleteChars(t *testing.T) {
292 s := NewScreen(6, 2)
293 writeString(s, "abcdef")
294 s.MoveTo(0, 2)
295
296 s.DeleteChars(2)
297 wantLines(t, s, "abef")
298
299 s.MoveTo(0, 2)
300 s.InsertChars(2)
301 wantLines(t, s, "ab ef")
302}
303
304func TestInsertAndDeleteCharsAreClampedToTheLine(t *testing.T) {
305 s := NewScreen(4, 2)
306 writeString(s, "abcd")
307 s.MoveTo(0, 2)
308
309 s.DeleteChars(99) // must not read past the line
310 wantLines(t, s, "ab")
311
312 s.InsertChars(99)
313 wantLines(t, s, "ab")
314}
315
316func TestInsertAndDeleteLines(t *testing.T) {
317 build := func() *Screen {
318 s := NewScreen(5, 4)
319 for row, text := range []string{"one", "two", "three", "four"} {
320 s.MoveTo(row, 0)
321 writeString(s, text)
322 }
323 s.MoveTo(1, 0)
324 return s
325 }
326
327 s := build()
328 s.InsertLines(1)
329 wantLines(t, s, "one", "", "two", "three")
330
331 s = build()
332 s.DeleteLines(1)
333 wantLines(t, s, "one", "three", "four", "")
334}
335
336func TestInsertLinesOutsideTheScrollRegionDoesNothing(t *testing.T) {
337 // A program with a status line on row 0 narrows the region so that its
338 // status line stays put; an insert from outside must not push it about.
339 s := NewScreen(5, 4)
340 s.MoveTo(0, 0)
341 writeString(s, "stay")
342 s.SetScrollRegion(1, 3)
343 s.MoveTo(0, 0)
344
345 s.InsertLines(2)
346
347 wantLines(t, s, "stay")
348}
349
350func TestTheScrollRegionLimitsScrolling(t *testing.T) {
351 s := NewScreen(5, 4)
352 for row, text := range []string{"head", "a", "b", "c"} {
353 s.MoveTo(row, 0)
354 writeString(s, text)
355 }
356
357 s.SetScrollRegion(1, 3)
358 s.MoveTo(3, 0)
359 s.LineFeed()
360
361 wantLines(t, s, "head", "b", "c", "")
362}
363
364func TestSettingTheScrollRegionMovesTheCursorHome(t *testing.T) {
365 s := NewScreen(5, 4)
366 s.MoveTo(3, 3)
367
368 s.SetScrollRegion(1, 3)
369
370 if got := s.Cursor(); got != (Cursor{}) {
371 t.Errorf("Cursor() = %+v, want the top left", got)
372 }
373}
374
375func TestAnImpossibleScrollRegionIsIgnored(t *testing.T) {
376 s := NewScreen(5, 4)
377 s.SetScrollRegion(1, 3)
378 s.MoveTo(2, 2)
379
380 s.SetScrollRegion(3, 1) // the wrong way round
381 s.SetScrollRegion(2, 2) // empty
382
383 s.MoveTo(3, 0)
384 s.LineFeed()
385 if got := s.Cursor().Row; got != 3 {
386 t.Errorf("Cursor().Row = %d, want the earlier region still in force", got)
387 }
388}
389
390func TestScrollingANarrowRegionIsNotHistory(t *testing.T) {
391 // Lines leaving a narrow region are part of a program redrawing its own
392 // display, not lines the session has finished with.
393 s := NewScreen(5, 4)
394 s.SetScrollRegion(1, 3)
395 s.MoveTo(3, 0)
396
397 s.LineFeed()
398
399 if got := s.ScrollbackLen(); got != 0 {
400 t.Errorf("ScrollbackLen() = %d, want nothing remembered", got)
401 }
402}
403
404func TestScrollbackKeepsWhatLeavesTheTop(t *testing.T) {
405 s := NewScreen(10, 2)
406 for _, text := range []string{"one", "two", "three", "four"} {
407 writeString(s, text)
408 s.CarriageReturn()
409 s.LineFeed()
410 }
411
412 if got := s.ScrollbackLen(); got != 3 {
413 t.Fatalf("ScrollbackLen() = %d, want 3", got)
414 }
415 if got := lineText(s.ScrollbackLine(0)); got != "one" {
416 t.Errorf("the oldest scrollback line is %q, want %q", got, "one")
417 }
418 if s.ScrollbackLine(-1) != nil || s.ScrollbackLine(99) != nil {
419 t.Error("ScrollbackLine outside the history returned something")
420 }
421}
422
423func TestScrollbackIsCapped(t *testing.T) {
424 s := NewScreen(10, 2)
425 s.SetMaxScrollback(3)
426
427 for i := range 20 {
428 writeString(s, string(rune('a'+i%26)))
429 s.CarriageReturn()
430 s.LineFeed()
431 }
432
433 if got := s.ScrollbackLen(); got != 3 {
434 t.Errorf("ScrollbackLen() = %d, want the cap of 3", got)
435 }
436}
437
438func TestScrollbackCanBeTurnedOff(t *testing.T) {
439 s := NewScreen(10, 2)
440 s.SetMaxScrollback(0)
441
442 for range 5 {
443 s.LineFeed()
444 }
445
446 if got := s.ScrollbackLen(); got != 0 {
447 t.Errorf("ScrollbackLen() = %d, want none kept", got)
448 }
449}
450
451func TestTheAlternateScreenIsAScratchSurface(t *testing.T) {
452 s := NewScreen(10, 3)
453 writeString(s, "before")
454
455 s.UseAlternate(true)
456
457 if !s.Alternate() {
458 t.Fatal("Alternate() = false after switching to it")
459 }
460 wantLines(t, s, "")
461 if got := s.Cursor(); got != (Cursor{}) {
462 t.Errorf("Cursor() = %+v, want the top left of a fresh screen", got)
463 }
464
465 writeString(s, "during")
466 s.UseAlternate(false)
467
468 if s.Alternate() {
469 t.Fatal("Alternate() = true after switching back")
470 }
471 wantLines(t, s, "before")
472}
473
474func TestSwitchingToTheScreenAlreadyShowingDoesNothing(t *testing.T) {
475 s := NewScreen(10, 3)
476 writeString(s, "kept")
477
478 s.UseAlternate(false) // already on the primary
479
480 wantLines(t, s, "kept")
481}
482
483func TestTheAlternateScreenHasNoScrollback(t *testing.T) {
484 s := NewScreen(10, 2)
485 s.UseAlternate(true)
486
487 for range 5 {
488 s.LineFeed()
489 }
490
491 if got := s.ScrollbackLen(); got != 0 {
492 t.Errorf("ScrollbackLen() = %d, want none on a scratch surface", got)
493 }
494}
495
496func TestLeavingTheAlternateScreenBringsBackTheHistory(t *testing.T) {
497 s := NewScreen(10, 2)
498 for _, text := range []string{"one", "two", "three"} {
499 writeString(s, text)
500 s.CarriageReturn()
501 s.LineFeed()
502 }
503 before := s.ScrollbackLen()
504
505 s.UseAlternate(true)
506 s.UseAlternate(false)
507
508 if got := s.ScrollbackLen(); got != before {
509 t.Errorf("ScrollbackLen() = %d, want the %d lines from before", got, before)
510 }
511}
512
513func TestResizeKeepsWhatFits(t *testing.T) {
514 s := NewScreen(10, 3)
515 for row, text := range []string{"one", "two", "three"} {
516 s.MoveTo(row, 0)
517 writeString(s, text)
518 }
519
520 s.Resize(20, 5)
521
522 if width, height := s.Size(); width != 20 || height != 5 {
523 t.Fatalf("Size() = %dx%d, want 20x5", width, height)
524 }
525 wantLines(t, s, "one", "two", "three", "", "")
526}
527
528func TestShrinkingKeepsTheNewestLinesAndRemembersTheRest(t *testing.T) {
529 s := NewScreen(10, 4)
530 for row, text := range []string{"one", "two", "three", "four"} {
531 s.MoveTo(row, 0)
532 writeString(s, text)
533 }
534
535 s.Resize(10, 2)
536
537 wantLines(t, s, "three", "four")
538 if got := s.ScrollbackLen(); got != 2 {
539 t.Fatalf("ScrollbackLen() = %d, want the two lost lines kept", got)
540 }
541 if got := lineText(s.ScrollbackLine(0)); got != "one" {
542 t.Errorf("the oldest remembered line is %q, want %q", got, "one")
543 }
544}
545
546func TestShrinkingAMostlyEmptyScreenKeepsWhatIsOnIt(t *testing.T) {
547 // A command that prints one line and exits leaves the cursor near the top
548 // and the rest of the screen blank. Dropping rows from the top to make
549 // room then throws the output away while keeping blank rows below it —
550 // which is how "echo TADA" in a tool window showed nothing at all.
551 s := NewScreen(20, 24)
552 writeString(s, "TADA")
553 s.LineFeed()
554 s.CarriageReturn()
555
556 s.Resize(20, 20)
557
558 if got := lineText(s.Line(0)); got != "TADA" {
559 t.Errorf("the first row is %q, want the output still on screen", got)
560 }
561 if got := s.ScrollbackLen(); got != 0 {
562 t.Errorf("ScrollbackLen() = %d; nothing had scrolled off, so nothing should be remembered", got)
563 }
564}
565
566func TestShrinkingDropsOnlyAsManyRowsAsTheCursorNeeds(t *testing.T) {
567 // Halfway between the two cases: content down to row 5, cursor on row 5,
568 // shrunk to 4 rows. Two rows have to go for the cursor to fit, and only
569 // two should.
570 s := NewScreen(10, 10)
571 for row, text := range []string{"a", "b", "c", "d", "e", "f"} {
572 s.MoveTo(row, 0)
573 writeString(s, text)
574 }
575
576 s.Resize(10, 4)
577
578 wantLines(t, s, "c", "d", "e", "f")
579 if got := s.ScrollbackLen(); got != 2 {
580 t.Errorf("ScrollbackLen() = %d, want the two rows above the kept ones", got)
581 }
582}
583
584func TestNarrowingCutsColumnsRatherThanReflowing(t *testing.T) {
585 // No terminal reflows on resize, and pretending to would move text the
586 // program believes it has already placed.
587 s := NewScreen(10, 2)
588 writeString(s, "abcdefghij")
589
590 s.Resize(4, 2)
591
592 wantLines(t, s, "abcd")
593}
594
595func TestResizingToTheSameSizeChangesNothing(t *testing.T) {
596 s := NewScreen(10, 3)
597 writeString(s, "kept")
598 s.MoveTo(2, 5)
599
600 s.Resize(10, 3)
601
602 wantLines(t, s, "kept")
603 if got := s.Cursor(); got != (Cursor{Row: 2, Col: 5}) {
604 t.Errorf("Cursor() = %+v, want it untouched", got)
605 }
606}
607
608func TestResizingWhileOnTheAlternateScreenKeepsThePrimaryUsable(t *testing.T) {
609 s := NewScreen(10, 3)
610 writeString(s, "primary")
611 s.UseAlternate(true)
612
613 s.Resize(20, 6)
614 s.UseAlternate(false)
615
616 if width, height := s.Size(); width != 20 || height != 6 {
617 t.Fatalf("Size() = %dx%d, want 20x6", width, height)
618 }
619 wantLines(t, s, "primary")
620 if got := len(s.Line(0)); got != 20 {
621 t.Errorf("the restored line is %d cells wide, want 20", got)
622 }
623}
624
625func TestResizeClampsTheCursor(t *testing.T) {
626 s := NewScreen(10, 5)
627 s.MoveTo(4, 9)
628
629 s.Resize(4, 2)
630
631 got := s.Cursor()
632 if got.Row > 1 || got.Col > 3 {
633 t.Errorf("Cursor() = %+v, want it inside a 4x2 screen", got)
634 }
635}
636
637func TestResetPutsEverythingBack(t *testing.T) {
638 s := NewScreen(10, 3)
639 writeString(s, "text")
640 s.SetStyle(tcell.StyleDefault.Bold(true))
641 s.SetScrollRegion(1, 2)
642 s.SetCursorVisible(false)
643 s.UseAlternate(true)
644
645 s.Reset()
646
647 wantLines(t, s, "", "", "")
648 if s.Alternate() {
649 t.Error("Alternate() = true after a reset")
650 }
651 if !s.CursorVisible() {
652 t.Error("CursorVisible() = false after a reset")
653 }
654 if got := s.Cursor(); got != (Cursor{}) {
655 t.Errorf("Cursor() = %+v, want the top left", got)
656 }
657 if _, _, attrs := s.Style().Decompose(); attrs&tcell.AttrBold != 0 {
658 t.Error("the style survived a reset")
659 }
660}
661
662func TestCellAtAndLineOutsideTheScreen(t *testing.T) {
663 s := NewScreen(4, 2)
664
665 if got := s.CellAt(99, 0).Rune; got != ' ' {
666 t.Errorf("CellAt off the screen gave %q, want a space", got)
667 }
668 if s.Line(-1) != nil || s.Line(99) != nil {
669 t.Error("Line off the screen returned something")
670 }
671 if got := s.LineText(99); got != "" {
672 t.Errorf("LineText off the screen gave %q", got)
673 }
674}
675
676func TestErasingKeepsTheCurrentBackground(t *testing.T) {
677 // A program painting a coloured panel and then clearing a line inside it
678 // expects the panel's colour to stay, not a hole.
679 s := NewScreen(5, 2)
680 panel := tcell.StyleDefault.Background(tcell.ColorNavy)
681 s.SetStyle(panel)
682
683 s.EraseLine(EraseAll)
684
685 if _, background, _ := s.CellAt(0, 0).Style.Decompose(); background != tcell.ColorNavy {
686 t.Errorf("the erased cell has background %v, want navy", background)
687 }
688}
689
690// lineText renders a line as a string with its trailing blanks removed.
691func lineText(line Line) string {
692 var out strings.Builder
693 for _, cell := range line {
694 out.WriteRune(cell.Rune)
695 }
696 return strings.TrimRight(out.String(), " ")
697}