fix: make evaluate not wait for scheduled navigations (#23402)

Fixes #23141.
This commit is contained in:
Dmitry Gozman 2023-05-31 14:08:44 -07:00 committed by GitHub
parent 2505d48b32
commit 6bb5c0a549
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 69 additions and 160 deletions

View file

@ -471,7 +471,7 @@ export abstract class BrowserContext extends SdkObject {
await frame.goto(internalMetadata, origin); await frame.goto(internalMetadata, origin);
const storage = await frame.evaluateExpression(`({ const storage = await frame.evaluateExpression(`({
localStorage: Object.keys(localStorage).map(name => ({ name, value: localStorage.getItem(name) })), localStorage: Object.keys(localStorage).map(name => ({ name, value: localStorage.getItem(name) })),
})`, false, undefined, 'utility'); })`, { world: 'utility' });
originStorage.localStorage = storage.localStorage; originStorage.localStorage = storage.localStorage;
if (storage.localStorage.length) if (storage.localStorage.length)
result.origins.push(originStorage); result.origins.push(originStorage);
@ -536,7 +536,7 @@ export abstract class BrowserContext extends SdkObject {
originState => { originState => {
for (const { name, value } of (originState.localStorage || [])) for (const { name, value } of (originState.localStorage || []))
localStorage.setItem(name, value); localStorage.setItem(name, value);
}`, true, originState, 'utility'); }`, { isFunction: true, world: 'utility' }, originState);
} }
await page.close(internalMetadata); await page.close(internalMetadata);
} }

View file

