cherry-pick(#18997): fix(reuse): stop pending operations upon reuse/disconnect

SHA 503f8f51dc
This commit is contained in:
Dmitry Gozman 2022-11-22 15:21:20 -08:00 committed by Andrey Lushnikov
parent 76dc43e9ba
commit 9ede66abd6
6 changed files with 41 additions and 1 deletions

View file

@ -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();

View file

@ -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 };
}

View file

@ -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<string, PageBinding>();
readonly _activeProgressControllers = new Set<ProgressController>();
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 };

View file

@ -78,6 +78,7 @@ export abstract class APIRequestContext extends SdkObject {
readonly fetchResponses: Map<string, Buffer> = new Map();
readonly fetchLog: Map<string, string[]> = new Map();
protected static allInstances: Set<APIRequestContext> = new Set();
readonly _activeProgressControllers = new Set<ProgressController>();
static findResponseBody(guid: string): Buffer | undefined {
for (const request of APIRequestContext.allInstances) {

View file

@ -63,6 +63,10 @@ export class ProgressController {
return this._lastIntermediateResult;
}
abort(error: Error) {
this._forceAbortPromise.reject(error);
}
async run<T>(task: (progress: Progress) => Promise<T>, timeout?: number): Promise<T> {
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);
}
}

View file

@ -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('<button onclick="window._clicked=true">click me</button>');
// 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);
});