v0.4.1 03674e0

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		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}

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