1package vt
2
3import "testing"
4
5func TestScreenRowIDsRotateWithRowsAndHistory(t *testing.T) {
6 s := NewScreenWithHistory(4, 3, HistoryConfig{MaxRows: 8, ChunkRows: 2})
7 initial := s.RowIDs()
8 assertUniqueNonzeroRowIDs(t, initial)
9
10 s.Write([]byte("AAAA\r\nBBBB\r\nCCCC\r\n"))
11
12 live := s.RowIDs()
13 if got, want := live, []RowID{initial[1], initial[2], live[2]}; len(got) != len(want) || got[0] != want[0] || got[1] != want[1] {
14 t.Fatalf("live row IDs = %v, want rows moved from %v with a fresh blank row", got, initial)
15 }
16 if live[2] == initial[0] || live[2] == initial[1] || live[2] == initial[2] {
17 t.Fatalf("new blank row reused a row ID: live=%v initial=%v", live, initial)
18 }
19 assertUniqueNonzeroRowIDs(t, live)
20
21 view := s.History().SealAndView()
22 if got, want := view.RowID(0), initial[0]; got != want {
23 t.Fatalf("evicted row ID = %d, want %d", got, want)
24 }
25 if got := view.FindRowID(initial[0]); got != 0 {
26 t.Fatalf("FindRowID(%d) = %d, want 0", initial[0], got)
27 }
28}
29
30func TestScreenRowIDsDoNotAppendInteriorOrAlternateScrollHistory(t *testing.T) {
31 s := NewScreenWithHistory(4, 4, HistoryConfig{MaxRows: 8})
32 s.Write([]byte("AAAA\r\nBBBB\r\nCCCC\r\nDDDD"))
33 s.Write([]byte("\x1b[2;4r\x1b[4;1H\n"))
34 if got := s.History().Len(); got != 0 {
35 t.Fatalf("interior scroll history rows = %d, want 0", got)
36 }
37 assertUniqueNonzeroRowIDs(t, s.RowIDs())
38 primary := s.RowIDs()
39
40 s.Write([]byte("\x1b[?1049h1111\r\n2222\r\n3333\r\n4444\r\n"))
41 if got := s.History().Len(); got != 0 {
42 t.Fatalf("alternate scroll history rows = %d, want 0", got)
43 }
44 s.Write([]byte("\x1b[?1049l"))
45 for i, id := range primary {
46 if got := s.RowID(i); got != id {
47 t.Fatalf("restored primary row %d ID = %d, want %d", i, got, id)
48 }
49 }
50}
51
52func TestScreenRowIDRefreshesOnSnapExpandedFullClear(t *testing.T) {
53 tests := []struct {
54 name string
55 x0, x1 int
56 wantRefresh bool
57 }{
58 {name: "snap-expanded full clear", x0: 1, x1: 4, wantRefresh: true},
59 {name: "partial clear", x0: 0, x1: 1, wantRefresh: false},
60 }
61
62 for _, tt := range tests {
63 t.Run(tt.name, func(t *testing.T) {
64 s := NewScreen(4, 1)
65 s.Write([]byte("界"))
66 before := s.RowID(0)
67
68 s.clearRow(0, tt.x0, tt.x1)
69
70 if got := s.RowID(0) != before; got != tt.wantRefresh {
71 t.Fatalf("row ID refreshed = %t, want %t", got, tt.wantRefresh)
72 }
73 })
74 }
75}
76
77func TestScreenRowIDsRefreshOnClearResetAndAlternateClone(t *testing.T) {
78 s := NewScreen(4, 3)
79 before := s.RowIDs()
80 s.Write([]byte("AAAA\x1b[2J"))
81 cleared := s.RowIDs()
82 assertFreshRowIDs(t, before, cleared)
83
84 s.Write([]byte("\x1bc"))
85 reset := s.RowIDs()
86 assertFreshRowIDs(t, cleared, reset)
87
88 s.Write([]byte("BBBB\x1b[?1049h"))
89 alternate := s.RowIDs()
90 assertFreshRowIDs(t, reset, alternate)
91 s.Write([]byte("CCCC\x1b[?1049l"))
92 for i, id := range reset {
93 if got := s.RowID(i); got != id {
94 t.Fatalf("cloned primary row %d ID = %d, want %d", i, got, id)
95 }
96 }
97}
98
99func TestResizeReflowCarriesSourceRowIDsIntoHistoryAndViewport(t *testing.T) {
100 b := newBuffer(8, 3)
101 b.frame.WriteRow(0, 0, historyRow("abcdefgh"))
102 b.frame.WriteRow(1, 0, historyRow("ijkl"))
103 b.frame.WriteRow(2, 0, historyRow("mnop"))
104 b.boundaries[0] = LineBound{End: 8, Soft: true}
105 b.boundaries[1] = LineBound{End: 4}
106 b.boundaries[2] = LineBound{End: 4}
107 b.rowIDs = []RowID{11, 12, 13}
108
109 active := &bufferCursor{row: 2, col: 4}
110 evicted, bounds, ids := b.resize(4, 2, active, nil)
111
112 if len(evicted) != 2 || bounds[0].End != 4 || ids[0] != 11 || ids[1] != 0 {
113 t.Fatalf("reflow eviction = rows=%d bound=%v IDs=%v, want two rows with source ID 11 then a generated ID", len(evicted), bounds, ids)
114 }
115 if got, want := b.rowIDs, []RowID{0, 13}; got[1] != want[1] || len(got) != len(want) {
116 t.Fatalf("reflow viewport IDs = %v, want generated continuation then source ID %d", got, want[1])
117 }
118}
119
120func TestHistoryRowIDsRemainStableAcrossViewsAndEviction(t *testing.T) {
121 h := NewHistory(HistoryConfig{MaxRows: 3, ChunkRows: 2})
122 for i, text := range []string{"one", "two"} {
123 if err := h.AppendWithID(historyRow(text), LineBound{End: len(text)}, RowID(i+1)); err != nil {
124 t.Fatal(err)
125 }
126 }
127 before := h.SealAndView()
128 if got, want := before.RowID(0), RowID(1); got != want {
129 t.Fatalf("sealed row ID = %d, want %d", got, want)
130 }
131
132 if err := h.AppendWithID(historyRow("three"), LineBound{End: 5}, RowID(3)); err != nil {
133 t.Fatal(err)
134 }
135 if got := before.FindRowID(2); got != 1 {
136 t.Fatalf("retained view FindRowID(2) = %d, want 1", got)
137 }
138 after := h.SealAndView()
139 if got := after.FindRowID(3); got != 2 {
140 t.Fatalf("new view FindRowID(3) = %d, want 2", got)
141 }
142
143 if err := h.AppendWithID(historyRow("four"), LineBound{End: 4}, RowID(4)); err != nil {
144 t.Fatal(err)
145 }
146 if got := before.FindRowID(1); got != 0 {
147 t.Fatalf("eviction mutated retained view FindRowID(1) = %d, want 0", got)
148 }
149 if got := h.View().FindRowID(1); got != -1 {
150 t.Fatalf("evicted current FindRowID(1) = %d, want -1", got)
151 }
152}
153
154func assertUniqueNonzeroRowIDs(t *testing.T, ids []RowID) {
155 t.Helper()
156 seen := make(map[RowID]struct{}, len(ids))
157 for _, id := range ids {
158 if id == 0 {
159 t.Fatal("row ID is zero")
160 }
161 if _, ok := seen[id]; ok {
162 t.Fatalf("duplicate row ID %d in %v", id, ids)
163 }
164 seen[id] = struct{}{}
165 }
166}
167
168func assertFreshRowIDs(t *testing.T, before, after []RowID) {
169 t.Helper()
170 assertUniqueNonzeroRowIDs(t, after)
171 seen := make(map[RowID]struct{}, len(before))
172 for _, id := range before {
173 seen[id] = struct{}{}
174 }
175 for _, id := range after {
176 if _, ok := seen[id]; ok {
177 t.Fatalf("row ID %d was reused: before=%v after=%v", id, before, after)
178 }
179 }
180}