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}