diff --git a/src/main/browser/offscreen-browser-backend-reclaim.test.ts b/src/main/browser/offscreen-browser-backend-reclaim.test.ts index 2ed00406305..157f26d89e9 100644 --- a/src/main/browser/offscreen-browser-backend-reclaim.test.ts +++ b/src/main/browser/offscreen-browser-backend-reclaim.test.ts @@ -552,6 +552,52 @@ describe('OffscreenBrowserBackend reclamation', () => { expect(await second).toBe(true) }) + it('does not let an abandoned wake clear a replacement page wake lock', async () => { + // Why: dropping the replacement's lock would let the next command see a + // live window with no wake in flight and drive a half-rebuilt renderer. + let releaseFirstSwap = (): void => {} + let releaseSecondSwap = (): void => {} + const h = createHarness() + await h.backend.createTab({ url: 'https://a', browserPageId: 'a' }) + h.clock.value += 120_000 + await h.backend.reclaimIdlePages() + + const bridge = h.bridge as unknown as { onProcessSwap: ReturnType } + bridge.onProcessSwap.mockImplementationOnce( + async () => new Promise((resolve) => (releaseFirstSwap = resolve)) + ) + const abandoned = h.backend.wakeTab('a') + await Promise.resolve() + + await h.backend.closeTab('a') + await h.backend.createTab({ url: 'https://replacement', browserPageId: 'a' }) + h.clock.value += 120_000 + await h.backend.reclaimIdlePages() + + bridge.onProcessSwap.mockImplementationOnce( + async () => new Promise((resolve) => (releaseSecondSwap = resolve)) + ) + const replacementWake = h.backend.wakeTab('a') + await Promise.resolve() + + // The old wake finishing must not release the replacement's lock. + releaseFirstSwap() + expect(await abandoned).toBe(false) + + let thirdSettled = false + const third = h.backend.wakeTab('a').then((value) => { + thirdSettled = true + return value + }) + await Promise.resolve() + await Promise.resolve() + expect(thirdSettled).toBe(false) + + releaseSecondSwap() + expect(await replacementWake).toBe(true) + expect(await third).toBe(true) + }) + it('reports false when waking a page it does not own', async () => { const h = createHarness() expect(await h.backend.wakeTab('nope')).toBe(false) diff --git a/src/main/browser/offscreen-browser-backend.ts b/src/main/browser/offscreen-browser-backend.ts index 49636eff5d9..e8154b8fc24 100644 --- a/src/main/browser/offscreen-browser-backend.ts +++ b/src/main/browser/offscreen-browser-backend.ts @@ -181,7 +181,13 @@ export class OffscreenBrowserBackend implements BrowserBackend { try { return await wake } finally { - this.waking.delete(browserPageId) + // Why: an abandoned wake can outlive a close and a replacement page's own + // wake. Clearing the entry blindly would drop the replacement's lock, and + // the next command would see a live window with no wake in flight and + // drive a renderer that is still being rebuilt. + if (this.waking.get(browserPageId) === wake) { + this.waking.delete(browserPageId) + } } }