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