fix(cef): preserve committed systemview identity (#445)
* fix(cef): preserve committed systemview identity * fix(cef): resolve internal favicons from committed URL
- commit
- 0d014a0f98dadb74f8c6f834e576cbc13d400017
- author
- bnema · · 2026-09-22 05:43 UTC
- committer
- GitHub · · 2026-09-22 05:43 UTC
- parent
- df4f3ad
- signature
- GPG Unverified
B5690EEEBB952194The key is unknown
6 files · +159 -21
M internal/infrastructure/cef/content_injector.go +5 -3
| @@ -3,7 +3,6 @@ package cef | |||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | 5 | "fmt" | |
| 6 | - | "strings" | |
| 7 | 6 | "sync" | |
| 8 | 7 | ||
| 9 | 8 | "github.com/bnema/dumber/internal/infrastructure/webutil" | |
| @@ -255,7 +254,10 @@ func (ci *contentInjector) RefreshScripts(ctx context.Context, wv port.WebView) | |||
| 255 | 254 | // OnLoadEnd callback's browser (not mutable WebView state), so events that | |
| 256 | 255 | // are provably stale are skipped: a replaced browser identifier (old main | |
| 257 | 256 | // frame during process swap) or a superseded intent ID (a newer navigation | |
| 258 | - | // committed while the callback was queued). Ambiguous cases fall back to skipping rather than | |
| 257 | + | // committed while the callback was queued). The raw committed CEF URL in the | |
| 258 | + | // event is compared only against the raw committed URL as an additional | |
| 259 | + | // stale-event guard; it is never compared against the conceptual URI. | |
| 260 | + | // Ambiguous cases fall back to skipping rather than | |
| 259 | 261 | // installing blind: an event captured without a browser cannot prove | |
| 260 | 262 | // identity, and the current browser gets its own load-end installation. | |
| 261 | 263 | // The installation is pinned to the validated document sequence, so a | |
| @@ -274,7 +276,7 @@ func (ci *contentInjector) onLoadEndForEvent(wv *WebView, event injectionEvent) | |||
| 274 | 276 | } | |
| 275 | 277 | wv.mu.Lock() | |
| 276 | 278 | if wv.browser != browser || wv.pendingIntentID != event.intentID || event.documentSeq == 0 || | |
| 277 | - | wv.documentSeq != event.documentSeq || strings.TrimSpace(wv.uri) != event.frameURL || | |
| 279 | + | wv.documentSeq != event.documentSeq || wv.committedURL != event.committedURL || | |
| 278 | 280 | wv.installedDocumentSeq == event.documentSeq { | |
| 279 | 281 | wv.mu.Unlock() | |
| 280 | 282 | return | |
| @@ -3,7 +3,6 @@ package cef | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | - | "strings" | |||
| 7 | "sync" | 6 | "sync" | ||
| 8 | 7 | ||||
| 9 | "github.com/bnema/dumber/internal/infrastructure/webutil" | 8 | "github.com/bnema/dumber/internal/infrastructure/webutil" | ||
| @@ -255,7 +254,10 @@ func (ci *contentInjector) RefreshScripts(ctx context.Context, wv port.WebView) | |||||
| 255 | // OnLoadEnd callback's browser (not mutable WebView state), so events that | 254 | // OnLoadEnd callback's browser (not mutable WebView state), so events that | ||
| 256 | // are provably stale are skipped: a replaced browser identifier (old main | 255 | // are provably stale are skipped: a replaced browser identifier (old main | ||
| 257 | // frame during process swap) or a superseded intent ID (a newer navigation | 256 | // frame during process swap) or a superseded intent ID (a newer navigation | ||
| 258 | - | // committed while the callback was queued). Ambiguous cases fall back to skipping rather than | 257 | + | // committed while the callback was queued). The raw committed CEF URL in the |
| 258 | + | // event is compared only against the raw committed URL as an additional | |||
| 259 | + | // stale-event guard; it is never compared against the conceptual URI. | |||
| 260 | + | // Ambiguous cases fall back to skipping rather than | |||
| 259 | // installing blind: an event captured without a browser cannot prove | 261 | // installing blind: an event captured without a browser cannot prove | ||
| 260 | // identity, and the current browser gets its own load-end installation. | 262 | // identity, and the current browser gets its own load-end installation. | ||
| 261 | // The installation is pinned to the validated document sequence, so a | 263 | // The installation is pinned to the validated document sequence, so a | ||
| @@ -274,7 +276,7 @@ func (ci *contentInjector) onLoadEndForEvent(wv *WebView, event injectionEvent) | |||||
| 274 | } | 276 | } | ||
| 275 | wv.mu.Lock() | 277 | wv.mu.Lock() | ||
| 276 | if wv.browser != browser || wv.pendingIntentID != event.intentID || event.documentSeq == 0 || | 278 | if wv.browser != browser || wv.pendingIntentID != event.intentID || event.documentSeq == 0 || | ||
| 277 | - | wv.documentSeq != event.documentSeq || strings.TrimSpace(wv.uri) != event.frameURL || | 279 | + | wv.documentSeq != event.documentSeq || wv.committedURL != event.committedURL || |
| 278 | wv.installedDocumentSeq == event.documentSeq { | 280 | wv.installedDocumentSeq == event.documentSeq { | ||
| 279 | wv.mu.Unlock() | 281 | wv.mu.Unlock() | ||
| 280 | return | 282 | return | ||
M internal/infrastructure/cef/favicon_discovery_test.go +46 -0
| @@ -72,6 +72,26 @@ func TestCEFOnFaviconUrlchangeForwardsOrderedCandidates(t *testing.T) { | |||
| 72 | 72 | require.Equal(t, []string{"https://example.com/favicon.png", "https://cdn.example.com/icon.ico"}, icons) | |
| 73 | 73 | } | |
| 74 | 74 | ||
| 75 | + | func TestCEFOnFaviconUrlchangeResolvesInternalCandidatesAgainstCommittedURL(t *testing.T) { | |
| 76 | + | prevDecode := decodeCEFStringList | |
| 77 | + | decodeCEFStringList = func(purecef.StringList) []string { return []string{"/favicon.png"} } | |
| 78 | + | defer func() { decodeCEFStringList = prevDecode }() | |
| 79 | + | ||
| 80 | + | var page string | |
| 81 | + | var icons []string | |
| 82 | + | browser := cefmocks.NewMockBrowser(t) | |
| 83 | + | browser.EXPECT().GetIdentifier().Return(int32(7)).Maybe() | |
| 84 | + | wv := &WebView{ctx: context.Background(), browser: browser, documentSeq: 1, callbacks: &port.WebViewCallbacks{ | |
| 85 | + | OnFaviconURLChanged: func(p string, urls []string) { page, icons = p, urls }, | |
| 86 | + | }} | |
| 87 | + | wv.setCommittedURLsLocked("https://dumber.invalid/history", "dumb://history") | |
| 88 | + | ||
| 89 | + | (&handlerSet{wv: wv}).OnFaviconUrlchange(browser, purecef.StringList(1)) | |
| 90 | + | ||
| 91 | + | require.Equal(t, "dumb://history", page) | |
| 92 | + | require.Equal(t, []string{"https://dumber.invalid/favicon.png"}, icons) | |
| 93 | + | } | |
| 94 | + | ||
| 75 | 95 | func TestCEFLoadingFinishedDiscoversMetadataBeforeFallback(t *testing.T) { | |
| 76 | 96 | prevNewStringVisitor := cefNewStringVisitor | |
| 77 | 97 | cefNewStringVisitor = func(visitor purecef.StringVisitor) purecef.StringVisitor { return visitor } | |
| @@ -97,6 +117,32 @@ func TestCEFLoadingFinishedDiscoversMetadataBeforeFallback(t *testing.T) { | |||
| 97 | 117 | require.Equal(t, []string{"https://example.com/meta.png", "https://example.com/favicon.ico"}, icons) | |
| 98 | 118 | } | |
| 99 | 119 | ||
| 120 | + | func TestCEFLoadingFinishedResolvesInternalMetadataAgainstCommittedURL(t *testing.T) { | |
| 121 | + | prevNewStringVisitor := cefNewStringVisitor | |
| 122 | + | cefNewStringVisitor = func(visitor purecef.StringVisitor) purecef.StringVisitor { return visitor } | |
| 123 | + | defer func() { cefNewStringVisitor = prevNewStringVisitor }() | |
| 124 | + | ||
| 125 | + | browser := cefmocks.NewMockBrowser(t) | |
| 126 | + | frame := cefmocks.NewMockFrame(t) | |
| 127 | + | browser.EXPECT().GetIdentifier().Return(int32(7)).Maybe() | |
| 128 | + | browser.EXPECT().GetMainFrame().Return(frame) | |
| 129 | + | frame.EXPECT().GetSource(mock.Anything).Run(func(visitor purecef.StringVisitor) { | |
| 130 | + | visitor.Visit(`<html><head><link rel="icon" href="/meta.png"></head></html>`) | |
| 131 | + | }).Return() | |
| 132 | + | ||
| 133 | + | var page string | |
| 134 | + | var icons []string | |
| 135 | + | wv := &WebView{ctx: context.Background(), browser: browser, documentSeq: 1, callbacks: &port.WebViewCallbacks{ | |
| 136 | + | OnFaviconURLChanged: func(p string, urls []string) { page, icons = p, urls }, | |
| 137 | + | }} | |
| 138 | + | wv.setCommittedURLsLocked("https://dumber.invalid/history", "dumb://history") | |
| 139 | + | ||
| 140 | + | (&handlerSet{wv: wv}).OnLoadingStateChange(browser, 0, 1, 0) | |
| 141 | + | ||
| 142 | + | require.Equal(t, "dumb://history", page) | |
| 143 | + | require.Equal(t, []string{"https://dumber.invalid/meta.png", "https://dumber.invalid/favicon.ico"}, icons) | |
| 144 | + | } | |
| 145 | + | ||
| 100 | 146 | func TestCEFLoadingFinishedForwardsFallbackCandidate(t *testing.T) { | |
| 101 | 147 | var page string | |
| 102 | 148 | var icons []string | |
| @@ -72,6 +72,26 @@ func TestCEFOnFaviconUrlchangeForwardsOrderedCandidates(t *testing.T) { | |||||
| 72 | require.Equal(t, []string{"https://example.com/favicon.png", "https://cdn.example.com/icon.ico"}, icons) | 72 | require.Equal(t, []string{"https://example.com/favicon.png", "https://cdn.example.com/icon.ico"}, icons) | ||
| 73 | } | 73 | } | ||
| 74 | 74 | ||||
| 75 | + | func TestCEFOnFaviconUrlchangeResolvesInternalCandidatesAgainstCommittedURL(t *testing.T) { | |||
| 76 | + | prevDecode := decodeCEFStringList | |||
| 77 | + | decodeCEFStringList = func(purecef.StringList) []string { return []string{"/favicon.png"} } | |||
| 78 | + | defer func() { decodeCEFStringList = prevDecode }() | |||
| 79 | + | ||||
| 80 | + | var page string | |||
| 81 | + | var icons []string | |||
| 82 | + | browser := cefmocks.NewMockBrowser(t) | |||
| 83 | + | browser.EXPECT().GetIdentifier().Return(int32(7)).Maybe() | |||
| 84 | + | wv := &WebView{ctx: context.Background(), browser: browser, documentSeq: 1, callbacks: &port.WebViewCallbacks{ | |||
| 85 | + | OnFaviconURLChanged: func(p string, urls []string) { page, icons = p, urls }, | |||
| 86 | + | }} | |||
| 87 | + | wv.setCommittedURLsLocked("https://dumber.invalid/history", "dumb://history") | |||
| 88 | + | ||||
| 89 | + | (&handlerSet{wv: wv}).OnFaviconUrlchange(browser, purecef.StringList(1)) | |||
| 90 | + | ||||
| 91 | + | require.Equal(t, "dumb://history", page) | |||
| 92 | + | require.Equal(t, []string{"https://dumber.invalid/favicon.png"}, icons) | |||
| 93 | + | } | |||
| 94 | + | ||||
| 75 | func TestCEFLoadingFinishedDiscoversMetadataBeforeFallback(t *testing.T) { | 95 | func TestCEFLoadingFinishedDiscoversMetadataBeforeFallback(t *testing.T) { | ||
| 76 | prevNewStringVisitor := cefNewStringVisitor | 96 | prevNewStringVisitor := cefNewStringVisitor | ||
| 77 | cefNewStringVisitor = func(visitor purecef.StringVisitor) purecef.StringVisitor { return visitor } | 97 | cefNewStringVisitor = func(visitor purecef.StringVisitor) purecef.StringVisitor { return visitor } | ||
| @@ -97,6 +117,32 @@ func TestCEFLoadingFinishedDiscoversMetadataBeforeFallback(t *testing.T) { | |||||
| 97 | require.Equal(t, []string{"https://example.com/meta.png", "https://example.com/favicon.ico"}, icons) | 117 | require.Equal(t, []string{"https://example.com/meta.png", "https://example.com/favicon.ico"}, icons) | ||
| 98 | } | 118 | } | ||
| 99 | 119 | ||||
| 120 | + | func TestCEFLoadingFinishedResolvesInternalMetadataAgainstCommittedURL(t *testing.T) { | |||
| 121 | + | prevNewStringVisitor := cefNewStringVisitor | |||
| 122 | + | cefNewStringVisitor = func(visitor purecef.StringVisitor) purecef.StringVisitor { return visitor } | |||
| 123 | + | defer func() { cefNewStringVisitor = prevNewStringVisitor }() | |||
| 124 | + | ||||
| 125 | + | browser := cefmocks.NewMockBrowser(t) | |||
| 126 | + | frame := cefmocks.NewMockFrame(t) | |||
| 127 | + | browser.EXPECT().GetIdentifier().Return(int32(7)).Maybe() | |||
| 128 | + | browser.EXPECT().GetMainFrame().Return(frame) | |||
| 129 | + | frame.EXPECT().GetSource(mock.Anything).Run(func(visitor purecef.StringVisitor) { | |||
| 130 | + | visitor.Visit(`<html><head><link rel="icon" href="/meta.png"></head></html>`) | |||
| 131 | + | }).Return() | |||
| 132 | + | ||||
| 133 | + | var page string | |||
| 134 | + | var icons []string | |||
| 135 | + | wv := &WebView{ctx: context.Background(), browser: browser, documentSeq: 1, callbacks: &port.WebViewCallbacks{ | |||
| 136 | + | OnFaviconURLChanged: func(p string, urls []string) { page, icons = p, urls }, | |||
| 137 | + | }} | |||
| 138 | + | wv.setCommittedURLsLocked("https://dumber.invalid/history", "dumb://history") | |||
| 139 | + | ||||
| 140 | + | (&handlerSet{wv: wv}).OnLoadingStateChange(browser, 0, 1, 0) | |||
| 141 | + | ||||
| 142 | + | require.Equal(t, "dumb://history", page) | |||
| 143 | + | require.Equal(t, []string{"https://dumber.invalid/meta.png", "https://dumber.invalid/favicon.ico"}, icons) | |||
| 144 | + | } | |||
| 145 | + | ||||
| 100 | func TestCEFLoadingFinishedForwardsFallbackCandidate(t *testing.T) { | 146 | func TestCEFLoadingFinishedForwardsFallbackCandidate(t *testing.T) { | ||
| 101 | var page string | 147 | var page string | ||
| 102 | var icons []string | 148 | var icons []string | ||
M internal/infrastructure/cef/injection_event_test.go +43 -1
| @@ -38,7 +38,7 @@ func newInjectionHarnessWithID(t *testing.T, uri string, browserID int32) *injec | |||
| 38 | 38 | h.browser.EXPECT().GetMainFrame().Return(frame).Maybe() | |
| 39 | 39 | h.browser.EXPECT().GetIdentifier().Return(browserID).Maybe() | |
| 40 | 40 | h.wv = &WebView{ctx: context.Background(), browser: h.browser, documentSeq: 1} | |
| 41 | - | h.wv.uri = uri | |
| 41 | + | h.wv.setCommittedURLsLocked(uri, toConceptualInternalURL(uri)) | |
| 42 | 42 | return h | |
| 43 | 43 | } | |
| 44 | 44 | ||
| @@ -212,3 +212,45 @@ func TestInjectionEvent_StaleCallbackBrowserSkipped(t *testing.T) { | |||
| 212 | 212 | testInjector().onLoadEndForEvent(h.wv, event) | |
| 213 | 213 | require.Zero(t, h.scriptCount(), "old-callback event must not install against the replacement browser") | |
| 214 | 214 | } | |
| 215 | + | ||
| 216 | + | // TestInjectionEvent_InternalSystemviewInstalls reproduces the production | |
| 217 | + | // identity split for internal systemviews: the WebView's conceptual URI is | |
| 218 | + | // dumb://<page> while the CEF callback frame URL is the raw committed | |
| 219 | + | // https://dumber.invalid/<page>. The stale-event guard must compare raw | |
| 220 | + | // against raw, so the bridge and message scripts install. | |
| 221 | + | func TestInjectionEvent_InternalSystemviewInstalls(t *testing.T) { | |
| 222 | + | for _, page := range []string{"history", "favorites"} { | |
| 223 | + | t.Run(page, func(t *testing.T) { | |
| 224 | + | h := newInjectionHarness(t, "https://example.com/a") | |
| 225 | + | rawURL := "https://dumber.invalid/" + page | |
| 226 | + | h.wv.updateURI(rawURL) | |
| 227 | + | require.Equal(t, "dumb://"+page, h.wv.URI(), "UI callbacks must keep the conceptual URI") | |
| 228 | + | ||
| 229 | + | testInjector().onLoadEndForEvent(h.wv, h.wv.captureInjectionEvent(h.browser, rawURL)) | |
| 230 | + | ||
| 231 | + | require.Equal(t, 7, h.scriptCount(), "internal install must stay a fixed small script set") | |
| 232 | + | h.mu.Lock() | |
| 233 | + | defer h.mu.Unlock() | |
| 234 | + | joined := "" | |
| 235 | + | for _, script := range h.scripts { | |
| 236 | + | joined += script | |
| 237 | + | } | |
| 238 | + | require.Contains(t, joined, "dumber-theme-vars") | |
| 239 | + | require.Contains(t, joined, "window.dumber") | |
| 240 | + | }) | |
| 241 | + | } | |
| 242 | + | } | |
| 243 | + | ||
| 244 | + | // TestInjectionEvent_StaleCommittedURLSkips verifies the raw-URL stale guard: | |
| 245 | + | // an event captured for one committed document must not install after a | |
| 246 | + | // newer commit replaced the raw URL. | |
| 247 | + | func TestInjectionEvent_StaleCommittedURLSkips(t *testing.T) { | |
| 248 | + | h := newInjectionHarness(t, "https://example.com/a") | |
| 249 | + | h.wv.updateURI("https://dumber.invalid/history") | |
| 250 | + | event := h.wv.captureInjectionEvent(h.browser, "https://dumber.invalid/history") | |
| 251 | + | ||
| 252 | + | h.wv.updateURI("https://dumber.invalid/favorites") | |
| 253 | + | ||
| 254 | + | testInjector().onLoadEndForEvent(h.wv, event) | |
| 255 | + | require.Zero(t, h.scriptCount(), "event for a superseded commit must not install") | |
| 256 | + | } | |
| @@ -38,7 +38,7 @@ func newInjectionHarnessWithID(t *testing.T, uri string, browserID int32) *injec | |||||
| 38 | h.browser.EXPECT().GetMainFrame().Return(frame).Maybe() | 38 | h.browser.EXPECT().GetMainFrame().Return(frame).Maybe() | ||
| 39 | h.browser.EXPECT().GetIdentifier().Return(browserID).Maybe() | 39 | h.browser.EXPECT().GetIdentifier().Return(browserID).Maybe() | ||
| 40 | h.wv = &WebView{ctx: context.Background(), browser: h.browser, documentSeq: 1} | 40 | h.wv = &WebView{ctx: context.Background(), browser: h.browser, documentSeq: 1} | ||
| 41 | - | h.wv.uri = uri | 41 | + | h.wv.setCommittedURLsLocked(uri, toConceptualInternalURL(uri)) |
| 42 | return h | 42 | return h | ||
| 43 | } | 43 | } | ||
| 44 | 44 | ||||
| @@ -212,3 +212,45 @@ func TestInjectionEvent_StaleCallbackBrowserSkipped(t *testing.T) { | |||||
| 212 | testInjector().onLoadEndForEvent(h.wv, event) | 212 | testInjector().onLoadEndForEvent(h.wv, event) | ||
| 213 | require.Zero(t, h.scriptCount(), "old-callback event must not install against the replacement browser") | 213 | require.Zero(t, h.scriptCount(), "old-callback event must not install against the replacement browser") | ||
| 214 | } | 214 | } | ||
| 215 | + | ||||
| 216 | + | // TestInjectionEvent_InternalSystemviewInstalls reproduces the production | |||
| 217 | + | // identity split for internal systemviews: the WebView's conceptual URI is | |||
| 218 | + | // dumb://<page> while the CEF callback frame URL is the raw committed | |||
| 219 | + | // https://dumber.invalid/<page>. The stale-event guard must compare raw | |||
| 220 | + | // against raw, so the bridge and message scripts install. | |||
| 221 | + | func TestInjectionEvent_InternalSystemviewInstalls(t *testing.T) { | |||
| 222 | + | for _, page := range []string{"history", "favorites"} { | |||
| 223 | + | t.Run(page, func(t *testing.T) { | |||
| 224 | + | h := newInjectionHarness(t, "https://example.com/a") | |||
| 225 | + | rawURL := "https://dumber.invalid/" + page | |||
| 226 | + | h.wv.updateURI(rawURL) | |||
| 227 | + | require.Equal(t, "dumb://"+page, h.wv.URI(), "UI callbacks must keep the conceptual URI") | |||
| 228 | + | ||||
| 229 | + | testInjector().onLoadEndForEvent(h.wv, h.wv.captureInjectionEvent(h.browser, rawURL)) | |||
| 230 | + | ||||
| 231 | + | require.Equal(t, 7, h.scriptCount(), "internal install must stay a fixed small script set") | |||
| 232 | + | h.mu.Lock() | |||
| 233 | + | defer h.mu.Unlock() | |||
| 234 | + | joined := "" | |||
| 235 | + | for _, script := range h.scripts { | |||
| 236 | + | joined += script | |||
| 237 | + | } | |||
| 238 | + | require.Contains(t, joined, "dumber-theme-vars") | |||
| 239 | + | require.Contains(t, joined, "window.dumber") | |||
| 240 | + | }) | |||
| 241 | + | } | |||
| 242 | + | } | |||
| 243 | + | ||||
| 244 | + | // TestInjectionEvent_StaleCommittedURLSkips verifies the raw-URL stale guard: | |||
| 245 | + | // an event captured for one committed document must not install after a | |||
| 246 | + | // newer commit replaced the raw URL. | |||
| 247 | + | func TestInjectionEvent_StaleCommittedURLSkips(t *testing.T) { | |||
| 248 | + | h := newInjectionHarness(t, "https://example.com/a") | |||
| 249 | + | h.wv.updateURI("https://dumber.invalid/history") | |||
| 250 | + | event := h.wv.captureInjectionEvent(h.browser, "https://dumber.invalid/history") | |||
| 251 | + | ||||
| 252 | + | h.wv.updateURI("https://dumber.invalid/favorites") | |||
| 253 | + | ||||
| 254 | + | testInjector().onLoadEndForEvent(h.wv, event) | |||
| 255 | + | require.Zero(t, h.scriptCount(), "event for a superseded commit must not install") | |||
| 256 | + | } | |||
M internal/infrastructure/cef/webview.go +48 -11
| @@ -256,7 +256,13 @@ type WebView struct { | |||
| 256 | 256 | popupOpenerBridgeParentURI string | |
| 257 | 257 | ||
| 258 | 258 | // State cache (mutex-protected). | |
| 259 | + | // uri is the conceptual/public URI used by UI callbacks (for example | |
| 260 | + | // dumb://history). committedURL is the raw committed CEF main-frame URL | |
| 261 | + | // (for example https://dumber.invalid/history). The two forms are updated | |
| 262 | + | // together through setCommittedURLsLocked; never compare one form against | |
| 263 | + | // the other. | |
| 259 | 264 | uri string | |
| 265 | + | committedURL string | |
| 260 | 266 | title string | |
| 261 | 267 | progress float64 | |
| 262 | 268 | canGoBack bool | |
| @@ -1148,9 +1154,11 @@ func (wv *WebView) activateNativePopup(popupID int32, targetURL string) (purecef | |||
| 1148 | 1154 | wv.setPendingNavigationLocked(toActualInternalURL(targetURL), time.Now()) | |
| 1149 | 1155 | } | |
| 1150 | 1156 | if strings.TrimSpace(wv.pendingURI) != "" { | |
| 1151 | - | wv.uri = toConceptualInternalURL(wv.pendingURI) | |
| 1157 | + | raw := strings.TrimSpace(wv.pendingURI) | |
| 1158 | + | wv.setCommittedURLsLocked(raw, toConceptualInternalURL(raw)) | |
| 1152 | 1159 | } else { | |
| 1153 | - | wv.uri = toConceptualInternalURL(targetURL) | |
| 1160 | + | raw := strings.TrimSpace(targetURL) | |
| 1161 | + | wv.setCommittedURLsLocked(raw, toConceptualInternalURL(raw)) | |
| 1154 | 1162 | } | |
| 1155 | 1163 | wv.isLoading = true | |
| 1156 | 1164 | return wv.client, true | |
| @@ -1461,10 +1469,20 @@ func (wv *WebView) NativeWidget() uintptr { | |||
| 1461 | 1469 | // --------------------------------------------------------------------------- | |
| 1462 | 1470 | ||
| 1463 | 1471 | func (wv *WebView) updateURI(uri string) { | |
| 1464 | - | uri = toConceptualInternalURL(uri) | |
| 1472 | + | wv.setCommittedURL(uri) | |
| 1473 | + | } | |
| 1474 | + | ||
| 1475 | + | // setCommittedURL is the single owner of main-frame commit identity: it | |
| 1476 | + | // updates the raw committed CEF URL and the conceptual/public URI together, | |
| 1477 | + | // preserving pending-navigation acknowledgement, diagnostics timestamps, and | |
| 1478 | + | // all current callbacks. raw is the unmodified committed URL; the conceptual | |
| 1479 | + | // URI is derived from it. | |
| 1480 | + | func (wv *WebView) setCommittedURL(raw string) { | |
| 1481 | + | raw = strings.TrimSpace(raw) | |
| 1482 | + | uri := toConceptualInternalURL(raw) | |
| 1465 | 1483 | now := time.Now() | |
| 1466 | 1484 | wv.mu.Lock() | |
| 1467 | - | wv.uri = uri | |
| 1485 | + | wv.setCommittedURLsLocked(raw, uri) | |
| 1468 | 1486 | wv.loadDiagLastAddressAt = now | |
| 1469 | 1487 | cb := wv.callbacks | |
| 1470 | 1488 | pendingMatched := wv.clearPendingNavigationIfEquivalentLocked(uri) | |
| @@ -1484,6 +1502,13 @@ func (wv *WebView) updateURI(uri string) { | |||
| 1484 | 1502 | wv.runNavigationCallbacks(uri) | |
| 1485 | 1503 | } | |
| 1486 | 1504 | ||
| 1505 | + | // setCommittedURLsLocked records the raw committed CEF main-frame URL and | |
| 1506 | + | // the conceptual/public URI derived from it. The caller must hold wv.mu. | |
| 1507 | + | func (wv *WebView) setCommittedURLsLocked(raw, uri string) { | |
| 1508 | + | wv.committedURL = raw | |
| 1509 | + | wv.uri = uri | |
| 1510 | + | } | |
| 1511 | + | ||
| 1487 | 1512 | func (wv *WebView) updateTitle(title string) { | |
| 1488 | 1513 | wv.mu.Lock() | |
| 1489 | 1514 | wv.title = title | |
| @@ -2285,6 +2310,17 @@ func (wv *WebView) claimPendingNavigationSubmission(intentID uint64, browserID i | |||
| 2285 | 2310 | return uri, pendingClaimReady | |
| 2286 | 2311 | } | |
| 2287 | 2312 | ||
| 2313 | + | // documentIdentity is the private identity of a committed main-frame | |
| 2314 | + | // document: the callback browser identifier, the navigation intent ID, the | |
| 2315 | + | // document sequence, and the raw committed CEF URL. Conceptual and raw URL | |
| 2316 | + | // forms are never compared against each other. | |
| 2317 | + | type documentIdentity struct { | |
| 2318 | + | browserID int32 | |
| 2319 | + | intentID uint64 | |
| 2320 | + | documentSeq uint64 | |
| 2321 | + | committedURL string | |
| 2322 | + | } | |
| 2323 | + | ||
| 2288 | 2324 | // injectionEvent captures the navigation identity observed at main-frame | |
| 2289 | 2325 | // load-end so the GTK-dispatched installation can drop stale events: an | |
| 2290 | 2326 | // old main frame firing during process swap, or a queued callback outlived | |
| @@ -2294,10 +2330,7 @@ func (wv *WebView) claimPendingNavigationSubmission(intentID uint64, browserID i | |||
| 2294 | 2330 | // identity. noBrowserID marks captures taken while no browser was | |
| 2295 | 2331 | // attached. The current URL alone never proves document identity. | |
| 2296 | 2332 | type injectionEvent struct { | |
| 2297 | - | browserID int32 | |
| 2298 | - | intentID uint64 | |
| 2299 | - | documentSeq uint64 | |
| 2300 | - | frameURL string | |
| 2333 | + | documentIdentity | |
| 2301 | 2334 | } | |
| 2302 | 2335 | ||
| 2303 | 2336 | // noBrowserID marks an injection event captured while no browser was | |
| @@ -2315,7 +2348,7 @@ const noBrowserID = int32(-1) | |||
| 2315 | 2348 | // for the same document share the sequence and are installed once. | |
| 2316 | 2349 | func (wv *WebView) captureInjectionEvent(browser purecef.Browser, frameURL string) injectionEvent { | |
| 2317 | 2350 | if wv == nil || browser == nil { | |
| 2318 | - | return injectionEvent{browserID: noBrowserID} | |
| 2351 | + | return injectionEvent{documentIdentity: documentIdentity{browserID: noBrowserID}} | |
| 2319 | 2352 | } | |
| 2320 | 2353 | browserID := browser.GetIdentifier() | |
| 2321 | 2354 | wv.mu.RLock() | |
| @@ -2323,8 +2356,12 @@ func (wv *WebView) captureInjectionEvent(browser purecef.Browser, frameURL strin | |||
| 2323 | 2356 | documentSeq := wv.documentSeq | |
| 2324 | 2357 | wv.mu.RUnlock() | |
| 2325 | 2358 | return injectionEvent{ | |
| 2326 | - | browserID: browserID, intentID: intentID, documentSeq: documentSeq, | |
| 2327 | - | frameURL: strings.TrimSpace(frameURL), | |
| 2359 | + | documentIdentity: documentIdentity{ | |
| 2360 | + | browserID: browserID, | |
| 2361 | + | intentID: intentID, | |
| 2362 | + | documentSeq: documentSeq, | |
| 2363 | + | committedURL: strings.TrimSpace(frameURL), | |
| 2364 | + | }, | |
| 2328 | 2365 | } | |
| 2329 | 2366 | } | |
| 2330 | 2367 | ||
| @@ -256,7 +256,13 @@ type WebView struct { | |||||
| 256 | popupOpenerBridgeParentURI string | 256 | popupOpenerBridgeParentURI string | ||
| 257 | 257 | ||||
| 258 | // State cache (mutex-protected). | 258 | // State cache (mutex-protected). | ||
| 259 | + | // uri is the conceptual/public URI used by UI callbacks (for example | |||
| 260 | + | // dumb://history). committedURL is the raw committed CEF main-frame URL | |||
| 261 | + | // (for example https://dumber.invalid/history). The two forms are updated | |||
| 262 | + | // together through setCommittedURLsLocked; never compare one form against | |||
| 263 | + | // the other. | |||
| 259 | uri string | 264 | uri string | ||
| 265 | + | committedURL string | |||
| 260 | title string | 266 | title string | ||
| 261 | progress float64 | 267 | progress float64 | ||
| 262 | canGoBack bool | 268 | canGoBack bool | ||
| @@ -1148,9 +1154,11 @@ func (wv *WebView) activateNativePopup(popupID int32, targetURL string) (purecef | |||||
| 1148 | wv.setPendingNavigationLocked(toActualInternalURL(targetURL), time.Now()) | 1154 | wv.setPendingNavigationLocked(toActualInternalURL(targetURL), time.Now()) | ||
| 1149 | } | 1155 | } | ||
| 1150 | if strings.TrimSpace(wv.pendingURI) != "" { | 1156 | if strings.TrimSpace(wv.pendingURI) != "" { | ||
| 1151 | - | wv.uri = toConceptualInternalURL(wv.pendingURI) | 1157 | + | raw := strings.TrimSpace(wv.pendingURI) |
| 1158 | + | wv.setCommittedURLsLocked(raw, toConceptualInternalURL(raw)) | |||
| 1152 | } else { | 1159 | } else { | ||
| 1153 | - | wv.uri = toConceptualInternalURL(targetURL) | 1160 | + | raw := strings.TrimSpace(targetURL) |
| 1161 | + | wv.setCommittedURLsLocked(raw, toConceptualInternalURL(raw)) | |||
| 1154 | } | 1162 | } | ||
| 1155 | wv.isLoading = true | 1163 | wv.isLoading = true | ||
| 1156 | return wv.client, true | 1164 | return wv.client, true | ||
| @@ -1461,10 +1469,20 @@ func (wv *WebView) NativeWidget() uintptr { | |||||
| 1461 | // --------------------------------------------------------------------------- | 1469 | // --------------------------------------------------------------------------- | ||
| 1462 | 1470 | ||||
| 1463 | func (wv *WebView) updateURI(uri string) { | 1471 | func (wv *WebView) updateURI(uri string) { | ||
| 1464 | - | uri = toConceptualInternalURL(uri) | 1472 | + | wv.setCommittedURL(uri) |
| 1473 | + | } | |||
| 1474 | + | ||||
| 1475 | + | // setCommittedURL is the single owner of main-frame commit identity: it | |||
| 1476 | + | // updates the raw committed CEF URL and the conceptual/public URI together, | |||
| 1477 | + | // preserving pending-navigation acknowledgement, diagnostics timestamps, and | |||
| 1478 | + | // all current callbacks. raw is the unmodified committed URL; the conceptual | |||
| 1479 | + | // URI is derived from it. | |||
| 1480 | + | func (wv *WebView) setCommittedURL(raw string) { | |||
| 1481 | + | raw = strings.TrimSpace(raw) | |||
| 1482 | + | uri := toConceptualInternalURL(raw) | |||
| 1465 | now := time.Now() | 1483 | now := time.Now() | ||
| 1466 | wv.mu.Lock() | 1484 | wv.mu.Lock() | ||
| 1467 | - | wv.uri = uri | 1485 | + | wv.setCommittedURLsLocked(raw, uri) |
| 1468 | wv.loadDiagLastAddressAt = now | 1486 | wv.loadDiagLastAddressAt = now | ||
| 1469 | cb := wv.callbacks | 1487 | cb := wv.callbacks | ||
| 1470 | pendingMatched := wv.clearPendingNavigationIfEquivalentLocked(uri) | 1488 | pendingMatched := wv.clearPendingNavigationIfEquivalentLocked(uri) | ||
| @@ -1484,6 +1502,13 @@ func (wv *WebView) updateURI(uri string) { | |||||
| 1484 | wv.runNavigationCallbacks(uri) | 1502 | wv.runNavigationCallbacks(uri) | ||
| 1485 | } | 1503 | } | ||
| 1486 | 1504 | ||||
| 1505 | + | // setCommittedURLsLocked records the raw committed CEF main-frame URL and | |||
| 1506 | + | // the conceptual/public URI derived from it. The caller must hold wv.mu. | |||
| 1507 | + | func (wv *WebView) setCommittedURLsLocked(raw, uri string) { | |||
| 1508 | + | wv.committedURL = raw | |||
| 1509 | + | wv.uri = uri | |||
| 1510 | + | } | |||
| 1511 | + | ||||
| 1487 | func (wv *WebView) updateTitle(title string) { | 1512 | func (wv *WebView) updateTitle(title string) { | ||
| 1488 | wv.mu.Lock() | 1513 | wv.mu.Lock() | ||
| 1489 | wv.title = title | 1514 | wv.title = title | ||
| @@ -2285,6 +2310,17 @@ func (wv *WebView) claimPendingNavigationSubmission(intentID uint64, browserID i | |||||
| 2285 | return uri, pendingClaimReady | 2310 | return uri, pendingClaimReady | ||
| 2286 | } | 2311 | } | ||
| 2287 | 2312 | ||||
| 2313 | + | // documentIdentity is the private identity of a committed main-frame | |||
| 2314 | + | // document: the callback browser identifier, the navigation intent ID, the | |||
| 2315 | + | // document sequence, and the raw committed CEF URL. Conceptual and raw URL | |||
| 2316 | + | // forms are never compared against each other. | |||
| 2317 | + | type documentIdentity struct { | |||
| 2318 | + | browserID int32 | |||
| 2319 | + | intentID uint64 | |||
| 2320 | + | documentSeq uint64 | |||
| 2321 | + | committedURL string | |||
| 2322 | + | } | |||
| 2323 | + | ||||
| 2288 | // injectionEvent captures the navigation identity observed at main-frame | 2324 | // injectionEvent captures the navigation identity observed at main-frame | ||
| 2289 | // load-end so the GTK-dispatched installation can drop stale events: an | 2325 | // load-end so the GTK-dispatched installation can drop stale events: an | ||
| 2290 | // old main frame firing during process swap, or a queued callback outlived | 2326 | // old main frame firing during process swap, or a queued callback outlived | ||
| @@ -2294,10 +2330,7 @@ func (wv *WebView) claimPendingNavigationSubmission(intentID uint64, browserID i | |||||
| 2294 | // identity. noBrowserID marks captures taken while no browser was | 2330 | // identity. noBrowserID marks captures taken while no browser was | ||
| 2295 | // attached. The current URL alone never proves document identity. | 2331 | // attached. The current URL alone never proves document identity. | ||
| 2296 | type injectionEvent struct { | 2332 | type injectionEvent struct { | ||
| 2297 | - | browserID int32 | 2333 | + | documentIdentity |
| 2298 | - | intentID uint64 | |||
| 2299 | - | documentSeq uint64 | |||
| 2300 | - | frameURL string | |||
| 2301 | } | 2334 | } | ||
| 2302 | 2335 | ||||
| 2303 | // noBrowserID marks an injection event captured while no browser was | 2336 | // noBrowserID marks an injection event captured while no browser was | ||
| @@ -2315,7 +2348,7 @@ const noBrowserID = int32(-1) | |||||
| 2315 | // for the same document share the sequence and are installed once. | 2348 | // for the same document share the sequence and are installed once. | ||
| 2316 | func (wv *WebView) captureInjectionEvent(browser purecef.Browser, frameURL string) injectionEvent { | 2349 | func (wv *WebView) captureInjectionEvent(browser purecef.Browser, frameURL string) injectionEvent { | ||
| 2317 | if wv == nil || browser == nil { | 2350 | if wv == nil || browser == nil { | ||
| 2318 | - | return injectionEvent{browserID: noBrowserID} | 2351 | + | return injectionEvent{documentIdentity: documentIdentity{browserID: noBrowserID}} |
| 2319 | } | 2352 | } | ||
| 2320 | browserID := browser.GetIdentifier() | 2353 | browserID := browser.GetIdentifier() | ||
| 2321 | wv.mu.RLock() | 2354 | wv.mu.RLock() | ||
| @@ -2323,8 +2356,12 @@ func (wv *WebView) captureInjectionEvent(browser purecef.Browser, frameURL strin | |||||
| 2323 | documentSeq := wv.documentSeq | 2356 | documentSeq := wv.documentSeq | ||
| 2324 | wv.mu.RUnlock() | 2357 | wv.mu.RUnlock() | ||
| 2325 | return injectionEvent{ | 2358 | return injectionEvent{ | ||
| 2326 | - | browserID: browserID, intentID: intentID, documentSeq: documentSeq, | 2359 | + | documentIdentity: documentIdentity{ |
| 2327 | - | frameURL: strings.TrimSpace(frameURL), | 2360 | + | browserID: browserID, |
| 2361 | + | intentID: intentID, | |||
| 2362 | + | documentSeq: documentSeq, | |||
| 2363 | + | committedURL: strings.TrimSpace(frameURL), | |||
| 2364 | + | }, | |||
| 2328 | } | 2365 | } | ||
| 2329 | } | 2366 | } | ||
| 2330 | 2367 | ||||
M internal/infrastructure/cef/webview_handlers.go +13 -6
| @@ -183,13 +183,16 @@ func (h *handlerSet) OnFaviconUrlchange(browser purecef.Browser, iconURLs purece | |||
| 183 | 183 | return | |
| 184 | 184 | } | |
| 185 | 185 | h.wv.mu.RLock() | |
| 186 | - | pageURL, cb, currentBrowser, documentSeq := h.wv.uri, h.wv.callbacks, h.wv.browser, h.wv.documentSeq | |
| 186 | + | pageURL, committedURL, cb, currentBrowser, documentSeq := h.wv.uri, h.wv.committedURL, h.wv.callbacks, h.wv.browser, h.wv.documentSeq | |
| 187 | 187 | h.wv.mu.RUnlock() | |
| 188 | 188 | if cb == nil || cb.OnFaviconURLChanged == nil || browser == nil || currentBrowser == nil || | |
| 189 | 189 | browser.GetIdentifier() != currentBrowser.GetIdentifier() { | |
| 190 | 190 | return | |
| 191 | 191 | } | |
| 192 | - | candidates := resolveFaviconCandidates(pageURL, decodeCEFStringList(iconURLs)) | |
| 192 | + | if committedURL == "" { | |
| 193 | + | committedURL = pageURL | |
| 194 | + | } | |
| 195 | + | candidates := resolveFaviconCandidates(committedURL, decodeCEFStringList(iconURLs)) | |
| 193 | 196 | if len(candidates) == 0 { | |
| 194 | 197 | return | |
| 195 | 198 | } | |
| @@ -389,9 +392,13 @@ func (h *handlerSet) OnLoadingStateChange(browser purecef.Browser, isloading, ca | |||
| 389 | 392 | h.wv.mu.RLock() | |
| 390 | 393 | cb := h.wv.callbacks | |
| 391 | 394 | pageURL := h.wv.uri | |
| 395 | + | committedURL := h.wv.committedURL | |
| 392 | 396 | h.wv.mu.RUnlock() | |
| 397 | + | if committedURL == "" { | |
| 398 | + | committedURL = pageURL | |
| 399 | + | } | |
| 393 | 400 | if !loading && cb != nil && cb.OnFaviconURLChanged != nil { | |
| 394 | - | h.emitDiscoveredFaviconCandidates(browser, pageURL, cb) | |
| 401 | + | h.emitDiscoveredFaviconCandidates(browser, pageURL, committedURL, cb) | |
| 395 | 402 | } | |
| 396 | 403 | if cb != nil && cb.OnLoadChanged != nil { | |
| 397 | 404 | if loading { | |
| @@ -432,7 +439,7 @@ func (wv *WebView) retainFaviconSourceVisitor(visitor purecef.StringVisitor) fun | |||
| 432 | 439 | } | |
| 433 | 440 | } | |
| 434 | 441 | ||
| 435 | - | func (h *handlerSet) emitDiscoveredFaviconCandidates(browser purecef.Browser, pageURL string, cb *port.WebViewCallbacks) { | |
| 442 | + | func (h *handlerSet) emitDiscoveredFaviconCandidates(browser purecef.Browser, pageURL, committedURL string, cb *port.WebViewCallbacks) { | |
| 436 | 443 | if h == nil || h.wv == nil || cb == nil || cb.OnFaviconURLChanged == nil { | |
| 437 | 444 | return | |
| 438 | 445 | } | |
| @@ -455,7 +462,7 @@ func (h *handlerSet) emitDiscoveredFaviconCandidates(browser purecef.Browser, pa | |||
| 455 | 462 | } | |
| 456 | 463 | h.wv.mu.Unlock() | |
| 457 | 464 | } | |
| 458 | - | fallback := fallbackFaviconCandidates(pageURL) | |
| 465 | + | fallback := fallbackFaviconCandidates(committedURL) | |
| 459 | 466 | emit := func(candidates []string) { | |
| 460 | 467 | if len(candidates) == 0 { | |
| 461 | 468 | return | |
| @@ -496,7 +503,7 @@ func (h *handlerSet) emitDiscoveredFaviconCandidates(browser purecef.Browser, pa | |||
| 496 | 503 | if engineWon { | |
| 497 | 504 | return | |
| 498 | 505 | } | |
| 499 | - | candidates := DiscoverFaviconCandidates(pageURL, source) | |
| 506 | + | candidates := DiscoverFaviconCandidates(committedURL, source) | |
| 500 | 507 | if len(candidates) == 0 { | |
| 501 | 508 | candidates = fallback | |
| 502 | 509 | } | |
| @@ -183,13 +183,16 @@ func (h *handlerSet) OnFaviconUrlchange(browser purecef.Browser, iconURLs purece | |||||
| 183 | return | 183 | return | ||
| 184 | } | 184 | } | ||
| 185 | h.wv.mu.RLock() | 185 | h.wv.mu.RLock() | ||
| 186 | - | pageURL, cb, currentBrowser, documentSeq := h.wv.uri, h.wv.callbacks, h.wv.browser, h.wv.documentSeq | 186 | + | pageURL, committedURL, cb, currentBrowser, documentSeq := h.wv.uri, h.wv.committedURL, h.wv.callbacks, h.wv.browser, h.wv.documentSeq |
| 187 | h.wv.mu.RUnlock() | 187 | h.wv.mu.RUnlock() | ||
| 188 | if cb == nil || cb.OnFaviconURLChanged == nil || browser == nil || currentBrowser == nil || | 188 | if cb == nil || cb.OnFaviconURLChanged == nil || browser == nil || currentBrowser == nil || | ||
| 189 | browser.GetIdentifier() != currentBrowser.GetIdentifier() { | 189 | browser.GetIdentifier() != currentBrowser.GetIdentifier() { | ||
| 190 | return | 190 | return | ||
| 191 | } | 191 | } | ||
| 192 | - | candidates := resolveFaviconCandidates(pageURL, decodeCEFStringList(iconURLs)) | 192 | + | if committedURL == "" { |
| 193 | + | committedURL = pageURL | |||
| 194 | + | } | |||
| 195 | + | candidates := resolveFaviconCandidates(committedURL, decodeCEFStringList(iconURLs)) | |||
| 193 | if len(candidates) == 0 { | 196 | if len(candidates) == 0 { | ||
| 194 | return | 197 | return | ||
| 195 | } | 198 | } | ||
| @@ -389,9 +392,13 @@ func (h *handlerSet) OnLoadingStateChange(browser purecef.Browser, isloading, ca | |||||
| 389 | h.wv.mu.RLock() | 392 | h.wv.mu.RLock() | ||
| 390 | cb := h.wv.callbacks | 393 | cb := h.wv.callbacks | ||
| 391 | pageURL := h.wv.uri | 394 | pageURL := h.wv.uri | ||
| 395 | + | committedURL := h.wv.committedURL | |||
| 392 | h.wv.mu.RUnlock() | 396 | h.wv.mu.RUnlock() | ||
| 397 | + | if committedURL == "" { | |||
| 398 | + | committedURL = pageURL | |||
| 399 | + | } | |||
| 393 | if !loading && cb != nil && cb.OnFaviconURLChanged != nil { | 400 | if !loading && cb != nil && cb.OnFaviconURLChanged != nil { | ||
| 394 | - | h.emitDiscoveredFaviconCandidates(browser, pageURL, cb) | 401 | + | h.emitDiscoveredFaviconCandidates(browser, pageURL, committedURL, cb) |
| 395 | } | 402 | } | ||
| 396 | if cb != nil && cb.OnLoadChanged != nil { | 403 | if cb != nil && cb.OnLoadChanged != nil { | ||
| 397 | if loading { | 404 | if loading { | ||
| @@ -432,7 +439,7 @@ func (wv *WebView) retainFaviconSourceVisitor(visitor purecef.StringVisitor) fun | |||||
| 432 | } | 439 | } | ||
| 433 | } | 440 | } | ||
| 434 | 441 | ||||
| 435 | - | func (h *handlerSet) emitDiscoveredFaviconCandidates(browser purecef.Browser, pageURL string, cb *port.WebViewCallbacks) { | 442 | + | func (h *handlerSet) emitDiscoveredFaviconCandidates(browser purecef.Browser, pageURL, committedURL string, cb *port.WebViewCallbacks) { |
| 436 | if h == nil || h.wv == nil || cb == nil || cb.OnFaviconURLChanged == nil { | 443 | if h == nil || h.wv == nil || cb == nil || cb.OnFaviconURLChanged == nil { | ||
| 437 | return | 444 | return | ||
| 438 | } | 445 | } | ||
| @@ -455,7 +462,7 @@ func (h *handlerSet) emitDiscoveredFaviconCandidates(browser purecef.Browser, pa | |||||
| 455 | } | 462 | } | ||
| 456 | h.wv.mu.Unlock() | 463 | h.wv.mu.Unlock() | ||
| 457 | } | 464 | } | ||
| 458 | - | fallback := fallbackFaviconCandidates(pageURL) | 465 | + | fallback := fallbackFaviconCandidates(committedURL) |
| 459 | emit := func(candidates []string) { | 466 | emit := func(candidates []string) { | ||
| 460 | if len(candidates) == 0 { | 467 | if len(candidates) == 0 { | ||
| 461 | return | 468 | return | ||
| @@ -496,7 +503,7 @@ func (h *handlerSet) emitDiscoveredFaviconCandidates(browser purecef.Browser, pa | |||||
| 496 | if engineWon { | 503 | if engineWon { | ||
| 497 | return | 504 | return | ||
| 498 | } | 505 | } | ||
| 499 | - | candidates := DiscoverFaviconCandidates(pageURL, source) | 506 | + | candidates := DiscoverFaviconCandidates(committedURL, source) |
| 500 | if len(candidates) == 0 { | 507 | if len(candidates) == 0 { | ||
| 501 | candidates = fallback | 508 | candidates = fallback | ||
| 502 | } | 509 | } | ||
M internal/infrastructure/cef/webview_handlers_test.go +4 -0
| @@ -231,6 +231,10 @@ func TestNativePopupActivationDoesNotLetInitialBlankLoadClearPendingNavigationBe | |||
| 231 | 231 | require.Equal(t, int32(55), wv.nativePopupID) | |
| 232 | 232 | require.Same(t, parent, wv.nativePopupParent) | |
| 233 | 233 | require.Equal(t, "https://example.com/oauth", wv.pendingNavigationURI()) | |
| 234 | + | wv.mu.RLock() | |
| 235 | + | require.Equal(t, "https://example.com/oauth", wv.committedURL) | |
| 236 | + | require.Equal(t, "https://example.com/oauth", wv.uri) | |
| 237 | + | wv.mu.RUnlock() | |
| 234 | 238 | ||
| 235 | 239 | wv.updateURI("about:blank") | |
| 236 | 240 | wv.updateLoadState(false, false, false) | |
| @@ -231,6 +231,10 @@ func TestNativePopupActivationDoesNotLetInitialBlankLoadClearPendingNavigationBe | |||||
| 231 | require.Equal(t, int32(55), wv.nativePopupID) | 231 | require.Equal(t, int32(55), wv.nativePopupID) | ||
| 232 | require.Same(t, parent, wv.nativePopupParent) | 232 | require.Same(t, parent, wv.nativePopupParent) | ||
| 233 | require.Equal(t, "https://example.com/oauth", wv.pendingNavigationURI()) | 233 | require.Equal(t, "https://example.com/oauth", wv.pendingNavigationURI()) | ||
| 234 | + | wv.mu.RLock() | |||
| 235 | + | require.Equal(t, "https://example.com/oauth", wv.committedURL) | |||
| 236 | + | require.Equal(t, "https://example.com/oauth", wv.uri) | |||
| 237 | + | wv.mu.RUnlock() | |||
| 234 | 238 | ||||
| 235 | wv.updateURI("about:blank") | 239 | wv.updateURI("about:blank") | ||
| 236 | wv.updateLoadState(false, false, false) | 240 | wv.updateLoadState(false, false, false) | ||