fix: enable util world bindings in firefox (#6546)

This commit is contained in:
Yury Semikhatsky 2021-05-12 22:19:27 +00:00 committed by GitHub
parent dc7f7f9a8c
commit 41df6607b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 94 additions and 74 deletions

View File

@ -23,7 +23,7 @@ import { Page, PageBinding, PageDelegate } from '../page';
import { ConnectionTransport } from '../transport';
import * as types from '../types';
import { ConnectionEvents, FFConnection } from './ffConnection';
import { FFPage } from './ffPage';
import { FFPage, UTILITY_WORLD_NAME } from './ffPage';
import { Protocol } from './protocol';
export class FFBrowser extends Browser {
@ -303,9 +303,8 @@ export class FFBrowserContext extends BrowserContext {
}
async _doExposeBinding(binding: PageBinding) {
if (binding.world !== 'main')
throw new Error('Only main context bindings are supported in Firefox.');
await this._browser._connection.send('Browser.addBinding', { browserContextId: this._browserContextId, name: binding.name, script: binding.source });
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 });
}
async _doUpdateRequestInterception(): Promise<void> {

View File

@ -33,7 +33,7 @@ import { Progress } from '../progress';
import { splitErrorMessage } from '../../utils/stackTrace';
import { debugLogger } from '../../utils/debugLogger';
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
export const UTILITY_WORLD_NAME = '__playwright_utility_world__';
export class FFPage implements PageDelegate {
readonly cspErrorsAsynchronousForInlineScipts = true;
@ -317,9 +317,8 @@ export class FFPage implements PageDelegate {
}
async exposeBinding(binding: PageBinding) {
if (binding.world !== 'main')
throw new Error('Only main context bindings are supported in Firefox.');
await this._session.send('Page.addBinding', { name: binding.name, script: binding.source });
const worldName = binding.world === 'utility' ? UTILITY_WORLD_NAME : '';
await this._session.send('Page.addBinding', { name: binding.name, script: binding.source, worldName });
}
didClose() {

View File

@ -581,36 +581,36 @@ export class PageBinding {
}
function takeHandle(arg: { name: string, seq: number }) {
const handle = (window as any)[arg.name]['handles'].get(arg.seq);
(window as any)[arg.name]['handles'].delete(arg.seq);
const handle = (globalThis as any)[arg.name]['handles'].get(arg.seq);
(globalThis as any)[arg.name]['handles'].delete(arg.seq);
return handle;
}
function deliverResult(arg: { name: string, seq: number, result: any }) {
(window as any)[arg.name]['callbacks'].get(arg.seq).resolve(arg.result);
(window as any)[arg.name]['callbacks'].delete(arg.seq);
(globalThis as any)[arg.name]['callbacks'].get(arg.seq).resolve(arg.result);
(globalThis as any)[arg.name]['callbacks'].delete(arg.seq);
}
function deliverError(arg: { name: string, seq: number, message: string, stack: string | undefined }) {
const error = new Error(arg.message);
error.stack = arg.stack;
(window as any)[arg.name]['callbacks'].get(arg.seq).reject(error);
(window as any)[arg.name]['callbacks'].delete(arg.seq);
(globalThis as any)[arg.name]['callbacks'].get(arg.seq).reject(error);
(globalThis as any)[arg.name]['callbacks'].delete(arg.seq);
}
function deliverErrorValue(arg: { name: string, seq: number, error: any }) {
(window as any)[arg.name]['callbacks'].get(arg.seq).reject(arg.error);
(window as any)[arg.name]['callbacks'].delete(arg.seq);
(globalThis as any)[arg.name]['callbacks'].get(arg.seq).reject(arg.error);
(globalThis as any)[arg.name]['callbacks'].delete(arg.seq);
}
}
}
function addPageBinding(bindingName: string, needsHandle: boolean) {
const binding = (window as any)[bindingName];
const binding = (globalThis as any)[bindingName];
if (binding.__installed)
return;
(window as any)[bindingName] = (...args: any[]) => {
const me = (window as any)[bindingName];
(globalThis as any)[bindingName] = (...args: any[]) => {
const me = (globalThis as any)[bindingName];
if (needsHandle && args.slice(1).some(arg => arg !== undefined))
throw new Error(`exposeBindingHandle supports a single argument, ${args.length} received`);
let callbacks = me['callbacks'];
@ -634,5 +634,5 @@ function addPageBinding(bindingName: string, needsHandle: boolean) {
}
return promise;
};
(window as any)[bindingName].__installed = true;
(globalThis as any)[bindingName].__installed = true;
}

View File

@ -20,14 +20,13 @@ import { generateSelector, querySelector } from './selectorGenerator';
import type { Point } from '../../../common/types';
import type { UIState } from '../recorder/recorderTypes';
declare global {
interface Window {
_playwrightRecorderPerformAction: (action: actions.Action) => Promise<void>;
_playwrightRecorderRecordAction: (action: actions.Action) => Promise<void>;
_playwrightRecorderState: () => Promise<UIState>;
_playwrightRecorderSetSelector: (selector: string) => Promise<void>;
_playwrightRefreshOverlay: () => void;
}
declare module globalThis {
let _playwrightRecorderPerformAction: (action: actions.Action) => Promise<void>;
let _playwrightRecorderRecordAction: (action: actions.Action) => Promise<void>;
let _playwrightRecorderState: () => Promise<UIState>;
let _playwrightRecorderSetSelector: (selector: string) => Promise<void>;
let _playwrightRefreshOverlay: () => void;
}
const scriptSymbol = Symbol('scriptSymbol');
@ -125,15 +124,15 @@ export class Recorder {
this._refreshListenersIfNeeded();
setInterval(() => {
this._refreshListenersIfNeeded();
if ((window as any)._recorderScriptReadyForTest) {
(window as any)._recorderScriptReadyForTest();
delete (window as any)._recorderScriptReadyForTest;
if (params.isUnderTest && !(this as any)._reportedReadyForTest) {
(this as any)._reportedReadyForTest = true;
console.error('Recorder script ready for test');
}
}, 500);
window._playwrightRefreshOverlay = () => {
globalThis._playwrightRefreshOverlay = () => {
this._pollRecorderMode().catch(e => console.log(e)); // eslint-disable-line no-console
};
window._playwrightRefreshOverlay();
globalThis._playwrightRefreshOverlay();
}
private _refreshListenersIfNeeded() {
@ -186,7 +185,7 @@ export class Recorder {
const pollPeriod = 1000;
if (this._pollRecorderModeTimer)
clearTimeout(this._pollRecorderModeTimer);
const state = await window._playwrightRecorderState().catch(e => null);
const state = await globalThis._playwrightRecorderState().catch(e => null);
if (!state) {
this._pollRecorderModeTimer = setTimeout(() => this._pollRecorderMode(), pollPeriod);
return;
@ -267,7 +266,7 @@ export class Recorder {
private _onClick(event: MouseEvent) {
if (this._mode === 'inspecting')
window._playwrightRecorderSetSelector(this._hoveredModel ? this._hoveredModel.selector : '');
globalThis._playwrightRecorderSetSelector(this._hoveredModel ? this._hoveredModel.selector : '');
if (this._shouldIgnoreMouseEvent(event))
return;
if (this._actionInProgress(event))
@ -349,8 +348,8 @@ export class Recorder {
const activeElement = this._deepActiveElement(document);
const result = activeElement ? generateSelector(this._injectedScript, activeElement) : null;
this._activeModel = result && result.selector ? result : null;
if ((window as any)._highlightUpdatedForTest)
(window as any)._highlightUpdatedForTest(result ? result.selector : null);
if (this._params.isUnderTest)
console.error('Highlight updated for test: ' + (result ? result.selector : null));
}
private _updateModelForHoveredElement() {
@ -365,8 +364,8 @@ export class Recorder {
return;
this._hoveredModel = selector ? { selector, elements } : null;
this._updateHighlight();
if ((window as any)._highlightUpdatedForTest)
(window as any)._highlightUpdatedForTest(selector);
if (this._params.isUnderTest)
console.error('Highlight updated for test: ' + selector);
}
private _updateHighlight() {
@ -455,7 +454,7 @@ export class Recorder {
}
if (elementType === 'file') {
window._playwrightRecorderRecordAction({
globalThis._playwrightRecorderRecordAction({
name: 'setInputFiles',
selector: this._activeModel!.selector,
signals: [],
@ -467,7 +466,7 @@ export class Recorder {
// Non-navigating actions are simply recorded by Playwright.
if (this._consumedDueWrongTarget(event))
return;
window._playwrightRecorderRecordAction({
globalThis._playwrightRecorderRecordAction({
name: 'fill',
selector: this._activeModel!.selector,
signals: [],
@ -564,7 +563,7 @@ export class Recorder {
private async _performAction(action: actions.Action) {
this._performingAction = true;
await window._playwrightRecorderPerformAction(action).catch(() => {});
await globalThis._playwrightRecorderPerformAction(action).catch(() => {});
this._performingAction = false;
// Action could have changed DOM, update hovered model selectors.
@ -572,11 +571,13 @@ export class Recorder {
// If that was a keyboard action, it similarly requires new selectors for active model.
this._onFocus();
if ((window as any)._actionPerformedForTest) {
(window as any)._actionPerformedForTest({
if (this._params.isUnderTest) {
// Serialize all to string as we cannot attribute console message to isolated world
// in Firefox.
console.error('Action performed for test: ' + JSON.stringify({
hovered: this._hoveredModel ? this._hoveredModel.selector : null,
active: this._activeModel ? this._activeModel.selector : null,
});
}));
}
}

View File

@ -29,7 +29,7 @@ test.describe('cli codegen', () => {
expect(selector).toBe('text=Submit');
const [message, sources] = await Promise.all([
page.waitForEvent('console'),
page.waitForEvent('console', msg => msg.type() !== 'error'),
recorder.waitForOutput('<javascript>', 'click'),
page.dispatchEvent('button', 'click', { detail: 1 })
]);
@ -77,7 +77,7 @@ await page.ClickAsync("text=Submit");`);
expect(selector).toBe('text=Submit');
const [message, sources] = await Promise.all([
page.waitForEvent('console'),
page.waitForEvent('console', msg => msg.type() !== 'error'),
recorder.waitForOutput('<javascript>', 'click'),
page.dispatchEvent('button', 'click', { detail: 1 })
]);
@ -103,7 +103,7 @@ await page.ClickAsync("text=Submit");`);
expect(selector).toBe('text=Submit');
const [message, sources] = await Promise.all([
page.waitForEvent('console'),
page.waitForEvent('console', msg => msg.type() !== 'error'),
recorder.waitForOutput('<javascript>', 'click'),
page.dispatchEvent('button', 'click', { detail: 1 })
]);
@ -155,7 +155,7 @@ await page.ClickAsync("text=Submit");`);
expect(divContents).toBe(`<div onclick="console.log('click')"> Some long text here </div>`);
const [message, sources] = await Promise.all([
page.waitForEvent('console'),
page.waitForEvent('console', msg => msg.type() !== 'error'),
recorder.waitForOutput('<javascript>', 'click'),
page.dispatchEvent('div', 'click', { detail: 1 })
]);
@ -173,7 +173,7 @@ await page.ClickAsync("text=Submit");`);
expect(selector).toBe('input[name="name"]');
const [message, sources] = await Promise.all([
page.waitForEvent('console'),
page.waitForEvent('console', msg => msg.type() !== 'error'),
recorder.waitForOutput('<javascript>', 'fill'),
page.fill('input', 'John')
]);
@ -208,7 +208,7 @@ await page.FillAsync(\"input[name=\\\"name\\\"]\", \"John\");`);
expect(selector).toBe('textarea[name="name"]');
const [message, sources] = await Promise.all([
page.waitForEvent('console'),
page.waitForEvent('console', msg => msg.type() !== 'error'),
recorder.waitForOutput('<javascript>', 'fill'),
page.fill('textarea', 'John')
]);
@ -322,7 +322,8 @@ await page.PressAsync(\"input[name=\\\"name\\\"]\", \"Shift+Enter\");`);
const messages: any[] = [];
page.on('console', message => {
messages.push(message);
if (message.type() !== 'error')
messages.push(message);
});
const [, sources] = await Promise.all([
recorder.waitForActionPerformed(),
@ -346,7 +347,7 @@ await page.PressAsync(\"input[name=\\\"name\\\"]\", \"Shift+Enter\");`);
expect(selector).toBe('input[name="accept"]');
const [message, sources] = await Promise.all([
page.waitForEvent('console'),
page.waitForEvent('console', msg => msg.type() !== 'error'),
recorder.waitForOutput('<javascript>', 'check'),
page.click('input')
]);
@ -383,7 +384,7 @@ await page.CheckAsync(\"input[name=\\\"accept\\\"]\");`);
expect(selector).toBe('input[name="accept"]');
const [message, sources] = await Promise.all([
page.waitForEvent('console'),
page.waitForEvent('console', msg => msg.type() !== 'error'),
recorder.waitForOutput('<javascript>', 'check'),
page.keyboard.press('Space')
]);
@ -403,7 +404,7 @@ await page.CheckAsync(\"input[name=\\\"accept\\\"]\");`);
expect(selector).toBe('input[name="accept"]');
const [message, sources] = await Promise.all([
page.waitForEvent('console'),
page.waitForEvent('console', msg => msg.type() !== 'error'),
recorder.waitForOutput('<javascript>', 'uncheck'),
page.click('input')
]);
@ -440,7 +441,7 @@ await page.UncheckAsync(\"input[name=\\\"accept\\\"]\");`);
expect(selector).toBe('select');
const [message, sources] = await Promise.all([
page.waitForEvent('console'),
page.waitForEvent('console', msg => msg.type() !== 'error'),
recorder.waitForOutput('<javascript>', 'select'),
page.selectOption('select', '2')
]);

View File

@ -442,7 +442,10 @@ await page1.GoToAsync("about:blank?foo");`);
`);
const messages: any[] = [];
page.on('console', message => messages.push(message.text()));
page.on('console', message => {
if (message.type() !== 'error')
messages.push(message.text());
});
await Promise.all([
page.click('button'),
recorder.waitForOutput('<javascript>', 'page.click')

View File

@ -93,12 +93,17 @@ class Recorder {
let callback;
const result = new Promise(f => callback = f);
await page.goto(url);
const frames = new Set<any>();
await page.exposeBinding('_recorderScriptReadyForTest', (source, arg) => {
frames.add(source.frame);
if (frames.size === frameCount)
callback(arg);
});
let msgCount = 0;
const listener = msg => {
if (msg.text() === 'Recorder script ready for test') {
++msgCount;
if (msgCount === frameCount) {
page.off('console', listener);
callback();
}
}
};
page.on('console', listener);
await Promise.all([
result,
page.setContent(content)
@ -128,23 +133,35 @@ class Recorder {
}
async waitForHighlight(action: () => Promise<void>): Promise<string> {
if (!this._highlightInstalled) {
this._highlightInstalled = true;
await this.page.exposeBinding('_highlightUpdatedForTest', (source, arg) => this._highlightCallback(arg));
}
let callback;
const result = new Promise<string>(f => callback = f);
const listener = async msg => {
const prefix = 'Highlight updated for test: ';
if (msg.text().startsWith(prefix)) {
this.page.off('console', listener);
callback(msg.text().substr(prefix.length));
}
};
this.page.on('console', listener);
const [ generatedSelector ] = await Promise.all([
new Promise<string>(f => this._highlightCallback = f),
result,
action()
]);
return generatedSelector;
}
async waitForActionPerformed(): Promise<{ hovered: string | null, active: string | null }> {
if (!this._actionReporterInstalled) {
this._actionReporterInstalled = true;
await this.page.exposeBinding('_actionPerformedForTest', (source, arg) => this._actionPerformedCallback(arg));
}
return await new Promise(f => this._actionPerformedCallback = f);
let callback;
const listener = async msg => {
const prefix = 'Action performed for test: ';
if (msg.text().startsWith(prefix)) {
this.page.off('console', listener);
const arg = JSON.parse(msg.text().substr(prefix.length));
callback(arg);
}
};
this.page.on('console', listener);
return new Promise(f => callback = f);
}
async hoverOverElement(selector: string): Promise<string> {

View File

@ -166,7 +166,7 @@ it.describe('pause', () => {
const scriptPromise = (async () => {
await page.pause();
await Promise.all([
page.waitForEvent('console'),
page.waitForEvent('console', msg => msg.type() === 'log' && msg.text() === '1'),
page.click('button'),
]);
})();