From 4b58d0076f7efaa0895e5f9c16d45befa6456eb8 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Mon, 24 Feb 2020 17:26:19 -0800 Subject: [PATCH] fix(api): split waitForSelectorMissing from waitForSelector This also drops the notion of visibility, which is pretty vague. --- README.md | 2 +- docs/api.md | 44 +++++++++++++------- src/dom.ts | 15 +++---- src/frames.ts | 49 ++++++++++------------ src/injected/injected.ts | 10 ----- src/page.ts | 10 +++-- src/types.ts | 2 - test/click.spec.js | 2 +- test/queryselector.spec.js | 11 ----- test/waittask.spec.js | 84 ++++++++------------------------------ 10 files changed, 84 insertions(+), 145 deletions(-) diff --git a/README.md b/README.md index f9b6bb1809..eae1d3cafd 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ Puppeteer is a Node library which provides a high-level API to control Chrome or We are the same team that originally built Puppeteer at Google, but has since then moved on. Puppeteer proved that there is a lot of interest in the new generation of ever-green, capable and reliable automation drivers. With Playwright, we'd like to take it one step further and offer the same functionality for **all** the popular rendering engines. We'd like to see Playwright vendor-neutral and shared governed. -With Playwright, we are making the APIs more testing-friendly as well. We are taking the lessons learned from Puppeteer and incorporate them into the API, for example, user agent / device emulation is set up consistently on the `BrowserContext` level to enable multi-page scenarios, `click` waits for the element to be available and visible by default, there is a way to wait for network and other events, etc. +With Playwright, we are making the APIs more testing-friendly as well. We are taking the lessons learned from Puppeteer and incorporate them into the API, for example, user agent / device emulation is set up consistently on the `BrowserContext` level to enable multi-page scenarios, `click` waits for the element to be available and clickable by default, there is a way to wait for network and other events, etc. Playwright also aims at being cloud-native. Rather than a single page, `BrowserContext` abstraction is now central to the library operation. `BrowserContext`s are isolated, they can be either created locally or provided as a service. diff --git a/docs/api.md b/docs/api.md index cba744f15a..34c6d0ec73 100644 --- a/docs/api.md +++ b/docs/api.md @@ -531,6 +531,7 @@ page.removeListener('request', logRequest); - [page.waitForRequest(urlOrPredicate[, options])](#pagewaitforrequesturlorpredicate-options) - [page.waitForResponse(urlOrPredicate[, options])](#pagewaitforresponseurlorpredicate-options) - [page.waitForSelector(selector[, options])](#pagewaitforselectorselector-options) +- [page.waitForSelectorMissing(selector[, options])](#pagewaitforselectormissingselector-options) - [page.workers()](#pageworkers) @@ -705,9 +706,8 @@ Shortcut for [page.mainFrame().$eval(selector, pageFunction)](#frameevalselector #### 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. +- returns: <[Promise]> Promise which resolves when element specified by selector string is added to 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 @@ -1447,7 +1447,6 @@ 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`. - `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. @@ -1456,8 +1455,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] or [xpath], depending on whether or not it starts with '//', 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.waitForSelector](#pagewaitforselectorselector-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 @@ -1601,9 +1599,8 @@ 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. +- returns: <[Promise]> Promise which resolves when element specified by selector string is added to 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 @@ -1628,6 +1625,19 @@ const { chromium } = require('playwright'); // Or 'firefox' or 'webkit'. ``` Shortcut for [page.mainFrame().waitForSelector(selector[, options])](#framewaitforselectorselector-options). + +#### page.waitForSelectorMissing(selector[, options]) +- `selector` <[string]> A selector of an element to wait for missing. +- `options` <[Object]> + - `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 removed from DOM. + +Wait for the `selector` to disappear from the page. If at the moment of calling +the method the `selector` already does not exist, the method will return +immediately. If the selector doesn't disappear after the `timeout` milliseconds of waiting, the function will throw. This method works across navigations. + +Shortcut for [page.mainFrame().waitForSelectorMissing(selector[, options])](#framewaitforselectormissingselector-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. @@ -1708,6 +1718,7 @@ An example of getting text from an iframe element: - [frame.waitForLoadState([options])](#framewaitforloadstateoptions) - [frame.waitForNavigation([options])](#framewaitfornavigationoptions) - [frame.waitForSelector(selector[, options])](#framewaitforselectorselector-options) +- [frame.waitForSelectorMissing(selector[, options])](#framewaitforselectormissingselector-options) #### frame.$(selector) @@ -1757,9 +1768,8 @@ 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. +- returns: <[Promise]> Promise which resolves when element specified by selector string is added to 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 @@ -2102,7 +2112,6 @@ 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`. - `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. @@ -2111,8 +2120,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] or [xpath], depending on whether or not it starts with '//', 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.waitForSelector](#framewaitforselectorselector-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 @@ -2209,9 +2217,8 @@ const [response] = await Promise.all([ #### 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. +- returns: <[Promise]> Promise which resolves when element specified by selector string is added to 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 @@ -2235,6 +2242,15 @@ const { webkit } = require('playwright'); // Or 'chromium' or 'firefox'. })(); ``` +#### frame.waitForSelectorMissing(selector[, options]) +- `selector` <[string]> A selector of an element to wait for missing. +- `options` <[Object]> + - `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 removed from DOM. + +Wait for the `selector` to disappear from the page. If at the moment of calling +the method the `selector` already does not exist, the method will return +immediately. If the selector doesn't disappear after the `timeout` milliseconds of waiting, the function will throw. This method works across navigations. ### class: ElementHandle * extends: [JSHandle] diff --git a/src/dom.ts b/src/dom.ts index 11ec858a4a..a692df17c2 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -581,18 +581,13 @@ 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 { +export function waitForSelectorTask(selector: string, present: boolean, timeout: number): Task { selector = normalizeSelector(selector); - return async (context: FrameExecutionContext) => context.evaluateHandle((injected: Injected, selector: string, visibility: types.Visibility, timeout: number) => { - const polling = visibility === 'any' ? '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; + return async (context: FrameExecutionContext) => context.evaluateHandle((injected: Injected, selector: string, present: boolean, timeout: number) => { + return injected.poll('mutation', selector, timeout, (element: Element | undefined) => { + return present ? element : !element; }); - }, await context._injected(), selector, visibility, timeout); + }, await context._injected(), selector, present, timeout); } export const setFileInputFunction = async (element: HTMLInputElement, payloads: types.FilePayload[]) => { diff --git a/src/frames.ts b/src/frames.ts index c784a1c49a..bf719cde04 100644 --- a/src/frames.ts +++ b/src/frames.ts @@ -556,9 +556,13 @@ 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 waitForSelector(selector: string, options?: types.TimeoutOptions): Promise | null> { + if (options && (options as any).waitFor) + throw new Error('Unsupported waitFor option, did you mean to click?'); + if (options && (options as any).visibility) + throw new Error('Unsupported visibility option, did you mean to waitForSelectorMissing?'); + const { timeout = this._page._timeoutSettings.timeout() } = (options || {}); + const handle = await this._waitForSelectorInUtilityContext(selector, timeout); const mainContext = await this._mainContext(); if (handle && handle._context !== mainContext) { const adopted = this._page._delegate.adoptElementHandle(handle, mainContext); @@ -568,7 +572,15 @@ export class Frame { return handle; } - async $wait(selector: string, options?: types.TimeoutOptions & { visibility?: types.Visibility }): Promise | null> { + async waitForSelectorMissing(selector: string, options?: types.TimeoutOptions): Promise { + if (options && (options as any).visibility) + throw new Error('Unsupported visibility option, did you mean to waitForSelector?'); + const { timeout = this._page._timeoutSettings.timeout() } = (options || {}); + const task = dom.waitForSelectorTask(selector, false /* present */, timeout); + await this._scheduleRerunnableTask(task, 'utility', timeout, `selector "${selector}" to be missing`); + } + + async $wait(selector: string, options?: types.TimeoutOptions): Promise | null> { return this.waitForSelector(selector, options); } @@ -842,7 +854,7 @@ export class Frame { await 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 = {}, ...args: any[]): Promise { if (helper.isString(selectorOrFunctionOrTimeout)) return this.waitForSelector(selectorOrFunctionOrTimeout, options) as any; if (helper.isNumber(selectorOrFunctionOrTimeout)) @@ -858,9 +870,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); if (!maybeHandle) - throw new Error('No node found for selector: ' + selectorToString(selector, 'any')); + throw new Error(`No node found for selector: ${selector}`); handle = maybeHandle; } else { const context = await this._context('utility'); @@ -871,14 +883,9 @@ 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, timeout: number): Promise | null> { + const task = dom.waitForSelectorTask(selector, true /* present */, timeout); + const result = await this._scheduleRerunnableTask(task, 'utility', timeout, `selector "${selector}"`); if (!result.asElement()) { await result.dispose(); return null; @@ -1061,15 +1068,3 @@ function createTimeoutPromise(timeout: number): Disposable dispose }; } - -function selectorToString(selector: string, visibility: types.Visibility): string { - let label; - switch (visibility) { - case 'visible': label = '[visible] '; break; - case 'hidden': label = '[hidden] '; break; - case 'any': - case undefined: - label = ''; break; - } - return `${label}${selector}`; -} diff --git a/src/injected/injected.ts b/src/injected/injected.ts index 46fd65ce70..561db74675 100644 --- a/src/injected/injected.ts +++ b/src/injected/injected.ts @@ -135,16 +135,6 @@ class Injected { return result; } - isVisible(element: Element): boolean { - if (!element.ownerDocument || !element.ownerDocument.defaultView) - return true; - const style = element.ownerDocument.defaultView.getComputedStyle(element); - if (!style || style.visibility === 'hidden') - return false; - const rect = element.getBoundingClientRect(); - return !!(rect.top || rect.bottom || rect.width || rect.height); - } - private _pollMutation(selector: string | undefined, predicate: Predicate, timeout: number): Promise { let timedOut = false; if (timeout) diff --git a/src/page.ts b/src/page.ts index 9fcb31a0b0..1cf56e0e12 100644 --- a/src/page.ts +++ b/src/page.ts @@ -211,11 +211,15 @@ export class Page extends platform.EventEmitter { return this.mainFrame().$(selector); } - async waitForSelector(selector: string, options?: types.TimeoutOptions & { visibility?: types.Visibility }): Promise | null> { + async waitForSelector(selector: string, options?: types.TimeoutOptions): Promise | null> { return this.mainFrame().waitForSelector(selector, options); } - async $wait(selector: string, options?: types.TimeoutOptions & { visibility?: types.Visibility }): Promise | null> { + async waitForSelectorMissing(selector: string, options?: types.TimeoutOptions): Promise { + return this.mainFrame().waitForSelectorMissing(selector, options); + } + + async $wait(selector: string, options?: types.TimeoutOptions): Promise | null> { return this.mainFrame().$wait(selector, options); } @@ -525,7 +529,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, ...args: any[]): Promise { return this.mainFrame().waitFor(selectorOrFunctionOrTimeout, options, ...args); } diff --git a/src/types.ts b/src/types.ts index b32932532b..db8e1f93c8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -38,8 +38,6 @@ 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 Polling = 'raf' | 'mutation' | number; export type WaitForFunctionOptions = TimeoutOptions & { polling?: Polling }; diff --git a/test/click.spec.js b/test/click.spec.js index 515b328505..375d7e7e4c 100644 --- a/test/click.spec.js +++ b/test/click.spec.js @@ -158,7 +158,7 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI const error = await page.click('button', { waitFor: 'visible' }).catch(e => e); expect(error.message).toBe('waitFor option should be a boolean, got "string"'); }); - it('should waitFor visible', async({page, server}) => { + it('should waitFor display:none to be cleared', async({page, server}) => { let done = false; await page.goto(server.PREFIX + '/input/button.html'); await page.$eval('button', b => b.style.display = 'none'); diff --git a/test/queryselector.spec.js b/test/queryselector.spec.js index 27161b4f69..1bfb1cb304 100644 --- a/test/queryselector.spec.js +++ b/test/queryselector.spec.js @@ -200,17 +200,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 68e6fb0f2f..30f0cf413a 100644 --- a/test/waittask.spec.js +++ b/test/waittask.spec.js @@ -284,61 +284,18 @@ module.exports.describe = function({testRunner, expect, product, playwright, FFO await waitForSelector; expect(boxFound).toBe(true); }); - it('should wait for visible', async({page, server}) => { - let divFound = false; - const waitForSelector = page.waitForSelector('div').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(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); - 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(divVisible).toBe(true); - }); - it('hidden should wait for visibility: 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 - expect(divHidden).toBe(false); - await page.evaluate(() => document.querySelector('div').style.setProperty('visibility', 'hidden')); - expect(await waitForSelector).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 - expect(divHidden).toBe(false); - await page.evaluate(() => document.querySelector('div').style.setProperty('display', 'none')); - expect(await waitForSelector).toBe(true); - expect(divHidden).toBe(true); - }); - it('hidden should wait for removal', async({page, server}) => { + it('waitForMissing should wait for removal', async({page, server}) => { await page.setContent(`
`); let divRemoved = false; - const waitForSelector = page.waitForSelector('div', { visibility: 'hidden' }).then(() => divRemoved = true); + const waitForSelector = page.waitForSelectorMissing('div').then(() => divRemoved = true); await page.waitForSelector('div'); // do a round trip expect(divRemoved).toBe(false); await page.evaluate(() => document.querySelector('div').remove()); - expect(await waitForSelector).toBe(true); + await waitForSelector; 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' }); - expect(handle).toBe(null); + it('should return immediately if waitForMissing non-existing element', async({page, server}) => { + await page.waitForSelectorMissing('non-existing'); }); it('should respect timeout', async({page, server}) => { let error = null; @@ -347,12 +304,12 @@ module.exports.describe = function({testRunner, expect, product, playwright, FFO expect(error.message).toContain('waiting for selector "div" failed: timeout'); expect(error).toBeInstanceOf(playwright.errors.TimeoutError); }); - it('should have an error message specifically for awaiting an element to be hidden', async({page, server}) => { + it('should have an error message specifically for waitForMissing', async({page, server}) => { await page.setContent(`
`); let error = null; - await page.waitForSelector('div', { visibility: 'hidden', timeout: 10 }).catch(e => error = e); + await page.waitForSelectorMissing('div', { timeout: 10 }).catch(e => error = e); expect(error).toBeTruthy(); - expect(error.message).toContain('waiting for selector "[hidden] div" failed: timeout'); + expect(error.message).toContain('waiting for selector "div" to be missing failed: timeout'); }); it('should respond to node attribute mutation', async({page, server}) => { let divFound = false; @@ -372,25 +329,20 @@ module.exports.describe = function({testRunner, expect, product, playwright, FFO await page.waitForSelector('.zombo', { timeout: 10 }).catch(e => error = e); expect(error.stack).toContain('waittask.spec.js'); }); - it('should throw for unknown waitFor option', async({page, server}) => { + it('should throw for waitFor option', async({page, server}) => { + await page.setContent('
test
'); + const error = await page.waitForSelector('section', { waitFor: 'foo' }).catch(e => e); + expect(error.message).toContain('Unsupported waitFor option, did you mean to click?'); + }); + it('should throw for visibility 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'); + expect(error.message).toContain('Unsupported visibility option, did you mean to waitForSelectorMissing?'); }); - it('should throw for numeric waitFor option', async({page, server}) => { + it('should throw for visibility option on waitForMissing', 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'); - }); - 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'); - }); - 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.waitForSelectorMissing('section', { visibility: 'foo' }).catch(e => e); + expect(error.message).toContain('Unsupported visibility option, did you mean to waitForSelector?'); }); it('should support >> selector syntax', async({page, server}) => { await page.goto(server.EMPTY_PAGE);