chore(console): expose window.playwright by default (#36921)

This commit is contained in:
Simon Knott 2025-08-07 08:52:08 +02:00 committed by GitHub
parent cc09a085ef
commit 60e9b0edc6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 95 additions and 61 deletions

View File

@ -2758,7 +2758,7 @@ Returns the opener for popup pages and `null` for others. If the opener has been
## async method: Page.pause
* since: v1.9
Pauses script execution. Playwright will stop executing the script and wait for the user to either press 'Resume'
Pauses script execution. Playwright will stop executing the script and wait for the user to either press the 'Resume'
button in the page overlay or to call `playwright.resume()` in the DevTools console.
User can inspect selectors or perform manual steps while paused. Resume will continue running the original script from

View File

@ -71,7 +71,7 @@ declare global {
interface Window {
playwright?: any;
inspect: (element: Element | undefined) => void;
__pw_resume: () => Promise<void>;
__pw_resume?: () => Promise<void>;
}
}
@ -139,6 +139,8 @@ export class ConsoleAPI {
}
private _resume() {
if (!this._injectedScript.window.__pw_resume)
return false;
this._injectedScript.window.__pw_resume().catch(() => {});
}
}

View File

@ -3599,8 +3599,8 @@ export interface Page {
opener(): Promise<null|Page>;
/**
* Pauses script execution. Playwright will stop executing the script and wait for the user to either press 'Resume'
* button in the page overlay or to call `playwright.resume()` in the DevTools console.
* Pauses script execution. Playwright will stop executing the script and wait for the user to either press the
* 'Resume' button in the page overlay or to call `playwright.resume()` in the DevTools console.
*
* User can inspect selectors or perform manual steps while paused. Resume will continue running the original script
* from the place it was paused.

View File

@ -296,7 +296,7 @@ export abstract class BrowserType extends SdkObject {
private _validateLaunchOptions(options: types.LaunchOptions): types.LaunchOptions {
const { devtools = false } = options;
let { headless = !devtools, downloadsPath, proxy } = options;
if (debugMode())
if (debugMode() === 'inspector')
headless = false;
if (downloadsPath && !path.isAbsolute(downloadsPath))
downloadsPath = path.join(process.cwd(), downloadsPath);

View File

@ -57,7 +57,7 @@ export class Chromium extends BrowserType {
constructor(parent: SdkObject) {
super(parent, 'chromium');
if (debugMode())
if (debugMode() === 'inspector')
this._devtools = this._createDevTools();
}

View File

@ -1628,11 +1628,11 @@ export class Frame extends SdkObject {
this._firedNetworkIdleSelf = false;
}
async extendInjectedScript(source: string, arg?: any): Promise<js.JSHandle> {
async extendInjectedScript(source: string, arg?: any) {
const context = await this._context('main');
const injectedScriptHandle = await context.injectedScript();
return injectedScriptHandle.evaluateHandle((injectedScript, { source, arg }) => {
return injectedScript.extend(source, arg);
await injectedScriptHandle.evaluate((injectedScript, { source, arg }) => {
injectedScript.extend(source, arg);
}, { source, arg });
}

View File

@ -23,7 +23,7 @@ export function debugMode() {
return 'console';
if (_debugMode === '0' || _debugMode === 'false')
return '';
return _debugMode ? 'inspector' : '';
return _debugMode ? 'inspector' : 'console';
}
const _isUnderTest = getAsBooleanFromENV('PWTEST_UNDER_TEST');

View File

@ -91,7 +91,7 @@ export const nodePlatform: Platform = {
inspectCustom: util.inspect.custom,
isDebugMode: () => !!debugMode(),
isDebugMode: () => debugMode() === 'inspector',
isJSDebuggerAttached: () => !!require('inspector').url(),

View File

@ -3599,8 +3599,8 @@ export interface Page {
opener(): Promise<null|Page>;
/**
* Pauses script execution. Playwright will stop executing the script and wait for the user to either press 'Resume'
* button in the page overlay or to call `playwright.resume()` in the DevTools console.
* Pauses script execution. Playwright will stop executing the script and wait for the user to either press the
* 'Resume' button in the page overlay or to call `playwright.resume()` in the DevTools console.
*
* User can inspect selectors or perform manual steps while paused. Resume will continue running the original script
* from the place it was paused.

View File

@ -233,7 +233,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
if (testIdAttribute)
playwrightLibrary.selectors.setTestIdAttribute(testIdAttribute);
testInfo.snapshotSuffix = process.platform;
if (debugMode())
if (debugMode() === 'inspector')
(testInfo as TestInfoImpl)._setDebugMode();
playwright._defaultContextOptions = _combinedContextOptions;

View File

@ -209,8 +209,6 @@ it('coverage should work', async ({ server, launchPersistent, browserName }) =>
});
it('should respect selectors', async ({ playwright, launchPersistent }) => {
const { page } = await launchPersistent();
const defaultContextCSS = () => ({
query(root, selector) {
return root.querySelector(selector);
@ -221,6 +219,7 @@ it('should respect selectors', async ({ playwright, launchPersistent }) => {
});
await playwright.selectors.register('defaultContextCSS', defaultContextCSS);
const { page } = await launchPersistent();
await page.setContent(`<div>hello</div>`);
expect(await page.innerHTML('css=div')).toBe('hello');
expect(await page.innerHTML('defaultContextCSS=div')).toBe('hello');

View File

@ -87,11 +87,7 @@ it.describe('pause', () => {
// @ts-ignore
await page.pause({ __testHookKeepTestTimeout: true });
})();
await Promise.all([
page.waitForFunction(() => (window as any).playwright && (window as any).playwright.resume).then(() => {
return page.evaluate('window.playwright.resume()');
})
]);
await page.waitForFunction(() => (window as any).playwright?.resume() !== false);
await scriptPromise;
});

View File

@ -76,15 +76,14 @@ it('should work when registered on global', async ({ browser, mode }) => {
});
it('should work with path', async ({ playwright, browser, asset }) => {
const page = await browser.newPage();
await playwright.selectors.register('foo', { path: asset('sectionselectorengine.js') });
const page = await browser.newPage();
await page.setContent('<section></section>');
expect(await page.$eval('foo=whatever', e => e.nodeName)).toBe('SECTION');
await page.close();
});
it('should work in main and isolated world', async ({ playwright, browser }) => {
const page = await browser.newPage();
const createDummySelector = () => ({
query(root, selector) {
return window['__answer'];
@ -95,6 +94,7 @@ it('should work in main and isolated world', async ({ playwright, browser }) =>
});
await playwright.selectors.register('main', createDummySelector);
await playwright.selectors.register('isolated', createDummySelector, { contentScript: true });
const page = await browser.newPage();
await page.setContent('<div><span><section></section></span></div>');
await page.evaluate(() => window['__answer'] = document.querySelector('span'));
// Works in main if asked.
@ -151,7 +151,6 @@ it('should throw "already registered" error when registering', { annotation: { t
});
it('should not rely on engines working from the root', async ({ playwright, browser }) => {
const page = await browser.newPage();
const createValueEngine = () => ({
query(root, selector) {
return root && root.value.includes(selector) ? root : undefined;
@ -160,15 +159,14 @@ it('should not rely on engines working from the root', async ({ playwright, brow
return root && root.value.includes(selector) ? [root] : [];
},
});
await playwright.selectors.register('__value', createValueEngine);
const page = await browser.newPage();
await page.setContent(`<input id=input1 value=value1><input id=input2 value=value2>`);
expect(await page.$eval('input >> __value=value2', e => e.id)).toBe('input2');
await page.close();
});
it('should throw a nice error if the selector returns a bad value', async ({ playwright, browser }) => {
const page = await browser.newPage();
const createFakeEngine = () => ({
query(root, selector) {
return [document.body];
@ -179,6 +177,7 @@ it('should throw a nice error if the selector returns a bad value', async ({ pla
});
await playwright.selectors.register('__fake', createFakeEngine);
const page = await browser.newPage();
const error = await page.$('__fake=value2').catch(e => e);
expect(error.message).toContain('Expected a Node but got [object Array]');
await page.close();

View File

@ -98,20 +98,3 @@ it('init script should run only once in iframe', async ({ page, server, browserN
'init script: ' + (browserName === 'firefox' ? 'no url yet' : '/frames/frame.html'),
]);
});
it('init script should not observe playwright internals', async ({ server, page, trace, isAndroid }) => {
it.skip(!!process.env.PW_CLOCK, 'clock installs globalThis.__pwClock');
it.skip(trace === 'on', 'tracing installs __playwright_snapshot_streamer');
it.fixme(isAndroid, 'There is probably context reuse between this test and some other test that installs a binding');
await page.addInitScript(() => {
window['check'] = () => {
const keys = Reflect.ownKeys(globalThis).map(k => k.toString());
return keys.find(name => name.includes('playwright') || name.includes('_pw')) || 'none';
};
window['found'] = window['check']();
});
await page.goto(server.EMPTY_PAGE);
expect(await page.evaluate(() => window['found'])).toBe('none');
expect(await page.evaluate(() => window['check']())).toBe('none');
});

View File

@ -119,7 +119,7 @@ it('should be atomic', async ({ playwright, page }) => {
}
});
await playwright.selectors.register('dispatchEvent', createDummySelector);
await page.setContent(`<div onclick="window._clicked=true">Hello</div>`);
await page.goto(`data:text/html,<div onclick="window._clicked=true">Hello</div>`);
await page.dispatchEvent('dispatchEvent=div', 'click');
expect(await page.evaluate(() => window['_clicked'])).toBe(true);
});

View File

@ -35,7 +35,7 @@ it('textContent should be atomic', async ({ playwright, page }) => {
}
});
await playwright.selectors.register('textContent', createDummySelector);
await page.setContent(`<div>Hello</div>`);
await page.goto(`data:text/html,<div>Hello</div>`);
const tc = await page.textContent('textContent=div');
expect(tc).toBe('Hello');
expect(await page.evaluate(() => document.querySelector('div').textContent)).toBe('modified');
@ -57,7 +57,7 @@ it('innerText should be atomic', async ({ playwright, page }) => {
}
});
await playwright.selectors.register('innerText', createDummySelector);
await page.setContent(`<div>Hello</div>`);
await page.goto(`data:text/html,<div>Hello</div>`);
const tc = await page.innerText('innerText=div');
expect(tc).toBe('Hello');
expect(await page.evaluate(() => document.querySelector('div').innerText)).toBe('modified');
@ -79,7 +79,7 @@ it('innerHTML should be atomic', async ({ playwright, page }) => {
}
});
await playwright.selectors.register('innerHTML', createDummySelector);
await page.setContent(`<div>Hello<span>world</span></div>`);
await page.goto(`data:text/html,<div>Hello<span>world</span></div>`);
const tc = await page.innerHTML('innerHTML=div');
expect(tc).toBe('Hello<span>world</span>');
expect(await page.evaluate(() => document.querySelector('div').innerHTML)).toBe('modified');
@ -101,7 +101,7 @@ it('getAttribute should be atomic', async ({ playwright, page }) => {
}
});
await playwright.selectors.register('getAttribute', createDummySelector);
await page.setContent(`<div foo=hello></div>`);
await page.goto(`data:text/html,<div foo=hello></div>`);
const tc = await page.getAttribute('getAttribute=div', 'foo');
expect(tc).toBe('hello');
expect(await page.evaluate(() => document.querySelector('div').getAttribute('foo'))).toBe('modified');
@ -123,7 +123,7 @@ it('isVisible should be atomic', async ({ playwright, page }) => {
}
});
await playwright.selectors.register('isVisible', createDummySelector);
await page.setContent(`<div>Hello</div>`);
await page.goto(`data:text/html,<div>Hello</div>`);
const result = await page.isVisible('isVisible=div');
expect(result).toBe(true);
expect(await page.evaluate(() => document.querySelector('div').style.display)).toBe('none');
@ -139,6 +139,6 @@ it('should take java-style string', async ({ playwright, page }) => {
}
}`;
await playwright.selectors.register('objectLiteral', createDummySelector);
await page.setContent(`<div>Hello</div>`);
await page.goto(`data:text/html,<div>Hello</div>`);
await page.textContent('objectLiteral=div');
});

View File

@ -896,7 +896,7 @@ test('page.pause() should disable test timeout', async ({ runInlineTest }) => {
expect(result.output).toContain('success!');
});
test('PWDEBUG=console should expose window.playwright', async ({ runInlineTest }) => {
test('window.playwright should be exposed by default', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/36772' } }, async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.ts': `
import { test, expect } from '@playwright/test';
@ -907,7 +907,60 @@ test('PWDEBUG=console should expose window.playwright', async ({ runInlineTest }
expect(bodyTag).toBe('BODY');
});
`,
}, {}, { PWDEBUG: 'console' });
}, {}, {});
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
});
test('window.playwright should not override existing property', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/36772' } }, async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
await page.setContent('<script>window.playwright = "foo"</script>');
expect(await page.evaluate(() => window.playwright)).toBe('foo');
});
`,
}, {}, {});
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
});
test('PWDEBUG=0 should opt-out from exposing window.playwright', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/36772' } }, async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
await page.setContent('<body></body>');
expect(await page.evaluate(() => window.playwright)).toBeUndefined();
});
`,
}, {}, { PWDEBUG: '0' });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
});
test('init script should not observe playwright internals', async ({ server, runInlineTest }) => {
test.skip(!!process.env.PW_CLOCK, 'clock installs globalThis.__pwClock');
const result = await runInlineTest({
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
await page.addInitScript(() => {
window['check'] = () => {
const keys = Reflect.ownKeys(globalThis).map(k => k.toString());
return keys.find(name => name.includes('playwright') || name.includes('_pw')) || 'none';
};
window['found'] = window['check']();
});
await page.goto("${server.EMPTY_PAGE}");
expect(await page.evaluate(() => window['found'])).toBe('none');
expect(await page.evaluate(() => window['check']())).toBe('none');
});
`,
}, {}, { PWDEBUG: '0' });
expect(result.exitCode).toBe(0);
});

View File

@ -1462,20 +1462,20 @@ pw:api | Close context
test('reading network request / response should not be listed as step', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/33558' }
}, async ({ runInlineTest, server }) => {
}, async ({ runInlineTest, server, page }) => {
const result = await runInlineTest({
'reporter.ts': stepIndentReporter,
'playwright.config.ts': `module.exports = { reporter: './reporter' };`,
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('waitForResponse step nesting', async ({ page }) => {
page.on('request', async request => {
await request.allHeaders();
});
page.on('response', async response => {
await response.text();
});
await page.goto('${server.EMPTY_PAGE}');
const [request, response] = await Promise.all([
page.waitForRequest('${server.EMPTY_PAGE}'),
page.waitForResponse('${server.EMPTY_PAGE}'),
page.goto('${server.EMPTY_PAGE}'),
]);
await request.allHeaders();
await response.text();
});
`
}, { reporter: '', workers: 1, timeout: 3000 });
@ -1489,7 +1489,9 @@ fixture | context
pw:api | Create context
fixture | page
pw:api | Create page
pw:api |Navigate to "/empty.html" @ a.test.ts:10
pw:api |Wait for event "request" @ a.test.ts:5
pw:api |Wait for event "response" @ a.test.ts:6
pw:api |Navigate to "/empty.html" @ a.test.ts:7
hook |After Hooks
fixture | page
fixture | context