Merge branch 'main' into separate-transport-from-testserverconnection
This commit is contained in:
commit
7b11e02ee3
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -34,3 +34,4 @@ test-results
|
|||
/tests/installation/.registry.json
|
||||
.cache/
|
||||
.eslintcache
|
||||
playwright.env
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# 🎭 Playwright
|
||||
|
||||
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop --> [](https://aka.ms/playwright/discord)
|
||||
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop --> [](https://aka.ms/playwright/discord)
|
||||
|
||||
## [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 |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| 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: |
|
||||
| Firefox <!-- GEN:firefox-version -->129.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
|
||||
|
|
|
|||
17
SUPPORT.md
Normal file
17
SUPPORT.md
Normal 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
|
||||
|
|
@ -60,7 +60,7 @@ An object with all the response HTTP headers associated with this response.
|
|||
- `name` <[string]> Name 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.
|
||||
|
||||
## async method: APIResponse.json
|
||||
|
|
|
|||
|
|
@ -297,8 +297,10 @@ testing frameworks should explicitly create [`method: Browser.newContext`] follo
|
|||
|
||||
## async method: Browser.removeAllListeners
|
||||
* 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
|
||||
* since: v1.47
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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.
|
||||
|
||||
```js
|
||||
|
|
@ -415,42 +415,13 @@ The order of evaluation of multiple scripts installed via [`method: BrowserConte
|
|||
[`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
|
||||
* since: v1.8
|
||||
* langs: js
|
||||
- `script` <[function]|[string]|[Object]>
|
||||
- `path` ?<[path]> Path to the JavaScript file. If `path` is a relative path, then it is resolved relative to the
|
||||
current working directory.
|
||||
- `content` ?<[string]> Raw script content.
|
||||
current working directory. Optional.
|
||||
- `content` ?<[string]> Raw script content. Optional.
|
||||
|
||||
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
|
||||
- `arg` ?<[Serializable]>
|
||||
|
||||
Optional JSON-serializable argument to pass to [`param: script`].
|
||||
* 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.
|
||||
Optional argument to pass to [`param: script`] (only supported when passing a function).
|
||||
|
||||
### param: BrowserContext.addInitScript.path
|
||||
* since: v1.8
|
||||
|
|
@ -1048,8 +1017,10 @@ Returns all open pages in the context.
|
|||
|
||||
## async method: BrowserContext.removeAllListeners
|
||||
* 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
|
||||
* since: v1.47
|
||||
|
|
|
|||
|
|
@ -866,7 +866,7 @@ await handle.SelectOptionAsync(new[] {
|
|||
### option: ElementHandle.selectOption.force = %%-input-force-%%
|
||||
* since: v1.13
|
||||
|
||||
### option: ElementHandle.selectOption.noWaitAfter = %%-input-no-wait-after-%%
|
||||
### option: ElementHandle.selectOption.noWaitAfter = %%-input-no-wait-after-removed-%%
|
||||
* since: v1.8
|
||||
|
||||
### option: ElementHandle.selectOption.timeout = %%-input-timeout-%%
|
||||
|
|
|
|||
|
|
@ -1543,7 +1543,7 @@ await frame.SelectOptionAsync("select#colors", new[] { "red", "green", "blue" })
|
|||
### option: Frame.selectOption.force = %%-input-force-%%
|
||||
* since: v1.13
|
||||
|
||||
### option: Frame.selectOption.noWaitAfter = %%-input-no-wait-after-%%
|
||||
### option: Frame.selectOption.noWaitAfter = %%-input-no-wait-after-removed-%%
|
||||
* since: v1.8
|
||||
|
||||
### option: Frame.selectOption.strict = %%-input-strict-%%
|
||||
|
|
|
|||
|
|
@ -2055,7 +2055,7 @@ await element.SelectOptionAsync(new[] { "red", "green", "blue" });
|
|||
### option: Locator.selectOption.force = %%-input-force-%%
|
||||
* since: v1.14
|
||||
|
||||
### option: Locator.selectOption.noWaitAfter = %%-input-no-wait-after-%%
|
||||
### option: Locator.selectOption.noWaitAfter = %%-input-no-wait-after-removed-%%
|
||||
* since: v1.14
|
||||
|
||||
### option: Locator.selectOption.timeout = %%-input-timeout-%%
|
||||
|
|
|
|||
|
|
@ -619,42 +619,13 @@ The order of evaluation of multiple scripts installed via [`method: BrowserConte
|
|||
[`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
|
||||
* since: v1.8
|
||||
* langs: js
|
||||
- `script` <[function]|[string]|[Object]>
|
||||
- `path` ?<[path]> Path to the JavaScript file. If `path` is a relative path, then it is resolved relative to the
|
||||
current working directory.
|
||||
- `content` ?<[string]> Raw script content.
|
||||
current working directory. Optional.
|
||||
- `content` ?<[string]> Raw script content. Optional.
|
||||
|
||||
Script to be evaluated in the page.
|
||||
|
||||
|
|
@ -670,9 +641,7 @@ Script to be evaluated in all pages in the browser context.
|
|||
* langs: js
|
||||
- `arg` ?<[Serializable]>
|
||||
|
||||
Optional JSON-serializable argument to pass to [`param: script`].
|
||||
* 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.
|
||||
Optional argument to pass to [`param: script`] (only supported when passing a function).
|
||||
|
||||
### param: Page.addInitScript.path
|
||||
* 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
|
||||
* since: v1.17
|
||||
regular [`Locator`] instead.
|
||||
- returns: <[FrameLocator]>
|
||||
|
||||
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
|
||||
* 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
|
||||
* since: v1.47
|
||||
|
|
@ -3742,7 +3727,7 @@ await page.SelectOptionAsync("select#colors", new[] { "red", "green", "blue" });
|
|||
### option: Page.selectOption.force = %%-input-force-%%
|
||||
* since: v1.13
|
||||
|
||||
### option: Page.selectOption.noWaitAfter = %%-input-no-wait-after-%%
|
||||
### option: Page.selectOption.noWaitAfter = %%-input-no-wait-after-removed-%%
|
||||
* since: v1.8
|
||||
|
||||
### option: Page.selectOption.strict = %%-input-strict-%%
|
||||
|
|
|
|||
|
|
@ -288,7 +288,7 @@ await Expect(page.GetByText("Strawberry")).ToBeVisibleAsync();
|
|||
|
||||
```java
|
||||
// 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")
|
||||
.setUpdate(true)
|
||||
);
|
||||
|
|
@ -386,7 +386,7 @@ await page.ExpectByTextAsync("Playwright", new() { Exact = true }).ToBeVisibleAs
|
|||
// Replay API requests from HAR.
|
||||
// Either use a matching response from the HAR,
|
||||
// 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")
|
||||
.setUpdate(false)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -722,3 +722,64 @@ export const test = base.extend({
|
|||
}, { 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.
|
||||
|
|
|
|||
|
|
@ -80,14 +80,14 @@ By default, Playwright will look up a closest tsconfig for each imported file by
|
|||
|
||||
```sh
|
||||
# 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.
|
||||
|
||||
```sh
|
||||
# Pass a specific tsconfig
|
||||
npx playwrigh test --tsconfig=tsconfig.test.json
|
||||
npx playwright test --tsconfig=tsconfig.test.json
|
||||
```
|
||||
|
||||
## Manually compile tests with TypeScript
|
||||
|
|
|
|||
18
package-lock.json
generated
18
package-lock.json
generated
|
|
@ -41,7 +41,7 @@
|
|||
"colors": "^1.4.0",
|
||||
"concurrently": "^6.2.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^16.0.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"electron": "^30.1.2",
|
||||
"esbuild": "^0.18.11",
|
||||
"eslint": "^8.55.0",
|
||||
|
|
@ -3293,15 +3293,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.3.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
|
||||
"integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==",
|
||||
"version": "16.4.5",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
|
||||
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/motdotla/dotenv?sponsor=1"
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/electron": {
|
||||
|
|
@ -6820,9 +6820,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/svelte": {
|
||||
"version": "4.2.9",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.9.tgz",
|
||||
"integrity": "sha512-hsoB/WZGEPFXeRRLPhPrbRz67PhP6sqYgvwcAs+gWdSQSvNDw+/lTeUJSWe5h2xC97Fz/8QxAOqItwBzNJPU8w==",
|
||||
"version": "4.2.19",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.19.tgz",
|
||||
"integrity": "sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.1",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.15",
|
||||
|
|
@ -8060,7 +8060,7 @@
|
|||
"playwright": "cli.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"svelte": "^4.2.8"
|
||||
"svelte": "^4.2.19"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@
|
|||
"colors": "^1.4.0",
|
||||
"concurrently": "^6.2.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^16.0.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"electron": "^30.1.2",
|
||||
"esbuild": "^0.18.11",
|
||||
"eslint": "^8.55.0",
|
||||
|
|
|
|||
|
|
@ -75,11 +75,16 @@ export const AttachmentLink: React.FunctionComponent<{
|
|||
attachment: TestAttachment,
|
||||
href?: string,
|
||||
linkName?: string,
|
||||
}> = ({ attachment, href, linkName }) => {
|
||||
openInNewTab?: boolean,
|
||||
}> = ({ attachment, href, linkName, openInNewTab }) => {
|
||||
return <TreeItem title={<span>
|
||||
{attachment.contentType === kMissingContentType ? icons.warning() : icons.attachment()}
|
||||
{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 ? () => {
|
||||
return [<div key={1} className='attachment-body'><CopyToClipboard value={attachment.body!}/>{linkifyText(attachment.body!)}</div>];
|
||||
} : undefined} depth={0} style={{ lineHeight: '32px' }}></TreeItem>;
|
||||
|
|
|
|||
|
|
@ -67,15 +67,16 @@ export const TestResultView: React.FC<{
|
|||
anchor: 'video' | 'diff' | '',
|
||||
}> = ({ result, anchor }) => {
|
||||
|
||||
const { screenshots, videos, traces, otherAttachments, diffs } = React.useMemo(() => {
|
||||
const { screenshots, videos, traces, otherAttachments, diffs, htmls } = React.useMemo(() => {
|
||||
const attachments = result?.attachments || [];
|
||||
const screenshots = new Set(attachments.filter(a => a.contentType.startsWith('image/')));
|
||||
const videos = attachments.filter(a => a.name === 'video');
|
||||
const traces = attachments.filter(a => a.name === 'trace');
|
||||
const htmls = attachments.filter(a => a.contentType.startsWith('text/html'));
|
||||
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);
|
||||
return { screenshots: [...screenshots], videos, traces, otherAttachments, diffs };
|
||||
return { screenshots: [...screenshots], videos, traces, otherAttachments, diffs, htmls };
|
||||
}, [result]);
|
||||
|
||||
const videoRef = React.useRef<HTMLDivElement>(null);
|
||||
|
|
@ -135,7 +136,10 @@ export const TestResultView: React.FC<{
|
|||
</div>)}
|
||||
</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>)}
|
||||
</AutoChip>}
|
||||
</div>;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
- debug@4.3.4 (https://github.com/debug-js/debug)
|
||||
- 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)
|
||||
- escape-string-regexp@2.0.0 (https://github.com/sindresorhus/escape-string-regexp)
|
||||
- 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
|
||||
|
||||
%% 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
|
||||
=========================================
|
||||
The MIT License (MIT)
|
||||
|
|
@ -1514,6 +1543,6 @@ END OF yazl@2.5.1 AND INFORMATION
|
|||
|
||||
SUMMARY BEGIN HERE
|
||||
=========================================
|
||||
Total Packages: 45
|
||||
Total Packages: 46
|
||||
=========================================
|
||||
END OF SUMMARY
|
||||
|
|
@ -3,15 +3,15 @@
|
|||
"browsers": [
|
||||
{
|
||||
"name": "chromium",
|
||||
"revision": "1131",
|
||||
"revision": "1133",
|
||||
"installByDefault": true,
|
||||
"browserVersion": "128.0.6613.36"
|
||||
"browserVersion": "129.0.6668.22"
|
||||
},
|
||||
{
|
||||
"name": "chromium-tip-of-tree",
|
||||
"revision": "1253",
|
||||
"revision": "1255",
|
||||
"installByDefault": false,
|
||||
"browserVersion": "130.0.6670.0"
|
||||
"browserVersion": "130.0.6684.0"
|
||||
},
|
||||
{
|
||||
"name": "firefox",
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
},
|
||||
{
|
||||
"name": "webkit",
|
||||
"revision": "2062",
|
||||
"revision": "2068",
|
||||
"installByDefault": true,
|
||||
"revisionOverrides": {
|
||||
"mac10.14": "1446",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
"colors": "1.4.0",
|
||||
"commander": "8.3.0",
|
||||
"debug": "^4.3.4",
|
||||
"dotenv": "^16.4.5",
|
||||
"graceful-fs": "4.2.10",
|
||||
"https-proxy-agent": "5.0.0",
|
||||
"jpeg-js": "0.4.4",
|
||||
|
|
@ -198,6 +199,17 @@
|
|||
"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": {
|
||||
"version": "2.0.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
"colors": "1.4.0",
|
||||
"commander": "8.3.0",
|
||||
"debug": "^4.3.4",
|
||||
"dotenv": "^16.4.5",
|
||||
"graceful-fs": "4.2.10",
|
||||
"https-proxy-agent": "5.0.0",
|
||||
"jpeg-js": "0.4.4",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ export const colors = colorsLibrary;
|
|||
import debugLibrary from 'debug';
|
||||
export const debug = debugLibrary;
|
||||
|
||||
import dotenvLibrary from 'dotenv';
|
||||
export const dotenv = dotenvLibrary;
|
||||
|
||||
export { getProxyForUrl } from 'proxy-from-env';
|
||||
|
||||
export { HttpsProxyAgent } from 'https-proxy-agent';
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import fs from 'fs';
|
|||
import os from 'os';
|
||||
import path from 'path';
|
||||
import type { Command } from '../utilsBundle';
|
||||
import { program } from '../utilsBundle';
|
||||
import { program, dotenv } from '../utilsBundle';
|
||||
export { program } from '../utilsBundle';
|
||||
import { runDriver, runServer, printApiJson, launchBrowserServer } from './driver';
|
||||
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) {
|
||||
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);
|
||||
dotenv.config({ path: 'playwright.env' });
|
||||
await context._enableRecorder({
|
||||
language,
|
||||
launchOptions,
|
||||
|
|
@ -570,7 +571,6 @@ async function codegen(options: Options & { target: string, output?: string, tes
|
|||
mode: 'recording',
|
||||
testIdAttributeName,
|
||||
outputFile: outputFile ? path.resolve(outputFile) : undefined,
|
||||
handleSIGINT: false,
|
||||
});
|
||||
await openPage(context, url);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -481,7 +481,6 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
mode?: 'recording' | 'inspecting',
|
||||
testIdAttributeName?: string,
|
||||
outputFile?: string,
|
||||
handleSIGINT?: boolean,
|
||||
}) {
|
||||
await this._channel.recorderSupplementEnable(params);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,37 +28,20 @@ export function envObjectToArray(env: types.Env): { name: string, value: string
|
|||
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') {
|
||||
const source = fun.toString();
|
||||
const argString = Object.is(arg, undefined) ? 'undefined' : JSON.stringify(arg);
|
||||
return `(${source})(${argString})`;
|
||||
}
|
||||
if (isString(fun)) {
|
||||
if (arg !== undefined)
|
||||
throw new Error('Cannot evaluate a string with arguments');
|
||||
if (arg !== undefined)
|
||||
throw new Error('Cannot evaluate a string with arguments');
|
||||
if (isString(fun))
|
||||
return fun;
|
||||
}
|
||||
if (fun.content !== undefined) {
|
||||
if (arg !== undefined)
|
||||
throw new Error('Cannot evaluate a string with arguments');
|
||||
if (fun.content !== undefined)
|
||||
return fun.content;
|
||||
}
|
||||
if (fun.path !== undefined) {
|
||||
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)
|
||||
source = addSourceUrlToScript(source, fun.path);
|
||||
return source;
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export type WaitForEventOptions = Function | { predicate?: Function, timeout?: n
|
|||
export type WaitForFunctionOptions = { timeout?: number, polling?: 'raf' | number };
|
||||
|
||||
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 StorageState = {
|
||||
cookies: channels.NetworkCookie[],
|
||||
|
|
|
|||
|
|
@ -961,7 +961,6 @@ scheme.BrowserContextRecorderSupplementEnableParams = tObject({
|
|||
device: tOptional(tString),
|
||||
saveStorage: tOptional(tString),
|
||||
outputFile: tOptional(tString),
|
||||
handleSIGINT: tOptional(tBoolean),
|
||||
omitCallTracking: tOptional(tBoolean),
|
||||
});
|
||||
scheme.BrowserContextRecorderSupplementEnableResult = tOptional(tObject({}));
|
||||
|
|
@ -1637,7 +1636,6 @@ scheme.FrameSelectOptionParams = tObject({
|
|||
}))),
|
||||
force: tOptional(tBoolean),
|
||||
timeout: tOptional(tNumber),
|
||||
noWaitAfter: tOptional(tBoolean),
|
||||
});
|
||||
scheme.FrameSelectOptionResult = tObject({
|
||||
values: tArray(tString),
|
||||
|
|
@ -2001,7 +1999,6 @@ scheme.ElementHandleSelectOptionParams = tObject({
|
|||
}))),
|
||||
force: tOptional(tBoolean),
|
||||
timeout: tOptional(tNumber),
|
||||
noWaitAfter: tOptional(tBoolean),
|
||||
});
|
||||
scheme.ElementHandleSelectOptionResult = tObject({
|
||||
values: tArray(tString),
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ import { BrowserContextAPIRequestContext } from './fetch';
|
|||
import type { Artifact } from './artifact';
|
||||
import { Clock } from './clock';
|
||||
import { ClientCertificatesProxy } from './socksClientCertificatesInterceptor';
|
||||
import { RecorderApp } from './recorder/recorderApp';
|
||||
|
||||
export abstract class BrowserContext extends SdkObject {
|
||||
static Events = {
|
||||
|
|
@ -130,19 +131,21 @@ export abstract class BrowserContext extends SdkObject {
|
|||
|
||||
// When PWDEBUG=1, show inspector for each context.
|
||||
if (debugMode() === 'inspector')
|
||||
await Recorder.show(this, { pauseOnNextStatement: true });
|
||||
await Recorder.show(this, RecorderApp.factory(this), { pauseOnNextStatement: true });
|
||||
|
||||
// When paused, show inspector.
|
||||
if (this._debugger.isPaused())
|
||||
Recorder.showInspector(this);
|
||||
Recorder.showInspector(this, RecorderApp.factory(this));
|
||||
|
||||
this._debugger.on(Debugger.Events.PausedStateChanged, () => {
|
||||
Recorder.showInspector(this);
|
||||
if (this._debugger.isPaused())
|
||||
Recorder.showInspector(this, RecorderApp.factory(this));
|
||||
});
|
||||
|
||||
if (debugMode() === 'console')
|
||||
await this.extendInjectedScript(consoleApiSource.source);
|
||||
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)
|
||||
await this.grantPermissions(this._options.permissions);
|
||||
|
|
|
|||
|
|
@ -609,7 +609,7 @@ class RouteImpl implements network.RouteDelegate {
|
|||
this._interceptionId = interceptionId;
|
||||
}
|
||||
|
||||
async continue(request: network.Request, overrides: types.NormalizedContinueOverrides): Promise<void> {
|
||||
async continue(overrides: types.NormalizedContinueOverrides): Promise<void> {
|
||||
this._alreadyContinuedParams = {
|
||||
requestId: this._interceptionId!,
|
||||
url: overrides.url,
|
||||
|
|
|
|||
|
|
@ -1131,17 +1131,21 @@ using Audits.issueAdded event.
|
|||
}
|
||||
|
||||
/**
|
||||
* Defines commands and events for browser extensions. Available if the client
|
||||
is connected using the --remote-debugging-pipe flag and
|
||||
the --enable-unsafe-extension-debugging flag is set.
|
||||
* Defines commands and events for browser extensions.
|
||||
*/
|
||||
export module Extensions {
|
||||
/**
|
||||
* Storage areas.
|
||||
*/
|
||||
export type StorageArea = "session"|"local"|"sync"|"managed";
|
||||
|
||||
|
||||
/**
|
||||
* Installs an unpacked extension from the filesystem similar to
|
||||
--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 = {
|
||||
/**
|
||||
|
|
@ -1155,6 +1159,81 @@ has been installed.
|
|||
*/
|
||||
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;
|
||||
}
|
||||
/**
|
||||
* CSS position-fallback rule representation.
|
||||
*/
|
||||
export interface CSSPositionFallbackRule {
|
||||
name: Value;
|
||||
/**
|
||||
* List of keyframes.
|
||||
*/
|
||||
tryRules: CSSTryRule[];
|
||||
}
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
|
|
@ -3496,7 +3561,7 @@ front-end.
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
|
@ -3646,6 +3711,13 @@ The property is always undefined now.
|
|||
compatibilityMode?: CompatibilityMode;
|
||||
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.
|
||||
*/
|
||||
|
|
@ -4693,6 +4765,17 @@ File wrapper.
|
|||
export type getFileInfoReturnValue = {
|
||||
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
|
||||
$x functions).
|
||||
|
|
@ -11369,7 +11452,7 @@ as an ad.
|
|||
* All Permissions Policy features. This enum should match the one defined
|
||||
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.
|
||||
*/
|
||||
|
|
@ -11784,7 +11867,7 @@ Example URLs: http://www.google.com/file.html -> "google.com"
|
|||
*/
|
||||
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 interface InstallabilityErrorArgument {
|
||||
/**
|
||||
|
|
@ -12298,6 +12381,10 @@ when bfcache navigation fails.
|
|||
* Frame's new url.
|
||||
*/
|
||||
url: string;
|
||||
/**
|
||||
* Navigation type
|
||||
*/
|
||||
navigationType: "fragment"|"historyApi"|"other";
|
||||
}
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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
|
||||
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.
|
||||
*/
|
||||
|
|
@ -20122,6 +20304,10 @@ Error was thrown.
|
|||
"Audits.checkContrast": Audits.checkContrastParameters;
|
||||
"Audits.checkFormsIssues": Audits.checkFormsIssuesParameters;
|
||||
"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.setAddresses": Autofill.setAddressesParameters;
|
||||
"Autofill.disable": Autofill.disableParameters;
|
||||
|
|
@ -20232,6 +20418,7 @@ Error was thrown.
|
|||
"DOM.setNodeStackTracesEnabled": DOM.setNodeStackTracesEnabledParameters;
|
||||
"DOM.getNodeStackTraces": DOM.getNodeStackTracesParameters;
|
||||
"DOM.getFileInfo": DOM.getFileInfoParameters;
|
||||
"DOM.getDetachedDomNodes": DOM.getDetachedDomNodesParameters;
|
||||
"DOM.setInspectedNode": DOM.setInspectedNodeParameters;
|
||||
"DOM.setNodeName": DOM.setNodeNameParameters;
|
||||
"DOM.setNodeValue": DOM.setNodeValueParameters;
|
||||
|
|
@ -20616,6 +20803,10 @@ Error was thrown.
|
|||
"PWA.launchFilesInApp": PWA.launchFilesInAppParameters;
|
||||
"PWA.openCurrentPageInApp": PWA.openCurrentPageInAppParameters;
|
||||
"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.disable": Console.disableParameters;
|
||||
"Console.enable": Console.enableParameters;
|
||||
|
|
@ -20722,6 +20913,10 @@ Error was thrown.
|
|||
"Audits.checkContrast": Audits.checkContrastReturnValue;
|
||||
"Audits.checkFormsIssues": Audits.checkFormsIssuesReturnValue;
|
||||
"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.setAddresses": Autofill.setAddressesReturnValue;
|
||||
"Autofill.disable": Autofill.disableReturnValue;
|
||||
|
|
@ -20832,6 +21027,7 @@ Error was thrown.
|
|||
"DOM.setNodeStackTracesEnabled": DOM.setNodeStackTracesEnabledReturnValue;
|
||||
"DOM.getNodeStackTraces": DOM.getNodeStackTracesReturnValue;
|
||||
"DOM.getFileInfo": DOM.getFileInfoReturnValue;
|
||||
"DOM.getDetachedDomNodes": DOM.getDetachedDomNodesReturnValue;
|
||||
"DOM.setInspectedNode": DOM.setInspectedNodeReturnValue;
|
||||
"DOM.setNodeName": DOM.setNodeNameReturnValue;
|
||||
"DOM.setNodeValue": DOM.setNodeValueReturnValue;
|
||||
|
|
@ -21216,6 +21412,10 @@ Error was thrown.
|
|||
"PWA.launchFilesInApp": PWA.launchFilesInAppReturnValue;
|
||||
"PWA.openCurrentPageInApp": PWA.openCurrentPageInAppReturnValue;
|
||||
"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.disable": Console.disableReturnValue;
|
||||
"Console.enable": Console.enableReturnValue;
|
||||
|
|
|
|||
3
packages/playwright-core/src/server/codegen/DEPS.list
Normal file
3
packages/playwright-core/src/server/codegen/DEPS.list
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[*]
|
||||
../../utils/
|
||||
../deviceDescriptors.ts
|
||||
|
|
@ -14,13 +14,9 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { BrowserContextOptions } from '../../..';
|
||||
import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language';
|
||||
import { sanitizeDeviceOptions, toSignalMap } from './language';
|
||||
import type { ActionInContext } from './codeGenerator';
|
||||
import type { Action } from './recorderActions';
|
||||
import type { MouseClickOptions } from './utils';
|
||||
import { toModifiers } from './utils';
|
||||
import type { BrowserContextOptions } from '../../../types/types';
|
||||
import type { ActionInContext, Language, LanguageGenerator, LanguageGeneratorOptions } from './types';
|
||||
import { sanitizeDeviceOptions, toClickOptions, toKeyboardModifiers, toSignalMap } from './language';
|
||||
import { escapeWithQuotes, asLocator } from '../../utils';
|
||||
import { deviceDescriptors } from '../deviceDescriptors';
|
||||
|
||||
|
|
@ -72,14 +68,8 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
|
|||
return formatter.format();
|
||||
}
|
||||
|
||||
let subject: string;
|
||||
if (actionInContext.frame.isMainFrame) {
|
||||
subject = pageAlias;
|
||||
} else {
|
||||
const locators = actionInContext.frame.selectorsChain.map(selector => `.FrameLocator(${quote(selector)})`);
|
||||
subject = `${pageAlias}${locators.join('')}`;
|
||||
}
|
||||
|
||||
const locators = actionInContext.frame.framePath.map(selector => `.${this._asLocator(selector)}.ContentFrame()`);
|
||||
const subject = `${pageAlias}${locators.join('')}`;
|
||||
const signals = toSignalMap(action);
|
||||
|
||||
if (signals.dialog) {
|
||||
|
|
@ -93,7 +83,7 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
|
|||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push(this._generateActionCall(subject, action));
|
||||
lines.push(this._generateActionCall(subject, actionInContext));
|
||||
|
||||
if (signals.download) {
|
||||
lines.unshift(`var download${signals.download.downloadAlias} = await ${pageAlias}.RunAndWaitForDownloadAsync(async () =>\n{`);
|
||||
|
|
@ -111,7 +101,8 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
|
|||
return formatter.format();
|
||||
}
|
||||
|
||||
private _generateActionCall(subject: string, action: Action): string {
|
||||
private _generateActionCall(subject: string, actionInContext: ActionInContext): string {
|
||||
const action = actionInContext.action;
|
||||
switch (action.name) {
|
||||
case 'openPage':
|
||||
throw Error('Not reached');
|
||||
|
|
@ -121,16 +112,7 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
|
|||
let method = '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;
|
||||
const options = toClickOptions(action);
|
||||
if (!Object.entries(options).length)
|
||||
return `await ${subject}.${this._asLocator(action.selector)}.${method}Async();`;
|
||||
const optionsString = formatObject(options, ' ', 'Locator' + method + 'Options');
|
||||
|
|
@ -145,7 +127,7 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
|
|||
case 'setInputFiles':
|
||||
return `await ${subject}.${this._asLocator(action.selector)}.SetInputFilesAsync(${formatObject(action.files)});`;
|
||||
case 'press': {
|
||||
const modifiers = toModifiers(action.modifiers);
|
||||
const modifiers = toKeyboardModifiers(action.modifiers);
|
||||
const shortcut = [...modifiers, action.key].join('+');
|
||||
return `await ${subject}.${this._asLocator(action.selector)}.PressAsync(${quote(shortcut)});`;
|
||||
}
|
||||
|
|
@ -14,13 +14,10 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { BrowserContextOptions } from '../../..';
|
||||
import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language';
|
||||
import { toSignalMap } from './language';
|
||||
import type { ActionInContext } from './codeGenerator';
|
||||
import type { Action } from './recorderActions';
|
||||
import type { MouseClickOptions } from './utils';
|
||||
import { toModifiers } from './utils';
|
||||
import type { BrowserContextOptions } from '../../../types/types';
|
||||
import type * as types from '../types';
|
||||
import type { ActionInContext, Language, LanguageGenerator, LanguageGeneratorOptions } from './types';
|
||||
import { toClickOptions, toKeyboardModifiers, toSignalMap } from './language';
|
||||
import { deviceDescriptors } from '../deviceDescriptors';
|
||||
import { JavaScriptFormatter } from './javascript';
|
||||
import { escapeWithQuotes, asLocator } from '../../utils';
|
||||
|
|
@ -63,16 +60,8 @@ export class JavaLanguageGenerator implements LanguageGenerator {
|
|||
return formatter.format();
|
||||
}
|
||||
|
||||
let subject: string;
|
||||
let inFrameLocator = false;
|
||||
if (actionInContext.frame.isMainFrame) {
|
||||
subject = pageAlias;
|
||||
} else {
|
||||
const locators = actionInContext.frame.selectorsChain.map(selector => `.frameLocator(${quote(selector)})`);
|
||||
subject = `${pageAlias}${locators.join('')}`;
|
||||
inFrameLocator = true;
|
||||
}
|
||||
|
||||
const locators = actionInContext.frame.framePath.map(selector => `.${this._asLocator(selector, false)}.contentFrame()`);
|
||||
const subject = `${pageAlias}${locators.join('')}`;
|
||||
const signals = toSignalMap(action);
|
||||
|
||||
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) {
|
||||
code = `Page ${signals.popup.popupAlias} = ${pageAlias}.waitForPopup(() -> {
|
||||
|
|
@ -101,7 +90,8 @@ export class JavaLanguageGenerator implements LanguageGenerator {
|
|||
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) {
|
||||
case 'openPage':
|
||||
throw Error('Not reached');
|
||||
|
|
@ -111,16 +101,7 @@ export class JavaLanguageGenerator implements LanguageGenerator {
|
|||
let method = '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;
|
||||
const options = toClickOptions(action);
|
||||
const optionsText = formatClickOptions(options);
|
||||
return `${subject}.${this._asLocator(action.selector, inFrameLocator)}.${method}(${optionsText});`;
|
||||
}
|
||||
|
|
@ -133,7 +114,7 @@ export class JavaLanguageGenerator implements LanguageGenerator {
|
|||
case 'setInputFiles':
|
||||
return `${subject}.${this._asLocator(action.selector, inFrameLocator)}.setInputFiles(${formatPath(action.files.length === 1 ? action.files[0] : action.files)});`;
|
||||
case 'press': {
|
||||
const modifiers = toModifiers(action.modifiers);
|
||||
const modifiers = toKeyboardModifiers(action.modifiers);
|
||||
const shortcut = [...modifiers, action.key].join('+');
|
||||
return `${subject}.${this._asLocator(action.selector, inFrameLocator)}.press(${quote(shortcut)});`;
|
||||
}
|
||||
|
|
@ -279,7 +260,7 @@ function formatContextOptions(contextOptions: BrowserContextOptions, deviceName:
|
|||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function formatClickOptions(options: MouseClickOptions) {
|
||||
function formatClickOptions(options: types.MouseClickOptions) {
|
||||
const lines = [];
|
||||
if (options.button)
|
||||
lines.push(` .setButton(MouseButton.${options.button.toUpperCase()})`);
|
||||
|
|
@ -14,13 +14,9 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { BrowserContextOptions } from '../../..';
|
||||
import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language';
|
||||
import { sanitizeDeviceOptions, toSignalMap } from './language';
|
||||
import type { ActionInContext } from './codeGenerator';
|
||||
import type { Action } from './recorderActions';
|
||||
import type { MouseClickOptions } from './utils';
|
||||
import { toModifiers } from './utils';
|
||||
import type { BrowserContextOptions } from '../../../types/types';
|
||||
import type { ActionInContext, Language, LanguageGenerator, LanguageGeneratorOptions } from './types';
|
||||
import { sanitizeDeviceOptions, toSignalMap, toKeyboardModifiers, toClickOptions } from './language';
|
||||
import { deviceDescriptors } from '../deviceDescriptors';
|
||||
import { escapeWithQuotes, asLocator } from '../../utils';
|
||||
|
||||
|
|
@ -52,14 +48,8 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
|
|||
return formatter.format();
|
||||
}
|
||||
|
||||
let subject: string;
|
||||
if (actionInContext.frame.isMainFrame) {
|
||||
subject = pageAlias;
|
||||
} else {
|
||||
const locators = actionInContext.frame.selectorsChain.map(selector => `.frameLocator(${quote(selector)})`);
|
||||
subject = `${pageAlias}${locators.join('')}`;
|
||||
}
|
||||
|
||||
const locators = actionInContext.frame.framePath.map(selector => `.${this._asLocator(selector)}.contentFrame()`);
|
||||
const subject = `${pageAlias}${locators.join('')}`;
|
||||
const signals = toSignalMap(action);
|
||||
|
||||
if (signals.dialog) {
|
||||
|
|
@ -74,7 +64,7 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
|
|||
if (signals.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)
|
||||
formatter.add(`const ${signals.popup.popupAlias} = await ${signals.popup.popupAlias}Promise;`);
|
||||
|
|
@ -84,7 +74,8 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
|
|||
return formatter.format();
|
||||
}
|
||||
|
||||
private _generateActionCall(subject: string, action: Action): string {
|
||||
private _generateActionCall(subject: string, actionInContext: ActionInContext): string {
|
||||
const action = actionInContext.action;
|
||||
switch (action.name) {
|
||||
case 'openPage':
|
||||
throw Error('Not reached');
|
||||
|
|
@ -94,16 +85,7 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
|
|||
let method = '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;
|
||||
const options = toClickOptions(action);
|
||||
const optionsString = formatOptions(options, false);
|
||||
return `await ${subject}.${this._asLocator(action.selector)}.${method}(${optionsString});`;
|
||||
}
|
||||
|
|
@ -116,7 +98,7 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
|
|||
case 'setInputFiles':
|
||||
return `await ${subject}.${this._asLocator(action.selector)}.setInputFiles(${formatObject(action.files.length === 1 ? action.files[0] : action.files)});`;
|
||||
case 'press': {
|
||||
const modifiers = toModifiers(action.modifiers);
|
||||
const modifiers = toKeyboardModifiers(action.modifiers);
|
||||
const shortcut = [...modifiers, action.key].join('+');
|
||||
return `await ${subject}.${this._asLocator(action.selector)}.press(${quote(shortcut)});`;
|
||||
}
|
||||
|
|
@ -276,3 +258,9 @@ export class JavaScriptFormatter {
|
|||
function quote(text: string) {
|
||||
return escapeWithQuotes(text, '\'');
|
||||
}
|
||||
|
||||
function wrapWithStep(description: string | undefined, body: string) {
|
||||
return description ? `await test.step(\`${description}\`, async () => {
|
||||
${body}
|
||||
});` : body;
|
||||
}
|
||||
|
|
@ -15,8 +15,7 @@
|
|||
*/
|
||||
|
||||
import { asLocator } from '../../utils';
|
||||
import type { ActionInContext } from './codeGenerator';
|
||||
import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language';
|
||||
import type { ActionInContext, Language, LanguageGenerator, LanguageGeneratorOptions } from './types';
|
||||
|
||||
export class JsonlLanguageGenerator implements LanguageGenerator {
|
||||
id = 'jsonl';
|
||||
84
packages/playwright-core/src/server/codegen/language.ts
Normal file
84
packages/playwright-core/src/server/codegen/language.ts
Normal 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;
|
||||
}
|
||||
37
packages/playwright-core/src/server/codegen/languages.ts
Normal file
37
packages/playwright-core/src/server/codegen/languages.ts
Normal 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(),
|
||||
]);
|
||||
}
|
||||
|
|
@ -14,13 +14,9 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { BrowserContextOptions } from '../../..';
|
||||
import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language';
|
||||
import { sanitizeDeviceOptions, toSignalMap } from './language';
|
||||
import type { ActionInContext } from './codeGenerator';
|
||||
import type { Action } from './recorderActions';
|
||||
import type { MouseClickOptions } from './utils';
|
||||
import { toModifiers } from './utils';
|
||||
import type { BrowserContextOptions } from '../../../types/types';
|
||||
import type { ActionInContext, Language, LanguageGenerator, LanguageGeneratorOptions } from './types';
|
||||
import { sanitizeDeviceOptions, toSignalMap, toKeyboardModifiers, toClickOptions } from './language';
|
||||
import { escapeWithQuotes, toSnakeCase, asLocator } from '../../utils';
|
||||
import { deviceDescriptors } from '../deviceDescriptors';
|
||||
|
||||
|
|
@ -59,20 +55,14 @@ export class PythonLanguageGenerator implements LanguageGenerator {
|
|||
return formatter.format();
|
||||
}
|
||||
|
||||
let subject: string;
|
||||
if (actionInContext.frame.isMainFrame) {
|
||||
subject = pageAlias;
|
||||
} else {
|
||||
const locators = actionInContext.frame.selectorsChain.map(selector => `.frame_locator(${quote(selector)})`);
|
||||
subject = `${pageAlias}${locators.join('')}`;
|
||||
}
|
||||
|
||||
const locators = actionInContext.frame.framePath.map(selector => `.${this._asLocator(selector)}.content_frame()`);
|
||||
const subject = `${pageAlias}${locators.join('')}`;
|
||||
const signals = toSignalMap(action);
|
||||
|
||||
if (signals.dialog)
|
||||
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) {
|
||||
code = `${this._asyncPrefix}with ${pageAlias}.expect_popup() as ${signals.popup.popupAlias}_info {
|
||||
|
|
@ -93,7 +83,8 @@ export class PythonLanguageGenerator implements LanguageGenerator {
|
|||
return formatter.format();
|
||||
}
|
||||
|
||||
private _generateActionCall(subject: string, action: Action): string {
|
||||
private _generateActionCall(subject: string, actionInContext: ActionInContext): string {
|
||||
const action = actionInContext.action;
|
||||
switch (action.name) {
|
||||
case 'openPage':
|
||||
throw Error('Not reached');
|
||||
|
|
@ -103,16 +94,7 @@ export class PythonLanguageGenerator implements LanguageGenerator {
|
|||
let method = '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;
|
||||
const options = toClickOptions(action);
|
||||
const optionsString = formatOptions(options, false);
|
||||
return `${subject}.${this._asLocator(action.selector)}.${method}(${optionsString})`;
|
||||
}
|
||||
|
|
@ -125,7 +107,7 @@ export class PythonLanguageGenerator implements LanguageGenerator {
|
|||
case 'setInputFiles':
|
||||
return `${subject}.${this._asLocator(action.selector)}.set_input_files(${formatValue(action.files.length === 1 ? action.files[0] : action.files)})`;
|
||||
case 'press': {
|
||||
const modifiers = toModifiers(action.modifiers);
|
||||
const modifiers = toKeyboardModifiers(action.modifiers);
|
||||
const shortcut = [...modifiers, action.key].join('+');
|
||||
return `${subject}.${this._asLocator(action.selector)}.press(${quote(shortcut)})`;
|
||||
}
|
||||
50
packages/playwright-core/src/server/codegen/types.ts
Normal file
50
packages/playwright-core/src/server/codegen/types.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -52,7 +52,6 @@ export class DebugController extends SdkObject {
|
|||
initialize(codegenId: string, sdkLanguage: Language) {
|
||||
this._codegenId = codegenId;
|
||||
this._sdkLanguage = sdkLanguage;
|
||||
Recorder.setAppFactory(async () => new InspectingRecorderApp(this));
|
||||
}
|
||||
|
||||
setAutoCloseAllowed(allowed: boolean) {
|
||||
|
|
@ -62,7 +61,6 @@ export class DebugController extends SdkObject {
|
|||
dispose() {
|
||||
this.setReportStateChanged(false);
|
||||
this.setAutoCloseAllowed(false);
|
||||
Recorder.setAppFactory(undefined);
|
||||
}
|
||||
|
||||
setReportStateChanged(enabled: boolean) {
|
||||
|
|
@ -199,7 +197,7 @@ export class DebugController extends SdkObject {
|
|||
const contexts = new Set<BrowserContext>();
|
||||
for (const page of this._playwright.allPages())
|
||||
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[];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"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": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -121,7 +121,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -132,7 +132,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 360,
|
||||
"height": 740
|
||||
|
|
@ -143,7 +143,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 740,
|
||||
"height": 360
|
||||
|
|
@ -154,7 +154,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 320,
|
||||
"height": 658
|
||||
|
|
@ -165,7 +165,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 658,
|
||||
"height": 320
|
||||
|
|
@ -176,7 +176,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 712,
|
||||
"height": 1138
|
||||
|
|
@ -187,7 +187,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 1138,
|
||||
"height": 712
|
||||
|
|
@ -1098,7 +1098,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"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": {
|
||||
"width": 384,
|
||||
"height": 640
|
||||
|
|
@ -1109,7 +1109,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 640,
|
||||
"height": 384
|
||||
|
|
@ -1120,7 +1120,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -1131,7 +1131,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -1142,7 +1142,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -1153,7 +1153,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -1164,7 +1164,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 800,
|
||||
"height": 1280
|
||||
|
|
@ -1175,7 +1175,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 1280,
|
||||
"height": 800
|
||||
|
|
@ -1186,7 +1186,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 384,
|
||||
"height": 640
|
||||
|
|
@ -1197,7 +1197,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 640,
|
||||
"height": 384
|
||||
|
|
@ -1208,7 +1208,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -1219,7 +1219,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -1230,7 +1230,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 412,
|
||||
"height": 732
|
||||
|
|
@ -1241,7 +1241,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 732,
|
||||
"height": 412
|
||||
|
|
@ -1252,7 +1252,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 412,
|
||||
"height": 732
|
||||
|
|
@ -1263,7 +1263,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 732,
|
||||
"height": 412
|
||||
|
|
@ -1274,7 +1274,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 412,
|
||||
"height": 732
|
||||
|
|
@ -1285,7 +1285,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 732,
|
||||
"height": 412
|
||||
|
|
@ -1296,7 +1296,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 600,
|
||||
"height": 960
|
||||
|
|
@ -1307,7 +1307,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 960,
|
||||
"height": 600
|
||||
|
|
@ -1362,7 +1362,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"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": {
|
||||
"width": 411,
|
||||
"height": 731
|
||||
|
|
@ -1373,7 +1373,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 731,
|
||||
"height": 411
|
||||
|
|
@ -1384,7 +1384,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 411,
|
||||
"height": 823
|
||||
|
|
@ -1395,7 +1395,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 823,
|
||||
"height": 411
|
||||
|
|
@ -1406,7 +1406,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 393,
|
||||
"height": 786
|
||||
|
|
@ -1417,7 +1417,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 786,
|
||||
"height": 393
|
||||
|
|
@ -1428,7 +1428,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 353,
|
||||
"height": 745
|
||||
|
|
@ -1439,7 +1439,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 745,
|
||||
"height": 353
|
||||
|
|
@ -1450,7 +1450,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 412,
|
||||
"height": 892
|
||||
|
|
@ -1465,7 +1465,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"height": 892,
|
||||
"width": 412
|
||||
|
|
@ -1480,7 +1480,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 393,
|
||||
"height": 851
|
||||
|
|
@ -1495,7 +1495,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 851,
|
||||
"height": 393
|
||||
|
|
@ -1510,7 +1510,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 412,
|
||||
"height": 915
|
||||
|
|
@ -1525,7 +1525,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 915,
|
||||
"height": 412
|
||||
|
|
@ -1540,7 +1540,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -1551,7 +1551,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -1562,7 +1562,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 1792,
|
||||
"height": 1120
|
||||
|
|
@ -1577,7 +1577,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 1792,
|
||||
"height": 1120
|
||||
|
|
@ -1622,7 +1622,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"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": {
|
||||
"width": 1920,
|
||||
"height": 1080
|
||||
|
|
@ -1637,7 +1637,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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": {
|
||||
"width": 1920,
|
||||
"height": 1080
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import type { Dialog } from '../dialog';
|
|||
import type { ConsoleMessage } from '../console';
|
||||
import { serializeError } from '../errors';
|
||||
import { ElementHandleDispatcher } from './elementHandlerDispatcher';
|
||||
import { RecorderApp } from '../recorder/recorderApp';
|
||||
|
||||
export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channels.BrowserContextChannel, DispatcherScope> implements channels.BrowserContextChannel {
|
||||
_type_EventTarget = true;
|
||||
|
|
@ -291,7 +292,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
|
|||
}
|
||||
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
}
|
||||
|
||||
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);
|
||||
return controller.run(async progress => {
|
||||
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));
|
||||
}
|
||||
|
||||
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[] = [];
|
||||
await this._retryAction(progress, 'select option', async () => {
|
||||
await progress.beforeInputAction(this);
|
||||
|
|
|
|||
|
|
@ -226,7 +226,7 @@ class FFRouteImpl implements network.RouteDelegate {
|
|||
this._request = request;
|
||||
}
|
||||
|
||||
async continue(request: network.Request, overrides: types.NormalizedContinueOverrides) {
|
||||
async continue(overrides: types.NormalizedContinueOverrides) {
|
||||
await this._session.sendMayFail('Network.resumeInterceptedRequest', {
|
||||
requestId: this._request._id,
|
||||
url: overrides.url,
|
||||
|
|
|
|||
|
|
@ -291,8 +291,7 @@ export class FrameManager {
|
|||
if (request._documentId)
|
||||
frame.setPendingDocument({ documentId: request._documentId, request });
|
||||
if (request._isFavicon) {
|
||||
if (route)
|
||||
route.continue(request, { isFallback: true }).catch(() => {});
|
||||
route?.continue({ isFallback: true }).catch(() => {});
|
||||
return;
|
||||
}
|
||||
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 elements = injected.querySelectorAll(info.parsed, root || document);
|
||||
const element: Element | undefined = elements[0];
|
||||
const visible = element ? injected.isVisible(element) : false;
|
||||
const visible = element ? injected.utils.isElementVisible(element) : false;
|
||||
let log = '';
|
||||
if (elements.length > 1) {
|
||||
if (info.strict)
|
||||
|
|
@ -1344,7 +1343,7 @@ export class Frame extends SdkObject {
|
|||
}, 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);
|
||||
return controller.run(async progress => {
|
||||
return await this._retryWithProgressIfNotConnected(progress, selector, options.strict, !options.force /* performLocatorHandlersCheckpoint */, handle => handle._selectOption(progress, elements, values, options));
|
||||
|
|
|
|||
|
|
@ -1,10 +1,21 @@
|
|||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
rules: {
|
||||
"no-restricted-globals": [
|
||||
"error",
|
||||
{ "name": "window" },
|
||||
{ "name": "document" },
|
||||
{ "name": "globalThis" },
|
||||
]
|
||||
}
|
||||
parser: "@typescript-eslint/parser",
|
||||
plugins: ["@typescript-eslint", "notice"],
|
||||
parserOptions: {
|
||||
ecmaVersion: 9,
|
||||
sourceType: "module",
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -216,7 +216,7 @@ export class ClockController {
|
|||
const sinceLastSync = now - this._realTime!.lastSyncTicks;
|
||||
this._realTime!.lastSyncTicks = now;
|
||||
// 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),
|
||||
};
|
||||
}
|
||||
|
|
@ -239,7 +239,12 @@ export class ClockController {
|
|||
|
||||
addTimer(options: { func: TimerHandler, type: TimerType, delay?: number | string, args?: any[] }): number {
|
||||
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');
|
||||
|
||||
let delay = options.delay ? +options.delay : 0;
|
||||
|
|
|
|||
|
|
@ -29,11 +29,13 @@ import type { CSSComplexSelectorList } from '../../utils/isomorphic/cssParser';
|
|||
import { generateSelector, type GenerateSelectorOptions } from './selectorGenerator';
|
||||
import type * as channels from '@protocol/channels';
|
||||
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 { asLocator } 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 };
|
||||
|
||||
|
|
@ -66,7 +68,25 @@ export class InjectedScript {
|
|||
// eslint-disable-next-line no-restricted-globals
|
||||
readonly window: Window & typeof globalThis;
|
||||
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
|
||||
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);
|
||||
}
|
||||
|
||||
isVisible(element: Element): boolean {
|
||||
return isElementVisible(element);
|
||||
}
|
||||
|
||||
async viewportRatio(element: Element): Promise<number> {
|
||||
return await new Promise(resolve => {
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
|
|
@ -567,9 +583,9 @@ export class InjectedScript {
|
|||
}
|
||||
|
||||
if (state === 'visible')
|
||||
return this.isVisible(element);
|
||||
return isElementVisible(element);
|
||||
if (state === 'hidden')
|
||||
return !this.isVisible(element);
|
||||
return !isElementVisible(element);
|
||||
|
||||
const disabled = getAriaDisabled(element);
|
||||
if (state === 'disabled')
|
||||
|
|
@ -1297,16 +1313,15 @@ export class InjectedScript {
|
|||
throw this.createStacklessError('Unknown expect matcher: ' + expression);
|
||||
}
|
||||
|
||||
getElementAccessibleName(element: Element, includeHidden?: boolean): string {
|
||||
return getElementAccessibleName(element, !!includeHidden);
|
||||
generateSimpleDomNode(selector: string): SimpleDomNode | undefined {
|
||||
const element = this.querySelector(this.parseSelector(selector), this.document.documentElement, true);
|
||||
if (!element)
|
||||
return;
|
||||
return generateSimpleDomNode(this, element);
|
||||
}
|
||||
|
||||
getElementAccessibleDescription(element: Element, includeHidden?: boolean): string {
|
||||
return getElementAccessibleDescription(element, !!includeHidden);
|
||||
}
|
||||
|
||||
getAriaRole(element: Element) {
|
||||
return getAriaRole(element);
|
||||
selectorForSimpleDomNodeId(nodeId: string) {
|
||||
return selectorForSimpleDomNodeId(this, nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
# module-level globals will be duplicated, which leads to subtle bugs.
|
||||
[*]
|
||||
|
|
|
|||
|
|
@ -21,9 +21,10 @@ import type { Mode, OverlayState, UIState } from '@recorder/recorderTypes';
|
|||
import type { ElementText } from '../selectorUtils';
|
||||
import type { Highlight, HighlightOptions } from '../highlight';
|
||||
import clipPaths from './clipPaths';
|
||||
import type { SimpleDomNode } from '../simpleDom';
|
||||
|
||||
interface RecorderDelegate {
|
||||
performAction?(action: actions.Action): Promise<void>;
|
||||
performAction?(action: actions.PerformOnRecordAction): Promise<void>;
|
||||
recordAction?(action: actions.Action): Promise<void>;
|
||||
setSelector?(selector: string): Promise<void>;
|
||||
setMode?(mode: Mode): Promise<void>;
|
||||
|
|
@ -168,7 +169,7 @@ class InspectTool implements RecorderTool {
|
|||
if (this._hoveredModel?.tooltipListItemSelected)
|
||||
this._reset(true);
|
||||
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) {
|
||||
if (this._assertVisibility) {
|
||||
this._recorder.delegate.recordAction?.({
|
||||
this._recorder.recordAction({
|
||||
name: 'assertVisible',
|
||||
selector,
|
||||
signals: [],
|
||||
});
|
||||
this._recorder.delegate.setMode?.('recording');
|
||||
this._recorder.setMode('recording');
|
||||
this._recorder.overlay?.flashToolSucceeded('assertingVisibility');
|
||||
} else {
|
||||
this._recorder.delegate.setSelector?.(selector);
|
||||
this._recorder.setSelector(selector);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -338,7 +339,7 @@ class RecordActionTool implements RecorderTool {
|
|||
const target = this._recorder.deepEventTarget(event);
|
||||
|
||||
if (target.nodeName === 'INPUT' && (target as HTMLInputElement).type.toLowerCase() === 'file') {
|
||||
this._recorder.delegate.recordAction?.({
|
||||
this._recorder.recordAction({
|
||||
name: 'setInputFiles',
|
||||
selector: this._activeModel!.selector,
|
||||
signals: [],
|
||||
|
|
@ -348,7 +349,7 @@ class RecordActionTool implements RecorderTool {
|
|||
}
|
||||
|
||||
if (isRangeInput(target)) {
|
||||
this._recorder.delegate.recordAction?.({
|
||||
this._recorder.recordAction({
|
||||
name: 'fill',
|
||||
// must use hoveredModel instead of activeModel for it to work in webkit
|
||||
selector: this._hoveredModel!.selector,
|
||||
|
|
@ -367,7 +368,7 @@ class RecordActionTool implements RecorderTool {
|
|||
// Non-navigating actions are simply recorded by Playwright.
|
||||
if (this._consumedDueWrongTarget(event))
|
||||
return;
|
||||
this._recorder.delegate.recordAction?.({
|
||||
this._recorder.recordAction({
|
||||
name: 'fill',
|
||||
selector: this._activeModel!.selector,
|
||||
signals: [],
|
||||
|
|
@ -483,26 +484,27 @@ class RecordActionTool implements RecorderTool {
|
|||
return true;
|
||||
}
|
||||
|
||||
private async _performAction(action: actions.Action) {
|
||||
private _performAction(action: actions.PerformOnRecordAction) {
|
||||
this._hoveredElement = null;
|
||||
this._hoveredModel = null;
|
||||
this._activeModel = null;
|
||||
this._recorder.updateHighlight(null, false);
|
||||
this._performingAction = true;
|
||||
await this._recorder.delegate.performAction?.(action).catch(() => {});
|
||||
this._performingAction = false;
|
||||
void this._recorder.performAction(action).then(() => {
|
||||
this._performingAction = false;
|
||||
|
||||
// If that was a keyboard action, it similarly requires new selectors for active model.
|
||||
this._onFocus(false);
|
||||
// If that was a keyboard action, it similarly requires new selectors for active model.
|
||||
this._onFocus(false);
|
||||
|
||||
if (this._recorder.injectedScript.isUnderTest) {
|
||||
// Serialize all to string as we cannot attribute console message to isolated world
|
||||
// in Firefox.
|
||||
console.error('Action performed for test: ' + JSON.stringify({ // eslint-disable-line no-console
|
||||
hovered: this._hoveredModel ? (this._hoveredModel as any).selector : null,
|
||||
active: this._activeModel ? (this._activeModel as any).selector : null,
|
||||
}));
|
||||
}
|
||||
if (this._recorder.injectedScript.isUnderTest) {
|
||||
// Serialize all to string as we cannot attribute console message to isolated world
|
||||
// in Firefox.
|
||||
console.error('Action performed for test: ' + JSON.stringify({ // eslint-disable-line no-console
|
||||
hovered: this._hoveredModel ? (this._hoveredModel as any).selector : null,
|
||||
active: this._activeModel ? (this._activeModel as any).selector : null,
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _shouldGenerateKeyPressFor(event: KeyboardEvent): boolean {
|
||||
|
|
@ -613,7 +615,7 @@ class TextAssertionTool implements RecorderTool {
|
|||
|
||||
onKeyDown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape')
|
||||
this._recorder.delegate.setMode?.('recording');
|
||||
this._recorder.setMode('recording');
|
||||
consumeEvent(event);
|
||||
}
|
||||
|
||||
|
|
@ -680,8 +682,8 @@ class TextAssertionTool implements RecorderTool {
|
|||
if (!this._action || !this._dialog.isShowing())
|
||||
return;
|
||||
this._dialog.close();
|
||||
this._recorder.delegate.recordAction?.(this._action);
|
||||
this._recorder.delegate.setMode?.('recording');
|
||||
this._recorder.recordAction(this._action);
|
||||
this._recorder.setMode('recording');
|
||||
}
|
||||
|
||||
private _showDialog() {
|
||||
|
|
@ -726,8 +728,8 @@ class TextAssertionTool implements RecorderTool {
|
|||
const action = this._generateAction();
|
||||
if (!action)
|
||||
return;
|
||||
this._recorder.delegate.recordAction?.(action);
|
||||
this._recorder.delegate.setMode?.('recording');
|
||||
this._recorder.recordAction(action);
|
||||
this._recorder.setMode('recording');
|
||||
this._recorder.overlay?.flashToolSucceeded('assertingValue');
|
||||
}
|
||||
}
|
||||
|
|
@ -799,7 +801,7 @@ class Overlay {
|
|||
this._dragState = { offsetX: this._offsetX, dragStart: { x: (event as MouseEvent).clientX, y: 0 } };
|
||||
}),
|
||||
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', () => {
|
||||
const newMode: Record<Mode, Mode> = {
|
||||
|
|
@ -812,19 +814,19 @@ class Overlay {
|
|||
'assertingVisibility': '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', () => {
|
||||
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', () => {
|
||||
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', () => {
|
||||
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;
|
||||
this._offsetX = Math.max(-halfGapSize, Math.min(halfGapSize, this._offsetX));
|
||||
this._updateVisualPosition();
|
||||
this._recorder.delegate.setOverlayState?.({ offsetX: this._offsetX });
|
||||
this._recorder.setOverlayState({ offsetX: this._offsetX });
|
||||
consumeEvent(event);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -924,9 +926,14 @@ export class Recorder {
|
|||
readonly highlight: Highlight;
|
||||
readonly overlay: Overlay | undefined;
|
||||
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;
|
||||
delegate: RecorderDelegate = {};
|
||||
private _delegate: RecorderDelegate = {};
|
||||
|
||||
constructor(injectedScript: InjectedScript) {
|
||||
this.document = injectedScript.document;
|
||||
|
|
@ -994,7 +1001,7 @@ export class Recorder {
|
|||
}
|
||||
|
||||
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) {
|
||||
// All good.
|
||||
|
|
@ -1155,7 +1162,7 @@ export class Recorder {
|
|||
tooltipText = this.injectedScript.utils.asLocator(this.state.language, model.selector);
|
||||
this.highlight.updateHighlight(model?.elements || [], { ...model, tooltipText });
|
||||
if (userGesture)
|
||||
this.delegate.highlightUpdated?.();
|
||||
this._delegate.highlightUpdated?.();
|
||||
}
|
||||
|
||||
private _ignoreOverlayEvent(event: Event) {
|
||||
|
|
@ -1172,6 +1179,26 @@ export class Recorder {
|
|||
}
|
||||
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 {
|
||||
|
|
@ -1361,8 +1388,8 @@ function createSvgElement(doc: Document, { tagName, attrs, children }: SvgJson):
|
|||
}
|
||||
|
||||
interface Embedder {
|
||||
__pw_recorderPerformAction(action: actions.Action): Promise<void>;
|
||||
__pw_recorderRecordAction(action: actions.Action): Promise<void>;
|
||||
__pw_recorderPerformAction(action: actions.PerformOnRecordAction, simpleDomNode?: SimpleDomNode): Promise<void>;
|
||||
__pw_recorderRecordAction(action: actions.Action, simpleDomNode?: SimpleDomNode): Promise<void>;
|
||||
__pw_recorderState(): Promise<UIState>;
|
||||
__pw_recorderSetSelector(selector: string): 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);
|
||||
}
|
||||
|
||||
async performAction(action: actions.Action) {
|
||||
await this._embedder.__pw_recorderPerformAction(action);
|
||||
async performAction(action: actions.PerformOnRecordAction, simpleDomNode?: SimpleDomNode) {
|
||||
await this._embedder.__pw_recorderPerformAction(action, simpleDomNode);
|
||||
}
|
||||
|
||||
async recordAction(action: actions.Action): Promise<void> {
|
||||
await this._embedder.__pw_recorderRecordAction(action);
|
||||
async recordAction(action: actions.Action, simpleDomNode?: SimpleDomNode): Promise<void> {
|
||||
await this._embedder.__pw_recorderRecordAction(action, simpleDomNode);
|
||||
}
|
||||
|
||||
async setSelector(selector: string): Promise<void> {
|
||||
|
|
|
|||
120
packages/playwright-core/src/server/injected/simpleDom.ts
Normal file
120
packages/playwright-core/src/server/injected/simpleDom.ts
Normal 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>`;
|
||||
}
|
||||
|
|
@ -324,7 +324,7 @@ export class Route extends SdkObject {
|
|||
this._request._setOverrides(overrides);
|
||||
if (!overrides.isFallback)
|
||||
this._request._context.emit(BrowserContext.Events.RequestContinued, this._request);
|
||||
await this._delegate.continue(this._request, overrides);
|
||||
await this._delegate.continue(overrides);
|
||||
this._endHandling();
|
||||
}
|
||||
|
||||
|
|
@ -612,7 +612,7 @@ export class WebSocket extends SdkObject {
|
|||
export interface RouteDelegate {
|
||||
abort(errorCode: string): 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.
|
||||
|
|
|
|||
|
|
@ -14,42 +14,25 @@
|
|||
* 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 { 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 { createGuid, isUnderTest, monotonicTime } from '../utils';
|
||||
import { metadataToCallLog } from './recorder/recorderUtils';
|
||||
import { Debugger } from './debugger';
|
||||
import { EventEmitter } from 'events';
|
||||
import { raceAgainstDeadline } from '../utils/timeoutRunner';
|
||||
import type { Language, LanguageGenerator } from './recorder/language';
|
||||
import * as fs from 'fs';
|
||||
import type { Point } from '../common/types';
|
||||
import * as consoleApiSource from '../generated/consoleApiSource';
|
||||
import { isUnderTest } from '../utils';
|
||||
import { locatorOrSelectorAsSelector } from '../utils/isomorphic/locatorParser';
|
||||
import { quoteCSSAttributeValue, eventsHelper, type RegisteredListener } from '../utils';
|
||||
import type { Dialog } from './dialog';
|
||||
|
||||
type BindingSource = { frame: Frame, page: Page };
|
||||
import { BrowserContext } from './browserContext';
|
||||
import { type Language } from './codegen/types';
|
||||
import { Debugger } from './debugger';
|
||||
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');
|
||||
|
||||
export type RecorderAppFactory = (recorder: Recorder) => Promise<IRecorderApp>;
|
||||
|
||||
export class Recorder implements InstrumentationListener {
|
||||
private _context: BrowserContext;
|
||||
private _mode: Mode;
|
||||
|
|
@ -61,40 +44,38 @@ export class Recorder implements InstrumentationListener {
|
|||
private _userSources = new Map<string, Source>();
|
||||
private _debugger: Debugger;
|
||||
private _contextRecorder: ContextRecorder;
|
||||
private _handleSIGINT: boolean | undefined;
|
||||
private _omitCallTracking = false;
|
||||
private _currentLanguage: Language;
|
||||
|
||||
private static recorderAppFactory: ((recorder: Recorder) => Promise<IRecorderApp>) | undefined;
|
||||
|
||||
static setAppFactory(recorderAppFactory: ((recorder: Recorder) => Promise<IRecorderApp>) | undefined) {
|
||||
Recorder.recorderAppFactory = recorderAppFactory;
|
||||
}
|
||||
|
||||
static showInspector(context: BrowserContext) {
|
||||
static showInspector(context: BrowserContext, recorderAppFactory: RecorderAppFactory) {
|
||||
const params: channels.BrowserContextRecorderSupplementEnableParams = {};
|
||||
if (isUnderTest())
|
||||
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>;
|
||||
if (!recorderPromise) {
|
||||
const recorder = new Recorder(context, params);
|
||||
recorderPromise = recorder.install().then(() => recorder);
|
||||
recorderPromise = Recorder._create(context, recorderAppFactory, params);
|
||||
(context as any)[recorderSymbol] = 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) {
|
||||
this._mode = params.mode || 'none';
|
||||
this._contextRecorder = new ContextRecorder(context, params);
|
||||
this._contextRecorder = new ContextRecorder(context, params, {});
|
||||
this._context = context;
|
||||
this._omitCallTracking = !!params.omitCallTracking;
|
||||
this._debugger = context.debugger();
|
||||
this._handleSIGINT = params.handleSIGINT;
|
||||
context.instrumentation.addListener(this, context);
|
||||
this._currentLanguage = this._contextRecorder.languageName();
|
||||
|
||||
|
|
@ -104,14 +85,7 @@ export class Recorder implements InstrumentationListener {
|
|||
}
|
||||
}
|
||||
|
||||
private static async defaultRecorderAppFactory(recorder: Recorder) {
|
||||
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);
|
||||
private async _install(recorderApp: IRecorderApp) {
|
||||
this._recorderApp = recorderApp;
|
||||
recorderApp.once('close', () => {
|
||||
this._debugger.resume(false);
|
||||
|
|
@ -158,7 +132,7 @@ export class Recorder implements InstrumentationListener {
|
|||
this._context.once(BrowserContext.Events.Close, () => {
|
||||
this._contextRecorder.dispose();
|
||||
this._context.instrumentation.removeListener(this);
|
||||
recorderApp.close().catch(() => {});
|
||||
this._recorderApp?.close().catch(() => {});
|
||||
});
|
||||
this._contextRecorder.on(ContextRecorder.Events.Change, (data: { sources: Source[], primaryFileName: string }) => {
|
||||
this._recorderSources = data.sources;
|
||||
|
|
@ -191,15 +165,8 @@ export class Recorder implements InstrumentationListener {
|
|||
});
|
||||
|
||||
await this._context.exposeBinding('__pw_recorderSetSelector', false, async ({ frame }, selector: string) => {
|
||||
const selectorPromises: Promise<string | undefined>[] = [];
|
||||
let currentFrame: Frame | null = frame;
|
||||
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);
|
||||
const selectorChain = await generateFrameSelector(frame);
|
||||
await this._recorderApp?.setSelector(buildFullSelector(selectorChain, selector), true);
|
||||
});
|
||||
|
||||
await this._context.exposeBinding('__pw_recorderSetMode', false, async ({ frame }, mode: Mode) => {
|
||||
|
|
@ -225,7 +192,7 @@ export class Recorder implements InstrumentationListener {
|
|||
this._pausedStateChanged();
|
||||
this._debugger.on(Debugger.Events.PausedStateChanged, () => this._pausedStateChanged());
|
||||
|
||||
(this._context as any).recorderAppForTest = recorderApp;
|
||||
(this._context as any).recorderAppForTest = this._recorderApp;
|
||||
}
|
||||
|
||||
_pausedStateChanged() {
|
||||
|
|
@ -369,329 +336,8 @@ export class Recorder implements InstrumentationListener {
|
|||
}
|
||||
}
|
||||
|
||||
class ContextRecorder extends EventEmitter {
|
||||
static Events = {
|
||||
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 isScreenshotCommand(metadata: CallMetadata) {
|
||||
return metadata.method.toLowerCase().includes('screenshot');
|
||||
}
|
||||
|
||||
function languageForFile(file: string) {
|
||||
|
|
@ -703,49 +349,3 @@ function languageForFile(file: string) {
|
|||
return 'csharp';
|
||||
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) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
[*]
|
||||
../
|
||||
../codegen/language.ts
|
||||
../codegen/languages.ts
|
||||
../isomorphic/**
|
||||
../registry/**
|
||||
../../common/
|
||||
../../generated/recorderSource.ts
|
||||
../../protocol/
|
||||
../../utils/**
|
||||
../../utilsBundle.ts
|
||||
|
|
|
|||
334
packages/playwright-core/src/server/recorder/contextRecorder.ts
Normal file
334
packages/playwright-core/src/server/recorder/contextRecorder.ts
Normal 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())}]`;
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -37,28 +37,28 @@ export type ActionBase = {
|
|||
signals: Signal[],
|
||||
};
|
||||
|
||||
export type ClickAction = ActionBase & {
|
||||
name: 'click',
|
||||
export type ActionWithSelector = ActionBase & {
|
||||
selector: string,
|
||||
};
|
||||
|
||||
export type ClickAction = ActionWithSelector & {
|
||||
name: 'click',
|
||||
button: 'left' | 'middle' | 'right',
|
||||
modifiers: number,
|
||||
clickCount: number,
|
||||
position?: Point,
|
||||
};
|
||||
|
||||
export type CheckAction = ActionBase & {
|
||||
export type CheckAction = ActionWithSelector & {
|
||||
name: 'check',
|
||||
selector: string,
|
||||
};
|
||||
|
||||
export type UncheckAction = ActionBase & {
|
||||
export type UncheckAction = ActionWithSelector & {
|
||||
name: 'uncheck',
|
||||
selector: string,
|
||||
};
|
||||
|
||||
export type FillAction = ActionBase & {
|
||||
export type FillAction = ActionWithSelector & {
|
||||
name: 'fill',
|
||||
selector: string,
|
||||
text: string,
|
||||
};
|
||||
|
||||
|
|
@ -83,44 +83,39 @@ export type PressAction = ActionBase & {
|
|||
modifiers: number,
|
||||
};
|
||||
|
||||
export type SelectAction = ActionBase & {
|
||||
export type SelectAction = ActionWithSelector & {
|
||||
name: 'select',
|
||||
selector: string,
|
||||
options: string[],
|
||||
};
|
||||
|
||||
export type SetInputFilesAction = ActionBase & {
|
||||
export type SetInputFilesAction = ActionWithSelector & {
|
||||
name: 'setInputFiles',
|
||||
selector: string,
|
||||
files: string[],
|
||||
};
|
||||
|
||||
export type AssertTextAction = ActionBase & {
|
||||
export type AssertTextAction = ActionWithSelector & {
|
||||
name: 'assertText',
|
||||
selector: string,
|
||||
text: string,
|
||||
substring: boolean,
|
||||
};
|
||||
|
||||
export type AssertValueAction = ActionBase & {
|
||||
export type AssertValueAction = ActionWithSelector & {
|
||||
name: 'assertValue',
|
||||
selector: string,
|
||||
value: string,
|
||||
};
|
||||
|
||||
export type AssertCheckedAction = ActionBase & {
|
||||
export type AssertCheckedAction = ActionWithSelector & {
|
||||
name: 'assertChecked',
|
||||
selector: string,
|
||||
checked: boolean,
|
||||
};
|
||||
|
||||
export type AssertVisibleAction = ActionBase & {
|
||||
export type AssertVisibleAction = ActionWithSelector & {
|
||||
name: 'assertVisible',
|
||||
selector: string,
|
||||
};
|
||||
|
||||
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 PerformOnRecordAction = ClickAction | CheckAction | UncheckAction | PressAction | SelectAction;
|
||||
|
||||
// Signals.
|
||||
|
||||
|
|
@ -148,14 +143,3 @@ export type DialogSignal = BaseSignal & {
|
|||
};
|
||||
|
||||
export type Signal = NavigationSignal | PopupSignal | DownloadSignal | DialogSignal;
|
||||
|
||||
type FrameDescriptionMainFrame = {
|
||||
isMainFrame: true;
|
||||
};
|
||||
|
||||
type FrameDescriptionChildFrame = {
|
||||
isMainFrame: false;
|
||||
selectorsChain: string[];
|
||||
};
|
||||
|
||||
export type FrameDescription = { pageAlias: string } & (FrameDescriptionMainFrame | FrameDescriptionChildFrame);
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import type { CallLog, EventData, Mode, Source } from '@recorder/recorderTypes';
|
|||
import { isUnderTest } from '../../utils';
|
||||
import { mime } from '../../utilsBundle';
|
||||
import { syncLocalStorageWithSettings } from '../launchApp';
|
||||
import type { Recorder } from '../recorder';
|
||||
import type { Recorder, RecorderAppFactory } from '../recorder';
|
||||
import type { BrowserContext } from '../browserContext';
|
||||
import { launchApp } from '../launchApp';
|
||||
|
||||
|
|
@ -113,7 +113,15 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
|
|||
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 headed = !!inspectedContext._browser.options.headful;
|
||||
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,
|
||||
headless: !!process.env.PWTEST_CLI_HEADLESS || (isUnderTest() && !headed),
|
||||
useWebSocket: !!process.env.PWTEST_RECORDER_PORT,
|
||||
handleSIGINT,
|
||||
handleSIGINT: false,
|
||||
args: process.env.PWTEST_RECORDER_PORT ? [`--remote-debugging-port=${process.env.PWTEST_RECORDER_PORT}`] : [],
|
||||
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> {
|
||||
if (userGesture) {
|
||||
if (this._recorder.mode() === 'inspecting') {
|
||||
if (this._recorder?.mode() === 'inspecting') {
|
||||
this._recorder.setMode('standby');
|
||||
this._page.bringToFront();
|
||||
} else {
|
||||
this._recorder.setMode('recording');
|
||||
this._recorder?.setMode('recording');
|
||||
}
|
||||
}
|
||||
await this._page.mainFrame().evaluateExpression(((data: { selector: string, userGesture?: boolean }) => {
|
||||
|
|
|
|||
|
|
@ -15,32 +15,19 @@
|
|||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import type { BrowserContextOptions, LaunchOptions } from '../../..';
|
||||
import type { Frame } from '../frames';
|
||||
import type { LanguageGenerator, LanguageGeneratorOptions } from './language';
|
||||
import type { Action, Signal, FrameDescription } from './recorderActions';
|
||||
import type { Signal } from './recorderActions';
|
||||
import type { ActionInContext } from '../codegen/types';
|
||||
|
||||
export type ActionInContext = {
|
||||
frame: FrameDescription;
|
||||
action: Action;
|
||||
committed?: boolean;
|
||||
};
|
||||
|
||||
export class CodeGenerator extends EventEmitter {
|
||||
export class RecorderCollection extends EventEmitter {
|
||||
private _currentAction: ActionInContext | null = null;
|
||||
private _lastAction: ActionInContext | null = null;
|
||||
private _actions: ActionInContext[] = [];
|
||||
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();
|
||||
|
||||
// Make a copy of options to modify them later.
|
||||
launchOptions = { headless: false, ...launchOptions };
|
||||
contextOptions = { ...contextOptions };
|
||||
this._enabled = enabled;
|
||||
this._options = { browserName, launchOptions, contextOptions, deviceName, saveStorage };
|
||||
this.restart();
|
||||
}
|
||||
|
||||
|
|
@ -51,6 +38,10 @@ export class CodeGenerator extends EventEmitter {
|
|||
this.emit('change');
|
||||
}
|
||||
|
||||
actions() {
|
||||
return this._actions;
|
||||
}
|
||||
|
||||
setEnabled(enabled: boolean) {
|
||||
this._enabled = enabled;
|
||||
}
|
||||
|
|
@ -146,7 +137,7 @@ export class CodeGenerator extends EventEmitter {
|
|||
this.addAction({
|
||||
frame: {
|
||||
pageAlias,
|
||||
isMainFrame: true,
|
||||
framePath: [],
|
||||
},
|
||||
committed: true,
|
||||
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 };
|
||||
}
|
||||
}
|
||||
128
packages/playwright-core/src/server/recorder/recorderRunner.ts
Normal file
128
packages/playwright-core/src/server/recorder/recorderRunner.ts
Normal 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);
|
||||
}
|
||||
|
|
@ -16,6 +16,10 @@
|
|||
|
||||
import type { CallMetadata } from '../instrumentation';
|
||||
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 {
|
||||
let title = metadata.apiName || metadata.method;
|
||||
|
|
@ -44,3 +48,27 @@ export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus)
|
|||
};
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -536,7 +536,7 @@ export module Protocol {
|
|||
/**
|
||||
* 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>).
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// or the page was closed. We should tolerate these errors.
|
||||
await this._session.sendMayFail('Network.interceptWithRequest', {
|
||||
|
|
|
|||
29
packages/playwright-core/src/utils/expectUtils.ts
Normal file
29
packages/playwright-core/src/utils/expectUtils.ts
Normal 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,
|
||||
}));
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ export * from './debug';
|
|||
export * from './debugLogger';
|
||||
export * from './env';
|
||||
export * from './eventsHelper';
|
||||
export * from './expectUtils';
|
||||
export * from './fileUtils';
|
||||
export * from './headers';
|
||||
export * from './hostPlatform';
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import path from 'path';
|
|||
|
||||
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 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 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;
|
||||
|
|
|
|||
244
packages/playwright-core/types/protocol.d.ts
vendored
244
packages/playwright-core/types/protocol.d.ts
vendored
|
|
@ -1131,17 +1131,21 @@ using Audits.issueAdded event.
|
|||
}
|
||||
|
||||
/**
|
||||
* Defines commands and events for browser extensions. Available if the client
|
||||
is connected using the --remote-debugging-pipe flag and
|
||||
the --enable-unsafe-extension-debugging flag is set.
|
||||
* Defines commands and events for browser extensions.
|
||||
*/
|
||||
export module Extensions {
|
||||
/**
|
||||
* Storage areas.
|
||||
*/
|
||||
export type StorageArea = "session"|"local"|"sync"|"managed";
|
||||
|
||||
|
||||
/**
|
||||
* Installs an unpacked extension from the filesystem similar to
|
||||
--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 = {
|
||||
/**
|
||||
|
|
@ -1155,6 +1159,81 @@ has been installed.
|
|||
*/
|
||||
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;
|
||||
}
|
||||
/**
|
||||
* CSS position-fallback rule representation.
|
||||
*/
|
||||
export interface CSSPositionFallbackRule {
|
||||
name: Value;
|
||||
/**
|
||||
* List of keyframes.
|
||||
*/
|
||||
tryRules: CSSTryRule[];
|
||||
}
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
|
|
@ -3496,7 +3561,7 @@ front-end.
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
|
@ -3646,6 +3711,13 @@ The property is always undefined now.
|
|||
compatibilityMode?: CompatibilityMode;
|
||||
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.
|
||||
*/
|
||||
|
|
@ -4693,6 +4765,17 @@ File wrapper.
|
|||
export type getFileInfoReturnValue = {
|
||||
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
|
||||
$x functions).
|
||||
|
|
@ -11369,7 +11452,7 @@ as an ad.
|
|||
* All Permissions Policy features. This enum should match the one defined
|
||||
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.
|
||||
*/
|
||||
|
|
@ -11784,7 +11867,7 @@ Example URLs: http://www.google.com/file.html -> "google.com"
|
|||
*/
|
||||
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 interface InstallabilityErrorArgument {
|
||||
/**
|
||||
|
|
@ -12298,6 +12381,10 @@ when bfcache navigation fails.
|
|||
* Frame's new url.
|
||||
*/
|
||||
url: string;
|
||||
/**
|
||||
* Navigation type
|
||||
*/
|
||||
navigationType: "fragment"|"historyApi"|"other";
|
||||
}
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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
|
||||
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.
|
||||
*/
|
||||
|
|
@ -20122,6 +20304,10 @@ Error was thrown.
|
|||
"Audits.checkContrast": Audits.checkContrastParameters;
|
||||
"Audits.checkFormsIssues": Audits.checkFormsIssuesParameters;
|
||||
"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.setAddresses": Autofill.setAddressesParameters;
|
||||
"Autofill.disable": Autofill.disableParameters;
|
||||
|
|
@ -20232,6 +20418,7 @@ Error was thrown.
|
|||
"DOM.setNodeStackTracesEnabled": DOM.setNodeStackTracesEnabledParameters;
|
||||
"DOM.getNodeStackTraces": DOM.getNodeStackTracesParameters;
|
||||
"DOM.getFileInfo": DOM.getFileInfoParameters;
|
||||
"DOM.getDetachedDomNodes": DOM.getDetachedDomNodesParameters;
|
||||
"DOM.setInspectedNode": DOM.setInspectedNodeParameters;
|
||||
"DOM.setNodeName": DOM.setNodeNameParameters;
|
||||
"DOM.setNodeValue": DOM.setNodeValueParameters;
|
||||
|
|
@ -20616,6 +20803,10 @@ Error was thrown.
|
|||
"PWA.launchFilesInApp": PWA.launchFilesInAppParameters;
|
||||
"PWA.openCurrentPageInApp": PWA.openCurrentPageInAppParameters;
|
||||
"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.disable": Console.disableParameters;
|
||||
"Console.enable": Console.enableParameters;
|
||||
|
|
@ -20722,6 +20913,10 @@ Error was thrown.
|
|||
"Audits.checkContrast": Audits.checkContrastReturnValue;
|
||||
"Audits.checkFormsIssues": Audits.checkFormsIssuesReturnValue;
|
||||
"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.setAddresses": Autofill.setAddressesReturnValue;
|
||||
"Autofill.disable": Autofill.disableReturnValue;
|
||||
|
|
@ -20832,6 +21027,7 @@ Error was thrown.
|
|||
"DOM.setNodeStackTracesEnabled": DOM.setNodeStackTracesEnabledReturnValue;
|
||||
"DOM.getNodeStackTraces": DOM.getNodeStackTracesReturnValue;
|
||||
"DOM.getFileInfo": DOM.getFileInfoReturnValue;
|
||||
"DOM.getDetachedDomNodes": DOM.getDetachedDomNodesReturnValue;
|
||||
"DOM.setInspectedNode": DOM.setInspectedNodeReturnValue;
|
||||
"DOM.setNodeName": DOM.setNodeNameReturnValue;
|
||||
"DOM.setNodeValue": DOM.setNodeValueReturnValue;
|
||||
|
|
@ -21216,6 +21412,10 @@ Error was thrown.
|
|||
"PWA.launchFilesInApp": PWA.launchFilesInAppReturnValue;
|
||||
"PWA.openCurrentPageInApp": PWA.openCurrentPageInAppReturnValue;
|
||||
"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.disable": Console.disableReturnValue;
|
||||
"Console.enable": Console.enableReturnValue;
|
||||
|
|
|
|||
178
packages/playwright-core/types/types.d.ts
vendored
178
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -288,41 +288,8 @@ export interface Page {
|
|||
* [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
|
||||
* 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 arg Optional JSON-serializable argument to pass to `script`.
|
||||
* - 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 arg Optional argument to pass to `script` (only supported when passing a function).
|
||||
*/
|
||||
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>;
|
||||
|
||||
/**
|
||||
* 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 options
|
||||
*/
|
||||
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 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.
|
||||
*/
|
||||
|
|
@ -3897,10 +3902,8 @@ export interface Page {
|
|||
force?: boolean;
|
||||
|
||||
/**
|
||||
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You
|
||||
* can opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as
|
||||
* navigating to inaccessible pages. Defaults to `false`.
|
||||
* @deprecated This option will default to `true` in the future.
|
||||
* This option has no effect.
|
||||
* @deprecated This option has no effect.
|
||||
*/
|
||||
noWaitAfter?: boolean;
|
||||
|
||||
|
|
@ -7023,10 +7026,8 @@ export interface Frame {
|
|||
force?: boolean;
|
||||
|
||||
/**
|
||||
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You
|
||||
* can opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as
|
||||
* navigating to inaccessible pages. Defaults to `false`.
|
||||
* @deprecated This option will default to `true` in the future.
|
||||
* This option has no effect.
|
||||
* @deprecated This option has no effect.
|
||||
*/
|
||||
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
|
||||
* 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.
|
||||
* "Incognito" browser contexts don't write any browsing data to disk.
|
||||
* Non-persistent browser contexts don't write any browsing data to disk.
|
||||
*
|
||||
* ```js
|
||||
* // 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)
|
||||
* and [page.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-page#page-add-init-script) 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 `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 arg Optional JSON-serializable argument to pass to `script`.
|
||||
* - 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 arg Optional argument to pass to `script` (only supported when passing a function).
|
||||
*/
|
||||
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 options
|
||||
*/
|
||||
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 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.
|
||||
*
|
||||
|
|
@ -9022,17 +9000,27 @@ export interface BrowserContext {
|
|||
*/
|
||||
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 options
|
||||
*/
|
||||
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 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
|
||||
* following:
|
||||
|
|
@ -11136,10 +11124,8 @@ export interface ElementHandle<T=Node> extends JSHandle<T> {
|
|||
force?: boolean;
|
||||
|
||||
/**
|
||||
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You
|
||||
* can opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as
|
||||
* navigating to inaccessible pages. Defaults to `false`.
|
||||
* @deprecated This option will default to `true` in the future.
|
||||
* This option has no effect.
|
||||
* @deprecated This option has no effect.
|
||||
*/
|
||||
noWaitAfter?: boolean;
|
||||
|
||||
|
|
@ -13331,10 +13317,8 @@ export interface Locator {
|
|||
force?: boolean;
|
||||
|
||||
/**
|
||||
* Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You
|
||||
* can opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as
|
||||
* navigating to inaccessible pages. Defaults to `false`.
|
||||
* @deprecated This option will default to `true` in the future.
|
||||
* This option has no effect.
|
||||
* @deprecated This option has no effect.
|
||||
*/
|
||||
noWaitAfter?: boolean;
|
||||
|
||||
|
|
@ -17336,8 +17320,8 @@ export interface APIResponse {
|
|||
headers(): { [key: string]: string; };
|
||||
|
||||
/**
|
||||
* An array with all the request 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.
|
||||
* 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.
|
||||
*/
|
||||
headersArray(): Array<{
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@
|
|||
"@sveltejs/vite-plugin-svelte": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"svelte": "^4.2.8"
|
||||
"svelte": "^4.2.19"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export interface TestServerInterface {
|
|||
|
||||
installBrowsers(params: {}): Promise<void>;
|
||||
|
||||
runGlobalSetup(params: {}): Promise<{
|
||||
runGlobalSetup(params: { outputDir?: string }): Promise<{
|
||||
report: ReportEntry[],
|
||||
status: reporterTypes.FullResult['status']
|
||||
}>;
|
||||
|
|
@ -81,6 +81,7 @@ export interface TestServerInterface {
|
|||
locations?: string[];
|
||||
grep?: string;
|
||||
grepInvert?: string;
|
||||
outputDir?: string;
|
||||
}): Promise<{
|
||||
report: ReportEntry[],
|
||||
status: reporterTypes.FullResult['status']
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ import {
|
|||
} from '../common/expectBundle';
|
||||
import { zones } from 'playwright-core/lib/utils';
|
||||
import { TestInfoImpl } from '../worker/testInfo';
|
||||
import { ExpectError } from './matcherHint';
|
||||
import { ExpectError, isExpectError } from './matcherHint';
|
||||
|
||||
// #region
|
||||
// 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 reportStepError = (jestError: ExpectError) => {
|
||||
const error = new ExpectError(jestError, customMessage, stackFrames);
|
||||
const reportStepError = (jestError: Error | unknown) => {
|
||||
const error = isExpectError(jestError) ? new ExpectError(jestError, customMessage, stackFrames) : jestError;
|
||||
step.complete({ error });
|
||||
if (this._info.isSoft)
|
||||
testInfo._failWithError(error);
|
||||
|
|
|
|||
|
|
@ -64,3 +64,7 @@ export class ExpectError extends Error {
|
|||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ import { colors } from 'playwright-core/lib/utilsBundle';
|
|||
import { expectTypes, callLogText } from '../util';
|
||||
import { toBeTruthy } from './toBeTruthy';
|
||||
import { toEqual } from './toEqual';
|
||||
import { toExpectedTextValues, toMatchText } from './toMatchText';
|
||||
import { constructURLBasedOnBaseURL, isRegExp, isString, isTextualMimeType, pollAgainstDeadline } from 'playwright-core/lib/utils';
|
||||
import { toMatchText } from './toMatchText';
|
||||
import { constructURLBasedOnBaseURL, isRegExp, isString, isTextualMimeType, pollAgainstDeadline, serializeExpectedTextValues } from 'playwright-core/lib/utils';
|
||||
import { currentTestInfo } from '../common/globals';
|
||||
import { TestInfoImpl } from '../worker/testInfo';
|
||||
import type { ExpectMatcherState } from '../../types/test';
|
||||
|
|
@ -163,12 +163,12 @@ export function toContainText(
|
|||
) {
|
||||
if (Array.isArray(expected)) {
|
||||
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 });
|
||||
}, expected, { ...options, contains: true });
|
||||
} else {
|
||||
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 });
|
||||
}, expected, options);
|
||||
}
|
||||
|
|
@ -181,7 +181,7 @@ export function toHaveAccessibleDescription(
|
|||
options?: { timeout?: number, ignoreCase?: boolean },
|
||||
) {
|
||||
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 });
|
||||
}, expected, options);
|
||||
}
|
||||
|
|
@ -193,7 +193,7 @@ export function toHaveAccessibleName(
|
|||
options?: { timeout?: number, ignoreCase?: boolean },
|
||||
) {
|
||||
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 });
|
||||
}, expected, options);
|
||||
}
|
||||
|
|
@ -218,7 +218,7 @@ export function toHaveAttribute(
|
|||
}, options);
|
||||
}
|
||||
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 });
|
||||
}, expected as (string | RegExp), options);
|
||||
}
|
||||
|
|
@ -231,12 +231,12 @@ export function toHaveClass(
|
|||
) {
|
||||
if (Array.isArray(expected)) {
|
||||
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 });
|
||||
}, expected, options);
|
||||
} else {
|
||||
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 });
|
||||
}, expected, options);
|
||||
}
|
||||
|
|
@ -261,7 +261,7 @@ export function toHaveCSS(
|
|||
options?: { timeout?: number },
|
||||
) {
|
||||
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 });
|
||||
}, expected, options);
|
||||
}
|
||||
|
|
@ -273,7 +273,7 @@ export function toHaveId(
|
|||
options?: { timeout?: number },
|
||||
) {
|
||||
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 });
|
||||
}, expected, options);
|
||||
}
|
||||
|
|
@ -299,7 +299,7 @@ export function toHaveRole(
|
|||
if (!isString(expected))
|
||||
throw new Error(`"role" argument in toHaveRole must be a string`);
|
||||
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 });
|
||||
}, expected, options);
|
||||
}
|
||||
|
|
@ -312,12 +312,12 @@ export function toHaveText(
|
|||
) {
|
||||
if (Array.isArray(expected)) {
|
||||
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 });
|
||||
}, expected, options);
|
||||
} else {
|
||||
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 });
|
||||
}, expected, options);
|
||||
}
|
||||
|
|
@ -330,7 +330,7 @@ export function toHaveValue(
|
|||
options?: { timeout?: number },
|
||||
) {
|
||||
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 });
|
||||
}, expected, options);
|
||||
}
|
||||
|
|
@ -342,7 +342,7 @@ export function toHaveValues(
|
|||
options?: { timeout?: number },
|
||||
) {
|
||||
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 });
|
||||
}, expected, options);
|
||||
}
|
||||
|
|
@ -355,7 +355,7 @@ export function toHaveTitle(
|
|||
) {
|
||||
const locator = page.locator(':root') as LocatorEx;
|
||||
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 });
|
||||
}, expected, options);
|
||||
}
|
||||
|
|
@ -370,7 +370,7 @@ export function toHaveURL(
|
|||
expected = typeof expected === 'string' ? constructURLBasedOnBaseURL(baseURL, expected) : expected;
|
||||
const locator = page.locator(':root') as LocatorEx;
|
||||
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 });
|
||||
}, expected, options);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,8 +15,6 @@
|
|||
*/
|
||||
|
||||
|
||||
import type { ExpectedTextValue } from '@protocol/channels';
|
||||
import { isRegExp, isString } from 'playwright-core/lib/utils';
|
||||
import { expectTypes, callLogText } from '../util';
|
||||
import {
|
||||
printReceivedStringContainExpectedResult,
|
||||
|
|
@ -95,14 +93,3 @@ export async function toMatchText(
|
|||
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,
|
||||
}));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -148,7 +148,10 @@ export class TestServerDispatcher implements TestServerInterface {
|
|||
async runGlobalSetup(params: Parameters<TestServerInterface['runGlobalSetup']>[0]): ReturnType<TestServerInterface['runGlobalSetup']> {
|
||||
await this.runGlobalTeardown();
|
||||
|
||||
const { config, error } = await this._loadConfig();
|
||||
const overrides: ConfigCLIOverrides = {
|
||||
outputDir: params.outputDir,
|
||||
};
|
||||
const { config, error } = await this._loadConfig(overrides);
|
||||
if (!config) {
|
||||
const { reporter, report } = await this._collectingInternalReporter();
|
||||
// Produce dummy config when it has an error.
|
||||
|
|
@ -256,6 +259,7 @@ export class TestServerDispatcher implements TestServerInterface {
|
|||
const overrides: ConfigCLIOverrides = {
|
||||
repeatEach: 1,
|
||||
retries: 0,
|
||||
outputDir: params.outputDir,
|
||||
};
|
||||
const { config, error } = await this._loadConfig(overrides);
|
||||
if (!config) {
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export async function detectChangedTestFiles(baseCommit: string, configDir: stri
|
|||
|
||||
const unknownRevision = error.output.some(line => line?.includes('unknown revision'));
|
||||
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) {
|
||||
throw new Error([
|
||||
`The repository is a shallow clone and does not have '${baseCommit}' available locally.`,
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import type { Attachment } from './testTracing';
|
|||
import type { StackFrame } from '@protocol/channels';
|
||||
|
||||
export interface TestStepInternal {
|
||||
complete(result: { error?: Error, attachments?: Attachment[] }): void;
|
||||
complete(result: { error?: Error | unknown, attachments?: Attachment[] }): void;
|
||||
stepId: string;
|
||||
title: string;
|
||||
category: 'hook' | 'fixture' | 'test.step' | 'expect' | 'attach' | string;
|
||||
|
|
@ -270,7 +270,7 @@ export class TestInfoImpl implements TestInfo {
|
|||
|
||||
step.endWallTime = Date.now();
|
||||
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;
|
||||
const error = serializeError(result.error);
|
||||
if (data.boxedStack)
|
||||
|
|
@ -327,13 +327,13 @@ export class TestInfoImpl implements TestInfo {
|
|||
this.status = 'interrupted';
|
||||
}
|
||||
|
||||
_failWithError(error: Error) {
|
||||
_failWithError(error: Error | unknown) {
|
||||
if (this.status === 'passed' || this.status === 'skipped')
|
||||
this.status = error instanceof TimeoutManagerError ? 'timedOut' : 'failed';
|
||||
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)
|
||||
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._tracing.appendForError(serialized);
|
||||
}
|
||||
|
|
|
|||
18
packages/playwright/types/test.d.ts
vendored
18
packages/playwright/types/test.d.ts
vendored
|
|
@ -1825,8 +1825,10 @@ type TestDetailsAnnotation = {
|
|||
description?: string;
|
||||
};
|
||||
|
||||
type TestDetailsTag = `@${string}`;
|
||||
|
||||
export type TestDetails = {
|
||||
tag?: string | string[];
|
||||
tag?: TestDetailsTag | TestDetailsTag[];
|
||||
annotation?: TestDetailsAnnotation | TestDetailsAnnotation[];
|
||||
}
|
||||
|
||||
|
|
@ -6565,15 +6567,17 @@ type MakeMatchers<R, T, ExtendedMatchers> = {
|
|||
rejects: MakeMatchers<Promise<R>, any, ExtendedMatchers>;
|
||||
} & 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 = {}> = {
|
||||
<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> & {
|
||||
/**
|
||||
* If you know how to test something, `.not` lets you test its opposite.
|
||||
*/
|
||||
not: BaseMatchers<Promise<void>, T>;
|
||||
};
|
||||
poll: <T = unknown>(actual: () => T | Promise<T>, messageOrOptions?: string | { message?: string, timeout?: number, intervals?: number[] }) => PollMatchers<Promise<void>, T, ExtendedMatchers>;
|
||||
extend<MoreMatchers extends Record<string, (this: ExpectMatcherState, receiver: any, ...args: any[]) => MatcherReturnType | Promise<MatcherReturnType>>>(matchers: MoreMatchers): Expect<ExtendedMatchers & MoreMatchers>;
|
||||
configure: (configuration: {
|
||||
message?: string,
|
||||
|
|
|
|||
|
|
@ -1753,7 +1753,6 @@ export type BrowserContextRecorderSupplementEnableParams = {
|
|||
device?: string,
|
||||
saveStorage?: string,
|
||||
outputFile?: string,
|
||||
handleSIGINT?: boolean,
|
||||
omitCallTracking?: boolean,
|
||||
};
|
||||
export type BrowserContextRecorderSupplementEnableOptions = {
|
||||
|
|
@ -1766,7 +1765,6 @@ export type BrowserContextRecorderSupplementEnableOptions = {
|
|||
device?: string,
|
||||
saveStorage?: string,
|
||||
outputFile?: string,
|
||||
handleSIGINT?: boolean,
|
||||
omitCallTracking?: boolean,
|
||||
};
|
||||
export type BrowserContextRecorderSupplementEnableResult = void;
|
||||
|
|
@ -2939,7 +2937,6 @@ export type FrameSelectOptionParams = {
|
|||
}[],
|
||||
force?: boolean,
|
||||
timeout?: number,
|
||||
noWaitAfter?: boolean,
|
||||
};
|
||||
export type FrameSelectOptionOptions = {
|
||||
strict?: boolean,
|
||||
|
|
@ -2952,7 +2949,6 @@ export type FrameSelectOptionOptions = {
|
|||
}[],
|
||||
force?: boolean,
|
||||
timeout?: number,
|
||||
noWaitAfter?: boolean,
|
||||
};
|
||||
export type FrameSelectOptionResult = {
|
||||
values: string[],
|
||||
|
|
@ -3555,7 +3551,6 @@ export type ElementHandleSelectOptionParams = {
|
|||
}[],
|
||||
force?: boolean,
|
||||
timeout?: number,
|
||||
noWaitAfter?: boolean,
|
||||
};
|
||||
export type ElementHandleSelectOptionOptions = {
|
||||
elements?: ElementHandleChannel[],
|
||||
|
|
@ -3567,7 +3562,6 @@ export type ElementHandleSelectOptionOptions = {
|
|||
}[],
|
||||
force?: boolean,
|
||||
timeout?: number,
|
||||
noWaitAfter?: boolean,
|
||||
};
|
||||
export type ElementHandleSelectOptionResult = {
|
||||
values: string[],
|
||||
|
|
|
|||
|
|
@ -1189,7 +1189,6 @@ BrowserContext:
|
|||
device: string?
|
||||
saveStorage: string?
|
||||
outputFile: string?
|
||||
handleSIGINT: boolean?
|
||||
omitCallTracking: boolean?
|
||||
|
||||
newCDPSession:
|
||||
|
|
@ -2185,7 +2184,6 @@ Frame:
|
|||
index: number?
|
||||
force: boolean?
|
||||
timeout: number?
|
||||
noWaitAfter: boolean?
|
||||
returns:
|
||||
values:
|
||||
type: array
|
||||
|
|
@ -2741,7 +2739,6 @@ ElementHandle:
|
|||
index: number?
|
||||
force: boolean?
|
||||
timeout: number?
|
||||
noWaitAfter: boolean?
|
||||
returns:
|
||||
values:
|
||||
type: array
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ const RequestTab: React.FunctionComponent<{
|
|||
React.useEffect(() => {
|
||||
const readResources = async () => {
|
||||
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 : '';
|
||||
if (resource.request.postData._sha1) {
|
||||
const response = await fetch(`sha1/${resource.request.postData._sha1}`);
|
||||
|
|
|
|||
|
|
@ -205,12 +205,14 @@ export const UIModeView: React.FC<{}> = ({
|
|||
interceptStdio: true,
|
||||
watchTestDirs: true
|
||||
});
|
||||
const { status, report } = await testServerConnection.runGlobalSetup({});
|
||||
const { status, report } = await testServerConnection.runGlobalSetup({
|
||||
outputDir: queryParams.outputDir,
|
||||
});
|
||||
teleSuiteUpdater.processGlobalReport(report);
|
||||
if (status !== 'passed')
|
||||
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);
|
||||
|
||||
testServerConnection.onReport(params => {
|
||||
|
|
@ -333,7 +335,7 @@ export const UIModeView: React.FC<{}> = ({
|
|||
commandQueue.current = commandQueue.current.then(async () => {
|
||||
setIsLoading(true);
|
||||
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);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ export const Workbench: React.FunctionComponent<{
|
|||
setRevealedStack(action?.stack);
|
||||
}, [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(() => {
|
||||
setSelectedTime(undefined);
|
||||
|
|
@ -179,9 +179,17 @@ export const Workbench: React.FunctionComponent<{
|
|||
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 = {
|
||||
id: 'source',
|
||||
title: 'Source',
|
||||
errorCount: fallbackSourceErrorCount,
|
||||
render: () => <SourceTab
|
||||
stack={revealedStack}
|
||||
sources={sources}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
18
tests/assets/modernizr/README.md
Normal file
18
tests/assets/modernizr/README.md
Normal 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.
|
||||
26
tests/assets/modernizr/index.html
Normal file
26
tests/assets/modernizr/index.html
Normal 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>
|
||||
|
|
@ -11,6 +11,249 @@
|
|||
"required": 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": {
|
||||
"search": true,
|
||||
"tel": true,
|
||||
|
|
@ -26,278 +269,140 @@
|
|||
"range": true,
|
||||
"color": true
|
||||
},
|
||||
"htmlimports": false,
|
||||
"history": true,
|
||||
"ie8compat": false,
|
||||
"applicationcache": false,
|
||||
"blobconstructor": true,
|
||||
"blob-constructor": true,
|
||||
"cookies": true,
|
||||
"cors": true,
|
||||
"customelements": true,
|
||||
"customprotocolhandler": false,
|
||||
"customevent": true,
|
||||
"dataview": true,
|
||||
"eventlistener": true,
|
||||
"geolocation": true,
|
||||
"json": true,
|
||||
"formvalidation": true,
|
||||
"localizednumber": false,
|
||||
"inputsearchevent": false,
|
||||
"placeholder": true,
|
||||
"requestautocomplete": false,
|
||||
"intl": true,
|
||||
"ligatures": true,
|
||||
"olreversed": true,
|
||||
"mathml": true,
|
||||
"mediasource": false,
|
||||
"hovermq": false,
|
||||
"pointermq": 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,
|
||||
"effectivetype": false,
|
||||
"lowbandwidth": false,
|
||||
"eventsource": 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,
|
||||
"xhrresponsetypeblob": true,
|
||||
"xhrresponsetypedocument": true,
|
||||
"xhrresponsetypejson": true,
|
||||
"xhrresponsetypetext": true,
|
||||
"svgclippaths": true,
|
||||
"svgforeignobject": true,
|
||||
"smil": true,
|
||||
"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,
|
||||
"xhrresponsetype": true,
|
||||
"xhr2": true,
|
||||
"notification": false,
|
||||
"pagevisibility": true,
|
||||
"performance": true,
|
||||
"pointerlock": false,
|
||||
"quotamanagement": false,
|
||||
"postmessage": {
|
||||
"structuredclones": true
|
||||
},
|
||||
"proxy": true,
|
||||
"queryselector": true,
|
||||
"prefetch": false,
|
||||
"requestanimationframe": true,
|
||||
"raf": true,
|
||||
"vibrate": false,
|
||||
"webintents": false,
|
||||
"lowbattery": false,
|
||||
"getrandomvalues": true,
|
||||
"backgroundblendmode": true,
|
||||
"objectfit": true,
|
||||
"object-fit": true,
|
||||
"regions": false,
|
||||
"wrapflow": false,
|
||||
"scriptasync": true,
|
||||
"scriptdefer": true,
|
||||
"scrolltooptions": true,
|
||||
"serviceworker": true,
|
||||
"speechrecognition": true,
|
||||
"filesystem": false,
|
||||
"requestautocomplete": false,
|
||||
"speechsynthesis": true,
|
||||
"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,
|
||||
"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,
|
||||
"datachannel": false,
|
||||
"datachannel": true,
|
||||
"getusermedia": true,
|
||||
"mediastream": true,
|
||||
"websockets": true,
|
||||
"websocketsbinary": true,
|
||||
"atobbtoa": true,
|
||||
"atob-btoa": true,
|
||||
"framed": false,
|
||||
"matchmedia": true,
|
||||
"ligatures": true,
|
||||
"cssanimations": true,
|
||||
"csspseudoanimations": true,
|
||||
"appearance": true,
|
||||
"backdropfilter": true,
|
||||
"backgroundcliptext": true,
|
||||
"bgpositionxy": true,
|
||||
"bgrepeatround": true,
|
||||
"bgrepeatspace": true,
|
||||
"backgroundsize": 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
|
||||
"pushmanager": false,
|
||||
"resizeobserver": true,
|
||||
"workertypeoption": true,
|
||||
"sharedworkers": true,
|
||||
"webworkers": true,
|
||||
"transferables": true,
|
||||
"xdomainrequest": false,
|
||||
"devicemotion2": true,
|
||||
"deviceorientation2": true,
|
||||
"deviceorientation3": true
|
||||
}
|
||||
4147
tests/assets/modernizr/modernizr.js
Normal file
4147
tests/assets/modernizr/modernizr.js
Normal file
File diff suppressed because one or more lines are too long
18
tests/assets/modernizr/roll.sh
Normal file
18
tests/assets/modernizr/roll.sh
Normal 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
|
||||
|
|
@ -11,6 +11,249 @@
|
|||
"required": 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": {
|
||||
"search": true,
|
||||
"tel": true,
|
||||
|
|
@ -26,278 +269,148 @@
|
|||
"range": true,
|
||||
"color": true
|
||||
},
|
||||
"htmlimports": false,
|
||||
"history": true,
|
||||
"ie8compat": false,
|
||||
"applicationcache": false,
|
||||
"blobconstructor": true,
|
||||
"blob-constructor": true,
|
||||
"cookies": true,
|
||||
"cors": true,
|
||||
"customelements": true,
|
||||
"customprotocolhandler": false,
|
||||
"customevent": true,
|
||||
"dataview": true,
|
||||
"eventlistener": true,
|
||||
"geolocation": true,
|
||||
"json": true,
|
||||
"formvalidation": true,
|
||||
"localizednumber": false,
|
||||
"inputsearchevent": false,
|
||||
"placeholder": true,
|
||||
"requestautocomplete": false,
|
||||
"intl": true,
|
||||
"ligatures": true,
|
||||
"olreversed": true,
|
||||
"mathml": true,
|
||||
"mediasource": true,
|
||||
"hovermq": true,
|
||||
"pointermq": 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,
|
||||
"effectivetype": false,
|
||||
"lowbandwidth": false,
|
||||
"eventsource": 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,
|
||||
"xhrresponsetypeblob": true,
|
||||
"xhrresponsetypedocument": true,
|
||||
"xhrresponsetypejson": true,
|
||||
"xhrresponsetypetext": true,
|
||||
"svgclippaths": true,
|
||||
"svgforeignobject": true,
|
||||
"smil": 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,
|
||||
"xhrresponsetype": true,
|
||||
"xhr2": true,
|
||||
"notification": true,
|
||||
"pagevisibility": true,
|
||||
"performance": true,
|
||||
"pointerlock": true,
|
||||
"quotamanagement": false,
|
||||
"postmessage": {
|
||||
"structuredclones": true
|
||||
},
|
||||
"proxy": true,
|
||||
"queryselector": true,
|
||||
"prefetch": false,
|
||||
"requestanimationframe": true,
|
||||
"raf": true,
|
||||
"vibrate": false,
|
||||
"webintents": false,
|
||||
"lowbattery": false,
|
||||
"getrandomvalues": true,
|
||||
"backgroundblendmode": true,
|
||||
"objectfit": true,
|
||||
"object-fit": true,
|
||||
"regions": false,
|
||||
"wrapflow": false,
|
||||
"scriptasync": true,
|
||||
"scriptdefer": true,
|
||||
"scrolltooptions": false,
|
||||
"serviceworker": true,
|
||||
"speechrecognition": true,
|
||||
"filesystem": false,
|
||||
"requestautocomplete": false,
|
||||
"speechsynthesis": true,
|
||||
"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,
|
||||
"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,
|
||||
"datachannel": false,
|
||||
"datachannel": true,
|
||||
"getusermedia": true,
|
||||
"mediastream": true,
|
||||
"websockets": true,
|
||||
"websocketsbinary": true,
|
||||
"atobbtoa": true,
|
||||
"atob-btoa": true,
|
||||
"framed": false,
|
||||
"matchmedia": true,
|
||||
"ligatures": true,
|
||||
"cssanimations": true,
|
||||
"csspseudoanimations": true,
|
||||
"appearance": true,
|
||||
"backdropfilter": true,
|
||||
"backgroundcliptext": true,
|
||||
"bgpositionxy": true,
|
||||
"bgrepeatround": true,
|
||||
"bgrepeatspace": true,
|
||||
"backgroundsize": 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
|
||||
"pushmanager": true,
|
||||
"resizeobserver": true,
|
||||
"workertypeoption": true,
|
||||
"sharedworkers": true,
|
||||
"webworkers": true,
|
||||
"transferables": true,
|
||||
"xdomainrequest": false,
|
||||
"devicemotion2": true,
|
||||
"deviceorientation2": false,
|
||||
"deviceorientation3": true
|
||||
}
|
||||
|
|
@ -13,6 +13,25 @@
|
|||
</style>
|
||||
<script src='script.js'></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>
|
||||
<body>
|
||||
<h1>Network Tab Test</h1>
|
||||
|
|
|
|||
|
|
@ -59,10 +59,8 @@ const test = baseTest.extend<BrowserTestTestFixtures, BrowserTestWorkerFixtures>
|
|||
}, { scope: 'worker' }],
|
||||
|
||||
allowsThirdParty: [async ({ browserName, browserMajorVersion, channel }, run) => {
|
||||
if (browserName === 'firefox' && !channel)
|
||||
await run(browserMajorVersion >= 103);
|
||||
else if (browserName === 'firefox' && channel === 'firefox-beta')
|
||||
await run(browserMajorVersion < 103 || browserMajorVersion >= 110);
|
||||
if (browserName === 'firefox')
|
||||
await run(true);
|
||||
else
|
||||
await run(false);
|
||||
}, { scope: 'worker' }],
|
||||
|
|
@ -74,10 +72,8 @@ const test = baseTest.extend<BrowserTestTestFixtures, BrowserTestWorkerFixtures>
|
|||
await run('Lax');
|
||||
else if (browserName === 'webkit' && !isLinux)
|
||||
await run('None');
|
||||
else if (browserName === 'firefox' && channel === 'firefox-beta')
|
||||
await run(browserMajorVersion >= 103 && browserMajorVersion < 110 ? 'Lax' : 'None');
|
||||
else if (browserName === 'firefox' && channel !== 'firefox-beta')
|
||||
await run(browserMajorVersion >= 103 ? 'None' : 'Lax');
|
||||
else if (browserName === 'firefox')
|
||||
await run('None');
|
||||
else
|
||||
throw new Error('unknown browser - ' + browserName);
|
||||
}, { scope: 'worker' }],
|
||||
|
|
|
|||
|
|
@ -141,7 +141,6 @@ it.describe('should proxy local network requests', () => {
|
|||
|
||||
it('should use ipv6 proxy', async ({ contextFactory, server, proxyServer, browserName }) => {
|
||||
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);
|
||||
const context = await contextFactory({
|
||||
proxy: { server: `[0:0:0:0:0:0:0:1]:${proxyServer.PORT}` }
|
||||
|
|
|
|||
|
|
@ -29,4 +29,12 @@ it.describe('block', () => {
|
|||
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([]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import os from 'os';
|
||||
import { browserTest as it, expect } from '../config/browserTest';
|
||||
|
||||
it.describe('mobile viewport', () => {
|
||||
|
|
@ -55,23 +54,19 @@ it.describe('mobile viewport', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('should be detectable by Modernizr', async ({ playwright, browser, server, browserName, platform }) => {
|
||||
it.skip(browserName === 'webkit' && platform === 'darwin' && parseInt(os.release(), 10) === 22, 'detect-touch.html uses Modernizr which uses WebGL. WebGL is not available in macOS-13 - https://bugs.webkit.org/show_bug.cgi?id=278277');
|
||||
it('should be detectable', async ({ playwright, browser, server, browserName, platform }) => {
|
||||
const iPhone = playwright.devices['iPhone 6'];
|
||||
const context = await browser.newContext({ ...iPhone });
|
||||
const page = await context.newPage();
|
||||
await page.goto(server.PREFIX + '/detect-touch.html');
|
||||
expect(await page.evaluate(() => document.body.textContent!.trim())).toBe('YES');
|
||||
expect(await page.evaluate(() => 'ontouchstart' in window || !!window.TouchEvent)).toBe(true);
|
||||
await context.close();
|
||||
});
|
||||
|
||||
it('should detect touch when applying viewport with touches', async ({ browser, server, browserName, platform }) => {
|
||||
it.skip(browserName === 'webkit' && platform === 'darwin' && parseInt(os.release(), 10) === 22, 'Modernizr uses WebGL. WebGL is not available in macOS-13 - https://bugs.webkit.org/show_bug.cgi?id=278277');
|
||||
const context = await browser.newContext({ viewport: { width: 800, height: 600 }, hasTouch: true });
|
||||
const page = await context.newPage();
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.addScriptTag({ url: server.PREFIX + '/modernizr.js' });
|
||||
expect(await page.evaluate(() => (window as any)['Modernizr'].touchevents)).toBe(true);
|
||||
expect(await page.evaluate(() => 'ontouchstart' in window || !!window.TouchEvent)).toBe(true);
|
||||
await context.close();
|
||||
});
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue