1package cef
2
3import (
4 "context"
5 "sync"
6 "testing"
7 "time"
8
9 purecef "github.com/bnema/purego-cef/cef"
10 cefmocks "github.com/bnema/purego-cef/cef/mocks"
11 "github.com/stretchr/testify/assert"
12 "github.com/stretchr/testify/mock"
13 "github.com/stretchr/testify/require"
14
15 "github.com/bnema/dumber/internal/application/dto"
16 "github.com/bnema/dumber/internal/application/port"
17 portmocks "github.com/bnema/dumber/internal/application/port/mocks"
18)
19
20type clipboardOrchestratorRecorder struct {
21 mu sync.Mutex
22 selection dto.SelectionClipboardInput
23 selectionCalls int
24}
25
26type stubDownloadPreparer struct{}
27
28func (stubDownloadPreparer) Execute(context.Context, port.DownloadPrepareInput) *port.DownloadPrepareOutput {
29 return &port.DownloadPrepareOutput{}
30}
31
32type controlledSelectionTimer struct {
33 stopped bool
34}
35
36func (t *controlledSelectionTimer) Stop() bool {
37 alreadyStopped := t.stopped
38 t.stopped = true
39 return !alreadyStopped
40}
41
42type controlledSelectionScheduler struct {
43 timers []*controlledSelectionTimer
44 callbacks []func()
45}
46
47func (s *controlledSelectionScheduler) schedule(_ time.Duration, fn func()) stoppableTimer {
48 timer := &controlledSelectionTimer{}
49 s.timers = append(s.timers, timer)
50 s.callbacks = append(s.callbacks, fn)
51 return timer
52}
53
54func (s *controlledSelectionScheduler) fire(index int) {
55 if index < 0 || index >= len(s.callbacks) {
56 return
57 }
58 if !s.timers[index].stopped {
59 s.callbacks[index]()
60 }
61}
62
63type stubFrame struct {
64 main bool
65 url string
66}
67
68func (f stubFrame) IsValid() bool { return true }
69func (f stubFrame) Undo() {}
70func (f stubFrame) Redo() {}
71func (f stubFrame) Cut() {}
72func (f stubFrame) Copy() {}
73func (f stubFrame) Paste() {}
74func (f stubFrame) PasteAndMatchStyle() {}
75func (f stubFrame) Del() {}
76func (f stubFrame) SelectAll() {}
77func (f stubFrame) ViewSource() {}
78func (f stubFrame) GetSource(purecef.StringVisitor) {}
79func (f stubFrame) GetText(purecef.StringVisitor) {}
80func (f stubFrame) LoadRequest(purecef.Request) {}
81func (f stubFrame) LoadURL(string) {}
82func (f stubFrame) ExecuteJavaScript(string, string, int32) {}
83func (f stubFrame) IsMain() bool { return f.main }
84func (f stubFrame) IsFocused() bool { return false }
85func (f stubFrame) GetName() string { return "" }
86func (f stubFrame) GetIdentifier() string { return "" }
87func (f stubFrame) GetParent() purecef.Frame { return nil }
88func (f stubFrame) GetURL() string { return f.url }
89func (f stubFrame) GetBrowser() purecef.Browser { return nil }
90func (f stubFrame) GetV8Context() purecef.V8Context { return nil }
91func (f stubFrame) VisitDom(purecef.Domvisitor) {}
92func (f stubFrame) SendProcessMessage(purecef.ProcessID, purecef.ProcessMessage) {}
93func (f stubFrame) CreateUrlrequest(purecef.Request, purecef.UrlrequestClient) purecef.Urlrequest {
94 return nil
95}
96
97type stubStoppableTimer struct{}
98
99func (stubStoppableTimer) Stop() bool { return true }
100
101type spyStoppableTimer struct{ stopped bool }
102
103func (t *spyStoppableTimer) Stop() bool {
104 t.stopped = true
105 return true
106}
107
108func TestPreparePaneHostedBrowsingContext_ClearsNativePopupFallbackState(t *testing.T) {
109 parent := &WebView{ctx: context.Background()}
110 timer := &spyStoppableTimer{}
111 wv := &WebView{
112 nativePopupCandidate: true,
113 nativePopupParent: parent,
114 nativePopupID: 77,
115 nativePopupFallbackStarted: true,
116 nativePopupFallbackTimer: timer,
117 pendingCreate: &pendingBrowserCreate{},
118 popupOpenerBridgeParent: parent,
119 popupOpenerBridgeParentURI: "https://example.com/login",
120 }
121
122 wv.PreparePaneHostedBrowsingContext()
123
124 require.False(t, wv.nativePopupCandidate)
125 require.Nil(t, wv.nativePopupParent)
126 require.Zero(t, wv.nativePopupID)
127 require.False(t, wv.nativePopupFallbackStarted)
128 require.Nil(t, wv.nativePopupFallbackTimer)
129 require.True(t, timer.stopped)
130 require.Nil(t, wv.popupOpenerBridgeParent)
131 require.Empty(t, wv.popupOpenerBridgeParentURI)
132}
133
134func TestStartNativePopupFallback_AllowsPopupShellAwaitingArm(t *testing.T) {
135 wv := &WebView{
136 nativePopupCandidate: true,
137 pendingCreate: &pendingBrowserCreate{},
138 }
139
140 require.True(t, wv.startNativePopupFallback())
141 require.False(t, wv.isNativePopupCandidate())
142 require.True(t, wv.nativePopupFallbackStarted)
143}
144
145func TestPreparePopupShellDirectBrowserCreation_EnablesSyntheticOpenerBridge(t *testing.T) {
146 parent := &WebView{ctx: context.Background()}
147 parent.updateURI("https://example.com/login")
148 wv := &WebView{
149 pendingCreate: &pendingBrowserCreate{},
150 nativePopupParent: parent,
151 }
152
153 require.True(t, wv.preparePopupShellDirectBrowserCreation())
154 require.Same(t, parent, wv.popupOpenerBridgeParent)
155 require.Equal(t, "https://example.com/login", wv.popupOpenerBridgeParentURI)
156}
157
158func TestPreparePopupShellDirectBrowserCreation_SkipsDestroyedParentOpenerBridge(t *testing.T) {
159 parent := &WebView{ctx: context.Background()}
160 parent.updateURI("https://example.com/login")
161 parent.destroyed.Store(true)
162 wv := &WebView{
163 pendingCreate: &pendingBrowserCreate{},
164 nativePopupParent: parent,
165 }
166
167 require.True(t, wv.preparePopupShellDirectBrowserCreation())
168 require.Nil(t, wv.popupOpenerBridgeParent)
169 require.Empty(t, wv.popupOpenerBridgeParentURI)
170}
171
172func TestScheduleNativePopupFallback_SkipsDestroyedWebView(t *testing.T) {
173 called := false
174 wv := &WebView{}
175 wv.destroyed.Store(true)
176 wv.nativePopupFallbackSchedule = func(time.Duration, func()) stoppableTimer {
177 called = true
178 return stubStoppableTimer{}
179 }
180
181 wv.scheduleNativePopupFallback(time.Millisecond, func() {
182 called = true
183 })
184
185 require.False(t, called)
186}
187
188func TestScheduleNativePopupFallback_DoesNotFireAfterDestroy(t *testing.T) {
189 called := false
190 var scheduled func()
191 wv := &WebView{}
192 wv.nativePopupFallbackSchedule = func(_ time.Duration, fn func()) stoppableTimer {
193 scheduled = fn
194 return stubStoppableTimer{}
195 }
196
197 wv.scheduleNativePopupFallback(time.Millisecond, func() {
198 called = true
199 })
200 require.NotNil(t, scheduled)
201
202 wv.destroyed.Store(true)
203 scheduled()
204
205 require.False(t, called)
206}
207
208func TestStartNativePopupFallback_RejectsDestroyedWebView(t *testing.T) {
209 wv := &WebView{
210 nativePopupCandidate: true,
211 pendingCreate: &pendingBrowserCreate{},
212 }
213 wv.destroyed.Store(true)
214
215 require.False(t, wv.startNativePopupFallback())
216}
217
218func TestNativePopupActivationDoesNotLetInitialBlankLoadClearPendingNavigationBeforeReplay(t *testing.T) {
219 parent := &WebView{}
220 wv := &WebView{
221 ctx: context.Background(),
222 nativePopupCandidate: true,
223 nativePopupParent: parent,
224 client: cefmocks.NewMockRawClient(t),
225 pendingCreate: &pendingBrowserCreate{},
226 }
227
228 rawClient, ok := wv.activateNativePopup(55, "https://example.com/oauth")
229 require.True(t, ok)
230 require.NotNil(t, rawClient)
231 require.Equal(t, int32(55), wv.nativePopupID)
232 require.Same(t, parent, wv.nativePopupParent)
233 require.Equal(t, "https://example.com/oauth", wv.pendingNavigationURI())
234
235 wv.updateURI("about:blank")
236 wv.updateLoadState(false, false, false)
237
238 require.Equal(t, "https://example.com/oauth", wv.pendingNavigationURI())
239}
240
241func TestHandleNativePopupAborted_PreservesPrimedNavigationForFallback(t *testing.T) {
242 closed := false
243 parent := &WebView{}
244 wv := &WebView{
245 nativePopupCandidate: true,
246 nativePopupParent: parent,
247 nativePopupID: 55,
248 pendingCreate: &pendingBrowserCreate{},
249 pendingURI: "https://example.com/oauth",
250 isLoading: true,
251 closeCallbacks: []func(){
252 func() { closed = true },
253 },
254 }
255
256 wv.handleNativePopupAborted()
257
258 require.False(t, closed)
259 require.Equal(t, "https://example.com/oauth", wv.pendingNavigationURI())
260 require.False(t, wv.isNativePopupCandidate())
261 require.Same(t, parent, wv.nativePopupParent)
262 require.Equal(t, int32(0), wv.nativePopupID)
263 require.True(t, wv.isLoading)
264}
265
266func TestConfigureNativePopupWindow_UsesSharedTextureWindowlessDefaults(t *testing.T) {
267 windowInfo := purecef.NewWindowInfo()
268 settings := purecef.NewBrowserSettings()
269
270 configureNativePopupWindow(&windowInfo, &settings, 72)
271
272 require.Equal(t, purecef.WindowHandle(0), windowInfo.ParentWindow)
273 require.Equal(t, int32(1), windowInfo.WindowlessRenderingEnabled)
274 require.Equal(t, int32(1), windowInfo.SharedTextureEnabled)
275 require.Equal(t, int32(72), settings.WindowlessFrameRate)
276 require.Equal(t, int32(1), settings.LocalStorage)
277 require.Equal(t, opaqueWhiteBackground, settings.BackgroundColor)
278}
279
280func TestOnLoadStartFiresCommittedAndUpdatesURI(t *testing.T) {
281 wv := &WebView{ctx: context.Background()}
282 var gotEvents []port.LoadEvent
283 wv.SetCallbacks(&port.WebViewCallbacks{
284 OnLoadChanged: func(event port.LoadEvent) {
285 gotEvents = append(gotEvents, event)
286 },
287 })
288
289 h := &handlerSet{wv: wv}
290 h.OnLoadStart(nil, stubFrame{main: true, url: "https://google.com"}, 0)
291
292 require.Len(t, gotEvents, 1)
293 require.Equal(t, port.LoadCommitted, gotEvents[0])
294 require.Equal(t, "https://google.com", wv.URI())
295}
296
297func TestOnLoadEndDoesNotDispatchBrowserLevelCompletion(t *testing.T) {
298 wv := &WebView{ctx: context.Background()}
299 var gotEvents []port.LoadEvent
300 var gotProgress []float64
301 wv.SetCallbacks(&port.WebViewCallbacks{
302 OnLoadChanged: func(event port.LoadEvent) {
303 gotEvents = append(gotEvents, event)
304 },
305 OnProgressChanged: func(progress float64) {
306 gotProgress = append(gotProgress, progress)
307 },
308 })
309
310 h := &handlerSet{wv: wv}
311 h.OnLoadEnd(nil, stubFrame{main: true, url: "https://reddit.com"}, 200)
312
313 assert.Empty(t, gotEvents)
314 assert.Empty(t, gotProgress)
315}
316
317func TestOnLoadErrorMainFrameCommitsFailedNavigation(t *testing.T) {
318 wv := &WebView{ctx: context.Background()}
319 wv.updateLoadState(true, true, false)
320 var gotEvents []port.LoadEvent
321 wv.SetCallbacks(&port.WebViewCallbacks{
322 OnLoadChanged: func(event port.LoadEvent) {
323 gotEvents = append(gotEvents, event)
324 },
325 })
326
327 h := &handlerSet{wv: wv}
328 h.OnLoadError(nil, stubFrame{main: true, url: "http://localhost:9"}, -102, "CONNECTION_REFUSED", "http://localhost:9")
329
330 require.Equal(t, []port.LoadEvent{port.LoadCommitted}, gotEvents)
331 require.Equal(t, "http://localhost:9", wv.URI())
332 require.True(t, wv.IsLoading())
333 require.True(t, wv.CanGoBack())
334}
335
336func TestOnLoadErrorFollowedByLoadingStateFinishTerminatesNavigation(t *testing.T) {
337 wv := &WebView{ctx: context.Background()}
338 wv.updateLoadState(true, false, false)
339 var gotEvents []port.LoadEvent
340 wv.SetCallbacks(&port.WebViewCallbacks{
341 OnLoadChanged: func(event port.LoadEvent) {
342 gotEvents = append(gotEvents, event)
343 },
344 })
345
346 h := &handlerSet{wv: wv}
347 h.OnLoadError(nil, stubFrame{main: true, url: "http://localhost:9"}, -102, "CONNECTION_REFUSED", "http://localhost:9")
348 h.OnLoadingStateChange(nil, 0, 0, 0)
349
350 require.Equal(t, []port.LoadEvent{port.LoadCommitted, port.LoadFinished}, gotEvents)
351 require.False(t, wv.IsLoading())
352}
353
354func TestOnLoadErrorAfterLoadStartDoesNotDispatchDuplicateEvents(t *testing.T) {
355 wv := &WebView{ctx: context.Background()}
356 var gotEvents []port.LoadEvent
357 wv.SetCallbacks(&port.WebViewCallbacks{
358 OnLoadChanged: func(event port.LoadEvent) {
359 gotEvents = append(gotEvents, event)
360 },
361 })
362
363 h := &handlerSet{wv: wv}
364 h.OnLoadStart(nil, stubFrame{main: true, url: "http://localhost:9"}, 0)
365 gotEvents = nil
366 h.OnLoadError(nil, stubFrame{main: true, url: "http://localhost:9"}, -102, "CONNECTION_REFUSED", "http://localhost:9")
367
368 require.Empty(t, gotEvents)
369}
370
371func TestOnLoadErrorAbortedNavigationDoesNotCommit(t *testing.T) {
372 wv := &WebView{ctx: context.Background()}
373 var gotEvents []port.LoadEvent
374 wv.SetCallbacks(&port.WebViewCallbacks{
375 OnLoadChanged: func(event port.LoadEvent) {
376 gotEvents = append(gotEvents, event)
377 },
378 })
379
380 h := &handlerSet{wv: wv}
381 h.OnLoadError(nil, stubFrame{main: true, url: "https://example.com/file.zip"}, cefErrAborted, "ERR_ABORTED", "https://example.com/file.zip")
382
383 require.Empty(t, gotEvents)
384 require.Empty(t, wv.URI())
385}
386
387func TestOnLoadErrorSubFrameDoesNotTerminateNavigation(t *testing.T) {
388 wv := &WebView{ctx: context.Background()}
389 var gotEvents []port.LoadEvent
390 wv.SetCallbacks(&port.WebViewCallbacks{
391 OnLoadChanged: func(event port.LoadEvent) {
392 gotEvents = append(gotEvents, event)
393 },
394 })
395
396 h := &handlerSet{wv: wv}
397 h.OnLoadError(nil, stubFrame{main: false, url: "http://localhost:9/frame"}, -102, "CONNECTION_REFUSED", "http://localhost:9/frame")
398
399 assert.Empty(t, gotEvents)
400}
401
402func TestOnTextSelectionChanged_ForwardsSelectionToClipboardOrchestrator(t *testing.T) {
403 recorder := &clipboardOrchestratorRecorder{}
404 orchestrator := portmocks.NewMockClipboardTextOrchestrator(t)
405 orchestrator.EXPECT().HandleSelectionUpdate(mock.Anything, mock.Anything).Run(func(_ context.Context, input dto.SelectionClipboardInput) {
406 recorder.mu.Lock()
407 defer recorder.mu.Unlock()
408 recorder.selection = input
409 recorder.selectionCalls++
410 }).Return(nil).Once()
411 zeroDelay := time.Duration(0)
412 wv := &WebView{
413 ctx: context.Background(),
414 id: 42,
415 selectionDebounceDelay: &zeroDelay,
416 engine: &Engine{
417 clipboardTextOrchestrator: orchestrator,
418 },
419 }
420 h := &selectionRenderHandlerForTest{wv: wv}
421
422 h.OnTextSelectionChanged(nil, "selected text", nil)
423
424 recorder.mu.Lock()
425 defer recorder.mu.Unlock()
426 require.Equal(t, 1, recorder.selectionCalls)
427 require.Equal(t, "selected text", recorder.selection.Text)
428 require.Equal(t, dto.ClipboardSourceCEF, recorder.selection.SourceEngine)
429 require.Equal(t, uint64(42), recorder.selection.ViewID)
430}
431
432func TestOnTextSelectionChanged_DebouncesAndCollapsesRapidUpdates(t *testing.T) {
433 recorder := &clipboardOrchestratorRecorder{}
434 scheduler := &controlledSelectionScheduler{}
435 orchestrator := portmocks.NewMockClipboardTextOrchestrator(t)
436 orchestrator.EXPECT().HandleSelectionUpdate(mock.Anything, mock.Anything).Run(func(_ context.Context, input dto.SelectionClipboardInput) {
437 recorder.mu.Lock()
438 defer recorder.mu.Unlock()
439 recorder.selection = input
440 recorder.selectionCalls++
441 }).Return(nil).Once()
442 wv := &WebView{
443 ctx: context.Background(),
444 id: 42,
445 selectionDebounceSchedule: scheduler.schedule,
446 engine: &Engine{
447 clipboardTextOrchestrator: orchestrator,
448 },
449 }
450 h := &selectionRenderHandlerForTest{wv: wv}
451
452 h.OnTextSelectionChanged(nil, "first selection", nil)
453 h.OnTextSelectionChanged(nil, "second selection", nil)
454
455 recorder.mu.Lock()
456 gotCalls := recorder.selectionCalls
457 recorder.mu.Unlock()
458 require.Zero(t, gotCalls)
459 require.Len(t, scheduler.callbacks, 2)
460
461 scheduler.fire(0)
462 recorder.mu.Lock()
463 gotCalls = recorder.selectionCalls
464 recorder.mu.Unlock()
465 require.Zero(t, gotCalls)
466
467 scheduler.fire(1)
468 recorder.mu.Lock()
469 defer recorder.mu.Unlock()
470 require.Equal(t, 1, recorder.selectionCalls)
471 require.Equal(t, "second selection", recorder.selection.Text)
472 require.Equal(t, "second selection", wv.selectedTextSnapshot())
473}
474
475func TestOnTextSelectionChanged_SuppressesAutoCopyWhenFocusedNodeEditableAndResumesWhenCleared(t *testing.T) {
476 recorder := &clipboardOrchestratorRecorder{}
477 scheduler := &controlledSelectionScheduler{}
478 orchestrator := portmocks.NewMockClipboardTextOrchestrator(t)
479 orchestrator.EXPECT().HandleSelectionUpdate(mock.Anything, mock.Anything).Run(func(_ context.Context, input dto.SelectionClipboardInput) {
480 recorder.mu.Lock()
481 defer recorder.mu.Unlock()
482 recorder.selection = input
483 recorder.selectionCalls++
484 }).Return(nil).Once()
485 wv := &WebView{
486 ctx: context.Background(),
487 id: 42,
488 selectionDebounceSchedule: scheduler.schedule,
489 engine: &Engine{
490 clipboardTextOrchestrator: orchestrator,
491 },
492 }
493 h := &selectionRenderHandlerForTest{wv: wv}
494 clientHandlers := &handlerSet{wv: wv}
495 frame := cefmocks.NewMockFrame(t)
496 oldFactory := newRendererBridgeProcessMessage
497 t.Cleanup(func() { newRendererBridgeProcessMessage = oldFactory })
498
499 newRendererBridgeProcessMessage = func(name string) purecef.ProcessMessage {
500 return newTestBridgeProcessMessage(name, true)
501 }
502 frame.EXPECT().SendProcessMessage(purecef.ProcessIDPidBrowser, mock.Anything).Run(func(_ purecef.ProcessID, message purecef.ProcessMessage) {
503 require.Equal(t, rendererBridgeMessageName, message.GetName())
504 args := message.GetArgumentList()
505 require.Equal(t, "editable_focus_changed", args.GetString(0))
506 require.Equal(t, "1", args.GetString(1))
507 clientHandlers.OnProcessMessageReceived(nil, nil, 0, message)
508 }).Once()
509 (&rendererBridgeProcessHandler{}).OnFocusedNodeChanged(nil, frame, stubEditableDomnode{editable: true})
510
511 h.OnTextSelectionChanged(nil, "editable selection", nil)
512 require.Equal(t, "editable selection", wv.selectedTextSnapshot())
513
514 recorder.mu.Lock()
515 gotCalls := recorder.selectionCalls
516 recorder.mu.Unlock()
517 require.Zero(t, gotCalls)
518 require.Empty(t, scheduler.callbacks)
519
520 newRendererBridgeProcessMessage = func(name string) purecef.ProcessMessage {
521 return newTestBridgeProcessMessage(name, false)
522 }
523 frame.EXPECT().SendProcessMessage(purecef.ProcessIDPidBrowser, mock.Anything).Run(func(_ purecef.ProcessID, message purecef.ProcessMessage) {
524 require.Equal(t, rendererBridgeMessageName, message.GetName())
525 args := message.GetArgumentList()
526 require.Equal(t, "editable_focus_changed", args.GetString(0))
527 require.Equal(t, "0", args.GetString(1))
528 clientHandlers.OnProcessMessageReceived(nil, nil, 0, message)
529 }).Once()
530 (&rendererBridgeProcessHandler{}).OnFocusedNodeChanged(nil, frame, stubEditableDomnode{editable: false})
531
532 h.OnTextSelectionChanged(nil, "free selection", nil)
533 require.Equal(t, "free selection", wv.selectedTextSnapshot())
534 recorder.mu.Lock()
535 gotCalls = recorder.selectionCalls
536 recorder.mu.Unlock()
537 require.Zero(t, gotCalls)
538 require.Len(t, scheduler.callbacks, 1)
539
540 scheduler.fire(0)
541 recorder.mu.Lock()
542 defer recorder.mu.Unlock()
543 require.Equal(t, 1, recorder.selectionCalls)
544 require.Equal(t, "free selection", recorder.selection.Text)
545}
546
547func TestSetEditableFocus_InvokesCallbackOnTransitions(t *testing.T) {
548 var states []bool
549 wv := &WebView{ctx: context.Background()}
550 wv.SetCallbacks(&port.WebViewCallbacks{
551 OnEditableFocusChanged: func(editable bool) {
552 states = append(states, editable)
553 },
554 })
555
556 wv.setEditableFocus(true)
557 wv.setEditableFocus(true)
558 wv.setEditableFocus(false)
559
560 require.Equal(t, []bool{true, false}, states)
561}
562
563func TestOnLoadStart_ClearsEditableFocus(t *testing.T) {
564 var states []bool
565 wv := &WebView{ctx: context.Background(), focusedEditable: true}
566 wv.SetCallbacks(&port.WebViewCallbacks{
567 OnEditableFocusChanged: func(editable bool) {
568 states = append(states, editable)
569 },
570 })
571 frame := cefmocks.NewMockFrame(t)
572 frame.EXPECT().IsMain().Return(true).Once()
573 frame.EXPECT().GetURL().Return("https://example.com").Maybe()
574
575 (&handlerSet{wv: wv}).OnLoadStart(nil, frame, 0)
576
577 require.Equal(t, []bool{false}, states)
578 require.False(t, wv.focusedEditable)
579}
580
581func TestDestroy_WithoutHostRunsCloseCallbacks(t *testing.T) {
582 called := false
583 wv := &WebView{closeCallbacks: []func(){func() { called = true }}}
584
585 wv.Destroy()
586
587 require.True(t, called)
588}
589
590func TestOnTextSelectionChanged_DoesNotEmitLateDebouncedUpdateAfterDestroy(t *testing.T) {
591 scheduler := &controlledSelectionScheduler{}
592 orchestrator := portmocks.NewMockClipboardTextOrchestrator(t)
593 wv := &WebView{
594 ctx: context.Background(),
595 id: 42,
596 selectionDebounceSchedule: scheduler.schedule,
597 engine: &Engine{
598 clipboardTextOrchestrator: orchestrator,
599 },
600 }
601 h := &selectionRenderHandlerForTest{wv: wv}
602
603 h.OnTextSelectionChanged(nil, "selected text", nil)
604 require.Len(t, scheduler.callbacks, 1)
605 wv.Destroy()
606 scheduler.fire(0)
607
608 orchestrator.AssertNotCalled(t, "HandleSelectionUpdate", mock.Anything, mock.Anything)
609 require.Equal(t, "selected text", wv.selectedTextSnapshot())
610}
611
612type stubEditableDomnode struct {
613 editable bool
614}
615
616func (n stubEditableDomnode) GetType() purecef.DomNodeType { return 0 }
617func (n stubEditableDomnode) IsText() bool { return false }
618func (n stubEditableDomnode) IsElement() bool { return true }
619func (n stubEditableDomnode) IsEditable() bool { return n.editable }
620func (n stubEditableDomnode) IsFormControlElement() bool { return false }
621func (n stubEditableDomnode) GetFormControlElementType() purecef.DomFormControlType { return 0 }
622func (n stubEditableDomnode) IsSame(that purecef.Domnode) bool {
623 other, ok := that.(stubEditableDomnode)
624 return ok && n == other
625}
626func (n stubEditableDomnode) GetName() string { return "" }
627func (n stubEditableDomnode) GetValue() string { return "" }
628func (n stubEditableDomnode) SetValue(string) int32 { return 0 }
629func (n stubEditableDomnode) GetAsMarkup() string { return "" }
630func (n stubEditableDomnode) GetDocument() purecef.Domdocument { return nil }
631func (n stubEditableDomnode) GetParent() purecef.Domnode { return nil }
632func (n stubEditableDomnode) GetPreviousSibling() purecef.Domnode { return nil }
633func (n stubEditableDomnode) GetNextSibling() purecef.Domnode { return nil }
634func (n stubEditableDomnode) HasChildren() bool { return false }
635func (n stubEditableDomnode) GetFirstChild() purecef.Domnode { return nil }
636func (n stubEditableDomnode) GetLastChild() purecef.Domnode { return nil }
637func (n stubEditableDomnode) GetElementTagName() string { return "" }
638func (n stubEditableDomnode) HasElementAttributes() bool { return false }
639func (n stubEditableDomnode) HasElementAttribute(string) bool { return false }
640func (n stubEditableDomnode) GetElementAttribute(string) string { return "" }
641func (n stubEditableDomnode) GetElementAttributes(purecef.StringMap) {}
642func (n stubEditableDomnode) SetElementAttribute(string, string) int32 { return 0 }
643func (n stubEditableDomnode) GetElementInnerText() string { return "" }
644func (n stubEditableDomnode) GetElementBounds() uintptr { return 0 }
645
646func TestOptionalHandlersAreAlwaysEnabled(t *testing.T) {
647 h := &handlerSet{}
648
649 // AudioHandler is always enabled (required for media decoding).
650 require.Same(t, h, h.GetAudioHandler())
651 require.Same(t, h, h.GetContextMenuHandler())
652 require.Nil(t, h.GetRenderHandler())
653}
654
655func TestRenderTextSelectionHookPreservesWebViewState(t *testing.T) {
656 wv := &WebView{ctx: context.Background()}
657
658 handleRenderTextSelectionChanged(wv, "selected via hook")
659
660 require.Equal(t, "selected via hook", wv.selectedTextSnapshot())
661}
662
663func TestGetDownloadHandler_EnabledWhenEngineConfigured(t *testing.T) {
664 eng := &Engine{}
665 require.NoError(t, eng.ConfigureDownloads(context.Background(), "/tmp/downloads", nil, stubDownloadPreparer{}))
666
667 h := &handlerSet{
668 wv: &WebView{
669 ctx: context.Background(),
670 engine: eng,
671 },
672 }
673
674 require.Same(t, h, h.GetDownloadHandler())
675}
676
677type selectionRenderHandlerForTest struct{ wv *WebView }
678
679func (h *selectionRenderHandlerForTest) OnTextSelectionChanged(_ purecef.Browser, selectedText string, _ *purecef.Range) {
680 handleRenderTextSelectionChanged(h.wv, selectedText)
681}