1package vt
2
3import (
4 "testing"
5
6 renderer "github.com/bnema/vev-vt/core"
7 "github.com/stretchr/testify/require"
8)
9
10func TestScreenSnapshotGeometryAndMetadata(t *testing.T) {
11 tests := []struct {
12 name string
13 screen func() *Screen
14 wantColumns int
15 wantRows int
16 wantTitle string
17 wantCursor CursorSnapshot
18 wantModes ModeSnapshot
19 checkRows func(t *testing.T, snapshot ScreenSnapshot)
20 }{
21 {
22 name: "nil receiver is the zero snapshot",
23 screen: func() *Screen { return nil },
24 wantCursor: CursorSnapshot{},
25 wantModes: ModeSnapshot{},
26 checkRows: func(t *testing.T, snapshot ScreenSnapshot) {
27 require.Nil(t, snapshot.Row(0))
28 require.Equal(t, LineBound{}, snapshot.Bound(0))
29 },
30 },
31 {
32 name: "normal viewport captures cursor modes and title",
33 screen: func() *Screen {
34 screen := NewScreen(6, 3)
35 screen.Write([]byte("\x1b]2;editor\x07\x1b[2;3H\x1b[?25l\x1b[2 q\x1b[?2004h\x1b[?2026h\x1b[?2031h\x1b[?1002h\x1b[?1006h"))
36 return screen
37 },
38 wantColumns: 6,
39 wantRows: 3,
40 wantTitle: "editor",
41 wantCursor: CursorSnapshot{Row: 1, Col: 2, Visible: false, Style: 2, StyleSet: true},
42 wantModes: ModeSnapshot{
43 BracketedPaste: true,
44 SynchronizedUpdate: true,
45 ColorSchemeMode: true,
46 MouseTracking: 1002,
47 MouseSGR: true,
48 },
49 checkRows: func(t *testing.T, snapshot ScreenSnapshot) {
50 require.Len(t, snapshot.Row(0), 6)
51 require.Equal(t, LineBound{}, snapshot.Bound(0))
52 },
53 },
54 {
55 name: "zero by zero preserves screen metadata",
56 screen: func() *Screen {
57 screen := NewScreen(2, 2)
58 screen.Write([]byte("\x1b]2;collapsed\x07\x1b[?2004h"))
59 screen.Resize(0, 0)
60 return screen
61 },
62 wantTitle: "collapsed",
63 wantCursor: CursorSnapshot{Visible: true},
64 wantModes: ModeSnapshot{BracketedPaste: true},
65 checkRows: func(t *testing.T, snapshot ScreenSnapshot) {
66 require.Nil(t, snapshot.Row(0))
67 require.Equal(t, LineBound{}, snapshot.Bound(0))
68 },
69 },
70 {
71 name: "zero columns retains empty physical rows and bounds",
72 screen: func() *Screen {
73 screen := NewScreen(2, 2)
74 screen.Write([]byte("\x1b]2;zero-columns\x07"))
75 screen.Resize(0, 2)
76 return screen
77 },
78 wantRows: 2,
79 wantTitle: "zero-columns",
80 wantCursor: CursorSnapshot{Visible: true},
81 checkRows: func(t *testing.T, snapshot ScreenSnapshot) {
82 require.NotNil(t, snapshot.Row(0))
83 require.Empty(t, snapshot.Row(0))
84 require.Empty(t, snapshot.Row(1))
85 require.Equal(t, LineBound{}, snapshot.Bound(1))
86 require.Nil(t, snapshot.Row(2))
87 },
88 },
89 {
90 name: "zero rows retains columns without rows",
91 screen: func() *Screen {
92 screen := NewScreen(2, 2)
93 screen.Write([]byte("\x1b]2;zero-rows\x07"))
94 screen.Resize(2, 0)
95 return screen
96 },
97 wantColumns: 2,
98 wantTitle: "zero-rows",
99 wantCursor: CursorSnapshot{Visible: true},
100 checkRows: func(t *testing.T, snapshot ScreenSnapshot) {
101 require.Nil(t, snapshot.Row(0))
102 require.Equal(t, LineBound{}, snapshot.Bound(0))
103 },
104 },
105 }
106
107 for _, test := range tests {
108 t.Run(test.name, func(t *testing.T) {
109 snapshot := test.screen().Snapshot()
110 require.Equal(t, test.wantColumns, snapshot.Columns())
111 require.Equal(t, test.wantRows, snapshot.Rows())
112 require.Equal(t, test.wantTitle, snapshot.Title())
113 require.Equal(t, test.wantCursor, snapshot.Cursor())
114 require.Equal(t, test.wantModes, snapshot.Modes())
115 test.checkRows(t, snapshot)
116 })
117 }
118}
119
120func TestScreenSnapshotColorSchemeModeExcludesHostPreference(t *testing.T) {
121 tests := []struct {
122 name string
123 run func(*Screen)
124 want bool
125 }{
126 {
127 name: "host preference alone is not DEC 2031 mode",
128 run: func(screen *Screen) { screen.SetColorScheme(true) },
129 want: false,
130 },
131 {
132 name: "DEC 2031 mode is captured independently",
133 run: func(screen *Screen) {
134 screen.SetColorScheme(false)
135 screen.Write([]byte("\x1b[?2031h"))
136 },
137 want: true,
138 },
139 }
140
141 for _, test := range tests {
142 t.Run(test.name, func(t *testing.T) {
143 screen := NewScreen(4, 2)
144 test.run(screen)
145 require.Equal(t, test.want, screen.Snapshot().Modes().ColorSchemeMode)
146 })
147 }
148}
149
150func TestScreenSnapshotTitle(t *testing.T) {
151 tests := []struct {
152 name string
153 feed string
154 want string
155 }{
156 {name: "empty title", want: ""},
157 {name: "latest title replaces the previous value", feed: "\x1b]2;first\x07\x1b]0;second\x07", want: "second"},
158 {name: "UTF-8 title is preserved", feed: "\x1b]2;éditeur 界\x07", want: "éditeur 界"},
159 }
160
161 for _, test := range tests {
162 t.Run(test.name, func(t *testing.T) {
163 screen := NewScreen(8, 2)
164 screen.Write([]byte(test.feed))
165 require.Equal(t, test.want, screen.Snapshot().Title())
166 })
167 }
168}
169
170func TestScreenSnapshotFidelity(t *testing.T) {
171 tests := []struct {
172 name string
173 make func() (*Screen, []renderer.Cell, []LineBound)
174 wantModes ModeSnapshot
175 }{
176 {
177 name: "styles wide continuation and bounds are exact",
178 make: func() (*Screen, []renderer.Cell, []LineBound) {
179 screen := NewScreen(4, 2)
180 style := renderer.Style{
181 Bold: true, Italic: true, Inverse: true,
182 Attrs: renderer.AttrDim | renderer.AttrUnderline | renderer.AttrBlink | renderer.AttrStrikethrough,
183 HasForegroundRGB: true, ForegroundRGB: renderer.RGB{R: 1, G: 2, B: 3},
184 Background: 17,
185 UnderlineStyle: renderer.UnderlineCurly,
186 HasUnderlineColorRGB: true, UnderlineColorRGB: renderer.RGB{R: 4, G: 5, B: 6},
187 }
188 cells := []renderer.Cell{
189 {Rune: '界', Style: style},
190 {Continuation: true, Style: style},
191 {Rune: 'x', Style: renderer.DefaultStyle()},
192 renderer.BlankCell(),
193 }
194 screen.frame.WriteRow(0, 0, cells)
195 screen.buffer.boundaries[0] = LineBound{End: 3, Soft: true}
196 return screen, cells, []LineBound{{End: 3, Soft: true}, {}}
197 },
198 },
199 {
200 name: "active alternate screen is captured",
201 make: func() (*Screen, []renderer.Cell, []LineBound) {
202 screen := NewScreen(3, 1)
203 screen.Write([]byte("primary\x1b[?1049halt"))
204 cells := append([]renderer.Cell(nil), screen.frame.Row(0)...)
205 return screen, cells, screen.LineBounds()
206 },
207 wantModes: ModeSnapshot{AlternateScreen: true},
208 },
209 }
210
211 for _, test := range tests {
212 t.Run(test.name, func(t *testing.T) {
213 screen, wantCells, wantBounds := test.make()
214 snapshot := screen.Snapshot()
215 require.Equal(t, screen.frame.Width, snapshot.Columns())
216 require.Equal(t, screen.frame.Height, snapshot.Rows())
217 require.Equal(t, wantCells, snapshot.Row(0))
218 require.Equal(t, test.wantModes, snapshot.Modes())
219 for row, want := range wantBounds {
220 require.Equal(t, want, snapshot.Bound(row))
221 }
222 })
223 }
224}
225
226func TestScreenSnapshotPreservesLogicalRowsAfterScroll(t *testing.T) {
227 screen := NewScreen(3, 2)
228 screen.Write([]byte("ABC"))
229 screen.Write([]byte("DEF"))
230 screen.Write([]byte("G"))
231
232 snapshot := screen.Snapshot()
233 for y, want := range []string{"DEF", "G "} {
234 require.Equal(t, want, rowText(snapshot.Row(y)))
235 require.Equal(t, screen.frame.Row(y), snapshot.Row(y))
236 require.Equal(t, screen.LineBounds()[y], snapshot.Bound(y))
237 }
238}
239
240func TestScreenSnapshotOwnership(t *testing.T) {
241 tests := []struct {
242 name string
243 mutate func(*Screen)
244 }{
245 {name: "later write", mutate: func(screen *Screen) { screen.Write([]byte("later")) }},
246 {name: "later scroll", mutate: func(screen *Screen) { screen.Write([]byte("1111222233334444")) }},
247 {name: "later resize", mutate: func(screen *Screen) { screen.Resize(2, 4) }},
248 {name: "later alternate entry", mutate: func(screen *Screen) { screen.Write([]byte("\x1b[?1049halt")) }},
249 {name: "later alternate entry and exit", mutate: func(screen *Screen) { screen.Write([]byte("\x1b[?1049halt\x1b[?1049l")) }},
250 }
251
252 for _, test := range tests {
253 t.Run(test.name, func(t *testing.T) {
254 screen := NewScreen(4, 2)
255 screen.Write([]byte("abcdEF"))
256 screen.buffer.boundaries[0] = LineBound{End: 4, Soft: true}
257 snapshot := screen.Snapshot()
258 wantRows := [][]renderer.Cell{snapshot.Row(0), snapshot.Row(1)}
259 wantBounds := []LineBound{snapshot.Bound(0), snapshot.Bound(1)}
260
261 test.mutate(screen)
262
263 require.Equal(t, wantRows[0], snapshot.Row(0))
264 require.Equal(t, wantRows[1], snapshot.Row(1))
265 require.Equal(t, wantBounds[0], snapshot.Bound(0))
266 require.Equal(t, wantBounds[1], snapshot.Bound(1))
267
268 liveBeforeCallerMutation := append([]renderer.Cell(nil), screen.frame.Row(0)...)
269 secondSnapshot := screen.Snapshot()
270 secondSnapshotBefore := secondSnapshot.Row(0)
271
272 callerCopy := snapshot.Row(0)
273 callerCopy[0] = renderer.Cell{Rune: 'z'}
274
275 require.Equal(t, wantRows[0], snapshot.Row(0))
276 require.Equal(t, liveBeforeCallerMutation, screen.frame.Row(0), "mutating Row copy must not change live Screen")
277 require.Equal(t, secondSnapshotBefore, secondSnapshot.Row(0), "mutating Row copy must not change another snapshot")
278 })
279 }
280}
281
282func TestScreenSnapshotBoundsAreIndependent(t *testing.T) {
283 tests := []struct {
284 name string
285 mutate func(*Screen)
286 }{
287 {
288 name: "mutating a single boundary entry after the snapshot",
289 mutate: func(screen *Screen) {
290 screen.buffer.boundaries[0] = LineBound{End: 1, Soft: false}
291 },
292 },
293 {
294 name: "replacing the whole boundaries slice after the snapshot",
295 mutate: func(screen *Screen) {
296 screen.buffer.boundaries = []LineBound{{End: 1, Soft: false}, {End: 1, Soft: false}}
297 },
298 },
299 }
300
301 for _, test := range tests {
302 t.Run(test.name, func(t *testing.T) {
303 screen := NewScreen(4, 2)
304 screen.Write([]byte("abcdEF"))
305 screen.buffer.boundaries[0] = LineBound{End: 4, Soft: true}
306 snapshot := screen.Snapshot()
307 want := snapshot.Bound(0)
308
309 test.mutate(screen)
310
311 require.Equal(t, want, snapshot.Bound(0), "snapshot bounds must not alias Screen.buffer.boundaries")
312 })
313 }
314}
315
316func TestScreenSnapshotDoesNotMutateHistoryOrDamage(t *testing.T) {
317 tests := []struct {
318 name string
319 make func() *Screen
320 }{
321 {
322 name: "screen without history",
323 make: func() *Screen {
324 screen := NewScreen(4, 2)
325 screen.ClearDamage()
326 screen.Write([]byte("abcdef"))
327 return screen
328 },
329 },
330 {
331 name: "screen with sealed chunks and mutable tail",
332 make: func() *Screen {
333 screen := NewScreenWithHistory(4, 2, HistoryConfig{MaxRows: 16, MaxBytes: 1 << 20, ChunkRows: 2})
334 screen.Write([]byte("111122223333444455"))
335 return screen
336 },
337 },
338 }
339
340 for _, test := range tests {
341 t.Run(test.name, func(t *testing.T) {
342 screen := test.make()
343 beforeDamage := screen.CaptureDamage()
344 var beforeHistory HistorySnapshotView
345 var beforeRows [][]renderer.Cell
346 var beforeBounds []LineBound
347 beforeCap := 0
348 var beforeByteCap uint64
349 if screen.History() != nil {
350 beforeHistory = screen.History().SnapshotView()
351 beforeView := screen.History().View()
352 beforeRows = make([][]renderer.Cell, beforeView.Len())
353 beforeBounds = make([]LineBound, beforeView.Len())
354 for i := range beforeRows {
355 beforeRows[i] = beforeView.Row(i)
356 beforeBounds[i] = beforeView.Bound(i)
357 }
358 beforeCap = screen.History().Cap()
359 beforeByteCap = screen.History().ByteCap()
360 }
361
362 _ = screen.Snapshot()
363
364 afterDamage := screen.CaptureDamage()
365 require.Equal(t, beforeDamage, afterDamage)
366 if screen.History() != nil {
367 afterHistory := screen.History().SnapshotView()
368 afterView := screen.History().View()
369 afterRows := make([][]renderer.Cell, afterView.Len())
370 afterBounds := make([]LineBound, afterView.Len())
371 for i := range afterRows {
372 afterRows[i] = afterView.Row(i)
373 afterBounds[i] = afterView.Bound(i)
374 }
375 require.Equal(t, beforeHistory.ChunkCount(), afterHistory.ChunkCount())
376 require.Equal(t, beforeHistory.Len(), afterHistory.Len())
377 require.Equal(t, beforeHistory.Cells(), afterHistory.Cells())
378 require.Equal(t, beforeHistory.Tail().Len(), afterHistory.Tail().Len())
379 require.Equal(t, beforeRows, afterRows)
380 require.Equal(t, beforeBounds, afterBounds)
381 require.Equal(t, beforeCap, screen.History().Cap())
382 require.Equal(t, beforeByteCap, screen.History().ByteCap())
383 }
384 })
385 }
386}
387
388func TestScreenSnapshotPreservesStaleDamageAcknowledge(t *testing.T) {
389 tests := []struct {
390 name string
391 mutate func(*Screen)
392 }{
393 {name: "write after capture", mutate: func(screen *Screen) { screen.Write([]byte("b")) }},
394 {name: "resize after capture", mutate: func(screen *Screen) { screen.Resize(6, 2) }},
395 }
396
397 for _, test := range tests {
398 t.Run(test.name, func(t *testing.T) {
399 screen := NewScreen(4, 2)
400 screen.ClearDamage()
401 screen.Write([]byte("a"))
402 captured := screen.CaptureDamage()
403
404 _ = screen.Snapshot()
405 test.mutate(screen)
406
407 require.False(t, screen.AcknowledgeDamage(captured.Generation))
408 require.Equal(t, []renderer.Damage{renderer.FullRedraw()}, screen.Damage())
409
410 screen.record(renderer.Damage{Kind: renderer.DamageText, X: 0, Y: 0, Width: 1, Height: 1, Count: 1})
411 require.Equal(t, []renderer.Damage{renderer.FullRedraw()}, screen.Damage(), "full redraw stays sticky until exact acknowledgement")
412 })
413 }
414}
415
416func TestScreenSnapshotPreservesExactDamageAcknowledge(t *testing.T) {
417 tests := []struct {
418 name string
419 }{
420 {name: "snapshot does not stale an exact damage capture"},
421 }
422
423 for _, test := range tests {
424 t.Run(test.name, func(t *testing.T) {
425 screen := NewScreen(4, 2)
426 screen.ClearDamage()
427 screen.Write([]byte("a"))
428 captured := screen.CaptureDamage()
429
430 _ = screen.Snapshot()
431
432 require.True(t, screen.AcknowledgeDamage(captured.Generation))
433 require.Empty(t, screen.Damage())
434 })
435 }
436}