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:
Pavel Feldman 2023-07-12 14:51:13 -07:00 committed by GitHub
parent ea1ec112d8
commit 53bf1995db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 148 additions and 42 deletions

35
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

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

View file

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

View file

@ -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> {

View file

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

View file

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

View file

@ -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,

View file

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

View file

@ -1 +1,2 @@
<!DOCTYPE html>
<title>Woof-Woof</title> <title>Woof-Woof</title>

View file

@ -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: [] }
] }, ] },
] ]

View file

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