Merge branch 'main' into separate-transport-from-testserverconnection

This commit is contained in:
Simon Knott 2024-09-03 10:49:23 +02:00
commit 7b11e02ee3
No known key found for this signature in database
GPG key ID: 8CEDC00028084AEC
123 changed files with 7313 additions and 2008 deletions

3
.gitignore vendored
View file

@ -33,4 +33,5 @@ test-results
/tests/installation/output/ /tests/installation/output/
/tests/installation/.registry.json /tests/installation/.registry.json
.cache/ .cache/
.eslintcache .eslintcache
playwright.env

View file

@ -1,6 +1,6 @@
# 🎭 Playwright # 🎭 Playwright
[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-128.0.6613.36-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-129.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[![WebKit version](https://img.shields.io/badge/webkit-18.0-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop --> [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord) [![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-129.0.6668.22-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-129.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[![WebKit version](https://img.shields.io/badge/webkit-18.0-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop --> [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord)
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright) ## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
@ -8,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
| | Linux | macOS | Windows | | | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: | | :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->128.0.6613.36<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Chromium <!-- GEN:chromium-version -->129.0.6668.22<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->18.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: | | WebKit <!-- GEN:webkit-version -->18.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Firefox <!-- GEN:firefox-version -->129.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Firefox <!-- GEN:firefox-version -->129.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |

17
SUPPORT.md Normal file
View file

@ -0,0 +1,17 @@
# Support
## How to file issues and get help
This project uses GitHub issues to track bugs and feature requests. Please search the [existing issues][gh-issues] before filing new ones to avoid duplicates. For new issues, file your bug or feature request as a new issue using corresponding template.
For help and questions about using this project, please see the [docs site for Playwright][docs].
Join our community [Discord Server][discord-server] to connect with other developers using Playwright and ask questions in our 'help-playwright' forum.
## Microsoft Support Policy
Support for Playwright is limited to the resources listed above.
[gh-issues]: https://github.com/microsoft/playwright/issues/
[docs]: https://playwright.dev/
[discord-server]: https://aka.ms/playwright/discord

View file

@ -60,7 +60,7 @@ An object with all the response HTTP headers associated with this response.
- `name` <[string]> Name of the header. - `name` <[string]> Name of the header.
- `value` <[string]> Value of the header. - `value` <[string]> Value of the header.
An array with all the request HTTP headers associated with this response. Header names are not lower-cased. An array with all the response HTTP headers associated with this response. Header names are not lower-cased.
Headers with multiple entries, such as `Set-Cookie`, appear in the array multiple times. Headers with multiple entries, such as `Set-Cookie`, appear in the array multiple times.
## async method: APIResponse.json ## async method: APIResponse.json

View file

@ -297,8 +297,10 @@ testing frameworks should explicitly create [`method: Browser.newContext`] follo
## async method: Browser.removeAllListeners ## async method: Browser.removeAllListeners
* since: v1.47 * since: v1.47
* langs: js
Removes all the listeners of the given type if the type is given. Otherwise removes all the listeners. Removes all the listeners of the given type (or all registered listeners if no type given).
Allows to wait for async listeners to complete or to ignore subsequent errors from these listeners.
### param: Browser.removeAllListeners.type ### param: Browser.removeAllListeners.type
* since: v1.47 * since: v1.47

View file

@ -6,7 +6,7 @@ BrowserContexts provide a way to operate multiple independent browser sessions.
If a page opens another page, e.g. with a `window.open` call, the popup will belong to the parent page's browser If a page opens another page, e.g. with a `window.open` call, the popup will belong to the parent page's browser
context. context.
Playwright allows creating "incognito" browser contexts with [`method: Browser.newContext`] method. "Incognito" browser Playwright allows creating isolated non-persistent browser contexts with [`method: Browser.newContext`] method. Non-persistent browser
contexts don't write any browsing data to disk. contexts don't write any browsing data to disk.
```js ```js
@ -415,42 +415,13 @@ The order of evaluation of multiple scripts installed via [`method: BrowserConte
[`method: Page.addInitScript`] is not defined. [`method: Page.addInitScript`] is not defined.
::: :::
**Bundling**
If you have a complex script split into several files, it needs to be bundled into a single file first. We recommend running [`esbuild`](https://esbuild.github.io/) or [`webpack`](https://webpack.js.org/) to produce a commonjs module and pass [`option: path`] and [`option: arg`].
```js browser title="mocks/mockRandom.ts"
// This script can import other files.
import { defaultValue } from './defaultValue';
export default function(value?: number) {
window.Math.random = () => value ?? defaultValue;
}
```
```sh
# bundle with esbuild
esbuild mocks/mockRandom.ts --bundle --format=cjs --outfile=mocks/mockRandom.js
```
```js title="tests/example.spec.ts"
const mockPath = { path: path.resolve(__dirname, '../mocks/mockRandom.js') };
// Passing 42 as an argument to the default export function.
await context.addInitScript({ path: mockPath }, 42);
// Make sure to pass something even if you do not need to pass an argument.
// This instructs Playwright to treat the file as a commonjs module.
await context.addInitScript({ path: mockPath }, '');
```
### param: BrowserContext.addInitScript.script ### param: BrowserContext.addInitScript.script
* since: v1.8 * since: v1.8
* langs: js * langs: js
- `script` <[function]|[string]|[Object]> - `script` <[function]|[string]|[Object]>
- `path` ?<[path]> Path to the JavaScript file. If `path` is a relative path, then it is resolved relative to the - `path` ?<[path]> Path to the JavaScript file. If `path` is a relative path, then it is resolved relative to the
current working directory. current working directory. Optional.
- `content` ?<[string]> Raw script content. - `content` ?<[string]> Raw script content. Optional.
Script to be evaluated in all pages in the browser context. Script to be evaluated in all pages in the browser context.
@ -466,9 +437,7 @@ Script to be evaluated in all pages in the browser context.
* langs: js * langs: js
- `arg` ?<[Serializable]> - `arg` ?<[Serializable]>
Optional JSON-serializable argument to pass to [`param: script`]. Optional argument to pass to [`param: script`] (only supported when passing a function).
* When `script` is a function, the argument is passed to it directly.
* When `script` is a file path, the file is assumed to be a commonjs module. The default export, either `module.exports` or `module.exports.default`, should be a function that's going to be executed with this argument.
### param: BrowserContext.addInitScript.path ### param: BrowserContext.addInitScript.path
* since: v1.8 * since: v1.8
@ -1048,8 +1017,10 @@ Returns all open pages in the context.
## async method: BrowserContext.removeAllListeners ## async method: BrowserContext.removeAllListeners
* since: v1.47 * since: v1.47
* langs: js
Removes all the listeners of the given type if the type is given. Otherwise removes all the listeners. Removes all the listeners of the given type (or all registered listeners if no type given).
Allows to wait for async listeners to complete or to ignore subsequent errors from these listeners.
### param: BrowserContext.removeAllListeners.type ### param: BrowserContext.removeAllListeners.type
* since: v1.47 * since: v1.47

View file

@ -866,7 +866,7 @@ await handle.SelectOptionAsync(new[] {
### option: ElementHandle.selectOption.force = %%-input-force-%% ### option: ElementHandle.selectOption.force = %%-input-force-%%
* since: v1.13 * since: v1.13
### option: ElementHandle.selectOption.noWaitAfter = %%-input-no-wait-after-%% ### option: ElementHandle.selectOption.noWaitAfter = %%-input-no-wait-after-removed-%%
* since: v1.8 * since: v1.8
### option: ElementHandle.selectOption.timeout = %%-input-timeout-%% ### option: ElementHandle.selectOption.timeout = %%-input-timeout-%%

View file

@ -1543,7 +1543,7 @@ await frame.SelectOptionAsync("select#colors", new[] { "red", "green", "blue" })
### option: Frame.selectOption.force = %%-input-force-%% ### option: Frame.selectOption.force = %%-input-force-%%
* since: v1.13 * since: v1.13
### option: Frame.selectOption.noWaitAfter = %%-input-no-wait-after-%% ### option: Frame.selectOption.noWaitAfter = %%-input-no-wait-after-removed-%%
* since: v1.8 * since: v1.8
### option: Frame.selectOption.strict = %%-input-strict-%% ### option: Frame.selectOption.strict = %%-input-strict-%%

View file

@ -2055,7 +2055,7 @@ await element.SelectOptionAsync(new[] { "red", "green", "blue" });
### option: Locator.selectOption.force = %%-input-force-%% ### option: Locator.selectOption.force = %%-input-force-%%
* since: v1.14 * since: v1.14
### option: Locator.selectOption.noWaitAfter = %%-input-no-wait-after-%% ### option: Locator.selectOption.noWaitAfter = %%-input-no-wait-after-removed-%%
* since: v1.14 * since: v1.14
### option: Locator.selectOption.timeout = %%-input-timeout-%% ### option: Locator.selectOption.timeout = %%-input-timeout-%%

View file

@ -619,42 +619,13 @@ The order of evaluation of multiple scripts installed via [`method: BrowserConte
[`method: Page.addInitScript`] is not defined. [`method: Page.addInitScript`] is not defined.
::: :::
**Bundling**
If you have a complex script split into several files, it needs to be bundled into a single file first. We recommend running [`esbuild`](https://esbuild.github.io/) or [`webpack`](https://webpack.js.org/) to produce a commonjs module and pass [`option: path`] and [`option: arg`].
```js browser title="mocks/mockRandom.ts"
// This script can import other files.
import { defaultValue } from './defaultValue';
export default function(value?: number) {
window.Math.random = () => value ?? defaultValue;
}
```
```sh
# bundle with esbuild
esbuild mocks/mockRandom.ts --bundle --format=cjs --outfile=mocks/mockRandom.js
```
```js title="tests/example.spec.ts"
const mockPath = { path: path.resolve(__dirname, '../mocks/mockRandom.js') };
// Passing 42 as an argument to the default export function.
await page.addInitScript({ path: mockPath }, 42);
// Make sure to pass something even if you do not need to pass an argument.
// This instructs Playwright to treat the file as a commonjs module.
await page.addInitScript({ path: mockPath }, '');
```
### param: Page.addInitScript.script ### param: Page.addInitScript.script
* since: v1.8 * since: v1.8
* langs: js * langs: js
- `script` <[function]|[string]|[Object]> - `script` <[function]|[string]|[Object]>
- `path` ?<[path]> Path to the JavaScript file. If `path` is a relative path, then it is resolved relative to the - `path` ?<[path]> Path to the JavaScript file. If `path` is a relative path, then it is resolved relative to the
current working directory. current working directory. Optional.
- `content` ?<[string]> Raw script content. - `content` ?<[string]> Raw script content. Optional.
Script to be evaluated in the page. Script to be evaluated in the page.
@ -670,9 +641,7 @@ Script to be evaluated in all pages in the browser context.
* langs: js * langs: js
- `arg` ?<[Serializable]> - `arg` ?<[Serializable]>
Optional JSON-serializable argument to pass to [`param: script`]. Optional argument to pass to [`param: script`] (only supported when passing a function).
* When `script` is a function, the argument is passed to it directly.
* When `script` is a file path, the file is assumed to be a commonjs module. The default export, either `module.exports` or `module.exports.default`, should be a function that's going to be executed with this argument.
### param: Page.addInitScript.path ### param: Page.addInitScript.path
* since: v1.8 * since: v1.8
@ -2195,6 +2164,7 @@ A glob pattern, regex pattern or predicate receiving frame's `url` as a [URL] ob
## method: Page.frameLocator ## method: Page.frameLocator
* since: v1.17 * since: v1.17
regular [`Locator`] instead.
- returns: <[FrameLocator]> - returns: <[FrameLocator]>
When working with iframes, you can create a frame locator that will enter the iframe and allow selecting elements When working with iframes, you can create a frame locator that will enter the iframe and allow selecting elements
@ -3372,8 +3342,23 @@ By default, after calling the handler Playwright will wait until the overlay bec
## async method: Page.removeAllListeners ## async method: Page.removeAllListeners
* since: v1.47 * since: v1.47
* langs: js
Removes all the listeners of the given type if the type is given. Otherwise removes all the listeners. Removes all the listeners of the given type (or all registered listeners if no type given).
Allows to wait for async listeners to complete or to ignore subsequent errors from these listeners.
**Usage**
```js
page.on('request', async request => {
const response = await request.response();
const body = await response.body();
console.log(body.byteLength);
});
await page.goto('https://playwright.dev', { waitUntil: 'domcontentloaded' });
// Waits for all the reported 'request' events to resolve.
await page.removeAllListeners('request', { behavior: 'wait' });
```
### param: Page.removeAllListeners.type ### param: Page.removeAllListeners.type
* since: v1.47 * since: v1.47
@ -3742,7 +3727,7 @@ await page.SelectOptionAsync("select#colors", new[] { "red", "green", "blue" });
### option: Page.selectOption.force = %%-input-force-%% ### option: Page.selectOption.force = %%-input-force-%%
* since: v1.13 * since: v1.13
### option: Page.selectOption.noWaitAfter = %%-input-no-wait-after-%% ### option: Page.selectOption.noWaitAfter = %%-input-no-wait-after-removed-%%
* since: v1.8 * since: v1.8
### option: Page.selectOption.strict = %%-input-strict-%% ### option: Page.selectOption.strict = %%-input-strict-%%

View file

@ -288,7 +288,7 @@ await Expect(page.GetByText("Strawberry")).ToBeVisibleAsync();
```java ```java
// Get the response from the HAR file // Get the response from the HAR file
page.routeFromHAR("./hars/fruit.har", new RouteFromHAROptions() page.routeFromHAR(Path.of("./hars/fruit.har"), new RouteFromHAROptions()
.setUrl("*/**/api/v1/fruits") .setUrl("*/**/api/v1/fruits")
.setUpdate(true) .setUpdate(true)
); );
@ -386,7 +386,7 @@ await page.ExpectByTextAsync("Playwright", new() { Exact = true }).ToBeVisibleAs
// Replay API requests from HAR. // Replay API requests from HAR.
// Either use a matching response from the HAR, // Either use a matching response from the HAR,
// or abort the request if nothing matches. // or abort the request if nothing matches.
page.routeFromHAR("./hars/fruit.har", new RouteFromHAROptions() page.routeFromHAR(Path.of("./hars/fruit.har"), new RouteFromHAROptions()
.setUrl("*/**/api/v1/fruits") .setUrl("*/**/api/v1/fruits")
.setUpdate(false) .setUpdate(false)
); );

View file

@ -191,7 +191,7 @@ test('should use custom proxy on a new context', async ({ browser }) => {
} }
}); });
const page = await context.newPage(); const page = await context.newPage();
await context.close(); await context.close();
}); });
``` ```

View file

@ -722,3 +722,64 @@ export const test = base.extend({
}, { title: 'my fixture' }], }, { title: 'my fixture' }],
}); });
``` ```
## Adding global beforeEach/afterEach hooks
[`method: Test.beforeEach`] and [`method: Test.afterEach`] hooks run before/after each test declared in the same file and same [`method: Test.describe`] block (if any). If you want to declare hooks that run before/after each test globally, you can declare them as auto fixtures like this:
```ts title="fixtures.ts"
import { test as base } from '@playwright/test';
export const test = base.extend<{ forEachTest: void }>({
forEachTest: [async ({ page }, use) => {
// This code runs before every test.
await page.goto('http://localhost:8000');
await use();
// This code runs after every test.
console.log('Last URL:', page.url());
}, { auto: true }], // automatically starts for every test.
});
```
And then import the fixtures in all your tests:
```ts title="mytest.spec.ts"
import { test } from './fixtures';
import { expect } from '@playwright/test';
test('basic', async ({ page }) => {
expect(page).toHaveURL('http://localhost:8000');
await page.goto('https://playwright.dev');
});
```
## Adding global beforeAll/afterAll hooks
[`method: Test.beforeAll`] and [`method: Test.afterAll`] hooks run before/after all tests declared in the same file and same [`method: Test.describe`] block (if any), once per worker process. If you want to declare hooks
that run before/after all tests in every file, you can declare them as auto fixtures with `scope: 'worker'` as follows:
```ts title="fixtures.ts"
import { test as base } from '@playwright/test';
export const test = base.extend<{}, { forEachWorker: void }>({
forEachWorker: [async ({}, use) => {
// This code runs before all the tests in the worker process.
console.log(`Starting test worker ${test.info().workerIndex}`);
await use();
// This code runs after all the tests in the worker process.
console.log(`Stopping test worker ${test.info().workerIndex}`);
}, { scope: 'worker', auto: true }], // automatically starts for every worker.
});
```
And then import the fixtures in all your tests:
```ts title="mytest.spec.ts"
import { test } from './fixtures';
import { expect } from '@playwright/test';
test('basic', async ({ }) => {
// ...
});
```
Note that the fixtures will still run once per [worker process](./test-parallel.md#worker-processes), but you don't need to redeclare them in every file.

View file

@ -80,14 +80,14 @@ By default, Playwright will look up a closest tsconfig for each imported file by
```sh ```sh
# Playwright will choose tsconfig automatically # Playwright will choose tsconfig automatically
npx playwrigh test npx playwright test
``` ```
Alternatively, you can specify a single tsconfig file to use in the command line, and Playwright will use it for all imported files, not only test files. Alternatively, you can specify a single tsconfig file to use in the command line, and Playwright will use it for all imported files, not only test files.
```sh ```sh
# Pass a specific tsconfig # Pass a specific tsconfig
npx playwrigh test --tsconfig=tsconfig.test.json npx playwright test --tsconfig=tsconfig.test.json
``` ```
## Manually compile tests with TypeScript ## Manually compile tests with TypeScript

18
package-lock.json generated
View file

@ -41,7 +41,7 @@
"colors": "^1.4.0", "colors": "^1.4.0",
"concurrently": "^6.2.1", "concurrently": "^6.2.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"dotenv": "^16.0.0", "dotenv": "^16.4.5",
"electron": "^30.1.2", "electron": "^30.1.2",
"esbuild": "^0.18.11", "esbuild": "^0.18.11",
"eslint": "^8.55.0", "eslint": "^8.55.0",
@ -3293,15 +3293,15 @@
} }
}, },
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "16.3.1", "version": "16.4.5",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
"integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
"funding": { "funding": {
"url": "https://github.com/motdotla/dotenv?sponsor=1" "url": "https://dotenvx.com"
} }
}, },
"node_modules/electron": { "node_modules/electron": {
@ -6820,9 +6820,9 @@
} }
}, },
"node_modules/svelte": { "node_modules/svelte": {
"version": "4.2.9", "version": "4.2.19",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.9.tgz", "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.19.tgz",
"integrity": "sha512-hsoB/WZGEPFXeRRLPhPrbRz67PhP6sqYgvwcAs+gWdSQSvNDw+/lTeUJSWe5h2xC97Fz/8QxAOqItwBzNJPU8w==", "integrity": "sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==",
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.2.1", "@ampproject/remapping": "^2.2.1",
"@jridgewell/sourcemap-codec": "^1.4.15", "@jridgewell/sourcemap-codec": "^1.4.15",
@ -8060,7 +8060,7 @@
"playwright": "cli.js" "playwright": "cli.js"
}, },
"devDependencies": { "devDependencies": {
"svelte": "^4.2.8" "svelte": "^4.2.19"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"

View file

@ -79,7 +79,7 @@
"colors": "^1.4.0", "colors": "^1.4.0",
"concurrently": "^6.2.1", "concurrently": "^6.2.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"dotenv": "^16.0.0", "dotenv": "^16.4.5",
"electron": "^30.1.2", "electron": "^30.1.2",
"esbuild": "^0.18.11", "esbuild": "^0.18.11",
"eslint": "^8.55.0", "eslint": "^8.55.0",

View file

@ -75,11 +75,16 @@ export const AttachmentLink: React.FunctionComponent<{
attachment: TestAttachment, attachment: TestAttachment,
href?: string, href?: string,
linkName?: string, linkName?: string,
}> = ({ attachment, href, linkName }) => { openInNewTab?: boolean,
}> = ({ attachment, href, linkName, openInNewTab }) => {
return <TreeItem title={<span> return <TreeItem title={<span>
{attachment.contentType === kMissingContentType ? icons.warning() : icons.attachment()} {attachment.contentType === kMissingContentType ? icons.warning() : icons.attachment()}
{attachment.path && <a href={href || attachment.path} download={downloadFileNameForAttachment(attachment)}>{linkName || attachment.name}</a>} {attachment.path && <a href={href || attachment.path} download={downloadFileNameForAttachment(attachment)}>{linkName || attachment.name}</a>}
{!attachment.path && <span>{linkifyText(attachment.name)}</span>} {!attachment.path && (
openInNewTab
? <a href={URL.createObjectURL(new Blob([attachment.body!], { type: attachment.contentType }))} target='_blank' rel='noreferrer' onClick={e => e.stopPropagation()}>{attachment.name}</a>
: <span>{linkifyText(attachment.name)}</span>
)}
</span>} loadChildren={attachment.body ? () => { </span>} loadChildren={attachment.body ? () => {
return [<div key={1} className='attachment-body'><CopyToClipboard value={attachment.body!}/>{linkifyText(attachment.body!)}</div>]; return [<div key={1} className='attachment-body'><CopyToClipboard value={attachment.body!}/>{linkifyText(attachment.body!)}</div>];
} : undefined} depth={0} style={{ lineHeight: '32px' }}></TreeItem>; } : undefined} depth={0} style={{ lineHeight: '32px' }}></TreeItem>;

View file

@ -67,15 +67,16 @@ export const TestResultView: React.FC<{
anchor: 'video' | 'diff' | '', anchor: 'video' | 'diff' | '',
}> = ({ result, anchor }) => { }> = ({ result, anchor }) => {
const { screenshots, videos, traces, otherAttachments, diffs } = React.useMemo(() => { const { screenshots, videos, traces, otherAttachments, diffs, htmls } = React.useMemo(() => {
const attachments = result?.attachments || []; const attachments = result?.attachments || [];
const screenshots = new Set(attachments.filter(a => a.contentType.startsWith('image/'))); const screenshots = new Set(attachments.filter(a => a.contentType.startsWith('image/')));
const videos = attachments.filter(a => a.name === 'video'); const videos = attachments.filter(a => a.name === 'video');
const traces = attachments.filter(a => a.name === 'trace'); const traces = attachments.filter(a => a.name === 'trace');
const htmls = attachments.filter(a => a.contentType.startsWith('text/html'));
const otherAttachments = new Set<TestAttachment>(attachments); const otherAttachments = new Set<TestAttachment>(attachments);
[...screenshots, ...videos, ...traces].forEach(a => otherAttachments.delete(a)); [...screenshots, ...videos, ...traces, ...htmls].forEach(a => otherAttachments.delete(a));
const diffs = groupImageDiffs(screenshots); const diffs = groupImageDiffs(screenshots);
return { screenshots: [...screenshots], videos, traces, otherAttachments, diffs }; return { screenshots: [...screenshots], videos, traces, otherAttachments, diffs, htmls };
}, [result]); }, [result]);
const videoRef = React.useRef<HTMLDivElement>(null); const videoRef = React.useRef<HTMLDivElement>(null);
@ -135,7 +136,10 @@ export const TestResultView: React.FC<{
</div>)} </div>)}
</AutoChip>} </AutoChip>}
{!!otherAttachments.size && <AutoChip header='Attachments'> {!!(otherAttachments.size + htmls.length) && <AutoChip header='Attachments'>
{[...htmls].map((a, i) => (
<AttachmentLink key={`html-link-${i}`} attachment={a} openInNewTab />)
)}
{[...otherAttachments].map((a, i) => <AttachmentLink key={`attachment-link-${i}`} attachment={a}></AttachmentLink>)} {[...otherAttachments].map((a, i) => <AttachmentLink key={`attachment-link-${i}`} attachment={a}></AttachmentLink>)}
</AutoChip>} </AutoChip>}
</div>; </div>;

View file

