diff --git a/packages/playwright-core/src/remote/playwrightConnection.ts b/packages/playwright-core/src/remote/playwrightConnection.ts index 64300e91f9a39..f223362b44b4c 100644 --- a/packages/playwright-core/src/remote/playwrightConnection.ts +++ b/packages/playwright-core/src/remote/playwrightConnection.ts @@ -208,6 +208,8 @@ export class PlaywrightConnection { for (const context of browser.contexts()) { if (!context.pages().length) await context.close(serverSideCallMetadata()); + else + await context.stopPendingOperations(); } if (!browser.contexts()) await browser.close(); diff --git a/packages/playwright-core/src/server/browser.ts b/packages/playwright-core/src/server/browser.ts index ce709e315a67d..080d9b5c1feca 100644 --- a/packages/playwright-core/src/server/browser.ts +++ b/packages/playwright-core/src/server/browser.ts @@ -106,6 +106,7 @@ export abstract class Browser extends SdkObject { this._contextForReuse = { context: await this.newContext(metadata, params), hash }; return { context: this._contextForReuse.context, needsReset: false }; } + await this._contextForReuse.context.stopPendingOperations(); return { context: this._contextForReuse.context, needsReset: true }; } diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index 7c556a998044b..576f844519353 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -26,7 +26,7 @@ import { helper } from './helper'; import * as network from './network'; import type { PageDelegate } from './page'; import { Page, PageBinding } from './page'; -import type { Progress } from './progress'; +import type { Progress, ProgressController } from './progress'; import type { Selectors } from './selectors'; import type * as types from './types'; import type * as channels from '@protocol/channels'; @@ -56,6 +56,7 @@ export abstract class BrowserContext extends SdkObject { readonly _timeoutSettings = new TimeoutSettings(); readonly _pageBindings = new Map(); + readonly _activeProgressControllers = new Set(); readonly _options: channels.BrowserNewContextParams; _requestInterceptor?: network.RouteHandler; private _isPersistentContext: boolean; @@ -145,6 +146,11 @@ export abstract class BrowserContext extends SdkObject { return true; } + async stopPendingOperations() { + for (const controller of this._activeProgressControllers) + controller.abort(new Error(`Context was reset for reuse.`)); + } + static reusableContextHash(params: channels.BrowserNewContextForReuseParams): string { const paramsCopy = { ...params }; diff --git a/packages/playwright-core/src/server/fetch.ts b/packages/playwright-core/src/server/fetch.ts index 44df8f6a5dae7..45ec599ead607 100644 --- a/packages/playwright-core/src/server/fetch.ts +++ b/packages/playwright-core/src/server/fetch.ts @@ -78,6 +78,7 @@ export abstract class APIRequestContext extends SdkObject { readonly fetchResponses: Map = new Map(); readonly fetchLog: Map = new Map(); protected static allInstances: Set = new Set(); + readonly _activeProgressControllers = new Set(); static findResponseBody(guid: string): Buffer | undefined { for (const request of APIRequestContext.allInstances) { diff --git a/packages/playwright-core/src/server/progress.ts b/packages/playwright-core/src/server/progress.ts index 61caf732f0d62..f0462877e09ee 100644 --- a/packages/playwright-core/src/server/progress.ts +++ b/packages/playwright-core/src/server/progress.ts @@ -63,6 +63,10 @@ export class ProgressController { return this._lastIntermediateResult; } + abort(error: Error) { + this._forceAbortPromise.reject(error); + } + async run(task: (progress: Progress) => Promise, timeout?: number): Promise { if (timeout) { this._timeout = timeout; @@ -71,6 +75,7 @@ export class ProgressController { assert(this._state === 'before'); this._state = 'running'; + this.sdkObject.attribution.context?._activeProgressControllers.add(this); const progress: Progress = { log: message => { @@ -117,6 +122,7 @@ export class ProgressController { await Promise.all(this._cleanups.splice(0).map(runCleanup)); throw e; } finally { + this.sdkObject.attribution.context?._activeProgressControllers.delete(this); clearTimeout(timer); } } diff --git a/tests/playwright-test/playwright.reuse.spec.ts b/tests/playwright-test/playwright.reuse.spec.ts index 1036e81229074..8017e99f44735 100644 --- a/tests/playwright-test/playwright.reuse.spec.ts +++ b/tests/playwright-test/playwright.reuse.spec.ts @@ -374,3 +374,27 @@ test('should reuse context with beforeunload', async ({ runInlineTest }) => { expect(result.exitCode).toBe(0); expect(result.passed).toBe(2); }); + +test('should cancel pending operations upon reuse', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'src/reuse.test.ts': ` + const { test } = pwt; + test('one', async ({ page }) => { + await Promise.race([ + page.getByText('click me').click().catch(e => {}), + page.waitForTimeout(2000), + ]); + }); + + test('two', async ({ page }) => { + await page.setContent(''); + // Give it time to erroneously click. + await page.waitForTimeout(2000); + expect(await page.evaluate('window._clicked')).toBe(undefined); + }); + `, + }, { workers: 1 }, { PW_TEST_REUSE_CONTEXT: '1' }); + + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(2); +});