Родитель
41e3e6d13f
Коммит
3b9e62432d
|
@ -47,7 +47,7 @@ export class FrameExecutionContext extends js.ExecutionContext {
|
||||||
readonly world: types.World | null;
|
readonly world: types.World | null;
|
||||||
|
|
||||||
constructor(delegate: js.ExecutionContextDelegate, frame: frames.Frame, world: types.World|null) {
|
constructor(delegate: js.ExecutionContextDelegate, frame: frames.Frame, world: types.World|null) {
|
||||||
super(frame, delegate);
|
super(frame, delegate, world || 'content-script');
|
||||||
this.frame = frame;
|
this.frame = frame;
|
||||||
this.world = world;
|
this.world = world;
|
||||||
}
|
}
|
||||||
|
@ -114,7 +114,7 @@ export class FrameExecutionContext extends js.ExecutionContext {
|
||||||
);
|
);
|
||||||
})();
|
})();
|
||||||
`;
|
`;
|
||||||
this._injectedScriptPromise = this.rawEvaluateHandle(source).then(objectId => new js.JSHandle(this, 'object', undefined, objectId));
|
this._injectedScriptPromise = this.rawEvaluateHandle(source).then(objectId => new js.JSHandle(this, 'object', 'InjectedScript', objectId));
|
||||||
}
|
}
|
||||||
return this._injectedScriptPromise;
|
return this._injectedScriptPromise;
|
||||||
}
|
}
|
||||||
|
@ -455,6 +455,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
// Do not await here, just in case the renderer is stuck (e.g. on alert)
|
// Do not await here, just in case the renderer is stuck (e.g. on alert)
|
||||||
// and we won't be able to cleanup.
|
// and we won't be able to cleanup.
|
||||||
hitTargetInterceptionHandle!.evaluate(h => h.stop()).catch(e => {});
|
hitTargetInterceptionHandle!.evaluate(h => h.stop()).catch(e => {});
|
||||||
|
hitTargetInterceptionHandle!.dispose();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -470,7 +471,9 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
if (restoreModifiers)
|
if (restoreModifiers)
|
||||||
await this._page.keyboard._ensureModifiers(restoreModifiers);
|
await this._page.keyboard._ensureModifiers(restoreModifiers);
|
||||||
if (hitTargetInterceptionHandle) {
|
if (hitTargetInterceptionHandle) {
|
||||||
const stopHitTargetInterception = hitTargetInterceptionHandle.evaluate(h => h.stop()).catch(e => 'done' as const);
|
const stopHitTargetInterception = hitTargetInterceptionHandle.evaluate(h => h.stop()).catch(e => 'done' as const).finally(() => {
|
||||||
|
hitTargetInterceptionHandle?.dispose();
|
||||||
|
});
|
||||||
if (!options.noWaitAfter) {
|
if (!options.noWaitAfter) {
|
||||||
// When noWaitAfter is passed, we do not want to accidentally stall on
|
// When noWaitAfter is passed, we do not want to accidentally stall on
|
||||||
// non-committed navigation blocking the evaluate.
|
// non-committed navigation blocking the evaluate.
|
||||||
|
|
|
@ -70,7 +70,7 @@ export class ElectronApplication extends SdkObject {
|
||||||
this._nodeElectronHandlePromise = new Promise(f => {
|
this._nodeElectronHandlePromise = new Promise(f => {
|
||||||
this._nodeSession.on('Runtime.executionContextCreated', async (event: any) => {
|
this._nodeSession.on('Runtime.executionContextCreated', async (event: any) => {
|
||||||
if (event.context.auxData && event.context.auxData.isDefault) {
|
if (event.context.auxData && event.context.auxData.isDefault) {
|
||||||
this._nodeExecutionContext = new js.ExecutionContext(this, new CRExecutionContext(this._nodeSession, event.context));
|
this._nodeExecutionContext = new js.ExecutionContext(this, new CRExecutionContext(this._nodeSession, event.context), 'electron');
|
||||||
f(await js.evaluate(this._nodeExecutionContext, false /* returnByValue */, `process.mainModule.require('electron')`));
|
f(await js.evaluate(this._nodeExecutionContext, false /* returnByValue */, `process.mainModule.require('electron')`));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -64,9 +64,11 @@ export class ExecutionContext extends SdkObject {
|
||||||
private _delegate: ExecutionContextDelegate;
|
private _delegate: ExecutionContextDelegate;
|
||||||
private _utilityScriptPromise: Promise<JSHandle> | undefined;
|
private _utilityScriptPromise: Promise<JSHandle> | undefined;
|
||||||
private _contextDestroyedRace = new ScopedRace();
|
private _contextDestroyedRace = new ScopedRace();
|
||||||
|
readonly worldNameForTest: string;
|
||||||
|
|
||||||
constructor(parent: SdkObject, delegate: ExecutionContextDelegate) {
|
constructor(parent: SdkObject, delegate: ExecutionContextDelegate, worldNameForTest: string) {
|
||||||
super(parent, 'execution-context');
|
super(parent, 'execution-context');
|
||||||
|
this.worldNameForTest = worldNameForTest;
|
||||||
this._delegate = delegate;
|
this._delegate = delegate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,7 +124,7 @@ export class ExecutionContext extends SdkObject {
|
||||||
${utilityScriptSource.source}
|
${utilityScriptSource.source}
|
||||||
return new (module.exports.UtilityScript())();
|
return new (module.exports.UtilityScript())();
|
||||||
})();`;
|
})();`;
|
||||||
this._utilityScriptPromise = this._raceAgainstContextDestroyed(this._delegate.rawEvaluateHandle(source).then(objectId => new JSHandle(this, 'object', undefined, objectId)));
|
this._utilityScriptPromise = this._raceAgainstContextDestroyed(this._delegate.rawEvaluateHandle(source).then(objectId => new JSHandle(this, 'object', 'UtilityScript', objectId)));
|
||||||
}
|
}
|
||||||
return this._utilityScriptPromise;
|
return this._utilityScriptPromise;
|
||||||
}
|
}
|
||||||
|
@ -153,6 +155,8 @@ export class JSHandle<T = any> extends SdkObject {
|
||||||
this._value = value;
|
this._value = value;
|
||||||
this._objectType = type;
|
this._objectType = type;
|
||||||
this._preview = this._objectId ? preview || `JSHandle@${this._objectType}` : String(value);
|
this._preview = this._objectId ? preview || `JSHandle@${this._objectType}` : String(value);
|
||||||
|
if (this._objectId && (globalThis as any).leakedJSHandles)
|
||||||
|
(globalThis as any).leakedJSHandles.set(this, new Error('Leaked JSHandle'));
|
||||||
}
|
}
|
||||||
|
|
||||||
callFunctionNoReply(func: Function, arg: any) {
|
callFunctionNoReply(func: Function, arg: any) {
|
||||||
|
@ -211,8 +215,11 @@ export class JSHandle<T = any> extends SdkObject {
|
||||||
if (this._disposed)
|
if (this._disposed)
|
||||||
return;
|
return;
|
||||||
this._disposed = true;
|
this._disposed = true;
|
||||||
if (this._objectId)
|
if (this._objectId) {
|
||||||
this._context.releaseHandle(this._objectId).catch(e => {});
|
this._context.releaseHandle(this._objectId).catch(e => {});
|
||||||
|
if ((globalThis as any).leakedJSHandles)
|
||||||
|
(globalThis as any).leakedJSHandles.delete(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override toString(): string {
|
override toString(): string {
|
||||||
|
@ -227,13 +234,16 @@ export class JSHandle<T = any> extends SdkObject {
|
||||||
return this._preview;
|
return this._preview;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
worldNameForTest(): string {
|
||||||
|
return this._context.worldNameForTest;
|
||||||
|
}
|
||||||
|
|
||||||
_setPreview(preview: string) {
|
_setPreview(preview: string) {
|
||||||
this._preview = preview;
|
this._preview = preview;
|
||||||
if (this._previewCallback)
|
if (this._previewCallback)
|
||||||
this._previewCallback(preview);
|
this._previewCallback(preview);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async objectCount(): Promise<number> {
|
async objectCount(): Promise<number> {
|
||||||
if (!this._objectId)
|
if (!this._objectId)
|
||||||
throw new Error('Can only count objects for a handle that points to the constructor prototype');
|
throw new Error('Can only count objects for a handle that points to the constructor prototype');
|
||||||
|
|
|
@ -723,7 +723,7 @@ export class Worker extends SdkObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
_createExecutionContext(delegate: js.ExecutionContextDelegate) {
|
_createExecutionContext(delegate: js.ExecutionContextDelegate) {
|
||||||
this._existingExecutionContext = new js.ExecutionContext(this, delegate);
|
this._existingExecutionContext = new js.ExecutionContext(this, delegate, 'worker');
|
||||||
this._executionContextCallback(this._existingExecutionContext);
|
this._executionContextCallback(this._existingExecutionContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,151 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MultiMap } from '../../packages/playwright-core/lib/utils/multimap';
|
||||||
|
import { test, expect } from './pageTest';
|
||||||
|
|
||||||
|
function leakedJSHandles(): string {
|
||||||
|
const map = new MultiMap();
|
||||||
|
for (const [h, e] of (globalThis as any).leakedJSHandles) {
|
||||||
|
const name = `[${h.worldNameForTest()}] ${h.preview()}`;
|
||||||
|
if (name === '[main] UtilityScript' || name === '[utility] UtilityScript' || name === '[main] InjectedScript' || name === '[utility] InjectedScript')
|
||||||
|
continue;
|
||||||
|
map.set(e.stack, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!map.size)
|
||||||
|
return '';
|
||||||
|
|
||||||
|
const lines: string[] = [];
|
||||||
|
lines.push('=============================');
|
||||||
|
lines.push('Leaked JSHandles:');
|
||||||
|
for (const key of map.keys()) {
|
||||||
|
lines.push('=============================');
|
||||||
|
for (const value of map.get(key))
|
||||||
|
lines.push(value);
|
||||||
|
lines.push('in ' + key);
|
||||||
|
}
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function objectCounts(pageImpl, constructorName: string): Promise<{ main: number, utility: number }> {
|
||||||
|
const result = { main: 0, utility: 0 };
|
||||||
|
for (const world of ['main', 'utility']) {
|
||||||
|
const context = await pageImpl.mainFrame()._context(world);
|
||||||
|
const prototype = await context.evaluateHandle(name => (window as any)[name].prototype, constructorName);
|
||||||
|
result[world] = await prototype.objectCount();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
test.beforeEach(() => {
|
||||||
|
(globalThis as any).leakedJSHandles = new Map();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterEach(() => {
|
||||||
|
(globalThis as any).leakedJSHandles = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('click should not leak', async ({ page, browserName, toImpl }) => {
|
||||||
|
await page.setContent(`
|
||||||
|
<button>static button 1</button>
|
||||||
|
<button>static button 2</button>
|
||||||
|
<div id="buttons"></div>
|
||||||
|
`);
|
||||||
|
// Create JS wrappers for static elements.
|
||||||
|
await page.evaluate(() => document.querySelectorAll('button'));
|
||||||
|
|
||||||
|
for (let i = 0; i < 25; ++i) {
|
||||||
|
await page.evaluate(i => {
|
||||||
|
const element = document.createElement('button');
|
||||||
|
element.textContent = 'dynamic ' + i;
|
||||||
|
document.getElementById('buttons').appendChild(element);
|
||||||
|
}, i);
|
||||||
|
await page.locator('#buttons > button').click();
|
||||||
|
await page.evaluate(() => {
|
||||||
|
document.getElementById('buttons').textContent = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(leakedJSHandles()).toBeFalsy();
|
||||||
|
|
||||||
|
if (browserName === 'chromium') {
|
||||||
|
const counts = await objectCounts(toImpl(page), 'HTMLButtonElement');
|
||||||
|
expect(counts.main + counts.utility).toBeGreaterThanOrEqual(2);
|
||||||
|
expect(counts.main + counts.utility).toBeLessThan(25);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fill should not leak', async ({ page, mode, browserName, toImpl }) => {
|
||||||
|
test.skip(mode !== 'default');
|
||||||
|
|
||||||
|
await page.setContent(`
|
||||||
|
<input value="static input 1"</input>
|
||||||
|
<input value="static input 2"</input>
|
||||||
|
<div id="inputs"></div>
|
||||||
|
`);
|
||||||
|
// Create JS wrappers for static elements.
|
||||||
|
await page.evaluate(() => document.querySelectorAll('input'));
|
||||||
|
|
||||||
|
for (let i = 0; i < 25; ++i) {
|
||||||
|
await page.evaluate(i => {
|
||||||
|
const element = document.createElement('input');
|
||||||
|
document.getElementById('inputs').appendChild(element);
|
||||||
|
}, i);
|
||||||
|
await page.locator('#inputs > input').fill('input ' + i);
|
||||||
|
await page.evaluate(() => {
|
||||||
|
document.getElementById('inputs').textContent = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(leakedJSHandles()).toBeFalsy();
|
||||||
|
|
||||||
|
if (browserName === 'chromium') {
|
||||||
|
const counts = await objectCounts(toImpl(page), 'HTMLInputElement');
|
||||||
|
expect(counts.main + counts.utility).toBeGreaterThanOrEqual(2);
|
||||||
|
expect(counts.main + counts.utility).toBeLessThan(25);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('expect should not leak', async ({ page, mode, browserName, toImpl }) => {
|
||||||
|
test.skip(mode !== 'default');
|
||||||
|
|
||||||
|
await page.setContent(`
|
||||||
|
<button>static button 1</button>
|
||||||
|
<button>static button 2</button>
|
||||||
|
<div id="buttons"></div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
for (let i = 0; i < 25; ++i) {
|
||||||
|
await page.evaluate(i => {
|
||||||
|
const element = document.createElement('button');
|
||||||
|
element.textContent = 'dynamic ' + i;
|
||||||
|
document.getElementById('buttons').appendChild(element);
|
||||||
|
}, i);
|
||||||
|
await expect(page.locator('#buttons > button')).toBeVisible();
|
||||||
|
await page.evaluate(() => {
|
||||||
|
document.getElementById('buttons').textContent = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(leakedJSHandles()).toBeFalsy();
|
||||||
|
|
||||||
|
if (browserName === 'chromium') {
|
||||||
|
const counts = await objectCounts(toImpl(page), 'HTMLButtonElement');
|
||||||
|
expect(counts.main + counts.utility).toBeGreaterThanOrEqual(2);
|
||||||
|
expect(counts.main + counts.utility).toBeLessThan(25);
|
||||||
|
}
|
||||||
|
});
|
Загрузка…
Ссылка в новой задаче