diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index 92f3f8a20b..84673f533d 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -507,7 +507,7 @@ export abstract class BrowserContext extends SdkObject { try { const storage = await page.mainFrame().nonStallingEvaluateInExistingContext(`({ localStorage: Object.keys(localStorage).map(name => ({ name, value: localStorage.getItem(name) })), - })`, false, 'utility'); + })`, 'utility'); if (storage.localStorage.length) result.origins.push({ origin, localStorage: storage.localStorage } as channels.OriginStorage); originsToSave.delete(origin); @@ -619,6 +619,10 @@ export abstract class BrowserContext extends SdkObject { return Promise.all(this.pages().map(installInPage)); } + async safeNonStallingEvaluateInAllFrames(expression: string, world: types.World, options: { throwOnJSErrors?: boolean } = {}) { + await Promise.all(this.pages().map(page => page.safeNonStallingEvaluateInAllFrames(expression, world, options))); + } + async _harStart(page: Page | null, options: channels.RecordHarOptions): Promise { const harId = createGuid(); this._harRecorders.set(harId, new HarRecorder(this, page, options)); diff --git a/packages/playwright-core/src/server/chromium/crDragDrop.ts b/packages/playwright-core/src/server/chromium/crDragDrop.ts index 39d10ee542..ea4af1ce7e 100644 --- a/packages/playwright-core/src/server/chromium/crDragDrop.ts +++ b/packages/playwright-core/src/server/chromium/crDragDrop.ts @@ -68,27 +68,27 @@ export class DragManager { let onDragIntercepted: (payload: Protocol.Input.dragInterceptedPayload) => void; const dragInterceptedPromise = new Promise(x => onDragIntercepted = x); - await Promise.all(this._crPage._page.frames().map(async frame => { - await frame.nonStallingEvaluateInExistingContext((function() { - let didStartDrag = Promise.resolve(false); - let dragEvent: Event|null = null; - const dragListener = (event: Event) => dragEvent = event; - const mouseListener = () => { - didStartDrag = new Promise(callback => { - window.addEventListener('dragstart', dragListener, { once: true, capture: true }); - setTimeout(() => callback(dragEvent ? !dragEvent.defaultPrevented : false), 0); - }); - }; - window.addEventListener('mousemove', mouseListener, { once: true, capture: true }); - window.__cleanupDrag = async () => { - const val = await didStartDrag; - window.removeEventListener('mousemove', mouseListener, { capture: true }); - window.removeEventListener('dragstart', dragListener, { capture: true }); - delete window.__cleanupDrag; - return val; - }; - }).toString(), true, 'utility').catch(() => {}); - })); + function setupDragListeners() { + let didStartDrag = Promise.resolve(false); + let dragEvent: Event|null = null; + const dragListener = (event: Event) => dragEvent = event; + const mouseListener = () => { + didStartDrag = new Promise(callback => { + window.addEventListener('dragstart', dragListener, { once: true, capture: true }); + setTimeout(() => callback(dragEvent ? !dragEvent.defaultPrevented : false), 0); + }); + }; + window.addEventListener('mousemove', mouseListener, { once: true, capture: true }); + window.__cleanupDrag = async () => { + const val = await didStartDrag; + window.removeEventListener('mousemove', mouseListener, { capture: true }); + window.removeEventListener('dragstart', dragListener, { capture: true }); + delete window.__cleanupDrag; + return val; + }; + } + + await this._crPage._page.safeNonStallingEvaluateInAllFrames(`(${setupDragListeners.toString()})()`, 'utility'); client.on('Input.dragIntercepted', onDragIntercepted!); try { @@ -102,7 +102,7 @@ export class DragManager { await moveCallback(); const expectingDrag = (await Promise.all(this._crPage._page.frames().map(async frame => { - return frame.nonStallingEvaluateInExistingContext('window.__cleanupDrag && window.__cleanupDrag()', false, 'utility').catch(() => false); + return frame.nonStallingEvaluateInExistingContext('window.__cleanupDrag && window.__cleanupDrag()', 'utility').catch(() => false); }))).some(x => x); this._dragState = expectingDrag ? (await dragInterceptedPromise).data : null; client.off('Input.dragIntercepted', onDragIntercepted!); diff --git a/packages/playwright-core/src/server/clock.ts b/packages/playwright-core/src/server/clock.ts index 5049e4766d..e77399c4fe 100644 --- a/packages/playwright-core/src/server/clock.ts +++ b/packages/playwright-core/src/server/clock.ts @@ -16,7 +16,6 @@ import type { BrowserContext } from './browserContext'; import * as clockSource from '../generated/clockSource'; -import { isJavaScriptErrorInEvaluate } from './javascript'; export class Clock { private _browserContext: BrowserContext; @@ -87,25 +86,12 @@ export class Clock { ${clockSource.source} globalThis.__pwClock = (module.exports.inject())(globalThis); })();`; - await this._addAndEvaluate(script); - } - - private async _addAndEvaluate(script: string) { await this._browserContext.addInitScript(script); - return await this._evaluateInFrames(script); + await this._evaluateInFrames(script); } private async _evaluateInFrames(script: string) { - const frames = this._browserContext.pages().map(page => page.frames()).flat(); - const results = await Promise.all(frames.map(async frame => { - try { - await frame.nonStallingEvaluateInExistingContext(script, false, 'main'); - } catch (e) { - if (isJavaScriptErrorInEvaluate(e)) - throw e; - } - })); - return results[0]; + await this._browserContext.safeNonStallingEvaluateInAllFrames(script, 'main', { throwOnJSErrors: true }); } } diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index c2a11d27be..c5bf0ba24f 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -581,12 +581,12 @@ export class Frame extends SdkObject { }); } - nonStallingEvaluateInExistingContext(expression: string, isFunction: boolean | undefined, world: types.World): Promise { + nonStallingEvaluateInExistingContext(expression: string, world: types.World): Promise { return this.raceAgainstEvaluationStallingEvents(() => { const context = this._contextData.get(world)?.context; if (!context) throw new Error('Frame does not yet have the execution context'); - return context.evaluateExpression(expression, { isFunction }); + return context.evaluateExpression(expression, { isFunction: false }); }); } diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index da9063821c..6206681048 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -749,6 +749,17 @@ export class Page extends SdkObject { this._frameThrottler.recharge(); } + async safeNonStallingEvaluateInAllFrames(expression: string, world: types.World, options: { throwOnJSErrors?: boolean } = {}) { + await Promise.all(this.frames().map(async frame => { + try { + await frame.nonStallingEvaluateInExistingContext(expression, world); + } catch (e) { + if (options.throwOnJSErrors && js.isJavaScriptErrorInEvaluate(e)) + throw e; + } + })); + } + async hideHighlight() { await Promise.all(this.frames().map(frame => frame.hideHighlight().catch(() => {}))); } diff --git a/packages/playwright-core/src/server/screenshotter.ts b/packages/playwright-core/src/server/screenshotter.ts index 9ecd49f899..485560b539 100644 --- a/packages/playwright-core/src/server/screenshotter.ts +++ b/packages/playwright-core/src/server/screenshotter.ts @@ -261,21 +261,17 @@ export class Screenshotter { if (disableAnimations) progress.log(' disabled all CSS animations'); const syncAnimations = this._page._delegate.shouldToggleStyleSheetToSyncAnimations(); - await Promise.all(this._page.frames().map(async frame => { - await frame.nonStallingEvaluateInExistingContext('(' + inPagePrepareForScreenshots.toString() + `)(${JSON.stringify(screenshotStyle)}, ${hideCaret}, ${disableAnimations}, ${syncAnimations})`, false, 'utility').catch(() => {}); - })); + await this._page.safeNonStallingEvaluateInAllFrames('(' + inPagePrepareForScreenshots.toString() + `)(${JSON.stringify(screenshotStyle)}, ${hideCaret}, ${disableAnimations}, ${syncAnimations})`, 'utility'); if (!process.env.PW_TEST_SCREENSHOT_NO_FONTS_READY) { progress.log('waiting for fonts to load...'); - await frame.nonStallingEvaluateInExistingContext('document.fonts.ready', false, 'utility').catch(() => {}); + await frame.nonStallingEvaluateInExistingContext('document.fonts.ready', 'utility').catch(() => {}); progress.log('fonts loaded'); } progress.cleanupWhenAborted(() => this._restorePageAfterScreenshot()); } async _restorePageAfterScreenshot() { - await Promise.all(this._page.frames().map(async frame => { - frame.nonStallingEvaluateInExistingContext('window.__pwCleanupScreenshot && window.__pwCleanupScreenshot()', false, 'utility').catch(() => {}); - })); + await this._page.safeNonStallingEvaluateInAllFrames('window.__pwCleanupScreenshot && window.__pwCleanupScreenshot()', 'utility'); } async _maskElements(progress: Progress, options: ScreenshotOptions): Promise<() => Promise> {