mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
fix: enable util world bindings in firefox (#6546)
This commit is contained in:
parent
dc7f7f9a8c
commit
41df6607b0
@ -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> {
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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')
|
||||
]);
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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'),
|
||||
]);
|
||||
})();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user