v0.1.0 7deb448

frame_test.go RawHistoryFolder

1package renderer
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 TestNewFrameCanonicalOffsets(t *testing.T) {
24	f := NewFrame(4, 3)
25	for y := range f.Height {
26		if f.lineOffset[y] != y*f.Width {
27			t.Fatalf("lineOffset[%d] = %d, want %d", y, f.lineOffset[y], y*f.Width)
28		}
29	}
30	if err := f.CheckInvariants(); err != nil {
31		t.Fatalf("fresh frame invariants: %v", err)
32	}
33}
34
35// TestCheckInvariantsDetectsBrokenRotation is the RED guard: a rotation that
36// duplicates a physical offset (the classic off-by-one bug) must be rejected.
37func TestCheckInvariantsDetectsBrokenRotation(t *testing.T) {
38	f := NewFrame(4, 3)
39	// Corrupt: two logical rows map to the same physical row.
40	f.lineOffset[1] = f.lineOffset[0]
41	if err := f.CheckInvariants(); err == nil {
42		t.Fatal("expected invariant violation for duplicate physical row, got nil")
43	}
44
45	f = NewFrame(4, 3)
46	f.lineOffset[0] = 1 // not a multiple of width
47	if err := f.CheckInvariants(); err == nil {
48		t.Fatal("expected invariant violation for non-multiple offset, got nil")
49	}
50
51	f = NewFrame(4, 3)
52	f.lineOffset[2] = 99 // out of range
53	if err := f.CheckInvariants(); err == nil {
54		t.Fatal("expected invariant violation for out-of-range offset, got nil")
55	}
56}
57
58func TestScrollUpEquivalence(t *testing.T) {
59	tests := []struct {
60		name          string
61		width, height int
62		rows          []string
63		top, bottom   int
64		n             int
65		want          []string
66	}{
67		{
68			name: "scroll up 1 full screen", width: 3, height: 4,
69			rows: []string{"aaa", "bbb", "ccc", "ddd"},
70			top:  0, bottom: 3, n: 1,
71			want: []string{"bbb", "ccc", "ddd", "   "},
72		},
73		{
74			name: "scroll up 2 full screen", width: 3, height: 4,
75			rows: []string{"aaa", "bbb", "ccc", "ddd"},
76			top:  0, bottom: 3, n: 2,
77			want: []string{"ccc", "ddd", "   ", "   "},
78		},
79		{
80			name: "scroll up region subset", width: 3, height: 5,
81			rows: []string{"aaa", "bbb", "ccc", "ddd", "eee"},
82			top:  1, bottom: 3, n: 1,
83			want: []string{"aaa", "ccc", "ddd", "   ", "eee"},
84		},
85	}
86	for _, tt := range tests {
87		t.Run(tt.name, func(t *testing.T) {
88			f := NewFrame(tt.width, tt.height)
89			fillFrame(f, tt.rows)
90			f.ScrollUp(tt.top, tt.bottom, tt.n)
91			for y := range f.Height {
92				if got := rowRunes(f, y); got != tt.want[y] {
93					t.Errorf("row %d = %q, want %q", y, got, tt.want[y])
94				}
95			}
96			if err := f.CheckInvariants(); err != nil {
97				t.Errorf("invariants after scroll up: %v", err)
98			}
99		})
100	}
101}
102
103func TestScrollDownEquivalence(t *testing.T) {
104	tests := []struct {
105		name          string
106		width, height int
107		rows          []string
108		top, bottom   int
109		n             int
110		want          []string
111	}{
112		{
113			name: "scroll down 1 full screen", width: 3, height: 4,
114			rows: []string{"aaa", "bbb", "ccc", "ddd"},
115			top:  0, bottom: 3, n: 1,
116			want: []string{"   ", "aaa", "bbb", "ccc"},
117		},
118		{
119			name: "scroll down 2 region subset", width: 3, height: 5,
120			rows: []string{"aaa", "bbb", "ccc", "ddd", "eee"},
121			top:  1, bottom: 4, n: 2,
122			want: []string{"aaa", "   ", "   ", "bbb", "ccc"},
123		},
124	}
125	for _, tt := range tests {
126		t.Run(tt.name, func(t *testing.T) {
127			f := NewFrame(tt.width, tt.height)
128			fillFrame(f, tt.rows)
129			f.ScrollDown(tt.top, tt.bottom, tt.n)
130			for y := range f.Height {
131				if got := rowRunes(f, y); got != tt.want[y] {
132					t.Errorf("row %d = %q, want %q", y, got, tt.want[y])
133				}
134			}
135			if err := f.CheckInvariants(); err != nil {
136				t.Errorf("invariants after scroll down: %v", err)
137			}
138		})
139	}
140}
141
142// TestRotationIsPermutationUnderRandomOps drives an arbitrary sequence of
143// scrolls and asserts lineOffset stays a valid permutation throughout.
144func TestRotationIsPermutationUnderRandomOps(t *testing.T) {
145	f := NewFrame(5, 6)
146	fillFrame(f, []string{"aaaaa", "bbbbb", "ccccc", "ddddd", "eeeee", "fffff"})
147	ops := []struct {
148		up          bool
149		top, bottom int
150		n           int
151	}{
152		{true, 0, 5, 1}, {false, 1, 4, 2}, {true, 2, 5, 3},
153		{false, 0, 5, 1}, {true, 0, 2, 2}, {false, 3, 5, 1},
154	}
155	for i, op := range ops {
156		if op.up {
157			f.ScrollUp(op.top, op.bottom, op.n)
158		} else {
159			f.ScrollDown(op.top, op.bottom, op.n)
160		}
161		if err := f.CheckInvariants(); err != nil {
162			t.Fatalf("op %d (%+v): invariants broken: %v", i, op, err)
163		}
164	}
165}

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 12

Tags 11

↑↓ moveenter openesc close