chore: introduce helpers for non-stalling eval on page/context (#31658)

This commit is contained in:
Dmitry Gozman 2024-07-12 02:26:16 -07:00 committed by GitHub
parent 1b85ec9dc2
commit 229000501e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 45 additions and 48 deletions

View File

@ -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<string> {
const harId = createGuid();
this._harRecorders.set(harId, new HarRecorder(this, page, options));

View File

@ -68,27 +68,27 @@ export class DragManager {
let onDragIntercepted: (payload: Protocol.Input.dragInterceptedPayload) => void;
const dragInterceptedPromise = new Promise<Protocol.Input.dragInterceptedPayload>(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<boolean>(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<boolean>(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!);

View File

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

View File

@ -581,12 +581,12 @@ export class Frame extends SdkObject {
});
}
nonStallingEvaluateInExistingContext(expression: string, isFunction: boolean | undefined, world: types.World): Promise<any> {
nonStallingEvaluateInExistingContext(expression: string, world: types.World): Promise<any> {
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 });
});
}

View File

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

View File

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