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

3
.gitignore vendored
View File

@ -17,4 +17,5 @@ drivers/
nohup.out
.trace
.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);
this._dispatchEvent('bindingCall', { binding });
return binding.promise();
}, 'main');
});
}
async fetch(params: channels.BrowserContextFetchParams): Promise<channels.BrowserContextFetchResult> {

View File

@ -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));

View File

@ -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 })
]);
}

View File

@ -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> {

View File

@ -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() {

View File

@ -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);

View File

@ -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);

View File

@ -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),

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
// 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();

View File

@ -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')

View File

@ -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) {

View File

@ -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:
*

View File

@ -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);