main 5041353

page_scroll_test.go RawHistoryFolder

1package usecase
2
3import (
4	"context"
5	"errors"
6	"testing"
7
8	"github.com/bnema/dumber/internal/application/port"
9)
10
11func TestPageScroll_SmallVerticalDown(t *testing.T) {
12	wv := newScrollableStub(nil)
13	uc := NewPageScrollUseCase()
14
15	err := uc.Scroll(context.Background(), wv, PageScrollDown)
16	if err != nil {
17		t.Fatalf("Scroll() unexpected error: %v", err)
18	}
19
20	if wv.lastRequest.Command != port.PageScrollCommandDown {
21		t.Fatalf("ScrollPage request.Command = %d, want %d", wv.lastRequest.Command, port.PageScrollCommandDown)
22	}
23	if wv.lastRequest.FallbackDX != 0 || wv.lastRequest.FallbackDY != 80 {
24		t.Fatalf("ScrollPage request fallback delta = (%d, %d), want (0, 80)", wv.lastRequest.FallbackDX, wv.lastRequest.FallbackDY)
25	}
26}
27
28func TestPageScroll_FastVerticalUp(t *testing.T) {
29	wv := newScrollableStub(nil)
30	uc := NewPageScrollUseCase()
31
32	err := uc.Scroll(context.Background(), wv, PageScrollUpFast)
33	if err != nil {
34		t.Fatalf("Scroll() unexpected error: %v", err)
35	}
36
37	if wv.lastRequest.Command != port.PageScrollCommandUpFast {
38		t.Fatalf("ScrollPage request.Command = %d, want %d", wv.lastRequest.Command, port.PageScrollCommandUpFast)
39	}
40	if wv.lastRequest.FallbackDX != 0 || wv.lastRequest.FallbackDY != -320 {
41		t.Fatalf("ScrollPage request fallback delta = (%d, %d), want (0, -320)", wv.lastRequest.FallbackDX, wv.lastRequest.FallbackDY)
42	}
43}
44
45func TestPageScroll_HorizontalDirectionMapping(t *testing.T) {
46	t.Run("left", func(t *testing.T) {
47		wv := newScrollableStub(nil)
48		uc := NewPageScrollUseCase()
49
50		if err := uc.Scroll(context.Background(), wv, PageScrollLeft); err != nil {
51			t.Fatalf("Scroll() unexpected error: %v", err)
52		}
53
54		if wv.lastRequest.Command != port.PageScrollCommandLeft {
55			t.Fatalf("ScrollPage request.Command = %d, want %d", wv.lastRequest.Command, port.PageScrollCommandLeft)
56		}
57		if wv.lastRequest.FallbackDX != -80 || wv.lastRequest.FallbackDY != 0 {
58			t.Fatalf("ScrollPage request fallback delta = (%d, %d), want (-80, 0)", wv.lastRequest.FallbackDX, wv.lastRequest.FallbackDY)
59		}
60	})
61
62	t.Run("right", func(t *testing.T) {
63		wv := newScrollableStub(nil)
64		uc := NewPageScrollUseCase()
65		if err := uc.Scroll(context.Background(), wv, PageScrollRight); err != nil {
66			t.Fatalf("Scroll() unexpected error: %v", err)
67		}
68
69		if wv.lastRequest.Command != port.PageScrollCommandRight {
70			t.Fatalf("ScrollPage request.Command = %d, want %d", wv.lastRequest.Command, port.PageScrollCommandRight)
71		}
72		if wv.lastRequest.FallbackDX != 80 || wv.lastRequest.FallbackDY != 0 {
73			t.Fatalf("ScrollPage request fallback delta = (%d, %d), want (80, 0)", wv.lastRequest.FallbackDX, wv.lastRequest.FallbackDY)
74		}
75	})
76}
77
78func TestPageScroll_UnsupportedWebView(t *testing.T) {
79	wv := &nonScrollableStub{}
80	uc := NewPageScrollUseCase()
81
82	err := uc.Scroll(context.Background(), wv, PageScrollDown)
83	if err == nil {
84		t.Fatal("Scroll() expected error for unsupported webview, got nil")
85	}
86	if err.Error() != "page scroll: webview does not support page scrolling" {
87		t.Fatalf("Scroll() error = %q, want %q", err.Error(), "page scroll: webview does not support page scrolling")
88	}
89}
90
91func TestPageScroll_UnsupportedWebView_AllCommandsStable(t *testing.T) {
92	wv := &nonScrollableStub{}
93	uc := NewPageScrollUseCase()
94
95	cmds := []PageScrollCommand{
96		PageScrollLeft,
97		PageScrollRight,
98		PageScrollUp,
99		PageScrollDown,
100		PageScrollUpFast,
101		PageScrollDownFast,
102	}
103
104	for _, cmd := range cmds {
105		err := uc.Scroll(context.Background(), wv, cmd)
106		if err == nil {
107			t.Fatalf("Scroll(command=%s) expected error for unsupported webview, got nil", cmd)
108		}
109		if err.Error() != "page scroll: webview does not support page scrolling" {
110			t.Fatalf("Scroll(command=%s) error = %q, want %q", cmd, err.Error(), "page scroll: webview does not support page scrolling")
111		}
112	}
113}
114
115func TestPageScroll_NilWebView_StableFailure(t *testing.T) {
116	uc := NewPageScrollUseCase()
117
118	err := uc.Scroll(context.Background(), nil, PageScrollDown)
119	if err == nil {
120		t.Fatal("Scroll() expected error for nil webview, got nil")
121	}
122	if err.Error() != "page scroll: nil webview" {
123		t.Fatalf("Scroll() error = %q, want %q", err.Error(), "page scroll: nil webview")
124	}
125}
126
127func TestPageScroll_PropagatesScrollError(t *testing.T) {
128	wantErr := errors.New("engine scroll failure")
129	wv := newScrollableStub(wantErr)
130	uc := NewPageScrollUseCase()
131
132	err := uc.Scroll(context.Background(), wv, PageScrollDown)
133	if err == nil {
134		t.Fatal("Scroll() expected error, got nil")
135	}
136	if !errors.Is(err, wantErr) {
137		t.Fatalf("Scroll() error = %v, want %v", err, wantErr)
138	}
139}
140
141func TestPageScroll_TapDistancesRemainFixed(t *testing.T) {
142	tests := []struct {
143		name string
144		cmd  PageScrollCommand
145		dx   int
146		dy   int
147	}{
148		{"left 80 px", PageScrollLeft, -80, 0},
149		{"right 80 px", PageScrollRight, 80, 0},
150		{"up 80 px", PageScrollUp, 0, -80},
151		{"down 80 px", PageScrollDown, 0, 80},
152		{"up fast 320 px", PageScrollUpFast, 0, -320},
153		{"down fast 320 px", PageScrollDownFast, 0, 320},
154	}
155	for _, tt := range tests {
156		t.Run(tt.name, func(t *testing.T) {
157			wv := newScrollableStub(nil)
158			if err := NewPageScrollUseCase().Scroll(context.Background(), wv, tt.cmd); err != nil {
159				t.Fatal(err)
160			}
161			if wv.lastRequest.FallbackDX != tt.dx || wv.lastRequest.FallbackDY != tt.dy {
162				t.Fatalf("tap delta=(%d,%d), want (%d,%d)", wv.lastRequest.FallbackDX, wv.lastRequest.FallbackDY, tt.dx, tt.dy)
163			}
164			if wv.lastRequest.Continuous {
165				t.Fatal("tap request must not be marked continuous")
166			}
167		})
168	}
169}
170
171func TestPageScroll_ScrollContinuousMarksRequest(t *testing.T) {
172	wv := newScrollableStub(nil)
173	if err := NewPageScrollUseCase().ScrollContinuous(context.Background(), wv, PageScrollDown); err != nil {
174		t.Fatal(err)
175	}
176	if !wv.lastRequest.Continuous || wv.lastRequest.FallbackDY != 80 {
177		t.Fatalf("continuous request = %+v, want continuous 80 px", wv.lastRequest)
178	}
179}
180
181func TestPageScroll_StopCancelsOptionalCapability(t *testing.T) {
182	wv := &cancelableScrollableStub{scrollableStub: newScrollableStub(nil)}
183	if err := NewPageScrollUseCase().Stop(context.Background(), wv); err != nil {
184		t.Fatal(err)
185	}
186	if wv.cancelCalls != 1 {
187		t.Fatalf("cancel calls=%d, want 1", wv.cancelCalls)
188	}
189	if err := NewPageScrollUseCase().Stop(context.Background(), newScrollableStub(nil)); err != nil {
190		t.Fatalf("non-cancelable WebKit-style adapter should be a no-op: %v", err)
191	}
192}
193
194func TestPageScroll_DeltaMethod(t *testing.T) {
195	tests := []struct {
196		cmd      PageScrollCommand
197		wantDx   int
198		wantDy   int
199		wantName string
200	}{
201		{PageScrollLeft, -80, 0, "left"},
202		{PageScrollRight, 80, 0, "right"},
203		{PageScrollUp, 0, -80, "up"},
204		{PageScrollDown, 0, 80, "down"},
205		{PageScrollUpFast, 0, -320, "up-fast"},
206		{PageScrollDownFast, 0, 320, "down-fast"},
207	}
208
209	for _, tt := range tests {
210		t.Run(tt.wantName, func(t *testing.T) {
211			dx, dy := scrollDelta(tt.cmd)
212			if dx != tt.wantDx || dy != tt.wantDy {
213				t.Errorf("scrollDelta(%v) = (%d, %d), want (%d, %d)", tt.cmd, dx, dy, tt.wantDx, tt.wantDy)
214			}
215			if got := tt.cmd.String(); got != tt.wantName {
216				t.Errorf("PageScrollCommand.String() = %q, want %q", got, tt.wantName)
217			}
218		})
219	}
220}
221
222func TestPageScroll_CommandIdentityPropagation(t *testing.T) {
223	// Prove that PageScrollDownFast forwards command identity separately
224	// from fallback delta (dy=320).
225	wv := newScrollableStub(nil)
226	uc := NewPageScrollUseCase()
227
228	err := uc.Scroll(context.Background(), wv, PageScrollDownFast)
229	if err != nil {
230		t.Fatalf("Scroll() unexpected error: %v", err)
231	}
232
233	if wv.lastRequest.Command != port.PageScrollCommandDownFast {
234		t.Fatalf("ScrollPage request.Command = %d, want %d (PageScrollDownFast)",
235			wv.lastRequest.Command, port.PageScrollCommandDownFast)
236	}
237	if int(wv.lastRequest.Command) == wv.lastRequest.FallbackDY {
238		t.Fatal("command identity must NOT be confused with fallback delta: Command and FallbackDY should differ")
239	}
240	if wv.lastRequest.FallbackDX != 0 || wv.lastRequest.FallbackDY != 320 {
241		t.Fatalf("ScrollPage request fallback delta = (%d, %d), want (0, 320)",
242			wv.lastRequest.FallbackDX, wv.lastRequest.FallbackDY)
243	}
244}
245
246func TestPageScroll_UnknownCommand_ReturnsNoOp(t *testing.T) {
247	dx, dy := scrollDelta(PageScrollCommand(999))
248	if dx != 0 || dy != 0 {
249		t.Errorf("scrollDelta(unknown) = (%d, %d), want (0, 0)", dx, dy)
250	}
251
252	got := PageScrollCommand(999).String()
253	if got != "unknown" {
254		t.Errorf("PageScrollCommand(999).String() = %q, want %q", got, "unknown")
255	}
256}
257
258// --- test helpers ---
259
260// scrollableStub satisfies both port.WebView (via embedded nonScrollableStub)
261// and port.PageScrollable (via ScrollPage).
262type scrollableStub struct {
263	*nonScrollableStub
264	lastRequest port.PageScrollRequest
265	scrollErr   error
266}
267
268type cancelableScrollableStub struct {
269	*scrollableStub
270	cancelCalls int
271}
272
273func (s *cancelableScrollableStub) CancelPageScroll(context.Context) {
274	s.cancelCalls++
275}
276
277func newScrollableStub(scrollErr error) *scrollableStub {
278	return &scrollableStub{
279		nonScrollableStub: &nonScrollableStub{},
280		scrollErr:         scrollErr,
281	}
282}
283
284// ScrollPage implements port.PageScrollable.
285func (s *scrollableStub) ScrollPage(_ context.Context, req port.PageScrollRequest) error {
286	s.lastRequest = req
287	return s.scrollErr
288}
289
290// nonScrollableStub satisfies port.WebView but NOT port.PageScrollable.
291type nonScrollableStub struct{}
292
293func (*nonScrollableStub) ID() port.WebViewID                              { return 0 }
294func (*nonScrollableStub) LoadURI(_ context.Context, _ string) error       { return nil }
295func (*nonScrollableStub) LoadHTML(_ context.Context, _, _ string) error   { return nil }
296func (*nonScrollableStub) Reload(_ context.Context) error                  { return nil }
297func (*nonScrollableStub) ReloadBypassCache(_ context.Context) error       { return nil }
298func (*nonScrollableStub) Stop(_ context.Context) error                    { return nil }
299func (*nonScrollableStub) GoBack(_ context.Context) error                  { return nil }
300func (*nonScrollableStub) GoForward(_ context.Context) error               { return nil }
301func (*nonScrollableStub) State() port.WebViewState                        { return port.WebViewState{} }
302func (*nonScrollableStub) URI() string                                     { return "" }
303func (*nonScrollableStub) Title() string                                   { return "" }
304func (*nonScrollableStub) IsLoading() bool                                 { return false }
305func (*nonScrollableStub) EstimatedProgress() float64                      { return 0 }
306func (*nonScrollableStub) CanGoBack() bool                                 { return false }
307func (*nonScrollableStub) CanGoForward() bool                              { return false }
308func (*nonScrollableStub) SetZoomLevel(_ context.Context, _ float64) error { return nil }
309func (*nonScrollableStub) GetZoomLevel() float64                           { return 1.0 }
310func (*nonScrollableStub) GetFindController() port.FindController          { return nil }
311func (*nonScrollableStub) SetCallbacks(_ *port.WebViewCallbacks)           {}
312func (*nonScrollableStub) RunJavaScript(_ context.Context, _ string)       {}
313func (*nonScrollableStub) SetBackgroundColor(_, _, _, _ float64)           {}
314func (*nonScrollableStub) ResetBackgroundToDefault()                       {}
315func (*nonScrollableStub) Favicon() port.Texture                           { return nil }
316func (*nonScrollableStub) Generation() uint64                              { return 0 }
317func (*nonScrollableStub) IsFullscreen() bool                              { return false }
318func (*nonScrollableStub) IsPlayingAudio() bool                            { return false }
319func (*nonScrollableStub) IsDestroyed() bool                               { return false }
320func (*nonScrollableStub) Destroy()                                        {}

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 8

Tags 42

↑↓ moveenter openesc close