webview_navigation_test.go RawHistoryFolder

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}

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