@ -176,7 +176,7 @@ export class CRPage implements PageDelegate {
async exposeBinding(binding: PageBinding) { async exposeBinding(binding: PageBinding) {
await this._forAllFrameSessions(frame => frame._initBinding(binding)); await this._forAllFrameSessions(frame => frame._initBinding(binding));
await Promise.all(this._page.frames().map(frame => frame.evaluateExpression(binding.source, false, {}).catch(e => {}))); await Promise.all(this._page.frames().map(frame => frame.evaluateExpression(binding.source).catch(e => {})));
} }
async removeExposedBindings() { async removeExposedBindings() {
@ -507,9 +507,9 @@ class FrameSession {
worldName: UTILITY_WORLD_NAME, worldName: UTILITY_WORLD_NAME,
}); });
for (const binding of this._crPage._browserContext._pageBindings.values()) for (const binding of this._crPage._browserContext._pageBindings.values())
frame.evaluateExpression(binding.source, false, undefined).catch(e => {}); frame.evaluateExpression(binding.source).catch(e => {});
for (const source of this._crPage._browserContext.initScripts) for (const source of this._crPage._browserContext.initScripts)
frame.evaluateExpression(source, false, undefined, 'main').catch(e => {}); frame.evaluateExpression(source).catch(e => {});
} }
const isInitialEmptyPage = this._isMainFrame() && this._page.mainFrame().url() === ':'; const isInitialEmptyPage = this._isMainFrame() && this._page.mainFrame().url() === ':';
if (isInitialEmptyPage) { if (isInitialEmptyPage) {

View file

@ -58,12 +58,12 @@ export class ElectronApplicationDispatcher extends Dispatcher<ElectronApplicatio
async evaluateExpression(params: channels.ElectronApplicationEvaluateExpressionParams): Promise<channels.ElectronApplicationEvaluateExpressionResult> { async evaluateExpression(params: channels.ElectronApplicationEvaluateExpressionParams): Promise<channels.ElectronApplicationEvaluateExpressionResult> {
const handle = await this._object._nodeElectronHandlePromise; const handle = await this._object._nodeElectronHandlePromise;
return { value: serializeResult(await handle.evaluateExpressionAndWaitForSignals(params.expression, params.isFunction, true /* returnByValue */, parseArgument(params.arg))) }; return { value: serializeResult(await handle.evaluateExpression(params.expression, { isFunction: params.isFunction }, parseArgument(params.arg))) };
} }
async evaluateExpressionHandle(params: channels.ElectronApplicationEvaluateExpressionHandleParams): Promise<channels.ElectronApplicationEvaluateExpressionHandleResult> { async evaluateExpressionHandle(params: channels.ElectronApplicationEvaluateExpressionHandleParams): Promise<channels.ElectronApplicationEvaluateExpressionHandleResult> {
const handle = await this._object._nodeElectronHandlePromise; const handle = await this._object._nodeElectronHandlePromise;
const result = await handle.evaluateExpressionAndWaitForSignals(params.expression, params.isFunction, false /* returnByValue */, parseArgument(params.arg)); const result = await handle.evaluateExpressionHandle(params.expression, { isFunction: params.isFunction }, parseArgument(params.arg));
return { handle: ElementHandleDispatcher.fromJSHandle(this, result) }; return { handle: ElementHandleDispatcher.fromJSHandle(this, result) };
} }

View file

@ -209,11 +209,11 @@ export class ElementHandleDispatcher extends JSHandleDispatcher implements chann
} }
async evalOnSelector(params: channels.ElementHandleEvalOnSelectorParams, metadata: CallMetadata): Promise<channels.ElementHandleEvalOnSelectorResult> { async evalOnSelector(params: channels.ElementHandleEvalOnSelectorParams, metadata: CallMetadata): Promise<channels.ElementHandleEvalOnSelectorResult> {
return { value: serializeResult(await this._elementHandle.evalOnSelectorAndWaitForSignals(params.selector, !!params.strict, params.expression, params.isFunction, parseArgument(params.arg))) }; return { value: serializeResult(await this._elementHandle.evalOnSelector(params.selector, !!params.strict, params.expression, params.isFunction, parseArgument(params.arg))) };
} }
async evalOnSelectorAll(params: channels.ElementHandleEvalOnSelectorAllParams, metadata: CallMetadata): Promise<channels.ElementHandleEvalOnSelectorAllResult> { async evalOnSelectorAll(params: channels.ElementHandleEvalOnSelectorAllParams, metadata: CallMetadata): Promise<channels.ElementHandleEvalOnSelectorAllResult> {
return { value: serializeResult(await this._elementHandle.evalOnSelectorAllAndWaitForSignals(params.selector, params.expression, params.isFunction, parseArgument(params.arg))) }; return { value: serializeResult(await this._elementHandle.evalOnSelectorAll(params.selector, params.expression, params.isFunction, parseArgument(params.arg))) };
} }
async waitForElementState(params: channels.ElementHandleWaitForElementStateParams, metadata: CallMetadata): Promise<void> { async waitForElementState(params: channels.ElementHandleWaitForElementStateParams, metadata: CallMetadata): Promise<void> {

View file

@ -76,11 +76,11 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameChannel, Pa
} }
async evaluateExpression(params: channels.FrameEvaluateExpressionParams, metadata: CallMetadata): Promise<channels.FrameEvaluateExpressionResult> { async evaluateExpression(params: channels.FrameEvaluateExpressionParams, metadata: CallMetadata): Promise<channels.FrameEvaluateExpressionResult> {
return { value: serializeResult(await this._frame.evaluateExpressionAndWaitForSignals(params.expression, { isFunction: params.isFunction, exposeUtilityScript: params.exposeUtilityScript }, parseArgument(params.arg), 'main')) }; return { value: serializeResult(await this._frame.evaluateExpression(params.expression, { isFunction: params.isFunction, exposeUtilityScript: params.exposeUtilityScript }, parseArgument(params.arg))) };
} }
async evaluateExpressionHandle(params: channels.FrameEvaluateExpressionHandleParams, metadata: CallMetadata): Promise<channels.FrameEvaluateExpressionHandleResult> { async evaluateExpressionHandle(params: channels.FrameEvaluateExpressionHandleParams, metadata: CallMetadata): Promise<channels.FrameEvaluateExpressionHandleResult> {
return { handle: ElementHandleDispatcher.fromJSHandle(this.parentScope(), await this._frame.evaluateExpressionHandleAndWaitForSignals(params.expression, params.isFunction, parseArgument(params.arg), 'main')) }; return { handle: ElementHandleDispatcher.fromJSHandle(this.parentScope(), await this._frame.evaluateExpressionHandle(params.expression, { isFunction: params.isFunction }, parseArgument(params.arg))) };
} }
async waitForSelector(params: channels.FrameWaitForSelectorParams, metadata: CallMetadata): Promise<channels.FrameWaitForSelectorResult> { async waitForSelector(params: channels.FrameWaitForSelectorParams, metadata: CallMetadata): Promise<channels.FrameWaitForSelectorResult> {
@ -92,11 +92,11 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameChannel, Pa
} }
async evalOnSelector(params: channels.FrameEvalOnSelectorParams, metadata: CallMetadata): Promise<channels.FrameEvalOnSelectorResult> { async evalOnSelector(params: channels.FrameEvalOnSelectorParams, metadata: CallMetadata): Promise<channels.FrameEvalOnSelectorResult> {
return { value: serializeResult(await this._frame.evalOnSelectorAndWaitForSignals(params.selector, !!params.strict, params.expression, params.isFunction, parseArgument(params.arg))) }; return { value: serializeResult(await this._frame.evalOnSelector(params.selector, !!params.strict, params.expression, params.isFunction, parseArgument(params.arg))) };
} }
async evalOnSelectorAll(params: channels.FrameEvalOnSelectorAllParams, metadata: CallMetadata): Promise<channels.FrameEvalOnSelectorAllResult> { async evalOnSelectorAll(params: channels.FrameEvalOnSelectorAllParams, metadata: CallMetadata): Promise<channels.FrameEvalOnSelectorAllResult> {
return { value: serializeResult(await this._frame.evalOnSelectorAllAndWaitForSignals(params.selector, params.expression, params.isFunction, parseArgument(params.arg))) }; return { value: serializeResult(await this._frame.evalOnSelectorAll(params.selector, params.expression, params.isFunction, parseArgument(params.arg))) };
} }
async querySelector(params: channels.FrameQuerySelectorParams, metadata: CallMetadata): Promise<channels.FrameQuerySelectorResult> { async querySelector(params: channels.FrameQuerySelectorParams, metadata: CallMetadata): Promise<channels.FrameQuerySelectorResult> {

View file

@ -36,11 +36,11 @@ export class JSHandleDispatcher extends Dispatcher<js.JSHandle, channels.JSHandl
} }
async evaluateExpression(params: channels.JSHandleEvaluateExpressionParams): Promise<channels.JSHandleEvaluateExpressionResult> { async evaluateExpression(params: channels.JSHandleEvaluateExpressionParams): Promise<channels.JSHandleEvaluateExpressionResult> {
return { value: serializeResult(await this._object.evaluateExpressionAndWaitForSignals(params.expression, params.isFunction, true /* returnByValue */, parseArgument(params.arg))) }; return { value: serializeResult(await this._object.evaluateExpression(params.expression, { isFunction: params.isFunction }, parseArgument(params.arg))) };
} }
async evaluateExpressionHandle(params: channels.JSHandleEvaluateExpressionHandleParams): Promise<channels.JSHandleEvaluateExpressionHandleResult> { async evaluateExpressionHandle(params: channels.JSHandleEvaluateExpressionHandleParams): Promise<channels.JSHandleEvaluateExpressionHandleResult> {
const jsHandle = await this._object.evaluateExpressionAndWaitForSignals(params.expression, params.isFunction, false /* returnByValue */, parseArgument(params.arg)); const jsHandle = await this._object.evaluateExpressionHandle(params.expression, { isFunction: params.isFunction }, parseArgument(params.arg));
return { handle: ElementHandleDispatcher.fromJSHandle(this.parentScope(), jsHandle) }; return { handle: ElementHandleDispatcher.fromJSHandle(this.parentScope(), jsHandle) };
} }

