Merge branch 'master' into patch-3

This commit is contained in:
Joel Einbinder 2020-02-24 18:29:46 -08:00 committed by GitHub
commit c464a91a75
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
86 changed files with 2164 additions and 1849 deletions

View file

@ -1,19 +1,12 @@
environment: environment:
matrix: matrix:
- nodejs_version: "8.16.0" - nodejs_version: "12"
FLAKINESS_DASHBOARD_NAME: Appveyor Chromium (Win + node8)
FLAKINESS_DASHBOARD_PASSWORD:
secure: g66jP+j6C+hkXLutBV9fdxB5fRJgcQQzy93SgQzXUmcCl/RjkJwnzyHvX0xfCVnv
build: off build: off
install: install:
- ps: $env:FLAKINESS_DASHBOARD_BUILD_URL="https://ci.appveyor.com/project/aslushnikov/playwright/builds/$env:APPVEYOR_BUILD_ID/job/$env:APPVEYOR_JOB_ID"
- ps: Install-Product node $env:nodejs_version - ps: Install-Product node $env:nodejs_version
- npm install - npm install
- if "%nodejs_version%" == "8.16.0" ( - npm run ctest
npm run lint && - npm run wtest
npm run coverage && - npm run ftest
npm run test-doclint &&
npm run test-types
)

View file

@ -10,6 +10,8 @@ on:
env: env:
CI: true CI: true
# Force terminal colors. @see https://www.npmjs.com/package/colors
FORCE_COLOR: 1
jobs: jobs:
chromium_linux: chromium_linux:

View file

