detached ff327ba

test(vt): prove screen snapshot ownership

commit
ff327ba2ff9986920b01694ec1f6073f56dd7bf0
author
brice · · 2026-07-28 14:10 UTC
committer
brice · · 2026-07-29 05:00 UTC
parent
0461ab8
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +133 -0
M pkg/vt/screen_snapshot_test.go +133 -0
@@ -3,6 +3,7 @@ package vt
33 import (
44 "testing"
55
6+ "github.com/bnema/vev/pkg/renderer"
67 "github.com/stretchr/testify/require"
78 )
89
@@ -167,3 +168,135 @@ func TestScreenSnapshotTitle(t *testing.T) {
167168 })
168169 }
169170 }
171+
172+func 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{LineBound{End: 3, Soft: true}, LineBound{}}
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+
228+func TestScreenSnapshotOwnership(t *testing.T) {
229+ tests := []struct {
230+ name string
231+ mutate func(*Screen)
232+ }{
233+ {name: "later write", mutate: func(screen *Screen) { screen.Write([]byte("later")) }},
234+ {name: "later scroll", mutate: func(screen *Screen) { screen.Write([]byte("1111222233334444")) }},
235+ {name: "later resize", mutate: func(screen *Screen) { screen.Resize(2, 4) }},
236+ {name: "later alternate entry", mutate: func(screen *Screen) { screen.Write([]byte("\x1b[?1049halt")) }},
237+ {name: "later alternate entry and exit", mutate: func(screen *Screen) { screen.Write([]byte("\x1b[?1049halt\x1b[?1049l")) }},
238+ }
239+
240+ for _, test := range tests {
241+ t.Run(test.name, func(t *testing.T) {
242+ screen := NewScreen(4, 2)
243+ screen.Write([]byte("abcdEF"))
244+ screen.buffer.boundaries[0] = LineBound{End: 4, Soft: true}
245+ snapshot := screen.Snapshot()
246+ wantRows := [][]renderer.Cell{snapshot.Row(0), snapshot.Row(1)}
247+ wantBounds := []LineBound{snapshot.Bound(0), snapshot.Bound(1)}
248+
249+ test.mutate(screen)
250+
251+ require.Equal(t, wantRows[0], snapshot.Row(0))
252+ require.Equal(t, wantRows[1], snapshot.Row(1))
253+ require.Equal(t, wantBounds[0], snapshot.Bound(0))
254+ require.Equal(t, wantBounds[1], snapshot.Bound(1))
255+
256+ liveBeforeCallerMutation := append([]renderer.Cell(nil), screen.Frame.Row(0)...)
257+ secondSnapshot := screen.Snapshot()
258+ secondSnapshotBefore := secondSnapshot.Row(0)
259+
260+ callerCopy := snapshot.Row(0)
261+ callerCopy[0] = renderer.Cell{Rune: 'z'}
262+
263+ require.Equal(t, wantRows[0], snapshot.BorrowedRow(0))
264+ require.Equal(t, liveBeforeCallerMutation, screen.Frame.Row(0), "mutating Row copy must not change live Screen")
265+ require.Equal(t, secondSnapshotBefore, secondSnapshot.Row(0), "mutating Row copy must not change another snapshot")
266+ })
267+ }
268+}
269+
270+func TestScreenSnapshotBoundsAreIndependent(t *testing.T) {
271+ tests := []struct {
272+ name string
273+ mutate func(*Screen)
274+ }{
275+ {
276+ name: "mutating a single boundary entry after the snapshot",
277+ mutate: func(screen *Screen) {
278+ screen.buffer.boundaries[0] = LineBound{End: 1, Soft: false}
279+ },
280+ },
281+ {
282+ name: "replacing the whole boundaries slice after the snapshot",
283+ mutate: func(screen *Screen) {
284+ screen.buffer.boundaries = []LineBound{{End: 1, Soft: false}, {End: 1, Soft: false}}
285+ },
286+ },
287+ }
288+
289+ for _, test := range tests {
290+ t.Run(test.name, func(t *testing.T) {
291+ screen := NewScreen(4, 2)
292+ screen.Write([]byte("abcdEF"))
293+ screen.buffer.boundaries[0] = LineBound{End: 4, Soft: true}
294+ snapshot := screen.Snapshot()
295+ want := snapshot.Bound(0)
296+
297+ test.mutate(screen)
298+
299+ require.Equal(t, want, snapshot.Bound(0), "snapshot bounds must not alias Screen.buffer.boundaries")
300+ })
301+ }
302+}
@@ -3,6 +3,7 @@ package vt
3 import (3 import (
4 "testing"4 "testing"
5 5
6+ "github.com/bnema/vev/pkg/renderer"
6 "github.com/stretchr/testify/require"7 "github.com/stretchr/testify/require"
7 )8 )
8 9
@@ -167,3 +168,135 @@ func TestScreenSnapshotTitle(t *testing.T) {
167 })168 })
168 }169 }
169 }170 }
171+
172+func 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{LineBound{End: 3, Soft: true}, LineBound{}}
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+
228+func TestScreenSnapshotOwnership(t *testing.T) {
229+ tests := []struct {
230+ name string
231+ mutate func(*Screen)
232+ }{
233+ {name: "later write", mutate: func(screen *Screen) { screen.Write([]byte("later")) }},
234+ {name: "later scroll", mutate: func(screen *Screen) { screen.Write([]byte("1111222233334444")) }},
235+ {name: "later resize", mutate: func(screen *Screen) { screen.Resize(2, 4) }},
236+ {name: "later alternate entry", mutate: func(screen *Screen) { screen.Write([]byte("\x1b[?1049halt")) }},
237+ {name: "later alternate entry and exit", mutate: func(screen *Screen) { screen.Write([]byte("\x1b[?1049halt\x1b[?1049l")) }},
238+ }
239+
240+ for _, test := range tests {
241+ t.Run(test.name, func(t *testing.T) {
242+ screen := NewScreen(4, 2)
243+ screen.Write([]byte("abcdEF"))
244+ screen.buffer.boundaries[0] = LineBound{End: 4, Soft: true}
245+ snapshot := screen.Snapshot()
246+ wantRows := [][]renderer.Cell{snapshot.Row(0), snapshot.Row(1)}
247+ wantBounds := []LineBound{snapshot.Bound(0), snapshot.Bound(1)}
248+
249+ test.mutate(screen)
250+
251+ require.Equal(t, wantRows[0], snapshot.Row(0))
252+ require.Equal(t, wantRows[1], snapshot.Row(1))
253+ require.Equal(t, wantBounds[0], snapshot.Bound(0))
254+ require.Equal(t, wantBounds[1], snapshot.Bound(1))
255+
256+ liveBeforeCallerMutation := append([]renderer.Cell(nil), screen.Frame.Row(0)...)
257+ secondSnapshot := screen.Snapshot()
258+ secondSnapshotBefore := secondSnapshot.Row(0)
259+
260+ callerCopy := snapshot.Row(0)
261+ callerCopy[0] = renderer.Cell{Rune: 'z'}
262+
263+ require.Equal(t, wantRows[0], snapshot.BorrowedRow(0))
264+ require.Equal(t, liveBeforeCallerMutation, screen.Frame.Row(0), "mutating Row copy must not change live Screen")
265+ require.Equal(t, secondSnapshotBefore, secondSnapshot.Row(0), "mutating Row copy must not change another snapshot")
266+ })
267+ }
268+}
269+
270+func TestScreenSnapshotBoundsAreIndependent(t *testing.T) {
271+ tests := []struct {
272+ name string
273+ mutate func(*Screen)
274+ }{
275+ {
276+ name: "mutating a single boundary entry after the snapshot",
277+ mutate: func(screen *Screen) {
278+ screen.buffer.boundaries[0] = LineBound{End: 1, Soft: false}
279+ },
280+ },
281+ {
282+ name: "replacing the whole boundaries slice after the snapshot",
283+ mutate: func(screen *Screen) {
284+ screen.buffer.boundaries = []LineBound{{End: 1, Soft: false}, {End: 1, Soft: false}}
285+ },
286+ },
287+ }
288+
289+ for _, test := range tests {
290+ t.Run(test.name, func(t *testing.T) {
291+ screen := NewScreen(4, 2)
292+ screen.Write([]byte("abcdEF"))
293+ screen.buffer.boundaries[0] = LineBound{End: 4, Soft: true}
294+ snapshot := screen.Snapshot()
295+ want := snapshot.Bound(0)
296+
297+ test.mutate(screen)
298+
299+ require.Equal(t, want, snapshot.Bound(0), "snapshot bounds must not alias Screen.buffer.boundaries")
300+ })
301+ }
302+}

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 12

Tags 11

↑↓ moveenter openesc close