1package vt
2
3import (
4 "testing"
5
6 "github.com/bnema/vev/pkg/renderer"
7)
8
9func evictedTexts(rows [][]renderer.Cell) []string {
10 out := make([]string, len(rows))
11 for i, row := range rows {
12 runes := make([]rune, len(row))
13 for x, c := range row {
14 runes[x] = c.Rune
15 }
16 out[i] = string(runes)
17 }
18 return out
19}
20
21func TestOnLineEvicted(t *testing.T) {
22 tests := []struct {
23 name string
24 write []byte
25 wantRows []string
26 wantFrame []string
27 }{
28 {
29 name: "newline scroll evicts top row",
30 write: []byte("AAAA\r\nBBBB\r\nCCCC\r\n"),
31 wantRows: []string{"AAAA"},
32 wantFrame: []string{"BBBB", "CCCC", " "},
33 },
34 {
35 name: "CSI S count evicts each top row in order",
36 write: []byte("AAAA\r\nBBBB\r\nCCCC\x1b[2S"),
37 wantRows: []string{"AAAA", "BBBB"},
38 wantFrame: []string{"CCCC", " ", " "},
39 },
40 }
41
42 for _, tt := range tests {
43 t.Run(tt.name, func(t *testing.T) {
44 s := NewScreen(4, 3)
45 var evicted [][]renderer.Cell
46 s.OnLineEvicted = func(row []renderer.Cell) {
47 evicted = append(evicted, append([]renderer.Cell(nil), row...))
48 }
49
50 s.Write(tt.write)
51
52 if got := evictedTexts(evicted); !equalStrings(got, tt.wantRows) {
53 t.Fatalf("evicted rows = %#v, want %#v", got, tt.wantRows)
54 }
55 for y, want := range tt.wantFrame {
56 if got := lineText(s, y); got != want {
57 t.Fatalf("line %d = %q, want %q", y, got, want)
58 }
59 }
60 })
61 }
62}
63
64func TestOnLineEvictedAltScreenAndRotation(t *testing.T) {
65 tests := []struct {
66 name string
67 write []byte
68 wantRows []string
69 }{
70 {
71 name: "alternate screen scroll does not evict",
72 write: []byte("main\x1b[?1049hAAAA\r\nBBBB\r\nCCCC\r\nDDDD\x1b[?1049lEEEE\r\n"),
73 wantRows: nil,
74 },
75 {
76 name: "evicted row follows logical top after prior lineOffset rotation",
77 write: []byte("AAAA\r\nBBBB\r\nCCCC\r\nDDDD\r\n"),
78 wantRows: []string{"AAAA", "BBBB"},
79 },
80 }
81
82 for _, tt := range tests {
83 t.Run(tt.name, func(t *testing.T) {
84 s := NewScreen(4, 3)
85 var evicted [][]renderer.Cell
86 s.OnLineEvicted = func(row []renderer.Cell) {
87 evicted = append(evicted, append([]renderer.Cell(nil), row...))
88 }
89
90 s.Write(tt.write)
91
92 if got := evictedTexts(evicted); !equalStrings(got, tt.wantRows) {
93 t.Fatalf("evicted rows = %#v, want %#v", got, tt.wantRows)
94 }
95 if err := s.Frame.CheckInvariants(); err != nil {
96 t.Fatalf("frame invariants after scrollback callback: %v", err)
97 }
98 })
99 }
100}
101
102func TestEvictedLinesCarryTheirBound(t *testing.T) {
103 tests := []struct {
104 name string
105 evict func(s *Screen)
106 wantSoft bool
107 }{
108 {
109 name: "scroll evicts a soft-wrapped row",
110 evict: func(s *Screen) {
111 // Filling all four columns only leaves deferred wrap pending. The
112 // fifth printable character triggers the soft wrap before scrolling.
113 s.Write([]byte("abcde"))
114 s.Write([]byte("\r\n\r\n\r\n\r\n"))
115 },
116 wantSoft: true,
117 },
118 {
119 name: "scroll evicts a hard row",
120 evict: func(s *Screen) {
121 s.Write([]byte("ab\r\n\r\n\r\n\r\n"))
122 },
123 wantSoft: false,
124 },
125 }
126
127 for _, tc := range tests {
128 t.Run(tc.name, func(t *testing.T) {
129 s := NewScreenWithHistory(4, 2, HistoryConfig{MaxRows: 16, MaxCells: 1024})
130 tc.evict(s)
131 view := s.History().SealAndView()
132 if view.Len() == 0 {
133 t.Fatal("nothing was evicted to history")
134 }
135 if got := view.Bound(0).Soft; got != tc.wantSoft {
136 t.Errorf("Bound(0).Soft = %v, want %v", got, tc.wantSoft)
137 }
138 })
139 }
140}
141
142func TestReflowEvictedLinesCarryTheirBound(t *testing.T) {
143 s := NewScreenWithHistory(8, 3, HistoryConfig{MaxRows: 16, MaxCells: 1024})
144 // Fill the grid with wrapped content, then shrink so rows are evicted by
145 // the reflow path in buffer.resize rather than by scrolling.
146 s.Write([]byte("aaaaaaaabbbbbbbbcccccccc"))
147 s.Resize(4, 2)
148
149 view := s.History().SealAndView()
150 if view.Len() == 0 {
151 t.Fatal("reflow evicted nothing to history")
152 }
153 for i := range view.Len() {
154 bound := view.Bound(i)
155 if bound.End == 0 {
156 t.Errorf("Bound(%d).End = 0, want the reflowed row extent", i)
157 }
158 }
159}
160
161func equalStrings(a, b []string) bool {
162 if len(a) != len(b) {
163 return false
164 }
165 for i := range a {
166 if a[i] != b[i] {
167 return false
168 }
169 }
170 return true
171}