v0.7.1 ff50ee4

screen_snapshot_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 13

↑↓ moveenter openesc close