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:
matrix:
- nodejs_version: "8.16.0"
FLAKINESS_DASHBOARD_NAME: Appveyor Chromium (Win + node8)
FLAKINESS_DASHBOARD_PASSWORD:
secure: g66jP+j6C+hkXLutBV9fdxB5fRJgcQQzy93SgQzXUmcCl/RjkJwnzyHvX0xfCVnv
- nodejs_version: "12"
build: off
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
- npm install
- if "%nodejs_version%" == "8.16.0" (
npm run lint &&
npm run coverage &&
npm run test-doclint &&
npm run test-types
)
- npm run ctest
- npm run wtest
- npm run ftest

View file

@ -10,6 +10,8 @@ on:
env:
CI: true
# Force terminal colors. @see https://www.npmjs.com/package/colors
FORCE_COLOR: 1
jobs:
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
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
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"
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: BrowserType](#class-browsertype)
- [class: ChromiumBrowser](#class-chromiumbrowser)
- [class: ChromiumBrowserContext](#class-chromiumbrowsercontext)
- [class: ChromiumCoverage](#class-chromiumcoverage)
- [class: ChromiumSession](#class-chromiumsession)
- [class: ChromiumTarget](#class-chromiumtarget)
@ -184,7 +185,7 @@ Indicates that the browser is connected.
- `options` <[Object]>
- `ignoreHTTPSErrors` <?[boolean]> Whether to ignore HTTPS errors during navigation. Defaults to `false`.
- `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.
- `height` <[number]> page height in pixels.
- `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]>
- `ignoreHTTPSErrors` <?[boolean]> Whether to ignore HTTPS errors during navigation. Defaults to `false`.
- `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.
- `height` <[number]> page height in pixels.
- `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.
#### 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.
@ -759,7 +760,7 @@ To disable authentication, pass `null`.
#### 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.
- `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.
- 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]>
- 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.
- `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.
- 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]>
- 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.
- `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.
- 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.
- `value` <[string]> Value to fill for the `<input>`, `<textarea>` or `[contenteditable]` element.
- `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.
- 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)
- `selector` <[string]> A selector of an element to focus. If there are multiple elements satisfying the selector, the first will be focused.
- `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.
- 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]>
- 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.
- `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.
- 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`.
- `index` <[number]> Matches by the index.
- `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.
- 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]>
- 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.
- `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.
- 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.
- `options` <[Object]>
- `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.
- returns: <[Promise]>
@ -1424,9 +1425,9 @@ Shortcut for [page.mainFrame().type(selector, text[, options])](#frametypeselect
#### 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.
- `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.
- 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.
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])
- `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]>
- `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.
- 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]>
- 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.
- `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.
- 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]>
- 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.
- `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.
- 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.
- `value` <[string]> Value to fill for the `<input>`, `<textarea>` or `[contenteditable]` element.
- `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.
- 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)
- `selector` <[string]> A selector of an element to focus. If there are multiple elements satisfying the selector, the first will be focused.
- `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.
- 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]>
- 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.
- `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.
- 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`.
- `index` <[number]> Matches by the index.
- `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.
- 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]>
- 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.
- `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.
- 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.
- `options` <[Object]>
- `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.
- returns: <[Promise]>
@ -2086,9 +2087,9 @@ await frame.type('#mytextarea', 'World', {delay: 100}); // Types slower, like a
#### 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.
- `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.
- 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.
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-1)
- [elementHandle.boundingBox()](#elementhandleboundingbox)
- [elementHandle.check()](#elementhandlecheck)
- [elementHandle.check([options])](#elementhandlecheckoptions)
- [elementHandle.click([options])](#elementhandleclickoptions)
- [elementHandle.contentFrame()](#elementhandlecontentframe)
- [elementHandle.dblclick([options])](#elementhandledblclickoptions)
@ -2279,8 +2280,7 @@ ElementHandle instances can be used as arguments in [`page.$eval()`](#pageevalse
- [elementHandle.toString()](#elementhandletostring)
- [elementHandle.tripleclick([options])](#elementhandletripleclickoptions)
- [elementHandle.type(text[, options])](#elementhandletypetext-options)
- [elementHandle.uncheck()](#elementhandleuncheck)
- [elementHandle.visibleRatio()](#elementhandlevisibleratio)
- [elementHandle.uncheck([options])](#elementhandleuncheckoptions)
<!-- GEN:stop -->
<!-- GEN:toc-extends-JSHandle -->
- [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.
#### 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.
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]>
- 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.
- `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.
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]>
- 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.
- `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.
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]>
- 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.
- `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.
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()
- 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.
@ -2494,6 +2503,8 @@ This method expects `elementHandle` to point to an [input element](https://devel
- x <[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.
- `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.
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');
```
#### elementHandle.uncheck()
- returns: <[Promise]> Promise which resolves when the element is successfully checked. Promise gets rejected if the operation fails.
#### elementHandle.uncheck([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 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.
#### 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
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`.
- `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`.
- 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.
@ -3524,16 +3533,9 @@ await browser.stopTracing();
```
<!-- GEN:toc -->
- [event: 'targetchanged'](#event-targetchanged)
- [event: 'targetcreated'](#event-targetcreated)
- [event: 'targetdestroyed'](#event-targetdestroyed)
- [chromiumBrowser.browserTarget()](#chromiumbrowserbrowsertarget)
- [chromiumBrowser.pageTarget(page)](#chromiumbrowserpagetargetpage)
- [chromiumBrowser.serviceWorker(target)](#chromiumbrowserserviceworkertarget)
- [chromiumBrowser.startTracing(page, [options])](#chromiumbrowserstarttracingpage-options)
- [chromiumBrowser.stopTracing()](#chromiumbrowserstoptracing)
- [chromiumBrowser.targets(context)](#chromiumbrowsertargetscontext)
- [chromiumBrowser.waitForTarget(predicate[, options])](#chromiumbrowserwaitfortargetpredicate-options)
<!-- GEN:stop -->
<!-- GEN:toc-extends-Browser -->
- [event: 'disconnected'](#event-disconnected)
@ -3544,43 +3546,11 @@ await browser.stopTracing();
- [browser.newPage([options])](#browsernewpageoptions)
<!-- 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()
- returns: <[Target]>
- returns: <[ChromiumTarget]>
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])
- `page` <[Page]> Optional, if specified, tracing includes screenshots of the given page.
- `options` <[Object]>
@ -3594,25 +3564,83 @@ Only one trace can be active at a time per browser.
#### chromiumBrowser.stopTracing()
- returns: <[Promise]<[Buffer]>> Promise which resolves to buffer with trace data.
#### chromiumBrowser.targets(context)
- `context` <[BrowserContext]> Optional, if specified, only targets from this context are returned.
- returns: <[Array]<[Target]>>
### class: ChromiumBrowserContext
An array of all active targets inside the Browser. In case of multiple browser contexts,
the method will return an array with all the targets in all browser contexts.
* extends: [BrowserContext]
#### chromiumBrowser.waitForTarget(predicate[, options])
- `predicate` <[function]\([Target]\):[boolean]> A function to be run for every target
Chromium-specific features including targets, service worker support, etc.
```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]>
- `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`:
```js
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
@ -3727,6 +3755,7 @@ to send messages.
- [chromiumTarget.createCDPSession()](#chromiumtargetcreatecdpsession)
- [chromiumTarget.opener()](#chromiumtargetopener)
- [chromiumTarget.page()](#chromiumtargetpage)
- [chromiumTarget.serviceWorker()](#chromiumtargetserviceworker)
- [chromiumTarget.type()](#chromiumtargettype)
- [chromiumTarget.url()](#chromiumtargeturl)
<!-- GEN:stop -->
@ -3743,7 +3772,7 @@ The browser context the target belongs to.
Creates a Chrome Devtools Protocol session attached to the target.
#### chromiumTarget.opener()
- returns: <?[Target]>
- returns: <?[ChromiumTarget]>
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`.
#### chromiumTarget.serviceWorker()
- returns: <[Promise]<?[Worker]>>
Attaches to the service worker target. If the target is not of type `"service_worker"`, returns `null`.
#### chromiumTarget.type()
- 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"
[ChildProcess]: https://nodejs.org/api/child_process.html "ChildProcess"
[ChromiumBrowser]: #class-chromiumbrowser "ChromiumBrowser"
[ChromiumBrowserContext]: #class-chromiumbrowsercontext "ChromiumBrowserContext"
[ChromiumSession]: #class-chromiumsession "ChromiumSession"
[ChromiumTarget]: #class-chromiumtarget "ChromiumTarget"
[ConsoleMessage]: #class-consolemessage "ConsoleMessage"
@ -3893,7 +3928,6 @@ const { chromium } = require('playwright');
[Response]: #class-response "Response"
[Selectors]: #class-selectors "Selectors"
[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"
[UIEvent.detail]: https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail "UIEvent.detail"
[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
* [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-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

View file

@ -21,7 +21,7 @@
"wunit": "npm run wtest",
"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": "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",
"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",
@ -62,6 +62,7 @@
"@types/ws": "^6.0.1",
"@typescript-eslint/eslint-plugin": "^2.6.1",
"@typescript-eslint/parser": "^2.6.1",
"colors": "^1.4.0",
"commonmark": "^0.28.1",
"cross-env": "^5.0.5",
"eslint": "^6.6.0",

View file

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

View file

@ -19,26 +19,8 @@ import { Page } from './page';
import * as network from './network';
import * as types from './types';
import { helper } from './helper';
import * as platform from './platform';
import { Events } from './events';
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 = {
viewport?: types.Viewport | null,
ignoreHTTPSErrors?: boolean,
@ -51,106 +33,44 @@ export type BrowserContextOptions = {
permissions?: { [key: string]: string[] };
};
export class BrowserContext extends platform.EventEmitter {
private readonly _delegate: BrowserContextDelegate;
readonly _options: BrowserContextOptions;
export interface BrowserContext {
setDefaultNavigationTimeout(timeout: number): void;
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;
private _closed = false;
readonly _options: BrowserContextOptions;
}
constructor(delegate: BrowserContextDelegate, options: BrowserContextOptions) {
super();
this._delegate = delegate;
this._timeoutSettings = new TimeoutSettings();
this._options = { ...options };
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);
export function assertBrowserContextIsNotOwned(context: BrowserContext) {
const pages = context._existingPages();
for (const page of pages) {
if (page._ownedContext)
throw new Error('Please use browser.newContext() for multi-page scripts that share the context.');
}
}
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 };
result.accuracy = result.accuracy || 0;
const { longitude, latitude, accuracy } = result;

View file

@ -18,9 +18,9 @@
import { Events } from './events';
import { Events as CommonEvents } from '../events';
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 { Page, Worker } from '../page';
import { Page } from '../page';
import { CRTarget } from './crTarget';
import { Protocol } from './protocol';
import { CRPage } from './crPage';
@ -30,12 +30,13 @@ import * as types from '../types';
import * as platform from '../platform';
import { readProtocolStream } from './crProtocolHelper';
import { ConnectionTransport, SlowMoTransport } from '../transport';
import { TimeoutSettings } from '../timeoutSettings';
export class CRBrowser extends platform.EventEmitter implements Browser {
_connection: CRConnection;
_client: CRSession;
readonly _defaultContext: BrowserContext;
private _contexts = new Map<string, BrowserContext>();
readonly _defaultContext: CRBrowserContext;
readonly _contexts = new Map<string, CRBrowserContext>();
_targets = new Map<string, CRTarget>();
private _tracingRecording = false;
@ -54,9 +55,9 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
this._connection = connection;
this._client = connection.rootSession;
this._defaultContext = this._createBrowserContext(null, {});
this._defaultContext = new CRBrowserContext(this, null, validateBrowserContextOptions({}));
this._connection.on(ConnectionEvents.Disconnected, () => {
for (const context of this.contexts())
for (const context of this._contexts.values())
context._browserClosed();
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));
}
_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> {
BrowserContext.validateOptions(options);
options = validateBrowserContextOptions(options);
const { browserContextId } = await this._client.send('Target.createBrowserContext');
const context = this._createBrowserContext(browserContextId, options);
const context = new CRBrowserContext(this, browserContextId, options);
await context._initialize();
this._contexts.set(browserContextId, context);
return context;
@ -181,7 +93,7 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
this._targets.set(event.targetInfo.targetId, target);
if (target._isInitialized || await target._initializedPromise)
this.emit(Events.CRBrowser.TargetCreated, target);
context.emit(Events.CRBrowserContext.TargetCreated, target);
}
async _targetDestroyed(event: { targetId: string; }) {
@ -190,7 +102,7 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
this._targets.delete(event.targetId);
target._didClose();
if (await target._initializedPromise)
this.emit(Events.CRBrowser.TargetDestroyed, target);
target.context().emit(Events.CRBrowserContext.TargetDestroyed, target);
}
_targetInfoChanged(event: Protocol.Target.targetInfoChangedPayload) {
@ -200,7 +112,7 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
const wasInitialized = target._isInitialized;
target._targetInfoChanged(event.targetInfo);
if (wasInitialized && previousURL !== target.url())
this.emit(Events.CRBrowser.TargetChanged, target);
target.context().emit(Events.CRBrowserContext.TargetChanged, target);
}
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);
}
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() {
const disconnected = new Promise(f => this._connection.once(ConnectionEvents.Disconnected, f));
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')!;
}
serviceWorker(target: CRTarget): Promise<Worker | null> {
return target._worker();
}
async startTracing(page: Page | undefined, options: { path?: string; screenshots?: boolean; categories?: string[]; } = {}) {
assert(!this._tracingRecording, 'Cannot start recording trace while already recording trace.');
this._tracingClient = page ? (page._delegate as CRPage)._client : this._client;
@ -291,15 +173,6 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
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 {
return !this._connection._closed;
}
@ -308,3 +181,165 @@ export class CRBrowser extends platform.EventEmitter implements Browser {
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)
message.sessionId = sessionId;
const data = JSON.stringify(message);
this._debugProtocol('SEND ► ' + data);
this._debugProtocol('SEND ► ' + (rewriteInjectedScriptEvaluationLog(message) || data));
this._transport.send(data);
return id;
}
@ -192,3 +192,10 @@ function rewriteError(error: Error, message: string): Error {
error.message = message;
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.
*/
import { CRBrowser } from './crBrowser';
import { BrowserContext } from '../browserContext';
import { CRBrowser, CRBrowserContext } from './crBrowser';
import { CRSession, CRSessionEvents } from './crConnection';
import { Events } from '../events';
import { Page, Worker } from '../page';
@ -30,7 +29,7 @@ const targetSymbol = Symbol('target');
export class CRTarget {
private _targetInfo: Protocol.Target.TargetInfo;
private readonly _browser: CRBrowser;
private readonly _browserContext: BrowserContext;
private readonly _browserContext: CRBrowserContext;
readonly _targetId: string;
private _sessionFactory: () => Promise<CRSession>;
private _pagePromise: Promise<Page> | null = null;
@ -47,7 +46,7 @@ export class CRTarget {
constructor(
browser: CRBrowser,
targetInfo: Protocol.Target.TargetInfo,
browserContext: BrowserContext,
browserContext: CRBrowserContext,
sessionFactory: () => Promise<CRSession>) {
this._targetInfo = targetInfo;
this._browser = browser;
@ -91,8 +90,8 @@ export class CRTarget {
return this._pagePromise;
}
async _worker(): Promise<Worker | null> {
if (this._targetInfo.type !== 'service_worker' && this._targetInfo.type !== 'shared_worker')
async serviceWorker(): Promise<Worker | null> {
if (this._targetInfo.type !== 'service_worker')
return null;
if (!this._workerPromise) {
// TODO(einbinder): Make workers send their console logs.
@ -120,7 +119,7 @@ export class CRTarget {
return 'other';
}
context(): BrowserContext {
context(): CRBrowserContext {
return this._browserContext;
}

View file

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

View file

@ -230,10 +230,17 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
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;
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();
if (waitFor)
await this._waitForHitTargetAt(point, options);
let restoreModifiers: input.Modifier[] | undefined;
if (options && 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);
}
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);
}
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);
}
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);
}
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);
}
@ -402,19 +409,20 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
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._page.keyboard.press(key, options);
}
async check() {
await this._setChecked(true);
async check(options?: types.WaitForOptions) {
await this._setChecked(true, options);
}
async uncheck() {
await this._setChecked(false);
async uncheck(options?: types.WaitForOptions) {
await this._setChecked(false, options);
}
private async _setChecked(state: boolean) {
private async _setChecked(state: boolean, options?: types.WaitForOptions) {
const isCheckboxChecked = async (): Promise<boolean> => {
return this._evaluateInUtility((node: 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)
return;
await this.click();
await this.click(options);
if (await isCheckboxChecked() !== state)
throw new Error('Unable to click checkbox');
}
@ -479,23 +487,56 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return result;
}
visibleRatio(): Promise<number> {
return this._evaluateInUtility(async (node: Node) => {
if (node.nodeType !== Node.ELEMENT_NODE)
throw new Error('Node is not of type HTMLElement');
const element = node as Element;
const visibleRatio = await new Promise<number>(resolve => {
const observer = new IntersectionObserver(entries => {
resolve(entries[0].intersectionRatio);
observer.disconnect();
});
observer.observe(element);
// Firefox doesn't call IntersectionObserver callback unless
// there are rafs.
requestAnimationFrame(() => {});
async _waitForStablePosition(options: types.TimeoutOptions = {}): Promise<void> {
const context = await this._context.frame._utilityContext();
const stablePromise = context.evaluate((injected: Injected, node: Node, timeout: number) => {
if (!node.isConnected)
throw new Error('Element is not attached to the DOM');
const element = node.nodeType === Node.ELEMENT_NODE ? (node as Element) : node.parentElement;
if (!element)
throw new Error('Element is not attached to the DOM');
let lastRect: types.Rect | undefined;
let counter = 0;
return injected.poll('raf', undefined, timeout, () => {
// First raf happens in the same animation frame as evaluation, so it does not produce
// 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 function waitForFunctionTask(selector: string | undefined, pageFunction: Function | string, options: types.WaitForFunctionOptions, ...args: any[]) {
const { polling = 'raf' } = options;
function assertPolling(polling: types.Polling) {
if (helper.isString(polling))
assert(polling === 'raf' || polling === 'mutation', 'Unknown polling option: ' + polling);
else if (helper.isNumber(polling))
assert(polling > 0, 'Cannot poll with non-positive interval: ' + polling);
else
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)';
if (selector !== undefined)
selector = normalizeSelector(selector);
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);
if (polling === 'raf')
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 {
return injected.poll(polling, selector, timeout, (element: Element | undefined): any => {
if (selector === undefined)
return innerPredicate(...args);
return innerPredicate(element, ...args);
}
});
}, await context._injected(), selector, predicateBody, polling, options.timeout || 0, ...args);
}
export function waitForSelectorTask(selector: string, visibility: types.Visibility, timeout: number): Task {
return async (context: FrameExecutionContext) => {
selector = normalizeSelector(selector);
return context.evaluateHandle((injected: Injected, selector: string, visibility: types.Visibility, timeout: number) => {
if (visibility !== 'any')
return injected.pollRaf(selector, predicate, timeout);
return injected.pollMutation(selector, predicate, timeout);
function predicate(element: Element | undefined): Element | boolean {
if (!element)
return visibility === 'hidden';
if (visibility === 'any')
return element;
return injected.isVisible(element) === (visibility === 'visible') ? element : false;
}
}, await context._injected(), selector, visibility, timeout);
};
selector = normalizeSelector(selector);
return async (context: FrameExecutionContext) => context.evaluateHandle((injected: Injected, selector: string, visibility: types.Visibility, timeout: number) => {
const polling = visibility === 'any' ? 'mutation' : 'raf';
return injected.poll(polling, selector, timeout, (element: Element | undefined): Element | boolean => {
if (!element)
return visibility === 'hidden';
if (visibility === 'any')
return element;
return injected.isVisible(element) === (visibility === 'visible') ? element : false;
});
}, await context._injected(), selector, visibility, timeout);
}
export const setFileInputFunction = async (element: HTMLInputElement, payloads: types.FilePayload[]) => {
@ -571,4 +605,5 @@ export const setFileInputFunction = async (element: HTMLInputElement, payloads:
dt.items.add(file);
element.files = dt.files;
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 { BrowserContext, BrowserContextOptions } from '../browserContext';
import { BrowserContext, BrowserContextOptions, validateBrowserContextOptions, assertBrowserContextIsNotOwned } from '../browserContext';
import { Events } from '../events';
import { assert, helper, RegisteredListener, debugError } from '../helper';
import * as network from '../network';
@ -27,12 +27,13 @@ import { FFPage } from './ffPage';
import * as platform from '../platform';
import { Protocol } from './protocol';
import { ConnectionTransport, SlowMoTransport } from '../transport';
import { TimeoutSettings } from '../timeoutSettings';
export class FFBrowser extends platform.EventEmitter implements Browser {
_connection: FFConnection;
_targets: Map<string, Target>;
readonly _defaultContext: BrowserContext;
private _contexts: Map<string, BrowserContext>;
readonly _contexts: Map<string, FFBrowserContext>;
private _eventListeners: RegisteredListener[];
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._targets = new Map();
this._defaultContext = this._createBrowserContext(null, {});
this._defaultContext = new FFBrowserContext(this, null, validateBrowserContextOptions({}));
this._contexts = new Map();
this._connection.on(ConnectionEvents.Disconnected, () => {
for (const context of this.contexts())
for (const context of this._contexts.values())
context._browserClosed();
this.emit(Events.Browser.Disconnected);
});
@ -67,13 +68,24 @@ export class FFBrowser extends platform.EventEmitter implements Browser {
}
async newContext(options: BrowserContextOptions = {}): Promise<BrowserContext> {
const viewport = options.viewport ? {
viewportSize: { width: options.viewport.width, height: options.viewport.height },
isMobile: !!options.viewport.isMobile,
deviceScaleFactor: options.viewport.deviceScaleFactor || 1,
hasTouch: !!options.viewport.isMobile,
} : undefined;
const {browserContextId} = await this._connection.send('Target.createBrowserContext', {
options = validateBrowserContextOptions(options);
let viewport;
if (options.viewport) {
viewport = {
viewportSize: { width: options.viewport.width, height: options.viewport.height },
isMobile: !!options.viewport.isMobile,
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,
bypassCSP: options.bypassCSP,
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.
if (options.ignoreHTTPSErrors)
await this._connection.send('Browser.setIgnoreHTTPSErrors', { enabled: true });
const context = this._createBrowserContext(browserContextId, options);
const context = new FFBrowserContext(this, browserContextId, options);
await context._initialize();
this._contexts.set(browserContextId, context);
return context;
@ -166,82 +178,6 @@ export class FFBrowser extends platform.EventEmitter implements Browser {
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) {
this._connection._debugProtocol = debugFunction;
}
@ -316,3 +252,116 @@ class Target {
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) {
message = JSON.stringify(message);
this._debugProtocol('SEND ► ' + message);
this._transport.send(message);
const data = JSON.stringify(message);
this._debugProtocol('SEND ► ' + (rewriteInjectedScriptEvaluationLog(message) || data));
this._transport.send(data);
}
async _onMessage(message: string) {
@ -226,3 +226,10 @@ function rewriteError(error: Error, message: string): Error {
error.message = message;
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 dom from './dom';
import * as network from './network';
import * as input from './input';
import { helper, assert, RegisteredListener } from './helper';
import { ClickOptions, MultiClickOptions, PointerActionOptions } from './input';
import { TimeoutError } from './errors';
import { Events } from './events';
import { Page } from './page';
@ -52,7 +52,6 @@ export type GotoResult = {
export type LifecycleEvent = 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2';
const kLifecycleEvents: Set<LifecycleEvent> = new Set(['load', 'domcontentloaded', 'networkidle0', 'networkidle2']);
export type WaitForOptions = types.TimeoutOptions & { waitFor?: types.Visibility | 'nowait' };
type ConsoleTagHandler = () => void;
export class FrameManager {
@ -428,9 +427,10 @@ export class Frame {
let resolve: (error: Error|void) => void;
const promise = new Promise<Error|void>(x => resolve = x);
const watch = (documentId: string, error?: Error) => {
if (documentId !== expectedDocumentId)
return resolve(new Error('Navigation interrupted by another one'));
resolve(error);
if (documentId === expectedDocumentId)
resolve(error);
else if (!error)
resolve(new Error('Navigation interrupted by another one'));
};
const dispose = () => this._documentWatchers.delete(watch);
this._documentWatchers.add(watch);
@ -780,43 +780,43 @@ export class Frame {
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);
await handle.click(options);
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);
await handle.dblclick(options);
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);
await handle.tripleclick(options);
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);
await handle.fill(value);
await handle.dispose();
}
async focus(selector: string, options?: WaitForOptions) {
async focus(selector: string, options?: types.WaitForOptions) {
const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options);
await handle.focus();
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);
await handle.hover(options);
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 values = value === undefined ? [] : Array.isArray(value) ? value : [value];
const result = await handle.select(...values);
@ -824,21 +824,21 @@ export class Frame {
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);
await handle.type(text, options);
await handle.dispose();
}
async check(selector: string, options?: WaitForOptions) {
async check(selector: string, options?: types.WaitForOptions) {
const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options);
await handle.check();
await handle.check(options);
await handle.dispose();
}
async uncheck(selector: string, options?: WaitForOptions) {
async uncheck(selector: string, options?: types.WaitForOptions) {
const handle = await this._optionallyWaitForSelectorInUtilityContext(selector, options);
await handle.uncheck();
await handle.uncheck(options);
await handle.dispose();
}
@ -852,13 +852,15 @@ export class Frame {
return Promise.reject(new Error('Unsupported target type: ' + (typeof selectorOrFunctionOrTimeout)));
}
private async _optionallyWaitForSelectorInUtilityContext(selector: string, options: WaitForOptions | undefined): Promise<dom.ElementHandle<Element>> {
const { timeout = this._page._timeoutSettings.timeout(), waitFor = 'visible' } = (options || {});
private async _optionallyWaitForSelectorInUtilityContext(selector: string, options: types.WaitForOptions | undefined): Promise<dom.ElementHandle<Element>> {
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>;
if (waitFor !== 'nowait') {
const maybeHandle = await this._waitForSelectorInUtilityContext(selector, waitFor, timeout);
if (waitFor) {
const maybeHandle = await this._waitForSelectorInUtilityContext(selector, 'any', timeout);
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;
} else {
const context = await this._context('utility');
@ -874,7 +876,7 @@ export class Frame {
if (waitFor === 'visible' || waitFor === 'hidden' || waitFor === 'any')
visibility = waitFor;
else
throw new Error(`Unsupported waitFor option "${waitFor}"`);
throw new Error(`Unsupported visibility option "${waitFor}"`);
const task = dom.waitForSelectorTask(selector, visibility, timeout);
const result = await this._scheduleRerunnableTask(task, 'utility', timeout, `selector "${selectorToString(selector, visibility)}"`);
if (!result.asElement()) {

View file

@ -102,6 +102,14 @@ class Helper {
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(
emitter: platform.EventEmitterType,
eventName: (string | symbol),

View file

@ -145,7 +145,7 @@ class Injected {
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;
if (timeout)
setTimeout(() => timedOut = true, timeout);
@ -178,7 +178,7 @@ class Injected {
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;
if (timeout)
setTimeout(() => timedOut = true, timeout);
@ -203,7 +203,7 @@ class Injected {
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;
if (timeout)
setTimeout(() => timedOut = true, timeout);
@ -226,6 +226,14 @@ class Injected {
onTimeout();
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;

View file

@ -485,43 +485,43 @@ export class Page extends platform.EventEmitter {
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);
}
async dblclick(selector: string, options?: frames.WaitForOptions & input.MultiClickOptions) {
async dblclick(selector: string, options?: input.MultiClickOptions & types.WaitForOptions) {
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);
}
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);
}
async focus(selector: string, options?: frames.WaitForOptions) {
async focus(selector: string, options?: types.WaitForOptions) {
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);
}
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);
}
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);
}
async check(selector: string, options?: frames.WaitForOptions) {
async check(selector: string, options?: types.WaitForOptions) {
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);
}

View file

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

View file

@ -68,7 +68,7 @@ export class Chromium implements BrowserType {
const { timeout = 30000 } = options || {};
const { browserServer, transport } = await this._launchServer(options, 'persistent', userDataDir);
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.
const browserContext = browser._defaultContext;
browserContext.close = () => browserServer.close();

View file

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

View file

@ -16,7 +16,7 @@
*/
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 * as network from '../network';
import { Page } from '../page';
@ -27,15 +27,16 @@ import { Protocol } from './protocol';
import { WKConnection, WKSession, kPageProxyMessageReceived, PageProxyMessageReceivedPayload } from './wkConnection';
import { WKPageProxy } from './wkPageProxy';
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';
export class WKBrowser extends platform.EventEmitter implements Browser {
private readonly _connection: WKConnection;
private readonly _browserSession: WKSession;
readonly _browserSession: WKSession;
readonly _defaultContext: BrowserContext;
private readonly _contexts = new Map<string, BrowserContext>();
private readonly _pageProxies = new Map<string, WKPageProxy>();
readonly _contexts = new Map<string, WKBrowserContext>();
readonly _pageProxies = new Map<string, WKPageProxy>();
private readonly _eventListeners: RegisteredListener[];
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._browserSession = this._connection.browserSession;
this._defaultContext = this._createBrowserContext(undefined, {});
this._defaultContext = new WKBrowserContext(this, undefined, validateBrowserContextOptions({}));
this._eventListeners = [
helper.addEventListener(this._browserSession, 'Browser.pageProxyCreated', this._onPageProxyCreated.bind(this)),
@ -64,7 +65,7 @@ export class WKBrowser extends platform.EventEmitter implements Browser {
}
_onDisconnect() {
for (const context of this.contexts())
for (const context of this._contexts.values())
context._browserClosed();
for (const pageProxy of this._pageProxies.values())
pageProxy.dispose();
@ -73,13 +74,10 @@ export class WKBrowser extends platform.EventEmitter implements Browser {
}
async newContext(options: BrowserContextOptions = {}): Promise<BrowserContext> {
options = validateBrowserContextOptions(options);
const { browserContextId } = await this._browserSession.send('Browser.createContext');
options.userAgent = options.userAgent || DEFAULT_USER_AGENT;
const context = this._createBrowserContext(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] });
const context = new WKBrowserContext(this, browserContextId, options);
await context._initialize();
this._contexts.set(browserContextId, context);
return context;
@ -166,81 +164,125 @@ export class WKBrowser extends platform.EventEmitter implements Browser {
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) {
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) {
message = JSON.stringify(message);
this._debugFunction('SEND ► ' + message);
this._transport.send(message);
const data = JSON.stringify(message);
this._debugFunction('SEND ► ' + (rewriteInjectedScriptEvaluationLog(message) || data));
this._transport.send(data);
}
private _dispatchMessage(message: string) {
@ -177,3 +177,10 @@ export function rewriteError(error: Error, message: string): Error {
export function isSwappedOutError(e: Error) {
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.pageY = undefined;
window.shiftKey = undefined;
window.pageX = undefined;
window.pageY = undefined;
document.querySelector('button').addEventListener('click', e => {
result = 'Clicked';
offsetX = e.offsetX;

View file

@ -26,28 +26,29 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;
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);
const context = await newContext();
const context = await browser.newContext();
expect(browser.contexts().length).toBe(1);
expect(browser.contexts().indexOf(context) !== -1).toBe(true);
await context.close();
expect(browser.contexts().length).toBe(0);
});
it('window.open should use parent tab context', async function({newContext, server}) {
const context = await newContext();
it('window.open should use parent tab context', async function({browser, server}) {
const context = await browser.newContext();
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE);
const [popupTarget] = await Promise.all([
const [popup] = await Promise.all([
utils.waitEvent(page, 'popup'),
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.
const context1 = await newContext();
const context2 = await newContext();
const context1 = await browser.newContext();
const context2 = await browser.newContext();
expect((await context1.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);
});
it('should propagate default viewport to the page', async({ newPage }) => {
const page = await newPage({ viewport: { width: 456, height: 789 } });
it('should propagate default viewport to the page', async({ browser }) => {
const context = await browser.newContext({ viewport: { width: 456, height: 789 } });
const page = await context.newPage();
expect(page.viewportSize().width).toBe(456);
expect(page.viewportSize().height).toBe(789);
expect(await page.evaluate('window.innerWidth')).toBe(456);
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 context = await newContext({ viewport });
const context = await browser.newContext({ viewport });
viewport.width = 567;
const page = await context.newPage();
expect(page.viewportSize().width).toBe(456);
expect(page.viewportSize().height).toBe(789);
expect(await page.evaluate('window.innerWidth')).toBe(456);
expect(await page.evaluate('window.innerHeight')).toBe(789);
await context.close();
});
});
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');
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([
server.waitForRequest('/empty.html'),
page.goto(server.EMPTY_PAGE),
]);
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');
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([
server.waitForRequest('/empty.html'),
utils.attachFrame(page, 'frame1', server.EMPTY_PAGE),
]);
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');
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');
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 context = await newContext(options);
const context = await browser.newContext(options);
options.userAgent = 'wrong';
const page = await context.newPage();
const [request] = await Promise.all([
@ -158,50 +174,60 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
page.goto(server.EMPTY_PAGE),
]);
expect(request.headers['user-agent']).toBe('foobar');
await context.close();
});
});
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.
{
const page = await newPage();
const context = await browser.newContext();
const page = await context.newPage();
await page.goto(server.PREFIX + '/csp.html');
await page.addScriptTag({content: 'window.__injected = 42;'}).catch(e => void e);
expect(await page.evaluate(() => window.__injected)).toBe(undefined);
await context.close();
}
// 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.addScriptTag({content: 'window.__injected = 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.
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.addScriptTag({content: 'window.__injected = 42;'}).catch(e => void e);
expect(await page.evaluate(() => window.__injected)).toBe(undefined);
await context.close();
}
// 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.addScriptTag({content: 'window.__injected = 42;'});
expect(await page.evaluate(() => window.__injected)).toBe(42);
await context.close();
}
});
it('should bypass after cross-process navigation', async({newPage, server}) => {
const page = await newPage({ bypassCSP: true });
it('should bypass after cross-process navigation', async({browser, server}) => {
const context = await browser.newContext({ bypassCSP: true });
const page = await context.newPage();
await page.goto(server.PREFIX + '/csp.html');
await page.addScriptTag({content: 'window.__injected = 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.addScriptTag({content: 'window.__injected = 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.
{
const page = await newPage();
const context = await browser.newContext();
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE);
const frame = await utils.attachFrame(page, 'frame1', server.PREFIX + '/csp.html');
await frame.addScriptTag({content: 'window.__injected = 42;'}).catch(e => void e);
expect(await frame.evaluate(() => window.__injected)).toBe(undefined);
await context.close();
}
// 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);
const frame = await utils.attachFrame(page, 'frame1', server.PREFIX + '/csp.html');
await frame.addScriptTag({content: 'window.__injected = 42;'}).catch(e => void e);
expect(await frame.evaluate(() => window.__injected)).toBe(42);
await context.close();
}
});
});
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>');
let error = null;
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');
else
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>');
expect(await page.evaluate('something')).toBe('forbidden');
await context.close();
}
});
it('should be able to navigate after disabling javascript', async({newPage, server}) => {
const page = await newPage({ javaScriptEnabled: false });
it('should be able to navigate after disabling javascript', async({browser, server}) => {
const context = await browser.newContext({ javaScriptEnabled: false });
const page = await context.newPage();
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;
describe('Target', function() {
it('Chromium.targets should return all of the targets', async({page, server, browser}) => {
// The pages will be the testing page and the original newtab page
const targets = browser.targets();
expect(targets.some(target => target.type() === 'page' &&
target.url() === 'about:blank')).toBeTruthy('Missing blank page');
expect(targets.some(target => target.type() === 'browser')).toBeTruthy('Missing browser target');
it('ChromiumBrowserContext.targets should return all of the targets', async({page, server, browser}) => {
const second = await page.context().newPage();
await second.goto(server.EMPTY_PAGE);
const targets = page.context().targets();
// The pages will be the testing page from the harness and the one created here.
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}) => {
// The pages will be the testing page
it('BrowserContext.pages should return all of the pages', async({page, server, context}) => {
const second = await page.context().newPage();
const allPages = await context.pages();
expect(allPages.length).toBe(1);
expect(allPages.length).toBe(2);
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}) => {
const targets = browser.targets();
const browserTarget = targets.find(target => target.type() === 'browser');
expect(browserTarget).toBe(browser.browserTarget());
it('should report browser target', async({browser}) => {
expect(browser.browserTarget()).toBeTruthy();
});
it('should report when a new page is created and closed', async({browser, page, server, context}) => {
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'),
]);
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(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();
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).not.toContain(otherPage);
});
it('should report when a service worker is created and destroyed', async({browser, page, server, context}) => {
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');
expect((await createdTarget).type()).toBe('service_worker');
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()));
expect(await destroyedTarget).toBe(await createdTarget);
});
it('should create a worker from a service worker', async({browser, page, server, context}) => {
await page.goto(server.PREFIX + '/serviceworkers/empty/sw.html');
const target = await browser.waitForTarget(target => target.type() === 'service_worker');
const worker = await browser.serviceWorker(target);
const target = await page.context().waitForTarget(target => target.type() === 'service_worker');
const worker = await target.serviceWorker();
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.evaluate(() => {
new SharedWorker('data:text/javascript,console.log("hi")');
});
const target = await browser.waitForTarget(target => target.type() === 'shared_worker');
const worker = await browser.serviceWorker(target);
expect(await worker.evaluate(() => self.toString())).toBe('[object SharedWorkerGlobalScope]');
const target = await page.context().waitForTarget(target => target.type() === 'shared_worker');
const worker = await target.serviceWorker();
expect(worker).toBe(null);
});
it('should report when a target url changes', async({browser, page, server, context}) => {
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 + '/');
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);
expect((await changedTarget).url()).toBe(server.EMPTY_PAGE);
});
@ -108,13 +111,13 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI
let targetChanged = false;
const listener = () => targetChanged = true;
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 target = await targetPromise;
expect(target.url()).toBe('about:blank');
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 target2 = await targetPromise2;
expect(target2.url()).toBe('about:blank');
@ -132,7 +135,7 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI
server.waitForRequest('/one-style.css')
]);
// 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();
// Issue a redirect.
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}) => {
await page.goto(server.EMPTY_PAGE);
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')
]);
expect((await createdTarget.page()).url()).toBe(server.PREFIX + '/popup/popup.html');
expect(createdTarget.opener()).toBe(browser.pageTarget(page));
expect(browser.pageTarget(page).opener()).toBe(null);
expect(createdTarget.opener()).toBe(page.context().pageTarget(page));
expect(page.context().pageTarget(page).opener()).toBe(null);
});
it('should close all belonging targets once closing context', async function({browser, newContext}) {
const targets = async (context) => (await browser.targets()).filter(t => t.type() === 'page' && t.context() === context);
const context = await newContext();
it('should close all belonging targets once closing context', async function({browser}) {
const context = await browser.newContext();
await context.newPage();
expect((await targets(context)).length).toBe(1);
expect((await context.targets()).length).toBe(1);
expect((await context.pages()).length).toBe(1);
await context.close();
expect((await targets(context)).length).toBe(0);
expect((await context.targets()).length).toBe(0);
});
});
describe('Chromium.waitForTarget', () => {
it('should wait for a target', async function({browser, server, newContext}) {
const context = await newContext();
it('should wait for a target', async function({server, browser}) {
const context = await browser.newContext();
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);
const page = await context.newPage();
expect(resolved).toBe(false);
await page.goto(server.EMPTY_PAGE);
const target = await targetPromise;
expect(await target.page()).toBe(page);
await context.close();
});
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);
});
it('should wait for a target', async function({browser, server}) {
const context = await browser.newContext();
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);
const page = await browser.newPage();
const page = await context.newPage();
expect(resolved).toBe(false);
await page.goto(server.EMPTY_PAGE);
const target = await targetPromise;
expect(await target.page()).toBe(page);
await page.context().close();
await context.close();
});
it('should fire target events', async function({browser, newContext, server}) {
const context = await newContext();
it('should fire target events', async function({browser, server}) {
const context = await browser.newContext();
const events = [];
browser.on('targetcreated', target => events.push('CREATED: ' + target.url()));
browser.on('targetchanged', target => events.push('CHANGED: ' + target.url()));
browser.on('targetdestroyed', target => events.push('DESTROYED: ' + target.url()));
context.on('targetcreated', target => events.push('CREATED: ' + target.url()));
context.on('targetchanged', target => events.push('CHANGED: ' + target.url()));
context.on('targetdestroyed', target => events.push('DESTROYED: ' + target.url()));
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE);
await page.close();
@ -206,6 +209,7 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI
`CHANGED: ${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() => {
const browserWithExtension = await playwright.launch(extensionOptions);
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 browserWithExtension.close();
expect(backgroundPageTarget).toBeTruthy();
});
it('target.page() should return a background_page', async({}) => {
const browserWithExtension = await playwright.launch(extensionOptions);
const backgroundPageTarget = await browserWithExtension.waitForTarget(target => target.type() === 'background_page');
const page = await backgroundPageTarget.page();
expect(await page.evaluate(() => 2 * 3)).toBe(6);
expect(await page.evaluate(() => window.MAGIC)).toBe(42);
const page = await browserWithExtension.newPage();
const backgroundPageTarget = await page.context().waitForTarget(target => target.type() === 'background_page');
const backgroundPage = await backgroundPageTarget.page();
expect(await backgroundPage.evaluate(() => 2 * 3)).toBe(6);
expect(await backgroundPage.evaluate(() => window.MAGIC)).toBe(42);
await browserWithExtension.close();
});
// 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();
await Promise.all([
context.newPage(),
browser.waitForTarget(target => target.context() === context && target.url().includes('devtools://')),
context.waitForTarget(target => target.url().includes('devtools://')),
]);
await browser.close();
});

View file

@ -59,9 +59,9 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
const browser = await playwright.launch(defaultBrowserOptions);
const context = await browser.newContext();
const events = [];
browser.on('targetcreated', target => target.context() === context && events.push('CREATED'));
browser.on('targetchanged', target => target.context() === context && events.push('CHANGED'));
browser.on('targetdestroyed', target => target.context() === context && events.push('DESTROYED'));
context.on('targetcreated', target => events.push('CREATED'));
context.on('targetchanged', target => events.push('CHANGED'));
context.on('targetdestroyed', target => events.push('DESTROYED'));
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE);
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}) {
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);
});
it('should load oopif iframes with subresources and request interception', async function({browser, page, server, context}) {
await page.route('*', request => request.continue());
await page.goto(server.PREFIX + '/dynamic-oopif.html');
expect(oopifs(browser).length).toBe(1);
expect(oopifs(page.context()).length).toBe(1);
});
});
};
function oopifs(browser) {
return browser.targets().filter(target => target._targetInfo.type === 'iframe');
function oopifs(context) {
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() {
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([
client.send('Runtime.enable'),
@ -36,7 +36,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
expect(foo).toBe('bar');
});
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');
const events = [];
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);
});
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('Debugger.enable');
// 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');
});
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');
const evalResponse = await client.send('Runtime.evaluate', {expression: '1 + 2', returnByValue: true});
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.');
});
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);
expect(error.stack).toContain('theSourceOfTheProblems');
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');
expect(await page.evaluate(() => window.CLICKED)).toBe(42);
});
it('should not throw UnhandledPromiseRejection when page closes', async({newContext, server}) => {
const context = await newContext();
it('should not throw UnhandledPromiseRejection when page closes', async({browser, server}) => {
const context = await browser.newContext();
const page = await context.newPage();
await Promise.all([
page.close(),
page.mouse.click(1, 2),
]).catch(e => {});
await context.close();
});
it('should click the button after navigation ', async({page, server}) => {
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');
expect(await page.evaluate(() => result)).toBe('Clicked');
});
it('should click with disabled javascript', async({newPage, server}) => {
const page = await newPage({ javaScriptEnabled: false });
it('should click with disabled javascript', async({browser, server}) => {
const context = await browser.newContext({ javaScriptEnabled: false });
const page = await context.newPage();
await page.goto(server.PREFIX + '/wrappedlink.html');
await Promise.all([
page.click('a'),
page.waitForNavigation()
]);
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}) => {
await page.setContent(`
@ -142,25 +145,18 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI
await page.click('button');
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;
await page.goto(server.PREFIX + '/input/button.html');
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(await page.evaluate(() => result)).toBe('Was not clicked');
});
it('should waitFor hidden', async({page, server}) => {
let error = null;
it('should throw for non-boolean waitFor', async({page, server}) => {
await page.goto(server.PREFIX + '/input/button.html');
const clicked = page.click('button', { waitFor: 'hidden' }).catch(e => error = e);
for (let i = 0; i < 5; i++)
await page.evaluate('1'); // Do a round trip.
expect(error).toBe(null);
await page.$eval('button', b => b.style.display = 'none');
await clicked;
expect(error.message).toBe('Node is either not visible or not an HTMLElement');
expect(await page.evaluate(() => result)).toBe('Was not clicked');
const error = await page.click('button', { waitFor: 'visible' }).catch(e => e);
expect(error.message).toBe('waitFor option should be a boolean, got "string"');
});
it('should waitFor visible', async({page, server}) => {
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}) => {
await page.goto(server.PREFIX + '/input/button.html');
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');
});
// @see https://github.com/GoogleChrome/puppeteer/issues/161
it('should not hang with touch-enabled viewports', async({server, newContext}) => {
const context = await newContext({ viewport: playwright.devices['iPhone 6'].viewport });
it('should not hang with touch-enabled viewports', async({server, browser}) => {
const context = await browser.newContext({ viewport: playwright.devices['iPhone 6'].viewport });
const page = await context.newPage();
await page.mouse.down();
await page.mouse.move(100, 10);
await page.mouse.up();
await context.close();
});
it('should scroll and click the button', async({page, server}) => {
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');
expect(await frame.evaluate(() => window.result)).toBe('Clicked');
});
it('should click the button with deviceScaleFactor set', async({newContext, server}) => {
const context = await newContext({ viewport: {width: 400, height: 400, deviceScaleFactor: 5} });
it('should click the button with deviceScaleFactor set', async({browser, server}) => {
const context = await browser.newContext({ viewport: {width: 400, height: 400, deviceScaleFactor: 5} });
const page = await context.newPage();
expect(await page.evaluate(() => window.devicePixelRatio)).toBe(5);
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');
await button.click();
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}) => {
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(() => offsetY)).toBe(WEBKIT ? 1910 + 8 : 1910);
});
it('should click the button with relative point with page scale', async({newPage, server}) => {
const page = await newPage({ viewport: { width: 400, height: 400, isMobile: true} });
it('should click the button with relative point with page scale', async({browser, server}) => {
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.$eval('button', button => {
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(() => 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}) => {
@ -403,35 +496,51 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROMI
await page.click('button');
expect(await page.evaluate('window.clicked')).toBe(true);
});
xit('should click on an animated button', async({page}) => {
const buttonSize = 50;
xit('should fail to click a button animated via CSS animations and setInterval', async({page}) => {
// 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 transition = 500;
const transition = 100;
await page.setContent(`
<html>
<body>
<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>
</div>
</body>
<script>
const animateLeft = () => {
const button = document.querySelector('#button');
document.querySelector('#button').style.left = button.style.left === '0px' ? '${containerWidth - buttonSize}px' : '0px';
};
window.clicked = 0;
window.setTimeout(animateLeft, 0);
window.setInterval(animateLeft, ${transition});
</script>
</html>
<html>
<body>
<div style="border: 1px solid black; height: 50px; overflow: auto; width: ${containerWidth}px;">
<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>
</body>
<script>
window.atLeft = true;
const animateLeft = () => {
const button = document.querySelector('button');
button.style.left = window.atLeft ? '${containerWidth - buttonSize}px' : '0px';
console.log('set ' + button.style.left);
window.atLeft = !window.atLeft;
};
window.clicked = 0;
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');
expect(await page.evaluate('window.clicked')).toBe(1);
expect(await page.evaluate('document.querySelector("#button").style.left')).toBe(`${containerWidth - buttonSize}px`);
await new Promise(resolve => setTimeout(resolve, 500));
await page.click('button');
expect(await page.evaluate('window.clicked')).toBe(2);
expect(await page.evaluate('document.querySelector("#button").style.left')).toBe('0px');
await page.evaluate(transition => {
window.setInterval(animateLeft, transition);
animateLeft();
}, transition);
const error1 = await page.click('button', { timeout: 250 }).catch(e => e);
expect(await page.evaluate('window.clicked')).toBe(0);
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);
expect(cookie).toBe('cookie=value');
});
it('should isolate cookies in browser contexts', async({context, server, newContext}) => {
const anotherContext = await newContext();
it('should isolate cookies in browser contexts', async({context, server, browser}) => {
const anotherContext = await browser.newContext();
await context.setCookies([{url: server.EMPTY_PAGE, name: 'isolatecookie', value: 'page1value'}]);
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(cookies2[0].name).toBe('isolatecookie');
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) => {
res.setHeader('Set-Cookie', 'session=value');
res.end();
@ -199,14 +200,15 @@ module.exports.describe = function({testRunner, expect, playwright, defaultBrows
expect(cookies.map(c => c.value).join(',')).toBe('value');
}
{
const context2 = await newContext();
const context2 = await browser.newContext();
const page = await context2.newPage();
await page.goto(server.EMPTY_PAGE);
const cookies = await context2.cookies();
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) => {
res.setHeader('Set-Cookie', 'persistent=persistent-value; max-age=3600');
res.end();
@ -215,7 +217,7 @@ module.exports.describe = function({testRunner, expect, playwright, defaultBrows
await page.goto(server.PREFIX + '/setcookie.html');
const context1 = context;
const context2 = await newContext();
const context2 = await browser.newContext();
const [page1, page2] = await Promise.all([context1.newPage(), context2.newPage()]);
await Promise.all([page1.goto(server.EMPTY_PAGE), page2.goto(server.EMPTY_PAGE)]);
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].value).toBe('persistent-value');
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 = [];
server.setRoute('/empty.html', (req, res) => {
cookie = req.headers.cookie || '';
@ -237,9 +240,11 @@ module.exports.describe = function({testRunner, expect, playwright, defaultBrows
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);
expect(cookie).toBe('');
await context.close();
}
});
it('should isolate cookies between launches', async({server}) => {
@ -449,8 +454,8 @@ module.exports.describe = function({testRunner, expect, playwright, defaultBrows
await context.clearCookies();
expect(await page.evaluate('document.cookie')).toBe('');
});
it('should isolate cookies when clearing', async({context, server, newContext}) => {
const anotherContext = await newContext();
it('should isolate cookies when clearing', async({context, server, browser}) => {
const anotherContext = await browser.newContext();
await context.setCookies([{url: server.EMPTY_PAGE, name: 'page1cookie', value: 'page1value'}]);
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();
expect((await context.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);
expect(pwBoundingBox).toEqual(webBoundingBox);
});
it('should work with page scale', async({newPage, server}) => {
const page = await newPage({ viewport: { width: 400, height: 400, isMobile: true} });
it('should work with page scale', async({browser, server}) => {
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');
const button = await page.$('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.width * 100)).toBe(200 * 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);
let error = null;
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}) => {
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() {
it.skip(FFOX)('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 before = await button.visibleRatio();
expect(Math.round(before * 10)).toBe(10 - i);
const before = await button.evaluate(button => {
return button.getBoundingClientRect().right - window.innerWidth;
});
expect(before).toBe(10 * i);
await button.scrollIntoViewIfNeeded();
const after = await button.visibleRatio();
expect(Math.round(after * 10)).toBe(10);
const after = await button.evaluate(button => {
return button.getBoundingClientRect().right - window.innerWidth;
});
expect(after <= 0).toBe(true);
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 iPhoneLandscape = playwright.devices['iPhone 6 landscape'];
describe('Page.viewport', function() {
describe('BrowserContext({viewport})', function() {
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});
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}) => {
const context = await newContext({ viewport: iPhone.viewport });
it('should support mobile emulation', async({browser, server}) => {
const context = await browser.newContext({ viewport: iPhone.viewport });
const page = await context.newPage();
await page.goto(server.PREFIX + '/mobile.html');
expect(await page.evaluate(() => window.innerWidth)).toBe(375);
await page.setViewportSize({width: 400, height: 300});
expect(await page.evaluate(() => window.innerWidth)).toBe(400);
await context.close();
});
it('should not have touch by default', async({page, server}) => {
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');
expect(await page.evaluate(() => document.body.textContent.trim())).toBe('NO');
});
it('should support touch emulation', async({newContext, server}) => {
const context = await newContext({ viewport: iPhone.viewport });
it('should support touch emulation', async({browser, server}) => {
const context = await browser.newContext({ viewport: iPhone.viewport });
const page = await context.newPage();
await page.goto(server.PREFIX + '/mobile.html');
expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(true);
expect(await page.evaluate(dispatchTouch)).toBe('Received touch');
await context.close();
function dispatchTouch() {
let fulfill;
@ -65,30 +74,34 @@ module.exports.describe = function({testRunner, expect, playwright, headless, FF
return promise;
}
});
it('should be detectable by Modernizr', async({newContext, server}) => {
const context = await newContext({ viewport: iPhone.viewport });
it('should be detectable by Modernizr', async({browser, server}) => {
const context = await browser.newContext({ viewport: iPhone.viewport });
const page = await context.newPage();
await page.goto(server.PREFIX + '/detect-touch.html');
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}) => {
const context = await newContext({ viewport: { width: 800, height: 600, isMobile: true } });
it('should detect touch when applying viewport with touches', async({browser, server}) => {
const context = await browser.newContext({ viewport: { width: 800, height: 600, isMobile: true } });
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE);
await page.addScriptTag({url: server.PREFIX + '/modernizr.js'});
expect(await page.evaluate(() => Modernizr.touchevents)).toBe(true);
await context.close();
});
it.skip(FFOX)('should support landscape emulation', async({newContext, server}) => {
const context1 = await newContext({ viewport: iPhone.viewport });
it.skip(FFOX)('should support landscape emulation', async({browser, server}) => {
const context1 = await browser.newContext({ viewport: iPhone.viewport });
const page1 = await context1.newPage();
await page1.goto(server.PREFIX + '/mobile.html');
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();
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}) => {
const context = await newContext({ viewport: { width: 300, height: 400, isMobile: true } });
it.skip(FFOX || WEBKIT)('should fire orientationchange event', async({browser, server}) => {
const context = await browser.newContext({ viewport: { width: 300, height: 400, isMobile: true } });
const page = await context.newPage();
await page.goto(server.PREFIX + '/mobile.html');
await page.evaluate(() => {
@ -103,35 +116,42 @@ module.exports.describe = function({testRunner, expect, playwright, headless, FF
const event2 = page.waitForEvent('console');
await page.setViewportSize({width: 300, height: 400});
expect((await event2).text()).toBe('2');
await context.close();
});
it.skip(FFOX)('default mobile viewports to 980 width', async({newContext, server}) => {
const context = await newContext({ viewport: {width: 320, height: 480, isMobile: true} });
it.skip(FFOX)('default mobile viewports to 980 width', async({browser, server}) => {
const context = await browser.newContext({ viewport: {width: 320, height: 480, isMobile: true} });
const page = await context.newPage();
await page.goto(server.PREFIX + '/empty.html');
expect(await page.evaluate(() => window.innerWidth)).toBe(980);
await context.close();
});
it.skip(FFOX)('respect meta viewport tag', async({newContext, server}) => {
const context = await newContext({ viewport: {width: 320, height: 480, isMobile: true} });
it.skip(FFOX)('respect meta viewport tag', async({browser, server}) => {
const context = await browser.newContext({ viewport: {width: 320, height: 480, isMobile: true} });
const page = await context.newPage();
await page.goto(server.PREFIX + '/mobile.html');
expect(await page.evaluate(() => window.innerWidth)).toBe(320);
await context.close();
});
});
describe('Page.emulate', function() {
it('should work', async({newPage, server}) => {
const page = await newPage({ viewport: iPhone.viewport, userAgent: iPhone.userAgent });
it('should work', async({browser, server}) => {
const context = await browser.newContext({viewport: iPhone.viewport, userAgent: iPhone.userAgent});
const page = await context.newPage();
await page.goto(server.PREFIX + '/mobile.html');
expect(await page.evaluate(() => window.innerWidth)).toBe(375);
expect(await page.evaluate(() => navigator.userAgent)).toContain('iPhone');
await context.close();
});
it('should support clicking', async({newPage, server}) => {
const page = await newPage({ viewport: iPhone.viewport, userAgent: iPhone.userAgent });
it('should support clicking', async({browser, server}) => {
const context = await browser.newContext({ viewport: iPhone.viewport, userAgent: iPhone.userAgent });
const page = await context.newPage();
await page.goto(server.PREFIX + '/input/button.html');
const button = await page.$('button');
await page.evaluate(button => button.style.marginTop = '200px', button);
await button.click();
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() {
it('should work', async ({ newPage }) => {
it('should work', async ({ browser }) => {
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)');
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)');
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)');
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)');
await context.close();
}
});
it('should throw for invalid timezone IDs', async({newPage}) => {
let error = null;
await newPage({ timezoneId: 'Foo/Bar' }).catch(e => error = e);
expect(error.message).toBe('Invalid timezone ID: Foo/Bar');
await newPage({ timezoneId: 'Baz/Qux' }).catch(e => error = e);
expect(error.message).toBe('Invalid timezone ID: Baz/Qux');
it('should throw for invalid timezone IDs when creating pages', async({browser}) => {
for (const timezoneId of ['Foo/Bar', 'Baz/Qux']) {
let error = null;
const context = await browser.newContext({ timezoneId });
const page = await context.newPage().catch(e => error = e);
expect(error.message).toBe(`Invalid timezone ID: ${timezoneId}`);
await context.close();
}
});
});
describe.skip(CHROMIUM || FFOX)('BrowserContext({locale})', function() {
it('should affect accept-language header', async({newPage, server}) => {
const page = await newPage({ locale: 'fr-CH' });
it('should affect accept-language header', async({browser, server}) => {
const context = await browser.newContext({ locale: 'fr-CH' });
const page = await context.newPage();
const [request] = await Promise.all([
server.waitForRequest('/empty.html'),
page.goto(server.EMPTY_PAGE),
]);
expect(request.headers['accept-language'].substr(0, 5)).toBe('fr-CH');
await context.close();
});
it('should affect navigator.language', async({newPage, server}) => {
const page = await newPage({ locale: 'fr-CH' });
it('should affect navigator.language', async({browser, server}) => {
const context = await browser.newContext({ locale: 'fr-CH' });
const page = await context.newPage();
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);
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);
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);
const formatted = 'Sat Nov 19 2016 10:12:34 GMT-0800 (Pacific Standard Time)';
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);
expect(await page.evaluate(() => new Date(1479579154987).toString())).toBe(
'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();
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);
const otherContext = await newContext();
const otherContext = await browser.newContext();
const otherPage = await otherContext.newPage();
await otherPage.goto(server.EMPTY_PAGE);
expect(await getPermission(page, 'geolocation')).toBe('prompt');
@ -94,6 +94,7 @@ module.exports.describe = function({testRunner, expect, WEBKIT}) {
await context.clearPermissions();
expect(await getPermission(page, 'geolocation')).toBe('prompt');
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"');
});
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 options = { geolocation };
const context = await newContext(options);
const context = await browser.newContext(options);
await context.setGeolocation({ longitude: 20, latitude: 20 });
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;
try {
const context = await newContext({ geolocation: {latitude: 10} });
const context = await browser.newContext({ geolocation: {latitude: 10} });
await context.close();
} catch (e) {
error = e;
}
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: {} };
options.permissions[server.PREFIX] = ['geolocation'];
const context = await newContext(options);
const context = await browser.newContext(options);
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE);
@ -85,6 +86,7 @@ module.exports.describe = function ({ testRunner, expect, FFOX, WEBKIT }) {
latitude: 10,
longitude: 10
});
await context.close();
});
});
};

View file

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

View file

@ -23,24 +23,28 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
const {it, fit, xit, dit} = testRunner;
const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;
describe('ignoreHTTPSErrors', function() {
it('should work', async({newPage, httpsServer}) => {
it('should work', async({browser, httpsServer}) => {
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);
expect(error).toBe(null);
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) => {
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'});
expect(page.frames().length).toBe(2);
// Make sure blocked iframe has functional execution context
// @see https://github.com/GoogleChrome/puppeteer/issues/2709
expect(await page.frames()[0].evaluate('1 + 2')).toBe(3);
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);
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 = [];
await page.exposeFunction('eventHandled', e => events.push(e));
await page.setContent(`
<input id=input type=file></input>
<script>
input.addEventListener('input', e => eventHandled({ type: e.type }));
input.addEventListener('change', e => eventHandled({ type: e.type }));
</script>`);
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[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);
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;
const page = await newPage();
const context = await browser.newContext();
const page = await context.newPage();
page.on('request', async request => {
try {
await request.continue();
@ -338,6 +339,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
});
await page.goto(server.EMPTY_PAGE);
expect(error.message).toContain('Request Interception is not enabled');
await context.close();
});
it('should intercept main resource during cross-process navigation', async({page, server}) => {
await page.goto(server.EMPTY_PAGE);
@ -559,12 +561,14 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
});
describe('ignoreHTTPSErrors', function() {
it('should work with request interception', async({newPage, httpsServer}) => {
const page = await newPage({ ignoreHTTPSErrors: true, interceptNetwork: true });
it('should work with request interception', async({browser, httpsServer}) => {
const context = await browser.newContext({ ignoreHTTPSErrors: true, interceptNetwork: true });
const page = await context.newPage();
await page.route('**/*', request => request.continue());
const response = await page.goto(httpsServer.EMPTY_PAGE);
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
it('should work with mobile viewports and cross process navigations', async({newContext, server}) => {
const context = await newContext({ viewport: {width: 360, height: 640, isMobile: true} });
it('should work with mobile viewports and cross process navigations', async({browser, server}) => {
const context = await browser.newContext({ viewport: {width: 360, height: 640, isMobile: true} });
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE);
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);
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 { performance } = require('perf_hooks');
/**
* @type {PageTestSuite}
@ -396,29 +395,39 @@ module.exports.describe = function({testRunner, expect, playwright, MAC, WIN, FF
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) {
let lastResponseFinished;
const finishResponse = response => {
lastResponseFinished = performance.now();
response.statusCode = 404;
response.end(`File not found`);
};
const waitForRequest = suffix => {
return Promise.all([
server.waitForRequest(suffix),
frame._page.waitForRequest(server.PREFIX + suffix),
])
}
let responses = {};
// Hold on to a bunch of requests without answering.
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-c.js', (req, res) => responses.c = res);
const initialFetchResourcesRequested = Promise.all([
server.waitForRequest('/fetch-request-a.js'),
server.waitForRequest('/fetch-request-b.js'),
server.waitForRequest('/fetch-request-c.js'),
waitForRequest('/fetch-request-a.js'),
waitForRequest('/fetch-request-b.js'),
waitForRequest('/fetch-request-c.js')
]);
let secondFetchResourceRequested;
if (signal === 'networkidle0') {
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' });
@ -442,10 +451,11 @@ module.exports.describe = function({testRunner, expect, playwright, MAC, WIN, FF
expect(responses.a).toBeTruthy();
expect(responses.b).toBeTruthy();
expect(responses.c).toBeTruthy();
// Finishing first response should leave 2 requests alive and trigger networkidle2.
finishResponse(responses.a);
let timer;
let timerTriggered = false;
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.
finishResponse(responses.b);
finishResponse(responses.c);
@ -454,11 +464,16 @@ module.exports.describe = function({testRunner, expect, playwright, MAC, WIN, FF
await secondFetchResourceRequested;
expect(actionFinished).toBe(false);
// Finishing the last response should trigger networkidle0.
timer = setTimeout(() => timerTriggered = true, 500);
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;
expect(performance.now() - lastResponseFinished).not.toBeLessThan(450);
clearTimeout(timer);
expect(timerTriggered).toBe(true);
if (!isSetContent)
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}) => {
await page.goto(server.PREFIX + '/input/textarea.html');
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');
await page.goto(server.PREFIX + '/input/textarea.html');
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');
});
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 slowMo = valueFromEnv('SLOW_MO', 0);
const CI = valueFromEnv('CI', false);
const dumpProtocolOnFailure = CI || valueFromEnv('DEBUGP', false);
const dumpProtocolOnFailure = valueFromEnv('DEBUGP', false);
function valueFromEnv(name, defaultValue) {
if (!(name in process.env))
@ -113,7 +112,6 @@ module.exports.describe = ({testRunner, product, playwrightPath}) => {
});
beforeEach(async(state, test) => {
const contexts = [];
const onLine = (line) => test.output += line + '\n';
if (dumpProtocolOnFailure)
state.browser._setDebugFunction(onLine);
@ -126,7 +124,6 @@ module.exports.describe = ({testRunner, product, playwrightPath}) => {
}
state.tearDown = async () => {
await Promise.all(contexts.map(c => c.close()));
if (rl) {
rl.removeListener('line', onLine);
rl.close();
@ -134,30 +131,25 @@ module.exports.describe = ({testRunner, product, playwrightPath}) => {
if (dumpProtocolOnFailure)
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();
});
describe('Page', function() {
beforeEach(async state => {
state.context = await state.newContext();
state.context = await state.browser.newContext();
state.page = await state.context.newPage();
});
afterEach(async state => {
await state.context.close();
state.context = 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;
describe('window.open', function() {
it.skip(CHROMIUM || WEBKIT)('should inherit user agent from browser context', async function({newContext, server}) {
const context = await newContext({
it.skip(CHROMIUM || WEBKIT)('should inherit user agent from browser context', async function({browser, server}) {
const context = await browser.newContext({
userAgent: 'hey'
});
const page = await context.newPage();
@ -36,8 +36,8 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
expect(userAgent).toBe('hey');
expect(request.headers['user-agent']).toBe('hey');
});
it.skip(CHROMIUM)('should inherit touch support from browser context', async function({newContext, server}) {
const context = await newContext({
it.skip(CHROMIUM)('should inherit touch support from browser context', async function({browser, server}) {
const context = await browser.newContext({
viewport: { width: 400, height: 500, isMobile: true }
});
const page = await context.newPage();
@ -49,8 +49,8 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
await context.close();
expect(hasTouch).toBe(true);
});
it.skip(CHROMIUM || WEBKIT)('should inherit viewport size from browser context', async function({newContext, server}) {
const context = await newContext({
it.skip(CHROMIUM || WEBKIT)('should inherit viewport size from browser context', async function({browser, server}) {
const context = await browser.newContext({
viewport: { width: 400, height: 500, isMobile: true }
});
const page = await context.newPage();
@ -65,8 +65,8 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
});
describe('Page.Events.Popup', function() {
it('should work', async({newContext}) => {
const context = await newContext();
it('should work', async({browser}) => {
const context = await browser.newContext();
const page = await context.newPage();
const [popup] = await Promise.all([
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);
await context.close();
});
it.skip(CHROMIUM)('should work with empty url', async({newContext}) => {
const context = await newContext();
it.skip(CHROMIUM)('should work with empty url', async({browser}) => {
const context = await browser.newContext();
const page = await context.newPage();
const [popup] = await Promise.all([
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);
await context.close();
});
it('should work with noopener', async({newContext}) => {
const context = await newContext();
it('should work with noopener', async({browser}) => {
const context = await browser.newContext();
const page = await context.newPage();
const [popup] = await Promise.all([
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);
await context.close();
});
it('should work with clicking target=_blank', async({newContext, server}) => {
const context = await newContext();
it('should work with clicking target=_blank', async({browser, server}) => {
const context = await browser.newContext();
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE);
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);
await context.close();
});
it('should work with fake-clicking target=_blank and rel=noopener', async({newContext, server}) => {
const context = await newContext();
it('should work with fake-clicking target=_blank and rel=noopener', async({browser, server}) => {
const context = await browser.newContext();
const page = await context.newPage();
// TODO: FFOX sends events for "one-style.html" request to both pages.
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);
await context.close();
});
it('should work with clicking target=_blank and rel=noopener', async({newContext, server}) => {
const context = await newContext();
it('should work with clicking target=_blank and rel=noopener', async({browser, server}) => {
const context = await browser.newContext();
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE);
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);
await context.close();
});
it('should not treat navigations as new popups', async({newContext, server}) => {
const context = await newContext();
it('should not treat navigations as new popups', async({browser, server}) => {
const context = await browser.newContext();
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE);
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');
});
it.skip(FFOX)('should work with a mobile viewport', async({newContext, server}) => {
const context = await newContext({viewport: { width: 320, height: 480, isMobile: true }});
it.skip(FFOX)('should work with a mobile viewport', async({browser, server}) => {
const context = await browser.newContext({viewport: { width: 320, height: 480, isMobile: true }});
const page = await context.newPage();
await page.goto(server.PREFIX + '/overflow.html');
const screenshot = await page.screenshot();
expect(screenshot).toBeGolden('screenshot-mobile.png');
await context.close();
});
it('should work for canvas', async({page, server}) => {
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();
expect(screenshot).toBeGolden('screenshot-element-fractional-offset.png');
});
it('should take screenshots when default viewport is null', async({server, newPage}) => {
const page = await newPage({ viewport: null });
it('should take screenshots when default viewport is null', async({server, browser}) => {
const context = await browser.newContext({ viewport: null });
const page = await context.newPage();
await page.goto(server.PREFIX + '/grid.html');
const sizeBefore = await page.evaluate(() => ({ width: document.body.offsetWidth, height: document.body.offsetHeight }));
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 }));
expect(sizeBefore.width).toBe(sizeAfter.width);
expect(sizeBefore.height).toBe(sizeAfter.height);
await context.close();
});
it('should take fullPage screenshots when default viewport is null', async({server, newPage}) => {
const page = await newPage({ viewport: null });
it('should take fullPage screenshots when default viewport is null', async({server, browser}) => {
const context = await browser.newContext({ viewport: null });
const page = await context.newPage();
await page.goto(server.PREFIX + '/grid.html');
const sizeBefore = await page.evaluate(() => ({ width: document.body.offsetWidth, height: document.body.offsetHeight }));
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 }));
expect(sizeBefore.width).toBe(sizeAfter.width);
expect(sizeBefore.height).toBe(sizeAfter.height);
await context.close();
});
it('should restore default viewport after fullPage screenshot', async({ newPage }) => {
const page = await newPage({ viewport: { width: 456, height: 789 } });
it('should restore default viewport after fullPage screenshot', async({ browser }) => {
const context = await browser.newContext({ viewport: { width: 456, height: 789 } });
const page = await context.newPage();
expect(page.viewportSize().width).toBe(456);
expect(page.viewportSize().height).toBe(789);
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(await page.evaluate('window.innerWidth')).toBe(456);
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}) => {
const page = await newPage({ viewport: null });
it('should take element screenshot when default viewport is null and restore back', async({server, browser}) => {
const context = await browser.newContext({viewport: null});
const page = await context.newPage({ viewport: null });
await page.setContent(`
<div style="height: 14px">oooo</div>
<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 }));
expect(sizeBefore.width).toBe(sizeAfter.width);
expect(sizeBefore.height).toBe(sizeAfter.height);
await context.close();
});
});
};

View file

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

4
test/types.d.ts vendored
View file

@ -66,8 +66,6 @@ type TestState = {
type BrowserState = TestState & {
browser: import('../src/browser').Browser;
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 & {
@ -100,4 +98,4 @@ interface TestServer {
CROSS_PROCESS_PREFIX: 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}) => {
await page.setContent('<section>test</section>');
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}) => {
await page.setContent('<section>test</section>');
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}) => {
await page.setContent('<section>test</section>');
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}) => {
await page.setContent('<section>test</section>');
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}) => {
await page.goto(server.EMPTY_PAGE);

View file

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

View file

@ -28,7 +28,7 @@ module.exports = { checkSources, expandPrefix };
function checkSources(sources, externalDependencies) {
// special treatment for Events.js
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) {
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))

View file

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

View file

@ -1,7 +1,5 @@
class Foo {
constructor() {
this.ddd = 10;
}
ddd = 10;
aaa() {}
@ -10,3 +8,4 @@ class Foo {
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() {
}
}
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 {
property1 = 1;
_property2 = 2;
constructor(delegate) {
this.property1 = 1;
this._property2 = 2;
}
get getter() {
@ -11,3 +11,5 @@ class A {
async method(foo, bar) {
}
}
export {A};

View file

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

View file

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

View file

@ -14,7 +14,11 @@
* 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 = {}) {
this._matchers = {};
Object.assign(this._matchers, DefaultMatchers);
@ -26,99 +30,202 @@ module.exports = class Matchers {
this._matchers[name] = matcher;
}
expect(value) {
return new Expect(value, this._matchers);
expect(received) {
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 {
constructor(value, matchers) {
constructor(received, matchers) {
this.not = {};
this.not.not = this;
for (const matcherName of Object.keys(matchers)) {
const matcher = matchers[matcherName];
this[matcherName] = applyMatcher.bind(null, matcherName, matcher, false /* inverse */, value);
this.not[matcherName] = applyMatcher.bind(null, matcherName, matcher, true /* inverse */, value);
this[matcherName] = applyMatcher.bind(null, matcherName, matcher, false /* inverse */, received);
this.not[matcherName] = applyMatcher.bind(null, matcherName, matcher, true /* inverse */, received);
}
function applyMatcher(matcherName, matcher, inverse, value, ...args) {
const result = matcher.call(null, value, ...args);
function applyMatcher(matcherName, matcher, inverse, received, ...args) {
const result = matcher.call(null, received, ...args);
const message = `expect.${inverse ? 'not.' : ''}${matcherName} failed` + (result.message ? `: ${result.message}` : '');
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 = {
toBe: function(value, other, message) {
message = message || `${value} == ${other}`;
return { pass: value === other, message };
toBe: function(received, expected, message) {
message = message || `${received} == ${expected}`;
return { pass: received === expected, message, formatter: toBeFormatter.bind(null, received, expected) };
},
toBeFalsy: function(value, message) {
message = message || `${value}`;
return { pass: !value, message };
toBeFalsy: function(received, message) {
message = message || `${received}`;
return { pass: !received, message };
},
toBeTruthy: function(value, message) {
message = message || `${value}`;
return { pass: !!value, message };
toBeTruthy: function(received, message) {
message = message || `${received}`;
return { pass: !!received, message };
},
toBeGreaterThan: function(value, other, message) {
message = message || `${value} > ${other}`;
return { pass: value > other, message };
toBeGreaterThan: function(received, other, message) {
message = message || `${received} > ${other}`;
return { pass: received > other, message };
},
toBeGreaterThanOrEqual: function(value, other, message) {
message = message || `${value} >= ${other}`;
return { pass: value >= other, message };
toBeGreaterThanOrEqual: function(received, other, message) {
message = message || `${received} >= ${other}`;
return { pass: received >= other, message };
},
toBeLessThan: function(value, other, message) {
message = message || `${value} < ${other}`;
return { pass: value < other, message };
toBeLessThan: function(received, other, message) {
message = message || `${received} < ${other}`;
return { pass: received < other, message };
},
toBeLessThanOrEqual: function(value, other, message) {
message = message || `${value} <= ${other}`;
return { pass: value <= other, message };
toBeLessThanOrEqual: function(received, other, message) {
message = message || `${received} <= ${other}`;
return { pass: received <= other, message };
},
toBeNull: function(value, message) {
message = message || `${value} == null`;
return { pass: value === null, message };
toBeNull: function(received, message) {
message = message || `${received} == null`;
return { pass: received === null, message };
},
toContain: function(value, other, message) {
message = message || `${value}${other}`;
return { pass: value.includes(other), message };
toContain: function(received, other, message) {
message = message || `${received}${other}`;
return { pass: received.includes(other), message };
},
toEqual: function(value, other, message) {
const valueJson = stringify(value);
const otherJson = stringify(other);
message = message || `\n${valueJson}${otherJson}`;
return { pass: valueJson === otherJson, message };
toEqual: function(received, other, message) {
let receivedJson = stringify(received);
let otherJson = stringify(other);
let formatter = objectFormatter.bind(null, receivedJson, otherJson);
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 {
pass: Math.abs(value - other) < Math.pow(10, -precision),
pass: Math.abs(received - other) < Math.pow(10, -precision),
message
};
},
toBeInstanceOf: function(value, other, message) {
message = message || `${value.constructor.name} instanceof ${other.name}`;
return { pass: value instanceof other, message };
toBeInstanceOf: function(received, other, message) {
message = message || `${received.constructor.name} instanceof ${other.name}`;
return { pass: received instanceof other, message };
},
};
function stringify(value) {
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;
const result = {};
for (const key of Object.keys(object).sort())

View file

@ -14,24 +14,20 @@
* limitations under the License.
*/
const RED_COLOR = '\x1b[31m';
const GREEN_COLOR = '\x1b[32m';
const GRAY_COLOR = '\x1b[90m';
const YELLOW_COLOR = '\x1b[33m';
const MAGENTA_COLOR = '\x1b[35m';
const RESET_COLOR = '\x1b[0m';
const fs = require('fs');
const colors = require('colors/safe');
const {MatchError} = require('./Matchers.js');
class Reporter {
constructor(runner, options = {}) {
const {
projectFolder = null,
showSlowTests = 3,
showSkippedTests = Infinity,
verbose = false,
summary = true,
} = options;
this._filePathToLines = new Map();
this._runner = runner;
this._projectFolder = projectFolder;
this._showSlowTests = showSlowTests;
this._showSkippedTests = showSkippedTests;
this._verbose = verbose;
@ -48,19 +44,20 @@ class Reporter {
this._testCounter = 0;
this._timestamp = Date.now();
const allTests = this._runner.tests();
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`);
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`);
if (allTests.length === runnableTests.length) {
console.log(`Running all ${colors.yellow(runnableTests.length)} tests on ${colors.yellow(this._runner.parallel())} worker${this._runner.parallel() > 1 ? 's' : ''}:\n`);
} else {
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) {
console.log(`${RED_COLOR}## ${result.toUpperCase()} ##${RESET_COLOR}`);
console.log(colors.red(`## ${result.toUpperCase()} ##`));
console.log('Message:');
console.log(` ${RED_COLOR}${message}${RESET_COLOR}`);
console.log(` ${colors.red(message)}`);
if (error && error.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');
const workerIds = Array.from(this._workersState.keys());
@ -69,23 +66,25 @@ class Reporter {
const {isRunning, test} = this._workersState.get(workerId);
let description = '';
if (isRunning)
description = `${YELLOW_COLOR}RUNNING${RESET_COLOR}`;
description = colors.yellow('RUNNING');
else if (test.result === 'ok')
description = `${GREEN_COLOR}OK${RESET_COLOR}`;
description = colors.green('OK');
else if (test.result === 'skipped')
description = `${YELLOW_COLOR}SKIPPED${RESET_COLOR}`;
description = colors.yellow('SKIPPED');
else if (test.result === 'failed')
description = `${RED_COLOR}FAILED${RESET_COLOR}`;
description = colors.red('FAILED');
else if (test.result === 'crashed')
description = `${RED_COLOR}CRASHED${RESET_COLOR}`;
description = colors.red('CRASHED');
else if (test.result === 'timedout')
description = `${RED_COLOR}TIMEDOUT${RESET_COLOR}`;
description = colors.red('TIMEDOUT');
else if (test.result === 'terminated')
description = `${MAGENTA_COLOR}TERMINATED${RESET_COLOR}`;
description = colors.magenta('TERMINATED');
else
description = `${RED_COLOR}<UNKNOWN>${RESET_COLOR}`;
console.log(` ${workerId}: [${description}] ${test.fullName} (${formatTestLocation(test)})`);
description = colors.red('<UNKNOWN>');
console.log(` ${workerId}: [${description}] ${test.fullName} (${formatLocation(test.location)})`);
}
console.log('');
console.log('');
process.exitCode = 2;
}
@ -105,24 +104,7 @@ class Reporter {
console.log('\nFailures:');
for (let i = 0; i < failedTests.length; ++i) {
const test = failedTests[i];
console.log(`${i + 1}) ${test.fullName} (${formatTestLocation(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'));
}
this._printVerboseTestResult(i + 1, test);
console.log('');
}
}
@ -132,12 +114,12 @@ class Reporter {
if (skippedTests.length > 0) {
console.log('\nSkipped:');
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) {
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) {
const test = slowTests[i];
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;
let summaryText = '';
if (failedTests.length || skippedTests.length) {
const summary = [`ok - ${GREEN_COLOR}${okTestsLength}${RESET_COLOR}`];
const summary = [`ok - ${colors.green(okTestsLength)}`];
if (failedTests.length)
summary.push(`failed - ${RED_COLOR}${failedTests.length}${RESET_COLOR}`);
summary.push(`failed - ${colors.red(failedTests.length)}`);
if (skippedTests.length)
summary.push(`skipped - ${YELLOW_COLOR}${skippedTests.length}${RESET_COLOR}`);
summaryText = `(${summary.join(', ')})`;
summary.push(`skipped - ${colors.yellow(skippedTests.length)}`);
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 seconds = milliseconds / 1000;
console.log(`Finished in ${YELLOW_COLOR}${seconds}${RESET_COLOR} 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');
console.log(`Finished in ${colors.yellow(seconds)} seconds`);
}
_onTestStarted(test, workerId) {
@ -203,55 +164,92 @@ class Reporter {
this._workersState.set(workerId, {test, isRunning: false});
if (this._verbose) {
++this._testCounter;
let prefix = `${this._testCounter})`;
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}`);
}
this._printVerboseTestResult(this._testCounter, test, workerId);
} else {
if (test.result === 'ok')
process.stdout.write(`${GREEN_COLOR}.${RESET_COLOR}`);
process.stdout.write(colors.green('.'));
else if (test.result === 'skipped')
process.stdout.write(`${YELLOW_COLOR}*${RESET_COLOR}`);
process.stdout.write(colors.yellow('*'));
else if (test.result === 'failed')
process.stdout.write(`${RED_COLOR}F${RESET_COLOR}`);
process.stdout.write(colors.red('F'));
else if (test.result === 'crashed')
process.stdout.write(`${RED_COLOR}C${RESET_COLOR}`);
process.stdout.write(colors.red('C'));
else if (test.result === 'terminated')
process.stdout.write(`${MAGENTA_COLOR}.${RESET_COLOR}`);
process.stdout.write(colors.magenta('.'));
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) {
const location = test.location;
function formatLocation(location) {
if (!location)
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;

View file

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

View file

@ -23,6 +23,7 @@ const Multimap = require('./Multimap');
const fs = require('fs');
const {SourceMapSupport} = require('./SourceMapSupport');
const debug = require('debug');
const {getCallerLocation} = require('./utils');
const INFINITE_TIMEOUT = 2147483647;
@ -41,7 +42,7 @@ class UserCallback {
});
this.timeout = timeout;
this.location = this._getLocation();
this.location = getCallerLocation(__filename);
}
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() {
this._terminateCallback(TerminatedError);
}
@ -119,6 +91,7 @@ class Test {
this.declaredMode = declaredMode;
this._userCallback = new UserCallback(callback, timeout);
this.location = this._userCallback.location;
this.timeout = timeout;
// Test results
this.result = null;
@ -422,7 +395,10 @@ class TestRunner extends EventEmitter {
const runnableTests = this._runnableTests();
this.emit(TestRunner.Events.Started, runnableTests);
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;
const result = {};
if (termination) {

View file

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

View file

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