View file

@ -52,10 +52,6 @@ export class FrameExecutionContext extends js.ExecutionContext {
this.world = world; this.world = world;
} }
override async waitForSignalsCreatedBy<T>(action: () => Promise<T>): Promise<T> {
return this.frame._page._frameManager.waitForSignalsCreatedBy(null, false, action);
}
override adoptIfNeeded(handle: js.JSHandle): Promise<js.JSHandle> | null { override adoptIfNeeded(handle: js.JSHandle): Promise<js.JSHandle> | null {
if (handle instanceof ElementHandle && handle._context !== this) if (handle instanceof ElementHandle && handle._context !== this)
return this.frame._page._delegate.adoptElementHandle(handle, this); return this.frame._page._delegate.adoptElementHandle(handle, this);
@ -74,16 +70,8 @@ export class FrameExecutionContext extends js.ExecutionContext {
return js.evaluateExpression(this, expression, { ...options, returnByValue: true }, arg); return js.evaluateExpression(this, expression, { ...options, returnByValue: true }, arg);
} }
async evaluateExpressionAndWaitForSignals(expression: string, options: { isFunction?: boolean, exposeUtilityScript?: boolean }, arg?: any): Promise<any> { async evaluateExpressionHandle(expression: string, options: { isFunction?: boolean, exposeUtilityScript?: boolean }, arg?: any): Promise<js.JSHandle<any>> {
return await this.frame._page._frameManager.waitForSignalsCreatedBy(null, false /* noWaitFor */, async () => { return js.evaluateExpression(this, expression, { ...options, returnByValue: false }, arg);
return this.evaluateExpression(expression, options, arg);
});
}
async evaluateExpressionHandleAndWaitForSignals(expression: string, options: { isFunction?: boolean, exposeUtilityScript?: boolean }, arg: any): Promise<any> {
return await this.frame._page._frameManager.waitForSignalsCreatedBy(null, false /* noWaitFor */, async () => {
return js.evaluateExpression(this, expression, { ...options, returnByValue: false }, arg);
});
} }
override createHandle(remoteObject: js.RemoteObject): js.JSHandle { override createHandle(remoteObject: js.RemoteObject): js.JSHandle {
@ -765,18 +753,18 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return this._frame.selectors.queryAll(selector, this); return this._frame.selectors.queryAll(selector, this);
} }
async evalOnSelectorAndWaitForSignals(selector: string, strict: boolean, expression: string, isFunction: boolean | undefined, arg: any): Promise<any> { async evalOnSelector(selector: string, strict: boolean, expression: string, isFunction: boolean | undefined, arg: any): Promise<any> {
const handle = await this._frame.selectors.query(selector, { strict }, this); const handle = await this._frame.selectors.query(selector, { strict }, this);
if (!handle) if (!handle)
throw new Error(`Error: failed to find element matching selector "${selector}"`); throw new Error(`Error: failed to find element matching selector "${selector}"`);
const result = await handle.evaluateExpressionAndWaitForSignals(expression, isFunction, true, arg); const result = await handle.evaluateExpression(expression, { isFunction }, arg);
handle.dispose(); handle.dispose();
return result; return result;
} }
async evalOnSelectorAllAndWaitForSignals(selector: string, expression: string, isFunction: boolean | undefined, arg: any): Promise<any> { async evalOnSelectorAll(selector: string, expression: string, isFunction: boolean | undefined, arg: any): Promise<any> {
const arrayHandle = await this._frame.selectors.queryArrayInMainWorld(selector, this); const arrayHandle = await this._frame.selectors.queryArrayInMainWorld(selector, this);
const result = await arrayHandle.evaluateExpressionAndWaitForSignals(expression, isFunction, true, arg); const result = await arrayHandle.evaluateExpression(expression, { isFunction }, arg);
arrayHandle.dispose(); arrayHandle.dispose();
return result; return result;
} }

View file

@ -145,7 +145,7 @@ export class RawMouseImpl implements input.RawMouse {
async wheel(x: number, y: number, buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, deltaX: number, deltaY: number): Promise<void> { async wheel(x: number, y: number, buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, deltaX: number, deltaY: number): Promise<void> {
// Wheel events hit the compositor first, so wait one frame for it to be synced. // Wheel events hit the compositor first, so wait one frame for it to be synced.
await this._page!.mainFrame().evaluateExpression(`new Promise(requestAnimationFrame)`, false, false, 'utility'); await this._page!.mainFrame().evaluateExpression(`new Promise(requestAnimationFrame)`, { world: 'utility' });
await this._client.send('Page.dispatchWheelEvent', { await this._client.send('Page.dispatchWheelEvent', {
deltaX, deltaX,
deltaY, deltaY,

View file

@ -751,21 +751,15 @@ export class Frame extends SdkObject {
return this._context('utility'); return this._context('utility');
} }
async evaluateExpressionHandleAndWaitForSignals(expression: string, isFunction: boolean | undefined, arg: any, world: types.World = 'main'): Promise<any> { async evaluateExpression(expression: string, options: { isFunction?: boolean, exposeUtilityScript?: boolean, world?: types.World } = {}, arg?: any): Promise<any> {
const context = await this._context(world); const context = await this._context(options.world ?? 'main');
const handle = await context.evaluateExpressionHandleAndWaitForSignals(expression, { isFunction }, arg); const value = await context.evaluateExpression(expression, options, arg);
return handle;
}
async evaluateExpression(expression: string, isFunction: boolean | undefined, arg: any, world: types.World = 'main'): Promise<any> {
const context = await this._context(world);
const value = await context.evaluateExpression(expression, { isFunction }, arg);
return value; return value;
} }
async evaluateExpressionAndWaitForSignals(expression: string, options: { isFunction?: boolean, exposeUtilityScript?: boolean }, arg: any, world: types.World = 'main'): Promise<any> { async evaluateExpressionHandle(expression: string, options: { isFunction?: boolean, exposeUtilityScript?: boolean, world?: types.World } = {}, arg?: any): Promise<js.JSHandle<any>> {
const context = await this._context(world); const context = await this._context(options.world ?? 'main');
const value = await context.evaluateExpressionAndWaitForSignals(expression, options, arg); const value = await context.evaluateExpressionHandle(expression, options, arg);
return value; return value;
} }
@ -841,18 +835,18 @@ export class Frame extends SdkObject {
}, { type, eventInit }, { mainWorld: true, ...options }); }, { type, eventInit }, { mainWorld: true, ...options });
} }
async evalOnSelectorAndWaitForSignals(selector: string, strict: boolean, expression: string, isFunction: boolean | undefined, arg: any): Promise<any> { async evalOnSelector(selector: string, strict: boolean, expression: string, isFunction: boolean | undefined, arg: any): Promise<any> {
const handle = await this.selectors.query(selector, { strict }); const handle = await this.selectors.query(selector, { strict });
if (!handle) if (!handle)
throw new Error(`Error: failed to find element matching selector "${selector}"`); throw new Error(`Error: failed to find element matching selector "${selector}"`);
const result = await handle.evaluateExpressionAndWaitForSignals(expression, isFunction, true, arg); const result = await handle.evaluateExpression(expression, { isFunction }, arg);
handle.dispose(); handle.dispose();
return result; return result;
} }
async evalOnSelectorAllAndWaitForSignals(selector: string, expression: string, isFunction: boolean | undefined, arg: any): Promise<any> { async evalOnSelectorAll(selector: string, expression: string, isFunction: boolean | undefined, arg: any): Promise<any> {
const arrayHandle = await this.selectors.queryArrayInMainWorld(selector); const arrayHandle = await this.selectors.queryArrayInMainWorld(selector);
const result = await arrayHandle.evaluateExpressionAndWaitForSignals(expression, isFunction, true, arg); const result = await arrayHandle.evaluateExpression(expression, { isFunction }, arg);
arrayHandle.dispose(); arrayHandle.dispose();
return result; return result;
} }

View file

@ -150,7 +150,7 @@ export class HarTracer {
title: document.title, title: document.title,
domContentLoaded: performance.timing.domContentLoadedEventStart, domContentLoaded: performance.timing.domContentLoadedEventStart,
}; };
}), true, undefined, 'utility').then(result => { }), { isFunction: true, world: 'utility' }).then(result => {
pageEntry.title = result.title; pageEntry.title = result.title;
if (!this._options.omitTiming) if (!this._options.omitTiming)
pageEntry.pageTimings.onContentLoad = result.domContentLoaded; pageEntry.pageTimings.onContentLoad = result.domContentLoaded;
@ -164,7 +164,7 @@ export class HarTracer {
title: document.title, title: document.title,
loaded: performance.timing.loadEventStart, loaded: performance.timing.loadEventStart,
}; };
}), true, undefined, 'utility').then(result => { }), { isFunction: true, world: 'utility' }).then(result => {
pageEntry.title = result.title; pageEntry.title = result.title;
if (!this._options.omitTiming) if (!this._options.omitTiming)
pageEntry.pageTimings.onLoad = result.loaded; pageEntry.pageTimings.onLoad = result.loaded;

View file

@ -108,10 +108,6 @@ export class ExecutionContext extends SdkObject {
return this._delegate.releaseHandle(objectId); return this._delegate.releaseHandle(objectId);
} }
async waitForSignalsCreatedBy<T>(action: () => Promise<T>): Promise<T> {
return action();
}
adoptIfNeeded(handle: JSHandle): Promise<JSHandle> | null { adoptIfNeeded(handle: JSHandle): Promise<JSHandle> | null {
return null; return null;
} }
@ -171,8 +167,14 @@ export class JSHandle<T = any> extends SdkObject {
return evaluate(this._context, false /* returnByValue */, pageFunction, this, arg); return evaluate(this._context, false /* returnByValue */, pageFunction, this, arg);
} }
async evaluateExpressionAndWaitForSignals(expression: string, isFunction: boolean | undefined, returnByValue: boolean, arg: any) { async evaluateExpression(expression: string, options: { isFunction?: boolean }, arg: any) {
const value = await evaluateExpressionAndWaitForSignals(this._context, returnByValue, expression, isFunction, this, arg); const value = await evaluateExpression(this._context, expression, { ...options, returnByValue: true }, this, arg);
await this._context.doSlowMo();
return value;
}
async evaluateExpressionHandle(expression: string, options: { isFunction?: boolean }, arg: any): Promise<JSHandle<any>> {
const value = await evaluateExpression(this._context, expression, { ...options, returnByValue: false }, this, arg);
await this._context.doSlowMo(); await this._context.doSlowMo();
return value; return value;
} }
@ -298,10 +300,6 @@ export async function evaluateExpression(context: ExecutionContext, expression:
} }
} }
export async function evaluateExpressionAndWaitForSignals(context: ExecutionContext, returnByValue: boolean, expression: string, isFunction: boolean | undefined, ...args: any[]): Promise<any> {
return await context.waitForSignalsCreatedBy(() => evaluateExpression(context, expression, { returnByValue, isFunction }, ...args));
}
export function parseUnserializableValue(unserializableValue: string): any { export function parseUnserializableValue(unserializableValue: string): any {
if (unserializableValue === 'NaN') if (unserializableValue === 'NaN')
return NaN; return NaN;

View file

@ -242,7 +242,7 @@ export class Recorder implements InstrumentationListener {
private _refreshOverlay() { private _refreshOverlay() {
for (const page of this._context.pages()) for (const page of this._context.pages())
page.mainFrame().evaluateExpression('window.__pw_refreshOverlay()', false, undefined, 'main').catch(() => {}); page.mainFrame().evaluateExpression('window.__pw_refreshOverlay()').catch(() => {});
} }
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) { async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata) {

View file

@ -149,25 +149,25 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
async setMode(mode: 'none' | 'recording' | 'inspecting'): Promise<void> { async setMode(mode: 'none' | 'recording' | 'inspecting'): Promise<void> {
await this._page.mainFrame().evaluateExpression(((mode: Mode) => { await this._page.mainFrame().evaluateExpression(((mode: Mode) => {
window.playwrightSetMode(mode); window.playwrightSetMode(mode);
}).toString(), true, mode, 'main').catch(() => {}); }).toString(), { isFunction: true }, mode).catch(() => {});
} }
async setFileIfNeeded(file: string): Promise<void> { async setFileIfNeeded(file: string): Promise<void> {
await this._page.mainFrame().evaluateExpression(((file: string) => { await this._page.mainFrame().evaluateExpression(((file: string) => {
window.playwrightSetFileIfNeeded(file); window.playwrightSetFileIfNeeded(file);
}).toString(), true, file, 'main').catch(() => {}); }).toString(), { isFunction: true }, file).catch(() => {});
} }
async setPaused(paused: boolean): Promise<void> { async setPaused(paused: boolean): Promise<void> {
await this._page.mainFrame().evaluateExpression(((paused: boolean) => { await this._page.mainFrame().evaluateExpression(((paused: boolean) => {
window.playwrightSetPaused(paused); window.playwrightSetPaused(paused);
}).toString(), true, paused, 'main').catch(() => {}); }).toString(), { isFunction: true }, paused).catch(() => {});
} }
async setSources(sources: Source[]): Promise<void> { async setSources(sources: Source[]): Promise<void> {
await this._page.mainFrame().evaluateExpression(((sources: Source[]) => { await this._page.mainFrame().evaluateExpression(((sources: Source[]) => {
window.playwrightSetSources(sources); window.playwrightSetSources(sources);
}).toString(), true, sources, 'main').catch(() => {}); }).toString(), { isFunction: true }, sources).catch(() => {});
// Testing harness for runCLI mode. // Testing harness for runCLI mode.
if (process.env.PWTEST_CLI_IS_UNDER_TEST && sources.length) if (process.env.PWTEST_CLI_IS_UNDER_TEST && sources.length)
@ -181,12 +181,12 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
} }
await this._page.mainFrame().evaluateExpression(((arg: any) => { await this._page.mainFrame().evaluateExpression(((arg: any) => {
window.playwrightSetSelector(arg.selector, arg.focus); window.playwrightSetSelector(arg.selector, arg.focus);
}).toString(), true, { selector, focus }, 'main').catch(() => {}); }).toString(), { isFunction: true }, { selector, focus }).catch(() => {});
} }
async updateCallLogs(callLogs: CallLog[]): Promise<void> { async updateCallLogs(callLogs: CallLog[]): Promise<void> {
await this._page.mainFrame().evaluateExpression(((callLogs: CallLog[]) => { await this._page.mainFrame().evaluateExpression(((callLogs: CallLog[]) => {
window.playwrightUpdateLogs(callLogs); window.playwrightUpdateLogs(callLogs);
}).toString(), true, callLogs, 'main').catch(() => {}); }).toString(), { isFunction: true }, callLogs).catch(() => {});
} }
} }

