diff --git a/src/browser/page.test.ts b/src/browser/page.test.ts index cf8d8f4c8..fc70a22fb 100644 --- a/src/browser/page.test.ts +++ b/src/browser/page.test.ts @@ -323,6 +323,28 @@ describe('Page active target tracking', () => { expect(retryCall[1]).not.toHaveProperty('page'); }); + it('retries on a bare "Page not found:" error without the stale-identity suffix', async () => { + // Under concurrent adapter calls the extension can reject with just + // "Page not found: " (no "— stale page identity" suffix) when the cached + // targetId was evicted. That still means the identity is dead, so goto() must + // drop it and retry once instead of cascading failures. + sendCommandFullMock + .mockResolvedValueOnce({ data: { url: 'https://example.com/first' }, page: 'page-1' }) + .mockRejectedValueOnce(new Error('Page not found: deadbeef')) + .mockResolvedValueOnce({ data: { url: 'https://example.com/second' }, page: 'page-2' }); + + const page = new Page('site:youtube', undefined, undefined, undefined, 'adapter', 'persistent'); + + await page.goto('https://example.com/first', { waitUntil: 'none' }); + await page.goto('https://example.com/second', { waitUntil: 'none' }); + expect(page.getActivePage()).toBe('page-2'); + + expect(sendCommandFullMock).toHaveBeenCalledTimes(3); + const retryCall = sendCommandFullMock.mock.calls[2]; + expect(retryCall[0]).toBe('navigate'); + expect(retryCall[1]).not.toHaveProperty('page'); + }); + it('does not retry stale page errors when no identity was cached', async () => { // _page is undefined on a fresh Page — there's nothing to drop, so propagate the // error instead of silently retrying with the same params. diff --git a/src/browser/page.ts b/src/browser/page.ts index eedf4b28c..5bb10dbd8 100644 --- a/src/browser/page.ts +++ b/src/browser/page.ts @@ -33,7 +33,7 @@ function isUnsupportedNetworkCaptureError(err: unknown): boolean { // to the session lease (or create a fresh tab). function isStalePageIdentityError(err: unknown): boolean { const message = err instanceof Error ? err.message : String(err); - return message.includes('stale page identity'); + return message.includes('stale page identity') || message.includes('Page not found:'); } /** @@ -94,9 +94,9 @@ export class Page extends BasePage { } catch (err) { // If our cached targetId went stale (tab closed externally, identity evicted), // drop the dead id and retry without it — the extension will resolve through the - // session lease or open a fresh automation tab. Without this, subsequent - // navigations in the same Page instance keep re-sending the same dead targetId - // and cascade into "Page not found:" failures. + // session lease or open a fresh automation tab. Without this, every subsequent + // adapter call in the same process keeps re-sending the same dead targetId and + // cascades into "Page not found:" failures across concurrent calls. if (!isStalePageIdentityError(err) || this._page === undefined) throw err; this._page = undefined; result = await sendCommandFull('navigate', {