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 }).
This commit is contained in:
Dmitry Gozman 2019-12-17 14:30:02 -08:00 committed by Pavel Feldman
parent 75ae9bfeee
commit fd927000ea
11 changed files with 221 additions and 308 deletions

View file

@ -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<T extends Node>(handle: ElementHandle<T>): Promise<ElementHandle<T>> {
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<ResolvedSelector> {
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<ElementHandle<Element> | null> {
const resolved = await this._resolveSelector(selector);
async _$(selector: string, scope?: ElementHandle): Promise<ElementHandle<Element> | 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<ElementHandle<Element>[]> {
const resolved = await this._resolveSelector(selector);
async _$array(selector: string, scope?: ElementHandle): Promise<js.JSHandle<Element[]>> {
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<ElementHandle<Element>[]> {
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<string | ScopedSelector> = 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<string | ScopedSelector> = 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<T extends Node = Node> extends js.JSHandle<T> {
@ -348,7 +294,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
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<T extends Node = Node> extends js.JSHandle<T> {
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<ElementHandle | null> {
return this._context._$(selector, this);
}
$(selector: string | types.Selector): Promise<ElementHandle | null> {
return this._context._$(this._scopedSelector(selector));
$$(selector: string): Promise<ElementHandle<Element>[]> {
return this._context._$$(selector, this);
}
$$(selector: string | types.Selector): Promise<ElementHandle<Element>[]> {
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<string | types.Selector> = (selector, pageFunction, ...args) => {
return this._context._$eval(this._scopedSelector(selector), pageFunction, ...args as any);
}
$$eval: types.$$Eval<string | types.Selector> = (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<ElementHandle<Element>[]> {
return this._context._$$({ scope: this, selector: 'xpath=' + expression });
return this._context._$$('xpath=' + expression, this);
}
isIntersectingViewport(): Promise<boolean> {
@ -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);
};
}

View file

@ -53,7 +53,7 @@ export type GotoResult = {
export type LifecycleEvent = 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2';
const kLifecycleEvents: Set<LifecycleEvent> = 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<dom.FrameExecutionContext> {
_context(contextType: ContextType): Promise<dom.FrameExecutionContext> {
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<dom.FrameExecutionContext> {
return this._context('main');
}
_utilityContext(): Promise<dom.FrameExecutionContext> {
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<dom.FrameExecutionContext> {
@ -366,9 +368,8 @@ export class Frame {
return context.evaluate(pageFunction, ...args as any);
}
async $(selector: string | types.Selector): Promise<dom.ElementHandle<Element> | null> {
const context = await this._mainContext();
return context._$(types.clearSelector(selector));
async $(selector: string, options?: WaitForOptions): Promise<dom.ElementHandle<Element> | null> {
return this._optionallyWaitForSelector('main', selector, options, true /* returnNull */);
}
async $x(expression: string): Promise<dom.ElementHandle<Element>[]> {
@ -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<dom.ElementHandle<Element>[]> {
async $$(selector: string): Promise<dom.ElementHandle<Element>[]> {
const context = await this._mainContext();
return context._$$(types.clearSelector(selector));
return context._$$(selector);
}
async content(): Promise<string> {
@ -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<string[]> {
const handle = await this._optionallyWaitForInUtilityContext(selector, options);
async select(selector: string, value: string | dom.ElementHandle | SelectOption | string[] | dom.ElementHandle[] | SelectOption[] | undefined, options?: WaitForOptions): Promise<string[]> {
const handle = await this._optionallyWaitForSelector('utility', selector, options);
const toDispose: Promise<dom.ElementHandle>[] = [];
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<js.JSHandle | null> {
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<dom.ElementHandle | null> {
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<dom.ElementHandle<Element> | null> {
const { timeout = this._page._timeoutSettings.timeout(), waitFor = undefined } = options;
let handle: dom.ElementHandle<Element> | 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<Element>;
} 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<dom.ElementHandle | null> {
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<dom.ElementHandle | null> {
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<dom.ElementHandle | null> {
return this.waitForSelector('xpath=' + xpath, options);
}
waitForFunction(pageFunction: Function | string, options: types.WaitForFunctionOptions = {}, ...args: any[]): Promise<js.JSHandle> {
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}`;
}

View file

@ -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]) {

View file

@ -186,8 +186,8 @@ export class Page extends EventEmitter {
this._timeoutSettings.setDefaultTimeout(timeout);
}
async $(selector: string | types.Selector): Promise<dom.ElementHandle<Element> | null> {
return this.mainFrame().$(selector);
async $(selector: string, options?: frames.WaitForOptions): Promise<dom.ElementHandle<Element> | null> {
return this.mainFrame().$(selector, options);
}
async _createSelector(name: string, handle: dom.ElementHandle<Element>): Promise<string> {
@ -210,7 +210,7 @@ export class Page extends EventEmitter {
return this.mainFrame().$$eval(selector, pageFunction, ...args as any);
}
async $$(selector: string | types.Selector): Promise<dom.ElementHandle<Element>[]> {
async $$(selector: string): Promise<dom.ElementHandle<Element>[]> {
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<string[]> {
select(selector: string, value: string | dom.ElementHandle | input.SelectOption | string[] | dom.ElementHandle[] | input.SelectOption[] | undefined, options?: frames.WaitForOptions): Promise<string[]> {
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<dom.ElementHandle | null> {
return this.mainFrame().waitForSelector(selector, options);
}
waitForXPath(xpath: string, options: types.TimeoutOptions = {}): Promise<dom.ElementHandle | null> {
return this.mainFrame().waitForXPath(xpath, options);
}
waitForFunction(pageFunction: Function | string, options: types.WaitForFunctionOptions, ...args: any[]): Promise<js.JSHandle> {
return this.mainFrame().waitForFunction(pageFunction, options, ...args);
}

View file

@ -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<Args extends any[]> = { [Index in keyof Args]: Args[Index] | js.JSHandle<Args[Index]> };
@ -14,8 +13,8 @@ type ElementForSelector<T> = T extends keyof HTMLElementTagNameMap ? HTMLElement
export type Evaluate = <Args extends any[], R>(pageFunction: PageFunction<Args, R>, ...args: Boxed<Args>) => Promise<R>;
export type EvaluateHandle = <Args extends any[], R>(pageFunction: PageFunction<Args, R>, ...args: Boxed<Args>) => Promise<Handle<R>>;
export type $Eval<O = string | Selector> = <Args extends any[], R, S extends O>(selector: S, pageFunction: PageFunctionOn<ElementForSelector<S>, Args, R>, ...args: Boxed<Args>) => Promise<R>;
export type $$Eval<O = string | Selector> = <Args extends any[], R, S extends O>(selector: S, pageFunction: PageFunctionOn<ElementForSelector<S>[], Args, R>, ...args: Boxed<Args>) => Promise<R>;
export type $Eval = <Args extends any[], R, S extends string>(selector: S, pageFunction: PageFunctionOn<ElementForSelector<S>, Args, R>, ...args: Boxed<Args>) => Promise<R>;
export type $$Eval = <Args extends any[], R, S extends string>(selector: S, pageFunction: PageFunctionOn<ElementForSelector<S>[], Args, R>, ...args: Boxed<Args>) => Promise<R>;
export type EvaluateOn<T> = <Args extends any[], R>(pageFunction: PageFunctionOn<T, Args, R>, ...args: Boxed<Args>) => Promise<R>;
export type EvaluateHandleOn<T> = <Args extends any[], R>(pageFunction: PageFunctionOn<T, Args, R>, ...args: Boxed<Args>) => Promise<Handle<R>>;
@ -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,

View file

@ -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');

View file

@ -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,

View file

@ -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');

View file

@ -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');

View file

@ -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('<section id="testAttribute" style="display: none">43543</section>');
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('<section id="testAttribute">43543</section>');
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('<section>hello</section>');
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('<section style="display: none">1</section><section style="display: none">2</section><section>3</section>');
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('<section>test</section>');
const element = await page.$('css=section', { waitFor: false });
expect(element).toBeTruthy();
});
it('should throw for unknown waitFor option', async({page, server}) => {
await page.setContent('<section>test</section>');
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('<section>test</section>');
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('<section>test</section>');
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('<section id="testAttribute">43543</section>');
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('<section id="testAttribute" style="display: none">43543</section>');
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('<section style="display: none">1</section><section style="display: none">2</section><section>3</section>');
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('<html><body><div class="second"><div class="inner" style="display:none">A</div></div></body></html>');
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}) => {

View file

@ -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(`<div class='zombo'>anything</div>`),
]);
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 = '<h3><div></div></h3>');
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(`<div style='display: none; visibility: hidden;'>1</div>`);
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(`<div style='display: none; visibility: hidden;'><div id="inner">hi</div></div>`);
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(`<div style='display: block;'></div>`);
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(`<div style='display: block;'></div>`);
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(`<div></div>`);
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(`<div></div>`);
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(`<div class='notZombo'></div>`);
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(`<div class='zombo'>anything</div>`);
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(`<p>red herring</p><p>hello world </p>`);
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(`<div class='zombo'>anything</div>`);
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(`<div>some text</div>`);
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');
});
});