1package ansi
2
3import "testing"
4
5// rowRunes renders a logical row to a string for readable assertions.
6func rowRunes(f Frame, y int) string {
7 out := make([]rune, f.Width)
8 for x := range f.Width {
9 out[x] = f.At(x, y).Rune
10 }
11 return string(out)
12}
13
14// fillFrame writes one distinct rune per logical row so rotation is observable.
15func fillFrame(f Frame, rows []string) {
16 for y, s := range rows {
17 for x, r := range s {
18 f.Set(x, y, Cell{Rune: r, Style: DefaultStyle()})
19 }
20 }
21}
22
23func TestNewFrameHasValidLogicalRows(t *testing.T) {
24 f := NewFrame(4, 3)
25 fillFrame(f, []string{"aaaa", "bbbb", "cccc"})
26 for y, want := range []string{"aaaa", "bbbb", "cccc"} {
27 if got := rowRunes(f, y); got != want {
28 t.Fatalf("row %d = %q, want %q", y, got, want)
29 }
30 }
31 if err := f.CheckInvariants(); err != nil {
32 t.Fatalf("fresh frame invariants: %v", err)
33 }
34}
35
36func TestFrameCloneIsIndependent(t *testing.T) {
37 f := NewFrame(2, 1)
38 f.Set(0, 0, Cell{Rune: 'a', Style: DefaultStyle()})
39 clone := f.Clone()
40 clone.Set(0, 0, Cell{Rune: 'b', Style: DefaultStyle()})
41 if got := f.At(0, 0).Rune; got != 'a' {
42 t.Fatalf("source changed through clone: %q", got)
43 }
44}
45
46func TestScrollUpEquivalence(t *testing.T) {
47 tests := []struct {
48 name string
49 width, height int
50 rows []string
51 top, bottom int
52 n int
53 want []string
54 }{
55 {
56 name: "scroll up 1 full screen", width: 3, height: 4,
57 rows: []string{"aaa", "bbb", "ccc", "ddd"},
58 top: 0, bottom: 3, n: 1,
59 want: []string{"bbb", "ccc", "ddd", " "},
60 },
61 {
62 name: "scroll up 2 full screen", width: 3, height: 4,
63 rows: []string{"aaa", "bbb", "ccc", "ddd"},
64 top: 0, bottom: 3, n: 2,
65 want: []string{"ccc", "ddd", " ", " "},
66 },
67 {
68 name: "scroll up region subset", width: 3, height: 5,
69 rows: []string{"aaa", "bbb", "ccc", "ddd", "eee"},
70 top: 1, bottom: 3, n: 1,
71 want: []string{"aaa", "ccc", "ddd", " ", "eee"},
72 },
73 }
74 for _, tt := range tests {
75 t.Run(tt.name, func(t *testing.T) {
76 f := NewFrame(tt.width, tt.height)
77 fillFrame(f, tt.rows)
78 f.ScrollUp(tt.top, tt.bottom, tt.n)
79 for y := range f.Height {
80 if got := rowRunes(f, y); got != tt.want[y] {
81 t.Errorf("row %d = %q, want %q", y, got, tt.want[y])
82 }
83 }
84 if err := f.CheckInvariants(); err != nil {
85 t.Errorf("invariants after scroll up: %v", err)
86 }
87 })
88 }
89}
90
91func TestScrollDownEquivalence(t *testing.T) {
92 tests := []struct {
93 name string
94 width, height int
95 rows []string
96 top, bottom int
97 n int
98 want []string
99 }{
100 {
101 name: "scroll down 1 full screen", width: 3, height: 4,
102 rows: []string{"aaa", "bbb", "ccc", "ddd"},
103 top: 0, bottom: 3, n: 1,
104 want: []string{" ", "aaa", "bbb", "ccc"},
105 },
106 {
107 name: "scroll down 2 region subset", width: 3, height: 5,
108 rows: []string{"aaa", "bbb", "ccc", "ddd", "eee"},
109 top: 1, bottom: 4, n: 2,
110 want: []string{"aaa", " ", " ", "bbb", "ccc"},
111 },
112 }
113 for _, tt := range tests {
114 t.Run(tt.name, func(t *testing.T) {
115 f := NewFrame(tt.width, tt.height)
116 fillFrame(f, tt.rows)
117 f.ScrollDown(tt.top, tt.bottom, tt.n)
118 for y := range f.Height {
119 if got := rowRunes(f, y); got != tt.want[y] {
120 t.Errorf("row %d = %q, want %q", y, got, tt.want[y])
121 }
122 }
123 if err := f.CheckInvariants(); err != nil {
124 t.Errorf("invariants after scroll down: %v", err)
125 }
126 })
127 }
128}
129
130// TestRotationIsPermutationUnderRandomOps drives an arbitrary sequence of
131// scrolls and asserts lineOffset stays a valid permutation throughout.
132func TestRotationIsPermutationUnderRandomOps(t *testing.T) {
133 f := NewFrame(5, 6)
134 fillFrame(f, []string{"aaaaa", "bbbbb", "ccccc", "ddddd", "eeeee", "fffff"})
135 ops := []struct {
136 up bool
137 top, bottom int
138 n int
139 }{
140 {true, 0, 5, 1}, {false, 1, 4, 2}, {true, 2, 5, 3},
141 {false, 0, 5, 1}, {true, 0, 2, 2}, {false, 3, 5, 1},
142 }
143 for i, op := range ops {
144 if op.up {
145 f.ScrollUp(op.top, op.bottom, op.n)
146 } else {
147 f.ScrollDown(op.top, op.bottom, op.n)
148 }
149 if err := f.CheckInvariants(); err != nil {
150 t.Fatalf("op %d (%+v): invariants broken: %v", i, op, err)
151 }
152 }
153}