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 Unverified
B5690EEEBB952194The 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 { | |||
| 413 | 413 | if browser == nil { | |
| 414 | 414 | return nil | |
| 415 | 415 | } | |
| 416 | - | wv.schedulePendingNavigationReplay(0) | |
| 416 | + | wv.schedulePendingNavigationReplay() | |
| 417 | 417 | return nil | |
| 418 | 418 | }) | |
| 419 | 419 | } | |
| @@ -786,7 +786,7 @@ func (wv *WebView) PrimePopupNavigation(uri string) { | |||
| 786 | 786 | wv.setPendingNavigationLocked(actualURI, time.Now()) | |
| 787 | 787 | wv.mu.Unlock() | |
| 788 | 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 | 2061 | 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 | 2073 | func (wv *WebView) setPendingNavigationLocked(uri string, at time.Time) { | |
| 2065 | 2074 | wv.pendingURI = uri | |
| 2066 | 2075 | wv.pendingURIStartedAt = time.Time{} | |
| @@ -2102,6 +2111,20 @@ func (wv *WebView) clearPendingNavigationIfEquivalentLocked(uri string) bool { | |||
| 2102 | 2111 | 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 | 2128 | func (wv *WebView) hasObservedAddressForPendingNavigationLocked() bool { | |
| 2106 | 2129 | if strings.TrimSpace(wv.pendingURI) == "" || strings.TrimSpace(wv.uri) == "" { | |
| 2107 | 2130 | return false | |
| @@ -2112,16 +2135,18 @@ func (wv *WebView) hasObservedAddressForPendingNavigationLocked() bool { | |||
| 2112 | 2135 | return !wv.loadDiagLastAddressAt.Before(wv.pendingURIStartedAt) | |
| 2113 | 2136 | } | |
| 2114 | 2137 | ||
| 2115 | - | func (wv *WebView) schedulePendingNavigationReplay(attempt int) { | |
| 2116 | - | if wv == nil || wv.destroyed.Load() { | |
| 2138 | + | func (wv *WebView) schedulePendingNavigationReplay() { | |
| 2139 | + | if wv == nil { | |
| 2117 | 2140 | return | |
| 2118 | 2141 | } | |
| 2119 | 2142 | // Capture the intent at schedule time; a rapid replacement installs a new | |
| 2120 | 2143 | // 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 { | |
| 2125 | 2150 | return | |
| 2126 | 2151 | } | |
| 2127 | 2152 | task := cefNewTask(cefTaskFunc(func() { | |
| @@ -2153,7 +2178,7 @@ func (wv *WebView) schedulePendingNavigationReplay(attempt int) { | |||
| 2153 | 2178 | return | |
| 2154 | 2179 | } | |
| 2155 | 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 | 2221 | Str("uri", logging.TruncateURL(uri, logging.PermissionLogURLMaxLen)). | |
| 2197 | 2222 | Msg("cef: pending navigation replay waiting for main frame") | |
| 2198 | 2223 | } | |
| 2199 | - | wv.schedulePendingNavigationReplay(attempt + 1) | |
| 2224 | + | wv.schedulePendingNavigationReplayForIntent(attempt+1, intentID) | |
| 2200 | 2225 | return | |
| 2201 | 2226 | } | |
| 2202 | 2227 | currentURL := frame.GetURL() | |
| @@ -2229,7 +2254,7 @@ func (wv *WebView) replayPendingNavigationForIntent(attempt int, intentID uint64 | |||
| 2229 | 2254 | case pendingClaimReplaced: | |
| 2230 | 2255 | // Browser was replaced between frame acquisition and claim; retry | |
| 2231 | 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 | 2258 | case pendingClaimReady: | |
| 2234 | 2259 | frame.LoadURL(submitURI) | |
| 2235 | 2260 | wv.resubmitIfBrowserReplaced(attempt, intentID, browser) | |
| @@ -2256,7 +2281,7 @@ func (wv *WebView) resubmitIfBrowserReplaced(attempt int, intentID uint64, submi | |||
| 2256 | 2281 | } | |
| 2257 | 2282 | wv.mu.Unlock() | |
| 2258 | 2283 | if replaced { | |
| 2259 | - | wv.schedulePendingNavigationReplay(attempt + 1) | |
| 2284 | + | wv.schedulePendingNavigationReplayForIntent(attempt+1, intentID) | |
| 2260 | 2285 | } | |
| 2261 | 2286 | } | |
| 2262 | 2287 | ||
| @@ -413,7 +413,7 @@ func (wv *WebView) LoadURI(_ context.Context, uri string) error { | |||||
| 413 | if browser == nil { | 413 | if browser == nil { | ||
| 414 | return nil | 414 | return nil | ||
| 415 | } | 415 | } | ||
| 416 | - | wv.schedulePendingNavigationReplay(0) | 416 | + | wv.schedulePendingNavigationReplay() |
| 417 | return nil | 417 | 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.pendingURI | 2061 | 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 = uri | 2074 | 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 true | 2111 | 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 false | 2130 | 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 | return | 2140 | return | ||
| 2118 | } | 2141 | } | ||
| 2119 | // Capture the intent at schedule time; a rapid replacement installs a new | 2142 | // 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.pendingIntentID | 2145 | + | 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 | return | 2150 | return | ||
| 2126 | } | 2151 | } | ||
| 2127 | task := cefNewTask(cefTaskFunc(func() { | 2152 | task := cefNewTask(cefTaskFunc(func() { | ||
| @@ -2153,7 +2178,7 @@ func (wv *WebView) schedulePendingNavigationReplay(attempt int) { | |||||
| 2153 | return | 2178 | 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 | return | 2225 | 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; retry | 2255 | // 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 | |||
| 586 | 586 | // If a queued startup navigation is still pending after about:blank finished, | |
| 587 | 587 | // replay it now that the initial main-frame load completed. | |
| 588 | 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 | 591 | if strings.EqualFold(strings.TrimSpace(frameURL), "about:blank") { | |
| 591 | - | log.Debug(). | |
| 592 | + | rearmed := h.wv.rearmPendingNavigationAfterBlankLoadEnd(pendingIntentID) | |
| 593 | + | log.Info(). | |
| 592 | 594 | Str("pending_uri", logging.TruncateURL(pendingURI, logging.PermissionLogURLMaxLen)). | |
| 595 | + | Bool("rearmed", rearmed). | |
| 593 | 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 | 1053 | host.Invalidate(purecef.PaintElementTypePetView) | |
| 1051 | 1054 | } | |
| 1052 | 1055 | if state.hasPendingNavigation { | |
| 1053 | - | h.wv.schedulePendingNavigationReplay(0) | |
| 1056 | + | h.wv.schedulePendingNavigationReplay() | |
| 1054 | 1057 | } | |
| 1055 | 1058 | if state.nativePopupParent != nil && state.nativePopupID != 0 { | |
| 1056 | 1059 | 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 ( | |||
| 11 | 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 | 78 | func TestWebViewReplayPendingNavigation_LoadsQueuedURIWhenMainFrameAvailable(t *testing.T) { | |
| 15 | 79 | browser := cefmocks.NewMockBrowser(t) | |
| 16 | 80 | frame := cefmocks.NewMockFrame(t) | |
| @@ -149,11 +213,51 @@ func TestWebViewSchedulePendingNavigationReplay_RetriesWhenTaskPostFails(t *test | |||
| 149 | 213 | ||
| 150 | 214 | wv := &WebView{ctx: context.Background(), browser: browser} | |
| 151 | 215 | wv.setPendingNavigationLocked("https://github.com/bnema", time.Now()) | |
| 152 | - | wv.schedulePendingNavigationReplay(0) | |
| 216 | + | wv.schedulePendingNavigationReplay() | |
| 153 | 217 | ||
| 154 | 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 | 261 | func TestWebViewReplayPendingNavigation_UsesCurrentBrowserAtExecutionTime(t *testing.T) { | |
| 158 | 262 | staleBrowser := cefmocks.NewMockBrowser(t) | |
| 159 | 263 | activeBrowser := cefmocks.NewMockBrowser(t) | |
| @@ -179,7 +283,7 @@ func TestWebViewReplayPendingNavigation_UsesCurrentBrowserAtExecutionTime(t *tes | |||
| 179 | 283 | ||
| 180 | 284 | wv := &WebView{ctx: context.Background(), browser: staleBrowser} | |
| 181 | 285 | wv.setPendingNavigationLocked("https://github.com/bnema", time.Now()) | |
| 182 | - | wv.schedulePendingNavigationReplay(0) | |
| 286 | + | wv.schedulePendingNavigationReplay() | |
| 183 | 287 | wv.mu.Lock() | |
| 184 | 288 | wv.browser = activeBrowser | |
| 185 | 289 | wv.mu.Unlock() | |
| @@ -313,9 +417,9 @@ func TestWebViewReplayPendingNavigation_SubmitsOncePerIntent(t *testing.T) { | |||
| 313 | 417 | wv := &WebView{ctx: context.Background(), browser: browser} | |
| 314 | 418 | wv.setPendingNavigationLocked("https://example.com/target", time.Now()) | |
| 315 | 419 | // OnAfterCreated schedules replay... | |
| 316 | - | wv.schedulePendingNavigationReplay(0) | |
| 420 | + | wv.schedulePendingNavigationReplay() | |
| 317 | 421 | // ...and blank OnLoadEnd schedules a second replay before commit. | |
| 318 | - | wv.schedulePendingNavigationReplay(0) | |
| 422 | + | wv.schedulePendingNavigationReplay() | |
| 319 | 423 | require.Len(t, scheduled, 2) | |
| 320 | 424 | for _, task := range scheduled { | |
| 321 | 425 | task.Execute() | |
| @@ -347,7 +451,7 @@ func TestWebViewReplayPendingNavigation_StaleIntentDoesNotResubmit(t *testing.T) | |||
| 347 | 451 | ||
| 348 | 452 | wv := &WebView{ctx: context.Background(), browser: browser} | |
| 349 | 453 | wv.setPendingNavigationLocked("https://example.com/first", time.Now()) | |
| 350 | - | wv.schedulePendingNavigationReplay(0) | |
| 454 | + | wv.schedulePendingNavigationReplay() | |
| 351 | 455 | require.Len(t, scheduled, 1) | |
| 352 | 456 | stale := scheduled[0] | |
| 353 | 457 | // Replacement installs a new intent; the stale task must not submit. | |
| @@ -385,7 +489,7 @@ func TestWebViewReplayPendingNavigation_RetriesSameIntentOnFailedPost(t *testing | |||
| 385 | 489 | wv.mu.RLock() | |
| 386 | 490 | intent := wv.pendingIntentID | |
| 387 | 491 | wv.mu.RUnlock() | |
| 388 | - | wv.schedulePendingNavigationReplay(0) | |
| 492 | + | wv.schedulePendingNavigationReplay() | |
| 389 | 493 | require.True(t, scheduledAfter) | |
| 390 | 494 | wv.mu.RLock() | |
| 391 | 495 | 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 = activeBrowser | 288 | 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.pendingIntentID | 490 | 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() | ||