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