chore: do not leak internal page handles after closing page (#24169)
Partial fix for https://github.com/microsoft/playwright/issues/6319 After this fix, the following scenario won't leak and the context state (cookies, storage, etc) can be reused by the new page sessions: ```js for (let i = 0; i < 1000; ++i) { const page = await context.newPage(); await page.goto('...'); await page.close('...'); } ```
This commit is contained in:
parent
ea1ec112d8
commit
53bf1995db
35
package-lock.json
generated
35
package-lock.json
generated
|
|
@ -49,6 +49,7 @@
|
||||||
"eslint-plugin-notice": "^0.9.10",
|
"eslint-plugin-notice": "^0.9.10",
|
||||||
"eslint-plugin-react-hooks": "^4.3.0",
|
"eslint-plugin-react-hooks": "^4.3.0",
|
||||||
"formidable": "^2.1.1",
|
"formidable": "^2.1.1",
|
||||||
|
"heapdump": "^0.3.15",
|
||||||
"license-checker": "^25.0.1",
|
"license-checker": "^25.0.1",
|
||||||
"mime": "^3.0.0",
|
"mime": "^3.0.0",
|
||||||
"ncp": "^2.0.0",
|
"ncp": "^2.0.0",
|
||||||
|
|
@ -4000,6 +4001,19 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/heapdump": {
|
||||||
|
"version": "0.3.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/heapdump/-/heapdump-0.3.15.tgz",
|
||||||
|
"integrity": "sha512-n8aSFscI9r3gfhOcAECAtXFaQ1uy4QSke6bnaL+iymYZ/dWs9cqDqHM+rALfsHUwukUbxsdlECZ0pKmJdQ/4OA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"dependencies": {
|
||||||
|
"nan": "^2.13.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/hexoid": {
|
"node_modules/hexoid": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|
@ -4531,6 +4545,12 @@
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/nan": {
|
||||||
|
"version": "2.17.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz",
|
||||||
|
"integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.6",
|
"version": "3.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
|
||||||
|
|
@ -9070,6 +9090,15 @@
|
||||||
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
|
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"heapdump": {
|
||||||
|
"version": "0.3.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/heapdump/-/heapdump-0.3.15.tgz",
|
||||||
|
"integrity": "sha512-n8aSFscI9r3gfhOcAECAtXFaQ1uy4QSke6bnaL+iymYZ/dWs9cqDqHM+rALfsHUwukUbxsdlECZ0pKmJdQ/4OA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"nan": "^2.13.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"hexoid": {
|
"hexoid": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dev": true
|
"dev": true
|
||||||
|
|
@ -9428,6 +9457,12 @@
|
||||||
"ms": {
|
"ms": {
|
||||||
"version": "2.1.2"
|
"version": "2.1.2"
|
||||||
},
|
},
|
||||||
|
"nan": {
|
||||||
|
"version": "2.17.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz",
|
||||||
|
"integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"nanoid": {
|
"nanoid": {
|
||||||
"version": "3.3.6",
|
"version": "3.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@
|
||||||
"eslint-plugin-notice": "^0.9.10",
|
"eslint-plugin-notice": "^0.9.10",
|
||||||
"eslint-plugin-react-hooks": "^4.3.0",
|
"eslint-plugin-react-hooks": "^4.3.0",
|
||||||
"formidable": "^2.1.1",
|
"formidable": "^2.1.1",
|
||||||
|
"heapdump": "^0.3.15",
|
||||||
"license-checker": "^25.0.1",
|
"license-checker": "^25.0.1",
|
||||||
"mime": "^3.0.0",
|
"mime": "^3.0.0",
|
||||||
"ncp": "^2.0.0",
|
"ncp": "^2.0.0",
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,8 @@ export class Dispatcher<Type extends { guid: string }, ChannelType, ParentScopeT
|
||||||
}
|
}
|
||||||
|
|
||||||
adopt(child: DispatcherScope) {
|
adopt(child: DispatcherScope) {
|
||||||
|
if (child._parent === this)
|
||||||
|
return;
|
||||||
const oldParent = child._parent!;
|
const oldParent = child._parent!;
|
||||||
oldParent._dispatchers.delete(child._guid);
|
oldParent._dispatchers.delete(child._guid);
|
||||||
this._dispatchers.set(child._guid, child);
|
this._dispatchers.set(child._guid, child);
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,8 @@ import type { CallMetadata } from '../instrumentation';
|
||||||
import type { WritableStreamDispatcher } from './writableStreamDispatcher';
|
import type { WritableStreamDispatcher } from './writableStreamDispatcher';
|
||||||
import { assert } from '../../utils';
|
import { assert } from '../../utils';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import type { PageDispatcher } from './pageDispatcher';
|
import { BrowserContextDispatcher } from './browserContextDispatcher';
|
||||||
|
import { PageDispatcher, WorkerDispatcher } from './pageDispatcher';
|
||||||
|
|
||||||
export class ElementHandleDispatcher extends JSHandleDispatcher implements channels.ElementHandleChannel {
|
export class ElementHandleDispatcher extends JSHandleDispatcher implements channels.ElementHandleChannel {
|
||||||
_type_ElementHandle = true;
|
_type_ElementHandle = true;
|
||||||
|
|
@ -57,12 +58,12 @@ export class ElementHandleDispatcher extends JSHandleDispatcher implements chann
|
||||||
|
|
||||||
async ownerFrame(params: channels.ElementHandleOwnerFrameParams, metadata: CallMetadata): Promise<channels.ElementHandleOwnerFrameResult> {
|
async ownerFrame(params: channels.ElementHandleOwnerFrameParams, metadata: CallMetadata): Promise<channels.ElementHandleOwnerFrameResult> {
|
||||||
const frame = await this._elementHandle.ownerFrame();
|
const frame = await this._elementHandle.ownerFrame();
|
||||||
return { frame: frame ? FrameDispatcher.from(this.parentScope() as PageDispatcher, frame) : undefined };
|
return { frame: frame ? FrameDispatcher.from(this._browserContextDispatcher(), frame) : undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
async contentFrame(params: channels.ElementHandleContentFrameParams, metadata: CallMetadata): Promise<channels.ElementHandleContentFrameResult> {
|
async contentFrame(params: channels.ElementHandleContentFrameParams, metadata: CallMetadata): Promise<channels.ElementHandleContentFrameResult> {
|
||||||
const frame = await this._elementHandle.contentFrame();
|
const frame = await this._elementHandle.contentFrame();
|
||||||
return { frame: frame ? FrameDispatcher.from(this.parentScope() as PageDispatcher, frame) : undefined };
|
return { frame: frame ? FrameDispatcher.from(this._browserContextDispatcher(), frame) : undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAttribute(params: channels.ElementHandleGetAttributeParams, metadata: CallMetadata): Promise<channels.ElementHandleGetAttributeResult> {
|
async getAttribute(params: channels.ElementHandleGetAttributeParams, metadata: CallMetadata): Promise<channels.ElementHandleGetAttributeResult> {
|
||||||
|
|
@ -223,4 +224,20 @@ export class ElementHandleDispatcher extends JSHandleDispatcher implements chann
|
||||||
async waitForSelector(params: channels.ElementHandleWaitForSelectorParams, metadata: CallMetadata): Promise<channels.ElementHandleWaitForSelectorResult> {
|
async waitForSelector(params: channels.ElementHandleWaitForSelectorParams, metadata: CallMetadata): Promise<channels.ElementHandleWaitForSelectorResult> {
|
||||||
return { element: ElementHandleDispatcher.fromNullable(this.parentScope(), await this._elementHandle.waitForSelector(metadata, params.selector, params)) };
|
return { element: ElementHandleDispatcher.fromNullable(this.parentScope(), await this._elementHandle.waitForSelector(metadata, params.selector, params)) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _browserContextDispatcher(): BrowserContextDispatcher {
|
||||||
|
const scope = this.parentScope();
|
||||||
|
if (scope instanceof BrowserContextDispatcher)
|
||||||
|
return scope;
|
||||||
|
if (scope instanceof PageDispatcher)
|
||||||
|
return scope.parentScope();
|
||||||
|
if ((scope instanceof WorkerDispatcher) || (scope instanceof FrameDispatcher)) {
|
||||||
|
const parentScope = scope.parentScope();
|
||||||
|
if (parentScope instanceof BrowserContextDispatcher)
|
||||||
|
return parentScope;
|
||||||
|
return parentScope.parentScope();
|
||||||
|
}
|
||||||
|
throw new Error('ElementHandle belongs to unexpected scope');
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,30 +26,34 @@ import type { CallMetadata } from '../instrumentation';
|
||||||
import type { WritableStreamDispatcher } from './writableStreamDispatcher';
|
import type { WritableStreamDispatcher } from './writableStreamDispatcher';
|
||||||
import { assert } from '../../utils';
|
import { assert } from '../../utils';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import type { BrowserContextDispatcher } from './browserContextDispatcher';
|
||||||
import type { PageDispatcher } from './pageDispatcher';
|
import type { PageDispatcher } from './pageDispatcher';
|
||||||
|
|
||||||
export class FrameDispatcher extends Dispatcher<Frame, channels.FrameChannel, PageDispatcher> implements channels.FrameChannel {
|
export class FrameDispatcher extends Dispatcher<Frame, channels.FrameChannel, BrowserContextDispatcher | PageDispatcher> implements channels.FrameChannel {
|
||||||
_type_Frame = true;
|
_type_Frame = true;
|
||||||
private _frame: Frame;
|
private _frame: Frame;
|
||||||
|
private _browserContextDispatcher: BrowserContextDispatcher;
|
||||||
|
|
||||||
static from(scope: PageDispatcher, frame: Frame): FrameDispatcher {
|
static from(scope: BrowserContextDispatcher, frame: Frame): FrameDispatcher {
|
||||||
const result = existingDispatcher<FrameDispatcher>(frame);
|
const result = existingDispatcher<FrameDispatcher>(frame);
|
||||||
return result || new FrameDispatcher(scope, frame);
|
return result || new FrameDispatcher(scope, frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromNullable(scope: PageDispatcher, frame: Frame | null): FrameDispatcher | undefined {
|
static fromNullable(scope: BrowserContextDispatcher, frame: Frame | null): FrameDispatcher | undefined {
|
||||||
if (!frame)
|
if (!frame)
|
||||||
return;
|
return;
|
||||||
return FrameDispatcher.from(scope, frame);
|
return FrameDispatcher.from(scope, frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
private constructor(scope: PageDispatcher, frame: Frame) {
|
private constructor(scope: BrowserContextDispatcher, frame: Frame) {
|
||||||
super(scope, frame, 'Frame', {
|
const pageDispatcher = existingDispatcher<PageDispatcher>(frame._page);
|
||||||
|
super(pageDispatcher || scope, frame, 'Frame', {
|
||||||
url: frame.url(),
|
url: frame.url(),
|
||||||
name: frame.name(),
|
name: frame.name(),
|
||||||
parentFrame: FrameDispatcher.fromNullable(scope, frame.parentFrame()),
|
parentFrame: FrameDispatcher.fromNullable(scope, frame.parentFrame()),
|
||||||
loadStates: Array.from(frame._firedLifecycleEvents),
|
loadStates: Array.from(frame._firedLifecycleEvents),
|
||||||
});
|
});
|
||||||
|
this._browserContextDispatcher = scope;
|
||||||
this._frame = frame;
|
this._frame = frame;
|
||||||
this.addObjectListener(Frame.Events.AddLifecycle, lifecycleEvent => {
|
this.addObjectListener(Frame.Events.AddLifecycle, lifecycleEvent => {
|
||||||
this._dispatchEvent('loadstate', { add: lifecycleEvent });
|
this._dispatchEvent('loadstate', { add: lifecycleEvent });
|
||||||
|
|
@ -62,17 +66,17 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameChannel, Pa
|
||||||
return;
|
return;
|
||||||
const params = { url: event.url, name: event.name, error: event.error ? event.error.message : undefined };
|
const params = { url: event.url, name: event.name, error: event.error ? event.error.message : undefined };
|
||||||
if (event.newDocument)
|
if (event.newDocument)
|
||||||
(params as any).newDocument = { request: RequestDispatcher.fromNullable(this.parentScope().parentScope(), event.newDocument.request || null) };
|
(params as any).newDocument = { request: RequestDispatcher.fromNullable(this._browserContextDispatcher, event.newDocument.request || null) };
|
||||||
this._dispatchEvent('navigated', params);
|
this._dispatchEvent('navigated', params);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async goto(params: channels.FrameGotoParams, metadata: CallMetadata): Promise<channels.FrameGotoResult> {
|
async goto(params: channels.FrameGotoParams, metadata: CallMetadata): Promise<channels.FrameGotoResult> {
|
||||||
return { response: ResponseDispatcher.fromNullable(this.parentScope().parentScope(), await this._frame.goto(metadata, params.url, params)) };
|
return { response: ResponseDispatcher.fromNullable(this._browserContextDispatcher, await this._frame.goto(metadata, params.url, params)) };
|
||||||
}
|
}
|
||||||
|
|
||||||
async frameElement(): Promise<channels.FrameFrameElementResult> {
|
async frameElement(): Promise<channels.FrameFrameElementResult> {
|
||||||
return { element: ElementHandleDispatcher.from(this.parentScope(), await this._frame.frameElement()) };
|
return { element: ElementHandleDispatcher.from(this, await this._frame.frameElement()) };
|
||||||
}
|
}
|
||||||
|
|
||||||
async evaluateExpression(params: channels.FrameEvaluateExpressionParams, metadata: CallMetadata): Promise<channels.FrameEvaluateExpressionResult> {
|
async evaluateExpression(params: channels.FrameEvaluateExpressionParams, metadata: CallMetadata): Promise<channels.FrameEvaluateExpressionResult> {
|
||||||
|
|
@ -80,11 +84,11 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameChannel, Pa
|
||||||
}
|
}
|
||||||
|
|
||||||
async evaluateExpressionHandle(params: channels.FrameEvaluateExpressionHandleParams, metadata: CallMetadata): Promise<channels.FrameEvaluateExpressionHandleResult> {
|
async evaluateExpressionHandle(params: channels.FrameEvaluateExpressionHandleParams, metadata: CallMetadata): Promise<channels.FrameEvaluateExpressionHandleResult> {
|
||||||
return { handle: ElementHandleDispatcher.fromJSHandle(this.parentScope(), await this._frame.evaluateExpressionHandle(params.expression, { isFunction: params.isFunction }, parseArgument(params.arg))) };
|
return { handle: ElementHandleDispatcher.fromJSHandle(this, await this._frame.evaluateExpressionHandle(params.expression, { isFunction: params.isFunction }, parseArgument(params.arg))) };
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitForSelector(params: channels.FrameWaitForSelectorParams, metadata: CallMetadata): Promise<channels.FrameWaitForSelectorResult> {
|
async waitForSelector(params: channels.FrameWaitForSelectorParams, metadata: CallMetadata): Promise<channels.FrameWaitForSelectorResult> {
|
||||||
return { element: ElementHandleDispatcher.fromNullable(this.parentScope(), await this._frame.waitForSelector(metadata, params.selector, params)) };
|
return { element: ElementHandleDispatcher.fromNullable(this, await this._frame.waitForSelector(metadata, params.selector, params)) };
|
||||||
}
|
}
|
||||||
|
|
||||||
async dispatchEvent(params: channels.FrameDispatchEventParams, metadata: CallMetadata): Promise<void> {
|
async dispatchEvent(params: channels.FrameDispatchEventParams, metadata: CallMetadata): Promise<void> {
|
||||||
|
|
@ -100,12 +104,12 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameChannel, Pa
|
||||||
}
|
}
|
||||||
|
|
||||||
async querySelector(params: channels.FrameQuerySelectorParams, metadata: CallMetadata): Promise<channels.FrameQuerySelectorResult> {
|
async querySelector(params: channels.FrameQuerySelectorParams, metadata: CallMetadata): Promise<channels.FrameQuerySelectorResult> {
|
||||||
return { element: ElementHandleDispatcher.fromNullable(this.parentScope(), await this._frame.querySelector(params.selector, params)) };
|
return { element: ElementHandleDispatcher.fromNullable(this, await this._frame.querySelector(params.selector, params)) };
|
||||||
}
|
}
|
||||||
|
|
||||||
async querySelectorAll(params: channels.FrameQuerySelectorAllParams, metadata: CallMetadata): Promise<channels.FrameQuerySelectorAllResult> {
|
async querySelectorAll(params: channels.FrameQuerySelectorAllParams, metadata: CallMetadata): Promise<channels.FrameQuerySelectorAllResult> {
|
||||||
const elements = await this._frame.querySelectorAll(params.selector);
|
const elements = await this._frame.querySelectorAll(params.selector);
|
||||||
return { elements: elements.map(e => ElementHandleDispatcher.from(this.parentScope(), e)) };
|
return { elements: elements.map(e => ElementHandleDispatcher.from(this, e)) };
|
||||||
}
|
}
|
||||||
|
|
||||||
async queryCount(params: channels.FrameQueryCountParams): Promise<channels.FrameQueryCountResult> {
|
async queryCount(params: channels.FrameQueryCountParams): Promise<channels.FrameQueryCountResult> {
|
||||||
|
|
@ -121,11 +125,11 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameChannel, Pa
|
||||||
}
|
}
|
||||||
|
|
||||||
async addScriptTag(params: channels.FrameAddScriptTagParams, metadata: CallMetadata): Promise<channels.FrameAddScriptTagResult> {
|
async addScriptTag(params: channels.FrameAddScriptTagParams, metadata: CallMetadata): Promise<channels.FrameAddScriptTagResult> {
|
||||||
return { element: ElementHandleDispatcher.from(this.parentScope(), await this._frame.addScriptTag(params)) };
|
return { element: ElementHandleDispatcher.from(this, await this._frame.addScriptTag(params)) };
|
||||||
}
|
}
|
||||||
|
|
||||||
async addStyleTag(params: channels.FrameAddStyleTagParams, metadata: CallMetadata): Promise<channels.FrameAddStyleTagResult> {
|
async addStyleTag(params: channels.FrameAddStyleTagParams, metadata: CallMetadata): Promise<channels.FrameAddStyleTagResult> {
|
||||||
return { element: ElementHandleDispatcher.from(this.parentScope(), await this._frame.addStyleTag(params)) };
|
return { element: ElementHandleDispatcher.from(this, await this._frame.addStyleTag(params)) };
|
||||||
}
|
}
|
||||||
|
|
||||||
async click(params: channels.FrameClickParams, metadata: CallMetadata): Promise<void> {
|
async click(params: channels.FrameClickParams, metadata: CallMetadata): Promise<void> {
|
||||||
|
|
@ -249,7 +253,7 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameChannel, Pa
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitForFunction(params: channels.FrameWaitForFunctionParams, metadata: CallMetadata): Promise<channels.FrameWaitForFunctionResult> {
|
async waitForFunction(params: channels.FrameWaitForFunctionParams, metadata: CallMetadata): Promise<channels.FrameWaitForFunctionResult> {
|
||||||
return { handle: ElementHandleDispatcher.fromJSHandle(this.parentScope(), await this._frame._waitForFunctionExpression(metadata, params.expression, params.isFunction, parseArgument(params.arg), params)) };
|
return { handle: ElementHandleDispatcher.fromJSHandle(this, await this._frame._waitForFunctionExpression(metadata, params.expression, params.isFunction, parseArgument(params.arg), params)) };
|
||||||
}
|
}
|
||||||
|
|
||||||
async title(params: channels.FrameTitleParams, metadata: CallMetadata): Promise<channels.FrameTitleResult> {
|
async title(params: channels.FrameTitleParams, metadata: CallMetadata): Promise<channels.FrameTitleResult> {
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,9 @@ import { ElementHandleDispatcher } from './elementHandlerDispatcher';
|
||||||
import { parseSerializedValue, serializeValue } from '../../protocol/serializers';
|
import { parseSerializedValue, serializeValue } from '../../protocol/serializers';
|
||||||
import type { PageDispatcher, WorkerDispatcher } from './pageDispatcher';
|
import type { PageDispatcher, WorkerDispatcher } from './pageDispatcher';
|
||||||
import type { ElectronApplicationDispatcher } from './electronDispatcher';
|
import type { ElectronApplicationDispatcher } from './electronDispatcher';
|
||||||
|
import type { FrameDispatcher } from './frameDispatcher';
|
||||||
|
|
||||||
export type JSHandleDispatcherParentScope = PageDispatcher | WorkerDispatcher | ElectronApplicationDispatcher;
|
export type JSHandleDispatcherParentScope = PageDispatcher | FrameDispatcher | WorkerDispatcher | ElectronApplicationDispatcher;
|
||||||
|
|
||||||
export class JSHandleDispatcher extends Dispatcher<js.JSHandle, channels.JSHandleChannel, JSHandleDispatcherParentScope> implements channels.JSHandleChannel {
|
export class JSHandleDispatcher extends Dispatcher<js.JSHandle, channels.JSHandleChannel, JSHandleDispatcherParentScope> implements channels.JSHandleChannel {
|
||||||
_type_JSHandle = true;
|
_type_JSHandle = true;
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,9 @@ import type { PageDispatcher } from './pageDispatcher';
|
||||||
import { FrameDispatcher } from './frameDispatcher';
|
import { FrameDispatcher } from './frameDispatcher';
|
||||||
import { WorkerDispatcher } from './pageDispatcher';
|
import { WorkerDispatcher } from './pageDispatcher';
|
||||||
|
|
||||||
export class RequestDispatcher extends Dispatcher<Request, channels.RequestChannel, BrowserContextDispatcher> implements channels.RequestChannel {
|
export class RequestDispatcher extends Dispatcher<Request, channels.RequestChannel, BrowserContextDispatcher | PageDispatcher | FrameDispatcher> implements channels.RequestChannel {
|
||||||
_type_Request: boolean;
|
_type_Request: boolean;
|
||||||
|
private _browserContextDispatcher: BrowserContextDispatcher;
|
||||||
|
|
||||||
static from(scope: BrowserContextDispatcher, request: Request): RequestDispatcher {
|
static from(scope: BrowserContextDispatcher, request: Request): RequestDispatcher {
|
||||||
const result = existingDispatcher<RequestDispatcher>(request);
|
const result = existingDispatcher<RequestDispatcher>(request);
|
||||||
|
|
@ -41,8 +42,13 @@ export class RequestDispatcher extends Dispatcher<Request, channels.RequestChann
|
||||||
|
|
||||||
private constructor(scope: BrowserContextDispatcher, request: Request) {
|
private constructor(scope: BrowserContextDispatcher, request: Request) {
|
||||||
const postData = request.postDataBuffer();
|
const postData = request.postDataBuffer();
|
||||||
super(scope, request, 'Request', {
|
// Always try to attach request to the page, if not, frame.
|
||||||
frame: FrameDispatcher.fromNullable(scope as any as PageDispatcher, request.frame()),
|
const frame = request.frame();
|
||||||
|
const page = request.frame()?._page;
|
||||||
|
const pageDispatcher = page ? existingDispatcher<PageDispatcher>(page) : null;
|
||||||
|
const frameDispatcher = frame ? FrameDispatcher.from(scope, frame) : null;
|
||||||
|
super(pageDispatcher || frameDispatcher || scope, request, 'Request', {
|
||||||
|
frame: FrameDispatcher.fromNullable(scope, request.frame()),
|
||||||
serviceWorker: WorkerDispatcher.fromNullable(scope, request.serviceWorker()),
|
serviceWorker: WorkerDispatcher.fromNullable(scope, request.serviceWorker()),
|
||||||
url: request.url(),
|
url: request.url(),
|
||||||
resourceType: request.resourceType(),
|
resourceType: request.resourceType(),
|
||||||
|
|
@ -53,6 +59,7 @@ export class RequestDispatcher extends Dispatcher<Request, channels.RequestChann
|
||||||
redirectedFrom: RequestDispatcher.fromNullable(scope, request.redirectedFrom()),
|
redirectedFrom: RequestDispatcher.fromNullable(scope, request.redirectedFrom()),
|
||||||
});
|
});
|
||||||
this._type_Request = true;
|
this._type_Request = true;
|
||||||
|
this._browserContextDispatcher = scope;
|
||||||
}
|
}
|
||||||
|
|
||||||
async rawRequestHeaders(params?: channels.RequestRawRequestHeadersParams): Promise<channels.RequestRawRequestHeadersResult> {
|
async rawRequestHeaders(params?: channels.RequestRawRequestHeadersParams): Promise<channels.RequestRawRequestHeadersResult> {
|
||||||
|
|
@ -60,26 +67,27 @@ export class RequestDispatcher extends Dispatcher<Request, channels.RequestChann
|
||||||
}
|
}
|
||||||
|
|
||||||
async response(): Promise<channels.RequestResponseResult> {
|
async response(): Promise<channels.RequestResponseResult> {
|
||||||
return { response: ResponseDispatcher.fromNullable(this.parentScope(), await this._object.response()) };
|
return { response: ResponseDispatcher.fromNullable(this._browserContextDispatcher, await this._object.response()) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ResponseDispatcher extends Dispatcher<Response, channels.ResponseChannel, BrowserContextDispatcher> implements channels.ResponseChannel {
|
export class ResponseDispatcher extends Dispatcher<Response, channels.ResponseChannel, RequestDispatcher> implements channels.ResponseChannel {
|
||||||
_type_Response = true;
|
_type_Response = true;
|
||||||
|
|
||||||
static from(scope: BrowserContextDispatcher, response: Response): ResponseDispatcher {
|
static from(scope: BrowserContextDispatcher, response: Response): ResponseDispatcher {
|
||||||
const result = existingDispatcher<ResponseDispatcher>(response);
|
const result = existingDispatcher<ResponseDispatcher>(response);
|
||||||
return result || new ResponseDispatcher(scope, response);
|
const requestDispatcher = RequestDispatcher.from(scope, response.request());
|
||||||
|
return result || new ResponseDispatcher(requestDispatcher, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromNullable(scope: BrowserContextDispatcher, response: Response | null): ResponseDispatcher | undefined {
|
static fromNullable(scope: BrowserContextDispatcher, response: Response | null): ResponseDispatcher | undefined {
|
||||||
return response ? ResponseDispatcher.from(scope, response) : undefined;
|
return response ? ResponseDispatcher.from(scope, response) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private constructor(scope: BrowserContextDispatcher, response: Response) {
|
private constructor(scope: RequestDispatcher, response: Response) {
|
||||||
super(scope, response, 'Response', {
|
super(scope, response, 'Response', {
|
||||||
// TODO: responses in popups can point to non-reported requests.
|
// TODO: responses in popups can point to non-reported requests.
|
||||||
request: RequestDispatcher.from(scope, response.request()),
|
request: scope,
|
||||||
url: response.url(),
|
url: response.url(),
|
||||||
status: response.status(),
|
status: response.status(),
|
||||||
statusText: response.statusText(),
|
statusText: response.statusText(),
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
|
||||||
// If we split pageCreated and pageReady, there should be no main frame during pageCreated.
|
// If we split pageCreated and pageReady, there should be no main frame during pageCreated.
|
||||||
|
|
||||||
// We will reparent it to the page below using adopt.
|
// We will reparent it to the page below using adopt.
|
||||||
const mainFrame = FrameDispatcher.from(parentScope as any as PageDispatcher, page.mainFrame());
|
const mainFrame = FrameDispatcher.from(parentScope, page.mainFrame());
|
||||||
|
|
||||||
super(parentScope, page, 'Page', {
|
super(parentScope, page, 'Page', {
|
||||||
mainFrame,
|
mainFrame,
|
||||||
|
|
@ -80,7 +80,7 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
|
||||||
this._dispatchEvent('download', { url: download.url, suggestedFilename: download.suggestedFilename(), artifact: ArtifactDispatcher.from(parentScope, download.artifact) });
|
this._dispatchEvent('download', { url: download.url, suggestedFilename: download.suggestedFilename(), artifact: ArtifactDispatcher.from(parentScope, download.artifact) });
|
||||||
});
|
});
|
||||||
this.addObjectListener(Page.Events.FileChooser, (fileChooser: FileChooser) => this._dispatchEvent('fileChooser', {
|
this.addObjectListener(Page.Events.FileChooser, (fileChooser: FileChooser) => this._dispatchEvent('fileChooser', {
|
||||||
element: ElementHandleDispatcher.from(this, fileChooser.element()),
|
element: ElementHandleDispatcher.from(mainFrame, fileChooser.element()),
|
||||||
isMultiple: fileChooser.isMultiple()
|
isMultiple: fileChooser.isMultiple()
|
||||||
}));
|
}));
|
||||||
this.addObjectListener(Page.Events.FrameAttached, frame => this._onFrameAttached(frame));
|
this.addObjectListener(Page.Events.FrameAttached, frame => this._onFrameAttached(frame));
|
||||||
|
|
@ -297,11 +297,11 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
|
||||||
}
|
}
|
||||||
|
|
||||||
_onFrameAttached(frame: Frame) {
|
_onFrameAttached(frame: Frame) {
|
||||||
this._dispatchEvent('frameAttached', { frame: FrameDispatcher.from(this, frame) });
|
this._dispatchEvent('frameAttached', { frame: FrameDispatcher.from(this.parentScope(), frame) });
|
||||||
}
|
}
|
||||||
|
|
||||||
_onFrameDetached(frame: Frame) {
|
_onFrameDetached(frame: Frame) {
|
||||||
this._dispatchEvent('frameDetached', { frame: FrameDispatcher.from(this, frame) });
|
this._dispatchEvent('frameDetached', { frame: FrameDispatcher.from(this.parentScope(), frame) });
|
||||||
}
|
}
|
||||||
|
|
||||||
override _onDispose() {
|
override _onDispose() {
|
||||||
|
|
@ -346,7 +346,7 @@ export class BindingCallDispatcher extends Dispatcher<{ guid: string }, channels
|
||||||
|
|
||||||
constructor(scope: PageDispatcher, name: string, needsHandle: boolean, source: { context: BrowserContext, page: Page, frame: Frame }, args: any[]) {
|
constructor(scope: PageDispatcher, name: string, needsHandle: boolean, source: { context: BrowserContext, page: Page, frame: Frame }, args: any[]) {
|
||||||
super(scope, { guid: 'bindingCall@' + createGuid() }, 'BindingCall', {
|
super(scope, { guid: 'bindingCall@' + createGuid() }, 'BindingCall', {
|
||||||
frame: FrameDispatcher.from(scope, source.frame),
|
frame: FrameDispatcher.from(scope.parentScope(), source.frame),
|
||||||
name,
|
name,
|
||||||
args: needsHandle ? undefined : args.map(serializeResult),
|
args: needsHandle ? undefined : args.map(serializeResult),
|
||||||
handle: needsHandle ? ElementHandleDispatcher.fromJSHandle(scope, args[0] as JSHandle) : undefined,
|
handle: needsHandle ? ElementHandleDispatcher.fromJSHandle(scope, args[0] as JSHandle) : undefined,
|
||||||
|
|
|
||||||
|
|
@ -325,7 +325,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
||||||
const testInfoImpl = testInfo as TestInfoImpl;
|
const testInfoImpl = testInfo as TestInfoImpl;
|
||||||
const videoMode = normalizeVideoMode(video);
|
const videoMode = normalizeVideoMode(video);
|
||||||
const captureVideo = shouldCaptureVideo(videoMode, testInfo) && !_reuseContext;
|
const captureVideo = shouldCaptureVideo(videoMode, testInfo) && !_reuseContext;
|
||||||
const contexts = new Map<BrowserContext, { pages: Page[] }>();
|
const contexts = new Map<BrowserContext, { pagesWithVideo: Page[] }>();
|
||||||
|
|
||||||
await use(async options => {
|
await use(async options => {
|
||||||
const hook = hookType(testInfoImpl);
|
const hook = hookType(testInfoImpl);
|
||||||
|
|
@ -343,9 +343,10 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
||||||
}
|
}
|
||||||
} : {};
|
} : {};
|
||||||
const context = await browser.newContext({ ...videoOptions, ...options });
|
const context = await browser.newContext({ ...videoOptions, ...options });
|
||||||
const contextData: { pages: Page[] } = { pages: [] };
|
const contextData: { pagesWithVideo: Page[] } = { pagesWithVideo: [] };
|
||||||
contexts.set(context, contextData);
|
contexts.set(context, contextData);
|
||||||
context.on('page', page => contextData.pages.push(page));
|
if (captureVideo)
|
||||||
|
context.on('page', page => contextData.pagesWithVideo.push(page));
|
||||||
return context;
|
return context;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -361,8 +362,8 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
||||||
const testFailed = testInfo.status !== testInfo.expectedStatus;
|
const testFailed = testInfo.status !== testInfo.expectedStatus;
|
||||||
const preserveVideo = captureVideo && (videoMode === 'on' || (testFailed && videoMode === 'retain-on-failure') || (videoMode === 'on-first-retry' && testInfo.retry === 1));
|
const preserveVideo = captureVideo && (videoMode === 'on' || (testFailed && videoMode === 'retain-on-failure') || (videoMode === 'on-first-retry' && testInfo.retry === 1));
|
||||||
if (preserveVideo) {
|
if (preserveVideo) {
|
||||||
const { pages } = contexts.get(context)!;
|
const { pagesWithVideo: pagesForVideo } = contexts.get(context)!;
|
||||||
const videos = pages.map(p => p.video()).filter(Boolean) as Video[];
|
const videos = pagesForVideo.map(p => p.video()).filter(Boolean) as Video[];
|
||||||
await Promise.all(videos.map(async v => {
|
await Promise.all(videos.map(async v => {
|
||||||
try {
|
try {
|
||||||
const savedPath = testInfo.outputPath(`video${counter ? '-' + counter : ''}.webm`);
|
const savedPath = testInfo.outputPath(`video${counter ? '-' + counter : ''}.webm`);
|
||||||
|
|
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
<title>Woof-Woof</title>
|
<title>Woof-Woof</title>
|
||||||
|
|
|
||||||
|
|
@ -69,10 +69,11 @@ it('should scope context handles', async ({ browserType, server, expectScopeStat
|
||||||
{ _guid: 'browser-type', objects: [
|
{ _guid: 'browser-type', objects: [
|
||||||
{ _guid: 'browser', objects: [
|
{ _guid: 'browser', objects: [
|
||||||
{ _guid: 'browser-context', objects: [
|
{ _guid: 'browser-context', objects: [
|
||||||
{ _guid: 'request', objects: [] },
|
|
||||||
{ _guid: 'response', objects: [] },
|
|
||||||
{ _guid: 'page', objects: [
|
{ _guid: 'page', objects: [
|
||||||
{ _guid: 'frame', objects: [] },
|
{ _guid: 'frame', objects: [] },
|
||||||
|
{ _guid: 'request', objects: [
|
||||||
|
{ _guid: 'response', objects: [] },
|
||||||
|
] },
|
||||||
] },
|
] },
|
||||||
{ _guid: 'request-context', objects: [] },
|
{ _guid: 'request-context', objects: [] },
|
||||||
{ _guid: 'tracing', objects: [] }
|
{ _guid: 'tracing', objects: [] }
|
||||||
|
|
@ -204,12 +205,13 @@ it('should not generate dispatchers for subresources w/o listeners', async ({ pa
|
||||||
{ _guid: 'browser-context', objects: [
|
{ _guid: 'browser-context', objects: [
|
||||||
{
|
{
|
||||||
_guid: 'page', objects: [
|
_guid: 'page', objects: [
|
||||||
{ _guid: 'frame', objects: [] }
|
{ _guid: 'frame', objects: [] },
|
||||||
|
{ _guid: 'request', objects: [
|
||||||
|
{ _guid: 'response', objects: [] },
|
||||||
|
] },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{ _guid: 'request', objects: [] },
|
|
||||||
{ _guid: 'request-context', objects: [] },
|
{ _guid: 'request-context', objects: [] },
|
||||||
{ _guid: 'response', objects: [] },
|
|
||||||
{ _guid: 'tracing', objects: [] }
|
{ _guid: 'tracing', objects: [] }
|
||||||
] },
|
] },
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -47,3 +47,37 @@ test('should not leak server-side objects', async ({ page }) => {
|
||||||
expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/browserContext').BrowserContext)).toBe(4);
|
expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/browserContext').BrowserContext)).toBe(4);
|
||||||
expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/browser').Browser)).toBe(4);
|
expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/browser').Browser)).toBe(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should not leak dispatchers after closing page', async ({ context, server }) => {
|
||||||
|
const pages = [];
|
||||||
|
const COUNT = 5;
|
||||||
|
for (let i = 0; i < COUNT; ++i) {
|
||||||
|
const page = await context.newPage();
|
||||||
|
// ensure listeners are registered
|
||||||
|
page.on('console', () => {});
|
||||||
|
await page.goto(server.PREFIX + '/title.html');
|
||||||
|
await page.evaluate(async i => {
|
||||||
|
console.log('message', i);
|
||||||
|
}, i);
|
||||||
|
pages.push(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/page').Page)).toBe(COUNT);
|
||||||
|
expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/dispatchers/networkDispatchers').RequestDispatcher)).toBe(COUNT);
|
||||||
|
expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/dispatchers/networkDispatchers').ResponseDispatcher)).toBe(COUNT);
|
||||||
|
expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/dispatchers/consoleMessageDispatcher').ConsoleMessageDispatcher)).toBe(COUNT);
|
||||||
|
|
||||||
|
for (const page of pages)
|
||||||
|
await page.close();
|
||||||
|
pages.length = 0;
|
||||||
|
|
||||||
|
expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/page').Page)).toBe(0);
|
||||||
|
expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/dispatchers/networkDispatchers').RequestDispatcher)).toBe(0);
|
||||||
|
expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/dispatchers/networkDispatchers').ResponseDispatcher)).toBe(0);
|
||||||
|
expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/dispatchers/consoleMessageDispatcher').ConsoleMessageDispatcher)).toBe(0);
|
||||||
|
|
||||||
|
expect(await queryObjectCount(require('../../packages/playwright-core/lib/client/page').Page)).toBeLessThan(COUNT);
|
||||||
|
expect(await queryObjectCount(require('../../packages/playwright-core/lib/server/page').Page)).toBe(0);
|
||||||
|
expect(await queryObjectCount(require('../../packages/playwright-core/lib/client/network').Request)).toBe(0);
|
||||||
|
expect(await queryObjectCount(require('../../packages/playwright-core/lib/client/network').Response)).toBe(0);
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue