v0.4.1 03674e0

screen_damage_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 TestScrollDamage(t *testing.T) {
11	tests := []struct {
12		name string
13		run  func(t *testing.T)
14	}{
15		{
16			name: "scroll produces both DamageScrollUp and DamageText",
17			run: func(t *testing.T) {
18				s := NewScreen(5, 2)
19				// Fill screen then cause a scroll.
20				s.Write([]byte("AAAAABBBBB")) // both rows filled
21				s.ClearDamage()
22
23				s.Write([]byte("CCCCC")) // forces a scroll up
24
25				d := s.Damage()
26				if len(d) == 0 {
27					t.Fatal("expected damage after scroll")
28				}
29
30				if !hasDamageKind(d, renderer.DamageScrollUp) {
31					t.Errorf("expected DamageScrollUp in %v", damageKinds(d))
32				}
33				if !hasDamageKind(d, renderer.DamageText) {
34					t.Errorf("expected DamageText in %v", damageKinds(d))
35				}
36			},
37		},
38		{
39			name: "scroll damage coordinates match the full-width scrolled region",
40			run: func(t *testing.T) {
41				s := NewScreen(5, 3)
42				s.Write([]byte("AAAAABBBBBCCCCC")) // fill all 3 rows
43				s.ClearDamage()
44
45				// Write another char to force scroll.
46				s.Write([]byte("D"))
47
48				d := s.Damage()
49				var scrollDamage *renderer.Damage
50				for i, dd := range d {
51					if dd.Kind == renderer.DamageScrollUp {
52						scrollDamage = &d[i]
53						break
54					}
55				}
56				if scrollDamage == nil {
57					t.Fatal("expected scroll damage")
58				}
59				if scrollDamage.X != 0 || scrollDamage.Y != 0 {
60					t.Errorf("scroll position: (%d,%d), want (0,0)", scrollDamage.X, scrollDamage.Y)
61				}
62				if scrollDamage.Width != 5 || scrollDamage.Height != 3 {
63					t.Errorf("scroll size: %dx%d, want 5x3", scrollDamage.Width, scrollDamage.Height)
64				}
65				if scrollDamage.Count != 1 {
66					t.Errorf("scroll count = %d, want 1", scrollDamage.Count)
67				}
68			},
69		},
70	}
71
72	for _, tt := range tests {
73		t.Run(tt.name, tt.run)
74	}
75}
76
77func TestDamageCoalescingBehavior(t *testing.T) {
78	tests := []struct {
79		name string
80		run  func(t *testing.T)
81	}{
82		{
83			name: "adjacent single-cell writes coalesce into one damage entry",
84			run: func(t *testing.T) {
85				s := NewScreen(20, 3)
86				s.Write([]byte("ABC")) // Three adjacent chars on one line
87
88				d := s.Damage()
89				// Should be coalesced into a single DamageText.
90				if len(d) != 1 {
91					t.Fatalf("expected 1 coalesced damage, got %d: %+v", len(d), d)
92				}
93				if d[0].Kind != renderer.DamageText || d[0].X != 0 || d[0].Y != 0 || d[0].Width != 3 || d[0].Height != 1 {
94					t.Errorf("unexpected coalesced damage: %+v", d[0])
95				}
96			},
97		},
98		{
99			name: "newline breaks coalescing across lines",
100			run: func(t *testing.T) {
101				s := NewScreen(10, 3)
102				s.Write([]byte("AB\nCD"))
103
104				d := s.Damage()
105				// "AB" on line 0, "CD" on line 1 → two separate DamageText items.
106				textCount := 0
107				for _, dd := range d {
108					if dd.Kind == renderer.DamageText {
109						textCount++
110					}
111				}
112				if textCount < 2 {
113					t.Errorf("expected at least 2 DamageText items (one per line), got %d", textCount)
114				}
115			},
116		},
117		{
118			name: "writing replaces the initial FullRedraw with text damage",
119			run: func(t *testing.T) {
120				s := NewScreen(5, 3)
121				// NewScreen already has FullRedraw in damage.
122				d := s.Damage()
123				if len(d) != 1 || d[0].Kind != renderer.DamageFullRedraw {
124					t.Fatalf("expected single FullRedraw, got %+v", d)
125				}
126
127				// Writing should replace it with text damage.
128				s.Write([]byte("X"))
129				d = s.Damage()
130				if len(d) == 0 {
131					t.Fatal("expected damage after write")
132				}
133				if d[0].Kind == renderer.DamageFullRedraw {
134					t.Error("FullRedraw should have been replaced by text damage")
135				}
136			},
137		},
138	}
139
140	for _, tt := range tests {
141		t.Run(tt.name, tt.run)
142	}
143}
144
145func TestDamageSaturationBoundsPendingRecords(t *testing.T) {
146	s := NewScreen(2, 2)
147	s.ClearDamage()
148
149	for i := range 1_100 {
150		s.record(renderer.Damage{Kind: renderer.DamageText, X: 0, Y: i % 2, Width: 1, Height: 1, Count: 1})
151	}
152	require.Equal(t, []renderer.Damage{renderer.FullRedraw()}, s.Damage())
153
154	// Saturation is sticky until the render owner acknowledges or clears it.
155	s.record(renderer.Damage{Kind: renderer.DamageText, X: 0, Y: 0, Width: 1, Height: 1, Count: 1})
156	require.Equal(t, []renderer.Damage{renderer.FullRedraw()}, s.Damage())
157
158	s.ClearDamage()
159	s.record(renderer.Damage{Kind: renderer.DamageText, X: 0, Y: 0, Width: 1, Height: 1, Count: 1})
160	require.Equal(t, []renderer.Damage{{Kind: renderer.DamageText, X: 0, Y: 0, Width: 1, Height: 1, Count: 1}}, s.Damage())
161}
162
163func TestClearDamage(t *testing.T) {
164	tests := []struct {
165		name string
166	}{
167		{name: "ClearDamage empties the damage list"},
168	}
169
170	for _, tt := range tests {
171		t.Run(tt.name, func(t *testing.T) {
172			s := NewScreen(5, 3)
173			s.Write([]byte("Hello"))
174			s.ClearDamage()
175			d := s.Damage()
176			if len(d) != 0 {
177				t.Fatalf("expected empty damage after ClearDamage, got %+v", d)
178			}
179		})
180	}
181}
182
183func TestDamageReturnsReference(t *testing.T) {
184	tests := []struct {
185		name string
186	}{
187		{name: "Damage() returns a reference to the internal slice"},
188	}
189
190	for _, tt := range tests {
191		t.Run(tt.name, func(t *testing.T) {
192			// Damage() should return a reference to internal slice (no copy).
193			s := NewScreen(10, 3)
194			s.Write([]byte("X"))
195			d1 := s.Damage()
196			// Internal slice should be the same (or at least reference the same backing).
197			if len(d1) == 0 {
198				t.Fatal("expected damage")
199			}
200			// Write more to trigger record.
201			s.Write([]byte("Y"))
202			d2 := s.Damage()
203			_ = d2
204			// Both d1 and d2 reference the internal slice; we just don't crash.
205		})
206	}
207}
208
209func TestDamageCaptureAcknowledgementPreservesConcurrentWrite(t *testing.T) {
210	s := NewScreen(8, 2)
211	s.ClearDamage()
212	s.Write([]byte("a"))
213
214	captured := s.CaptureDamage()
215	require.NotEmpty(t, captured.Damage)
216	s.Write([]byte("b"))
217
218	require.False(t, s.AcknowledgeDamage(captured.Generation))
219	require.Equal(t, []renderer.Damage{renderer.FullRedraw()}, s.Damage(), "stale acknowledgement must retain later writes conservatively")
220}
221
222func TestDamageCaptureAcknowledgementClearsMatchingGeneration(t *testing.T) {
223	s := NewScreen(8, 2)
224	s.ClearDamage()
225	s.Write([]byte("a"))
226
227	captured := s.CaptureDamage()
228	require.True(t, s.AcknowledgeDamage(captured.Generation))
229	require.Empty(t, s.Damage())
230}
231
232func TestDamageCaptureOwnsDamageSlice(t *testing.T) {
233	s := NewScreen(8, 2)
234	s.ClearDamage()
235	s.Write([]byte("a"))
236
237	captured := s.CaptureDamage()
238	s.ClearDamage()
239	require.NotEmpty(t, captured.Damage, "captured damage must not alias mutable screen storage")
240}

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