fix(recorder): move recorder scripts into the main world (#8761)

This commit is contained in:
Pavel Feldman 2021-09-08 14:27:05 -07:00 committed by GitHub
parent 6e97ac300c
commit 5a305a9c2e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 53 additions and 112 deletions

1
.gitignore vendored
View file

@ -18,3 +18,4 @@ nohup.out
.trace .trace
.tmp .tmp
allure* allure*
playwright-report

View file

@ -104,7 +104,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
const binding = new BindingCallDispatcher(this._scope, params.name, !!params.needsHandle, source, args); const binding = new BindingCallDispatcher(this._scope, params.name, !!params.needsHandle, source, args);
this._dispatchEvent('bindingCall', { binding }); this._dispatchEvent('bindingCall', { binding });
return binding.promise(); return binding.promise();
}, 'main'); });
} }
async fetch(params: channels.BrowserContextFetchParams): Promise<channels.BrowserContextFetchResult> { async fetch(params: channels.BrowserContextFetchParams): Promise<channels.BrowserContextFetchResult> {

View file

@ -110,7 +110,7 @@ export abstract class BrowserContext extends SdkObject {
}); });
if (debugMode() === 'console') if (debugMode() === 'console')
await this.extendInjectedScript('main', consoleApiSource.source); await this.extendInjectedScript(consoleApiSource.source);
} }
async _ensureVideosPath() { async _ensureVideosPath() {
@ -168,16 +168,15 @@ export abstract class BrowserContext extends SdkObject {
return this._doSetHTTPCredentials(httpCredentials); return this._doSetHTTPCredentials(httpCredentials);
} }
async exposeBinding(name: string, needsHandle: boolean, playwrightBinding: frames.FunctionWithSource, world: types.World): Promise<void> { async exposeBinding(name: string, needsHandle: boolean, playwrightBinding: frames.FunctionWithSource): Promise<void> {
const identifier = PageBinding.identifier(name, world); if (this._pageBindings.has(name))
if (this._pageBindings.has(identifier))
throw new Error(`Function "${name}" has been already registered`); throw new Error(`Function "${name}" has been already registered`);
for (const page of this.pages()) { 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`); throw new Error(`Function "${name}" has been already registered in one of the pages`);
} }
const binding = new PageBinding(name, playwrightBinding, needsHandle, world); const binding = new PageBinding(name, playwrightBinding, needsHandle);
this._pageBindings.set(identifier, binding); this._pageBindings.set(name, binding);
await this._doExposeBinding(binding); await this._doExposeBinding(binding);
} }
@ -373,8 +372,8 @@ export abstract class BrowserContext extends SdkObject {
} }
} }
async extendInjectedScript(world: types.World, source: string, arg?: any) { async extendInjectedScript(source: string, arg?: any) {
const installInFrame = (frame: frames.Frame) => frame.extendInjectedScript(world, source, arg).catch(() => {}); const installInFrame = (frame: frames.Frame) => frame.extendInjectedScript(source, arg).catch(() => {});
const installInPage = (page: Page) => { const installInPage = (page: Page) => {
page.on(Page.Events.InternalFrameNavigatedToNewDocument, installInFrame); page.on(Page.Events.InternalFrameNavigatedToNewDocument, installInFrame);
return Promise.all(page.frames().map(installInFrame)); return Promise.all(page.frames().map(installInFrame));

View file

@ -172,7 +172,7 @@ export class CRPage implements PageDelegate {
async exposeBinding(binding: PageBinding) { async exposeBinding(binding: PageBinding) {
await this._forAllFrameSessions(frame => frame._initBinding(binding)); 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> { async updateExtraHTTPHeaders(): Promise<void> {
@ -474,7 +474,7 @@ class FrameSession {
worldName: UTILITY_WORLD_NAME, worldName: UTILITY_WORLD_NAME,
}); });
for (const binding of this._crPage._browserContext._pageBindings.values()) 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) for (const source of this._crPage._browserContext._evaluateOnNewDocumentSources)
frame.evaluateExpression(source, false, undefined, 'main').catch(e => {}); frame.evaluateExpression(source, false, undefined, 'main').catch(e => {});
} }
@ -758,10 +758,9 @@ class FrameSession {
} }
async _initBinding(binding: PageBinding) { async _initBinding(binding: PageBinding) {
const worldName = binding.world === 'utility' ? UTILITY_WORLD_NAME : undefined;
await Promise.all([ await Promise.all([
this._client.send('Runtime.addBinding', { name: binding.name, executionContextName: worldName }), this._client.send('Runtime.addBinding', { name: binding.name }),
this._client.send('Page.addScriptToEvaluateOnNewDocument', { source: binding.source, worldName }) this._client.send('Page.addScriptToEvaluateOnNewDocument', { source: binding.source })
]); ]);
} }

View file

@ -24,7 +24,7 @@ import { Page, PageBinding, PageDelegate } from '../page';
import { ConnectionTransport } from '../transport'; import { ConnectionTransport } from '../transport';
import * as types from '../types'; import * as types from '../types';
import { ConnectionEvents, FFConnection } from './ffConnection'; import { ConnectionEvents, FFConnection } from './ffConnection';
import { FFPage, UTILITY_WORLD_NAME } from './ffPage'; import { FFPage } from './ffPage';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
export class FFBrowser extends Browser { export class FFBrowser extends Browser {
@ -326,8 +326,7 @@ export class FFBrowserContext extends BrowserContext {
} }
async _doExposeBinding(binding: PageBinding) { async _doExposeBinding(binding: PageBinding) {
const worldName = binding.world === 'utility' ? UTILITY_WORLD_NAME : ''; await this._browser._connection.send('Browser.addBinding', { browserContextId: this._browserContextId, name: binding.name, script: binding.source });
await this._browser._connection.send('Browser.addBinding', { browserContextId: this._browserContextId, worldName, name: binding.name, script: binding.source });
} }
async _doUpdateRequestInterception(): Promise<void> { async _doUpdateRequestInterception(): Promise<void> {

View file

@ -317,8 +317,7 @@ export class FFPage implements PageDelegate {
} }
async exposeBinding(binding: PageBinding) { async exposeBinding(binding: PageBinding) {
const worldName = binding.world === 'utility' ? UTILITY_WORLD_NAME : ''; await this._session.send('Page.addBinding', { name: binding.name, script: binding.source });
await this._session.send('Page.addBinding', { name: binding.name, script: binding.source, worldName });
} }
didClose() { didClose() {

View file

@ -1299,8 +1299,8 @@ export class Frame extends SdkObject {
this._networkIdleTimer = undefined; this._networkIdleTimer = undefined;
} }
async extendInjectedScript(world: types.World, source: string, arg?: any): Promise<js.JSHandle> { async extendInjectedScript(source: string, arg?: any): Promise<js.JSHandle> {
const context = await this._context(world); const context = await this._context('main');
const injectedScriptHandle = await context.injectedScript(); const injectedScriptHandle = await context.injectedScript();
return injectedScriptHandle.evaluateHandle((injectedScript, {source, arg}) => { return injectedScriptHandle.evaluateHandle((injectedScript, {source, arg}) => {
return injectedScript.extend(source, arg); return injectedScript.extend(source, arg);

View file

@ -268,14 +268,13 @@ export class Page extends SdkObject {
this._timeoutSettings.setDefaultTimeout(timeout); this._timeoutSettings.setDefaultTimeout(timeout);
} }
async exposeBinding(name: string, needsHandle: boolean, playwrightBinding: frames.FunctionWithSource, world: types.World = 'main') { async exposeBinding(name: string, needsHandle: boolean, playwrightBinding: frames.FunctionWithSource) {
const identifier = PageBinding.identifier(name, world); if (this._pageBindings.has(name))
if (this._pageBindings.has(identifier))
throw new Error(`Function "${name}" has been already registered`); 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`); throw new Error(`Function "${name}" has been already registered in the browser context`);
const binding = new PageBinding(name, playwrightBinding, needsHandle, world); const binding = new PageBinding(name, playwrightBinding, needsHandle);
this._pageBindings.set(identifier, binding); this._pageBindings.set(name, binding);
await this._delegate.exposeBinding(binding); await this._delegate.exposeBinding(binding);
} }
@ -490,9 +489,8 @@ export class Page extends SdkObject {
return [...this._browserContext._pageBindings.values(), ...this._pageBindings.values()]; return [...this._browserContext._pageBindings.values(), ...this._pageBindings.values()];
} }
getBinding(name: string, world: types.World) { getBinding(name: string) {
const identifier = PageBinding.identifier(name, world); return this._pageBindings.get(name) || this._browserContext._pageBindings.get(name);
return this._pageBindings.get(identifier) || this._browserContext._pageBindings.get(identifier);
} }
setScreencastOptions(options: { width: number, height: number, quality: number } | null) { setScreencastOptions(options: { width: number, height: number, quality: number } | null) {
@ -549,25 +547,19 @@ export class PageBinding {
readonly playwrightFunction: frames.FunctionWithSource; readonly playwrightFunction: frames.FunctionWithSource;
readonly source: string; readonly source: string;
readonly needsHandle: boolean; 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.name = name;
this.playwrightFunction = playwrightFunction; this.playwrightFunction = playwrightFunction;
this.source = `(${addPageBinding.toString()})(${JSON.stringify(name)}, ${needsHandle})`; this.source = `(${addPageBinding.toString()})(${JSON.stringify(name)}, ${needsHandle})`;
this.needsHandle = 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) { static async dispatch(page: Page, payload: string, context: dom.FrameExecutionContext) {
const {name, seq, args} = JSON.parse(payload); const {name, seq, args} = JSON.parse(payload);
try { try {
assert(context.world); assert(context.world);
const binding = page.getBinding(name, context.world)!; const binding = page.getBinding(name)!;
let result: any; let result: any;
if (binding.needsHandle) { if (binding.needsHandle) {
const handle = await context.evaluateHandle(takeHandle, { name, seq }).catch(e => null); const handle = await context.evaluateHandle(takeHandle, { name, seq }).catch(e => null);

View file

@ -29,8 +29,6 @@ declare module globalThis {
let _playwrightRefreshOverlay: () => void; let _playwrightRefreshOverlay: () => void;
} }
const scriptSymbol = Symbol('scriptSymbol');
export class Recorder { export class Recorder {
private _injectedScript: InjectedScript; private _injectedScript: InjectedScript;
private _performingAction = false; private _performingAction = false;
@ -132,9 +130,8 @@ export class Recorder {
} }
private _refreshListenersIfNeeded() { private _refreshListenersIfNeeded() {
if ((document.documentElement as any)[scriptSymbol]) if (this._outerGlassPaneElement.parentElement)
return; return;
(document.documentElement as any)[scriptSymbol] = true;
removeEventListeners(this._listeners); removeEventListeners(this._listeners);
this._listeners = [ this._listeners = [
addEventListener(document, 'click', event => this._onClick(event as MouseEvent), true), addEventListener(document, 'click', event => this._onClick(event as MouseEvent), true),

View file

@ -184,11 +184,11 @@ export class RecorderSupplement implements InstrumentationListener {
// Input actions that potentially lead to navigation are intercepted on the page and are // Input actions that potentially lead to navigation are intercepted on the page and are
// performed by the Playwright. // performed by the Playwright.
await this._context.exposeBinding('_playwrightRecorderPerformAction', false, 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. // Other non-essential actions are simply being recorded.
await this._context.exposeBinding('_playwrightRecorderRecordAction', false, 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 => { await this._context.exposeBinding('_playwrightRecorderState', false, source => {
let actionSelector = this._highlightedSelector; let actionSelector = this._highlightedSelector;
@ -205,20 +205,20 @@ export class RecorderSupplement implements InstrumentationListener {
actionSelector, actionSelector,
}; };
return uiState; return uiState;
}, 'utility'); });
await this._context.exposeBinding('_playwrightRecorderSetSelector', false, async (_, selector: string) => { await this._context.exposeBinding('_playwrightRecorderSetSelector', false, async (_, selector: string) => {
this._setMode('none'); this._setMode('none');
await this._recorderApp?.setSelector(selector, true); await this._recorderApp?.setSelector(selector, true);
await this._recorderApp?.bringToFront(); await this._recorderApp?.bringToFront();
}, 'utility'); });
await this._context.exposeBinding('_playwrightResume', false, () => { await this._context.exposeBinding('_playwrightResume', false, () => {
this._debugger.resume(false); this._debugger.resume(false);
}, 'main'); });
await this._context.extendInjectedScript('utility', recorderSource.source, { isUnderTest: isUnderTest() }); await this._context.extendInjectedScript(recorderSource.source, { isUnderTest: isUnderTest() });
await this._context.extendInjectedScript('main', consoleApiSource.source); await this._context.extendInjectedScript(consoleApiSource.source);
if (this._debugger.isPaused()) if (this._debugger.isPaused())
this._pausedStateChanged(); this._pausedStateChanged();

View file

@ -169,7 +169,7 @@ Please run 'npx playwright install' to install Playwright browsers
await controller.run(async progress => { await controller.run(async progress => {
await context._browser._defaultContext!._loadDefaultContextAsIs(progress); await context._browser._defaultContext!._loadDefaultContextAsIs(progress);
}); });
await context.extendInjectedScript('main', consoleApiSource.source); await context.extendInjectedScript(consoleApiSource.source);
const [page] = context.pages(); const [page] = context.pages();
if (traceViewerBrowser === 'chromium') if (traceViewerBrowser === 'chromium')

View file

@ -308,7 +308,7 @@ export class WKBrowserContext extends BrowserContext {
async _doAddInitScript(source: string) { async _doAddInitScript(source: string) {
this._evaluateOnNewDocumentSources.push(source); this._evaluateOnNewDocumentSources.push(source);
for (const page of this.pages()) for (const page of this.pages())
await (page._delegate as WKPage)._updateBootstrapScript('main'); await (page._delegate as WKPage)._updateBootstrapScript();
} }
async _doExposeBinding(binding: PageBinding) { async _doExposeBinding(binding: PageBinding) {

View file

@ -185,12 +185,10 @@ export class WKPage implements PageDelegate {
promises.push(session.send('Page.overrideUserAgent', { value: contextOptions.userAgent })); promises.push(session.send('Page.overrideUserAgent', { value: contextOptions.userAgent }));
if (this._page._state.mediaType || this._page._state.colorScheme || this._page._state.reducedMotion) 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)); 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();
const bootstrapScript = this._calculateBootstrapScript(world); if (bootstrapScript.length)
if (bootstrapScript.length) promises.push(session.send('Page.setBootstrapScript', { source: bootstrapScript }));
promises.push(session.send('Page.setBootstrapScript', { source: bootstrapScript, worldName: webkitWorldName(world) })); this._page.frames().map(frame => frame.evaluateExpression(bootstrapScript, false, undefined).catch(e => {}));
this._page.frames().map(frame => frame.evaluateExpression(bootstrapScript, false, undefined, world).catch(e => {}));
}
if (contextOptions.bypassCSP) if (contextOptions.bypassCSP)
promises.push(session.send('Page.setBypassCSP', { enabled: true })); promises.push(session.send('Page.setBypassCSP', { enabled: true }));
if (this._page._state.emulatedSize) { if (this._page._state.emulatedSize) {
@ -720,38 +718,34 @@ export class WKPage implements PageDelegate {
} }
async exposeBinding(binding: PageBinding): Promise<void> { async exposeBinding(binding: PageBinding): Promise<void> {
await this._updateBootstrapScript(binding.world); await this._updateBootstrapScript();
await this._evaluateBindingScript(binding); await this._evaluateBindingScript(binding);
} }
private async _evaluateBindingScript(binding: PageBinding): Promise<void> { private async _evaluateBindingScript(binding: PageBinding): Promise<void> {
const script = this._bindingToScript(binding); 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> { async evaluateOnNewDocument(script: string): Promise<void> {
await this._updateBootstrapScript('main'); await this._updateBootstrapScript();
} }
private _bindingToScript(binding: PageBinding): string { private _bindingToScript(binding: PageBinding): string {
return `self.${binding.name} = (param) => console.debug('${BINDING_CALL_MESSAGE}', {}, param); ${binding.source}`; 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[] = []; const scripts: string[] = [];
for (const binding of this._page.allBindings()) { for (const binding of this._page.allBindings())
if (binding.world === world) scripts.push(this._bindingToScript(binding));
scripts.push(this._bindingToScript(binding)); scripts.push(...this._browserContext._evaluateOnNewDocumentSources);
} scripts.push(...this._page._evaluateOnNewDocumentSources);
if (world === 'main') {
scripts.push(...this._browserContext._evaluateOnNewDocumentSources);
scripts.push(...this._page._evaluateOnNewDocumentSources);
}
return scripts.join(';'); return scripts.join(';');
} }
async _updateBootstrapScript(world: types.World): Promise<void> { async _updateBootstrapScript(): Promise<void> {
await this._updateState('Page.setBootstrapScript', { source: this._calculateBootstrapScript(world), worldName: webkitWorldName(world) }); await this._updateState('Page.setBootstrapScript', { source: this._calculateBootstrapScript() });
} }
async closePage(runBeforeUnload: boolean): Promise<void> { 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: * WebKit Remote Addresses look like:
* *

View file

@ -16,7 +16,6 @@
*/ */
import { test as it, expect } from './pageTest'; import { test as it, expect } from './pageTest';
import { attachFrame } from '../config/utils';
import type { ElementHandle } from '../../index'; import type { ElementHandle } from '../../index';
it('exposeBinding should work', async ({page}) => { 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); 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}) => { it('exposeBinding(handle) should work with element handles', async ({ page}) => {
let cb; let cb;
const promise = new Promise(f => cb = f); const promise = new Promise(f => cb = f);