diff --git a/src/server/firefox/ffBrowser.ts b/src/server/firefox/ffBrowser.ts index 372d3adaaf..0ad5648425 100644 --- a/src/server/firefox/ffBrowser.ts +++ b/src/server/firefox/ffBrowser.ts @@ -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 { diff --git a/src/server/firefox/ffPage.ts b/src/server/firefox/ffPage.ts index 876e44ec32..f571fcd0cf 100644 --- a/src/server/firefox/ffPage.ts +++ b/src/server/firefox/ffPage.ts @@ -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() { diff --git a/src/server/page.ts b/src/server/page.ts index ce53829e94..57dc55306a 100644 --- a/src/server/page.ts +++ b/src/server/page.ts @@ -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; } diff --git a/src/server/supplements/injected/recorder.ts b/src/server/supplements/injected/recorder.ts index 6806e79bf7..7e0ca50ee3 100644 --- a/src/server/supplements/injected/recorder.ts +++ b/src/server/supplements/injected/recorder.ts @@ -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; - _playwrightRecorderRecordAction: (action: actions.Action) => Promise; - _playwrightRecorderState: () => Promise; - _playwrightRecorderSetSelector: (selector: string) => Promise; - _playwrightRefreshOverlay: () => void; - } + +declare module globalThis { + let _playwrightRecorderPerformAction: (action: actions.Action) => Promise; + let _playwrightRecorderRecordAction: (action: actions.Action) => Promise; + let _playwrightRecorderState: () => Promise; + let _playwrightRecorderSetSelector: (selector: string) => Promise; + 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, - }); + })); } } diff --git a/tests/inspector/cli-codegen-1.spec.ts b/tests/inspector/cli-codegen-1.spec.ts index 1f46510a0c..a1cdd5bd32 100644 --- a/tests/inspector/cli-codegen-1.spec.ts +++ b/tests/inspector/cli-codegen-1.spec.ts @@ -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('', '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('', '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('', 'click'), page.dispatchEvent('button', 'click', { detail: 1 }) ]); @@ -155,7 +155,7 @@ await page.ClickAsync("text=Submit");`); expect(divContents).toBe(`
Some long text here
`); const [message, sources] = await Promise.all([ - page.waitForEvent('console'), + page.waitForEvent('console', msg => msg.type() !== 'error'), recorder.waitForOutput('', '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('', '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('', '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('', '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('', '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('', '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('', 'select'), page.selectOption('select', '2') ]); diff --git a/tests/inspector/cli-codegen-2.spec.ts b/tests/inspector/cli-codegen-2.spec.ts index 12f0019133..b65c207961 100644 --- a/tests/inspector/cli-codegen-2.spec.ts +++ b/tests/inspector/cli-codegen-2.spec.ts @@ -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('', 'page.click') diff --git a/tests/inspector/inspectorTest.ts b/tests/inspector/inspectorTest.ts index 0f48c726fa..42cff5aab4 100644 --- a/tests/inspector/inspectorTest.ts +++ b/tests/inspector/inspectorTest.ts @@ -93,12 +93,17 @@ class Recorder { let callback; const result = new Promise(f => callback = f); await page.goto(url); - const frames = new Set(); - 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): Promise { - if (!this._highlightInstalled) { - this._highlightInstalled = true; - await this.page.exposeBinding('_highlightUpdatedForTest', (source, arg) => this._highlightCallback(arg)); - } + let callback; + const result = new Promise(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(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 { diff --git a/tests/inspector/pause.spec.ts b/tests/inspector/pause.spec.ts index 9799c4dd5e..09e9191c49 100644 --- a/tests/inspector/pause.spec.ts +++ b/tests/inspector/pause.spec.ts @@ -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'), ]); })();