cherry-pick(#18997): fix(reuse): stop pending operations upon reuse/disconnect
SHA 503f8f51dc
This commit is contained in:
parent
76dc43e9ba
commit
9ede66abd6
|
|
@ -208,6 +208,8 @@ export class PlaywrightConnection {
|
||||||
for (const context of browser.contexts()) {
|
for (const context of browser.contexts()) {
|
||||||
if (!context.pages().length)
|
if (!context.pages().length)
|
||||||
await context.close(serverSideCallMetadata());
|
await context.close(serverSideCallMetadata());
|
||||||
|
else
|
||||||
|
await context.stopPendingOperations();
|
||||||
}
|
}
|
||||||
if (!browser.contexts())
|
if (!browser.contexts())
|
||||||
await browser.close();
|
await browser.close();
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,7 @@ export abstract class Browser extends SdkObject {
|
||||||
this._contextForReuse = { context: await this.newContext(metadata, params), hash };
|
this._contextForReuse = { context: await this.newContext(metadata, params), hash };
|
||||||
return { context: this._contextForReuse.context, needsReset: false };
|
return { context: this._contextForReuse.context, needsReset: false };
|
||||||
}
|
}
|
||||||
|
await this._contextForReuse.context.stopPendingOperations();
|
||||||
return { context: this._contextForReuse.context, needsReset: true };
|
return { context: this._contextForReuse.context, needsReset: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ import { helper } from './helper';
|
||||||
import * as network from './network';
|
import * as network from './network';
|
||||||
import type { PageDelegate } from './page';
|
import type { PageDelegate } from './page';
|
||||||
import { Page, PageBinding } 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 { Selectors } from './selectors';
|
||||||
import type * as types from './types';
|
import type * as types from './types';
|
||||||
import type * as channels from '@protocol/channels';
|
import type * as channels from '@protocol/channels';
|
||||||
|
|
@ -56,6 +56,7 @@ export abstract class BrowserContext extends SdkObject {
|
||||||
|
|
||||||
readonly _timeoutSettings = new TimeoutSettings();
|
readonly _timeoutSettings = new TimeoutSettings();
|
||||||
readonly _pageBindings = new Map<string, PageBinding>();
|
readonly _pageBindings = new Map<string, PageBinding>();
|
||||||
|
readonly _activeProgressControllers = new Set<ProgressController>();
|
||||||
readonly _options: channels.BrowserNewContextParams;
|
readonly _options: channels.BrowserNewContextParams;
|
||||||
_requestInterceptor?: network.RouteHandler;
|
_requestInterceptor?: network.RouteHandler;
|
||||||
private _isPersistentContext: boolean;
|
private _isPersistentContext: boolean;
|
||||||
|
|
@ -145,6 +146,11 @@ export abstract class BrowserContext extends SdkObject {
|
||||||
return true;
|
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 {
|
static reusableContextHash(params: channels.BrowserNewContextForReuseParams): string {
|
||||||
const paramsCopy = { ...params };
|
const paramsCopy = { ...params };
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@ export abstract class APIRequestContext extends SdkObject {
|
||||||
readonly fetchResponses: Map<string, Buffer> = new Map();
|
readonly fetchResponses: Map<string, Buffer> = new Map();
|
||||||
readonly fetchLog: Map<string, string[]> = new Map();
|
readonly fetchLog: Map<string, string[]> = new Map();
|
||||||
protected static allInstances: Set<APIRequestContext> = new Set();
|
protected static allInstances: Set<APIRequestContext> = new Set();
|
||||||
|
readonly _activeProgressControllers = new Set<ProgressController>();
|
||||||
|
|
||||||
static findResponseBody(guid: string): Buffer | undefined {
|
static findResponseBody(guid: string): Buffer | undefined {
|
||||||
for (const request of APIRequestContext.allInstances) {
|
for (const request of APIRequestContext.allInstances) {
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,10 @@ export class ProgressController {
|
||||||
return this._lastIntermediateResult;
|
return this._lastIntermediateResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abort(error: Error) {
|
||||||
|
this._forceAbortPromise.reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
async run<T>(task: (progress: Progress) => Promise<T>, timeout?: number): Promise<T> {
|
async run<T>(task: (progress: Progress) => Promise<T>, timeout?: number): Promise<T> {
|
||||||
if (timeout) {
|
if (timeout) {
|
||||||
this._timeout = timeout;
|
this._timeout = timeout;
|
||||||
|
|
@ -71,6 +75,7 @@ export class ProgressController {
|
||||||
|
|
||||||
assert(this._state === 'before');
|
assert(this._state === 'before');
|
||||||
this._state = 'running';
|
this._state = 'running';
|
||||||
|
this.sdkObject.attribution.context?._activeProgressControllers.add(this);
|
||||||
|
|
||||||
const progress: Progress = {
|
const progress: Progress = {
|
||||||
log: message => {
|
log: message => {
|
||||||
|
|
@ -117,6 +122,7 @@ export class ProgressController {
|
||||||
await Promise.all(this._cleanups.splice(0).map(runCleanup));
|
await Promise.all(this._cleanups.splice(0).map(runCleanup));
|
||||||
throw e;
|
throw e;
|
||||||
} finally {
|
} finally {
|
||||||
|
this.sdkObject.attribution.context?._activeProgressControllers.delete(this);
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -374,3 +374,27 @@ test('should reuse context with beforeunload', async ({ runInlineTest }) => {
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
expect(result.passed).toBe(2);
|
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);
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue