/** * 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 === '[electron] UtilityScript' || name === '[main] InjectedScript' || name === '[utility] InjectedScript' || name === '[electron] ElectronModule') 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(`
`); // 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(`
`); // 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(`
`); 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); } }); test('waitFor should not leak', async ({ page, mode, browserName, toImpl }) => { test.skip(mode !== 'default'); await page.setContent(`
`); 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').waitFor(); 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); } });