From 7fab923b02996bd60aa382582a99ff817767c717 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 5 Mar 2020 09:23:07 -0800 Subject: [PATCH] api: waitForElement accepts waitFor: attached|detached|visible|hidden This includes rename waitForSelector -> waitForElement and removes $wait. --- docs/api.md | 179 ++++++++++++++---------------------- src/dom.ts | 25 +++-- src/frames.ts | 42 ++++----- src/page.ts | 10 +- src/types.ts | 2 +- test/chromium/oopif.spec.js | 2 +- test/launcher.spec.js | 8 +- test/queryselector.spec.js | 11 --- test/waittask.spec.js | 120 +++++++++++++----------- 9 files changed, 174 insertions(+), 225 deletions(-) diff --git a/docs/api.md b/docs/api.md index a86c0f32fc4d7..ee41958aec554 100644 --- a/docs/api.md +++ b/docs/api.md @@ -94,7 +94,7 @@ const iPhone = devices['iPhone 6']; - returns: <[Object]> - `TimeoutError` <[function]> A class of [TimeoutError]. -Playwright methods might throw errors if they are unable to fulfill a request. For example, [page.waitForSelector(selector[, options])](#pagewaitforselectorselector-options) +Playwright methods might throw errors if they are unable to fulfill a request. For example, [page.waitForElement(selector[, options])](#pagewaitforelementselector-options) might fail if the selector doesn't match any nodes during the given timeframe. For certain types of errors Playwright uses specific error classes. @@ -103,7 +103,7 @@ These classes are available via [`browserType.errors`](#browsertypeerrors) or [` An example of handling a timeout error: ```js try { - await page.waitForSelector('.foo'); + await page.waitForElement('.foo'); } catch (e) { if (e instanceof playwright.errors.TimeoutError) { // Do something if this is a timeout. @@ -620,7 +620,6 @@ page.removeListener('request', logRequest); - [page.$$(selector)](#pageselector-1) - [page.$$eval(selector, pageFunction[, ...args])](#pageevalselector-pagefunction-args) - [page.$eval(selector, pageFunction[, ...args])](#pageevalselector-pagefunction-args-1) -- [page.$wait(selector[, options])](#pagewaitselector-options) - [page.accessibility](#pageaccessibility) - [page.addInitScript(script[, ...args])](#pageaddinitscriptscript-args) - [page.addScriptTag(options)](#pageaddscripttagoptions) @@ -667,13 +666,13 @@ page.removeListener('request', logRequest); - [page.url()](#pageurl) - [page.viewportSize()](#pageviewportsize) - [page.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])](#pagewaitforselectororfunctionortimeout-options-args) +- [page.waitForElement(selector[, options])](#pagewaitforelementselector-options) - [page.waitForEvent(event[, optionsOrPredicate])](#pagewaitforeventevent-optionsorpredicate) - [page.waitForFunction(pageFunction[, options[, ...args]])](#pagewaitforfunctionpagefunction-options-args) - [page.waitForLoadState([options])](#pagewaitforloadstateoptions) - [page.waitForNavigation([options])](#pagewaitfornavigationoptions) - [page.waitForRequest(urlOrPredicate[, options])](#pagewaitforrequesturlorpredicate-options) - [page.waitForResponse(urlOrPredicate[, options])](#pagewaitforresponseurlorpredicate-options) -- [page.waitForSelector(selector[, options])](#pagewaitforselectorselector-options) - [page.workers()](#pageworkers) @@ -842,25 +841,6 @@ const html = await page.$eval('.main-container', e => e.outerHTML); Shortcut for [page.mainFrame().$eval(selector, pageFunction)](#frameevalselector-pagefunction-args). -#### page.$wait(selector[, options]) -- `selector` <[string]> A selector of an element to wait for -- `options` <[Object]> - - `visibility` <"visible"|"hidden"|"any"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`). Defaults to `any`. - - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. -- returns: <[Promise]> Promise which resolves when element specified by selector string is added to DOM. Resolves to `null` if waiting for `hidden: true` and selector is not found in DOM. - -Wait for the `selector` to appear in page. If at the moment of calling -the method the `selector` already exists, the method will return -immediately. If the selector doesn't appear after the `timeout` milliseconds of waiting, the function will throw. - -This method works across navigations: -```js -const handle = await page.$wait(selector); -await handle.click(); -``` - -This is a shortcut to [page.waitForSelector(selector[, options])](#pagewaitforselectorselector-options). - #### page.accessibility - returns: <[Accessibility]> @@ -1585,7 +1565,7 @@ This is a shortcut for [page.mainFrame().url()](#frameurl) #### page.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]]) - `selectorOrFunctionOrTimeout` <[string]|[number]|[function]> A [selector], predicate or timeout to wait for - `options` <[Object]> Optional waiting parameters - - `visibility` <"visible"|"hidden"|"any"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`). Defaults to `any`. + - `waitFor` <"attached"|"detached"|"visible"|"hidden"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`attached`) or not present in dom (`detached`). Defaults to `attached`. - `polling` <[number]|"raf"|"mutation"> An interval at which the `pageFunction` is executed, defaults to `raf`. If `polling` is a number, then it is treated as an interval in milliseconds at which the function would be executed. If `polling` is a string, then it can be one of the following values: - `'raf'` - to constantly execute `pageFunction` in `requestAnimationFrame` callback. This is the tightest polling mode which is suitable to observe styling changes. - `'mutation'` - to execute `pageFunction` on every DOM mutation. @@ -1594,7 +1574,7 @@ This is a shortcut for [page.mainFrame().url()](#frameurl) - returns: <[Promise]<[JSHandle]>> Promise which resolves to a JSHandle of the success value This method behaves differently with respect to the type of the first parameter: -- if `selectorOrFunctionOrTimeout` is a `string`, then the first argument is treated as a [selector] and the method is a shortcut for [page.waitForSelector](#pagewaitforselectorselector-options) +- if `selectorOrFunctionOrTimeout` is a `string`, then the first argument is treated as a [selector] and the method is a shortcut for [page.waitForElement](#pagewaitforelementselector-options) - if `selectorOrFunctionOrTimeout` is a `function`, then the first argument is treated as a predicate to wait for and the method is a shortcut for [page.waitForFunction()](#pagewaitforfunctionpagefunction-options-args). - if `selectorOrFunctionOrTimeout` is a `number`, then the first argument is treated as a timeout in milliseconds and the method returns a promise which resolves after the timeout - otherwise, an exception is thrown @@ -1615,7 +1595,35 @@ const selector = '.foo'; await page.waitFor(selector => !!document.querySelector(selector), {}, selector); ``` -Shortcut for [page.mainFrame().waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])](#framewaitforselectororfunctionortimeout-options-args). +Shortcut for [page.mainFrame().waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])](#framewaitforelementorfunctionortimeout-options-args). + +#### page.waitForElement(selector[, options]) +- `selector` <[string]> A selector of an element to wait for +- `options` <[Object]> + - `waitFor` <"attached"|"detached"|"visible"|"hidden"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`attached`) or not present in dom (`detached`). Defaults to `attached`. + - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. +- returns: <[Promise]> Promise which resolves when element specified by selector satisfies `waitFor` option. Resolves to `null` if waiting for `hidden` or `detached`. + +Wait for the `selector` to satisfy `waitFor` option (either appear/disappear from dom, or become visible/hidden). If at the moment of calling the method `selector` already satisfies the condition, the method will return immediately. If the selector doesn't satisfy the condition for the `timeout` milliseconds, the function will throw. + +This method works across navigations: +```js +const { chromium } = require('playwright'); // Or 'firefox' or 'webkit'. + +(async () => { + const browser = await chromium.launch(); + const page = await browser.newPage(); + let currentURL; + page + .waitForElement('img') + .then(() => console.log('First URL with image: ' + currentURL)); + for (currentURL of ['https://example.com', 'https://google.com', 'https://bbc.com']) { + await page.goto(currentURL); + } + await browser.close(); +})(); +``` +Shortcut for [page.mainFrame().waitForElement(selector[, options])](#framewaitforelementselector-options). #### page.waitForEvent(event[, optionsOrPredicate]) - `event` <[string]> Event name, same one would pass into `page.on(event)`. @@ -1733,36 +1741,6 @@ const finalResponse = await page.waitForResponse(response => response.url() === return finalResponse.ok(); ``` -#### page.waitForSelector(selector[, options]) -- `selector` <[string]> A selector of an element to wait for -- `options` <[Object]> - - `visibility` <"visible"|"hidden"|"any"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`). Defaults to `any`. - - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. -- returns: <[Promise]> Promise which resolves when element specified by selector string is added to DOM. Resolves to `null` if waiting for `hidden: true` and selector is not found in DOM. - -Wait for the `selector` to appear in page. If at the moment of calling -the method the `selector` already exists, the method will return -immediately. If the selector doesn't appear after the `timeout` milliseconds of waiting, the function will throw. - -This method works across navigations: -```js -const { chromium } = require('playwright'); // Or 'firefox' or 'webkit'. - -(async () => { - const browser = await chromium.launch(); - const page = await browser.newPage(); - let currentURL; - page - .waitForSelector('img') - .then(() => console.log('First URL with image: ' + currentURL)); - for (currentURL of ['https://example.com', 'https://google.com', 'https://bbc.com']) { - await page.goto(currentURL); - } - await browser.close(); -})(); -``` -Shortcut for [page.mainFrame().waitForSelector(selector[, options])](#framewaitforselectorselector-options). - #### page.workers() - returns: <[Array]<[Worker]>> This method returns all of the dedicated [WebWorkers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) associated with the page. @@ -1819,7 +1797,6 @@ An example of getting text from an iframe element: - [frame.$$(selector)](#frameselector-1) - [frame.$$eval(selector, pageFunction[, ...args])](#frameevalselector-pagefunction-args) - [frame.$eval(selector, pageFunction[, ...args])](#frameevalselector-pagefunction-args-1) -- [frame.$wait(selector[, options])](#framewaitselector-options) - [frame.addScriptTag(options)](#frameaddscripttagoptions) - [frame.addStyleTag(options)](#frameaddstyletagoptions) - [frame.check(selector, [options])](#framecheckselector-options) @@ -1845,10 +1822,10 @@ An example of getting text from an iframe element: - [frame.uncheck(selector, [options])](#frameuncheckselector-options) - [frame.url()](#frameurl) - [frame.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])](#framewaitforselectororfunctionortimeout-options-args) +- [frame.waitForElement(selector[, options])](#framewaitforelementselector-options) - [frame.waitForFunction(pageFunction[, options[, ...args]])](#framewaitforfunctionpagefunction-options-args) - [frame.waitForLoadState([options])](#framewaitforloadstateoptions) - [frame.waitForNavigation([options])](#framewaitfornavigationoptions) -- [frame.waitForSelector(selector[, options])](#framewaitforselectorselector-options) #### frame.$(selector) @@ -1895,25 +1872,6 @@ const preloadHref = await frame.$eval('link[rel=preload]', el => el.href); const html = await frame.$eval('.main-container', e => e.outerHTML); ``` -#### frame.$wait(selector[, options]) -- `selector` <[string]> A selector of an element to wait for -- `options` <[Object]> - - `visibility` <"visible"|"hidden"|"any"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`). Defaults to `any`. - - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. -- returns: <[Promise]> Promise which resolves when element specified by selector string is added to DOM. Resolves to `null` if waiting for `hidden: true` and selector is not found in DOM. - -Wait for the `selector` to appear in page. If at the moment of calling -the method the `selector` already exists, the method will return -immediately. If the selector doesn't appear after the `timeout` milliseconds of waiting, the function will throw. - -This method works across navigations: -```js -const handle = await page.$wait(selector); -await handle.click(); -``` - -This is a shortcut to [frame.waitForSelector(selector[, options])](#framewaitforselectorselector-options). - #### frame.addScriptTag(options) - `options` <[Object]> - `url` <[string]> URL of a script to be added. @@ -2243,7 +2201,7 @@ Returns frame's url. #### frame.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]]) - `selectorOrFunctionOrTimeout` <[string]|[number]|[function]> A [selector], predicate or timeout to wait for - `options` <[Object]> Optional waiting parameters - - `visibility` <"visible"|"hidden"|"any"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`). Defaults to `any`. + - `waitFor` <"attached"|"detached"|"visible"|"hidden"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`attached`) or not present in dom (`detached`). Defaults to `attached`. - `polling` <[number]|"raf"|"mutation"> An interval at which the `pageFunction` is executed, defaults to `raf`. If `polling` is a number, then it is treated as an interval in milliseconds at which the function would be executed. If `polling` is a string, then it can be one of the following values: - `'raf'` - to constantly execute `pageFunction` in `requestAnimationFrame` callback. This is the tightest polling mode which is suitable to observe styling changes. - `'mutation'` - to execute `pageFunction` on every DOM mutation. @@ -2252,7 +2210,7 @@ Returns frame's url. - returns: <[Promise]<[JSHandle]>> Promise which resolves to a JSHandle of the success value This method behaves differently with respect to the type of the first parameter: -- if `selectorOrFunctionOrTimeout` is a `string`, then the first argument is treated as a [selector] and the method is a shortcut for [frame.waitForSelector](#framewaitforselectorselector-options) +- if `selectorOrFunctionOrTimeout` is a `string`, then the first argument is treated as a [selector] and the method is a shortcut for [frame.waitForElement](#framewaitforelementselector-options) - if `selectorOrFunctionOrTimeout` is a `function`, then the first argument is treated as a predicate to wait for and the method is a shortcut for [frame.waitForFunction()](#framewaitforfunctionpagefunction-options-args). - if `selectorOrFunctionOrTimeout` is a `number`, then the first argument is treated as a timeout in milliseconds and the method returns a promise which resolves after the timeout - otherwise, an exception is thrown @@ -2273,6 +2231,33 @@ const selector = '.foo'; await page.waitFor(selector => !!document.querySelector(selector), {}, selector); ``` +#### frame.waitForElement(selector[, options]) +- `selector` <[string]> A selector of an element to wait for +- `options` <[Object]> + - `waitFor` <"attached"|"detached"|"visible"|"hidden"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`attached`) or not present in dom (`detached`). Defaults to `attached`. + - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. +- returns: <[Promise]> Promise which resolves when element specified by selector satisfies `waitFor` option. Resolves to `null` if waiting for `hidden` or `detached`. + +Wait for the `selector` to satisfy `waitFor` option (either appear/disappear from dom, or become visible/hidden). If at the moment of calling the method `selector` already satisfies the condition, the method will return immediately. If the selector doesn't satisfy the condition for the `timeout` milliseconds, the function will throw. + +This method works across navigations: +```js +const { webkit } = require('playwright'); // Or 'chromium' or 'firefox'. + +(async () => { + const browser = await webkit.launch(); + const page = await browser.newPage(); + let currentURL; + page.mainFrame() + .waitForElement('img') + .then(() => console.log('First URL with image: ' + currentURL)); + for (currentURL of ['https://example.com', 'https://google.com', 'https://bbc.com']) { + await page.goto(currentURL); + } + await browser.close(); +})(); +``` + #### frame.waitForFunction(pageFunction[, options[, ...args]]) - `pageFunction` <[function]|[string]> Function to be evaluated in browser context - `options` <[Object]> Optional waiting parameters @@ -2346,36 +2331,6 @@ const [response] = await Promise.all([ **NOTE** Usage of the [History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API) to change the URL is considered a navigation. -#### frame.waitForSelector(selector[, options]) -- `selector` <[string]> A selector of an element to wait for -- `options` <[Object]> - - `visibility` <"visible"|"hidden"|"any"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`). Defaults to `any`. - - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. -- returns: <[Promise]> Promise which resolves when element specified by selector string is added to DOM. Resolves to `null` if waiting for `hidden: true` and selector is not found in DOM. - -Wait for the `selector` to appear in page. If at the moment of calling -the method the `selector` already exists, the method will return -immediately. If the selector doesn't appear after the `timeout` milliseconds of waiting, the function will throw. - -This method works across navigations: -```js -const { webkit } = require('playwright'); // Or 'chromium' or 'firefox'. - -(async () => { - const browser = await webkit.launch(); - const page = await browser.newPage(); - let currentURL; - page.mainFrame() - .waitForSelector('img') - .then(() => console.log('First URL with image: ' + currentURL)); - for (currentURL of ['https://example.com', 'https://google.com', 'https://bbc.com']) { - await page.goto(currentURL); - } - await browser.close(); -})(); -``` - - ### class: ElementHandle * extends: [JSHandle] @@ -3322,7 +3277,7 @@ const { selectors, firefox } = require('playwright'); // Or 'chromium' or 'webk * extends: [Error] -TimeoutError is emitted whenever certain operations are terminated due to timeout, e.g. [page.waitForSelector(selector[, options])](#pagewaitforselectorselector-options) or [browserType.launch([options])](#browsertypelaunchoptions). +TimeoutError is emitted whenever certain operations are terminated due to timeout, e.g. [page.waitForElement(selector[, options])](#pagewaitforelementselector-options) or [browserType.launch([options])](#browsertypelaunchoptions). ### class: Accessibility @@ -3550,7 +3505,7 @@ Download browser binary if it is missing. - returns: <[Object]> - `TimeoutError` <[function]> A class of [TimeoutError]. -Playwright methods might throw errors if they are unable to fulfill a request. For example, [page.waitForSelector(selector[, options])](#pagewaitforselectorselector-options) +Playwright methods might throw errors if they are unable to fulfill a request. For example, [page.waitForElement(selector[, options])](#pagewaitforelementselector-options) might fail if the selector doesn't match any nodes during the given timeframe. For certain types of errors Playwright uses specific error classes. @@ -3560,7 +3515,7 @@ An example of handling a timeout error: ```js const { webkit } = require('playwright'); // Or 'chromium' or 'firefox'. try { - await page.waitForSelector('.foo'); + await page.waitForElement('.foo'); } catch (e) { if (e instanceof webkit.errors.TimeoutError) { // Do something if this is a timeout. diff --git a/src/dom.ts b/src/dom.ts index 2f24076b0221d..818592723f98e 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -82,7 +82,7 @@ export class FrameExecutionContext extends js.ExecutionContext { return super._createHandle(remoteObject); } - _injected(): Promise { + _injected(): Promise> { const selectors = Selectors._instance(); if (this._injectedPromise && selectors._generation !== this._injectedGeneration) { this._injectedPromise.then(handle => handle.dispose()); @@ -456,17 +456,22 @@ export function waitForFunctionTask(selector: string | undefined, pageFunction: }, await context._injected(), selector, predicateBody, polling, options.timeout || 0, ...args); } -export function waitForSelectorTask(selector: string, visibility: types.Visibility, timeout: number): Task { - return async (context: FrameExecutionContext) => context.evaluateHandle((injected: Injected, selector: string, visibility: types.Visibility, timeout: number) => { - const polling = visibility === 'any' ? 'mutation' : 'raf'; +export function waitForSelectorTask(selector: string, waitFor: 'attached' | 'detached' | 'visible' | 'hidden', timeout: number): Task { + return async (context: FrameExecutionContext) => context.evaluateHandle((injected, selector, waitFor, timeout) => { + const polling = (waitFor === 'attached' || waitFor === 'detached') ? 'mutation' : 'raf'; return injected.poll(polling, selector, timeout, (element: Element | undefined): Element | boolean => { - if (!element) - return visibility === 'hidden'; - if (visibility === 'any') - return element; - return injected.isVisible(element) === (visibility === 'visible') ? element : false; + switch (waitFor) { + case 'attached': + return element || false; + case 'detached': + return !element; + case 'visible': + return element && injected.isVisible(element) ? element : false; + case 'hidden': + return !element || !injected.isVisible(element); + } }); - }, await context._injected(), selector, visibility, timeout); + }, await context._injected(), selector, waitFor, timeout); } export const setFileInputFunction = async (element: HTMLInputElement, payloads: types.FilePayload[]) => { diff --git a/src/frames.ts b/src/frames.ts index a12612ef9f3f0..49cd424763701 100644 --- a/src/frames.ts +++ b/src/frames.ts @@ -589,9 +589,10 @@ export class Frame { return handle; } - async waitForSelector(selector: string, options?: types.TimeoutOptions & { visibility?: types.Visibility }): Promise | null> { - const { timeout = this._page._timeoutSettings.timeout(), visibility = 'any' } = (options || {}); - const handle = await this._waitForSelectorInUtilityContext(selector, visibility, timeout); + async waitForElement(selector: string, options?: types.WaitForElementOptions): Promise | null> { + if (options && (options as any).visibility) + throw new Error('options.visibility is not supported, did you mean options.waitFor?'); + const handle = await this._waitForSelectorInUtilityContext(selector, options); const mainContext = await this._mainContext(); if (handle && handle._context !== mainContext) { const adopted = this._page._delegate.adoptElementHandle(handle, mainContext); @@ -601,10 +602,6 @@ export class Frame { return handle; } - async $wait(selector: string, options?: types.TimeoutOptions & { visibility?: types.Visibility }): Promise | null> { - return this.waitForSelector(selector, options); - } - $eval: types.$Eval = async (selector, pageFunction, ...args) => { const context = await this._mainContext(); const elementHandle = await context._$(selector); @@ -875,9 +872,9 @@ export class Frame { handle.dispose(); } - async waitFor(selectorOrFunctionOrTimeout: (string | number | Function), options: types.WaitForFunctionOptions & { visibility?: types.Visibility } = {}, ...args: any[]): Promise { + async waitFor(selectorOrFunctionOrTimeout: (string | number | Function), options: types.WaitForFunctionOptions & types.WaitForElementOptions = {}, ...args: any[]): Promise { if (helper.isString(selectorOrFunctionOrTimeout)) - return this.waitForSelector(selectorOrFunctionOrTimeout, options) as any; + return this.waitForElement(selectorOrFunctionOrTimeout, options) as any; if (helper.isNumber(selectorOrFunctionOrTimeout)) return new Promise(fulfill => setTimeout(fulfill, selectorOrFunctionOrTimeout)); if (typeof selectorOrFunctionOrTimeout === 'function') @@ -891,9 +888,9 @@ export class Frame { throw new Error('waitFor option should be a boolean, got "' + (typeof waitFor) + '"'); let handle: dom.ElementHandle; if (waitFor) { - const maybeHandle = await this._waitForSelectorInUtilityContext(selector, 'any', timeout); + const maybeHandle = await this._waitForSelectorInUtilityContext(selector, { timeout, waitFor: 'attached' }); if (!maybeHandle) - throw new Error('No node found for selector: ' + selectorToString(selector, 'any')); + throw new Error('No node found for selector: ' + selectorToString(selector, 'attached')); handle = maybeHandle; } else { const context = await this._context('utility'); @@ -904,14 +901,12 @@ export class Frame { return handle; } - private async _waitForSelectorInUtilityContext(selector: string, waitFor: types.Visibility, timeout: number): Promise | null> { - let visibility: types.Visibility = 'any'; - if (waitFor === 'visible' || waitFor === 'hidden' || waitFor === 'any') - visibility = waitFor; - else - throw new Error(`Unsupported visibility option "${waitFor}"`); - const task = dom.waitForSelectorTask(selector, visibility, timeout); - const result = await this._scheduleRerunnableTask(task, 'utility', timeout, `selector "${selectorToString(selector, visibility)}"`); + private async _waitForSelectorInUtilityContext(selector: string, options?: types.WaitForElementOptions): Promise | null> { + const { timeout = this._page._timeoutSettings.timeout(), waitFor = 'attached' } = (options || {}); + if (!['attached', 'detached', 'visible', 'hidden'].includes(waitFor)) + throw new Error(`Unsupported waitFor option "${waitFor}"`); + const task = dom.waitForSelectorTask(selector, waitFor, timeout); + const result = await this._scheduleRerunnableTask(task, 'utility', timeout, `selector "${selectorToString(selector, waitFor)}"`); if (!result.asElement()) { result.dispose(); return null; @@ -1095,14 +1090,13 @@ function createTimeoutPromise(timeout: number): Disposable }; } -function selectorToString(selector: string, visibility: types.Visibility): string { +function selectorToString(selector: string, waitFor: 'attached' | 'detached' | 'visible' | 'hidden'): string { let label; - switch (visibility) { + switch (waitFor) { case 'visible': label = '[visible] '; break; case 'hidden': label = '[hidden] '; break; - case 'any': - case undefined: - label = ''; break; + case 'attached': label = ''; break; + case 'detached': label = '[detached]'; break; } return `${label}${selector}`; } diff --git a/src/page.ts b/src/page.ts index 98c6ac5f14749..afde8c8d97e9c 100644 --- a/src/page.ts +++ b/src/page.ts @@ -231,12 +231,8 @@ export class Page extends platform.EventEmitter { return this.mainFrame().$(selector); } - async waitForSelector(selector: string, options?: types.TimeoutOptions & { visibility?: types.Visibility }): Promise | null> { - return this.mainFrame().waitForSelector(selector, options); - } - - async $wait(selector: string, options?: types.TimeoutOptions & { visibility?: types.Visibility }): Promise | null> { - return this.mainFrame().$wait(selector, options); + async waitForElement(selector: string, options?: types.WaitForElementOptions): Promise | null> { + return this.mainFrame().waitForElement(selector, options); } evaluateHandle: types.EvaluateHandle = async (pageFunction, ...args) => { @@ -483,7 +479,7 @@ export class Page extends platform.EventEmitter { return this.mainFrame().uncheck(selector, options); } - async waitFor(selectorOrFunctionOrTimeout: (string | number | Function), options?: types.WaitForFunctionOptions & { visibility?: types.Visibility }, ...args: any[]): Promise { + async waitFor(selectorOrFunctionOrTimeout: (string | number | Function), options?: types.WaitForFunctionOptions & types.WaitForElementOptions, ...args: any[]): Promise { return this.mainFrame().waitFor(selectorOrFunctionOrTimeout, options, ...args); } diff --git a/src/types.ts b/src/types.ts index baeda8b9147d7..67f371a661e09 100644 --- a/src/types.ts +++ b/src/types.ts @@ -41,7 +41,7 @@ export type Quad = [ Point, Point, Point, Point ]; export type TimeoutOptions = { timeout?: number }; export type WaitForOptions = TimeoutOptions & { waitFor?: boolean }; -export type Visibility = 'visible' | 'hidden' | 'any'; +export type WaitForElementOptions = TimeoutOptions & { waitFor?: 'attached' | 'detached' | 'visible' | 'hidden' }; export type Polling = 'raf' | 'mutation' | number; export type WaitForFunctionOptions = TimeoutOptions & { polling?: Polling }; diff --git a/test/chromium/oopif.spec.js b/test/chromium/oopif.spec.js index da01a9fd7b9f3..cf4a615e2a0c1 100644 --- a/test/chromium/oopif.spec.js +++ b/test/chromium/oopif.spec.js @@ -85,7 +85,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p document.body.appendChild(frame); return new Promise(x => frame.onload = x); }); - await page.waitForSelector('iframe[src="https://google.com/"]'); + await page.waitForElement('iframe[src="https://google.com/"]'); const urls = page.frames().map(frame => frame.url()).sort(); expect(urls).toEqual([ server.EMPTY_PAGE, diff --git a/test/launcher.spec.js b/test/launcher.spec.js index 699a11dfa5213..d2941046dbcf3 100644 --- a/test/launcher.spec.js +++ b/test/launcher.spec.js @@ -171,15 +171,15 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p expect(error.message).toContain('Navigation failed because browser has disconnected!'); await browserServer.close(); }); - it('should reject waitForSelector when browser closes', async({server}) => { + it('should reject waitForElement when browser closes', async({server}) => { server.setRoute('/empty.html', () => {}); const browserServer = await playwright.launchServer({...defaultBrowserOptions }); const remote = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() }); const page = await remote.newPage(); - const watchdog = page.waitForSelector('div', { timeout: 60000 }).catch(e => e); + const watchdog = page.waitForElement('div', { timeout: 60000 }).catch(e => e); - // Make sure the previous waitForSelector has time to make it to the browser before we disconnect. - await page.waitForSelector('body'); + // Make sure the previous waitForElement has time to make it to the browser before we disconnect. + await page.waitForElement('body'); await remote.close(); const error = await watchdog; diff --git a/test/queryselector.spec.js b/test/queryselector.spec.js index 5ed72303d69c2..9f62ff0fdd3e9 100644 --- a/test/queryselector.spec.js +++ b/test/queryselector.spec.js @@ -211,17 +211,6 @@ module.exports.describe = function({testRunner, expect, selectors, FFOX, CHROMIU const element = await page.$('css=section >> css=div'); expect(element).toBeTruthy(); }); - it('should respect waitFor visibility', async({page, server}) => { - await page.setContent('
43543
'); - expect(await page.waitForSelector('css=section', { waitFor: 'visible'})).toBeTruthy(); - expect(await page.waitForSelector('css=section', { waitFor: 'any'})).toBeTruthy(); - expect(await page.waitForSelector('css=section')).toBeTruthy(); - - await page.setContent(''); - expect(await page.waitForSelector('css=section', { waitFor: 'hidden'})).toBeTruthy(); - expect(await page.waitForSelector('css=section', { waitFor: 'any'})).toBeTruthy(); - expect(await page.waitForSelector('css=section')).toBeTruthy(); - }); }); describe('Page.$$', function() { diff --git a/test/waittask.spec.js b/test/waittask.spec.js index 3592102c2debb..9c1ec16acda0d 100644 --- a/test/waittask.spec.js +++ b/test/waittask.spec.js @@ -207,19 +207,19 @@ module.exports.describe = function({testRunner, expect, product, playwright, FFO }); }); - describe('Frame.waitForSelector', function() { + describe('Frame.waitForElement', function() { const addElement = tag => document.body.appendChild(document.createElement(tag)); it('should immediately resolve promise if node exists', async({page, server}) => { await page.goto(server.EMPTY_PAGE); const frame = page.mainFrame(); - await frame.waitForSelector('*'); + await frame.waitForElement('*'); await frame.evaluate(addElement, 'div'); - await frame.waitForSelector('div'); + await frame.waitForElement('div'); }); it('should work with removed MutationObserver', async({page, server}) => { await page.evaluate(() => delete window.MutationObserver); const [handle] = await Promise.all([ - page.waitForSelector('.zombo'), + page.waitForElement('.zombo'), page.setContent(`
anything
`), ]); expect(await page.evaluate(x => x.textContent, handle)).toBe('anything'); @@ -227,7 +227,7 @@ module.exports.describe = function({testRunner, expect, product, playwright, FFO it('should resolve promise when node is added', async({page, server}) => { await page.goto(server.EMPTY_PAGE); const frame = page.mainFrame(); - const watchdog = frame.waitForSelector('div'); + const watchdog = frame.waitForElement('div'); await frame.evaluate(addElement, 'br'); await frame.evaluate(addElement, 'div'); const eHandle = await watchdog; @@ -236,7 +236,7 @@ module.exports.describe = function({testRunner, expect, product, playwright, FFO }); it('should work when node is added through innerHTML', async({page, server}) => { await page.goto(server.EMPTY_PAGE); - const watchdog = page.waitForSelector('h3 div'); + const watchdog = page.waitForElement('h3 div'); await page.evaluate(addElement, 'span'); await page.evaluate(() => document.querySelector('span').innerHTML = '

'); await watchdog; @@ -245,7 +245,7 @@ module.exports.describe = function({testRunner, expect, product, playwright, FFO await page.goto(server.EMPTY_PAGE); await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); const otherFrame = page.frames()[1]; - const watchdog = page.waitForSelector('div'); + const watchdog = page.waitForElement('div'); await otherFrame.evaluate(addElement, 'div'); await page.evaluate(addElement, 'div'); const eHandle = await watchdog; @@ -256,17 +256,17 @@ module.exports.describe = function({testRunner, expect, product, playwright, FFO await utils.attachFrame(page, 'frame2', server.EMPTY_PAGE); const frame1 = page.frames()[1]; const frame2 = page.frames()[2]; - const waitForSelectorPromise = frame2.waitForSelector('div'); + const waitForElementPromise = frame2.waitForElement('div'); await frame1.evaluate(addElement, 'div'); await frame2.evaluate(addElement, 'div'); - const eHandle = await waitForSelectorPromise; + const eHandle = await waitForElementPromise; expect(await eHandle.ownerFrame()).toBe(frame2); }); it('should throw when frame is detached', async({page, server}) => { await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); const frame = page.frames()[1]; let waitError = null; - const waitPromise = frame.waitForSelector('.box').catch(e => waitError = e); + const waitPromise = frame.waitForElement('.box').catch(e => waitError = e); await utils.detachFrame(page, 'frame1'); await waitPromise; expect(waitError).toBeTruthy(); @@ -274,74 +274,74 @@ module.exports.describe = function({testRunner, expect, product, playwright, FFO }); it('should survive cross-process navigation', async({page, server}) => { let boxFound = false; - const waitForSelector = page.waitForSelector('.box').then(() => boxFound = true); + const waitForElement = page.waitForElement('.box').then(() => boxFound = true); await page.goto(server.EMPTY_PAGE); expect(boxFound).toBe(false); await page.reload(); expect(boxFound).toBe(false); await page.goto(server.CROSS_PROCESS_PREFIX + '/grid.html'); - await waitForSelector; + await waitForElement; expect(boxFound).toBe(true); }); it('should wait for visible', async({page, server}) => { let divFound = false; - const waitForSelector = page.waitForSelector('div').then(() => divFound = true); + const waitForElement = page.waitForElement('div', { waitFor: 'visible' }).then(() => divFound = true); await page.setContent(`
1
`); expect(divFound).toBe(false); await page.evaluate(() => document.querySelector('div').style.removeProperty('display')); expect(divFound).toBe(false); await page.evaluate(() => document.querySelector('div').style.removeProperty('visibility')); - expect(await waitForSelector).toBe(true); + expect(await waitForElement).toBe(true); expect(divFound).toBe(true); }); it('should wait for visible recursively', async({page, server}) => { let divVisible = false; - const waitForSelector = page.waitForSelector('div#inner', { visibility: 'visible' }).then(() => divVisible = true); + const waitForElement = page.waitForElement('div#inner', { waitFor: 'visible' }).then(() => divVisible = true); await page.setContent(`
hi
`); expect(divVisible).toBe(false); await page.evaluate(() => document.querySelector('div').style.removeProperty('display')); expect(divVisible).toBe(false); await page.evaluate(() => document.querySelector('div').style.removeProperty('visibility')); - expect(await waitForSelector).toBe(true); + expect(await waitForElement).toBe(true); expect(divVisible).toBe(true); }); - it('hidden should wait for visibility: hidden', async({page, server}) => { + it('hidden should wait for hidden', async({page, server}) => { let divHidden = false; await page.setContent(`
`); - const waitForSelector = page.waitForSelector('div', { visibility: 'hidden' }).then(() => divHidden = true); - await page.waitForSelector('div'); // do a round trip + const waitForElement = page.waitForElement('div', { waitFor: 'hidden' }).then(() => divHidden = true); + await page.waitForElement('div'); // do a round trip expect(divHidden).toBe(false); await page.evaluate(() => document.querySelector('div').style.setProperty('visibility', 'hidden')); - expect(await waitForSelector).toBe(true); + expect(await waitForElement).toBe(true); expect(divHidden).toBe(true); }); it('hidden should wait for display: none', async({page, server}) => { let divHidden = false; await page.setContent(`
`); - const waitForSelector = page.waitForSelector('div', { visibility: 'hidden' }).then(() => divHidden = true); - await page.waitForSelector('div'); // do a round trip + const waitForElement = page.waitForElement('div', { waitFor: 'hidden' }).then(() => divHidden = true); + await page.waitForElement('div'); // do a round trip expect(divHidden).toBe(false); await page.evaluate(() => document.querySelector('div').style.setProperty('display', 'none')); - expect(await waitForSelector).toBe(true); + expect(await waitForElement).toBe(true); expect(divHidden).toBe(true); }); it('hidden should wait for removal', async({page, server}) => { await page.setContent(`
`); let divRemoved = false; - const waitForSelector = page.waitForSelector('div', { visibility: 'hidden' }).then(() => divRemoved = true); - await page.waitForSelector('div'); // do a round trip + const waitForElement = page.waitForElement('div', { waitFor: 'hidden' }).then(() => divRemoved = true); + await page.waitForElement('div'); // do a round trip expect(divRemoved).toBe(false); await page.evaluate(() => document.querySelector('div').remove()); - expect(await waitForSelector).toBe(true); + expect(await waitForElement).toBe(true); expect(divRemoved).toBe(true); }); it('should return null if waiting to hide non-existing element', async({page, server}) => { - const handle = await page.waitForSelector('non-existing', { visibility: 'hidden' }); + const handle = await page.waitForElement('non-existing', { waitFor: 'hidden' }); expect(handle).toBe(null); }); it('should respect timeout', async({page, server}) => { let error = null; - await page.waitForSelector('div', { timeout: 10 }).catch(e => error = e); + await page.waitForElement('div', { timeout: 10 }).catch(e => error = e); expect(error).toBeTruthy(); expect(error.message).toContain('waiting for selector "div" failed: timeout'); expect(error).toBeInstanceOf(playwright.errors.TimeoutError); @@ -349,52 +349,52 @@ module.exports.describe = function({testRunner, expect, product, playwright, FFO it('should have an error message specifically for awaiting an element to be hidden', async({page, server}) => { await page.setContent(`
`); let error = null; - await page.waitForSelector('div', { visibility: 'hidden', timeout: 10 }).catch(e => error = e); + await page.waitForElement('div', { waitFor: 'hidden', timeout: 10 }).catch(e => error = e); expect(error).toBeTruthy(); expect(error.message).toContain('waiting for selector "[hidden] div" failed: timeout'); }); it('should respond to node attribute mutation', async({page, server}) => { let divFound = false; - const waitForSelector = page.waitForSelector('.zombo').then(() => divFound = true); + const waitForElement = page.waitForElement('.zombo').then(() => divFound = true); await page.setContent(`
`); expect(divFound).toBe(false); await page.evaluate(() => document.querySelector('div').className = 'zombo'); - expect(await waitForSelector).toBe(true); + expect(await waitForElement).toBe(true); }); it('should return the element handle', async({page, server}) => { - const waitForSelector = page.waitForSelector('.zombo'); + const waitForElement = page.waitForElement('.zombo'); await page.setContent(`
anything
`); - expect(await page.evaluate(x => x.textContent, await waitForSelector)).toBe('anything'); + expect(await page.evaluate(x => x.textContent, await waitForElement)).toBe('anything'); }); it('should have correct stack trace for timeout', async({page, server}) => { let error; - await page.waitForSelector('.zombo', { timeout: 10 }).catch(e => error = e); + await page.waitForElement('.zombo', { timeout: 10 }).catch(e => error = e); expect(error.stack).toContain('waittask.spec.js'); }); it('should throw for unknown waitFor option', async({page, server}) => { await page.setContent('
test
'); - const error = await page.waitForSelector('section', { visibility: 'foo' }).catch(e => e); - expect(error.message).toContain('Unsupported visibility option'); + const error = await page.waitForElement('section', { waitFor: 'foo' }).catch(e => e); + expect(error.message).toContain('Unsupported waitFor option'); }); - it('should throw for numeric waitFor option', async({page, server}) => { + it('should throw for visibility option', async({page, server}) => { await page.setContent('
test
'); - const error = await page.waitForSelector('section', { visibility: 123 }).catch(e => e); - expect(error.message).toContain('Unsupported visibility option'); + const error = await page.waitForElement('section', { visibility: 'hidden' }).catch(e => e); + expect(error.message).toBe('options.visibility is not supported, did you mean options.waitFor?'); }); it('should throw for true waitFor option', async({page, server}) => { await page.setContent('
test
'); - const error = await page.waitForSelector('section', { visibility: true }).catch(e => e); - expect(error.message).toContain('Unsupported visibility option'); + const error = await page.waitForElement('section', { waitFor: true }).catch(e => e); + expect(error.message).toContain('Unsupported waitFor option'); }); it('should throw for false waitFor option', async({page, server}) => { await page.setContent('
test
'); - const error = await page.waitForSelector('section', { visibility: false }).catch(e => e); - expect(error.message).toContain('Unsupported visibility option'); + const error = await page.waitForElement('section', { waitFor: false }).catch(e => e); + expect(error.message).toContain('Unsupported waitFor option'); }); it('should support >> selector syntax', async({page, server}) => { await page.goto(server.EMPTY_PAGE); const frame = page.mainFrame(); - const watchdog = frame.waitForSelector('css=div >> css=span'); + const watchdog = frame.waitForElement('css=div >> css=span'); await frame.evaluate(addElement, 'br'); await frame.evaluate(addElement, 'div'); await frame.evaluate(() => document.querySelector('div').appendChild(document.createElement('span'))); @@ -402,24 +402,34 @@ module.exports.describe = function({testRunner, expect, product, playwright, FFO const tagName = await eHandle.getProperty('tagName').then(e => e.jsonValue()); expect(tagName).toBe('SPAN'); }); - it('$wait alias should work', async({page, server}) => { - await page.setContent('
test
'); - const handle = await page.$wait('section'); - expect(await handle.evaluate(e => e.textContent)).toBe('test'); + it('should wait for detached if already detached', async({page, server}) => { + await page.setContent('
43543
'); + expect(await page.waitForElement('css=div', { waitFor: 'detached'})).toBe(null); + }); + it('should wait for detached', async({page, server}) => { + await page.setContent('
43543
'); + let done = false; + const waitFor = page.waitForElement('css=div', { waitFor: 'detached'}).then(() => done = true); + expect(done).toBe(false); + await page.waitForElement('css=section'); + expect(done).toBe(false); + await page.$eval('div', div => div.remove()); + expect(await waitFor).toBe(true); + expect(done).toBe(true); }); }); - describe('Frame.waitForSelector xpath', function() { + describe('Frame.waitForElement xpath', function() { const addElement = tag => document.body.appendChild(document.createElement(tag)); it('should support some fancy xpath', async({page, server}) => { await page.setContent(`

red herring

hello world

`); - const waitForXPath = page.waitForSelector('//p[normalize-space(.)="hello world"]'); + const waitForXPath = page.waitForElement('//p[normalize-space(.)="hello world"]'); expect(await page.evaluate(x => x.textContent, await waitForXPath)).toBe('hello world '); }); it('should respect timeout', async({page}) => { let error = null; - await page.waitForSelector('//div', { timeout: 10 }).catch(e => error = e); + await page.waitForElement('//div', { timeout: 10 }).catch(e => error = e); expect(error).toBeTruthy(); expect(error.message).toContain('waiting for selector "//div" failed: timeout'); expect(error).toBeInstanceOf(playwright.errors.TimeoutError); @@ -429,7 +439,7 @@ module.exports.describe = function({testRunner, expect, product, playwright, FFO await utils.attachFrame(page, 'frame2', server.EMPTY_PAGE); const frame1 = page.frames()[1]; const frame2 = page.frames()[2]; - const waitForXPathPromise = frame2.waitForSelector('//div'); + const waitForXPathPromise = frame2.waitForElement('//div'); await frame1.evaluate(addElement, 'div'); await frame2.evaluate(addElement, 'div'); const eHandle = await waitForXPathPromise; @@ -439,20 +449,20 @@ module.exports.describe = function({testRunner, expect, product, playwright, FFO await utils.attachFrame(page, 'frame1', server.EMPTY_PAGE); const frame = page.frames()[1]; let waitError = null; - const waitPromise = frame.waitForSelector('//*[@class="box"]').catch(e => waitError = e); + const waitPromise = frame.waitForElement('//*[@class="box"]').catch(e => waitError = e); await utils.detachFrame(page, 'frame1'); await waitPromise; expect(waitError).toBeTruthy(); expect(waitError.message).toContain('waitForFunction failed: frame got detached.'); }); it('should return the element handle', async({page, server}) => { - const waitForXPath = page.waitForSelector('//*[@class="zombo"]'); + const waitForXPath = page.waitForElement('//*[@class="zombo"]'); await page.setContent(`
anything
`); expect(await page.evaluate(x => x.textContent, await waitForXPath)).toBe('anything'); }); it('should allow you to select an element with single slash', async({page, server}) => { await page.setContent(`
some text
`); - const waitForXPath = page.waitForSelector('//html/body/div'); + const waitForXPath = page.waitForElement('//html/body/div'); expect(await page.evaluate(x => x.textContent, await waitForXPath)).toBe('some text'); }); });