diff --git a/src/dispatchers/electronDispatcher.ts b/src/dispatchers/electronDispatcher.ts index 74bbc43b18545..3bec8a756e381 100644 --- a/src/dispatchers/electronDispatcher.ts +++ b/src/dispatchers/electronDispatcher.ts @@ -51,12 +51,12 @@ export class ElectronApplicationDispatcher extends Dispatcher { const handle = this._object._nodeElectronHandle!; - return { value: serializeResult(await handle.evaluateExpression(params.expression, params.isFunction, true /* returnByValue */, parseArgument(params.arg))) }; + return { value: serializeResult(await handle.evaluateExpressionAndWaitForSignals(params.expression, params.isFunction, true /* returnByValue */, parseArgument(params.arg))) }; } async evaluateExpressionHandle(params: channels.ElectronApplicationEvaluateExpressionHandleParams): Promise { const handle = this._object._nodeElectronHandle!; - const result = await handle.evaluateExpression(params.expression, params.isFunction, false /* returnByValue */, parseArgument(params.arg)); + const result = await handle.evaluateExpressionAndWaitForSignals(params.expression, params.isFunction, false /* returnByValue */, parseArgument(params.arg)); return { handle: createHandle(this._scope, result) }; } diff --git a/src/dispatchers/elementHandlerDispatcher.ts b/src/dispatchers/elementHandlerDispatcher.ts index a66d890bc0919..946c100a372ca 100644 --- a/src/dispatchers/elementHandlerDispatcher.ts +++ b/src/dispatchers/elementHandlerDispatcher.ts @@ -171,11 +171,11 @@ export class ElementHandleDispatcher extends JSHandleDispatcher implements chann } async evalOnSelector(params: channels.ElementHandleEvalOnSelectorParams, metadata: CallMetadata): Promise { - return { value: serializeResult(await this._elementHandle.$evalExpression(params.selector, params.expression, params.isFunction, parseArgument(params.arg))) }; + return { value: serializeResult(await this._elementHandle.evalOnSelectorAndWaitForSignals(params.selector, params.expression, params.isFunction, parseArgument(params.arg))) }; } async evalOnSelectorAll(params: channels.ElementHandleEvalOnSelectorAllParams, metadata: CallMetadata): Promise { - return { value: serializeResult(await this._elementHandle.$$evalExpression(params.selector, params.expression, params.isFunction, parseArgument(params.arg))) }; + return { value: serializeResult(await this._elementHandle.evalOnSelectorAllAndWaitForSignals(params.selector, params.expression, params.isFunction, parseArgument(params.arg))) }; } async waitForElementState(params: channels.ElementHandleWaitForElementStateParams, metadata: CallMetadata): Promise { diff --git a/src/dispatchers/frameDispatcher.ts b/src/dispatchers/frameDispatcher.ts index d71f135f31991..048645c0d901b 100644 --- a/src/dispatchers/frameDispatcher.ts +++ b/src/dispatchers/frameDispatcher.ts @@ -77,11 +77,11 @@ export class FrameDispatcher extends Dispatcher { - return { value: serializeResult(await this._frame._$evalExpression(params.selector, params.expression, params.isFunction, parseArgument(params.arg))) }; + return { value: serializeResult(await this._frame.evalOnSelectorAndWaitForSignals(params.selector, params.expression, params.isFunction, parseArgument(params.arg))) }; } async evalOnSelectorAll(params: channels.FrameEvalOnSelectorAllParams, metadata: CallMetadata): Promise { - return { value: serializeResult(await this._frame._$$evalExpression(params.selector, params.expression, params.isFunction, parseArgument(params.arg))) }; + return { value: serializeResult(await this._frame.evalOnSelectorAllAndWaitForSignals(params.selector, params.expression, params.isFunction, parseArgument(params.arg))) }; } async querySelector(params: channels.FrameQuerySelectorParams, metadata: CallMetadata): Promise { diff --git a/src/dispatchers/jsHandleDispatcher.ts b/src/dispatchers/jsHandleDispatcher.ts index 65a48998d2577..9c78bc7791fe2 100644 --- a/src/dispatchers/jsHandleDispatcher.ts +++ b/src/dispatchers/jsHandleDispatcher.ts @@ -31,11 +31,11 @@ export class JSHandleDispatcher extends Dispatcher { - return { value: serializeResult(await this._object.evaluateExpression(params.expression, params.isFunction, true /* returnByValue */, parseArgument(params.arg))) }; + return { value: serializeResult(await this._object.evaluateExpressionAndWaitForSignals(params.expression, params.isFunction, true /* returnByValue */, parseArgument(params.arg))) }; } async evaluateExpressionHandle(params: channels.JSHandleEvaluateExpressionHandleParams): Promise { - const jsHandle = await this._object.evaluateExpression(params.expression, params.isFunction, false /* returnByValue */, parseArgument(params.arg)); + const jsHandle = await this._object.evaluateExpressionAndWaitForSignals(params.expression, params.isFunction, false /* returnByValue */, parseArgument(params.arg)); return { handle: createHandle(this._scope, jsHandle) }; } diff --git a/src/dispatchers/pageDispatcher.ts b/src/dispatchers/pageDispatcher.ts index 6f8087e4f1207..24ec17014c430 100644 --- a/src/dispatchers/pageDispatcher.ts +++ b/src/dispatchers/pageDispatcher.ts @@ -251,11 +251,11 @@ export class WorkerDispatcher extends Dispatcher { - return { value: serializeResult(await this._object._evaluateExpression(params.expression, params.isFunction, parseArgument(params.arg))) }; + return { value: serializeResult(await this._object.evaluateExpression(params.expression, params.isFunction, parseArgument(params.arg))) }; } async evaluateExpressionHandle(params: channels.WorkerEvaluateExpressionHandleParams, metadata: CallMetadata): Promise { - return { handle: createHandle(this._scope, await this._object._evaluateExpressionHandle(params.expression, params.isFunction, parseArgument(params.arg))) }; + return { handle: createHandle(this._scope, await this._object.evaluateExpressionHandle(params.expression, params.isFunction, parseArgument(params.arg))) }; } } diff --git a/src/server/dom.ts b/src/server/dom.ts index b7bb4dc469938..7f8d0cd945b9e 100644 --- a/src/server/dom.ts +++ b/src/server/dom.ts @@ -37,6 +37,10 @@ export class FrameExecutionContext extends js.ExecutionContext { this.world = world; } + async waitForSignalsCreatedBy(action: () => Promise): Promise { + return this.frame._page._frameManager.waitForSignalsCreatedBy(null, false, action); + } + adoptIfNeeded(handle: js.JSHandle): Promise | null { if (handle instanceof ElementHandle && handle._context !== this) return this.frame._page._delegate.adoptElementHandle(handle, this); @@ -654,18 +658,18 @@ export class ElementHandle extends js.JSHandle { return this._page.selectors._queryAll(this._context.frame, selector, this, true /* adoptToMain */); } - async $evalExpression(selector: string, expression: string, isFunction: boolean | undefined, arg: any): Promise { + async evalOnSelectorAndWaitForSignals(selector: string, expression: string, isFunction: boolean | undefined, arg: any): Promise { const handle = await this._page.selectors._query(this._context.frame, selector, this); if (!handle) throw new Error(`Error: failed to find element matching selector "${selector}"`); - const result = await handle.evaluateExpression(expression, isFunction, true, arg); + const result = await handle.evaluateExpressionAndWaitForSignals(expression, isFunction, true, arg); handle.dispose(); return result; } - async $$evalExpression(selector: string, expression: string, isFunction: boolean | undefined, arg: any): Promise { + async evalOnSelectorAllAndWaitForSignals(selector: string, expression: string, isFunction: boolean | undefined, arg: any): Promise { const arrayHandle = await this._page.selectors._queryArray(this._context.frame, selector, this); - const result = await arrayHandle.evaluateExpression(expression, isFunction, true, arg); + const result = await arrayHandle.evaluateExpressionAndWaitForSignals(expression, isFunction, true, arg); arrayHandle.dispose(); return result; } diff --git a/src/server/frames.ts b/src/server/frames.ts index 08fd97c5a0fc8..9f7fe310fcd68 100644 --- a/src/server/frames.ts +++ b/src/server/frames.ts @@ -656,18 +656,18 @@ export class Frame extends SdkObject { await this._page._doSlowMo(); } - async _$evalExpression(selector: string, expression: string, isFunction: boolean | undefined, arg: any): Promise { + async evalOnSelectorAndWaitForSignals(selector: string, expression: string, isFunction: boolean | undefined, arg: any): Promise { const handle = await this.$(selector); if (!handle) throw new Error(`Error: failed to find element matching selector "${selector}"`); - const result = await handle.evaluateExpression(expression, isFunction, true, arg); + const result = await handle.evaluateExpressionAndWaitForSignals(expression, isFunction, true, arg); handle.dispose(); return result; } - async _$$evalExpression(selector: string, expression: string, isFunction: boolean | undefined, arg: any): Promise { + async evalOnSelectorAllAndWaitForSignals(selector: string, expression: string, isFunction: boolean | undefined, arg: any): Promise { const arrayHandle = await this._page.selectors._queryArray(this, selector); - const result = await arrayHandle.evaluateExpression(expression, isFunction, true, arg); + const result = await arrayHandle.evaluateExpressionAndWaitForSignals(expression, isFunction, true, arg); arrayHandle.dispose(); return result; } diff --git a/src/server/javascript.ts b/src/server/javascript.ts index 7687cdd1d3359..2eac1a143a428 100644 --- a/src/server/javascript.ts +++ b/src/server/javascript.ts @@ -60,6 +60,10 @@ export class ExecutionContext extends SdkObject { this._delegate = delegate; } + async waitForSignalsCreatedBy(action: () => Promise): Promise { + return action(); + } + adoptIfNeeded(handle: JSHandle): Promise | null { return null; } @@ -122,8 +126,8 @@ export class JSHandle extends SdkObject { return evaluate(this._context, false /* returnByValue */, pageFunction, this, arg); } - async evaluateExpression(expression: string, isFunction: boolean | undefined, returnByValue: boolean, arg: any) { - const value = await evaluateExpression(this._context, returnByValue, expression, isFunction, this, arg); + async evaluateExpressionAndWaitForSignals(expression: string, isFunction: boolean | undefined, returnByValue: boolean, arg: any) { + const value = await evaluateExpressionAndWaitForSignals(this._context, returnByValue, expression, isFunction, this, arg); await this._context.doSlowMo(); return value; } @@ -225,6 +229,10 @@ export async function evaluateExpression(context: ExecutionContext, returnByValu } } +export async function evaluateExpressionAndWaitForSignals(context: ExecutionContext, returnByValue: boolean, expression: string, isFunction?: boolean, ...args: any[]): Promise { + return await context.waitForSignalsCreatedBy(() => evaluateExpression(context, returnByValue, expression, isFunction, ...args)); +} + export function parseUnserializableValue(unserializableValue: string): any { if (unserializableValue === 'NaN') return NaN; diff --git a/src/server/page.ts b/src/server/page.ts index 16d5738ef9d5c..7c4125ba6c92a 100644 --- a/src/server/page.ts +++ b/src/server/page.ts @@ -528,11 +528,11 @@ export class Worker extends SdkObject { return this._url; } - async _evaluateExpression(expression: string, isFunction: boolean | undefined, arg: any): Promise { + async evaluateExpression(expression: string, isFunction: boolean | undefined, arg: any): Promise { return js.evaluateExpression(await this._executionContextPromise, true /* returnByValue */, expression, isFunction, arg); } - async _evaluateExpressionHandle(expression: string, isFunction: boolean | undefined, arg: any): Promise { + async evaluateExpressionHandle(expression: string, isFunction: boolean | undefined, arg: any): Promise { return js.evaluateExpression(await this._executionContextPromise, false /* returnByValue */, expression, isFunction, arg); } } diff --git a/test/page-autowaiting-basic.spec.ts b/test/page-autowaiting-basic.spec.ts index 74ad3eacb9606..c4cb6ffdd3685 100644 --- a/test/page-autowaiting-basic.spec.ts +++ b/test/page-autowaiting-basic.spec.ts @@ -15,18 +15,22 @@ * limitations under the License. */ +import { TestServer } from '../utils/testserver'; import { it, expect } from './fixtures'; -it('should await navigation when clicking anchor', async ({page, server}) => { +function initServer(server: TestServer): string[] { const messages = []; server.setRoute('/empty.html', async (req, res) => { messages.push('route'); res.setHeader('Content-Type', 'text/html'); res.end(``); }); + return messages; +} - await page.setContent(`empty.html`); - +it('should await navigation when clicking anchor', async ({page, server}) => { + const messages = initServer(server); + await page.setContent(`empty.html`); await Promise.all([ page.click('a').then(() => messages.push('click')), page.waitForEvent('framenavigated').then(() => messages.push('navigated')), @@ -34,14 +38,50 @@ it('should await navigation when clicking anchor', async ({page, server}) => { expect(messages.join('|')).toBe('route|navigated|click'); }); -it('should await cross-process navigation when clicking anchor', async ({page, server}) => { - const messages = []; - server.setRoute('/empty.html', async (req, res) => { - messages.push('route'); - res.setHeader('Content-Type', 'text/html'); - res.end(``); - }); +it('should await navigation when clicking anchor programmatically', async ({page, server}) => { + const messages = initServer(server); + await page.setContent(`empty.html`); + 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(`empty.html`); + 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(`empty.html`); + 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(`empty.html`); + 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(`empty.html`); await Promise.all([ @@ -51,6 +91,17 @@ it('should await cross-process navigation when clicking anchor', async ({page, s 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(`empty.html`); + + 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) => { @@ -73,13 +124,7 @@ it('should await form-get on click', async ({page, server}) => { }); it('should await form-post on click', async ({page, server}) => { - const messages = []; - server.setRoute('/empty.html', async (req, res) => { - messages.push('route'); - res.setHeader('Content-Type', 'text/html'); - res.end(``); - }); - + const messages = initServer(server); await page.setContent(`
@@ -94,12 +139,7 @@ it('should await form-post on click', async ({page, server}) => { }); it('should await navigation when assigning location', async ({page, server}) => { - const messages = []; - server.setRoute('/empty.html', async (req, res) => { - messages.push('route'); - res.setHeader('Content-Type', 'text/html'); - res.end(``); - }); + 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')), @@ -120,14 +160,8 @@ it('should await navigation when assigning location twice', async ({page, server }); it('should await navigation when evaluating reload', async ({page, server}) => { - const messages = []; await page.goto(server.EMPTY_PAGE); - server.setRoute('/empty.html', async (req, res) => { - messages.push('route'); - res.setHeader('Content-Type', 'text/html'); - res.end(``); - }); - + const messages = initServer(server); await Promise.all([ page.evaluate(`window.location.reload()`).then(() => messages.push('evaluate')), page.waitForEvent('framenavigated').then(() => messages.push('navigated')), @@ -135,48 +169,21 @@ it('should await navigation when evaluating reload', async ({page, server}) => { expect(messages.join('|')).toBe('route|navigated|evaluate'); }); -it('should await navigating specified target', async ({page, server}) => { - const messages = []; - server.setRoute('/empty.html', async (req, res) => { - messages.push('route'); - res.setHeader('Content-Type', 'text/html'); - res.end(``); - }); - - await page.setContent(` - empty.html - - `); - const frame = page.frame({ name: 'target' }); - await Promise.all([ - page.click('a').then(() => messages.push('click')), - page.waitForEvent('framenavigated').then(() => messages.push('navigated')), - ]); - expect(frame.url()).toBe(server.EMPTY_PAGE); - expect(messages.join('|')).toBe('route|navigated|click'); -}); - it('should work with noWaitAfter: true', async ({page, server}) => { server.setRoute('/empty.html', async () => {}); - await page.setContent(`empty.html`); + await page.setContent(`empty.html`); await page.click('a', { noWaitAfter: true }); }); it('should work with dblclick noWaitAfter: true', async ({page, server}) => { server.setRoute('/empty.html', async () => {}); - await page.setContent(`empty.html`); + await page.setContent(`empty.html`); await page.dblclick('a', { noWaitAfter: true }); }); it('should work with waitForLoadState(load)', async ({page, server}) => { - const messages = []; - server.setRoute('/empty.html', async (req, res) => { - messages.push('route'); - res.setHeader('Content-Type', 'text/html'); - res.end(``); - }); - - await page.setContent(`empty.html`); + const messages = initServer(server); + await page.setContent(`empty.html`); await Promise.all([ page.click('a').then(() => page.waitForLoadState('load')).then(() => messages.push('clickload')), page.waitForEvent('framenavigated').then(() => page.waitForLoadState('domcontentloaded')).then(() => messages.push('domcontentloaded')),