mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
fix(recorder): move recorder scripts into the main world (#8761)
This commit is contained in:
parent
6e97ac300c
commit
5a305a9c2e
3
.gitignore
vendored
3
.gitignore
vendored
@ -17,4 +17,5 @@ drivers/
|
||||
nohup.out
|
||||
.trace
|
||||
.tmp
|
||||
allure*
|
||||
allure*
|
||||
playwright-report
|
||||
|
||||
@ -104,7 +104,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
|
||||
const binding = new BindingCallDispatcher(this._scope, params.name, !!params.needsHandle, source, args);
|
||||
this._dispatchEvent('bindingCall', { binding });
|
||||
return binding.promise();
|
||||
}, 'main');
|
||||
});
|
||||
}
|
||||
|
||||
async fetch(params: channels.BrowserContextFetchParams): Promise<channels.BrowserContextFetchResult> {
|
||||
|
||||
@ -110,7 +110,7 @@ export abstract class BrowserContext extends SdkObject {
|
||||
});
|
||||
|
||||
if (debugMode() === 'console')
|
||||
await this.extendInjectedScript('main', consoleApiSource.source);
|
||||
await this.extendInjectedScript(consoleApiSource.source);
|
||||
}
|
||||
|
||||
async _ensureVideosPath() {
|
||||
@ -168,16 +168,15 @@ export abstract class BrowserContext extends SdkObject {
|
||||
return this._doSetHTTPCredentials(httpCredentials);
|
||||
}
|
||||
|
||||
async exposeBinding(name: string, needsHandle: boolean, playwrightBinding: frames.FunctionWithSource, world: types.World): Promise<void> {
|
||||
const identifier = PageBinding.identifier(name, world);
|
||||
if (this._pageBindings.has(identifier))
|
||||
async exposeBinding(name: string, needsHandle: boolean, playwrightBinding: frames.FunctionWithSource): Promise<void> {
|
||||
if (this._pageBindings.has(name))
|
||||
throw new Error(`Function "${name}" has been already registered`);
|
||||
for (const page of this.pages()) {
|
||||
if (page.getBinding(name, world))
|
||||
if (page.getBinding(name))
|
||||
throw new Error(`Function "${name}" has been already registered in one of the pages`);
|
||||
}
|
||||
const binding = new PageBinding(name, playwrightBinding, needsHandle, world);
|
||||
this._pageBindings.set(identifier, binding);
|
||||
const binding = new PageBinding(name, playwrightBinding, needsHandle);
|
||||
this._pageBindings.set(name, binding);
|
||||
await this._doExposeBinding(binding);
|
||||
}
|
||||
|
||||
@ -373,8 +372,8 @@ export abstract class BrowserContext extends SdkObject {
|
||||
}
|
||||
}
|
||||
|
||||
async extendInjectedScript(world: types.World, source: string, arg?: any) {
|
||||
const installInFrame = (frame: frames.Frame) => frame.extendInjectedScript(world, source, arg).catch(() => {});
|
||||
async extendInjectedScript(source: string, arg?: any) {
|
||||
const installInFrame = (frame: frames.Frame) => frame.extendInjectedScript(source, arg).catch(() => {});
|
||||
const installInPage = (page: Page) => {
|
||||
page.on(Page.Events.InternalFrameNavigatedToNewDocument, installInFrame);
|
||||
return Promise.all(page.frames().map(installInFrame));
|
||||
|
||||
@ -172,7 +172,7 @@ export class CRPage implements PageDelegate {
|
||||
|
||||
async exposeBinding(binding: PageBinding) {
|
||||
await this._forAllFrameSessions(frame => frame._initBinding(binding));
|
||||
await Promise.all(this._page.frames().map(frame => frame.evaluateExpression(binding.source, false, {}, binding.world).catch(e => {})));
|
||||
await Promise.all(this._page.frames().map(frame => frame.evaluateExpression(binding.source, false, {}).catch(e => {})));
|
||||
}
|
||||
|
||||
async updateExtraHTTPHeaders(): Promise<void> {
|
||||
@ -474,7 +474,7 @@ class FrameSession {
|
||||
worldName: UTILITY_WORLD_NAME,
|
||||
});
|
||||
for (const binding of this._crPage._browserContext._pageBindings.values())
|
||||
frame.evaluateExpression(binding.source, false, undefined, binding.world).catch(e => {});
|
||||
frame.evaluateExpression(binding.source, false, undefined).catch(e => {});
|
||||
for (const source of this._crPage._browserContext._evaluateOnNewDocumentSources)
|
||||
frame.evaluateExpression(source, false, undefined, 'main').catch(e => {});
|
||||
}
|
||||
@ -758,10 +758,9 @@ class FrameSession {
|
||||
}
|
||||
|
||||
async _initBinding(binding: PageBinding) {
|
||||
const worldName = binding.world === 'utility' ? UTILITY_WORLD_NAME : undefined;
|
||||
await Promise.all([
|
||||
this._client.send('Runtime.addBinding', { name: binding.name, executionContextName: worldName }),
|
||||
this._client.send('Page.addScriptToEvaluateOnNewDocument', { source: binding.source, worldName })
|
||||
this._client.send('Runtime.addBinding', { name: binding.name }),
|
||||
this._client.send('Page.addScriptToEvaluateOnNewDocument', { source: binding.source })
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -24,7 +24,7 @@ import { Page, PageBinding, PageDelegate } from '../page';
|
||||
import { ConnectionTransport } from '../transport';
|
||||
import * as types from '../types';
|
||||
import { ConnectionEvents, FFConnection } from './ffConnection';
|
||||
import { FFPage, UTILITY_WORLD_NAME } from './ffPage';
|
||||
import { FFPage } from './ffPage';
|
||||
import { Protocol } from './protocol';
|
||||
|
||||
export class FFBrowser extends Browser {
|
||||
@ -326,8 +326,7 @@ export class FFBrowserContext extends BrowserContext {
|
||||
}
|
||||
|
||||
async _doExposeBinding(binding: PageBinding) {
|
||||
const worldName = binding.world === 'utility' ? UTILITY_WORLD_NAME : '';
|
||||
await this._browser._connection.send('Browser.addBinding', { browserContextId: this._browserContextId, worldName, name: binding.name, script: binding.source });
|
||||
await this._browser._connection.send('Browser.addBinding', { browserContextId: this._browserContextId, name: binding.name, script: binding.source });
|
||||
}
|
||||
|
||||
async _doUpdateRequestInterception(): Promise<void> {
|
||||
|
||||
@ -317,8 +317,7 @@ export class FFPage implements PageDelegate {
|
||||
}
|
||||
|
||||
async exposeBinding(binding: PageBinding) {
|
||||
const worldName = binding.world === 'utility' ? UTILITY_WORLD_NAME : '';
|
||||
await this._session.send('Page.addBinding', { name: binding.name, script: binding.source, worldName });
|
||||
await this._session.send('Page.addBinding', { name: binding.name, script: binding.source });
|
||||
}
|
||||
|
||||
didClose() {
|
||||
|
||||
@ -1299,8 +1299,8 @@ export class Frame extends SdkObject {
|
||||
this._networkIdleTimer = undefined;
|
||||
}
|
||||
|
||||
async extendInjectedScript(world: types.World, source: string, arg?: any): Promise<js.JSHandle> {
|
||||
const context = await this._context(world);
|
||||
async extendInjectedScript(source: string, arg?: any): Promise<js.JSHandle> {
|
||||
const context = await this._context('main');
|
||||
const injectedScriptHandle = await context.injectedScript();
|
||||
return injectedScriptHandle.evaluateHandle((injectedScript, {source, arg}) => {
|
||||
return injectedScript.extend(source, arg);
|
||||
|
||||
@ -268,14 +268,13 @@ export class Page extends SdkObject {
|
||||
this._timeoutSettings.setDefaultTimeout(timeout);
|
||||
}
|
||||
|
||||
async exposeBinding(name: string, needsHandle: boolean, playwrightBinding: frames.FunctionWithSource, world: types.World = 'main') {
|
||||
const identifier = PageBinding.identifier(name, world);
|
||||
if (this._pageBindings.has(identifier))
|
||||
async exposeBinding(name: string, needsHandle: boolean, playwrightBinding: frames.FunctionWithSource) {
|
||||
if (this._pageBindings.has(name))
|
||||
throw new Error(`Function "${name}" has been already registered`);
|
||||
if (this._browserContext._pageBindings.has(identifier))
|
||||
if (this._browserContext._pageBindings.has(name))
|
||||
throw new Error(`Function "${name}" has been already registered in the browser context`);
|
||||
const binding = new PageBinding(name, playwrightBinding, needsHandle, world);
|
||||
this._pageBindings.set(identifier, binding);
|
||||
const binding = new PageBinding(name, playwrightBinding, needsHandle);
|
||||
this._pageBindings.set(name, binding);
|
||||
await this._delegate.exposeBinding(binding);
|
||||
}
|
||||
|
||||
@ -490,9 +489,8 @@ export class Page extends SdkObject {
|
||||
return [...this._browserContext._pageBindings.values(), ...this._pageBindings.values()];
|
||||
}
|
||||
|
||||
getBinding(name: string, world: types.World) {
|
||||
const identifier = PageBinding.identifier(name, world);
|
||||
return this._pageBindings.get(identifier) || this._browserContext._pageBindings.get(identifier);
|
||||
getBinding(name: string) {
|
||||
return this._pageBindings.get(name) || this._browserContext._pageBindings.get(name);
|
||||
}
|
||||
|
||||
setScreencastOptions(options: { width: number, height: number, quality: number } | null) {
|
||||
@ -549,25 +547,19 @@ export class PageBinding {
|
||||
readonly playwrightFunction: frames.FunctionWithSource;
|
||||
readonly source: string;
|
||||
readonly needsHandle: boolean;
|
||||
readonly world: types.World;
|
||||
|
||||
constructor(name: string, playwrightFunction: frames.FunctionWithSource, needsHandle: boolean, world: types.World) {
|
||||
constructor(name: string, playwrightFunction: frames.FunctionWithSource, needsHandle: boolean) {
|
||||
this.name = name;
|
||||
this.playwrightFunction = playwrightFunction;
|
||||
this.source = `(${addPageBinding.toString()})(${JSON.stringify(name)}, ${needsHandle})`;
|
||||
this.needsHandle = needsHandle;
|
||||
this.world = world;
|
||||
}
|
||||
|
||||
static identifier(name: string, world: types.World) {
|
||||
return world + ':' + name;
|
||||
}
|
||||
|
||||
static async dispatch(page: Page, payload: string, context: dom.FrameExecutionContext) {
|
||||
const {name, seq, args} = JSON.parse(payload);
|
||||
try {
|
||||
assert(context.world);
|
||||
const binding = page.getBinding(name, context.world)!;
|
||||
const binding = page.getBinding(name)!;
|
||||
let result: any;
|
||||
if (binding.needsHandle) {
|
||||
const handle = await context.evaluateHandle(takeHandle, { name, seq }).catch(e => null);
|
||||
|
||||
@ -29,8 +29,6 @@ declare module globalThis {
|
||||
let _playwrightRefreshOverlay: () => void;
|
||||
}
|
||||
|
||||
const scriptSymbol = Symbol('scriptSymbol');
|
||||
|
||||
export class Recorder {
|
||||
private _injectedScript: InjectedScript;
|
||||
private _performingAction = false;
|
||||
@ -132,9 +130,8 @@ export class Recorder {
|
||||
}
|
||||
|
||||
private _refreshListenersIfNeeded() {
|
||||
if ((document.documentElement as any)[scriptSymbol])
|
||||
if (this._outerGlassPaneElement.parentElement)
|
||||
return;
|
||||
(document.documentElement as any)[scriptSymbol] = true;
|
||||
removeEventListeners(this._listeners);
|
||||
this._listeners = [
|
||||
addEventListener(document, 'click', event => this._onClick(event as MouseEvent), true),
|
||||
|
||||
@ -184,11 +184,11 @@ export class RecorderSupplement implements InstrumentationListener {
|
||||
// Input actions that potentially lead to navigation are intercepted on the page and are
|
||||
// performed by the Playwright.
|
||||
await this._context.exposeBinding('_playwrightRecorderPerformAction', false,
|
||||
(source: BindingSource, action: actions.Action) => this._performAction(source.frame, action), 'utility');
|
||||
(source: BindingSource, action: actions.Action) => this._performAction(source.frame, action));
|
||||
|
||||
// Other non-essential actions are simply being recorded.
|
||||
await this._context.exposeBinding('_playwrightRecorderRecordAction', false,
|
||||
(source: BindingSource, action: actions.Action) => this._recordAction(source.frame, action), 'utility');
|
||||
(source: BindingSource, action: actions.Action) => this._recordAction(source.frame, action));
|
||||
|
||||
await this._context.exposeBinding('_playwrightRecorderState', false, source => {
|
||||
let actionSelector = this._highlightedSelector;
|
||||
@ -205,20 +205,20 @@ export class RecorderSupplement implements InstrumentationListener {
|
||||
actionSelector,
|
||||
};
|
||||
return uiState;
|
||||
}, 'utility');
|
||||
});
|
||||
|
||||
await this._context.exposeBinding('_playwrightRecorderSetSelector', false, async (_, selector: string) => {
|
||||
this._setMode('none');
|
||||
await this._recorderApp?.setSelector(selector, true);
|
||||
await this._recorderApp?.bringToFront();
|
||||
}, 'utility');
|
||||
});
|
||||
|
||||
await this._context.exposeBinding('_playwrightResume', false, () => {
|
||||
this._debugger.resume(false);
|
||||
}, 'main');
|
||||
});
|
||||
|
||||
await this._context.extendInjectedScript('utility', recorderSource.source, { isUnderTest: isUnderTest() });
|
||||
await this._context.extendInjectedScript('main', consoleApiSource.source);
|
||||
await this._context.extendInjectedScript(recorderSource.source, { isUnderTest: isUnderTest() });
|
||||
await this._context.extendInjectedScript(consoleApiSource.source);
|
||||
|
||||
if (this._debugger.isPaused())
|
||||
this._pausedStateChanged();
|
||||
|
||||
@ -169,7 +169,7 @@ Please run 'npx playwright install' to install Playwright browsers
|
||||
await controller.run(async progress => {
|
||||
await context._browser._defaultContext!._loadDefaultContextAsIs(progress);
|
||||
});
|
||||
await context.extendInjectedScript('main', consoleApiSource.source);
|
||||
await context.extendInjectedScript(consoleApiSource.source);
|
||||
const [page] = context.pages();
|
||||
|
||||
if (traceViewerBrowser === 'chromium')
|
||||
|
||||
@ -308,7 +308,7 @@ export class WKBrowserContext extends BrowserContext {
|
||||
async _doAddInitScript(source: string) {
|
||||
this._evaluateOnNewDocumentSources.push(source);
|
||||
for (const page of this.pages())
|
||||
await (page._delegate as WKPage)._updateBootstrapScript('main');
|
||||
await (page._delegate as WKPage)._updateBootstrapScript();
|
||||
}
|
||||
|
||||
async _doExposeBinding(binding: PageBinding) {
|
||||
|
||||
@ -185,12 +185,10 @@ export class WKPage implements PageDelegate {
|
||||
promises.push(session.send('Page.overrideUserAgent', { value: contextOptions.userAgent }));
|
||||
if (this._page._state.mediaType || this._page._state.colorScheme || this._page._state.reducedMotion)
|
||||
promises.push(WKPage._setEmulateMedia(session, this._page._state.mediaType, this._page._state.colorScheme, this._page._state.reducedMotion));
|
||||
for (const world of ['main', 'utility'] as const) {
|
||||
const bootstrapScript = this._calculateBootstrapScript(world);
|
||||
if (bootstrapScript.length)
|
||||
promises.push(session.send('Page.setBootstrapScript', { source: bootstrapScript, worldName: webkitWorldName(world) }));
|
||||
this._page.frames().map(frame => frame.evaluateExpression(bootstrapScript, false, undefined, world).catch(e => {}));
|
||||
}
|
||||
const bootstrapScript = this._calculateBootstrapScript();
|
||||
if (bootstrapScript.length)
|
||||
promises.push(session.send('Page.setBootstrapScript', { source: bootstrapScript }));
|
||||
this._page.frames().map(frame => frame.evaluateExpression(bootstrapScript, false, undefined).catch(e => {}));
|
||||
if (contextOptions.bypassCSP)
|
||||
promises.push(session.send('Page.setBypassCSP', { enabled: true }));
|
||||
if (this._page._state.emulatedSize) {
|
||||
@ -720,38 +718,34 @@ export class WKPage implements PageDelegate {
|
||||
}
|
||||
|
||||
async exposeBinding(binding: PageBinding): Promise<void> {
|
||||
await this._updateBootstrapScript(binding.world);
|
||||
await this._updateBootstrapScript();
|
||||
await this._evaluateBindingScript(binding);
|
||||
}
|
||||
|
||||
private async _evaluateBindingScript(binding: PageBinding): Promise<void> {
|
||||
const script = this._bindingToScript(binding);
|
||||
await Promise.all(this._page.frames().map(frame => frame.evaluateExpression(script, false, {}, binding.world).catch(e => {})));
|
||||
await Promise.all(this._page.frames().map(frame => frame.evaluateExpression(script, false, {}).catch(e => {})));
|
||||
}
|
||||
|
||||
async evaluateOnNewDocument(script: string): Promise<void> {
|
||||
await this._updateBootstrapScript('main');
|
||||
await this._updateBootstrapScript();
|
||||
}
|
||||
|
||||
private _bindingToScript(binding: PageBinding): string {
|
||||
return `self.${binding.name} = (param) => console.debug('${BINDING_CALL_MESSAGE}', {}, param); ${binding.source}`;
|
||||
}
|
||||
|
||||
private _calculateBootstrapScript(world: types.World): string {
|
||||
private _calculateBootstrapScript(): string {
|
||||
const scripts: string[] = [];
|
||||
for (const binding of this._page.allBindings()) {
|
||||
if (binding.world === world)
|
||||
scripts.push(this._bindingToScript(binding));
|
||||
}
|
||||
if (world === 'main') {
|
||||
scripts.push(...this._browserContext._evaluateOnNewDocumentSources);
|
||||
scripts.push(...this._page._evaluateOnNewDocumentSources);
|
||||
}
|
||||
for (const binding of this._page.allBindings())
|
||||
scripts.push(this._bindingToScript(binding));
|
||||
scripts.push(...this._browserContext._evaluateOnNewDocumentSources);
|
||||
scripts.push(...this._page._evaluateOnNewDocumentSources);
|
||||
return scripts.join(';');
|
||||
}
|
||||
|
||||
async _updateBootstrapScript(world: types.World): Promise<void> {
|
||||
await this._updateState('Page.setBootstrapScript', { source: this._calculateBootstrapScript(world), worldName: webkitWorldName(world) });
|
||||
async _updateBootstrapScript(): Promise<void> {
|
||||
await this._updateState('Page.setBootstrapScript', { source: this._calculateBootstrapScript() });
|
||||
}
|
||||
|
||||
async closePage(runBeforeUnload: boolean): Promise<void> {
|
||||
@ -1100,13 +1094,6 @@ export class WKPage implements PageDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
function webkitWorldName(world: types.World) {
|
||||
switch (world) {
|
||||
case 'main': return undefined;
|
||||
case 'utility': return UTILITY_WORLD_NAME;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WebKit Remote Addresses look like:
|
||||
*
|
||||
|
||||
@ -16,7 +16,6 @@
|
||||
*/
|
||||
|
||||
import { test as it, expect } from './pageTest';
|
||||
import { attachFrame } from '../config/utils';
|
||||
import type { ElementHandle } from '../../index';
|
||||
|
||||
it('exposeBinding should work', async ({page}) => {
|
||||
@ -238,37 +237,6 @@ it('should not result in unhandled rejection', async ({page, isAndroid}) => {
|
||||
expect(await page.evaluate('1 + 1').catch(e => e)).toBeInstanceOf(Error);
|
||||
});
|
||||
|
||||
it('should work with internal bindings', async ({page, toImpl, server, mode, browserName, isElectron, isAndroid}) => {
|
||||
it.skip(mode !== 'default');
|
||||
it.skip(browserName !== 'chromium');
|
||||
it.skip(isAndroid);
|
||||
it.skip(isElectron);
|
||||
|
||||
const implPage: import('../../src/server/page').Page = toImpl(page);
|
||||
let foo;
|
||||
await implPage.exposeBinding('foo', false, ({}, arg) => {
|
||||
foo = arg;
|
||||
}, 'utility');
|
||||
expect(await page.evaluate('!!window.foo')).toBe(false);
|
||||
expect(await implPage.mainFrame().evaluateExpression('!!window.foo', false, {}, 'utility')).toBe(true);
|
||||
expect(foo).toBe(undefined);
|
||||
await implPage.mainFrame().evaluateExpression('window.foo(123)', false, {}, 'utility');
|
||||
expect(foo).toBe(123);
|
||||
|
||||
// should work after reload
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
expect(await page.evaluate('!!window.foo')).toBe(false);
|
||||
await implPage.mainFrame().evaluateExpression('window.foo(456)', false, {}, 'utility');
|
||||
expect(foo).toBe(456);
|
||||
|
||||
// should work inside frames
|
||||
const frame = await attachFrame(page, 'myframe', server.CROSS_PROCESS_PREFIX + '/empty.html');
|
||||
expect(await frame.evaluate('!!window.foo')).toBe(false);
|
||||
const implFrame: import('../../src/server/frames').Frame = toImpl(frame);
|
||||
await implFrame.evaluateExpression('window.foo(789)', false, {}, 'utility');
|
||||
expect(foo).toBe(789);
|
||||
});
|
||||
|
||||
it('exposeBinding(handle) should work with element handles', async ({ page}) => {
|
||||
let cb;
|
||||
const promise = new Promise(f => cb = f);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user