@ -55,14 +55,12 @@ Install [azure-cli](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli
### 2. Export "az" to the mingw world ### 2. Export "az" to the mingw world
Run `cmd` as administrator and run the following line: The easiest away to export "az" to mingw is to create `c:\mozilla-build\bin\az` with the following content:
``` ```
> echo cmd.exe /c "\"C:\Program Files (x86)\Microsoft SDKs\Azure\CLI2\wbin\az.cmd\" $1 $2 $3 $4 $5 $6 $7 $8 $9 ${10} ${11} ${12} ${13} ${14} ${15} ${16}" > "%SYSTEMROOT%\az" cmd.exe /c "\"C:\Program Files (x86)\Microsoft SDKs\Azure\CLI2\wbin\az.cmd\" $1 $2 $3 $4 $5 $6 $7 $8 $9 ${10} ${11} ${12} ${13} ${14} ${15} ${16}"
``` ```
This command will create a `c:\Windows\az` file that will call azure-cli with passed parameters (Not the most beautiful solution, but it works!)
### 3. Install node.js ### 3. Install node.js
Node.js: https://nodejs.org/en/download/ Node.js: https://nodejs.org/en/download/

View file

@ -1 +1 @@
1151 1154

View file

@ -1,3 +1,3 @@
REMOTE_URL="https://github.com/webkit/webkit" REMOTE_URL="https://github.com/webkit/webkit"
BASE_BRANCH="master" BASE_BRANCH="master"
BASE_REVISION="dd26eacc87436a7d494c9980aa9872fc83cf9dd7" BASE_REVISION="3f421745bf9a96eaa99c7b75cfccbf455740aeb6"

File diff suppressed because it is too large Load diff

View file

@ -25,6 +25,7 @@
- [class: BrowserServer](#class-browserserver) - [class: BrowserServer](#class-browserserver)
- [class: BrowserType](#class-browsertype) - [class: BrowserType](#class-browsertype)
- [class: ChromiumBrowser](#class-chromiumbrowser) - [class: ChromiumBrowser](#class-chromiumbrowser)
- [class: ChromiumBrowserContext](#class-chromiumbrowsercontext)
- [class: ChromiumCoverage](#class-chromiumcoverage) - [class: ChromiumCoverage](#class-chromiumcoverage)
- [class: ChromiumSession](#class-chromiumsession) - [class: ChromiumSession](#class-chromiumsession)
- [class: ChromiumTarget](#class-chromiumtarget) - [class: ChromiumTarget](#class-chromiumtarget)
@ -184,7 +185,7 @@ Indicates that the browser is connected.
- `options` <[Object]> - `options` <[Object]>
- `ignoreHTTPSErrors` <?[boolean]> Whether to ignore HTTPS errors during navigation. Defaults to `false`. - `ignoreHTTPSErrors` <?[boolean]> Whether to ignore HTTPS errors during navigation. Defaults to `false`.
- `bypassCSP` <?[boolean]> Toggles bypassing page's Content-Security-Policy. - `bypassCSP` <?[boolean]> Toggles bypassing page's Content-Security-Policy.
- `viewport` <?[Object]> Sets a consistent viewport for each page. Defaults to an 800x600 viewport. `null` disables the default viewport. - `viewport` <?[Object]> Sets a consistent viewport for each page. Defaults to an 1280x720 viewport. `null` disables the default viewport.
- `width` <[number]> page width in pixels. - `width` <[number]> page width in pixels.
- `height` <[number]> page height in pixels. - `height` <[number]> page height in pixels.
- `deviceScaleFactor` <[number]> Specify device scale factor (can be thought of as dpr). Defaults to `1`. - `deviceScaleFactor` <[number]> Specify device scale factor (can be thought of as dpr). Defaults to `1`.
@ -217,7 +218,7 @@ Creates a new browser context. It won't share cookies/cache with other browser c
- `options` <[Object]> - `options` <[Object]>
- `ignoreHTTPSErrors` <?[boolean]> Whether to ignore HTTPS errors during navigation. Defaults to `false`. - `ignoreHTTPSErrors` <?[boolean]> Whether to ignore HTTPS errors during navigation. Defaults to `false`.
- `bypassCSP` <?[boolean]> Toggles bypassing page's Content-Security-Policy. - `bypassCSP` <?[boolean]> Toggles bypassing page's Content-Security-Policy.
- `viewport` <?[Object]> Sets a consistent viewport for each page. Defaults to an 800x600 viewport. `null` disables the default viewport. - `viewport` <?[Object]> Sets a consistent viewport for each page. Defaults to an 1280x720 viewport. `null` disables the default viewport.
- `width` <[number]> page width in pixels. - `width` <[number]> page width in pixels.
- `height` <[number]> page height in pixels. - `height` <[number]> page height in pixels.
- `deviceScaleFactor` <[number]> Specify device scale factor (can be thought of as dpr). Defaults to `1`. - `deviceScaleFactor` <[number]> Specify device scale factor (can be thought of as dpr). Defaults to `1`.
@ -331,7 +332,7 @@ If URLs are specified, only cookies that affect those URLs are returned.
Creates a new page in the browser context. Creates a new page in the browser context.
#### browserContext.pages() #### browserContext.pages()
- returns: <[Promise]<[Array]<[Page]>>> Promise which resolves to an array of all open pages. Non visible pages, such as `"background_page"`, will not be listed here. You can find them using [target.page()](#targetpage). - returns: <[Promise]<[Array]<[Page]>>> Promise which resolves to an array of all open pages. Non visible pages, such as `"background_page"`, will not be listed here. You can find them using [chromiumTarget.page()](#chromiumtargetpage).
An array of all pages inside the browser context. An array of all pages inside the browser context.
@ -759,7 +760,7 @@ To disable authentication, pass `null`.
#### page.check(selector, [options]) #### page.check(selector, [options])
- `selector` <[string]> A selector to search for checkbox or radio button to check. If there are multiple elements satisfying the selector, the first will be checked. - `selector` <[string]> A selector to search for checkbox or radio button to check. If there are multiple elements satisfying the selector, the first will be checked.
- `options` <[Object]> - `options` <[Object]>
- `waitFor` <"visible"|"hidden"|"any"|"nowait"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`) or do not wait at all (`nowait`). Defaults to `visible`. - `waitFor` <[boolean]> Whether to wait for the element to be present in the dom, stop moving (for example, wait until css transition finishes) and potentially receive pointer events at the click point (for example, wait until element becomes non-obscured by other elements). Defaults to `true`.
- `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully checked. The Promise will be rejected if there is no element matching `selector`. - returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully checked. The Promise will be rejected if there is no element matching `selector`.
@ -778,7 +779,7 @@ Shortcut for [page.mainFrame().check(selector[, options])](#framecheckselector-o
- x <[number]> - x <[number]>
- y <[number]> - y <[number]>
- `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the click, and then restores current modifiers back. If not specified, currently pressed modifiers are used. - `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the click, and then restores current modifiers back. If not specified, currently pressed modifiers are used.
- `waitFor` <"visible"|"hidden"|"any"|"nowait"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`) or do not wait at all (`nowait`). Defaults to `visible`. - `waitFor` <[boolean]> Whether to wait for the element to be present in the dom, stop moving (for example, wait until css transition finishes) and potentially receive pointer events at the click point (for example, wait until element becomes non-obscured by other elements). Defaults to `true`.
- `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully clicked. The Promise will be rejected if there is no element matching `selector`. - returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully clicked. The Promise will be rejected if there is no element matching `selector`.
@ -834,7 +835,7 @@ Browser-specific Coverage implementation, only available for Chromium atm. See [
- x <[number]> - x <[number]>
- y <[number]> - y <[number]>
- `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the double click, and then restores current modifiers back. If not specified, currently pressed modifiers are used. - `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the double click, and then restores current modifiers back. If not specified, currently pressed modifiers are used.
- `waitFor` <"visible"|"hidden"|"any"|"nowait"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`) or do not wait at all (`nowait`). Defaults to `visible`. - `waitFor` <[boolean]> Whether to wait for the element to be present in the dom, stop moving (for example, wait until css transition finishes) and potentially receive pointer events at the click point (for example, wait until element becomes non-obscured by other elements). Defaults to `true`.
- `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully double clicked. The Promise will be rejected if there is no element matching `selector`. - returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully double clicked. The Promise will be rejected if there is no element matching `selector`.
@ -1032,7 +1033,7 @@ const fs = require('fs');
- `selector` <[string]> A selector to query page for. - `selector` <[string]> A selector to query page for.
- `value` <[string]> Value to fill for the `<input>`, `<textarea>` or `[contenteditable]` element. - `value` <[string]> Value to fill for the `<input>`, `<textarea>` or `[contenteditable]` element.
- `options` <[Object]> - `options` <[Object]>
- `waitFor` <"visible"|"hidden"|"any"|"nowait"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`) or do not wait at all (`nowait`). Defaults to `visible`. - `waitFor` <[boolean]> Whether to wait for the element to be present in the dom. Defaults to `true`.
- `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully filled. The promise will be rejected if there is no element matching `selector`. - 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`.
@ -1046,7 +1047,7 @@ Shortcut for [page.mainFrame().fill()](#framefillselector-value)
#### page.focus(selector, options) #### page.focus(selector, options)
- `selector` <[string]> A selector of an element to focus. If there are multiple elements satisfying the selector, the first will be focused. - `selector` <[string]> A selector of an element to focus. If there are multiple elements satisfying the selector, the first will be focused.
- `options` <[Object]> - `options` <[Object]>
- `waitFor` <"visible"|"hidden"|"any"|"nowait"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`) or do not wait at all (`nowait`). Defaults to `visible`. - `waitFor` <[boolean]> Whether to wait for the element to be present in the dom. Defaults to `true`.
- `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully focused. The promise will be rejected if there is no element matching `selector`. - 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`.
@ -1118,7 +1119,7 @@ Shortcut for [page.mainFrame().goto(url, options)](#framegotourl-options)
- x <[number]> - x <[number]>
- y <[number]> - y <[number]>
- `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the hover, and then restores current modifiers back. If not specified, currently pressed modifiers are used. - `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the hover, and then restores current modifiers back. If not specified, currently pressed modifiers are used.
- `waitFor` <"visible"|"hidden"|"any"|"nowait"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`) or do not wait at all (`nowait`). Defaults to `visible`. - `waitFor` <[boolean]> Whether to wait for the element to be present in the dom, stop moving (for example, wait until css transition finishes) and potentially receive pointer events at the click point (for example, wait until element becomes non-obscured by other elements). Defaults to `true`.
- `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully hovered. Promise gets rejected if there's no element matching `selector`. - returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully hovered. Promise gets rejected if there's no element matching `selector`.
@ -1281,7 +1282,7 @@ await browser.close();
- `label` <[string]> Matches by `option.label`. - `label` <[string]> Matches by `option.label`.
- `index` <[number]> Matches by the index. - `index` <[number]> Matches by the index.
- `options` <[Object]> - `options` <[Object]>
- `waitFor` <"visible"|"hidden"|"any"|"nowait"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`) or do not wait at all (`nowait`). Defaults to `visible`. - `waitFor` <[boolean]> Whether to wait for the element to be present in the dom. Defaults to `true`.
- `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- returns: <[Promise]<[Array]<[string]>>> An array of option values that have been successfully selected. - returns: <[Promise]<[Array]<[string]>>> An array of option values that have been successfully selected.
@ -1388,7 +1389,7 @@ Shortcut for [page.mainFrame().title()](#frametitle).
- x <[number]> - x <[number]>
- y <[number]> - y <[number]>
- `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the triple click, and then restores current modifiers back. If not specified, currently pressed modifiers are used. - `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the triple click, and then restores current modifiers back. If not specified, currently pressed modifiers are used.
- `waitFor` <"visible"|"hidden"|"any"|"nowait"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`) or do not wait at all (`nowait`). Defaults to `visible`. - `waitFor` <[boolean]> Whether to wait for the element to be present in the dom, stop moving (for example, wait until css transition finishes) and potentially receive pointer events at the click point (for example, wait until element becomes non-obscured by other elements). Defaults to `true`.
- `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully triple clicked. The Promise will be rejected if there is no element matching `selector`. - returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully triple clicked. The Promise will be rejected if there is no element matching `selector`.
@ -1406,7 +1407,7 @@ Shortcut for [page.mainFrame().tripleclick(selector[, options])](#frametriplecli
- `text` <[string]> A text to type into a focused element. - `text` <[string]> A text to type into a focused element.
- `options` <[Object]> - `options` <[Object]>
- `delay` <[number]> Time to wait between key presses in milliseconds. Defaults to 0. - `delay` <[number]> Time to wait between key presses in milliseconds. Defaults to 0.
- `waitFor` <"visible"|"hidden"|"any"|"nowait"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`) or do not wait at all (`nowait`). Defaults to `visible`. - `waitFor` <[boolean]> Whether to wait for the element to be present in the dom. Defaults to `true`.
- `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- returns: <[Promise]> - returns: <[Promise]>
@ -1424,9 +1425,9 @@ Shortcut for [page.mainFrame().type(selector, text[, options])](#frametypeselect
#### page.uncheck(selector, [options]) #### page.uncheck(selector, [options])
- `selector` <[string]> A selector to search for uncheckbox to check. If there are multiple elements satisfying the selector, the first will be checked. - `selector` <[string]> A selector to search for uncheckbox to check. If there are multiple elements satisfying the selector, the first will be checked.
- `options` <[Object]> - `options` <[Object]>
- `waitFor` <"visible"|"hidden"|"any"|"nowait"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`) or do not wait at all (`nowait`). Defaults to `visible`. - `waitFor` <[boolean]> Whether to wait for the element to be present in the dom, stop moving (for example, wait until css transition finishes) and potentially receive pointer events at the click point (for example, wait until element becomes non-obscured by other elements). Defaults to `true`.
- `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully checked. The Promise will be rejected if there is no element matching `selector`. - returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully unchecked. The Promise will be rejected if there is no element matching `selector`.
This method fetches an element with `selector`, if element is not already unchecked, it scrolls it into view if needed, and then uses [page.click](#pageclickselector-options) to click in the center of the element. This method fetches an element with `selector`, if element is not already unchecked, it scrolls it into view if needed, and then uses [page.click](#pageclickselector-options) to click in the center of the element.
If there's no element matching `selector`, the method throws an error. If there's no element matching `selector`, the method throws an error.
@ -1794,7 +1795,7 @@ Adds a `<link rel="stylesheet">` tag into the page with the desired url or a `<s
#### frame.check(selector, [options]) #### frame.check(selector, [options])
- `selector` <[string]> A selector to search for checkbox to check. If there are multiple elements satisfying the selector, the first will be checked. - `selector` <[string]> A selector to search for checkbox to check. If there are multiple elements satisfying the selector, the first will be checked.
- `options` <[Object]> - `options` <[Object]>
- `waitFor` <"visible"|"hidden"|"any"|"nowait"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`) or do not wait at all (`nowait`). Defaults to `visible`. - `waitFor` <[boolean]> Whether to wait for the element to be present in the dom, stop moving (for example, wait until css transition finishes) and potentially receive pointer events at the click point (for example, wait until element becomes non-obscured by other elements). Defaults to `true`.
- `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully checked. The Promise will be rejected if there is no element matching `selector`. - returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully checked. The Promise will be rejected if there is no element matching `selector`.
@ -1814,7 +1815,7 @@ If there's no element matching `selector`, the method throws an error.
- x <[number]> - x <[number]>
- y <[number]> - y <[number]>
- `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the click, and then restores current modifiers back. If not specified, currently pressed modifiers are used. - `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the click, and then restores current modifiers back. If not specified, currently pressed modifiers are used.
- `waitFor` <"visible"|"hidden"|"any"|"nowait"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`) or do not wait at all (`nowait`). Defaults to `visible`. - `waitFor` <[boolean]> Whether to wait for the element to be present in the dom, stop moving (for example, wait until css transition finishes) and potentially receive pointer events at the click point (for example, wait until element becomes non-obscured by other elements). Defaults to `true`.
- `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully clicked. The Promise will be rejected if there is no element matching `selector`. - returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully clicked. The Promise will be rejected if there is no element matching `selector`.
@ -1844,7 +1845,7 @@ Gets the full HTML contents of the frame, including the doctype.
- x <[number]> - x <[number]>
- y <[number]> - y <[number]>
- `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the double click, and then restores current modifiers back. If not specified, currently pressed modifiers are used. - `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the double click, and then restores current modifiers back. If not specified, currently pressed modifiers are used.
- `waitFor` <"visible"|"hidden"|"any"|"nowait"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`) or do not wait at all (`nowait`). Defaults to `visible`. - `waitFor` <[boolean]> Whether to wait for the element to be present in the dom, stop moving (for example, wait until css transition finishes) and potentially receive pointer events at the click point (for example, wait until element becomes non-obscured by other elements). Defaults to `true`.
- `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully double clicked. The Promise will be rejected if there is no element matching `selector`. - returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully double clicked. The Promise will be rejected if there is no element matching `selector`.
@ -1916,7 +1917,7 @@ await resultHandle.dispose();
- `selector` <[string]> A selector to query page for. - `selector` <[string]> A selector to query page for.
- `value` <[string]> Value to fill for the `<input>`, `<textarea>` or `[contenteditable]` element. - `value` <[string]> Value to fill for the `<input>`, `<textarea>` or `[contenteditable]` element.
- `options` <[Object]> - `options` <[Object]>
- `waitFor` <"visible"|"hidden"|"any"|"nowait"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`) or do not wait at all (`nowait`). Defaults to `visible`. - `waitFor` <[boolean]> Whether to wait for the element to be present in the dom. Defaults to `true`.
- `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully filled. The promise will be rejected if there is no element matching `selector`. - 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`.
@ -1926,7 +1927,7 @@ If there's no text `<input>`, `<textarea>` or `[contenteditable]` element matchi
#### frame.focus(selector, options) #### frame.focus(selector, options)
- `selector` <[string]> A selector of an element to focus. If there are multiple elements satisfying the selector, the first will be focused. - `selector` <[string]> A selector of an element to focus. If there are multiple elements satisfying the selector, the first will be focused.
- `options` <[Object]> - `options` <[Object]>
- `waitFor` <"visible"|"hidden"|"any"|"nowait"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`) or do not wait at all (`nowait`). Defaults to `visible`. - `waitFor` <[boolean]> Whether to wait for the element to be present in the dom. Defaults to `true`.
- `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully focused. The promise will be rejected if there is no element matching `selector`. - 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`.
@ -1979,7 +1980,7 @@ console.log(frame === contentFrame); // -> true
- x <[number]> - x <[number]>
- y <[number]> - y <[number]>
- `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the hover, and then restores current modifiers back. If not specified, currently pressed modifiers are used. - `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the hover, and then restores current modifiers back. If not specified, currently pressed modifiers are used.
- `waitFor` <"visible"|"hidden"|"any"|"nowait"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`) or do not wait at all (`nowait`). Defaults to `visible`. - `waitFor` <[boolean]> Whether to wait for the element to be present in the dom, stop moving (for example, wait until css transition finishes) and potentially receive pointer events at the click point (for example, wait until element becomes non-obscured by other elements). Defaults to `true`.
- `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully hovered. Promise gets rejected if there's no element matching `selector`. - returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully hovered. Promise gets rejected if there's no element matching `selector`.
@ -2010,7 +2011,7 @@ If the name is empty, returns the id attribute instead.
- `label` <[string]> Matches by `option.label`. - `label` <[string]> Matches by `option.label`.
- `index` <[number]> Matches by the index. - `index` <[number]> Matches by the index.
- `options` <[Object]> - `options` <[Object]>
- `waitFor` <"visible"|"hidden"|"any"|"nowait"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`) or do not wait at all (`nowait`). Defaults to `visible`. - `waitFor` <[boolean]> Whether to wait for the element to be present in the dom. Defaults to `true`.
- `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- returns: <[Promise]<[Array]<[string]>>> An array of option values that have been successfully selected. - returns: <[Promise]<[Array]<[string]>>> An array of option values that have been successfully selected.
@ -2054,7 +2055,7 @@ frame.select('select#colors', { value: 'blue' }, { index: 2 }, 'red');
- x <[number]> - x <[number]>
- y <[number]> - y <[number]>
- `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the triple click, and then restores current modifiers back. If not specified, currently pressed modifiers are used. - `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the triple click, and then restores current modifiers back. If not specified, currently pressed modifiers are used.
- `waitFor` <"visible"|"hidden"|"any"|"nowait"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`) or do not wait at all (`nowait`). Defaults to `visible`. - `waitFor` <[boolean]> Whether to wait for the element to be present in the dom, stop moving (for example, wait until css transition finishes) and potentially receive pointer events at the click point (for example, wait until element becomes non-obscured by other elements). Defaults to `true`.
- `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully triple clicked. The Promise will be rejected if there is no element matching `selector`. - returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully triple clicked. The Promise will be rejected if there is no element matching `selector`.
@ -2070,7 +2071,7 @@ Bear in mind that if the first or second click of the `tripleclick()` triggers a
- `text` <[string]> A text to type into a focused element. - `text` <[string]> A text to type into a focused element.
- `options` <[Object]> - `options` <[Object]>
- `delay` <[number]> Time to wait between key presses in milliseconds. Defaults to 0. - `delay` <[number]> Time to wait between key presses in milliseconds. Defaults to 0.
- `waitFor` <"visible"|"hidden"|"any"|"nowait"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`) or do not wait at all (`nowait`). Defaults to `visible`. - `waitFor` <[boolean]> Whether to wait for the element to be present in the dom. Defaults to `true`.
- `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- returns: <[Promise]> - returns: <[Promise]>
@ -2086,9 +2087,9 @@ await frame.type('#mytextarea', 'World', {delay: 100}); // Types slower, like a
#### frame.uncheck(selector, [options]) #### frame.uncheck(selector, [options])
- `selector` <[string]> A selector to search for uncheckbox to check. If there are multiple elements satisfying the selector, the first will be checked. - `selector` <[string]> A selector to search for uncheckbox to check. If there are multiple elements satisfying the selector, the first will be checked.
- `options` <[Object]> - `options` <[Object]>
- `waitFor` <"visible"|"hidden"|"any"|"nowait"> Wait for element to become visible (`visible`), hidden (`hidden`), present in dom (`any`) or do not wait at all (`nowait`). Defaults to `visible`. - `waitFor` <[boolean]> Whether to wait for the element to be present in the dom, stop moving (for example, wait until css transition finishes) and potentially receive pointer events at the click point (for example, wait until element becomes non-obscured by other elements). Defaults to `true`.
- `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods. - `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully checked. The Promise will be rejected if there is no element matching `selector`. - returns: <[Promise]> Promise which resolves when the element matching `selector` is successfully unchecked. The Promise will be rejected if there is no element matching `selector`.
This method fetches an element with `selector`, if element is not already unchecked, it scrolls it into view if needed, and then uses [frame.click](#frameclickselector-options) to click in the center of the element. This method fetches an element with `selector`, if element is not already unchecked, it scrolls it into view if needed, and then uses [frame.click](#frameclickselector-options) to click in the center of the element.
If there's no element matching `selector`, the method throws an error. If there's no element matching `selector`, the method throws an error.
@ -2263,7 +2264,7 @@ ElementHandle instances can be used as arguments in [`page.$eval()`](#pageevalse
- [elementHandle.$$eval(selector, pageFunction[, ...args])](#elementhandleevalselector-pagefunction-args) - [elementHandle.$$eval(selector, pageFunction[, ...args])](#elementhandleevalselector-pagefunction-args)
- [elementHandle.$eval(selector, pageFunction[, ...args])](#elementhandleevalselector-pagefunction-args-1) - [elementHandle.$eval(selector, pageFunction[, ...args])](#elementhandleevalselector-pagefunction-args-1)
- [elementHandle.boundingBox()](#elementhandleboundingbox) - [elementHandle.boundingBox()](#elementhandleboundingbox)
- [elementHandle.check()](#elementhandlecheck) - [elementHandle.check([options])](#elementhandlecheckoptions)
- [elementHandle.click([options])](#elementhandleclickoptions) - [elementHandle.click([options])](#elementhandleclickoptions)
- [elementHandle.contentFrame()](#elementhandlecontentframe) - [elementHandle.contentFrame()](#elementhandlecontentframe)
- [elementHandle.dblclick([options])](#elementhandledblclickoptions) - [elementHandle.dblclick([options])](#elementhandledblclickoptions)
@ -2279,8 +2280,7 @@ ElementHandle instances can be used as arguments in [`page.$eval()`](#pageevalse
- [elementHandle.toString()](#elementhandletostring) - [elementHandle.toString()](#elementhandletostring)
- [elementHandle.tripleclick([options])](#elementhandletripleclickoptions) - [elementHandle.tripleclick([options])](#elementhandletripleclickoptions)
- [elementHandle.type(text[, options])](#elementhandletypetext-options) - [elementHandle.type(text[, options])](#elementhandletypetext-options)
- [elementHandle.uncheck()](#elementhandleuncheck) - [elementHandle.uncheck([options])](#elementhandleuncheckoptions)
- [elementHandle.visibleRatio()](#elementhandlevisibleratio)
<!-- GEN:stop --> <!-- GEN:stop -->
<!-- GEN:toc-extends-JSHandle --> <!-- GEN:toc-extends-JSHandle -->
- [jsHandle.asElement()](#jshandleaselement) - [jsHandle.asElement()](#jshandleaselement)
@ -2352,7 +2352,10 @@ expect(await tweetHandle.$eval('.retweets', node => node.innerText)).toBe('10');
This method returns the bounding box of the element (relative to the main frame), or `null` if the element is not visible. This method returns the bounding box of the element (relative to the main frame), or `null` if the element is not visible.
#### elementHandle.check() #### elementHandle.check([options])
- `options` <[Object]>
- `waitFor` <[boolean]> Whether to wait for the element to stop moving (for example, wait until css transition finishes) and potentially receive pointer events at the click point (for example, wait until element becomes non-obscured by other elements). Defaults to `true`.
- `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- returns: <[Promise]> Promise which resolves when the element is successfully checked. Promise gets rejected if the operation fails. - returns: <[Promise]> Promise which resolves when the element is successfully checked. Promise gets rejected if the operation fails.
If element is not already checked, it scrolls it into view if needed, and then uses [elementHandle.click](#elementhandleclickoptions) to click in the center of the element. If element is not already checked, it scrolls it into view if needed, and then uses [elementHandle.click](#elementhandleclickoptions) to click in the center of the element.
@ -2366,6 +2369,8 @@ If element is not already checked, it scrolls it into view if needed, and then u
- x <[number]> - x <[number]>
- y <[number]> - y <[number]>
- `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the click, and then restores current modifiers back. If not specified, currently pressed modifiers are used. - `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the click, and then restores current modifiers back. If not specified, currently pressed modifiers are used.
- `waitFor` <[boolean]> Whether to wait for the element to stop moving (for example, wait until css transition finishes) and potentially receive pointer events at the click point (for example, wait until element becomes non-obscured by other elements). Defaults to `true`.
- `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- returns: <[Promise]> Promise which resolves when the element is successfully clicked. Promise gets rejected if the element is detached from DOM. - returns: <[Promise]> Promise which resolves when the element is successfully clicked. Promise gets rejected if the element is detached from DOM.
This method scrolls element into view if needed, and then uses [page.mouse](#pagemouse) to click in the center of the element. This method scrolls element into view if needed, and then uses [page.mouse](#pagemouse) to click in the center of the element.
@ -2382,6 +2387,8 @@ If the element is detached from DOM, the method throws an error.
- x <[number]> - x <[number]>
- y <[number]> - y <[number]>
- `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the double click, and then restores current modifiers back. If not specified, currently pressed modifiers are used. - `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the double click, and then restores current modifiers back. If not specified, currently pressed modifiers are used.
- `waitFor` <[boolean]> Whether to wait for the element to stop moving (for example, wait until css transition finishes) and potentially receive pointer events at the click point (for example, wait until element becomes non-obscured by other elements). Defaults to `true`.
- `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- returns: <[Promise]> Promise which resolves when the element is successfully double clicked. Promise gets rejected if the element is detached from DOM. - returns: <[Promise]> Promise which resolves when the element is successfully double clicked. Promise gets rejected if the element is detached from DOM.
This method scrolls element into view if needed, and then uses [page.mouse](#pagemouse) to click in the center of the element. This method scrolls element into view if needed, and then uses [page.mouse](#pagemouse) to click in the center of the element.
@ -2409,6 +2416,8 @@ Calls [focus](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus
- x <[number]> - x <[number]>
- y <[number]> - y <[number]>
- `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the hover, and then restores current modifiers back. If not specified, currently pressed modifiers are used. - `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the hover, and then restores current modifiers back. If not specified, currently pressed modifiers are used.
- `waitFor` <[boolean]> Whether to wait for the element to stop moving (for example, wait until css transition finishes) and potentially receive pointer events at the click point (for example, wait until element becomes non-obscured by other elements). Defaults to `true`.
- `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- returns: <[Promise]> Promise which resolves when the element is successfully hovered. - returns: <[Promise]> Promise which resolves when the element is successfully hovered.
This method scrolls element into view if needed, and then uses [page.mouse](#pagemouse) to hover over the center of the element. This method scrolls element into view if needed, and then uses [page.mouse](#pagemouse) to hover over the center of the element.
@ -2444,7 +2453,7 @@ If the element is detached from DOM, the method throws an error.
#### elementHandle.scrollIntoViewIfNeeded() #### elementHandle.scrollIntoViewIfNeeded()
- returns: <[Promise]> Resolves after the element has been scrolled into view. - returns: <[Promise]> Resolves after the element has been scrolled into view.
This method tries to scroll element into view, unless it is completely visible as defined by [IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)'s ```ratio```. See also [elementHandle.visibleRatio()](#elementhandlevisibleratio). This method tries to scroll element into view, unless it is completely visible as defined by [IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)'s ```ratio```.
Throws when ```elementHandle``` does not point to an element [connected](https://developer.mozilla.org/en-US/docs/Web/API/Node/isConnected) to a Document or a ShadowRoot. Throws when ```elementHandle``` does not point to an element [connected](https://developer.mozilla.org/en-US/docs/Web/API/Node/isConnected) to a Document or a ShadowRoot.
@ -2494,6 +2503,8 @@ This method expects `elementHandle` to point to an [input element](https://devel
- x <[number]> - x <[number]>
- y <[number]> - y <[number]>
- `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the triple click, and then restores current modifiers back. If not specified, currently pressed modifiers are used. - `modifiers` <[Array]<"Alt"|"Control"|"Meta"|"Shift">> Modifier keys to press. Ensures that only these modifiers are pressed during the triple click, and then restores current modifiers back. If not specified, currently pressed modifiers are used.
- `waitFor` <[boolean]> Whether to wait for the element to stop moving (for example, wait until css transition finishes) and potentially receive pointer events at the click point (for example, wait until element becomes non-obscured by other elements). Defaults to `true`.
- `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- returns: <[Promise]> Promise which resolves when the element is successfully triple clicked. Promise gets rejected if the element is detached from DOM. - returns: <[Promise]> Promise which resolves when the element is successfully triple clicked. Promise gets rejected if the element is detached from DOM.
This method scrolls element into view if needed, and then uses [page.mouse](#pagemouse) to click in the center of the element. This method scrolls element into view if needed, and then uses [page.mouse](#pagemouse) to click in the center of the element.
@ -2525,16 +2536,14 @@ await elementHandle.type('some text');
await elementHandle.press('Enter'); await elementHandle.press('Enter');
``` ```
#### elementHandle.uncheck() #### elementHandle.uncheck([options])
- returns: <[Promise]> Promise which resolves when the element is successfully checked. Promise gets rejected if the operation fails. - `options` <[Object]>
- `waitFor` <[boolean]> Whether to wait for the element to stop moving (for example, wait until css transition finishes) and potentially receive pointer events at the click point (for example, wait until element becomes non-obscured by other elements). Defaults to `true`.
- `timeout` <[number]> Maximum time in milliseconds, defaults to 30 seconds, pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
- returns: <[Promise]> Promise which resolves when the element is successfully unchecked. Promise gets rejected if the operation fails.
If element is not already unchecked, it scrolls it into view if needed, and then uses [elementHandle.click](#elementhandleclickoptions) to click in the center of the element. If element is not already unchecked, it scrolls it into view if needed, and then uses [elementHandle.click](#elementhandleclickoptions) to click in the center of the element.
#### elementHandle.visibleRatio()
- returns: <[Promise]<[number]>> Returns the visible ratio as defined by [IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API).
Positive ratio means that some part of the element is visible in the current viewport. Ratio equal to one means that element is completely visible.
### class: JSHandle ### class: JSHandle
JSHandle represents an in-page JavaScript object. JSHandles can be created with the [page.evaluateHandle](#pageevaluatehandlepagefunction-args) method. JSHandle represents an in-page JavaScript object. JSHandles can be created with the [page.evaluateHandle](#pageevaluatehandlepagefunction-args) method.
@ -3469,7 +3478,7 @@ const browser = await chromium.launch({ // Or 'firefox' or 'webkit'.
- `dumpio` <[boolean]> Whether to pipe the browser process stdout and stderr into `process.stdout` and `process.stderr`. Defaults to `false`. - `dumpio` <[boolean]> Whether to pipe the browser process stdout and stderr into `process.stdout` and `process.stderr`. Defaults to `false`.
- `env` <[Object]> Specify environment variables that will be visible to the browser. Defaults to `process.env`. - `env` <[Object]> Specify environment variables that will be visible to the browser. Defaults to `process.env`.
- `devtools` <[boolean]> **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the `headless` option will be set `false`. - `devtools` <[boolean]> **Chromium-only** Whether to auto-open a Developer Tools panel for each tab. If this option is `true`, the `headless` option will be set `false`.
- returns: <[Promise]<[BrowserServer]>> Promise which resolves to the browser app instance. - returns: <[Promise]<[BrowserContext]>> Promise which resolves to the browser app instance.
Launches browser instance that uses persistent storage located at `userDataDir`. If `userDataDir` is not specified, temporary folder is created for the persistent storage. That folder is deleted when browser closes. Launches browser instance that uses persistent storage located at `userDataDir`. If `userDataDir` is not specified, temporary folder is created for the persistent storage. That folder is deleted when browser closes.
@ -3524,16 +3533,9 @@ await browser.stopTracing();
``` ```
<!-- GEN:toc --> <!-- GEN:toc -->
- [event: 'targetchanged'](#event-targetchanged)
- [event: 'targetcreated'](#event-targetcreated)
- [event: 'targetdestroyed'](#event-targetdestroyed)
- [chromiumBrowser.browserTarget()](#chromiumbrowserbrowsertarget) - [chromiumBrowser.browserTarget()](#chromiumbrowserbrowsertarget)
- [chromiumBrowser.pageTarget(page)](#chromiumbrowserpagetargetpage)
- [chromiumBrowser.serviceWorker(target)](#chromiumbrowserserviceworkertarget)
- [chromiumBrowser.startTracing(page, [options])](#chromiumbrowserstarttracingpage-options) - [chromiumBrowser.startTracing(page, [options])](#chromiumbrowserstarttracingpage-options)
- [chromiumBrowser.stopTracing()](#chromiumbrowserstoptracing) - [chromiumBrowser.stopTracing()](#chromiumbrowserstoptracing)
- [chromiumBrowser.targets(context)](#chromiumbrowsertargetscontext)
- [chromiumBrowser.waitForTarget(predicate[, options])](#chromiumbrowserwaitfortargetpredicate-options)
<!-- GEN:stop --> <!-- GEN:stop -->
<!-- GEN:toc-extends-Browser --> <!-- GEN:toc-extends-Browser -->
- [event: 'disconnected'](#event-disconnected) - [event: 'disconnected'](#event-disconnected)
@ -3544,43 +3546,11 @@ await browser.stopTracing();
- [browser.newPage([options])](#browsernewpageoptions) - [browser.newPage([options])](#browsernewpageoptions)
<!-- GEN:stop --> <!-- GEN:stop -->
#### event: 'targetchanged'
- <[Target]>
Emitted when the url of a target changes.
> **NOTE** This includes target changes in incognito browser contexts.
#### event: 'targetcreated'
- <[Target]>
Emitted when a target is created, for example when a new page is opened by [`window.open`](https://developer.mozilla.org/en-US/docs/Web/API/Window/open) or [`browserContext.newPage`](#browsercontextnewpage).
> **NOTE** This includes target creations in incognito browser contexts.
#### event: 'targetdestroyed'
- <[Target]>
Emitted when a target is destroyed, for example when a page is closed.
> **NOTE** This includes target destructions in incognito browser contexts.
#### chromiumBrowser.browserTarget() #### chromiumBrowser.browserTarget()
- returns: <[Target]> - returns: <[ChromiumTarget]>
Returns browser target. Returns browser target.
#### chromiumBrowser.pageTarget(page)
- `page` <[Page]> Page to return target for.
- returns: <[Target]> a target given page was created from.
#### chromiumBrowser.serviceWorker(target)
- `target` <[ChromiumTarget]> Target to treat as a service worker
- returns: <[Promise]<[ChromiumWorker]>>
Attaches to the service worker target.
#### chromiumBrowser.startTracing(page, [options]) #### chromiumBrowser.startTracing(page, [options])
- `page` <[Page]> Optional, if specified, tracing includes screenshots of the given page. - `page` <[Page]> Optional, if specified, tracing includes screenshots of the given page.
- `options` <[Object]> - `options` <[Object]>
@ -3594,25 +3564,83 @@ Only one trace can be active at a time per browser.
#### chromiumBrowser.stopTracing() #### chromiumBrowser.stopTracing()
- returns: <[Promise]<[Buffer]>> Promise which resolves to buffer with trace data. - returns: <[Promise]<[Buffer]>> Promise which resolves to buffer with trace data.
#### chromiumBrowser.targets(context) ### class: ChromiumBrowserContext
- `context` <[BrowserContext]> Optional, if specified, only targets from this context are returned.
- returns: <[Array]<[Target]>>
An array of all active targets inside the Browser. In case of multiple browser contexts, * extends: [BrowserContext]
the method will return an array with all the targets in all browser contexts.
#### chromiumBrowser.waitForTarget(predicate[, options]) Chromium-specific features including targets, service worker support, etc.
- `predicate` <[function]\([Target]\):[boolean]> A function to be run for every target
```js
const backroundPageTarget = await context.waitForTarget(target => target.type() === 'background_page');
const backgroundPage = await backroundPageTarget.page();
```
<!-- GEN:toc -->
- [event: 'targetchanged'](#event-targetchanged)
- [event: 'targetcreated'](#event-targetcreated)
- [event: 'targetdestroyed'](#event-targetdestroyed)
- [chromiumBrowserContext.pageTarget(page)](#chromiumbrowsercontextpagetargetpage)
- [chromiumBrowserContext.targets()](#chromiumbrowsercontexttargets)
- [chromiumBrowserContext.waitForTarget(predicate[, options])](#chromiumbrowsercontextwaitfortargetpredicate-options)
<!-- GEN:stop -->
<!-- GEN:toc-extends-BrowserContext -->
- [event: 'close'](#event-close)
- [browserContext.clearCookies()](#browsercontextclearcookies)
- [browserContext.clearPermissions()](#browsercontextclearpermissions)
- [browserContext.close()](#browsercontextclose)
- [browserContext.cookies([...urls])](#browsercontextcookiesurls)
- [browserContext.newPage()](#browsercontextnewpage)
- [browserContext.pages()](#browsercontextpages)
- [browserContext.setCookies(cookies)](#browsercontextsetcookiescookies)
- [browserContext.setDefaultNavigationTimeout(timeout)](#browsercontextsetdefaultnavigationtimeouttimeout)
- [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout)
- [browserContext.setGeolocation(geolocation)](#browsercontextsetgeolocationgeolocation)
- [browserContext.setPermissions(origin, permissions[])](#browsercontextsetpermissionsorigin-permissions)
<!-- GEN:stop -->
#### event: 'targetchanged'
- <[ChromiumTarget]>
Emitted when the url of a target changes.
> **NOTE** Only includes targets from this browser context.
#### event: 'targetcreated'
- <[ChromiumTarget]>
Emitted when a target is created, for example when a new page is opened by [`window.open`](https://developer.mozilla.org/en-US/docs/Web/API/Window/open) or [`browserContext.newPage`](#browsercontextnewpage).
> **NOTE** Only includes targets from this browser context.
#### event: 'targetdestroyed'
- <[ChromiumTarget]>
Emitted when a target is destroyed, for example when a page is closed.
> **NOTE** Only includes targets from this browser context.
#### chromiumBrowserContext.pageTarget(page)
- `page` <[Page]> Page to return target for.
- returns: <[ChromiumTarget]> a target given page was created from.
#### chromiumBrowserContext.targets()
- returns: <[Array]<[ChromiumTarget]>>
An array of all active targets inside the browser context.
#### chromiumBrowserContext.waitForTarget(predicate[, options])
- `predicate` <[function]\([ChromiumTarget]\):[boolean]> A function to be run for every target
- `options` <[Object]> - `options` <[Object]>
- `timeout` <[number]> Maximum wait time in milliseconds. Pass `0` to disable the timeout. Defaults to 30 seconds. - `timeout` <[number]> Maximum wait time in milliseconds. Pass `0` to disable the timeout. Defaults to 30 seconds.
- returns: <[Promise]<[Target]>> Promise which resolves to the first target found that matches the `predicate` function. - returns: <[Promise]<[ChromiumTarget]>> Promise which resolves to the first target found that matches the `predicate` function.
This searches for a target in all browser contexts. This searches for a target in the browser context.
An example of finding a target for a page opened via `window.open`: An example of finding a target for a page opened via `window.open`:
```js ```js
await page.evaluate(() => window.open('https://www.example.com/')); await page.evaluate(() => window.open('https://www.example.com/'));
const newWindowTarget = await browser.chromium.waitForTarget(target => target.url() === 'https://www.example.com/'); const newWindowTarget = await page.context().waitForTarget(target => target.url() === 'https://www.example.com/');
``` ```
### class: ChromiumCoverage ### class: ChromiumCoverage
@ -3727,6 +3755,7 @@ to send messages.
- [chromiumTarget.createCDPSession()](#chromiumtargetcreatecdpsession) - [chromiumTarget.createCDPSession()](#chromiumtargetcreatecdpsession)
- [chromiumTarget.opener()](#chromiumtargetopener) - [chromiumTarget.opener()](#chromiumtargetopener)
- [chromiumTarget.page()](#chromiumtargetpage) - [chromiumTarget.page()](#chromiumtargetpage)
- [chromiumTarget.serviceWorker()](#chromiumtargetserviceworker)
- [chromiumTarget.type()](#chromiumtargettype) - [chromiumTarget.type()](#chromiumtargettype)
- [chromiumTarget.url()](#chromiumtargeturl) - [chromiumTarget.url()](#chromiumtargeturl)
<!-- GEN:stop --> <!-- GEN:stop -->
@ -3743,7 +3772,7 @@ The browser context the target belongs to.
Creates a Chrome Devtools Protocol session attached to the target. Creates a Chrome Devtools Protocol session attached to the target.
#### chromiumTarget.opener() #### chromiumTarget.opener()
- returns: <?[Target]> - returns: <?[ChromiumTarget]>
Get the target that opened this target. Top-level targets return `null`. Get the target that opened this target. Top-level targets return `null`.
@ -3752,6 +3781,11 @@ Get the target that opened this target. Top-level targets return `null`.
If the target is not of type `"page"` or `"background_page"`, returns `null`. If the target is not of type `"page"` or `"background_page"`, returns `null`.
#### chromiumTarget.serviceWorker()
- returns: <[Promise]<?[Worker]>>
Attaches to the service worker target. If the target is not of type `"service_worker"`, returns `null`.
#### chromiumTarget.type() #### chromiumTarget.type()
- returns: <"page"|"background_page"|"service_worker"|"shared_worker"|"other"|"browser"> - returns: <"page"|"background_page"|"service_worker"|"shared_worker"|"other"|"browser">
@ -3868,6 +3902,7 @@ const { chromium } = require('playwright');
[Buffer]: https://nodejs.org/api/buffer.html#buffer_class_buffer "Buffer" [Buffer]: https://nodejs.org/api/buffer.html#buffer_class_buffer "Buffer"
[ChildProcess]: https://nodejs.org/api/child_process.html "ChildProcess" [ChildProcess]: https://nodejs.org/api/child_process.html "ChildProcess"
[ChromiumBrowser]: #class-chromiumbrowser "ChromiumBrowser" [ChromiumBrowser]: #class-chromiumbrowser "ChromiumBrowser"
[ChromiumBrowserContext]: #class-chromiumbrowsercontext "ChromiumBrowserContext"
[ChromiumSession]: #class-chromiumsession "ChromiumSession" [ChromiumSession]: #class-chromiumsession "ChromiumSession"
[ChromiumTarget]: #class-chromiumtarget "ChromiumTarget" [ChromiumTarget]: #class-chromiumtarget "ChromiumTarget"
[ConsoleMessage]: #class-consolemessage "ConsoleMessage" [ConsoleMessage]: #class-consolemessage "ConsoleMessage"
@ -3893,7 +3928,6 @@ const { chromium } = require('playwright');
[Response]: #class-response "Response" [Response]: #class-response "Response"
[Selectors]: #class-selectors "Selectors" [Selectors]: #class-selectors "Selectors"
[Serializable]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#Description "Serializable" [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" [TimeoutError]: #class-timeouterror "TimeoutError"
[UIEvent.detail]: https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail "UIEvent.detail" [UIEvent.detail]: https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail "UIEvent.detail"
[URL]: https://nodejs.org/api/url.html [URL]: https://nodejs.org/api/url.html

View file

@ -25,6 +25,7 @@
* [e2e Boilerplates](https://github.com/e2e-boilerplates?utf8=%E2%9C%93&q=playwright): Project boilerplates for using Playwright with TypeScript, Cucumber, Jest, and other libraries * [e2e Boilerplates](https://github.com/e2e-boilerplates?utf8=%E2%9C%93&q=playwright): Project boilerplates for using Playwright with TypeScript, Cucumber, Jest, and other libraries
* [react-app-playwright](https://github.com/KyleADay/react-app-playwright): Using Playwright with a create-react-app project * [react-app-playwright](https://github.com/KyleADay/react-app-playwright): Using Playwright with a create-react-app project
* [playwright-react-typescript-jest-example](https://github.com/azemetre/playwright-react-typescript-jest-example): Using Playwright + Jest with a custom webpack configuration for React + Typescript project
* [playwright-mocha](https://github.com/roggerfe/playwright-mocha): Using Playwright with Mocha and Chai * [playwright-mocha](https://github.com/roggerfe/playwright-mocha): Using Playwright with Mocha and Chai
* [playwright-github-actions](https://github.com/arjun27/playwright-github-actions/actions): Playwright setup on GitHub Actions * [playwright-github-actions](https://github.com/arjun27/playwright-github-actions/actions): Playwright setup on GitHub Actions
* [playwright-azure-functions](https://github.com/arjun27/playwright-azure-functions): Playwright setup on Azure Functions * [playwright-azure-functions](https://github.com/arjun27/playwright-azure-functions): Playwright setup on Azure Functions

View file

@ -21,7 +21,7 @@
"wunit": "npm run wtest", "wunit": "npm run wtest",
"debug-test": "node --inspect-brk test/test.js", "debug-test": "node --inspect-brk test/test.js",
"test-doclint": "node utils/doclint/check_public_api/test/test.js && node utils/doclint/preprocessor/test.js", "test-doclint": "node utils/doclint/check_public_api/test/test.js && node utils/doclint/preprocessor/test.js",
"test": "npm run lint --silent && npm run coverage && npm run test-doclint && node utils/testrunner/test/test.js", "test": "npm run lint --silent && npm run ccoverage && npm run fcoverage && npm run wcoverage && npm run test-doclint && node utils/testrunner/test/test.js",
"prepare": "node prepare.js", "prepare": "node prepare.js",
"lint": "([ \"$CI\" = true ] && eslint --quiet -f codeframe --ext js,ts ./src || eslint --ext js,ts ./src) && npm run tsc && npm run doc", "lint": "([ \"$CI\" = true ] && eslint --quiet -f codeframe --ext js,ts ./src || eslint --ext js,ts ./src) && npm run tsc && npm run doc",
"doc": "node utils/doclint/cli.js", "doc": "node utils/doclint/cli.js",
@ -62,6 +62,7 @@
"@types/ws": "^6.0.1", "@types/ws": "^6.0.1",
"@typescript-eslint/eslint-plugin": "^2.6.1", "@typescript-eslint/eslint-plugin": "^2.6.1",
"@typescript-eslint/parser": "^2.6.1", "@typescript-eslint/parser": "^2.6.1",
"colors": "^1.4.0",
"commonmark": "^0.28.1", "commonmark": "^0.28.1",
"cross-env": "^5.0.5", "cross-env": "^5.0.5",
"eslint": "^6.6.0", "eslint": "^6.6.0",

View file

@ -29,6 +29,7 @@ export { FileChooser, Page, Worker } from './page';
export { Selectors } from './selectors'; export { Selectors } from './selectors';
export { CRBrowser as ChromiumBrowser } from './chromium/crBrowser'; export { CRBrowser as ChromiumBrowser } from './chromium/crBrowser';
export { CRBrowserContext as ChromiumBrowserContext } from './chromium/crBrowser';
export { CRCoverage as ChromiumCoverage } from './chromium/crCoverage'; export { CRCoverage as ChromiumCoverage } from './chromium/crCoverage';
export { CRSession as ChromiumSession } from './chromium/crConnection'; export { CRSession as ChromiumSession } from './chromium/crConnection';
export { CRTarget as ChromiumTarget } from './chromium/crTarget'; export { CRTarget as ChromiumTarget } from './chromium/crTarget';

View file

@ -19,26 +19,8 @@ import { Page } from './page';
import * as network from './network'; import * as network from './network';
import * as types from './types'; import * as types from './types';
import { helper } from './helper'; import { helper } from './helper';
import * as platform from './platform';
import { Events } from './events';
import { TimeoutSettings } from './timeoutSettings'; import { TimeoutSettings } from './timeoutSettings';
export interface BrowserContextDelegate {
pages(): Promise<Page[]>;
existingPages(): Page[];
newPage(): Promise<Page>;
close(): Promise<void>;
cookies(): Promise<network.NetworkCookie[]>;
setCookies(cookies: network.SetNetworkCookieParam[]): Promise<void>;
clearCookies(): Promise<void>;
setPermissions(origin: string, permissions: string[]): Promise<void>;
clearPermissions(): Promise<void>;
setGeolocation(geolocation: types.Geolocation | null): Promise<void>;
}
export type BrowserContextOptions = { export type BrowserContextOptions = {
viewport?: types.Viewport | null, viewport?: types.Viewport | null,
ignoreHTTPSErrors?: boolean, ignoreHTTPSErrors?: boolean,
@ -51,106 +33,44 @@ export type BrowserContextOptions = {
permissions?: { [key: string]: string[] }; permissions?: { [key: string]: string[] };
}; };
export class BrowserContext extends platform.EventEmitter { export interface BrowserContext {
private readonly _delegate: BrowserContextDelegate; setDefaultNavigationTimeout(timeout: number): void;
readonly _options: BrowserContextOptions; setDefaultTimeout(timeout: number): void;
pages(): Promise<Page[]>;
newPage(): Promise<Page>;
cookies(...urls: string[]): Promise<network.NetworkCookie[]>;
setCookies(cookies: network.SetNetworkCookieParam[]): Promise<void>;
clearCookies(): Promise<void>;
setPermissions(origin: string, permissions: string[]): Promise<void>;
clearPermissions(): Promise<void>;
setGeolocation(geolocation: types.Geolocation | null): Promise<void>;
close(): Promise<void>;
_existingPages(): Page[];
readonly _timeoutSettings: TimeoutSettings; readonly _timeoutSettings: TimeoutSettings;
private _closed = false; readonly _options: BrowserContextOptions;
}
constructor(delegate: BrowserContextDelegate, options: BrowserContextOptions) { export function assertBrowserContextIsNotOwned(context: BrowserContext) {
super(); const pages = context._existingPages();
this._delegate = delegate; for (const page of pages) {
this._timeoutSettings = new TimeoutSettings(); if (page._ownedContext)
this._options = { ...options }; throw new Error('Please use browser.newContext() for multi-page scripts that share the context.');
if (!this._options.viewport && this._options.viewport !== null)
this._options.viewport = { width: 800, height: 600 };
if (this._options.viewport)
this._options.viewport = { ...this._options.viewport };
if (this._options.geolocation)
this._options.geolocation = verifyGeolocation(this._options.geolocation);
}
async _initialize() {
const entries = Object.entries(this._options.permissions || {});
await Promise.all(entries.map(entry => this.setPermissions(entry[0], entry[1])));
if (this._options.geolocation)
await this.setGeolocation(this._options.geolocation);
}
_existingPages(): Page[] {
return this._delegate.existingPages();
}
setDefaultNavigationTimeout(timeout: number) {
this._timeoutSettings.setDefaultNavigationTimeout(timeout);
}
setDefaultTimeout(timeout: number) {
this._timeoutSettings.setDefaultTimeout(timeout);
}
async pages(): Promise<Page[]> {
return this._delegate.pages();
}
async newPage(): Promise<Page> {
const pages = this._delegate.existingPages();
for (const page of pages) {
if (page._ownedContext)
throw new Error('Please use browser.newContext() for multi-page scripts that share the context.');
}
return this._delegate.newPage();
}
async cookies(...urls: string[]): Promise<network.NetworkCookie[]> {
return network.filterCookies(await this._delegate.cookies(), urls);
}
async setCookies(cookies: network.SetNetworkCookieParam[]) {
await this._delegate.setCookies(network.rewriteCookies(cookies));
}
async clearCookies() {
await this._delegate.clearCookies();
}
async setPermissions(origin: string, permissions: string[]): Promise<void> {
await this._delegate.setPermissions(origin, permissions);
}
async clearPermissions() {
await this._delegate.clearPermissions();
}
async setGeolocation(geolocation: types.Geolocation | null): Promise<void> {
if (geolocation)
geolocation = verifyGeolocation(geolocation);
this._options.geolocation = geolocation || undefined;
await this._delegate.setGeolocation(geolocation);
}
async close() {
if (this._closed)
return;
await this._delegate.close();
this._closed = true;
this.emit(Events.BrowserContext.Close);
}
static validateOptions(options: BrowserContextOptions) {
if (options.geolocation)
verifyGeolocation(options.geolocation);
}
_browserClosed() {
this._closed = true;
for (const page of this._delegate.existingPages())
page._didClose();
this.emit(Events.BrowserContext.Close);
} }
} }
function verifyGeolocation(geolocation: types.Geolocation): types.Geolocation { export function validateBrowserContextOptions(options: BrowserContextOptions): BrowserContextOptions {
const result = { ...options };
if (!result.viewport && result.viewport !== null)
result.viewport = { width: 1280, height: 720 };
if (result.viewport)
result.viewport = { ...result.viewport };
if (result.geolocation)
result.geolocation = verifyGeolocation(result.geolocation);
return result;
}
export function verifyGeolocation(geolocation: types.Geolocation): types.Geolocation {
const result = { ...geolocation }; const result = { ...geolocation };
result.accuracy = result.accuracy || 0; result.accuracy = result.accuracy || 0;
const { longitude, latitude, accuracy } = result; const { longitude, latitude, accuracy } = result;

View file

@ -18,9 +18,9 @@
import { Events } from './events'; import { Events } from './events';
import { Events as CommonEvents } from '../events'; import { Events as CommonEvents } from '../events';
import { assert, helper } from '../helper'; import { assert, helper } from '../helper';
import { BrowserContext, BrowserContextOptions } from '../browserContext'; import { BrowserContext, BrowserContextOptions, validateBrowserContextOptions, assertBrowserContextIsNotOwned, verifyGeolocation } from '../browserContext';
import { CRConnection, ConnectionEvents, CRSession } from './crConnection'; import { CRConnection, ConnectionEvents, CRSession } from './crConnection';
import { Page, Worker } from '../page'; import { Page } from '../page';
import { CRTarget } from './crTarget'; import { CRTarget } from './crTarget';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import { CRPage } from './crPage'; import { CRPage } from './crPage';
@ -30,12 +30,13 @@ import * as types from '../types';
import * as platform from '../platform'; import * as platform from '../platform';
import { readProtocolStream } from './crProtocolHelper'; import { readProtocolStream } from './crProtocolHelper';
import { ConnectionTransport, SlowMoTransport } from '../transport'; import { ConnectionTransport, SlowMoTransport } from '../transport';
import { TimeoutSettings } from '../timeoutSettings';
export class CRBrowser extends platform.EventEmitter implements Browser { export class CRBrowser extends platform.EventEmitter implements Browser {
_connection: CRConnection; _connection: CRConnection;
_client: CRSession; _client: CRSession;
readonly _defaultContext: BrowserContext; readonly _defaultContext: CRBrowserContext;
private _contexts = new Map<string, BrowserContext>(); readonly _contexts = new Map<string, CRBrowserContext>();
_targets = new Map<string, CRTarget>(); _targets = new Map<string, CRTarget>();
private _tracingRecording = false; private _tracingRecording = false;
@ -54,9 +55,9 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
this._connection = connection; this._connection = connection;
this._client = connection.rootSession; this._client = connection.rootSession;
this._defaultContext = this._createBrowserContext(null, {}); this._defaultContext = new CRBrowserContext(this, null, validateBrowserContextOptions({}));
this._connection.on(ConnectionEvents.Disconnected, () => { this._connection.on(ConnectionEvents.Disconnected, () => {
for (const context of this.contexts()) for (const context of this._contexts.values())
context._browserClosed(); context._browserClosed();
this.emit(CommonEvents.Browser.Disconnected); this.emit(CommonEvents.Browser.Disconnected);
}); });
@ -65,99 +66,10 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
this._client.on('Target.targetInfoChanged', this._targetInfoChanged.bind(this)); this._client.on('Target.targetInfoChanged', this._targetInfoChanged.bind(this));
} }
_createBrowserContext(contextId: string | null, options: BrowserContextOptions): BrowserContext {
const context = new BrowserContext({
pages: async (): Promise<Page[]> => {
const targets = this._allTargets().filter(target => target.context() === context && target.type() === 'page');
const pages = await Promise.all(targets.map(target => target.page()));
return pages.filter(page => !!page) as Page[];
},
existingPages: (): Page[] => {
const pages: Page[] = [];
for (const target of this._allTargets()) {
if (target.context() === context && target._crPage)
pages.push(target._crPage.page());
}
return pages;
},
newPage: async (): Promise<Page> => {
const { targetId } = await this._client.send('Target.createTarget', { url: 'about:blank', browserContextId: contextId || undefined });
const target = this._targets.get(targetId)!;
assert(await target._initializedPromise, 'Failed to create target for page');
const page = await target.page();
return page!;
},
close: async (): Promise<void> => {
assert(contextId, 'Non-incognito profiles cannot be closed!');
await this._client.send('Target.disposeBrowserContext', { browserContextId: contextId });
this._contexts.delete(contextId);
},
cookies: async (): Promise<network.NetworkCookie[]> => {
const { cookies } = await this._client.send('Storage.getCookies', { browserContextId: contextId || undefined });
return cookies.map(c => {
const copy: any = { sameSite: 'None', ...c };
delete copy.size;
delete copy.priority;
return copy as network.NetworkCookie;
});
},
clearCookies: async (): Promise<void> => {
await this._client.send('Storage.clearCookies', { browserContextId: contextId || undefined });
},
setCookies: async (cookies: network.SetNetworkCookieParam[]): Promise<void> => {
await this._client.send('Storage.setCookies', { cookies, browserContextId: contextId || undefined });
},
setPermissions: async (origin: string, permissions: string[]): Promise<void> => {
const webPermissionToProtocol = new Map<string, Protocol.Browser.PermissionType>([
['geolocation', 'geolocation'],
['midi', 'midi'],
['notifications', 'notifications'],
['camera', 'videoCapture'],
['microphone', 'audioCapture'],
['background-sync', 'backgroundSync'],
['ambient-light-sensor', 'sensors'],
['accelerometer', 'sensors'],
['gyroscope', 'sensors'],
['magnetometer', 'sensors'],
['accessibility-events', 'accessibilityEvents'],
['clipboard-read', 'clipboardReadWrite'],
['clipboard-write', 'clipboardSanitizedWrite'],
['payment-handler', 'paymentHandler'],
// chrome-specific permissions we have.
['midi-sysex', 'midiSysex'],
]);
const filtered = permissions.map(permission => {
const protocolPermission = webPermissionToProtocol.get(permission);
if (!protocolPermission)
throw new Error('Unknown permission: ' + permission);
return protocolPermission;
});
await this._client.send('Browser.grantPermissions', { origin, browserContextId: contextId || undefined, permissions: filtered });
},
clearPermissions: async () => {
await this._client.send('Browser.resetPermissions', { browserContextId: contextId || undefined });
},
setGeolocation: async (geolocation: types.Geolocation | null): Promise<void> => {
for (const page of await context.pages())
await (page._delegate as CRPage)._client.send('Emulation.setGeolocationOverride', geolocation || {});
}
}, options);
return context;
}
async newContext(options: BrowserContextOptions = {}): Promise<BrowserContext> { async newContext(options: BrowserContextOptions = {}): Promise<BrowserContext> {
BrowserContext.validateOptions(options); options = validateBrowserContextOptions(options);
const { browserContextId } = await this._client.send('Target.createBrowserContext'); const { browserContextId } = await this._client.send('Target.createBrowserContext');
const context = this._createBrowserContext(browserContextId, options); const context = new CRBrowserContext(this, browserContextId, options);
await context._initialize(); await context._initialize();
this._contexts.set(browserContextId, context); this._contexts.set(browserContextId, context);
return context; return context;
@ -181,7 +93,7 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
this._targets.set(event.targetInfo.targetId, target); this._targets.set(event.targetInfo.targetId, target);
if (target._isInitialized || await target._initializedPromise) if (target._isInitialized || await target._initializedPromise)
this.emit(Events.CRBrowser.TargetCreated, target); context.emit(Events.CRBrowserContext.TargetCreated, target);
} }
async _targetDestroyed(event: { targetId: string; }) { async _targetDestroyed(event: { targetId: string; }) {
@ -190,7 +102,7 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
this._targets.delete(event.targetId); this._targets.delete(event.targetId);
target._didClose(); target._didClose();
if (await target._initializedPromise) if (await target._initializedPromise)
this.emit(Events.CRBrowser.TargetDestroyed, target); target.context().emit(Events.CRBrowserContext.TargetDestroyed, target);
} }
_targetInfoChanged(event: Protocol.Target.targetInfoChangedPayload) { _targetInfoChanged(event: Protocol.Target.targetInfoChangedPayload) {
@ -200,7 +112,7 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
const wasInitialized = target._isInitialized; const wasInitialized = target._isInitialized;
target._targetInfoChanged(event.targetInfo); target._targetInfoChanged(event.targetInfo);
if (wasInitialized && previousURL !== target.url()) if (wasInitialized && previousURL !== target.url())
this.emit(Events.CRBrowser.TargetChanged, target); target.context().emit(Events.CRBrowserContext.TargetChanged, target);
} }
async _closePage(page: Page) { async _closePage(page: Page) {
@ -211,32 +123,6 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
return Array.from(this._targets.values()).filter(target => target._isInitialized); return Array.from(this._targets.values()).filter(target => target._isInitialized);
} }
async waitForTarget(predicate: (arg0: CRTarget) => boolean, options: { timeout?: number; } | undefined = {}): Promise<CRTarget> {
const {
timeout = 30000
} = options;
const existingTarget = this._allTargets().find(predicate);
if (existingTarget)
return existingTarget;
let resolve: (target: CRTarget) => void;
const targetPromise = new Promise<CRTarget>(x => resolve = x);
this.on(Events.CRBrowser.TargetCreated, check);
this.on(Events.CRBrowser.TargetChanged, check);
try {
if (!timeout)
return await targetPromise;
return await helper.waitWithTimeout(targetPromise, 'target', timeout);
} finally {
this.removeListener(Events.CRBrowser.TargetCreated, check);
this.removeListener(Events.CRBrowser.TargetChanged, check);
}
function check(target: CRTarget) {
if (predicate(target))
resolve(target);
}
}
async close() { async close() {
const disconnected = new Promise(f => this._connection.once(ConnectionEvents.Disconnected, f)); const disconnected = new Promise(f => this._connection.once(ConnectionEvents.Disconnected, f));
await Promise.all(this.contexts().map(context => context.close())); await Promise.all(this.contexts().map(context => context.close()));
@ -248,10 +134,6 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
return [...this._targets.values()].find(t => t.type() === 'browser')!; return [...this._targets.values()].find(t => t.type() === 'browser')!;
} }
serviceWorker(target: CRTarget): Promise<Worker | null> {
return target._worker();
}
async startTracing(page: Page | undefined, options: { path?: string; screenshots?: boolean; categories?: string[]; } = {}) { async startTracing(page: Page | undefined, options: { path?: string; screenshots?: boolean; categories?: string[]; } = {}) {
assert(!this._tracingRecording, 'Cannot start recording trace while already recording trace.'); assert(!this._tracingRecording, 'Cannot start recording trace while already recording trace.');
this._tracingClient = page ? (page._delegate as CRPage)._client : this._client; this._tracingClient = page ? (page._delegate as CRPage)._client : this._client;
@ -291,15 +173,6 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
return contentPromise; return contentPromise;
} }
targets(context?: BrowserContext): CRTarget[] {
const targets = this._allTargets();
return context ? targets.filter(t => t.context() === context) : targets;
}
pageTarget(page: Page): CRTarget {
return CRTarget.fromPage(page);
}
isConnected(): boolean { isConnected(): boolean {
return !this._connection._closed; return !this._connection._closed;
} }
@ -308,3 +181,165 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
this._connection._debugProtocol = debugFunction; this._connection._debugProtocol = debugFunction;
} }
} }
export class CRBrowserContext extends platform.EventEmitter implements BrowserContext {
readonly _browser: CRBrowser;
readonly _browserContextId: string | null;
readonly _options: BrowserContextOptions;
readonly _timeoutSettings: TimeoutSettings;
private _closed = false;
constructor(browser: CRBrowser, browserContextId: string | null, options: BrowserContextOptions) {
super();
this._browser = browser;
this._browserContextId = browserContextId;
this._timeoutSettings = new TimeoutSettings();
this._options = options;
}
async _initialize() {
const entries = Object.entries(this._options.permissions || {});
await Promise.all(entries.map(entry => this.setPermissions(entry[0], entry[1])));
if (this._options.geolocation)
await this.setGeolocation(this._options.geolocation);
}
_existingPages(): Page[] {
const pages: Page[] = [];
for (const target of this._browser._allTargets()) {
if (target.context() === this && target._crPage)
pages.push(target._crPage.page());
}
return pages;
}
setDefaultNavigationTimeout(timeout: number) {
this._timeoutSettings.setDefaultNavigationTimeout(timeout);
}
setDefaultTimeout(timeout: number) {
this._timeoutSettings.setDefaultTimeout(timeout);
}
async pages(): Promise<Page[]> {
const targets = this._browser._allTargets().filter(target => target.context() === this && target.type() === 'page');
const pages = await Promise.all(targets.map(target => target.page()));
return pages.filter(page => !!page) as Page[];
}
async newPage(): Promise<Page> {
assertBrowserContextIsNotOwned(this);
const { targetId } = await this._browser._client.send('Target.createTarget', { url: 'about:blank', browserContextId: this._browserContextId || undefined });
const target = this._browser._targets.get(targetId)!;
assert(await target._initializedPromise, 'Failed to create target for page');
const page = await target.page();
return page!;
}
async cookies(...urls: string[]): Promise<network.NetworkCookie[]> {
const { cookies } = await this._browser._client.send('Storage.getCookies', { browserContextId: this._browserContextId || undefined });
return network.filterCookies(cookies.map(c => {
const copy: any = { sameSite: 'None', ...c };
delete copy.size;
delete copy.priority;
return copy as network.NetworkCookie;
}), urls);
}
async setCookies(cookies: network.SetNetworkCookieParam[]) {
await this._browser._client.send('Storage.setCookies', { cookies: network.rewriteCookies(cookies), browserContextId: this._browserContextId || undefined });
}
async clearCookies() {
await this._browser._client.send('Storage.clearCookies', { browserContextId: this._browserContextId || undefined });
}
async setPermissions(origin: string, permissions: string[]): Promise<void> {
const webPermissionToProtocol = new Map<string, Protocol.Browser.PermissionType>([
['geolocation', 'geolocation'],
['midi', 'midi'],
['notifications', 'notifications'],
['camera', 'videoCapture'],
['microphone', 'audioCapture'],
['background-sync', 'backgroundSync'],
['ambient-light-sensor', 'sensors'],
['accelerometer', 'sensors'],
['gyroscope', 'sensors'],
['magnetometer', 'sensors'],
['accessibility-events', 'accessibilityEvents'],
['clipboard-read', 'clipboardReadWrite'],
['clipboard-write', 'clipboardSanitizedWrite'],
['payment-handler', 'paymentHandler'],
// chrome-specific permissions we have.
['midi-sysex', 'midiSysex'],
]);
const filtered = permissions.map(permission => {
const protocolPermission = webPermissionToProtocol.get(permission);
if (!protocolPermission)
throw new Error('Unknown permission: ' + permission);
return protocolPermission;
});
await this._browser._client.send('Browser.grantPermissions', { origin, browserContextId: this._browserContextId || undefined, permissions: filtered });
}
async clearPermissions() {
await this._browser._client.send('Browser.resetPermissions', { browserContextId: this._browserContextId || undefined });
}
async setGeolocation(geolocation: types.Geolocation | null): Promise<void> {
if (geolocation)
geolocation = verifyGeolocation(geolocation);
this._options.geolocation = geolocation || undefined;
for (const page of this._existingPages())
await (page._delegate as CRPage)._client.send('Emulation.setGeolocationOverride', geolocation || {});
}
async close() {
if (this._closed)
return;
assert(this._browserContextId, 'Non-incognito profiles cannot be closed!');
await this._browser._client.send('Target.disposeBrowserContext', { browserContextId: this._browserContextId });
this._browser._contexts.delete(this._browserContextId);
this._closed = true;
this.emit(CommonEvents.BrowserContext.Close);
}
pageTarget(page: Page): CRTarget {
return CRTarget.fromPage(page);
}
targets(): CRTarget[] {
return this._browser._allTargets().filter(t => t.context() === this);
}
async waitForTarget(predicate: (arg0: CRTarget) => boolean, options: { timeout?: number; } = {}): Promise<CRTarget> {
const { timeout = 30000 } = options;
const existingTarget = this._browser._allTargets().find(predicate);
if (existingTarget)
return existingTarget;
let resolve: (target: CRTarget) => void;
const targetPromise = new Promise<CRTarget>(x => resolve = x);
this.on(Events.CRBrowserContext.TargetCreated, check);
this.on(Events.CRBrowserContext.TargetChanged, check);
try {
if (!timeout)
return await targetPromise;
return await helper.waitWithTimeout(targetPromise, 'target', timeout);
} finally {
this.removeListener(Events.CRBrowserContext.TargetCreated, check);
this.removeListener(Events.CRBrowserContext.TargetChanged, check);
}
function check(target: CRTarget) {
if (predicate(target))
resolve(target);
}
}
_browserClosed() {
this._closed = true;
for (const page of this._existingPages())
page._didClose();
this.emit(CommonEvents.BrowserContext.Close);
}
}

View file

@ -60,7 +60,7 @@ export class CRConnection extends platform.EventEmitter {
if (sessionId) if (sessionId)
message.sessionId = sessionId; message.sessionId = sessionId;
const data = JSON.stringify(message); const data = JSON.stringify(message);
this._debugProtocol('SEND ► ' + data); this._debugProtocol('SEND ► ' + (rewriteInjectedScriptEvaluationLog(message) || data));
this._transport.send(data); this._transport.send(data);
return id; return id;
} }
@ -192,3 +192,10 @@ function rewriteError(error: Error, message: string): Error {
error.message = message; error.message = message;
return error; return error;
} }
function rewriteInjectedScriptEvaluationLog(message: any): string | undefined {
// Injected script is very long and clutters protocol logs.
// To increase development velocity, we skip replace it with short description in the log.
if (message.method === 'Runtime.evaluate' && message.params && message.params.expression && message.params.expression.includes('src/injected/injected.ts'))
return `{"id":${message.id} [evaluate injected script]}`;
}

View file

@ -15,8 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { CRBrowser } from './crBrowser'; import { CRBrowser, CRBrowserContext } from './crBrowser';
import { BrowserContext } from '../browserContext';
import { CRSession, CRSessionEvents } from './crConnection'; import { CRSession, CRSessionEvents } from './crConnection';
import { Events } from '../events'; import { Events } from '../events';
import { Page, Worker } from '../page'; import { Page, Worker } from '../page';
@ -30,7 +29,7 @@ const targetSymbol = Symbol('target');
export class CRTarget { export class CRTarget {
private _targetInfo: Protocol.Target.TargetInfo; private _targetInfo: Protocol.Target.TargetInfo;
private readonly _browser: CRBrowser; private readonly _browser: CRBrowser;
private readonly _browserContext: BrowserContext; private readonly _browserContext: CRBrowserContext;
readonly _targetId: string; readonly _targetId: string;
private _sessionFactory: () => Promise<CRSession>; private _sessionFactory: () => Promise<CRSession>;
private _pagePromise: Promise<Page> | null = null; private _pagePromise: Promise<Page> | null = null;
@ -47,7 +46,7 @@ export class CRTarget {
constructor( constructor(
browser: CRBrowser, browser: CRBrowser,
targetInfo: Protocol.Target.TargetInfo, targetInfo: Protocol.Target.TargetInfo,
browserContext: BrowserContext, browserContext: CRBrowserContext,
sessionFactory: () => Promise<CRSession>) { sessionFactory: () => Promise<CRSession>) {
this._targetInfo = targetInfo; this._targetInfo = targetInfo;
this._browser = browser; this._browser = browser;
@ -91,8 +90,8 @@ export class CRTarget {
return this._pagePromise; return this._pagePromise;
} }
async _worker(): Promise<Worker | null> { async serviceWorker(): Promise<Worker | null> {
if (this._targetInfo.type !== 'service_worker' && this._targetInfo.type !== 'shared_worker') if (this._targetInfo.type !== 'service_worker')
return null; return null;
if (!this._workerPromise) { if (!this._workerPromise) {
// TODO(einbinder): Make workers send their console logs. // TODO(einbinder): Make workers send their console logs.
@ -120,7 +119,7 @@ export class CRTarget {
return 'other'; return 'other';
} }
context(): BrowserContext { context(): CRBrowserContext {
return this._browserContext; return this._browserContext;
} }

View file

@ -16,7 +16,7 @@
*/ */
export const Events = { export const Events = {
CRBrowser: { CRBrowserContext: {
TargetCreated: 'targetcreated', TargetCreated: 'targetcreated',
TargetDestroyed: 'targetdestroyed', TargetDestroyed: 'targetdestroyed',
TargetChanged: 'targetchanged', TargetChanged: 'targetchanged',

View file

@ -230,10 +230,17 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return point; return point;
} }
async _performPointerAction(action: (point: types.Point) => Promise<void>, options?: input.PointerActionOptions): Promise<void> { async _performPointerAction(action: (point: types.Point) => Promise<void>, options?: input.PointerActionOptions & types.WaitForOptions): Promise<void> {
const { waitFor = true } = (options || {});
if (!helper.isBoolean(waitFor))
throw new Error('waitFor option should be a boolean, got "' + (typeof waitFor) + '"');
if (waitFor)
await this._waitForStablePosition(options);
const relativePoint = options ? options.relativePoint : undefined; const relativePoint = options ? options.relativePoint : undefined;
await this._scrollRectIntoViewIfNeeded(relativePoint ? { x: relativePoint.x, y: relativePoint.y, width: 0, height: 0 } : undefined); await this._scrollRectIntoViewIfNeeded(relativePoint ? { x: relativePoint.x, y: relativePoint.y, width: 0, height: 0 } : undefined);
const point = relativePoint ? await this._relativePoint(relativePoint) : await this._clickablePoint(); const point = relativePoint ? await this._relativePoint(relativePoint) : await this._clickablePoint();
if (waitFor)
await this._waitForHitTargetAt(point, options);
let restoreModifiers: input.Modifier[] | undefined; let restoreModifiers: input.Modifier[] | undefined;
if (options && options.modifiers) if (options && options.modifiers)
restoreModifiers = await this._page.keyboard._ensureModifiers(options.modifiers); restoreModifiers = await this._page.keyboard._ensureModifiers(options.modifiers);
@ -242,19 +249,19 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
await this._page.keyboard._ensureModifiers(restoreModifiers); await this._page.keyboard._ensureModifiers(restoreModifiers);
} }
hover(options?: input.PointerActionOptions): Promise<void> { hover(options?: input.PointerActionOptions & types.WaitForOptions): Promise<void> {
return this._performPointerAction(point => this._page.mouse.move(point.x, point.y), options); return this._performPointerAction(point => this._page.mouse.move(point.x, point.y), options);
} }
click(options?: input.ClickOptions): Promise<void> { click(options?: input.ClickOptions & types.WaitForOptions): Promise<void> {
return this._performPointerAction(point => this._page.mouse.click(point.x, point.y, options), options); return this._performPointerAction(point => this._page.mouse.click(point.x, point.y, options), options);
} }
dblclick(options?: input.MultiClickOptions): Promise<void> { dblclick(options?: input.MultiClickOptions & types.WaitForOptions): Promise<void> {
return this._performPointerAction(point => this._page.mouse.dblclick(point.x, point.y, options), options); return this._performPointerAction(point => this._page.mouse.dblclick(point.x, point.y, options), options);
} }
tripleclick(options?: input.MultiClickOptions): Promise<void> { tripleclick(options?: input.MultiClickOptions & types.WaitForOptions): Promise<void> {
return this._performPointerAction(point => this._page.mouse.tripleclick(point.x, point.y, options), options); return this._performPointerAction(point => this._page.mouse.tripleclick(point.x, point.y, options), options);
} }
@ -402,19 +409,20 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
await this._page.keyboard.type(text, options); await this._page.keyboard.type(text, options);
} }
async press(key: string, options: { delay?: number; text?: string; } | undefined) { async press(key: string, options?: { delay?: number, text?: string }) {
await this.focus(); await this.focus();
await this._page.keyboard.press(key, options); await this._page.keyboard.press(key, options);
} }
async check() {
await this._setChecked(true); async check(options?: types.WaitForOptions) {
await this._setChecked(true, options);
} }
async uncheck() { async uncheck(options?: types.WaitForOptions) {
await this._setChecked(false); await this._setChecked(false, options);
} }
private async _setChecked(state: boolean) { private async _setChecked(state: boolean, options?: types.WaitForOptions) {
const isCheckboxChecked = async (): Promise<boolean> => { const isCheckboxChecked = async (): Promise<boolean> => {
return this._evaluateInUtility((node: Node) => { return this._evaluateInUtility((node: Node) => {
if (node.nodeType !== Node.ELEMENT_NODE) if (node.nodeType !== Node.ELEMENT_NODE)
@ -442,7 +450,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
if (await isCheckboxChecked() === state) if (await isCheckboxChecked() === state)
return; return;
await this.click(); await this.click(options);
if (await isCheckboxChecked() !== state) if (await isCheckboxChecked() !== state)
throw new Error('Unable to click checkbox'); throw new Error('Unable to click checkbox');
} }
@ -479,23 +487,56 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return result; return result;
} }
visibleRatio(): Promise<number> { async _waitForStablePosition(options: types.TimeoutOptions = {}): Promise<void> {
return this._evaluateInUtility(async (node: Node) => { const context = await this._context.frame._utilityContext();
if (node.nodeType !== Node.ELEMENT_NODE) const stablePromise = context.evaluate((injected: Injected, node: Node, timeout: number) => {
throw new Error('Node is not of type HTMLElement'); if (!node.isConnected)
const element = node as Element; throw new Error('Element is not attached to the DOM');
const visibleRatio = await new Promise<number>(resolve => { const element = node.nodeType === Node.ELEMENT_NODE ? (node as Element) : node.parentElement;
const observer = new IntersectionObserver(entries => { if (!element)
resolve(entries[0].intersectionRatio); throw new Error('Element is not attached to the DOM');
observer.disconnect();
}); let lastRect: types.Rect | undefined;
observer.observe(element); let counter = 0;
// Firefox doesn't call IntersectionObserver callback unless return injected.poll('raf', undefined, timeout, () => {
// there are rafs. // First raf happens in the same animation frame as evaluation, so it does not produce
requestAnimationFrame(() => {}); // any client rect difference compared to synchronous call. We skip the synchronous call
// and only force layout during actual rafs as a small optimisation.
if (++counter === 1)
return false;
const clientRect = element.getBoundingClientRect();
const rect = { x: clientRect.top, y: clientRect.left, width: clientRect.width, height: clientRect.height };
const isStable = lastRect && rect.x === lastRect.x && rect.y === lastRect.y && rect.width === lastRect.width && rect.height === lastRect.height;
lastRect = rect;
return isStable;
}); });
return visibleRatio; }, await context._injected(), this, options.timeout || 0);
}); await helper.waitWithTimeout(stablePromise, 'element to stop moving', options.timeout || 0);
}
async _waitForHitTargetAt(point: types.Point, options: types.TimeoutOptions = {}): Promise<void> {
const frame = await this.ownerFrame();
if (frame && frame.parentFrame()) {
const element = await frame.frameElement();
const box = await element.boundingBox();
if (!box)
throw new Error('Element is not attached to the DOM');
// Translate from viewport coordinates to frame coordinates.
point = { x: point.x - box.x, y: point.y - box.y };
}
const context = await this._context.frame._utilityContext();
const hitTargetPromise = context.evaluate((injected: Injected, node: Node, timeout: number, point: types.Point) => {
const element = node.nodeType === Node.ELEMENT_NODE ? (node as Element) : node.parentElement;
if (!element)
throw new Error('Element is not attached to the DOM');
return injected.poll('raf', undefined, timeout, () => {
let hitElement = injected.utils.deepElementFromPoint(document, point.x, point.y);
while (hitElement && hitElement !== element)
hitElement = injected.utils.parentElementOrShadowHost(hitElement);
return hitElement === element;
});
}, await context._injected(), this, options.timeout || 0, point);
await helper.waitWithTimeout(hitTargetPromise, 'element to receive mouse events', options.timeout || 0);
} }
} }
@ -514,51 +555,44 @@ function normalizeSelector(selector: string): string {
export type Task = (context: FrameExecutionContext) => Promise<js.JSHandle>; export type Task = (context: FrameExecutionContext) => Promise<js.JSHandle>;
export function waitForFunctionTask(selector: string | undefined, pageFunction: Function | string, options: types.WaitForFunctionOptions, ...args: any[]) { function assertPolling(polling: types.Polling) {
const { polling = 'raf' } = options;
if (helper.isString(polling)) if (helper.isString(polling))
assert(polling === 'raf' || polling === 'mutation', 'Unknown polling option: ' + polling); assert(polling === 'raf' || polling === 'mutation', 'Unknown polling option: ' + polling);
else if (helper.isNumber(polling)) else if (helper.isNumber(polling))
assert(polling > 0, 'Cannot poll with non-positive interval: ' + polling); assert(polling > 0, 'Cannot poll with non-positive interval: ' + polling);
else else
throw new Error('Unknown polling options: ' + polling); throw new Error('Unknown polling options: ' + polling);
}
export function waitForFunctionTask(selector: string | undefined, pageFunction: Function | string, options: types.WaitForFunctionOptions, ...args: any[]): Task {
const { polling = 'raf' } = options;
assertPolling(polling);
const predicateBody = helper.isString(pageFunction) ? 'return (' + pageFunction + ')' : 'return (' + pageFunction + ')(...args)'; const predicateBody = helper.isString(pageFunction) ? 'return (' + pageFunction + ')' : 'return (' + pageFunction + ')(...args)';
if (selector !== undefined) if (selector !== undefined)
selector = normalizeSelector(selector); selector = normalizeSelector(selector);
return async (context: FrameExecutionContext) => context.evaluateHandle((injected: Injected, selector: string | undefined, predicateBody: string, polling: types.Polling, timeout: number, ...args) => { return async (context: FrameExecutionContext) => context.evaluateHandle((injected: Injected, selector: string | undefined, predicateBody: string, polling: types.Polling, timeout: number, ...args) => {
const innerPredicate = new Function('...args', predicateBody); const innerPredicate = new Function('...args', predicateBody);
if (polling === 'raf') return injected.poll(polling, selector, timeout, (element: Element | undefined): any => {
return injected.pollRaf(selector, predicate, timeout);
if (polling === 'mutation')
return injected.pollMutation(selector, predicate, timeout);
return injected.pollInterval(selector, polling, predicate, timeout);
function predicate(element: Element | undefined): any {
if (selector === undefined) if (selector === undefined)
return innerPredicate(...args); return innerPredicate(...args);
return innerPredicate(element, ...args); return innerPredicate(element, ...args);
} });
}, await context._injected(), selector, predicateBody, polling, options.timeout || 0, ...args); }, await context._injected(), selector, predicateBody, polling, options.timeout || 0, ...args);
} }
export function waitForSelectorTask(selector: string, visibility: types.Visibility, timeout: number): Task { export function waitForSelectorTask(selector: string, visibility: types.Visibility, timeout: number): Task {
return async (context: FrameExecutionContext) => { selector = normalizeSelector(selector);
selector = normalizeSelector(selector); return async (context: FrameExecutionContext) => context.evaluateHandle((injected: Injected, selector: string, visibility: types.Visibility, timeout: number) => {
return context.evaluateHandle((injected: Injected, selector: string, visibility: types.Visibility, timeout: number) => { const polling = visibility === 'any' ? 'mutation' : 'raf';
if (visibility !== 'any') return injected.poll(polling, selector, timeout, (element: Element | undefined): Element | boolean => {
return injected.pollRaf(selector, predicate, timeout); if (!element)
return injected.pollMutation(selector, predicate, timeout); return visibility === 'hidden';
if (visibility === 'any')
function predicate(element: Element | undefined): Element | boolean { return element;
if (!element) return injected.isVisible(element) === (visibility === 'visible') ? element : false;
return visibility === 'hidden'; });
if (visibility === 'any') }, await context._injected(), selector, visibility, timeout);
return element;
return injected.isVisible(element) === (visibility === 'visible') ? element : false;
}
}, await context._injected(), selector, visibility, timeout);
};
} }
export const setFileInputFunction = async (element: HTMLInputElement, payloads: types.FilePayload[]) => { export const setFileInputFunction = async (element: HTMLInputElement, payloads: types.FilePayload[]) => {
@ -571,4 +605,5 @@ export const setFileInputFunction = async (element: HTMLInputElement, payloads:
dt.items.add(file); dt.items.add(file);
element.files = dt.files; element.files = dt.files;
element.dispatchEvent(new Event('input', { 'bubbles': true })); element.dispatchEvent(new Event('input', { 'bubbles': true }));
element.dispatchEvent(new Event('change', { 'bubbles': true }));
}; };

View file

@ -16,7 +16,7 @@
*/ */
import { Browser, createPageInNewContext } from '../browser'; import { Browser, createPageInNewContext } from '../browser';
import { BrowserContext, BrowserContextOptions } from '../browserContext'; import { BrowserContext, BrowserContextOptions, validateBrowserContextOptions, assertBrowserContextIsNotOwned } from '../browserContext';
import { Events } from '../events'; import { Events } from '../events';
import { assert, helper, RegisteredListener, debugError } from '../helper'; import { assert, helper, RegisteredListener, debugError } from '../helper';
import * as network from '../network'; import * as network from '../network';
@ -27,12 +27,13 @@ import { FFPage } from './ffPage';
import * as platform from '../platform'; import * as platform from '../platform';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import { ConnectionTransport, SlowMoTransport } from '../transport'; import { ConnectionTransport, SlowMoTransport } from '../transport';
import { TimeoutSettings } from '../timeoutSettings';
export class FFBrowser extends platform.EventEmitter implements Browser { export class FFBrowser extends platform.EventEmitter implements Browser {
_connection: FFConnection; _connection: FFConnection;
_targets: Map<string, Target>; _targets: Map<string, Target>;
readonly _defaultContext: BrowserContext; readonly _defaultContext: BrowserContext;
private _contexts: Map<string, BrowserContext>; readonly _contexts: Map<string, FFBrowserContext>;
private _eventListeners: RegisteredListener[]; private _eventListeners: RegisteredListener[];
static async connect(transport: ConnectionTransport, slowMo?: number): Promise<FFBrowser> { static async connect(transport: ConnectionTransport, slowMo?: number): Promise<FFBrowser> {
@ -47,10 +48,10 @@ export class FFBrowser extends platform.EventEmitter implements Browser {
this._connection = connection; this._connection = connection;
this._targets = new Map(); this._targets = new Map();
this._defaultContext = this._createBrowserContext(null, {}); this._defaultContext = new FFBrowserContext(this, null, validateBrowserContextOptions({}));
this._contexts = new Map(); this._contexts = new Map();
this._connection.on(ConnectionEvents.Disconnected, () => { this._connection.on(ConnectionEvents.Disconnected, () => {
for (const context of this.contexts()) for (const context of this._contexts.values())
context._browserClosed(); context._browserClosed();
this.emit(Events.Browser.Disconnected); this.emit(Events.Browser.Disconnected);
}); });
@ -67,13 +68,24 @@ export class FFBrowser extends platform.EventEmitter implements Browser {
} }
async newContext(options: BrowserContextOptions = {}): Promise<BrowserContext> { async newContext(options: BrowserContextOptions = {}): Promise<BrowserContext> {
const viewport = options.viewport ? { options = validateBrowserContextOptions(options);
viewportSize: { width: options.viewport.width, height: options.viewport.height }, let viewport;
isMobile: !!options.viewport.isMobile, if (options.viewport) {
deviceScaleFactor: options.viewport.deviceScaleFactor || 1, viewport = {
hasTouch: !!options.viewport.isMobile, viewportSize: { width: options.viewport.width, height: options.viewport.height },
} : undefined; isMobile: !!options.viewport.isMobile,
const {browserContextId} = await this._connection.send('Target.createBrowserContext', { deviceScaleFactor: options.viewport.deviceScaleFactor || 1,
hasTouch: !!options.viewport.isMobile,
};
} else if (options.viewport !== null) {
viewport = {
viewportSize: { width: 1280, height: 720 },
isMobile: false,
deviceScaleFactor: 1,
hasTouch: false,
};
}
const { browserContextId } = await this._connection.send('Target.createBrowserContext', {
userAgent: options.userAgent, userAgent: options.userAgent,
bypassCSP: options.bypassCSP, bypassCSP: options.bypassCSP,
javaScriptDisabled: options.javaScriptEnabled === false ? true : undefined, javaScriptDisabled: options.javaScriptEnabled === false ? true : undefined,
@ -82,7 +94,7 @@ export class FFBrowser extends platform.EventEmitter implements Browser {
// TODO: move ignoreHTTPSErrors to browser context level. // TODO: move ignoreHTTPSErrors to browser context level.
if (options.ignoreHTTPSErrors) if (options.ignoreHTTPSErrors)
await this._connection.send('Browser.setIgnoreHTTPSErrors', { enabled: true }); await this._connection.send('Browser.setIgnoreHTTPSErrors', { enabled: true });
const context = this._createBrowserContext(browserContextId, options); const context = new FFBrowserContext(this, browserContextId, options);
await context._initialize(); await context._initialize();
this._contexts.set(browserContextId, context); this._contexts.set(browserContextId, context);
return context; return context;
@ -166,82 +178,6 @@ export class FFBrowser extends platform.EventEmitter implements Browser {
await disconnected; await disconnected;
} }
_createBrowserContext(browserContextId: string | null, options: BrowserContextOptions): BrowserContext {
BrowserContext.validateOptions(options);
const context = new BrowserContext({
pages: async (): Promise<Page[]> => {
const targets = this._allTargets().filter(target => target.context() === context && target.type() === 'page');
const pages = await Promise.all(targets.map(target => target.page()));
return pages.filter(page => !!page);
},
existingPages: (): Page[] => {
const pages: Page[] = [];
for (const target of this._allTargets()) {
if (target.context() === context && target._ffPage)
pages.push(target._ffPage._page);
}
return pages;
},
newPage: async (): Promise<Page> => {
const {targetId} = await this._connection.send('Target.newPage', {
browserContextId: browserContextId || undefined
});
const target = this._targets.get(targetId)!;
return target.page();
},
close: async (): Promise<void> => {
assert(browserContextId, 'Non-incognito profiles cannot be closed!');
await this._connection.send('Target.removeBrowserContext', { browserContextId });
this._contexts.delete(browserContextId);
},
cookies: async (): Promise<network.NetworkCookie[]> => {
const { cookies } = await this._connection.send('Browser.getCookies', { browserContextId: browserContextId || undefined });
return cookies.map(c => {
const copy: any = { ... c };
delete copy.size;
return copy as network.NetworkCookie;
});
},
clearCookies: async (): Promise<void> => {
await this._connection.send('Browser.clearCookies', { browserContextId: browserContextId || undefined });
},
setCookies: async (cookies: network.SetNetworkCookieParam[]): Promise<void> => {
await this._connection.send('Browser.setCookies', { browserContextId: browserContextId || undefined, cookies });
},
setPermissions: async (origin: string, permissions: string[]): Promise<void> => {
const webPermissionToProtocol = new Map<string, 'geo' | 'microphone' | 'camera' | 'desktop-notifications'>([
['geolocation', 'geo'],
['microphone', 'microphone'],
['camera', 'camera'],
['notifications', 'desktop-notifications'],
]);
const filtered = permissions.map(permission => {
const protocolPermission = webPermissionToProtocol.get(permission);
if (!protocolPermission)
throw new Error('Unknown permission: ' + permission);
return protocolPermission;
});
await this._connection.send('Browser.grantPermissions', {origin, browserContextId: browserContextId || undefined, permissions: filtered});
},
clearPermissions: async () => {
await this._connection.send('Browser.resetPermissions', { browserContextId: browserContextId || undefined });
},
setGeolocation: async (geolocation: types.Geolocation | null): Promise<void> => {
throw new Error('Geolocation emulation is not supported in Firefox');
}
}, options);
return context;
}
_setDebugFunction(debugFunction: (message: string) => void) { _setDebugFunction(debugFunction: (message: string) => void) {
this._connection._debugProtocol = debugFunction; this._connection._debugProtocol = debugFunction;
} }
@ -316,3 +252,116 @@ class Target {
return this._browser; return this._browser;
} }
} }
export class FFBrowserContext extends platform.EventEmitter implements BrowserContext {
readonly _browser: FFBrowser;
readonly _browserContextId: string | null;
readonly _options: BrowserContextOptions;
readonly _timeoutSettings: TimeoutSettings;
private _closed = false;
constructor(browser: FFBrowser, browserContextId: string | null, options: BrowserContextOptions) {
super();
this._browser = browser;
this._browserContextId = browserContextId;
this._timeoutSettings = new TimeoutSettings();
this._options = options;
}
async _initialize() {
const entries = Object.entries(this._options.permissions || {});
await Promise.all(entries.map(entry => this.setPermissions(entry[0], entry[1])));
if (this._options.geolocation)
await this.setGeolocation(this._options.geolocation);
}
_existingPages(): Page[] {
const pages: Page[] = [];
for (const target of this._browser._allTargets()) {
if (target.context() === this && target._ffPage)
pages.push(target._ffPage._page);
}
return pages;
}
setDefaultNavigationTimeout(timeout: number) {
this._timeoutSettings.setDefaultNavigationTimeout(timeout);
}
setDefaultTimeout(timeout: number) {
this._timeoutSettings.setDefaultTimeout(timeout);
}
async pages(): Promise<Page[]> {
const targets = this._browser._allTargets().filter(target => target.context() === this && target.type() === 'page');
const pages = await Promise.all(targets.map(target => target.page()));
return pages.filter(page => !!page);
}
async newPage(): Promise<Page> {
assertBrowserContextIsNotOwned(this);
const {targetId} = await this._browser._connection.send('Target.newPage', {
browserContextId: this._browserContextId || undefined
});
const target = this._browser._targets.get(targetId)!;
return target.page();
}
async cookies(...urls: string[]): Promise<network.NetworkCookie[]> {
const { cookies } = await this._browser._connection.send('Browser.getCookies', { browserContextId: this._browserContextId || undefined });
return network.filterCookies(cookies.map(c => {
const copy: any = { ... c };
delete copy.size;
return copy as network.NetworkCookie;
}), urls);
}
async setCookies(cookies: network.SetNetworkCookieParam[]) {
await this._browser._connection.send('Browser.setCookies', { browserContextId: this._browserContextId || undefined, cookies: network.rewriteCookies(cookies) });
}
async clearCookies() {
await this._browser._connection.send('Browser.clearCookies', { browserContextId: this._browserContextId || undefined });
}
async setPermissions(origin: string, permissions: string[]): Promise<void> {
const webPermissionToProtocol = new Map<string, 'geo' | 'microphone' | 'camera' | 'desktop-notifications'>([
['geolocation', 'geo'],
['microphone', 'microphone'],
['camera', 'camera'],
['notifications', 'desktop-notifications'],
]);
const filtered = permissions.map(permission => {
const protocolPermission = webPermissionToProtocol.get(permission);
if (!protocolPermission)
throw new Error('Unknown permission: ' + permission);
return protocolPermission;
});
await this._browser._connection.send('Browser.grantPermissions', {origin, browserContextId: this._browserContextId || undefined, permissions: filtered});
}
async clearPermissions() {
await this._browser._connection.send('Browser.resetPermissions', { browserContextId: this._browserContextId || undefined });
}
async setGeolocation(geolocation: types.Geolocation | null): Promise<void> {
throw new Error('Geolocation emulation is not supported in Firefox');
}
async close() {
if (this._closed)
return;
assert(this._browserContextId, 'Non-incognito profiles cannot be closed!');
await this._browser._connection.send('Target.removeBrowserContext', { browserContextId: this._browserContextId });
this._browser._contexts.delete(this._browserContextId);
this._closed = true;
this.emit(Events.BrowserContext.Close);
}
_browserClosed() {
this._closed = true;
for (const page of this._existingPages())
page._didClose();
this.emit(Events.BrowserContext.Close);
}
}

View file

@ -84,9 +84,9 @@ export class FFConnection extends platform.EventEmitter {
} }
_rawSend(message: any) { _rawSend(message: any) {
message = JSON.stringify(message); const data = JSON.stringify(message);
this._debugProtocol('SEND ► ' + message); this._debugProtocol('SEND ► ' + (rewriteInjectedScriptEvaluationLog(message) || data));
this._transport.send(message); this._transport.send(data);
} }
async _onMessage(message: string) { async _onMessage(message: string) {
@ -226,3 +226,10 @@ function rewriteError(error: Error, message: string): Error {
error.message = message; error.message = message;
return error; return error;
} }
function rewriteInjectedScriptEvaluationLog(message: any): string | undefined {
// Injected script is very long and clutters protocol logs.
// To increase development velocity, we skip replace it with short description in the log.
if (message.method === 'Runtime.evaluate' && message.params && message.params.expression && message.params.expression.includes('src/injected/injected.ts'))
return `{"id":${message.id} [evaluate injected script]}`;
}

View file

@ -19,8 +19,8 @@ import * as types from './types';
import * as js from './javascript'; import * as js from './javascript';
import * as dom from './dom'; import * as dom from './dom';
import * as network from './network'; import * as network from './network';
import * as input from './input';
import { helper, assert, RegisteredListener } from './helper'; import { helper, assert, RegisteredListener } from './helper';
import { ClickOptions, MultiClickOptions, PointerActionOptions } from './input';
import { TimeoutError } from './errors'; import { TimeoutError } from './errors';
import { Events } from './events'; import { Events } from './events';
import { Page } from './page'; import { Page } from './page';
@ -52,7 +52,6 @@ export type GotoResult = {
export type LifecycleEvent = 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'; export type LifecycleEvent = 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2';
const kLifecycleEvents: Set<LifecycleEvent> = new Set(['load', 'domcontentloaded', 'networkidle0', 'networkidle2']); const kLifecycleEvents: Set<LifecycleEvent> = new Set(['load', 'domcontentloaded', 'networkidle0', 'networkidle2']);
export type WaitForOptions = types.TimeoutOptions & { waitFor?: types.Visibility | 'nowait' };
type ConsoleTagHandler = () => void; type ConsoleTagHandler = () => void;
export class FrameManager { export class FrameManager {
@ -428,9 +427,10 @@ export class Frame {
let resolve: (error: Error|void) => void; let resolve: (error: Error|void) => void;
const promise = new Promise<Error|void>(x => resolve = x); const promise = new Promise<Error|void>(x => resolve = x);
const watch = (documentId: string, error?: Error) => { const watch = (documentId: string, error?: Error) => {
if (documentId !== expectedDocumentId) if (documentId === expectedDocumentId)
return resolve(new Error('Navigation interrupted by another one')); resolve(error);
resolve(error); else if (!error)
resolve(new Error('Navigation interrupted by another one'));
}; };
const dispose = () => this._documentWatchers.delete(watch); const dispose = () => this._documentWatchers.delete(watch);
this._documentWatchers.add(watch); this._documentWatchers.add(watch);
@ -780,43 +780,43 @@ export class Frame {
return result!; return result!;
} }
async click(selector: string, options?: WaitForOptions & ClickOptions) { async click(selector: string, options?: input.ClickOptions & types.WaitForOptions) {
const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options); const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options);
await handle.click(options); await handle.click(options);
await handle.dispose(); await handle.dispose();
} }
async dblclick(selector: string, options?: WaitForOptions & MultiClickOptions) { async dblclick(selector: string, options?: input.MultiClickOptions & types.WaitForOptions) {
const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options); const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options);
await handle.dblclick(options); await handle.dblclick(options);
await handle.dispose(); await handle.dispose();
} }
async tripleclick(selector: string, options?: WaitForOptions & MultiClickOptions) { async tripleclick(selector: string, options?: input.MultiClickOptions & types.WaitForOptions) {
const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options); const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options);
await handle.tripleclick(options); await handle.tripleclick(options);
await handle.dispose(); await handle.dispose();
} }
async fill(selector: string, value: string, options?: WaitForOptions) { async fill(selector: string, value: string, options?: types.WaitForOptions) {
const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options); const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options);
await handle.fill(value); await handle.fill(value);
await handle.dispose(); await handle.dispose();
} }
async focus(selector: string, options?: WaitForOptions) { async focus(selector: string, options?: types.WaitForOptions) {
const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options); const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options);
await handle.focus(); await handle.focus();
await handle.dispose(); await handle.dispose();
} }
async hover(selector: string, options?: WaitForOptions & PointerActionOptions) { async hover(selector: string, options?: input.PointerActionOptions & types.WaitForOptions) {
const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options); const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options);
await handle.hover(options); await handle.hover(options);
await handle.dispose(); await handle.dispose();
} }
async select(selector: string, value: string | dom.ElementHandle | types.SelectOption | string[] | dom.ElementHandle[] | types.SelectOption[] | undefined, options?: WaitForOptions): Promise<string[]> { async select(selector: string, value: string | dom.ElementHandle | types.SelectOption | string[] | dom.ElementHandle[] | types.SelectOption[] | undefined, options?: types.WaitForOptions): Promise<string[]> {
const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options); const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options);
const values = value === undefined ? [] : Array.isArray(value) ? value : [value]; const values = value === undefined ? [] : Array.isArray(value) ? value : [value];
const result = await handle.select(...values); const result = await handle.select(...values);
@ -824,21 +824,21 @@ export class Frame {
return result; return result;
} }
async type(selector: string, text: string, options?: WaitForOptions & { delay?: number }) { async type(selector: string, text: string, options?: { delay?: number } & types.WaitForOptions) {
const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options); const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options);
await handle.type(text, options); await handle.type(text, options);
await handle.dispose(); await handle.dispose();
} }
async check(selector: string, options?: WaitForOptions) { async check(selector: string, options?: types.WaitForOptions) {
const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options); const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options);
await handle.check(); await handle.check(options);
await handle.dispose(); await handle.dispose();
} }
async uncheck(selector: string, options?: WaitForOptions) { async uncheck(selector: string, options?: types.WaitForOptions) {
const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options); const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options);
await handle.uncheck(); await handle.uncheck(options);
await handle.dispose(); await handle.dispose();
} }
@ -852,13 +852,15 @@ export class Frame {
return Promise.reject(new Error('Unsupported target type: ' + (typeof selectorOrFunctionOrTimeout))); return Promise.reject(new Error('Unsupported target type: ' + (typeof selectorOrFunctionOrTimeout)));
} }
private async _optionallyWaitForSelectorInUtilityContext(selector: string, options: WaitForOptions | undefined): Promise<dom.ElementHandle<Element>> { private async _optionallyWaitForSelectorInUtilityContext(selector: string, options: types.WaitForOptions | undefined): Promise<dom.ElementHandle<Element>> {
const { timeout = this._page._timeoutSettings.timeout(), waitFor = 'visible' } = (options || {}); const { timeout = this._page._timeoutSettings.timeout(), waitFor = true } = (options || {});
if (!helper.isBoolean(waitFor))
throw new Error('waitFor option should be a boolean, got "' + (typeof waitFor) + '"');
let handle: dom.ElementHandle<Element>; let handle: dom.ElementHandle<Element>;
if (waitFor !== 'nowait') { if (waitFor) {
const maybeHandle = await this._waitForSelectorInUtilityContext(selector, waitFor, timeout); const maybeHandle = await this._waitForSelectorInUtilityContext(selector, 'any', timeout);
if (!maybeHandle) if (!maybeHandle)
throw new Error('No node found for selector: ' + selectorToString(selector, waitFor)); throw new Error('No node found for selector: ' + selectorToString(selector, 'any'));
handle = maybeHandle; handle = maybeHandle;
} else { } else {
const context = await this._context('utility'); const context = await this._context('utility');
@ -874,7 +876,7 @@ export class Frame {
if (waitFor === 'visible' || waitFor === 'hidden' || waitFor === 'any') if (waitFor === 'visible' || waitFor === 'hidden' || waitFor === 'any')
visibility = waitFor; visibility = waitFor;
else else
throw new Error(`Unsupported waitFor option "${waitFor}"`); throw new Error(`Unsupported visibility option "${waitFor}"`);
const task = dom.waitForSelectorTask(selector, visibility, timeout); const task = dom.waitForSelectorTask(selector, visibility, timeout);
const result = await this._scheduleRerunnableTask(task, 'utility', timeout, `selector "${selectorToString(selector, visibility)}"`); const result = await this._scheduleRerunnableTask(task, 'utility', timeout, `selector "${selectorToString(selector, visibility)}"`);
if (!result.asElement()) { if (!result.asElement()) {

View file

@ -102,6 +102,14 @@ class Helper {
return obj instanceof RegExp || Object.prototype.toString.call(obj) === '[object RegExp]'; return obj instanceof RegExp || Object.prototype.toString.call(obj) === '[object RegExp]';
} }
static isObject(obj: any): obj is NonNullable<object> {
return typeof obj === 'object' && obj !== null;
}
static isBoolean(obj: any): obj is boolean {
return typeof obj === 'boolean' || obj instanceof Boolean;
}
static async waitForEvent( static async waitForEvent(
emitter: platform.EventEmitterType, emitter: platform.EventEmitterType,
eventName: (string | symbol), eventName: (string | symbol),

View file

@ -145,7 +145,7 @@ class Injected {
return !!(rect.top || rect.bottom || rect.width || rect.height); return !!(rect.top || rect.bottom || rect.width || rect.height);
} }
pollMutation(selector: string | undefined, predicate: Predicate, timeout: number): Promise<any> { private _pollMutation(selector: string | undefined, predicate: Predicate, timeout: number): Promise<any> {
let timedOut = false; let timedOut = false;
if (timeout) if (timeout)
setTimeout(() => timedOut = true, timeout); setTimeout(() => timedOut = true, timeout);
@ -178,7 +178,7 @@ class Injected {
return result; return result;
} }
pollRaf(selector: string | undefined, predicate: Predicate, timeout: number): Promise<any> { private _pollRaf(selector: string | undefined, predicate: Predicate, timeout: number): Promise<any> {
let timedOut = false; let timedOut = false;
if (timeout) if (timeout)
setTimeout(() => timedOut = true, timeout); setTimeout(() => timedOut = true, timeout);
@ -203,7 +203,7 @@ class Injected {
return result; return result;
} }
pollInterval(selector: string | undefined, pollInterval: number, predicate: Predicate, timeout: number): Promise<any> { private _pollInterval(selector: string | undefined, pollInterval: number, predicate: Predicate, timeout: number): Promise<any> {
let timedOut = false; let timedOut = false;
if (timeout) if (timeout)
setTimeout(() => timedOut = true, timeout); setTimeout(() => timedOut = true, timeout);
@ -226,6 +226,14 @@ class Injected {
onTimeout(); onTimeout();
return result; return result;
} }
poll(polling: 'raf' | 'mutation' | number, selector: string | undefined, timeout: number, predicate: Predicate): Promise<any> {
if (polling === 'raf')
return this._pollRaf(selector, predicate, timeout);
if (polling === 'mutation')
return this._pollMutation(selector, predicate, timeout);
return this._pollInterval(selector, polling, predicate, timeout);
}
} }
export default Injected; export default Injected;

View file

@ -485,43 +485,43 @@ export class Page extends platform.EventEmitter {
return this._closed; return this._closed;
} }
async click(selector: string, options?: frames.WaitForOptions & input.ClickOptions) { async click(selector: string, options?: input.ClickOptions & types.WaitForOptions) {
return this.mainFrame().click(selector, options); return this.mainFrame().click(selector, options);
} }
async dblclick(selector: string, options?: frames.WaitForOptions & input.MultiClickOptions) { async dblclick(selector: string, options?: input.MultiClickOptions & types.WaitForOptions) {
return this.mainFrame().dblclick(selector, options); return this.mainFrame().dblclick(selector, options);
} }
async tripleclick(selector: string, options?: frames.WaitForOptions & input.MultiClickOptions) { async tripleclick(selector: string, options?: input.MultiClickOptions & types.WaitForOptions) {
return this.mainFrame().tripleclick(selector, options); return this.mainFrame().tripleclick(selector, options);
} }
async fill(selector: string, value: string, options?: frames.WaitForOptions) { async fill(selector: string, value: string, options?: types.WaitForOptions) {
return this.mainFrame().fill(selector, value, options); return this.mainFrame().fill(selector, value, options);
} }
async focus(selector: string, options?: frames.WaitForOptions) { async focus(selector: string, options?: types.WaitForOptions) {
return this.mainFrame().focus(selector, options); return this.mainFrame().focus(selector, options);
} }
async hover(selector: string, options?: frames.WaitForOptions & input.PointerActionOptions) { async hover(selector: string, options?: input.PointerActionOptions & types.WaitForOptions) {
return this.mainFrame().hover(selector, options); return this.mainFrame().hover(selector, options);
} }
async select(selector: string, value: string | dom.ElementHandle | types.SelectOption | string[] | dom.ElementHandle[] | types.SelectOption[] | undefined, options?: frames.WaitForOptions): Promise<string[]> { async select(selector: string, value: string | dom.ElementHandle | types.SelectOption | string[] | dom.ElementHandle[] | types.SelectOption[] | undefined, options?: types.WaitForOptions): Promise<string[]> {
return this.mainFrame().select(selector, value, options); return this.mainFrame().select(selector, value, options);
} }
async type(selector: string, text: string, options?: frames.WaitForOptions & { delay?: number }) { async type(selector: string, text: string, options?: { delay?: number } & types.WaitForOptions) {
return this.mainFrame().type(selector, text, options); return this.mainFrame().type(selector, text, options);
} }
async check(selector: string, options?: frames.WaitForOptions) { async check(selector: string, options?: types.WaitForOptions) {
return this.mainFrame().check(selector, options); return this.mainFrame().check(selector, options);
} }
async uncheck(selector: string, options?: frames.WaitForOptions) { async uncheck(selector: string, options?: types.WaitForOptions) {
return this.mainFrame().uncheck(selector, options); return this.mainFrame().uncheck(selector, options);
} }

View file

@ -34,6 +34,10 @@ export type LaunchOptions = BrowserArgOptions & {
handleSIGTERM?: boolean, handleSIGTERM?: boolean,
handleSIGHUP?: boolean, handleSIGHUP?: boolean,
timeout?: number, timeout?: number,
/**
* Whether to dump stdio of the browser, this is useful for example when
* diagnosing browser launch issues.
*/
dumpio?: boolean, dumpio?: boolean,
env?: {[key: string]: string} | undefined env?: {[key: string]: string} | undefined
}; };

View file

@ -68,7 +68,7 @@ export class Chromium implements BrowserType {
const { timeout = 30000 } = options || {}; const { timeout = 30000 } = options || {};
const { browserServer, transport } = await this._launchServer(options, 'persistent', userDataDir); const { browserServer, transport } = await this._launchServer(options, 'persistent', userDataDir);
const browser = await CRBrowser.connect(transport!); const browser = await CRBrowser.connect(transport!);
await helper.waitWithTimeout(browser.waitForTarget(t => t.type() === 'page'), 'first page', timeout); await helper.waitWithTimeout(browser._defaultContext.waitForTarget(t => t.type() === 'page'), 'first page', timeout);
// Hack: for typical launch scenario, ensure that close waits for actual process termination. // Hack: for typical launch scenario, ensure that close waits for actual process termination.
const browserContext = browser._defaultContext; const browserContext = browser._defaultContext;
browserContext.close = () => browserServer.close(); browserContext.close = () => browserServer.close();

View file

@ -36,6 +36,8 @@ export type Rect = Size & Point;
export type Quad = [ Point, Point, Point, Point ]; export type Quad = [ Point, Point, Point, Point ];
export type TimeoutOptions = { timeout?: number }; export type TimeoutOptions = { timeout?: number };
export type WaitForOptions = TimeoutOptions & { waitFor?: boolean };
export type Visibility = 'visible' | 'hidden' | 'any'; export type Visibility = 'visible' | 'hidden' | 'any';
export type Polling = 'raf' | 'mutation' | number; export type Polling = 'raf' | 'mutation' | number;

View file

@ -16,7 +16,7 @@
*/ */
import { Browser, createPageInNewContext } from '../browser'; import { Browser, createPageInNewContext } from '../browser';
import { BrowserContext, BrowserContextOptions } from '../browserContext'; import { BrowserContext, BrowserContextOptions, validateBrowserContextOptions, assertBrowserContextIsNotOwned, verifyGeolocation } from '../browserContext';
import { assert, helper, RegisteredListener } from '../helper'; import { assert, helper, RegisteredListener } from '../helper';
import * as network from '../network'; import * as network from '../network';
import { Page } from '../page'; import { Page } from '../page';
@ -27,15 +27,16 @@ import { Protocol } from './protocol';
import { WKConnection, WKSession, kPageProxyMessageReceived, PageProxyMessageReceivedPayload } from './wkConnection'; import { WKConnection, WKSession, kPageProxyMessageReceived, PageProxyMessageReceivedPayload } from './wkConnection';
import { WKPageProxy } from './wkPageProxy'; import { WKPageProxy } from './wkPageProxy';
import * as platform from '../platform'; import * as platform from '../platform';
import { TimeoutSettings } from '../timeoutSettings';
const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.4 Safari/605.1.15'; const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.4 Safari/605.1.15';
export class WKBrowser extends platform.EventEmitter implements Browser { export class WKBrowser extends platform.EventEmitter implements Browser {
private readonly _connection: WKConnection; private readonly _connection: WKConnection;
private readonly _browserSession: WKSession; readonly _browserSession: WKSession;
readonly _defaultContext: BrowserContext; readonly _defaultContext: BrowserContext;
private readonly _contexts = new Map<string, BrowserContext>(); readonly _contexts = new Map<string, WKBrowserContext>();
private readonly _pageProxies = new Map<string, WKPageProxy>(); readonly _pageProxies = new Map<string, WKPageProxy>();
private readonly _eventListeners: RegisteredListener[]; private readonly _eventListeners: RegisteredListener[];
private _firstPageProxyCallback?: () => void; private _firstPageProxyCallback?: () => void;
@ -51,7 +52,7 @@ export class WKBrowser extends platform.EventEmitter implements Browser {
this._connection = new WKConnection(transport, this._onDisconnect.bind(this)); this._connection = new WKConnection(transport, this._onDisconnect.bind(this));
this._browserSession = this._connection.browserSession; this._browserSession = this._connection.browserSession;
this._defaultContext = this._createBrowserContext(undefined, {}); this._defaultContext = new WKBrowserContext(this, undefined, validateBrowserContextOptions({}));
this._eventListeners = [ this._eventListeners = [
helper.addEventListener(this._browserSession, 'Browser.pageProxyCreated', this._onPageProxyCreated.bind(this)), helper.addEventListener(this._browserSession, 'Browser.pageProxyCreated', this._onPageProxyCreated.bind(this)),
@ -64,7 +65,7 @@ export class WKBrowser extends platform.EventEmitter implements Browser {
} }
_onDisconnect() { _onDisconnect() {
for (const context of this.contexts()) for (const context of this._contexts.values())
context._browserClosed(); context._browserClosed();
for (const pageProxy of this._pageProxies.values()) for (const pageProxy of this._pageProxies.values())
pageProxy.dispose(); pageProxy.dispose();
@ -73,13 +74,10 @@ export class WKBrowser extends platform.EventEmitter implements Browser {
} }
async newContext(options: BrowserContextOptions = {}): Promise<BrowserContext> { async newContext(options: BrowserContextOptions = {}): Promise<BrowserContext> {
options = validateBrowserContextOptions(options);
const { browserContextId } = await this._browserSession.send('Browser.createContext'); const { browserContextId } = await this._browserSession.send('Browser.createContext');
options.userAgent = options.userAgent || DEFAULT_USER_AGENT; options.userAgent = options.userAgent || DEFAULT_USER_AGENT;
const context = this._createBrowserContext(browserContextId, options); const context = new WKBrowserContext(this, browserContextId, options);
if (options.ignoreHTTPSErrors)
await this._browserSession.send('Browser.setIgnoreCertificateErrors', { browserContextId, ignore: true });
if (options.locale)
await this._browserSession.send('Browser.setLanguages', { browserContextId, languages: [options.locale] });
await context._initialize(); await context._initialize();
this._contexts.set(browserContextId, context); this._contexts.set(browserContextId, context);
return context; return context;
@ -166,81 +164,125 @@ export class WKBrowser extends platform.EventEmitter implements Browser {
await disconnected; await disconnected;
} }
_createBrowserContext(browserContextId: string | undefined, options: BrowserContextOptions): BrowserContext {
BrowserContext.validateOptions(options);
const context = new BrowserContext({
pages: async (): Promise<Page[]> => {
const pageProxies = Array.from(this._pageProxies.values()).filter(proxy => proxy._browserContext === context);
return await Promise.all(pageProxies.map(proxy => proxy.page()));
},
existingPages: (): Page[] => {
const pages: Page[] = [];
for (const pageProxy of this._pageProxies.values()) {
if (pageProxy._browserContext !== context)
continue;
const page = pageProxy.existingPage();
if (page)
pages.push(page);
}
return pages;
},
newPage: async (): Promise<Page> => {
const { pageProxyId } = await this._browserSession.send('Browser.createPage', { browserContextId });
const pageProxy = this._pageProxies.get(pageProxyId)!;
return await pageProxy.page();
},
close: async (): Promise<void> => {
assert(browserContextId, 'Non-incognito profiles cannot be closed!');
await this._browserSession.send('Browser.deleteContext', { browserContextId: browserContextId });
this._contexts.delete(browserContextId);
},
cookies: async (): Promise<network.NetworkCookie[]> => {
const { cookies } = await this._browserSession.send('Browser.getAllCookies', { browserContextId });
return cookies.map((c: network.NetworkCookie) => ({
...c,
expires: c.expires === 0 ? -1 : c.expires
}));
},
clearCookies: async (): Promise<void> => {
await this._browserSession.send('Browser.deleteAllCookies', { browserContextId });
},
setCookies: async (cookies: network.SetNetworkCookieParam[]): Promise<void> => {
const cc = cookies.map(c => ({ ...c, session: c.expires === -1 || c.expires === undefined })) as Protocol.Browser.SetCookieParam[];
await this._browserSession.send('Browser.setCookies', { cookies: cc, browserContextId });
},
setPermissions: async (origin: string, permissions: string[]): Promise<void> => {
const webPermissionToProtocol = new Map<string, string>([
['geolocation', 'geolocation'],
]);
const filtered = permissions.map(permission => {
const protocolPermission = webPermissionToProtocol.get(permission);
if (!protocolPermission)
throw new Error('Unknown permission: ' + permission);
return protocolPermission;
});
await this._browserSession.send('Browser.grantPermissions', { origin, browserContextId, permissions: filtered });
},
clearPermissions: async () => {
await this._browserSession.send('Browser.resetPermissions', { browserContextId });
},
setGeolocation: async (geolocation: types.Geolocation | null): Promise<void> => {
const payload: any = geolocation ? { ...geolocation, timestamp: Date.now() } : undefined;
await this._browserSession.send('Browser.setGeolocationOverride', { browserContextId, geolocation: payload });
}
}, options);
return context;
}
_setDebugFunction(debugFunction: (message: string) => void) { _setDebugFunction(debugFunction: (message: string) => void) {
this._connection._debugFunction = debugFunction; this._connection._debugFunction = debugFunction;
} }
} }
export class WKBrowserContext extends platform.EventEmitter implements BrowserContext {
readonly _browser: WKBrowser;
readonly _browserContextId: string | undefined;
readonly _options: BrowserContextOptions;
readonly _timeoutSettings: TimeoutSettings;
private _closed = false;
constructor(browser: WKBrowser, browserContextId: string | undefined, options: BrowserContextOptions) {
super();
this._browser = browser;
this._browserContextId = browserContextId;
this._timeoutSettings = new TimeoutSettings();
this._options = options;
}
async _initialize() {
if (this._options.ignoreHTTPSErrors)
await this._browser._browserSession.send('Browser.setIgnoreCertificateErrors', { browserContextId: this._browserContextId, ignore: true });
if (this._options.locale)
await this._browser._browserSession.send('Browser.setLanguages', { browserContextId: this._browserContextId, languages: [this._options.locale] });
const entries = Object.entries(this._options.permissions || {});
await Promise.all(entries.map(entry => this.setPermissions(entry[0], entry[1])));
if (this._options.geolocation)
await this.setGeolocation(this._options.geolocation);
}
_existingPages(): Page[] {
const pages: Page[] = [];
for (const pageProxy of this._browser._pageProxies.values()) {
if (pageProxy._browserContext !== this)
continue;
const page = pageProxy.existingPage();
if (page)
pages.push(page);
}
return pages;
}
setDefaultNavigationTimeout(timeout: number) {
this._timeoutSettings.setDefaultNavigationTimeout(timeout);
}
setDefaultTimeout(timeout: number) {
this._timeoutSettings.setDefaultTimeout(timeout);
}
async pages(): Promise<Page[]> {
const pageProxies = Array.from(this._browser._pageProxies.values()).filter(proxy => proxy._browserContext === this);
return await Promise.all(pageProxies.map(proxy => proxy.page()));
}
async newPage(): Promise<Page> {
assertBrowserContextIsNotOwned(this);
const { pageProxyId } = await this._browser._browserSession.send('Browser.createPage', { browserContextId: this._browserContextId });
const pageProxy = this._browser._pageProxies.get(pageProxyId)!;
return await pageProxy.page();
}
async cookies(...urls: string[]): Promise<network.NetworkCookie[]> {
const { cookies } = await this._browser._browserSession.send('Browser.getAllCookies', { browserContextId: this._browserContextId });
return network.filterCookies(cookies.map((c: network.NetworkCookie) => ({
...c,
expires: c.expires === 0 ? -1 : c.expires
})), urls);
}
async setCookies(cookies: network.SetNetworkCookieParam[]) {
const cc = network.rewriteCookies(cookies).map(c => ({ ...c, session: c.expires === -1 || c.expires === undefined })) as Protocol.Browser.SetCookieParam[];
await this._browser._browserSession.send('Browser.setCookies', { cookies: cc, browserContextId: this._browserContextId });
}
async clearCookies() {
await this._browser._browserSession.send('Browser.deleteAllCookies', { browserContextId: this._browserContextId });
}
async setPermissions(origin: string, permissions: string[]): Promise<void> {
const webPermissionToProtocol = new Map<string, string>([
['geolocation', 'geolocation'],
]);
const filtered = permissions.map(permission => {
const protocolPermission = webPermissionToProtocol.get(permission);
if (!protocolPermission)
throw new Error('Unknown permission: ' + permission);
return protocolPermission;
});
await this._browser._browserSession.send('Browser.grantPermissions', { origin, browserContextId: this._browserContextId, permissions: filtered });
}
async clearPermissions() {
await this._browser._browserSession.send('Browser.resetPermissions', { browserContextId: this._browserContextId });
}
async setGeolocation(geolocation: types.Geolocation | null): Promise<void> {
if (geolocation)
geolocation = verifyGeolocation(geolocation);
this._options.geolocation = geolocation || undefined;
const payload: any = geolocation ? { ...geolocation, timestamp: Date.now() } : undefined;
await this._browser._browserSession.send('Browser.setGeolocationOverride', { browserContextId: this._browserContextId, geolocation: payload });
}
async close() {
if (this._closed)
return;
assert(this._browserContextId, 'Non-incognito profiles cannot be closed!');
await this._browser._browserSession.send('Browser.deleteContext', { browserContextId: this._browserContextId });
this._browser._contexts.delete(this._browserContextId);
this._closed = true;
this.emit(Events.BrowserContext.Close);
}
_browserClosed() {
this._closed = true;
for (const page of this._existingPages())
page._didClose();
this.emit(Events.BrowserContext.Close);
}
}

View file

@ -53,9 +53,9 @@ export class WKConnection {
} }
rawSend(message: any) { rawSend(message: any) {
message = JSON.stringify(message); const data = JSON.stringify(message);
this._debugFunction('SEND ► ' + message); this._debugFunction('SEND ► ' + (rewriteInjectedScriptEvaluationLog(message) || data));
this._transport.send(message); this._transport.send(data);
} }
private _dispatchMessage(message: string) { private _dispatchMessage(message: string) {
@ -177,3 +177,10 @@ export function rewriteError(error: Error, message: string): Error {
export function isSwappedOutError(e: Error) { export function isSwappedOutError(e: Error) {
return e.message.includes('Target was swapped out.'); return e.message.includes('Target was swapped out.');
} }
function rewriteInjectedScriptEvaluationLog(message: any): string | undefined {
// Injected script is very long and clutters protocol logs.
// To increase development velocity, we skip replace it with short description in the log.
if (message.params && message.params.message && message.params.message.includes('Runtime.evaluate') && message.params.message.includes('src/injected/injected.ts'))
return `{"id":${message.id},"method":"${message.method}","params":{"message":[evaluate injected script],"targetId":"${message.params.targetId}"},"pageProxyId":${message.pageProxyId}}`;
}

View file

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang=en>
<title>Examples of DataTransfer's setData(), getData() and clearData()</title>
<meta content="width=device-width">
<style>
div:not(.mouse-helper) {
margin: 0em;
padding: 2em;
}
#source {
color: blue;
border: 1px solid black;
}
#target {
border: 1px solid black;
}
</style>
<script>
function dragstart_handler(ev) {
console.log("dragStart");
// Change the source element's background color to signify drag has started
ev.currentTarget.style.border = "dashed";
// Set the drag's format and data. Use the event target's id for the data
ev.dataTransfer.setData("text/plain", ev.target.id);
}
function dragover_handler(ev) {
console.log("dragOver");
ev.preventDefault();
}
function drop_handler(ev) {
console.log("Drop");
ev.preventDefault();
// Get the data, which is the id of the drop target
var data = ev.dataTransfer.getData("text");
ev.target.appendChild(document.getElementById(data));
// Clear the drag data cache (for all formats/types)
ev.dataTransfer.clearData();
}
</script>
<body>
<script src="input/mouse-helper.js"></script>
<h1>Examples of <code>DataTransfer</code>: <code>setData()</code>, <code>getData()</code>, <code>clearData()</code></h1>
<div>
<p id="source" ondragstart="dragstart_handler(event);" draggable="true">
Select this element, drag it to the Drop Zone and then release the selection to move the element.</p>
</div>
<div id="target" ondrop="drop_handler(event);" ondragover="dragover_handler(event);">Drop Zone</div>
</body>
</html>

View file

@ -13,6 +13,8 @@
window.pageX = undefined; window.pageX = undefined;
window.pageY = undefined; window.pageY = undefined;
window.shiftKey = undefined; window.shiftKey = undefined;
window.pageX = undefined;
window.pageY = undefined;
document.querySelector('button').addEventListener('click', e => { document.querySelector('button').addEventListener('click', e => {
result = 'Clicked'; result = 'Clicked';
offsetX = e.offsetX; offsetX = e.offsetX;

View file

@ -26,28 +26,29 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;
describe('BrowserContext', function() { describe('BrowserContext', function() {
it('should create new context', async function({browser, newContext}) { it('should create new context', async function({browser}) {
expect(browser.contexts().length).toBe(0); expect(browser.contexts().length).toBe(0);
const context = await newContext(); const context = await browser.newContext();
expect(browser.contexts().length).toBe(1); expect(browser.contexts().length).toBe(1);
expect(browser.contexts().indexOf(context) !== -1).toBe(true); expect(browser.contexts().indexOf(context) !== -1).toBe(true);
await context.close(); await context.close();
expect(browser.contexts().length).toBe(0); expect(browser.contexts().length).toBe(0);
}); });
it('window.open should use parent tab context', async function({newContext, server}) { it('window.open should use parent tab context', async function({browser, server}) {
const context = await newContext(); const context = await browser.newContext();
const page = await context.newPage(); const page = await context.newPage();
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
const [popupTarget] = await Promise.all([ const [popup] = await Promise.all([
utils.waitEvent(page, 'popup'), utils.waitEvent(page, 'popup'),
page.evaluate(url => window.open(url), server.EMPTY_PAGE) page.evaluate(url => window.open(url), server.EMPTY_PAGE)
]); ]);
expect(popupTarget.context()).toBe(context); expect(popup.context()).toBe(context);
await context.close();
}); });
it('should isolate localStorage and cookies', async function({browser, newContext, server}) { it('should isolate localStorage and cookies', async function({browser, server}) {
// Create two incognito contexts. // Create two incognito contexts.
const context1 = await newContext(); const context1 = await browser.newContext();
const context2 = await newContext(); const context2 = await browser.newContext();
expect((await context1.pages()).length).toBe(0); expect((await context1.pages()).length).toBe(0);
expect((await context2.pages()).length).toBe(0); expect((await context2.pages()).length).toBe(0);
@ -88,69 +89,84 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
]); ]);
expect(browser.contexts().length).toBe(0); expect(browser.contexts().length).toBe(0);
}); });
it('should propagate default viewport to the page', async({ newPage }) => { it('should propagate default viewport to the page', async({ browser }) => {
const page = await newPage({ viewport: { width: 456, height: 789 } }); const context = await browser.newContext({ viewport: { width: 456, height: 789 } });
const page = await context.newPage();
expect(page.viewportSize().width).toBe(456); expect(page.viewportSize().width).toBe(456);
expect(page.viewportSize().height).toBe(789); expect(page.viewportSize().height).toBe(789);
expect(await page.evaluate('window.innerWidth')).toBe(456); expect(await page.evaluate('window.innerWidth')).toBe(456);
expect(await page.evaluate('window.innerHeight')).toBe(789); expect(await page.evaluate('window.innerHeight')).toBe(789);
await context.close();
}); });
it('should make a copy of default viewport', async({ newContext }) => { it('should make a copy of default viewport', async({ browser }) => {
const viewport = { width: 456, height: 789 }; const viewport = { width: 456, height: 789 };
const context = await newContext({ viewport }); const context = await browser.newContext({ viewport });
viewport.width = 567; viewport.width = 567;
const page = await context.newPage(); const page = await context.newPage();
expect(page.viewportSize().width).toBe(456); expect(page.viewportSize().width).toBe(456);
expect(page.viewportSize().height).toBe(789); expect(page.viewportSize().height).toBe(789);
expect(await page.evaluate('window.innerWidth')).toBe(456); expect(await page.evaluate('window.innerWidth')).toBe(456);
expect(await page.evaluate('window.innerHeight')).toBe(789); expect(await page.evaluate('window.innerHeight')).toBe(789);
await context.close();
}); });
}); });
describe('BrowserContext({userAgent})', function() { describe('BrowserContext({userAgent})', function() {
it('should work', async({newPage, server}) => { it('should work', async({browser, server}) => {
{ {
const page = await newPage(); const context = await browser.newContext();
const page = await context.newPage();
expect(await page.evaluate(() => navigator.userAgent)).toContain('Mozilla'); expect(await page.evaluate(() => navigator.userAgent)).toContain('Mozilla');
await context.close();
} }
{ {
const page = await newPage({ userAgent: 'foobar' }); const context = await browser.newContext({ userAgent: 'foobar' });
const page = await context.newPage();
const [request] = await Promise.all([ const [request] = await Promise.all([
server.waitForRequest('/empty.html'), server.waitForRequest('/empty.html'),
page.goto(server.EMPTY_PAGE), page.goto(server.EMPTY_PAGE),
]); ]);
expect(request.headers['user-agent']).toBe('foobar'); expect(request.headers['user-agent']).toBe('foobar');
await context.close();
} }
}); });
it('should work for subframes', async({newPage, server}) => { it('should work for subframes', async({browser, server}) => {
{ {
const page = await newPage(); const context = await browser.newContext();
const page = await context.newPage();
expect(await page.evaluate(() => navigator.userAgent)).toContain('Mozilla'); expect(await page.evaluate(() => navigator.userAgent)).toContain('Mozilla');
await context.close();
} }
{ {
const page = await newPage({ userAgent: 'foobar' }); const context = await browser.newContext({ userAgent: 'foobar' });
const page = await context.newPage();
const [request] = await Promise.all([ const [request] = await Promise.all([
server.waitForRequest('/empty.html'), server.waitForRequest('/empty.html'),
utils.attachFrame(page, 'frame1', server.EMPTY_PAGE), utils.attachFrame(page, 'frame1', server.EMPTY_PAGE),
]); ]);
expect(request.headers['user-agent']).toBe('foobar'); expect(request.headers['user-agent']).toBe('foobar');
await context.close();
} }
}); });
it('should emulate device user-agent', async({newPage, server}) => { it('should emulate device user-agent', async({browser, server}) => {
{ {
const page = await newPage(); const context = await browser.newContext();
const page = await context.newPage();
await page.goto(server.PREFIX + '/mobile.html'); await page.goto(server.PREFIX + '/mobile.html');
expect(await page.evaluate(() => navigator.userAgent)).not.toContain('iPhone'); expect(await page.evaluate(() => navigator.userAgent)).not.toContain('iPhone');
await context.close();
} }
{ {
const page = await newPage({ userAgent: playwright.devices['iPhone 6'].userAgent }); const context = await browser.newContext({ userAgent: playwright.devices['iPhone 6'].userAgent });
const page = await context.newPage();
await page.goto(server.PREFIX + '/mobile.html'); await page.goto(server.PREFIX + '/mobile.html');
expect(await page.evaluate(() => navigator.userAgent)).toContain('iPhone'); expect(await page.evaluate(() => navigator.userAgent)).toContain('iPhone');
await context.close();
} }
}); });
it('should make a copy of default options', async({newContext, server}) => { it('should make a copy of default options', async({browser, server}) => {
const options = { userAgent: 'foobar' }; const options = { userAgent: 'foobar' };
const context = await newContext(options); const context = await browser.newContext(options);
options.userAgent = 'wrong'; options.userAgent = 'wrong';
const page = await context.newPage(); const page = await context.newPage();
const [request] = await Promise.all([ const [request] = await Promise.all([
@ -158,50 +174,60 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
page.goto(server.EMPTY_PAGE), page.goto(server.EMPTY_PAGE),
]); ]);
expect(request.headers['user-agent']).toBe('foobar'); expect(request.headers['user-agent']).toBe('foobar');
await context.close();
}); });
}); });
describe('BrowserContext({bypassCSP})', function() { describe('BrowserContext({bypassCSP})', function() {
it('should bypass CSP meta tag', async({newPage, server}) => { it('should bypass CSP meta tag', async({browser, server}) => {
// Make sure CSP prohibits addScriptTag. // Make sure CSP prohibits addScriptTag.
{ {
const page = await newPage(); const context = await browser.newContext();
const page = await context.newPage();
await page.goto(server.PREFIX + '/csp.html'); await page.goto(server.PREFIX + '/csp.html');
await page.addScriptTag({content: 'window.__injected = 42;'}).catch(e => void e); await page.addScriptTag({content: 'window.__injected = 42;'}).catch(e => void e);
expect(await page.evaluate(() => window.__injected)).toBe(undefined); expect(await page.evaluate(() => window.__injected)).toBe(undefined);
await context.close();
} }
// By-pass CSP and try one more time. // By-pass CSP and try one more time.
{ {
const page = await newPage({ bypassCSP: true }); const context = await browser.newContext({ bypassCSP: true });
const page = await context.newPage();
await page.goto(server.PREFIX + '/csp.html'); await page.goto(server.PREFIX + '/csp.html');
await page.addScriptTag({content: 'window.__injected = 42;'}); await page.addScriptTag({content: 'window.__injected = 42;'});
expect(await page.evaluate(() => window.__injected)).toBe(42); expect(await page.evaluate(() => window.__injected)).toBe(42);
await context.close();
} }
}); });
it('should bypass CSP header', async({newPage, server}) => { it('should bypass CSP header', async({browser, server}) => {
// Make sure CSP prohibits addScriptTag. // Make sure CSP prohibits addScriptTag.
server.setCSP('/empty.html', 'default-src "self"'); server.setCSP('/empty.html', 'default-src "self"');
{ {
const page = await newPage(); const context = await browser.newContext();
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
await page.addScriptTag({content: 'window.__injected = 42;'}).catch(e => void e); await page.addScriptTag({content: 'window.__injected = 42;'}).catch(e => void e);
expect(await page.evaluate(() => window.__injected)).toBe(undefined); expect(await page.evaluate(() => window.__injected)).toBe(undefined);
await context.close();
} }
// By-pass CSP and try one more time. // By-pass CSP and try one more time.
{ {
const page = await newPage({ bypassCSP: true }); const context = await browser.newContext({ bypassCSP: true });
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
await page.addScriptTag({content: 'window.__injected = 42;'}); await page.addScriptTag({content: 'window.__injected = 42;'});
expect(await page.evaluate(() => window.__injected)).toBe(42); expect(await page.evaluate(() => window.__injected)).toBe(42);
await context.close();
} }
}); });
it('should bypass after cross-process navigation', async({newPage, server}) => { it('should bypass after cross-process navigation', async({browser, server}) => {
const page = await newPage({ bypassCSP: true }); const context = await browser.newContext({ bypassCSP: true });
const page = await context.newPage();
await page.goto(server.PREFIX + '/csp.html'); await page.goto(server.PREFIX + '/csp.html');
await page.addScriptTag({content: 'window.__injected = 42;'}); await page.addScriptTag({content: 'window.__injected = 42;'});
expect(await page.evaluate(() => window.__injected)).toBe(42); expect(await page.evaluate(() => window.__injected)).toBe(42);
@ -209,32 +235,38 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
await page.goto(server.CROSS_PROCESS_PREFIX + '/csp.html'); await page.goto(server.CROSS_PROCESS_PREFIX + '/csp.html');
await page.addScriptTag({content: 'window.__injected = 42;'}); await page.addScriptTag({content: 'window.__injected = 42;'});
expect(await page.evaluate(() => window.__injected)).toBe(42); expect(await page.evaluate(() => window.__injected)).toBe(42);
await context.close();
}); });
it('should bypass CSP in iframes as well', async({newPage, server}) => { it('should bypass CSP in iframes as well', async({browser, server}) => {
// Make sure CSP prohibits addScriptTag in an iframe. // Make sure CSP prohibits addScriptTag in an iframe.
{ {
const page = await newPage(); const context = await browser.newContext();
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
const frame = await utils.attachFrame(page, 'frame1', server.PREFIX + '/csp.html'); const frame = await utils.attachFrame(page, 'frame1', server.PREFIX + '/csp.html');
await frame.addScriptTag({content: 'window.__injected = 42;'}).catch(e => void e); await frame.addScriptTag({content: 'window.__injected = 42;'}).catch(e => void e);
expect(await frame.evaluate(() => window.__injected)).toBe(undefined); expect(await frame.evaluate(() => window.__injected)).toBe(undefined);
await context.close();
} }
// By-pass CSP and try one more time. // By-pass CSP and try one more time.
{ {
const page = await newPage({ bypassCSP: true }); const context = await browser.newContext({ bypassCSP: true });
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
const frame = await utils.attachFrame(page, 'frame1', server.PREFIX + '/csp.html'); const frame = await utils.attachFrame(page, 'frame1', server.PREFIX + '/csp.html');
await frame.addScriptTag({content: 'window.__injected = 42;'}).catch(e => void e); await frame.addScriptTag({content: 'window.__injected = 42;'}).catch(e => void e);
expect(await frame.evaluate(() => window.__injected)).toBe(42); expect(await frame.evaluate(() => window.__injected)).toBe(42);
await context.close();
} }
}); });
}); });
describe('BrowserContext({javaScriptEnabled})', function() { describe('BrowserContext({javaScriptEnabled})', function() {
it('should work', async({newPage}) => { it('should work', async({browser}) => {
{ {
const page = await newPage({ javaScriptEnabled: false }); const context = await browser.newContext({ javaScriptEnabled: false });
const page = await context.newPage();
await page.goto('data:text/html, <script>var something = "forbidden"</script>'); await page.goto('data:text/html, <script>var something = "forbidden"</script>');
let error = null; let error = null;
await page.evaluate('something').catch(e => error = e); await page.evaluate('something').catch(e => error = e);
@ -242,17 +274,22 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
expect(error.message).toContain('Can\'t find variable: something'); expect(error.message).toContain('Can\'t find variable: something');
else else
expect(error.message).toContain('something is not defined'); expect(error.message).toContain('something is not defined');
await context.close();
} }
{ {
const page = await newPage(); const context = await browser.newContext();
const page = await context.newPage();
await page.goto('data:text/html, <script>var something = "forbidden"</script>'); await page.goto('data:text/html, <script>var something = "forbidden"</script>');
expect(await page.evaluate('something')).toBe('forbidden'); expect(await page.evaluate('something')).toBe('forbidden');
await context.close();
} }
}); });
it('should be able to navigate after disabling javascript', async({newPage, server}) => { it('should be able to navigate after disabling javascript', async({browser, server}) => {
const page = await newPage({ javaScriptEnabled: false }); const context = await browser.newContext({ javaScriptEnabled: false });
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
await context.close();
}); });
}); });
}; };

View file

@ -25,28 +25,31 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI
const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;
describe('Target', function() { describe('Target', function() {
it('Chromium.targets should return all of the targets', async({page, server, browser}) => { it('ChromiumBrowserContext.targets should return all of the targets', async({page, server, browser}) => {
// The pages will be the testing page and the original newtab page const second = await page.context().newPage();
const targets = browser.targets(); await second.goto(server.EMPTY_PAGE);
expect(targets.some(target => target.type() === 'page' && const targets = page.context().targets();
target.url() === 'about:blank')).toBeTruthy('Missing blank page'); // The pages will be the testing page from the harness and the one created here.
expect(targets.some(target => target.type() === 'browser')).toBeTruthy('Missing browser target'); expect(targets.length).toBe(2);
expect(targets.some(target => target.type() !== 'page')).toBe(false);
expect(targets.some(target => target.url() === 'about:blank')).toBeTruthy('Missing blank page');
expect(targets.some(target => target.url() === server.EMPTY_PAGE)).toBeTruthy('Missing new page');
await second.close();
}); });
it('Browser.pages should return all of the pages', async({page, server, context}) => { it('BrowserContext.pages should return all of the pages', async({page, server, context}) => {
// The pages will be the testing page const second = await page.context().newPage();
const allPages = await context.pages(); const allPages = await context.pages();
expect(allPages.length).toBe(1); expect(allPages.length).toBe(2);
expect(allPages).toContain(page); expect(allPages).toContain(page);
expect(allPages[0]).not.toBe(allPages[1]); expect(allPages).toContain(second);
await second.close();
}); });
it('should contain browser target', async({browser}) => { it('should report browser target', async({browser}) => {
const targets = browser.targets(); expect(browser.browserTarget()).toBeTruthy();
const browserTarget = targets.find(target => target.type() === 'browser');
expect(browserTarget).toBe(browser.browserTarget());
}); });
it('should report when a new page is created and closed', async({browser, page, server, context}) => { it('should report when a new page is created and closed', async({browser, page, server, context}) => {
const [otherPage] = await Promise.all([ const [otherPage] = await Promise.all([
browser.waitForTarget(target => target.url() === server.CROSS_PROCESS_PREFIX + '/empty.html').then(target => target.page()), page.context().waitForTarget(target => target.url() === server.CROSS_PROCESS_PREFIX + '/empty.html').then(target => target.page()),
page.evaluate(url => window.open(url), server.CROSS_PROCESS_PREFIX + '/empty.html'), page.evaluate(url => window.open(url), server.CROSS_PROCESS_PREFIX + '/empty.html'),
]); ]);
expect(otherPage.url()).toContain(server.CROSS_PROCESS_PREFIX); expect(otherPage.url()).toContain(server.CROSS_PROCESS_PREFIX);
@ -57,50 +60,50 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI
expect(allPages).toContain(page); expect(allPages).toContain(page);
expect(allPages).toContain(otherPage); expect(allPages).toContain(otherPage);
const closePagePromise = new Promise(fulfill => browser.once('targetdestroyed', target => fulfill(target.page()))); const closePagePromise = new Promise(fulfill => page.context().once('targetdestroyed', target => fulfill(target.page())));
await otherPage.close(); await otherPage.close();
expect(await closePagePromise).toBe(otherPage); expect(await closePagePromise).toBe(otherPage);
allPages = await Promise.all(browser.targets().map(target => target.page())); allPages = await Promise.all(page.context().targets().map(target => target.page()));
expect(allPages).toContain(page); expect(allPages).toContain(page);
expect(allPages).not.toContain(otherPage); expect(allPages).not.toContain(otherPage);
}); });
it('should report when a service worker is created and destroyed', async({browser, page, server, context}) => { it('should report when a service worker is created and destroyed', async({browser, page, server, context}) => {
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
const createdTarget = new Promise(fulfill => browser.once('targetcreated', target => fulfill(target))); const createdTarget = new Promise(fulfill => page.context().once('targetcreated', target => fulfill(target)));
await page.goto(server.PREFIX + '/serviceworkers/empty/sw.html'); await page.goto(server.PREFIX + '/serviceworkers/empty/sw.html');
expect((await createdTarget).type()).toBe('service_worker'); expect((await createdTarget).type()).toBe('service_worker');
expect((await createdTarget).url()).toBe(server.PREFIX + '/serviceworkers/empty/sw.js'); expect((await createdTarget).url()).toBe(server.PREFIX + '/serviceworkers/empty/sw.js');
const destroyedTarget = new Promise(fulfill => browser.once('targetdestroyed', target => fulfill(target))); const destroyedTarget = new Promise(fulfill => page.context().once('targetdestroyed', target => fulfill(target)));
await page.evaluate(() => window.registrationPromise.then(registration => registration.unregister())); await page.evaluate(() => window.registrationPromise.then(registration => registration.unregister()));
expect(await destroyedTarget).toBe(await createdTarget); expect(await destroyedTarget).toBe(await createdTarget);
}); });
it('should create a worker from a service worker', async({browser, page, server, context}) => { it('should create a worker from a service worker', async({browser, page, server, context}) => {
await page.goto(server.PREFIX + '/serviceworkers/empty/sw.html'); await page.goto(server.PREFIX + '/serviceworkers/empty/sw.html');
const target = await browser.waitForTarget(target => target.type() === 'service_worker'); const target = await page.context().waitForTarget(target => target.type() === 'service_worker');
const worker = await browser.serviceWorker(target); const worker = await target.serviceWorker();
expect(await worker.evaluate(() => self.toString())).toBe('[object ServiceWorkerGlobalScope]'); expect(await worker.evaluate(() => self.toString())).toBe('[object ServiceWorkerGlobalScope]');
}); });
it('should create a worker from a shared worker', async({browser, page, server, context}) => { it('should not create a worker from a shared worker', async({browser, page, server, context}) => {
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
await page.evaluate(() => { await page.evaluate(() => {
new SharedWorker('data:text/javascript,console.log("hi")'); new SharedWorker('data:text/javascript,console.log("hi")');
}); });
const target = await browser.waitForTarget(target => target.type() === 'shared_worker'); const target = await page.context().waitForTarget(target => target.type() === 'shared_worker');
const worker = await browser.serviceWorker(target); const worker = await target.serviceWorker();
expect(await worker.evaluate(() => self.toString())).toBe('[object SharedWorkerGlobalScope]'); expect(worker).toBe(null);
}); });
it('should report when a target url changes', async({browser, page, server, context}) => { it('should report when a target url changes', async({browser, page, server, context}) => {
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
let changedTarget = new Promise(fulfill => browser.once('targetchanged', target => fulfill(target))); let changedTarget = new Promise(fulfill => page.context().once('targetchanged', target => fulfill(target)));
await page.goto(server.CROSS_PROCESS_PREFIX + '/'); await page.goto(server.CROSS_PROCESS_PREFIX + '/');
expect((await changedTarget).url()).toBe(server.CROSS_PROCESS_PREFIX + '/'); expect((await changedTarget).url()).toBe(server.CROSS_PROCESS_PREFIX + '/');
changedTarget = new Promise(fulfill => browser.once('targetchanged', target => fulfill(target))); changedTarget = new Promise(fulfill => page.context().once('targetchanged', target => fulfill(target)));
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
expect((await changedTarget).url()).toBe(server.EMPTY_PAGE); expect((await changedTarget).url()).toBe(server.EMPTY_PAGE);
}); });
@ -108,13 +111,13 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI
let targetChanged = false; let targetChanged = false;
const listener = () => targetChanged = true; const listener = () => targetChanged = true;
browser.on('targetchanged', listener); browser.on('targetchanged', listener);
const targetPromise = new Promise(fulfill => browser.once('targetcreated', target => fulfill(target))); const targetPromise = new Promise(fulfill => context.once('targetcreated', target => fulfill(target)));
const newPagePromise = context.newPage(); const newPagePromise = context.newPage();
const target = await targetPromise; const target = await targetPromise;
expect(target.url()).toBe('about:blank'); expect(target.url()).toBe('about:blank');
const newPage = await newPagePromise; const newPage = await newPagePromise;
const targetPromise2 = new Promise(fulfill => browser.once('targetcreated', target => fulfill(target))); const targetPromise2 = new Promise(fulfill => context.once('targetcreated', target => fulfill(target)));
const evaluatePromise = newPage.evaluate(() => window.open('about:blank')); const evaluatePromise = newPage.evaluate(() => window.open('about:blank'));
const target2 = await targetPromise2; const target2 = await targetPromise2;
expect(target2.url()).toBe('about:blank'); expect(target2.url()).toBe('about:blank');
@ -132,7 +135,7 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI
server.waitForRequest('/one-style.css') server.waitForRequest('/one-style.css')
]); ]);
// Connect to the opened page. // Connect to the opened page.
const target = await browser.waitForTarget(target => target.url().includes('one-style.html')); const target = await page.context().waitForTarget(target => target.url().includes('one-style.html'));
const newPage = await target.page(); const newPage = await target.page();
// Issue a redirect. // Issue a redirect.
serverResponse.writeHead(302, { location: '/injectedstyle.css' }); serverResponse.writeHead(302, { location: '/injectedstyle.css' });
@ -145,59 +148,59 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI
it('should have an opener', async({browser, page, server, context}) => { it('should have an opener', async({browser, page, server, context}) => {
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
const [createdTarget] = await Promise.all([ const [createdTarget] = await Promise.all([
new Promise(fulfill => browser.once('targetcreated', target => fulfill(target))), new Promise(fulfill => page.context().once('targetcreated', target => fulfill(target))),
page.goto(server.PREFIX + '/popup/window-open.html') page.goto(server.PREFIX + '/popup/window-open.html')
]); ]);
expect((await createdTarget.page()).url()).toBe(server.PREFIX + '/popup/popup.html'); expect((await createdTarget.page()).url()).toBe(server.PREFIX + '/popup/popup.html');
expect(createdTarget.opener()).toBe(browser.pageTarget(page)); expect(createdTarget.opener()).toBe(page.context().pageTarget(page));
expect(browser.pageTarget(page).opener()).toBe(null); expect(page.context().pageTarget(page).opener()).toBe(null);
}); });
it('should close all belonging targets once closing context', async function({browser, newContext}) { it('should close all belonging targets once closing context', async function({browser}) {
const targets = async (context) => (await browser.targets()).filter(t => t.type() === 'page' && t.context() === context); const context = await browser.newContext();
const context = await newContext();
await context.newPage(); await context.newPage();
expect((await targets(context)).length).toBe(1); expect((await context.targets()).length).toBe(1);
expect((await context.pages()).length).toBe(1); expect((await context.pages()).length).toBe(1);
await context.close(); await context.close();
expect((await targets(context)).length).toBe(0); expect((await context.targets()).length).toBe(0);
}); });
}); });
describe('Chromium.waitForTarget', () => { describe('Chromium.waitForTarget', () => {
it('should wait for a target', async function({browser, server, newContext}) { it('should wait for a target', async function({server, browser}) {
const context = await newContext(); const context = await browser.newContext();
let resolved = false; let resolved = false;
const targetPromise = browser.waitForTarget(target => target.context() === context && target.url() === server.EMPTY_PAGE); const targetPromise = context.waitForTarget(target => target.url() === server.EMPTY_PAGE);
targetPromise.then(() => resolved = true); targetPromise.then(() => resolved = true);
const page = await context.newPage(); const page = await context.newPage();
expect(resolved).toBe(false); expect(resolved).toBe(false);
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
const target = await targetPromise; const target = await targetPromise;
expect(await target.page()).toBe(page); expect(await target.page()).toBe(page);
await context.close();
}); });
it('should timeout waiting for a non-existent target', async function({browser, context, server}) { it('should timeout waiting for a non-existent target', async function({browser, context, server}) {
const error = await browser.waitForTarget(target => target.context() === context && target.url() === server.EMPTY_PAGE, {timeout: 1}).catch(e => e); const error = await context.waitForTarget(target => target.url() === server.EMPTY_PAGE, {timeout: 1}).catch(e => e);
expect(error).toBeInstanceOf(playwright.errors.TimeoutError); expect(error).toBeInstanceOf(playwright.errors.TimeoutError);
}); });
it('should wait for a target', async function({browser, server}) { it('should wait for a target', async function({browser, server}) {
const context = await browser.newContext();
let resolved = false; let resolved = false;
const targetPromise = browser.waitForTarget(target => target.url() === server.EMPTY_PAGE); const targetPromise = context.waitForTarget(target => target.url() === server.EMPTY_PAGE);
targetPromise.then(() => resolved = true); targetPromise.then(() => resolved = true);
const page = await browser.newPage(); const page = await context.newPage();
expect(resolved).toBe(false); expect(resolved).toBe(false);
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
const target = await targetPromise; const target = await targetPromise;
expect(await target.page()).toBe(page); expect(await target.page()).toBe(page);
await page.context().close(); await context.close();
}); });
it('should fire target events', async function({browser, newContext, server}) { it('should fire target events', async function({browser, server}) {
const context = await newContext(); const context = await browser.newContext();
const events = []; const events = [];
browser.on('targetcreated', target => events.push('CREATED: ' + target.url())); context.on('targetcreated', target => events.push('CREATED: ' + target.url()));
browser.on('targetchanged', target => events.push('CHANGED: ' + target.url())); context.on('targetchanged', target => events.push('CHANGED: ' + target.url()));
browser.on('targetdestroyed', target => events.push('DESTROYED: ' + target.url())); context.on('targetdestroyed', target => events.push('DESTROYED: ' + target.url()));
const page = await context.newPage(); const page = await context.newPage();
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
await page.close(); await page.close();
@ -206,6 +209,7 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI
`CHANGED: ${server.EMPTY_PAGE}`, `CHANGED: ${server.EMPTY_PAGE}`,
`DESTROYED: ${server.EMPTY_PAGE}` `DESTROYED: ${server.EMPTY_PAGE}`
]); ]);
await context.close();
}); });
}); });

View file

@ -51,17 +51,18 @@ module.exports.describe = function({testRunner, expect, playwright, defaultBrows
it('background_page target type should be available', async() => { it('background_page target type should be available', async() => {
const browserWithExtension = await playwright.launch(extensionOptions); const browserWithExtension = await playwright.launch(extensionOptions);
const page = await browserWithExtension.newPage(); const page = await browserWithExtension.newPage();
const backgroundPageTarget = await browserWithExtension.waitForTarget(target => target.type() === 'background_page'); const backgroundPageTarget = await page.context().waitForTarget(target => target.type() === 'background_page');
await page.close(); await page.close();
await browserWithExtension.close(); await browserWithExtension.close();
expect(backgroundPageTarget).toBeTruthy(); expect(backgroundPageTarget).toBeTruthy();
}); });
it('target.page() should return a background_page', async({}) => { it('target.page() should return a background_page', async({}) => {
const browserWithExtension = await playwright.launch(extensionOptions); const browserWithExtension = await playwright.launch(extensionOptions);
const backgroundPageTarget = await browserWithExtension.waitForTarget(target => target.type() === 'background_page'); const page = await browserWithExtension.newPage();
const page = await backgroundPageTarget.page(); const backgroundPageTarget = await page.context().waitForTarget(target => target.type() === 'background_page');
expect(await page.evaluate(() => 2 * 3)).toBe(6); const backgroundPage = await backgroundPageTarget.page();
expect(await page.evaluate(() => window.MAGIC)).toBe(42); expect(await backgroundPage.evaluate(() => 2 * 3)).toBe(6);
expect(await backgroundPage.evaluate(() => window.MAGIC)).toBe(42);
await browserWithExtension.close(); await browserWithExtension.close();
}); });
// TODO: Support OOOPIF. @see https://github.com/GoogleChrome/puppeteer/issues/2548 // TODO: Support OOOPIF. @see https://github.com/GoogleChrome/puppeteer/issues/2548
@ -91,7 +92,7 @@ module.exports.describe = function({testRunner, expect, playwright, defaultBrows
const context = await browser.newContext(); const context = await browser.newContext();
await Promise.all([ await Promise.all([
context.newPage(), context.newPage(),
browser.waitForTarget(target => target.context() === context && target.url().includes('devtools://')), context.waitForTarget(target => target.url().includes('devtools://')),
]); ]);
await browser.close(); await browser.close();
}); });

View file

@ -59,9 +59,9 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
const browser = await playwright.launch(defaultBrowserOptions); const browser = await playwright.launch(defaultBrowserOptions);
const context = await browser.newContext(); const context = await browser.newContext();
const events = []; const events = [];
browser.on('targetcreated', target => target.context() === context && events.push('CREATED')); context.on('targetcreated', target => events.push('CREATED'));
browser.on('targetchanged', target => target.context() === context && events.push('CHANGED')); context.on('targetchanged', target => events.push('CHANGED'));
browser.on('targetdestroyed', target => target.context() === context && events.push('DESTROYED')); context.on('targetdestroyed', target => events.push('DESTROYED'));
const page = await context.newPage(); const page = await context.newPage();
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
await page.close(); await page.close();

View file

@ -43,18 +43,18 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
}); });
xit('should report oopif frames', async function({browser, page, server, context}) { xit('should report oopif frames', async function({browser, page, server, context}) {
await page.goto(server.PREFIX + '/dynamic-oopif.html'); await page.goto(server.PREFIX + '/dynamic-oopif.html');
expect(oopifs(browser).length).toBe(1); expect(oopifs(page.context()).length).toBe(1);
expect(page.frames().length).toBe(2); expect(page.frames().length).toBe(2);
}); });
it('should load oopif iframes with subresources and request interception', async function({browser, page, server, context}) { it('should load oopif iframes with subresources and request interception', async function({browser, page, server, context}) {
await page.route('*', request => request.continue()); await page.route('*', request => request.continue());
await page.goto(server.PREFIX + '/dynamic-oopif.html'); await page.goto(server.PREFIX + '/dynamic-oopif.html');
expect(oopifs(browser).length).toBe(1); expect(oopifs(page.context()).length).toBe(1);
}); });
}); });
}; };
function oopifs(browser) { function oopifs(context) {
return browser.targets().filter(target => target._targetInfo.type === 'iframe'); return context.targets().filter(target => target._targetInfo.type === 'iframe');
} }

View file

@ -26,7 +26,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
describe('Chromium.createCDPSession', function() { describe('Chromium.createCDPSession', function() {
it('should work', async function({page, browser, server}) { it('should work', async function({page, browser, server}) {
const client = await browser.pageTarget(page).createCDPSession(); const client = await page.context().pageTarget(page).createCDPSession();
await Promise.all([ await Promise.all([
client.send('Runtime.enable'), client.send('Runtime.enable'),
@ -36,7 +36,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
expect(foo).toBe('bar'); expect(foo).toBe('bar');
}); });
it('should send events', async function({page, browser, server}) { it('should send events', async function({page, browser, server}) {
const client = await browser.pageTarget(page).createCDPSession(); const client = await page.context().pageTarget(page).createCDPSession();
await client.send('Network.enable'); await client.send('Network.enable');
const events = []; const events = [];
client.on('Network.requestWillBeSent', event => events.push(event)); client.on('Network.requestWillBeSent', event => events.push(event));
@ -44,7 +44,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
expect(events.length).toBe(1); expect(events.length).toBe(1);
}); });
it('should enable and disable domains independently', async function({page, browser, server}) { it('should enable and disable domains independently', async function({page, browser, server}) {
const client = await browser.pageTarget(page).createCDPSession(); const client = await page.context().pageTarget(page).createCDPSession();
await client.send('Runtime.enable'); await client.send('Runtime.enable');
await client.send('Debugger.enable'); await client.send('Debugger.enable');
// JS coverage enables and then disables Debugger domain. // JS coverage enables and then disables Debugger domain.
@ -59,7 +59,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
expect(event.url).toBe('foo.js'); expect(event.url).toBe('foo.js');
}); });
it('should be able to detach session', async function({page, browser, server}) { it('should be able to detach session', async function({page, browser, server}) {
const client = await browser.pageTarget(page).createCDPSession(); const client = await page.context().pageTarget(page).createCDPSession();
await client.send('Runtime.enable'); await client.send('Runtime.enable');
const evalResponse = await client.send('Runtime.evaluate', {expression: '1 + 2', returnByValue: true}); const evalResponse = await client.send('Runtime.evaluate', {expression: '1 + 2', returnByValue: true});
expect(evalResponse.result.value).toBe(3); expect(evalResponse.result.value).toBe(3);
@ -73,7 +73,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
expect(error.message).toContain('Session closed.'); expect(error.message).toContain('Session closed.');
}); });
it('should throw nice errors', async function({page, browser}) { it('should throw nice errors', async function({page, browser}) {
const client = await browser.pageTarget(page).createCDPSession(); const client = await page.context().pageTarget(page).createCDPSession();
const error = await theSourceOfTheProblems().catch(error => error); const error = await theSourceOfTheProblems().catch(error => error);
expect(error.stack).toContain('theSourceOfTheProblems'); expect(error.stack).toContain('theSourceOfTheProblems');
expect(error.message).toContain('ThisCommand.DoesNotExist'); expect(error.message).toContain('ThisCommand.DoesNotExist');

View file

@ -59,13 +59,14 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI
await page.click('span'); await page.click('span');
expect(await page.evaluate(() => window.CLICKED)).toBe(42); expect(await page.evaluate(() => window.CLICKED)).toBe(42);
}); });
it('should not throw UnhandledPromiseRejection when page closes', async({newContext, server}) => { it('should not throw UnhandledPromiseRejection when page closes', async({browser, server}) => {
const context = await newContext(); const context = await browser.newContext();
const page = await context.newPage(); const page = await context.newPage();
await Promise.all([ await Promise.all([
page.close(), page.close(),
page.mouse.click(1, 2), page.mouse.click(1, 2),
]).catch(e => {}); ]).catch(e => {});
await context.close();
}); });
it('should click the button after navigation ', async({page, server}) => { it('should click the button after navigation ', async({page, server}) => {
await page.goto(server.PREFIX + '/input/button.html'); await page.goto(server.PREFIX + '/input/button.html');
@ -81,14 +82,16 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI
await page.click('button'); await page.click('button');
expect(await page.evaluate(() => result)).toBe('Clicked'); expect(await page.evaluate(() => result)).toBe('Clicked');
}); });
it('should click with disabled javascript', async({newPage, server}) => { it('should click with disabled javascript', async({browser, server}) => {
const page = await newPage({ javaScriptEnabled: false }); const context = await browser.newContext({ javaScriptEnabled: false });
const page = await context.newPage();
await page.goto(server.PREFIX + '/wrappedlink.html'); await page.goto(server.PREFIX + '/wrappedlink.html');
await Promise.all([ await Promise.all([
page.click('a'), page.click('a'),
page.waitForNavigation() page.waitForNavigation()
]); ]);
expect(page.url()).toBe(server.PREFIX + '/wrappedlink.html#clicked'); expect(page.url()).toBe(server.PREFIX + '/wrappedlink.html#clicked');
await context.close();
}); });
it('should click when one of inline box children is outside of viewport', async({page, server}) => { it('should click when one of inline box children is outside of viewport', async({page, server}) => {
await page.setContent(` await page.setContent(`
@ -142,25 +145,18 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI
await page.click('button'); await page.click('button');
expect(await page.evaluate(() => result)).toBe('Clicked'); expect(await page.evaluate(() => result)).toBe('Clicked');
}); });
it('should waitFor hidden when already hidden', async({page, server}) => { it('should not wait with false waitFor', async({page, server}) => {
let error = null; let error = null;
await page.goto(server.PREFIX + '/input/button.html'); await page.goto(server.PREFIX + '/input/button.html');
await page.$eval('button', b => b.style.display = 'none'); await page.$eval('button', b => b.style.display = 'none');
await page.click('button', { waitFor: 'hidden' }).catch(e => error = e); await page.click('button', { waitFor: false }).catch(e => error = e);
expect(error.message).toBe('Node is either not visible or not an HTMLElement'); expect(error.message).toBe('Node is either not visible or not an HTMLElement');
expect(await page.evaluate(() => result)).toBe('Was not clicked'); expect(await page.evaluate(() => result)).toBe('Was not clicked');
}); });
it('should waitFor hidden', async({page, server}) => { it('should throw for non-boolean waitFor', async({page, server}) => {
let error = null;
await page.goto(server.PREFIX + '/input/button.html'); await page.goto(server.PREFIX + '/input/button.html');
const clicked = page.click('button', { waitFor: 'hidden' }).catch(e => error = e); const error = await page.click('button', { waitFor: 'visible' }).catch(e => e);
for (let i = 0; i < 5; i++) expect(error.message).toBe('waitFor option should be a boolean, got "string"');
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}) => { it('should waitFor visible', async({page, server}) => {
let done = false; let done = false;
@ -218,16 +214,17 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI
it('should fail to click a missing button', async({page, server}) => { it('should fail to click a missing button', async({page, server}) => {
await page.goto(server.PREFIX + '/input/button.html'); await page.goto(server.PREFIX + '/input/button.html');
let error = null; let error = null;
await page.click('button.does-not-exist', { waitFor: 'nowait' }).catch(e => error = e); await page.click('button.does-not-exist', { waitFor: false }).catch(e => error = e);
expect(error.message).toBe('No node found for selector: button.does-not-exist'); expect(error.message).toBe('No node found for selector: button.does-not-exist');
}); });
// @see https://github.com/GoogleChrome/puppeteer/issues/161 // @see https://github.com/GoogleChrome/puppeteer/issues/161
it('should not hang with touch-enabled viewports', async({server, newContext}) => { it('should not hang with touch-enabled viewports', async({server, browser}) => {
const context = await newContext({ viewport: playwright.devices['iPhone 6'].viewport }); const context = await browser.newContext({ viewport: playwright.devices['iPhone 6'].viewport });
const page = await context.newPage(); const page = await context.newPage();
await page.mouse.down(); await page.mouse.down();
await page.mouse.move(100, 10); await page.mouse.move(100, 10);
await page.mouse.up(); await page.mouse.up();
await context.close();
}); });
it('should scroll and click the button', async({page, server}) => { it('should scroll and click the button', async({page, server}) => {
await page.goto(server.PREFIX + '/input/scrollable.html'); await page.goto(server.PREFIX + '/input/scrollable.html');
@ -298,8 +295,8 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI
await frame.click('button'); await frame.click('button');
expect(await frame.evaluate(() => window.result)).toBe('Clicked'); expect(await frame.evaluate(() => window.result)).toBe('Clicked');
}); });
it('should click the button with deviceScaleFactor set', async({newContext, server}) => { it('should click the button with deviceScaleFactor set', async({browser, server}) => {
const context = await newContext({ viewport: {width: 400, height: 400, deviceScaleFactor: 5} }); const context = await browser.newContext({ viewport: {width: 400, height: 400, deviceScaleFactor: 5} });
const page = await context.newPage(); const page = await context.newPage();
expect(await page.evaluate(() => window.devicePixelRatio)).toBe(5); expect(await page.evaluate(() => window.devicePixelRatio)).toBe(5);
await page.setContent('<div style="width:100px;height:100px">spacer</div>'); await page.setContent('<div style="width:100px;height:100px">spacer</div>');
@ -308,6 +305,7 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI
const button = await frame.$('button'); const button = await frame.$('button');
await button.click(); await button.click();
expect(await frame.evaluate(() => window.result)).toBe('Clicked'); expect(await frame.evaluate(() => window.result)).toBe('Clicked');
await context.close();
}); });
it('should click the button with px border with relative point', async({page, server}) => { it('should click the button with px border with relative point', async({page, server}) => {
await page.goto(server.PREFIX + '/input/button.html'); await page.goto(server.PREFIX + '/input/button.html');
@ -357,8 +355,9 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI
expect(await page.evaluate(() => offsetX)).toBe(WEBKIT ? 1900 + 8 : 1900); expect(await page.evaluate(() => offsetX)).toBe(WEBKIT ? 1900 + 8 : 1900);
expect(await page.evaluate(() => offsetY)).toBe(WEBKIT ? 1910 + 8 : 1910); expect(await page.evaluate(() => offsetY)).toBe(WEBKIT ? 1910 + 8 : 1910);
}); });
it('should click the button with relative point with page scale', async({newPage, server}) => { it('should click the button with relative point with page scale', async({browser, server}) => {
const page = await newPage({ viewport: { width: 400, height: 400, isMobile: true} }); const context = await browser.newContext({ viewport: { width: 400, height: 400, isMobile: true} });
const page = await context.newPage();
await page.goto(server.PREFIX + '/input/button.html'); await page.goto(server.PREFIX + '/input/button.html');
await page.$eval('button', button => { await page.$eval('button', button => {
button.style.borderWidth = '8px'; button.style.borderWidth = '8px';
@ -376,6 +375,100 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI
} }
expect(await page.evaluate(() => pageX)).toBe(expected.x); expect(await page.evaluate(() => pageX)).toBe(expected.x);
expect(await page.evaluate(() => pageY)).toBe(expected.y); expect(await page.evaluate(() => pageY)).toBe(expected.y);
await context.close();
});
it('should wait for stable position', async({page, server}) => {
await page.goto(server.PREFIX + '/input/button.html');
await page.$eval('button', button => {
button.style.transition = 'margin 500ms linear 0s';
button.style.marginLeft = '200px';
button.style.borderWidth = '0';
button.style.width = '200px';
button.style.height = '20px';
// Set display to "block" - otherwise Firefox layouts with non-even
// values on Linux.
button.style.display = 'block';
document.body.style.margin = '0';
});
await page.click('button');
expect(await page.evaluate(() => window.result)).toBe('Clicked');
expect(await page.evaluate(() => pageX)).toBe(300);
expect(await page.evaluate(() => pageY)).toBe(10);
});
it('should timeout waiting for stable position', async({page, server}) => {
await page.goto(server.PREFIX + '/input/button.html');
const button = await page.$('button');
await button.evaluate(button => {
button.style.transition = 'margin 5s linear 0s';
button.style.marginLeft = '200px';
});
const error = await button.click({ timeout: 100 }).catch(e => e);
expect(error.message).toContain('timeout 100ms exceeded');
});
it('should wait for becoming hit target', async({page, server}) => {
await page.goto(server.PREFIX + '/input/button.html');
await page.$eval('button', button => {
button.style.borderWidth = '0';
button.style.width = '200px';
button.style.height = '20px';
document.body.style.margin = '0';
document.body.style.position = 'relative';
const flyOver = document.createElement('div');
flyOver.className = 'flyover';
flyOver.style.position = 'absolute';
flyOver.style.width = '400px';
flyOver.style.height = '20px';
flyOver.style.left = '-200px';
flyOver.style.top = '0';
flyOver.style.background = 'red';
document.body.appendChild(flyOver);
});
let clicked = false;
const clickPromise = page.click('button').then(() => clicked = true);
expect(clicked).toBe(false);
await page.$eval('.flyover', flyOver => flyOver.style.left = '0');
await page.evaluate(() => new Promise(requestAnimationFrame));
await page.evaluate(() => new Promise(requestAnimationFrame));
expect(clicked).toBe(false);
await page.$eval('.flyover', flyOver => flyOver.style.left = '200px');
await clickPromise;
expect(clicked).toBe(true);
expect(await page.evaluate(() => window.result)).toBe('Clicked');
});
it('should timeout waiting for hit target', async({page, server}) => {
await page.goto(server.PREFIX + '/input/button.html');
const button = await page.$('button');
await page.evaluate(() => {
document.body.style.position = 'relative';
const blocker = document.createElement('div');
blocker.style.position = 'absolute';
blocker.style.width = '400px';
blocker.style.height = '20px';
blocker.style.left = '0';
blocker.style.top = '0';
document.body.appendChild(blocker);
});
const error = await button.click({ timeout: 100 }).catch(e => e);
expect(error.message).toContain('timeout 100ms exceeded');
});
it('should fail when obscured and not waiting for hit target', async({page, server}) => {
await page.goto(server.PREFIX + '/input/button.html');
const button = await page.$('button');
await page.evaluate(() => {
document.body.style.position = 'relative';
const blocker = document.createElement('div');
blocker.style.position = 'absolute';
blocker.style.width = '400px';
blocker.style.height = '20px';
blocker.style.left = '0';
blocker.style.top = '0';
document.body.appendChild(blocker);
});
await button.click({ waitFor: false });
expect(await page.evaluate(() => window.result)).toBe('Was not clicked');
}); });
it('should update modifiers correctly', async({page, server}) => { it('should update modifiers correctly', async({page, server}) => {
@ -403,35 +496,51 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI
await page.click('button'); await page.click('button');
expect(await page.evaluate('window.clicked')).toBe(true); expect(await page.evaluate('window.clicked')).toBe(true);
}); });
xit('should click on an animated button', async({page}) => { xit('should fail to click a button animated via CSS animations and setInterval', async({page}) => {
const buttonSize = 50; // This test has a setInterval that consistently animates a button.
// It checks that we detect the button to be continuously animating, and never try to click it.
// This test exposes two issues:
// - Chromium headless does not issue rafs between first and second animateLeft() calls.
// - Chromium and WebKit keep element bounds the same when for 2 frames when changing left to a new value.
const buttonSize = 10;
const containerWidth = 500; const containerWidth = 500;
const transition = 500; const transition = 100;
await page.setContent(` await page.setContent(`
<html> <html>
<body> <body>
<div style="border: 1px solid black; height: 50px; overflow: auto; width: ${containerWidth}px;"> <div style="border: 1px solid black; height: 50px; overflow: auto; width: ${containerWidth}px;">
<button id="button" style="height: ${buttonSize}px; width: ${buttonSize}px; transition: left ${transition}ms linear 0s; left: 0; position: relative" onClick="window.clicked++">hi</button> <button style="border: none; height: ${buttonSize}px; width: ${buttonSize}px; transition: left ${transition}ms linear 0s; left: 0; position: relative" onClick="window.clicked++"></button>
</div> </div>
</body> </body>
<script> <script>
const animateLeft = () => { window.atLeft = true;
const button = document.querySelector('#button'); const animateLeft = () => {
document.querySelector('#button').style.left = button.style.left === '0px' ? '${containerWidth - buttonSize}px' : '0px'; const button = document.querySelector('button');
}; button.style.left = window.atLeft ? '${containerWidth - buttonSize}px' : '0px';
window.clicked = 0; console.log('set ' + button.style.left);
window.setTimeout(animateLeft, 0); window.atLeft = !window.atLeft;
window.setInterval(animateLeft, ${transition}); };
</script> window.clicked = 0;
</html> const dump = () => {
const button = document.querySelector('button');
const clientRect = button.getBoundingClientRect();
const rect = { x: clientRect.top, y: clientRect.left, width: clientRect.width, height: clientRect.height };
requestAnimationFrame(dump);
};
dump();
</script>
</html>
`); `);
await page.click('button'); await page.evaluate(transition => {
expect(await page.evaluate('window.clicked')).toBe(1); window.setInterval(animateLeft, transition);
expect(await page.evaluate('document.querySelector("#button").style.left')).toBe(`${containerWidth - buttonSize}px`); animateLeft();
await new Promise(resolve => setTimeout(resolve, 500)); }, transition);
await page.click('button'); const error1 = await page.click('button', { timeout: 250 }).catch(e => e);
expect(await page.evaluate('window.clicked')).toBe(2); expect(await page.evaluate('window.clicked')).toBe(0);
expect(await page.evaluate('document.querySelector("#button").style.left')).toBe('0px'); expect(error1.message).toContain('timeout 250ms exceeded');
const error2 = await page.click('button', { timeout: 250 }).catch(e => e);
expect(await page.evaluate('window.clicked')).toBe(0);
expect(error2.message).toContain('timeout 250ms exceeded');
}); });
}); });

View file

@ -168,8 +168,8 @@ module.exports.describe = function({testRunner, expect, playwright, defaultBrows
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
expect(cookie).toBe('cookie=value'); expect(cookie).toBe('cookie=value');
}); });
it('should isolate cookies in browser contexts', async({context, server, newContext}) => { it('should isolate cookies in browser contexts', async({context, server, browser}) => {
const anotherContext = await newContext(); const anotherContext = await browser.newContext();
await context.setCookies([{url: server.EMPTY_PAGE, name: 'isolatecookie', value: 'page1value'}]); await context.setCookies([{url: server.EMPTY_PAGE, name: 'isolatecookie', value: 'page1value'}]);
await anotherContext.setCookies([{url: server.EMPTY_PAGE, name: 'isolatecookie', value: 'page2value'}]); await anotherContext.setCookies([{url: server.EMPTY_PAGE, name: 'isolatecookie', value: 'page2value'}]);
@ -181,8 +181,9 @@ module.exports.describe = function({testRunner, expect, playwright, defaultBrows
expect(cookies1[0].value).toBe('page1value'); expect(cookies1[0].value).toBe('page1value');
expect(cookies2[0].name).toBe('isolatecookie'); expect(cookies2[0].name).toBe('isolatecookie');
expect(cookies2[0].value).toBe('page2value'); expect(cookies2[0].value).toBe('page2value');
await anotherContext.close();
}); });
it('should isolate session cookies', async({context, server, newContext}) => { it('should isolate session cookies', async({context, server, browser}) => {
server.setRoute('/setcookie.html', (req, res) => { server.setRoute('/setcookie.html', (req, res) => {
res.setHeader('Set-Cookie', 'session=value'); res.setHeader('Set-Cookie', 'session=value');
res.end(); res.end();
@ -199,14 +200,15 @@ module.exports.describe = function({testRunner, expect, playwright, defaultBrows
expect(cookies.map(c => c.value).join(',')).toBe('value'); expect(cookies.map(c => c.value).join(',')).toBe('value');
} }
{ {
const context2 = await newContext(); const context2 = await browser.newContext();
const page = await context2.newPage(); const page = await context2.newPage();
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
const cookies = await context2.cookies(); const cookies = await context2.cookies();
expect(cookies[0] && cookies[0].name).toBe(undefined); expect(cookies[0] && cookies[0].name).toBe(undefined);
await context2.close();
} }
}); });
it.skip(WEBKIT)('should isolate persistent cookies', async({context, server, newContext}) => { it.skip(WEBKIT)('should isolate persistent cookies', async({context, server, browser}) => {
server.setRoute('/setcookie.html', (req, res) => { server.setRoute('/setcookie.html', (req, res) => {
res.setHeader('Set-Cookie', 'persistent=persistent-value; max-age=3600'); res.setHeader('Set-Cookie', 'persistent=persistent-value; max-age=3600');
res.end(); res.end();
@ -215,7 +217,7 @@ module.exports.describe = function({testRunner, expect, playwright, defaultBrows
await page.goto(server.PREFIX + '/setcookie.html'); await page.goto(server.PREFIX + '/setcookie.html');
const context1 = context; const context1 = context;
const context2 = await newContext(); const context2 = await browser.newContext();
const [page1, page2] = await Promise.all([context1.newPage(), context2.newPage()]); const [page1, page2] = await Promise.all([context1.newPage(), context2.newPage()]);
await Promise.all([page1.goto(server.EMPTY_PAGE), page2.goto(server.EMPTY_PAGE)]); await Promise.all([page1.goto(server.EMPTY_PAGE), page2.goto(server.EMPTY_PAGE)]);
const [cookies1, cookies2] = await Promise.all([context1.cookies(), context2.cookies()]); const [cookies1, cookies2] = await Promise.all([context1.cookies(), context2.cookies()]);
@ -223,8 +225,9 @@ module.exports.describe = function({testRunner, expect, playwright, defaultBrows
expect(cookies1[0].name).toBe('persistent'); expect(cookies1[0].name).toBe('persistent');
expect(cookies1[0].value).toBe('persistent-value'); expect(cookies1[0].value).toBe('persistent-value');
expect(cookies2.length).toBe(0); expect(cookies2.length).toBe(0);
await context2.close();
}); });
it('should isolate send cookie header', async({server, context, newContext}) => { it('should isolate send cookie header', async({server, context, browser}) => {
let cookie = []; let cookie = [];
server.setRoute('/empty.html', (req, res) => { server.setRoute('/empty.html', (req, res) => {
cookie = req.headers.cookie || ''; cookie = req.headers.cookie || '';
@ -237,9 +240,11 @@ module.exports.describe = function({testRunner, expect, playwright, defaultBrows
expect(cookie).toBe('sendcookie=value'); expect(cookie).toBe('sendcookie=value');
} }
{ {
const page = await (await newContext()).newPage(); const context = await browser.newContext();
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
expect(cookie).toBe(''); expect(cookie).toBe('');
await context.close();
} }
}); });
it('should isolate cookies between launches', async({server}) => { it('should isolate cookies between launches', async({server}) => {
@ -449,8 +454,8 @@ module.exports.describe = function({testRunner, expect, playwright, defaultBrows
await context.clearCookies(); await context.clearCookies();
expect(await page.evaluate('document.cookie')).toBe(''); expect(await page.evaluate('document.cookie')).toBe('');
}); });
it('should isolate cookies when clearing', async({context, server, newContext}) => { it('should isolate cookies when clearing', async({context, server, browser}) => {
const anotherContext = await newContext(); const anotherContext = await browser.newContext();
await context.setCookies([{url: server.EMPTY_PAGE, name: 'page1cookie', value: 'page1value'}]); await context.setCookies([{url: server.EMPTY_PAGE, name: 'page1cookie', value: 'page1value'}]);
await anotherContext.setCookies([{url: server.EMPTY_PAGE, name: 'page2cookie', value: 'page2value'}]); await anotherContext.setCookies([{url: server.EMPTY_PAGE, name: 'page2cookie', value: 'page2value'}]);
@ -464,6 +469,7 @@ module.exports.describe = function({testRunner, expect, playwright, defaultBrows
await anotherContext.clearCookies(); await anotherContext.clearCookies();
expect((await context.cookies()).length).toBe(0); expect((await context.cookies()).length).toBe(0);
expect((await anotherContext.cookies()).length).toBe(0); expect((await anotherContext.cookies()).length).toBe(0);
await anotherContext.close();
}); });
}); });
}; };

View file

@ -68,8 +68,9 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
}, element); }, element);
expect(pwBoundingBox).toEqual(webBoundingBox); expect(pwBoundingBox).toEqual(webBoundingBox);
}); });
it('should work with page scale', async({newPage, server}) => { it('should work with page scale', async({browser, server}) => {
const page = await newPage({ viewport: { width: 400, height: 400, isMobile: true} }); const context = await browser.newContext({ viewport: { width: 400, height: 400, isMobile: true} });
const page = await context.newPage();
await page.goto(server.PREFIX + '/input/button.html'); await page.goto(server.PREFIX + '/input/button.html');
const button = await page.$('button'); const button = await page.$('button');
await button.evaluate(button => { await button.evaluate(button => {
@ -85,6 +86,35 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
expect(Math.round(box.y * 100)).toBe(23 * 100); expect(Math.round(box.y * 100)).toBe(23 * 100);
expect(Math.round(box.width * 100)).toBe(200 * 100); expect(Math.round(box.width * 100)).toBe(200 * 100);
expect(Math.round(box.height * 100)).toBe(20 * 100); expect(Math.round(box.height * 100)).toBe(20 * 100);
await context.close();
});
it('should work when inline box child is outside of viewport', async({page, server}) => {
await page.setContent(`
<style>
i {
position: absolute;
top: -1000px;
}
body {
margin: 0;
font-size: 12px;
}
</style>
<span><i>woof</i><b>doggo</b></span>
`);
const handle = await page.$('span');
const box = await handle.boundingBox();
const webBoundingBox = await handle.evaluate(e => {
const rect = e.getBoundingClientRect();
return {x: rect.x, y: rect.y, width: rect.width, height: rect.height};
});
const round = box => ({
x: Math.round(box.x * 100),
y: Math.round(box.y * 100),
width: Math.round(box.width * 100),
height: Math.round(box.height * 100),
});
expect(round(box)).toEqual(round(webBoundingBox));
}); });
}); });
@ -228,7 +258,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
await page.evaluate(button => button.remove(), button); await page.evaluate(button => button.remove(), button);
let error = null; let error = null;
await button.click().catch(err => error = err); await button.click().catch(err => error = err);
expect(error.message).toContain('Node is detached from document'); expect(error.message).toContain('Element is not attached to the DOM');
}); });
it('should throw for hidden nodes', async({page, server}) => { it('should throw for hidden nodes', async({page, server}) => {
await page.goto(server.PREFIX + '/input/button.html'); await page.goto(server.PREFIX + '/input/button.html');
@ -268,36 +298,20 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
}); });
}); });
describe('ElementHandle.visibleRatio', function() {
it('should work', async({page, server}) => {
await page.goto(server.PREFIX + '/offscreenbuttons.html');
for (let i = 0; i < 11; ++i) {
const button = await page.$('#btn' + i);
const ratio = await button.visibleRatio();
expect(Math.round(ratio * 10)).toBe(10 - i);
}
});
it('should work when Node is removed', async({page, server}) => {
await page.goto(server.PREFIX + '/offscreenbuttons.html');
await page.evaluate(() => delete window['Node']);
for (let i = 0; i < 11; ++i) {
const button = await page.$('#btn' + i);
const ratio = await button.visibleRatio();
expect(Math.round(ratio * 10)).toBe(10 - i);
}
});
});
describe('ElementHandle.scrollIntoViewIfNeeded', function() { describe('ElementHandle.scrollIntoViewIfNeeded', function() {
it.skip(FFOX)('should work', async({page, server}) => { it.skip(FFOX)('should work', async({page, server}) => {
await page.goto(server.PREFIX + '/offscreenbuttons.html'); await page.goto(server.PREFIX + '/offscreenbuttons.html');
for (let i = 0; i < 11; ++i) { for (let i = 0; i < 11; ++i) {
const button = await page.$('#btn' + i); const button = await page.$('#btn' + i);
const before = await button.visibleRatio(); const before = await button.evaluate(button => {
expect(Math.round(before * 10)).toBe(10 - i); return button.getBoundingClientRect().right - window.innerWidth;
});
expect(before).toBe(10 * i);
await button.scrollIntoViewIfNeeded(); await button.scrollIntoViewIfNeeded();
const after = await button.visibleRatio(); const after = await button.evaluate(button => {
expect(Math.round(after * 10)).toBe(10); return button.getBoundingClientRect().right - window.innerWidth;
});
expect(after <= 0).toBe(true);
await page.evaluate(() => window.scrollTo(0, 0)); await page.evaluate(() => window.scrollTo(0, 0));
} }
}); });

View file

@ -25,19 +25,27 @@ module.exports.describe = function({testRunner, expect, playwright, headless, FF
const iPhone = playwright.devices['iPhone 6']; const iPhone = playwright.devices['iPhone 6'];
const iPhoneLandscape = playwright.devices['iPhone 6 landscape']; const iPhoneLandscape = playwright.devices['iPhone 6 landscape'];
describe('Page.viewport', function() { describe('BrowserContext({viewport})', function() {
it('should get the proper viewport size', async({page, server}) => { it('should get the proper viewport size', async({page, server}) => {
expect(page.viewportSize()).toEqual({width: 800, height: 600}); expect(page.viewportSize()).toEqual({width: 1280, height: 720});
expect(await page.evaluate(() => window.innerWidth)).toBe(1280);
expect(await page.evaluate(() => window.innerHeight)).toBe(720);
});
it('should set the proper viewport size', async({page, server}) => {
expect(page.viewportSize()).toEqual({width: 1280, height: 720});
await page.setViewportSize({width: 123, height: 456}); await page.setViewportSize({width: 123, height: 456});
expect(page.viewportSize()).toEqual({width: 123, height: 456}); expect(page.viewportSize()).toEqual({width: 123, height: 456});
expect(await page.evaluate(() => window.innerWidth)).toBe(123);
expect(await page.evaluate(() => window.innerHeight)).toBe(456);
}); });
it('should support mobile emulation', async({newContext, server}) => { it('should support mobile emulation', async({browser, server}) => {
const context = await newContext({ viewport: iPhone.viewport }); const context = await browser.newContext({ viewport: iPhone.viewport });
const page = await context.newPage(); const page = await context.newPage();
await page.goto(server.PREFIX + '/mobile.html'); await page.goto(server.PREFIX + '/mobile.html');
expect(await page.evaluate(() => window.innerWidth)).toBe(375); expect(await page.evaluate(() => window.innerWidth)).toBe(375);
await page.setViewportSize({width: 400, height: 300}); await page.setViewportSize({width: 400, height: 300});
expect(await page.evaluate(() => window.innerWidth)).toBe(400); expect(await page.evaluate(() => window.innerWidth)).toBe(400);
await context.close();
}); });
it('should not have touch by default', async({page, server}) => { it('should not have touch by default', async({page, server}) => {
await page.goto(server.PREFIX + '/mobile.html'); await page.goto(server.PREFIX + '/mobile.html');
@ -45,12 +53,13 @@ module.exports.describe = function({testRunner, expect, playwright, headless, FF
await page.goto(server.PREFIX + '/detect-touch.html'); await page.goto(server.PREFIX + '/detect-touch.html');
expect(await page.evaluate(() => document.body.textContent.trim())).toBe('NO'); expect(await page.evaluate(() => document.body.textContent.trim())).toBe('NO');
}); });
it('should support touch emulation', async({newContext, server}) => { it('should support touch emulation', async({browser, server}) => {
const context = await newContext({ viewport: iPhone.viewport }); const context = await browser.newContext({ viewport: iPhone.viewport });
const page = await context.newPage(); const page = await context.newPage();
await page.goto(server.PREFIX + '/mobile.html'); await page.goto(server.PREFIX + '/mobile.html');
expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(true); expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(true);
expect(await page.evaluate(dispatchTouch)).toBe('Received touch'); expect(await page.evaluate(dispatchTouch)).toBe('Received touch');
await context.close();
function dispatchTouch() { function dispatchTouch() {
let fulfill; let fulfill;
@ -65,30 +74,34 @@ module.exports.describe = function({testRunner, expect, playwright, headless, FF
return promise; return promise;
} }
}); });
it('should be detectable by Modernizr', async({newContext, server}) => { it('should be detectable by Modernizr', async({browser, server}) => {
const context = await newContext({ viewport: iPhone.viewport }); const context = await browser.newContext({ viewport: iPhone.viewport });
const page = await context.newPage(); const page = await context.newPage();
await page.goto(server.PREFIX + '/detect-touch.html'); await page.goto(server.PREFIX + '/detect-touch.html');
expect(await page.evaluate(() => document.body.textContent.trim())).toBe('YES'); expect(await page.evaluate(() => document.body.textContent.trim())).toBe('YES');
await context.close();
}); });
it('should detect touch when applying viewport with touches', async({newContext, server}) => { it('should detect touch when applying viewport with touches', async({browser, server}) => {
const context = await newContext({ viewport: { width: 800, height: 600, isMobile: true } }); const context = await browser.newContext({ viewport: { width: 800, height: 600, isMobile: true } });
const page = await context.newPage(); const page = await context.newPage();
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
await page.addScriptTag({url: server.PREFIX + '/modernizr.js'}); await page.addScriptTag({url: server.PREFIX + '/modernizr.js'});
expect(await page.evaluate(() => Modernizr.touchevents)).toBe(true); expect(await page.evaluate(() => Modernizr.touchevents)).toBe(true);
await context.close();
}); });
it.skip(FFOX)('should support landscape emulation', async({newContext, server}) => { it.skip(FFOX)('should support landscape emulation', async({browser, server}) => {
const context1 = await newContext({ viewport: iPhone.viewport }); const context1 = await browser.newContext({ viewport: iPhone.viewport });
const page1 = await context1.newPage(); const page1 = await context1.newPage();
await page1.goto(server.PREFIX + '/mobile.html'); await page1.goto(server.PREFIX + '/mobile.html');
expect(await page1.evaluate(() => matchMedia('(orientation: landscape)').matches)).toBe(false); expect(await page1.evaluate(() => matchMedia('(orientation: landscape)').matches)).toBe(false);
const context2 = await newContext({ viewport: iPhoneLandscape.viewport }); const context2 = await browser.newContext({ viewport: iPhoneLandscape.viewport });
const page2 = await context2.newPage(); const page2 = await context2.newPage();
expect(await page2.evaluate(() => matchMedia('(orientation: landscape)').matches)).toBe(true); expect(await page2.evaluate(() => matchMedia('(orientation: landscape)').matches)).toBe(true);
await context1.close();
await context2.close();
}); });
it.skip(FFOX || WEBKIT)('should fire orientationchange event', async({newContext, server}) => { it.skip(FFOX || WEBKIT)('should fire orientationchange event', async({browser, server}) => {
const context = await newContext({ viewport: { width: 300, height: 400, isMobile: true } }); const context = await browser.newContext({ viewport: { width: 300, height: 400, isMobile: true } });
const page = await context.newPage(); const page = await context.newPage();
await page.goto(server.PREFIX + '/mobile.html'); await page.goto(server.PREFIX + '/mobile.html');
await page.evaluate(() => { await page.evaluate(() => {
@ -103,35 +116,42 @@ module.exports.describe = function({testRunner, expect, playwright, headless, FF
const event2 = page.waitForEvent('console'); const event2 = page.waitForEvent('console');
await page.setViewportSize({width: 300, height: 400}); await page.setViewportSize({width: 300, height: 400});
expect((await event2).text()).toBe('2'); expect((await event2).text()).toBe('2');
await context.close();
}); });
it.skip(FFOX)('default mobile viewports to 980 width', async({newContext, server}) => { it.skip(FFOX)('default mobile viewports to 980 width', async({browser, server}) => {
const context = await newContext({ viewport: {width: 320, height: 480, isMobile: true} }); const context = await browser.newContext({ viewport: {width: 320, height: 480, isMobile: true} });
const page = await context.newPage(); const page = await context.newPage();
await page.goto(server.PREFIX + '/empty.html'); await page.goto(server.PREFIX + '/empty.html');
expect(await page.evaluate(() => window.innerWidth)).toBe(980); expect(await page.evaluate(() => window.innerWidth)).toBe(980);
await context.close();
}); });
it.skip(FFOX)('respect meta viewport tag', async({newContext, server}) => { it.skip(FFOX)('respect meta viewport tag', async({browser, server}) => {
const context = await newContext({ viewport: {width: 320, height: 480, isMobile: true} }); const context = await browser.newContext({ viewport: {width: 320, height: 480, isMobile: true} });
const page = await context.newPage(); const page = await context.newPage();
await page.goto(server.PREFIX + '/mobile.html'); await page.goto(server.PREFIX + '/mobile.html');
expect(await page.evaluate(() => window.innerWidth)).toBe(320); expect(await page.evaluate(() => window.innerWidth)).toBe(320);
await context.close();
}); });
}); });
describe('Page.emulate', function() { describe('Page.emulate', function() {
it('should work', async({newPage, server}) => { it('should work', async({browser, server}) => {
const page = await newPage({ viewport: iPhone.viewport, userAgent: iPhone.userAgent }); const context = await browser.newContext({viewport: iPhone.viewport, userAgent: iPhone.userAgent});
const page = await context.newPage();
await page.goto(server.PREFIX + '/mobile.html'); await page.goto(server.PREFIX + '/mobile.html');
expect(await page.evaluate(() => window.innerWidth)).toBe(375); expect(await page.evaluate(() => window.innerWidth)).toBe(375);
expect(await page.evaluate(() => navigator.userAgent)).toContain('iPhone'); expect(await page.evaluate(() => navigator.userAgent)).toContain('iPhone');
await context.close();
}); });
it('should support clicking', async({newPage, server}) => { it('should support clicking', async({browser, server}) => {
const page = await newPage({ viewport: iPhone.viewport, userAgent: iPhone.userAgent }); const context = await browser.newContext({ viewport: iPhone.viewport, userAgent: iPhone.userAgent });
const page = await context.newPage();
await page.goto(server.PREFIX + '/input/button.html'); await page.goto(server.PREFIX + '/input/button.html');
const button = await page.$('button'); const button = await page.$('button');
await page.evaluate(button => button.style.marginTop = '200px', button); await page.evaluate(button => button.style.marginTop = '200px', button);
await button.click(); await button.click();
expect(await page.evaluate(() => result)).toBe('Clicked'); expect(await page.evaluate(() => result)).toBe('Clicked');
await context.close();
}); });
}); });
@ -194,72 +214,94 @@ module.exports.describe = function({testRunner, expect, playwright, headless, FF
}); });
describe.skip(FFOX)('BrowserContext({timezoneId})', function() { describe.skip(FFOX)('BrowserContext({timezoneId})', function() {
it('should work', async ({ newPage }) => { it('should work', async ({ browser }) => {
const func = () => new Date(1479579154987).toString(); const func = () => new Date(1479579154987).toString();
{ {
const page = await newPage({ timezoneId: 'America/Jamaica' }); const context = await browser.newContext({ timezoneId: 'America/Jamaica' });
const page = await context.newPage();
expect(await page.evaluate(func)).toBe('Sat Nov 19 2016 13:12:34 GMT-0500 (Eastern Standard Time)'); expect(await page.evaluate(func)).toBe('Sat Nov 19 2016 13:12:34 GMT-0500 (Eastern Standard Time)');
await context.close();
} }
{ {
const page = await newPage({ timezoneId: 'Pacific/Honolulu' }); const context = await browser.newContext({ timezoneId: 'Pacific/Honolulu' });
const page = await context.newPage();
expect(await page.evaluate(func)).toBe('Sat Nov 19 2016 08:12:34 GMT-1000 (Hawaii-Aleutian Standard Time)'); expect(await page.evaluate(func)).toBe('Sat Nov 19 2016 08:12:34 GMT-1000 (Hawaii-Aleutian Standard Time)');
await context.close();
} }
{ {
const page = await newPage({ timezoneId: 'America/Buenos_Aires' }); const context = await browser.newContext({ timezoneId: 'America/Buenos_Aires' });
const page = await context.newPage();
expect(await page.evaluate(func)).toBe('Sat Nov 19 2016 15:12:34 GMT-0300 (Argentina Standard Time)'); expect(await page.evaluate(func)).toBe('Sat Nov 19 2016 15:12:34 GMT-0300 (Argentina Standard Time)');
await context.close();
} }
{ {
const page = await newPage({ timezoneId: 'Europe/Berlin' }); const context = await browser.newContext({ timezoneId: 'Europe/Berlin' });
const page = await context.newPage();
expect(await page.evaluate(func)).toBe('Sat Nov 19 2016 19:12:34 GMT+0100 (Central European Standard Time)'); expect(await page.evaluate(func)).toBe('Sat Nov 19 2016 19:12:34 GMT+0100 (Central European Standard Time)');
await context.close();
} }
}); });
it('should throw for invalid timezone IDs', async({newPage}) => { it('should throw for invalid timezone IDs when creating pages', async({browser}) => {
let error = null; for (const timezoneId of ['Foo/Bar', 'Baz/Qux']) {
await newPage({ timezoneId: 'Foo/Bar' }).catch(e => error = e); let error = null;
expect(error.message).toBe('Invalid timezone ID: Foo/Bar'); const context = await browser.newContext({ timezoneId });
await newPage({ timezoneId: 'Baz/Qux' }).catch(e => error = e); const page = await context.newPage().catch(e => error = e);
expect(error.message).toBe('Invalid timezone ID: Baz/Qux'); expect(error.message).toBe(`Invalid timezone ID: ${timezoneId}`);
await context.close();
}
}); });
}); });
describe.skip(CHROMIUM || FFOX)('BrowserContext({locale})', function() { describe.skip(CHROMIUM || FFOX)('BrowserContext({locale})', function() {
it('should affect accept-language header', async({newPage, server}) => { it('should affect accept-language header', async({browser, server}) => {
const page = await newPage({ locale: 'fr-CH' }); const context = await browser.newContext({ locale: 'fr-CH' });
const page = await context.newPage();
const [request] = await Promise.all([ const [request] = await Promise.all([
server.waitForRequest('/empty.html'), server.waitForRequest('/empty.html'),
page.goto(server.EMPTY_PAGE), page.goto(server.EMPTY_PAGE),
]); ]);
expect(request.headers['accept-language'].substr(0, 5)).toBe('fr-CH'); expect(request.headers['accept-language'].substr(0, 5)).toBe('fr-CH');
await context.close();
}); });
it('should affect navigator.language', async({newPage, server}) => { it('should affect navigator.language', async({browser, server}) => {
const page = await newPage({ locale: 'fr-CH' }); const context = await browser.newContext({ locale: 'fr-CH' });
const page = await context.newPage();
expect(await page.evaluate(() => navigator.language)).toBe('fr-CH'); expect(await page.evaluate(() => navigator.language)).toBe('fr-CH');
await context.close();
}); });
it('should format number', async({newPage, server}) => { it('should format number', async({browser, server}) => {
{ {
const page = await newPage({ locale: 'en-US' }); const context = await browser.newContext({ locale: 'en-US' });
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
expect(await page.evaluate(() => (1000000.50).toLocaleString())).toBe('1,000,000.5'); expect(await page.evaluate(() => (1000000.50).toLocaleString())).toBe('1,000,000.5');
await context.close();
} }
{ {
const page = await newPage({ locale: 'fr-CH' }); const context = await browser.newContext({ locale: 'fr-CH' });
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
expect(await page.evaluate(() => (1000000.50).toLocaleString().replace(/\s/g, ' '))).toBe('1 000 000,5'); expect(await page.evaluate(() => (1000000.50).toLocaleString().replace(/\s/g, ' '))).toBe('1 000 000,5');
await context.close();
} }
}); });
it('should format date', async({newPage, server}) => { it('should format date', async({browser, server}) => {
{ {
const page = await newPage({ locale: 'en-US', timezoneId: 'America/Los_Angeles' }); const context = await browser.newContext({ locale: 'en-US', timezoneId: 'America/Los_Angeles' });
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
const formatted = 'Sat Nov 19 2016 10:12:34 GMT-0800 (Pacific Standard Time)'; const formatted = 'Sat Nov 19 2016 10:12:34 GMT-0800 (Pacific Standard Time)';
expect(await page.evaluate(() => new Date(1479579154987).toString())).toBe(formatted); expect(await page.evaluate(() => new Date(1479579154987).toString())).toBe(formatted);
await context.close();
} }
{ {
const page = await newPage({ locale: 'de-DE', timezoneId: 'Europe/Berlin' }); const context = await browser.newContext({ locale: 'de-DE', timezoneId: 'Europe/Berlin' });
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
expect(await page.evaluate(() => new Date(1479579154987).toString())).toBe( expect(await page.evaluate(() => new Date(1479579154987).toString())).toBe(
'Sat Nov 19 2016 19:12:34 GMT+0100 (Mitteleuropäische Normalzeit)'); 'Sat Nov 19 2016 19:12:34 GMT+0100 (Mitteleuropäische Normalzeit)');
await context.close();
} }
}); });
}); });

View file

@ -78,9 +78,9 @@ module.exports.describe = function({testRunner, expect, WEBKIT}) {
await context.clearPermissions(); await context.clearPermissions();
expect(await page.evaluate(() => window['events'])).toEqual(['prompt', 'denied', 'granted', 'prompt']); expect(await page.evaluate(() => window['events'])).toEqual(['prompt', 'denied', 'granted', 'prompt']);
}); });
it('should isolate permissions between browser contexs', async({page, server, context, newContext}) => { it('should isolate permissions between browser contexs', async({page, server, context, browser}) => {
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
const otherContext = await newContext(); const otherContext = await browser.newContext();
const otherPage = await otherContext.newPage(); const otherPage = await otherContext.newPage();
await otherPage.goto(server.EMPTY_PAGE); await otherPage.goto(server.EMPTY_PAGE);
expect(await getPermission(page, 'geolocation')).toBe('prompt'); expect(await getPermission(page, 'geolocation')).toBe('prompt');
@ -94,6 +94,7 @@ module.exports.describe = function({testRunner, expect, WEBKIT}) {
await context.clearPermissions(); await context.clearPermissions();
expect(await getPermission(page, 'geolocation')).toBe('prompt'); expect(await getPermission(page, 'geolocation')).toBe('prompt');
expect(await getPermission(otherPage, 'geolocation')).toBe('granted'); expect(await getPermission(otherPage, 'geolocation')).toBe('granted');
await otherContext.close();
}); });
}); });
}; };

View file

@ -54,27 +54,28 @@ module.exports.describe = function ({ testRunner, expect, FFOX, WEBKIT }) {
} }
expect(error.message).toContain('Invalid latitude "undefined"'); expect(error.message).toContain('Invalid latitude "undefined"');
}); });
it('should not modify passed default options object', async({newContext}) => { it('should not modify passed default options object', async({browser}) => {
const geolocation = { longitude: 10, latitude: 10 }; const geolocation = { longitude: 10, latitude: 10 };
const options = { geolocation }; const options = { geolocation };
const context = await newContext(options); const context = await browser.newContext(options);
await context.setGeolocation({ longitude: 20, latitude: 20 }); await context.setGeolocation({ longitude: 20, latitude: 20 });
expect(options.geolocation).toBe(geolocation); expect(options.geolocation).toBe(geolocation);
await context.close();
}); });
it('should throw with missing longitude in default options', async({newContext}) => { it('should throw with missing longitude in default options', async({browser}) => {
let error = null; let error = null;
try { try {
const context = await newContext({ geolocation: {latitude: 10} }); const context = await browser.newContext({ geolocation: {latitude: 10} });
await context.close(); await context.close();
} catch (e) { } catch (e) {
error = e; error = e;
} }
expect(error.message).toContain('Invalid longitude "undefined"'); expect(error.message).toContain('Invalid longitude "undefined"');
}); });
it('should use context options', async({newContext, server}) => { it('should use context options', async({browser, server}) => {
const options = { geolocation: { longitude: 10, latitude: 10 }, permissions: {} }; const options = { geolocation: { longitude: 10, latitude: 10 }, permissions: {} };
options.permissions[server.PREFIX] = ['geolocation']; options.permissions[server.PREFIX] = ['geolocation'];
const context = await newContext(options); const context = await browser.newContext(options);
const page = await context.newPage(); const page = await context.newPage();
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
@ -85,6 +86,7 @@ module.exports.describe = function ({ testRunner, expect, FFOX, WEBKIT }) {
latitude: 10, latitude: 10,
longitude: 10 longitude: 10
}); });
await context.close();
}); });
}); });
}; };

View file

@ -20,6 +20,7 @@ const Diff = require('text-diff');
const PNG = require('pngjs').PNG; const PNG = require('pngjs').PNG;
const jpeg = require('jpeg-js'); const jpeg = require('jpeg-js');
const pixelmatch = require('pixelmatch'); const pixelmatch = require('pixelmatch');
const c = require('colors/safe');
module.exports = {compare}; module.exports = {compare};
@ -51,7 +52,7 @@ function compareImages(actualBuffer, expectedBuffer, mimeType) {
const expected = mimeType === 'image/png' ? PNG.sync.read(expectedBuffer) : jpeg.decode(expectedBuffer); const expected = mimeType === 'image/png' ? PNG.sync.read(expectedBuffer) : jpeg.decode(expectedBuffer);
if (expected.width !== actual.width || expected.height !== actual.height) { if (expected.width !== actual.width || expected.height !== actual.height) {
return { return {
errorMessage: `Sizes differ: expected image ${expected.width}px X ${expected.height}px, but got ${actual.width}px X ${actual.height}px. ` errorMessage: `Sizes differ; expected image ${expected.width}px X ${expected.height}px, but got ${actual.width}px X ${actual.height}px. `
}; };
} }
const diff = new PNG({width: expected.width, height: expected.height}); const diff = new PNG({width: expected.width, height: expected.height});
@ -110,23 +111,34 @@ function compare(goldenPath, outputPath, actual, goldenName) {
if (!comparator) { if (!comparator) {
return { return {
pass: false, pass: false,
message: 'Failed to find comparator with type ' + mimeType + ': ' + goldenName message: 'Failed to find comparator with type ' + mimeType + ': ' + goldenName,
}; };
} }
const result = comparator(actual, expected, mimeType); const result = comparator(actual, expected, mimeType);
if (!result) if (!result)
return { pass: true }; return { pass: true };
ensureOutputDir(); ensureOutputDir();
const output = [
c.red(`GOLDEN FAILED: `) + c.yellow('"' + goldenName + '"'),
];
if (result.errorMessage)
output.push(' ' + result.errorMessage);
output.push('');
output.push(`Expected: ${c.yellow(expectedPath)}`);
if (goldenPath === outputPath) { if (goldenPath === outputPath) {
fs.writeFileSync(addSuffix(actualPath, '-actual'), actual); const filepath = addSuffix(actualPath, '-actual');
fs.writeFileSync(filepath, actual);
output.push(`Received: ${c.yellow(filepath)}`);
} else { } else {
fs.writeFileSync(actualPath, actual); fs.writeFileSync(actualPath, actual);
// Copy expected to the output/ folder for convenience. // Copy expected to the output/ folder for convenience.
fs.writeFileSync(addSuffix(actualPath, '-expected'), expected); fs.writeFileSync(addSuffix(actualPath, '-expected'), expected);
output.push(`Received: ${c.yellow(actualPath)}`);
} }
if (result.diff) { if (result.diff) {
const diffPath = addSuffix(actualPath, '-diff', result.diffExtension); const diffPath = addSuffix(actualPath, '-diff', result.diffExtension);
fs.writeFileSync(diffPath, result.diff); fs.writeFileSync(diffPath, result.diff);
output.push(` Diff: ${c.yellow(diffPath)}`);
} }
let message = goldenName + ' mismatch!'; let message = goldenName + ' mismatch!';
@ -134,7 +146,8 @@ function compare(goldenPath, outputPath, actual, goldenName) {
message += ' ' + result.errorMessage; message += ' ' + result.errorMessage;
return { return {
pass: false, pass: false,
message: message + ' ' + messageSuffix message: message + ' ' + messageSuffix,
formatter: () => output.join('\n'),
}; };
function ensureOutputDir() { function ensureOutputDir() {

View file

@ -23,24 +23,28 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
const {it, fit, xit, dit} = testRunner; const {it, fit, xit, dit} = testRunner;
const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;
describe('ignoreHTTPSErrors', function() { describe('ignoreHTTPSErrors', function() {
it('should work', async({newPage, httpsServer}) => { it('should work', async({browser, httpsServer}) => {
let error = null; let error = null;
const page = await newPage({ ignoreHTTPSErrors: true }); const context = await browser.newContext({ ignoreHTTPSErrors: true });
const page = await context.newPage();
const response = await page.goto(httpsServer.EMPTY_PAGE).catch(e => error = e); const response = await page.goto(httpsServer.EMPTY_PAGE).catch(e => error = e);
expect(error).toBe(null); expect(error).toBe(null);
expect(response.ok()).toBe(true); expect(response.ok()).toBe(true);
await context.close();
}); });
it('should work with mixed content', async({newPage, server, httpsServer}) => { it('should work with mixed content', async({browser, server, httpsServer}) => {
httpsServer.setRoute('/mixedcontent.html', (req, res) => { httpsServer.setRoute('/mixedcontent.html', (req, res) => {
res.end(`<iframe src=${server.EMPTY_PAGE}></iframe>`); res.end(`<iframe src=${server.EMPTY_PAGE}></iframe>`);
}); });
const page = await newPage({ ignoreHTTPSErrors: true }); const context = await browser.newContext({ ignoreHTTPSErrors: true });
const page = await context.newPage();
await page.goto(httpsServer.PREFIX + '/mixedcontent.html', {waitUntil: 'load'}); await page.goto(httpsServer.PREFIX + '/mixedcontent.html', {waitUntil: 'load'});
expect(page.frames().length).toBe(2); expect(page.frames().length).toBe(2);
// Make sure blocked iframe has functional execution context // Make sure blocked iframe has functional execution context
// @see https://github.com/GoogleChrome/puppeteer/issues/2709 // @see https://github.com/GoogleChrome/puppeteer/issues/2709
expect(await page.frames()[0].evaluate('1 + 2')).toBe(3); expect(await page.frames()[0].evaluate('1 + 2')).toBe(3);
expect(await page.frames()[1].evaluate('2 + 3')).toBe(5); expect(await page.frames()[1].evaluate('2 + 3')).toBe(5);
await context.close();
}); });
}); });
}; };

View file

@ -193,17 +193,19 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI
path.relative(process.cwd(), __dirname + '/assets/pptr.png')).catch(e => error = e); path.relative(process.cwd(), __dirname + '/assets/pptr.png')).catch(e => error = e);
expect(error).not.toBe(null); expect(error).not.toBe(null);
}); });
it('should emit input change event', async({page, server}) => { it('should emit input and change events', async({page, server}) => {
const events = []; const events = [];
await page.exposeFunction('eventHandled', e => events.push(e)); await page.exposeFunction('eventHandled', e => events.push(e));
await page.setContent(` await page.setContent(`
<input id=input type=file></input> <input id=input type=file></input>
<script> <script>
input.addEventListener('input', e => eventHandled({ type: e.type })); input.addEventListener('input', e => eventHandled({ type: e.type }));
input.addEventListener('change', e => eventHandled({ type: e.type }));
</script>`); </script>`);
await (await page.$('input')).setInputFiles(FILE_TO_UPLOAD); await (await page.$('input')).setInputFiles(FILE_TO_UPLOAD);
expect(events.length).toBe(1); expect(events.length).toBe(2);
expect(events[0].type).toBe('input'); expect(events[0].type).toBe('input');
expect(events[1].type).toBe('change');
}); });
}); });

View file

@ -326,9 +326,10 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
await request.continue().catch(e => error = e); await request.continue().catch(e => error = e);
expect(error).toBe(null); expect(error).toBe(null);
}); });
it('should throw if interception is not enabled', async({newPage, server}) => { it('should throw if interception is not enabled', async({browser, server}) => {
let error = null; let error = null;
const page = await newPage(); const context = await browser.newContext();
const page = await context.newPage();
page.on('request', async request => { page.on('request', async request => {
try { try {
await request.continue(); await request.continue();
@ -338,6 +339,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
}); });
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
expect(error.message).toContain('Request Interception is not enabled'); expect(error.message).toContain('Request Interception is not enabled');
await context.close();
}); });
it('should intercept main resource during cross-process navigation', async({page, server}) => { it('should intercept main resource during cross-process navigation', async({page, server}) => {
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
@ -559,12 +561,14 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
}); });
describe('ignoreHTTPSErrors', function() { describe('ignoreHTTPSErrors', function() {
it('should work with request interception', async({newPage, httpsServer}) => { it('should work with request interception', async({browser, httpsServer}) => {
const page = await newPage({ ignoreHTTPSErrors: true, interceptNetwork: true }); const context = await browser.newContext({ ignoreHTTPSErrors: true, interceptNetwork: true });
const page = await context.newPage();
await page.route('**/*', request => request.continue()); await page.route('**/*', request => request.continue());
const response = await page.goto(httpsServer.EMPTY_PAGE); const response = await page.goto(httpsServer.EMPTY_PAGE);
expect(response.status()).toBe(200); expect(response.status()).toBe(200);
await context.close();
}); });
}); });

View file

@ -133,8 +133,8 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT,
]); ]);
}); });
// @see https://crbug.com/929806 // @see https://crbug.com/929806
it('should work with mobile viewports and cross process navigations', async({newContext, server}) => { it('should work with mobile viewports and cross process navigations', async({browser, server}) => {
const context = await newContext({ viewport: {width: 360, height: 640, isMobile: true} }); const context = await browser.newContext({ viewport: {width: 360, height: 640, isMobile: true} });
const page = await context.newPage(); const page = await context.newPage();
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
await page.goto(server.CROSS_PROCESS_PREFIX + '/mobile.html'); await page.goto(server.CROSS_PROCESS_PREFIX + '/mobile.html');
@ -147,6 +147,17 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT,
await page.mouse.click(30, 40); await page.mouse.click(30, 40);
expect(await page.evaluate('result')).toEqual({x: 30, y: 40}); expect(await page.evaluate('result')).toEqual({x: 30, y: 40});
await context.close();
});
describe('Drag and Drop', function() {
xit('should work', async({server, page}) => {
await page.goto(server.PREFIX + '/drag-n-drop.html');
await page.hover('#source');
await page.mouse.down();
await page.hover('#target');
await page.mouse.up();
expect(await page.$eval('#target', target => target.contains(document.querySelector('#source')))).toBe(true, 'could not find source in target');
})
}); });
}); });
}; };

View file

@ -16,7 +16,6 @@
*/ */
const utils = require('./utils'); const utils = require('./utils');
const { performance } = require('perf_hooks');
/** /**
* @type {PageTestSuite} * @type {PageTestSuite}
@ -396,29 +395,39 @@ module.exports.describe = function({testRunner, expect, playwright, MAC, WIN, FF
expect(response.status()).toBe(200); expect(response.status()).toBe(200);
}); });
/**
* @param {import('../src/frames').Frame} frame
* @param {TestServer} server
* @param {'networkidle0'|'networkidle2'} signal
* @param {() => Promise<void>} action
* @param {boolean} isSetContent
*/
async function networkIdleTest(frame, server, signal, action, isSetContent) { async function networkIdleTest(frame, server, signal, action, isSetContent) {
let lastResponseFinished;
const finishResponse = response => { const finishResponse = response => {
lastResponseFinished = performance.now();
response.statusCode = 404; response.statusCode = 404;
response.end(`File not found`); response.end(`File not found`);
}; };
const waitForRequest = suffix => {
return Promise.all([
server.waitForRequest(suffix),
frame._page.waitForRequest(server.PREFIX + suffix),
])
}
let responses = {}; let responses = {};
// Hold on to a bunch of requests without answering. // Hold on to a bunch of requests without answering.
server.setRoute('/fetch-request-a.js', (req, res) => responses.a = res); server.setRoute('/fetch-request-a.js', (req, res) => responses.a = res);
server.setRoute('/fetch-request-b.js', (req, res) => responses.b = res); server.setRoute('/fetch-request-b.js', (req, res) => responses.b = res);
server.setRoute('/fetch-request-c.js', (req, res) => responses.c = res); server.setRoute('/fetch-request-c.js', (req, res) => responses.c = res);
const initialFetchResourcesRequested = Promise.all([ const initialFetchResourcesRequested = Promise.all([
server.waitForRequest('/fetch-request-a.js'), waitForRequest('/fetch-request-a.js'),
server.waitForRequest('/fetch-request-b.js'), waitForRequest('/fetch-request-b.js'),
server.waitForRequest('/fetch-request-c.js'), waitForRequest('/fetch-request-c.js')
]); ]);
let secondFetchResourceRequested; let secondFetchResourceRequested;
if (signal === 'networkidle0') { if (signal === 'networkidle0') {
server.setRoute('/fetch-request-d.js', (req, res) => responses.d = res); server.setRoute('/fetch-request-d.js', (req, res) => responses.d = res);
secondFetchResourceRequested = server.waitForRequest('/fetch-request-d.js'); secondFetchResourceRequested = waitForRequest('/fetch-request-d.js');
} }
const waitForLoadPromise = isSetContent ? Promise.resolve() : frame.waitForNavigation({ waitUntil: 'load' }); const waitForLoadPromise = isSetContent ? Promise.resolve() : frame.waitForNavigation({ waitUntil: 'load' });
@ -442,10 +451,11 @@ module.exports.describe = function({testRunner, expect, playwright, MAC, WIN, FF
expect(responses.a).toBeTruthy(); expect(responses.a).toBeTruthy();
expect(responses.b).toBeTruthy(); expect(responses.b).toBeTruthy();
expect(responses.c).toBeTruthy(); expect(responses.c).toBeTruthy();
// Finishing first response should leave 2 requests alive and trigger networkidle2. let timer;
finishResponse(responses.a); let timerTriggered = false;
if (signal === 'networkidle0') { if (signal === 'networkidle0') {
// Finishing first response should leave 2 requests alive and trigger networkidle2.
finishResponse(responses.a);
// Finishing two more responses should trigger the second round. // Finishing two more responses should trigger the second round.
finishResponse(responses.b); finishResponse(responses.b);
finishResponse(responses.c); finishResponse(responses.c);
@ -454,11 +464,16 @@ module.exports.describe = function({testRunner, expect, playwright, MAC, WIN, FF
await secondFetchResourceRequested; await secondFetchResourceRequested;
expect(actionFinished).toBe(false); expect(actionFinished).toBe(false);
// Finishing the last response should trigger networkidle0. // Finishing the last response should trigger networkidle0.
timer = setTimeout(() => timerTriggered = true, 500);
finishResponse(responses.d); finishResponse(responses.d);
} else {
timer = setTimeout(() => timerTriggered = true, 500);
// Finishing first response should leave 2 requests alive and trigger networkidle2.
finishResponse(responses.a);
} }
const response = await actionPromise; const response = await actionPromise;
expect(performance.now() - lastResponseFinished).not.toBeLessThan(450); clearTimeout(timer);
expect(timerTriggered).toBe(true);
if (!isSetContent) if (!isSetContent)
expect(response.ok()).toBe(true); expect(response.ok()).toBe(true);

View file

@ -1045,12 +1045,12 @@ module.exports.describe = function({testRunner, expect, headless, playwright, FF
it('should throw on hidden and invisible elements', async({page, server}) => { it('should throw on hidden and invisible elements', async({page, server}) => {
await page.goto(server.PREFIX + '/input/textarea.html'); await page.goto(server.PREFIX + '/input/textarea.html');
await page.$eval('input', i => i.style.display = 'none'); await page.$eval('input', i => i.style.display = 'none');
const invisibleError = await page.fill('input', 'some value', { waitFor: 'nowait' }).catch(e => e); const invisibleError = await page.fill('input', 'some value', { waitFor: false }).catch(e => e);
expect(invisibleError.message).toBe('Element is not visible'); expect(invisibleError.message).toBe('Element is not visible');
await page.goto(server.PREFIX + '/input/textarea.html'); await page.goto(server.PREFIX + '/input/textarea.html');
await page.$eval('input', i => i.style.visibility = 'hidden'); await page.$eval('input', i => i.style.visibility = 'hidden');
const hiddenError = await page.fill('input', 'some value', { waitFor: 'nowait' }).catch(e => e); const hiddenError = await page.fill('input', 'some value', { waitFor: false }).catch(e => e);
expect(hiddenError.message).toBe('Element is hidden'); expect(hiddenError.message).toBe('Element is hidden');
}); });
it('should be able to fill the body', async({page}) => { it('should be able to fill the body', async({page}) => {

View file

@ -44,8 +44,7 @@ module.exports.describe = ({testRunner, product, playwrightPath}) => {
const headless = !!valueFromEnv('HEADLESS', true); const headless = !!valueFromEnv('HEADLESS', true);
const slowMo = valueFromEnv('SLOW_MO', 0); const slowMo = valueFromEnv('SLOW_MO', 0);
const CI = valueFromEnv('CI', false); const dumpProtocolOnFailure = valueFromEnv('DEBUGP', false);
const dumpProtocolOnFailure = CI || valueFromEnv('DEBUGP', false);
function valueFromEnv(name, defaultValue) { function valueFromEnv(name, defaultValue) {
if (!(name in process.env)) if (!(name in process.env))
@ -113,7 +112,6 @@ module.exports.describe = ({testRunner, product, playwrightPath}) => {
}); });
beforeEach(async(state, test) => { beforeEach(async(state, test) => {
const contexts = [];
const onLine = (line) => test.output += line + '\n'; const onLine = (line) => test.output += line + '\n';
if (dumpProtocolOnFailure) if (dumpProtocolOnFailure)
state.browser._setDebugFunction(onLine); state.browser._setDebugFunction(onLine);
@ -126,7 +124,6 @@ module.exports.describe = ({testRunner, product, playwrightPath}) => {
} }
state.tearDown = async () => { state.tearDown = async () => {
await Promise.all(contexts.map(c => c.close()));
if (rl) { if (rl) {
rl.removeListener('line', onLine); rl.removeListener('line', onLine);
rl.close(); rl.close();
@ -134,30 +131,25 @@ module.exports.describe = ({testRunner, product, playwrightPath}) => {
if (dumpProtocolOnFailure) if (dumpProtocolOnFailure)
state.browser._setDebugFunction(() => void 0); state.browser._setDebugFunction(() => void 0);
}; };
state.newContext = async (options) => {
const context = await state.browser.newContext(options);
contexts.push(context);
return context;
};
state.newPage = async (options) => {
const context = await state.newContext(options);
return await context.newPage();
};
}); });
afterEach(async state => { afterEach(async (state, test) => {
if (state.browser.contexts().length !== 0) {
if (test.result === 'ok')
console.warn(`\nWARNING: test "${test.fullName}" (${test.location.fileName}:${test.location.lineNumber}) did not close all created contexts!\n`);
await Promise.all(state.browser.contexts().map(context => context.close()));
}
await state.tearDown(); await state.tearDown();
}); });
describe('Page', function() { describe('Page', function() {
beforeEach(async state => { beforeEach(async state => {
state.context = await state.newContext(); state.context = await state.browser.newContext();
state.page = await state.context.newPage(); state.page = await state.context.newPage();
}); });
afterEach(async state => { afterEach(async state => {
await state.context.close();
state.context = null; state.context = null;
state.page = null; state.page = null;
}); });

View file

@ -20,8 +20,8 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;
describe('window.open', function() { describe('window.open', function() {
it.skip(CHROMIUM || WEBKIT)('should inherit user agent from browser context', async function({newContext, server}) { it.skip(CHROMIUM || WEBKIT)('should inherit user agent from browser context', async function({browser, server}) {
const context = await newContext({ const context = await browser.newContext({
userAgent: 'hey' userAgent: 'hey'
}); });
const page = await context.newPage(); const page = await context.newPage();
@ -36,8 +36,8 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
expect(userAgent).toBe('hey'); expect(userAgent).toBe('hey');
expect(request.headers['user-agent']).toBe('hey'); expect(request.headers['user-agent']).toBe('hey');
}); });
it.skip(CHROMIUM)('should inherit touch support from browser context', async function({newContext, server}) { it.skip(CHROMIUM)('should inherit touch support from browser context', async function({browser, server}) {
const context = await newContext({ const context = await browser.newContext({
viewport: { width: 400, height: 500, isMobile: true } viewport: { width: 400, height: 500, isMobile: true }
}); });
const page = await context.newPage(); const page = await context.newPage();
@ -49,8 +49,8 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
await context.close(); await context.close();
expect(hasTouch).toBe(true); expect(hasTouch).toBe(true);
}); });
it.skip(CHROMIUM || WEBKIT)('should inherit viewport size from browser context', async function({newContext, server}) { it.skip(CHROMIUM || WEBKIT)('should inherit viewport size from browser context', async function({browser, server}) {
const context = await newContext({ const context = await browser.newContext({
viewport: { width: 400, height: 500, isMobile: true } viewport: { width: 400, height: 500, isMobile: true }
}); });
const page = await context.newPage(); const page = await context.newPage();
@ -65,8 +65,8 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
}); });
describe('Page.Events.Popup', function() { describe('Page.Events.Popup', function() {
it('should work', async({newContext}) => { it('should work', async({browser}) => {
const context = await newContext(); const context = await browser.newContext();
const page = await context.newPage(); const page = await context.newPage();
const [popup] = await Promise.all([ const [popup] = await Promise.all([
new Promise(x => page.once('popup', x)), new Promise(x => page.once('popup', x)),
@ -76,8 +76,8 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
expect(await popup.evaluate(() => !!window.opener)).toBe(true); expect(await popup.evaluate(() => !!window.opener)).toBe(true);
await context.close(); await context.close();
}); });
it.skip(CHROMIUM)('should work with empty url', async({newContext}) => { it.skip(CHROMIUM)('should work with empty url', async({browser}) => {
const context = await newContext(); const context = await browser.newContext();
const page = await context.newPage(); const page = await context.newPage();
const [popup] = await Promise.all([ const [popup] = await Promise.all([
new Promise(x => page.once('popup', x)), new Promise(x => page.once('popup', x)),
@ -87,8 +87,8 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
expect(await popup.evaluate(() => !!window.opener)).toBe(true); expect(await popup.evaluate(() => !!window.opener)).toBe(true);
await context.close(); await context.close();
}); });
it('should work with noopener', async({newContext}) => { it('should work with noopener', async({browser}) => {
const context = await newContext(); const context = await browser.newContext();
const page = await context.newPage(); const page = await context.newPage();
const [popup] = await Promise.all([ const [popup] = await Promise.all([
new Promise(x => page.once('popup', x)), new Promise(x => page.once('popup', x)),
@ -98,8 +98,8 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
expect(await popup.evaluate(() => !!window.opener)).toBe(false); expect(await popup.evaluate(() => !!window.opener)).toBe(false);
await context.close(); await context.close();
}); });
it('should work with clicking target=_blank', async({newContext, server}) => { it('should work with clicking target=_blank', async({browser, server}) => {
const context = await newContext(); const context = await browser.newContext();
const page = await context.newPage(); const page = await context.newPage();
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
await page.setContent('<a target=_blank rel="opener" href="/one-style.html">yo</a>'); await page.setContent('<a target=_blank rel="opener" href="/one-style.html">yo</a>');
@ -111,8 +111,8 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
expect(await popup.evaluate(() => !!window.opener)).toBe(true); expect(await popup.evaluate(() => !!window.opener)).toBe(true);
await context.close(); await context.close();
}); });
it('should work with fake-clicking target=_blank and rel=noopener', async({newContext, server}) => { it('should work with fake-clicking target=_blank and rel=noopener', async({browser, server}) => {
const context = await newContext(); const context = await browser.newContext();
const page = await context.newPage(); const page = await context.newPage();
// TODO: FFOX sends events for "one-style.html" request to both pages. // TODO: FFOX sends events for "one-style.html" request to both pages.
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
@ -127,8 +127,8 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
expect(await popup.evaluate(() => !!window.opener)).toBe(false); expect(await popup.evaluate(() => !!window.opener)).toBe(false);
await context.close(); await context.close();
}); });
it('should work with clicking target=_blank and rel=noopener', async({newContext, server}) => { it('should work with clicking target=_blank and rel=noopener', async({browser, server}) => {
const context = await newContext(); const context = await browser.newContext();
const page = await context.newPage(); const page = await context.newPage();
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
await page.setContent('<a target=_blank rel=noopener href="/one-style.html">yo</a>'); await page.setContent('<a target=_blank rel=noopener href="/one-style.html">yo</a>');
@ -140,8 +140,8 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
expect(await popup.evaluate(() => !!window.opener)).toBe(false); expect(await popup.evaluate(() => !!window.opener)).toBe(false);
await context.close(); await context.close();
}); });
it('should not treat navigations as new popups', async({newContext, server}) => { it('should not treat navigations as new popups', async({browser, server}) => {
const context = await newContext(); const context = await browser.newContext();
const page = await context.newPage(); const page = await context.newPage();
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
await page.setContent('<a target=_blank rel=noopener href="/one-style.html">yo</a>'); await page.setContent('<a target=_blank rel=noopener href="/one-style.html">yo</a>');

View file

@ -156,12 +156,13 @@ module.exports.describe = function({testRunner, expect, product, FFOX, CHROMIUM,
}); });
expect(Buffer.from(screenshot, 'base64')).toBeGolden('screenshot-sanity.png'); expect(Buffer.from(screenshot, 'base64')).toBeGolden('screenshot-sanity.png');
}); });
it.skip(FFOX)('should work with a mobile viewport', async({newContext, server}) => { it.skip(FFOX)('should work with a mobile viewport', async({browser, server}) => {
const context = await newContext({viewport: { width: 320, height: 480, isMobile: true }}); const context = await browser.newContext({viewport: { width: 320, height: 480, isMobile: true }});
const page = await context.newPage(); const page = await context.newPage();
await page.goto(server.PREFIX + '/overflow.html'); await page.goto(server.PREFIX + '/overflow.html');
const screenshot = await page.screenshot(); const screenshot = await page.screenshot();
expect(screenshot).toBeGolden('screenshot-mobile.png'); expect(screenshot).toBeGolden('screenshot-mobile.png');
await context.close();
}); });
it('should work for canvas', async({page, server}) => { it('should work for canvas', async({page, server}) => {
await page.setViewportSize({width: 500, height: 500}); await page.setViewportSize({width: 500, height: 500});
@ -358,8 +359,9 @@ module.exports.describe = function({testRunner, expect, product, FFOX, CHROMIUM,
const screenshot = await elementHandle.screenshot(); const screenshot = await elementHandle.screenshot();
expect(screenshot).toBeGolden('screenshot-element-fractional-offset.png'); expect(screenshot).toBeGolden('screenshot-element-fractional-offset.png');
}); });
it('should take screenshots when default viewport is null', async({server, newPage}) => { it('should take screenshots when default viewport is null', async({server, browser}) => {
const page = await newPage({ viewport: null }); const context = await browser.newContext({ viewport: null });
const page = await context.newPage();
await page.goto(server.PREFIX + '/grid.html'); await page.goto(server.PREFIX + '/grid.html');
const sizeBefore = await page.evaluate(() => ({ width: document.body.offsetWidth, height: document.body.offsetHeight })); const sizeBefore = await page.evaluate(() => ({ width: document.body.offsetWidth, height: document.body.offsetHeight }));
const screenshot = await page.screenshot(); const screenshot = await page.screenshot();
@ -368,9 +370,11 @@ module.exports.describe = function({testRunner, expect, product, FFOX, CHROMIUM,
const sizeAfter = await page.evaluate(() => ({ width: document.body.offsetWidth, height: document.body.offsetHeight })); const sizeAfter = await page.evaluate(() => ({ width: document.body.offsetWidth, height: document.body.offsetHeight }));
expect(sizeBefore.width).toBe(sizeAfter.width); expect(sizeBefore.width).toBe(sizeAfter.width);
expect(sizeBefore.height).toBe(sizeAfter.height); expect(sizeBefore.height).toBe(sizeAfter.height);
await context.close();
}); });
it('should take fullPage screenshots when default viewport is null', async({server, newPage}) => { it('should take fullPage screenshots when default viewport is null', async({server, browser}) => {
const page = await newPage({ viewport: null }); const context = await browser.newContext({ viewport: null });
const page = await context.newPage();
await page.goto(server.PREFIX + '/grid.html'); await page.goto(server.PREFIX + '/grid.html');
const sizeBefore = await page.evaluate(() => ({ width: document.body.offsetWidth, height: document.body.offsetHeight })); const sizeBefore = await page.evaluate(() => ({ width: document.body.offsetWidth, height: document.body.offsetHeight }));
const screenshot = await page.screenshot({ const screenshot = await page.screenshot({
@ -381,9 +385,11 @@ module.exports.describe = function({testRunner, expect, product, FFOX, CHROMIUM,
const sizeAfter = await page.evaluate(() => ({ width: document.body.offsetWidth, height: document.body.offsetHeight })); const sizeAfter = await page.evaluate(() => ({ width: document.body.offsetWidth, height: document.body.offsetHeight }));
expect(sizeBefore.width).toBe(sizeAfter.width); expect(sizeBefore.width).toBe(sizeAfter.width);
expect(sizeBefore.height).toBe(sizeAfter.height); expect(sizeBefore.height).toBe(sizeAfter.height);
await context.close();
}); });
it('should restore default viewport after fullPage screenshot', async({ newPage }) => { it('should restore default viewport after fullPage screenshot', async({ browser }) => {
const page = await newPage({ viewport: { width: 456, height: 789 } }); const context = await browser.newContext({ viewport: { width: 456, height: 789 } });
const page = await context.newPage();
expect(page.viewportSize().width).toBe(456); expect(page.viewportSize().width).toBe(456);
expect(page.viewportSize().height).toBe(789); expect(page.viewportSize().height).toBe(789);
expect(await page.evaluate('window.innerWidth')).toBe(456); expect(await page.evaluate('window.innerWidth')).toBe(456);
@ -394,9 +400,11 @@ module.exports.describe = function({testRunner, expect, product, FFOX, CHROMIUM,
expect(page.viewportSize().height).toBe(789); expect(page.viewportSize().height).toBe(789);
expect(await page.evaluate('window.innerWidth')).toBe(456); expect(await page.evaluate('window.innerWidth')).toBe(456);
expect(await page.evaluate('window.innerHeight')).toBe(789); expect(await page.evaluate('window.innerHeight')).toBe(789);
await context.close();
}); });
it('should take element screenshot when default viewport is null and restore back', async({server, newPage}) => { it('should take element screenshot when default viewport is null and restore back', async({server, browser}) => {
const page = await newPage({ viewport: null }); const context = await browser.newContext({viewport: null});
const page = await context.newPage({ viewport: null });
await page.setContent(` await page.setContent(`
<div style="height: 14px">oooo</div> <div style="height: 14px">oooo</div>
<style> <style>
@ -421,8 +429,7 @@ module.exports.describe = function({testRunner, expect, product, FFOX, CHROMIUM,
const sizeAfter = await page.evaluate(() => ({ width: document.body.offsetWidth, height: document.body.offsetHeight })); const sizeAfter = await page.evaluate(() => ({ width: document.body.offsetWidth, height: document.body.offsetHeight }));
expect(sizeBefore.width).toBe(sizeAfter.width); expect(sizeBefore.width).toBe(sizeAfter.width);
expect(sizeBefore.height).toBe(sizeAfter.height); expect(sizeBefore.height).toBe(sizeAfter.height);
await context.close();
}); });
}); });
}; };

View file

@ -124,7 +124,6 @@ if (process.env.CI && testRunner.hasFocusedTestsOrSuites()) {
new Reporter(testRunner, { new Reporter(testRunner, {
verbose: process.argv.includes('--verbose'), verbose: process.argv.includes('--verbose'),
summary: !process.argv.includes('--verbose'), summary: !process.argv.includes('--verbose'),
projectFolder: utils.projectRoot(),
showSlowTests: process.env.CI ? 5 : 0, showSlowTests: process.env.CI ? 5 : 0,
showSkippedTests: 10, showSkippedTests: 10,
}); });

4
test/types.d.ts vendored
View file

@ -66,8 +66,6 @@ type TestState = {
type BrowserState = TestState & { type BrowserState = TestState & {
browser: import('../src/browser').Browser; browser: import('../src/browser').Browser;
browserServer: import('../src/server/browserServer').BrowserServer; browserServer: import('../src/server/browserServer').BrowserServer;
newPage: (options?: import('../src/browserContext').BrowserContextOptions) => Promise<import('../src/page').Page>;
newContext: (options?: import('../src/browserContext').BrowserContextOptions) => Promise<import('../src/browserContext').BrowserContext>;
}; };
type PageState = BrowserState & { type PageState = BrowserState & {
@ -100,4 +98,4 @@ interface TestServer {
CROSS_PROCESS_PREFIX: string; CROSS_PROCESS_PREFIX: string;
EMPTY_PAGE: string; EMPTY_PAGE: string;
} }

View file

@ -375,22 +375,22 @@ module.exports.describe = function({testRunner, expect, product, playwright, FFO
it('should throw for unknown waitFor option', async({page, server}) => { it('should throw for unknown waitFor option', async({page, server}) => {
await page.setContent('<section>test</section>'); await page.setContent('<section>test</section>');
const error = await page.waitForSelector('section', { visibility: 'foo' }).catch(e => e); const error = await page.waitForSelector('section', { visibility: 'foo' }).catch(e => e);
expect(error.message).toContain('Unsupported waitFor option'); expect(error.message).toContain('Unsupported visibility option');
}); });
it('should throw for numeric waitFor option', async({page, server}) => { it('should throw for numeric waitFor option', async({page, server}) => {
await page.setContent('<section>test</section>'); await page.setContent('<section>test</section>');
const error = await page.waitForSelector('section', { visibility: 123 }).catch(e => e); const error = await page.waitForSelector('section', { visibility: 123 }).catch(e => e);
expect(error.message).toContain('Unsupported waitFor option'); expect(error.message).toContain('Unsupported visibility option');
}); });
it('should throw for true waitFor option', async({page, server}) => { it('should throw for true waitFor option', async({page, server}) => {
await page.setContent('<section>test</section>'); await page.setContent('<section>test</section>');
const error = await page.waitForSelector('section', { visibility: true }).catch(e => e); const error = await page.waitForSelector('section', { visibility: true }).catch(e => e);
expect(error.message).toContain('Unsupported waitFor option'); expect(error.message).toContain('Unsupported visibility option');
}); });
it('should throw for false waitFor option', async({page, server}) => { it('should throw for false waitFor option', async({page, server}) => {
await page.setContent('<section>test</section>'); await page.setContent('<section>test</section>');
const error = await page.waitForSelector('section', { visibility: false }).catch(e => e); const error = await page.waitForSelector('section', { visibility: false }).catch(e => e);
expect(error.message).toContain('Unsupported waitFor option'); expect(error.message).toContain('Unsupported visibility option');
}); });
it('should support >> selector syntax', async({page, server}) => { it('should support >> selector syntax', async({page, server}) => {
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);

View file

@ -73,7 +73,7 @@ Documentation.Class = class {
} }
} }
validateOrder(errors) { validateOrder(errors, cls) {
const members = this.membersArray; const members = this.membersArray;
// Events should go first. // Events should go first.
let eventIndex = 0; let eventIndex = 0;

View file

@ -28,7 +28,7 @@ module.exports = { checkSources, expandPrefix };
function checkSources(sources, externalDependencies) { function checkSources(sources, externalDependencies) {
// special treatment for Events.js // special treatment for Events.js
const classEvents = new Map(); const classEvents = new Map();
const eventsSources = sources.filter(source => source.name().startsWith('events.ts')); const eventsSources = sources.filter(source => source.name().startsWith('events.'));
for (const eventsSource of eventsSources) { for (const eventsSource of eventsSources) {
const {Events} = require(eventsSource.filePath().endsWith('.js') ? eventsSource.filePath() : eventsSource.filePath().replace(/\bsrc\b/, 'lib').replace('.ts', '.js')); const {Events} = require(eventsSource.filePath().endsWith('.js') ? eventsSource.filePath() : eventsSource.filePath().replace(/\bsrc\b/, 'lib').replace('.ts', '.js'));
for (const [className, events] of Object.entries(Events)) for (const [className, events] of Object.entries(Events))

View file

@ -306,7 +306,7 @@ module.exports = async function(page, sources) {
// Push base class documentation to derived classes. // Push base class documentation to derived classes.
for (const [name, clazz] of documentation.classes.entries()) { for (const [name, clazz] of documentation.classes.entries()) {
clazz.validateOrder(errors); clazz.validateOrder(errors, clazz);
if (!clazz.extends || clazz.extends === 'EventEmitter' || clazz.extends === 'Error') if (!clazz.extends || clazz.extends === 'EventEmitter' || clazz.extends === 'Error')
continue; continue;

View file

@ -0,0 +1,13 @@
class Foo {
test() {
}
title(arg: number) {
}
}
class Bar {
}
export {Bar};
export {Foo};

View file

@ -1,13 +0,0 @@
class Foo {
test() {
}
/**
* @param {number} arg
*/
title(arg) {
}
}
class Bar {
}

View file

@ -18,3 +18,5 @@ class Foo {
async asyncFunction() { async asyncFunction() {
} }
} }
export {Foo};

View file

@ -1,7 +1,5 @@
class Foo { class Foo {
constructor() { ddd = 10;
this.ddd = 10;
}
aaa() {} aaa() {}
@ -10,3 +8,4 @@ class Foo {
ccc() {} ccc() {}
} }
export {Foo};

View file

@ -0,0 +1,11 @@
class Foo {
foo(arg1: string, arg3 = {}) {
}
test(...filePaths : string[]) {
}
bar(options: {visibility?: boolean}) {
}
}
export {Foo};

View file

@ -1,19 +0,0 @@
class Foo {
/**
* @param {string} arg1
*/
foo(arg1, arg3 = {}) {
}
/**
* @param {...string} filePaths
*/
test(...filePaths) {
}
/**
* @param {{visibility?: boolean}} options
*/
bar(options) {
}
}

View file

@ -0,0 +1,2 @@
export {Foo} from './foo';
export {Other} from './other';

View file

@ -1,2 +0,0 @@
class Foo {
}

View file

@ -0,0 +1,2 @@
export class Foo {
}

View file

@ -1,2 +0,0 @@
class Other {
}

View file

@ -0,0 +1,2 @@
export class Other {
}

View file

@ -14,3 +14,5 @@ class Foo {
money$$money() { money$$money() {
} }
} }
export {Foo};

View file

@ -0,0 +1,5 @@
class Foo {
a = 42;
b = 'hello';
}
export {Foo};

View file

@ -1,7 +0,0 @@
class Foo {
constructor() {
this.a = 42;
this.b = 'hello';
this.emit('done');
}
}

View file

@ -1,7 +1,7 @@
class A { class A {
property1 = 1;
_property2 = 2;
constructor(delegate) { constructor(delegate) {
this.property1 = 1;
this._property2 = 2;
} }
get getter() { get getter() {
@ -11,3 +11,5 @@ class A {
async method(foo, bar) { async method(foo, bar) {
} }
} }
export {A};

View file

@ -13,3 +13,6 @@ class B extends A {
bar(override) { bar(override) {
} }
} }
export {A};
export {B};

View file

@ -34,7 +34,7 @@ let browserContext;
let page; let page;
beforeAll(async function() { beforeAll(async function() {
browser = await playwright.launch(); browser = await playwright.chromium.launch();
page = await browser.newPage(); page = await browser.newPage();
}); });
@ -65,8 +65,9 @@ async function testLint(state, test) {
}); });
const mdSources = await Source.readdir(dirPath, '.md'); const mdSources = await Source.readdir(dirPath, '.md');
const tsSources = await Source.readdir(dirPath, '.ts');
const jsSources = await Source.readdir(dirPath, '.js'); const jsSources = await Source.readdir(dirPath, '.js');
const messages = await checkPublicAPI(page, mdSources, jsSources); const messages = await checkPublicAPI(page, mdSources, jsSources.concat(tsSources));
const errors = messages.map(message => message.text); const errors = messages.map(message => message.text);
expect(errors.join('\n')).toBeGolden('result.txt'); expect(errors.join('\n')).toBeGolden('result.txt');
} }
@ -86,8 +87,9 @@ async function testJSBuilder(state, test) {
const {expect} = new Matchers({ const {expect} = new Matchers({
toBeGolden: GoldenUtils.compare.bind(null, dirPath, dirPath) toBeGolden: GoldenUtils.compare.bind(null, dirPath, dirPath)
}); });
const sources = await Source.readdir(dirPath, '.js'); const jsSources = await Source.readdir(dirPath, '.js');
const {documentation} = await jsBuilder(sources); const tsSources = await Source.readdir(dirPath, '.ts');
const {documentation} = await jsBuilder.checkSources(jsSources.concat(tsSources));
expect(serialize(documentation)).toBeGolden('result.txt'); expect(serialize(documentation)).toBeGolden('result.txt');
} }

View file

@ -14,7 +14,11 @@
* limitations under the License. * limitations under the License.
*/ */
module.exports = class Matchers { const {getCallerLocation} = require('./utils.js');
const colors = require('colors/safe');
const Diff = require('text-diff');
class Matchers {
constructor(customMatchers = {}) { constructor(customMatchers = {}) {
this._matchers = {}; this._matchers = {};
Object.assign(this._matchers, DefaultMatchers); Object.assign(this._matchers, DefaultMatchers);
@ -26,99 +30,202 @@ module.exports = class Matchers {
this._matchers[name] = matcher; this._matchers[name] = matcher;
} }
expect(value) { expect(received) {
return new Expect(value, this._matchers); return new Expect(received, this._matchers);
} }
}; };
class MatchError extends Error {
constructor(message, formatter) {
super(message);
this.name = this.constructor.name;
this.formatter = formatter;
this.location = getCallerLocation(__filename);
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = {Matchers, MatchError};
class Expect { class Expect {
constructor(value, matchers) { constructor(received, matchers) {
this.not = {}; this.not = {};
this.not.not = this; this.not.not = this;
for (const matcherName of Object.keys(matchers)) { for (const matcherName of Object.keys(matchers)) {
const matcher = matchers[matcherName]; const matcher = matchers[matcherName];
this[matcherName] = applyMatcher.bind(null, matcherName, matcher, false /* inverse */, value); this[matcherName] = applyMatcher.bind(null, matcherName, matcher, false /* inverse */, received);
this.not[matcherName] = applyMatcher.bind(null, matcherName, matcher, true /* inverse */, value); this.not[matcherName] = applyMatcher.bind(null, matcherName, matcher, true /* inverse */, received);
} }
function applyMatcher(matcherName, matcher, inverse, value, ...args) { function applyMatcher(matcherName, matcher, inverse, received, ...args) {
const result = matcher.call(null, value, ...args); const result = matcher.call(null, received, ...args);
const message = `expect.${inverse ? 'not.' : ''}${matcherName} failed` + (result.message ? `: ${result.message}` : ''); const message = `expect.${inverse ? 'not.' : ''}${matcherName} failed` + (result.message ? `: ${result.message}` : '');
if (result.pass === inverse) if (result.pass === inverse)
throw new Error(message); throw new MatchError(message, result.formatter || defaultFormatter.bind(null, received));
} }
} }
} }
function defaultFormatter(received) {
return `Received: ${colors.red(JSON.stringify(received))}`;
}
function stringFormatter(received, expected) {
const diff = new Diff();
const result = diff.main(expected, received);
diff.cleanupSemantic(result);
const highlighted = result.map(([type, text]) => {
if (type === -1)
return colors.bgRed(text);
if (type === 1)
return colors.bgGreen.black(text);
return text;
}).join('');
const output = [
`Expected: ${expected}`,
`Received: ${highlighted}`,
];
for (let i = 0; i < Math.min(expected.length, received.length); ++i) {
if (expected[i] !== received[i]) {
const padding = ' '.repeat('Expected: '.length);
const firstDiffCharacter = '~'.repeat(i) + '^';
output.push(colors.red(padding + firstDiffCharacter));
break;
}
}
return output.join('\n');
}
function objectFormatter(received, expected) {
const receivedLines = received.split('\n');
const expectedLines = expected.split('\n');
const encodingMap = new Map();
const decodingMap = new Map();
const doEncodeLines = (lines) => {
let encoded = '';
for (const line of lines) {
let code = encodingMap.get(line);
if (!code) {
code = String.fromCodePoint(encodingMap.size);
encodingMap.set(line, code);
decodingMap.set(code, line);
}
encoded += code;
}
return encoded;
};
const doDecodeLines = (text) => {
let decoded = [];
for (const codepoint of [...text])
decoded.push(decodingMap.get(codepoint));
return decoded;
}
let receivedEncoded = doEncodeLines(received.split('\n'));
let expectedEncoded = doEncodeLines(expected.split('\n'));
const diff = new Diff();
const result = diff.main(expectedEncoded, receivedEncoded);
diff.cleanupSemantic(result);
const highlighted = result.map(([type, text]) => {
const lines = doDecodeLines(text);
if (type === -1)
return lines.map(line => '- ' + colors.bgRed(line));
if (type === 1)
return lines.map(line => '+ ' + colors.bgGreen.black(line));
return lines.map(line => ' ' + line);
}).flat().join('\n');
return `Received:\n${highlighted}`;
}
function toBeFormatter(received, expected) {
if (typeof expected === 'string' && typeof received === 'string') {
return stringFormatter(JSON.stringify(received), JSON.stringify(expected));
}
return [
`Expected: ${JSON.stringify(expected)}`,
`Received: ${colors.red(JSON.stringify(received))}`,
].join('\n');
}
const DefaultMatchers = { const DefaultMatchers = {
toBe: function(value, other, message) { toBe: function(received, expected, message) {
message = message || `${value} == ${other}`; message = message || `${received} == ${expected}`;
return { pass: value === other, message }; return { pass: received === expected, message, formatter: toBeFormatter.bind(null, received, expected) };
}, },
toBeFalsy: function(value, message) { toBeFalsy: function(received, message) {
message = message || `${value}`; message = message || `${received}`;
return { pass: !value, message }; return { pass: !received, message };
}, },
toBeTruthy: function(value, message) { toBeTruthy: function(received, message) {
message = message || `${value}`; message = message || `${received}`;
return { pass: !!value, message }; return { pass: !!received, message };
}, },
toBeGreaterThan: function(value, other, message) { toBeGreaterThan: function(received, other, message) {
message = message || `${value} > ${other}`; message = message || `${received} > ${other}`;
return { pass: value > other, message }; return { pass: received > other, message };
}, },
toBeGreaterThanOrEqual: function(value, other, message) { toBeGreaterThanOrEqual: function(received, other, message) {
message = message || `${value} >= ${other}`; message = message || `${received} >= ${other}`;
return { pass: value >= other, message }; return { pass: received >= other, message };
}, },
toBeLessThan: function(value, other, message) { toBeLessThan: function(received, other, message) {
message = message || `${value} < ${other}`; message = message || `${received} < ${other}`;
return { pass: value < other, message }; return { pass: received < other, message };
}, },
toBeLessThanOrEqual: function(value, other, message) { toBeLessThanOrEqual: function(received, other, message) {
message = message || `${value} <= ${other}`; message = message || `${received} <= ${other}`;
return { pass: value <= other, message }; return { pass: received <= other, message };
}, },
toBeNull: function(value, message) { toBeNull: function(received, message) {
message = message || `${value} == null`; message = message || `${received} == null`;
return { pass: value === null, message }; return { pass: received === null, message };
}, },
toContain: function(value, other, message) { toContain: function(received, other, message) {
message = message || `${value}${other}`; message = message || `${received}${other}`;
return { pass: value.includes(other), message }; return { pass: received.includes(other), message };
}, },
toEqual: function(value, other, message) { toEqual: function(received, other, message) {
const valueJson = stringify(value); let receivedJson = stringify(received);
const otherJson = stringify(other); let otherJson = stringify(other);
message = message || `\n${valueJson}${otherJson}`; let formatter = objectFormatter.bind(null, receivedJson, otherJson);
return { pass: valueJson === otherJson, message }; if (receivedJson.length < 40 && otherJson.length < 40) {
receivedJson = receivedJson.split('\n').map(line => line.trim()).join(' ');
otherJson = otherJson.split('\n').map(line => line.trim()).join(' ');
formatter = stringFormatter.bind(null, receivedJson, otherJson);
}
message = message || `\n${receivedJson}${otherJson}`;
return { pass: receivedJson === otherJson, message, formatter };
}, },
toBeCloseTo: function(value, other, precision, message) { toBeCloseTo: function(received, other, precision, message) {
return { return {
pass: Math.abs(value - other) < Math.pow(10, -precision), pass: Math.abs(received - other) < Math.pow(10, -precision),
message message
}; };
}, },
toBeInstanceOf: function(value, other, message) { toBeInstanceOf: function(received, other, message) {
message = message || `${value.constructor.name} instanceof ${other.name}`; message = message || `${received.constructor.name} instanceof ${other.name}`;
return { pass: value instanceof other, message }; return { pass: received instanceof other, message };
}, },
}; };
function stringify(value) { function stringify(value) {
function stabilize(key, object) { function stabilize(key, object) {
if (typeof object !== 'object' || object === undefined || object === null) if (typeof object !== 'object' || object === undefined || object === null || Array.isArray(object))
return object; return object;
const result = {}; const result = {};
for (const key of Object.keys(object).sort()) for (const key of Object.keys(object).sort())

View file

@ -14,24 +14,20 @@
* limitations under the License. * limitations under the License.
*/ */
const RED_COLOR = '\x1b[31m'; const fs = require('fs');
const GREEN_COLOR = '\x1b[32m'; const colors = require('colors/safe');
const GRAY_COLOR = '\x1b[90m'; const {MatchError} = require('./Matchers.js');
const YELLOW_COLOR = '\x1b[33m';
const MAGENTA_COLOR = '\x1b[35m';
const RESET_COLOR = '\x1b[0m';
class Reporter { class Reporter {
constructor(runner, options = {}) { constructor(runner, options = {}) {
const { const {
projectFolder = null,
showSlowTests = 3, showSlowTests = 3,
showSkippedTests = Infinity, showSkippedTests = Infinity,
verbose = false, verbose = false,
summary = true, summary = true,
} = options; } = options;
this._filePathToLines = new Map();
this._runner = runner; this._runner = runner;
this._projectFolder = projectFolder;
this._showSlowTests = showSlowTests; this._showSlowTests = showSlowTests;
this._showSkippedTests = showSkippedTests; this._showSkippedTests = showSkippedTests;
this._verbose = verbose; this._verbose = verbose;
@ -48,19 +44,20 @@ class Reporter {
this._testCounter = 0; this._testCounter = 0;
this._timestamp = Date.now(); this._timestamp = Date.now();
const allTests = this._runner.tests(); const allTests = this._runner.tests();
if (allTests.length === runnableTests.length) if (allTests.length === runnableTests.length) {
console.log(`Running all ${YELLOW_COLOR}${runnableTests.length}${RESET_COLOR} tests on ${YELLOW_COLOR}${this._runner.parallel()}${RESET_COLOR} worker(s):\n`); console.log(`Running all ${colors.yellow(runnableTests.length)} tests on ${colors.yellow(this._runner.parallel())} worker${this._runner.parallel() > 1 ? 's' : ''}:\n`);
else } else {
console.log(`Running ${YELLOW_COLOR}${runnableTests.length}${RESET_COLOR} focused tests out of total ${YELLOW_COLOR}${allTests.length}${RESET_COLOR} on ${YELLOW_COLOR}${this._runner.parallel()}${RESET_COLOR} worker(s):\n`); console.log(`Running ${colors.yellow(runnableTests.length)} focused tests out of total ${colors.yellow(allTests.length)} on ${colors.yellow(this._runner.parallel())} worker${this._runner.parallel() > 1 ? 's' : ''}:\n`);
}
} }
_printTermination(result, message, error) { _printTermination(result, message, error) {
console.log(`${RED_COLOR}## ${result.toUpperCase()} ##${RESET_COLOR}`); console.log(colors.red(`## ${result.toUpperCase()} ##`));
console.log('Message:'); console.log('Message:');
console.log(` ${RED_COLOR}${message}${RESET_COLOR}`); console.log(` ${colors.red(message)}`);
if (error && error.stack) { if (error && error.stack) {
console.log('Stack:'); console.log('Stack:');
console.log(error.stack.split('\n').map(line => ' ' + line).join('\n')); console.log(padLines(error.stack, 2));
} }
console.log('WORKERS STATE'); console.log('WORKERS STATE');
const workerIds = Array.from(this._workersState.keys()); const workerIds = Array.from(this._workersState.keys());
@ -69,23 +66,25 @@ class Reporter {
const {isRunning, test} = this._workersState.get(workerId); const {isRunning, test} = this._workersState.get(workerId);
let description = ''; let description = '';
if (isRunning) if (isRunning)
description = `${YELLOW_COLOR}RUNNING${RESET_COLOR}`; description = colors.yellow('RUNNING');
else if (test.result === 'ok') else if (test.result === 'ok')
description = `${GREEN_COLOR}OK${RESET_COLOR}`; description = colors.green('OK');
else if (test.result === 'skipped') else if (test.result === 'skipped')
description = `${YELLOW_COLOR}SKIPPED${RESET_COLOR}`; description = colors.yellow('SKIPPED');
else if (test.result === 'failed') else if (test.result === 'failed')
description = `${RED_COLOR}FAILED${RESET_COLOR}`; description = colors.red('FAILED');
else if (test.result === 'crashed') else if (test.result === 'crashed')
description = `${RED_COLOR}CRASHED${RESET_COLOR}`; description = colors.red('CRASHED');
else if (test.result === 'timedout') else if (test.result === 'timedout')
description = `${RED_COLOR}TIMEDOUT${RESET_COLOR}`; description = colors.red('TIMEDOUT');
else if (test.result === 'terminated') else if (test.result === 'terminated')
description = `${MAGENTA_COLOR}TERMINATED${RESET_COLOR}`; description = colors.magenta('TERMINATED');
else else
description = `${RED_COLOR}<UNKNOWN>${RESET_COLOR}`; description = colors.red('<UNKNOWN>');
console.log(` ${workerId}: [${description}] ${test.fullName} (${formatTestLocation(test)})`); console.log(` ${workerId}: [${description}] ${test.fullName} (${formatLocation(test.location)})`);
} }
console.log('');
console.log('');
process.exitCode = 2; process.exitCode = 2;
} }
@ -105,24 +104,7 @@ class Reporter {
console.log('\nFailures:'); console.log('\nFailures:');
for (let i = 0; i < failedTests.length; ++i) { for (let i = 0; i < failedTests.length; ++i) {
const test = failedTests[i]; const test = failedTests[i];
console.log(`${i + 1}) ${test.fullName} (${formatTestLocation(test)})`); this._printVerboseTestResult(i + 1, test);
if (test.result === 'timedout') {
console.log(' Message:');
console.log(` ${RED_COLOR}Timeout Exceeded ${this._runner.timeout()}ms${RESET_COLOR}`);
} else if (test.result === 'crashed') {
console.log(' Message:');
console.log(` ${RED_COLOR}CRASHED${RESET_COLOR}`);
} else {
console.log(' Message:');
console.log(` ${RED_COLOR}${test.error.message || test.error}${RESET_COLOR}`);
console.log(' Stack:');
if (test.error.stack)
console.log(this._beautifyStack(test.error.stack));
}
if (test.output) {
console.log(' Output:');
console.log(test.output.split('\n').map(line => ' ' + line).join('\n'));
}
console.log(''); console.log('');
} }
} }
@ -132,12 +114,12 @@ class Reporter {
if (skippedTests.length > 0) { if (skippedTests.length > 0) {
console.log('\nSkipped:'); console.log('\nSkipped:');
skippedTests.slice(0, this._showSkippedTests).forEach((test, index) => { skippedTests.slice(0, this._showSkippedTests).forEach((test, index) => {
console.log(`${index + 1}) ${test.fullName} (${formatTestLocation(test)})`); console.log(`${index + 1}) ${test.fullName} (${formatLocation(test.location)})`);
}); });
} }
if (this._showSkippedTests < skippedTests.length) { if (this._showSkippedTests < skippedTests.length) {
console.log(''); console.log('');
console.log(`... and ${YELLOW_COLOR}${skippedTests.length - this._showSkippedTests}${RESET_COLOR} more skipped tests ...`); console.log(`... and ${colors.yellow(skippedTests.length - this._showSkippedTests)} more skipped tests ...`);
} }
} }
@ -151,7 +133,7 @@ class Reporter {
for (let i = 0; i < slowTests.length; ++i) { for (let i = 0; i < slowTests.length; ++i) {
const test = slowTests[i]; const test = slowTests[i];
const duration = test.endTimestamp - test.startTimestamp; const duration = test.endTimestamp - test.startTimestamp;
console.log(` (${i + 1}) ${YELLOW_COLOR}${duration / 1000}s${RESET_COLOR} - ${test.fullName} (${formatTestLocation(test)})`); console.log(` (${i + 1}) ${colors.yellow((duration / 1000) + 's')} - ${test.fullName} (${formatLocation(test.location)})`);
} }
} }
@ -160,39 +142,18 @@ class Reporter {
const okTestsLength = executedTests.length - failedTests.length - skippedTests.length; const okTestsLength = executedTests.length - failedTests.length - skippedTests.length;
let summaryText = ''; let summaryText = '';
if (failedTests.length || skippedTests.length) { if (failedTests.length || skippedTests.length) {
const summary = [`ok - ${GREEN_COLOR}${okTestsLength}${RESET_COLOR}`]; const summary = [`ok - ${colors.green(okTestsLength)}`];
if (failedTests.length) if (failedTests.length)
summary.push(`failed - ${RED_COLOR}${failedTests.length}${RESET_COLOR}`); summary.push(`failed - ${colors.red(failedTests.length)}`);
if (skippedTests.length) if (skippedTests.length)
summary.push(`skipped - ${YELLOW_COLOR}${skippedTests.length}${RESET_COLOR}`); summary.push(`skipped - ${colors.yellow(skippedTests.length)}`);
summaryText = `(${summary.join(', ')})`; summaryText = ` (${summary.join(', ')})`;
} }
console.log(`\nRan ${executedTests.length} ${summaryText} of ${tests.length} test(s)`); console.log(`\nRan ${executedTests.length}${summaryText} of ${tests.length} test${tests.length > 1 ? 's' : ''}`);
const milliseconds = Date.now() - this._timestamp; const milliseconds = Date.now() - this._timestamp;
const seconds = milliseconds / 1000; const seconds = milliseconds / 1000;
console.log(`Finished in ${YELLOW_COLOR}${seconds}${RESET_COLOR} seconds`); console.log(`Finished in ${colors.yellow(seconds)} seconds`);
}
_beautifyStack(stack) {
if (!this._projectFolder)
return stack;
const lines = stack.split('\n').map(line => ' ' + line);
// Find last stack line that include testrunner code.
let index = 0;
while (index < lines.length && !lines[index].includes(__dirname))
++index;
while (index < lines.length && lines[index].includes(__dirname))
++index;
if (index >= lines.length)
return stack;
const line = lines[index];
const fromIndex = line.lastIndexOf(this._projectFolder) + this._projectFolder.length;
let toIndex = line.lastIndexOf(')');
if (toIndex === -1)
toIndex = line.length;
lines[index] = line.substring(0, fromIndex) + YELLOW_COLOR + line.substring(fromIndex, toIndex) + RESET_COLOR + line.substring(toIndex);
return lines.join('\n');
} }
_onTestStarted(test, workerId) { _onTestStarted(test, workerId) {
@ -203,55 +164,92 @@ class Reporter {
this._workersState.set(workerId, {test, isRunning: false}); this._workersState.set(workerId, {test, isRunning: false});
if (this._verbose) { if (this._verbose) {
++this._testCounter; ++this._testCounter;
let prefix = `${this._testCounter})`; this._printVerboseTestResult(this._testCounter, test, workerId);
if (this._runner.parallel() > 1)
prefix += ` ${GRAY_COLOR}[worker = ${workerId}]${RESET_COLOR}`;
if (test.result === 'ok') {
console.log(`${prefix} ${GREEN_COLOR}[ OK ]${RESET_COLOR} ${test.fullName} (${formatTestLocation(test)})`);
} else if (test.result === 'terminated') {
console.log(`${prefix} ${MAGENTA_COLOR}[ TERMINATED ]${RESET_COLOR} ${test.fullName} (${formatTestLocation(test)})`);
} else if (test.result === 'crashed') {
console.log(`${prefix} ${RED_COLOR}[ CRASHED ]${RESET_COLOR} ${test.fullName} (${formatTestLocation(test)})`);
} else if (test.result === 'skipped') {
console.log(`${prefix} ${YELLOW_COLOR}[SKIP]${RESET_COLOR} ${test.fullName} (${formatTestLocation(test)})`);
} else if (test.result === 'failed') {
console.log(`${prefix} ${RED_COLOR}[FAIL]${RESET_COLOR} ${test.fullName} (${formatTestLocation(test)})`);
console.log(' Message:');
console.log(` ${RED_COLOR}${test.error.message || test.error}${RESET_COLOR}`);
console.log(' Stack:');
if (test.error.stack)
console.log(this._beautifyStack(test.error.stack));
if (test.output) {
console.log(' Output:');
console.log(test.output.split('\n').map(line => ' ' + line).join('\n'));
}
} else if (test.result === 'timedout') {
console.log(`${prefix} ${RED_COLOR}[TIME]${RESET_COLOR} ${test.fullName} (${formatTestLocation(test)})`);
console.log(' Message:');
console.log(` ${RED_COLOR}Timeout Exceeded ${this._runner.timeout()}ms${RESET_COLOR}`);
}
} else { } else {
if (test.result === 'ok') if (test.result === 'ok')
process.stdout.write(`${GREEN_COLOR}.${RESET_COLOR}`); process.stdout.write(colors.green('.'));
else if (test.result === 'skipped') else if (test.result === 'skipped')
process.stdout.write(`${YELLOW_COLOR}*${RESET_COLOR}`); process.stdout.write(colors.yellow('*'));
else if (test.result === 'failed') else if (test.result === 'failed')
process.stdout.write(`${RED_COLOR}F${RESET_COLOR}`); process.stdout.write(colors.red('F'));
else if (test.result === 'crashed') else if (test.result === 'crashed')
process.stdout.write(`${RED_COLOR}C${RESET_COLOR}`); process.stdout.write(colors.red('C'));
else if (test.result === 'terminated') else if (test.result === 'terminated')
process.stdout.write(`${MAGENTA_COLOR}.${RESET_COLOR}`); process.stdout.write(colors.magenta('.'));
else if (test.result === 'timedout') else if (test.result === 'timedout')
process.stdout.write(`${RED_COLOR}T${RESET_COLOR}`); process.stdout.write(colors.red('T'));
}
}
_printVerboseTestResult(resultIndex, test, workerId = undefined) {
let prefix = `${resultIndex})`;
if (this._runner.parallel() > 1 && workerId !== undefined)
prefix += ' ' + colors.gray(`[worker = ${workerId}]`);
if (test.result === 'ok') {
console.log(`${prefix} ${colors.green('[OK]')} ${test.fullName} (${formatLocation(test.location)})`);
} else if (test.result === 'terminated') {
console.log(`${prefix} ${colors.magenta('[TERMINATED]')} ${test.fullName} (${formatLocation(test.location)})`);
} else if (test.result === 'crashed') {
console.log(`${prefix} ${colors.red('[CRASHED]')} ${test.fullName} (${formatLocation(test.location)})`);
} else if (test.result === 'skipped') {
console.log(`${prefix} ${colors.yellow('[SKIP]')} ${test.fullName} (${formatLocation(test.location)})`);
} else if (test.result === 'timedout') {
console.log(`${prefix} ${colors.red(`[TIMEOUT ${test.timeout}ms]`)} ${test.fullName} (${formatLocation(test.location)})`);
} else if (test.result === 'failed') {
console.log(`${prefix} ${colors.red('[FAIL]')} ${test.fullName} (${formatLocation(test.location)})`);
if (test.error instanceof MatchError) {
let lines = this._filePathToLines.get(test.error.location.filePath);
if (!lines) {
try {
lines = fs.readFileSync(test.error.location.filePath, 'utf8').split('\n');
} catch (e) {
lines = [];
}
this._filePathToLines.set(test.error.location.filePath, lines);
}
const lineNumber = test.error.location.lineNumber;
if (lineNumber < lines.length) {
const lineNumberLength = (lineNumber + 1 + '').length;
const FROM = Math.max(test.location.lineNumber - 1, lineNumber - 5);
const snippet = lines.slice(FROM, lineNumber).map((line, index) => ` ${(FROM + index + 1 + '').padStart(lineNumberLength, ' ')} | ${line}`).join('\n');
const pointer = ` ` + ' '.repeat(lineNumberLength) + ' ' + '~'.repeat(test.error.location.columnNumber - 1) + '^';
console.log('\n' + snippet + '\n' + colors.grey(pointer) + '\n');
}
console.log(padLines(test.error.formatter(), 4));
console.log('');
} else {
console.log(' Message:');
console.log(` ${colors.red(test.error.message || test.error)}`);
if (test.error.stack) {
console.log(' Stack:');
let stack = test.error.stack;
// Highlight first test location, if any.
const match = stack.match(new RegExp(test.location.filePath + ':(\\d+):(\\d+)'));
if (match) {
const [, line, column] = match;
const fileName = `${test.location.fileName}:${line}:${column}`;
stack = stack.substring(0, match.index) + stack.substring(match.index).replace(fileName, colors.yellow(fileName));
}
console.log(padLines(stack, 4));
}
}
if (test.output) {
console.log(' Output:');
console.log(padLines(test.output, 4));
}
} }
} }
} }
function formatTestLocation(test) { function formatLocation(location) {
const location = test.location;
if (!location) if (!location)
return ''; return '';
return `${YELLOW_COLOR}${location.fileName}:${location.lineNumber}:${location.columnNumber}${RESET_COLOR}`; return colors.yellow(`${location.fileName}:${location.lineNumber}:${location.columnNumber}`);
}
function padLines(text, spaces = 0) {
const indent = ' '.repeat(spaces);
return text.split('\n').map(line => indent + line).join('\n');
} }
module.exports = Reporter; module.exports = Reporter;

View file

@ -27,6 +27,8 @@ class SourceMapSupport {
} }
async rewriteStackTraceWithSourceMaps(error) { async rewriteStackTraceWithSourceMaps(error) {
if (!error.stack || typeof error.stack !== 'string')
return;
const stackFrames = error.stack.split('\n'); const stackFrames = error.stack.split('\n');
for (let i = 0; i < stackFrames.length; ++i) { for (let i = 0; i < stackFrames.length; ++i) {
const stackFrame = stackFrames[i]; const stackFrame = stackFrames[i];

View file

@ -23,6 +23,7 @@ const Multimap = require('./Multimap');
const fs = require('fs'); const fs = require('fs');
const {SourceMapSupport} = require('./SourceMapSupport'); const {SourceMapSupport} = require('./SourceMapSupport');
const debug = require('debug'); const debug = require('debug');
const {getCallerLocation} = require('./utils');
const INFINITE_TIMEOUT = 2147483647; const INFINITE_TIMEOUT = 2147483647;
@ -41,7 +42,7 @@ class UserCallback {
}); });
this.timeout = timeout; this.timeout = timeout;
this.location = this._getLocation(); this.location = getCallerLocation(__filename);
} }
async run(...args) { async run(...args) {
@ -62,35 +63,6 @@ class UserCallback {
} }
} }
_getLocation() {
const error = new Error();
const stackFrames = error.stack.split('\n').slice(1);
// Find first stackframe that doesn't point to this file.
for (let frame of stackFrames) {
frame = frame.trim();
if (!frame.startsWith('at '))
return null;
if (frame.endsWith(')')) {
const from = frame.indexOf('(');
frame = frame.substring(from + 1, frame.length - 1);
} else {
frame = frame.substring('at '.length);
}
const match = frame.match(/^(.*):(\d+):(\d+)$/);
if (!match)
return null;
const filePath = match[1];
const lineNumber = parseInt(match[2], 10);
const columnNumber = parseInt(match[3], 10);
if (filePath === __filename)
continue;
const fileName = filePath.split(path.sep).pop();
return { fileName, filePath, lineNumber, columnNumber };
}
return null;
}
terminate() { terminate() {
this._terminateCallback(TerminatedError); this._terminateCallback(TerminatedError);
} }
@ -119,6 +91,7 @@ class Test {
this.declaredMode = declaredMode; this.declaredMode = declaredMode;
this._userCallback = new UserCallback(callback, timeout); this._userCallback = new UserCallback(callback, timeout);
this.location = this._userCallback.location; this.location = this._userCallback.location;
this.timeout = timeout;
// Test results // Test results
this.result = null; this.result = null;
@ -422,7 +395,10 @@ class TestRunner extends EventEmitter {
const runnableTests = this._runnableTests(); const runnableTests = this._runnableTests();
this.emit(TestRunner.Events.Started, runnableTests); this.emit(TestRunner.Events.Started, runnableTests);
this._runningPass = new TestPass(this, this._rootSuite, runnableTests, this._parallel, this._breakOnFailure); this._runningPass = new TestPass(this, this._rootSuite, runnableTests, this._parallel, this._breakOnFailure);
const termination = await this._runningPass.run(); const termination = await this._runningPass.run().catch(e => {
console.error(e);
throw e;
});
this._runningPass = null; this._runningPass = null;
const result = {}; const result = {};
if (termination) { if (termination) {

View file

@ -16,6 +16,6 @@
const TestRunner = require('./TestRunner'); const TestRunner = require('./TestRunner');
const Reporter = require('./Reporter'); const Reporter = require('./Reporter');
const Matchers = require('./Matchers'); const {Matchers} = require('./Matchers');
module.exports = { TestRunner, Reporter, Matchers }; module.exports = { TestRunner, Reporter, Matchers };

View file

@ -8,7 +8,6 @@ require('./testrunner.spec.js').addTests({testRunner, expect});
new Reporter(testRunner, { new Reporter(testRunner, {
verbose: process.argv.includes('--verbose'), verbose: process.argv.includes('--verbose'),
summary: true, summary: true,
projectFolder: require('path').join(__dirname, '..'),
showSlowTests: 0, showSlowTests: 0,
}); });
testRunner.run(); testRunner.run();

32
utils/testrunner/utils.js Normal file
View file

@ -0,0 +1,32 @@
const path = require('path');
module.exports = {
getCallerLocation: function(filename) {
const error = new Error();
const stackFrames = error.stack.split('\n').slice(1);
// Find first stackframe that doesn't point to this file.
for (let frame of stackFrames) {
frame = frame.trim();
if (!frame.startsWith('at '))
return null;
if (frame.endsWith(')')) {
const from = frame.indexOf('(');
frame = frame.substring(from + 1, frame.length - 1);
} else {
frame = frame.substring('at '.length);
}
const match = frame.match(/^(.*):(\d+):(\d+)$/);
if (!match)
return null;
const filePath = match[1];
const lineNumber = parseInt(match[2], 10);
const columnNumber = parseInt(match[3], 10);
if (filePath === __filename || filePath === filename)
continue;
const fileName = filePath.split(path.sep).pop();
return { fileName, filePath, lineNumber, columnNumber };
}
return null;
},
};