v0.2.0 4859eb8

frame_test.go RawHistoryFolder

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}

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