mirror of
				https://github.com/microsoft/playwright.git
				synced 2025-06-26 21:40:17 +00:00 
			
		
		
		
	
							parent
							
								
									41e3e6d13f
								
							
						
					
					
						commit
						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);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										151
									
								
								tests/page/page-leaks.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								tests/page/page-leaks.spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -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);
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user