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

View file

@ -176,7 +176,7 @@ export class CRPage implements PageDelegate {
async exposeBinding(binding: PageBinding) {
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() {
@ -507,9 +507,9 @@ class FrameSession {
worldName: UTILITY_WORLD_NAME,
});
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)
frame.evaluateExpression(source, false, undefined, 'main').catch(e => {});
frame.evaluateExpression(source).catch(e => {});
}
const isInitialEmptyPage = this._isMainFrame() && this._page.mainFrame().url() === ':';
if (isInitialEmptyPage) {

View file

@ -58,12 +58,12 @@ export class ElectronApplicationDispatcher extends Dispatcher<ElectronApplicatio
async evaluateExpression(params: channels.ElectronApplicationEvaluateExpressionParams): Promise<channels.ElectronApplicationEvaluateExpressionResult> {
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> {
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) };
}

View file

@ -209,11 +209,11 @@ export class ElementHandleDispatcher extends JSHandleDispatcher implements chann
}
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> {
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> {

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> {
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> {
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> {
@ -92,11 +92,11 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameChannel, Pa
}
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> {
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> {

View file

@ -36,11 +36,11 @@ export class JSHandleDispatcher extends Dispatcher<js.JSHandle, channels.JSHandl
}
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> {
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) };
}

View file

@ -52,10 +52,6 @@ export class FrameExecutionContext extends js.ExecutionContext {
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 {
if (handle instanceof ElementHandle && handle._context !== 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);
}
async evaluateExpressionAndWaitForSignals(expression: string, options: { isFunction?: boolean, exposeUtilityScript?: boolean }, arg?: any): Promise<any> {
return await this.frame._page._frameManager.waitForSignalsCreatedBy(null, false /* noWaitFor */, async () => {
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);
});
async evaluateExpressionHandle(expression: string, options: { isFunction?: boolean, exposeUtilityScript?: boolean }, arg?: any): Promise<js.JSHandle<any>> {
return js.evaluateExpression(this, expression, { ...options, returnByValue: false }, arg);
}
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);
}
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);
if (!handle)
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();
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 result = await arrayHandle.evaluateExpressionAndWaitForSignals(expression, isFunction, true, arg);
const result = await arrayHandle.evaluateExpression(expression, { isFunction }, arg);
arrayHandle.dispose();
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> {
// 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', {
deltaX,
deltaY,

View file

@ -751,21 +751,15 @@ export class Frame extends SdkObject {
return this._context('utility');
}
async evaluateExpressionHandleAndWaitForSignals(expression: string, isFunction: boolean | undefined, arg: any, world: types.World = 'main'): Promise<any> {
const context = await this._context(world);
const handle = await context.evaluateExpressionHandleAndWaitForSignals(expression, { isFunction }, 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);
async evaluateExpression(expression: string, options: { isFunction?: boolean, exposeUtilityScript?: boolean, world?: types.World } = {}, arg?: any): Promise<any> {
const context = await this._context(options.world ?? 'main');
const value = await context.evaluateExpression(expression, options, arg);
return value;
}
async evaluateExpressionAndWaitForSignals(expression: string, options: { isFunction?: boolean, exposeUtilityScript?: boolean }, arg: any, world: types.World = 'main'): Promise<any> {
const context = await this._context(world);
const value = await context.evaluateExpressionAndWaitForSignals(expression, options, arg);
async evaluateExpressionHandle(expression: string, options: { isFunction?: boolean, exposeUtilityScript?: boolean, world?: types.World } = {}, arg?: any): Promise<js.JSHandle<any>> {
const context = await this._context(options.world ?? 'main');
const value = await context.evaluateExpressionHandle(expression, options, arg);
return value;
}
@ -841,18 +835,18 @@ export class Frame extends SdkObject {
}, { 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 });
if (!handle)
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();
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 result = await arrayHandle.evaluateExpressionAndWaitForSignals(expression, isFunction, true, arg);
const result = await arrayHandle.evaluateExpression(expression, { isFunction }, arg);
arrayHandle.dispose();
return result;
}

View file

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

View file

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

View file

@ -242,7 +242,7 @@ export class Recorder implements InstrumentationListener {
private _refreshOverlay() {
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) {

View file

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

View file

@ -153,7 +153,7 @@ export class RawMouseImpl implements input.RawMouse {
throw new Error('Mouse wheel is not supported in mobile WebKit');
await this._session!.send('Page.updateScrollingState');
// 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', {
x,
y,

View file

@ -202,7 +202,7 @@ export class WKPage implements PageDelegate {
const bootstrapScript = this._calculateBootstrapScript();
if (bootstrapScript.length)
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)
promises.push(session.send('Page.setBypassCSP', { enabled: true }));
const emulatedSize = this._page.emulatedSize();
@ -767,7 +767,7 @@ export class WKPage implements PageDelegate {
async exposeBinding(binding: PageBinding): Promise<void> {
this._session.send('Runtime.addBinding', { name: binding.name });
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> {

View file

@ -141,7 +141,7 @@ class UIMode {
private _dispatchEvent(message: any) {
// 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() {

View file

@ -89,3 +89,18 @@ it('should access page after beforeunload', async ({ page, server }) => {
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');
});
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 }) => {
const messages = initServer(server);
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');
});
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 }) => {
const messages = [];
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');
});
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 }) => {
server.setRoute('/empty.html', async () => {});
await page.setContent(`<a id="anchor" href="${server.EMPTY_PAGE}">empty.html</a>`);