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/require"
12)
13
14func TestWebViewRearmPendingNavigationAfterBlankLoadEnd(t *testing.T) {
15 wv := &WebView{}
16 wv.setPendingNavigationLocked("https://example.com", time.Now())
17 intentID := wv.pendingIntentID
18 wv.mu.Lock()
19 wv.pendingIssued = true
20 wv.pendingURIStartedAt = time.Now()
21 wv.mu.Unlock()
22
23 require.True(t, wv.rearmPendingNavigationAfterBlankLoadEnd(intentID))
24 wv.mu.RLock()
25 require.Equal(t, intentID, wv.pendingIntentID)
26 require.False(t, wv.pendingIssued)
27 require.True(t, wv.pendingURIStartedAt.IsZero())
28 wv.mu.RUnlock()
29}
30
31func TestWebViewRearmPendingNavigationAfterBlankLoadEnd_NoOp(t *testing.T) {
32 wv := &WebView{}
33 require.False(t, wv.rearmPendingNavigationAfterBlankLoadEnd(0))
34 wv.setPendingNavigationLocked("https://example.com", time.Now())
35 require.False(t, wv.rearmPendingNavigationAfterBlankLoadEnd(wv.pendingIntentID))
36}
37
38func TestWebViewRearmPendingNavigationAfterBlankLoadEnd_DoesNotRearmOrReplayReplacementIntent(t *testing.T) {
39 browser := cefmocks.NewMockBrowser(t)
40 frame := cefmocks.NewMockFrame(t)
41 frame.EXPECT().GetURL().Return("").Once()
42 frame.EXPECT().LoadURL("https://new.example").Once()
43 browser.EXPECT().GetMainFrame().Return(frame).Once()
44 browser.EXPECT().GetIdentifier().Return(int32(1)).Twice()
45 wv := &WebView{ctx: context.Background(), browser: browser}
46 wv.setPendingNavigationLocked("https://old.example", time.Now())
47 observedIntentID := wv.pendingIntentID
48
49 // A newer navigation is installed and submitted after the handler snapshots
50 // the old intent but before its rearm helper acquires the mutex.
51 wv.setPendingNavigationLocked("https://new.example", time.Now())
52 newIntentID := wv.pendingIntentID
53 wv.replayPendingNavigationForIntent(0, newIntentID)
54 wv.mu.RLock()
55 startedAt := wv.pendingURIStartedAt
56 wv.mu.RUnlock()
57
58 require.False(t, wv.rearmPendingNavigationAfterBlankLoadEnd(observedIntentID))
59
60 oldTask := cefNewTask
61 oldPost := cefPostTask
62 defer func() { cefNewTask, cefPostTask = oldTask, oldPost }()
63 cefNewTask = func(task purecef.Task) purecef.Task { return task }
64 cefPostTask = func(_ purecef.ThreadID, task purecef.Task) int32 {
65 task.Execute()
66 return 1
67 }
68 wv.schedulePendingNavigationReplayForIntent(0, observedIntentID)
69
70 wv.mu.RLock()
71 defer wv.mu.RUnlock()
72 require.Equal(t, "https://new.example", wv.pendingURI)
73 require.Equal(t, newIntentID, wv.pendingIntentID)
74 require.True(t, wv.pendingIssued, "replacement intent must remain issued")
75 require.Equal(t, startedAt, wv.pendingURIStartedAt, "replacement intent must not be rearmed")
76}
77
78func TestWebViewReplayPendingNavigation_LoadsQueuedURIWhenMainFrameAvailable(t *testing.T) {
79 browser := cefmocks.NewMockBrowser(t)
80 frame := cefmocks.NewMockFrame(t)
81 frame.EXPECT().GetURL().Return("").Once()
82 frame.EXPECT().LoadURL("https://github.com/bnema").Once()
83 browser.EXPECT().GetMainFrame().Return(frame).Once()
84 browser.EXPECT().GetIdentifier().Return(int32(1)).Twice()
85
86 wv := &WebView{ctx: context.Background(), browser: browser}
87 wv.setPendingNavigationLocked("https://github.com/bnema", time.Now())
88 wv.replayPendingNavigationForIntent(0, wv.pendingIntentID)
89
90 wv.mu.RLock()
91 defer wv.mu.RUnlock()
92 require.Equal(t, "https://github.com/bnema", wv.pendingURI)
93}
94
95// TestWebViewClaimPendingNavigationSubmission_RejectsInstanceReplacement
96// reproduces a browser swap landing between the identifier check and the
97// final claim: the replacement carries the same identifier, so only the
98// instance-identity guard can catch it. The claim must report replaced
99// without marking the intent issued, leaving the retry path able to submit
100// against the current browser.
101func TestWebViewClaimPendingNavigationSubmission_RejectsInstanceReplacement(t *testing.T) {
102 browserA := cefmocks.NewMockBrowser(t)
103 browserB := cefmocks.NewMockBrowser(t)
104 entered := make(chan struct{})
105 var enterOnce sync.Once
106 proceed := make(chan struct{})
107 browserA.EXPECT().GetIdentifier().RunAndReturn(func() int32 {
108 enterOnce.Do(func() { close(entered) })
109 <-proceed
110 return int32(7)
111 }).Once()
112
113 wv := &WebView{ctx: context.Background(), browser: browserA}
114 wv.setPendingNavigationLocked("https://example.com", time.Now())
115 intentID := wv.pendingIntentID
116 require.NotZero(t, intentID)
117
118 claimed := make(chan pendingClaimResult, 1)
119 go func() {
120 _, result := wv.claimPendingNavigationSubmission(intentID, int32(7))
121 claimed <- result
122 }()
123
124 select {
125 case <-entered:
126 case <-time.After(10 * time.Second):
127 t.Fatal("claim did not reach the identifier check")
128 }
129
130 // Swap the attached browser while the claim is past the snapshot: same
131 // identifier, different instance.
132 wv.mu.Lock()
133 wv.browser = browserB
134 wv.mu.Unlock()
135 close(proceed)
136
137 select {
138 case result := <-claimed:
139 require.Equal(t, pendingClaimReplaced, result, "instance replacement must not claim the intent")
140 case <-time.After(10 * time.Second):
141 t.Fatal("claim did not return after the swap")
142 }
143
144 wv.mu.RLock()
145 defer wv.mu.RUnlock()
146 require.False(t, wv.pendingIssued, "replaced claim must leave the intent unissued for retry")
147 require.Equal(t, intentID, wv.pendingIntentID)
148}
149
150func TestWebViewReplayPendingNavigation_RetriesWhenMainFrameUnavailable(t *testing.T) {
151 browser := cefmocks.NewMockBrowser(t)
152 browser.EXPECT().GetMainFrame().Return(purecef.Frame(nil)).Once()
153
154 oldTask := cefNewTask
155 oldDelayed := cefPostDelayedTask
156 defer func() {
157 cefNewTask = oldTask
158 cefPostDelayedTask = oldDelayed
159 }()
160 cefNewTask = func(task purecef.Task) purecef.Task { return task }
161
162 scheduled := false
163 cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, delayMs int64) int32 {
164 require.Equal(t, purecef.ThreadIDTidUi, threadID)
165 require.NotNil(t, task)
166 require.Equal(t, int64(pendingNavigationRetryDelay/time.Millisecond), delayMs)
167 scheduled = true
168 return 1
169 }
170
171 wv := &WebView{ctx: context.Background(), browser: browser}
172 wv.setPendingNavigationLocked("https://github.com/bnema", time.Now())
173 wv.replayPendingNavigationForIntent(0, wv.pendingIntentID)
174
175 require.True(t, scheduled)
176 wv.mu.RLock()
177 defer wv.mu.RUnlock()
178 require.Equal(t, "https://github.com/bnema", wv.pendingURI)
179}
180
181func TestWebViewSchedulePendingNavigationReplay_RetriesWhenTaskPostFails(t *testing.T) {
182 browser := cefmocks.NewMockBrowser(t)
183
184 oldTask := cefNewTask
185 oldPost := cefPostTask
186 oldDelayed := cefPostDelayedTask
187 oldAfter := cefScheduleAfter
188 defer func() {
189 cefNewTask = oldTask
190 cefPostTask = oldPost
191 cefPostDelayedTask = oldDelayed
192 cefScheduleAfter = oldAfter
193 }()
194 cefNewTask = func(task purecef.Task) purecef.Task { return task }
195
196 retried := false
197 cefPostTask = func(threadID purecef.ThreadID, task purecef.Task) int32 {
198 require.Equal(t, purecef.ThreadIDTidUi, threadID)
199 require.NotNil(t, task)
200 return 0
201 }
202 cefScheduleAfter = func(delay time.Duration, fn func()) {
203 require.Equal(t, pendingNavigationRetryDelay, delay)
204 fn()
205 }
206 cefPostDelayedTask = func(threadID purecef.ThreadID, task purecef.Task, delayMs int64) int32 {
207 require.Equal(t, purecef.ThreadIDTidUi, threadID)
208 require.NotNil(t, task)
209 require.Equal(t, int64(pendingNavigationRetryDelay/time.Millisecond), delayMs)
210 retried = true
211 return 1
212 }
213
214 wv := &WebView{ctx: context.Background(), browser: browser}
215 wv.setPendingNavigationLocked("https://github.com/bnema", time.Now())
216 wv.schedulePendingNavigationReplay()
217
218 require.True(t, retried)
219}
220
221func TestWebViewSchedulePendingNavigationReplay_RetryStaysOnCapturedIntent(t *testing.T) {
222 browser := cefmocks.NewMockBrowser(t)
223 wv := &WebView{ctx: context.Background(), browser: browser}
224 wv.setPendingNavigationLocked("https://old.example", time.Now())
225
226 oldTask := cefNewTask
227 oldPost := cefPostTask
228 oldDelayed := cefPostDelayedTask
229 oldAfter := cefScheduleAfter
230 t.Cleanup(func() {
231 cefNewTask = oldTask
232 cefPostTask = oldPost
233 cefPostDelayedTask = oldDelayed
234 cefScheduleAfter = oldAfter
235 })
236 cefNewTask = func(task purecef.Task) purecef.Task { return task }
237 var retry func()
238 cefPostTask = func(_ purecef.ThreadID, _ purecef.Task) int32 { return 0 }
239 cefScheduleAfter = func(delay time.Duration, fn func()) {
240 require.Equal(t, pendingNavigationRetryDelay, delay)
241 retry = fn
242 }
243 cefPostDelayedTask = func(_ purecef.ThreadID, task purecef.Task, delayMs int64) int32 {
244 require.Equal(t, int64(pendingNavigationRetryDelay/time.Millisecond), delayMs)
245 task.Execute()
246 return 1
247 }
248
249 wv.schedulePendingNavigationReplay()
250 require.NotNil(t, retry)
251 wv.setPendingNavigationLocked("https://new.example", time.Now())
252 retry()
253
254 browser.AssertNotCalled(t, "GetMainFrame")
255 wv.mu.RLock()
256 defer wv.mu.RUnlock()
257 require.Equal(t, "https://new.example", wv.pendingURI)
258 require.False(t, wv.pendingIssued)
259}
260
261func TestWebViewReplayPendingNavigation_UsesCurrentBrowserAtExecutionTime(t *testing.T) {
262 staleBrowser := cefmocks.NewMockBrowser(t)
263 activeBrowser := cefmocks.NewMockBrowser(t)
264 frame := cefmocks.NewMockFrame(t)
265 frame.EXPECT().GetURL().Return("").Once()
266 frame.EXPECT().LoadURL("https://github.com/bnema").Once()
267 activeBrowser.EXPECT().GetMainFrame().Return(frame).Once()
268 activeBrowser.EXPECT().GetIdentifier().Return(int32(2)).Twice()
269
270 oldTask := cefNewTask
271 oldPost := cefPostTask
272 defer func() {
273 cefNewTask = oldTask
274 cefPostTask = oldPost
275 }()
276 var scheduled purecef.Task
277 cefNewTask = func(task purecef.Task) purecef.Task { return task }
278 cefPostTask = func(threadID purecef.ThreadID, task purecef.Task) int32 {
279 require.Equal(t, purecef.ThreadIDTidUi, threadID)
280 scheduled = task
281 return 1
282 }
283
284 wv := &WebView{ctx: context.Background(), browser: staleBrowser}
285 wv.setPendingNavigationLocked("https://github.com/bnema", time.Now())
286 wv.schedulePendingNavigationReplay()
287 wv.mu.Lock()
288 wv.browser = activeBrowser
289 wv.mu.Unlock()
290
291 require.NotNil(t, scheduled)
292 scheduled.Execute()
293}
294
295func TestWebViewLoadURI_QueuesPendingNavigationReplayForExistingBrowser(t *testing.T) {
296 browser := cefmocks.NewMockBrowser(t)
297 frame := cefmocks.NewMockFrame(t)
298 browser.EXPECT().GetMainFrame().Return(frame).Once()
299 browser.EXPECT().GetIdentifier().Return(int32(1)).Twice()
300 frame.EXPECT().GetURL().Return("").Once()
301 frame.EXPECT().LoadURL("github.com/bnema").Once()
302
303 oldTask := cefNewTask
304 oldPost := cefPostTask
305 defer func() {
306 cefNewTask = oldTask
307 cefPostTask = oldPost
308 }()
309 cefNewTask = func(task purecef.Task) purecef.Task { return task }
310 cefPostTask = func(threadID purecef.ThreadID, task purecef.Task) int32 {
311 require.Equal(t, purecef.ThreadIDTidUi, threadID)
312 require.NotNil(t, task)
313 task.Execute()
314 return 1
315 }
316
317 wv := &WebView{ctx: context.Background(), browser: browser}
318 require.NoError(t, wv.LoadURI(context.Background(), "github.com/bnema"))
319
320 wv.mu.RLock()
321 defer wv.mu.RUnlock()
322 require.Equal(t, "github.com/bnema", wv.pendingURI)
323}
324
325func TestWebViewUpdateURI_ClearsMatchingPendingNavigation(t *testing.T) {
326 wv := &WebView{ctx: context.Background(), pendingURI: "https://github.com/bnema"}
327
328 wv.updateURI("https://github.com/bnema")
329
330 require.Empty(t, wv.pendingNavigationURI())
331}
332
333func TestWebViewUpdateLoadState_ClearsStalePendingNavigationAfterStartedRedirectCompletes(t *testing.T) {
334 wv := &WebView{ctx: context.Background()}
335 pending := "https://duckduckgo.com/?q=pi+agent+reddit"
336 startedAt := time.Now().Add(-time.Second)
337 wv.mu.Lock()
338 wv.setPendingNavigationLocked(pending, startedAt.Add(-time.Millisecond))
339 wv.markPendingNavigationStartedLocked(pending, startedAt)
340 wv.mu.Unlock()
341 wv.updateURI("https://www.reddit.com/r/somewhere/")
342
343 wv.updateLoadState(false, true, false)
344
345 require.Empty(t, wv.pendingNavigationURI())
346}
347
348func TestWebViewUpdateLoadState_KeepsPendingNavigationBeforeReplayStarts(t *testing.T) {
349 wv := &WebView{ctx: context.Background()}
350 pending := "https://duckduckgo.com/?q=pi+agent+reddit"
351 wv.updateURI("https://old.example/still-finishing")
352 wv.mu.Lock()
353 wv.setPendingNavigationLocked(pending, time.Now())
354 wv.mu.Unlock()
355
356 wv.updateLoadState(false, true, false)
357
358 require.Equal(t, pending, wv.pendingNavigationURI())
359}
360
361func TestWebViewUpdateLoadState_DoesNotClearPendingNavigationWhenOnlyOlderAddressWasObserved(t *testing.T) {
362 wv := &WebView{ctx: context.Background()}
363 pending := "https://duckduckgo.com/?q=pi+agent+reddit"
364 observedAt := time.Now()
365 wv.mu.Lock()
366 wv.uri = "https://old.example/final"
367 wv.loadDiagLastAddressAt = observedAt
368 wv.setPendingNavigationLocked(pending, observedAt.Add(time.Millisecond))
369 wv.markPendingNavigationStartedLocked(pending, observedAt.Add(time.Millisecond))
370 wv.mu.Unlock()
371
372 wv.updateLoadState(false, true, false)
373
374 require.Equal(t, pending, wv.pendingNavigationURI())
375}
376
377func TestWebViewUpdateLoadState_KeepsPendingNavigationWhileLoading(t *testing.T) {
378 wv := &WebView{ctx: context.Background()}
379 pending := "https://duckduckgo.com/?q=pi+agent+reddit"
380 startedAt := time.Now().Add(-time.Second)
381 wv.mu.Lock()
382 wv.setPendingNavigationLocked(pending, startedAt.Add(-time.Millisecond))
383 wv.markPendingNavigationStartedLocked(pending, startedAt)
384 wv.mu.Unlock()
385 wv.updateURI("https://www.reddit.com/r/somewhere/")
386
387 wv.updateLoadState(true, true, false)
388
389 require.Equal(t, pending, wv.pendingNavigationURI())
390}
391
392// TestWebViewReplayPendingNavigation_SubmitsOncePerIntent reproduces the
393// fresh-window race: queue an intent, run OnAfterCreated replay with the
394// frame URL still blank, then deliver blank OnLoadEnd before commit. Exactly
395// one LoadURL must be issued for the intent.
396func TestWebViewReplayPendingNavigation_SubmitsOncePerIntent(t *testing.T) {
397 browser := cefmocks.NewMockBrowser(t)
398 frame := cefmocks.NewMockFrame(t)
399 frame.EXPECT().GetURL().Return("about:blank")
400 frame.EXPECT().LoadURL("https://example.com/target").Once()
401 browser.EXPECT().GetMainFrame().Return(frame)
402 browser.EXPECT().GetIdentifier().Return(int32(1)).Twice()
403
404 oldTask := cefNewTask
405 oldPost := cefPostTask
406 defer func() {
407 cefNewTask = oldTask
408 cefPostTask = oldPost
409 }()
410 cefNewTask = func(task purecef.Task) purecef.Task { return task }
411 var scheduled []purecef.Task
412 cefPostTask = func(_ purecef.ThreadID, task purecef.Task) int32 {
413 scheduled = append(scheduled, task)
414 return 1
415 }
416
417 wv := &WebView{ctx: context.Background(), browser: browser}
418 wv.setPendingNavigationLocked("https://example.com/target", time.Now())
419 // OnAfterCreated schedules replay...
420 wv.schedulePendingNavigationReplay()
421 // ...and blank OnLoadEnd schedules a second replay before commit.
422 wv.schedulePendingNavigationReplay()
423 require.Len(t, scheduled, 2)
424 for _, task := range scheduled {
425 task.Execute()
426 }
427}
428
429// TestWebViewReplayPendingNavigation_StaleIntentDoesNotResubmit verifies a
430// rapid replacement URL invalidates the previously scheduled task.
431func TestWebViewReplayPendingNavigation_StaleIntentDoesNotResubmit(t *testing.T) {
432 browser := cefmocks.NewMockBrowser(t)
433 frame := cefmocks.NewMockFrame(t)
434 frame.EXPECT().GetURL().Return("").Once()
435 frame.EXPECT().LoadURL("https://example.com/second").Once()
436 browser.EXPECT().GetMainFrame().Return(frame).Once()
437 browser.EXPECT().GetIdentifier().Return(int32(1)).Twice()
438
439 oldTask := cefNewTask
440 oldPost := cefPostTask
441 defer func() {
442 cefNewTask = oldTask
443 cefPostTask = oldPost
444 }()
445 cefNewTask = func(task purecef.Task) purecef.Task { return task }
446 var scheduled []purecef.Task
447 cefPostTask = func(_ purecef.ThreadID, task purecef.Task) int32 {
448 scheduled = append(scheduled, task)
449 return 1
450 }
451
452 wv := &WebView{ctx: context.Background(), browser: browser}
453 wv.setPendingNavigationLocked("https://example.com/first", time.Now())
454 wv.schedulePendingNavigationReplay()
455 require.Len(t, scheduled, 1)
456 stale := scheduled[0]
457 // Replacement installs a new intent; the stale task must not submit.
458 wv.setPendingNavigationLocked("https://example.com/second", time.Now())
459 stale.Execute()
460 // Current intent still unissued; a fresh replay submits it once.
461 wv.replayPendingNavigationForIntent(0, wv.pendingIntentID)
462}
463
464// TestWebViewReplayPendingNavigation_RetriesSameIntentOnFailedPost ensures a
465// failed task post retries without consuming or duplicating the intent.
466func TestWebViewReplayPendingNavigation_RetriesSameIntentOnFailedPost(t *testing.T) {
467 browser := cefmocks.NewMockBrowser(t)
468
469 oldTask := cefNewTask
470 oldPost := cefPostTask
471 oldAfter := cefScheduleAfter
472 defer func() {
473 cefNewTask = oldTask
474 cefPostTask = oldPost
475 cefScheduleAfter = oldAfter
476 }()
477 cefNewTask = func(task purecef.Task) purecef.Task { return task }
478 cefPostTask = func(_ purecef.ThreadID, _ purecef.Task) int32 {
479 return 0
480 }
481 scheduledAfter := false
482 cefScheduleAfter = func(delay time.Duration, _ func()) {
483 require.Equal(t, pendingNavigationRetryDelay, delay)
484 scheduledAfter = true
485 }
486
487 wv := &WebView{ctx: context.Background(), browser: browser}
488 wv.setPendingNavigationLocked("https://example.com/target", time.Now())
489 wv.mu.RLock()
490 intent := wv.pendingIntentID
491 wv.mu.RUnlock()
492 wv.schedulePendingNavigationReplay()
493 require.True(t, scheduledAfter)
494 wv.mu.RLock()
495 defer wv.mu.RUnlock()
496 require.Equal(t, intent, wv.pendingIntentID)
497 require.False(t, wv.pendingIssued)
498 require.Equal(t, "https://example.com/target", wv.pendingURI)
499}
500
501// TestWebViewSetPendingNavigation_RepeatedSameURLInstallsNewIntent ensures
502// explicit reloads and repeated navigations still work: each request is a
503// new intent even for an identical URL.
504func TestWebViewSetPendingNavigation_RepeatedSameURLInstallsNewIntent(t *testing.T) {
505 wv := &WebView{ctx: context.Background()}
506 wv.setPendingNavigationLocked("https://example.com/page", time.Now())
507 first := wv.pendingIntentID
508 require.NotZero(t, first)
509 wv.mu.Lock()
510 wv.pendingIssued = true
511 wv.mu.Unlock()
512 wv.setPendingNavigationLocked("https://example.com/page", time.Now())
513 require.NotEqual(t, first, wv.pendingIntentID)
514 require.False(t, wv.pendingIssued)
515}
516
517// TestWebViewReplayPendingNavigation_SameURLReloadSubmits verifies an
518// unissued explicit intent for the currently displayed URL proceeds to
519// LoadURL instead of being cleared as already active.
520func TestWebViewReplayPendingNavigation_SameURLReloadSubmits(t *testing.T) {
521 browser := cefmocks.NewMockBrowser(t)
522 frame := cefmocks.NewMockFrame(t)
523 frame.EXPECT().GetURL().Return("https://example.com/page").Once()
524 frame.EXPECT().LoadURL("https://example.com/page").Once()
525 browser.EXPECT().GetMainFrame().Return(frame).Once()
526 browser.EXPECT().GetIdentifier().Return(int32(1)).Twice()
527
528 wv := &WebView{ctx: context.Background(), browser: browser}
529 wv.setPendingNavigationLocked("https://example.com/page", time.Now())
530 wv.replayPendingNavigationForIntent(0, wv.pendingIntentID)
531
532 wv.mu.RLock()
533 defer wv.mu.RUnlock()
534 require.True(t, wv.pendingIssued)
535}
536
537func TestWebViewReplayPendingNavigation_ClearsIssuanceWhenBrowserReplacedAfterClaim(t *testing.T) {
538 const uri = "https://example.com/replaced-after-claim"
539 browserA := cefmocks.NewMockBrowser(t)
540 browserB := cefmocks.NewMockBrowser(t)
541 frameA := cefmocks.NewMockFrame(t)
542 frameB := cefmocks.NewMockFrame(t)
543
544 browserA.EXPECT().GetIdentifier().Return(int32(7)).Twice()
545 browserA.EXPECT().GetMainFrame().Return(frameA).Once()
546 frameA.EXPECT().GetURL().Return("").Once()
547 entered := make(chan struct{})
548 var enterOnce sync.Once
549 proceed := make(chan struct{})
550 frameA.EXPECT().LoadURL(uri).RunAndReturn(func(string) {
551 enterOnce.Do(func() { close(entered) })
552 <-proceed
553 }).Once()
554
555 browserB.EXPECT().GetIdentifier().Return(int32(7)).Twice()
556 browserB.EXPECT().GetMainFrame().Return(frameB).Once()
557 frameB.EXPECT().GetURL().Return("").Once()
558 frameB.EXPECT().LoadURL(uri).Once()
559
560 oldTask := cefNewTask
561 oldDelayed := cefPostDelayedTask
562 defer func() {
563 cefNewTask = oldTask
564 cefPostDelayedTask = oldDelayed
565 }()
566 cefNewTask = func(task purecef.Task) purecef.Task { return task }
567 var scheduled purecef.Task
568 // The post-claim retry schedules with attempt+1, which takes the
569 // delayed-task path; capture it to drive the retry synchronously.
570 cefPostDelayedTask = func(_ purecef.ThreadID, task purecef.Task, _ int64) int32 {
571 scheduled = task
572 return 1
573 }
574
575 wv := &WebView{ctx: context.Background(), browser: browserA}
576 wv.setPendingNavigationLocked(uri, time.Now())
577 intentID := wv.pendingIntentID
578
579 done := make(chan struct{})
580 go func() {
581 defer close(done)
582 wv.replayPendingNavigationForIntent(0, intentID)
583 }()
584
585 select {
586 case <-entered:
587 case <-time.After(10 * time.Second):
588 t.Fatal("replay did not reach LoadURL")
589 }
590
591 // Replace the attached browser while LoadURL is in flight on the stale
592 // frame: the claim already marked the intent issued.
593 wv.mu.Lock()
594 wv.browser = browserB
595 wv.mu.Unlock()
596 close(proceed)
597
598 select {
599 case <-done:
600 case <-time.After(10 * time.Second):
601 t.Fatal("replay did not return after LoadURL")
602 }
603
604 wv.mu.RLock()
605 require.False(t, wv.pendingIssued, "post-claim replacement must reopen the issuance for retry")
606 require.Equal(t, intentID, wv.pendingIntentID, "the intent itself must survive the replacement")
607 wv.mu.RUnlock()
608 require.NotNil(t, scheduled, "a retry against the current browser must be scheduled")
609
610 // Driving the retry submits the surviving intent to the new browser.
611 scheduled.Execute()
612 wv.mu.RLock()
613 defer wv.mu.RUnlock()
614 require.True(t, wv.pendingIssued, "retry must issue the intent on the current browser")
615}