From 3c8672c82c9d39af0905a77290f117e131c8f5c3 Mon Sep 17 00:00:00 2001 From: Devin Rousso Date: Tue, 9 Jun 2026 13:41:14 -0600 Subject: [PATCH] fix(firefox): treat `navigationCommitted` events without `navigationId` as same-document URL updates Firefox juggler replays the current frame state by emitting `Page.navigationCommitted` with no `navigationId` after a process swap (e.g. while restoring a persistent profile) in Windows these replayed events can arrive after `Page.ready`, racing with a user `goto` and causing `Frame.gotoImpl` to throw `"interrupted by another navigation"` treat these as same-document URL updates so they don't interrupt any in-flight new-document navigation in `Frame.gotoImpl` --- .../src/server/firefox/ffPage.ts | 9 ++++++++- tests/library/defaultbrowsercontext-2.spec.ts | 17 +++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/playwright-core/src/server/firefox/ffPage.ts b/packages/playwright-core/src/server/firefox/ffPage.ts index 12f386f3560b9..f291c5f6fbd7f 100644 --- a/packages/playwright-core/src/server/firefox/ffPage.ts +++ b/packages/playwright-core/src/server/firefox/ffPage.ts @@ -239,11 +239,18 @@ export class FFPage implements PageDelegate { } _onNavigationCommitted(params: Protocol.Page.navigationCommittedPayload) { + if (!params.navigationId) { + // Firefox replays the current navigation without a navigationId during a process swap (e.g. when restoring a persistent profile). + // Treat these as same-document URL updates so they don't interrupt any in-flight new-document navigation in Frame.gotoImpl. + this._page.frameManager.frameCommittedSameDocumentNavigation(params.frameId, params.url); + return; + } + for (const [workerId, worker] of this._workers) { if (worker.frameId === params.frameId) this._onWorkerDestroyed({ workerId }); } - this._page.frameManager.frameCommittedNewDocumentNavigation(params.frameId, params.url, params.name || '', params.navigationId || '', false); + this._page.frameManager.frameCommittedNewDocumentNavigation(params.frameId, params.url, params.name || '', params.navigationId, false); } _onSameDocumentNavigation(params: Protocol.Page.sameDocumentNavigationPayload) { diff --git a/tests/library/defaultbrowsercontext-2.spec.ts b/tests/library/defaultbrowsercontext-2.spec.ts index 516f114a8b141..d583cb0b0bf5a 100644 --- a/tests/library/defaultbrowsercontext-2.spec.ts +++ b/tests/library/defaultbrowsercontext-2.spec.ts @@ -142,6 +142,23 @@ it('should create userDataDir if it does not exist', async ({ createUserDataDir, expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); }); +it('should goto about:blank on relaunched persistent context', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41216' }, +}, async ({ browserType, createUserDataDir }) => { + const userDataDir = await createUserDataDir(); + + const context1 = await browserType.launchPersistentContext(userDataDir); + await context1.pages()[0].goto('about:blank'); + await context1.close(); + + // When relaunching with an existing profile, Firefox session restore can race with the user's goto and cause "interrupted by another navigation". + // This issue is timing-sensitive and might not fire on every run, so rely on CI's --repeat-each matrix for coverage. + const context2 = await browserType.launchPersistentContext(userDataDir); + await context2.pages()[0].goto('about:blank'); + expect(context2.pages()[0].url()).toBe('about:blank'); + await context2.close(); +}); + it('should have default URL when launching browser', async ({ launchPersistent }) => { const { context } = await launchPersistent(); const urls = context.pages().map(page => page.url());