From fd927000eafec5f3e102aa835b8f5542a431975d Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 17 Dec 2019 14:30:02 -0800 Subject: [PATCH] feature: update api around selectors (#280) - Selector is again a string. - Most methods taking selector also accept waitFor option. - Available waitFor options are: 'visible', 'hidden', 'any' === true, false === undefined. - waitForXPath is removed. - waitForSelector is replaced by $(selector, { waitFor: true }). --- src/dom.ts | 117 ++++++++------------------- src/frames.ts | 145 ++++++++++++++++++---------------- src/input.ts | 2 +- src/page.ts | 30 +++---- src/types.ts | 27 +------ test/chromium/connect.spec.js | 2 +- test/chromium/headful.spec.js | 2 +- test/click.spec.js | 47 +++++++---- test/page.spec.js | 15 ++-- test/queryselector.spec.js | 74 ++++++----------- test/waittask.spec.js | 68 ++++++++-------- 11 files changed, 221 insertions(+), 308 deletions(-) diff --git a/src/dom.ts b/src/dom.ts index 8d10de6945..74428cf3ba 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -11,9 +11,6 @@ import { assert, helper, debugError } from './helper'; import Injected from './injected/injected'; import { Page } from './page'; -type ScopedSelector = types.Selector & { scope?: ElementHandle }; -type ResolvedSelector = { scope?: ElementHandle, selector: string, visibility: types.Visibility, disposeScope?: boolean }; - export class FrameExecutionContext extends js.ExecutionContext { private readonly _frame: frames.Frame; @@ -47,52 +44,26 @@ export class FrameExecutionContext extends js.ExecutionContext { return this._injectedPromise; } - async _adoptElementHandle(handle: ElementHandle): Promise> { - assert(handle.executionContext() !== this, 'Should not adopt to the same context'); - return this._frame._page._delegate.adoptElementHandle(handle, this); - } - - async _resolveSelector(selector: string | ScopedSelector): Promise { - if (helper.isString(selector)) - return { selector: normalizeSelector(selector), visibility: 'any' }; - if (selector.scope && selector.scope.executionContext() !== this) { - const scope = await this._adoptElementHandle(selector.scope); - return { scope, selector: normalizeSelector(selector.selector), disposeScope: true, visibility: selector.visibility || 'any' }; - } - return { scope: selector.scope, selector: normalizeSelector(selector.selector), visibility: selector.visibility || 'any' }; - } - - async _$(selector: string | ScopedSelector): Promise | null> { - const resolved = await this._resolveSelector(selector); + async _$(selector: string, scope?: ElementHandle): Promise | null> { const handle = await this.evaluateHandle( - (injected: Injected, selector: string, visibility: types.Visibility, scope?: Node) => { - const element = injected.querySelector(selector, scope || document); - if (visibility === 'any' || !element) - return element; - return injected.isVisible(element) === (visibility === 'visible') ? element : undefined; - }, - await this._injected(), resolved.selector, resolved.visibility, resolved.scope + (injected: Injected, selector: string, scope?: Node) => injected.querySelector(selector, scope || document), + await this._injected(), normalizeSelector(selector), scope ); - if (resolved.disposeScope) - await resolved.scope.dispose(); if (!handle.asElement()) await handle.dispose(); return handle.asElement(); } - async _$$(selector: string | ScopedSelector): Promise[]> { - const resolved = await this._resolveSelector(selector); + async _$array(selector: string, scope?: ElementHandle): Promise> { const arrayHandle = await this.evaluateHandle( - (injected: Injected, selector: string, visibility: types.Visibility, scope?: Node) => { - const elements = injected.querySelectorAll(selector, scope || document); - if (visibility !== 'any') - return elements.filter(element => injected.isVisible(element) === (visibility === 'visible')); - return elements; - }, - await this._injected(), resolved.selector, resolved.visibility, resolved.scope + (injected: Injected, selector: string, scope?: Node) => injected.querySelectorAll(selector, scope || document), + await this._injected(), normalizeSelector(selector), scope ); - if (resolved.disposeScope) - await resolved.scope.dispose(); + return arrayHandle; + } + + async _$$(selector: string, scope?: ElementHandle): Promise[]> { + const arrayHandle = await this._$array(selector, scope); const properties = await arrayHandle.getProperties(); await arrayHandle.dispose(); const result = []; @@ -105,31 +76,6 @@ export class FrameExecutionContext extends js.ExecutionContext { } return result; } - - _$eval: types.$Eval = async (selector, pageFunction, ...args) => { - const elementHandle = await this._$(selector); - if (!elementHandle) - throw new Error(`Error: failed to find element matching selector "${types.selectorToString(selector)}"`); - const result = await elementHandle.evaluate(pageFunction, ...args as any); - await elementHandle.dispose(); - return result; - } - - _$$eval: types.$$Eval = async (selector, pageFunction, ...args) => { - const resolved = await this._resolveSelector(selector); - const arrayHandle = await this.evaluateHandle( - (injected: Injected, selector: string, visibility: types.Visibility, scope?: Node) => { - const elements = injected.querySelectorAll(selector, scope || document); - if (visibility !== 'any') - return elements.filter(element => injected.isVisible(element) === (visibility === 'visible')); - return elements; - }, - await this._injected(), resolved.selector, resolved.visibility, resolved.scope - ); - const result = await arrayHandle.evaluate(pageFunction, ...args as any); - await arrayHandle.dispose(); - return result; - } } export class ElementHandle extends js.JSHandle { @@ -348,7 +294,7 @@ export class ElementHandle extends js.JSHandle { throw new Error(errorMessage); } - async type(text: string, options: { delay: (number | undefined); } | undefined) { + async type(text: string, options?: { delay?: number }) { await this.focus(); await this._page.keyboard.type(text, options); } @@ -366,31 +312,32 @@ export class ElementHandle extends js.JSHandle { return this._page._screenshotter.screenshotElement(this, options); } - private _scopedSelector(selector: string | types.Selector): string | ScopedSelector { - selector = types.clearSelector(selector); - if (helper.isString(selector)) - selector = { selector }; - return { scope: this, selector: selector.selector, visibility: selector.visibility }; + $(selector: string): Promise { + return this._context._$(selector, this); } - $(selector: string | types.Selector): Promise { - return this._context._$(this._scopedSelector(selector)); + $$(selector: string): Promise[]> { + return this._context._$$(selector, this); } - $$(selector: string | types.Selector): Promise[]> { - return this._context._$$(this._scopedSelector(selector)); + $eval: types.$Eval = async (selector, pageFunction, ...args) => { + const elementHandle = await this._context._$(selector, this); + if (!elementHandle) + throw new Error(`Error: failed to find element matching selector "${selector}"`); + const result = await elementHandle.evaluate(pageFunction, ...args as any); + await elementHandle.dispose(); + return result; } - $eval: types.$Eval = (selector, pageFunction, ...args) => { - return this._context._$eval(this._scopedSelector(selector), pageFunction, ...args as any); - } - - $$eval: types.$$Eval = (selector, pageFunction, ...args) => { - return this._context._$$eval(this._scopedSelector(selector), pageFunction, ...args as any); + $$eval: types.$$Eval = async (selector, pageFunction, ...args) => { + const arrayHandle = await this._context._$array(selector, this); + const result = await arrayHandle.evaluate(pageFunction, ...args as any); + await arrayHandle.dispose(); + return result; } $x(expression: string): Promise[]> { - return this._context._$$({ scope: this, selector: 'xpath=' + expression }); + return this._context._$$('xpath=' + expression, this); } isIntersectingViewport(): Promise { @@ -446,9 +393,9 @@ export function waitForFunctionTask(pageFunction: Function | string, options: ty }, await context._injected(), predicateBody, polling, options.timeout, ...args); } -export function waitForSelectorTask(selector: string | types.Selector, timeout: number): Task { +export function waitForSelectorTask(selector: string, visibility: types.Visibility | undefined, timeout: number): Task { return async (context: FrameExecutionContext) => { - const resolved = await context._resolveSelector(selector); + selector = normalizeSelector(selector); return context.evaluateHandle((injected: Injected, selector: string, visibility: types.Visibility, timeout: number, scope?: Node) => { if (visibility !== 'any') return injected.pollRaf(predicate, timeout); @@ -462,6 +409,6 @@ export function waitForSelectorTask(selector: string | types.Selector, timeout: return element; return injected.isVisible(element) === (visibility === 'visible') ? element : false; } - }, await context._injected(), resolved.selector, resolved.visibility, timeout, resolved.scope); + }, await context._injected(), selector, visibility, timeout, undefined); }; } diff --git a/src/frames.ts b/src/frames.ts index dab49ccd68..19ad9fb855 100644 --- a/src/frames.ts +++ b/src/frames.ts @@ -53,7 +53,7 @@ export type GotoResult = { export type LifecycleEvent = 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'; const kLifecycleEvents: Set = new Set(['load', 'domcontentloaded', 'networkidle0', 'networkidle2']); -export type WaitForOptions = types.TimeoutOptions & { waitFor?: boolean }; +export type WaitForOptions = types.TimeoutOptions & { waitFor?: boolean | 'visible' | 'hidden' | 'any' }; export class FrameManager { private _page: Page; @@ -340,16 +340,18 @@ export class Frame { return watcher.navigationResponse(); } - _mainContext(): Promise { + _context(contextType: ContextType): Promise { if (this._detached) throw new Error(`Execution Context is not available in detached frame "${this.url()}" (are you trying to evaluate?)`); - return this._contextData.get('main').contextPromise; + return this._contextData.get(contextType).contextPromise; + } + + _mainContext(): Promise { + return this._context('main'); } _utilityContext(): Promise { - if (this._detached) - throw new Error(`Execution Context is not available in detached frame "${this.url()}" (are you trying to evaluate?)`); - return this._contextData.get('utility').contextPromise; + return this._context('utility'); } executionContext(): Promise { @@ -366,9 +368,8 @@ export class Frame { return context.evaluate(pageFunction, ...args as any); } - async $(selector: string | types.Selector): Promise | null> { - const context = await this._mainContext(); - return context._$(types.clearSelector(selector)); + async $(selector: string, options?: WaitForOptions): Promise | null> { + return this._optionallyWaitForSelector('main', selector, options, true /* returnNull */); } async $x(expression: string): Promise[]> { @@ -378,17 +379,25 @@ export class Frame { $eval: types.$Eval = async (selector, pageFunction, ...args) => { const context = await this._mainContext(); - return context._$eval(selector, pageFunction, ...args as any); + const elementHandle = await context._$(selector); + if (!elementHandle) + throw new Error(`Error: failed to find element matching selector "${selector}"`); + const result = await elementHandle.evaluate(pageFunction, ...args as any); + await elementHandle.dispose(); + return result; } $$eval: types.$$Eval = async (selector, pageFunction, ...args) => { const context = await this._mainContext(); - return context._$$eval(selector, pageFunction, ...args as any); + const arrayHandle = await context._$array(selector); + const result = await arrayHandle.evaluate(pageFunction, ...args as any); + await arrayHandle.dispose(); + return result; } - async $$(selector: string | types.Selector): Promise[]> { + async $$(selector: string): Promise[]> { const context = await this._mainContext(); - return context._$$(types.clearSelector(selector)); + return context._$$(selector); } async content(): Promise { @@ -581,50 +590,50 @@ export class Frame { return result; } - async click(selector: string | types.Selector, options?: WaitForOptions & ClickOptions) { - const handle = await this._optionallyWaitForInUtilityContext(selector, options); + async click(selector: string, options?: WaitForOptions & ClickOptions) { + const handle = await this._optionallyWaitForSelector('utility', selector, options); await handle.click(options); await handle.dispose(); } - async dblclick(selector: string | types.Selector, options?: WaitForOptions & MultiClickOptions) { - const handle = await this._optionallyWaitForInUtilityContext(selector, options); + async dblclick(selector: string, options?: WaitForOptions & MultiClickOptions) { + const handle = await this._optionallyWaitForSelector('utility', selector, options); await handle.dblclick(options); await handle.dispose(); } - async tripleclick(selector: string | types.Selector, options?: WaitForOptions & MultiClickOptions) { - const handle = await this._optionallyWaitForInUtilityContext(selector, options); + async tripleclick(selector: string, options?: WaitForOptions & MultiClickOptions) { + const handle = await this._optionallyWaitForSelector('utility', selector, options); await handle.tripleclick(options); await handle.dispose(); } - async fill(selector: string | types.Selector, value: string, options?: WaitForOptions) { - const handle = await this._optionallyWaitForInUtilityContext(selector, options); + async fill(selector: string, value: string, options?: WaitForOptions) { + const handle = await this._optionallyWaitForSelector('utility', selector, options); await handle.fill(value); await handle.dispose(); } - async focus(selector: string | types.Selector, options?: WaitForOptions) { - const handle = await this._optionallyWaitForInUtilityContext(selector, options); + async focus(selector: string, options?: WaitForOptions) { + const handle = await this._optionallyWaitForSelector('utility', selector, options); await handle.focus(); await handle.dispose(); } - async hover(selector: string | types.Selector, options?: WaitForOptions & PointerActionOptions) { - const handle = await this._optionallyWaitForInUtilityContext(selector, options); + async hover(selector: string, options?: WaitForOptions & PointerActionOptions) { + const handle = await this._optionallyWaitForSelector('utility', selector, options); await handle.hover(options); await handle.dispose(); } - async select(selector: string | types.Selector, value: string | dom.ElementHandle | SelectOption | string[] | dom.ElementHandle[] | SelectOption[] | undefined, options?: WaitForOptions): Promise { - const handle = await this._optionallyWaitForInUtilityContext(selector, options); + async select(selector: string, value: string | dom.ElementHandle | SelectOption | string[] | dom.ElementHandle[] | SelectOption[] | undefined, options?: WaitForOptions): Promise { + const handle = await this._optionallyWaitForSelector('utility', selector, options); const toDispose: Promise[] = []; - const values = value === undefined ? [] : value instanceof Array ? value : [value]; + const values = value === undefined ? [] : Array.isArray(value) ? value : [value]; const context = await this._utilityContext(); const adoptedValues = await Promise.all(values.map(async value => { if (value instanceof dom.ElementHandle && value.executionContext() !== context) { - const adopted = context._adoptElementHandle(value); + const adopted = this._page._delegate.adoptElementHandle(value, context); toDispose.push(adopted); return adopted; } @@ -636,17 +645,15 @@ export class Frame { return result; } - async type(selector: string | types.Selector, text: string, options: WaitForOptions & { delay: (number | undefined); } | undefined) { - const context = await this._utilityContext(); - const handle = await context._$(types.clearSelector(selector)); - assert(handle, 'No node found for selector: ' + types.selectorToString(selector)); + async type(selector: string, text: string, options?: WaitForOptions & { delay?: number }) { + const handle = await this._optionallyWaitForSelector('utility', selector, options); await handle.type(text, options); await handle.dispose(); } waitFor(selectorOrFunctionOrTimeout: (string | number | Function), options: any = {}, ...args: any[]): Promise { if (helper.isString(selectorOrFunctionOrTimeout)) - return this.waitForSelector(selectorOrFunctionOrTimeout as string, options) as any; + return this.$(selectorOrFunctionOrTimeout as string, { waitFor: true, ...options }) as any; if (helper.isNumber(selectorOrFunctionOrTimeout)) return new Promise(fulfill => setTimeout(fulfill, selectorOrFunctionOrTimeout as number)); if (typeof selectorOrFunctionOrTimeout === 'function') @@ -654,45 +661,35 @@ export class Frame { return Promise.reject(new Error('Unsupported target type: ' + (typeof selectorOrFunctionOrTimeout))); } - private async _optionallyWaitForInUtilityContext(selector: string | types.Selector,options: WaitForOptions): Promise { - let handle: dom.ElementHandle | null; - if (options && options.waitFor) { - handle = await this._waitForSelectorInUtilityContext(selector, options); + private async _optionallyWaitForSelector(contextType: ContextType, selector: string, options: WaitForOptions = {}, returnNull?: boolean): Promise | null> { + const { timeout = this._page._timeoutSettings.timeout(), waitFor = undefined } = options; + let handle: dom.ElementHandle | null; + if (waitFor) { + let visibility: types.Visibility = 'any'; + if (waitFor === 'visible' || waitFor === 'hidden' || waitFor === 'any') + visibility = waitFor; + else if (waitFor === true) + visibility = 'any'; + else + throw new Error(`Unsupported waitFor option "${waitFor}"`); + const task = dom.waitForSelectorTask(selector, visibility, timeout); + const result = await this._scheduleRerunnableTask(task, contextType, timeout, `selector "${selectorToString(selector, visibility)}"`); + if (!result.asElement()) { + await result.dispose(); + if (returnNull) + return null; + throw new Error('No node found for selector: ' + selectorToString(selector, visibility)); + } + handle = result.asElement() as dom.ElementHandle; } else { - const context = await this._utilityContext(); - handle = await context._$(types.clearSelector(selector)); + const context = await this._context(contextType); + handle = await context._$(selector); + if (!returnNull) + assert(handle, 'No node found for selector: ' + selector); } - assert(handle, 'No node found for selector: ' + types.selectorToString(selector)); return handle; } - private async _waitForSelectorInUtilityContext(selector: string | types.Selector, options: types.TimeoutOptions = {}): Promise { - const { timeout = this._page._timeoutSettings.timeout() } = options; - const task = dom.waitForSelectorTask(types.clearSelector(selector), timeout); - const handle = await this._scheduleRerunnableTask(task, 'utility', timeout, `selector "${types.selectorToString(selector)}"`); - if (!handle.asElement()) { - await handle.dispose(); - return null; - } - return handle.asElement(); - } - - async waitForSelector(selector: string | types.Selector, options: types.TimeoutOptions = {}): Promise { - const handle = await this._waitForSelectorInUtilityContext(selector, options); - if (!handle) - return null; - const maincontext = await this._mainContext(); - if (handle.executionContext() === maincontext) - return handle.asElement(); - const adopted = await maincontext._adoptElementHandle(handle.asElement()); - await handle.dispose(); - return adopted; - } - - async waitForXPath(xpath: string, options: types.TimeoutOptions = {}): Promise { - return this.waitForSelector('xpath=' + xpath, options); - } - waitForFunction(pageFunction: Function | string, options: types.WaitForFunctionOptions = {}, ...args: any[]): Promise { options = { timeout: this._page._timeoutSettings.timeout(), ...options }; const task = dom.waitForFunctionTask(pageFunction, options, ...args); @@ -969,3 +966,15 @@ class LifecycleWatcher { clearTimeout(this._maximumTimer); } } + +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}`; +} \ No newline at end of file diff --git a/src/input.ts b/src/input.ts index 0c3b0f846c..c9c71682e8 100644 --- a/src/input.ts +++ b/src/input.ts @@ -133,7 +133,7 @@ export class Keyboard { await this._raw.sendText(text); } - async type(text: string, options: { delay: (number | undefined); } | undefined) { + async type(text: string, options?: { delay?: number }) { const delay = (options && options.delay) || null; for (const char of text) { if (keyboardLayout.keyDefinitions[char]) { diff --git a/src/page.ts b/src/page.ts index 2d1dbe46e5..a404f44620 100644 --- a/src/page.ts +++ b/src/page.ts @@ -186,8 +186,8 @@ export class Page extends EventEmitter { this._timeoutSettings.setDefaultTimeout(timeout); } - async $(selector: string | types.Selector): Promise | null> { - return this.mainFrame().$(selector); + async $(selector: string, options?: frames.WaitForOptions): Promise | null> { + return this.mainFrame().$(selector, options); } async _createSelector(name: string, handle: dom.ElementHandle): Promise { @@ -210,7 +210,7 @@ export class Page extends EventEmitter { return this.mainFrame().$$eval(selector, pageFunction, ...args as any); } - async $$(selector: string | types.Selector): Promise[]> { + async $$(selector: string): Promise[]> { return this.mainFrame().$$(selector); } @@ -458,35 +458,35 @@ export class Page extends EventEmitter { return this._closed; } - click(selector: string | types.Selector, options?: frames.WaitForOptions & input.ClickOptions) { + click(selector: string, options?: frames.WaitForOptions & input.ClickOptions) { return this.mainFrame().click(selector, options); } - dblclick(selector: string | types.Selector, options?: frames.WaitForOptions & input.MultiClickOptions) { + dblclick(selector: string, options?: frames.WaitForOptions & input.MultiClickOptions) { return this.mainFrame().dblclick(selector, options); } - tripleclick(selector: string | types.Selector, options?: frames.WaitForOptions & input.MultiClickOptions) { + tripleclick(selector: string, options?: frames.WaitForOptions & input.MultiClickOptions) { return this.mainFrame().tripleclick(selector, options); } - fill(selector: string | types.Selector, value: string, options?: frames.WaitForOptions) { + fill(selector: string, value: string, options?: frames.WaitForOptions) { return this.mainFrame().fill(selector, value, options); } - focus(selector: string | types.Selector, options?: frames.WaitForOptions) { + focus(selector: string, options?: frames.WaitForOptions) { return this.mainFrame().focus(selector, options); } - hover(selector: string | types.Selector, options?: frames.WaitForOptions & input.PointerActionOptions) { + hover(selector: string, options?: frames.WaitForOptions & input.PointerActionOptions) { return this.mainFrame().hover(selector, options); } - select(selector: string | types.Selector, value: string | dom.ElementHandle | input.SelectOption | string[] | dom.ElementHandle[] | input.SelectOption[] | undefined, options?: frames.WaitForOptions): Promise { + select(selector: string, value: string | dom.ElementHandle | input.SelectOption | string[] | dom.ElementHandle[] | input.SelectOption[] | undefined, options?: frames.WaitForOptions): Promise { return this.mainFrame().select(selector, value, options); } - type(selector: string | types.Selector, text: string, options: frames.WaitForOptions & { delay: (number | undefined); } | undefined) { + type(selector: string, text: string, options?: frames.WaitForOptions & { delay?: number }) { return this.mainFrame().type(selector, text, options); } @@ -494,14 +494,6 @@ export class Page extends EventEmitter { return this.mainFrame().waitFor(selectorOrFunctionOrTimeout, options, ...args); } - waitForSelector(selector: string | types.Selector, options: types.TimeoutOptions = {}): Promise { - return this.mainFrame().waitForSelector(selector, options); - } - - waitForXPath(xpath: string, options: types.TimeoutOptions = {}): Promise { - return this.mainFrame().waitForXPath(xpath, options); - } - waitForFunction(pageFunction: Function | string, options: types.WaitForFunctionOptions, ...args: any[]): Promise { return this.mainFrame().waitForFunction(pageFunction, options, ...args); } diff --git a/src/types.ts b/src/types.ts index 84889c82a1..930ca34c42 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,7 +2,6 @@ // Licensed under the MIT license. import * as js from './javascript'; -import { helper } from './helper'; import * as dom from './dom'; type Boxed = { [Index in keyof Args]: Args[Index] | js.JSHandle }; @@ -14,8 +13,8 @@ type ElementForSelector = T extends keyof HTMLElementTagNameMap ? HTMLElement export type Evaluate = (pageFunction: PageFunction, ...args: Boxed) => Promise; export type EvaluateHandle = (pageFunction: PageFunction, ...args: Boxed) => Promise>; -export type $Eval = (selector: S, pageFunction: PageFunctionOn, Args, R>, ...args: Boxed) => Promise; -export type $$Eval = (selector: S, pageFunction: PageFunctionOn[], Args, R>, ...args: Boxed) => Promise; +export type $Eval = (selector: S, pageFunction: PageFunctionOn, Args, R>, ...args: Boxed) => Promise; +export type $$Eval = (selector: S, pageFunction: PageFunctionOn[], Args, R>, ...args: Boxed) => Promise; export type EvaluateOn = (pageFunction: PageFunctionOn, ...args: Boxed) => Promise; export type EvaluateHandleOn = (pageFunction: PageFunctionOn, ...args: Boxed) => Promise>; @@ -26,32 +25,10 @@ export type Quad = [ Point, Point, Point, Point ]; export type TimeoutOptions = { timeout?: number }; export type Visibility = 'visible' | 'hidden' | 'any'; -export type Selector = { selector: string, visibility?: Visibility }; export type Polling = 'raf' | 'mutation' | number; export type WaitForFunctionOptions = TimeoutOptions & { polling?: Polling }; -export function selectorToString(selector: string | Selector): string { - if (typeof selector === 'string') - return selector; - let label; - switch (selector.visibility) { - case 'visible': label = '[visible] '; break; - case 'hidden': label = '[hidden] '; break; - case 'any': - case undefined: - label = ''; break; - } - return `${label}${selector.selector}`; -} - -// Ensures that we don't use accidental properties in selector, e.g. scope. -export function clearSelector(selector: string | Selector): string | Selector { - if (helper.isString(selector)) - return selector; - return { selector: selector.selector, visibility: selector.visibility }; -} - export type ElementScreenshotOptions = { type?: 'png' | 'jpeg', path?: string, diff --git a/test/chromium/connect.spec.js b/test/chromium/connect.spec.js index 230819b6f4..97f831e159 100644 --- a/test/chromium/connect.spec.js +++ b/test/chromium/connect.spec.js @@ -119,7 +119,7 @@ module.exports.addTests = function({testRunner, expect, defaultBrowserOptions, p const browser = await playwright.launch(defaultBrowserOptions); const remote = await playwright.connect({...defaultBrowserOptions, browserWSEndpoint: browser.chromium.wsEndpoint()}); const page = await remote.newPage(); - const watchdog = page.waitForSelector('div', {timeout: 60000}).catch(e => e); + const watchdog = page.$('div', { waitFor: true, timeout: 60000 }).catch(e => e); remote.disconnect(); const error = await watchdog; expect(error.message).toContain('Protocol error'); diff --git a/test/chromium/headful.spec.js b/test/chromium/headful.spec.js index 649602cac1..6027adc2fb 100644 --- a/test/chromium/headful.spec.js +++ b/test/chromium/headful.spec.js @@ -99,7 +99,7 @@ module.exports.addTests = function({testRunner, expect, playwright, defaultBrows document.body.appendChild(frame); return new Promise(x => frame.onload = x); }); - await page.waitForSelector('iframe[src="https://google.com/"]'); + await page.$('iframe[src="https://google.com/"]', { waitFor: true }); const urls = page.frames().map(frame => frame.url()).sort(); expect(urls).toEqual([ server.EMPTY_PAGE, diff --git a/test/click.spec.js b/test/click.spec.js index 4e26fb0bc9..ec9803ac90 100644 --- a/test/click.spec.js +++ b/test/click.spec.js @@ -126,31 +126,44 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME ]); }); - it('should respect selector visibilty', async({page, server}) => { + it('should waitFor visible when already visible', async({page, server}) => { await page.goto(server.PREFIX + '/input/button.html'); - await page.click({selector: 'button', visibility: 'visible'}); + await page.click('button', { waitFor: 'visible' }); expect(await page.evaluate(() => result)).toBe('Clicked'); - + }); + it('should waitFor hidden when already hidden', async({page, server}) => { let error = null; await page.goto(server.PREFIX + '/input/button.html'); - await page.click({selector: 'button', visibility: 'hidden'}).catch(e => error = e); - expect(error.message).toBe('No node found for selector: [hidden] button'); - expect(await page.evaluate(() => result)).toBe('Was not clicked'); - - error = null; - await page.goto(server.PREFIX + '/input/button.html'); await page.$eval('button', b => b.style.display = 'none'); - await page.click({selector: 'button', visibility: 'visible'}).catch(e => error = e); - expect(error.message).toBe('No node found for selector: [visible] button'); - expect(await page.evaluate(() => result)).toBe('Was not clicked'); - - error = null; - await page.goto(server.PREFIX + '/input/button.html'); - await page.$eval('button', b => b.style.display = 'none'); - await page.click({selector: 'button', visibility: 'hidden'}).catch(e => error = e); + await page.click('button', { waitFor: 'hidden' }).catch(e => error = e); expect(error.message).toBe('Node is either not visible or not an HTMLElement'); expect(await page.evaluate(() => result)).toBe('Was not clicked'); }); + it('should waitFor hidden', async({page, server}) => { + let error = null; + await page.goto(server.PREFIX + '/input/button.html'); + const clicked = page.click('button', { waitFor: 'hidden' }).catch(e => error = e); + for (let i = 0; i < 5; i++) + await page.evaluate('1'); // Do a round trip. + expect(error).toBe(null); + await page.$eval('button', b => b.style.display = 'none'); + await clicked; + expect(error.message).toBe('Node is either not visible or not an HTMLElement'); + expect(await page.evaluate(() => result)).toBe('Was not clicked'); + }); + it('should waitFor visible', async({page, server}) => { + let done = false; + await page.goto(server.PREFIX + '/input/button.html'); + await page.$eval('button', b => b.style.display = 'none'); + const clicked = page.click('button', { waitFor: 'visible' }).then(() => done = true); + for (let i = 0; i < 5; i++) + await page.evaluate('1'); // Do a round trip. + expect(done).toBe(false); + await page.$eval('button', b => b.style.display = 'block'); + await clicked; + expect(done).toBe(true); + expect(await page.evaluate(() => result)).toBe('Clicked'); + }); it('should click wrapped links', async({page, server}) => { await page.goto(server.PREFIX + '/wrappedlink.html'); diff --git a/test/page.spec.js b/test/page.spec.js index 4953eb02f4..36676d1f0e 100644 --- a/test/page.spec.js +++ b/test/page.spec.js @@ -1207,19 +1207,16 @@ module.exports.addTests = function({testRunner, expect, headless, playwright, FF }); it('should respect selector visibilty', async({page, server}) => { await page.goto(server.PREFIX + '/input/textarea.html'); - await page.fill({selector: 'input', visibility: 'visible'}, 'some value'); + await page.fill('input', 'some value', { waitFor: 'visible' }); expect(await page.evaluate(() => result)).toBe('some value'); - let error = null; - await page.goto(server.PREFIX + '/input/textarea.html'); - await page.fill({selector: 'input', visibility: 'hidden'}, 'some value').catch(e => error = e); - expect(error.message).toBe('No node found for selector: [hidden] input'); - - error = null; await page.goto(server.PREFIX + '/input/textarea.html'); await page.$eval('input', i => i.style.display = 'none'); - await page.fill({selector: 'input', visibility: 'visible'}, 'some value').catch(e => error = e); - expect(error.message).toBe('No node found for selector: [visible] input'); + await Promise.all([ + page.fill('input', 'some value', { waitFor: 'visible' }), + page.$eval('input', i => i.style.display = 'block'), + ]); + expect(await page.evaluate(() => result)).toBe('some value'); }); it('should throw on disabled and readonly elements', async({page, server}) => { await page.goto(server.PREFIX + '/input/textarea.html'); diff --git a/test/queryselector.spec.js b/test/queryselector.spec.js index 4f29d94ede..4e74c9a7f0 100644 --- a/test/queryselector.spec.js +++ b/test/queryselector.spec.js @@ -61,19 +61,6 @@ module.exports.addTests = function({testRunner, expect, product, FFOX, CHROME, W const idAttribute = await page.$eval('section', e => e.id); expect(idAttribute).toBe('testAttribute'); }); - it('should respect visibility', async({page, server}) => { - let error = null; - await page.setContent(''); - await page.$eval({selector: 'css=section', visibility: 'visible'}, e => e.id).catch(e => error = e); - expect(error.message).toContain('failed to find element matching selector "[visible] css=section"'); - expect(await page.$eval({selector: 'css=section', visibility: 'hidden'}, e => e.id)).toBe('testAttribute'); - - error = null; - await page.setContent('
43543
'); - await page.$eval({selector: 'css=section', visibility: 'hidden'}, e => e.id).catch(e => error = e); - expect(error.message).toContain('failed to find element matching selector "[hidden] css=section"'); - expect(await page.$eval({selector: 'css=section', visibility: 'visible'}, e => e.id)).toBe('testAttribute'); - }); it('should accept arguments', async({page, server}) => { await page.setContent('
hello
'); const text = await page.$eval('section', (e, suffix) => e.textContent + suffix, ' world!'); @@ -153,12 +140,6 @@ module.exports.addTests = function({testRunner, expect, product, FFOX, CHROME, W const spansCount = await page.$$eval('css=div >> css=div >> css=span', spans => spans.length); expect(spansCount).toBe(2); }); - it('should respect visibility', async({page, server}) => { - await page.setContent('
1
2
3
'); - expect(await page.$$eval({selector: 'css=section', visibility: 'visible'}, x => x.length)).toBe(1); - expect(await page.$$eval({selector: 'css=section', visibility: 'hidden'}, x => x.length)).toBe(2); - expect(await page.$$eval({selector: 'css=section'}, x => x.length)).toBe(3); - }); }); describe('Page.$', function() { @@ -181,6 +162,25 @@ module.exports.addTests = function({testRunner, expect, product, FFOX, CHROME, W const element = await page.$('non-existing-element'); expect(element).toBe(null); }); + it('should return null for non-existing element with waitFor:false', async({page, server}) => { + const element = await page.$('non-existing-element', { waitFor: false }); + expect(element).toBe(null); + }); + it('should query existing element with waitFor:false', async({page, server}) => { + await page.setContent('
test
'); + const element = await page.$('css=section', { waitFor: false }); + expect(element).toBeTruthy(); + }); + it('should throw for unknown waitFor option', async({page, server}) => { + await page.setContent('
test
'); + const error = await page.$('section', { waitFor: 'foo' }).catch(e => e); + expect(error.message).toContain('Unsupported waitFor option'); + }); + it('should throw for numeric waitFor option', async({page, server}) => { + await page.setContent('
test
'); + const error = await page.$('section', { waitFor: 123 }).catch(e => e); + expect(error.message).toContain('Unsupported waitFor option'); + }); it('should auto-detect xpath selector', async({page, server}) => { await page.setContent('
test
'); const element = await page.$('//html/body/section'); @@ -201,16 +201,16 @@ module.exports.addTests = function({testRunner, expect, product, FFOX, CHROME, W const element = await page.$('css=section >> css=div'); expect(element).toBeTruthy(); }); - it('should respect visibility', async({page, server}) => { + it('should respect waitFor visibility', async({page, server}) => { await page.setContent('
43543
'); - expect(await page.$({selector: 'css=section', visibility: 'visible'})).toBeTruthy(); - expect(await page.$({selector: 'css=section', visibility: 'hidden'})).not.toBeTruthy(); - expect(await page.$({selector: 'css=section'})).toBeTruthy(); + expect(await page.$('css=section', { waitFor: 'visible'})).toBeTruthy(); + expect(await page.$('css=section', { waitFor: 'any'})).toBeTruthy(); + expect(await page.$('css=section')).toBeTruthy(); await page.setContent(''); - expect(await page.$({selector: 'css=section', visibility: 'visible'})).not.toBeTruthy(); - expect(await page.$({selector: 'css=section', visibility: 'hidden'})).toBeTruthy(); - expect(await page.$({selector: 'css=section'})).toBeTruthy(); + expect(await page.$('css=section', { waitFor: 'hidden'})).toBeTruthy(); + expect(await page.$('css=section', { waitFor: 'any'})).toBeTruthy(); + expect(await page.$('css=section')).toBeTruthy(); }); }); @@ -227,12 +227,6 @@ module.exports.addTests = function({testRunner, expect, product, FFOX, CHROME, W const elements = await page.$$('div'); expect(elements.length).toBe(0); }); - it('should respect visibility', async({page, server}) => { - await page.setContent('
1
2
3
'); - expect((await page.$$({selector: 'css=section', visibility: 'visible'})).length).toBe(1); - expect((await page.$$({selector: 'css=section', visibility: 'hidden'})).length).toBe(2); - expect((await page.$$({selector: 'css=section'})).length).toBe(3); - }); }); describe('Path.$x', function() { @@ -281,22 +275,6 @@ module.exports.addTests = function({testRunner, expect, product, FFOX, CHROME, W const second = await html.$('.third'); expect(second).toBe(null); }); - - it('should respect visibility', async({page, server}) => { - await page.goto(server.PREFIX + '/playground.html'); - await page.setContent('
'); - const second = await page.$('html .second'); - - let inner = await second.$({selector: '.inner', visibility: 'visible'}); - expect(inner).not.toBeTruthy(); - - inner = await second.$({selector: '.inner', visibility: 'hidden'}); - expect(await inner.evaluate(e => e.textContent)).toBe('A'); - - await inner.evaluate(e => e.style.display = 'block'); - inner = await second.$({selector: '.inner', visibility: 'visible'}); - expect(await inner.evaluate(e => e.textContent)).toBe('A'); - }); }); describe('ElementHandle.$eval', function() { it('should work', async({page, server}) => { diff --git a/test/waittask.spec.js b/test/waittask.spec.js index 4ed1b01f61..de4cf887e9 100644 --- a/test/waittask.spec.js +++ b/test/waittask.spec.js @@ -205,21 +205,21 @@ module.exports.addTests = function({testRunner, expect, product, playwright, FFO }); }); - describe('Frame.waitForSelector', function() { + describe('Frame.$ waitFor', 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.$('*', { waitFor: true }); await frame.evaluate(addElement, 'div'); - await frame.waitForSelector('div'); + await frame.$('div', { waitFor: true }); }); it.skip(FFOX)('should work with removed MutationObserver', async({page, server}) => { await page.evaluate(() => delete window.MutationObserver); const [handle] = await Promise.all([ - page.waitForSelector('.zombo'), + page.$('.zombo', { waitFor: true }), page.setContent(`
anything
`), ]); expect(await page.evaluate(x => x.textContent, handle)).toBe('anything'); @@ -228,7 +228,7 @@ module.exports.addTests = 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.$('div', { waitFor: true }); await frame.evaluate(addElement, 'br'); await frame.evaluate(addElement, 'div'); const eHandle = await watchdog; @@ -238,17 +238,17 @@ module.exports.addTests = 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.$('h3 div', { waitFor: true }); await page.evaluate(addElement, 'span'); await page.evaluate(() => document.querySelector('span').innerHTML = '

'); await watchdog; }); - it('Page.waitForSelector is shortcut for main frame', async({page, server}) => { + it('Page.$ waitFor is shortcut for main frame', async({page, server}) => { 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.$('div', { waitFor: true }); await otherFrame.evaluate(addElement, 'div'); await page.evaluate(addElement, 'div'); const eHandle = await watchdog; @@ -260,7 +260,7 @@ module.exports.addTests = 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 waitForSelectorPromise = frame2.$('div', { waitFor: true }); await frame1.evaluate(addElement, 'div'); await frame2.evaluate(addElement, 'div'); const eHandle = await waitForSelectorPromise; @@ -271,7 +271,7 @@ module.exports.addTests = 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('.box').catch(e => waitError = e); + const waitPromise = frame.$('.box', { waitFor: true }).catch(e => waitError = e); await utils.detachFrame(page, 'frame1'); await waitPromise; expect(waitError).toBeTruthy(); @@ -279,7 +279,7 @@ module.exports.addTests = 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 waitForSelector = page.$('.box', { waitFor: true }).then(() => boxFound = true); await page.goto(server.EMPTY_PAGE); expect(boxFound).toBe(false); await page.reload(); @@ -290,7 +290,7 @@ module.exports.addTests = function({testRunner, expect, product, playwright, FFO }); it('should wait for visible', async({page, server}) => { let divFound = false; - const waitForSelector = page.waitForSelector({selector: 'div', visibility: 'visible'}).then(() => divFound = true); + const waitForSelector = page.$('div', { waitFor: 'visible' }).then(() => divFound = true); await page.setContent(`
1
`); expect(divFound).toBe(false); await page.evaluate(() => document.querySelector('div').style.removeProperty('display')); @@ -301,7 +301,7 @@ module.exports.addTests = function({testRunner, expect, product, playwright, FFO }); it('should wait for visible recursively', async({page, server}) => { let divVisible = false; - const waitForSelector = page.waitForSelector({selector: 'div#inner', visibility: 'visible'}).then(() => divVisible = true); + const waitForSelector = page.$('div#inner', { waitFor: 'visible' }).then(() => divVisible = true); await page.setContent(`
hi
`); expect(divVisible).toBe(false); await page.evaluate(() => document.querySelector('div').style.removeProperty('display')); @@ -313,8 +313,8 @@ module.exports.addTests = function({testRunner, expect, product, playwright, FFO it('hidden should wait for visibility: hidden', async({page, server}) => { let divHidden = false; await page.setContent(`
`); - const waitForSelector = page.waitForSelector({selector: 'div', visibility: 'hidden'}).then(() => divHidden = true); - await page.waitForSelector('div'); // do a round trip + const waitForSelector = page.$('div', { waitFor: 'hidden' }).then(() => divHidden = true); + await page.$('div', { waitFor: true }); // do a round trip expect(divHidden).toBe(false); await page.evaluate(() => document.querySelector('div').style.setProperty('visibility', 'hidden')); expect(await waitForSelector).toBe(true); @@ -323,8 +323,8 @@ module.exports.addTests = function({testRunner, expect, product, playwright, FFO it('hidden should wait for display: none', async({page, server}) => { let divHidden = false; await page.setContent(`
`); - const waitForSelector = page.waitForSelector({selector: 'div', visibility: 'hidden'}).then(() => divHidden = true); - await page.waitForSelector('div'); // do a round trip + const waitForSelector = page.$('div', { waitFor: 'hidden' }).then(() => divHidden = true); + await page.$('div', { waitFor: true }); // do a round trip expect(divHidden).toBe(false); await page.evaluate(() => document.querySelector('div').style.setProperty('display', 'none')); expect(await waitForSelector).toBe(true); @@ -333,20 +333,20 @@ module.exports.addTests = function({testRunner, expect, product, playwright, FFO it('hidden should wait for removal', async({page, server}) => { await page.setContent(`
`); let divRemoved = false; - const waitForSelector = page.waitForSelector({selector: 'div', visibility: 'hidden'}).then(() => divRemoved = true); - await page.waitForSelector('div'); // do a round trip + const waitForSelector = page.$('div', { waitFor: 'hidden' }).then(() => divRemoved = true); + await page.$('div', { waitFor: true }); // do a round trip expect(divRemoved).toBe(false); await page.evaluate(() => document.querySelector('div').remove()); expect(await waitForSelector).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({selector: 'non-existing', visibility: 'hidden' }); + const handle = await page.$('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.$('div', { waitFor: true, 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); @@ -354,34 +354,34 @@ module.exports.addTests = 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({selector: 'div', visibility: 'hidden'}, {timeout: 10}).catch(e => error = e); + await page.$('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 waitForSelector = page.$('.zombo', { waitFor: true }).then(() => divFound = true); await page.setContent(`
`); expect(divFound).toBe(false); await page.evaluate(() => document.querySelector('div').className = 'zombo'); expect(await waitForSelector).toBe(true); }); it('should return the element handle', async({page, server}) => { - const waitForSelector = page.waitForSelector('.zombo'); + const waitForSelector = page.$('.zombo', { waitFor: true }); await page.setContent(`
anything
`); expect(await page.evaluate(x => x.textContent, await waitForSelector)).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.$('.zombo', { waitFor: true, timeout: 10 }).catch(e => error = e); expect(error.stack).toContain('waittask.spec.js'); }); 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.$('css=div >> css=span', { waitFor: true }); await frame.evaluate(addElement, 'br'); await frame.evaluate(addElement, 'div'); await frame.evaluate(() => document.querySelector('div').appendChild(document.createElement('span'))); @@ -391,19 +391,19 @@ module.exports.addTests = function({testRunner, expect, product, playwright, FFO }); }); - describe('Frame.waitForXPath', function() { + describe('Frame.$ waitFor 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.waitForXPath('//p[normalize-space(.)="hello world"]'); + const waitForXPath = page.$('//p[normalize-space(.)="hello world"]', { waitFor: true }); expect(await page.evaluate(x => x.textContent, await waitForXPath)).toBe('hello world '); }); it('should respect timeout', async({page}) => { let error = null; - await page.waitForXPath('//div', {timeout: 10}).catch(e => error = e); + await page.$('//div', { waitFor: true, timeout: 10 }).catch(e => error = e); expect(error).toBeTruthy(); - expect(error.message).toContain('waiting for selector "xpath=//div" failed: timeout'); + expect(error.message).toContain('waiting for selector "//div" failed: timeout'); expect(error).toBeInstanceOf(playwright.errors.TimeoutError); }); it('should run in specified frame', async({page, server}) => { @@ -411,7 +411,7 @@ module.exports.addTests = 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.waitForXPath('//div'); + const waitForXPathPromise = frame2.$('//div', { waitFor: true }); await frame1.evaluate(addElement, 'div'); await frame2.evaluate(addElement, 'div'); const eHandle = await waitForXPathPromise; @@ -421,20 +421,20 @@ module.exports.addTests = 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.waitForXPath('//*[@class="box"]').catch(e => waitError = e); + const waitPromise = frame.$('//*[@class="box"]', { waitFor: true }).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.waitForXPath('//*[@class="zombo"]'); + const waitForXPath = page.$('//*[@class="zombo"]', { waitFor: true }); 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.waitForXPath('/html/body/div'); + const waitForXPath = page.$('//html/body/div', { waitFor: true }); expect(await page.evaluate(x => x.textContent, await waitForXPath)).toBe('some text'); }); });