v0.5.0 31affde

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

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