detached f97971b

fix(cef): replay startup navigation dropped during about:blank commit (#449)

commit
f97971bd99edf48b1c29c35376a31ba3de48a398
author
bnema · · 2026-09-23 04:53 UTC
committer
GitHub · · 2026-09-23 04:53 UTC
parent
0d014a0
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
3 files · +154 -22
M internal/infrastructure/cef/webview.go +37 -12
@@ -413,7 +413,7 @@ func (wv *WebView) LoadURI(_ context.Context, uri string) error {
413413 if browser == nil {
414414 return nil
415415 }
416- wv.schedulePendingNavigationReplay(0)
416+ wv.schedulePendingNavigationReplay()
417417 return nil
418418 })
419419 }
@@ -786,7 +786,7 @@ func (wv *WebView) PrimePopupNavigation(uri string) {
786786 wv.setPendingNavigationLocked(actualURI, time.Now())
787787 wv.mu.Unlock()
788788 if browser != nil {
789- wv.schedulePendingNavigationReplay(0)
789+ wv.schedulePendingNavigationReplay()
790790 }
791791 }
792792
@@ -2061,6 +2061,15 @@ func (wv *WebView) pendingNavigationURI() string {
20612061 return wv.pendingURI
20622062 }
20632063
2064+func (wv *WebView) pendingNavigationSnapshot() (string, uint64) {
2065+ if wv == nil {
2066+ return "", 0
2067+ }
2068+ wv.mu.RLock()
2069+ defer wv.mu.RUnlock()
2070+ return wv.pendingURI, wv.pendingIntentID
2071+}
2072+
20642073 func (wv *WebView) setPendingNavigationLocked(uri string, at time.Time) {
20652074 wv.pendingURI = uri
20662075 wv.pendingURIStartedAt = time.Time{}
@@ -2102,6 +2111,20 @@ func (wv *WebView) clearPendingNavigationIfEquivalentLocked(uri string) bool {
21022111 return true
21032112 }
21042113
2114+// rearmPendingNavigationAfterBlankLoadEnd reopens an issued intent after the
2115+// initial about:blank commit: Chromium can drop LoadURL while that commit is in progress.
2116+// The intent ID prevents a stale load-end callback from rearming a newer request.
2117+func (wv *WebView) rearmPendingNavigationAfterBlankLoadEnd(intentID uint64) bool {
2118+ wv.mu.Lock()
2119+ defer wv.mu.Unlock()
2120+ if intentID == 0 || wv.pendingIntentID != intentID || wv.pendingURI == "" || !wv.pendingIssued {
2121+ return false
2122+ }
2123+ wv.pendingIssued = false
2124+ wv.pendingURIStartedAt = time.Time{}
2125+ return true
2126+}
2127+
21052128 func (wv *WebView) hasObservedAddressForPendingNavigationLocked() bool {
21062129 if strings.TrimSpace(wv.pendingURI) == "" || strings.TrimSpace(wv.uri) == "" {
21072130 return false
@@ -2112,16 +2135,18 @@ func (wv *WebView) hasObservedAddressForPendingNavigationLocked() bool {
21122135 return !wv.loadDiagLastAddressAt.Before(wv.pendingURIStartedAt)
21132136 }
21142137
2115-func (wv *WebView) schedulePendingNavigationReplay(attempt int) {
2116- if wv == nil || wv.destroyed.Load() {
2138+func (wv *WebView) schedulePendingNavigationReplay() {
2139+ if wv == nil {
21172140 return
21182141 }
21192142 // Capture the intent at schedule time; a rapid replacement installs a new
21202143 // intent and stale tasks for the old ID must return without submitting.
2121- wv.mu.RLock()
2122- intentID := wv.pendingIntentID
2123- wv.mu.RUnlock()
2124- if intentID == 0 {
2144+ _, intentID := wv.pendingNavigationSnapshot()
2145+ wv.schedulePendingNavigationReplayForIntent(0, intentID)
2146+}
2147+
2148+func (wv *WebView) schedulePendingNavigationReplayForIntent(attempt int, intentID uint64) {
2149+ if wv == nil || wv.destroyed.Load() || intentID == 0 {
21252150 return
21262151 }
21272152 task := cefNewTask(cefTaskFunc(func() {
@@ -2153,7 +2178,7 @@ func (wv *WebView) schedulePendingNavigationReplay(attempt int) {
21532178 return
21542179 }
21552180 cefScheduleAfter(pendingNavigationRetryDelay, func() {
2156- wv.schedulePendingNavigationReplay(attempt + 1)
2181+ wv.schedulePendingNavigationReplayForIntent(attempt+1, intentID)
21572182 })
21582183 }
21592184
@@ -2196,7 +2221,7 @@ func (wv *WebView) replayPendingNavigationForIntent(attempt int, intentID uint64
21962221 Str("uri", logging.TruncateURL(uri, logging.PermissionLogURLMaxLen)).
21972222 Msg("cef: pending navigation replay waiting for main frame")
21982223 }
2199- wv.schedulePendingNavigationReplay(attempt + 1)
2224+ wv.schedulePendingNavigationReplayForIntent(attempt+1, intentID)
22002225 return
22012226 }
22022227 currentURL := frame.GetURL()
@@ -2229,7 +2254,7 @@ func (wv *WebView) replayPendingNavigationForIntent(attempt int, intentID uint64
22292254 case pendingClaimReplaced:
22302255 // Browser was replaced between frame acquisition and claim; retry
22312256 // the same intent so the new browser is used at execution time.
2232- wv.schedulePendingNavigationReplay(attempt + 1)
2257+ wv.schedulePendingNavigationReplayForIntent(attempt+1, intentID)
22332258 case pendingClaimReady:
22342259 frame.LoadURL(submitURI)
22352260 wv.resubmitIfBrowserReplaced(attempt, intentID, browser)
@@ -2256,7 +2281,7 @@ func (wv *WebView) resubmitIfBrowserReplaced(attempt int, intentID uint64, submi
22562281 }
22572282 wv.mu.Unlock()
22582283 if replaced {
2259- wv.schedulePendingNavigationReplay(attempt + 1)
2284+ wv.schedulePendingNavigationReplayForIntent(attempt+1, intentID)
22602285 }
22612286 }
22622287
@@ -413,7 +413,7 @@ func (wv *WebView) LoadURI(_ context.Context, uri string) error {
413 if browser == nil {413 if browser == nil {
414 return nil414 return nil
415 }415 }
416- wv.schedulePendingNavigationReplay(0)416+ wv.schedulePendingNavigationReplay()
417 return nil417 return nil
418 })418 })
419 }419 }
@@ -786,7 +786,7 @@ func (wv *WebView) PrimePopupNavigation(uri string) {
786 wv.setPendingNavigationLocked(actualURI, time.Now())786 wv.setPendingNavigationLocked(actualURI, time.Now())
787 wv.mu.Unlock()787 wv.mu.Unlock()
788 if browser != nil {788 if browser != nil {
789- wv.schedulePendingNavigationReplay(0)789+ wv.schedulePendingNavigationReplay()
790 }790 }
791 }791 }
792 792
@@ -2061,6 +2061,15 @@ func (wv *WebView) pendingNavigationURI() string {
2061 return wv.pendingURI2061 return wv.pendingURI
2062 }2062 }
2063 2063
2064+func (wv *WebView) pendingNavigationSnapshot() (string, uint64) {
2065+ if wv == nil {
2066+ return "", 0
2067+ }
2068+ wv.mu.RLock()
2069+ defer wv.mu.RUnlock()
2070+ return wv.pendingURI, wv.pendingIntentID
2071+}
2072+
2064 func (wv *WebView) setPendingNavigationLocked(uri string, at time.Time) {2073 func (wv *WebView) setPendingNavigationLocked(uri string, at time.Time) {
2065 wv.pendingURI = uri2074 wv.pendingURI = uri
2066 wv.pendingURIStartedAt = time.Time{}2075 wv.pendingURIStartedAt = time.Time{}
@@ -2102,6 +2111,20 @@ func (wv *WebView) clearPendingNavigationIfEquivalentLocked(uri string) bool {
2102 return true2111 return true
2103 }2112 }
2104 2113
2114+// rearmPendingNavigationAfterBlankLoadEnd reopens an issued intent after the
2115+// initial about:blank commit: Chromium can drop LoadURL while that commit is in progress.
2116+// The intent ID prevents a stale load-end callback from rearming a newer request.
2117+func (wv *WebView) rearmPendingNavigationAfterBlankLoadEnd(intentID uint64) bool {
2118+ wv.mu.Lock()
2119+ defer wv.mu.Unlock()
2120+ if intentID == 0 || wv.pendingIntentID != intentID || wv.pendingURI == "" || !wv.pendingIssued {
2121+ return false
2122+ }
2123+ wv.pendingIssued = false
2124+ wv.pendingURIStartedAt = time.Time{}
2125+ return true
2126+}
2127+
2105 func (wv *WebView) hasObservedAddressForPendingNavigationLocked() bool {2128 func (wv *WebView) hasObservedAddressForPendingNavigationLocked() bool {
2106 if strings.TrimSpace(wv.pendingURI) == "" || strings.TrimSpace(wv.uri) == "" {2129 if strings.TrimSpace(wv.pendingURI) == "" || strings.TrimSpace(wv.uri) == "" {
2107 return false2130 return false
@@ -2112,16 +2135,18 @@ func (wv *WebView) hasObservedAddressForPendingNavigationLocked() bool {
2112 return !wv.loadDiagLastAddressAt.Before(wv.pendingURIStartedAt)2135 return !wv.loadDiagLastAddressAt.Before(wv.pendingURIStartedAt)
2113 }2136 }
2114 2137
2115-func (wv *WebView) schedulePendingNavigationReplay(attempt int) {2138+func (wv *WebView) schedulePendingNavigationReplay() {
2116- if wv == nil || wv.destroyed.Load() {2139+ if wv == nil {
2117 return2140 return
2118 }2141 }
2119 // Capture the intent at schedule time; a rapid replacement installs a new2142 // Capture the intent at schedule time; a rapid replacement installs a new
2120 // intent and stale tasks for the old ID must return without submitting.2143 // intent and stale tasks for the old ID must return without submitting.
2121- wv.mu.RLock()2144+ _, intentID := wv.pendingNavigationSnapshot()
2122- intentID := wv.pendingIntentID2145+ wv.schedulePendingNavigationReplayForIntent(0, intentID)
2123- wv.mu.RUnlock()2146+}
2124- if intentID == 0 {2147+
2148+func (wv *WebView) schedulePendingNavigationReplayForIntent(attempt int, intentID uint64) {
2149+ if wv == nil || wv.destroyed.Load() || intentID == 0 {
2125 return2150 return
2126 }2151 }
2127 task := cefNewTask(cefTaskFunc(func() {2152 task := cefNewTask(cefTaskFunc(func() {
@@ -2153,7 +2178,7 @@ func (wv *WebView) schedulePendingNavigationReplay(attempt int) {
2153 return2178 return
2154 }2179 }
2155 cefScheduleAfter(pendingNavigationRetryDelay, func() {2180 cefScheduleAfter(pendingNavigationRetryDelay, func() {
2156- wv.schedulePendingNavigationReplay(attempt + 1)2181+ wv.schedulePendingNavigationReplayForIntent(attempt+1, intentID)
2157 })2182 })
2158 }2183 }
2159 2184
@@ -2196,7 +2221,7 @@ func (wv *WebView) replayPendingNavigationForIntent(attempt int, intentID uint64
2196 Str("uri", logging.TruncateURL(uri, logging.PermissionLogURLMaxLen)).2221 Str("uri", logging.TruncateURL(uri, logging.PermissionLogURLMaxLen)).
2197 Msg("cef: pending navigation replay waiting for main frame")2222 Msg("cef: pending navigation replay waiting for main frame")
2198 }2223 }
2199- wv.schedulePendingNavigationReplay(attempt + 1)2224+ wv.schedulePendingNavigationReplayForIntent(attempt+1, intentID)
2200 return2225 return
2201 }2226 }
2202 currentURL := frame.GetURL()2227 currentURL := frame.GetURL()
@@ -2229,7 +2254,7 @@ func (wv *WebView) replayPendingNavigationForIntent(attempt int, intentID uint64
2229 case pendingClaimReplaced:2254 case pendingClaimReplaced:
2230 // Browser was replaced between frame acquisition and claim; retry2255 // Browser was replaced between frame acquisition and claim; retry
2231 // the same intent so the new browser is used at execution time.2256 // the same intent so the new browser is used at execution time.
2232- wv.schedulePendingNavigationReplay(attempt + 1)2257+ wv.schedulePendingNavigationReplayForIntent(attempt+1, intentID)
2233 case pendingClaimReady:2258 case pendingClaimReady:
2234 frame.LoadURL(submitURI)2259 frame.LoadURL(submitURI)
2235 wv.resubmitIfBrowserReplaced(attempt, intentID, browser)2260 wv.resubmitIfBrowserReplaced(attempt, intentID, browser)
@@ -2256,7 +2281,7 @@ func (wv *WebView) resubmitIfBrowserReplaced(attempt int, intentID uint64, submi
2256 }2281 }
2257 wv.mu.Unlock()2282 wv.mu.Unlock()
2258 if replaced {2283 if replaced {
2259- wv.schedulePendingNavigationReplay(attempt + 1)2284+ wv.schedulePendingNavigationReplayForIntent(attempt+1, intentID)
2260 }2285 }
2261 }2286 }
2262 2287
M internal/infrastructure/cef/webview_handlers.go +7 -4
@@ -586,12 +586,15 @@ func (h *handlerSet) OnLoadEnd(browser purecef.Browser, frame purecef.Frame, htt
586586 // If a queued startup navigation is still pending after about:blank finished,
587587 // replay it now that the initial main-frame load completed.
588588 frameURL := frame.GetURL()
589- if pendingURI := h.wv.pendingNavigationURI(); pendingURI != "" && !pendingURIEquivalent(frameURL, pendingURI) {
589+ pendingURI, pendingIntentID := h.wv.pendingNavigationSnapshot()
590+ if pendingURI != "" && !pendingURIEquivalent(frameURL, pendingURI) {
590591 if strings.EqualFold(strings.TrimSpace(frameURL), "about:blank") {
591- log.Debug().
592+ rearmed := h.wv.rearmPendingNavigationAfterBlankLoadEnd(pendingIntentID)
593+ log.Info().
592594 Str("pending_uri", logging.TruncateURL(pendingURI, logging.PermissionLogURLMaxLen)).
595+ Bool("rearmed", rearmed).
593596 Msg("cef: replaying pending navigation after about:blank load end")
594- h.wv.schedulePendingNavigationReplay(0)
597+ h.wv.schedulePendingNavigationReplayForIntent(0, pendingIntentID)
595598 }
596599 }
597600
@@ -1050,7 +1053,7 @@ func (h *handlerSet) finishAfterCreated(
10501053 host.Invalidate(purecef.PaintElementTypePetView)
10511054 }
10521055 if state.hasPendingNavigation {
1053- h.wv.schedulePendingNavigationReplay(0)
1056+ h.wv.schedulePendingNavigationReplay()
10541057 }
10551058 if state.nativePopupParent != nil && state.nativePopupID != 0 {
10561059 state.nativePopupParent.clearPendingNativePopup(state.nativePopupID, h.wv)
@@ -586,12 +586,15 @@ func (h *handlerSet) OnLoadEnd(browser purecef.Browser, frame purecef.Frame, htt
586 // If a queued startup navigation is still pending after about:blank finished,586 // If a queued startup navigation is still pending after about:blank finished,
587 // replay it now that the initial main-frame load completed.587 // replay it now that the initial main-frame load completed.
588 frameURL := frame.GetURL()588 frameURL := frame.GetURL()
589- if pendingURI := h.wv.pendingNavigationURI(); pendingURI != "" && !pendingURIEquivalent(frameURL, pendingURI) {589+ pendingURI, pendingIntentID := h.wv.pendingNavigationSnapshot()
590+ if pendingURI != "" && !pendingURIEquivalent(frameURL, pendingURI) {
590 if strings.EqualFold(strings.TrimSpace(frameURL), "about:blank") {591 if strings.EqualFold(strings.TrimSpace(frameURL), "about:blank") {
591- log.Debug().592+ rearmed := h.wv.rearmPendingNavigationAfterBlankLoadEnd(pendingIntentID)
593+ log.Info().
592 Str("pending_uri", logging.TruncateURL(pendingURI, logging.PermissionLogURLMaxLen)).594 Str("pending_uri", logging.TruncateURL(pendingURI, logging.PermissionLogURLMaxLen)).
595+ Bool("rearmed", rearmed).
593 Msg("cef: replaying pending navigation after about:blank load end")596 Msg("cef: replaying pending navigation after about:blank load end")
594- h.wv.schedulePendingNavigationReplay(0)597+ h.wv.schedulePendingNavigationReplayForIntent(0, pendingIntentID)
595 }598 }
596 }599 }
597 600
@@ -1050,7 +1053,7 @@ func (h *handlerSet) finishAfterCreated(
1050 host.Invalidate(purecef.PaintElementTypePetView)1053 host.Invalidate(purecef.PaintElementTypePetView)
1051 }1054 }
1052 if state.hasPendingNavigation {1055 if state.hasPendingNavigation {
1053- h.wv.schedulePendingNavigationReplay(0)1056+ h.wv.schedulePendingNavigationReplay()
1054 }1057 }
1055 if state.nativePopupParent != nil && state.nativePopupID != 0 {1058 if state.nativePopupParent != nil && state.nativePopupID != 0 {
1056 state.nativePopupParent.clearPendingNativePopup(state.nativePopupID, h.wv)1059 state.nativePopupParent.clearPendingNativePopup(state.nativePopupID, h.wv)
M internal/infrastructure/cef/webview_navigation_test.go +110 -6
@@ -11,6 +11,70 @@ import (
1111 "github.com/stretchr/testify/require"
1212 )
1313
14+func 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+
31+func 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+
38+func 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+
1478 func TestWebViewReplayPendingNavigation_LoadsQueuedURIWhenMainFrameAvailable(t *testing.T) {
1579 browser := cefmocks.NewMockBrowser(t)
1680 frame := cefmocks.NewMockFrame(t)
@@ -149,11 +213,51 @@ func TestWebViewSchedulePendingNavigationReplay_RetriesWhenTaskPostFails(t *test
149213
150214 wv := &WebView{ctx: context.Background(), browser: browser}
151215 wv.setPendingNavigationLocked("https://github.com/bnema", time.Now())
152- wv.schedulePendingNavigationReplay(0)
216+ wv.schedulePendingNavigationReplay()
153217
154218 require.True(t, retried)
155219 }
156220
221+func 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+
157261 func TestWebViewReplayPendingNavigation_UsesCurrentBrowserAtExecutionTime(t *testing.T) {
158262 staleBrowser := cefmocks.NewMockBrowser(t)
159263 activeBrowser := cefmocks.NewMockBrowser(t)
@@ -179,7 +283,7 @@ func TestWebViewReplayPendingNavigation_UsesCurrentBrowserAtExecutionTime(t *tes
179283
180284 wv := &WebView{ctx: context.Background(), browser: staleBrowser}
181285 wv.setPendingNavigationLocked("https://github.com/bnema", time.Now())
182- wv.schedulePendingNavigationReplay(0)
286+ wv.schedulePendingNavigationReplay()
183287 wv.mu.Lock()
184288 wv.browser = activeBrowser
185289 wv.mu.Unlock()
@@ -313,9 +417,9 @@ func TestWebViewReplayPendingNavigation_SubmitsOncePerIntent(t *testing.T) {
313417 wv := &WebView{ctx: context.Background(), browser: browser}
314418 wv.setPendingNavigationLocked("https://example.com/target", time.Now())
315419 // OnAfterCreated schedules replay...
316- wv.schedulePendingNavigationReplay(0)
420+ wv.schedulePendingNavigationReplay()
317421 // ...and blank OnLoadEnd schedules a second replay before commit.
318- wv.schedulePendingNavigationReplay(0)
422+ wv.schedulePendingNavigationReplay()
319423 require.Len(t, scheduled, 2)
320424 for _, task := range scheduled {
321425 task.Execute()
@@ -347,7 +451,7 @@ func TestWebViewReplayPendingNavigation_StaleIntentDoesNotResubmit(t *testing.T)
347451
348452 wv := &WebView{ctx: context.Background(), browser: browser}
349453 wv.setPendingNavigationLocked("https://example.com/first", time.Now())
350- wv.schedulePendingNavigationReplay(0)
454+ wv.schedulePendingNavigationReplay()
351455 require.Len(t, scheduled, 1)
352456 stale := scheduled[0]
353457 // Replacement installs a new intent; the stale task must not submit.
@@ -385,7 +489,7 @@ func TestWebViewReplayPendingNavigation_RetriesSameIntentOnFailedPost(t *testing
385489 wv.mu.RLock()
386490 intent := wv.pendingIntentID
387491 wv.mu.RUnlock()
388- wv.schedulePendingNavigationReplay(0)
492+ wv.schedulePendingNavigationReplay()
389493 require.True(t, scheduledAfter)
390494 wv.mu.RLock()
391495 defer wv.mu.RUnlock()
@@ -11,6 +11,70 @@ import (
11 "github.com/stretchr/testify/require"11 "github.com/stretchr/testify/require"
12 )12 )
13 13
14+func 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+
31+func 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+
38+func 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+
14 func TestWebViewReplayPendingNavigation_LoadsQueuedURIWhenMainFrameAvailable(t *testing.T) {78 func TestWebViewReplayPendingNavigation_LoadsQueuedURIWhenMainFrameAvailable(t *testing.T) {
15 browser := cefmocks.NewMockBrowser(t)79 browser := cefmocks.NewMockBrowser(t)
16 frame := cefmocks.NewMockFrame(t)80 frame := cefmocks.NewMockFrame(t)
@@ -149,11 +213,51 @@ func TestWebViewSchedulePendingNavigationReplay_RetriesWhenTaskPostFails(t *test
149 213
150 wv := &WebView{ctx: context.Background(), browser: browser}214 wv := &WebView{ctx: context.Background(), browser: browser}
151 wv.setPendingNavigationLocked("https://github.com/bnema", time.Now())215 wv.setPendingNavigationLocked("https://github.com/bnema", time.Now())
152- wv.schedulePendingNavigationReplay(0)216+ wv.schedulePendingNavigationReplay()
153 217
154 require.True(t, retried)218 require.True(t, retried)
155 }219 }
156 220
221+func 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+
157 func TestWebViewReplayPendingNavigation_UsesCurrentBrowserAtExecutionTime(t *testing.T) {261 func TestWebViewReplayPendingNavigation_UsesCurrentBrowserAtExecutionTime(t *testing.T) {
158 staleBrowser := cefmocks.NewMockBrowser(t)262 staleBrowser := cefmocks.NewMockBrowser(t)
159 activeBrowser := cefmocks.NewMockBrowser(t)263 activeBrowser := cefmocks.NewMockBrowser(t)
@@ -179,7 +283,7 @@ func TestWebViewReplayPendingNavigation_UsesCurrentBrowserAtExecutionTime(t *tes
179 283
180 wv := &WebView{ctx: context.Background(), browser: staleBrowser}284 wv := &WebView{ctx: context.Background(), browser: staleBrowser}
181 wv.setPendingNavigationLocked("https://github.com/bnema", time.Now())285 wv.setPendingNavigationLocked("https://github.com/bnema", time.Now())
182- wv.schedulePendingNavigationReplay(0)286+ wv.schedulePendingNavigationReplay()
183 wv.mu.Lock()287 wv.mu.Lock()
184 wv.browser = activeBrowser288 wv.browser = activeBrowser
185 wv.mu.Unlock()289 wv.mu.Unlock()
@@ -313,9 +417,9 @@ func TestWebViewReplayPendingNavigation_SubmitsOncePerIntent(t *testing.T) {
313 wv := &WebView{ctx: context.Background(), browser: browser}417 wv := &WebView{ctx: context.Background(), browser: browser}
314 wv.setPendingNavigationLocked("https://example.com/target", time.Now())418 wv.setPendingNavigationLocked("https://example.com/target", time.Now())
315 // OnAfterCreated schedules replay...419 // OnAfterCreated schedules replay...
316- wv.schedulePendingNavigationReplay(0)420+ wv.schedulePendingNavigationReplay()
317 // ...and blank OnLoadEnd schedules a second replay before commit.421 // ...and blank OnLoadEnd schedules a second replay before commit.
318- wv.schedulePendingNavigationReplay(0)422+ wv.schedulePendingNavigationReplay()
319 require.Len(t, scheduled, 2)423 require.Len(t, scheduled, 2)
320 for _, task := range scheduled {424 for _, task := range scheduled {
321 task.Execute()425 task.Execute()
@@ -347,7 +451,7 @@ func TestWebViewReplayPendingNavigation_StaleIntentDoesNotResubmit(t *testing.T)
347 451
348 wv := &WebView{ctx: context.Background(), browser: browser}452 wv := &WebView{ctx: context.Background(), browser: browser}
349 wv.setPendingNavigationLocked("https://example.com/first", time.Now())453 wv.setPendingNavigationLocked("https://example.com/first", time.Now())
350- wv.schedulePendingNavigationReplay(0)454+ wv.schedulePendingNavigationReplay()
351 require.Len(t, scheduled, 1)455 require.Len(t, scheduled, 1)
352 stale := scheduled[0]456 stale := scheduled[0]
353 // Replacement installs a new intent; the stale task must not submit.457 // Replacement installs a new intent; the stale task must not submit.
@@ -385,7 +489,7 @@ func TestWebViewReplayPendingNavigation_RetriesSameIntentOnFailedPost(t *testing
385 wv.mu.RLock()489 wv.mu.RLock()
386 intent := wv.pendingIntentID490 intent := wv.pendingIntentID
387 wv.mu.RUnlock()491 wv.mu.RUnlock()
388- wv.schedulePendingNavigationReplay(0)492+ wv.schedulePendingNavigationReplay()
389 require.True(t, scheduledAfter)493 require.True(t, scheduledAfter)
390 wv.mu.RLock()494 wv.mu.RLock()
391 defer wv.mu.RUnlock()495 defer wv.mu.RUnlock()

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