diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index fba95fac1f..7248d151b6 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -2934,6 +2934,12 @@ await page.waitForRequest(request => request.url().searchParams.get('foo') === ' Request URL string, regex or predicate receiving [Request] object. +### param: Page.waitForRequest.urlOrPredicate +* langs: js +- `urlOrPredicate` <[string]|[RegExp]|[function]\([Request]\):[boolean]|[Promise]<[boolean]>> + +Request URL string, regex or predicate receiving [Request] object. + ### option: Page.waitForRequest.timeout - `timeout` <[float]> @@ -2990,6 +2996,12 @@ return response.ok Request URL string, regex or predicate receiving [Response] object. +### param: Page.waitForResponse.urlOrPredicate +* langs: js +- `urlOrPredicate` <[string]|[RegExp]|[function]\([Response]\):[boolean]|[Promise]<[boolean]>> + +Request URL string, regex or predicate receiving [Response] object. + ### option: Page.waitForResponse.timeout - `timeout` <[float]> diff --git a/src/client/page.ts b/src/client/page.ts index ad9fe6055a..9df2fe60af 100644 --- a/src/client/page.ts +++ b/src/client/page.ts @@ -377,7 +377,7 @@ export class Page extends ChannelOwner this._mainFrame.waitForURL(url, options)); } - async waitForRequest(urlOrPredicate: string | RegExp | ((r: Request) => boolean), options: { timeout?: number } = {}): Promise { + async waitForRequest(urlOrPredicate: string | RegExp | ((r: Request) => boolean | Promise), options: { timeout?: number } = {}): Promise { return this._wrapApiCall('page.waitForRequest', async (channel: channels.PageChannel) => { const predicate = (request: Request) => { if (isString(urlOrPredicate) || isRegExp(urlOrPredicate)) @@ -390,7 +390,7 @@ export class Page extends ChannelOwner boolean), options: { timeout?: number } = {}): Promise { + async waitForResponse(urlOrPredicate: string | RegExp | ((r: Response) => boolean | Promise), options: { timeout?: number } = {}): Promise { return this._wrapApiCall('page.waitForResponse', async (channel: channels.PageChannel) => { const predicate = (response: Response) => { if (isString(urlOrPredicate) || isRegExp(urlOrPredicate)) diff --git a/src/client/waiter.ts b/src/client/waiter.ts index 5ba59cfa08..48218b55c8 100644 --- a/src/client/waiter.ts +++ b/src/client/waiter.ts @@ -42,12 +42,12 @@ export class Waiter { return new Waiter(channelOwner, `${target}.waitForEvent(${event})`); } - async waitForEvent(emitter: EventEmitter, event: string, predicate?: (arg: T) => boolean): Promise { + async waitForEvent(emitter: EventEmitter, event: string, predicate?: (arg: T) => boolean | Promise): Promise { const { promise, dispose } = waitForEvent(emitter, event, predicate); return this.waitForPromise(promise, dispose); } - rejectOnEvent(emitter: EventEmitter, event: string, error: Error, predicate?: (arg: T) => boolean) { + rejectOnEvent(emitter: EventEmitter, event: string, error: Error, predicate?: (arg: T) => boolean | Promise) { const { promise, dispose } = waitForEvent(emitter, event, predicate); this._rejectOn(promise.then(() => { throw error; }), dispose); } @@ -92,12 +92,12 @@ export class Waiter { } } -function waitForEvent(emitter: EventEmitter, event: string, predicate?: (arg: T) => boolean): { promise: Promise, dispose: () => void } { +function waitForEvent(emitter: EventEmitter, event: string, predicate?: (arg: T) => boolean | Promise): { promise: Promise, dispose: () => void } { let listener: (eventArg: any) => void; const promise = new Promise((resolve, reject) => { - listener = (eventArg: any) => { + listener = async (eventArg: any) => { try { - if (predicate && !predicate(eventArg)) + if (predicate && !(await predicate(eventArg))) return; emitter.removeListener(event, listener); resolve(eventArg); diff --git a/tests/page-wait-for-response.spec.ts b/tests/page-wait-for-response.spec.ts index 2c352fe1f4..031b5329df 100644 --- a/tests/page-wait-for-response.spec.ts +++ b/tests/page-wait-for-response.spec.ts @@ -53,7 +53,7 @@ it('should log the url', async ({page}) => { it('should work with predicate', async ({page, server}) => { await page.goto(server.EMPTY_PAGE); const [response] = await Promise.all([ - page.waitForEvent('request', response => response.url() === server.PREFIX + '/digits/2.png'), + page.waitForEvent('response', response => response.url() === server.PREFIX + '/digits/2.png'), page.evaluate(() => { fetch('/digits/1.png'); fetch('/digits/2.png'); @@ -63,6 +63,44 @@ it('should work with predicate', async ({page, server}) => { expect(response.url()).toBe(server.PREFIX + '/digits/2.png'); }); +it('should work with async predicate', async ({page, server}) => { + await page.goto(server.EMPTY_PAGE); + const [response1, response2] = await Promise.all([ + page.waitForEvent('response', async response => { + const text = await response.text(); + return text.includes('contents of the file'); + }), + page.waitForResponse(async response => { + const text = await response.text(); + return text.includes('bar'); + }), + page.evaluate(() => { + fetch('/simple.json').then(r => r.json()); + fetch('/file-to-upload.txt').then(r => r.text()); + }) + ]); + expect(response1.url()).toBe(server.PREFIX + '/file-to-upload.txt'); + expect(response2.url()).toBe(server.PREFIX + '/simple.json'); +}); + +it('sync predicate should be only called once', async ({page, server}) => { + await page.goto(server.EMPTY_PAGE); + let counter = 0; + const [response] = await Promise.all([ + page.waitForEvent('response', response => { + ++counter; + return response.url() === server.PREFIX + '/digits/1.png'; + }), + page.evaluate(() => { + fetch('/digits/1.png'); + fetch('/digits/2.png'); + fetch('/digits/3.png'); + }) + ]); + expect(response.url()).toBe(server.PREFIX + '/digits/1.png'); + expect(counter).toBe(1); +}); + it('should work with no timeout', async ({page, server}) => { await page.goto(server.EMPTY_PAGE); const [response] = await Promise.all([ diff --git a/types/types.d.ts b/types/types.d.ts index e562c795bb..d0d23fb0aa 100644 --- a/types/types.d.ts +++ b/types/types.d.ts @@ -2875,7 +2875,7 @@ export interface Page { /** * Emitted when the page closes. */ - waitForEvent(event: 'close', optionsOrPredicate?: { predicate?: (page: Page) => boolean, timeout?: number } | ((page: Page) => boolean)): Promise; + waitForEvent(event: 'close', optionsOrPredicate?: { predicate?: (page: Page) => boolean | Promise, timeout?: number } | ((page: Page) => boolean | Promise)): Promise; /** * Emitted when JavaScript within the page calls one of console API methods, e.g. `console.log` or `console.dir`. Also @@ -2894,7 +2894,7 @@ export interface Page { * ``` * */ - waitForEvent(event: 'console', optionsOrPredicate?: { predicate?: (consoleMessage: ConsoleMessage) => boolean, timeout?: number } | ((consoleMessage: ConsoleMessage) => boolean)): Promise; + waitForEvent(event: 'console', optionsOrPredicate?: { predicate?: (consoleMessage: ConsoleMessage) => boolean | Promise, timeout?: number } | ((consoleMessage: ConsoleMessage) => boolean | Promise)): Promise; /** * Emitted when the page crashes. Browser pages might crash if they try to allocate too much memory. When the page crashes, @@ -2914,7 +2914,7 @@ export interface Page { * ``` * */ - waitForEvent(event: 'crash', optionsOrPredicate?: { predicate?: (page: Page) => boolean, timeout?: number } | ((page: Page) => boolean)): Promise; + waitForEvent(event: 'crash', optionsOrPredicate?: { predicate?: (page: Page) => boolean | Promise, timeout?: number } | ((page: Page) => boolean | Promise)): Promise; /** * Emitted when a JavaScript dialog appears, such as `alert`, `prompt`, `confirm` or `beforeunload`. Listener **must** @@ -2926,13 +2926,13 @@ export interface Page { * > NOTE: When no [page.on('dialog')](https://playwright.dev/docs/api/class-page#pageondialog) listeners are present, all * dialogs are automatically dismissed. */ - waitForEvent(event: 'dialog', optionsOrPredicate?: { predicate?: (dialog: Dialog) => boolean, timeout?: number } | ((dialog: Dialog) => boolean)): Promise; + waitForEvent(event: 'dialog', optionsOrPredicate?: { predicate?: (dialog: Dialog) => boolean | Promise, timeout?: number } | ((dialog: Dialog) => boolean | Promise)): Promise; /** * Emitted when the JavaScript [`DOMContentLoaded`](https://developer.mozilla.org/en-US/docs/Web/Events/DOMContentLoaded) * event is dispatched. */ - waitForEvent(event: 'domcontentloaded', optionsOrPredicate?: { predicate?: (page: Page) => boolean, timeout?: number } | ((page: Page) => boolean)): Promise; + waitForEvent(event: 'domcontentloaded', optionsOrPredicate?: { predicate?: (page: Page) => boolean | Promise, timeout?: number } | ((page: Page) => boolean | Promise)): Promise; /** * Emitted when attachment download started. User can access basic file operations on downloaded content via the passed @@ -2942,7 +2942,7 @@ export interface Page { * downloaded content. If `acceptDownloads` is not set, download events are emitted, but the actual download is not * performed and user has no access to the downloaded files. */ - waitForEvent(event: 'download', optionsOrPredicate?: { predicate?: (download: Download) => boolean, timeout?: number } | ((download: Download) => boolean)): Promise; + waitForEvent(event: 'download', optionsOrPredicate?: { predicate?: (download: Download) => boolean | Promise, timeout?: number } | ((download: Download) => boolean | Promise)): Promise; /** * Emitted when a file chooser is supposed to appear, such as after clicking the ``. Playwright can @@ -2957,32 +2957,32 @@ export interface Page { * ``` * */ - waitForEvent(event: 'filechooser', optionsOrPredicate?: { predicate?: (fileChooser: FileChooser) => boolean, timeout?: number } | ((fileChooser: FileChooser) => boolean)): Promise; + waitForEvent(event: 'filechooser', optionsOrPredicate?: { predicate?: (fileChooser: FileChooser) => boolean | Promise, timeout?: number } | ((fileChooser: FileChooser) => boolean | Promise)): Promise; /** * Emitted when a frame is attached. */ - waitForEvent(event: 'frameattached', optionsOrPredicate?: { predicate?: (frame: Frame) => boolean, timeout?: number } | ((frame: Frame) => boolean)): Promise; + waitForEvent(event: 'frameattached', optionsOrPredicate?: { predicate?: (frame: Frame) => boolean | Promise, timeout?: number } | ((frame: Frame) => boolean | Promise)): Promise; /** * Emitted when a frame is detached. */ - waitForEvent(event: 'framedetached', optionsOrPredicate?: { predicate?: (frame: Frame) => boolean, timeout?: number } | ((frame: Frame) => boolean)): Promise; + waitForEvent(event: 'framedetached', optionsOrPredicate?: { predicate?: (frame: Frame) => boolean | Promise, timeout?: number } | ((frame: Frame) => boolean | Promise)): Promise; /** * Emitted when a frame is navigated to a new url. */ - waitForEvent(event: 'framenavigated', optionsOrPredicate?: { predicate?: (frame: Frame) => boolean, timeout?: number } | ((frame: Frame) => boolean)): Promise; + waitForEvent(event: 'framenavigated', optionsOrPredicate?: { predicate?: (frame: Frame) => boolean | Promise, timeout?: number } | ((frame: Frame) => boolean | Promise)): Promise; /** * Emitted when the JavaScript [`load`](https://developer.mozilla.org/en-US/docs/Web/Events/load) event is dispatched. */ - waitForEvent(event: 'load', optionsOrPredicate?: { predicate?: (page: Page) => boolean, timeout?: number } | ((page: Page) => boolean)): Promise; + waitForEvent(event: 'load', optionsOrPredicate?: { predicate?: (page: Page) => boolean | Promise, timeout?: number } | ((page: Page) => boolean | Promise)): Promise; /** * Emitted when an uncaught exception happens within the page. */ - waitForEvent(event: 'pageerror', optionsOrPredicate?: { predicate?: (error: Error) => boolean, timeout?: number } | ((error: Error) => boolean)): Promise; + waitForEvent(event: 'pageerror', optionsOrPredicate?: { predicate?: (error: Error) => boolean | Promise, timeout?: number } | ((error: Error) => boolean | Promise)): Promise; /** * Emitted when the page opens a new tab or window. This event is emitted in addition to the @@ -3005,14 +3005,14 @@ export interface Page { * [page.waitForLoadState([state, options])](https://playwright.dev/docs/api/class-page#pagewaitforloadstatestate-options) * to wait until the page gets to a particular state (you should not need it in most cases). */ - waitForEvent(event: 'popup', optionsOrPredicate?: { predicate?: (page: Page) => boolean, timeout?: number } | ((page: Page) => boolean)): Promise; + waitForEvent(event: 'popup', optionsOrPredicate?: { predicate?: (page: Page) => boolean | Promise, timeout?: number } | ((page: Page) => boolean | Promise)): Promise; /** * Emitted when a page issues a request. The [request] object is read-only. In order to intercept and mutate requests, see * [page.route(url, handler)](https://playwright.dev/docs/api/class-page#pagerouteurl-handler) or * [browserContext.route(url, handler)](https://playwright.dev/docs/api/class-browsercontext#browsercontextrouteurl-handler). */ - waitForEvent(event: 'request', optionsOrPredicate?: { predicate?: (request: Request) => boolean, timeout?: number } | ((request: Request) => boolean)): Promise; + waitForEvent(event: 'request', optionsOrPredicate?: { predicate?: (request: Request) => boolean | Promise, timeout?: number } | ((request: Request) => boolean | Promise)): Promise; /** * Emitted when a request fails, for example by timing out. @@ -3021,30 +3021,30 @@ export interface Page { * complete with [page.on('requestfinished')](https://playwright.dev/docs/api/class-page#pageonrequestfinished) event and * not with [page.on('requestfailed')](https://playwright.dev/docs/api/class-page#pageonrequestfailed). */ - waitForEvent(event: 'requestfailed', optionsOrPredicate?: { predicate?: (request: Request) => boolean, timeout?: number } | ((request: Request) => boolean)): Promise; + waitForEvent(event: 'requestfailed', optionsOrPredicate?: { predicate?: (request: Request) => boolean | Promise, timeout?: number } | ((request: Request) => boolean | Promise)): Promise; /** * Emitted when a request finishes successfully after downloading the response body. For a successful response, the * sequence of events is `request`, `response` and `requestfinished`. */ - waitForEvent(event: 'requestfinished', optionsOrPredicate?: { predicate?: (request: Request) => boolean, timeout?: number } | ((request: Request) => boolean)): Promise; + waitForEvent(event: 'requestfinished', optionsOrPredicate?: { predicate?: (request: Request) => boolean | Promise, timeout?: number } | ((request: Request) => boolean | Promise)): Promise; /** * Emitted when [response] status and headers are received for a request. For a successful response, the sequence of events * is `request`, `response` and `requestfinished`. */ - waitForEvent(event: 'response', optionsOrPredicate?: { predicate?: (response: Response) => boolean, timeout?: number } | ((response: Response) => boolean)): Promise; + waitForEvent(event: 'response', optionsOrPredicate?: { predicate?: (response: Response) => boolean | Promise, timeout?: number } | ((response: Response) => boolean | Promise)): Promise; /** * Emitted when [WebSocket] request is sent. */ - waitForEvent(event: 'websocket', optionsOrPredicate?: { predicate?: (webSocket: WebSocket) => boolean, timeout?: number } | ((webSocket: WebSocket) => boolean)): Promise; + waitForEvent(event: 'websocket', optionsOrPredicate?: { predicate?: (webSocket: WebSocket) => boolean | Promise, timeout?: number } | ((webSocket: WebSocket) => boolean | Promise)): Promise; /** * Emitted when a dedicated [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) is spawned by the * page. */ - waitForEvent(event: 'worker', optionsOrPredicate?: { predicate?: (worker: Worker) => boolean, timeout?: number } | ((worker: Worker) => boolean)): Promise; + waitForEvent(event: 'worker', optionsOrPredicate?: { predicate?: (worker: Worker) => boolean | Promise, timeout?: number } | ((worker: Worker) => boolean | Promise)): Promise; /** @@ -3151,7 +3151,7 @@ export interface Page { * @param urlOrPredicate Request URL string, regex or predicate receiving [Request] object. * @param options */ - waitForRequest(urlOrPredicate: string|RegExp|((request: Request) => boolean), options?: { + waitForRequest(urlOrPredicate: string|RegExp|((request: Request) => boolean|Promise), options?: { /** * Maximum wait time in milliseconds, defaults to 30 seconds, pass `0` to disable the timeout. The default value can be * changed by using the @@ -3172,7 +3172,7 @@ export interface Page { * @param urlOrPredicate Request URL string, regex or predicate receiving [Response] object. * @param options */ - waitForResponse(urlOrPredicate: string|RegExp|((response: Response) => boolean), options?: { + waitForResponse(urlOrPredicate: string|RegExp|((response: Response) => boolean|Promise), options?: { /** * Maximum wait time in milliseconds, defaults to 30 seconds, pass `0` to disable the timeout. The default value can be * changed by using the @@ -5333,7 +5333,7 @@ export interface BrowserContext { * ``` * */ - waitForEvent(event: 'backgroundpage', optionsOrPredicate?: { predicate?: (page: Page) => boolean, timeout?: number } | ((page: Page) => boolean)): Promise; + waitForEvent(event: 'backgroundpage', optionsOrPredicate?: { predicate?: (page: Page) => boolean | Promise, timeout?: number } | ((page: Page) => boolean | Promise)): Promise; /** * Emitted when Browser context gets closed. This might happen because of one of the following: @@ -5341,7 +5341,7 @@ export interface BrowserContext { * - Browser application is closed or crashed. * - The [browser.close()](https://playwright.dev/docs/api/class-browser#browserclose) method was called. */ - waitForEvent(event: 'close', optionsOrPredicate?: { predicate?: (browserContext: BrowserContext) => boolean, timeout?: number } | ((browserContext: BrowserContext) => boolean)): Promise; + waitForEvent(event: 'close', optionsOrPredicate?: { predicate?: (browserContext: BrowserContext) => boolean | Promise, timeout?: number } | ((browserContext: BrowserContext) => boolean | Promise)): Promise; /** * The event is emitted when a new Page is created in the BrowserContext. The page may still be loading. The event will @@ -5364,14 +5364,14 @@ export interface BrowserContext { * [page.waitForLoadState([state, options])](https://playwright.dev/docs/api/class-page#pagewaitforloadstatestate-options) * to wait until the page gets to a particular state (you should not need it in most cases). */ - waitForEvent(event: 'page', optionsOrPredicate?: { predicate?: (page: Page) => boolean, timeout?: number } | ((page: Page) => boolean)): Promise; + waitForEvent(event: 'page', optionsOrPredicate?: { predicate?: (page: Page) => boolean | Promise, timeout?: number } | ((page: Page) => boolean | Promise)): Promise; /** * > NOTE: Service workers are only supported on Chromium-based browsers. * * Emitted when new service worker is created in the context. */ - waitForEvent(event: 'serviceworker', optionsOrPredicate?: { predicate?: (worker: Worker) => boolean, timeout?: number } | ((worker: Worker) => boolean)): Promise; + waitForEvent(event: 'serviceworker', optionsOrPredicate?: { predicate?: (worker: Worker) => boolean | Promise, timeout?: number } | ((worker: Worker) => boolean | Promise)): Promise; } /** @@ -7369,13 +7369,13 @@ export interface ElectronApplication { /** * This event is issued when the application closes. */ - waitForEvent(event: 'close', optionsOrPredicate?: { predicate?: () => boolean, timeout?: number } | (() => boolean)): Promise; + waitForEvent(event: 'close', optionsOrPredicate?: { predicate?: () => boolean | Promise, timeout?: number } | (() => boolean | Promise)): Promise; /** * This event is issued for every window that is created **and loaded** in Electron. It contains a [Page] that can be used * for Playwright automation. */ - waitForEvent(event: 'window', optionsOrPredicate?: { predicate?: (page: Page) => boolean, timeout?: number } | ((page: Page) => boolean)): Promise; + waitForEvent(event: 'window', optionsOrPredicate?: { predicate?: (page: Page) => boolean | Promise, timeout?: number } | ((page: Page) => boolean | Promise)): Promise; /** @@ -8108,7 +8108,7 @@ export interface AndroidDevice { /** * Emitted when a new WebView instance is detected. */ - waitForEvent(event: 'webview', optionsOrPredicate?: { predicate?: (androidWebView: AndroidWebView) => boolean, timeout?: number } | ((androidWebView: AndroidWebView) => boolean)): Promise; + waitForEvent(event: 'webview', optionsOrPredicate?: { predicate?: (androidWebView: AndroidWebView) => boolean | Promise, timeout?: number } | ((androidWebView: AndroidWebView) => boolean | Promise)): Promise; /** @@ -10249,7 +10249,7 @@ export interface WebSocket { /** * Fired when the websocket closes. */ - waitForEvent(event: 'close', optionsOrPredicate?: { predicate?: (webSocket: WebSocket) => boolean, timeout?: number } | ((webSocket: WebSocket) => boolean)): Promise; + waitForEvent(event: 'close', optionsOrPredicate?: { predicate?: (webSocket: WebSocket) => boolean | Promise, timeout?: number } | ((webSocket: WebSocket) => boolean | Promise)): Promise; /** * Fired when the websocket receives a frame. @@ -10259,12 +10259,12 @@ export interface WebSocket { * frame payload */ payload: string|Buffer; -}) => boolean, timeout?: number } | ((data: { +}) => boolean | Promise, timeout?: number } | ((data: { /** * frame payload */ payload: string|Buffer; -}) => boolean)): Promise<{ +}) => boolean | Promise)): Promise<{ /** * frame payload */ @@ -10279,12 +10279,12 @@ export interface WebSocket { * frame payload */ payload: string|Buffer; -}) => boolean, timeout?: number } | ((data: { +}) => boolean | Promise, timeout?: number } | ((data: { /** * frame payload */ payload: string|Buffer; -}) => boolean)): Promise<{ +}) => boolean | Promise)): Promise<{ /** * frame payload */ @@ -10294,7 +10294,7 @@ export interface WebSocket { /** * Fired when the websocket has an error. */ - waitForEvent(event: 'socketerror', optionsOrPredicate?: { predicate?: (string: String) => boolean, timeout?: number } | ((string: String) => boolean)): Promise; + waitForEvent(event: 'socketerror', optionsOrPredicate?: { predicate?: (string: String) => boolean | Promise, timeout?: number } | ((string: String) => boolean | Promise)): Promise; } diff --git a/utils/generate_types/index.js b/utils/generate_types/index.js index 6e63134c0a..fa47471a92 100644 --- a/utils/generate_types/index.js +++ b/utils/generate_types/index.js @@ -228,7 +228,7 @@ function classBody(classDesc) { for (const {eventName, params, comment, type} of eventDescriptions) { if (comment) parts.push(writeComment(comment, ' ')); - parts.push(` ${member.alias}(event: '${eventName}', optionsOrPredicate?: { predicate?: (${params}) => boolean, timeout?: number } | ((${params}) => boolean)): Promise<${type}>;\n`); + parts.push(` ${member.alias}(event: '${eventName}', optionsOrPredicate?: { predicate?: (${params}) => boolean | Promise, timeout?: number } | ((${params}) => boolean | Promise)): Promise<${type}>;\n`); } return parts.join('\n');