wheel_test.go RawHistoryFolder

1package webterm
2
3import (
4	"context"
5	"io"
6	"strings"
7	"testing"
8	"time"
9
10	"github.com/bnema/vev-vt/html/browser"
11	"github.com/bnema/vev/internal/domain"
12	"github.com/stretchr/testify/require"
13)
14
15func TestWheelNotchesConvertModes(t *testing.T) {
16	tests := []struct {
17		name      string
18		deltaY    float64
19		deltaMode int
20		want      float64
21	}{
22		{"pixel notch", 100, 0, 1},
23		{"pixel fraction", 25, 0, 0.25},
24		{"pixel negative", -100, 0, -1},
25		{"line notch", 3, 1, 1},
26		{"line fraction", 1, 1, 1.0 / 3.0},
27		{"page notch", 1, 2, 10},
28		{"page negative", -1, 2, -10},
29		{"unknown mode", 100, 7, 0},
30	}
31	for _, test := range tests {
32		t.Run(test.name, func(t *testing.T) {
33			if got := wheelNotches(test.deltaY, test.deltaMode); got != test.want {
34				t.Fatalf("wheelNotches(%v, %d) = %v, want %v", test.deltaY, test.deltaMode, got, test.want)
35			}
36		})
37	}
38}
39
40func TestConsumeWheelQuantizesNotches(t *testing.T) {
41	tests := []struct {
42		name          string
43		deltas        []float64
44		mode          int
45		shift         bool
46		wantReports   []int
47		wantUp        []bool
48		wantRemainder float64
49	}{
50		{
51			name:          "one pixel notch down",
52			deltas:        []float64{100},
53			wantReports:   []int{1},
54			wantUp:        []bool{false},
55			wantRemainder: 0,
56		},
57		{
58			name:          "one pixel notch up",
59			deltas:        []float64{-100},
60			wantReports:   []int{1},
61			wantUp:        []bool{true},
62			wantRemainder: 0,
63		},
64		{
65			name:          "micro burst forms one notch",
66			deltas:        []float64{5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5},
67			wantReports:   []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
68			wantUp:        make([]bool, 20),
69			wantRemainder: 0,
70		},
71		{
72			name:          "same distance at once or fragmented",
73			deltas:        []float64{200},
74			wantReports:   []int{2},
75			wantUp:        []bool{false},
76			wantRemainder: 0,
77		},
78		{
79			name:          "line mode notch",
80			deltas:        []float64{3},
81			mode:          1,
82			wantReports:   []int{1},
83			wantUp:        []bool{false},
84			wantRemainder: 0,
85		},
86		{
87			name:          "remainder carries over",
88			deltas:        []float64{150, 150},
89			wantReports:   []int{1, 2},
90			wantUp:        []bool{false, false},
91			wantRemainder: 0,
92		},
93		{
94			name:          "shift multiplies notches",
95			deltas:        []float64{100},
96			shift:         true,
97			wantReports:   []int{10},
98			wantUp:        []bool{false},
99			wantRemainder: 0,
100		},
101		{
102			name:          "emission is capped",
103			deltas:        []float64{5000},
104			wantReports:   []int{10},
105			wantUp:        []bool{false},
106			wantRemainder: 0,
107		},
108		{
109			name:          "accumulator is capped",
110			deltas:        []float64{50000, 0},
111			wantReports:   []int{10, 0},
112			wantUp:        []bool{false, false},
113			wantRemainder: 0,
114		},
115	}
116	for _, test := range tests {
117		t.Run(test.name, func(t *testing.T) {
118			terminal := &Terminal{}
119			if len(test.wantReports) != len(test.deltas) {
120				t.Fatalf("test declares %d deltas but %d expectations", len(test.deltas), len(test.wantReports))
121			}
122			for i, delta := range test.deltas {
123				reports, button := terminal.consumeWheel(delta, test.mode, test.shift)
124				if reports != test.wantReports[i] {
125					t.Fatalf("delta %d: reports = %d, want %d", i, reports, test.wantReports[i])
126				}
127				if reports == 0 {
128					continue
129				}
130				wantButton := 65
131				if test.wantUp[i] {
132					wantButton = 64
133				}
134				if button != wantButton {
135					t.Fatalf("delta %d: button = %d, want %d", i, button, wantButton)
136				}
137			}
138			if terminal.wheelAcc != test.wantRemainder {
139				t.Fatalf("remainder = %v, want %v", terminal.wheelAcc, test.wantRemainder)
140			}
141		})
142	}
143}
144
145func TestConsumeWheelKeepsDirectionAcrossFractions(t *testing.T) {
146	terminal := &Terminal{}
147	if reports, _ := terminal.consumeWheel(-90, 0, false); reports != 0 {
148		t.Fatalf("reports = %d, want 0 (banked)", reports)
149	}
150	reports, button := terminal.consumeWheel(-30, 0, false)
151	if reports != 1 || button != 64 {
152		t.Fatalf("reports = %d button = %d, want 1/64", reports, button)
153	}
154}
155
156func TestWheelHorizontalOnlyPreservesRemainder(t *testing.T) {
157	ctx, cancel := context.WithTimeout(t.Context(), time.Second)
158	defer cancel()
159	terminal, err := New(ctx, domain.Geometry{Size: domain.Size{Cols: 10, Rows: 4}})
160	require.NoError(t, err)
161	defer terminal.Close()
162	_, err = terminal.Write([]byte("\x1b[?1000h\x1b[?1006h"))
163	require.NoError(t, err)
164	require.NoError(t, terminal.Flush())
165	up := browser.Event{Kind: browser.EventWheel, Wheel: &browser.WheelEvent{DeltaY: 90, DeltaMode: 0, Row: 0, Column: 0}}
166	require.NoError(t, terminal.Handle(ctx, up))
167	if terminal.wheelAcc != 0.9 {
168		t.Fatalf("remainder = %v, want 0.9", terminal.wheelAcc)
169	}
170	// A pure horizontal swipe is ignored without wiping banked vertical.
171	horizontal := browser.Event{Kind: browser.EventWheel, Wheel: &browser.WheelEvent{DeltaX: 50, DeltaY: 0, DeltaMode: 0, Row: 0, Column: 0}}
172	require.NoError(t, terminal.Handle(ctx, horizontal))
173	if terminal.wheelAcc != 0.9 {
174		t.Fatalf("remainder = %v, want 0.9 (horizontal must not reset)", terminal.wheelAcc)
175	}
176}
177
178func TestWheelIgnoredInputResetsRemainder(t *testing.T) {
179	terminal := &Terminal{}
180	ctx := context.Background()
181	down := browser.Event{
182		Kind: browser.EventWheel,
183		Wheel: &browser.WheelEvent{
184			DeltaY: 90, DeltaMode: 0, Row: 9999, Column: 9999,
185		},
186	}
187	// Out-of-bounds cells are ignored and must drop the banked fraction.
188	if err := terminal.Handle(ctx, down); err != nil {
189		t.Fatal(err)
190	}
191	if terminal.wheelAcc != 0 {
192		t.Fatalf("remainder = %v, want 0", terminal.wheelAcc)
193	}
194}
195
196func TestHandleWheelShiftOmitsShiftModifier(t *testing.T) {
197	ctx, cancel := context.WithTimeout(t.Context(), time.Second)
198	defer cancel()
199	terminal, err := New(ctx, domain.Geometry{Size: domain.Size{Cols: 10, Rows: 4}})
200	require.NoError(t, err)
201	defer terminal.Close()
202	_, err = terminal.Write([]byte("\x1b[?1000h\x1b[?1006h"))
203	require.NoError(t, err)
204	require.NoError(t, terminal.Flush())
205
206	// Shift is consumed as the x10 multiplier: reports stay plain wheel
207	// buttons (65) instead of Shift-flagged buttons (69).
208	event := browser.Event{Kind: browser.EventWheel, Wheel: &browser.WheelEvent{
209		DeltaY: 100, DeltaMode: 0, Row: 0, Column: 0, Modifiers: browser.Modifiers{Shift: true},
210	}}
211	require.NoError(t, terminal.Handle(ctx, event))
212	want := strings.Repeat("\x1b[<65;1;1M", 10)
213	data := make([]byte, len(want))
214	_, err = io.ReadFull(terminal.In(), data)
215	require.NoError(t, err)
216	require.Equal(t, want, string(data))
217}
218
219func TestHandleWheelEmitsOneReportPerNotch(t *testing.T) {
220	ctx, cancel := context.WithTimeout(t.Context(), time.Second)
221	defer cancel()
222	terminal, err := New(ctx, domain.Geometry{Size: domain.Size{Cols: 10, Rows: 4}})
223	require.NoError(t, err)
224	defer terminal.Close()
225	_, err = terminal.Write([]byte("\x1b[?1000h\x1b[?1006h"))
226	require.NoError(t, err)
227	require.NoError(t, terminal.Flush())
228
229	wheel := func(deltaY float64) browser.Event {
230		return browser.Event{Kind: browser.EventWheel, Wheel: &browser.WheelEvent{
231			DeltaY: deltaY, DeltaMode: 0, Row: 0, Column: 0,
232		}}
233	}
234	// A 5px micro-event is banked: nothing is sent yet.
235	require.NoError(t, terminal.Handle(ctx, wheel(5)))
236	// Nineteen more micro-events complete the notch: exactly one report.
237	for i := 0; i < 19; i++ {
238		require.NoError(t, terminal.Handle(ctx, wheel(5)))
239	}
240	want := "\x1b[<65;1;1M"
241	data := make([]byte, len(want))
242	_, err = io.ReadFull(terminal.In(), data)
243	require.NoError(t, err)
244	require.Equal(t, want, string(data))
245}

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