View file

@ -150,7 +150,7 @@ class StdinServer {
private _loadTrace(url: string) { private _loadTrace(url: string) {
clearTimeout(this._pollTimer); clearTimeout(this._pollTimer);
this._page?.mainFrame().evaluateExpression(`window.setTraceURL(${JSON.stringify(url)})`, false, undefined).catch(() => {}); this._page?.mainFrame().evaluateExpression(`window.setTraceURL(${JSON.stringify(url)})`).catch(() => {});
} }
private _pollLoadTrace(url: string) { private _pollLoadTrace(url: string) {

View file

@ -153,7 +153,7 @@ export class RawMouseImpl implements input.RawMouse {
throw new Error('Mouse wheel is not supported in mobile WebKit'); throw new Error('Mouse wheel is not supported in mobile WebKit');
await this._session!.send('Page.updateScrollingState'); await this._session!.send('Page.updateScrollingState');
// Wheel events hit the compositor first, so wait one frame for it to be synced. // Wheel events hit the compositor first, so wait one frame for it to be synced.
await this._page!.mainFrame().evaluateExpression(`new Promise(requestAnimationFrame)`, false, false, 'utility'); await this._page!.mainFrame().evaluateExpression(`new Promise(requestAnimationFrame)`, { world: 'utility' });
await this._pageProxySession.send('Input.dispatchWheelEvent', { await this._pageProxySession.send('Input.dispatchWheelEvent', {
x, x,
y, y,

View file

@ -202,7 +202,7 @@ export class WKPage implements PageDelegate {
const bootstrapScript = this._calculateBootstrapScript(); const bootstrapScript = this._calculateBootstrapScript();
if (bootstrapScript.length) if (bootstrapScript.length)
promises.push(session.send('Page.setBootstrapScript', { source: bootstrapScript })); promises.push(session.send('Page.setBootstrapScript', { source: bootstrapScript }));
this._page.frames().map(frame => frame.evaluateExpression(bootstrapScript, false, undefined).catch(e => {})); this._page.frames().map(frame => frame.evaluateExpression(bootstrapScript).catch(e => {}));
if (contextOptions.bypassCSP) if (contextOptions.bypassCSP)
promises.push(session.send('Page.setBypassCSP', { enabled: true })); promises.push(session.send('Page.setBypassCSP', { enabled: true }));
const emulatedSize = this._page.emulatedSize(); const emulatedSize = this._page.emulatedSize();
@ -767,7 +767,7 @@ export class WKPage implements PageDelegate {
async exposeBinding(binding: PageBinding): Promise<void> { async exposeBinding(binding: PageBinding): Promise<void> {
this._session.send('Runtime.addBinding', { name: binding.name }); this._session.send('Runtime.addBinding', { name: binding.name });
await this._updateBootstrapScript(); await this._updateBootstrapScript();
await Promise.all(this._page.frames().map(frame => frame.evaluateExpression(binding.source, false, {}).catch(e => {}))); await Promise.all(this._page.frames().map(frame => frame.evaluateExpression(binding.source).catch(e => {})));
} }
async removeExposedBindings(): Promise<void> { async removeExposedBindings(): Promise<void> {

View file

@ -141,7 +141,7 @@ class UIMode {
private _dispatchEvent(message: any) { private _dispatchEvent(message: any) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
this._page.mainFrame().evaluateExpression(dispatchFuncSource, true, message).catch(e => this._originalStderrWrite.call(process.stderr, String(e))); this._page.mainFrame().evaluateExpression(dispatchFuncSource, { isFunction: true }, message).catch(e => this._originalStderrWrite.call(process.stderr, String(e)));
} }
private async _listTests() { private async _listTests() {

View file

@ -89,3 +89,18 @@ it('should access page after beforeunload', async ({ page, server }) => {
await page.evaluate(() => document.title); await page.evaluate(() => document.title);
}); });
it('should not stall on evaluate when dismissing beforeunload', async ({ page, server }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/23141' });
await page.goto(server.PREFIX + '/beforeunload.html');
// We have to interact with a page so that 'beforeunload' handlers fire.
await page.click('body');
await Promise.all([
page.evaluate(() => {
window.location.reload();
}),
page.waitForEvent('dialog').then(dialog => dialog.dismiss()),
]);
});

View file

@ -43,48 +43,6 @@ it('should not stall on JS navigation link', async ({ page, browserName }) => {
await page.click('a'); await page.click('a');
}); });
it('should await navigation when clicking anchor programmatically', async ({ page, server }) => {
const messages = initServer(server);
await page.setContent(`<a id="anchor" href="${server.EMPTY_PAGE}">empty.html</a>`);
await Promise.all([
page.evaluate(() => (window as any).anchor.click()).then(() => messages.push('click')),
page.waitForEvent('framenavigated').then(() => messages.push('navigated')),
]);
expect(messages.join('|')).toBe('route|navigated|click');
});
it('should await navigation when clicking anchor via $eval', async ({ page, server }) => {
const messages = initServer(server);
await page.setContent(`<a id="anchor" href="${server.EMPTY_PAGE}">empty.html</a>`);
await Promise.all([
page.$eval('#anchor', anchor => (anchor as any).click()).then(() => messages.push('click')),
page.waitForEvent('framenavigated').then(() => messages.push('navigated')),
]);
expect(messages.join('|')).toBe('route|navigated|click');
});
it('should await navigation when clicking anchor via handle.eval', async ({ page, server }) => {
const messages = initServer(server);
await page.setContent(`<a id="anchor" href="${server.EMPTY_PAGE}">empty.html</a>`);
const handle = await page.evaluateHandle('document');
await Promise.all([
handle.evaluate(doc => (doc as any).getElementById('anchor').click()).then(() => messages.push('click')),
page.waitForEvent('framenavigated').then(() => messages.push('navigated')),
]);
expect(messages.join('|')).toBe('route|navigated|click');
});
it('should await navigation when clicking anchor via handle.$eval', async ({ page, server }) => {
const messages = initServer(server);
await page.setContent(`<a id="anchor" href="${server.EMPTY_PAGE}">empty.html</a>`);
const handle = await page.$('body');
await Promise.all([
handle.$eval('#anchor', anchor => (anchor as any).click()).then(() => messages.push('click')),
page.waitForEvent('framenavigated').then(() => messages.push('navigated')),
]);
expect(messages.join('|')).toBe('route|navigated|click');
});
it('should await cross-process navigation when clicking anchor', async ({ page, server }) => { it('should await cross-process navigation when clicking anchor', async ({ page, server }) => {
const messages = initServer(server); const messages = initServer(server);
await page.setContent(`<a href="${server.CROSS_PROCESS_PREFIX + '/empty.html'}">empty.html</a>`); await page.setContent(`<a href="${server.CROSS_PROCESS_PREFIX + '/empty.html'}">empty.html</a>`);
@ -96,17 +54,6 @@ it('should await cross-process navigation when clicking anchor', async ({ page,
expect(messages.join('|')).toBe('route|navigated|click'); expect(messages.join('|')).toBe('route|navigated|click');
}); });
it('should await cross-process navigation when clicking anchor programatically', async ({ page, server }) => {
const messages = initServer(server);
await page.setContent(`<a id="anchor" href="${server.CROSS_PROCESS_PREFIX + '/empty.html'}">empty.html</a>`);
await Promise.all([
page.evaluate(() => (window as any).anchor.click()).then(() => messages.push('click')),
page.waitForEvent('framenavigated').then(() => messages.push('navigated')),
]);
expect(messages.join('|')).toBe('route|navigated|click');
});
it('should await form-get on click', async ({ page, server }) => { it('should await form-get on click', async ({ page, server }) => {
const messages = []; const messages = [];
server.setRoute('/empty.html?foo=bar', async (req, res) => { server.setRoute('/empty.html?foo=bar', async (req, res) => {
@ -143,39 +90,6 @@ it('should await form-post on click', async ({ page, server }) => {
expect(messages.join('|')).toBe('route|navigated|click'); expect(messages.join('|')).toBe('route|navigated|click');
}); });
it('should await navigation when assigning location', async ({ page, server }) => {
const messages = initServer(server);
await Promise.all([
page.evaluate(`window.location.href = "${server.EMPTY_PAGE}"`).then(() => messages.push('evaluate')),
page.waitForEvent('framenavigated').then(() => messages.push('navigated')),
]);
expect(messages.join('|')).toBe('route|navigated|evaluate');
});
it('should await navigation when assigning location twice', async ({ page, server }) => {
const messages = [];
server.setRoute('/empty.html?cancel', async (req, res) => { res.end('done'); });
server.setRoute('/empty.html?override', async (req, res) => { messages.push('routeoverride'); res.end('done'); });
await page.evaluate(`
window.location.href = "${server.EMPTY_PAGE}?cancel";
window.location.href = "${server.EMPTY_PAGE}?override";
`);
messages.push('evaluate');
expect(messages.join('|')).toBe('routeoverride|evaluate');
});
it('should await navigation when evaluating reload', async ({ page, server, browserName }) => {
it.skip(browserName === 'firefox', 'With fission enabled, navigations in Firefox start asynchronously');
await page.goto(server.EMPTY_PAGE);
const messages = initServer(server);
await Promise.all([
page.evaluate(`window.location.reload()`).then(() => messages.push('evaluate')),
page.waitForEvent('framenavigated').then(() => messages.push('navigated')),
]);
expect(messages.join('|')).toBe('route|navigated|evaluate');
});
it('should work with noWaitAfter: true', async ({ page, server }) => { it('should work with noWaitAfter: true', async ({ page, server }) => {
server.setRoute('/empty.html', async () => {}); server.setRoute('/empty.html', async () => {});
await page.setContent(`<a id="anchor" href="${server.EMPTY_PAGE}">empty.html</a>`); await page.setContent(`<a id="anchor" href="${server.EMPTY_PAGE}">empty.html</a>`);