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() {}