feat(selectors): support optional "visible" property in all selectors (#129)

This commit is contained in:
Dmitry Gozman 2019-12-04 13:11:10 -08:00 committed by GitHub
parent e358b47f76
commit fc5898892b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 397 additions and 209 deletions

View File

@ -310,6 +310,9 @@
* [coverage.stopCSSCoverage()](#coveragestopcsscoverage)
* [coverage.stopJSCoverage()](#coveragestopjscoverage)
- [class: TimeoutError](#class-timeouterror)
- [class: Selector](#class-selector)
* [selector.selector](#selectorselector)
* [selector.visible](#selectorvisible)
<!-- GEN:stop -->
### Overview
@ -1049,7 +1052,7 @@ Emitted when a request finishes successfully.
Emitted when a [response] is received.
#### page.$(selector)
- `selector` <[string]> A [selector] to query page for
- `selector` <[string]|[Selector]> A [selector] to query page for
- returns: <[Promise]<?[ElementHandle]>>
The method runs `document.querySelector` within the page. If no element matches the selector, the return value resolves to `null`.
@ -1057,7 +1060,7 @@ The method runs `document.querySelector` within the page. If no element matches
Shortcut for [page.mainFrame().$(selector)](#frameselector).
#### page.$$(selector)
- `selector` <[string]> A [selector] to query page for
- `selector` <[string]|[Selector]> A [selector] to query page for
- returns: <[Promise]<[Array]<[ElementHandle]>>>
The method runs `document.querySelectorAll` within the page. If no elements match the selector, the return value resolves to `[]`.
@ -1065,7 +1068,7 @@ The method runs `document.querySelectorAll` within the page. If no elements matc
Shortcut for [page.mainFrame().$$(selector)](#frameselector-1).
#### page.$$eval(selector, pageFunction[, ...args])
- `selector` <[string]> A [selector] to query page for
- `selector` <[string]|[Selector]> A [selector] to query page for
- `pageFunction` <[function]\([Array]<[Element]>\)> Function to be evaluated in browser context
- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction`
- returns: <[Promise]<[Serializable]>> Promise which resolves to the return value of `pageFunction`
@ -1080,7 +1083,7 @@ const divsCounts = await page.$$eval('div', divs => divs.length);
```
#### page.$eval(selector, pageFunction[, ...args])
- `selector` <[string]> A [selector] to query page for
- `selector` <[string]|[Selector]> A [selector] to query page for
- `pageFunction` <[function]\([Element]\)> Function to be evaluated in browser context
- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction`
- returns: <[Promise]<[Serializable]>> Promise which resolves to the return value of `pageFunction`
@ -1145,7 +1148,7 @@ Get the browser the page belongs to.
Get the browser context that the page belongs to.
#### page.click(selector[, options])
- `selector` <[string]> A [selector] to search for element to click. If there are multiple elements satisfying the selector, the first will be clicked.
- `selector` <[string]|[Selector]> A [selector] to search for element to click. If there are multiple elements satisfying the selector, the first will be clicked.
- `options` <[Object]>
- `button` <"left"|"right"|"middle"> Defaults to `left`.
- `clickCount` <[number]> defaults to 1. See [UIEvent.detail].
@ -1192,7 +1195,7 @@ Gets the full HTML contents of the page, including the doctype.
- returns: <[Coverage]>
#### page.dblclick(selector[, options])
- `selector` <[string]> A [selector] to search for element to double click. If there are multiple elements satisfying the selector, the first will be double clicked.
- `selector` <[string]|[Selector]> A [selector] to search for element to double click. If there are multiple elements satisfying the selector, the first will be double clicked.
- `options` <[Object]>
- `button` <"left"|"right"|"middle"> Defaults to `left`.
- `delay` <[number]> Time to wait between `mousedown` and `mouseup` in milliseconds. Defaults to 0.
@ -1431,7 +1434,7 @@ const fs = require('fs');
```
#### page.fill(selector, value)
- `selector` <[string]> A [selector] to query page for.
- `selector` <[string]|[Selector]> A [selector] to query page for.
- `value` <[string]> Value to fill for the `<input>`, `<textarea>` or `[contenteditable]` element.
- returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully filled. The promise will be rejected if there is no element matching `selector`.
@ -1441,7 +1444,7 @@ If there's no text `<input>`, `<textarea>` or `[contenteditable]` element matchi
Shortcut for [page.mainFrame().fill()](#framefillselector-value)
#### page.focus(selector)
- `selector` <[string]> A [selector] of an element to focus. If there are multiple elements satisfying the selector, the first will be focused.
- `selector` <[string]|[Selector]> A [selector] of an element to focus. If there are multiple elements satisfying the selector, the first will be focused.
- returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully focused. The promise will be rejected if there is no element matching `selector`.
This method fetches an element with `selector` and focuses it.
@ -1506,7 +1509,7 @@ Navigate to the next page in history.
Shortcut for [page.mainFrame().goto(url, options)](#framegotourl-options)
#### page.hover(selector[, options])
- `selector` <[string]> A [selector] to search for element to hover. If there are multiple elements satisfying the selector, the first will be hovered.
- `selector` <[string]|[Selector]> A [selector] to search for element to hover. If there are multiple elements satisfying the selector, the first will be hovered.
- `options` <[Object]>
- `relativePoint` <[Object]> A point to hover relative to the top-left corner of element padding box. If not specified, hovers over some visible point of the element.
- x <[number]>
@ -1575,7 +1578,7 @@ Page is guaranteed to have a main frame which persists during navigations.
> **NOTE** Screenshots take at least 1/6 second on OS X. See https://crbug.com/741689 for discussion.
#### page.select(selector, ...values)
- `selector` <[string]> A [selector] to query page for.
- `selector` <[string]|[Selector]> A [selector] to query page for.
- `...values` <...[string]|[ElementHandle]|[Object]> Options to select. If the `<select>` has the `multiple` attribute, all matching options are selected, otherwise only the first option matching one of the passed options is selected. String values are equivalent to `{value:'string'}`. Option is considered matching if all specified properties match.
- `value` <[string]> Matches by `option.value`.
- `label` <[string]> Matches by `option.label`.
@ -1714,7 +1717,7 @@ await page.goto('https://example.com');
Shortcut for [page.mainFrame().title()](#frametitle).
#### page.tripleclick(selector[, options])
- `selector` <[string]> A [selector] to search for element to triple click. If there are multiple elements satisfying the selector, the first will be triple clicked.
- `selector` <[string]|[Selector]> A [selector] to search for element to triple click. If there are multiple elements satisfying the selector, the first will be triple clicked.
- `options` <[Object]>
- `button` <"left"|"right"|"middle"> Defaults to `left`.
- `delay` <[number]> Time to wait between `mousedown` and `mouseup` in milliseconds. Defaults to 0.
@ -1734,7 +1737,7 @@ Bear in mind that if the first or second click of the `tripleclick()` triggers a
Shortcut for [page.mainFrame().tripleclick(selector[, options])](#frametripleclickselector-options).
#### page.type(selector, text[, options])
- `selector` <[string]> A [selector] of an element to type into. If there are multiple elements satisfying the selector, the first will be used.
- `selector` <[string]|[Selector]> A [selector] of an element to type into. If there are multiple elements satisfying the selector, the first will be used.
- `text` <[string]> A text to type into a focused element.
- `options` <[Object]>
- `delay` <[number]> Time to wait between key presses in milliseconds. Defaults to 0.
@ -1907,10 +1910,8 @@ return finalResponse.ok();
```
#### page.waitForSelector(selector[, options])
- `selector` <[string]> A [selector] of an element to wait for
- `selector` <[string]|[Selector]> A [selector] of an element to wait for
- `options` <[Object]> Optional waiting parameters
- `visible` <[boolean]> wait for element to be present in DOM and to be visible, i.e. to not have `display: none` or `visibility: hidden` CSS properties. Defaults to `false`.
- `hidden` <[boolean]> wait for element to not be found in the DOM or to be hidden, i.e. have `display: none` or `visibility: hidden` CSS properties. Defaults to `false`.
- `timeout` <[number]> maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can be changed by using the [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) method.
- returns: <[Promise]<?[ElementHandle]>> Promise which resolves when element specified by selector string is added to DOM. Resolves to `null` if waiting for `hidden: true` and selector is not found in DOM.
@ -1940,8 +1941,6 @@ Shortcut for [page.mainFrame().waitForSelector(selector[, options])](#framewaitf
#### page.waitForXPath(xpath[, options])
- `xpath` <[string]> A [xpath] of an element to wait for
- `options` <[Object]> Optional waiting parameters
- `visible` <[boolean]> wait for element to be present in DOM and to be visible, i.e. to not have `display: none` or `visibility: hidden` CSS properties. Defaults to `false`.
- `hidden` <[boolean]> wait for element to not be found in the DOM or to be hidden, i.e. have `display: none` or `visibility: hidden` CSS properties. Defaults to `false`.
- `timeout` <[number]> maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can be changed by using the [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) method.
- returns: <[Promise]<?[ElementHandle]>> Promise which resolves when element specified by xpath string is added to DOM. Resolves to `null` if waiting for `hidden: true` and xpath is not found in DOM.
@ -2503,19 +2502,19 @@ An example of getting text from an iframe element:
```
#### frame.$(selector)
- `selector` <[string]> A [selector] to query frame for
- `selector` <[string]|[Selector]> A [selector] to query frame for
- returns: <[Promise]<?[ElementHandle]>> Promise which resolves to ElementHandle pointing to the frame element.
The method queries frame for the selector. If there's no such element within the frame, the method will resolve to `null`.
#### frame.$$(selector)
- `selector` <[string]> A [selector] to query frame for
- `selector` <[string]|[Selector]> A [selector] to query frame for
- returns: <[Promise]<[Array]<[ElementHandle]>>> Promise which resolves to ElementHandles pointing to the frame elements.
The method runs `document.querySelectorAll` within the frame. If no elements match the selector, the return value resolves to `[]`.
#### frame.$$eval(selector, pageFunction[, ...args])
- `selector` <[string]> A [selector] to query frame for
- `selector` <[string]|[Selector]> A [selector] to query frame for
- `pageFunction` <[function]\([Array]<[Element]>\)> Function to be evaluated in browser context
- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction`
- returns: <[Promise]<[Serializable]>> Promise which resolves to the return value of `pageFunction`
@ -2530,7 +2529,7 @@ const divsCounts = await frame.$$eval('div', divs => divs.length);
```
#### frame.$eval(selector, pageFunction[, ...args])
- `selector` <[string]> A [selector] to query frame for
- `selector` <[string]|[Selector]> A [selector] to query frame for
- `pageFunction` <[function]\([Element]\)> Function to be evaluated in browser context
- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction`
- returns: <[Promise]<[Serializable]>> Promise which resolves to the return value of `pageFunction`
@ -2575,7 +2574,7 @@ Adds a `<link rel="stylesheet">` tag into the page with the desired url or a `<s
- returns: <[Array]<[Frame]>>
#### frame.click(selector[, options])
- `selector` <[string]> A [selector] to search for element to click. If there are multiple elements satisfying the selector, the first will be clicked.
- `selector` <[string]|[Selector]> A [selector] to search for element to click. If there are multiple elements satisfying the selector, the first will be clicked.
- `options` <[Object]>
- `button` <"left"|"right"|"middle"> Defaults to `left`.
- `clickCount` <[number]> defaults to 1. See [UIEvent.detail].
@ -2604,7 +2603,7 @@ const [response] = await Promise.all([
Gets the full HTML contents of the frame, including the doctype.
#### frame.dblclick(selector[, options])
- `selector` <[string]> A [selector] to search for element to double click. If there are multiple elements satisfying the selector, the first will be double clicked.
- `selector` <[string]|[Selector]> A [selector] to search for element to double click. If there are multiple elements satisfying the selector, the first will be double clicked.
- `options` <[Object]>
- `button` <"left"|"right"|"middle"> Defaults to `left`.
- `delay` <[number]> Time to wait between `mousedown` and `mouseup` in milliseconds. Defaults to 0.
@ -2685,7 +2684,7 @@ await resultHandle.dispose();
Returns promise that resolves to the frame's default execution context.
#### frame.fill(selector, value)
- `selector` <[string]> A [selector] to query page for.
- `selector` <[string]|[Selector]> A [selector] to query page for.
- `value` <[string]> Value to fill for the `<input>`, `<textarea>` or `[contenteditable]` element.
- returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully filled. The promise will be rejected if there is no element matching `selector`.
@ -2693,7 +2692,7 @@ This method focuses the element and triggers an `input` event after filling.
If there's no text `<input>`, `<textarea>` or `[contenteditable]` element matching `selector`, the method throws an error.
#### frame.focus(selector)
- `selector` <[string]> A [selector] of an element to focus. If there are multiple elements satisfying the selector, the first will be focused.
- `selector` <[string]|[Selector]> A [selector] of an element to focus. If there are multiple elements satisfying the selector, the first will be focused.
- returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully focused. The promise will be rejected if there is no element matching `selector`.
This method fetches an element with `selector` and focuses it.
@ -2726,7 +2725,7 @@ If there's no element matching `selector`, the method throws an error.
#### frame.hover(selector[, options])
- `selector` <[string]> A [selector] to search for element to hover. If there are multiple elements satisfying the selector, the first will be hovered.
- `selector` <[string]|[Selector]> A [selector] to search for element to hover. If there are multiple elements satisfying the selector, the first will be hovered.
- `options` <[Object]>
- `relativePoint` <[Object]> A point to hover relative to the top-left corner of element padding box. If not specified, hovers over some visible point of the element.
- x <[number]>
@ -2755,7 +2754,7 @@ If the name is empty, returns the id attribute instead.
- returns: <?[Frame]> Parent frame, if any. Detached frames and main frames return `null`.
#### frame.select(selector, ...values)
- `selector` <[string]> A [selector] to query frame for.
- `selector` <[string]|[Selector]> A [selector] to query frame for.
- `...values` <...[string]|[ElementHandle]|[Object]> Options to select. If the `<select>` has the `multiple` attribute, all matching options are selected, otherwise only the first option matching one of the passed options is selected. String values are equivalent to `{value:'string'}`. Option is considered matching if all specified properties match.
- `value` <[string]> Matches by `option.value`.
- `label` <[string]> Matches by `option.label`.
@ -2794,7 +2793,7 @@ frame.select('select#colors', { value: 'blue' }, { index: 2 }, 'red');
- returns: <[Promise]<[string]>> The page's title.
#### frame.tripleclick(selector[, options])
- `selector` <[string]> A [selector] to search for element to triple click. If there are multiple elements satisfying the selector, the first will be triple clicked.
- `selector` <[string]|[Selector]> A [selector] to search for element to triple click. If there are multiple elements satisfying the selector, the first will be triple clicked.
- `options` <[Object]>
- `button` <"left"|"right"|"middle"> Defaults to `left`.
- `delay` <[number]> Time to wait between `mousedown` and `mouseup` in milliseconds. Defaults to 0.
@ -2812,7 +2811,7 @@ Bear in mind that if the first or second click of the `tripleclick()` triggers a
> **NOTE** `frame.tripleclick()` dispatches three `click` events and a single `dblclick` event.
#### frame.type(selector, text[, options])
- `selector` <[string]> A [selector] of an element to type into. If there are multiple elements satisfying the selector, the first will be used.
- `selector` <[string]|[Selector]> A [selector] of an element to type into. If there are multiple elements satisfying the selector, the first will be used.
- `text` <[string]> A text to type into a focused element.
- `options` <[Object]>
- `delay` <[number]> Time to wait between key presses in milliseconds. Defaults to 0.
@ -2916,10 +2915,8 @@ const [response] = await Promise.all([
#### frame.waitForSelector(selector[, options])
- `selector` <[string]> A [selector] of an element to wait for
- `selector` <[string]|[Selector]> A [selector] of an element to wait for
- `options` <[Object]> Optional waiting parameters
- `visible` <[boolean]> wait for element to be present in DOM and to be visible, i.e. to not have `display: none` or `visibility: hidden` CSS properties. Defaults to `false`.
- `hidden` <[boolean]> wait for element to not be found in the DOM or to be hidden, i.e. have `display: none` or `visibility: hidden` CSS properties. Defaults to `false`.
- `timeout` <[number]> maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can be changed by using the [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) method.
- returns: <[Promise]<?[ElementHandle]>> Promise which resolves when element specified by selector string is added to DOM. Resolves to `null` if waiting for `hidden: true` and selector is not found in DOM.
@ -2948,8 +2945,6 @@ const playwright = require('playwright');
#### frame.waitForXPath(xpath[, options])
- `xpath` <[string]> A [xpath] of an element to wait for
- `options` <[Object]> Optional waiting parameters
- `visible` <[boolean]> wait for element to be present in DOM and to be visible, i.e. to not have `display: none` or `visibility: hidden` CSS properties. Defaults to `false`.
- `hidden` <[boolean]> wait for element to not be found in the DOM or to be hidden, i.e. have `display: none` or `visibility: hidden` CSS properties. Defaults to `false`.
- `timeout` <[number]> maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can be changed by using the [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) method.
- returns: <[Promise]<?[ElementHandle]>> Promise which resolves when element specified by xpath string is added to DOM. Resolves to `null` if waiting for `hidden: true` and xpath is not found in DOM.
@ -3281,19 +3276,19 @@ ElementHandle prevents DOM element from garbage collection unless the handle is
ElementHandle instances can be used as arguments in [`page.$eval()`](#pageevalselector-pagefunction-args) and [`page.evaluate()`](#pageevaluatepagefunction-args) methods.
#### elementHandle.$(selector)
- `selector` <[string]> A [selector] to query element for
- `selector` <[string]|[Selector]> A [selector] to query element for
- returns: <[Promise]<?[ElementHandle]>>
The method runs `element.querySelector` within the page. If no element matches the selector, the return value resolves to `null`.
#### elementHandle.$$(selector)
- `selector` <[string]> A [selector] to query element for
- `selector` <[string]|[Selector]> A [selector] to query element for
- returns: <[Promise]<[Array]<[ElementHandle]>>>
The method runs `element.querySelectorAll` within the page. If no elements match the selector, the return value resolves to `[]`.
#### elementHandle.$$eval(selector, pageFunction[, ...args])
- `selector` <[string]> A [selector] to query page for
- `selector` <[string]|[Selector]> A [selector] to query page for
- `pageFunction` <[function]\([Array]<[Element]>\)> Function to be evaluated in browser context
- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction`
- returns: <[Promise]<[Serializable]>> Promise which resolves to the return value of `pageFunction`
@ -3315,7 +3310,7 @@ expect(await feedHandle.$$eval('.tweet', nodes => nodes.map(n => n.innerText))).
```
#### elementHandle.$eval(selector, pageFunction[, ...args])
- `selector` <[string]> A [selector] to query page for
- `selector` <[string]|[Selector]> A [selector] to query page for
- `pageFunction` <[function]\([Element]\)> Function to be evaluated in browser context
- `...args` <...[Serializable]|[JSHandle]> Arguments to pass to `pageFunction`
- returns: <[Promise]<[Serializable]>> Promise which resolves to the return value of `pageFunction`
@ -3843,6 +3838,59 @@ reported.
TimeoutError is emitted whenever certain operations are terminated due to timeout, e.g. [page.waitForSelector(selector[, options])](#pagewaitforselectorselector-options) or [playwright.launch([options])](#playwrightlaunchoptions).
### class: Selector
Selector describes an element in the page. It can be used to obtain `ElementHandle` (see [page.$()](#pageselector) for example) or shortcut element operations to avoid intermediate handle (see [page.click()](#pageclickselector-options) for example).
All methods accepting selector also accept a string shorthand which is equivalent to `{selector: 'string'}`.
#### selector.selector
- returns: <[string]> Selector in the following format: `engine=body [>> engine=body]*`. Here `engine` is one of the supported selector engines (currently, either `css` or `xpath`), and `body` is a selector body in the format of the particular engine. When multiple `engine=body` clauses are present (separated by `>>`), next one is queried relative to the previous one's result.
For convenience, selectors in the wrong format are heuristically converted to the right format:
- selector starting with `//` is assumed to be `xpath=selector`;
- otherwise selector is assumed to be `css=selector`.
```js
// queries 'div' css selector
const handle = await page.$('css=div');
// queries '//html/body/div' xpath selector
const handle = await page.$('xpath=//html/body/div');
// queries 'span' css selector inside the result of '//html/body/div' xpath selector
const handle = await page.$('xpath=//html/body/div >> css=span');
// converted to 'css=div'
const handle = await page.$('div');
// converted to 'xpath=//html/body/div'
const handle = await page.$('//html/body/div');
// queries 'span' css selector inside the div handle
const handle = await divHandle.$('css=span');
```
#### selector.visible
- returns: <[boolean]> Optional visibility to check for. If `true`, only visible elements match. If `false`, only non-visible elements match. If `undefined`, all elements match.
Note that elements are first queried by `selector`, and only after that are checked for visiblity. In particular, [page.$()](#pageselector) will not skip to the first visible element, but instead return `null` if the first matching element is not visible.
Element is defined visible if it does not have `visibility: hidden` CSS property and it's bounding box is not empty.
```js
// queries 'div', and only returns it when visible
const handle = await page.$({selector: 'css=div', visible: true});
// queries 'div', and only returns it when non-visible
const handle = await page.$({selector: 'css=div', visible: false});
// queries 'div', and returns it no matter the visibility
const handle = await page.$({selector: 'css=div'});
// returns all visible 'div' elements
const handles = await page.$$({selector: 'css=div', visible: true});
```
[AXNode]: #accessibilitysnapshotoptions "AXNode"
@ -3876,6 +3924,7 @@ TimeoutError is emitted whenever certain operations are terminated due to timeou
[Promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise "Promise"
[Request]: #class-request "Request"
[Response]: #class-response "Response"
[Selector]: #class-selector "Selector"
[Serializable]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#Description "Serializable"
[Target]: #class-target "Target"
[TimeoutError]: #class-timeouterror "TimeoutError"

View File

@ -222,7 +222,7 @@ export class Page extends EventEmitter {
this._timeoutSettings.setDefaultTimeout(timeout);
}
async $(selector: string): Promise<dom.ElementHandle | null> {
async $(selector: string | types.Selector): Promise<dom.ElementHandle | null> {
return this.mainFrame().$(selector);
}
@ -239,7 +239,7 @@ export class Page extends EventEmitter {
return this.mainFrame().$$eval(selector, pageFunction, ...args as any);
}
async $$(selector: string): Promise<dom.ElementHandle[]> {
async $$(selector: string | types.Selector): Promise<dom.ElementHandle[]> {
return this.mainFrame().$$(selector);
}
@ -535,35 +535,35 @@ export class Page extends EventEmitter {
return this._mouse;
}
click(selector: string, options?: ClickOptions) {
click(selector: string | types.Selector, options?: ClickOptions) {
return this.mainFrame().click(selector, options);
}
dblclick(selector: string, options?: MultiClickOptions) {
dblclick(selector: string | types.Selector, options?: MultiClickOptions) {
return this.mainFrame().dblclick(selector, options);
}
tripleclick(selector: string, options?: MultiClickOptions) {
tripleclick(selector: string | types.Selector, options?: MultiClickOptions) {
return this.mainFrame().tripleclick(selector, options);
}
fill(selector: string, value: string) {
fill(selector: string | types.Selector, value: string) {
return this.mainFrame().fill(selector, value);
}
focus(selector: string) {
focus(selector: string | types.Selector) {
return this.mainFrame().focus(selector);
}
hover(selector: string, options?: PointerActionOptions) {
hover(selector: string | types.Selector, options?: PointerActionOptions) {
return this.mainFrame().hover(selector, options);
}
select(selector: string, ...values: (string | dom.ElementHandle | SelectOption)[]): Promise<string[]> {
select(selector: string | types.Selector, ...values: (string | dom.ElementHandle | SelectOption)[]): Promise<string[]> {
return this.mainFrame().select(selector, ...values);
}
type(selector: string, text: string, options: { delay: (number | undefined); } | undefined) {
type(selector: string | types.Selector, text: string, options: { delay: (number | undefined); } | undefined) {
return this.mainFrame().type(selector, text, options);
}
@ -571,15 +571,15 @@ export class Page extends EventEmitter {
return this.mainFrame().waitFor(selectorOrFunctionOrTimeout, options, ...args);
}
waitForSelector(selector: string, options: { visible?: boolean; hidden?: boolean; timeout?: number; } = {}): Promise<dom.ElementHandle | null> {
waitForSelector(selector: string | types.Selector, options: types.TimeoutOptions = {}): Promise<dom.ElementHandle | null> {
return this.mainFrame().waitForSelector(selector, options);
}
waitForXPath(xpath: string, options: { visible?: boolean; hidden?: boolean; timeout?: number; } = {}): Promise<dom.ElementHandle | null> {
waitForXPath(xpath: string, options: types.TimeoutOptions = {}): Promise<dom.ElementHandle | null> {
return this.mainFrame().waitForXPath(xpath, options);
}
waitForFunction(pageFunction: Function | string, options: dom.WaitForFunctionOptions, ...args: any[]): Promise<js.JSHandle> {
waitForFunction(pageFunction: Function | string, options: types.WaitForFunctionOptions, ...args: any[]): Promise<js.JSHandle> {
return this.mainFrame().waitForFunction(pageFunction, options, ...args);
}
}

View File

@ -10,6 +10,7 @@ import * as cssSelectorEngineSource from './generated/cssSelectorEngineSource';
import * as xpathSelectorEngineSource from './generated/xpathSelectorEngineSource';
import { assert, helper } from './helper';
import Injected from './injected/injected';
import { SelectorRoot } from './injected/selectorEngine';
export interface DOMWorldDelegate {
keyboard: input.Keyboard;
@ -25,10 +26,8 @@ export interface DOMWorldDelegate {
adoptElementHandle(handle: ElementHandle, to: DOMWorld): Promise<ElementHandle>;
}
type SelectorRoot = Element | ShadowRoot | Document;
type ResolvedSelector = { root?: ElementHandle, selector: string, disposeRoot?: boolean };
type Selector = string | { root?: ElementHandle, selector: string };
export type ScopedSelector = types.Selector & { scope?: ElementHandle };
type ResolvedSelector = { scope?: ElementHandle, selector: string, visible?: boolean, disposeScope?: boolean };
export class DOMWorld {
readonly context: js.ExecutionContext;
@ -65,37 +64,47 @@ export class DOMWorld {
return this.delegate.adoptElementHandle(handle, this);
}
private async _resolveSelector(selector: Selector): Promise<ResolvedSelector> {
async resolveSelector(selector: string | ScopedSelector): Promise<ResolvedSelector> {
if (helper.isString(selector))
return { selector: normalizeSelector(selector) };
if (selector.root && selector.root.executionContext() !== this.context) {
const root = await this.adoptElementHandle(selector.root);
return { root, selector: normalizeSelector(selector.selector), disposeRoot: true };
if (selector.scope && selector.scope.executionContext() !== this.context) {
const scope = await this.adoptElementHandle(selector.scope);
return { scope, selector: normalizeSelector(selector.selector), disposeScope: true, visible: selector.visible };
}
return { root: selector.root, selector: normalizeSelector(selector.selector) };
return { scope: selector.scope, selector: normalizeSelector(selector.selector), visible: selector.visible };
}
async $(selector: Selector): Promise<ElementHandle | null> {
const resolved = await this._resolveSelector(selector);
async $(selector: string | ScopedSelector): Promise<ElementHandle | null> {
const resolved = await this.resolveSelector(selector);
const handle = await this.context.evaluateHandle(
(injected: Injected, selector: string, root: SelectorRoot | undefined) => injected.querySelector(selector, root || document),
await this.injected(), resolved.selector, resolved.root
(injected: Injected, selector: string, scope: SelectorRoot | undefined, visible: boolean | undefined) => {
const element = injected.querySelector(selector, scope || document);
if (visible === undefined || !element)
return element;
return injected.isVisible(element) === visible ? element : undefined;
},
await this.injected(), resolved.selector, resolved.scope, resolved.visible
);
if (resolved.disposeRoot)
await resolved.root.dispose();
if (resolved.disposeScope)
await resolved.scope.dispose();
if (!handle.asElement())
await handle.dispose();
return handle.asElement();
}
async $$(selector: Selector): Promise<ElementHandle[]> {
const resolved = await this._resolveSelector(selector);
async $$(selector: string | ScopedSelector): Promise<ElementHandle[]> {
const resolved = await this.resolveSelector(selector);
const arrayHandle = await this.context.evaluateHandle(
(injected: Injected, selector: string, root: SelectorRoot | undefined) => injected.querySelectorAll(selector, root || document),
await this.injected(), resolved.selector, resolved.root
(injected: Injected, selector: string, scope: SelectorRoot | undefined, visible: boolean | undefined) => {
const elements = injected.querySelectorAll(selector, scope || document);
if (visible !== undefined)
return elements.filter(element => injected.isVisible(element) === visible);
return elements;
},
await this.injected(), resolved.selector, resolved.scope, resolved.visible
);
if (resolved.disposeRoot)
await resolved.root.dispose();
if (resolved.disposeScope)
await resolved.scope.dispose();
const properties = await arrayHandle.getProperties();
await arrayHandle.dispose();
const result = [];
@ -109,20 +118,25 @@ export class DOMWorld {
return result;
}
$eval: types.$Eval<Selector> = async (selector, pageFunction, ...args) => {
$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 "${selectorToString(selector)}"`);
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<Selector> = async (selector, pageFunction, ...args) => {
const resolved = await this._resolveSelector(selector);
$$eval: types.$$Eval<string | ScopedSelector> = async (selector, pageFunction, ...args) => {
const resolved = await this.resolveSelector(selector);
const arrayHandle = await this.context.evaluateHandle(
(injected: Injected, selector: string, root: SelectorRoot | undefined) => injected.querySelectorAll(selector, root || document),
await this.injected(), resolved.selector, resolved.root
(injected: Injected, selector: string, scope: SelectorRoot | undefined, visible: boolean | undefined) => {
const elements = injected.querySelectorAll(selector, scope || document);
if (visible !== undefined)
return elements.filter(element => injected.isVisible(element) === visible);
return elements;
},
await this.injected(), resolved.selector, resolved.scope, resolved.visible
);
const result = await arrayHandle.evaluate(pageFunction, ...args as any);
await arrayHandle.dispose();
@ -223,6 +237,7 @@ export class ElementHandle extends js.JSHandle {
if (error)
throw new Error(error);
await this.focus();
// TODO: we should check that focus() succeeded.
await this._world.delegate.keyboard.sendCharacters(value);
}
@ -254,24 +269,31 @@ export class ElementHandle extends js.JSHandle {
return this._world.delegate.screenshot(this, options);
}
$(selector: string): Promise<ElementHandle | null> {
return this._world.$({ root: this, selector });
private _scopedSelector(selector: string | types.Selector): string | ScopedSelector {
selector = types.clearSelector(selector);
if (helper.isString(selector))
selector = { selector };
return { scope: this, selector: selector.selector, visible: selector.visible };
}
$$(selector: string): Promise<ElementHandle[]> {
return this._world.$$({ root: this, selector });
$(selector: string | types.Selector): Promise<ElementHandle | null> {
return this._world.$(this._scopedSelector(selector));
}
$eval: types.$Eval = (selector, pageFunction, ...args) => {
return this._world.$eval({ root: this, selector }, pageFunction, ...args as any);
$$(selector: string | types.Selector): Promise<ElementHandle[]> {
return this._world.$$(this._scopedSelector(selector));
}
$$eval: types.$$Eval = (selector, pageFunction, ...args) => {
return this._world.$$eval({ root: this, selector }, pageFunction, ...args as any);
$eval: types.$Eval<string | types.Selector> = (selector, pageFunction, ...args) => {
return this._world.$eval(this._scopedSelector(selector), pageFunction, ...args as any);
}
$$eval: types.$$Eval<string | types.Selector> = (selector, pageFunction, ...args) => {
return this._world.$$eval(this._scopedSelector(selector), pageFunction, ...args as any);
}
$x(expression: string): Promise<ElementHandle[]> {
return this._world.$$({ root: this, selector: 'xpath=' + expression });
return this._world.$$({ scope: this, selector: 'xpath=' + expression });
}
isIntersectingViewport(): Promise<boolean> {
@ -300,18 +322,9 @@ function normalizeSelector(selector: string): string {
return 'css=' + selector;
}
function selectorToString(selector: Selector): string {
if (typeof selector === 'string')
return selector;
return `:scope >> ${selector.selector}`;
}
export type Task = (domWorld: DOMWorld) => Promise<js.JSHandle>;
export type Polling = 'raf' | 'mutation' | number;
export type WaitForFunctionOptions = { polling?: Polling, timeout?: number };
export function waitForFunctionTask(pageFunction: Function | string, options: WaitForFunctionOptions, ...args: any[]) {
export function waitForFunctionTask(pageFunction: Function | string, options: types.WaitForFunctionOptions, ...args: any[]) {
const { polling = 'raf' } = options;
if (helper.isString(polling))
assert(polling === 'raf' || polling === 'mutation', 'Unknown polling option: ' + polling);
@ -321,7 +334,7 @@ export function waitForFunctionTask(pageFunction: Function | string, options: Wa
throw new Error('Unknown polling options: ' + polling);
const predicateBody = helper.isString(pageFunction) ? 'return (' + pageFunction + ')' : 'return (' + pageFunction + ')(...args)';
return async (domWorld: DOMWorld) => domWorld.context.evaluateHandle((injected: Injected, predicateBody: string, polling: Polling, timeout: number, ...args) => {
return async (domWorld: DOMWorld) => domWorld.context.evaluateHandle((injected: Injected, predicateBody: string, polling: types.Polling, timeout: number, ...args) => {
const predicate = new Function('...args', predicateBody);
if (polling === 'raf')
return injected.pollRaf(predicate, timeout, ...args);
@ -331,32 +344,23 @@ export function waitForFunctionTask(pageFunction: Function | string, options: Wa
}, await domWorld.injected(), predicateBody, polling, options.timeout, ...args);
}
export type WaitForSelectorOptions = { visible?: boolean, hidden?: boolean, timeout?: number };
export function waitForSelectorTask(selector: string | ScopedSelector, timeout: number): Task {
return async (domWorld: DOMWorld) => {
// TODO: we should not be able to adopt selector scope from a different document - handle this case.
const resolved = await domWorld.resolveSelector(selector);
return domWorld.context.evaluateHandle((injected: Injected, selector: string, scope: SelectorRoot | undefined, visible: boolean | undefined, timeout: number) => {
if (visible !== undefined)
return injected.pollRaf(predicate, timeout);
return injected.pollMutation(predicate, timeout);
export function waitForSelectorTask(selector: string, options: WaitForSelectorOptions): Task {
const { visible: waitForVisible = false, hidden: waitForHidden = false } = options;
selector = normalizeSelector(selector);
return async (domWorld: DOMWorld) => domWorld.context.evaluateHandle((injected: Injected, selector: string, waitForVisible: boolean, waitForHidden: boolean, timeout: number) => {
if (waitForVisible || waitForHidden)
return injected.pollRaf(predicate, timeout);
return injected.pollMutation(predicate, timeout);
function predicate(): Element | boolean {
const element = injected.querySelector(selector, document);
if (!element)
return waitForHidden;
if (!waitForVisible && !waitForHidden)
return element;
const style = window.getComputedStyle(element);
const isVisible = style && style.visibility !== 'hidden' && hasVisibleBoundingBox();
const success = (waitForVisible === isVisible || waitForHidden === !isVisible);
return success ? element : false;
function hasVisibleBoundingBox(): boolean {
const rect = element.getBoundingClientRect();
return !!(rect.top || rect.bottom || rect.width || rect.height);
function predicate(): Element | boolean {
const element = injected.querySelector(selector, scope || document);
if (!element)
return visible === false;
if (visible === undefined)
return element;
return injected.isVisible(element) === visible ? element : false;
}
}
}, await domWorld.injected(), selector, waitForVisible, waitForHidden, options.timeout);
}, await domWorld.injected(), resolved.selector, resolved.scope, resolved.visible, timeout);
};
}

View File

@ -118,7 +118,7 @@ export class Page extends EventEmitter {
}
async emulateMedia(options: {
type?: ""|"screen"|"print",
type?: ''|'screen'|'print',
colorScheme?: 'dark' | 'light' | 'no-preference' }) {
assert(!options.type || input.mediaTypes.has(options.type), 'Unsupported media type: ' + options.type);
assert(!options.colorScheme || input.mediaColorSchemes.has(options.colorScheme), 'Unsupported color scheme: ' + options.colorScheme);
@ -457,35 +457,35 @@ export class Page extends EventEmitter {
return this.mainFrame().addStyleTag(options);
}
click(selector: string, options?: input.ClickOptions) {
click(selector: string | types.Selector, options?: input.ClickOptions) {
return this.mainFrame().click(selector, options);
}
dblclick(selector: string, options?: input.MultiClickOptions) {
dblclick(selector: string | types.Selector, options?: input.MultiClickOptions) {
return this.mainFrame().dblclick(selector, options);
}
tripleclick(selector: string, options?: input.MultiClickOptions) {
tripleclick(selector: string | types.Selector, options?: input.MultiClickOptions) {
return this.mainFrame().tripleclick(selector, options);
}
fill(selector: string, value: string) {
fill(selector: string | types.Selector, value: string) {
return this.mainFrame().fill(selector, value);
}
select(selector: string, ...values: Array<string>): Promise<Array<string>> {
select(selector: string | types.Selector, ...values: Array<string>): Promise<Array<string>> {
return this._frameManager.mainFrame().select(selector, ...values);
}
type(selector: string, text: string, options: { delay: (number | undefined); } | undefined) {
type(selector: string | types.Selector, text: string, options: { delay: (number | undefined); } | undefined) {
return this._frameManager.mainFrame().type(selector, text, options);
}
focus(selector: string) {
focus(selector: string | types.Selector) {
return this._frameManager.mainFrame().focus(selector);
}
hover(selector: string) {
hover(selector: string | types.Selector) {
return this._frameManager.mainFrame().hover(selector);
}
@ -493,15 +493,15 @@ export class Page extends EventEmitter {
return this._frameManager.mainFrame().waitFor(selectorOrFunctionOrTimeout, options, ...args);
}
waitForFunction(pageFunction: Function | string, options: dom.WaitForFunctionOptions, ...args): Promise<js.JSHandle> {
waitForFunction(pageFunction: Function | string, options: types.WaitForFunctionOptions, ...args): Promise<js.JSHandle> {
return this._frameManager.mainFrame().waitForFunction(pageFunction, options, ...args);
}
waitForSelector(selector: string, options: { timeout?: number; visible?: boolean; hidden?: boolean; } | undefined = {}): Promise<dom.ElementHandle> {
waitForSelector(selector: string | types.Selector, options?: types.TimeoutOptions): Promise<dom.ElementHandle> {
return this._frameManager.mainFrame().waitForSelector(selector, options);
}
waitForXPath(xpath: string, options: { timeout?: number; visible?: boolean; hidden?: boolean; } | undefined = {}): Promise<dom.ElementHandle> {
waitForXPath(xpath: string, options?: types.TimeoutOptions): Promise<dom.ElementHandle> {
return this._frameManager.mainFrame().waitForXPath(xpath, options);
}
@ -509,11 +509,11 @@ export class Page extends EventEmitter {
return this._frameManager.mainFrame().title();
}
$(selector: string): Promise<dom.ElementHandle | null> {
$(selector: string | types.Selector): Promise<dom.ElementHandle | null> {
return this._frameManager.mainFrame().$(selector);
}
$$(selector: string): Promise<Array<dom.ElementHandle>> {
$$(selector: string | types.Selector): Promise<Array<dom.ElementHandle>> {
return this._frameManager.mainFrame().$$(selector);
}

View File

@ -122,9 +122,9 @@ export class Frame {
return context.evaluate(pageFunction, ...args as any);
}
async $(selector: string): Promise<dom.ElementHandle | null> {
async $(selector: string | types.Selector): Promise<dom.ElementHandle | null> {
const domWorld = await this._mainDOMWorld();
return domWorld.$(selector);
return domWorld.$(types.clearSelector(selector));
}
async $x(expression: string): Promise<dom.ElementHandle[]> {
@ -142,9 +142,9 @@ export class Frame {
return domWorld.$$eval(selector, pageFunction, ...args as any);
}
async $$(selector: string): Promise<dom.ElementHandle[]> {
async $$(selector: string | types.Selector): Promise<dom.ElementHandle[]> {
const domWorld = await this._mainDOMWorld();
return domWorld.$$(selector);
return domWorld.$$(types.clearSelector(selector));
}
async content(): Promise<string> {
@ -300,58 +300,58 @@ export class Frame {
}
}
async click(selector: string, options?: ClickOptions) {
async click(selector: string | types.Selector, options?: ClickOptions) {
const domWorld = await this._utilityDOMWorld();
const handle = await domWorld.$(selector);
assert(handle, 'No node found for selector: ' + selector);
const handle = await domWorld.$(types.clearSelector(selector));
assert(handle, 'No node found for selector: ' + types.selectorToString(selector));
await handle.click(options);
await handle.dispose();
}
async dblclick(selector: string, options?: MultiClickOptions) {
async dblclick(selector: string | types.Selector, options?: MultiClickOptions) {
const domWorld = await this._utilityDOMWorld();
const handle = await domWorld.$(selector);
assert(handle, 'No node found for selector: ' + selector);
const handle = await domWorld.$(types.clearSelector(selector));
assert(handle, 'No node found for selector: ' + types.selectorToString(selector));
await handle.dblclick(options);
await handle.dispose();
}
async tripleclick(selector: string, options?: MultiClickOptions) {
async tripleclick(selector: string | types.Selector, options?: MultiClickOptions) {
const domWorld = await this._utilityDOMWorld();
const handle = await domWorld.$(selector);
assert(handle, 'No node found for selector: ' + selector);
const handle = await domWorld.$(types.clearSelector(selector));
assert(handle, 'No node found for selector: ' + types.selectorToString(selector));
await handle.tripleclick(options);
await handle.dispose();
}
async fill(selector: string, value: string) {
async fill(selector: string | types.Selector, value: string) {
const domWorld = await this._utilityDOMWorld();
const handle = await domWorld.$(selector);
assert(handle, 'No node found for selector: ' + selector);
const handle = await domWorld.$(types.clearSelector(selector));
assert(handle, 'No node found for selector: ' + types.selectorToString(selector));
await handle.fill(value);
await handle.dispose();
}
async focus(selector: string) {
async focus(selector: string | types.Selector) {
const domWorld = await this._utilityDOMWorld();
const handle = await domWorld.$(selector);
assert(handle, 'No node found for selector: ' + selector);
const handle = await domWorld.$(types.clearSelector(selector));
assert(handle, 'No node found for selector: ' + types.selectorToString(selector));
await handle.focus();
await handle.dispose();
}
async hover(selector: string, options?: PointerActionOptions) {
async hover(selector: string | types.Selector, options?: PointerActionOptions) {
const domWorld = await this._utilityDOMWorld();
const handle = await domWorld.$(selector);
assert(handle, 'No node found for selector: ' + selector);
const handle = await domWorld.$(types.clearSelector(selector));
assert(handle, 'No node found for selector: ' + types.selectorToString(selector));
await handle.hover(options);
await handle.dispose();
}
async select(selector: string, ...values: (string | dom.ElementHandle | SelectOption)[]): Promise<string[]> {
async select(selector: string | types.Selector, ...values: (string | dom.ElementHandle | SelectOption)[]): Promise<string[]> {
const domWorld = await this._utilityDOMWorld();
const handle = await domWorld.$(selector);
assert(handle, 'No node found for selector: ' + selector);
const handle = await domWorld.$(types.clearSelector(selector));
assert(handle, 'No node found for selector: ' + types.selectorToString(selector));
const toDispose: Promise<dom.ElementHandle>[] = [];
const adoptedValues = await Promise.all(values.map(async value => {
if (value instanceof dom.ElementHandle && value.executionContext() !== domWorld.context) {
@ -367,10 +367,10 @@ export class Frame {
return result;
}
async type(selector: string, text: string, options: { delay: (number | undefined); } | undefined) {
async type(selector: string | types.Selector, text: string, options: { delay: (number | undefined); } | undefined) {
const domWorld = await this._utilityDOMWorld();
const handle = await domWorld.$(selector);
assert(handle, 'No node found for selector: ' + selector);
const handle = await domWorld.$(types.clearSelector(selector));
assert(handle, 'No node found for selector: ' + types.selectorToString(selector));
await handle.type(text, options);
await handle.dispose();
}
@ -385,10 +385,10 @@ export class Frame {
return Promise.reject(new Error('Unsupported target type: ' + (typeof selectorOrFunctionOrTimeout)));
}
async waitForSelector(selector: string, options: dom.WaitForSelectorOptions = {}): Promise<dom.ElementHandle | null> {
const task = dom.waitForSelectorTask(selector, { timeout: this._timeoutSettings.timeout(), ...options });
const title = `selector "${selector}"${options.hidden ? ' to be hidden' : ''}`;
const handle = await this._scheduleRerunnableTask(task, 'utility', options.timeout, title);
async waitForSelector(selector: string | types.Selector, options: types.TimeoutOptions = {}): Promise<dom.ElementHandle | null> {
const { timeout = this._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;
@ -401,11 +401,11 @@ export class Frame {
return adopted;
}
async waitForXPath(xpath: string, options: dom.WaitForSelectorOptions = {}): Promise<dom.ElementHandle | null> {
async waitForXPath(xpath: string, options: types.TimeoutOptions = {}): Promise<dom.ElementHandle | null> {
return this.waitForSelector('xpath=' + xpath, options);
}
waitForFunction(pageFunction: Function | string, options: dom.WaitForFunctionOptions = {}, ...args: any[]): Promise<js.JSHandle> {
waitForFunction(pageFunction: Function | string, options: types.WaitForFunctionOptions = {}, ...args: any[]): Promise<js.JSHandle> {
options = { timeout: this._timeoutSettings.timeout(), ...options };
const task = dom.waitForFunctionTask(pageFunction, options, ...args);
return this._scheduleRerunnableTask(task, 'main', options.timeout);

View File

@ -82,6 +82,16 @@ class Injected {
return result;
}
isVisible(element: Element): boolean {
if (!element.ownerDocument || !element.ownerDocument.defaultView)
return true;
const style = element.ownerDocument.defaultView.getComputedStyle(element);
if (!style || style.visibility === 'hidden')
return false;
const rect = element.getBoundingClientRect();
return !!(rect.top || rect.bottom || rect.width || rect.height);
}
pollMutation(predicate: Function, timeout: number, ...args: any[]): Promise<any> {
let timedOut = false;
if (timeout)

View File

@ -2,6 +2,7 @@
// Licensed under the MIT license.
import * as js from './javascript';
import { helper } from './helper';
type Boxed<Args extends any[]> = { [Index in keyof Args]: Args[Index] | js.JSHandle };
type PageFunction<Args extends any[], R = any> = string | ((...args: Args) => R | Promise<R>);
@ -9,10 +10,30 @@ type PageFunctionOn<On, Args extends any[], R = any> = string | ((on: On, ...arg
export type Evaluate = <Args extends any[], R>(pageFunction: PageFunction<Args, R>, ...args: Boxed<Args>) => Promise<R>;
export type EvaluateHandle = <Args extends any[]>(pageFunction: PageFunction<Args>, ...args: Boxed<Args>) => Promise<js.JSHandle>;
export type $Eval<S = string> = <Args extends any[], R>(selector: S, pageFunction: PageFunctionOn<Element, Args, R>, ...args: Boxed<Args>) => Promise<R>;
export type $$Eval<S = string> = <Args extends any[], R>(selector: S, pageFunction: PageFunctionOn<Element[], Args, R>, ...args: Boxed<Args>) => Promise<R>;
export type $Eval<S = string | Selector> = <Args extends any[], R>(selector: S, pageFunction: PageFunctionOn<Element, Args, R>, ...args: Boxed<Args>) => Promise<R>;
export type $$Eval<S = string | Selector> = <Args extends any[], R>(selector: S, pageFunction: PageFunctionOn<Element[], Args, R>, ...args: Boxed<Args>) => Promise<R>;
export type EvaluateOn = <Args extends any[], R>(pageFunction: PageFunctionOn<any, Args, R>, ...args: Boxed<Args>) => Promise<R>;
export type EvaluateHandleOn = <Args extends any[]>(pageFunction: PageFunctionOn<any, Args>, ...args: Boxed<Args>) => Promise<js.JSHandle>;
export type Rect = { x: number, y: number, width: number, height: number };
export type Point = { x: number, y: number };
export type TimeoutOptions = { timeout?: number };
export type Selector = { selector: string, visible?: boolean };
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;
return `${selector.visible ? '[visible] ' : selector.visible === false ? '[hidden] ' : ''}${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, visible: selector.visible };
}

View File

@ -78,7 +78,7 @@ export class Launcher {
stdio = ['ignore', 'ignore', 'ignore', 'pipe', 'pipe'];
webkitArguments.push('--inspector-pipe');
if (options.headless !== false)
webkitArguments.push('--headless');
webkitArguments.push('--headless');
const webkitProcess = childProcess.spawn(
webkitExecutable,
webkitArguments,

View File

@ -206,7 +206,7 @@ export class Page extends EventEmitter {
this._timeoutSettings.setDefaultTimeout(timeout);
}
async $(selector: string): Promise<dom.ElementHandle | null> {
async $(selector: string | types.Selector): Promise<dom.ElementHandle | null> {
return this.mainFrame().$(selector);
}
@ -223,7 +223,7 @@ export class Page extends EventEmitter {
return this.mainFrame().$$eval(selector, pageFunction, ...args as any);
}
async $$(selector: string): Promise<dom.ElementHandle[]> {
async $$(selector: string | types.Selector): Promise<dom.ElementHandle[]> {
return this.mainFrame().$$(selector);
}
@ -466,35 +466,35 @@ export class Page extends EventEmitter {
return this._mouse;
}
click(selector: string, options?: ClickOptions) {
click(selector: string | types.Selector, options?: ClickOptions) {
return this.mainFrame().click(selector, options);
}
dblclick(selector: string, options?: MultiClickOptions) {
dblclick(selector: string | types.Selector, options?: MultiClickOptions) {
return this.mainFrame().dblclick(selector, options);
}
tripleclick(selector: string, options?: MultiClickOptions) {
tripleclick(selector: string | types.Selector, options?: MultiClickOptions) {
return this.mainFrame().tripleclick(selector, options);
}
hover(selector: string) {
hover(selector: string | types.Selector) {
return this.mainFrame().hover(selector);
}
fill(selector: string, value: string) {
fill(selector: string | types.Selector, value: string) {
return this.mainFrame().fill(selector, value);
}
focus(selector: string) {
focus(selector: string | types.Selector) {
return this.mainFrame().focus(selector);
}
select(selector: string, ...values: string[]): Promise<string[]> {
select(selector: string | types.Selector, ...values: string[]): Promise<string[]> {
return this.mainFrame().select(selector, ...values);
}
type(selector: string, text: string, options: { delay: (number | undefined); } | undefined) {
type(selector: string | types.Selector, text: string, options: { delay: (number | undefined); } | undefined) {
return this.mainFrame().type(selector, text, options);
}
@ -502,15 +502,15 @@ export class Page extends EventEmitter {
return this.mainFrame().waitFor(selectorOrFunctionOrTimeout, options, ...args);
}
waitForSelector(selector: string, options: { visible?: boolean; hidden?: boolean; timeout?: number; } = {}): Promise<dom.ElementHandle | null> {
waitForSelector(selector: string | types.Selector, options?: types.TimeoutOptions): Promise<dom.ElementHandle | null> {
return this.mainFrame().waitForSelector(selector, options);
}
waitForXPath(xpath: string, options: { visible?: boolean; hidden?: boolean; timeout?: number; } = {}): Promise<dom.ElementHandle | null> {
waitForXPath(xpath: string, options?: types.TimeoutOptions): Promise<dom.ElementHandle | null> {
return this.mainFrame().waitForXPath(xpath, options);
}
waitForFunction(pageFunction: Function | string, options: dom.WaitForFunctionOptions, ...args: any[]): Promise<js.JSHandle> {
waitForFunction(pageFunction: Function | string, options: types.WaitForFunctionOptions, ...args: any[]): Promise<js.JSHandle> {
return this.mainFrame().waitForFunction(pageFunction, options, ...args);
}
}

View File

@ -126,6 +126,32 @@ module.exports.addTests = function({testRunner, expect, playwright, FFOX, CHROME
]);
});
it('should respect selector visibilty', async({page, server}) => {
await page.goto(server.PREFIX + '/input/button.html');
await page.click({selector: 'button', visible: true});
expect(await page.evaluate(() => result)).toBe('Clicked');
let error = null;
await page.goto(server.PREFIX + '/input/button.html');
await page.click({selector: 'button', visible: false}).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', visible: true}).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', visible: false}).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 click wrapped links', async({page, server}) => {
await page.goto(server.PREFIX + '/wrappedlink.html');
await page.click('a');

View File

@ -1119,6 +1119,27 @@ module.exports.addTests = function({testRunner, expect, headless, playwright, FF
await page.fill('textarea', 123).catch(e => error = e);
expect(error.message).toContain('Value must be string.');
});
it('should respect selector visibilty', async({page, server}) => {
await page.goto(server.PREFIX + '/input/textarea.html');
await page.fill({selector: 'input', visible: true}, 'some value');
expect(await page.evaluate(() => result)).toBe('some value');
let error = null;
await page.goto(server.PREFIX + '/input/textarea.html');
await page.fill({selector: 'input', visible: false}, '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', visible: true}, 'some value').catch(e => error = e);
expect(error.message).toBe('No node found for selector: [visible] input');
await page.goto(server.PREFIX + '/input/textarea.html');
await page.$eval('input', i => i.style.display = 'none');
await page.fill({selector: 'input', visible: false}, 'some value');
expect(await page.evaluate(() => result)).toBe('');
});
});
// FIXME: WebKit shouldn't send targetDestroyed on PSON so that we could

View File

@ -35,6 +35,19 @@ 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', visible: true}, 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', visible: false}, e => e.id)).toBe('testAttribute');
error = null;
await page.setContent('<section id="testAttribute">43543</section>');
await page.$eval({selector: 'css=section', visible: false}, 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', visible: true}, 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!');
@ -107,6 +120,12 @@ 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', visible: true}, x => x.length)).toBe(1);
expect(await page.$$eval({selector: 'css=section', visible: false}, x => x.length)).toBe(2);
expect(await page.$$eval({selector: 'css=section'}, x => x.length)).toBe(3);
});
});
describe('Page.$', function() {
@ -139,6 +158,17 @@ 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}) => {
await page.setContent('<section id="testAttribute">43543</section>');
expect(await page.$({selector: 'css=section', visible: true})).toBeTruthy();
expect(await page.$({selector: 'css=section', visible: false})).not.toBeTruthy();
expect(await page.$({selector: 'css=section'})).toBeTruthy();
await page.setContent('<section id="testAttribute" style="display: none">43543</section>');
expect(await page.$({selector: 'css=section', visible: true})).not.toBeTruthy();
expect(await page.$({selector: 'css=section', visible: false})).toBeTruthy();
expect(await page.$({selector: 'css=section'})).toBeTruthy();
});
});
describe('Page.$$', function() {
@ -154,6 +184,12 @@ 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', visible: true})).length).toBe(1);
expect((await page.$$({selector: 'css=section', visible: false})).length).toBe(2);
expect((await page.$$({selector: 'css=section'})).length).toBe(3);
});
});
describe('Path.$x', function() {
@ -192,6 +228,22 @@ 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', visible: true});
expect(inner).not.toBeTruthy();
inner = await second.$({selector: '.inner', visible: false});
expect(await inner.evaluate(e => e.textContent)).toBe('A');
await inner.evaluate(e => e.style.display = 'block');
inner = await second.$({selector: '.inner', visible: true});
expect(await inner.evaluate(e => e.textContent)).toBe('A');
});
});
describe('ElementHandle.$eval', function() {
it('should work', async({page, server}) => {
@ -214,7 +266,7 @@ module.exports.addTests = function({testRunner, expect, product, FFOX, CHROME, W
await page.setContent(htmlContent);
const elementHandle = await page.$('#myId');
const errorMessage = await elementHandle.$eval('.a', node => node.innerText).catch(error => error.message);
expect(errorMessage).toBe(`Error: failed to find element matching selector ":scope >> .a"`);
expect(errorMessage).toBe(`Error: failed to find element matching selector ".a"`);
});
});
describe('ElementHandle.$$eval', function() {

View File

@ -289,7 +289,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('div', {visible: true}).then(() => divFound = true);
const waitForSelector = page.waitForSelector({selector: 'div', visible: true}).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'));
@ -300,7 +300,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('div#inner', {visible: true}).then(() => divVisible = true);
const waitForSelector = page.waitForSelector({selector: 'div#inner', visible: true}).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'));
@ -312,7 +312,7 @@ 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('div', {hidden: true}).then(() => divHidden = true);
const waitForSelector = page.waitForSelector({selector: 'div', visible: false}).then(() => divHidden = true);
await page.waitForSelector('div'); // do a round trip
expect(divHidden).toBe(false);
await page.evaluate(() => document.querySelector('div').style.setProperty('visibility', 'hidden'));
@ -322,7 +322,7 @@ 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('div', {hidden: true}).then(() => divHidden = true);
const waitForSelector = page.waitForSelector({selector: 'div', visible: false}).then(() => divHidden = true);
await page.waitForSelector('div'); // do a round trip
expect(divHidden).toBe(false);
await page.evaluate(() => document.querySelector('div').style.setProperty('display', 'none'));
@ -332,7 +332,7 @@ 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('div', {hidden: true}).then(() => divRemoved = true);
const waitForSelector = page.waitForSelector({selector: 'div', visible: false}).then(() => divRemoved = true);
await page.waitForSelector('div'); // do a round trip
expect(divRemoved).toBe(false);
await page.evaluate(() => document.querySelector('div').remove());
@ -340,7 +340,7 @@ module.exports.addTests = function({testRunner, expect, product, playwright, FFO
expect(divRemoved).toBe(true);
});
it('should return null if waiting to hide non-existing element', async({page, server}) => {
const handle = await page.waitForSelector('non-existing', { hidden: true });
const handle = await page.waitForSelector({selector: 'non-existing', visible: false });
expect(handle).toBe(null);
});
it('should respect timeout', async({page, server}) => {
@ -353,9 +353,9 @@ 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('div', {hidden: true, timeout: 10}).catch(e => error = e);
await page.waitForSelector({selector: 'div', visible: false}, {timeout: 10}).catch(e => error = e);
expect(error).toBeTruthy();
expect(error.message).toContain('waiting for selector "div" to be hidden failed: timeout');
expect(error.message).toContain('waiting for selector "[hidden] div" failed: timeout');
});
it('should respond to node attribute mutation', async({page, server}) => {
@ -426,16 +426,6 @@ module.exports.addTests = function({testRunner, expect, product, playwright, FFO
expect(waitError).toBeTruthy();
expect(waitError.message).toContain('waitForFunction failed: frame got detached.');
});
it('hidden should wait for display: none', async({page, server}) => {
let divHidden = false;
await page.setContent(`<div style='display: block;'></div>`);
const waitForXPath = page.waitForXPath('//div', {hidden: true}).then(() => divHidden = true);
await page.waitForXPath('//div'); // do a round trip
expect(divHidden).toBe(false);
await page.evaluate(() => document.querySelector('div').style.setProperty('display', 'none'));
expect(await waitForXPath).toBe(true);
expect(divHidden).toBe(true);
});
it('should return the element handle', async({page, server}) => {
const waitForXPath = page.waitForXPath('//*[@class="zombo"]');
await page.setContent(`<div class='zombo'>anything</div>`);

View File

@ -154,6 +154,15 @@ function checkSources(sources) {
typeName = 'Object';
const nextCircular = [typeName].concat(circular);
if (typeName === 'Selector') {
if (!excludeClasses.has(typeName)) {
const properties = type.getProperties().map(property => serializeSymbol(property, nextCircular));
classes.push(new Documentation.Class(typeName, properties));
excludeClasses.add(typeName);
}
return new Documentation.Type(typeName, []);
}
if (isRegularObject(type)) {
let properties = undefined;
if (!circular.includes(typeName))

View File

@ -114,8 +114,10 @@ function checkSorting(doc) {
function filterJSDocumentation(jsSources, jsDocumentation) {
const apijs = jsSources.find(source => source.name() === 'api.ts');
let includedClasses = null;
if (apijs)
if (apijs) {
includedClasses = new Set(Object.keys(require(path.join(apijs.filePath(), '..', '..', 'lib', 'api.js')).Chromium));
includedClasses.add('Selector');
}
// Filter private classes and methods.
const classes = [];
for (const cls of jsDocumentation.classesArray) {

View File

@ -3,6 +3,7 @@ const path = require('path');
const fs = require('fs');
const StreamZip = require('node-stream-zip');
const vm = require('vm');
const os = require('os');
async function generateChromeProtocol(revision) {
const outputPath = path.join(__dirname, '..', '..', 'src', 'chromium', 'protocol.d.ts');
@ -120,7 +121,10 @@ async function generateFirefoxProtocol(revision) {
const outputPath = path.join(__dirname, '..', '..', 'src', 'firefox', 'protocol.d.ts');
if (revision.local && fs.existsSync(outputPath))
return;
const zip = new StreamZip({file: path.join(revision.executablePath, '..', 'omni.ja'), storeEntries: true});
const omnija = os.platform() === 'darwin' ?
path.join(revision.executablePath, '..', '..', 'Resources', 'omni.ja') :
path.join(revision.executablePath, '..', 'omni.ja');
const zip = new StreamZip({file: omnija, storeEntries: true});
// @ts-ignore
await new Promise(x => zip.on('ready', x));
const data = zip.entryDataSync(zip.entry('chrome/juggler/content/protocol/Protocol.js'))