1package ansi_test
2
3import (
4 "bytes"
5 "testing"
6
7 vt "github.com/bnema/vev-vt"
8 "github.com/bnema/vev-vt/ansi"
9)
10
11func TestBidirectionalScrollReplay(t *testing.T) {
12 for _, down := range []bool{false, true} {
13 for _, count := range []int{1, 3} {
14 frame := vt.NewFrame(12, 10)
15 for y := range frame.Height {
16 frame.FillRow(y, 0, frame.Width, vt.Cell{Rune: rune('a' + y), Style: vt.Style{Foreground: y, Background: -1}})
17 }
18 r := ansi.New(ansi.Capabilities{})
19 terminal := vt.NewScreen(frame.Width, frame.Height)
20 initial, err := r.Draw(frame, nil)
21 if err != nil {
22 t.Fatal(err)
23 }
24 terminal.Write(initial)
25 kind, exposed := vt.DamageScrollUp, 8-count
26 if down {
27 kind, exposed = vt.DamageScrollDown, 2
28 frame.ScrollDown(2, 7, count)
29 } else {
30 frame.ScrollUp(2, 7, count)
31 }
32 for y := exposed; y < exposed+count; y++ {
33 frame.FillRow(y, 0, frame.Width, vt.Cell{Rune: 'X', Style: vt.DefaultStyle()})
34 }
35 damage := []vt.Damage{
36 {Kind: kind, Y: 2, Width: 12, Height: 6, Count: count},
37 {Kind: vt.DamageText, Y: exposed, Width: 12, Height: count},
38 }
39 prepared, err := r.Prepare(frame, damage, false)
40 if err != nil {
41 t.Fatal(err)
42 }
43 if !bytes.Contains(prepared.Bytes(), []byte("\x1b[3;8r")) {
44 t.Fatalf("missing scroll region: %q", prepared.Bytes())
45 }
46 terminal.Write(prepared.Bytes())
47 prepared.Commit()
48 for y := range frame.Height {
49 for x := range frame.Width {
50 if !frame.Cell(x, y).Equal(terminal.Cell(x, y)) {
51 t.Fatalf("down=%v count=%d cell %d,%d mismatch", down, count, x, y)
52 }
53 }
54 }
55 if err := frame.CheckInvariants(); err != nil {
56 t.Fatal(err)
57 }
58 unchanged, err := r.Draw(frame, nil)
59 if err != nil || len(unchanged) != 0 {
60 t.Fatalf("committed shadow differs: %q, %v", unchanged, err)
61 }
62 }
63 }
64}
65
66func TestDownwardScrollMismatchFallsBack(t *testing.T) {
67 committed := vt.NewFrame(10, 8)
68 frame := committed.Clone()
69 frame.Set(0, 4, vt.Cell{Rune: 'X', Style: vt.DefaultStyle()})
70 for _, width := range []int{9, 10} {
71 candidate, err := ansi.PlanDelta(frame, []vt.Damage{{Kind: vt.DamageScrollDown, Y: 1, Width: width, Height: 6, Count: 1}}, committed, false)
72 if err != nil {
73 t.Fatal(err)
74 }
75 if !candidate.Plan.Snapshot || candidate.Plan.Scroll.Height != 0 {
76 t.Fatalf("unsafe scroll accepted: %+v", candidate.Plan)
77 }
78 }
79}
80
81// A stale commit followed by the current draw's commit, or a Reset between
82// Prepare and Commit, must never leave a shadow that disagrees with the
83// terminal: the next Prepare is a full snapshot.
84func TestCommitAfterStaleCommitOrResetKeepsTerminalExact(t *testing.T) {
85 for _, tc := range []string{"stale-then-current", "reset-between"} {
86 t.Run(tc, func(t *testing.T) {
87 frame := vt.NewFrame(10, 4)
88 for y := range frame.Height {
89 frame.FillRow(y, 0, frame.Width, vt.Cell{Rune: rune('a' + y), Style: vt.DefaultStyle()})
90 }
91 r := ansi.New(ansi.Capabilities{})
92 terminal := vt.NewScreen(frame.Width, frame.Height)
93 initial, err := r.Draw(frame, nil)
94 if err != nil {
95 t.Fatal(err)
96 }
97 terminal.Write(initial)
98
99 frame.Set(1, 1, vt.Cell{Rune: 'A', Style: vt.DefaultStyle()})
100 a, err := r.Prepare(frame, []vt.Damage{{Kind: vt.DamageText, X: 1, Y: 1, Width: 1, Height: 1}}, false)
101 if err != nil {
102 t.Fatal(err)
103 }
104 terminal.Write(a.Bytes())
105 // The caller reverts the cell before the next draw; only row 2 is damaged.
106 frame.Set(1, 1, vt.Cell{Rune: 'b', Style: vt.DefaultStyle()})
107 frame.Set(2, 2, vt.Cell{Rune: 'B', Style: vt.DefaultStyle()})
108 if tc == "reset-between" {
109 r.Reset()
110 a.Commit()
111 } else {
112 b, err := r.Prepare(frame, []vt.Damage{{Kind: vt.DamageText, X: 2, Y: 2, Width: 1, Height: 1}}, false)
113 if err != nil {
114 t.Fatal(err)
115 }
116 terminal.Write(b.Bytes())
117 a.Commit()
118 b.Commit()
119 }
120 next, err := r.Prepare(frame, nil, false)
121 if err != nil {
122 t.Fatal(err)
123 }
124 terminal.Write(next.Bytes())
125 next.Commit()
126 for y := range frame.Height {
127 for x := range frame.Width {
128 if !frame.Cell(x, y).Equal(terminal.Cell(x, y)) {
129 t.Fatalf("cell %d,%d = %+v, want %+v", x, y, terminal.Cell(x, y), frame.Cell(x, y))
130 }
131 }
132 }
133 })
134 }
135}
136
137func TestStaleCommitForcesSnapshotAndReplaysCorrectly(t *testing.T) {
138 for _, deliverB := range []bool{false, true} {
139 frame := vt.NewFrame(10, 4)
140 for y := range frame.Height {
141 frame.FillRow(y, 0, frame.Width, vt.Cell{Rune: rune('a' + y), Style: vt.DefaultStyle()})
142 }
143 r := ansi.New(ansi.Capabilities{})
144 terminal := vt.NewScreen(frame.Width, frame.Height)
145 initial, err := r.Draw(frame, nil)
146 if err != nil {
147 t.Fatal(err)
148 }
149 terminal.Write(initial)
150
151 frame.Set(1, 1, vt.Cell{Rune: 'A', Style: vt.DefaultStyle()})
152 a, err := r.Prepare(frame, []vt.Damage{{Kind: vt.DamageText, X: 1, Y: 1, Width: 1, Height: 1}}, false)
153 if err != nil {
154 t.Fatal(err)
155 }
156 terminal.Write(a.Bytes())
157 frame.Set(2, 2, vt.Cell{Rune: 'B', Style: vt.Style{Bold: true, Foreground: 2, Background: -1}})
158 b, err := r.Prepare(frame, []vt.Damage{{Kind: vt.DamageText, X: 2, Y: 2, Width: 1, Height: 1}}, false)
159 if err != nil {
160 t.Fatal(err)
161 }
162 if deliverB {
163 terminal.Write(b.Bytes())
164 }
165 a.Commit() // superseded by b: must drop the committed shadow
166
167 next, err := r.Prepare(frame, nil, false)
168 if err != nil {
169 t.Fatal(err)
170 }
171 if len(next.Bytes()) == 0 || !bytes.Contains(next.Bytes(), []byte("a")) || !bytes.Contains(next.Bytes(), []byte("d")) {
172 t.Fatalf("deliverB=%v: next prepare is not a full snapshot: %q", deliverB, next.Bytes())
173 }
174 terminal.Write(next.Bytes())
175 next.Commit()
176 for y := range frame.Height {
177 for x := range frame.Width {
178 if !frame.Cell(x, y).Equal(terminal.Cell(x, y)) {
179 t.Fatalf("deliverB=%v: cell %d,%d = %+v, want %+v", deliverB, x, y, terminal.Cell(x, y), frame.Cell(x, y))
180 }
181 }
182 }
183 unchanged, err := r.Draw(frame, nil)
184 if err != nil || len(unchanged) != 0 {
185 t.Fatalf("committed shadow differs after snapshot: %q, %v", unchanged, err)
186 }
187 }
188}