Родитель
41e3e6d13f
Коммит
3b9e62432d
|
@ -47,7 +47,7 @@ export class FrameExecutionContext extends js.ExecutionContext {
|
|||
readonly 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.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;
|
||||
}
|
||||
|
@ -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)
|
||||
// and we won't be able to cleanup.
|
||||
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)
|
||||
await this._page.keyboard._ensureModifiers(restoreModifiers);
|
||||
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) {
|
||||
// When noWaitAfter is passed, we do not want to accidentally stall on
|
||||
// non-committed navigation blocking the evaluate.
|
||||
|
|
|
@ -70,7 +70,7 @@ export class ElectronApplication extends SdkObject {
|
|||
this._nodeElectronHandlePromise = new Promise(f => {
|
||||
this._nodeSession.on('Runtime.executionContextCreated', async (event: any) => {
|
||||
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')`));
|
||||
}
|
||||
});
|
||||
|
|
|
@ -64,9 +64,11 @@ export class ExecutionContext extends SdkObject {
|
|||
private _delegate: ExecutionContextDelegate;
|
||||
private _utilityScriptPromise: Promise<JSHandle> | undefined;
|
||||
private _contextDestroyedRace = new ScopedRace();
|
||||
readonly worldNameForTest: string;
|
||||
|
||||
constructor(parent: SdkObject, delegate: ExecutionContextDelegate) {
|
||||
constructor(parent: SdkObject, delegate: ExecutionContextDelegate, worldNameForTest: string) {
|
||||
super(parent, 'execution-context');
|
||||
this.worldNameForTest = worldNameForTest;
|
||||
this._delegate = delegate;
|
||||
}
|
||||
|
||||
|
@ -122,7 +124,7 @@ export class ExecutionContext extends SdkObject {
|
|||
${utilityScriptSource.source}
|
||||
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;
|
||||
}
|
||||
|
@ -153,6 +155,8 @@ export class JSHandle<T = any> extends SdkObject {
|
|||
this._value = value;
|
||||
this._objectType = type;
|
||||
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) {
|
||||
|
@ -211,8 +215,11 @@ export class JSHandle<T = any> extends SdkObject {
|
|||
if (this._disposed)
|
||||
return;
|
||||
this._disposed = true;
|
||||
if (this._objectId)
|
||||
if (this._objectId) {
|
||||
this._context.releaseHandle(this._objectId).catch(e => {});
|
||||
if ((globalThis as any).leakedJSHandles)
|
||||
(globalThis as any).leakedJSHandles.delete(this);
|
||||
}
|
||||
}
|
||||
|
||||
override toString(): string {
|
||||
|
@ -227,13 +234,16 @@ export class JSHandle<T = any> extends SdkObject {
|
|||
return this._preview;
|
||||
}
|
||||
|
||||
worldNameForTest(): string {
|
||||
return this._context.worldNameForTest;
|
||||
}
|
||||
|
||||
_setPreview(preview: string) {
|
||||
this._preview = preview;
|
||||
if (this._previewCallback)
|
||||
this._previewCallback(preview);
|
||||
}
|
||||
|
||||
|
||||
async objectCount(): Promise<number> {
|
||||
if (!this._objectId)
|
||||
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) {
|
||||
this._existingExecutionContext = new js.ExecutionContext(this, delegate);
|
||||
this._existingExecutionContext = new js.ExecutionContext(this, delegate, 'worker');
|
||||
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);
|
||||
}
|
||||
});
|
Загрузка…
Ссылка в новой задаче