@ -16,6 +16,7 @@ This project incorporates components from the projects listed below. The origina
- concat-map@0.0.1 (https://github.com/substack/node-concat-map) - concat-map@0.0.1 (https://github.com/substack/node-concat-map)
- debug@4.3.4 (https://github.com/debug-js/debug) - debug@4.3.4 (https://github.com/debug-js/debug)
- define-lazy-prop@2.0.0 (https://github.com/sindresorhus/define-lazy-prop) - define-lazy-prop@2.0.0 (https://github.com/sindresorhus/define-lazy-prop)
- dotenv@16.4.5 (https://github.com/motdotla/dotenv)
- end-of-stream@1.4.4 (https://github.com/mafintosh/end-of-stream) - end-of-stream@1.4.4 (https://github.com/mafintosh/end-of-stream)
- escape-string-regexp@2.0.0 (https://github.com/sindresorhus/escape-string-regexp) - escape-string-regexp@2.0.0 (https://github.com/sindresorhus/escape-string-regexp)
- extract-zip@2.0.1 (https://github.com/maxogden/extract-zip) - extract-zip@2.0.1 (https://github.com/maxogden/extract-zip)
@ -472,6 +473,34 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
========================================= =========================================
END OF define-lazy-prop@2.0.0 AND INFORMATION END OF define-lazy-prop@2.0.0 AND INFORMATION
%% dotenv@16.4.5 NOTICES AND INFORMATION BEGIN HERE
=========================================
Copyright (c) 2015, Scott Motte
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
=========================================
END OF dotenv@16.4.5 AND INFORMATION
%% end-of-stream@1.4.4 NOTICES AND INFORMATION BEGIN HERE %% end-of-stream@1.4.4 NOTICES AND INFORMATION BEGIN HERE
========================================= =========================================
The MIT License (MIT) The MIT License (MIT)
@ -1514,6 +1543,6 @@ END OF yazl@2.5.1 AND INFORMATION
SUMMARY BEGIN HERE SUMMARY BEGIN HERE
========================================= =========================================
Total Packages: 45 Total Packages: 46
========================================= =========================================
END OF SUMMARY END OF SUMMARY

View file

@ -3,15 +3,15 @@
"browsers": [ "browsers": [
{ {
"name": "chromium", "name": "chromium",
"revision": "1131", "revision": "1133",
"installByDefault": true, "installByDefault": true,
"browserVersion": "128.0.6613.36" "browserVersion": "129.0.6668.22"
}, },
{ {
"name": "chromium-tip-of-tree", "name": "chromium-tip-of-tree",
"revision": "1253", "revision": "1255",
"installByDefault": false, "installByDefault": false,
"browserVersion": "130.0.6670.0" "browserVersion": "130.0.6684.0"
}, },
{ {
"name": "firefox", "name": "firefox",
@ -27,7 +27,7 @@
}, },
{ {
"name": "webkit", "name": "webkit",
"revision": "2062", "revision": "2068",
"installByDefault": true, "installByDefault": true,
"revisionOverrides": { "revisionOverrides": {
"mac10.14": "1446", "mac10.14": "1446",

View file

@ -11,6 +11,7 @@
"colors": "1.4.0", "colors": "1.4.0",
"commander": "8.3.0", "commander": "8.3.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"dotenv": "^16.4.5",
"graceful-fs": "4.2.10", "graceful-fs": "4.2.10",
"https-proxy-agent": "5.0.0", "https-proxy-agent": "5.0.0",
"jpeg-js": "0.4.4", "jpeg-js": "0.4.4",
@ -198,6 +199,17 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/dotenv": {
"version": "16.4.5",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/escape-string-regexp": { "node_modules/escape-string-regexp": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
@ -560,6 +572,11 @@
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
"integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==" "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="
}, },
"dotenv": {
"version": "16.4.5",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg=="
},
"escape-string-regexp": { "escape-string-regexp": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",

View file

@ -12,6 +12,7 @@
"colors": "1.4.0", "colors": "1.4.0",
"commander": "8.3.0", "commander": "8.3.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"dotenv": "^16.4.5",
"graceful-fs": "4.2.10", "graceful-fs": "4.2.10",
"https-proxy-agent": "5.0.0", "https-proxy-agent": "5.0.0",
"jpeg-js": "0.4.4", "jpeg-js": "0.4.4",

View file

@ -20,6 +20,9 @@ export const colors = colorsLibrary;
import debugLibrary from 'debug'; import debugLibrary from 'debug';
export const debug = debugLibrary; export const debug = debugLibrary;
import dotenvLibrary from 'dotenv';
export const dotenv = dotenvLibrary;
export { getProxyForUrl } from 'proxy-from-env'; export { getProxyForUrl } from 'proxy-from-env';
export { HttpsProxyAgent } from 'https-proxy-agent'; export { HttpsProxyAgent } from 'https-proxy-agent';

View file

@ -20,7 +20,7 @@ import fs from 'fs';
import os from 'os'; import os from 'os';
import path from 'path'; import path from 'path';
import type { Command } from '../utilsBundle'; import type { Command } from '../utilsBundle';
import { program } from '../utilsBundle'; import { program, dotenv } from '../utilsBundle';
export { program } from '../utilsBundle'; export { program } from '../utilsBundle';
import { runDriver, runServer, printApiJson, launchBrowserServer } from './driver'; import { runDriver, runServer, printApiJson, launchBrowserServer } from './driver';
import { runTraceInBrowser, runTraceViewerApp } from '../server/trace/viewer/traceViewer'; import { runTraceInBrowser, runTraceViewerApp } from '../server/trace/viewer/traceViewer';
@ -561,6 +561,7 @@ async function open(options: Options, url: string | undefined, language: string)
async function codegen(options: Options & { target: string, output?: string, testIdAttribute?: string }, url: string | undefined) { async function codegen(options: Options & { target: string, output?: string, testIdAttribute?: string }, url: string | undefined) {
const { target: language, output: outputFile, testIdAttribute: testIdAttributeName } = options; const { target: language, output: outputFile, testIdAttribute: testIdAttributeName } = options;
const { context, launchOptions, contextOptions } = await launchContext(options, !!process.env.PWTEST_CLI_HEADLESS, process.env.PWTEST_CLI_EXECUTABLE_PATH); const { context, launchOptions, contextOptions } = await launchContext(options, !!process.env.PWTEST_CLI_HEADLESS, process.env.PWTEST_CLI_EXECUTABLE_PATH);
dotenv.config({ path: 'playwright.env' });
await context._enableRecorder({ await context._enableRecorder({
language, language,
launchOptions, launchOptions,
@ -570,7 +571,6 @@ async function codegen(options: Options & { target: string, output?: string, tes
mode: 'recording', mode: 'recording',
testIdAttributeName, testIdAttributeName,
outputFile: outputFile ? path.resolve(outputFile) : undefined, outputFile: outputFile ? path.resolve(outputFile) : undefined,
handleSIGINT: false,
}); });
await openPage(context, url); await openPage(context, url);
} }

View file

@ -481,7 +481,6 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
mode?: 'recording' | 'inspecting', mode?: 'recording' | 'inspecting',
testIdAttributeName?: string, testIdAttributeName?: string,
outputFile?: string, outputFile?: string,
handleSIGINT?: boolean,
}) { }) {
await this._channel.recorderSupplementEnable(params); await this._channel.recorderSupplementEnable(params);
} }

View file

@ -28,37 +28,20 @@ export function envObjectToArray(env: types.Env): { name: string, value: string
return result; return result;
} }
export async function evaluationScript(fun: Function | string | { path?: string, content?: string }, arg: any, addSourceUrl: boolean = true): Promise<string> { export async function evaluationScript(fun: Function | string | { path?: string, content?: string }, arg?: any, addSourceUrl: boolean = true): Promise<string> {
if (typeof fun === 'function') { if (typeof fun === 'function') {
const source = fun.toString(); const source = fun.toString();
const argString = Object.is(arg, undefined) ? 'undefined' : JSON.stringify(arg); const argString = Object.is(arg, undefined) ? 'undefined' : JSON.stringify(arg);
return `(${source})(${argString})`; return `(${source})(${argString})`;
} }
if (isString(fun)) { if (arg !== undefined)
if (arg !== undefined) throw new Error('Cannot evaluate a string with arguments');
throw new Error('Cannot evaluate a string with arguments'); if (isString(fun))
return fun; return fun;
} if (fun.content !== undefined)
if (fun.content !== undefined) {
if (arg !== undefined)
throw new Error('Cannot evaluate a string with arguments');
return fun.content; return fun.content;
}
if (fun.path !== undefined) { if (fun.path !== undefined) {
let source = await fs.promises.readFile(fun.path, 'utf8'); let source = await fs.promises.readFile(fun.path, 'utf8');
if (arg !== undefined) {
// Assume a CJS module that has a function default export.
source = `(() => {
var exports = {}; var module = { exports };
${source}
let __pw_result__ = module.exports;
if (__pw_result__ && typeof __pw_result__ === 'object' && ('default' in __pw_result__))
__pw_result__ = __pw_result__['default'];
if (typeof __pw_result__ !== 'function')
return __pw_result__;
return __pw_result__(${JSON.stringify(arg)});
})()`;
}
if (addSourceUrl) if (addSourceUrl)
source = addSourceUrlToScript(source, fun.path); source = addSourceUrlToScript(source, fun.path);
return source; return source;

View file

@ -33,7 +33,7 @@ export type WaitForEventOptions = Function | { predicate?: Function, timeout?: n
export type WaitForFunctionOptions = { timeout?: number, polling?: 'raf' | number }; export type WaitForFunctionOptions = { timeout?: number, polling?: 'raf' | number };
export type SelectOption = { value?: string, label?: string, index?: number, valueOrLabel?: string }; export type SelectOption = { value?: string, label?: string, index?: number, valueOrLabel?: string };
export type SelectOptionOptions = { force?: boolean, timeout?: number, noWaitAfter?: boolean }; export type SelectOptionOptions = { force?: boolean, timeout?: number };
export type FilePayload = { name: string, mimeType: string, buffer: Buffer }; export type FilePayload = { name: string, mimeType: string, buffer: Buffer };
export type StorageState = { export type StorageState = {
cookies: channels.NetworkCookie[], cookies: channels.NetworkCookie[],

View file

@ -961,7 +961,6 @@ scheme.BrowserContextRecorderSupplementEnableParams = tObject({
device: tOptional(tString), device: tOptional(tString),
saveStorage: tOptional(tString), saveStorage: tOptional(tString),
outputFile: tOptional(tString), outputFile: tOptional(tString),
handleSIGINT: tOptional(tBoolean),
omitCallTracking: tOptional(tBoolean), omitCallTracking: tOptional(tBoolean),
}); });
scheme.BrowserContextRecorderSupplementEnableResult = tOptional(tObject({})); scheme.BrowserContextRecorderSupplementEnableResult = tOptional(tObject({}));
@ -1637,7 +1636,6 @@ scheme.FrameSelectOptionParams = tObject({
}))), }))),
force: tOptional(tBoolean), force: tOptional(tBoolean),
timeout: tOptional(tNumber), timeout: tOptional(tNumber),
noWaitAfter: tOptional(tBoolean),
}); });
scheme.FrameSelectOptionResult = tObject({ scheme.FrameSelectOptionResult = tObject({
values: tArray(tString), values: tArray(tString),
@ -2001,7 +1999,6 @@ scheme.ElementHandleSelectOptionParams = tObject({
}))), }))),
force: tOptional(tBoolean), force: tOptional(tBoolean),
timeout: tOptional(tNumber), timeout: tOptional(tNumber),
noWaitAfter: tOptional(tBoolean),
}); });
scheme.ElementHandleSelectOptionResult = tObject({ scheme.ElementHandleSelectOptionResult = tObject({
values: tArray(tString), values: tArray(tString),

View file

@ -43,6 +43,7 @@ import { BrowserContextAPIRequestContext } from './fetch';
import type { Artifact } from './artifact'; import type { Artifact } from './artifact';
import { Clock } from './clock'; import { Clock } from './clock';
import { ClientCertificatesProxy } from './socksClientCertificatesInterceptor'; import { ClientCertificatesProxy } from './socksClientCertificatesInterceptor';
import { RecorderApp } from './recorder/recorderApp';
export abstract class BrowserContext extends SdkObject { export abstract class BrowserContext extends SdkObject {
static Events = { static Events = {
@ -130,19 +131,21 @@ export abstract class BrowserContext extends SdkObject {
// When PWDEBUG=1, show inspector for each context. // When PWDEBUG=1, show inspector for each context.
if (debugMode() === 'inspector') if (debugMode() === 'inspector')
await Recorder.show(this, { pauseOnNextStatement: true }); await Recorder.show(this, RecorderApp.factory(this), { pauseOnNextStatement: true });
// When paused, show inspector. // When paused, show inspector.
if (this._debugger.isPaused()) if (this._debugger.isPaused())
Recorder.showInspector(this); Recorder.showInspector(this, RecorderApp.factory(this));
this._debugger.on(Debugger.Events.PausedStateChanged, () => { this._debugger.on(Debugger.Events.PausedStateChanged, () => {
Recorder.showInspector(this); if (this._debugger.isPaused())
Recorder.showInspector(this, RecorderApp.factory(this));
}); });
if (debugMode() === 'console') if (debugMode() === 'console')
await this.extendInjectedScript(consoleApiSource.source); await this.extendInjectedScript(consoleApiSource.source);
if (this._options.serviceWorkers === 'block') if (this._options.serviceWorkers === 'block')
await this.addInitScript(`\nnavigator.serviceWorker.register = async () => { console.warn('Service Worker registration blocked by Playwright'); };\n`); await this.addInitScript(`\nif (navigator.serviceWorker) navigator.serviceWorker.register = async () => { console.warn('Service Worker registration blocked by Playwright'); };\n`);
if (this._options.permissions) if (this._options.permissions)
await this.grantPermissions(this._options.permissions); await this.grantPermissions(this._options.permissions);

View file

@ -609,7 +609,7 @@ class RouteImpl implements network.RouteDelegate {
this._interceptionId = interceptionId; this._interceptionId = interceptionId;
} }
async continue(request: network.Request, overrides: types.NormalizedContinueOverrides): Promise<void> { async continue(overrides: types.NormalizedContinueOverrides): Promise<void> {
this._alreadyContinuedParams = { this._alreadyContinuedParams = {
requestId: this._interceptionId!, requestId: this._interceptionId!,
url: overrides.url, url: overrides.url,

View file

@ -1131,17 +1131,21 @@ using Audits.issueAdded event.
} }
/** /**
* Defines commands and events for browser extensions. Available if the client * Defines commands and events for browser extensions.
is connected using the --remote-debugging-pipe flag and
the --enable-unsafe-extension-debugging flag is set.
*/ */
export module Extensions { export module Extensions {
/**
* Storage areas.
*/
export type StorageArea = "session"|"local"|"sync"|"managed";
/** /**
* Installs an unpacked extension from the filesystem similar to * Installs an unpacked extension from the filesystem similar to
--load-extension CLI flags. Returns extension ID once the extension --load-extension CLI flags. Returns extension ID once the extension
has been installed. has been installed. Available if the client is connected using the
--remote-debugging-pipe flag and the --enable-unsafe-extension-debugging
flag is set.
*/ */
export type loadUnpackedParameters = { export type loadUnpackedParameters = {
/** /**
@ -1155,6 +1159,81 @@ has been installed.
*/ */
id: string; id: string;
} }
/**
* Gets data from extension storage in the given `storageArea`. If `keys` is
specified, these are used to filter the result.
*/
export type getStorageItemsParameters = {
/**
* ID of extension.
*/
id: string;
/**
* StorageArea to retrieve data from.
*/
storageArea: StorageArea;
/**
* Keys to retrieve.
*/
keys?: string[];
}
export type getStorageItemsReturnValue = {
data: { [key: string]: string };
}
/**
* Removes `keys` from extension storage in the given `storageArea`.
*/
export type removeStorageItemsParameters = {
/**
* ID of extension.
*/
id: string;
/**
* StorageArea to remove data from.
*/
storageArea: StorageArea;
/**
* Keys to remove.
*/
keys: string[];
}
export type removeStorageItemsReturnValue = {
}
/**
* Clears extension storage in the given `storageArea`.
*/
export type clearStorageItemsParameters = {
/**
* ID of extension.
*/
id: string;
/**
* StorageArea to remove data from.
*/
storageArea: StorageArea;
}
export type clearStorageItemsReturnValue = {
}
/**
* Sets `values` in extension storage in the given `storageArea`. The provided `values`
will be merged with existing values in the storage area.
*/
export type setStorageItemsParameters = {
/**
* ID of extension.
*/
id: string;
/**
* StorageArea to set data in.
*/
storageArea: StorageArea;
/**
* Values to set.
*/
values: { [key: string]: string };
}
export type setStorageItemsReturnValue = {
}
} }
/** /**
@ -2532,16 +2611,6 @@ stylesheet rules) this rule came from.
*/ */
style: CSSStyle; style: CSSStyle;
} }
/**
* CSS position-fallback rule representation.
*/
export interface CSSPositionFallbackRule {
name: Value;
/**
* List of keyframes.
*/
tryRules: CSSTryRule[];
}
/** /**
* CSS @position-try rule representation. * CSS @position-try rule representation.
*/ */
@ -2888,10 +2957,6 @@ attributes) for a DOM node identified by `nodeId`.
* A list of CSS keyframed animations matching this node. * A list of CSS keyframed animations matching this node.
*/ */
cssKeyframesRules?: CSSKeyframesRule[]; cssKeyframesRules?: CSSKeyframesRule[];
/**
* A list of CSS position fallbacks matching this node.
*/
cssPositionFallbackRules?: CSSPositionFallbackRule[];
/** /**
* A list of CSS @position-try rules matching this node, based on the position-try-fallbacks property. * A list of CSS @position-try rules matching this node, based on the position-try-fallbacks property.
*/ */
@ -3496,7 +3561,7 @@ front-end.
/** /**
* Pseudo element type. * Pseudo element type.
*/ */
export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"marker"|"backdrop"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"; export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"marker"|"backdrop"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scroll-next-button"|"scroll-prev-button"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new";
/** /**
* Shadow root type. * Shadow root type.
*/ */
@ -3646,6 +3711,13 @@ The property is always undefined now.
compatibilityMode?: CompatibilityMode; compatibilityMode?: CompatibilityMode;
assignedSlot?: BackendNode; assignedSlot?: BackendNode;
} }
/**
* A structure to hold the top-level node of a detached tree and an array of its retained descendants.
*/
export interface DetachedElementInfo {
treeNode: Node;
retainedNodeIds: NodeId[];
}
/** /**
* A structure holding an RGBA color. * A structure holding an RGBA color.
*/ */
@ -4693,6 +4765,17 @@ File wrapper.
export type getFileInfoReturnValue = { export type getFileInfoReturnValue = {
path: string; path: string;
} }
/**
* Returns list of detached nodes
*/
export type getDetachedDomNodesParameters = {
}
export type getDetachedDomNodesReturnValue = {
/**
* The list of detached nodes
*/
detachedNodes: DetachedElementInfo[];
}
/** /**
* Enables console to refer to the node with given id via $x (see Command Line API for more details * Enables console to refer to the node with given id via $x (see Command Line API for more details
$x functions). $x functions).
@ -11369,7 +11452,7 @@ as an ad.
* All Permissions Policy features. This enum should match the one defined * All Permissions Policy features. This enum should match the one defined
in third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5. in third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5.
*/ */
export type PermissionsPolicyFeature = "accelerometer"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"cross-origin-isolated"|"deferred-fetch"|"digital-credentials-get"|"direct-sockets"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking"; export type PermissionsPolicyFeature = "accelerometer"|"all-screens-capture"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"cross-origin-isolated"|"deferred-fetch"|"digital-credentials-get"|"direct-sockets"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"media-playback-while-not-visible"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking";
/** /**
* Reason for a permissions policy feature to be disabled. * Reason for a permissions policy feature to be disabled.
*/ */
@ -11784,7 +11867,7 @@ Example URLs: http://www.google.com/file.html -> "google.com"
*/ */
fixed?: number; fixed?: number;
} }
export type ClientNavigationReason = "formSubmissionGet"|"formSubmissionPost"|"httpHeaderRefresh"|"scriptInitiated"|"metaTagRefresh"|"pageBlockInterstitial"|"reload"|"anchorClick"; export type ClientNavigationReason = "anchorClick"|"formSubmissionGet"|"formSubmissionPost"|"httpHeaderRefresh"|"initialFrameNavigation"|"metaTagRefresh"|"other"|"pageBlockInterstitial"|"reload"|"scriptInitiated";
export type ClientNavigationDisposition = "currentTab"|"newTab"|"newWindow"|"download"; export type ClientNavigationDisposition = "currentTab"|"newTab"|"newWindow"|"download";
export interface InstallabilityErrorArgument { export interface InstallabilityErrorArgument {
/** /**
@ -12298,6 +12381,10 @@ when bfcache navigation fails.
* Frame's new url. * Frame's new url.
*/ */
url: string; url: string;
/**
* Navigation type
*/
navigationType: "fragment"|"historyApi"|"other";
} }
/** /**
* Compressed image data requested by the `startScreencast`. * Compressed image data requested by the `startScreencast`.
@ -16922,7 +17009,7 @@ possible for multiple rule sets and links to trigger a single attempt.
/** /**
* List of FinalStatus reasons for Prerender2. * List of FinalStatus reasons for Prerender2.
*/ */
export type PrerenderFinalStatus = "Activated"|"Destroyed"|"LowEndDevice"|"InvalidSchemeRedirect"|"InvalidSchemeNavigation"|"NavigationRequestBlockedByCsp"|"MainFrameNavigation"|"MojoBinderPolicy"|"RendererProcessCrashed"|"RendererProcessKilled"|"Download"|"TriggerDestroyed"|"NavigationNotCommitted"|"NavigationBadHttpStatus"|"ClientCertRequested"|"NavigationRequestNetworkError"|"CancelAllHostsForTesting"|"DidFailLoad"|"Stop"|"SslCertificateError"|"LoginAuthRequested"|"UaChangeRequiresReload"|"BlockedByClient"|"AudioOutputDeviceRequested"|"MixedContent"|"TriggerBackgrounded"|"MemoryLimitExceeded"|"DataSaverEnabled"|"TriggerUrlHasEffectiveUrl"|"ActivatedBeforeStarted"|"InactivePageRestriction"|"StartFailed"|"TimeoutBackgrounded"|"CrossSiteRedirectInInitialNavigation"|"CrossSiteNavigationInInitialNavigation"|"SameSiteCrossOriginRedirectNotOptInInInitialNavigation"|"SameSiteCrossOriginNavigationNotOptInInInitialNavigation"|"ActivationNavigationParameterMismatch"|"ActivatedInBackground"|"EmbedderHostDisallowed"|"ActivationNavigationDestroyedBeforeSuccess"|"TabClosedByUserGesture"|"TabClosedWithoutUserGesture"|"PrimaryMainFrameRendererProcessCrashed"|"PrimaryMainFrameRendererProcessKilled"|"ActivationFramePolicyNotCompatible"|"PreloadingDisabled"|"BatterySaverEnabled"|"ActivatedDuringMainFrameNavigation"|"PreloadingUnsupportedByWebContents"|"CrossSiteRedirectInMainFrameNavigation"|"CrossSiteNavigationInMainFrameNavigation"|"SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation"|"SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation"|"MemoryPressureOnTrigger"|"MemoryPressureAfterTriggered"|"PrerenderingDisabledByDevTools"|"SpeculationRuleRemoved"|"ActivatedWithAuxiliaryBrowsingContexts"|"MaxNumOfRunningEagerPrerendersExceeded"|"MaxNumOfRunningNonEagerPrerendersExceeded"|"MaxNumOfRunningEmbedderPrerendersExceeded"|"PrerenderingUrlHasEffectiveUrl"|"RedirectedPrerenderingUrlHasEffectiveUrl"|"ActivationUrlHasEffectiveUrl"|"JavaScriptInterfaceAdded"|"JavaScriptInterfaceRemoved"|"AllPrerenderingCanceled"|"WindowClosed"; export type PrerenderFinalStatus = "Activated"|"Destroyed"|"LowEndDevice"|"InvalidSchemeRedirect"|"InvalidSchemeNavigation"|"NavigationRequestBlockedByCsp"|"MainFrameNavigation"|"MojoBinderPolicy"|"RendererProcessCrashed"|"RendererProcessKilled"|"Download"|"TriggerDestroyed"|"NavigationNotCommitted"|"NavigationBadHttpStatus"|"ClientCertRequested"|"NavigationRequestNetworkError"|"CancelAllHostsForTesting"|"DidFailLoad"|"Stop"|"SslCertificateError"|"LoginAuthRequested"|"UaChangeRequiresReload"|"BlockedByClient"|"AudioOutputDeviceRequested"|"MixedContent"|"TriggerBackgrounded"|"MemoryLimitExceeded"|"DataSaverEnabled"|"TriggerUrlHasEffectiveUrl"|"ActivatedBeforeStarted"|"InactivePageRestriction"|"StartFailed"|"TimeoutBackgrounded"|"CrossSiteRedirectInInitialNavigation"|"CrossSiteNavigationInInitialNavigation"|"SameSiteCrossOriginRedirectNotOptInInInitialNavigation"|"SameSiteCrossOriginNavigationNotOptInInInitialNavigation"|"ActivationNavigationParameterMismatch"|"ActivatedInBackground"|"EmbedderHostDisallowed"|"ActivationNavigationDestroyedBeforeSuccess"|"TabClosedByUserGesture"|"TabClosedWithoutUserGesture"|"PrimaryMainFrameRendererProcessCrashed"|"PrimaryMainFrameRendererProcessKilled"|"ActivationFramePolicyNotCompatible"|"PreloadingDisabled"|"BatterySaverEnabled"|"ActivatedDuringMainFrameNavigation"|"PreloadingUnsupportedByWebContents"|"CrossSiteRedirectInMainFrameNavigation"|"CrossSiteNavigationInMainFrameNavigation"|"SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation"|"SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation"|"MemoryPressureOnTrigger"|"MemoryPressureAfterTriggered"|"PrerenderingDisabledByDevTools"|"SpeculationRuleRemoved"|"ActivatedWithAuxiliaryBrowsingContexts"|"MaxNumOfRunningEagerPrerendersExceeded"|"MaxNumOfRunningNonEagerPrerendersExceeded"|"MaxNumOfRunningEmbedderPrerendersExceeded"|"PrerenderingUrlHasEffectiveUrl"|"RedirectedPrerenderingUrlHasEffectiveUrl"|"ActivationUrlHasEffectiveUrl"|"JavaScriptInterfaceAdded"|"JavaScriptInterfaceRemoved"|"AllPrerenderingCanceled"|"WindowClosed"|"SlowNetwork"|"OtherPrerenderedPageActivated";
/** /**
* Preloading status values, see also PreloadingTriggeringOutcome. This * Preloading status values, see also PreloadingTriggeringOutcome. This
status is shared by prefetchStatusUpdated and prerenderStatusUpdated. status is shared by prefetchStatusUpdated and prerenderStatusUpdated.
@ -17270,6 +17357,101 @@ supported yet.
} }
} }
/**
* This domain allows configuring virtual Bluetooth devices to test
the web-bluetooth API.
*/
export module BluetoothEmulation {
/**
* Indicates the various states of Central.
*/
export type CentralState = "absent"|"powered-off"|"powered-on";
/**
* Stores the manufacturer data
*/
export interface ManufacturerData {
/**
* Company identifier
https://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/company_identifiers/company_identifiers.yaml
https://usb.org/developers
*/
key: number;
/**
* Manufacturer-specific data
*/
data: binary;
}
/**
* Stores the byte data of the advertisement packet sent by a Bluetooth device.
*/
export interface ScanRecord {
name?: string;
uuids?: string[];
/**
* Stores the external appearance description of the device.
*/
appearance?: number;
/**
* Stores the transmission power of a broadcasting device.
*/
txPower?: number;
/**
* Key is the company identifier and the value is an array of bytes of
manufacturer specific data.
*/
manufacturerData?: ManufacturerData[];
}
/**
* Stores the advertisement packet information that is sent by a Bluetooth device.
*/
export interface ScanEntry {
deviceAddress: string;
rssi: number;
scanRecord: ScanRecord;
}
/**
* Enable the BluetoothEmulation domain.
*/
export type enableParameters = {
/**
* State of the simulated central.
*/
state: CentralState;
}
export type enableReturnValue = {
}
/**
* Disable the BluetoothEmulation domain.
*/
export type disableParameters = {
}
export type disableReturnValue = {
}
/**
* Simulates a peripheral with |address|, |name| and |knownServiceUuids|
that has already been connected to the system.
*/
export type simulatePreconnectedPeripheralParameters = {
address: string;
name: string;
manufacturerData: ManufacturerData[];
knownServiceUuids: string[];
}
export type simulatePreconnectedPeripheralReturnValue = {
}
/**
* Simulates an advertisement packet described in |entry| being received by
the central.
*/
export type simulateAdvertisementParameters = {
entry: ScanEntry;
}
export type simulateAdvertisementReturnValue = {
}
}
/** /**
* This domain is deprecated - use Runtime or Log instead. * This domain is deprecated - use Runtime or Log instead.
*/ */
@ -20122,6 +20304,10 @@ Error was thrown.
"Audits.checkContrast": Audits.checkContrastParameters; "Audits.checkContrast": Audits.checkContrastParameters;
"Audits.checkFormsIssues": Audits.checkFormsIssuesParameters; "Audits.checkFormsIssues": Audits.checkFormsIssuesParameters;
"Extensions.loadUnpacked": Extensions.loadUnpackedParameters; "Extensions.loadUnpacked": Extensions.loadUnpackedParameters;
"Extensions.getStorageItems": Extensions.getStorageItemsParameters;
"Extensions.removeStorageItems": Extensions.removeStorageItemsParameters;
"Extensions.clearStorageItems": Extensions.clearStorageItemsParameters;
"Extensions.setStorageItems": Extensions.setStorageItemsParameters;
"Autofill.trigger": Autofill.triggerParameters; "Autofill.trigger": Autofill.triggerParameters;
"Autofill.setAddresses": Autofill.setAddressesParameters; "Autofill.setAddresses": Autofill.setAddressesParameters;
"Autofill.disable": Autofill.disableParameters; "Autofill.disable": Autofill.disableParameters;
@ -20232,6 +20418,7 @@ Error was thrown.
"DOM.setNodeStackTracesEnabled": DOM.setNodeStackTracesEnabledParameters; "DOM.setNodeStackTracesEnabled": DOM.setNodeStackTracesEnabledParameters;
"DOM.getNodeStackTraces": DOM.getNodeStackTracesParameters; "DOM.getNodeStackTraces": DOM.getNodeStackTracesParameters;
"DOM.getFileInfo": DOM.getFileInfoParameters; "DOM.getFileInfo": DOM.getFileInfoParameters;
"DOM.getDetachedDomNodes": DOM.getDetachedDomNodesParameters;
"DOM.setInspectedNode": DOM.setInspectedNodeParameters; "DOM.setInspectedNode": DOM.setInspectedNodeParameters;
"DOM.setNodeName": DOM.setNodeNameParameters; "DOM.setNodeName": DOM.setNodeNameParameters;
"DOM.setNodeValue": DOM.setNodeValueParameters; "DOM.setNodeValue": DOM.setNodeValueParameters;
@ -20616,6 +20803,10 @@ Error was thrown.
"PWA.launchFilesInApp": PWA.launchFilesInAppParameters; "PWA.launchFilesInApp": PWA.launchFilesInAppParameters;
"PWA.openCurrentPageInApp": PWA.openCurrentPageInAppParameters; "PWA.openCurrentPageInApp": PWA.openCurrentPageInAppParameters;
"PWA.changeAppUserSettings": PWA.changeAppUserSettingsParameters; "PWA.changeAppUserSettings": PWA.changeAppUserSettingsParameters;
"BluetoothEmulation.enable": BluetoothEmulation.enableParameters;
"BluetoothEmulation.disable": BluetoothEmulation.disableParameters;
"BluetoothEmulation.simulatePreconnectedPeripheral": BluetoothEmulation.simulatePreconnectedPeripheralParameters;
"BluetoothEmulation.simulateAdvertisement": BluetoothEmulation.simulateAdvertisementParameters;
"Console.clearMessages": Console.clearMessagesParameters; "Console.clearMessages": Console.clearMessagesParameters;
"Console.disable": Console.disableParameters; "Console.disable": Console.disableParameters;
"Console.enable": Console.enableParameters; "Console.enable": Console.enableParameters;
@ -20722,6 +20913,10 @@ Error was thrown.
"Audits.checkContrast": Audits.checkContrastReturnValue; "Audits.checkContrast": Audits.checkContrastReturnValue;
"Audits.checkFormsIssues": Audits.checkFormsIssuesReturnValue; "Audits.checkFormsIssues": Audits.checkFormsIssuesReturnValue;
"Extensions.loadUnpacked": Extensions.loadUnpackedReturnValue; "Extensions.loadUnpacked": Extensions.loadUnpackedReturnValue;
"Extensions.getStorageItems": Extensions.getStorageItemsReturnValue;
"Extensions.removeStorageItems": Extensions.removeStorageItemsReturnValue;
"Extensions.clearStorageItems": Extensions.clearStorageItemsReturnValue;
"Extensions.setStorageItems": Extensions.setStorageItemsReturnValue;
"Autofill.trigger": Autofill.triggerReturnValue; "Autofill.trigger": Autofill.triggerReturnValue;
"Autofill.setAddresses": Autofill.setAddressesReturnValue; "Autofill.setAddresses": Autofill.setAddressesReturnValue;
"Autofill.disable": Autofill.disableReturnValue; "Autofill.disable": Autofill.disableReturnValue;
@ -20832,6 +21027,7 @@ Error was thrown.
"DOM.setNodeStackTracesEnabled": DOM.setNodeStackTracesEnabledReturnValue; "DOM.setNodeStackTracesEnabled": DOM.setNodeStackTracesEnabledReturnValue;
"DOM.getNodeStackTraces": DOM.getNodeStackTracesReturnValue; "DOM.getNodeStackTraces": DOM.getNodeStackTracesReturnValue;
"DOM.getFileInfo": DOM.getFileInfoReturnValue; "DOM.getFileInfo": DOM.getFileInfoReturnValue;
"DOM.getDetachedDomNodes": DOM.getDetachedDomNodesReturnValue;
"DOM.setInspectedNode": DOM.setInspectedNodeReturnValue; "DOM.setInspectedNode": DOM.setInspectedNodeReturnValue;
"DOM.setNodeName": DOM.setNodeNameReturnValue; "DOM.setNodeName": DOM.setNodeNameReturnValue;
"DOM.setNodeValue": DOM.setNodeValueReturnValue; "DOM.setNodeValue": DOM.setNodeValueReturnValue;
@ -21216,6 +21412,10 @@ Error was thrown.
"PWA.launchFilesInApp": PWA.launchFilesInAppReturnValue; "PWA.launchFilesInApp": PWA.launchFilesInAppReturnValue;
"PWA.openCurrentPageInApp": PWA.openCurrentPageInAppReturnValue; "PWA.openCurrentPageInApp": PWA.openCurrentPageInAppReturnValue;
"PWA.changeAppUserSettings": PWA.changeAppUserSettingsReturnValue; "PWA.changeAppUserSettings": PWA.changeAppUserSettingsReturnValue;
"BluetoothEmulation.enable": BluetoothEmulation.enableReturnValue;
"BluetoothEmulation.disable": BluetoothEmulation.disableReturnValue;
"BluetoothEmulation.simulatePreconnectedPeripheral": BluetoothEmulation.simulatePreconnectedPeripheralReturnValue;
"BluetoothEmulation.simulateAdvertisement": BluetoothEmulation.simulateAdvertisementReturnValue;
"Console.clearMessages": Console.clearMessagesReturnValue; "Console.clearMessages": Console.clearMessagesReturnValue;
"Console.disable": Console.disableReturnValue; "Console.disable": Console.disableReturnValue;
"Console.enable": Console.enableReturnValue; "Console.enable": Console.enableReturnValue;

View file

@ -0,0 +1,3 @@
[*]
../../utils/
../deviceDescriptors.ts

View file

@ -14,13 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
import type { BrowserContextOptions } from '../../..'; import type { BrowserContextOptions } from '../../../types/types';
import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language'; import type { ActionInContext, Language, LanguageGenerator, LanguageGeneratorOptions } from './types';
import { sanitizeDeviceOptions, toSignalMap } from './language'; import { sanitizeDeviceOptions, toClickOptions, toKeyboardModifiers, toSignalMap } from './language';
import type { ActionInContext } from './codeGenerator';
import type { Action } from './recorderActions';
import type { MouseClickOptions } from './utils';
import { toModifiers } from './utils';
import { escapeWithQuotes, asLocator } from '../../utils'; import { escapeWithQuotes, asLocator } from '../../utils';
import { deviceDescriptors } from '../deviceDescriptors'; import { deviceDescriptors } from '../deviceDescriptors';
@ -72,14 +68,8 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
return formatter.format(); return formatter.format();
} }
let subject: string; const locators = actionInContext.frame.framePath.map(selector => `.${this._asLocator(selector)}.ContentFrame()`);
if (actionInContext.frame.isMainFrame) { const subject = `${pageAlias}${locators.join('')}`;
subject = pageAlias;
} else {
const locators = actionInContext.frame.selectorsChain.map(selector => `.FrameLocator(${quote(selector)})`);
subject = `${pageAlias}${locators.join('')}`;
}
const signals = toSignalMap(action); const signals = toSignalMap(action);
if (signals.dialog) { if (signals.dialog) {
@ -93,7 +83,7 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
} }
const lines: string[] = []; const lines: string[] = [];
lines.push(this._generateActionCall(subject, action)); lines.push(this._generateActionCall(subject, actionInContext));
if (signals.download) { if (signals.download) {
lines.unshift(`var download${signals.download.downloadAlias} = await ${pageAlias}.RunAndWaitForDownloadAsync(async () =>\n{`); lines.unshift(`var download${signals.download.downloadAlias} = await ${pageAlias}.RunAndWaitForDownloadAsync(async () =>\n{`);
@ -111,7 +101,8 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
return formatter.format(); return formatter.format();
} }
private _generateActionCall(subject: string, action: Action): string { private _generateActionCall(subject: string, actionInContext: ActionInContext): string {
const action = actionInContext.action;
switch (action.name) { switch (action.name) {
case 'openPage': case 'openPage':
throw Error('Not reached'); throw Error('Not reached');
@ -121,16 +112,7 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
let method = 'Click'; let method = 'Click';
if (action.clickCount === 2) if (action.clickCount === 2)
method = 'DblClick'; method = 'DblClick';
const modifiers = toModifiers(action.modifiers); const options = toClickOptions(action);
const options: MouseClickOptions = {};
if (action.button !== 'left')
options.button = action.button;
if (modifiers.length)
options.modifiers = modifiers;
if (action.clickCount > 2)
options.clickCount = action.clickCount;
if (action.position)
options.position = action.position;
if (!Object.entries(options).length) if (!Object.entries(options).length)
return `await ${subject}.${this._asLocator(action.selector)}.${method}Async();`; return `await ${subject}.${this._asLocator(action.selector)}.${method}Async();`;
const optionsString = formatObject(options, ' ', 'Locator' + method + 'Options'); const optionsString = formatObject(options, ' ', 'Locator' + method + 'Options');
@ -145,7 +127,7 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
case 'setInputFiles': case 'setInputFiles':
return `await ${subject}.${this._asLocator(action.selector)}.SetInputFilesAsync(${formatObject(action.files)});`; return `await ${subject}.${this._asLocator(action.selector)}.SetInputFilesAsync(${formatObject(action.files)});`;
case 'press': { case 'press': {
const modifiers = toModifiers(action.modifiers); const modifiers = toKeyboardModifiers(action.modifiers);
const shortcut = [...modifiers, action.key].join('+'); const shortcut = [...modifiers, action.key].join('+');
return `await ${subject}.${this._asLocator(action.selector)}.PressAsync(${quote(shortcut)});`; return `await ${subject}.${this._asLocator(action.selector)}.PressAsync(${quote(shortcut)});`;
} }

View file

@ -14,13 +14,10 @@
* limitations under the License. * limitations under the License.
*/ */
import type { BrowserContextOptions } from '../../..'; import type { BrowserContextOptions } from '../../../types/types';
import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language'; import type * as types from '../types';
import { toSignalMap } from './language'; import type { ActionInContext, Language, LanguageGenerator, LanguageGeneratorOptions } from './types';
import type { ActionInContext } from './codeGenerator'; import { toClickOptions, toKeyboardModifiers, toSignalMap } from './language';
import type { Action } from './recorderActions';
import type { MouseClickOptions } from './utils';
import { toModifiers } from './utils';
import { deviceDescriptors } from '../deviceDescriptors'; import { deviceDescriptors } from '../deviceDescriptors';
import { JavaScriptFormatter } from './javascript'; import { JavaScriptFormatter } from './javascript';
import { escapeWithQuotes, asLocator } from '../../utils'; import { escapeWithQuotes, asLocator } from '../../utils';
@ -63,16 +60,8 @@ export class JavaLanguageGenerator implements LanguageGenerator {
return formatter.format(); return formatter.format();
} }
let subject: string; const locators = actionInContext.frame.framePath.map(selector => `.${this._asLocator(selector, false)}.contentFrame()`);
let inFrameLocator = false; const subject = `${pageAlias}${locators.join('')}`;
if (actionInContext.frame.isMainFrame) {
subject = pageAlias;
} else {
const locators = actionInContext.frame.selectorsChain.map(selector => `.frameLocator(${quote(selector)})`);
subject = `${pageAlias}${locators.join('')}`;
inFrameLocator = true;
}
const signals = toSignalMap(action); const signals = toSignalMap(action);
if (signals.dialog) { if (signals.dialog) {
@ -82,7 +71,7 @@ export class JavaLanguageGenerator implements LanguageGenerator {
});`); });`);
} }
let code = this._generateActionCall(subject, action, inFrameLocator); let code = this._generateActionCall(subject, actionInContext, !!actionInContext.frame.framePath.length);
if (signals.popup) { if (signals.popup) {
code = `Page ${signals.popup.popupAlias} = ${pageAlias}.waitForPopup(() -> { code = `Page ${signals.popup.popupAlias} = ${pageAlias}.waitForPopup(() -> {
@ -101,7 +90,8 @@ export class JavaLanguageGenerator implements LanguageGenerator {
return formatter.format(); return formatter.format();
} }
private _generateActionCall(subject: string, action: Action, inFrameLocator: boolean): string { private _generateActionCall(subject: string, actionInContext: ActionInContext, inFrameLocator: boolean): string {
const action = actionInContext.action;
switch (action.name) { switch (action.name) {
case 'openPage': case 'openPage':
throw Error('Not reached'); throw Error('Not reached');
@ -111,16 +101,7 @@ export class JavaLanguageGenerator implements LanguageGenerator {
let method = 'click'; let method = 'click';
if (action.clickCount === 2) if (action.clickCount === 2)
method = 'dblclick'; method = 'dblclick';
const modifiers = toModifiers(action.modifiers); const options = toClickOptions(action);
const options: MouseClickOptions = {};
if (action.button !== 'left')
options.button = action.button;
if (modifiers.length)
options.modifiers = modifiers;
if (action.clickCount > 2)
options.clickCount = action.clickCount;
if (action.position)
options.position = action.position;
const optionsText = formatClickOptions(options); const optionsText = formatClickOptions(options);
return `${subject}.${this._asLocator(action.selector, inFrameLocator)}.${method}(${optionsText});`; return `${subject}.${this._asLocator(action.selector, inFrameLocator)}.${method}(${optionsText});`;
} }
@ -133,7 +114,7 @@ export class JavaLanguageGenerator implements LanguageGenerator {
case 'setInputFiles': case 'setInputFiles':
return `${subject}.${this._asLocator(action.selector, inFrameLocator)}.setInputFiles(${formatPath(action.files.length === 1 ? action.files[0] : action.files)});`; return `${subject}.${this._asLocator(action.selector, inFrameLocator)}.setInputFiles(${formatPath(action.files.length === 1 ? action.files[0] : action.files)});`;
case 'press': { case 'press': {
const modifiers = toModifiers(action.modifiers); const modifiers = toKeyboardModifiers(action.modifiers);
const shortcut = [...modifiers, action.key].join('+'); const shortcut = [...modifiers, action.key].join('+');
return `${subject}.${this._asLocator(action.selector, inFrameLocator)}.press(${quote(shortcut)});`; return `${subject}.${this._asLocator(action.selector, inFrameLocator)}.press(${quote(shortcut)});`;
} }
@ -279,7 +260,7 @@ function formatContextOptions(contextOptions: BrowserContextOptions, deviceName:
return lines.join('\n'); return lines.join('\n');
} }
function formatClickOptions(options: MouseClickOptions) { function formatClickOptions(options: types.MouseClickOptions) {
const lines = []; const lines = [];
if (options.button) if (options.button)
lines.push(` .setButton(MouseButton.${options.button.toUpperCase()})`); lines.push(` .setButton(MouseButton.${options.button.toUpperCase()})`);

View file

@ -14,13 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
import type { BrowserContextOptions } from '../../..'; import type { BrowserContextOptions } from '../../../types/types';
import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language'; import type { ActionInContext, Language, LanguageGenerator, LanguageGeneratorOptions } from './types';
import { sanitizeDeviceOptions, toSignalMap } from './language'; import { sanitizeDeviceOptions, toSignalMap, toKeyboardModifiers, toClickOptions } from './language';
import type { ActionInContext } from './codeGenerator';
import type { Action } from './recorderActions';
import type { MouseClickOptions } from './utils';
import { toModifiers } from './utils';
import { deviceDescriptors } from '../deviceDescriptors'; import { deviceDescriptors } from '../deviceDescriptors';
import { escapeWithQuotes, asLocator } from '../../utils'; import { escapeWithQuotes, asLocator } from '../../utils';
@ -52,14 +48,8 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
return formatter.format(); return formatter.format();
} }
let subject: string; const locators = actionInContext.frame.framePath.map(selector => `.${this._asLocator(selector)}.contentFrame()`);
if (actionInContext.frame.isMainFrame) { const subject = `${pageAlias}${locators.join('')}`;
subject = pageAlias;
} else {
const locators = actionInContext.frame.selectorsChain.map(selector => `.frameLocator(${quote(selector)})`);
subject = `${pageAlias}${locators.join('')}`;
}
const signals = toSignalMap(action); const signals = toSignalMap(action);
if (signals.dialog) { if (signals.dialog) {
@ -74,7 +64,7 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
if (signals.download) if (signals.download)
formatter.add(`const download${signals.download.downloadAlias}Promise = ${pageAlias}.waitForEvent('download');`); formatter.add(`const download${signals.download.downloadAlias}Promise = ${pageAlias}.waitForEvent('download');`);
formatter.add(this._generateActionCall(subject, action)); formatter.add(wrapWithStep(actionInContext.description, this._generateActionCall(subject, actionInContext)));
if (signals.popup) if (signals.popup)
formatter.add(`const ${signals.popup.popupAlias} = await ${signals.popup.popupAlias}Promise;`); formatter.add(`const ${signals.popup.popupAlias} = await ${signals.popup.popupAlias}Promise;`);
@ -84,7 +74,8 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
return formatter.format(); return formatter.format();
} }
private _generateActionCall(subject: string, action: Action): string { private _generateActionCall(subject: string, actionInContext: ActionInContext): string {
const action = actionInContext.action;
switch (action.name) { switch (action.name) {
case 'openPage': case 'openPage':
throw Error('Not reached'); throw Error('Not reached');
@ -94,16 +85,7 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
let method = 'click'; let method = 'click';
if (action.clickCount === 2) if (action.clickCount === 2)
method = 'dblclick'; method = 'dblclick';
const modifiers = toModifiers(action.modifiers); const options = toClickOptions(action);
const options: MouseClickOptions = {};
if (action.button !== 'left')
options.button = action.button;
if (modifiers.length)
options.modifiers = modifiers;
if (action.clickCount > 2)
options.clickCount = action.clickCount;
if (action.position)
options.position = action.position;
const optionsString = formatOptions(options, false); const optionsString = formatOptions(options, false);
return `await ${subject}.${this._asLocator(action.selector)}.${method}(${optionsString});`; return `await ${subject}.${this._asLocator(action.selector)}.${method}(${optionsString});`;
} }
@ -116,7 +98,7 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
case 'setInputFiles': case 'setInputFiles':
return `await ${subject}.${this._asLocator(action.selector)}.setInputFiles(${formatObject(action.files.length === 1 ? action.files[0] : action.files)});`; return `await ${subject}.${this._asLocator(action.selector)}.setInputFiles(${formatObject(action.files.length === 1 ? action.files[0] : action.files)});`;
case 'press': { case 'press': {
const modifiers = toModifiers(action.modifiers); const modifiers = toKeyboardModifiers(action.modifiers);
const shortcut = [...modifiers, action.key].join('+'); const shortcut = [...modifiers, action.key].join('+');
return `await ${subject}.${this._asLocator(action.selector)}.press(${quote(shortcut)});`; return `await ${subject}.${this._asLocator(action.selector)}.press(${quote(shortcut)});`;
} }
@ -276,3 +258,9 @@ export class JavaScriptFormatter {
function quote(text: string) { function quote(text: string) {
return escapeWithQuotes(text, '\''); return escapeWithQuotes(text, '\'');
} }
function wrapWithStep(description: string | undefined, body: string) {
return description ? `await test.step(\`${description}\`, async () => {
${body}
});` : body;
}

View file

@ -15,8 +15,7 @@
*/ */
import { asLocator } from '../../utils'; import { asLocator } from '../../utils';
import type { ActionInContext } from './codeGenerator'; import type { ActionInContext, Language, LanguageGenerator, LanguageGeneratorOptions } from './types';
import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language';
export class JsonlLanguageGenerator implements LanguageGenerator { export class JsonlLanguageGenerator implements LanguageGenerator {
id = 'jsonl'; id = 'jsonl';

View file

@ -0,0 +1,84 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { BrowserContextOptions } from '../../..';
import type * as actions from '../recorder/recorderActions';
import type * as types from '../types';
import type { ActionInContext, LanguageGenerator, LanguageGeneratorOptions } from './types';
export function generateCode(actions: ActionInContext[], languageGenerator: LanguageGenerator, options: LanguageGeneratorOptions) {
const header = languageGenerator.generateHeader(options);
const footer = languageGenerator.generateFooter(options.saveStorage);
const actionTexts = actions.map(a => languageGenerator.generateAction(a)).filter(Boolean);
const text = [header, ...actionTexts, footer].join('\n');
return { header, footer, actionTexts, text };
}
export function sanitizeDeviceOptions(device: any, options: BrowserContextOptions): BrowserContextOptions {
// Filter out all the properties from the device descriptor.
const cleanedOptions: Record<string, any> = {};
for (const property in options) {
if (JSON.stringify(device[property]) !== JSON.stringify((options as any)[property]))
cleanedOptions[property] = (options as any)[property];
}
return cleanedOptions;
}
export function toSignalMap(action: actions.Action) {
let popup: actions.PopupSignal | undefined;
let download: actions.DownloadSignal | undefined;
let dialog: actions.DialogSignal | undefined;
for (const signal of action.signals) {
if (signal.name === 'popup')
popup = signal;
else if (signal.name === 'download')
download = signal;
else if (signal.name === 'dialog')
dialog = signal;
}
return {
popup,
download,
dialog,
};
}
export function toKeyboardModifiers(modifiers: number): types.SmartKeyboardModifier[] {
const result: types.SmartKeyboardModifier[] = [];
if (modifiers & 1)
result.push('Alt');
if (modifiers & 2)
result.push('ControlOrMeta');
if (modifiers & 4)
result.push('ControlOrMeta');
if (modifiers & 8)
result.push('Shift');
return result;
}
export function toClickOptions(action: actions.ClickAction): types.MouseClickOptions {
const modifiers = toKeyboardModifiers(action.modifiers);
const options: types.MouseClickOptions = {};
if (action.button !== 'left')
options.button = action.button;
if (modifiers.length)
options.modifiers = modifiers;
if (action.clickCount > 2)
options.clickCount = action.clickCount;
if (action.position)
options.position = action.position;
return options;
}

View file

@ -0,0 +1,37 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { JavaLanguageGenerator } from './java';
import { JavaScriptLanguageGenerator } from './javascript';
import { JsonlLanguageGenerator } from './jsonl';
import { CSharpLanguageGenerator } from './csharp';
import { PythonLanguageGenerator } from './python';
export function languageSet() {
return new Set([
new JavaLanguageGenerator('junit'),
new JavaLanguageGenerator('library'),
new JavaScriptLanguageGenerator(/* isPlaywrightTest */false),
new JavaScriptLanguageGenerator(/* isPlaywrightTest */true),
new PythonLanguageGenerator(/* isAsync */false, /* isPytest */true),
new PythonLanguageGenerator(/* isAsync */false, /* isPytest */false),
new PythonLanguageGenerator(/* isAsync */true, /* isPytest */false),
new CSharpLanguageGenerator('mstest'),
new CSharpLanguageGenerator('nunit'),
new CSharpLanguageGenerator('library'),
new JsonlLanguageGenerator(),
]);
}

View file

@ -14,13 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
import type { BrowserContextOptions } from '../../..'; import type { BrowserContextOptions } from '../../../types/types';
import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language'; import type { ActionInContext, Language, LanguageGenerator, LanguageGeneratorOptions } from './types';
import { sanitizeDeviceOptions, toSignalMap } from './language'; import { sanitizeDeviceOptions, toSignalMap, toKeyboardModifiers, toClickOptions } from './language';
import type { ActionInContext } from './codeGenerator';
import type { Action } from './recorderActions';
import type { MouseClickOptions } from './utils';
import { toModifiers } from './utils';
import { escapeWithQuotes, toSnakeCase, asLocator } from '../../utils'; import { escapeWithQuotes, toSnakeCase, asLocator } from '../../utils';
import { deviceDescriptors } from '../deviceDescriptors'; import { deviceDescriptors } from '../deviceDescriptors';
@ -59,20 +55,14 @@ export class PythonLanguageGenerator implements LanguageGenerator {
return formatter.format(); return formatter.format();
} }
let subject: string; const locators = actionInContext.frame.framePath.map(selector => `.${this._asLocator(selector)}.content_frame()`);
if (actionInContext.frame.isMainFrame) { const subject = `${pageAlias}${locators.join('')}`;
subject = pageAlias;
} else {
const locators = actionInContext.frame.selectorsChain.map(selector => `.frame_locator(${quote(selector)})`);
subject = `${pageAlias}${locators.join('')}`;
}
const signals = toSignalMap(action); const signals = toSignalMap(action);
if (signals.dialog) if (signals.dialog)
formatter.add(` ${pageAlias}.once("dialog", lambda dialog: dialog.dismiss())`); formatter.add(` ${pageAlias}.once("dialog", lambda dialog: dialog.dismiss())`);
let code = `${this._awaitPrefix}${this._generateActionCall(subject, action)}`; let code = `${this._awaitPrefix}${this._generateActionCall(subject, actionInContext)}`;
if (signals.popup) { if (signals.popup) {
code = `${this._asyncPrefix}with ${pageAlias}.expect_popup() as ${signals.popup.popupAlias}_info { code = `${this._asyncPrefix}with ${pageAlias}.expect_popup() as ${signals.popup.popupAlias}_info {
@ -93,7 +83,8 @@ export class PythonLanguageGenerator implements LanguageGenerator {
return formatter.format(); return formatter.format();
} }
private _generateActionCall(subject: string, action: Action): string { private _generateActionCall(subject: string, actionInContext: ActionInContext): string {
const action = actionInContext.action;
switch (action.name) { switch (action.name) {
case 'openPage': case 'openPage':
throw Error('Not reached'); throw Error('Not reached');
@ -103,16 +94,7 @@ export class PythonLanguageGenerator implements LanguageGenerator {
let method = 'click'; let method = 'click';
if (action.clickCount === 2) if (action.clickCount === 2)
method = 'dblclick'; method = 'dblclick';
const modifiers = toModifiers(action.modifiers); const options = toClickOptions(action);
const options: MouseClickOptions = {};
if (action.button !== 'left')
options.button = action.button;
if (modifiers.length)
options.modifiers = modifiers;
if (action.clickCount > 2)
options.clickCount = action.clickCount;
if (action.position)
options.position = action.position;
const optionsString = formatOptions(options, false); const optionsString = formatOptions(options, false);
return `${subject}.${this._asLocator(action.selector)}.${method}(${optionsString})`; return `${subject}.${this._asLocator(action.selector)}.${method}(${optionsString})`;
} }
@ -125,7 +107,7 @@ export class PythonLanguageGenerator implements LanguageGenerator {
case 'setInputFiles': case 'setInputFiles':
return `${subject}.${this._asLocator(action.selector)}.set_input_files(${formatValue(action.files.length === 1 ? action.files[0] : action.files)})`; return `${subject}.${this._asLocator(action.selector)}.set_input_files(${formatValue(action.files.length === 1 ? action.files[0] : action.files)})`;
case 'press': { case 'press': {
const modifiers = toModifiers(action.modifiers); const modifiers = toKeyboardModifiers(action.modifiers);
const shortcut = [...modifiers, action.key].join('+'); const shortcut = [...modifiers, action.key].join('+');
return `${subject}.${this._asLocator(action.selector)}.press(${quote(shortcut)})`; return `${subject}.${this._asLocator(action.selector)}.press(${quote(shortcut)})`;
} }

View file

@ -0,0 +1,50 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { BrowserContextOptions, LaunchOptions } from '../../../types/types';
import type * as actions from '../recorder/recorderActions';
import type { Language } from '../../utils';
export type { Language } from '../../utils';
export type LanguageGeneratorOptions = {
browserName: string;
launchOptions: LaunchOptions;
contextOptions: BrowserContextOptions;
deviceName?: string;
saveStorage?: string;
};
export type FrameDescription = {
pageAlias: string;
framePath: string[];
};
export type ActionInContext = {
frame: FrameDescription;
description?: string;
action: actions.Action;
committed?: boolean;
};
export interface LanguageGenerator {
id: string;
groupName: string;
name: string;
highlighter: Language;
generateHeader(options: LanguageGeneratorOptions): string;
generateAction(actionInContext: ActionInContext): string;
generateFooter(saveStorage: string | undefined): string;
}

View file

@ -52,7 +52,6 @@ export class DebugController extends SdkObject {
initialize(codegenId: string, sdkLanguage: Language) { initialize(codegenId: string, sdkLanguage: Language) {
this._codegenId = codegenId; this._codegenId = codegenId;
this._sdkLanguage = sdkLanguage; this._sdkLanguage = sdkLanguage;
Recorder.setAppFactory(async () => new InspectingRecorderApp(this));
} }
setAutoCloseAllowed(allowed: boolean) { setAutoCloseAllowed(allowed: boolean) {
@ -62,7 +61,6 @@ export class DebugController extends SdkObject {
dispose() { dispose() {
this.setReportStateChanged(false); this.setReportStateChanged(false);
this.setAutoCloseAllowed(false); this.setAutoCloseAllowed(false);
Recorder.setAppFactory(undefined);
} }
setReportStateChanged(enabled: boolean) { setReportStateChanged(enabled: boolean) {
@ -199,7 +197,7 @@ export class DebugController extends SdkObject {
const contexts = new Set<BrowserContext>(); const contexts = new Set<BrowserContext>();
for (const page of this._playwright.allPages()) for (const page of this._playwright.allPages())
contexts.add(page.context()); contexts.add(page.context());
const result = await Promise.all([...contexts].map(c => Recorder.show(c, { omitCallTracking: true }))); const result = await Promise.all([...contexts].map(c => Recorder.show(c, () => Promise.resolve(new InspectingRecorderApp(this)), { omitCallTracking: true })));
return result.filter(Boolean) as Recorder[]; return result.filter(Boolean) as Recorder[];
} }

View file

@ -110,7 +110,7 @@
"defaultBrowserType": "webkit" "defaultBrowserType": "webkit"
}, },
"Galaxy S5": { "Galaxy S5": {
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 360, "width": 360,
"height": 640 "height": 640
@ -121,7 +121,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Galaxy S5 landscape": { "Galaxy S5 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 640, "width": 640,
"height": 360 "height": 360
@ -132,7 +132,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Galaxy S8": { "Galaxy S8": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 360, "width": 360,
"height": 740 "height": 740
@ -143,7 +143,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Galaxy S8 landscape": { "Galaxy S8 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 740, "width": 740,
"height": 360 "height": 360
@ -154,7 +154,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Galaxy S9+": { "Galaxy S9+": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 320, "width": 320,
"height": 658 "height": 658
@ -165,7 +165,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Galaxy S9+ landscape": { "Galaxy S9+ landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 658, "width": 658,
"height": 320 "height": 320
@ -176,7 +176,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Galaxy Tab S4": { "Galaxy Tab S4": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Safari/537.36",
"viewport": { "viewport": {
"width": 712, "width": 712,
"height": 1138 "height": 1138
@ -187,7 +187,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Galaxy Tab S4 landscape": { "Galaxy Tab S4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Safari/537.36",
"viewport": { "viewport": {
"width": 1138, "width": 1138,
"height": 712 "height": 712
@ -1098,7 +1098,7 @@
"defaultBrowserType": "webkit" "defaultBrowserType": "webkit"
}, },
"LG Optimus L70": { "LG Optimus L70": {
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.22 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 384, "width": 384,
"height": 640 "height": 640
@ -1109,7 +1109,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"LG Optimus L70 landscape": { "LG Optimus L70 landscape": {
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.22 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 640, "width": 640,
"height": 384 "height": 384
@ -1120,7 +1120,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Microsoft Lumia 550": { "Microsoft Lumia 550": {
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36 Edge/14.14263", "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36 Edge/14.14263",
"viewport": { "viewport": {
"width": 640, "width": 640,
"height": 360 "height": 360
@ -1131,7 +1131,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Microsoft Lumia 550 landscape": { "Microsoft Lumia 550 landscape": {
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36 Edge/14.14263", "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36 Edge/14.14263",
"viewport": { "viewport": {
"width": 360, "width": 360,
"height": 640 "height": 640
@ -1142,7 +1142,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Microsoft Lumia 950": { "Microsoft Lumia 950": {
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36 Edge/14.14263", "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36 Edge/14.14263",
"viewport": { "viewport": {
"width": 360, "width": 360,
"height": 640 "height": 640
@ -1153,7 +1153,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Microsoft Lumia 950 landscape": { "Microsoft Lumia 950 landscape": {
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36 Edge/14.14263", "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36 Edge/14.14263",
"viewport": { "viewport": {
"width": 640, "width": 640,
"height": 360 "height": 360
@ -1164,7 +1164,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 10": { "Nexus 10": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Safari/537.36",
"viewport": { "viewport": {
"width": 800, "width": 800,
"height": 1280 "height": 1280
@ -1175,7 +1175,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 10 landscape": { "Nexus 10 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Safari/537.36",
"viewport": { "viewport": {
"width": 1280, "width": 1280,
"height": 800 "height": 800
@ -1186,7 +1186,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 4": { "Nexus 4": {
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 384, "width": 384,
"height": 640 "height": 640
@ -1197,7 +1197,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 4 landscape": { "Nexus 4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 640, "width": 640,
"height": 384 "height": 384
@ -1208,7 +1208,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 5": { "Nexus 5": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 360, "width": 360,
"height": 640 "height": 640
@ -1219,7 +1219,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 5 landscape": { "Nexus 5 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 640, "width": 640,
"height": 360 "height": 360
@ -1230,7 +1230,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 5X": { "Nexus 5X": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 412, "width": 412,
"height": 732 "height": 732
@ -1241,7 +1241,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 5X landscape": { "Nexus 5X landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 732, "width": 732,
"height": 412 "height": 412
@ -1252,7 +1252,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 6": { "Nexus 6": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 412, "width": 412,
"height": 732 "height": 732
@ -1263,7 +1263,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 6 landscape": { "Nexus 6 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 732, "width": 732,
"height": 412 "height": 412
@ -1274,7 +1274,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 6P": { "Nexus 6P": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 412, "width": 412,
"height": 732 "height": 732
@ -1285,7 +1285,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 6P landscape": { "Nexus 6P landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 732, "width": 732,
"height": 412 "height": 412
@ -1296,7 +1296,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 7": { "Nexus 7": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Safari/537.36",
"viewport": { "viewport": {
"width": 600, "width": 600,
"height": 960 "height": 960
@ -1307,7 +1307,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 7 landscape": { "Nexus 7 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Safari/537.36",
"viewport": { "viewport": {
"width": 960, "width": 960,
"height": 600 "height": 600
@ -1362,7 +1362,7 @@
"defaultBrowserType": "webkit" "defaultBrowserType": "webkit"
}, },
"Pixel 2": { "Pixel 2": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 411, "width": 411,
"height": 731 "height": 731
@ -1373,7 +1373,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 2 landscape": { "Pixel 2 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 731, "width": 731,
"height": 411 "height": 411
@ -1384,7 +1384,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 2 XL": { "Pixel 2 XL": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 411, "width": 411,
"height": 823 "height": 823
@ -1395,7 +1395,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 2 XL landscape": { "Pixel 2 XL landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 823, "width": 823,
"height": 411 "height": 411
@ -1406,7 +1406,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 3": { "Pixel 3": {
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 393, "width": 393,
"height": 786 "height": 786
@ -1417,7 +1417,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 3 landscape": { "Pixel 3 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 786, "width": 786,
"height": 393 "height": 393
@ -1428,7 +1428,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 4": { "Pixel 4": {
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 353, "width": 353,
"height": 745 "height": 745
@ -1439,7 +1439,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 4 landscape": { "Pixel 4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 745, "width": 745,
"height": 353 "height": 353
@ -1450,7 +1450,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 4a (5G)": { "Pixel 4a (5G)": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36",
"screen": { "screen": {
"width": 412, "width": 412,
"height": 892 "height": 892
@ -1465,7 +1465,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 4a (5G) landscape": { "Pixel 4a (5G) landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36",
"screen": { "screen": {
"height": 892, "height": 892,
"width": 412 "width": 412
@ -1480,7 +1480,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 5": { "Pixel 5": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36",
"screen": { "screen": {
"width": 393, "width": 393,
"height": 851 "height": 851
@ -1495,7 +1495,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 5 landscape": { "Pixel 5 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36",
"screen": { "screen": {
"width": 851, "width": 851,
"height": 393 "height": 393
@ -1510,7 +1510,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 7": { "Pixel 7": {
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36",
"screen": { "screen": {
"width": 412, "width": 412,
"height": 915 "height": 915
@ -1525,7 +1525,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 7 landscape": { "Pixel 7 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36",
"screen": { "screen": {
"width": 915, "width": 915,
"height": 412 "height": 412
@ -1540,7 +1540,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Moto G4": { "Moto G4": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 360, "width": 360,
"height": 640 "height": 640
@ -1551,7 +1551,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Moto G4 landscape": { "Moto G4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 640, "width": 640,
"height": 360 "height": 360
@ -1562,7 +1562,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Desktop Chrome HiDPI": { "Desktop Chrome HiDPI": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Safari/537.36",
"screen": { "screen": {
"width": 1792, "width": 1792,
"height": 1120 "height": 1120
@ -1577,7 +1577,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Desktop Edge HiDPI": { "Desktop Edge HiDPI": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36 Edg/128.0.6613.36", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Safari/537.36 Edg/129.0.6668.22",
"screen": { "screen": {
"width": 1792, "width": 1792,
"height": 1120 "height": 1120
@ -1622,7 +1622,7 @@
"defaultBrowserType": "webkit" "defaultBrowserType": "webkit"
}, },
"Desktop Chrome": { "Desktop Chrome": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Safari/537.36",
"screen": { "screen": {
"width": 1920, "width": 1920,
"height": 1080 "height": 1080
@ -1637,7 +1637,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Desktop Edge": { "Desktop Edge": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36 Edg/128.0.6613.36", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Safari/537.36 Edg/129.0.6668.22",
"screen": { "screen": {
"width": 1920, "width": 1920,
"height": 1080 "height": 1080

View file

@ -39,6 +39,7 @@ import type { Dialog } from '../dialog';
import type { ConsoleMessage } from '../console'; import type { ConsoleMessage } from '../console';
import { serializeError } from '../errors'; import { serializeError } from '../errors';
import { ElementHandleDispatcher } from './elementHandlerDispatcher'; import { ElementHandleDispatcher } from './elementHandlerDispatcher';
import { RecorderApp } from '../recorder/recorderApp';
export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channels.BrowserContextChannel, DispatcherScope> implements channels.BrowserContextChannel { export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channels.BrowserContextChannel, DispatcherScope> implements channels.BrowserContextChannel {
_type_EventTarget = true; _type_EventTarget = true;
@ -291,7 +292,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
} }
async recorderSupplementEnable(params: channels.BrowserContextRecorderSupplementEnableParams): Promise<void> { async recorderSupplementEnable(params: channels.BrowserContextRecorderSupplementEnableParams): Promise<void> {
await Recorder.show(this._context, params); await Recorder.show(this._context, RecorderApp.factory(this._context), params);
} }
async pause(params: channels.BrowserContextPauseParams, metadata: CallMetadata) { async pause(params: channels.BrowserContextPauseParams, metadata: CallMetadata) {

View file

@ -536,7 +536,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return this._retryPointerAction(progress, 'tap', true /* waitForEnabled */, point => this._page.touchscreen.tap(point.x, point.y), { ...options, waitAfter: 'disabled' }); return this._retryPointerAction(progress, 'tap', true /* waitForEnabled */, point => this._page.touchscreen.tap(point.x, point.y), { ...options, waitAfter: 'disabled' });
} }
async selectOption(metadata: CallMetadata, elements: ElementHandle[], values: types.SelectOption[], options: { noWaitAfter?: boolean } & types.CommonActionOptions): Promise<string[]> { async selectOption(metadata: CallMetadata, elements: ElementHandle[], values: types.SelectOption[], options: types.CommonActionOptions): Promise<string[]> {
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
return controller.run(async progress => { return controller.run(async progress => {
const result = await this._selectOption(progress, elements, values, options); const result = await this._selectOption(progress, elements, values, options);
@ -544,7 +544,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
}, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options));
} }
async _selectOption(progress: Progress, elements: ElementHandle[], values: types.SelectOption[], options: { noWaitAfter?: boolean } & types.CommonActionOptions): Promise<string[] | 'error:notconnected'> { async _selectOption(progress: Progress, elements: ElementHandle[], values: types.SelectOption[], options: types.CommonActionOptions): Promise<string[] | 'error:notconnected'> {
let resultingOptions: string[] = []; let resultingOptions: string[] = [];
await this._retryAction(progress, 'select option', async () => { await this._retryAction(progress, 'select option', async () => {
await progress.beforeInputAction(this); await progress.beforeInputAction(this);

View file

@ -226,7 +226,7 @@ class FFRouteImpl implements network.RouteDelegate {
this._request = request; this._request = request;
} }
async continue(request: network.Request, overrides: types.NormalizedContinueOverrides) { async continue(overrides: types.NormalizedContinueOverrides) {
await this._session.sendMayFail('Network.resumeInterceptedRequest', { await this._session.sendMayFail('Network.resumeInterceptedRequest', {
requestId: this._request._id, requestId: this._request._id,
url: overrides.url, url: overrides.url,

View file

@ -291,8 +291,7 @@ export class FrameManager {
if (request._documentId) if (request._documentId)
frame.setPendingDocument({ documentId: request._documentId, request }); frame.setPendingDocument({ documentId: request._documentId, request });
if (request._isFavicon) { if (request._isFavicon) {
if (route) route?.continue({ isFallback: true }).catch(() => {});
route.continue(request, { isFallback: true }).catch(() => {});
return; return;
} }
this._page.emitOnContext(BrowserContext.Events.Request, request); this._page.emitOnContext(BrowserContext.Events.Request, request);
@ -800,7 +799,7 @@ export class Frame extends SdkObject {
const result = await resolved.injected.evaluateHandle((injected, { info, root }) => { const result = await resolved.injected.evaluateHandle((injected, { info, root }) => {
const elements = injected.querySelectorAll(info.parsed, root || document); const elements = injected.querySelectorAll(info.parsed, root || document);
const element: Element | undefined = elements[0]; const element: Element | undefined = elements[0];
const visible = element ? injected.isVisible(element) : false; const visible = element ? injected.utils.isElementVisible(element) : false;
let log = ''; let log = '';
if (elements.length > 1) { if (elements.length > 1) {
if (info.strict) if (info.strict)
@ -1344,7 +1343,7 @@ export class Frame extends SdkObject {
}, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options));
} }
async selectOption(metadata: CallMetadata, selector: string, elements: dom.ElementHandle[], values: types.SelectOption[], options: { noWaitAfter?: boolean } & types.CommonActionOptions = {}): Promise<string[]> { async selectOption(metadata: CallMetadata, selector: string, elements: dom.ElementHandle[], values: types.SelectOption[], options: types.CommonActionOptions = {}): Promise<string[]> {
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
return controller.run(async progress => { return controller.run(async progress => {
return await this._retryWithProgressIfNotConnected(progress, selector, options.strict, !options.force /* performLocatorHandlersCheckpoint */, handle => handle._selectOption(progress, elements, values, options)); return await this._retryWithProgressIfNotConnected(progress, selector, options.strict, !options.force /* performLocatorHandlersCheckpoint */, handle => handle._selectOption(progress, elements, values, options));

View file

@ -1,10 +1,21 @@
const path = require('path');
module.exports = { module.exports = {
rules: { parser: "@typescript-eslint/parser",
"no-restricted-globals": [ plugins: ["@typescript-eslint", "notice"],
"error", parserOptions: {
{ "name": "window" }, ecmaVersion: 9,
{ "name": "document" }, sourceType: "module",
{ "name": "globalThis" }, project: path.join(__dirname, '../../../../../tsconfig.json'),
] },
} rules: {
"no-restricted-globals": [
"error",
{ "name": "window" },
{ "name": "document" },
{ "name": "globalThis" },
],
'@typescript-eslint/no-floating-promises': 'error',
"@typescript-eslint/no-unnecessary-boolean-literal-compare": 2,
},
}; };

View file

@ -216,7 +216,7 @@ export class ClockController {
const sinceLastSync = now - this._realTime!.lastSyncTicks; const sinceLastSync = now - this._realTime!.lastSyncTicks;
this._realTime!.lastSyncTicks = now; this._realTime!.lastSyncTicks = now;
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
this._runTo(shiftTicks(this._now.ticks, sinceLastSync)).catch(e => console.error(e)).then(() => this._updateRealTimeTimer()); void this._runTo(shiftTicks(this._now.ticks, sinceLastSync)).catch(e => console.error(e)).then(() => this._updateRealTimeTimer());
}, callAt - this._now.ticks), }, callAt - this._now.ticks),
}; };
} }
@ -239,7 +239,12 @@ export class ClockController {
addTimer(options: { func: TimerHandler, type: TimerType, delay?: number | string, args?: any[] }): number { addTimer(options: { func: TimerHandler, type: TimerType, delay?: number | string, args?: any[] }): number {
this._replayLogOnce(); this._replayLogOnce();
if (options.func === undefined)
if (options.type === TimerType.AnimationFrame && !options.func)
throw new Error('Callback must be provided to requestAnimationFrame calls');
if (options.type === TimerType.IdleCallback && !options.func)
throw new Error('Callback must be provided to requestIdleCallback calls');
if ([TimerType.Timeout, TimerType.Interval].includes(options.type) && !options.func && options.delay === undefined)
throw new Error('Callback must be provided to timer calls'); throw new Error('Callback must be provided to timer calls');
let delay = options.delay ? +options.delay : 0; let delay = options.delay ? +options.delay : 0;

View file

@ -29,11 +29,13 @@ import type { CSSComplexSelectorList } from '../../utils/isomorphic/cssParser';
import { generateSelector, type GenerateSelectorOptions } from './selectorGenerator'; import { generateSelector, type GenerateSelectorOptions } from './selectorGenerator';
import type * as channels from '@protocol/channels'; import type * as channels from '@protocol/channels';
import { Highlight } from './highlight'; import { Highlight } from './highlight';
import { getChecked, getAriaDisabled, getAriaRole, getElementAccessibleName, getElementAccessibleDescription } from './roleUtils'; import { getChecked, getAriaDisabled, getAriaRole, getElementAccessibleName, getElementAccessibleDescription, beginAriaCaches, endAriaCaches } from './roleUtils';
import { kLayoutSelectorNames, type LayoutSelectorName, layoutSelectorScore } from './layoutSelectorUtils'; import { kLayoutSelectorNames, type LayoutSelectorName, layoutSelectorScore } from './layoutSelectorUtils';
import { asLocator } from '../../utils/isomorphic/locatorGenerators'; import { asLocator } from '../../utils/isomorphic/locatorGenerators';
import type { Language } from '../../utils/isomorphic/locatorGenerators'; import type { Language } from '../../utils/isomorphic/locatorGenerators';
import { cacheNormalizedWhitespaces, normalizeWhiteSpace, trimStringWithEllipsis } from '../../utils/isomorphic/stringUtils'; import { cacheNormalizedWhitespaces, escapeHTML, escapeHTMLAttribute, normalizeWhiteSpace, trimStringWithEllipsis } from '../../utils/isomorphic/stringUtils';
import { selectorForSimpleDomNodeId, generateSimpleDomNode } from './simpleDom';
import type { SimpleDomNode } from './simpleDom';
export type FrameExpectParams = Omit<channels.FrameExpectParams, 'expectedValue'> & { expectedValue?: any }; export type FrameExpectParams = Omit<channels.FrameExpectParams, 'expectedValue'> & { expectedValue?: any };
@ -66,7 +68,25 @@ export class InjectedScript {
// eslint-disable-next-line no-restricted-globals // eslint-disable-next-line no-restricted-globals
readonly window: Window & typeof globalThis; readonly window: Window & typeof globalThis;
readonly document: Document; readonly document: Document;
readonly utils = { isInsideScope, elementText, asLocator, normalizeWhiteSpace, cacheNormalizedWhitespaces };
// Recorder must use any external dependencies through InjectedScript.
// Otherwise it will end up with a copy of all modules it uses, and any
// module-level globals will be duplicated, which leads to subtle bugs.
readonly utils = {
asLocator,
beginAriaCaches,
cacheNormalizedWhitespaces,
elementText,
endAriaCaches,
escapeHTML,
escapeHTMLAttribute,
getAriaRole,
getElementAccessibleDescription,
getElementAccessibleName,
isElementVisible,
isInsideScope,
normalizeWhiteSpace,
};
// eslint-disable-next-line no-restricted-globals // eslint-disable-next-line no-restricted-globals
constructor(window: Window & typeof globalThis, isUnderTest: boolean, sdkLanguage: Language, testIdAttributeNameForStrictErrorAndConsoleCodegen: string, stableRafCount: number, browserName: string, customEngines: { name: string, engine: SelectorEngine }[]) { constructor(window: Window & typeof globalThis, isUnderTest: boolean, sdkLanguage: Language, testIdAttributeNameForStrictErrorAndConsoleCodegen: string, stableRafCount: number, browserName: string, customEngines: { name: string, engine: SelectorEngine }[]) {
@ -426,10 +446,6 @@ export class InjectedScript {
return new constrFunction(this, params); return new constrFunction(this, params);
} }
isVisible(element: Element): boolean {
return isElementVisible(element);
}
async viewportRatio(element: Element): Promise<number> { async viewportRatio(element: Element): Promise<number> {
return await new Promise(resolve => { return await new Promise(resolve => {
const observer = new IntersectionObserver(entries => { const observer = new IntersectionObserver(entries => {
@ -567,9 +583,9 @@ export class InjectedScript {
} }
if (state === 'visible') if (state === 'visible')
return this.isVisible(element); return isElementVisible(element);
if (state === 'hidden') if (state === 'hidden')
return !this.isVisible(element); return !isElementVisible(element);
const disabled = getAriaDisabled(element); const disabled = getAriaDisabled(element);
if (state === 'disabled') if (state === 'disabled')
@ -1297,16 +1313,15 @@ export class InjectedScript {
throw this.createStacklessError('Unknown expect matcher: ' + expression); throw this.createStacklessError('Unknown expect matcher: ' + expression);
} }
getElementAccessibleName(element: Element, includeHidden?: boolean): string { generateSimpleDomNode(selector: string): SimpleDomNode | undefined {
return getElementAccessibleName(element, !!includeHidden); const element = this.querySelector(this.parseSelector(selector), this.document.documentElement, true);
if (!element)
return;
return generateSimpleDomNode(this, element);
} }
getElementAccessibleDescription(element: Element, includeHidden?: boolean): string { selectorForSimpleDomNodeId(nodeId: string) {
return getElementAccessibleDescription(element, !!includeHidden); return selectorForSimpleDomNodeId(this, nodeId);
}
getAriaRole(element: Element) {
return getAriaRole(element);
} }
} }

View file

@ -1,4 +1,4 @@
# Recorder must use any external dependencies through InjectedScript. # Recorder must use any external dependencies through injectedScript.utils.
# Otherwise it will end up with a copy of all modules it uses, and any # Otherwise it will end up with a copy of all modules it uses, and any
# module-level globals will be duplicated, which leads to subtle bugs. # module-level globals will be duplicated, which leads to subtle bugs.
[*] [*]

View file

@ -21,9 +21,10 @@ import type { Mode, OverlayState, UIState } from '@recorder/recorderTypes';
import type { ElementText } from '../selectorUtils'; import type { ElementText } from '../selectorUtils';
import type { Highlight, HighlightOptions } from '../highlight'; import type { Highlight, HighlightOptions } from '../highlight';
import clipPaths from './clipPaths'; import clipPaths from './clipPaths';
import type { SimpleDomNode } from '../simpleDom';
interface RecorderDelegate { interface RecorderDelegate {
performAction?(action: actions.Action): Promise<void>; performAction?(action: actions.PerformOnRecordAction): Promise<void>;
recordAction?(action: actions.Action): Promise<void>; recordAction?(action: actions.Action): Promise<void>;
setSelector?(selector: string): Promise<void>; setSelector?(selector: string): Promise<void>;
setMode?(mode: Mode): Promise<void>; setMode?(mode: Mode): Promise<void>;
@ -168,7 +169,7 @@ class InspectTool implements RecorderTool {
if (this._hoveredModel?.tooltipListItemSelected) if (this._hoveredModel?.tooltipListItemSelected)
this._reset(true); this._reset(true);
else if (this._assertVisibility) else if (this._assertVisibility)
this._recorder.delegate.setMode?.('recording'); this._recorder.setMode('recording');
} }
} }
@ -182,15 +183,15 @@ class InspectTool implements RecorderTool {
private _commit(selector: string) { private _commit(selector: string) {
if (this._assertVisibility) { if (this._assertVisibility) {
this._recorder.delegate.recordAction?.({ this._recorder.recordAction({
name: 'assertVisible', name: 'assertVisible',
selector, selector,
signals: [], signals: [],
}); });
this._recorder.delegate.setMode?.('recording'); this._recorder.setMode('recording');
this._recorder.overlay?.flashToolSucceeded('assertingVisibility'); this._recorder.overlay?.flashToolSucceeded('assertingVisibility');
} else { } else {
this._recorder.delegate.setSelector?.(selector); this._recorder.setSelector(selector);
} }
} }
@ -338,7 +339,7 @@ class RecordActionTool implements RecorderTool {
const target = this._recorder.deepEventTarget(event); const target = this._recorder.deepEventTarget(event);
if (target.nodeName === 'INPUT' && (target as HTMLInputElement).type.toLowerCase() === 'file') { if (target.nodeName === 'INPUT' && (target as HTMLInputElement).type.toLowerCase() === 'file') {
this._recorder.delegate.recordAction?.({ this._recorder.recordAction({
name: 'setInputFiles', name: 'setInputFiles',
selector: this._activeModel!.selector, selector: this._activeModel!.selector,
signals: [], signals: [],
@ -348,7 +349,7 @@ class RecordActionTool implements RecorderTool {
} }
if (isRangeInput(target)) { if (isRangeInput(target)) {
this._recorder.delegate.recordAction?.({ this._recorder.recordAction({
name: 'fill', name: 'fill',
// must use hoveredModel instead of activeModel for it to work in webkit // must use hoveredModel instead of activeModel for it to work in webkit
selector: this._hoveredModel!.selector, selector: this._hoveredModel!.selector,
@ -367,7 +368,7 @@ class RecordActionTool implements RecorderTool {
// Non-navigating actions are simply recorded by Playwright. // Non-navigating actions are simply recorded by Playwright.
if (this._consumedDueWrongTarget(event)) if (this._consumedDueWrongTarget(event))
return; return;
this._recorder.delegate.recordAction?.({ this._recorder.recordAction({
name: 'fill', name: 'fill',
selector: this._activeModel!.selector, selector: this._activeModel!.selector,
signals: [], signals: [],
@ -483,26 +484,27 @@ class RecordActionTool implements RecorderTool {
return true; return true;
} }
private async _performAction(action: actions.Action) { private _performAction(action: actions.PerformOnRecordAction) {
this._hoveredElement = null; this._hoveredElement = null;
this._hoveredModel = null; this._hoveredModel = null;
this._activeModel = null; this._activeModel = null;
this._recorder.updateHighlight(null, false); this._recorder.updateHighlight(null, false);
this._performingAction = true; this._performingAction = true;
await this._recorder.delegate.performAction?.(action).catch(() => {}); void this._recorder.performAction(action).then(() => {
this._performingAction = false; this._performingAction = false;
// If that was a keyboard action, it similarly requires new selectors for active model. // If that was a keyboard action, it similarly requires new selectors for active model.
this._onFocus(false); this._onFocus(false);
if (this._recorder.injectedScript.isUnderTest) { if (this._recorder.injectedScript.isUnderTest) {
// Serialize all to string as we cannot attribute console message to isolated world // Serialize all to string as we cannot attribute console message to isolated world
// in Firefox. // in Firefox.
console.error('Action performed for test: ' + JSON.stringify({ // eslint-disable-line no-console console.error('Action performed for test: ' + JSON.stringify({ // eslint-disable-line no-console
hovered: this._hoveredModel ? (this._hoveredModel as any).selector : null, hovered: this._hoveredModel ? (this._hoveredModel as any).selector : null,
active: this._activeModel ? (this._activeModel as any).selector : null, active: this._activeModel ? (this._activeModel as any).selector : null,
})); }));
} }
});
} }
private _shouldGenerateKeyPressFor(event: KeyboardEvent): boolean { private _shouldGenerateKeyPressFor(event: KeyboardEvent): boolean {
@ -613,7 +615,7 @@ class TextAssertionTool implements RecorderTool {
onKeyDown(event: KeyboardEvent) { onKeyDown(event: KeyboardEvent) {
if (event.key === 'Escape') if (event.key === 'Escape')
this._recorder.delegate.setMode?.('recording'); this._recorder.setMode('recording');
consumeEvent(event); consumeEvent(event);
} }
@ -680,8 +682,8 @@ class TextAssertionTool implements RecorderTool {
if (!this._action || !this._dialog.isShowing()) if (!this._action || !this._dialog.isShowing())
return; return;
this._dialog.close(); this._dialog.close();
this._recorder.delegate.recordAction?.(this._action); this._recorder.recordAction(this._action);
this._recorder.delegate.setMode?.('recording'); this._recorder.setMode('recording');
} }
private _showDialog() { private _showDialog() {
@ -726,8 +728,8 @@ class TextAssertionTool implements RecorderTool {
const action = this._generateAction(); const action = this._generateAction();
if (!action) if (!action)
return; return;
this._recorder.delegate.recordAction?.(action); this._recorder.recordAction(action);
this._recorder.delegate.setMode?.('recording'); this._recorder.setMode('recording');
this._recorder.overlay?.flashToolSucceeded('assertingValue'); this._recorder.overlay?.flashToolSucceeded('assertingValue');
} }
} }
@ -799,7 +801,7 @@ class Overlay {
this._dragState = { offsetX: this._offsetX, dragStart: { x: (event as MouseEvent).clientX, y: 0 } }; this._dragState = { offsetX: this._offsetX, dragStart: { x: (event as MouseEvent).clientX, y: 0 } };
}), }),
addEventListener(this._recordToggle, 'click', () => { addEventListener(this._recordToggle, 'click', () => {
this._recorder.delegate.setMode?.(this._recorder.state.mode === 'none' || this._recorder.state.mode === 'standby' || this._recorder.state.mode === 'inspecting' ? 'recording' : 'standby'); this._recorder.setMode(this._recorder.state.mode === 'none' || this._recorder.state.mode === 'standby' || this._recorder.state.mode === 'inspecting' ? 'recording' : 'standby');
}), }),
addEventListener(this._pickLocatorToggle, 'click', () => { addEventListener(this._pickLocatorToggle, 'click', () => {
const newMode: Record<Mode, Mode> = { const newMode: Record<Mode, Mode> = {
@ -812,19 +814,19 @@ class Overlay {
'assertingVisibility': 'recording-inspecting', 'assertingVisibility': 'recording-inspecting',
'assertingValue': 'recording-inspecting', 'assertingValue': 'recording-inspecting',
}; };
this._recorder.delegate.setMode?.(newMode[this._recorder.state.mode]); this._recorder.setMode(newMode[this._recorder.state.mode]);
}), }),
addEventListener(this._assertVisibilityToggle, 'click', () => { addEventListener(this._assertVisibilityToggle, 'click', () => {
if (!this._assertVisibilityToggle.classList.contains('disabled')) if (!this._assertVisibilityToggle.classList.contains('disabled'))
this._recorder.delegate.setMode?.(this._recorder.state.mode === 'assertingVisibility' ? 'recording' : 'assertingVisibility'); this._recorder.setMode(this._recorder.state.mode === 'assertingVisibility' ? 'recording' : 'assertingVisibility');
}), }),
addEventListener(this._assertTextToggle, 'click', () => { addEventListener(this._assertTextToggle, 'click', () => {
if (!this._assertTextToggle.classList.contains('disabled')) if (!this._assertTextToggle.classList.contains('disabled'))
this._recorder.delegate.setMode?.(this._recorder.state.mode === 'assertingText' ? 'recording' : 'assertingText'); this._recorder.setMode(this._recorder.state.mode === 'assertingText' ? 'recording' : 'assertingText');
}), }),
addEventListener(this._assertValuesToggle, 'click', () => { addEventListener(this._assertValuesToggle, 'click', () => {
if (!this._assertValuesToggle.classList.contains('disabled')) if (!this._assertValuesToggle.classList.contains('disabled'))
this._recorder.delegate.setMode?.(this._recorder.state.mode === 'assertingValue' ? 'recording' : 'assertingValue'); this._recorder.setMode(this._recorder.state.mode === 'assertingValue' ? 'recording' : 'assertingValue');
}), }),
]; ];
} }
@ -890,7 +892,7 @@ class Overlay {
const halfGapSize = (this._recorder.injectedScript.window.innerWidth - this._measure.width) / 2 - 10; const halfGapSize = (this._recorder.injectedScript.window.innerWidth - this._measure.width) / 2 - 10;
this._offsetX = Math.max(-halfGapSize, Math.min(halfGapSize, this._offsetX)); this._offsetX = Math.max(-halfGapSize, Math.min(halfGapSize, this._offsetX));
this._updateVisualPosition(); this._updateVisualPosition();
this._recorder.delegate.setOverlayState?.({ offsetX: this._offsetX }); this._recorder.setOverlayState({ offsetX: this._offsetX });
consumeEvent(event); consumeEvent(event);
return true; return true;
} }
@ -924,9 +926,14 @@ export class Recorder {
readonly highlight: Highlight; readonly highlight: Highlight;
readonly overlay: Overlay | undefined; readonly overlay: Overlay | undefined;
private _stylesheet: CSSStyleSheet; private _stylesheet: CSSStyleSheet;
state: UIState = { mode: 'none', testIdAttributeName: 'data-testid', language: 'javascript', overlay: { offsetX: 0 } }; state: UIState = {
mode: 'none',
testIdAttributeName: 'data-testid',
language: 'javascript',
overlay: { offsetX: 0 },
};
readonly document: Document; readonly document: Document;
delegate: RecorderDelegate = {}; private _delegate: RecorderDelegate = {};
constructor(injectedScript: InjectedScript) { constructor(injectedScript: InjectedScript) {
this.document = injectedScript.document; this.document = injectedScript.document;
@ -994,7 +1001,7 @@ export class Recorder {
} }
setUIState(state: UIState, delegate: RecorderDelegate) { setUIState(state: UIState, delegate: RecorderDelegate) {
this.delegate = delegate; this._delegate = delegate;
if (state.actionPoint && this.state.actionPoint && state.actionPoint.x === this.state.actionPoint.x && state.actionPoint.y === this.state.actionPoint.y) { if (state.actionPoint && this.state.actionPoint && state.actionPoint.x === this.state.actionPoint.x && state.actionPoint.y === this.state.actionPoint.y) {
// All good. // All good.
@ -1155,7 +1162,7 @@ export class Recorder {
tooltipText = this.injectedScript.utils.asLocator(this.state.language, model.selector); tooltipText = this.injectedScript.utils.asLocator(this.state.language, model.selector);
this.highlight.updateHighlight(model?.elements || [], { ...model, tooltipText }); this.highlight.updateHighlight(model?.elements || [], { ...model, tooltipText });
if (userGesture) if (userGesture)
this.delegate.highlightUpdated?.(); this._delegate.highlightUpdated?.();
} }
private _ignoreOverlayEvent(event: Event) { private _ignoreOverlayEvent(event: Event) {
@ -1172,6 +1179,26 @@ export class Recorder {
} }
return event.composedPath()[0] as HTMLElement; return event.composedPath()[0] as HTMLElement;
} }
setMode(mode: Mode) {
void this._delegate.setMode?.(mode);
}
async performAction(action: actions.PerformOnRecordAction) {
await this._delegate.performAction?.(action).catch(() => {});
}
recordAction(action: actions.Action) {
void this._delegate.recordAction?.(action);
}
setOverlayState(state: { offsetX: number; }) {
void this._delegate.setOverlayState?.(state);
}
setSelector(selector: string) {
void this._delegate.setSelector?.(selector);
}
} }
class Dialog { class Dialog {
@ -1361,8 +1388,8 @@ function createSvgElement(doc: Document, { tagName, attrs, children }: SvgJson):
} }
interface Embedder { interface Embedder {
__pw_recorderPerformAction(action: actions.Action): Promise<void>; __pw_recorderPerformAction(action: actions.PerformOnRecordAction, simpleDomNode?: SimpleDomNode): Promise<void>;
__pw_recorderRecordAction(action: actions.Action): Promise<void>; __pw_recorderRecordAction(action: actions.Action, simpleDomNode?: SimpleDomNode): Promise<void>;
__pw_recorderState(): Promise<UIState>; __pw_recorderState(): Promise<UIState>;
__pw_recorderSetSelector(selector: string): Promise<void>; __pw_recorderSetSelector(selector: string): Promise<void>;
__pw_recorderSetMode(mode: Mode): Promise<void>; __pw_recorderSetMode(mode: Mode): Promise<void>;
@ -1407,12 +1434,12 @@ export class PollingRecorder implements RecorderDelegate {
this._pollRecorderModeTimer = this._recorder.injectedScript.builtinSetTimeout(() => this._pollRecorderMode(), pollPeriod); this._pollRecorderModeTimer = this._recorder.injectedScript.builtinSetTimeout(() => this._pollRecorderMode(), pollPeriod);
} }
async performAction(action: actions.Action) { async performAction(action: actions.PerformOnRecordAction, simpleDomNode?: SimpleDomNode) {
await this._embedder.__pw_recorderPerformAction(action); await this._embedder.__pw_recorderPerformAction(action, simpleDomNode);
} }
async recordAction(action: actions.Action): Promise<void> { async recordAction(action: actions.Action, simpleDomNode?: SimpleDomNode): Promise<void> {
await this._embedder.__pw_recorderRecordAction(action); await this._embedder.__pw_recorderRecordAction(action, simpleDomNode);
} }
async setSelector(selector: string): Promise<void> { async setSelector(selector: string): Promise<void> {

View file

@ -0,0 +1,120 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { InjectedScript } from './injectedScript';
const leafRoles = new Set([
'button',
'checkbox',
'combobox',
'link',
'textbox',
]);
export type SimpleDom = {
markup: string;
elements: Map<string, Element>;
};
export type SimpleDomNode = {
dom: SimpleDom;
id: string;
tag: string;
};
let lastDom: SimpleDom | undefined;
export function generateSimpleDom(injectedScript: InjectedScript): SimpleDom {
return generate(injectedScript).dom;
}
export function generateSimpleDomNode(injectedScript: InjectedScript, target: Element): SimpleDomNode {
return generate(injectedScript, target).node!;
}
export function selectorForSimpleDomNodeId(injectedScript: InjectedScript, id: string): string {
const element = lastDom?.elements.get(id);
if (!element)
throw new Error(`Internal error: element with id "${id}" not found`);
return injectedScript.generateSelectorSimple(element);
}
function generate(injectedScript: InjectedScript, target?: Element): { dom: SimpleDom, node?: SimpleDomNode } {
const normalizeWhitespace = (text: string) => text.replace(/[\s\n]+/g, match => match.includes('\n') ? '\n' : ' ');
const tokens: string[] = [];
const elements = new Map<string, Element>();
let lastId = 0;
let resultTarget: { tag: string, id: string } | undefined;
const visit = (node: Node) => {
if (node.nodeType === Node.TEXT_NODE) {
tokens.push(node.nodeValue!);
return;
}
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as Element;
if (element.nodeName === 'SCRIPT' || element.nodeName === 'STYLE' || element.nodeName === 'NOSCRIPT')
return;
if (injectedScript.utils.isElementVisible(element)) {
const role = injectedScript.utils.getAriaRole(element) as string;
if (role && leafRoles.has(role)) {
let value: string | undefined;
if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA')
value = (element as HTMLInputElement | HTMLTextAreaElement).value;
const name = injectedScript.utils.getElementAccessibleName(element, false);
const structuralId = String(++lastId);
elements.set(structuralId, element);
tokens.push(renderTag(injectedScript, role, name, structuralId, { value }));
if (element === target) {
const tagNoValue = renderTag(injectedScript, role, name, structuralId);
resultTarget = { tag: tagNoValue, id: structuralId };
}
return;
}
}
for (let child = element.firstChild; child; child = child.nextSibling)
visit(child);
}
};
injectedScript.utils.beginAriaCaches();
try {
visit(injectedScript.document.body);
} finally {
injectedScript.utils.endAriaCaches();
}
const dom = {
markup: normalizeWhitespace(tokens.join(' ')),
elements
};
if (target && !resultTarget)
throw new Error('Target element is not in the simple DOM');
lastDom = dom;
return { dom, node: resultTarget ? { dom, ...resultTarget } : undefined };
}
function renderTag(injectedScript: InjectedScript, role: string, name: string, id: string, params?: { value?: string }): string {
const escapedTextContent = injectedScript.utils.escapeHTML(name);
const escapedValue = injectedScript.utils.escapeHTMLAttribute(params?.value || '');
switch (role) {
case 'button': return `<button id="${id}">${escapedTextContent}</button>`;
case 'link': return `<a id="${id}">${escapedTextContent}</a>`;
case 'textbox': return `<input id="${id}" title="${escapedTextContent}" value="${escapedValue}"></input>`;
}
return `<div role=${role} id="${id}">${escapedTextContent}</div>`;
}

View file

@ -324,7 +324,7 @@ export class Route extends SdkObject {
this._request._setOverrides(overrides); this._request._setOverrides(overrides);
if (!overrides.isFallback) if (!overrides.isFallback)
this._request._context.emit(BrowserContext.Events.RequestContinued, this._request); this._request._context.emit(BrowserContext.Events.RequestContinued, this._request);
await this._delegate.continue(this._request, overrides); await this._delegate.continue(overrides);
this._endHandling(); this._endHandling();
} }
@ -612,7 +612,7 @@ export class WebSocket extends SdkObject {
export interface RouteDelegate { export interface RouteDelegate {
abort(errorCode: string): Promise<void>; abort(errorCode: string): Promise<void>;
fulfill(response: types.NormalizedFulfillResponse): Promise<void>; fulfill(response: types.NormalizedFulfillResponse): Promise<void>;
continue(request: Request, overrides: types.NormalizedContinueOverrides): Promise<void>; continue(overrides: types.NormalizedContinueOverrides): Promise<void>;
} }
// List taken from https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml with extra 306 and 418 codes. // List taken from https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml with extra 306 and 418 codes.

View file

@ -14,42 +14,25 @@
* limitations under the License. * limitations under the License.
*/ */
import * as fs from 'fs';
import type * as actions from './recorder/recorderActions';
import type * as channels from '@protocol/channels'; import type * as channels from '@protocol/channels';
import type { ActionInContext } from './recorder/codeGenerator';
import { CodeGenerator } from './recorder/codeGenerator';
import { toClickOptions, toModifiers } from './recorder/utils';
import { Page } from './page';
import { Frame } from './frames';
import { BrowserContext } from './browserContext';
import { JavaLanguageGenerator } from './recorder/java';
import { JavaScriptLanguageGenerator } from './recorder/javascript';
import { JsonlLanguageGenerator } from './recorder/jsonl';
import { CSharpLanguageGenerator } from './recorder/csharp';
import { PythonLanguageGenerator } from './recorder/python';
import * as recorderSource from '../generated/recorderSource';
import * as consoleApiSource from '../generated/consoleApiSource';
import { EmptyRecorderApp } from './recorder/recorderApp';
import type { IRecorderApp } from './recorder/recorderApp';
import { RecorderApp } from './recorder/recorderApp';
import type { CallMetadata, InstrumentationListener, SdkObject } from './instrumentation';
import type { Point } from '../common/types';
import type { CallLog, CallLogStatus, EventData, Mode, OverlayState, Source, UIState } from '@recorder/recorderTypes'; import type { CallLog, CallLogStatus, EventData, Mode, OverlayState, Source, UIState } from '@recorder/recorderTypes';
import { createGuid, isUnderTest, monotonicTime } from '../utils'; import * as fs from 'fs';
import { metadataToCallLog } from './recorder/recorderUtils'; import type { Point } from '../common/types';
import { Debugger } from './debugger'; import * as consoleApiSource from '../generated/consoleApiSource';
import { EventEmitter } from 'events'; import { isUnderTest } from '../utils';
import { raceAgainstDeadline } from '../utils/timeoutRunner';
import type { Language, LanguageGenerator } from './recorder/language';
import { locatorOrSelectorAsSelector } from '../utils/isomorphic/locatorParser'; import { locatorOrSelectorAsSelector } from '../utils/isomorphic/locatorParser';
import { quoteCSSAttributeValue, eventsHelper, type RegisteredListener } from '../utils'; import { BrowserContext } from './browserContext';
import type { Dialog } from './dialog'; import { type Language } from './codegen/types';
import { Debugger } from './debugger';
type BindingSource = { frame: Frame, page: Page }; import type { CallMetadata, InstrumentationListener, SdkObject } from './instrumentation';
import { ContextRecorder, generateFrameSelector } from './recorder/contextRecorder';
import { type IRecorderApp } from './recorder/recorderApp';
import { buildFullSelector, metadataToCallLog } from './recorder/recorderUtils';
const recorderSymbol = Symbol('recorderSymbol'); const recorderSymbol = Symbol('recorderSymbol');
export type RecorderAppFactory = (recorder: Recorder) => Promise<IRecorderApp>;
export class Recorder implements InstrumentationListener { export class Recorder implements InstrumentationListener {
private _context: BrowserContext; private _context: BrowserContext;
private _mode: Mode; private _mode: Mode;
@ -61,40 +44,38 @@ export class Recorder implements InstrumentationListener {
private _userSources = new Map<string, Source>(); private _userSources = new Map<string, Source>();
private _debugger: Debugger; private _debugger: Debugger;
private _contextRecorder: ContextRecorder; private _contextRecorder: ContextRecorder;
private _handleSIGINT: boolean | undefined;
private _omitCallTracking = false; private _omitCallTracking = false;
private _currentLanguage: Language; private _currentLanguage: Language;
private static recorderAppFactory: ((recorder: Recorder) => Promise<IRecorderApp>) | undefined; static showInspector(context: BrowserContext, recorderAppFactory: RecorderAppFactory) {
static setAppFactory(recorderAppFactory: ((recorder: Recorder) => Promise<IRecorderApp>) | undefined) {
Recorder.recorderAppFactory = recorderAppFactory;
}
static showInspector(context: BrowserContext) {
const params: channels.BrowserContextRecorderSupplementEnableParams = {}; const params: channels.BrowserContextRecorderSupplementEnableParams = {};
if (isUnderTest()) if (isUnderTest())
params.language = process.env.TEST_INSPECTOR_LANGUAGE; params.language = process.env.TEST_INSPECTOR_LANGUAGE;
Recorder.show(context, params).catch(() => {}); Recorder.show(context, recorderAppFactory, params).catch(() => {});
} }
static show(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams = {}): Promise<Recorder> { static show(context: BrowserContext, recorderAppFactory: RecorderAppFactory, params: channels.BrowserContextRecorderSupplementEnableParams = {}): Promise<Recorder> {
let recorderPromise = (context as any)[recorderSymbol] as Promise<Recorder>; let recorderPromise = (context as any)[recorderSymbol] as Promise<Recorder>;
if (!recorderPromise) { if (!recorderPromise) {
const recorder = new Recorder(context, params); recorderPromise = Recorder._create(context, recorderAppFactory, params);
recorderPromise = recorder.install().then(() => recorder);
(context as any)[recorderSymbol] = recorderPromise; (context as any)[recorderSymbol] = recorderPromise;
} }
return recorderPromise; return recorderPromise;
} }
private static async _create(context: BrowserContext, recorderAppFactory: RecorderAppFactory, params: channels.BrowserContextRecorderSupplementEnableParams = {}): Promise<Recorder> {
const recorder = new Recorder(context, params);
const recorderApp = await recorderAppFactory(recorder);
await recorder._install(recorderApp);
return recorder;
}
constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams) { constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams) {
this._mode = params.mode || 'none'; this._mode = params.mode || 'none';
this._contextRecorder = new ContextRecorder(context, params); this._contextRecorder = new ContextRecorder(context, params, {});
this._context = context; this._context = context;
this._omitCallTracking = !!params.omitCallTracking; this._omitCallTracking = !!params.omitCallTracking;
this._debugger = context.debugger(); this._debugger = context.debugger();
this._handleSIGINT = params.handleSIGINT;
context.instrumentation.addListener(this, context); context.instrumentation.addListener(this, context);
this._currentLanguage = this._contextRecorder.languageName(); this._currentLanguage = this._contextRecorder.languageName();
@ -104,14 +85,7 @@ export class Recorder implements InstrumentationListener {
} }
} }
private static async defaultRecorderAppFactory(recorder: Recorder) { private async _install(recorderApp: IRecorderApp) {
if (process.env.PW_CODEGEN_NO_INSPECTOR)
return new EmptyRecorderApp();
return await RecorderApp.open(recorder, recorder._context, recorder._handleSIGINT);
}
async install() {
const recorderApp = await (Recorder.recorderAppFactory || Recorder.defaultRecorderAppFactory)(this);
this._recorderApp = recorderApp; this._recorderApp = recorderApp;
recorderApp.once('close', () => { recorderApp.once('close', () => {
this._debugger.resume(false); this._debugger.resume(false);
@ -158,7 +132,7 @@ export class Recorder implements InstrumentationListener {
this._context.once(BrowserContext.Events.Close, () => { this._context.once(BrowserContext.Events.Close, () => {
this._contextRecorder.dispose(); this._contextRecorder.dispose();
this._context.instrumentation.removeListener(this); this._context.instrumentation.removeListener(this);
recorderApp.close().catch(() => {}); this._recorderApp?.close().catch(() => {});
}); });
this._contextRecorder.on(ContextRecorder.Events.Change, (data: { sources: Source[], primaryFileName: string }) => { this._contextRecorder.on(ContextRecorder.Events.Change, (data: { sources: Source[], primaryFileName: string }) => {
this._recorderSources = data.sources; this._recorderSources = data.sources;
@ -191,15 +165,8 @@ export class Recorder implements InstrumentationListener {
}); });
await this._context.exposeBinding('__pw_recorderSetSelector', false, async ({ frame }, selector: string) => { await this._context.exposeBinding('__pw_recorderSetSelector', false, async ({ frame }, selector: string) => {
const selectorPromises: Promise<string | undefined>[] = []; const selectorChain = await generateFrameSelector(frame);
let currentFrame: Frame | null = frame; await this._recorderApp?.setSelector(buildFullSelector(selectorChain, selector), true);
while (currentFrame) {
selectorPromises.push(findFrameSelector(currentFrame));
currentFrame = currentFrame.parentFrame();
}
const fullSelector = (await Promise.all(selectorPromises)).filter(Boolean);
fullSelector.push(selector);
await this._recorderApp?.setSelector(fullSelector.join(' >> internal:control=enter-frame >> '), true);
}); });
await this._context.exposeBinding('__pw_recorderSetMode', false, async ({ frame }, mode: Mode) => { await this._context.exposeBinding('__pw_recorderSetMode', false, async ({ frame }, mode: Mode) => {
@ -225,7 +192,7 @@ export class Recorder implements InstrumentationListener {
this._pausedStateChanged(); this._pausedStateChanged();
this._debugger.on(Debugger.Events.PausedStateChanged, () => this._pausedStateChanged()); this._debugger.on(Debugger.Events.PausedStateChanged, () => this._pausedStateChanged());
(this._context as any).recorderAppForTest = recorderApp; (this._context as any).recorderAppForTest = this._recorderApp;
} }
_pausedStateChanged() { _pausedStateChanged() {
@ -369,329 +336,8 @@ export class Recorder implements InstrumentationListener {
} }
} }
class ContextRecorder extends EventEmitter { function isScreenshotCommand(metadata: CallMetadata) {
static Events = { return metadata.method.toLowerCase().includes('screenshot');
Change: 'change'
};
private _generator: CodeGenerator;
private _pageAliases = new Map<Page, string>();
private _lastPopupOrdinal = 0;
private _lastDialogOrdinal = -1;
private _lastDownloadOrdinal = -1;
private _timers = new Set<NodeJS.Timeout>();
private _context: BrowserContext;
private _params: channels.BrowserContextRecorderSupplementEnableParams;
private _recorderSources: Source[];
private _throttledOutputFile: ThrottledFile | null = null;
private _orderedLanguages: LanguageGenerator[] = [];
private _listeners: RegisteredListener[] = [];
constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams) {
super();
this._context = context;
this._params = params;
this._recorderSources = [];
const language = params.language || context.attribution.playwright.options.sdkLanguage;
this.setOutput(language, params.outputFile);
const generator = new CodeGenerator(context._browser.options.name, params.mode === 'recording', params.launchOptions || {}, params.contextOptions || {}, params.device, params.saveStorage);
generator.on('change', () => {
this._recorderSources = [];
for (const languageGenerator of this._orderedLanguages) {
const { header, footer, actions, text } = generator.generateStructure(languageGenerator);
const source: Source = {
isRecorded: true,
label: languageGenerator.name,
group: languageGenerator.groupName,
id: languageGenerator.id,
text,
header,
footer,
actions,
language: languageGenerator.highlighter,
highlight: []
};
source.revealLine = text.split('\n').length - 1;
this._recorderSources.push(source);
if (languageGenerator === this._orderedLanguages[0])
this._throttledOutputFile?.setContent(source.text);
}
this.emit(ContextRecorder.Events.Change, {
sources: this._recorderSources,
primaryFileName: this._orderedLanguages[0].id
});
});
context.on(BrowserContext.Events.BeforeClose, () => {
this._throttledOutputFile?.flush();
});
this._listeners.push(eventsHelper.addEventListener(process, 'exit', () => {
this._throttledOutputFile?.flush();
}));
this._generator = generator;
}
setOutput(codegenId: string, outputFile?: string) {
const languages = new Set([
new JavaLanguageGenerator('junit'),
new JavaLanguageGenerator('library'),
new JavaScriptLanguageGenerator(/* isPlaywrightTest */false),
new JavaScriptLanguageGenerator(/* isPlaywrightTest */true),
new PythonLanguageGenerator(/* isAsync */false, /* isPytest */true),
new PythonLanguageGenerator(/* isAsync */false, /* isPytest */false),
new PythonLanguageGenerator(/* isAsync */true, /* isPytest */false),
new CSharpLanguageGenerator('mstest'),
new CSharpLanguageGenerator('nunit'),
new CSharpLanguageGenerator('library'),
new JsonlLanguageGenerator(),
]);
const primaryLanguage = [...languages].find(l => l.id === codegenId);
if (!primaryLanguage)
throw new Error(`\n===============================\nUnsupported language: '${codegenId}'\n===============================\n`);
languages.delete(primaryLanguage);
this._orderedLanguages = [primaryLanguage, ...languages];
this._throttledOutputFile = outputFile ? new ThrottledFile(outputFile) : null;
this._generator?.restart();
}
languageName(id?: string): Language {
for (const lang of this._orderedLanguages) {
if (!id || lang.id === id)
return lang.highlighter;
}
return 'javascript';
}
async install() {
this._context.on(BrowserContext.Events.Page, (page: Page) => this._onPage(page));
for (const page of this._context.pages())
this._onPage(page);
this._context.on(BrowserContext.Events.Dialog, (dialog: Dialog) => this._onDialog(dialog.page()));
// Input actions that potentially lead to navigation are intercepted on the page and are
// performed by the Playwright.
await this._context.exposeBinding('__pw_recorderPerformAction', false,
(source: BindingSource, action: actions.Action) => this._performAction(source.frame, action));
// Other non-essential actions are simply being recorded.
await this._context.exposeBinding('__pw_recorderRecordAction', false,
(source: BindingSource, action: actions.Action) => this._recordAction(source.frame, action));
await this._context.extendInjectedScript(recorderSource.source);
}
setEnabled(enabled: boolean) {
this._generator.setEnabled(enabled);
}
dispose() {
for (const timer of this._timers)
clearTimeout(timer);
this._timers.clear();
eventsHelper.removeEventListeners(this._listeners);
}
private async _onPage(page: Page) {
// First page is called page, others are called popup1, popup2, etc.
const frame = page.mainFrame();
page.on('close', () => {
this._generator.addAction({
frame: this._describeMainFrame(page),
committed: true,
action: {
name: 'closePage',
signals: [],
}
});
this._pageAliases.delete(page);
});
frame.on(Frame.Events.InternalNavigation, event => {
if (event.isPublic)
this._onFrameNavigated(frame, page);
});
page.on(Page.Events.Download, () => this._onDownload(page));
const suffix = this._pageAliases.size ? String(++this._lastPopupOrdinal) : '';
const pageAlias = 'page' + suffix;
this._pageAliases.set(page, pageAlias);
if (page.opener()) {
this._onPopup(page.opener()!, page);
} else {
this._generator.addAction({
frame: this._describeMainFrame(page),
committed: true,
action: {
name: 'openPage',
url: page.mainFrame().url(),
signals: [],
}
});
}
}
clearScript(): void {
this._generator.restart();
if (this._params.mode === 'recording') {
for (const page of this._context.pages())
this._onFrameNavigated(page.mainFrame(), page);
}
}
private _describeMainFrame(page: Page): actions.FrameDescription {
return {
pageAlias: this._pageAliases.get(page)!,
isMainFrame: true,
};
}
private async _describeFrame(frame: Frame): Promise<actions.FrameDescription> {
const page = frame._page;
const pageAlias = this._pageAliases.get(page)!;
const chain: Frame[] = [];
for (let ancestor: Frame | null = frame; ancestor; ancestor = ancestor.parentFrame())
chain.push(ancestor);
chain.reverse();
if (chain.length === 1)
return this._describeMainFrame(page);
const selectorPromises: Promise<string | undefined>[] = [];
for (let i = 0; i < chain.length - 1; i++)
selectorPromises.push(findFrameSelector(chain[i + 1]));
const result = await raceAgainstDeadline(() => Promise.all(selectorPromises), monotonicTime() + 2000);
if (!result.timedOut && result.result.every(selector => !!selector)) {
return {
pageAlias,
isMainFrame: false,
selectorsChain: result.result as string[],
};
}
// Best effort to find a selector for the frame.
const selectorsChain = [];
for (let i = 0; i < chain.length - 1; i++) {
if (chain[i].name())
selectorsChain.push(`iframe[name=${quoteCSSAttributeValue(chain[i].name())}]`);
else
selectorsChain.push(`iframe[src=${quoteCSSAttributeValue(chain[i].url())}]`);
}
return {
pageAlias,
isMainFrame: false,
selectorsChain,
};
}
testIdAttributeName(): string {
return this._params.testIdAttributeName || this._context.selectors().testIdAttributeName() || 'data-testid';
}
private async _performAction(frame: Frame, action: actions.Action) {
// Commit last action so that no further signals are added to it.
this._generator.commitLastAction();
const frameDescription = await this._describeFrame(frame);
const actionInContext: ActionInContext = {
frame: frameDescription,
action
};
const perform = async (action: string, params: any, cb: (callMetadata: CallMetadata) => Promise<any>) => {
const callMetadata: CallMetadata = {
id: `call@${createGuid()}`,
apiName: 'frame.' + action,
objectId: frame.guid,
pageId: frame._page.guid,
frameId: frame.guid,
startTime: monotonicTime(),
endTime: 0,
type: 'Frame',
method: action,
params,
log: [],
};
this._generator.willPerformAction(actionInContext);
try {
await frame.instrumentation.onBeforeCall(frame, callMetadata);
await cb(callMetadata);
} catch (e) {
callMetadata.endTime = monotonicTime();
await frame.instrumentation.onAfterCall(frame, callMetadata);
this._generator.performedActionFailed(actionInContext);
return;
}
callMetadata.endTime = monotonicTime();
await frame.instrumentation.onAfterCall(frame, callMetadata);
this._setCommittedAfterTimeout(actionInContext);
this._generator.didPerformAction(actionInContext);
};
const kActionTimeout = 5000;
if (action.name === 'click') {
const { options } = toClickOptions(action);
await perform('click', { selector: action.selector }, callMetadata => frame.click(callMetadata, action.selector, { ...options, timeout: kActionTimeout, strict: true }));
}
if (action.name === 'press') {
const modifiers = toModifiers(action.modifiers);
const shortcut = [...modifiers, action.key].join('+');
await perform('press', { selector: action.selector, key: shortcut }, callMetadata => frame.press(callMetadata, action.selector, shortcut, { timeout: kActionTimeout, strict: true }));
}
if (action.name === 'check')
await perform('check', { selector: action.selector }, callMetadata => frame.check(callMetadata, action.selector, { timeout: kActionTimeout, strict: true }));
if (action.name === 'uncheck')
await perform('uncheck', { selector: action.selector }, callMetadata => frame.uncheck(callMetadata, action.selector, { timeout: kActionTimeout, strict: true }));
if (action.name === 'select') {
const values = action.options.map(value => ({ value }));
await perform('selectOption', { selector: action.selector, values }, callMetadata => frame.selectOption(callMetadata, action.selector, [], values, { timeout: kActionTimeout, strict: true }));
}
}
private async _recordAction(frame: Frame, action: actions.Action) {
// Commit last action so that no further signals are added to it.
this._generator.commitLastAction();
const frameDescription = await this._describeFrame(frame);
const actionInContext: ActionInContext = {
frame: frameDescription,
action
};
this._setCommittedAfterTimeout(actionInContext);
this._generator.addAction(actionInContext);
}
private _setCommittedAfterTimeout(actionInContext: ActionInContext) {
const timer = setTimeout(() => {
// Commit the action after 5 seconds so that no further signals are added to it.
actionInContext.committed = true;
this._timers.delete(timer);
}, isUnderTest() ? 500 : 5000);
this._timers.add(timer);
}
private _onFrameNavigated(frame: Frame, page: Page) {
const pageAlias = this._pageAliases.get(page);
this._generator.signal(pageAlias!, frame, { name: 'navigation', url: frame.url() });
}
private _onPopup(page: Page, popup: Page) {
const pageAlias = this._pageAliases.get(page)!;
const popupAlias = this._pageAliases.get(popup)!;
this._generator.signal(pageAlias, page.mainFrame(), { name: 'popup', popupAlias });
}
private _onDownload(page: Page) {
const pageAlias = this._pageAliases.get(page)!;
++this._lastDownloadOrdinal;
this._generator.signal(pageAlias, page.mainFrame(), { name: 'download', downloadAlias: this._lastDownloadOrdinal ? String(this._lastDownloadOrdinal) : '' });
}
private _onDialog(page: Page) {
const pageAlias = this._pageAliases.get(page)!;
++this._lastDialogOrdinal;
this._generator.signal(pageAlias, page.mainFrame(), { name: 'dialog', dialogAlias: this._lastDialogOrdinal ? String(this._lastDialogOrdinal) : '' });
}
} }
function languageForFile(file: string) { function languageForFile(file: string) {
@ -703,49 +349,3 @@ function languageForFile(file: string) {
return 'csharp'; return 'csharp';
return 'javascript'; return 'javascript';
} }
class ThrottledFile {
private _file: string;
private _timer: NodeJS.Timeout | undefined;
private _text: string | undefined;
constructor(file: string) {
this._file = file;
}
setContent(text: string) {
this._text = text;
if (!this._timer)
this._timer = setTimeout(() => this.flush(), 250);
}
flush(): void {
if (this._timer) {
clearTimeout(this._timer);
this._timer = undefined;
}
if (this._text)
fs.writeFileSync(this._file, this._text);
this._text = undefined;
}
}
function isScreenshotCommand(metadata: CallMetadata) {
return metadata.method.toLowerCase().includes('screenshot');
}
async function findFrameSelector(frame: Frame): Promise<string | undefined> {
try {
const parent = frame.parentFrame();
const frameElement = await frame.frameElement();
if (!frameElement || !parent)
return;
const utility = await parent._utilityContext();
const injected = await utility.injectedScript();
const selector = await injected.evaluate((injected, element) => {
return injected.generateSelectorSimple(element as Element, { testIdAttributeName: '', omitInternalEngines: true });
}, frameElement);
return selector;
} catch (e) {
}
}

View file

@ -1,8 +1,11 @@
[*] [*]
../ ../
../codegen/language.ts
../codegen/languages.ts
../isomorphic/** ../isomorphic/**
../registry/** ../registry/**
../../common/ ../../common/
../../generated/recorderSource.ts
../../protocol/ ../../protocol/
../../utils/** ../../utils/**
../../utilsBundle.ts ../../utilsBundle.ts

View file

@ -0,0 +1,334 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type * as channels from '@protocol/channels';
import type { Source } from '@recorder/recorderTypes';
import { EventEmitter } from 'events';
import * as recorderSource from '../../generated/recorderSource';
import { eventsHelper, isUnderTest, monotonicTime, quoteCSSAttributeValue, type RegisteredListener } from '../../utils';
import { raceAgainstDeadline } from '../../utils/timeoutRunner';
import { BrowserContext } from '../browserContext';
import type { ActionInContext, FrameDescription, LanguageGeneratorOptions, Language, LanguageGenerator } from '../codegen/types';
import { languageSet } from '../codegen/languages';
import type { Dialog } from '../dialog';
import { Frame } from '../frames';
import { Page } from '../page';
import type * as actions from './recorderActions';
import { performAction } from './recorderRunner';
import { ThrottledFile } from './throttledFile';
import { RecorderCollection } from './recorderCollection';
import { generateCode } from '../codegen/language';
type BindingSource = { frame: Frame, page: Page };
export interface ContextRecorderDelegate {
rewriteActionInContext?(pageAliases: Map<Page, string>, actionInContext: ActionInContext): Promise<void>;
}
export class ContextRecorder extends EventEmitter {
static Events = {
Change: 'change'
};
private _collection: RecorderCollection;
private _pageAliases = new Map<Page, string>();
private _lastPopupOrdinal = 0;
private _lastDialogOrdinal = -1;
private _lastDownloadOrdinal = -1;
private _timers = new Set<NodeJS.Timeout>();
private _context: BrowserContext;
private _params: channels.BrowserContextRecorderSupplementEnableParams;
private _delegate: ContextRecorderDelegate;
private _recorderSources: Source[];
private _throttledOutputFile: ThrottledFile | null = null;
private _orderedLanguages: LanguageGenerator[] = [];
private _listeners: RegisteredListener[] = [];
constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams, delegate: ContextRecorderDelegate) {
super();
this._context = context;
this._params = params;
this._delegate = delegate;
this._recorderSources = [];
const language = params.language || context.attribution.playwright.options.sdkLanguage;
this.setOutput(language, params.outputFile);
// Make a copy of options to modify them later.
const languageGeneratorOptions: LanguageGeneratorOptions = {
browserName: context._browser.options.name,
launchOptions: { headless: false, ...params.launchOptions },
contextOptions: { ...params.contextOptions },
deviceName: params.device,
saveStorage: params.saveStorage,
};
const collection = new RecorderCollection(params.mode === 'recording');
collection.on('change', () => {
this._recorderSources = [];
for (const languageGenerator of this._orderedLanguages) {
const { header, footer, actionTexts, text } = generateCode(collection.actions(), languageGenerator, languageGeneratorOptions);
const source: Source = {
isRecorded: true,
label: languageGenerator.name,
group: languageGenerator.groupName,
id: languageGenerator.id,
text,
header,
footer,
actions: actionTexts,
language: languageGenerator.highlighter,
highlight: []
};
source.revealLine = text.split('\n').length - 1;
this._recorderSources.push(source);
if (languageGenerator === this._orderedLanguages[0])
this._throttledOutputFile?.setContent(source.text);
}
this.emit(ContextRecorder.Events.Change, {
sources: this._recorderSources,
primaryFileName: this._orderedLanguages[0].id
});
});
context.on(BrowserContext.Events.BeforeClose, () => {
this._throttledOutputFile?.flush();
});
this._listeners.push(eventsHelper.addEventListener(process, 'exit', () => {
this._throttledOutputFile?.flush();
}));
this._collection = collection;
}
setOutput(codegenId: string, outputFile?: string) {
const languages = languageSet();
const primaryLanguage = [...languages].find(l => l.id === codegenId);
if (!primaryLanguage)
throw new Error(`\n===============================\nUnsupported language: '${codegenId}'\n===============================\n`);
languages.delete(primaryLanguage);
this._orderedLanguages = [primaryLanguage, ...languages];
this._throttledOutputFile = outputFile ? new ThrottledFile(outputFile) : null;
this._collection?.restart();
}
languageName(id?: string): Language {
for (const lang of this._orderedLanguages) {
if (!id || lang.id === id)
return lang.highlighter;
}
return 'javascript';
}
async install() {
this._context.on(BrowserContext.Events.Page, (page: Page) => this._onPage(page));
for (const page of this._context.pages())
this._onPage(page);
this._context.on(BrowserContext.Events.Dialog, (dialog: Dialog) => this._onDialog(dialog.page()));
// Input actions that potentially lead to navigation are intercepted on the page and are
// performed by the Playwright.
await this._context.exposeBinding('__pw_recorderPerformAction', false,
(source: BindingSource, action: actions.PerformOnRecordAction) => this._performAction(source.frame, action));
// Other non-essential actions are simply being recorded.
await this._context.exposeBinding('__pw_recorderRecordAction', false,
(source: BindingSource, action: actions.Action) => this._recordAction(source.frame, action));
await this._context.extendInjectedScript(recorderSource.source);
}
setEnabled(enabled: boolean) {
this._collection.setEnabled(enabled);
}
dispose() {
for (const timer of this._timers)
clearTimeout(timer);
this._timers.clear();
eventsHelper.removeEventListeners(this._listeners);
}
private async _onPage(page: Page) {
// First page is called page, others are called popup1, popup2, etc.
const frame = page.mainFrame();
page.on('close', () => {
this._collection.addAction({
frame: this._describeMainFrame(page),
committed: true,
action: {
name: 'closePage',
signals: [],
}
});
this._pageAliases.delete(page);
});
frame.on(Frame.Events.InternalNavigation, event => {
if (event.isPublic)
this._onFrameNavigated(frame, page);
});
page.on(Page.Events.Download, () => this._onDownload(page));
const suffix = this._pageAliases.size ? String(++this._lastPopupOrdinal) : '';
const pageAlias = 'page' + suffix;
this._pageAliases.set(page, pageAlias);
if (page.opener()) {
this._onPopup(page.opener()!, page);
} else {
this._collection.addAction({
frame: this._describeMainFrame(page),
committed: true,
action: {
name: 'openPage',
url: page.mainFrame().url(),
signals: [],
}
});
}
}
clearScript(): void {
this._collection.restart();
if (this._params.mode === 'recording') {
for (const page of this._context.pages())
this._onFrameNavigated(page.mainFrame(), page);
}
}
private _describeMainFrame(page: Page): FrameDescription {
return {
pageAlias: this._pageAliases.get(page)!,
framePath: [],
};
}
private async _describeFrame(frame: Frame): Promise<FrameDescription> {
return {
pageAlias: this._pageAliases.get(frame._page)!,
framePath: await generateFrameSelector(frame),
};
}
testIdAttributeName(): string {
return this._params.testIdAttributeName || this._context.selectors().testIdAttributeName() || 'data-testid';
}
private async _performAction(frame: Frame, action: actions.PerformOnRecordAction) {
// Commit last action so that no further signals are added to it.
this._collection.commitLastAction();
const frameDescription = await this._describeFrame(frame);
const actionInContext: ActionInContext = {
frame: frameDescription,
action,
description: undefined,
};
await this._delegate.rewriteActionInContext?.(this._pageAliases, actionInContext);
this._collection.willPerformAction(actionInContext);
const success = await performAction(this._pageAliases, actionInContext);
if (success) {
this._collection.didPerformAction(actionInContext);
this._setCommittedAfterTimeout(actionInContext);
} else {
this._collection.performedActionFailed(actionInContext);
}
}
private async _recordAction(frame: Frame, action: actions.Action) {
// Commit last action so that no further signals are added to it.
this._collection.commitLastAction();
const frameDescription = await this._describeFrame(frame);
const actionInContext: ActionInContext = {
frame: frameDescription,
action,
description: undefined,
};
await this._delegate.rewriteActionInContext?.(this._pageAliases, actionInContext);
this._setCommittedAfterTimeout(actionInContext);
this._collection.addAction(actionInContext);
}
private _setCommittedAfterTimeout(actionInContext: ActionInContext) {
const timer = setTimeout(() => {
// Commit the action after 5 seconds so that no further signals are added to it.
actionInContext.committed = true;
this._timers.delete(timer);
}, isUnderTest() ? 500 : 5000);
this._timers.add(timer);
}
private _onFrameNavigated(frame: Frame, page: Page) {
const pageAlias = this._pageAliases.get(page);
this._collection.signal(pageAlias!, frame, { name: 'navigation', url: frame.url() });
}
private _onPopup(page: Page, popup: Page) {
const pageAlias = this._pageAliases.get(page)!;
const popupAlias = this._pageAliases.get(popup)!;
this._collection.signal(pageAlias, page.mainFrame(), { name: 'popup', popupAlias });
}
private _onDownload(page: Page) {
const pageAlias = this._pageAliases.get(page)!;
++this._lastDownloadOrdinal;
this._collection.signal(pageAlias, page.mainFrame(), { name: 'download', downloadAlias: this._lastDownloadOrdinal ? String(this._lastDownloadOrdinal) : '' });
}
private _onDialog(page: Page) {
const pageAlias = this._pageAliases.get(page)!;
++this._lastDialogOrdinal;
this._collection.signal(pageAlias, page.mainFrame(), { name: 'dialog', dialogAlias: this._lastDialogOrdinal ? String(this._lastDialogOrdinal) : '' });
}
}
export async function generateFrameSelector(frame: Frame): Promise<string[]> {
const selectorPromises: Promise<string>[] = [];
while (frame) {
const parent = frame.parentFrame();
if (!parent)
break;
selectorPromises.push(generateFrameSelectorInParent(parent, frame));
frame = parent;
}
const result = await Promise.all(selectorPromises);
return result.reverse();
}
async function generateFrameSelectorInParent(parent: Frame, frame: Frame): Promise<string> {
const result = await raceAgainstDeadline(async () => {
try {
const frameElement = await frame.frameElement();
if (!frameElement || !parent)
return;
const utility = await parent._utilityContext();
const injected = await utility.injectedScript();
const selector = await injected.evaluate((injected, element) => {
return injected.generateSelectorSimple(element as Element);
}, frameElement);
return selector;
} catch (e) {
return e.toString();
}
}, monotonicTime() + 2000);
if (!result.timedOut && result.result)
return result.result;
if (frame.name())
return `iframe[name=${quoteCSSAttributeValue(frame.name())}]`;
return `iframe[src=${quoteCSSAttributeValue(frame.url())}]`;
}

View file

@ -1,71 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { BrowserContextOptions, LaunchOptions } from '../../..';
import type { Language } from '../../utils';
import type { ActionInContext } from './codeGenerator';
import type { Action, DialogSignal, DownloadSignal, PopupSignal } from './recorderActions';
export type { Language } from '../../utils';
export type LanguageGeneratorOptions = {
browserName: string;
launchOptions: LaunchOptions;
contextOptions: BrowserContextOptions;
deviceName?: string;
saveStorage?: string;
};
export type LocatorType = 'default' | 'role' | 'text' | 'label' | 'placeholder' | 'alt' | 'title' | 'test-id' | 'nth' | 'first' | 'last' | 'has-text';
export type LocatorBase = 'page' | 'locator' | 'frame-locator';
export interface LanguageGenerator {
id: string;
groupName: string;
name: string;
highlighter: Language;
generateHeader(options: LanguageGeneratorOptions): string;
generateAction(actionInContext: ActionInContext): string;
generateFooter(saveStorage: string | undefined): string;
}
export function sanitizeDeviceOptions(device: any, options: BrowserContextOptions): BrowserContextOptions {
// Filter out all the properties from the device descriptor.
const cleanedOptions: Record<string, any> = {};
for (const property in options) {
if (JSON.stringify(device[property]) !== JSON.stringify((options as any)[property]))
cleanedOptions[property] = (options as any)[property];
}
return cleanedOptions;
}
export function toSignalMap(action: Action) {
let popup: PopupSignal | undefined;
let download: DownloadSignal | undefined;
let dialog: DialogSignal | undefined;
for (const signal of action.signals) {
if (signal.name === 'popup')
popup = signal;
else if (signal.name === 'download')
download = signal;
else if (signal.name === 'dialog')
dialog = signal;
}
return {
popup,
download,
dialog,
};
}

View file

@ -37,28 +37,28 @@ export type ActionBase = {
signals: Signal[], signals: Signal[],
}; };
export type ClickAction = ActionBase & { export type ActionWithSelector = ActionBase & {
name: 'click',
selector: string, selector: string,
};
export type ClickAction = ActionWithSelector & {
name: 'click',
button: 'left' | 'middle' | 'right', button: 'left' | 'middle' | 'right',
modifiers: number, modifiers: number,
clickCount: number, clickCount: number,
position?: Point, position?: Point,
}; };
export type CheckAction = ActionBase & { export type CheckAction = ActionWithSelector & {
name: 'check', name: 'check',
selector: string,
}; };
export type UncheckAction = ActionBase & { export type UncheckAction = ActionWithSelector & {
name: 'uncheck', name: 'uncheck',
selector: string,
}; };
export type FillAction = ActionBase & { export type FillAction = ActionWithSelector & {
name: 'fill', name: 'fill',
selector: string,
text: string, text: string,
}; };
@ -83,44 +83,39 @@ export type PressAction = ActionBase & {
modifiers: number, modifiers: number,
}; };
export type SelectAction = ActionBase & { export type SelectAction = ActionWithSelector & {
name: 'select', name: 'select',
selector: string,
options: string[], options: string[],
}; };
export type SetInputFilesAction = ActionBase & { export type SetInputFilesAction = ActionWithSelector & {
name: 'setInputFiles', name: 'setInputFiles',
selector: string,
files: string[], files: string[],
}; };
export type AssertTextAction = ActionBase & { export type AssertTextAction = ActionWithSelector & {
name: 'assertText', name: 'assertText',
selector: string,
text: string, text: string,
substring: boolean, substring: boolean,
}; };
export type AssertValueAction = ActionBase & { export type AssertValueAction = ActionWithSelector & {
name: 'assertValue', name: 'assertValue',
selector: string,
value: string, value: string,
}; };
export type AssertCheckedAction = ActionBase & { export type AssertCheckedAction = ActionWithSelector & {
name: 'assertChecked', name: 'assertChecked',
selector: string,
checked: boolean, checked: boolean,
}; };
export type AssertVisibleAction = ActionBase & { export type AssertVisibleAction = ActionWithSelector & {
name: 'assertVisible', name: 'assertVisible',
selector: string,
}; };
export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageAction | UncheckAction | FillAction | NavigateAction | PressAction | SelectAction | SetInputFilesAction | AssertTextAction | AssertValueAction | AssertCheckedAction | AssertVisibleAction; export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageAction | UncheckAction | FillAction | NavigateAction | PressAction | SelectAction | SetInputFilesAction | AssertTextAction | AssertValueAction | AssertCheckedAction | AssertVisibleAction;
export type AssertAction = AssertCheckedAction | AssertValueAction | AssertTextAction | AssertVisibleAction; export type AssertAction = AssertCheckedAction | AssertValueAction | AssertTextAction | AssertVisibleAction;
export type PerformOnRecordAction = ClickAction | CheckAction | UncheckAction | PressAction | SelectAction;
// Signals. // Signals.
@ -148,14 +143,3 @@ export type DialogSignal = BaseSignal & {
}; };
export type Signal = NavigationSignal | PopupSignal | DownloadSignal | DialogSignal; export type Signal = NavigationSignal | PopupSignal | DownloadSignal | DialogSignal;
type FrameDescriptionMainFrame = {
isMainFrame: true;
};
type FrameDescriptionChildFrame = {
isMainFrame: false;
selectorsChain: string[];
};
export type FrameDescription = { pageAlias: string } & (FrameDescriptionMainFrame | FrameDescriptionChildFrame);

View file

@ -24,7 +24,7 @@ import type { CallLog, EventData, Mode, Source } from '@recorder/recorderTypes';
import { isUnderTest } from '../../utils'; import { isUnderTest } from '../../utils';
import { mime } from '../../utilsBundle'; import { mime } from '../../utilsBundle';
import { syncLocalStorageWithSettings } from '../launchApp'; import { syncLocalStorageWithSettings } from '../launchApp';
import type { Recorder } from '../recorder'; import type { Recorder, RecorderAppFactory } from '../recorder';
import type { BrowserContext } from '../browserContext'; import type { BrowserContext } from '../browserContext';
import { launchApp } from '../launchApp'; import { launchApp } from '../launchApp';
@ -113,7 +113,15 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
await mainFrame.goto(serverSideCallMetadata(), 'https://playwright/index.html'); await mainFrame.goto(serverSideCallMetadata(), 'https://playwright/index.html');
} }
static async open(recorder: Recorder, inspectedContext: BrowserContext, handleSIGINT: boolean | undefined): Promise<IRecorderApp> { static factory(context: BrowserContext): RecorderAppFactory {
return async recorder => {
if (process.env.PW_CODEGEN_NO_INSPECTOR)
return new EmptyRecorderApp();
return await RecorderApp._open(recorder, context);
};
}
private static async _open(recorder: Recorder, inspectedContext: BrowserContext): Promise<IRecorderApp> {
const sdkLanguage = inspectedContext.attribution.playwright.options.sdkLanguage; const sdkLanguage = inspectedContext.attribution.playwright.options.sdkLanguage;
const headed = !!inspectedContext._browser.options.headful; const headed = !!inspectedContext._browser.options.headful;
const recorderPlaywright = (require('../playwright').createPlaywright as typeof import('../playwright').createPlaywright)({ sdkLanguage: 'javascript', isInternalPlaywright: true }); const recorderPlaywright = (require('../playwright').createPlaywright as typeof import('../playwright').createPlaywright)({ sdkLanguage: 'javascript', isInternalPlaywright: true });
@ -125,7 +133,7 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
noDefaultViewport: true, noDefaultViewport: true,
headless: !!process.env.PWTEST_CLI_HEADLESS || (isUnderTest() && !headed), headless: !!process.env.PWTEST_CLI_HEADLESS || (isUnderTest() && !headed),
useWebSocket: !!process.env.PWTEST_RECORDER_PORT, useWebSocket: !!process.env.PWTEST_RECORDER_PORT,
handleSIGINT, handleSIGINT: false,
args: process.env.PWTEST_RECORDER_PORT ? [`--remote-debugging-port=${process.env.PWTEST_RECORDER_PORT}`] : [], args: process.env.PWTEST_RECORDER_PORT ? [`--remote-debugging-port=${process.env.PWTEST_RECORDER_PORT}`] : [],
executablePath: inspectedContext._browser.options.isChromium ? inspectedContext._browser.options.customExecutablePath : undefined, executablePath: inspectedContext._browser.options.isChromium ? inspectedContext._browser.options.customExecutablePath : undefined,
} }
@ -170,11 +178,11 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
async setSelector(selector: string, userGesture?: boolean): Promise<void> { async setSelector(selector: string, userGesture?: boolean): Promise<void> {
if (userGesture) { if (userGesture) {
if (this._recorder.mode() === 'inspecting') { if (this._recorder?.mode() === 'inspecting') {
this._recorder.setMode('standby'); this._recorder.setMode('standby');
this._page.bringToFront(); this._page.bringToFront();
} else { } else {
this._recorder.setMode('recording'); this._recorder?.setMode('recording');
} }
} }
await this._page.mainFrame().evaluateExpression(((data: { selector: string, userGesture?: boolean }) => { await this._page.mainFrame().evaluateExpression(((data: { selector: string, userGesture?: boolean }) => {

View file

@ -15,32 +15,19 @@
*/ */
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import type { BrowserContextOptions, LaunchOptions } from '../../..';
import type { Frame } from '../frames'; import type { Frame } from '../frames';
import type { LanguageGenerator, LanguageGeneratorOptions } from './language'; import type { Signal } from './recorderActions';
import type { Action, Signal, FrameDescription } from './recorderActions'; import type { ActionInContext } from '../codegen/types';
export type ActionInContext = { export class RecorderCollection extends EventEmitter {
frame: FrameDescription;
action: Action;
committed?: boolean;
};
export class CodeGenerator extends EventEmitter {
private _currentAction: ActionInContext | null = null; private _currentAction: ActionInContext | null = null;
private _lastAction: ActionInContext | null = null; private _lastAction: ActionInContext | null = null;
private _actions: ActionInContext[] = []; private _actions: ActionInContext[] = [];
private _enabled: boolean; private _enabled: boolean;
private _options: LanguageGeneratorOptions;
constructor(browserName: string, enabled: boolean, launchOptions: LaunchOptions, contextOptions: BrowserContextOptions, deviceName: string | undefined, saveStorage: string | undefined) { constructor(enabled: boolean) {
super(); super();
// Make a copy of options to modify them later.
launchOptions = { headless: false, ...launchOptions };
contextOptions = { ...contextOptions };
this._enabled = enabled; this._enabled = enabled;
this._options = { browserName, launchOptions, contextOptions, deviceName, saveStorage };
this.restart(); this.restart();
} }
@ -51,6 +38,10 @@ export class CodeGenerator extends EventEmitter {
this.emit('change'); this.emit('change');
} }
actions() {
return this._actions;
}
setEnabled(enabled: boolean) { setEnabled(enabled: boolean) {
this._enabled = enabled; this._enabled = enabled;
} }
@ -146,7 +137,7 @@ export class CodeGenerator extends EventEmitter {
this.addAction({ this.addAction({
frame: { frame: {
pageAlias, pageAlias,
isMainFrame: true, framePath: [],
}, },
committed: true, committed: true,
action: { action: {
@ -157,12 +148,4 @@ export class CodeGenerator extends EventEmitter {
}); });
} }
} }
generateStructure(languageGenerator: LanguageGenerator) {
const header = languageGenerator.generateHeader(this._options);
const footer = languageGenerator.generateFooter(this._options.saveStorage);
const actions = this._actions.map(a => languageGenerator.generateAction(a)).filter(Boolean);
const text = [header, ...actions, footer].join('\n');
return { header, footer, actions, text };
}
} }

View file

@ -0,0 +1,128 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createGuid, monotonicTime, serializeExpectedTextValues } from '../../utils';
import { toClickOptions, toKeyboardModifiers } from '../codegen/language';
import type { ActionInContext } from '../codegen/types';
import type { Frame } from '../frames';
import type { CallMetadata } from '../instrumentation';
import type { Page } from '../page';
import { buildFullSelector } from './recorderUtils';
async function innerPerformAction(mainFrame: Frame, action: string, params: any, cb: (callMetadata: CallMetadata) => Promise<any>): Promise<boolean> {
const callMetadata: CallMetadata = {
id: `call@${createGuid()}`,
apiName: 'frame.' + action,
objectId: mainFrame.guid,
pageId: mainFrame._page.guid,
frameId: mainFrame.guid,
startTime: monotonicTime(),
endTime: 0,
type: 'Frame',
method: action,
params,
log: [],
};
try {
await mainFrame.instrumentation.onBeforeCall(mainFrame, callMetadata);
await cb(callMetadata);
} catch (e) {
callMetadata.endTime = monotonicTime();
await mainFrame.instrumentation.onAfterCall(mainFrame, callMetadata);
return false;
}
callMetadata.endTime = monotonicTime();
await mainFrame.instrumentation.onAfterCall(mainFrame, callMetadata);
return true;
}
export async function performAction(pageAliases: Map<Page, string>, actionInContext: ActionInContext): Promise<boolean> {
const pageAlias = actionInContext.frame.pageAlias;
const page = [...pageAliases.entries()].find(([, alias]) => pageAlias === alias)?.[0];
if (!page)
throw new Error('Internal error: page not found');
const mainFrame = page.mainFrame();
const { action } = actionInContext;
const kActionTimeout = 5000;
if (action.name === 'navigate')
return await innerPerformAction(mainFrame, 'goto', { url: action.url }, callMetadata => mainFrame.goto(callMetadata, action.url, { timeout: kActionTimeout }));
if (action.name === 'openPage')
throw Error('Not reached');
if (action.name === 'closePage')
return await innerPerformAction(mainFrame, 'close', {}, callMetadata => mainFrame._page.close(callMetadata));
const selector = buildFullSelector(actionInContext.frame.framePath, action.selector);
if (action.name === 'click') {
const options = toClickOptions(action);
return await innerPerformAction(mainFrame, 'click', { selector }, callMetadata => mainFrame.click(callMetadata, selector, { ...options, timeout: kActionTimeout, strict: true }));
}
if (action.name === 'press') {
const modifiers = toKeyboardModifiers(action.modifiers);
const shortcut = [...modifiers, action.key].join('+');
return await innerPerformAction(mainFrame, 'press', { selector, key: shortcut }, callMetadata => mainFrame.press(callMetadata, selector, shortcut, { timeout: kActionTimeout, strict: true }));
}
if (action.name === 'fill')
return await innerPerformAction(mainFrame, 'fill', { selector, text: action.text }, callMetadata => mainFrame.fill(callMetadata, selector, action.text, { timeout: kActionTimeout, strict: true }));
if (action.name === 'setInputFiles')
return await innerPerformAction(mainFrame, 'setInputFiles', { selector, files: action.files }, callMetadata => mainFrame.setInputFiles(callMetadata, selector, { selector, payloads: [], timeout: kActionTimeout, strict: true }));
if (action.name === 'check')
return await innerPerformAction(mainFrame, 'check', { selector }, callMetadata => mainFrame.check(callMetadata, selector, { timeout: kActionTimeout, strict: true }));
if (action.name === 'uncheck')
return await innerPerformAction(mainFrame, 'uncheck', { selector }, callMetadata => mainFrame.uncheck(callMetadata, selector, { timeout: kActionTimeout, strict: true }));
if (action.name === 'select') {
const values = action.options.map(value => ({ value }));
return await innerPerformAction(mainFrame, 'selectOption', { selector, values }, callMetadata => mainFrame.selectOption(callMetadata, selector, [], values, { timeout: kActionTimeout, strict: true }));
}
if (action.name === 'assertChecked') {
return await innerPerformAction(mainFrame, 'expect', { selector }, callMetadata => mainFrame.expect(callMetadata, selector, {
selector,
expression: 'to.be.checked',
isNot: !action.checked,
timeout: kActionTimeout,
}));
}
if (action.name === 'assertText') {
return await innerPerformAction(mainFrame, 'expect', { selector }, callMetadata => mainFrame.expect(callMetadata, selector, {
selector,
expression: 'to.have.text',
expectedText: serializeExpectedTextValues([action.text], { matchSubstring: true, normalizeWhiteSpace: true }),
isNot: false,
timeout: kActionTimeout,
}));
}
if (action.name === 'assertValue') {
return await innerPerformAction(mainFrame, 'expect', { selector }, callMetadata => mainFrame.expect(callMetadata, selector, {
selector,
expression: 'to.have.value',
expectedValue: action.value,
isNot: false,
timeout: kActionTimeout,
}));
}
if (action.name === 'assertVisible') {
return await innerPerformAction(mainFrame, 'expect', { selector }, callMetadata => mainFrame.expect(callMetadata, selector, {
selector,
expression: 'to.be.visible',
isNot: false,
timeout: kActionTimeout,
}));
}
throw new Error('Internal error: unexpected action ' + (action as any).name);
}

View file

@ -16,6 +16,10 @@
import type { CallMetadata } from '../instrumentation'; import type { CallMetadata } from '../instrumentation';
import type { CallLog, CallLogStatus } from '@recorder/recorderTypes'; import type { CallLog, CallLogStatus } from '@recorder/recorderTypes';
import type { Page } from '../page';
import type { ActionInContext } from '../codegen/types';
import type { Frame } from '../frames';
import type * as actions from './recorderActions';
export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus): CallLog { export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus): CallLog {
let title = metadata.apiName || metadata.method; let title = metadata.apiName || metadata.method;
@ -44,3 +48,27 @@ export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus)
}; };
return callLog; return callLog;
} }
export function buildFullSelector(framePath: string[], selector: string) {
return [...framePath, selector].join(' >> internal:control=enter-frame >> ');
}
export function mainFrameForAction(pageAliases: Map<Page, string>, actionInContext: ActionInContext): Frame {
const pageAlias = actionInContext.frame.pageAlias;
const page = [...pageAliases.entries()].find(([, alias]) => pageAlias === alias)?.[0];
if (!page)
throw new Error('Internal error: page not found');
return page.mainFrame();
}
export async function frameForAction(pageAliases: Map<Page, string>, actionInContext: ActionInContext, action: actions.ActionWithSelector): Promise<Frame> {
const pageAlias = actionInContext.frame.pageAlias;
const page = [...pageAliases.entries()].find(([, alias]) => pageAlias === alias)?.[0];
if (!page)
throw new Error('Internal error: page not found');
const fullSelector = buildFullSelector(actionInContext.frame.framePath, action.selector);
const result = await page.mainFrame().selectors.resolveFrameForSelector(fullSelector);
if (!result)
throw new Error('Internal error: frame not found');
return result.frame;
}

View file

@ -0,0 +1,43 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as fs from 'fs';
export class ThrottledFile {
private _file: string;
private _timer: NodeJS.Timeout | undefined;
private _text: string | undefined;
constructor(file: string) {
this._file = file;
}
setContent(text: string) {
this._text = text;
if (!this._timer)
this._timer = setTimeout(() => this.flush(), 250);
}
flush(): void {
if (this._timer) {
clearTimeout(this._timer);
this._timer = undefined;
}
if (this._text)
fs.writeFileSync(this._file, this._text);
this._text = undefined;
}
}

View file

@ -1,51 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Frame } from '../frames';
import type { SmartKeyboardModifier } from '../types';
import type * as actions from './recorderActions';
export type MouseClickOptions = Parameters<Frame['click']>[2];
export function toClickOptions(action: actions.ClickAction): { method: 'click' | 'dblclick', options: MouseClickOptions } {
let method: 'click' | 'dblclick' = 'click';
if (action.clickCount === 2)
method = 'dblclick';
const modifiers = toModifiers(action.modifiers);
const options: MouseClickOptions = {};
if (action.button !== 'left')
options.button = action.button;
if (modifiers.length)
options.modifiers = modifiers;
if (action.clickCount > 2)
options.clickCount = action.clickCount;
if (action.position)
options.position = action.position;
return { method, options };
}
export function toModifiers(modifiers: number): SmartKeyboardModifier[] {
const result: SmartKeyboardModifier[] = [];
if (modifiers & 1)
result.push('Alt');
if (modifiers & 2)
result.push('ControlOrMeta');
if (modifiers & 4)
result.push('ControlOrMeta');
if (modifiers & 8)
result.push('Shift');
return result;
}

View file

@ -536,7 +536,7 @@ export module Protocol {
/** /**
* Pseudo-style identifier (see <code>enum PseudoId</code> in <code>RenderStyleConstants.h</code>). * Pseudo-style identifier (see <code>enum PseudoId</code> in <code>RenderStyleConstants.h</code>).
*/ */
export type PseudoId = "first-line"|"first-letter"|"grammar-error"|"highlight"|"marker"|"before"|"after"|"selection"|"backdrop"|"spelling-error"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"|"-webkit-scrollbar"|"-webkit-resizer"|"-webkit-scrollbar-thumb"|"-webkit-scrollbar-button"|"-webkit-scrollbar-track"|"-webkit-scrollbar-track-piece"|"-webkit-scrollbar-corner"; export type PseudoId = "first-line"|"first-letter"|"grammar-error"|"highlight"|"marker"|"before"|"after"|"selection"|"backdrop"|"spelling-error"|"target-text"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"|"-webkit-scrollbar"|"-webkit-resizer"|"-webkit-scrollbar-thumb"|"-webkit-scrollbar-button"|"-webkit-scrollbar-track"|"-webkit-scrollbar-track-piece"|"-webkit-scrollbar-corner";
/** /**
* Pseudo-style identifier (see <code>enum PseudoId</code> in <code>RenderStyleConstants.h</code>). * Pseudo-style identifier (see <code>enum PseudoId</code> in <code>RenderStyleConstants.h</code>).
*/ */

View file

@ -141,7 +141,7 @@ export class WKRouteImpl implements network.RouteDelegate {
}); });
} }
async continue(request: network.Request, overrides: types.NormalizedContinueOverrides) { async continue(overrides: types.NormalizedContinueOverrides) {
// In certain cases, protocol will return error if the request was already canceled // In certain cases, protocol will return error if the request was already canceled
// or the page was closed. We should tolerate these errors. // or the page was closed. We should tolerate these errors.
await this._session.sendMayFail('Network.interceptWithRequest', { await this._session.sendMayFail('Network.interceptWithRequest', {

View file

@ -0,0 +1,29 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { ExpectedTextValue } from '@protocol/channels';
import { isRegExp, isString } from './rtti';
export function serializeExpectedTextValues(items: (string | RegExp)[], options: { matchSubstring?: boolean, normalizeWhiteSpace?: boolean, ignoreCase?: boolean } = {}): ExpectedTextValue[] {
return items.map(i => ({
string: isString(i) ? i : undefined,
regexSource: isRegExp(i) ? i.source : undefined,
regexFlags: isRegExp(i) ? i.flags : undefined,
matchSubstring: options.matchSubstring,
ignoreCase: options.ignoreCase,
normalizeWhiteSpace: options.normalizeWhiteSpace,
}));
}

View file

@ -21,6 +21,7 @@ export * from './debug';
export * from './debugLogger'; export * from './debugLogger';
export * from './env'; export * from './env';
export * from './eventsHelper'; export * from './eventsHelper';
export * from './expectUtils';
export * from './fileUtils'; export * from './fileUtils';
export * from './headers'; export * from './headers';
export * from './hostPlatform'; export * from './hostPlatform';

View file

@ -19,6 +19,7 @@ import path from 'path';
export const colors: typeof import('../bundles/utils/node_modules/colors/safe') = require('./utilsBundleImpl').colors; export const colors: typeof import('../bundles/utils/node_modules/colors/safe') = require('./utilsBundleImpl').colors;
export const debug: typeof import('../bundles/utils/node_modules/@types/debug') = require('./utilsBundleImpl').debug; export const debug: typeof import('../bundles/utils/node_modules/@types/debug') = require('./utilsBundleImpl').debug;
export const dotenv: typeof import('../bundles/utils/node_modules/dotenv') = require('./utilsBundleImpl').dotenv;
export const getProxyForUrl: typeof import('../bundles/utils/node_modules/@types/proxy-from-env').getProxyForUrl = require('./utilsBundleImpl').getProxyForUrl; export const getProxyForUrl: typeof import('../bundles/utils/node_modules/@types/proxy-from-env').getProxyForUrl = require('./utilsBundleImpl').getProxyForUrl;
export const HttpsProxyAgent: typeof import('../bundles/utils/node_modules/https-proxy-agent').HttpsProxyAgent = require('./utilsBundleImpl').HttpsProxyAgent; export const HttpsProxyAgent: typeof import('../bundles/utils/node_modules/https-proxy-agent').HttpsProxyAgent = require('./utilsBundleImpl').HttpsProxyAgent;
export const jpegjs: typeof import('../bundles/utils/node_modules/jpeg-js') = require('./utilsBundleImpl').jpegjs; export const jpegjs: typeof import('../bundles/utils/node_modules/jpeg-js') = require('./utilsBundleImpl').jpegjs;

View file

@ -1131,17 +1131,21 @@ using Audits.issueAdded event.
} }
/** /**
* Defines commands and events for browser extensions. Available if the client * Defines commands and events for browser extensions.
is connected using the --remote-debugging-pipe flag and
the --enable-unsafe-extension-debugging flag is set.
*/ */
export module Extensions { export module Extensions {
/**
* Storage areas.
*/
export type StorageArea = "session"|"local"|"sync"|"managed";
/** /**
* Installs an unpacked extension from the filesystem similar to * Installs an unpacked extension from the filesystem similar to
--load-extension CLI flags. Returns extension ID once the extension --load-extension CLI flags. Returns extension ID once the extension
has been installed. has been installed. Available if the client is connected using the
--remote-debugging-pipe flag and the --enable-unsafe-extension-debugging
flag is set.
*/ */
export type loadUnpackedParameters = { export type loadUnpackedParameters = {
/** /**
@ -1155,6 +1159,81 @@ has been installed.
*/ */
id: string; id: string;
} }
/**
* Gets data from extension storage in the given `storageArea`. If `keys` is
specified, these are used to filter the result.
*/
export type getStorageItemsParameters = {
/**
* ID of extension.
*/
id: string;
/**
* StorageArea to retrieve data from.
*/
storageArea: StorageArea;
/**
* Keys to retrieve.
*/
keys?: string[];
}
export type getStorageItemsReturnValue = {
data: { [key: string]: string };
}
/**
* Removes `keys` from extension storage in the given `storageArea`.
*/
export type removeStorageItemsParameters = {
/**
* ID of extension.
*/
id: string;
/**
* StorageArea to remove data from.
*/
storageArea: StorageArea;
/**
* Keys to remove.
*/
keys: string[];
}
export type removeStorageItemsReturnValue = {
}
/**
* Clears extension storage in the given `storageArea`.
*/
export type clearStorageItemsParameters = {
/**
* ID of extension.
*/
id: string;
/**
* StorageArea to remove data from.
*/
storageArea: StorageArea;
}
export type clearStorageItemsReturnValue = {
}
/**
* Sets `values` in extension storage in the given `storageArea`. The provided `values`
will be merged with existing values in the storage area.
*/
export type setStorageItemsParameters = {
/**
* ID of extension.
*/
id: string;
/**
* StorageArea to set data in.
*/
storageArea: StorageArea;
/**
* Values to set.
*/
values: { [key: string]: string };
}
export type setStorageItemsReturnValue = {
}
} }
/** /**
@ -2532,16 +2611,6 @@ stylesheet rules) this rule came from.
*/ */
style: CSSStyle; style: CSSStyle;
} }
/**
* CSS position-fallback rule representation.
*/
export interface CSSPositionFallbackRule {
name: Value;
/**
* List of keyframes.
*/
tryRules: CSSTryRule[];
}
/** /**
* CSS @position-try rule representation. * CSS @position-try rule representation.
*/ */
@ -2888,10 +2957,6 @@ attributes) for a DOM node identified by `nodeId`.
* A list of CSS keyframed animations matching this node. * A list of CSS keyframed animations matching this node.
*/ */
cssKeyframesRules?: CSSKeyframesRule[]; cssKeyframesRules?: CSSKeyframesRule[];
/**
* A list of CSS position fallbacks matching this node.
*/
cssPositionFallbackRules?: CSSPositionFallbackRule[];
/** /**
* A list of CSS @position-try rules matching this node, based on the position-try-fallbacks property. * A list of CSS @position-try rules matching this node, based on the position-try-fallbacks property.
*/ */
@ -3496,7 +3561,7 @@ front-end.
/** /**
* Pseudo element type. * Pseudo element type.
*/ */
export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"marker"|"backdrop"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"; export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"marker"|"backdrop"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scroll-next-button"|"scroll-prev-button"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new";
/** /**
* Shadow root type. * Shadow root type.
*/ */
@ -3646,6 +3711,13 @@ The property is always undefined now.
compatibilityMode?: CompatibilityMode; compatibilityMode?: CompatibilityMode;
assignedSlot?: BackendNode; assignedSlot?: BackendNode;
} }
/**
* A structure to hold the top-level node of a detached tree and an array of its retained descendants.
*/
export interface DetachedElementInfo {
treeNode: Node;
retainedNodeIds: NodeId[];
}
/** /**
* A structure holding an RGBA color. * A structure holding an RGBA color.
*/ */
@ -4693,6 +4765,17 @@ File wrapper.
export type getFileInfoReturnValue = { export type getFileInfoReturnValue = {
path: string; path: string;
} }
/**
* Returns list of detached nodes
*/
export type getDetachedDomNodesParameters = {
}
export type getDetachedDomNodesReturnValue = {
/**
* The list of detached nodes
*/
detachedNodes: DetachedElementInfo[];
}
/** /**
* Enables console to refer to the node with given id via $x (see Command Line API for more details * Enables console to refer to the node with given id via $x (see Command Line API for more details
$x functions). $x functions).
@ -11369,7 +11452,7 @@ as an ad.
* All Permissions Policy features. This enum should match the one defined * All Permissions Policy features. This enum should match the one defined
in third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5. in third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5.
*/ */
export type PermissionsPolicyFeature = "accelerometer"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"cross-origin-isolated"|"deferred-fetch"|"digital-credentials-get"|"direct-sockets"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking"; export type PermissionsPolicyFeature = "accelerometer"|"all-screens-capture"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"cross-origin-isolated"|"deferred-fetch"|"digital-credentials-get"|"direct-sockets"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"media-playback-while-not-visible"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking";
/** /**
* Reason for a permissions policy feature to be disabled. * Reason for a permissions policy feature to be disabled.
*/ */
@ -11784,7 +11867,7 @@ Example URLs: http://www.google.com/file.html -> "google.com"
*/ */
fixed?: number; fixed?: number;
} }
export type ClientNavigationReason = "formSubmissionGet"|"formSubmissionPost"|"httpHeaderRefresh"|"scriptInitiated"|"metaTagRefresh"|"pageBlockInterstitial"|"reload"|"anchorClick"; export type ClientNavigationReason = "anchorClick"|"formSubmissionGet"|"formSubmissionPost"|"httpHeaderRefresh"|"initialFrameNavigation"|"metaTagRefresh"|"other"|"pageBlockInterstitial"|"reload"|"scriptInitiated";
export type ClientNavigationDisposition = "currentTab"|"newTab"|"newWindow"|"download"; export type ClientNavigationDisposition = "currentTab"|"newTab"|"newWindow"|"download";
export interface InstallabilityErrorArgument { export interface InstallabilityErrorArgument {
/** /**
@ -12298,6 +12381,10 @@ when bfcache navigation fails.
* Frame's new url. * Frame's new url.
*/ */
url: string; url: string;
/**
* Navigation type
*/
navigationType: "fragment"|"historyApi"|"other";
} }
/** /**
* Compressed image data requested by the `startScreencast`. * Compressed image data requested by the `startScreencast`.
@ -16922,7 +17009,7 @@ possible for multiple rule sets and links to trigger a single attempt.
/** /**
* List of FinalStatus reasons for Prerender2. * List of FinalStatus reasons for Prerender2.
*/ */
export type PrerenderFinalStatus = "Activated"|"Destroyed"|"LowEndDevice"|"InvalidSchemeRedirect"|"InvalidSchemeNavigation"|"NavigationRequestBlockedByCsp"|"MainFrameNavigation"|"MojoBinderPolicy"|"RendererProcessCrashed"|"RendererProcessKilled"|"Download"|"TriggerDestroyed"|"NavigationNotCommitted"|"NavigationBadHttpStatus"|"ClientCertRequested"|"NavigationRequestNetworkError"|"CancelAllHostsForTesting"|"DidFailLoad"|"Stop"|"SslCertificateError"|"LoginAuthRequested"|"UaChangeRequiresReload"|"BlockedByClient"|"AudioOutputDeviceRequested"|"MixedContent"|"TriggerBackgrounded"|"MemoryLimitExceeded"|"DataSaverEnabled"|"TriggerUrlHasEffectiveUrl"|"ActivatedBeforeStarted"|"InactivePageRestriction"|"StartFailed"|"TimeoutBackgrounded"|"CrossSiteRedirectInInitialNavigation"|"CrossSiteNavigationInInitialNavigation"|"SameSiteCrossOriginRedirectNotOptInInInitialNavigation"|"SameSiteCrossOriginNavigationNotOptInInInitialNavigation"|"ActivationNavigationParameterMismatch"|"ActivatedInBackground"|"EmbedderHostDisallowed"|"ActivationNavigationDestroyedBeforeSuccess"|"TabClosedByUserGesture"|"TabClosedWithoutUserGesture"|"PrimaryMainFrameRendererProcessCrashed"|"PrimaryMainFrameRendererProcessKilled"|"ActivationFramePolicyNotCompatible"|"PreloadingDisabled"|"BatterySaverEnabled"|"ActivatedDuringMainFrameNavigation"|"PreloadingUnsupportedByWebContents"|"CrossSiteRedirectInMainFrameNavigation"|"CrossSiteNavigationInMainFrameNavigation"|"SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation"|"SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation"|"MemoryPressureOnTrigger"|"MemoryPressureAfterTriggered"|"PrerenderingDisabledByDevTools"|"SpeculationRuleRemoved"|"ActivatedWithAuxiliaryBrowsingContexts"|"MaxNumOfRunningEagerPrerendersExceeded"|"MaxNumOfRunningNonEagerPrerendersExceeded"|"MaxNumOfRunningEmbedderPrerendersExceeded"|"PrerenderingUrlHasEffectiveUrl"|"RedirectedPrerenderingUrlHasEffectiveUrl"|"ActivationUrlHasEffectiveUrl"|"JavaScriptInterfaceAdded"|"JavaScriptInterfaceRemoved"|"AllPrerenderingCanceled"|"WindowClosed"; export type PrerenderFinalStatus = "Activated"|"Destroyed"|"LowEndDevice"|"InvalidSchemeRedirect"|"InvalidSchemeNavigation"|"NavigationRequestBlockedByCsp"|"MainFrameNavigation"|"MojoBinderPolicy"|"RendererProcessCrashed"|"RendererProcessKilled"|"Download"|"TriggerDestroyed"|"NavigationNotCommitted"|"NavigationBadHttpStatus"|"ClientCertRequested"|"NavigationRequestNetworkError"|"CancelAllHostsForTesting"|"DidFailLoad"|"Stop"|"SslCertificateError"|"LoginAuthRequested"|"UaChangeRequiresReload"|"BlockedByClient"|"AudioOutputDeviceRequested"|"MixedContent"|"TriggerBackgrounded"|"MemoryLimitExceeded"|"DataSaverEnabled"|"TriggerUrlHasEffectiveUrl"|"ActivatedBeforeStarted"|"InactivePageRestriction"|"StartFailed"|"TimeoutBackgrounded"|"CrossSiteRedirectInInitialNavigation"|"CrossSiteNavigationInInitialNavigation"|"SameSiteCrossOriginRedirectNotOptInInInitialNavigation"|"SameSiteCrossOriginNavigationNotOptInInInitialNavigation"|"ActivationNavigationParameterMismatch"|"ActivatedInBackground"|"EmbedderHostDisallowed"|"ActivationNavigationDestroyedBeforeSuccess"|"TabClosedByUserGesture"|"TabClosedWithoutUserGesture"|"PrimaryMainFrameRendererProcessCrashed"|"PrimaryMainFrameRendererProcessKilled"|"ActivationFramePolicyNotCompatible"|"PreloadingDisabled"|"BatterySaverEnabled"|"ActivatedDuringMainFrameNavigation"|"PreloadingUnsupportedByWebContents"|"CrossSiteRedirectInMainFrameNavigation"|"CrossSiteNavigationInMainFrameNavigation"|"SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation"|"SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation"|"MemoryPressureOnTrigger"|"MemoryPressureAfterTriggered"|"PrerenderingDisabledByDevTools"|"SpeculationRuleRemoved"|"ActivatedWithAuxiliaryBrowsingContexts"|"MaxNumOfRunningEagerPrerendersExceeded"|"MaxNumOfRunningNonEagerPrerendersExceeded"|"MaxNumOfRunningEmbedderPrerendersExceeded"|"PrerenderingUrlHasEffectiveUrl"|"RedirectedPrerenderingUrlHasEffectiveUrl"|"ActivationUrlHasEffectiveUrl"|"JavaScriptInterfaceAdded"|"JavaScriptInterfaceRemoved"|"AllPrerenderingCanceled"|"WindowClosed"|"SlowNetwork"|"OtherPrerenderedPageActivated";
/** /**
* Preloading status values, see also PreloadingTriggeringOutcome. This * Preloading status values, see also PreloadingTriggeringOutcome. This
status is shared by prefetchStatusUpdated and prerenderStatusUpdated. status is shared by prefetchStatusUpdated and prerenderStatusUpdated.
@ -17270,6 +17357,101 @@ supported yet.
} }
} }
/**
* This domain allows configuring virtual Bluetooth devices to test
the web-bluetooth API.
*/
export module BluetoothEmulation {
/**
* Indicates the various states of Central.
*/
export type CentralState = "absent"|"powered-off"|"powered-on";
/**
* Stores the manufacturer data
*/
export interface ManufacturerData {
/**
* Company identifier
https://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/company_identifiers/company_identifiers.yaml
https://usb.org/developers
*/
key: number;
/**
* Manufacturer-specific data
*/
data: binary;
}
/**
* Stores the byte data of the advertisement packet sent by a Bluetooth device.
*/
export interface ScanRecord {
name?: string;
uuids?: string[];
/**
* Stores the external appearance description of the device.
*/
appearance?: number;
/**
* Stores the transmission power of a broadcasting device.
*/
txPower?: number;
/**
* Key is the company identifier and the value is an array of bytes of
manufacturer specific data.
*/
manufacturerData?: ManufacturerData[];
}
/**
* Stores the advertisement packet information that is sent by a Bluetooth device.
*/
export interface ScanEntry {
deviceAddress: string;
rssi: number;
scanRecord: ScanRecord;
}
/**
* Enable the BluetoothEmulation domain.
*/
export type enableParameters = {
/**
* State of the simulated central.
*/
state: CentralState;
}
export type enableReturnValue = {
}
/**
* Disable the BluetoothEmulation domain.
*/
export type disableParameters = {
}
export type disableReturnValue = {
}
/**
* Simulates a peripheral with |address|, |name| and |knownServiceUuids|
that has already been connected to the system.
*/
export type simulatePreconnectedPeripheralParameters = {
address: string;
name: string;
manufacturerData: ManufacturerData[];
knownServiceUuids: string[];
}
export type simulatePreconnectedPeripheralReturnValue = {
}
/**
* Simulates an advertisement packet described in |entry| being received by
the central.
*/
export type simulateAdvertisementParameters = {
entry: ScanEntry;
}
export type simulateAdvertisementReturnValue = {
}
}
/** /**
* This domain is deprecated - use Runtime or Log instead. * This domain is deprecated - use Runtime or Log instead.
*/ */
@ -20122,6 +20304,10 @@ Error was thrown.
"Audits.checkContrast": Audits.checkContrastParameters; "Audits.checkContrast": Audits.checkContrastParameters;
"Audits.checkFormsIssues": Audits.checkFormsIssuesParameters; "Audits.checkFormsIssues": Audits.checkFormsIssuesParameters;
"Extensions.loadUnpacked": Extensions.loadUnpackedParameters; "Extensions.loadUnpacked": Extensions.loadUnpackedParameters;
"Extensions.getStorageItems": Extensions.getStorageItemsParameters;
"Extensions.removeStorageItems": Extensions.removeStorageItemsParameters;
"Extensions.clearStorageItems": Extensions.clearStorageItemsParameters;
"Extensions.setStorageItems": Extensions.setStorageItemsParameters;
"Autofill.trigger": Autofill.triggerParameters; "Autofill.trigger": Autofill.triggerParameters;
"Autofill.setAddresses": Autofill.setAddressesParameters; "Autofill.setAddresses": Autofill.setAddressesParameters;
"Autofill.disable": Autofill.disableParameters; "Autofill.disable": Autofill.disableParameters;
@ -20232,6 +20418,7 @@ Error was thrown.
"DOM.setNodeStackTracesEnabled": DOM.setNodeStackTracesEnabledParameters; "DOM.setNodeStackTracesEnabled": DOM.setNodeStackTracesEnabledParameters;
"DOM.getNodeStackTraces": DOM.getNodeStackTracesParameters; "DOM.getNodeStackTraces": DOM.getNodeStackTracesParameters;
"DOM.getFileInfo": DOM.getFileInfoParameters; "DOM.getFileInfo": DOM.getFileInfoParameters;
"DOM.getDetachedDomNodes": DOM.getDetachedDomNodesParameters;
"DOM.setInspectedNode": DOM.setInspectedNodeParameters; "DOM.setInspectedNode": DOM.setInspectedNodeParameters;
"DOM.setNodeName": DOM.setNodeNameParameters; "DOM.setNodeName": DOM.setNodeNameParameters;
"DOM.setNodeValue": DOM.setNodeValueParameters; "DOM.setNodeValue": DOM.setNodeValueParameters;
@ -20616,6 +20803,10 @@ Error was thrown.
"PWA.launchFilesInApp": PWA.launchFilesInAppParameters; "PWA.launchFilesInApp": PWA.launchFilesInAppParameters;
"PWA.openCurrentPageInApp": PWA.openCurrentPageInAppParameters; "PWA.openCurrentPageInApp": PWA.openCurrentPageInAppParameters;
"PWA.changeAppUserSettings": PWA.changeAppUserSettingsParameters; "PWA.changeAppUserSettings": PWA.changeAppUserSettingsParameters;
"BluetoothEmulation.enable": BluetoothEmulation.enableParameters;
"BluetoothEmulation.disable": BluetoothEmulation.disableParameters;
"BluetoothEmulation.simulatePreconnectedPeripheral": BluetoothEmulation.simulatePreconnectedPeripheralParameters;
"BluetoothEmulation.simulateAdvertisement": BluetoothEmulation.simulateAdvertisementParameters;
"Console.clearMessages": Console.clearMessagesParameters; "Console.clearMessages": Console.clearMessagesParameters;
"Console.disable": Console.disableParameters; "Console.disable": Console.disableParameters;
"Console.enable": Console.enableParameters; "Console.enable": Console.enableParameters;
@ -20722,6 +20913,10 @@ Error was thrown.
"Audits.checkContrast": Audits.checkContrastReturnValue; "Audits.checkContrast": Audits.checkContrastReturnValue;
"Audits.checkFormsIssues": Audits.checkFormsIssuesReturnValue; "Audits.checkFormsIssues": Audits.checkFormsIssuesReturnValue;
"Extensions.loadUnpacked": Extensions.loadUnpackedReturnValue; "Extensions.loadUnpacked": Extensions.loadUnpackedReturnValue;
"Extensions.getStorageItems": Extensions.getStorageItemsReturnValue;
"Extensions.removeStorageItems": Extensions.removeStorageItemsReturnValue;
"Extensions.clearStorageItems": Extensions.clearStorageItemsReturnValue;
"Extensions.setStorageItems": Extensions.setStorageItemsReturnValue;
"Autofill.trigger": Autofill.triggerReturnValue; "Autofill.trigger": Autofill.triggerReturnValue;
"Autofill.setAddresses": Autofill.setAddressesReturnValue; "Autofill.setAddresses": Autofill.setAddressesReturnValue;
"Autofill.disable": Autofill.disableReturnValue; "Autofill.disable": Autofill.disableReturnValue;
@ -20832,6 +21027,7 @@ Error was thrown.
"DOM.setNodeStackTracesEnabled": DOM.setNodeStackTracesEnabledReturnValue; "DOM.setNodeStackTracesEnabled": DOM.setNodeStackTracesEnabledReturnValue;
"DOM.getNodeStackTraces": DOM.getNodeStackTracesReturnValue; "DOM.getNodeStackTraces": DOM.getNodeStackTracesReturnValue;
"DOM.getFileInfo": DOM.getFileInfoReturnValue; "DOM.getFileInfo": DOM.getFileInfoReturnValue;
"DOM.getDetachedDomNodes": DOM.getDetachedDomNodesReturnValue;
"DOM.setInspectedNode": DOM.setInspectedNodeReturnValue; "DOM.setInspectedNode": DOM.setInspectedNodeReturnValue;
"DOM.setNodeName": DOM.setNodeNameReturnValue; "DOM.setNodeName": DOM.setNodeNameReturnValue;
"DOM.setNodeValue": DOM.setNodeValueReturnValue; "DOM.setNodeValue": DOM.setNodeValueReturnValue;
@ -21216,6 +21412,10 @@ Error was thrown.
"PWA.launchFilesInApp": PWA.launchFilesInAppReturnValue; "PWA.launchFilesInApp": PWA.launchFilesInAppReturnValue;
"PWA.openCurrentPageInApp": PWA.openCurrentPageInAppReturnValue; "PWA.openCurrentPageInApp": PWA.openCurrentPageInAppReturnValue;
"PWA.changeAppUserSettings": PWA.changeAppUserSettingsReturnValue; "PWA.changeAppUserSettings": PWA.changeAppUserSettingsReturnValue;
"BluetoothEmulation.enable": BluetoothEmulation.enableReturnValue;
"BluetoothEmulation.disable": BluetoothEmulation.disableReturnValue;
"BluetoothEmulation.simulatePreconnectedPeripheral": BluetoothEmulation.simulatePreconnectedPeripheralReturnValue;
"BluetoothEmulation.simulateAdvertisement": BluetoothEmulation.simulateAdvertisementReturnValue;
"Console.clearMessages": Console.clearMessagesReturnValue; "Console.clearMessages": Console.clearMessagesReturnValue;
"Console.disable": Console.disableReturnValue; "Console.disable": Console.disableReturnValue;
"Console.enable": Console.enableReturnValue; "Console.enable": Console.enableReturnValue;

View file

@ -288,41 +288,8 @@ export interface Page {
* [browserContext.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script) * [browserContext.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script)
* and [page.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-page#page-add-init-script) is not * and [page.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-page#page-add-init-script) is not
* defined. * defined.
*
* **Bundling**
*
* If you have a complex script split into several files, it needs to be bundled into a single file first. We
* recommend running [`esbuild`](https://esbuild.github.io/) or [`webpack`](https://webpack.js.org/) to produce a
* commonjs module and pass `path` and `arg`.
*
* ```js
* // mocks/mockRandom.ts
* // This script can import other files.
* import { defaultValue } from './defaultValue';
*
* export default function(value?: number) {
* window.Math.random = () => value ?? defaultValue;
* }
* ```
*
* ```js
* // tests/example.spec.ts
* const mockPath = { path: path.resolve(__dirname, '../mocks/mockRandom.js') };
*
* // Passing 42 as an argument to the default export function.
* await page.addInitScript({ path: mockPath }, 42);
*
* // Make sure to pass something even if you do not need to pass an argument.
* // This instructs Playwright to treat the file as a commonjs module.
* await page.addInitScript({ path: mockPath }, '');
* ```
*
* @param script Script to be evaluated in the page. * @param script Script to be evaluated in the page.
* @param arg Optional JSON-serializable argument to pass to `script`. * @param arg Optional argument to pass to `script` (only supported when passing a function).
* - When `script` is a function, the argument is passed to it directly.
* - When `script` is a file path, the file is assumed to be a commonjs module. The default export, either
* `module.exports` or `module.exports.default`, should be a function that's going to be executed with this
* argument.
*/ */
addInitScript<Arg>(script: PageFunction<Arg, any> | { path?: string, content?: string }, arg?: Arg): Promise<void>; addInitScript<Arg>(script: PageFunction<Arg, any> | { path?: string, content?: string }, arg?: Arg): Promise<void>;
@ -898,17 +865,55 @@ export interface Page {
exposeBinding(name: string, playwrightBinding: (source: BindingSource, ...args: any[]) => any, options?: { handle?: boolean }): Promise<void>; exposeBinding(name: string, playwrightBinding: (source: BindingSource, ...args: any[]) => any, options?: { handle?: boolean }): Promise<void>;
/** /**
* Removes all the listeners of the given type if the type is given. Otherwise removes all the listeners. * Removes all the listeners of the given type (or all registered listeners if no type given). Allows to wait for
* async listeners to complete or to ignore subsequent errors from these listeners.
*
* **Usage**
*
* ```js
* page.on('request', async request => {
* const response = await request.response();
* const body = await response.body();
* console.log(body.byteLength);
* });
* await page.goto('https://playwright.dev', { waitUntil: 'domcontentloaded' });
* // Waits for all the reported 'request' events to resolve.
* await page.removeAllListeners('request', { behavior: 'wait' });
* ```
*
* @param type * @param type
* @param options * @param options
*/ */
removeAllListeners(type?: string): this; removeAllListeners(type?: string): this;
/** /**
* Removes all the listeners of the given type if the type is given. Otherwise removes all the listeners. * Removes all the listeners of the given type (or all registered listeners if no type given). Allows to wait for
* async listeners to complete or to ignore subsequent errors from these listeners.
*
* **Usage**
*
* ```js
* page.on('request', async request => {
* const response = await request.response();
* const body = await response.body();
* console.log(body.byteLength);
* });
* await page.goto('https://playwright.dev', { waitUntil: 'domcontentloaded' });
* // Waits for all the reported 'request' events to resolve.
* await page.removeAllListeners('request', { behavior: 'wait' });
* ```
*
* @param type * @param type
* @param options * @param options
*/ */
removeAllListeners(type: string | undefined, options: { behavior?: 'wait'|'ignoreErrors'|'default' }): Promise<void>; removeAllListeners(type: string | undefined, options: {
/**
* Specifies whether to wait for already running listeners and what to do if they throw errors:
* - `'default'` - do not wait for current listener calls (if any) to finish, if the listener throws, it may result in unhandled error
* - `'wait'` - wait for current listener calls (if any) to finish
* - `'ignoreErrors'` - do not wait for current listener calls (if any) to finish, all errors thrown by the listeners after removal are silently caught
*/
behavior?: 'wait'|'ignoreErrors'|'default'
}): Promise<void>;
/** /**
* Emitted when the page closes. * Emitted when the page closes.
*/ */
@ -3897,10 +3902,8 @@ export interface Page {
force?: boolean; force?: boolean;
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You * This option has no effect.
* can opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as * @deprecated This option has no effect.
* navigating to inaccessible pages. Defaults to `false`.
* @deprecated This option will default to `true` in the future.
*/ */
noWaitAfter?: boolean; noWaitAfter?: boolean;
@ -7023,10 +7026,8 @@ export interface Frame {
force?: boolean; force?: boolean;
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You * This option has no effect.
* can opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as * @deprecated This option has no effect.
* navigating to inaccessible pages. Defaults to `false`.
* @deprecated This option will default to `true` in the future.
*/ */
noWaitAfter?: boolean; noWaitAfter?: boolean;
@ -7571,9 +7572,9 @@ export interface Frame {
* If a page opens another page, e.g. with a `window.open` call, the popup will belong to the parent page's browser * If a page opens another page, e.g. with a `window.open` call, the popup will belong to the parent page's browser
* context. * context.
* *
* Playwright allows creating "incognito" browser contexts with * Playwright allows creating isolated non-persistent browser contexts with
* [browser.newContext([options])](https://playwright.dev/docs/api/class-browser#browser-new-context) method. * [browser.newContext([options])](https://playwright.dev/docs/api/class-browser#browser-new-context) method.
* "Incognito" browser contexts don't write any browsing data to disk. * Non-persistent browser contexts don't write any browsing data to disk.
* *
* ```js * ```js
* // Create a new incognito browser context * // Create a new incognito browser context
@ -7699,56 +7700,33 @@ export interface BrowserContext {
* [browserContext.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script) * [browserContext.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script)
* and [page.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-page#page-add-init-script) is not * and [page.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-page#page-add-init-script) is not
* defined. * defined.
*
* **Bundling**
*
* If you have a complex script split into several files, it needs to be bundled into a single file first. We
* recommend running [`esbuild`](https://esbuild.github.io/) or [`webpack`](https://webpack.js.org/) to produce a
* commonjs module and pass `path` and `arg`.
*
* ```js
* // mocks/mockRandom.ts
* // This script can import other files.
* import { defaultValue } from './defaultValue';
*
* export default function(value?: number) {
* window.Math.random = () => value ?? defaultValue;
* }
* ```
*
* ```js
* // tests/example.spec.ts
* const mockPath = { path: path.resolve(__dirname, '../mocks/mockRandom.js') };
*
* // Passing 42 as an argument to the default export function.
* await context.addInitScript({ path: mockPath }, 42);
*
* // Make sure to pass something even if you do not need to pass an argument.
* // This instructs Playwright to treat the file as a commonjs module.
* await context.addInitScript({ path: mockPath }, '');
* ```
*
* @param script Script to be evaluated in all pages in the browser context. * @param script Script to be evaluated in all pages in the browser context.
* @param arg Optional JSON-serializable argument to pass to `script`. * @param arg Optional argument to pass to `script` (only supported when passing a function).
* - When `script` is a function, the argument is passed to it directly.
* - When `script` is a file path, the file is assumed to be a commonjs module. The default export, either
* `module.exports` or `module.exports.default`, should be a function that's going to be executed with this
* argument.
*/ */
addInitScript<Arg>(script: PageFunction<Arg, any> | { path?: string, content?: string }, arg?: Arg): Promise<void>; addInitScript<Arg>(script: PageFunction<Arg, any> | { path?: string, content?: string }, arg?: Arg): Promise<void>;
/** /**
* Removes all the listeners of the given type if the type is given. Otherwise removes all the listeners. * Removes all the listeners of the given type (or all registered listeners if no type given). Allows to wait for
* async listeners to complete or to ignore subsequent errors from these listeners.
* @param type * @param type
* @param options * @param options
*/ */
removeAllListeners(type?: string): this; removeAllListeners(type?: string): this;
/** /**
* Removes all the listeners of the given type if the type is given. Otherwise removes all the listeners. * Removes all the listeners of the given type (or all registered listeners if no type given). Allows to wait for
* async listeners to complete or to ignore subsequent errors from these listeners.
* @param type * @param type
* @param options * @param options
*/ */
removeAllListeners(type: string | undefined, options: { behavior?: 'wait'|'ignoreErrors'|'default' }): Promise<void>; removeAllListeners(type: string | undefined, options: {
/**
* Specifies whether to wait for already running listeners and what to do if they throw errors:
* - `'default'` - do not wait for current listener calls (if any) to finish, if the listener throws, it may result in unhandled error
* - `'wait'` - wait for current listener calls (if any) to finish
* - `'ignoreErrors'` - do not wait for current listener calls (if any) to finish, all errors thrown by the listeners after removal are silently caught
*/
behavior?: 'wait'|'ignoreErrors'|'default'
}): Promise<void>;
/** /**
* **NOTE** Only works with Chromium browser's persistent context. * **NOTE** Only works with Chromium browser's persistent context.
* *
@ -9022,17 +9000,27 @@ export interface BrowserContext {
*/ */
export interface Browser { export interface Browser {
/** /**
* Removes all the listeners of the given type if the type is given. Otherwise removes all the listeners. * Removes all the listeners of the given type (or all registered listeners if no type given). Allows to wait for
* async listeners to complete or to ignore subsequent errors from these listeners.
* @param type * @param type
* @param options * @param options
*/ */
removeAllListeners(type?: string): this; removeAllListeners(type?: string): this;
/** /**
* Removes all the listeners of the given type if the type is given. Otherwise removes all the listeners. * Removes all the listeners of the given type (or all registered listeners if no type given). Allows to wait for
* async listeners to complete or to ignore subsequent errors from these listeners.
* @param type * @param type
* @param options * @param options
*/ */
removeAllListeners(type: string | undefined, options: { behavior?: 'wait'|'ignoreErrors'|'default' }): Promise<void>; removeAllListeners(type: string | undefined, options: {
/**
* Specifies whether to wait for already running listeners and what to do if they throw errors:
* - `'default'` - do not wait for current listener calls (if any) to finish, if the listener throws, it may result in unhandled error
* - `'wait'` - wait for current listener calls (if any) to finish
* - `'ignoreErrors'` - do not wait for current listener calls (if any) to finish, all errors thrown by the listeners after removal are silently caught
*/
behavior?: 'wait'|'ignoreErrors'|'default'
}): Promise<void>;
/** /**
* Emitted when Browser gets disconnected from the browser application. This might happen because of one of the * Emitted when Browser gets disconnected from the browser application. This might happen because of one of the
* following: * following:
@ -11136,10 +11124,8 @@ export interface ElementHandle<T=Node> extends JSHandle<T> {
force?: boolean; force?: boolean;
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You * This option has no effect.
* can opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as * @deprecated This option has no effect.
* navigating to inaccessible pages. Defaults to `false`.
* @deprecated This option will default to `true` in the future.
*/ */
noWaitAfter?: boolean; noWaitAfter?: boolean;
@ -13331,10 +13317,8 @@ export interface Locator {
force?: boolean; force?: boolean;
/** /**
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You * This option has no effect.
* can opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as * @deprecated This option has no effect.
* navigating to inaccessible pages. Defaults to `false`.
* @deprecated This option will default to `true` in the future.
*/ */
noWaitAfter?: boolean; noWaitAfter?: boolean;
@ -17336,8 +17320,8 @@ export interface APIResponse {
headers(): { [key: string]: string; }; headers(): { [key: string]: string; };
/** /**
* An array with all the request HTTP headers associated with this response. Header names are not lower-cased. Headers * An array with all the response HTTP headers associated with this response. Header names are not lower-cased.
* with multiple entries, such as `Set-Cookie`, appear in the array multiple times. * Headers with multiple entries, such as `Set-Cookie`, appear in the array multiple times.
*/ */
headersArray(): Array<{ headersArray(): Array<{
/** /**

View file

@ -34,7 +34,7 @@
"@sveltejs/vite-plugin-svelte": "^3.0.1" "@sveltejs/vite-plugin-svelte": "^3.0.1"
}, },
"devDependencies": { "devDependencies": {
"svelte": "^4.2.8" "svelte": "^4.2.19"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"

View file

@ -44,7 +44,7 @@ export interface TestServerInterface {
installBrowsers(params: {}): Promise<void>; installBrowsers(params: {}): Promise<void>;
runGlobalSetup(params: {}): Promise<{ runGlobalSetup(params: { outputDir?: string }): Promise<{
report: ReportEntry[], report: ReportEntry[],
status: reporterTypes.FullResult['status'] status: reporterTypes.FullResult['status']
}>; }>;
@ -81,6 +81,7 @@ export interface TestServerInterface {
locations?: string[]; locations?: string[];
grep?: string; grep?: string;
grepInvert?: string; grepInvert?: string;
outputDir?: string;
}): Promise<{ }): Promise<{
report: ReportEntry[], report: ReportEntry[],
status: reporterTypes.FullResult['status'] status: reporterTypes.FullResult['status']

View file

@ -60,7 +60,7 @@ import {
} from '../common/expectBundle'; } from '../common/expectBundle';
import { zones } from 'playwright-core/lib/utils'; import { zones } from 'playwright-core/lib/utils';
import { TestInfoImpl } from '../worker/testInfo'; import { TestInfoImpl } from '../worker/testInfo';
import { ExpectError } from './matcherHint'; import { ExpectError, isExpectError } from './matcherHint';
// #region // #region
// Mirrored from https://github.com/facebook/jest/blob/f13abff8df9a0e1148baf3584bcde6d1b479edc7/packages/expect/src/print.ts // Mirrored from https://github.com/facebook/jest/blob/f13abff8df9a0e1148baf3584bcde6d1b479edc7/packages/expect/src/print.ts
@ -289,8 +289,8 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
const step = testInfo._addStep(stepInfo); const step = testInfo._addStep(stepInfo);
const reportStepError = (jestError: ExpectError) => { const reportStepError = (jestError: Error | unknown) => {
const error = new ExpectError(jestError, customMessage, stackFrames); const error = isExpectError(jestError) ? new ExpectError(jestError, customMessage, stackFrames) : jestError;
step.complete({ error }); step.complete({ error });
if (this._info.isSoft) if (this._info.isSoft)
testInfo._failWithError(error); testInfo._failWithError(error);

View file

@ -64,3 +64,7 @@ export class ExpectError extends Error {
this.stack = this.name + ': ' + this.message + '\n' + stringifyStackFrames(stackFrames).join('\n'); this.stack = this.name + ': ' + this.message + '\n' + stringifyStackFrames(stackFrames).join('\n');
} }
} }
export function isExpectError(e: unknown): e is ExpectError {
return e instanceof Error && 'matcherResult' in e;
}

View file

@ -20,8 +20,8 @@ import { colors } from 'playwright-core/lib/utilsBundle';
import { expectTypes, callLogText } from '../util'; import { expectTypes, callLogText } from '../util';
import { toBeTruthy } from './toBeTruthy'; import { toBeTruthy } from './toBeTruthy';
import { toEqual } from './toEqual'; import { toEqual } from './toEqual';
import { toExpectedTextValues, toMatchText } from './toMatchText'; import { toMatchText } from './toMatchText';
import { constructURLBasedOnBaseURL, isRegExp, isString, isTextualMimeType, pollAgainstDeadline } from 'playwright-core/lib/utils'; import { constructURLBasedOnBaseURL, isRegExp, isString, isTextualMimeType, pollAgainstDeadline, serializeExpectedTextValues } from 'playwright-core/lib/utils';
import { currentTestInfo } from '../common/globals'; import { currentTestInfo } from '../common/globals';
import { TestInfoImpl } from '../worker/testInfo'; import { TestInfoImpl } from '../worker/testInfo';
import type { ExpectMatcherState } from '../../types/test'; import type { ExpectMatcherState } from '../../types/test';
@ -163,12 +163,12 @@ export function toContainText(
) { ) {
if (Array.isArray(expected)) { if (Array.isArray(expected)) {
return toEqual.call(this, 'toContainText', locator, 'Locator', async (isNot, timeout) => { return toEqual.call(this, 'toContainText', locator, 'Locator', async (isNot, timeout) => {
const expectedText = toExpectedTextValues(expected, { matchSubstring: true, normalizeWhiteSpace: true, ignoreCase: options.ignoreCase }); const expectedText = serializeExpectedTextValues(expected, { matchSubstring: true, normalizeWhiteSpace: true, ignoreCase: options.ignoreCase });
return await locator._expect('to.contain.text.array', { expectedText, isNot, useInnerText: options.useInnerText, timeout }); return await locator._expect('to.contain.text.array', { expectedText, isNot, useInnerText: options.useInnerText, timeout });
}, expected, { ...options, contains: true }); }, expected, { ...options, contains: true });
} else { } else {
return toMatchText.call(this, 'toContainText', locator, 'Locator', async (isNot, timeout) => { return toMatchText.call(this, 'toContainText', locator, 'Locator', async (isNot, timeout) => {
const expectedText = toExpectedTextValues([expected], { matchSubstring: true, normalizeWhiteSpace: true, ignoreCase: options.ignoreCase }); const expectedText = serializeExpectedTextValues([expected], { matchSubstring: true, normalizeWhiteSpace: true, ignoreCase: options.ignoreCase });
return await locator._expect('to.have.text', { expectedText, isNot, useInnerText: options.useInnerText, timeout }); return await locator._expect('to.have.text', { expectedText, isNot, useInnerText: options.useInnerText, timeout });
}, expected, options); }, expected, options);
} }
@ -181,7 +181,7 @@ export function toHaveAccessibleDescription(
options?: { timeout?: number, ignoreCase?: boolean }, options?: { timeout?: number, ignoreCase?: boolean },
) { ) {
return toMatchText.call(this, 'toHaveAccessibleDescription', locator, 'Locator', async (isNot, timeout) => { return toMatchText.call(this, 'toHaveAccessibleDescription', locator, 'Locator', async (isNot, timeout) => {
const expectedText = toExpectedTextValues([expected], { ignoreCase: options?.ignoreCase }); const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase });
return await locator._expect('to.have.accessible.description', { expectedText, isNot, timeout }); return await locator._expect('to.have.accessible.description', { expectedText, isNot, timeout });
}, expected, options); }, expected, options);
} }
@ -193,7 +193,7 @@ export function toHaveAccessibleName(
options?: { timeout?: number, ignoreCase?: boolean }, options?: { timeout?: number, ignoreCase?: boolean },
) { ) {
return toMatchText.call(this, 'toHaveAccessibleName', locator, 'Locator', async (isNot, timeout) => { return toMatchText.call(this, 'toHaveAccessibleName', locator, 'Locator', async (isNot, timeout) => {
const expectedText = toExpectedTextValues([expected], { ignoreCase: options?.ignoreCase }); const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase });
return await locator._expect('to.have.accessible.name', { expectedText, isNot, timeout }); return await locator._expect('to.have.accessible.name', { expectedText, isNot, timeout });
}, expected, options); }, expected, options);
} }
@ -218,7 +218,7 @@ export function toHaveAttribute(
}, options); }, options);
} }
return toMatchText.call(this, 'toHaveAttribute', locator, 'Locator', async (isNot, timeout) => { return toMatchText.call(this, 'toHaveAttribute', locator, 'Locator', async (isNot, timeout) => {
const expectedText = toExpectedTextValues([expected as (string | RegExp)], { ignoreCase: options?.ignoreCase }); const expectedText = serializeExpectedTextValues([expected as (string | RegExp)], { ignoreCase: options?.ignoreCase });
return await locator._expect('to.have.attribute.value', { expressionArg: name, expectedText, isNot, timeout }); return await locator._expect('to.have.attribute.value', { expressionArg: name, expectedText, isNot, timeout });
}, expected as (string | RegExp), options); }, expected as (string | RegExp), options);
} }
@ -231,12 +231,12 @@ export function toHaveClass(
) { ) {
if (Array.isArray(expected)) { if (Array.isArray(expected)) {
return toEqual.call(this, 'toHaveClass', locator, 'Locator', async (isNot, timeout) => { return toEqual.call(this, 'toHaveClass', locator, 'Locator', async (isNot, timeout) => {
const expectedText = toExpectedTextValues(expected); const expectedText = serializeExpectedTextValues(expected);
return await locator._expect('to.have.class.array', { expectedText, isNot, timeout }); return await locator._expect('to.have.class.array', { expectedText, isNot, timeout });
}, expected, options); }, expected, options);
} else { } else {
return toMatchText.call(this, 'toHaveClass', locator, 'Locator', async (isNot, timeout) => { return toMatchText.call(this, 'toHaveClass', locator, 'Locator', async (isNot, timeout) => {
const expectedText = toExpectedTextValues([expected]); const expectedText = serializeExpectedTextValues([expected]);
return await locator._expect('to.have.class', { expectedText, isNot, timeout }); return await locator._expect('to.have.class', { expectedText, isNot, timeout });
}, expected, options); }, expected, options);
} }
@ -261,7 +261,7 @@ export function toHaveCSS(
options?: { timeout?: number }, options?: { timeout?: number },
) { ) {
return toMatchText.call(this, 'toHaveCSS', locator, 'Locator', async (isNot, timeout) => { return toMatchText.call(this, 'toHaveCSS', locator, 'Locator', async (isNot, timeout) => {
const expectedText = toExpectedTextValues([expected]); const expectedText = serializeExpectedTextValues([expected]);
return await locator._expect('to.have.css', { expressionArg: name, expectedText, isNot, timeout }); return await locator._expect('to.have.css', { expressionArg: name, expectedText, isNot, timeout });
}, expected, options); }, expected, options);
} }
@ -273,7 +273,7 @@ export function toHaveId(
options?: { timeout?: number }, options?: { timeout?: number },
) { ) {
return toMatchText.call(this, 'toHaveId', locator, 'Locator', async (isNot, timeout) => { return toMatchText.call(this, 'toHaveId', locator, 'Locator', async (isNot, timeout) => {
const expectedText = toExpectedTextValues([expected]); const expectedText = serializeExpectedTextValues([expected]);
return await locator._expect('to.have.id', { expectedText, isNot, timeout }); return await locator._expect('to.have.id', { expectedText, isNot, timeout });
}, expected, options); }, expected, options);
} }
@ -299,7 +299,7 @@ export function toHaveRole(
if (!isString(expected)) if (!isString(expected))
throw new Error(`"role" argument in toHaveRole must be a string`); throw new Error(`"role" argument in toHaveRole must be a string`);
return toMatchText.call(this, 'toHaveRole', locator, 'Locator', async (isNot, timeout) => { return toMatchText.call(this, 'toHaveRole', locator, 'Locator', async (isNot, timeout) => {
const expectedText = toExpectedTextValues([expected]); const expectedText = serializeExpectedTextValues([expected]);
return await locator._expect('to.have.role', { expectedText, isNot, timeout }); return await locator._expect('to.have.role', { expectedText, isNot, timeout });
}, expected, options); }, expected, options);
} }
@ -312,12 +312,12 @@ export function toHaveText(
) { ) {
if (Array.isArray(expected)) { if (Array.isArray(expected)) {
return toEqual.call(this, 'toHaveText', locator, 'Locator', async (isNot, timeout) => { return toEqual.call(this, 'toHaveText', locator, 'Locator', async (isNot, timeout) => {
const expectedText = toExpectedTextValues(expected, { normalizeWhiteSpace: true, ignoreCase: options.ignoreCase }); const expectedText = serializeExpectedTextValues(expected, { normalizeWhiteSpace: true, ignoreCase: options.ignoreCase });
return await locator._expect('to.have.text.array', { expectedText, isNot, useInnerText: options?.useInnerText, timeout }); return await locator._expect('to.have.text.array', { expectedText, isNot, useInnerText: options?.useInnerText, timeout });
}, expected, options); }, expected, options);
} else { } else {
return toMatchText.call(this, 'toHaveText', locator, 'Locator', async (isNot, timeout) => { return toMatchText.call(this, 'toHaveText', locator, 'Locator', async (isNot, timeout) => {
const expectedText = toExpectedTextValues([expected], { normalizeWhiteSpace: true, ignoreCase: options.ignoreCase }); const expectedText = serializeExpectedTextValues([expected], { normalizeWhiteSpace: true, ignoreCase: options.ignoreCase });
return await locator._expect('to.have.text', { expectedText, isNot, useInnerText: options?.useInnerText, timeout }); return await locator._expect('to.have.text', { expectedText, isNot, useInnerText: options?.useInnerText, timeout });
}, expected, options); }, expected, options);
} }
@ -330,7 +330,7 @@ export function toHaveValue(
options?: { timeout?: number }, options?: { timeout?: number },
) { ) {
return toMatchText.call(this, 'toHaveValue', locator, 'Locator', async (isNot, timeout) => { return toMatchText.call(this, 'toHaveValue', locator, 'Locator', async (isNot, timeout) => {
const expectedText = toExpectedTextValues([expected]); const expectedText = serializeExpectedTextValues([expected]);
return await locator._expect('to.have.value', { expectedText, isNot, timeout }); return await locator._expect('to.have.value', { expectedText, isNot, timeout });
}, expected, options); }, expected, options);
} }
@ -342,7 +342,7 @@ export function toHaveValues(
options?: { timeout?: number }, options?: { timeout?: number },
) { ) {
return toEqual.call(this, 'toHaveValues', locator, 'Locator', async (isNot, timeout) => { return toEqual.call(this, 'toHaveValues', locator, 'Locator', async (isNot, timeout) => {
const expectedText = toExpectedTextValues(expected); const expectedText = serializeExpectedTextValues(expected);
return await locator._expect('to.have.values', { expectedText, isNot, timeout }); return await locator._expect('to.have.values', { expectedText, isNot, timeout });
}, expected, options); }, expected, options);
} }
@ -355,7 +355,7 @@ export function toHaveTitle(
) { ) {
const locator = page.locator(':root') as LocatorEx; const locator = page.locator(':root') as LocatorEx;
return toMatchText.call(this, 'toHaveTitle', locator, 'Locator', async (isNot, timeout) => { return toMatchText.call(this, 'toHaveTitle', locator, 'Locator', async (isNot, timeout) => {
const expectedText = toExpectedTextValues([expected], { normalizeWhiteSpace: true }); const expectedText = serializeExpectedTextValues([expected], { normalizeWhiteSpace: true });
return await locator._expect('to.have.title', { expectedText, isNot, timeout }); return await locator._expect('to.have.title', { expectedText, isNot, timeout });
}, expected, options); }, expected, options);
} }
@ -370,7 +370,7 @@ export function toHaveURL(
expected = typeof expected === 'string' ? constructURLBasedOnBaseURL(baseURL, expected) : expected; expected = typeof expected === 'string' ? constructURLBasedOnBaseURL(baseURL, expected) : expected;
const locator = page.locator(':root') as LocatorEx; const locator = page.locator(':root') as LocatorEx;
return toMatchText.call(this, 'toHaveURL', locator, 'Locator', async (isNot, timeout) => { return toMatchText.call(this, 'toHaveURL', locator, 'Locator', async (isNot, timeout) => {
const expectedText = toExpectedTextValues([expected], { ignoreCase: options?.ignoreCase }); const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase });
return await locator._expect('to.have.url', { expectedText, isNot, timeout }); return await locator._expect('to.have.url', { expectedText, isNot, timeout });
}, expected, options); }, expected, options);
} }

View file

@ -15,8 +15,6 @@
*/ */
import type { ExpectedTextValue } from '@protocol/channels';
import { isRegExp, isString } from 'playwright-core/lib/utils';
import { expectTypes, callLogText } from '../util'; import { expectTypes, callLogText } from '../util';
import { import {
printReceivedStringContainExpectedResult, printReceivedStringContainExpectedResult,
@ -95,14 +93,3 @@ export async function toMatchText(
timeout: timedOut ? timeout : undefined, timeout: timedOut ? timeout : undefined,
}; };
} }
export function toExpectedTextValues(items: (string | RegExp)[], options: { matchSubstring?: boolean, normalizeWhiteSpace?: boolean, ignoreCase?: boolean } = {}): ExpectedTextValue[] {
return items.map(i => ({
string: isString(i) ? i : undefined,
regexSource: isRegExp(i) ? i.source : undefined,
regexFlags: isRegExp(i) ? i.flags : undefined,
matchSubstring: options.matchSubstring,
ignoreCase: options.ignoreCase,
normalizeWhiteSpace: options.normalizeWhiteSpace,
}));
}

View file

@ -148,7 +148,10 @@ export class TestServerDispatcher implements TestServerInterface {
async runGlobalSetup(params: Parameters<TestServerInterface['runGlobalSetup']>[0]): ReturnType<TestServerInterface['runGlobalSetup']> { async runGlobalSetup(params: Parameters<TestServerInterface['runGlobalSetup']>[0]): ReturnType<TestServerInterface['runGlobalSetup']> {
await this.runGlobalTeardown(); await this.runGlobalTeardown();
const { config, error } = await this._loadConfig(); const overrides: ConfigCLIOverrides = {
outputDir: params.outputDir,
};
const { config, error } = await this._loadConfig(overrides);
if (!config) { if (!config) {
const { reporter, report } = await this._collectingInternalReporter(); const { reporter, report } = await this._collectingInternalReporter();
// Produce dummy config when it has an error. // Produce dummy config when it has an error.
@ -256,6 +259,7 @@ export class TestServerDispatcher implements TestServerInterface {
const overrides: ConfigCLIOverrides = { const overrides: ConfigCLIOverrides = {
repeatEach: 1, repeatEach: 1,
retries: 0, retries: 0,
outputDir: params.outputDir,
}; };
const { config, error } = await this._loadConfig(overrides); const { config, error } = await this._loadConfig(overrides);
if (!config) { if (!config) {

View file

@ -30,7 +30,7 @@ export async function detectChangedTestFiles(baseCommit: string, configDir: stri
const unknownRevision = error.output.some(line => line?.includes('unknown revision')); const unknownRevision = error.output.some(line => line?.includes('unknown revision'));
if (unknownRevision) { if (unknownRevision) {
const isShallowClone = childProcess.execSync('git rev-parse --is-shallow-repository', { encoding: 'utf-8', stdio: 'pipe' }).trim() === 'true'; const isShallowClone = childProcess.execSync('git rev-parse --is-shallow-repository', { encoding: 'utf-8', stdio: 'pipe', cwd: configDir }).trim() === 'true';
if (isShallowClone) { if (isShallowClone) {
throw new Error([ throw new Error([
`The repository is a shallow clone and does not have '${baseCommit}' available locally.`, `The repository is a shallow clone and does not have '${baseCommit}' available locally.`,

View file

@ -30,7 +30,7 @@ import type { Attachment } from './testTracing';
import type { StackFrame } from '@protocol/channels'; import type { StackFrame } from '@protocol/channels';
export interface TestStepInternal { export interface TestStepInternal {
complete(result: { error?: Error, attachments?: Attachment[] }): void; complete(result: { error?: Error | unknown, attachments?: Attachment[] }): void;
stepId: string; stepId: string;
title: string; title: string;
category: 'hook' | 'fixture' | 'test.step' | 'expect' | 'attach' | string; category: 'hook' | 'fixture' | 'test.step' | 'expect' | 'attach' | string;
@ -270,7 +270,7 @@ export class TestInfoImpl implements TestInfo {
step.endWallTime = Date.now(); step.endWallTime = Date.now();
if (result.error) { if (result.error) {
if (!(result.error as any)[stepSymbol]) if (typeof result.error === 'object' && !(result.error as any)?.[stepSymbol])
(result.error as any)[stepSymbol] = step; (result.error as any)[stepSymbol] = step;
const error = serializeError(result.error); const error = serializeError(result.error);
if (data.boxedStack) if (data.boxedStack)
@ -327,13 +327,13 @@ export class TestInfoImpl implements TestInfo {
this.status = 'interrupted'; this.status = 'interrupted';
} }
_failWithError(error: Error) { _failWithError(error: Error | unknown) {
if (this.status === 'passed' || this.status === 'skipped') if (this.status === 'passed' || this.status === 'skipped')
this.status = error instanceof TimeoutManagerError ? 'timedOut' : 'failed'; this.status = error instanceof TimeoutManagerError ? 'timedOut' : 'failed';
const serialized = serializeError(error); const serialized = serializeError(error);
const step = (error as any)[stepSymbol] as TestStepInternal | undefined; const step: TestStepInternal | undefined = typeof error === 'object' ? (error as any)?.[stepSymbol] : undefined;
if (step && step.boxedStack) if (step && step.boxedStack)
serialized.stack = `${error.name}: ${error.message}\n${stringifyStackFrames(step.boxedStack).join('\n')}`; serialized.stack = `${(error as Error).name}: ${(error as Error).message}\n${stringifyStackFrames(step.boxedStack).join('\n')}`;
this.errors.push(serialized); this.errors.push(serialized);
this._tracing.appendForError(serialized); this._tracing.appendForError(serialized);
} }

View file

@ -1825,8 +1825,10 @@ type TestDetailsAnnotation = {
description?: string; description?: string;
}; };
type TestDetailsTag = `@${string}`;
export type TestDetails = { export type TestDetails = {
tag?: string | string[]; tag?: TestDetailsTag | TestDetailsTag[];
annotation?: TestDetailsAnnotation | TestDetailsAnnotation[]; annotation?: TestDetailsAnnotation | TestDetailsAnnotation[];
} }
@ -6565,15 +6567,17 @@ type MakeMatchers<R, T, ExtendedMatchers> = {
rejects: MakeMatchers<Promise<R>, any, ExtendedMatchers>; rejects: MakeMatchers<Promise<R>, any, ExtendedMatchers>;
} & IfAny<T, AllMatchers<R, T>, SpecificMatchers<R, T> & ToUserMatcherObject<ExtendedMatchers, T>>; } & IfAny<T, AllMatchers<R, T>, SpecificMatchers<R, T> & ToUserMatcherObject<ExtendedMatchers, T>>;
type PollMatchers<R, T, ExtendedMatchers> = {
/**
* If you know how to test something, `.not` lets you test its opposite.
*/
not: PollMatchers<R, T, ExtendedMatchers>;
} & BaseMatchers<R, T> & ToUserMatcherObject<ExtendedMatchers, T>;
export type Expect<ExtendedMatchers = {}> = { export type Expect<ExtendedMatchers = {}> = {
<T = unknown>(actual: T, messageOrOptions?: string | { message?: string }): MakeMatchers<void, T, ExtendedMatchers>; <T = unknown>(actual: T, messageOrOptions?: string | { message?: string }): MakeMatchers<void, T, ExtendedMatchers>;
soft: <T = unknown>(actual: T, messageOrOptions?: string | { message?: string }) => MakeMatchers<void, T, ExtendedMatchers>; soft: <T = unknown>(actual: T, messageOrOptions?: string | { message?: string }) => MakeMatchers<void, T, ExtendedMatchers>;
poll: <T = unknown>(actual: () => T | Promise<T>, messageOrOptions?: string | { message?: string, timeout?: number, intervals?: number[] }) => BaseMatchers<Promise<void>, T> & { poll: <T = unknown>(actual: () => T | Promise<T>, messageOrOptions?: string | { message?: string, timeout?: number, intervals?: number[] }) => PollMatchers<Promise<void>, T, ExtendedMatchers>;
/**
* If you know how to test something, `.not` lets you test its opposite.
*/
not: BaseMatchers<Promise<void>, T>;
};
extend<MoreMatchers extends Record<string, (this: ExpectMatcherState, receiver: any, ...args: any[]) => MatcherReturnType | Promise<MatcherReturnType>>>(matchers: MoreMatchers): Expect<ExtendedMatchers & MoreMatchers>; extend<MoreMatchers extends Record<string, (this: ExpectMatcherState, receiver: any, ...args: any[]) => MatcherReturnType | Promise<MatcherReturnType>>>(matchers: MoreMatchers): Expect<ExtendedMatchers & MoreMatchers>;
configure: (configuration: { configure: (configuration: {
message?: string, message?: string,

View file

@ -1753,7 +1753,6 @@ export type BrowserContextRecorderSupplementEnableParams = {
device?: string, device?: string,
saveStorage?: string, saveStorage?: string,
outputFile?: string, outputFile?: string,
handleSIGINT?: boolean,
omitCallTracking?: boolean, omitCallTracking?: boolean,
}; };
export type BrowserContextRecorderSupplementEnableOptions = { export type BrowserContextRecorderSupplementEnableOptions = {
@ -1766,7 +1765,6 @@ export type BrowserContextRecorderSupplementEnableOptions = {
device?: string, device?: string,
saveStorage?: string, saveStorage?: string,
outputFile?: string, outputFile?: string,
handleSIGINT?: boolean,
omitCallTracking?: boolean, omitCallTracking?: boolean,
}; };
export type BrowserContextRecorderSupplementEnableResult = void; export type BrowserContextRecorderSupplementEnableResult = void;
@ -2939,7 +2937,6 @@ export type FrameSelectOptionParams = {
}[], }[],
force?: boolean, force?: boolean,
timeout?: number, timeout?: number,
noWaitAfter?: boolean,
}; };
export type FrameSelectOptionOptions = { export type FrameSelectOptionOptions = {
strict?: boolean, strict?: boolean,
@ -2952,7 +2949,6 @@ export type FrameSelectOptionOptions = {
}[], }[],
force?: boolean, force?: boolean,
timeout?: number, timeout?: number,
noWaitAfter?: boolean,
}; };
export type FrameSelectOptionResult = { export type FrameSelectOptionResult = {
values: string[], values: string[],
@ -3555,7 +3551,6 @@ export type ElementHandleSelectOptionParams = {
}[], }[],
force?: boolean, force?: boolean,
timeout?: number, timeout?: number,
noWaitAfter?: boolean,
}; };
export type ElementHandleSelectOptionOptions = { export type ElementHandleSelectOptionOptions = {
elements?: ElementHandleChannel[], elements?: ElementHandleChannel[],
@ -3567,7 +3562,6 @@ export type ElementHandleSelectOptionOptions = {
}[], }[],
force?: boolean, force?: boolean,
timeout?: number, timeout?: number,
noWaitAfter?: boolean,
}; };
export type ElementHandleSelectOptionResult = { export type ElementHandleSelectOptionResult = {
values: string[], values: string[],

View file

@ -1189,7 +1189,6 @@ BrowserContext:
device: string? device: string?
saveStorage: string? saveStorage: string?
outputFile: string? outputFile: string?
handleSIGINT: boolean?
omitCallTracking: boolean? omitCallTracking: boolean?
newCDPSession: newCDPSession:
@ -2185,7 +2184,6 @@ Frame:
index: number? index: number?
force: boolean? force: boolean?
timeout: number? timeout: number?
noWaitAfter: boolean?
returns: returns:
values: values:
type: array type: array
@ -2741,7 +2739,6 @@ ElementHandle:
index: number? index: number?
force: boolean? force: boolean?
timeout: number? timeout: number?
noWaitAfter: boolean?
returns: returns:
values: values:
type: array type: array

View file

@ -59,7 +59,7 @@ const RequestTab: React.FunctionComponent<{
React.useEffect(() => { React.useEffect(() => {
const readResources = async () => { const readResources = async () => {
if (resource.request.postData) { if (resource.request.postData) {
const requestContentTypeHeader = resource.request.headers.find(q => q.name === 'Content-Type'); const requestContentTypeHeader = resource.request.headers.find(q => q.name.toLowerCase() === 'content-type');
const requestContentType = requestContentTypeHeader ? requestContentTypeHeader.value : ''; const requestContentType = requestContentTypeHeader ? requestContentTypeHeader.value : '';
if (resource.request.postData._sha1) { if (resource.request.postData._sha1) {
const response = await fetch(`sha1/${resource.request.postData._sha1}`); const response = await fetch(`sha1/${resource.request.postData._sha1}`);

View file

@ -205,12 +205,14 @@ export const UIModeView: React.FC<{}> = ({
interceptStdio: true, interceptStdio: true,
watchTestDirs: true watchTestDirs: true
}); });
const { status, report } = await testServerConnection.runGlobalSetup({}); const { status, report } = await testServerConnection.runGlobalSetup({
outputDir: queryParams.outputDir,
});
teleSuiteUpdater.processGlobalReport(report); teleSuiteUpdater.processGlobalReport(report);
if (status !== 'passed') if (status !== 'passed')
return; return;
const result = await testServerConnection.listTests({ projects: queryParams.projects, locations: queryParams.args, grep: queryParams.grep, grepInvert: queryParams.grepInvert }); const result = await testServerConnection.listTests({ projects: queryParams.projects, locations: queryParams.args, grep: queryParams.grep, grepInvert: queryParams.grepInvert, outputDir: queryParams.outputDir });
teleSuiteUpdater.processListReport(result.report); teleSuiteUpdater.processListReport(result.report);
testServerConnection.onReport(params => { testServerConnection.onReport(params => {
@ -333,7 +335,7 @@ export const UIModeView: React.FC<{}> = ({
commandQueue.current = commandQueue.current.then(async () => { commandQueue.current = commandQueue.current.then(async () => {
setIsLoading(true); setIsLoading(true);
try { try {
const result = await testServerConnection.listTests({ projects: queryParams.projects, locations: queryParams.args, grep: queryParams.grep, grepInvert: queryParams.grepInvert }); const result = await testServerConnection.listTests({ projects: queryParams.projects, locations: queryParams.args, grep: queryParams.grep, grepInvert: queryParams.grepInvert, outputDir: queryParams.outputDir });
teleSuiteUpdater.processListReport(result.report); teleSuiteUpdater.processListReport(result.report);
} catch (e) { } catch (e) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console

View file

@ -83,7 +83,7 @@ export const Workbench: React.FunctionComponent<{
setRevealedStack(action?.stack); setRevealedStack(action?.stack);
}, [setSelectedActionImpl, setRevealedStack]); }, [setSelectedActionImpl, setRevealedStack]);
const sources = React.useMemo(() => model?.sources || new Map(), [model]); const sources = React.useMemo(() => model?.sources || new Map<string, modelUtil.SourceModel>(), [model]);
React.useEffect(() => { React.useEffect(() => {
setSelectedTime(undefined); setSelectedTime(undefined);
@ -179,9 +179,17 @@ export const Workbench: React.FunctionComponent<{
selectPropertiesTab('source'); selectPropertiesTab('source');
}} /> }} />
}; };
// Fallback location w/o action stands for file / test.
// Render error count on Source tab for that case.
let fallbackSourceErrorCount: number | undefined = undefined;
if (!selectedAction && fallbackLocation)
fallbackSourceErrorCount = fallbackLocation.source?.errors.length;
const sourceTab: TabbedPaneTabModel = { const sourceTab: TabbedPaneTabModel = {
id: 'source', id: 'source',
title: 'Source', title: 'Source',
errorCount: fallbackSourceErrorCount,
render: () => <SourceTab render: () => <SourceTab
stack={revealedStack} stack={revealedStack}
sources={sources} sources={sources}

View file

@ -1,12 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Detect Touch Test</title>
<script src='modernizr.js'></script>
</head>
<body style="font-size:30vmin">
<script>
document.body.textContent = Modernizr.touchevents ? 'YES' : 'NO';
</script>
</body>
</html>

View file

@ -1,33 +0,0 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// one.ts
var one_exports = {};
__export(one_exports, {
default: () => one_default
});
module.exports = __toCommonJS(one_exports);
// two.ts
var value = 42;
// one.ts
function one_default(arg) {
window.injected = arg ?? value;
}

View file

@ -1,21 +0,0 @@
<script src='modernizr.js'></script>
<body></body>
<script>
const report = {};
for (const name in Modernizr) {
if (name.startsWith('_'))
continue;
if (['on', 'testAllProps', 'testProp', 'addTest', 'prefixed'].includes(name))
continue;
let value = Modernizr[name];
report[name] = value;
}
report['devicemotion2'] = 'ondevicemotion' in window;
report['deviceorientation2'] = 'orientation' in window;
report['deviceorientation3'] = 'ondeviceorientation' in window;
document.body.style.whiteSpace = 'pre';
document.body.textContent = JSON.stringify(report, undefined, 4);
window.report = JSON.parse(document.body.textContent);
</script>

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,18 @@
# Playwright Modernizr tests
## Rolling Modernizr
- [modernizr.com](modernizr.com) isn't getting updated anymore, see [here](https://github.com/Modernizr/Modernizr/issues/2490) and [here](https://github.com/Modernizr/Modernizr/commit/db96bdaff995a1d4abccb0dc69c77db7b47ad614). It only contains version 3.6.
- This is why we build it from source ourselves, using `roll.sh` (they recommend it).
## Updating expectations
1. `npx http-server .`
1. Navigate to `http://127.0.0.1:8080/tests/assets/modernizr/index.html`
Do this with:
- Safari Technology Preview
- Apple iPhone
Make sure to change the updated file's name.

View file

@ -0,0 +1,26 @@
<script src='modernizr.js'></script>
<body></body>
<script>
function serialize(value) {
if (typeof value !== 'object')
return value;
const copy = {};
for (const key in value) {
if (typeof value[key] === 'function')
continue;
if (key.startsWith('_'))
continue;
copy[key] = serialize(value[key]);
}
return copy;
}
const report = serialize(Modernizr);
report['devicemotion2'] = 'ondevicemotion' in window;
report['deviceorientation2'] = 'orientation' in window;
report['deviceorientation3'] = 'ondeviceorientation' in window;
document.body.style.whiteSpace = 'pre';
document.body.textContent = JSON.stringify(report, undefined, 2);
window.report = JSON.parse(document.body.textContent);
</script>

View file

@ -11,6 +11,249 @@
"required": true, "required": true,
"step": true "step": true
}, },
"adownload": true,
"aping": true,
"areaping": true,
"ambientlight": false,
"applicationcache": false,
"audio": {
"ogg": "",
"mp3": "probably",
"opus": "probably",
"wav": "probably",
"m4a": "maybe"
},
"audioloop": true,
"webaudio": true,
"batteryapi": false,
"battery-api": false,
"lowbattery": false,
"blobconstructor": true,
"blob-constructor": true,
"broadcastchannel": true,
"canvas": true,
"canvasblending": true,
"todataurljpeg": true,
"todataurlpng": true,
"todataurlwebp": false,
"canvaswinding": true,
"canvastext": true,
"clipboard": {
"read": true,
"readtext": true,
"write": true,
"writetext": true
},
"contenteditable": true,
"contextmenu": false,
"cors": true,
"crypto": true,
"getrandomvalues": true,
"cssall": true,
"cssanimations": true,
"appearance": true,
"aspectratio": true,
"backdropfilter": true,
"backgroundblendmode": true,
"backgroundcliptext": true,
"bgpositionshorthand": true,
"bgpositionxy": true,
"bgrepeatround": true,
"bgrepeatspace": true,
"backgroundsize": true,
"bgsizecover": true,
"borderimage": true,
"borderradius": true,
"boxdecorationbreak": true,
"boxshadow": true,
"boxsizing": true,
"csscalc": true,
"checked": true,
"csschunit": true,
"csscolumns": {
"width": true,
"span": true,
"fill": true,
"gap": true,
"rule": true,
"rulecolor": true,
"rulestyle": true,
"rulewidth": true,
"breakbefore": true,
"breakafter": true,
"breakinside": true
},
"cssgridlegacy": false,
"cssgrid": true,
"cubicbezierrange": true,
"customproperties": true,
"displayrunin": false,
"display-runin": false,
"displaytable": true,
"display-table": true,
"ellipsis": true,
"cssescape": true,
"cssexunit": true,
"supports": true,
"cssfilters": true,
"flexbox": true,
"flexboxlegacy": true,
"flexboxtweener": false,
"flexgap": true,
"flexwrap": true,
"focusvisible": true,
"focuswithin": true,
"fontdisplay": true,
"fontface": true,
"generatedcontent": true,
"cssgradients": true,
"hairline": true,
"hsla": true,
"cssinvalid": true,
"lastchild": true,
"cssmask": true,
"mediaqueries": true,
"multiplebgs": true,
"nthchild": true,
"objectfit": true,
"object-fit": true,
"opacity": true,
"overflowscrolling": true,
"csspointerevents": true,
"csspositionsticky": true,
"csspseudoanimations": true,
"csstransitions": true,
"csspseudotransitions": true,
"cssreflections": true,
"regions": false,
"cssremunit": true,
"cssresize": true,
"rgba": true,
"cssscrollbar": false,
"scrollsnappoints": true,
"shapes": true,
"siblinggeneral": true,
"subpixelfont": true,
"target": true,
"textalignlast": true,
"textdecoration": {
"line": true,
"style": true,
"color": true,
"skip": true,
"skipink": true
},
"textshadow": true,
"csstransforms": true,
"csstransforms3d": true,
"csstransformslevel2": true,
"preserve3d": true,
"userselect": true,
"cssvalid": true,
"variablefonts": true,
"cssvhunit": true,
"cssvmaxunit": false,
"cssvminunit": true,
"cssvwunit": true,
"willchange": true,
"wrapflow": false,
"customelements": true,
"customprotocolhandler": false,
"dart": false,
"dataview": true,
"classlist": true,
"createelementattrs": false,
"createelement-attrs": false,
"dataset": true,
"documentfragment": true,
"hidden": true,
"intersectionobserver": true,
"microdata": false,
"mutationobserver": true,
"passiveeventlisteners": true,
"shadowroot": true,
"shadowrootlegacy": false,
"bdi": true,
"details": true,
"outputelem": true,
"picture": true,
"progressbar": true,
"meter": true,
"ruby": true,
"template": true,
"time": false,
"texttrackapi": true,
"track": true,
"unknownelements": true,
"emoji": true,
"es5array": true,
"es5date": true,
"es5function": true,
"es5object": true,
"strictmode": true,
"es5string": true,
"json": true,
"es5syntax": true,
"es5undefined": true,
"es5": true,
"es6array": true,
"arrow": true,
"es6class": true,
"es6collections": true,
"generators": true,
"es6math": true,
"es6number": true,
"es6object": true,
"promises": true,
"restparameters": true,
"spreadarray": true,
"stringtemplate": true,
"es6string": true,
"es6symbol": true,
"es7array": true,
"restdestructuringarray": true,
"restdestructuringobject": true,
"spreadobject": true,
"es8object": true,
"customevent": true,
"devicemotion": true,
"deviceorientation": true,
"eventlistener": true,
"forcetouch": false,
"hashchange": true,
"oninput": true,
"pointerevents": true,
"proximity": false,
"filereader": true,
"filesystem": false,
"flash": false,
"fullscreen": false,
"gamepads": true,
"geolocation": true,
"hiddenscroll": true,
"history": true,
"htmlimports": false,
"ie8compat": false,
"sandbox": true,
"seamless": false,
"srcdoc": true,
"imgcrossorigin": true,
"lazyloading": true,
"sizes": true,
"srcset": true,
"capture": true,
"fileinput": true,
"fileinputdirectory": true,
"inputformaction": true,
"input-formaction": true,
"formattribute": true,
"inputformenctype": true,
"input-formenctype": true,
"inputformmethod": true,
"inputformnovalidate": true,
"input-formnovalidate": true,
"inputformtarget": true,
"input-formtarget": true,
"inputtypes": { "inputtypes": {
"search": true, "search": true,
"tel": true, "tel": true,
@ -26,278 +269,140 @@
"range": true, "range": true,
"color": true "color": true
}, },
"htmlimports": false, "formvalidation": true,
"history": true, "localizednumber": false,
"ie8compat": false, "inputsearchevent": false,
"applicationcache": false, "placeholder": true,
"blobconstructor": true, "requestautocomplete": false,
"blob-constructor": true, "intl": true,
"cookies": true, "ligatures": true,
"cors": true, "olreversed": true,
"customelements": true, "mathml": true,
"customprotocolhandler": false, "mediasource": false,
"customevent": true, "hovermq": false,
"dataview": true, "pointermq": true,
"eventlistener": true,
"geolocation": true,
"json": true,
"messagechannel": true, "messagechannel": true,
"notification": false,
"postmessage": true,
"queryselector": true,
"serviceworker": true,
"svg": true,
"templatestrings": true,
"typedarrays": true,
"websockets": true,
"xdomainrequest": false,
"webaudio": true,
"cssescape": true,
"focuswithin": true,
"supports": true,
"target": true,
"microdata": false,
"mutationobserver": true,
"passiveeventlisteners": true,
"picture": true,
"es5array": true,
"es5date": true,
"es5function": true,
"beacon": true, "beacon": true,
"effectivetype": false,
"lowbandwidth": false, "lowbandwidth": false,
"eventsource": true, "eventsource": true,
"fetch": true, "fetch": true,
"xhrresponsetype": true,
"xhr2": true,
"speechsynthesis": true,
"localstorage": true,
"sessionstorage": true,
"websqldatabase": true,
"es5object": true,
"svgfilters": true,
"strictmode": true,
"es5string": true,
"es5syntax": true,
"es5undefined": true,
"es5": true,
"es6array": true,
"arrow": true,
"es6collections": true,
"generators": true,
"es6math": true,
"es6number": true,
"es6object": true,
"promises": true,
"es6string": true,
"devicemotion": true,
"devicemotion2": true,
"deviceorientation": true,
"deviceorientation2": true,
"deviceorientation3": true,
"filereader": true,
"urlparser": true,
"urlsearchparams": true,
"framed": false,
"webworkers": true,
"contextmenu": false,
"cssall": true,
"willchange": true,
"classlist": true,
"documentfragment": true,
"contains": false,
"audio": true,
"canvas": true,
"canvastext": true,
"contenteditable": true,
"emoji": false,
"olreversed": true,
"userdata": false,
"video": true,
"vml": false,
"webanimations": true,
"webgl": true,
"adownload": true,
"audioloop": true,
"canvasblending": true,
"todataurljpeg": true,
"todataurlpng": true,
"todataurlwebp": false,
"canvaswinding": true,
"bgpositionshorthand": true,
"multiplebgs": true,
"csspointerevents": true,
"cssremunit": true,
"rgba": true,
"preserve3d": true,
"createelementattrs": false,
"createelement-attrs": false,
"dataset": true,
"hidden": true,
"outputelem": true,
"progressbar": true,
"meter": true,
"ruby": true,
"template": true,
"srcset": true,
"time": false,
"texttrackapi": true,
"track": true,
"unknownelements": true,
"inputformaction": true,
"input-formaction": true,
"inputformenctype": true,
"input-formenctype": true,
"inputformmethod": true,
"inputformtarget": false,
"input-formtarget": false,
"scriptasync": true,
"scriptdefer": true,
"stylescoped": false,
"capture": true,
"fileinput": true,
"formattribute": true,
"placeholder": true,
"sandbox": true,
"inlinesvg": true,
"textareamaxlength": true,
"videocrossorigin": true,
"webglextensions": true,
"seamless": false,
"srcdoc": true,
"imgcrossorigin": true,
"hashchange": true,
"inputsearchevent": false,
"ambientlight": false,
"datalistelem": true,
"videoloop": true,
"csscalc": true,
"cubicbezierrange": true,
"cssgradients": true,
"opacity": true,
"csspositionsticky": true,
"csschunit": true,
"cssexunit": true,
"hsla": true,
"videopreload": true,
"getusermedia": true,
"websocketsbinary": true,
"atobbtoa": true,
"atob-btoa": true,
"sharedworkers": true,
"bdi": true,
"xhrresponsetypearraybuffer": true, "xhrresponsetypearraybuffer": true,
"xhrresponsetypeblob": true, "xhrresponsetypeblob": true,
"xhrresponsetypedocument": true, "xhrresponsetypedocument": true,
"xhrresponsetypejson": true, "xhrresponsetypejson": true,
"xhrresponsetypetext": true, "xhrresponsetypetext": true,
"svgclippaths": true, "xhrresponsetype": true,
"svgforeignobject": true, "xhr2": true,
"smil": true, "notification": false,
"hiddenscroll": true,
"mathml": true,
"touchevents": true,
"unicoderange": true,
"unicode": true,
"checked": true,
"displaytable": true,
"display-table": true,
"fontface": true,
"generatedcontent": true,
"hairline": true,
"cssinvalid": true,
"lastchild": true,
"nthchild": true,
"cssscrollbar": false,
"siblinggeneral": true,
"subpixelfont": true,
"cssvalid": true,
"cssvhunit": false,
"cssvmaxunit": false,
"cssvminunit": true,
"cssvwunit": true,
"details": true,
"oninput": true,
"formvalidation": true,
"localizednumber": false,
"mediaqueries": true,
"flash": false,
"proximity": false,
"sizes": true,
"hovermq": false,
"pointermq": true,
"svgasimg": true,
"pointerevents": true,
"fileinputdirectory": true,
"textshadow": true,
"batteryapi": false,
"battery-api": false,
"crypto": true,
"dart": false,
"forcetouch": false,
"fullscreen": false,
"gamepads": true,
"intl": true,
"pagevisibility": true, "pagevisibility": true,
"performance": true, "performance": true,
"pointerlock": false, "pointerlock": false,
"quotamanagement": false, "postmessage": {
"structuredclones": true
},
"proxy": true,
"queryselector": true,
"prefetch": false,
"requestanimationframe": true, "requestanimationframe": true,
"raf": true, "raf": true,
"vibrate": false, "scriptasync": true,
"webintents": false, "scriptdefer": true,
"lowbattery": false, "scrolltooptions": true,
"getrandomvalues": true, "serviceworker": true,
"backgroundblendmode": true,
"objectfit": true,
"object-fit": true,
"regions": false,
"wrapflow": false,
"speechrecognition": true, "speechrecognition": true,
"filesystem": false, "speechsynthesis": true,
"requestautocomplete": false, "cookies": true,
"localstorage": true,
"quotamanagement": false,
"sessionstorage": true,
"userdata": false,
"websqldatabase": true,
"stylescoped": false,
"svg": true,
"svgasimg": true,
"svgclippaths": true,
"svgfilters": true,
"svgforeignobject": true,
"inlinesvg": true,
"smil": true,
"textareamaxlength": true,
"textencoder": true,
"textdecoder": true,
"typedarrays": true,
"unicoderange": true,
"bloburls": true, "bloburls": true,
"transferables": true, "urlparser": true,
"urlsearchparams": true,
"vibrate": false,
"video": {
"ogg": "",
"h264": "probably",
"h265": "",
"webm": "probably",
"vp9": "probably",
"hls": "probably",
"av1": ""
},
"videocrossorigin": true,
"videoloop": true,
"videopreload": true,
"vml": false,
"webintents": false,
"webanimations": true,
"publickeycredential": true,
"webgl": true,
"webglextensions": {
"ANGLE_instanced_arrays": true,
"EXT_blend_minmax": true,
"EXT_clip_control": true,
"EXT_color_buffer_half_float": true,
"EXT_depth_clamp": true,
"EXT_frag_depth": true,
"EXT_polygon_offset_clamp": true,
"EXT_shader_texture_lod": true,
"EXT_texture_filter_anisotropic": true,
"EXT_sRGB": true,
"KHR_parallel_shader_compile": true,
"OES_element_index_uint": true,
"OES_fbo_render_mipmap": true,
"OES_standard_derivatives": true,
"OES_texture_float": true,
"OES_texture_half_float": true,
"OES_texture_half_float_linear": true,
"OES_vertex_array_object": true,
"WEBGL_color_buffer_float": true,
"WEBGL_compressed_texture_astc": true,
"WEBGL_compressed_texture_etc": true,
"WEBGL_compressed_texture_etc1": true,
"WEBGL_compressed_texture_pvrtc": true,
"WEBKIT_WEBGL_compressed_texture_pvrtc": true,
"WEBGL_debug_renderer_info": true,
"WEBGL_debug_shaders": true,
"WEBGL_depth_texture": true,
"WEBGL_draw_buffers": true,
"WEBGL_lose_context": true,
"WEBGL_multi_draw": true,
"WEBGL_polygon_mode": true
},
"peerconnection": true, "peerconnection": true,
"datachannel": false, "datachannel": true,
"getusermedia": true,
"mediastream": true,
"websockets": true,
"websocketsbinary": true,
"atobbtoa": true,
"atob-btoa": true,
"framed": false,
"matchmedia": true, "matchmedia": true,
"ligatures": true, "pushmanager": false,
"cssanimations": true, "resizeobserver": true,
"csspseudoanimations": true, "workertypeoption": true,
"appearance": true, "sharedworkers": true,
"backdropfilter": true, "webworkers": true,
"backgroundcliptext": true, "transferables": true,
"bgpositionxy": true, "xdomainrequest": false,
"bgrepeatround": true, "devicemotion2": true,
"bgrepeatspace": true, "deviceorientation2": true,
"backgroundsize": true, "deviceorientation3": true
"bgsizecover": true,
"borderimage": true,
"borderradius": true,
"boxshadow": true,
"boxsizing": true,
"csscolumns": true,
"cssgridlegacy": false,
"cssgrid": true,
"displayrunin": false,
"display-runin": false,
"ellipsis": true,
"cssfilters": true,
"flexbox": true,
"flexboxlegacy": true,
"flexboxtweener": false,
"flexwrap": true,
"cssmask": true,
"overflowscrolling": true,
"cssreflections": true,
"cssresize": true,
"scrollsnappoints": true,
"shapes": true,
"textalignlast": true,
"csstransforms": true,
"csstransforms3d": true,
"csstransformslevel2": true,
"csstransitions": true,
"csspseudotransitions": true,
"userselect": true,
"variablefonts": true
} }

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,18 @@
#!/usr/bin/env bash
MODERNIZR_VERSION="44fa7b07c367a1814e8699e3a2f15c53fbe32df7"
cd "$(dirname "$0")"
rm -rf Modernizr
git clone https://github.com/Modernizr/Modernizr
cd Modernizr
git checkout $MODERNIZR_VERSION
npm ci
# Modernizr minifier is not working, hence we minify with ESBuild.
./bin/modernizr --config lib/config-all.json
npx esbuild --bundle modernizr.js --minify --outfile=../modernizr.js
cd ..
rm -rf Modernizr

View file

@ -11,6 +11,249 @@
"required": true, "required": true,
"step": true "step": true
}, },
"adownload": true,
"aping": true,
"areaping": true,
"ambientlight": false,
"applicationcache": false,
"audio": {
"ogg": "",
"mp3": "probably",
"opus": "probably",
"wav": "probably",
"m4a": "maybe"
},
"audioloop": true,
"webaudio": true,
"batteryapi": false,
"battery-api": false,
"lowbattery": false,
"blobconstructor": true,
"blob-constructor": true,
"broadcastchannel": true,
"canvas": true,
"canvasblending": true,
"todataurljpeg": true,
"todataurlpng": true,
"todataurlwebp": false,
"canvaswinding": true,
"canvastext": true,
"clipboard": {
"read": true,
"readtext": true,
"write": true,
"writetext": true
},
"contenteditable": true,
"contextmenu": false,
"cors": true,
"crypto": true,
"getrandomvalues": true,
"cssall": true,
"cssanimations": true,
"appearance": true,
"aspectratio": true,
"backdropfilter": true,
"backgroundblendmode": true,
"backgroundcliptext": true,
"bgpositionshorthand": true,
"bgpositionxy": true,
"bgrepeatround": true,
"bgrepeatspace": true,
"backgroundsize": true,
"bgsizecover": true,
"borderimage": true,
"borderradius": true,
"boxdecorationbreak": true,
"boxshadow": true,
"boxsizing": true,
"csscalc": true,
"checked": true,
"csschunit": true,
"csscolumns": {
"width": true,
"span": true,
"fill": true,
"gap": true,
"rule": true,
"rulecolor": true,
"rulestyle": true,
"rulewidth": true,
"breakbefore": true,
"breakafter": true,
"breakinside": true
},
"cssgridlegacy": false,
"cssgrid": true,
"cubicbezierrange": true,
"customproperties": true,
"displayrunin": false,
"display-runin": false,
"displaytable": true,
"display-table": true,
"ellipsis": true,
"cssescape": true,
"cssexunit": true,
"supports": true,
"cssfilters": true,
"flexbox": true,
"flexboxlegacy": true,
"flexboxtweener": false,
"flexgap": true,
"flexwrap": true,
"focusvisible": true,
"focuswithin": true,
"fontdisplay": true,
"fontface": true,
"generatedcontent": true,
"cssgradients": true,
"hairline": true,
"hsla": true,
"cssinvalid": true,
"lastchild": true,
"cssmask": true,
"mediaqueries": true,
"multiplebgs": true,
"nthchild": true,
"objectfit": true,
"object-fit": true,
"opacity": true,
"overflowscrolling": false,
"csspointerevents": true,
"csspositionsticky": true,
"csspseudoanimations": true,
"csstransitions": true,
"csspseudotransitions": true,
"cssreflections": true,
"regions": false,
"cssremunit": true,
"cssresize": true,
"rgba": true,
"cssscrollbar": true,
"scrollsnappoints": true,
"shapes": true,
"siblinggeneral": true,
"subpixelfont": true,
"target": true,
"textalignlast": true,
"textdecoration": {
"line": true,
"style": true,
"color": true,
"skip": true,
"skipink": true
},
"textshadow": true,
"csstransforms": true,
"csstransforms3d": true,
"csstransformslevel2": true,
"preserve3d": true,
"userselect": true,
"cssvalid": true,
"variablefonts": true,
"cssvhunit": true,
"cssvmaxunit": true,
"cssvminunit": true,
"cssvwunit": true,
"willchange": true,
"wrapflow": false,
"customelements": true,
"customprotocolhandler": false,
"dart": false,
"dataview": true,
"classlist": true,
"createelementattrs": false,
"createelement-attrs": false,
"dataset": true,
"documentfragment": true,
"hidden": true,
"intersectionobserver": true,
"microdata": false,
"mutationobserver": true,
"passiveeventlisteners": true,
"shadowroot": true,
"shadowrootlegacy": false,
"bdi": true,
"details": true,
"outputelem": true,
"picture": true,
"progressbar": true,
"meter": true,
"ruby": true,
"template": true,
"time": false,
"texttrackapi": true,
"track": true,
"unknownelements": true,
"emoji": true,
"es5array": true,
"es5date": true,
"es5function": true,
"es5object": true,
"strictmode": true,
"es5string": true,
"json": true,
"es5syntax": true,
"es5undefined": true,
"es5": true,
"es6array": true,
"arrow": true,
"es6class": true,
"es6collections": true,
"generators": true,
"es6math": true,
"es6number": true,
"es6object": true,
"promises": true,
"restparameters": true,
"spreadarray": true,
"stringtemplate": true,
"es6string": true,
"es6symbol": true,
"es7array": true,
"restdestructuringarray": true,
"restdestructuringobject": true,
"spreadobject": true,
"es8object": true,
"customevent": true,
"devicemotion": true,
"deviceorientation": true,
"eventlistener": true,
"forcetouch": false,
"hashchange": true,
"oninput": true,
"pointerevents": true,
"proximity": false,
"filereader": true,
"filesystem": false,
"flash": false,
"fullscreen": true,
"gamepads": true,
"geolocation": true,
"hiddenscroll": true,
"history": true,
"htmlimports": false,
"ie8compat": false,
"sandbox": true,
"seamless": false,
"srcdoc": true,
"imgcrossorigin": true,
"lazyloading": true,
"sizes": true,
"srcset": true,
"capture": false,
"fileinput": true,
"fileinputdirectory": true,
"inputformaction": true,
"input-formaction": true,
"formattribute": true,
"inputformenctype": true,
"input-formenctype": true,
"inputformmethod": true,
"inputformnovalidate": true,
"input-formnovalidate": true,
"inputformtarget": true,
"input-formtarget": true,
"inputtypes": { "inputtypes": {
"search": true, "search": true,
"tel": true, "tel": true,
@ -26,278 +269,148 @@
"range": true, "range": true,
"color": true "color": true
}, },
"htmlimports": false, "formvalidation": true,
"history": true, "localizednumber": false,
"ie8compat": false, "inputsearchevent": false,
"applicationcache": false, "placeholder": true,
"blobconstructor": true, "requestautocomplete": false,
"blob-constructor": true, "intl": true,
"cookies": true, "ligatures": true,
"cors": true, "olreversed": true,
"customelements": true, "mathml": true,
"customprotocolhandler": false, "mediasource": true,
"customevent": true, "hovermq": true,
"dataview": true, "pointermq": true,
"eventlistener": true,
"geolocation": true,
"json": true,
"messagechannel": true, "messagechannel": true,
"notification": true,
"postmessage": true,
"queryselector": true,
"serviceworker": true,
"svg": true,
"templatestrings": true,
"typedarrays": true,
"websockets": true,
"xdomainrequest": false,
"webaudio": true,
"cssescape": true,
"focuswithin": true,
"supports": true,
"target": true,
"microdata": false,
"mutationobserver": true,
"passiveeventlisteners": true,
"picture": true,
"es5array": true,
"es5date": true,
"es5function": true,
"beacon": true, "beacon": true,
"effectivetype": false,
"lowbandwidth": false, "lowbandwidth": false,
"eventsource": true, "eventsource": true,
"fetch": true, "fetch": true,
"xhrresponsetype": true,
"xhr2": true,
"speechsynthesis": true,
"localstorage": true,
"sessionstorage": true,
"websqldatabase": true,
"es5object": true,
"svgfilters": true,
"strictmode": true,
"es5string": true,
"es5syntax": true,
"es5undefined": true,
"es5": true,
"es6array": true,
"arrow": true,
"es6collections": true,
"generators": true,
"es6math": true,
"es6number": true,
"es6object": true,
"promises": true,
"es6string": true,
"devicemotion": false,
"devicemotion2": false,
"deviceorientation": false,
"deviceorientation2": false,
"deviceorientation3": false,
"filereader": true,
"urlparser": true,
"urlsearchparams": true,
"framed": false,
"webworkers": true,
"contextmenu": false,
"cssall": true,
"willchange": true,
"classlist": true,
"documentfragment": true,
"contains": false,
"audio": true,
"canvas": true,
"canvastext": true,
"contenteditable": true,
"emoji": true,
"olreversed": true,
"userdata": false,
"video": true,
"vml": false,
"webanimations": true,
"webgl": true,
"adownload": true,
"audioloop": true,
"canvasblending": true,
"todataurljpeg": true,
"todataurlpng": true,
"todataurlwebp": false,
"canvaswinding": true,
"bgpositionshorthand": true,
"multiplebgs": true,
"csspointerevents": true,
"cssremunit": true,
"rgba": true,
"preserve3d": true,
"createelementattrs": false,
"createelement-attrs": false,
"dataset": true,
"hidden": true,
"outputelem": true,
"progressbar": true,
"meter": true,
"ruby": true,
"template": true,
"srcset": true,
"time": false,
"texttrackapi": true,
"track": true,
"unknownelements": true,
"inputformaction": true,
"input-formaction": true,
"inputformenctype": true,
"input-formenctype": true,
"inputformmethod": true,
"inputformtarget": false,
"input-formtarget": false,
"scriptasync": true,
"scriptdefer": true,
"stylescoped": false,
"capture": false,
"fileinput": true,
"formattribute": true,
"placeholder": true,
"sandbox": true,
"inlinesvg": true,
"textareamaxlength": true,
"videocrossorigin": true,
"webglextensions": true,
"seamless": false,
"srcdoc": true,
"imgcrossorigin": true,
"hashchange": true,
"inputsearchevent": false,
"ambientlight": false,
"datalistelem": true,
"videoloop": true,
"csscalc": true,
"cubicbezierrange": true,
"cssgradients": true,
"opacity": true,
"csspositionsticky": true,
"csschunit": true,
"cssexunit": true,
"hsla": true,
"videopreload": true,
"getusermedia": true,
"websocketsbinary": true,
"atobbtoa": true,
"atob-btoa": true,
"sharedworkers": true,
"bdi": true,
"xhrresponsetypearraybuffer": true, "xhrresponsetypearraybuffer": true,
"xhrresponsetypeblob": true, "xhrresponsetypeblob": true,
"xhrresponsetypedocument": true, "xhrresponsetypedocument": true,
"xhrresponsetypejson": true, "xhrresponsetypejson": true,
"xhrresponsetypetext": true, "xhrresponsetypetext": true,
"svgclippaths": true, "xhrresponsetype": true,
"svgforeignobject": true, "xhr2": true,
"smil": true, "notification": true,
"hiddenscroll": true,
"mathml": true,
"touchevents": false,
"unicoderange": true,
"unicode": true,
"checked": true,
"displaytable": true,
"display-table": true,
"fontface": true,
"generatedcontent": true,
"hairline": true,
"cssinvalid": true,
"lastchild": true,
"nthchild": true,
"cssscrollbar": true,
"siblinggeneral": true,
"subpixelfont": true,
"cssvalid": true,
"cssvhunit": true,
"cssvmaxunit": true,
"cssvminunit": true,
"cssvwunit": true,
"details": true,
"oninput": true,
"formvalidation": true,
"localizednumber": false,
"mediaqueries": true,
"flash": false,
"proximity": false,
"sizes": true,
"hovermq": true,
"pointermq": true,
"svgasimg": true,
"pointerevents": true,
"fileinputdirectory": true,
"textshadow": true,
"batteryapi": false,
"battery-api": false,
"crypto": true,
"dart": false,
"forcetouch": false,
"fullscreen": true,
"gamepads": true,
"intl": true,
"pagevisibility": true, "pagevisibility": true,
"performance": true, "performance": true,
"pointerlock": true, "pointerlock": true,
"quotamanagement": false, "postmessage": {
"structuredclones": true
},
"proxy": true,
"queryselector": true,
"prefetch": false,
"requestanimationframe": true, "requestanimationframe": true,
"raf": true, "raf": true,
"vibrate": false, "scriptasync": true,
"webintents": false, "scriptdefer": true,
"lowbattery": false, "scrolltooptions": false,
"getrandomvalues": true, "serviceworker": true,
"backgroundblendmode": true,
"objectfit": true,
"object-fit": true,
"regions": false,
"wrapflow": false,
"speechrecognition": true, "speechrecognition": true,
"filesystem": false, "speechsynthesis": true,
"requestautocomplete": false, "cookies": true,
"localstorage": true,
"quotamanagement": false,
"sessionstorage": true,
"userdata": false,
"websqldatabase": true,
"stylescoped": false,
"svg": true,
"svgasimg": true,
"svgclippaths": true,
"svgfilters": true,
"svgforeignobject": true,
"inlinesvg": true,
"smil": true,
"textareamaxlength": true,
"textencoder": true,
"textdecoder": true,
"typedarrays": true,
"unicoderange": true,
"bloburls": true, "bloburls": true,
"transferables": true, "urlparser": true,
"urlsearchparams": true,
"vibrate": false,
"video": {
"ogg": "",
"h264": "probably",
"h265": "",
"webm": "probably",
"vp9": "probably",
"hls": "probably",
"av1": ""
},
"videocrossorigin": true,
"videoloop": true,
"videopreload": true,
"vml": false,
"webintents": false,
"webanimations": true,
"publickeycredential": true,
"webgl": true,
"webglextensions": {
"ANGLE_instanced_arrays": true,
"EXT_blend_minmax": true,
"EXT_clip_control": true,
"EXT_color_buffer_half_float": true,
"EXT_depth_clamp": true,
"EXT_float_blend": true,
"EXT_frag_depth": true,
"EXT_polygon_offset_clamp": true,
"EXT_shader_texture_lod": true,
"EXT_texture_compression_bptc": true,
"EXT_texture_compression_rgtc": true,
"EXT_texture_filter_anisotropic": true,
"EXT_texture_mirror_clamp_to_edge": true,
"EXT_sRGB": true,
"KHR_parallel_shader_compile": true,
"OES_element_index_uint": true,
"OES_fbo_render_mipmap": true,
"OES_standard_derivatives": true,
"OES_texture_float": true,
"OES_texture_float_linear": true,
"OES_texture_half_float": true,
"OES_texture_half_float_linear": true,
"OES_vertex_array_object": true,
"WEBGL_blend_func_extended": true,
"WEBGL_color_buffer_float": true,
"WEBGL_compressed_texture_astc": true,
"WEBGL_compressed_texture_etc": true,
"WEBGL_compressed_texture_etc1": true,
"WEBGL_compressed_texture_pvrtc": true,
"WEBKIT_WEBGL_compressed_texture_pvrtc": true,
"WEBGL_compressed_texture_s3tc": true,
"WEBGL_compressed_texture_s3tc_srgb": true,
"WEBGL_debug_renderer_info": true,
"WEBGL_debug_shaders": true,
"WEBGL_depth_texture": true,
"WEBGL_draw_buffers": true,
"WEBGL_lose_context": true,
"WEBGL_multi_draw": true,
"WEBGL_polygon_mode": true
},
"peerconnection": true, "peerconnection": true,
"datachannel": false, "datachannel": true,
"getusermedia": true,
"mediastream": true,
"websockets": true,
"websocketsbinary": true,
"atobbtoa": true,
"atob-btoa": true,
"framed": false,
"matchmedia": true, "matchmedia": true,
"ligatures": true, "pushmanager": true,
"cssanimations": true, "resizeobserver": true,
"csspseudoanimations": true, "workertypeoption": true,
"appearance": true, "sharedworkers": true,
"backdropfilter": true, "webworkers": true,
"backgroundcliptext": true, "transferables": true,
"bgpositionxy": true, "xdomainrequest": false,
"bgrepeatround": true, "devicemotion2": true,
"bgrepeatspace": true, "deviceorientation2": false,
"backgroundsize": true, "deviceorientation3": true
"bgsizecover": true, }
"borderimage": true,
"borderradius": true,
"boxshadow": true,
"boxsizing": true,
"csscolumns": true,
"cssgridlegacy": false,
"cssgrid": true,
"displayrunin": false,
"display-runin": false,
"ellipsis": true,
"cssfilters": true,
"flexbox": true,
"flexboxlegacy": true,
"flexboxtweener": false,
"flexwrap": true,
"cssmask": true,
"overflowscrolling": false,
"cssreflections": true,
"cssresize": true,
"scrollsnappoints": true,
"shapes": true,
"textalignlast": true,
"csstransforms": true,
"csstransforms3d": true,
"csstransformslevel2": true,
"csstransitions": true,
"csspseudotransitions": true,
"userselect": true,
"variablefonts": true
}

View file

@ -13,6 +13,25 @@
</style> </style>
<script src='script.js'></script> <script src='script.js'></script>
<script>fetch('/api/endpoint')</script> <script>fetch('/api/endpoint')</script>
<script>
const body = JSON.stringify({
data: {
key: 'value',
array: ['value-1', 'value-2'],
},
});
fetch('/post-data-1', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body,
});
fetch('/post-data-2', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body,
});
</script>
</head> </head>
<body> <body>
<h1>Network Tab Test</h1> <h1>Network Tab Test</h1>

View file

@ -59,10 +59,8 @@ const test = baseTest.extend<BrowserTestTestFixtures, BrowserTestWorkerFixtures>
}, { scope: 'worker' }], }, { scope: 'worker' }],
allowsThirdParty: [async ({ browserName, browserMajorVersion, channel }, run) => { allowsThirdParty: [async ({ browserName, browserMajorVersion, channel }, run) => {
if (browserName === 'firefox' && !channel) if (browserName === 'firefox')
await run(browserMajorVersion >= 103); await run(true);
else if (browserName === 'firefox' && channel === 'firefox-beta')
await run(browserMajorVersion < 103 || browserMajorVersion >= 110);
else else
await run(false); await run(false);
}, { scope: 'worker' }], }, { scope: 'worker' }],
@ -74,10 +72,8 @@ const test = baseTest.extend<BrowserTestTestFixtures, BrowserTestWorkerFixtures>
await run('Lax'); await run('Lax');
else if (browserName === 'webkit' && !isLinux) else if (browserName === 'webkit' && !isLinux)
await run('None'); await run('None');
else if (browserName === 'firefox' && channel === 'firefox-beta') else if (browserName === 'firefox')
await run(browserMajorVersion >= 103 && browserMajorVersion < 110 ? 'Lax' : 'None'); await run('None');
else if (browserName === 'firefox' && channel !== 'firefox-beta')
await run(browserMajorVersion >= 103 ? 'None' : 'Lax');
else else
throw new Error('unknown browser - ' + browserName); throw new Error('unknown browser - ' + browserName);
}, { scope: 'worker' }], }, { scope: 'worker' }],

View file

@ -141,7 +141,6 @@ it.describe('should proxy local network requests', () => {
it('should use ipv6 proxy', async ({ contextFactory, server, proxyServer, browserName }) => { it('should use ipv6 proxy', async ({ contextFactory, server, proxyServer, browserName }) => {
it.fail(browserName === 'firefox', 'page.goto: NS_ERROR_UNKNOWN_HOST'); it.fail(browserName === 'firefox', 'page.goto: NS_ERROR_UNKNOWN_HOST');
it.fail(!!process.env.INSIDE_DOCKER, 'docker does not support IPv6 by default');
proxyServer.forwardTo(server.PORT); proxyServer.forwardTo(server.PORT);
const context = await contextFactory({ const context = await contextFactory({
proxy: { server: `[0:0:0:0:0:0:0:1]:${proxyServer.PORT}` } proxy: { server: `[0:0:0:0:0:0:0:1]:${proxyServer.PORT}` }

View file

@ -29,4 +29,12 @@ it.describe('block', () => {
page.goto(server.PREFIX + '/serviceworkers/empty/sw.html'), page.goto(server.PREFIX + '/serviceworkers/empty/sw.html'),
]); ]);
}); });
it('should not throw error on about:blank', async ({ page }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32292' });
const errors = [];
page.on('pageerror', error => errors.push(error));
await page.goto('about:blank');
expect(errors).toEqual([]);
});
}); });

Some files were not shown because too many files have changed in this diff Show more