Merge branch 'main' into tar-download-3rd-party-lib

This commit is contained in:
Simon Knott 2025-01-21 11:42:52 +01:00
commit 746bb68096
No known key found for this signature in database
GPG key ID: 8CEDC00028084AEC
129 changed files with 1534 additions and 1819 deletions

View file

@ -1,6 +1,6 @@
# 🎭 Playwright
[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-132.0.6834.57-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-133.0.3-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[![WebKit version](https://img.shields.io/badge/webkit-18.2-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop --> [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord)
[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-133.0.6943.16-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-134.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[![WebKit version](https://img.shields.io/badge/webkit-18.2-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop --> [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord)
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
@ -8,9 +8,9 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
| | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->132.0.6834.57<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Chromium <!-- GEN:chromium-version -->133.0.6943.16<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->18.2<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Firefox <!-- GEN:firefox-version -->133.0.3<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Firefox <!-- GEN:firefox-version -->134.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
Headless execution is supported for all browsers on all platforms. Check out [system requirements](https://playwright.dev/docs/intro#system-requirements) for details.

View file

@ -155,7 +155,7 @@ Additional locator to match.
- returns: <[string]>
Captures the aria snapshot of the given element.
Read more about [aria snapshots](../aria-snapshots.md) and [`method: LocatorAssertions.toMatchAriaSnapshot#2`] for the corresponding assertion.
Read more about [aria snapshots](../aria-snapshots.md) and [`method: LocatorAssertions.toMatchAriaSnapshot#1`] for the corresponding assertion.
**Usage**

View file

@ -446,7 +446,7 @@ Expected options currently selected.
* since: v1.49
* langs: python
The opposite of [`method: LocatorAssertions.toMatchAriaSnapshot#2`].
The opposite of [`method: LocatorAssertions.toMatchAriaSnapshot#1`].
### param: LocatorAssertions.NotToMatchAriaSnapshot.expected
* since: v1.49
@ -1313,7 +1313,7 @@ await Expect(locator).ToHaveAccessibleNameAsync("Save to disk");
### param: LocatorAssertions.toHaveAccessibleName.name
* since: v1.44
- `name` <[string]|[RegExp]|[Array]<[string]|[RegExp]>>
- `name` <[string]|[RegExp]>
Expected accessible name.
@ -1413,49 +1413,48 @@ Attribute name.
* langs:
- alias-java: hasClass
Ensures the [Locator] points to an element with given CSS classes. This needs to be a full match
or using a relaxed regular expression.
Ensures the [Locator] points to an element with given CSS classes. When a string is provided, it must fully match the element's `class` attribute. To match individual classes or perform partial matches, use a regular expression:
**Usage**
```html
<div class='selected row' id='component'></div>
<div class='middle selected row' id='component'></div>
```
```js
const locator = page.locator('#component');
await expect(locator).toHaveClass(/selected/);
await expect(locator).toHaveClass('selected row');
await expect(locator).toHaveClass('middle selected row');
await expect(locator).toHaveClass(/(^|\s)selected(\s|$)/);
```
```java
assertThat(page.locator("#component")).hasClass(Pattern.compile("selected"));
assertThat(page.locator("#component")).hasClass("selected row");
assertThat(page.locator("#component")).hasClass(Pattern.compile("(^|\\s)selected(\\s|$)"));
assertThat(page.locator("#component")).hasClass("middle selected row");
```
```python async
from playwright.async_api import expect
locator = page.locator("#component")
await expect(locator).to_have_class(re.compile(r"selected"))
await expect(locator).to_have_class("selected row")
await expect(locator).to_have_class(re.compile(r"(^|\\s)selected(\\s|$)"))
await expect(locator).to_have_class("middle selected row")
```
```python sync
from playwright.sync_api import expect
locator = page.locator("#component")
expect(locator).to_have_class(re.compile(r"selected"))
expect(locator).to_have_class("selected row")
expect(locator).to_have_class(re.compile(r"(^|\\s)selected(\\s|$)"))
expect(locator).to_have_class("middle selected row")
```
```csharp
var locator = Page.Locator("#component");
await Expect(locator).ToHaveClassAsync(new Regex("selected"));
await Expect(locator).ToHaveClassAsync("selected row");
await Expect(locator).ToHaveClassAsync(new Regex("(^|\\s)selected(\\s|$)"));
await Expect(locator).ToHaveClassAsync("middle selected row");
```
Note that if array is passed as an expected value, entire lists of elements can be asserted:
When an array is passed, the method asserts that the list of elements located matches the corresponding list of expected class values. Each element's class attribute is matched against the corresponding string or regular expression in the array:
```js
const locator = page.locator('list > .component');
@ -2182,57 +2181,6 @@ Expected options currently selected.
## async method: LocatorAssertions.toMatchAriaSnapshot#1
* since: v1.50
* langs:
- alias-java: matchesAriaSnapshot
Asserts that the target element matches the given [accessibility snapshot](../aria-snapshots.md).
**Usage**
```js
await expect(page.locator('body')).toMatchAriaSnapshot();
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'snapshot' });
await expect(page.locator('body')).toMatchAriaSnapshot({ path: '/path/to/snapshot.yml' });
```
```python async
await expect(page.locator('body')).to_match_aria_snapshot(path='/path/to/snapshot.yml')
```
```python sync
expect(page.locator('body')).to_match_aria_snapshot(path='/path/to/snapshot.yml')
```
```csharp
await Expect(page.Locator("body")).ToMatchAriaSnapshotAsync(new { Path = "/path/to/snapshot.yml" });
```
```java
assertThat(page.locator("body")).matchesAriaSnapshot(new LocatorAssertions.MatchesAriaSnapshotOptions().setPath("/path/to/snapshot.yml"));
```
### option: LocatorAssertions.toMatchAriaSnapshot#1.name
* since: v1.50
* langs: js
- `name` <[string]>
Name of the snapshot to store in the snapshot folder corresponding to this test. Generates ordinal name if not specified.
### option: LocatorAssertions.toMatchAriaSnapshot#1.path
* since: v1.50
- `path` <[string]>
Path to the YAML snapshot file.
### option: LocatorAssertions.toMatchAriaSnapshot#1.timeout = %%-js-assertions-timeout-%%
* since: v1.50
### option: LocatorAssertions.toMatchAriaSnapshot#1.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.50
## async method: LocatorAssertions.toMatchAriaSnapshot#2
* since: v1.49
* langs:
- alias-java: matchesAriaSnapshot
@ -2281,12 +2229,56 @@ assertThat(page.locator("body")).matchesAriaSnapshot("""
""");
```
### param: LocatorAssertions.toMatchAriaSnapshot#2.expected
### param: LocatorAssertions.toMatchAriaSnapshot#1.expected
* since: v1.49
- `expected` <string>
### option: LocatorAssertions.toMatchAriaSnapshot#2.timeout = %%-js-assertions-timeout-%%
### option: LocatorAssertions.toMatchAriaSnapshot#1.timeout = %%-js-assertions-timeout-%%
* since: v1.49
### option: LocatorAssertions.toMatchAriaSnapshot#2.timeout = %%-csharp-java-python-assertions-timeout-%%
### option: LocatorAssertions.toMatchAriaSnapshot#1.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.49
## async method: LocatorAssertions.toMatchAriaSnapshot#2
* since: v1.50
* langs: js
Asserts that the target element matches the given [accessibility snapshot](../aria-snapshots.md).
**Usage**
```js
await expect(page.locator('body')).toMatchAriaSnapshot();
await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'snapshot' });
await expect(page.locator('body')).toMatchAriaSnapshot({ path: '/path/to/snapshot.yml' });
```
```python async
await expect(page.locator('body')).to_match_aria_snapshot(path='/path/to/snapshot.yml')
```
```python sync
expect(page.locator('body')).to_match_aria_snapshot(path='/path/to/snapshot.yml')
```
```csharp
await Expect(page.Locator("body")).ToMatchAriaSnapshotAsync(new { Path = "/path/to/snapshot.yml" });
```
```java
assertThat(page.locator("body")).matchesAriaSnapshot(new LocatorAssertions.MatchesAriaSnapshotOptions().setPath("/path/to/snapshot.yml"));
```
### option: LocatorAssertions.toMatchAriaSnapshot#2.name
* since: v1.50
* langs: js
- `name` <[string]>
Name of the snapshot to store in the snapshot (screenshot) folder corresponding to this test.
Generates sequential names if not specified.
### option: LocatorAssertions.toMatchAriaSnapshot#2.timeout = %%-js-assertions-timeout-%%
* since: v1.50
### option: LocatorAssertions.toMatchAriaSnapshot#2.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.50

View file

@ -154,7 +154,7 @@ structure of a page, use the [Chrome DevTools Accessibility Pane](https://develo
## Snapshot matching
The [`method: LocatorAssertions.toMatchAriaSnapshot#2`] assertion method in Playwright compares the accessible
The [`method: LocatorAssertions.toMatchAriaSnapshot#1`] assertion method in Playwright compares the accessible
structure of the locator scope with a predefined aria snapshot template, helping validate the page's state against
testing requirements.

View file

@ -452,7 +452,7 @@ Certain Enterprise Browser Policies may impact Playwright's ability to launch an
:::
:::warning
Google Chrome and Microsoft Edge have switched to a [new headless mode](https://developer.chrome.com/docs/chromium/headless) implementation that is closer to a regular headed mode. This differs from [chromium headless shell](https://developer.chrome.com/blog/chrome-headless-shell) that is used in Playwright by default when running headless, so expect different behavior in some cases. See [issue #33566](https://github.com/microsoft/playwright/issues/33566) fore details.
Google Chrome and Microsoft Edge have switched to a [new headless mode](https://developer.chrome.com/docs/chromium/headless) implementation that is closer to a regular headed mode. This differs from [chromium headless shell](https://developer.chrome.com/blog/chrome-headless-shell) that is used in Playwright by default when running headless, so expect different behavior in some cases. See [issue #33566](https://github.com/microsoft/playwright/issues/33566) for details.
:::
```js

View file

@ -9,7 +9,7 @@ toc_max_heading_level: 2
### Aria snapshots
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot#2`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot#1`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
```csharp
await page.GotoAsync("https://playwright.dev");

View file

@ -8,7 +8,7 @@ toc_max_heading_level: 2
### Aria snapshots
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot#2`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot#1`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
```java
page.navigate("https://playwright.dev");

View file

@ -15,7 +15,7 @@ import LiteYouTube from '@site/src/components/LiteYouTube';
### Aria snapshots
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot#2`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot#1`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
```js
await page.goto('https://playwright.dev');

View file

@ -8,7 +8,7 @@ toc_max_heading_level: 2
### Aria snapshots
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot#2`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot#1`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
```python
page.goto("https://playwright.dev")

View file

@ -1767,117 +1767,62 @@ Whether to box the step in the report. Defaults to `false`. When the step is box
Specifies a custom location for the step to be shown in test reports and trace viewer. By default, location of the [`method: Test.step`] call is shown.
## async method: Test.step.skip
* since: v1.50
- returns: <[void]>
Mark a test step as "skip" to temporarily disable its execution, useful for steps that are currently failing and planned for a near-term fix. Playwright will not run the step.
**Usage**
You can declare a skipped step, and Playwright will not run it.
```js
import { test, expect } from '@playwright/test';
test('my test', async ({ page }) => {
// ...
await test.step.skip('not yet ready', async () => {
// ...
});
});
```
### param: Test.step.skip.title
* since: v1.50
- `title` <[string]>
Step name.
### param: Test.step.skip.body
* since: v1.50
- `body` <[function]\(\):[Promise]<[any]>>
Step body.
### option: Test.step.skip.box
* since: v1.50
- `box` <boolean>
Whether to box the step in the report. Defaults to `false`. When the step is boxed, errors thrown from the step internals point to the step call site. See below for more details.
### option: Test.step.skip.location
* since: v1.50
- `location` <[Location]>
Specifies a custom location for the step to be shown in test reports and trace viewer. By default, location of the [`method: Test.step`] call is shown.
### option: Test.step.skip.timeout
* since: v1.50
- `timeout` <[float]>
Maximum time in milliseconds for the step to finish. Defaults to `0` (no timeout).
### option: Test.step.timeout
* since: v1.50
- `timeout` <[float]>
Maximum time in milliseconds for the step to finish. Defaults to `0` (no timeout).
## async method: Test.step.fail
* since: v1.50
- returns: <[void]>
Marks a test step as "should fail". Playwright runs this test step and ensures that it actually fails. This is useful for documentation purposes to acknowledge that some functionality is broken until it is fixed.
:::note
If the step exceeds the timeout, a [TimeoutError] is thrown. This indicates the step did not fail as expected.
:::
**Usage**
You can declare a test step as failing, so that Playwright ensures it actually fails.
```js
import { test, expect } from '@playwright/test';
test('my test', async ({ page }) => {
// ...
await test.step.fail('currently failing', async () => {
// ...
});
});
```
### param: Test.step.fail.title
* since: v1.50
- `title` <[string]>
Step name.
### param: Test.step.fail.body
* since: v1.50
- `body` <[function]\(\):[Promise]<[any]>>
Step body.
### option: Test.step.fail.box
* since: v1.50
- `box` <boolean>
Whether to box the step in the report. Defaults to `false`. When the step is boxed, errors thrown from the step internals point to the step call site. See below for more details.
### option: Test.step.fail.location
* since: v1.50
- `location` <[Location]>
Specifies a custom location for the step to be shown in test reports and trace viewer. By default, location of the [`method: Test.step`] call is shown.
### option: Test.step.fail.timeout
* since: v1.50
- `timeout` <[float]>
Maximum time in milliseconds for the step to finish. Defaults to `0` (no timeout).
## async method: Test.step.fixme
* since: v1.50
- returns: <[void]>
Mark a test step as "fixme", with the intention to fix it. Playwright will not run the step.
**Usage**
You can declare a test step as failing, so that Playwright ensures it actually fails.
```js
import { test, expect } from '@playwright/test';
test('my test', async ({ page }) => {
// ...
await test.step.fixme('not yet ready', async () => {
// ...
});
});
```
### param: Test.step.fixme.title
* since: v1.50
- `title` <[string]>
Step name.
### param: Test.step.fixme.body
* since: v1.50
- `body` <[function]\(\):[Promise]<[any]>>
Step body.
### option: Test.step.fixme.box
* since: v1.50
- `box` <boolean>
Whether to box the step in the report. Defaults to `false`. When the step is boxed, errors thrown from the step internals point to the step call site. See below for more details.
### option: Test.step.fixme.location
* since: v1.50
- `location` <[Location]>
Specifies a custom location for the step to be shown in test reports and trace viewer. By default, location of the [`method: Test.step`] call is shown.
### option: Test.step.fixme.timeout
* since: v1.50
- `timeout` <[float]>
Maximum time in milliseconds for the step to finish. Defaults to `0` (no timeout).
The maximum time, in milliseconds, allowed for the step to complete. If the step does not complete within the specified timeout, the [`method: Test.step`] method will throw a [TimeoutError]. Defaults to `0` (no timeout).
## method: Test.use
* since: v1.10

View file

@ -594,10 +594,10 @@ export default defineConfig({
* since: v1.50
- type: ?<[UpdateSourceMethod]<"overwrite"|"3way"|"patch">>
Defines how to update the source code snapshots.
* `'overwrite'` - Overwrite the source code snapshot with the actual result.
* `'3way'` - Use a three-way merge to update the source code snapshot.
* `'patch'` - Use a patch to update the source code snapshot. This is the default.
Defines how to update snapshots in the source code.
* `'patch'` - Create a unified diff file that can be used to update the source code later. This is the default.
* `'3way'` - Generate merge conflict markers in source code. This allows user to manually pick relevant changes, as if they are resolving a merge conflict in the IDE.
* `'overwrite'` - Overwrite the source code with the new snapshot values.
## property: TestConfig.use
* since: v1.10

60
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "playwright-internal",
"version": "1.50.0-next",
"version": "1.51.0-next",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "playwright-internal",
"version": "1.50.0-next",
"version": "1.51.0-next",
"license": "Apache-2.0",
"workspaces": [
"packages/*"
@ -7751,10 +7751,10 @@
"version": "0.0.0"
},
"packages/playwright": {
"version": "1.50.0-next",
"version": "1.51.0-next",
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.50.0-next"
"playwright-core": "1.51.0-next"
},
"bin": {
"playwright": "cli.js"
@ -7768,11 +7768,11 @@
},
"packages/playwright-browser-chromium": {
"name": "@playwright/browser-chromium",
"version": "1.50.0-next",
"version": "1.51.0-next",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.50.0-next"
"playwright-core": "1.51.0-next"
},
"engines": {
"node": ">=18"
@ -7780,11 +7780,11 @@
},
"packages/playwright-browser-firefox": {
"name": "@playwright/browser-firefox",
"version": "1.50.0-next",
"version": "1.51.0-next",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.50.0-next"
"playwright-core": "1.51.0-next"
},
"engines": {
"node": ">=18"
@ -7792,22 +7792,22 @@
},
"packages/playwright-browser-webkit": {
"name": "@playwright/browser-webkit",
"version": "1.50.0-next",
"version": "1.51.0-next",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.50.0-next"
"playwright-core": "1.51.0-next"
},
"engines": {
"node": ">=18"
}
},
"packages/playwright-chromium": {
"version": "1.50.0-next",
"version": "1.51.0-next",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.50.0-next"
"playwright-core": "1.51.0-next"
},
"bin": {
"playwright": "cli.js"
@ -7817,7 +7817,7 @@
}
},
"packages/playwright-core": {
"version": "1.50.0-next",
"version": "1.51.0-next",
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
@ -7828,11 +7828,11 @@
},
"packages/playwright-ct-core": {
"name": "@playwright/experimental-ct-core",
"version": "1.50.0-next",
"version": "1.51.0-next",
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.50.0-next",
"playwright-core": "1.50.0-next",
"playwright": "1.51.0-next",
"playwright-core": "1.51.0-next",
"vite": "^5.2.8"
},
"engines": {
@ -7841,10 +7841,10 @@
},
"packages/playwright-ct-react": {
"name": "@playwright/experimental-ct-react",
"version": "1.50.0-next",
"version": "1.51.0-next",
"license": "Apache-2.0",
"dependencies": {
"@playwright/experimental-ct-core": "1.50.0-next",
"@playwright/experimental-ct-core": "1.51.0-next",
"@vitejs/plugin-react": "^4.2.1"
},
"bin": {
@ -7856,10 +7856,10 @@
},
"packages/playwright-ct-react17": {
"name": "@playwright/experimental-ct-react17",
"version": "1.50.0-next",
"version": "1.51.0-next",
"license": "Apache-2.0",
"dependencies": {
"@playwright/experimental-ct-core": "1.50.0-next",
"@playwright/experimental-ct-core": "1.51.0-next",
"@vitejs/plugin-react": "^4.2.1"
},
"bin": {
@ -7871,10 +7871,10 @@
},
"packages/playwright-ct-svelte": {
"name": "@playwright/experimental-ct-svelte",
"version": "1.50.0-next",
"version": "1.51.0-next",
"license": "Apache-2.0",
"dependencies": {
"@playwright/experimental-ct-core": "1.50.0-next",
"@playwright/experimental-ct-core": "1.51.0-next",
"@sveltejs/vite-plugin-svelte": "^3.0.1"
},
"bin": {
@ -7889,10 +7889,10 @@
},
"packages/playwright-ct-vue": {
"name": "@playwright/experimental-ct-vue",
"version": "1.50.0-next",
"version": "1.51.0-next",
"license": "Apache-2.0",
"dependencies": {
"@playwright/experimental-ct-core": "1.50.0-next",
"@playwright/experimental-ct-core": "1.51.0-next",
"@vitejs/plugin-vue": "^5.2.0"
},
"bin": {
@ -7903,11 +7903,11 @@
}
},
"packages/playwright-firefox": {
"version": "1.50.0-next",
"version": "1.51.0-next",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.50.0-next"
"playwright-core": "1.51.0-next"
},
"bin": {
"playwright": "cli.js"
@ -7918,10 +7918,10 @@
},
"packages/playwright-test": {
"name": "@playwright/test",
"version": "1.50.0-next",
"version": "1.51.0-next",
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.50.0-next"
"playwright": "1.51.0-next"
},
"bin": {
"playwright": "cli.js"
@ -7931,11 +7931,11 @@
}
},
"packages/playwright-webkit": {
"version": "1.50.0-next",
"version": "1.51.0-next",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.50.0-next"
"playwright-core": "1.51.0-next"
},
"bin": {
"playwright": "cli.js"

View file

@ -1,7 +1,7 @@
{
"name": "playwright-internal",
"private": true,
"version": "1.50.0-next",
"version": "1.51.0-next",
"description": "A high-level API to automate web browsers",
"repository": {
"type": "git",

View file

@ -60,11 +60,6 @@
color: var(--color-scale-orange-6);
border: 1px solid var(--color-scale-orange-4);
}
.label-color-gray {
background-color: var(--color-scale-gray-0);
color: var(--color-scale-gray-6);
border: 1px solid var(--color-scale-gray-4);
}
}
@media(prefers-color-scheme: dark) {
@ -98,11 +93,6 @@
color: var(--color-scale-orange-2);
border: 1px solid var(--color-scale-orange-4);
}
.label-color-gray {
background-color: var(--color-scale-gray-9);
color: var(--color-scale-gray-2);
border: 1px solid var(--color-scale-gray-4);
}
}
.attachment-body {

View file

@ -21,7 +21,7 @@ import { TreeItem } from './treeItem';
import { CopyToClipboard } from './copyToClipboard';
import './links.css';
import { linkifyText } from '@web/renderUtils';
import { clsx } from '@web/uiUtils';
import { clsx, useFlash } from '@web/uiUtils';
export function navigate(href: string | URL) {
window.history.pushState({}, '', href);
@ -73,7 +73,8 @@ export const AttachmentLink: React.FunctionComponent<{
linkName?: string,
openInNewTab?: boolean,
}> = ({ attachment, result, href, linkName, openInNewTab }) => {
const isAnchored = useIsAnchored('attachment-' + result.attachments.indexOf(attachment));
const [flash, triggerFlash] = useFlash();
useAnchor('attachment-' + result.attachments.indexOf(attachment), triggerFlash);
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>}
@ -84,7 +85,7 @@ export const AttachmentLink: React.FunctionComponent<{
)}
</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' }} selected={isAnchored}></TreeItem>;
} : undefined} depth={0} style={{ lineHeight: '32px' }} flash={flash}></TreeItem>;
};
export const SearchParamsContext = React.createContext<URLSearchParams>(new URLSearchParams(window.location.hash.slice(1)));
@ -118,12 +119,12 @@ const kMissingContentType = 'x-playwright/missing';
export type AnchorID = string | string[] | ((id: string) => boolean) | undefined;
export function useAnchor(id: AnchorID, onReveal: () => void) {
export function useAnchor(id: AnchorID, onReveal: React.EffectCallback) {
const searchParams = React.useContext(SearchParamsContext);
const isAnchored = useIsAnchored(id);
React.useEffect(() => {
if (isAnchored)
onReveal();
return onReveal();
}, [isAnchored, onReveal, searchParams]);
}

View file

@ -176,27 +176,14 @@ const StepTreeItem: React.FC<{
}> = ({ test, step, result, depth }) => {
return <TreeItem title={<span aria-label={step.title}>
<span style={{ float: 'right' }}>{msToString(step.duration)}</span>
{statusIcon(step.error || step.duration === -1 ? 'failed' : 'passed')}
{step.attachments.length > 0 && <a style={{ float: 'right' }} title={`reveal attachment`} href={testResultHref({ test, result, anchor: `attachment-${step.attachments[0]}` })} onClick={evt => { evt.stopPropagation(); }}>{icons.attachment()}</a>}
{statusIcon(step.error || step.duration === -1 ? 'failed' : (step.skipped ? 'skipped' : 'passed'))}
<span>{step.title}</span>
{step.count > 1 && <> <span className='test-result-counter'>{step.count}</span></>}
{step.location && <span className='test-result-path'> {step.location.file}:{step.location.line}</span>}
</span>} loadChildren={step.steps.length + (step.snippet ? 1 : 0) ? () => {
</span>} loadChildren={step.steps.length || step.snippet ? () => {
const snippet = step.snippet ? [<TestErrorView testId='test-snippet' key='line' error={step.snippet}/>] : [];
const steps = step.steps.map((s, i) => <StepTreeItem key={i} step={s} depth={depth + 1} result={result} test={test} />);
const attachments = step.attachments.map(attachmentIndex => (
<a key={'' + attachmentIndex}
href={testResultHref({ test, result, anchor: `attachment-${attachmentIndex}` })}
style={{ paddingLeft: depth * 22 + 4, textDecoration: 'none' }}
>
<span
style={{ margin: '8px 0 0 8px', padding: '2px 10px', cursor: 'pointer' }}
className='label label-color-gray'
title={`see "${result.attachments[attachmentIndex].name}"`}
>
{icons.attachment()}{result.attachments[attachmentIndex].name}
</span>
</a>
));
return snippet.concat(steps, attachments);
return snippet.concat(steps);
} : undefined} depth={depth}/>;
};

View file

@ -25,11 +25,14 @@
cursor: pointer;
}
.tree-item-title.selected {
text-decoration: underline var(--color-underlinenav-icon);
text-decoration-thickness: 1.5px;
}
.tree-item-body {
min-height: 18px;
}
.yellow-flash {
animation: yellowflash-bg 2s;
}
@keyframes yellowflash-bg {
from { background: var(--color-attention-subtle); }
to { background: transparent; }
}

View file

@ -25,12 +25,12 @@ export const TreeItem: React.FunctionComponent<{
onClick?: () => void,
expandByDefault?: boolean,
depth: number,
selected?: boolean,
style?: React.CSSProperties,
}> = ({ title, loadChildren, onClick, expandByDefault, depth, selected, style }) => {
flash?: boolean
}> = ({ title, loadChildren, onClick, expandByDefault, depth, style, flash }) => {
const [expanded, setExpanded] = React.useState(expandByDefault || false);
return <div className={'tree-item'} style={style}>
<span className={clsx('tree-item-title', selected && 'selected')} style={{ whiteSpace: 'nowrap', paddingLeft: depth * 22 + 4 }} onClick={() => { onClick?.(); setExpanded(!expanded); }} >
return <div className={clsx('tree-item', flash && 'yellow-flash')} style={style}>
<span className='tree-item-title' style={{ whiteSpace: 'nowrap', paddingLeft: depth * 22 + 4 }} onClick={() => { onClick?.(); setExpanded(!expanded); }} >
{loadChildren && !!expanded && icons.downArrow()}
{loadChildren && !expanded && icons.rightArrow()}
{!loadChildren && <span style={{ visibility: 'hidden' }}>{icons.rightArrow()}</span>}

View file

@ -110,4 +110,5 @@ export type TestStep = {
steps: TestStep[];
attachments: number[];
count: number;
skipped?: boolean;
};

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/browser-chromium",
"version": "1.50.0-next",
"version": "1.51.0-next",
"description": "Playwright package that automatically installs Chromium",
"repository": {
"type": "git",
@ -27,6 +27,6 @@
"install": "node install.js"
},
"dependencies": {
"playwright-core": "1.50.0-next"
"playwright-core": "1.51.0-next"
}
}

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/browser-firefox",
"version": "1.50.0-next",
"version": "1.51.0-next",
"description": "Playwright package that automatically installs Firefox",
"repository": {
"type": "git",
@ -27,6 +27,6 @@
"install": "node install.js"
},
"dependencies": {
"playwright-core": "1.50.0-next"
"playwright-core": "1.51.0-next"
}
}

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/browser-webkit",
"version": "1.50.0-next",
"version": "1.51.0-next",
"description": "Playwright package that automatically installs WebKit",
"repository": {
"type": "git",
@ -27,6 +27,6 @@
"install": "node install.js"
},
"dependencies": {
"playwright-core": "1.50.0-next"
"playwright-core": "1.51.0-next"
}
}

View file

@ -1,6 +1,6 @@
{
"name": "playwright-chromium",
"version": "1.50.0-next",
"version": "1.51.0-next",
"description": "A high-level API to automate Chromium",
"repository": {
"type": "git",
@ -30,6 +30,6 @@
"install": "node install.js"
},
"dependencies": {
"playwright-core": "1.50.0-next"
"playwright-core": "1.51.0-next"
}
}

View file

@ -32,6 +32,12 @@ if ! command -v curl >/dev/null; then
apt-get install -y curl
fi
# GnuPG is not preinstalled in slim images
if ! command -v gpg >/dev/null; then
apt-get update
apt-get install -y gpg
fi
# 3. Add the GPG key, the apt repo, update the apt cache, and install the package
curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > /tmp/microsoft.gpg
install -o root -g root -m 644 /tmp/microsoft.gpg /etc/apt/trusted.gpg.d/

View file

@ -32,6 +32,12 @@ if ! command -v curl >/dev/null; then
apt-get install -y curl
fi
# GnuPG is not preinstalled in slim images
if ! command -v gpg >/dev/null; then
apt-get update
apt-get install -y gpg
fi
# 3. Add the GPG key, the apt repo, update the apt cache, and install the package
curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > /tmp/microsoft.gpg
install -o root -g root -m 644 /tmp/microsoft.gpg /etc/apt/trusted.gpg.d/

View file

@ -32,6 +32,12 @@ if ! command -v curl >/dev/null; then
apt-get install -y curl
fi
# GnuPG is not preinstalled in slim images
if ! command -v gpg >/dev/null; then
apt-get update
apt-get install -y gpg
fi
# 3. Add the GPG key, the apt repo, update the apt cache, and install the package
curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > /tmp/microsoft.gpg
install -o root -g root -m 644 /tmp/microsoft.gpg /etc/apt/trusted.gpg.d/

View file

@ -3,31 +3,31 @@
"browsers": [
{
"name": "chromium",
"revision": "1153",
"revision": "1155",
"installByDefault": true,
"browserVersion": "132.0.6834.57"
"browserVersion": "133.0.6943.16"
},
{
"name": "chromium-tip-of-tree",
"revision": "1293",
"revision": "1295",
"installByDefault": false,
"browserVersion": "133.0.6943.0"
"browserVersion": "134.0.6960.0"
},
{
"name": "firefox",
"revision": "1470",
"revision": "1471",
"installByDefault": true,
"browserVersion": "133.0.3"
"browserVersion": "134.0"
},
{
"name": "firefox-beta",
"revision": "1466",
"revision": "1467",
"installByDefault": false,
"browserVersion": "133.0b9"
},
{
"name": "webkit",
"revision": "2122",
"revision": "2123",
"installByDefault": true,
"revisionOverrides": {
"debian11-x64": "2105",

View file

@ -1,6 +1,6 @@
{
"name": "playwright-core",
"version": "1.50.0-next",
"version": "1.51.0-next",
"description": "A high-level API to automate web browsers",
"repository": {
"type": "git",

View file

@ -65,7 +65,6 @@ commandWithOpenOptions('codegen [url]', 'open page and generate code for user ac
[
['-o, --output <file name>', 'saves the generated script to a file'],
['--target <language>', `language to generate, one of javascript, playwright-test, python, python-async, python-pytest, csharp, csharp-mstest, csharp-nunit, java, java-junit`, codegenId()],
['--save-trace <filename>', 'record a trace for the session and save it to a file'],
['--test-id-attribute <attributeName>', 'use the specified attribute to generate data test ID selectors'],
]).action(function(url, options) {
codegen(options, url).catch(logErrorAndExit);
@ -353,7 +352,6 @@ type Options = {
saveHar?: string;
saveHarGlob?: string;
saveStorage?: string;
saveTrace?: string;
timeout: string;
timezone?: string;
viewportSize?: string;
@ -508,8 +506,6 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro
if (closingBrowser)
return;
closingBrowser = true;
if (options.saveTrace)
await context.tracing.stop({ path: options.saveTrace });
if (options.saveStorage)
await context.storageState({ path: options.saveStorage }).catch(e => null);
if (options.saveHar)
@ -536,9 +532,6 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro
context.setDefaultTimeout(timeout);
context.setDefaultNavigationTimeout(timeout);
if (options.saveTrace)
await context.tracing.start({ screenshots: true, snapshots: true });
// Omit options that we add automatically for presentation purpose.
delete launchOptions.headless;
delete launchOptions.executablePath;
@ -595,7 +588,6 @@ async function codegen(options: Options & { target: string, output?: string, tes
device: options.device,
saveStorage: options.saveStorage,
mode: 'recording',
codegenMode: process.env.PW_RECORDER_IS_TRACE_VIEWER ? 'trace-events' : 'actions',
testIdAttributeName,
outputFile: outputFile ? path.resolve(outputFile) : undefined,
handleSIGINT: false,

View file

@ -970,7 +970,6 @@ scheme.BrowserContextPauseResult = tOptional(tObject({}));
scheme.BrowserContextEnableRecorderParams = tObject({
language: tOptional(tString),
mode: tOptional(tEnum(['inspecting', 'recording'])),
codegenMode: tOptional(tEnum(['actions', 'trace-events'])),
pauseOnNextStatement: tOptional(tBoolean),
testIdAttributeName: tOptional(tString),
launchOptions: tOptional(tAny),

View file

@ -19,6 +19,7 @@ import type * as types from '../types';
import type { BidiSession } from './bidiConnection';
import * as bidi from './third_party/bidiProtocol';
import { getBidiKeyValue } from './third_party/bidiKeyboard';
import { resolveSmartModifierString } from '../input';
export class RawKeyboardImpl implements input.RawKeyboard {
private _session: BidiSession;
@ -31,16 +32,17 @@ export class RawKeyboardImpl implements input.RawKeyboard {
this._session = session;
}
async keydown(modifiers: Set<types.KeyboardModifier>, code: string, keyCode: number, keyCodeWithoutLocation: number, key: string, location: number, autoRepeat: boolean, text: string | undefined): Promise<void> {
async keydown(modifiers: Set<types.KeyboardModifier>, keyName: string, description: input.KeyDescription, autoRepeat: boolean): Promise<void> {
keyName = resolveSmartModifierString(keyName);
const actions: bidi.Input.KeySourceAction[] = [];
actions.push({ type: 'keyDown', value: getBidiKeyValue(code) });
// TODO: add modifiers?
actions.push({ type: 'keyDown', value: getBidiKeyValue(keyName) });
await this._performActions(actions);
}
async keyup(modifiers: Set<types.KeyboardModifier>, code: string, keyCode: number, keyCodeWithoutLocation: number, key: string, location: number): Promise<void> {
async keyup(modifiers: Set<types.KeyboardModifier>, keyName: string, description: input.KeyDescription): Promise<void> {
keyName = resolveSmartModifierString(keyName);
const actions: bidi.Input.KeySourceAction[] = [];
actions.push({ type: 'keyUp', value: getBidiKeyValue(code) });
actions.push({ type: 'keyUp', value: getBidiKeyValue(keyName) });
await this._performActions(actions);
}

View file

@ -96,10 +96,10 @@ export class BidiNetworkManager {
function relativeToStart(time: number): number {
if (!time)
return -1;
return (time - startTime) / 1000;
return (time - startTime);
}
const timing: network.ResourceTiming = {
startTime: startTime / 1000,
startTime: startTime,
requestStart: relativeToStart(timings.requestStart),
responseStart: relativeToStart(timings.responseStart),
domainLookupStart: relativeToStart(timings.dnsStart),
@ -130,7 +130,7 @@ export class BidiNetworkManager {
// Keep redirected requests in the map for future reference as redirectedFrom.
const isRedirected = response.status() >= 300 && response.status() <= 399;
const responseEndTime = params.request.timings.responseEnd / 1000 - response.timing().startTime;
const responseEndTime = params.request.timings.responseEnd - response.timing().startTime;
if (isRedirected) {
response._requestFinished(responseEndTime);
} else {

View file

@ -7,18 +7,18 @@
/* eslint-disable curly */
export const getBidiKeyValue = (code: string) => {
switch (code) {
export const getBidiKeyValue = (keyName: string) => {
switch (keyName) {
case '\r':
case '\n':
code = 'Enter';
keyName = 'Enter';
break;
}
// Measures the number of code points rather than UTF-16 code units.
if ([...code].length === 1) {
return code;
if ([...keyName].length === 1) {
return keyName;
}
switch (code) {
switch (keyName) {
case 'Cancel':
return '\uE001';
case 'Help':
@ -228,6 +228,6 @@ export const getBidiKeyValue = (code: string) => {
case 'Quote':
return '"';
default:
throw new Error(`Unknown key: "${code}"`);
throw new Error(`Unknown key: "${keyName}"`);
}
};

View file

@ -133,6 +133,12 @@ function defaultProfilePreferences(
'dom.max_chrome_script_run_time': 0,
'dom.max_script_run_time': 0,
// Disable background timer throttling to allow tests to run in parallel
// without a decrease in performance.
'dom.min_background_timeout_value': 0,
'dom.min_background_timeout_value_without_budget_throttling': 0,
'dom.timeout.enable_budget_timer_throttling': false,
// Only load extensions from the application and user profile
// AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
'extensions.autoDisableScopes': 0,
@ -175,6 +181,9 @@ function defaultProfilePreferences(
// Show chrome errors and warnings in the error console
'javascript.options.showInConsole': true,
// Do not throttle rendering (requestAnimationFrame) in background tabs
'layout.testing.top-level-always-active': true,
// Disable download and usage of OpenH264: and Widevine plugins
'media.gmp-manager.updateEnabled': false,

View file

@ -130,7 +130,7 @@ export abstract class BrowserContext extends SdkObject {
// When PWDEBUG=1, show inspector for each context.
if (debugMode() === 'inspector')
await Recorder.show('actions', this, RecorderApp.factory(this), { pauseOnNextStatement: true });
await Recorder.show(this, RecorderApp.factory(this), { pauseOnNextStatement: true });
// When paused, show inspector.
if (this._debugger.isPaused())

View file

@ -38,7 +38,10 @@ export const chromiumSwitches = [
// ThirdPartyStoragePartitioning - https://github.com/microsoft/playwright/issues/32230
// LensOverlay - Hides the Lens feature in the URL address bar. Its not working in unofficial builds.
// PlzDedicatedWorker - https://github.com/microsoft/playwright/issues/31747
'--disable-features=ImprovedCookieControls,LazyFrameLoading,GlobalMediaControls,DestroyProfileOnBrowserClose,MediaRouter,DialMediaRouteProvider,AcceptCHFrame,AutoExpandDetailsElement,CertificateTransparencyComponentUpdater,AvoidUnnecessaryBeforeUnloadCheckSync,Translate,HttpsUpgrades,PaintHolding,ThirdPartyStoragePartitioning,LensOverlay,PlzDedicatedWorker',
// DeferRendererTasksAfterInput - this makes Page.frameScheduledNavigation arrive much later after a click,
// making our navigation auto-wait after click not working. Can be removed once we deperecate noWaitAfter.
// See https://github.com/microsoft/playwright/pull/34372.
'--disable-features=ImprovedCookieControls,LazyFrameLoading,GlobalMediaControls,DestroyProfileOnBrowserClose,MediaRouter,DialMediaRouteProvider,AcceptCHFrame,AutoExpandDetailsElement,CertificateTransparencyComponentUpdater,AvoidUnnecessaryBeforeUnloadCheckSync,Translate,HttpsUpgrades,PaintHolding,ThirdPartyStoragePartitioning,LensOverlay,PlzDedicatedWorker,DeferRendererTasksAfterInput',
'--allow-pre-commit-input',
'--disable-hang-monitor',
'--disable-ipc-flooding-protection',

View file

@ -50,14 +50,15 @@ export class RawKeyboardImpl implements input.RawKeyboard {
return commands.map(c => c.substring(0, c.length - 1));
}
async keydown(modifiers: Set<types.KeyboardModifier>, code: string, keyCode: number, keyCodeWithoutLocation: number, key: string, location: number, autoRepeat: boolean, text: string | undefined): Promise<void> {
async keydown(modifiers: Set<types.KeyboardModifier>, keyName: string, description: input.KeyDescription, autoRepeat: boolean): Promise<void> {
const { code, key, location, text } = description;
if (code === 'Escape' && await this._dragManger.cancelDrag())
return;
const commands = this._commandsForCode(code, modifiers);
await this._client.send('Input.dispatchKeyEvent', {
type: text ? 'keyDown' : 'rawKeyDown',
modifiers: toModifiersMask(modifiers),
windowsVirtualKeyCode: keyCodeWithoutLocation,
windowsVirtualKeyCode: description.keyCodeWithoutLocation,
code,
commands,
key,
@ -69,12 +70,13 @@ export class RawKeyboardImpl implements input.RawKeyboard {
});
}
async keyup(modifiers: Set<types.KeyboardModifier>, code: string, keyCode: number, keyCodeWithoutLocation: number, key: string, location: number): Promise<void> {
async keyup(modifiers: Set<types.KeyboardModifier>, keyName: string, description: input.KeyDescription): Promise<void> {
const { code, key, location } = description;
await this._client.send('Input.dispatchKeyEvent', {
type: 'keyUp',
modifiers: toModifiersMask(modifiers),
key,
windowsVirtualKeyCode: keyCodeWithoutLocation,
windowsVirtualKeyCode: description.keyCodeWithoutLocation,
code,
location
});

View file

@ -685,8 +685,8 @@ percentage [0 - 100] for scroll driven animations
/**
* The unique request id.
*/
requestId: Network.RequestId;
url?: string;
requestId?: Network.RequestId;
url: string;
}
/**
* Information about the frame affected by an inspector issue.
@ -697,6 +697,20 @@ percentage [0 - 100] for scroll driven animations
export type CookieExclusionReason = "ExcludeSameSiteUnspecifiedTreatedAsLax"|"ExcludeSameSiteNoneInsecure"|"ExcludeSameSiteLax"|"ExcludeSameSiteStrict"|"ExcludeInvalidSameParty"|"ExcludeSamePartyCrossPartyContext"|"ExcludeDomainNonASCII"|"ExcludeThirdPartyCookieBlockedInFirstPartySet"|"ExcludeThirdPartyPhaseout"|"ExcludePortMismatch"|"ExcludeSchemeMismatch";
export type CookieWarningReason = "WarnSameSiteUnspecifiedCrossSiteContext"|"WarnSameSiteNoneInsecure"|"WarnSameSiteUnspecifiedLaxAllowUnsafe"|"WarnSameSiteStrictLaxDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeLax"|"WarnSameSiteLaxCrossDowngradeStrict"|"WarnSameSiteLaxCrossDowngradeLax"|"WarnAttributeValueExceedsMaxSize"|"WarnDomainNonASCII"|"WarnThirdPartyPhaseout"|"WarnCrossSiteRedirectDowngradeChangesInclusion"|"WarnDeprecationTrialMetadata"|"WarnThirdPartyCookieHeuristic";
export type CookieOperation = "SetCookie"|"ReadCookie";
/**
* Represents the category of insight that a cookie issue falls under.
*/
export type InsightType = "GitHubResource"|"GracePeriod"|"Heuristics";
/**
* Information about the suggested solution to a cookie issue.
*/
export interface CookieIssueInsight {
type: InsightType;
/**
* Link to table entry in third-party cookie migration readiness list.
*/
tableEntryUrl?: string;
}
/**
* This information is currently necessary, as the front-end has a difficult
time finding a specific cookie. With this, we can convey specific error
@ -721,6 +735,10 @@ may be used by the front-end as additional context.
siteForCookies?: string;
cookieUrl?: string;
request?: AffectedRequest;
/**
* The recommended solution to the issue.
*/
insight?: CookieIssueInsight;
}
export type MixedContentResolutionStatus = "MixedContentBlocked"|"MixedContentAutomaticallyUpgraded"|"MixedContentWarning";
export type MixedContentResourceType = "AttributionSrc"|"Audio"|"Beacon"|"CSPReport"|"Download"|"EventSource"|"Favicon"|"Font"|"Form"|"Frame"|"Image"|"Import"|"JSON"|"Manifest"|"Ping"|"PluginData"|"PluginResource"|"Prefetch"|"Resource"|"Script"|"ServiceWorker"|"SharedWorker"|"SpeculationRules"|"Stylesheet"|"Track"|"Video"|"Worker"|"XMLHttpRequest"|"XSLT";
@ -758,7 +776,7 @@ Does not always exist (e.g. for unsafe form submission urls).
* Enum indicating the reason a response has been blocked. These reasons are
refinements of the net error BLOCKED_BY_RESPONSE.
*/
export type BlockedByResponseReason = "CoepFrameResourceNeedsCoepHeader"|"CoopSandboxedIFrameCannotNavigateToCoopPage"|"CorpNotSameOrigin"|"CorpNotSameOriginAfterDefaultedToSameOriginByCoep"|"CorpNotSameOriginAfterDefaultedToSameOriginByDip"|"CorpNotSameOriginAfterDefaultedToSameOriginByCoepAndDip"|"CorpNotSameSite";
export type BlockedByResponseReason = "CoepFrameResourceNeedsCoepHeader"|"CoopSandboxedIFrameCannotNavigateToCoopPage"|"CorpNotSameOrigin"|"CorpNotSameOriginAfterDefaultedToSameOriginByCoep"|"CorpNotSameOriginAfterDefaultedToSameOriginByDip"|"CorpNotSameOriginAfterDefaultedToSameOriginByCoepAndDip"|"CorpNotSameSite"|"SRIMessageSignatureMismatch";
/**
* Details for a request that has been blocked with the BLOCKED_BY_RESPONSE
code. Currently only used for COEP/COOP, but may be extended to include
@ -963,6 +981,15 @@ features, encourage the use of new ones, and provide general guidance.
failureMessage: string;
requestId?: Network.RequestId;
}
export type SelectElementAccessibilityIssueReason = "DisallowedSelectChild"|"DisallowedOptGroupChild"|"NonPhrasingContentOptionChild"|"InteractiveContentOptionChild"|"InteractiveContentLegendChild";
/**
* This isue warns about errors in the select element content model.
*/
export interface SelectElementAccessibilityIssueDetails {
nodeId: DOM.BackendNodeId;
selectElementAccessibilityIssueReason: SelectElementAccessibilityIssueReason;
hasDisallowedAttributes: boolean;
}
export type StyleSheetLoadingIssueReason = "LateImportRule"|"RequestFailed";
/**
* This issue warns when a referenced stylesheet couldn't be loaded.
@ -1005,7 +1032,7 @@ registrations being ignored.
optional fields in InspectorIssueDetails to convey more specific
information about the kind of issue.
*/
export type InspectorIssueCode = "CookieIssue"|"MixedContentIssue"|"BlockedByResponseIssue"|"HeavyAdIssue"|"ContentSecurityPolicyIssue"|"SharedArrayBufferIssue"|"LowTextContrastIssue"|"CorsIssue"|"AttributionReportingIssue"|"QuirksModeIssue"|"NavigatorUserAgentIssue"|"GenericIssue"|"DeprecationIssue"|"ClientHintIssue"|"FederatedAuthRequestIssue"|"BounceTrackingIssue"|"CookieDeprecationMetadataIssue"|"StylesheetLoadingIssue"|"FederatedAuthUserInfoRequestIssue"|"PropertyRuleIssue"|"SharedDictionaryIssue";
export type InspectorIssueCode = "CookieIssue"|"MixedContentIssue"|"BlockedByResponseIssue"|"HeavyAdIssue"|"ContentSecurityPolicyIssue"|"SharedArrayBufferIssue"|"LowTextContrastIssue"|"CorsIssue"|"AttributionReportingIssue"|"QuirksModeIssue"|"NavigatorUserAgentIssue"|"GenericIssue"|"DeprecationIssue"|"ClientHintIssue"|"FederatedAuthRequestIssue"|"BounceTrackingIssue"|"CookieDeprecationMetadataIssue"|"StylesheetLoadingIssue"|"FederatedAuthUserInfoRequestIssue"|"PropertyRuleIssue"|"SharedDictionaryIssue"|"SelectElementAccessibilityIssue";
/**
* This struct holds a list of optional fields with additional information
specific to the kind of issue. When adding a new issue code, please also
@ -1033,6 +1060,7 @@ add a new optional field to this type.
propertyRuleIssueDetails?: PropertyRuleIssueDetails;
federatedAuthUserInfoRequestIssueDetails?: FederatedAuthUserInfoRequestIssueDetails;
sharedDictionaryIssueDetails?: SharedDictionaryIssueDetails;
selectElementAccessibilityIssueDetails?: SelectElementAccessibilityIssueDetails;
}
/**
* A unique id for a DevTools inspector issue. Allows other entities (e.g.
@ -1534,7 +1562,7 @@ events afterwards if enabled and recording.
*/
windowState?: WindowState;
}
export type PermissionType = "accessibilityEvents"|"audioCapture"|"backgroundSync"|"backgroundFetch"|"capturedSurfaceControl"|"clipboardReadWrite"|"clipboardSanitizedWrite"|"displayCapture"|"durableStorage"|"flash"|"geolocation"|"idleDetection"|"localFonts"|"midi"|"midiSysex"|"nfc"|"notifications"|"paymentHandler"|"periodicBackgroundSync"|"protectedMediaIdentifier"|"sensors"|"storageAccess"|"speakerSelection"|"topLevelStorageAccess"|"videoCapture"|"videoCapturePanTiltZoom"|"wakeLockScreen"|"wakeLockSystem"|"webAppInstallation"|"windowManagement";
export type PermissionType = "ar"|"audioCapture"|"automaticFullscreen"|"backgroundFetch"|"backgroundSync"|"cameraPanTiltZoom"|"capturedSurfaceControl"|"clipboardReadWrite"|"clipboardSanitizedWrite"|"displayCapture"|"durableStorage"|"geolocation"|"handTracking"|"idleDetection"|"keyboardLock"|"localFonts"|"midi"|"midiSysex"|"nfc"|"notifications"|"paymentHandler"|"periodicBackgroundSync"|"pointerLock"|"protectedMediaIdentifier"|"sensors"|"smartCard"|"speakerSelection"|"storageAccess"|"topLevelStorageAccess"|"videoCapture"|"vr"|"wakeLockScreen"|"wakeLockSystem"|"webAppInstallation"|"webPrinting"|"windowManagement";
export type PermissionSetting = "granted"|"denied"|"prompt";
/**
* Definition of PermissionDescriptor defined in the Permissions API:
@ -1961,6 +1989,19 @@ inspector" rules), "regular" for regular stylesheets.
*/
matches: RuleMatch[];
}
/**
* CSS style coming from animations with the name of the animation.
*/
export interface CSSAnimationStyle {
/**
* The name of the animation.
*/
name?: string;
/**
* The style coming from the animation.
*/
style: CSSStyle;
}
/**
* Inherited CSS rule collection from ancestor node.
*/
@ -1974,6 +2015,19 @@ inspector" rules), "regular" for regular stylesheets.
*/
matchedCSSRules: RuleMatch[];
}
/**
* Inherited CSS style collection for animated styles from ancestor node.
*/
export interface InheritedAnimatedStyleEntry {
/**
* Styles coming from the animations of the ancestor, if any, in the style inheritance chain.
*/
animationStyles?: CSSAnimationStyle[];
/**
* The style coming from the transitions of the ancestor, if any, in the style inheritance chain.
*/
transitionsStyle?: CSSStyle;
}
/**
* Inherited pseudo element matches from pseudos of an ancestor node.
*/
@ -2897,6 +2951,21 @@ the browser.
}
export type forcePseudoStateReturnValue = {
}
/**
* Ensures that the given node is in its starting-style state.
*/
export type forceStartingStyleParameters = {
/**
* The element id for which to force the starting-style state.
*/
nodeId: DOM.NodeId;
/**
* Boolean indicating if this is on or off.
*/
forced: boolean;
}
export type forceStartingStyleReturnValue = {
}
export type getBackgroundColorsParameters = {
/**
* Id of the node to get background colors for.
@ -2934,6 +3003,46 @@ be ignored (as if the image had failed to load).
*/
computedStyle: CSSComputedStyleProperty[];
}
/**
* Resolve the specified values in the context of the provided element.
For example, a value of '1em' is evaluated according to the computed
'font-size' of the element and a value 'calc(1px + 2px)' will be
resolved to '3px'.
*/
export type resolveValuesParameters = {
/**
* Substitution functions (var()/env()/attr()) and cascade-dependent
keywords (revert/revert-layer) do not work.
*/
values: string[];
/**
* Id of the node in whose context the expression is evaluated
*/
nodeId: DOM.NodeId;
/**
* Only longhands and custom property names are accepted.
*/
propertyName?: string;
/**
* Pseudo element type, only works for pseudo elements that generate
elements in the tree, such as ::before and ::after.
*/
pseudoType?: DOM.PseudoType;
/**
* Pseudo element custom ident.
*/
pseudoIdentifier?: string;
}
export type resolveValuesReturnValue = {
results: string[];
}
export type getLonghandPropertiesParameters = {
shorthandName: string;
value: string;
}
export type getLonghandPropertiesReturnValue = {
longhandProperties: CSSProperty[];
}
/**
* Returns the styles defined inline (explicitly in the "style" attribute and implicitly, using DOM
attributes) for a DOM node identified by `nodeId`.
@ -2951,6 +3060,28 @@ attributes) for a DOM node identified by `nodeId`.
*/
attributesStyle?: CSSStyle;
}
/**
* Returns the styles coming from animations & transitions
including the animation & transition styles coming from inheritance chain.
*/
export type getAnimatedStylesForNodeParameters = {
nodeId: DOM.NodeId;
}
export type getAnimatedStylesForNodeReturnValue = {
/**
* Styles coming from animations.
*/
animationStyles?: CSSAnimationStyle[];
/**
* Style coming from transitions.
*/
transitionsStyle?: CSSStyle;
/**
* Inherited style entries for animationsStyle and transitionsStyle from
the inheritance chain of the element.
*/
inherited?: InheritedAnimatedStyleEntry[];
}
/**
* Returns requested styles for a DOM node identified by `nodeId`.
*/
@ -3603,7 +3734,7 @@ front-end.
/**
* Pseudo element type.
*/
export type PseudoType = "first-line"|"first-letter"|"check"|"before"|"after"|"select-arrow"|"marker"|"backdrop"|"column"|"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"|"placeholder"|"file-selector-button"|"details-content"|"picker";
export type PseudoType = "first-line"|"first-letter"|"checkmark"|"before"|"after"|"picker-icon"|"marker"|"backdrop"|"column"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scroll-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"|"placeholder"|"file-selector-button"|"details-content"|"picker";
/**
* Shadow root type.
*/
@ -8616,7 +8747,7 @@ applicable or not known.
/**
* The reason why request was blocked.
*/
export type BlockedReason = "other"|"csp"|"mixed-content"|"origin"|"inspector"|"subresource-filter"|"content-type"|"coep-frame-resource-needs-coep-header"|"coop-sandboxed-iframe-cannot-navigate-to-coop-page"|"corp-not-same-origin"|"corp-not-same-origin-after-defaulted-to-same-origin-by-coep"|"corp-not-same-origin-after-defaulted-to-same-origin-by-dip"|"corp-not-same-origin-after-defaulted-to-same-origin-by-coep-and-dip"|"corp-not-same-site";
export type BlockedReason = "other"|"csp"|"mixed-content"|"origin"|"inspector"|"subresource-filter"|"content-type"|"coep-frame-resource-needs-coep-header"|"coop-sandboxed-iframe-cannot-navigate-to-coop-page"|"corp-not-same-origin"|"corp-not-same-origin-after-defaulted-to-same-origin-by-coep"|"corp-not-same-origin-after-defaulted-to-same-origin-by-dip"|"corp-not-same-origin-after-defaulted-to-same-origin-by-coep-and-dip"|"corp-not-same-site"|"sri-message-signature-mismatch";
/**
* The reason why request was blocked.
*/
@ -9917,6 +10048,9 @@ are represented by the invalid cookie line string instead of a proper cookie.
blockedCookies: BlockedSetCookieWithReason[];
/**
* Raw response headers as they were received over the wire.
Duplicate headers in the response are represented as a single key with their values
concatentated using `\n` as the separator.
See also `headersText` that contains verbatim text for HTTP/1.*.
*/
headers: Headers;
/**
@ -9962,6 +10096,9 @@ Only one responseReceivedEarlyHints may be fired for eached responseReceived eve
requestId: RequestId;
/**
* Raw response headers as they were received over the wire.
Duplicate headers in the response are represented as a single key with their values
concatentated using `\n` as the separator.
See also `headersText` that contains verbatim text for HTTP/1.*.
*/
headers: Headers;
}
@ -9978,7 +10115,7 @@ or after the response was received.
of the operation already exists und thus, the operation was abort
preemptively (e.g. a cache hit).
*/
status: "Ok"|"InvalidArgument"|"MissingIssuerKeys"|"FailedPrecondition"|"ResourceExhausted"|"AlreadyExists"|"ResourceLimited"|"Unauthorized"|"BadResponse"|"InternalError"|"UnknownError"|"FulfilledLocally";
status: "Ok"|"InvalidArgument"|"MissingIssuerKeys"|"FailedPrecondition"|"ResourceExhausted"|"AlreadyExists"|"ResourceLimited"|"Unauthorized"|"BadResponse"|"InternalError"|"UnknownError"|"FulfilledLocally"|"SiteIssuerLimit";
type: TrustTokenOperationType;
requestId: RequestId;
/**
@ -10672,6 +10809,26 @@ should be omitted for worker targets.
export type loadNetworkResourceReturnValue = {
resource: LoadNetworkResourcePageResult;
}
/**
* Sets Controls for third-party cookie access
Page reload is required before the new cookie bahavior will be observed
*/
export type setCookieControlsParameters = {
/**
* Whether 3pc restriction is enabled.
*/
enableThirdPartyCookieRestriction: boolean;
/**
* Whether 3pc grace period exception should be enabled; false by default.
*/
disableThirdPartyCookieMetadata: boolean;
/**
* Whether 3pc heuristics exceptions should be enabled; false by default.
*/
disableThirdPartyCookieHeuristics: boolean;
}
export type setCookieControlsReturnValue = {
}
}
/**
@ -11545,7 +11702,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"|"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"|"controlled-frame"|"cross-origin-isolated"|"deferred-fetch"|"digital-credentials-get"|"direct-sockets"|"direct-sockets-private"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"fenced-unpartitioned-storage-read"|"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"|"popins"|"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-app-installation"|"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"|"controlled-frame"|"cross-origin-isolated"|"deferred-fetch"|"deferred-fetch-minimal"|"digital-credentials-get"|"direct-sockets"|"direct-sockets-private"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"fenced-unpartitioned-storage-read"|"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"|"popins"|"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-app-installation"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking";
/**
* Reason for a permissions policy feature to be disabled.
*/
@ -15519,6 +15676,14 @@ Parts of the URL other than those constituting origin are ignored.
* The initial URL the page will be navigated to. An empty string indicates about:blank.
*/
url: string;
/**
* Frame left origin in DIP (headless chrome only).
*/
left?: number;
/**
* Frame top origin in DIP (headless chrome only).
*/
top?: number;
/**
* Frame width in DIP (headless chrome only).
*/
@ -17151,6 +17316,16 @@ possible for multiple rule sets and links to trigger a single attempt.
ruleSetIds: RuleSetId[];
nodeIds: DOM.BackendNodeId[];
}
/**
* Chrome manages different types of preloads together using a
concept of preloading pipeline. For example, if a site uses a
SpeculationRules for prerender, Chrome first starts a prefetch and
then upgrades it to prerender.
CDP events for them are emitted separately but they share
`PreloadPipelineId`.
*/
export type PreloadPipelineId = string;
/**
* List of FinalStatus reasons for Prerender2.
*/
@ -17198,6 +17373,7 @@ filter out the ones that aren't necessary to the developers.
*/
export type prefetchStatusUpdatedPayload = {
key: PreloadingAttemptKey;
pipelineId: PreloadPipelineId;
/**
* The frame id of the frame initiating prefetch.
*/
@ -17212,6 +17388,7 @@ filter out the ones that aren't necessary to the developers.
*/
export type prerenderStatusUpdatedPayload = {
key: PreloadingAttemptKey;
pipelineId: PreloadPipelineId;
status: PreloadingStatus;
prerenderStatus?: PrerenderFinalStatus;
/**
@ -17922,6 +18099,10 @@ variables as its properties.
* Content hash of the script, SHA-256.
*/
hash: string;
/**
* For Wasm modules, the content of the `build_id` custom section.
*/
buildId: string;
/**
* Embedder-specific auxiliary data likely matching {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string}
*/
@ -17996,6 +18177,10 @@ scripts upon enabling debugger.
* Content hash of the script, SHA-256.
*/
hash: string;
/**
* For Wasm modules, the content of the `build_id` custom section.
*/
buildId: string;
/**
* Embedder-specific auxiliary data likely matching {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string}
*/
@ -20507,9 +20692,13 @@ Error was thrown.
"CSS.disable": CSS.disableParameters;
"CSS.enable": CSS.enableParameters;
"CSS.forcePseudoState": CSS.forcePseudoStateParameters;
"CSS.forceStartingStyle": CSS.forceStartingStyleParameters;
"CSS.getBackgroundColors": CSS.getBackgroundColorsParameters;
"CSS.getComputedStyleForNode": CSS.getComputedStyleForNodeParameters;
"CSS.resolveValues": CSS.resolveValuesParameters;
"CSS.getLonghandProperties": CSS.getLonghandPropertiesParameters;
"CSS.getInlineStylesForNode": CSS.getInlineStylesForNodeParameters;
"CSS.getAnimatedStylesForNode": CSS.getAnimatedStylesForNodeParameters;
"CSS.getMatchedStylesForNode": CSS.getMatchedStylesForNodeParameters;
"CSS.getMediaQueries": CSS.getMediaQueriesParameters;
"CSS.getPlatformFontsForNode": CSS.getPlatformFontsForNodeParameters;
@ -20751,6 +20940,7 @@ Error was thrown.
"Network.getSecurityIsolationStatus": Network.getSecurityIsolationStatusParameters;
"Network.enableReportingApi": Network.enableReportingApiParameters;
"Network.loadNetworkResource": Network.loadNetworkResourceParameters;
"Network.setCookieControls": Network.setCookieControlsParameters;
"Overlay.disable": Overlay.disableParameters;
"Overlay.enable": Overlay.enableParameters;
"Overlay.getHighlightObjectForTest": Overlay.getHighlightObjectForTestParameters;
@ -21119,9 +21309,13 @@ Error was thrown.
"CSS.disable": CSS.disableReturnValue;
"CSS.enable": CSS.enableReturnValue;
"CSS.forcePseudoState": CSS.forcePseudoStateReturnValue;
"CSS.forceStartingStyle": CSS.forceStartingStyleReturnValue;
"CSS.getBackgroundColors": CSS.getBackgroundColorsReturnValue;
"CSS.getComputedStyleForNode": CSS.getComputedStyleForNodeReturnValue;
"CSS.resolveValues": CSS.resolveValuesReturnValue;
"CSS.getLonghandProperties": CSS.getLonghandPropertiesReturnValue;
"CSS.getInlineStylesForNode": CSS.getInlineStylesForNodeReturnValue;
"CSS.getAnimatedStylesForNode": CSS.getAnimatedStylesForNodeReturnValue;
"CSS.getMatchedStylesForNode": CSS.getMatchedStylesForNodeReturnValue;
"CSS.getMediaQueries": CSS.getMediaQueriesReturnValue;
"CSS.getPlatformFontsForNode": CSS.getPlatformFontsForNodeReturnValue;
@ -21363,6 +21557,7 @@ Error was thrown.
"Network.getSecurityIsolationStatus": Network.getSecurityIsolationStatusReturnValue;
"Network.enableReportingApi": Network.enableReportingApiReturnValue;
"Network.loadNetworkResource": Network.loadNetworkResourceReturnValue;
"Network.setCookieControls": Network.setCookieControlsReturnValue;
"Overlay.disable": Overlay.disableReturnValue;
"Overlay.enable": Overlay.enableReturnValue;
"Overlay.getHighlightObjectForTest": Overlay.getHighlightObjectForTestReturnValue;

View file

@ -28,7 +28,7 @@ export class JsonlLanguageGenerator implements LanguageGenerator {
const locator = (actionInContext.action as any).selector ? JSON.parse(asLocator('jsonl', (actionInContext.action as any).selector)) : undefined;
const entry = {
...actionInContext.action,
pageAlias: actionInContext.frame.pageAlias,
...actionInContext.frame,
locator,
};
return JSON.stringify(entry);

View file

@ -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/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 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/133.0.6943.16 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/132.0.6834.57 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/133.0.6943.16 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/132.0.6834.57 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/133.0.6943.16 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/132.0.6834.57 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/133.0.6943.16 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/132.0.6834.57 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/133.0.6943.16 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/132.0.6834.57 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/133.0.6943.16 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/132.0.6834.57 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 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/133.0.6943.16 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/132.0.6834.57 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/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 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/133.0.6943.16 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/132.0.6834.57 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/133.0.6943.16 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/132.0.6834.57 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 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/133.0.6943.16 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/132.0.6834.57 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/133.0.6943.16 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/132.0.6834.57 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/133.0.6943.16 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/132.0.6834.57 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/133.0.6943.16 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/132.0.6834.57 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/133.0.6943.16 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/132.0.6834.57 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/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Mobile Safari/537.36",
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Safari/537.36",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Safari/537.36 Edg/132.0.6834.57",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Safari/537.36 Edg/133.0.6943.16",
"screen": {
"width": 1792,
"height": 1120
@ -1592,7 +1592,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Firefox HiDPI": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0.3) Gecko/20100101 Firefox/133.0.3",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:134.0) Gecko/20100101 Firefox/134.0",
"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/132.0.6834.57 Safari/537.36",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 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/132.0.6834.57 Safari/537.36 Edg/132.0.6834.57",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.16 Safari/537.36 Edg/133.0.6943.16",
"screen": {
"width": 1920,
"height": 1080
@ -1652,7 +1652,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Firefox": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0.3) Gecko/20100101 Firefox/133.0.3",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:134.0) Gecko/20100101 Firefox/134.0",
"screen": {
"width": 1920,
"height": 1080

View file

@ -39,7 +39,6 @@ import type { Dialog } from '../dialog';
import type { ConsoleMessage } from '../console';
import { serializeError } from '../errors';
import { ElementHandleDispatcher } from './elementHandlerDispatcher';
import { RecorderInTraceViewer } from '../recorder/recorderInTraceViewer';
import { RecorderApp } from '../recorder/recorderApp';
import { WebSocketRouteDispatcher } from './webSocketRouteDispatcher';
@ -301,17 +300,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
}
async enableRecorder(params: channels.BrowserContextEnableRecorderParams): Promise<void> {
if (params.codegenMode === 'trace-events') {
await this._context.tracing.start({
name: 'trace',
snapshots: true,
screenshots: true,
live: true,
});
await Recorder.show('trace-events', this._context, RecorderInTraceViewer.factory(this._context), params);
} else {
await Recorder.show('actions', this._context, RecorderApp.factory(this._context), params);
}
await Recorder.show(this._context, RecorderApp.factory(this._context), params);
}
async pause(params: channels.BrowserContextPauseParams, metadata: CallMetadata) {

View file

@ -61,13 +61,15 @@ export class RawKeyboardImpl implements input.RawKeyboard {
this._client = client;
}
async keydown(modifiers: Set<types.KeyboardModifier>, code: string, keyCode: number, keyCodeWithoutLocation: number, key: string, location: number, autoRepeat: boolean, text: string | undefined): Promise<void> {
async keydown(modifiers: Set<types.KeyboardModifier>, keyName: string, description: input.KeyDescription, autoRepeat: boolean): Promise<void> {
let text = description.text;
// Firefox will figure out Enter by itself
if (text === '\r')
text = '';
const { code, key, location } = description;
await this._client.send('Page.dispatchKeyEvent', {
type: 'keydown',
keyCode: keyCodeWithoutLocation,
keyCode: description.keyCodeWithoutLocation,
code,
key,
repeat: autoRepeat,
@ -76,11 +78,12 @@ export class RawKeyboardImpl implements input.RawKeyboard {
});
}
async keyup(modifiers: Set<types.KeyboardModifier>, code: string, keyCode: number, keyCodeWithoutLocation: number, key: string, location: number): Promise<void> {
async keyup(modifiers: Set<types.KeyboardModifier>, keyName: string, description: input.KeyDescription): Promise<void> {
const { code, key, location } = description;
await this._client.send('Page.dispatchKeyEvent', {
type: 'keyup',
key,
keyCode: keyCodeWithoutLocation,
keyCode: description.keyCodeWithoutLocation,
code,
location,
repeat: false

View file

@ -1437,8 +1437,6 @@ export class InjectedScript {
received = elements.map(e => options.useInnerText ? (e as HTMLElement).innerText : elementText(new Map(), e).full);
else if (expression === 'to.have.class.array')
received = elements.map(e => e.classList.toString());
else if (expression === 'to.have.accessible.name.array')
received = elements.map(e => getElementAccessibleName(e, false));
if (received && options.expectedText) {
// "To match an array" is "to contain an array" + "equal length"

View file

@ -1138,25 +1138,28 @@ export class Recorder {
let highlight: HighlightModel | 'clear' | 'noop' = 'noop';
if (state.actionSelector !== this._lastHighlightedSelector) {
this._lastHighlightedSelector = state.actionSelector;
const model = state.actionSelector ? querySelector(this.injectedScript, state.actionSelector, this.document) : null;
highlight = model?.elements.length ? model : 'clear';
this._lastHighlightedSelector = model?.elements.length ? state.actionSelector : undefined;
}
const ariaTemplateJSON = JSON.stringify(state.ariaTemplate);
if (this._lastHighlightedAriaTemplateJSON !== ariaTemplateJSON) {
this._lastHighlightedAriaTemplateJSON = ariaTemplateJSON;
const elements = state.ariaTemplate ? this.injectedScript.getAllByAria(this.document, state.ariaTemplate) : [];
if (elements.length)
if (elements.length) {
highlight = { elements };
else
highlight = 'clear';
this._lastHighlightedAriaTemplateJSON = ariaTemplateJSON;
} else {
if (!this._lastHighlightedSelector)
highlight = 'clear';
this._lastHighlightedAriaTemplateJSON = 'undefined';
}
}
if (highlight === 'clear')
this.clearHighlight();
else if (highlight !== 'noop')
this.updateHighlight(highlight, false);
this._updateHighlight(highlight, false);
}
clearHighlight() {
@ -1299,6 +1302,12 @@ export class Recorder {
}
updateHighlight(model: HighlightModel | null, userGesture: boolean) {
this._lastHighlightedSelector = undefined;
this._lastHighlightedAriaTemplateJSON = 'undefined';
this._updateHighlight(model, userGesture);
}
private _updateHighlight(model: HighlightModel | null, userGesture: boolean) {
let tooltipText = model?.tooltipText;
if (tooltipText === undefined && !model?.tooltipList && model?.selector)
tooltipText = this.injectedScript.utils.asLocator(this.state.language, model.selector);

View file

@ -22,7 +22,7 @@ import type { CallMetadata } from './instrumentation';
export const keypadLocation = keyboardLayout.keypadLocation;
type KeyDescription = {
export type KeyDescription = {
keyCode: number,
keyCodeWithoutLocation: number,
key: string,
@ -35,8 +35,8 @@ type KeyDescription = {
const kModifiers: types.KeyboardModifier[] = ['Alt', 'Control', 'Meta', 'Shift'];
export interface RawKeyboard {
keydown(modifiers: Set<types.KeyboardModifier>, code: string, keyCode: number, keyCodeWithoutLocation: number, key: string, location: number, autoRepeat: boolean, text: string | undefined): Promise<void>;
keyup(modifiers: Set<types.KeyboardModifier>, code: string, keyCode: number, keyCodeWithoutLocation: number, key: string, location: number): Promise<void>;
keydown(modifiers: Set<types.KeyboardModifier>, keyName: string, description: KeyDescription, autoRepeat: boolean): Promise<void>;
keyup(modifiers: Set<types.KeyboardModifier>, keyName: string, description: KeyDescription): Promise<void>;
sendText(text: string): Promise<void>;
}
@ -55,8 +55,7 @@ export class Keyboard {
this._pressedKeys.add(description.code);
if (kModifiers.includes(description.key as types.KeyboardModifier))
this._pressedModifiers.add(description.key as types.KeyboardModifier);
const text = description.text;
await this._raw.keydown(this._pressedModifiers, description.code, description.keyCode, description.keyCodeWithoutLocation, description.key, description.location, autoRepeat, text);
await this._raw.keydown(this._pressedModifiers, key, description, autoRepeat);
}
private _keyDescriptionForString(str: string): KeyDescription {
@ -77,7 +76,7 @@ export class Keyboard {
if (kModifiers.includes(description.key as types.KeyboardModifier))
this._pressedModifiers.delete(description.key as types.KeyboardModifier);
this._pressedKeys.delete(description.code);
await this._raw.keyup(this._pressedModifiers, description.code, description.keyCode, description.keyCodeWithoutLocation, description.key, description.location);
await this._raw.keyup(this._pressedModifiers, key, description);
}
async insertText(text: string) {

View file

@ -27,9 +27,8 @@ import { Debugger } from './debugger';
import type { CallMetadata, InstrumentationListener, SdkObject } from './instrumentation';
import { ContextRecorder, generateFrameSelector } from './recorder/contextRecorder';
import type { IRecorderAppFactory, IRecorderApp, IRecorder } from './recorder/recorderFrontend';
import { metadataToCallLog } from './recorder/recorderUtils';
import { buildFullSelector, metadataToCallLog } from './recorder/recorderUtils';
import type * as actions from '@recorder/actions';
import { buildFullSelector } from '../utils/isomorphic/recorderUtils';
import { stringifySelector } from '../utils/isomorphic/selectorParser';
import type { Frame } from './frames';
import type { AriaTemplateNode } from '@isomorphic/ariaSnapshot';
@ -54,33 +53,33 @@ export class Recorder implements InstrumentationListener, IRecorder {
static async showInspector(context: BrowserContext, params: channels.BrowserContextEnableRecorderParams, recorderAppFactory: IRecorderAppFactory) {
if (isUnderTest())
params.language = process.env.TEST_INSPECTOR_LANGUAGE;
return await Recorder.show('actions', context, recorderAppFactory, params);
return await Recorder.show(context, recorderAppFactory, params);
}
static showInspectorNoReply(context: BrowserContext, recorderAppFactory: IRecorderAppFactory) {
Recorder.showInspector(context, {}, recorderAppFactory).catch(() => {});
}
static show(codegenMode: 'actions' | 'trace-events', context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextEnableRecorderParams): Promise<Recorder> {
static show(context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextEnableRecorderParams): Promise<Recorder> {
let recorderPromise = (context as any)[recorderSymbol] as Promise<Recorder>;
if (!recorderPromise) {
recorderPromise = Recorder._create(codegenMode, context, recorderAppFactory, params);
recorderPromise = Recorder._create(context, recorderAppFactory, params);
(context as any)[recorderSymbol] = recorderPromise;
}
return recorderPromise;
}
private static async _create(codegenMode: 'actions' | 'trace-events', context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextEnableRecorderParams = {}): Promise<Recorder> {
const recorder = new Recorder(codegenMode, context, params);
private static async _create(context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextEnableRecorderParams = {}): Promise<Recorder> {
const recorder = new Recorder(context, params);
const recorderApp = await recorderAppFactory(recorder);
await recorder._install(recorderApp);
return recorder;
}
constructor(codegenMode: 'actions' | 'trace-events', context: BrowserContext, params: channels.BrowserContextEnableRecorderParams) {
constructor(context: BrowserContext, params: channels.BrowserContextEnableRecorderParams) {
this._mode = params.mode || 'none';
this.handleSIGINT = params.handleSIGINT;
this._contextRecorder = new ContextRecorder(codegenMode, context, params, {});
this._contextRecorder = new ContextRecorder(context, params, {});
this._context = context;
this._omitCallTracking = !!params.omitCallTracking;
this._debugger = context.debugger();

View file

@ -10,6 +10,3 @@
../../utils/**
../../utilsBundle.ts
../../zipBundle.ts
[recorderInTraceViewer.ts]
../trace/viewer/traceViewer.ts

View file

@ -54,11 +54,9 @@ export class ContextRecorder extends EventEmitter {
private _throttledOutputFile: ThrottledFile | null = null;
private _orderedLanguages: LanguageGenerator[] = [];
private _listeners: RegisteredListener[] = [];
private _codegenMode: 'actions' | 'trace-events';
constructor(codegenMode: 'actions' | 'trace-events', context: BrowserContext, params: channels.BrowserContextEnableRecorderParams, delegate: ContextRecorderDelegate) {
constructor(context: BrowserContext, params: channels.BrowserContextEnableRecorderParams, delegate: ContextRecorderDelegate) {
super();
this._codegenMode = codegenMode;
this._context = context;
this._params = params;
this._delegate = delegate;
@ -150,12 +148,6 @@ export class ContextRecorder extends EventEmitter {
setEnabled(enabled: boolean) {
this._collection.setEnabled(enabled);
if (this._codegenMode === 'trace-events') {
if (enabled)
this._context.tracing.startChunk({ name: 'trace', title: 'trace' }).catch(() => {});
else
this._context.tracing.stopChunk({ mode: 'discard' }).catch(() => {});
}
}
dispose() {

View file

@ -20,10 +20,8 @@ import type { Page } from '../page';
import type { Signal } from '../../../../recorder/src/actions';
import type * as actions from '@recorder/actions';
import { monotonicTime } from '../../utils/time';
import { callMetadataForAction, collapseActions } from './recorderUtils';
import { serializeError } from '../errors';
import { collapseActions } from './recorderUtils';
import { performAction } from './recorderRunner';
import type { CallMetadata } from '@protocol/callMetadata';
import { isUnderTest } from '../../utils/debug';
export class RecorderCollection extends EventEmitter {
@ -46,8 +44,8 @@ export class RecorderCollection extends EventEmitter {
}
async performAction(actionInContext: actions.ActionInContext) {
await this._addAction(actionInContext, async callMetadata => {
await performAction(callMetadata, this._pageAliases, actionInContext);
await this._addAction(actionInContext, async () => {
await performAction(this._pageAliases, actionInContext);
});
}
@ -60,7 +58,7 @@ export class RecorderCollection extends EventEmitter {
this._addAction(actionInContext).catch(() => {});
}
private async _addAction(actionInContext: actions.ActionInContext, callback?: (callMetadata: CallMetadata) => Promise<void>) {
private async _addAction(actionInContext: actions.ActionInContext, callback?: () => Promise<void>) {
if (!this._enabled)
return;
if (actionInContext.action.name === 'openPage' || actionInContext.action.name === 'closePage') {
@ -69,18 +67,10 @@ export class RecorderCollection extends EventEmitter {
return;
}
const { callMetadata, mainFrame } = callMetadataForAction(this._pageAliases, actionInContext);
await mainFrame.instrumentation.onBeforeCall(mainFrame, callMetadata);
this._actions.push(actionInContext);
this._fireChange();
const error = await callback?.(callMetadata).catch((e: Error) => e);
callMetadata.endTime = monotonicTime();
actionInContext.endTime = callMetadata.endTime;
callMetadata.error = error ? serializeError(error) : undefined;
// Do not wait for onAfterCall so that performAction returned immediately after the action.
mainFrame.instrumentation.onAfterCall(mainFrame, callMetadata).then(() => {
this._fireChange();
}).catch(() => {});
await callback?.().catch();
actionInContext.endTime = monotonicTime();
}
signal(pageAlias: string, frame: Frame, signal: Signal) {

View file

@ -1,126 +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 path from 'path';
import type { CallLog, ElementInfo, Mode, Source } from '@recorder/recorderTypes';
import { EventEmitter } from 'events';
import type { IRecorder, IRecorderApp, IRecorderAppFactory } from './recorderFrontend';
import { installRootRedirect, openTraceViewerApp, startTraceViewerServer } from '../trace/viewer/traceViewer';
import type { TraceViewerServerOptions } from '../trace/viewer/traceViewer';
import type { BrowserContext } from '../browserContext';
import type { HttpServer, Transport } from '../../utils/httpServer';
import type { Page } from '../page';
import { ManualPromise } from '../../utils/manualPromise';
import type * as actions from '@recorder/actions';
export class RecorderInTraceViewer extends EventEmitter implements IRecorderApp {
readonly wsEndpointForTest: string | undefined;
private _transport: RecorderTransport;
private _tracePage: Page;
private _traceServer: HttpServer;
static factory(context: BrowserContext): IRecorderAppFactory {
return async (recorder: IRecorder) => {
const transport = new RecorderTransport();
const trace = path.join(context._browser.options.tracesDir, 'trace');
const { wsEndpointForTest, tracePage, traceServer } = await openApp(trace, { transport, headless: !context._browser.options.headful });
return new RecorderInTraceViewer(transport, tracePage, traceServer, wsEndpointForTest);
};
}
constructor(transport: RecorderTransport, tracePage: Page, traceServer: HttpServer, wsEndpointForTest: string | undefined) {
super();
this._transport = transport;
this._transport.eventSink.resolve(this);
this._tracePage = tracePage;
this._traceServer = traceServer;
this.wsEndpointForTest = wsEndpointForTest;
this._tracePage.once('close', () => {
this.close();
});
}
async close(): Promise<void> {
await this._tracePage.context().close({ reason: 'Recorder window closed' });
await this._traceServer.stop();
}
async setPaused(paused: boolean): Promise<void> {
this._transport.deliverEvent('setPaused', { paused });
}
async setMode(mode: Mode): Promise<void> {
this._transport.deliverEvent('setMode', { mode });
}
async setRunningFile(file: string | undefined): Promise<void> {
this._transport.deliverEvent('setRunningFile', { file });
}
async elementPicked(elementInfo: ElementInfo, userGesture?: boolean): Promise<void> {
this._transport.deliverEvent('elementPicked', { elementInfo, userGesture });
}
async updateCallLogs(callLogs: CallLog[]): Promise<void> {
this._transport.deliverEvent('updateCallLogs', { callLogs });
}
async setSources(sources: Source[]): Promise<void> {
this._transport.deliverEvent('setSources', { sources });
if (process.env.PWTEST_CLI_IS_UNDER_TEST && sources.length) {
if ((process as any)._didSetSourcesForTest(sources[0].text))
this.close();
}
}
async setActions(actions: actions.ActionInContext[], sources: Source[]): Promise<void> {
this._transport.deliverEvent('setActions', { actions, sources });
}
}
async function openApp(trace: string, options?: TraceViewerServerOptions & { headless?: boolean }): Promise<{ wsEndpointForTest: string | undefined, tracePage: Page, traceServer: HttpServer }> {
const traceServer = await startTraceViewerServer(options);
await installRootRedirect(traceServer, [trace], { ...options, webApp: 'recorder.html' });
const page = await openTraceViewerApp(traceServer.urlPrefix('precise'), 'chromium', options);
return { wsEndpointForTest: page.context()._browser.options.wsEndpoint, tracePage: page, traceServer };
}
class RecorderTransport implements Transport {
private _connected = new ManualPromise<void>();
readonly eventSink = new ManualPromise<EventEmitter>();
constructor() {
}
onconnect() {
this._connected.resolve();
}
async dispatch(method: string, params: any): Promise<any> {
const eventSink = await this.eventSink;
eventSink.emit('event', { event: method, params });
}
onclose() {
}
deliverEvent(method: string, params: any) {
this._connected.then(() => this.sendEvent?.(method, params));
}
sendEvent?: (method: string, params: any) => void;
close?: () => void;
}

View file

@ -16,14 +16,14 @@
import { serializeExpectedTextValues } from '../../utils';
import { toKeyboardModifiers } from '../codegen/language';
import type { CallMetadata } from '../instrumentation';
import { serverSideCallMetadata } from '../instrumentation';
import type { Page } from '../page';
import type * as actions from '@recorder/actions';
import type * as types from '../types';
import { mainFrameForAction } from './recorderUtils';
import { buildFullSelector } from '../../utils/isomorphic/recorderUtils';
import { buildFullSelector, mainFrameForAction } from './recorderUtils';
export async function performAction(callMetadata: CallMetadata, pageAliases: Map<Page, string>, actionInContext: actions.ActionInContext) {
export async function performAction(pageAliases: Map<Page, string>, actionInContext: actions.ActionInContext) {
const callMetadata = serverSideCallMetadata();
const mainFrame = mainFrameForAction(pageAliases, actionInContext);
const { action } = actionInContext;

View file

@ -19,8 +19,10 @@ import type { CallLog, CallLogStatus } from '@recorder/recorderTypes';
import type { Page } from '../page';
import type { Frame } from '../frames';
import type * as actions from '@recorder/actions';
import { createGuid } from '../../utils';
import { buildFullSelector, traceParamsForAction } from '../../utils/isomorphic/recorderUtils';
export function buildFullSelector(framePath: string[], selector: string) {
return [...framePath, selector].join(' >> internal:control=enter-frame >> ');
}
export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus): CallLog {
let title = metadata.apiName || metadata.method;
@ -70,26 +72,6 @@ export async function frameForAction(pageAliases: Map<Page, string>, actionInCon
return result.frame;
}
export function callMetadataForAction(pageAliases: Map<Page, string>, actionInContext: actions.ActionInContext): { callMetadata: CallMetadata, mainFrame: Frame } {
const mainFrame = mainFrameForAction(pageAliases, actionInContext);
const { method, apiName, params } = traceParamsForAction(actionInContext);
const callMetadata: CallMetadata = {
id: `call@${createGuid()}`,
apiName,
objectId: mainFrame.guid,
pageId: mainFrame._page.guid,
frameId: mainFrame.guid,
startTime: actionInContext.startTime,
endTime: 0,
type: 'Frame',
method,
params,
log: [],
};
return { callMetadata, mainFrame };
}
export function collapseActions(actions: actions.ActionInContext[]): actions.ActionInContext[] {
const result: actions.ActionInContext[] = [];
for (const action of actions) {

View file

@ -95,7 +95,7 @@ export async function installDependenciesLinux(targets: Set<DependencyGroup>, dr
for (const target of targets) {
const info = deps[platform];
if (!info) {
console.warn(`Cannot install dependencies for ${platform}!`); // eslint-disable-line no-console
console.warn(`Cannot install dependencies for ${platform} with Playwright ${getPlaywrightVersion()}!`); // eslint-disable-line no-console
return;
}
libraries.push(...info[target]);

View file

@ -59,12 +59,13 @@ export class RawKeyboardImpl implements input.RawKeyboard {
this._session = session;
}
async keydown(modifiers: Set<types.KeyboardModifier>, code: string, keyCode: number, keyCodeWithoutLocation: number, key: string, location: number, autoRepeat: boolean, text: string | undefined): Promise<void> {
async keydown(modifiers: Set<types.KeyboardModifier>, keyName: string, description: input.KeyDescription, autoRepeat: boolean): Promise<void> {
const parts = [];
for (const modifier of (['Shift', 'Control', 'Alt', 'Meta']) as types.KeyboardModifier[]) {
if (modifiers.has(modifier))
parts.push(modifier);
}
const { code, keyCode, key, text } = description;
parts.push(code);
const shortcut = parts.join('+');
let commands = macEditingCommands[shortcut];
@ -80,18 +81,19 @@ export class RawKeyboardImpl implements input.RawKeyboard {
unmodifiedText: text,
autoRepeat,
macCommands: commands,
isKeypad: location === input.keypadLocation
isKeypad: description.location === input.keypadLocation
});
}
async keyup(modifiers: Set<types.KeyboardModifier>, code: string, keyCode: number, keyCodeWithoutLocation: number, key: string, location: number): Promise<void> {
async keyup(modifiers: Set<types.KeyboardModifier>, keyName: string, description: input.KeyDescription): Promise<void> {
const { code, key } = description;
await this._pageProxySession.send('Input.dispatchKeyEvent', {
type: 'keyUp',
modifiers: toModifiersMask(modifiers),
key,
windowsVirtualKeyCode: keyCode,
windowsVirtualKeyCode: description.keyCode,
code,
isKeypad: location === input.keypadLocation
isKeypad: description.location === input.keypadLocation
});
}

View file

@ -74,14 +74,18 @@ function calculatePlatform(): { hostPlatform: HostPlatform, isOfficiallySupporte
// KDE Neon is ubuntu-based and has the same versions.
// TUXEDO OS is ubuntu-based and has the same versions.
if (distroInfo?.id === 'ubuntu' || distroInfo?.id === 'pop' || distroInfo?.id === 'neon' || distroInfo?.id === 'tuxedo') {
const isOfficiallySupportedPlatform = distroInfo?.id === 'ubuntu';
if (parseInt(distroInfo.version, 10) <= 19)
const isUbuntu = distroInfo?.id === 'ubuntu';
const version = distroInfo?.version;
const major = parseInt(distroInfo.version, 10);
if (major < 20)
return { hostPlatform: ('ubuntu18.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: false };
if (parseInt(distroInfo.version, 10) <= 21)
return { hostPlatform: ('ubuntu20.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform };
if (parseInt(distroInfo.version, 10) <= 22)
return { hostPlatform: ('ubuntu22.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform };
return { hostPlatform: ('ubuntu24.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform };
if (major < 22)
return { hostPlatform: ('ubuntu20.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: isUbuntu && version === '20.04' };
if (major < 24)
return { hostPlatform: ('ubuntu22.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: isUbuntu && version === '22.04' };
if (major < 26)
return { hostPlatform: ('ubuntu24.04' + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: isUbuntu && version === '24.04' };
return { hostPlatform: ('ubuntu' + distroInfo.version + archSuffix) as HostPlatform, isOfficiallySupportedPlatform: false };
}
// Linux Mint is ubuntu-based but does not have the same versions
if (distroInfo?.id === 'linuxmint') {

View file

@ -180,8 +180,9 @@ export function parseAriaSnapshot(yaml: YamlLibrary, text: string, options: yaml
// - role "name":
// - child
const valueIsSequence = value instanceof yaml.YAMLSeq ;
const valueIsSequence = value instanceof yaml.YAMLSeq;
if (valueIsSequence) {
container.children.push(childNode);
convertSeq(childNode, value as yamlTypes.YAMLSeq);
continue;
}

View file

@ -43,7 +43,7 @@ export function parseCSS(selector: string, customNames: Set<string>): { selector
if (!(tokens[tokens.length - 1] instanceof css.EOFToken))
tokens.push(new css.EOFToken());
} catch (e) {
const newMessage = e.message + ` while parsing selector "${selector}"`;
const newMessage = e.message + ` while parsing css selector "${selector}". Did you mean to CSS.escape it?`;
const index = (e.stack || '').indexOf(e.message);
if (index !== -1)
e.stack = e.stack.substring(0, index) + newMessage + e.stack.substring(index + e.message.length);
@ -68,13 +68,13 @@ export function parseCSS(selector: string, customNames: Set<string>): { selector
(token instanceof css.PercentageToken);
});
if (unsupportedToken)
throw new InvalidSelectorError(`Unsupported token "${unsupportedToken.toSource()}" while parsing selector "${selector}"`);
throw new InvalidSelectorError(`Unsupported token "${unsupportedToken.toSource()}" while parsing css selector "${selector}". Did you mean to CSS.escape it?`);
let pos = 0;
const names = new Set<string>();
function unexpected() {
return new InvalidSelectorError(`Unexpected token "${tokens[pos].toSource()}" while parsing selector "${selector}"`);
return new InvalidSelectorError(`Unexpected token "${tokens[pos].toSource()}" while parsing css selector "${selector}". Did you mean to CSS.escape it?`);
}
function skipWhitespace() {
@ -246,7 +246,7 @@ export function parseCSS(selector: string, customNames: Set<string>): { selector
if (!isEOF())
throw unexpected();
if (result.some(arg => typeof arg !== 'object' || !('simples' in arg)))
throw new InvalidSelectorError(`Error while parsing selector "${selector}"`);
throw new InvalidSelectorError(`Error while parsing css selector "${selector}". Did you mean to CSS.escape it?`);
return { selector: result as CSSComplexSelector[], names: Array.from(names) };
}

View file

@ -1,163 +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 * as recorderActions from '@recorder/actions';
import type * as channels from '@protocol/channels';
import type * as types from '../../server/types';
export function buildFullSelector(framePath: string[], selector: string) {
return [...framePath, selector].join(' >> internal:control=enter-frame >> ');
}
const kDefaultTimeout = 5_000;
export function traceParamsForAction(actionInContext: recorderActions.ActionInContext): { method: string, apiName: string, params: any } {
const { action } = actionInContext;
switch (action.name) {
case 'navigate': {
const params: channels.FrameGotoParams = {
url: action.url,
};
return { method: 'goto', apiName: 'page.goto', params };
}
case 'openPage':
case 'closePage':
throw new Error('Not reached');
}
const selector = buildFullSelector(actionInContext.frame.framePath, action.selector);
switch (action.name) {
case 'click': {
const params: channels.FrameClickParams = {
selector,
strict: true,
modifiers: toKeyboardModifiers(action.modifiers),
button: action.button,
clickCount: action.clickCount,
position: action.position,
};
return { method: 'click', apiName: 'locator.click', params };
}
case 'press': {
const params: channels.FramePressParams = {
selector,
strict: true,
key: [...toKeyboardModifiers(action.modifiers), action.key].join('+'),
};
return { method: 'press', apiName: 'locator.press', params };
}
case 'fill': {
const params: channels.FrameFillParams = {
selector,
strict: true,
value: action.text,
};
return { method: 'fill', apiName: 'locator.fill', params };
}
case 'setInputFiles': {
const params: channels.FrameSetInputFilesParams = {
selector,
strict: true,
localPaths: action.files,
};
return { method: 'setInputFiles', apiName: 'locator.setInputFiles', params };
}
case 'check': {
const params: channels.FrameCheckParams = {
selector,
strict: true,
};
return { method: 'check', apiName: 'locator.check', params };
}
case 'uncheck': {
const params: channels.FrameUncheckParams = {
selector,
strict: true,
};
return { method: 'uncheck', apiName: 'locator.uncheck', params };
}
case 'select': {
const params: channels.FrameSelectOptionParams = {
selector,
strict: true,
options: action.options.map(option => ({ value: option })),
};
return { method: 'selectOption', apiName: 'locator.selectOption', params };
}
case 'assertChecked': {
const params: channels.FrameExpectParams = {
selector: action.selector,
expression: 'to.be.checked',
isNot: !action.checked,
timeout: kDefaultTimeout,
};
return { method: 'expect', apiName: 'expect.toBeChecked', params };
}
case 'assertText': {
const params: channels.FrameExpectParams = {
selector,
expression: 'to.have.text',
expectedText: [],
isNot: false,
timeout: kDefaultTimeout,
};
return { method: 'expect', apiName: 'expect.toContainText', params };
}
case 'assertValue': {
const params: channels.FrameExpectParams = {
selector,
expression: 'to.have.value',
expectedValue: undefined,
isNot: false,
timeout: kDefaultTimeout,
};
return { method: 'expect', apiName: 'expect.toHaveValue', params };
}
case 'assertVisible': {
const params: channels.FrameExpectParams = {
selector,
expression: 'to.be.visible',
isNot: false,
timeout: kDefaultTimeout,
};
return { method: 'expect', apiName: 'expect.toBeVisible', params };
}
case 'assertSnapshot': {
const params: channels.FrameExpectParams = {
selector,
expression: 'to.match.snapshot',
expectedText: [],
isNot: false,
timeout: kDefaultTimeout,
};
return { method: 'expect', apiName: 'expect.toMatchAriaSnapshot', params };
}
}
}
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;
}

View file

@ -156,8 +156,3 @@ export function compressCallLog(log: string[]): string[] {
}
return lines;
}
export type ExpectZone = {
title: string;
stepId: string;
};

View file

@ -16,7 +16,7 @@
import { AsyncLocalStorage } from 'async_hooks';
export type ZoneType = 'apiZone' | 'expectZone' | 'stepZone';
export type ZoneType = 'apiZone' | 'stepZone';
class ZoneManager {
private readonly _asyncLocalStorage = new AsyncLocalStorage<Zone | undefined>();

View file

@ -685,8 +685,8 @@ percentage [0 - 100] for scroll driven animations
/**
* The unique request id.
*/
requestId: Network.RequestId;
url?: string;
requestId?: Network.RequestId;
url: string;
}
/**
* Information about the frame affected by an inspector issue.
@ -697,6 +697,20 @@ percentage [0 - 100] for scroll driven animations
export type CookieExclusionReason = "ExcludeSameSiteUnspecifiedTreatedAsLax"|"ExcludeSameSiteNoneInsecure"|"ExcludeSameSiteLax"|"ExcludeSameSiteStrict"|"ExcludeInvalidSameParty"|"ExcludeSamePartyCrossPartyContext"|"ExcludeDomainNonASCII"|"ExcludeThirdPartyCookieBlockedInFirstPartySet"|"ExcludeThirdPartyPhaseout"|"ExcludePortMismatch"|"ExcludeSchemeMismatch";
export type CookieWarningReason = "WarnSameSiteUnspecifiedCrossSiteContext"|"WarnSameSiteNoneInsecure"|"WarnSameSiteUnspecifiedLaxAllowUnsafe"|"WarnSameSiteStrictLaxDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeLax"|"WarnSameSiteLaxCrossDowngradeStrict"|"WarnSameSiteLaxCrossDowngradeLax"|"WarnAttributeValueExceedsMaxSize"|"WarnDomainNonASCII"|"WarnThirdPartyPhaseout"|"WarnCrossSiteRedirectDowngradeChangesInclusion"|"WarnDeprecationTrialMetadata"|"WarnThirdPartyCookieHeuristic";
export type CookieOperation = "SetCookie"|"ReadCookie";
/**
* Represents the category of insight that a cookie issue falls under.
*/
export type InsightType = "GitHubResource"|"GracePeriod"|"Heuristics";
/**
* Information about the suggested solution to a cookie issue.
*/
export interface CookieIssueInsight {
type: InsightType;
/**
* Link to table entry in third-party cookie migration readiness list.
*/
tableEntryUrl?: string;
}
/**
* This information is currently necessary, as the front-end has a difficult
time finding a specific cookie. With this, we can convey specific error
@ -721,6 +735,10 @@ may be used by the front-end as additional context.
siteForCookies?: string;
cookieUrl?: string;
request?: AffectedRequest;
/**
* The recommended solution to the issue.
*/
insight?: CookieIssueInsight;
}
export type MixedContentResolutionStatus = "MixedContentBlocked"|"MixedContentAutomaticallyUpgraded"|"MixedContentWarning";
export type MixedContentResourceType = "AttributionSrc"|"Audio"|"Beacon"|"CSPReport"|"Download"|"EventSource"|"Favicon"|"Font"|"Form"|"Frame"|"Image"|"Import"|"JSON"|"Manifest"|"Ping"|"PluginData"|"PluginResource"|"Prefetch"|"Resource"|"Script"|"ServiceWorker"|"SharedWorker"|"SpeculationRules"|"Stylesheet"|"Track"|"Video"|"Worker"|"XMLHttpRequest"|"XSLT";
@ -758,7 +776,7 @@ Does not always exist (e.g. for unsafe form submission urls).
* Enum indicating the reason a response has been blocked. These reasons are
refinements of the net error BLOCKED_BY_RESPONSE.
*/
export type BlockedByResponseReason = "CoepFrameResourceNeedsCoepHeader"|"CoopSandboxedIFrameCannotNavigateToCoopPage"|"CorpNotSameOrigin"|"CorpNotSameOriginAfterDefaultedToSameOriginByCoep"|"CorpNotSameOriginAfterDefaultedToSameOriginByDip"|"CorpNotSameOriginAfterDefaultedToSameOriginByCoepAndDip"|"CorpNotSameSite";
export type BlockedByResponseReason = "CoepFrameResourceNeedsCoepHeader"|"CoopSandboxedIFrameCannotNavigateToCoopPage"|"CorpNotSameOrigin"|"CorpNotSameOriginAfterDefaultedToSameOriginByCoep"|"CorpNotSameOriginAfterDefaultedToSameOriginByDip"|"CorpNotSameOriginAfterDefaultedToSameOriginByCoepAndDip"|"CorpNotSameSite"|"SRIMessageSignatureMismatch";
/**
* Details for a request that has been blocked with the BLOCKED_BY_RESPONSE
code. Currently only used for COEP/COOP, but may be extended to include
@ -963,6 +981,15 @@ features, encourage the use of new ones, and provide general guidance.
failureMessage: string;
requestId?: Network.RequestId;
}
export type SelectElementAccessibilityIssueReason = "DisallowedSelectChild"|"DisallowedOptGroupChild"|"NonPhrasingContentOptionChild"|"InteractiveContentOptionChild"|"InteractiveContentLegendChild";
/**
* This isue warns about errors in the select element content model.
*/
export interface SelectElementAccessibilityIssueDetails {
nodeId: DOM.BackendNodeId;
selectElementAccessibilityIssueReason: SelectElementAccessibilityIssueReason;
hasDisallowedAttributes: boolean;
}
export type StyleSheetLoadingIssueReason = "LateImportRule"|"RequestFailed";
/**
* This issue warns when a referenced stylesheet couldn't be loaded.
@ -1005,7 +1032,7 @@ registrations being ignored.
optional fields in InspectorIssueDetails to convey more specific
information about the kind of issue.
*/
export type InspectorIssueCode = "CookieIssue"|"MixedContentIssue"|"BlockedByResponseIssue"|"HeavyAdIssue"|"ContentSecurityPolicyIssue"|"SharedArrayBufferIssue"|"LowTextContrastIssue"|"CorsIssue"|"AttributionReportingIssue"|"QuirksModeIssue"|"NavigatorUserAgentIssue"|"GenericIssue"|"DeprecationIssue"|"ClientHintIssue"|"FederatedAuthRequestIssue"|"BounceTrackingIssue"|"CookieDeprecationMetadataIssue"|"StylesheetLoadingIssue"|"FederatedAuthUserInfoRequestIssue"|"PropertyRuleIssue"|"SharedDictionaryIssue";
export type InspectorIssueCode = "CookieIssue"|"MixedContentIssue"|"BlockedByResponseIssue"|"HeavyAdIssue"|"ContentSecurityPolicyIssue"|"SharedArrayBufferIssue"|"LowTextContrastIssue"|"CorsIssue"|"AttributionReportingIssue"|"QuirksModeIssue"|"NavigatorUserAgentIssue"|"GenericIssue"|"DeprecationIssue"|"ClientHintIssue"|"FederatedAuthRequestIssue"|"BounceTrackingIssue"|"CookieDeprecationMetadataIssue"|"StylesheetLoadingIssue"|"FederatedAuthUserInfoRequestIssue"|"PropertyRuleIssue"|"SharedDictionaryIssue"|"SelectElementAccessibilityIssue";
/**
* This struct holds a list of optional fields with additional information
specific to the kind of issue. When adding a new issue code, please also
@ -1033,6 +1060,7 @@ add a new optional field to this type.
propertyRuleIssueDetails?: PropertyRuleIssueDetails;
federatedAuthUserInfoRequestIssueDetails?: FederatedAuthUserInfoRequestIssueDetails;
sharedDictionaryIssueDetails?: SharedDictionaryIssueDetails;
selectElementAccessibilityIssueDetails?: SelectElementAccessibilityIssueDetails;
}
/**
* A unique id for a DevTools inspector issue. Allows other entities (e.g.
@ -1534,7 +1562,7 @@ events afterwards if enabled and recording.
*/
windowState?: WindowState;
}
export type PermissionType = "accessibilityEvents"|"audioCapture"|"backgroundSync"|"backgroundFetch"|"capturedSurfaceControl"|"clipboardReadWrite"|"clipboardSanitizedWrite"|"displayCapture"|"durableStorage"|"flash"|"geolocation"|"idleDetection"|"localFonts"|"midi"|"midiSysex"|"nfc"|"notifications"|"paymentHandler"|"periodicBackgroundSync"|"protectedMediaIdentifier"|"sensors"|"storageAccess"|"speakerSelection"|"topLevelStorageAccess"|"videoCapture"|"videoCapturePanTiltZoom"|"wakeLockScreen"|"wakeLockSystem"|"webAppInstallation"|"windowManagement";
export type PermissionType = "ar"|"audioCapture"|"automaticFullscreen"|"backgroundFetch"|"backgroundSync"|"cameraPanTiltZoom"|"capturedSurfaceControl"|"clipboardReadWrite"|"clipboardSanitizedWrite"|"displayCapture"|"durableStorage"|"geolocation"|"handTracking"|"idleDetection"|"keyboardLock"|"localFonts"|"midi"|"midiSysex"|"nfc"|"notifications"|"paymentHandler"|"periodicBackgroundSync"|"pointerLock"|"protectedMediaIdentifier"|"sensors"|"smartCard"|"speakerSelection"|"storageAccess"|"topLevelStorageAccess"|"videoCapture"|"vr"|"wakeLockScreen"|"wakeLockSystem"|"webAppInstallation"|"webPrinting"|"windowManagement";
export type PermissionSetting = "granted"|"denied"|"prompt";
/**
* Definition of PermissionDescriptor defined in the Permissions API:
@ -1961,6 +1989,19 @@ inspector" rules), "regular" for regular stylesheets.
*/
matches: RuleMatch[];
}
/**
* CSS style coming from animations with the name of the animation.
*/
export interface CSSAnimationStyle {
/**
* The name of the animation.
*/
name?: string;
/**
* The style coming from the animation.
*/
style: CSSStyle;
}
/**
* Inherited CSS rule collection from ancestor node.
*/
@ -1974,6 +2015,19 @@ inspector" rules), "regular" for regular stylesheets.
*/
matchedCSSRules: RuleMatch[];
}
/**
* Inherited CSS style collection for animated styles from ancestor node.
*/
export interface InheritedAnimatedStyleEntry {
/**
* Styles coming from the animations of the ancestor, if any, in the style inheritance chain.
*/
animationStyles?: CSSAnimationStyle[];
/**
* The style coming from the transitions of the ancestor, if any, in the style inheritance chain.
*/
transitionsStyle?: CSSStyle;
}
/**
* Inherited pseudo element matches from pseudos of an ancestor node.
*/
@ -2897,6 +2951,21 @@ the browser.
}
export type forcePseudoStateReturnValue = {
}
/**
* Ensures that the given node is in its starting-style state.
*/
export type forceStartingStyleParameters = {
/**
* The element id for which to force the starting-style state.
*/
nodeId: DOM.NodeId;
/**
* Boolean indicating if this is on or off.
*/
forced: boolean;
}
export type forceStartingStyleReturnValue = {
}
export type getBackgroundColorsParameters = {
/**
* Id of the node to get background colors for.
@ -2934,6 +3003,46 @@ be ignored (as if the image had failed to load).
*/
computedStyle: CSSComputedStyleProperty[];
}
/**
* Resolve the specified values in the context of the provided element.
For example, a value of '1em' is evaluated according to the computed
'font-size' of the element and a value 'calc(1px + 2px)' will be
resolved to '3px'.
*/
export type resolveValuesParameters = {
/**
* Substitution functions (var()/env()/attr()) and cascade-dependent
keywords (revert/revert-layer) do not work.
*/
values: string[];
/**
* Id of the node in whose context the expression is evaluated
*/
nodeId: DOM.NodeId;
/**
* Only longhands and custom property names are accepted.
*/
propertyName?: string;
/**
* Pseudo element type, only works for pseudo elements that generate
elements in the tree, such as ::before and ::after.
*/
pseudoType?: DOM.PseudoType;
/**
* Pseudo element custom ident.
*/
pseudoIdentifier?: string;
}
export type resolveValuesReturnValue = {
results: string[];
}
export type getLonghandPropertiesParameters = {
shorthandName: string;
value: string;
}
export type getLonghandPropertiesReturnValue = {
longhandProperties: CSSProperty[];
}
/**
* Returns the styles defined inline (explicitly in the "style" attribute and implicitly, using DOM
attributes) for a DOM node identified by `nodeId`.
@ -2951,6 +3060,28 @@ attributes) for a DOM node identified by `nodeId`.
*/
attributesStyle?: CSSStyle;
}
/**
* Returns the styles coming from animations & transitions
including the animation & transition styles coming from inheritance chain.
*/
export type getAnimatedStylesForNodeParameters = {
nodeId: DOM.NodeId;
}
export type getAnimatedStylesForNodeReturnValue = {
/**
* Styles coming from animations.
*/
animationStyles?: CSSAnimationStyle[];
/**
* Style coming from transitions.
*/
transitionsStyle?: CSSStyle;
/**
* Inherited style entries for animationsStyle and transitionsStyle from
the inheritance chain of the element.
*/
inherited?: InheritedAnimatedStyleEntry[];
}
/**
* Returns requested styles for a DOM node identified by `nodeId`.
*/
@ -3603,7 +3734,7 @@ front-end.
/**
* Pseudo element type.
*/
export type PseudoType = "first-line"|"first-letter"|"check"|"before"|"after"|"select-arrow"|"marker"|"backdrop"|"column"|"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"|"placeholder"|"file-selector-button"|"details-content"|"picker";
export type PseudoType = "first-line"|"first-letter"|"checkmark"|"before"|"after"|"picker-icon"|"marker"|"backdrop"|"column"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scroll-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"|"placeholder"|"file-selector-button"|"details-content"|"picker";
/**
* Shadow root type.
*/
@ -8616,7 +8747,7 @@ applicable or not known.
/**
* The reason why request was blocked.
*/
export type BlockedReason = "other"|"csp"|"mixed-content"|"origin"|"inspector"|"subresource-filter"|"content-type"|"coep-frame-resource-needs-coep-header"|"coop-sandboxed-iframe-cannot-navigate-to-coop-page"|"corp-not-same-origin"|"corp-not-same-origin-after-defaulted-to-same-origin-by-coep"|"corp-not-same-origin-after-defaulted-to-same-origin-by-dip"|"corp-not-same-origin-after-defaulted-to-same-origin-by-coep-and-dip"|"corp-not-same-site";
export type BlockedReason = "other"|"csp"|"mixed-content"|"origin"|"inspector"|"subresource-filter"|"content-type"|"coep-frame-resource-needs-coep-header"|"coop-sandboxed-iframe-cannot-navigate-to-coop-page"|"corp-not-same-origin"|"corp-not-same-origin-after-defaulted-to-same-origin-by-coep"|"corp-not-same-origin-after-defaulted-to-same-origin-by-dip"|"corp-not-same-origin-after-defaulted-to-same-origin-by-coep-and-dip"|"corp-not-same-site"|"sri-message-signature-mismatch";
/**
* The reason why request was blocked.
*/
@ -9917,6 +10048,9 @@ are represented by the invalid cookie line string instead of a proper cookie.
blockedCookies: BlockedSetCookieWithReason[];
/**
* Raw response headers as they were received over the wire.
Duplicate headers in the response are represented as a single key with their values
concatentated using `\n` as the separator.
See also `headersText` that contains verbatim text for HTTP/1.*.
*/
headers: Headers;
/**
@ -9962,6 +10096,9 @@ Only one responseReceivedEarlyHints may be fired for eached responseReceived eve
requestId: RequestId;
/**
* Raw response headers as they were received over the wire.
Duplicate headers in the response are represented as a single key with their values
concatentated using `\n` as the separator.
See also `headersText` that contains verbatim text for HTTP/1.*.
*/
headers: Headers;
}
@ -9978,7 +10115,7 @@ or after the response was received.
of the operation already exists und thus, the operation was abort
preemptively (e.g. a cache hit).
*/
status: "Ok"|"InvalidArgument"|"MissingIssuerKeys"|"FailedPrecondition"|"ResourceExhausted"|"AlreadyExists"|"ResourceLimited"|"Unauthorized"|"BadResponse"|"InternalError"|"UnknownError"|"FulfilledLocally";
status: "Ok"|"InvalidArgument"|"MissingIssuerKeys"|"FailedPrecondition"|"ResourceExhausted"|"AlreadyExists"|"ResourceLimited"|"Unauthorized"|"BadResponse"|"InternalError"|"UnknownError"|"FulfilledLocally"|"SiteIssuerLimit";
type: TrustTokenOperationType;
requestId: RequestId;
/**
@ -10672,6 +10809,26 @@ should be omitted for worker targets.
export type loadNetworkResourceReturnValue = {
resource: LoadNetworkResourcePageResult;
}
/**
* Sets Controls for third-party cookie access
Page reload is required before the new cookie bahavior will be observed
*/
export type setCookieControlsParameters = {
/**
* Whether 3pc restriction is enabled.
*/
enableThirdPartyCookieRestriction: boolean;
/**
* Whether 3pc grace period exception should be enabled; false by default.
*/
disableThirdPartyCookieMetadata: boolean;
/**
* Whether 3pc heuristics exceptions should be enabled; false by default.
*/
disableThirdPartyCookieHeuristics: boolean;
}
export type setCookieControlsReturnValue = {
}
}
/**
@ -11545,7 +11702,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"|"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"|"controlled-frame"|"cross-origin-isolated"|"deferred-fetch"|"digital-credentials-get"|"direct-sockets"|"direct-sockets-private"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"fenced-unpartitioned-storage-read"|"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"|"popins"|"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-app-installation"|"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"|"controlled-frame"|"cross-origin-isolated"|"deferred-fetch"|"deferred-fetch-minimal"|"digital-credentials-get"|"direct-sockets"|"direct-sockets-private"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"fenced-unpartitioned-storage-read"|"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"|"popins"|"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-app-installation"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking";
/**
* Reason for a permissions policy feature to be disabled.
*/
@ -15519,6 +15676,14 @@ Parts of the URL other than those constituting origin are ignored.
* The initial URL the page will be navigated to. An empty string indicates about:blank.
*/
url: string;
/**
* Frame left origin in DIP (headless chrome only).
*/
left?: number;
/**
* Frame top origin in DIP (headless chrome only).
*/
top?: number;
/**
* Frame width in DIP (headless chrome only).
*/
@ -17151,6 +17316,16 @@ possible for multiple rule sets and links to trigger a single attempt.
ruleSetIds: RuleSetId[];
nodeIds: DOM.BackendNodeId[];
}
/**
* Chrome manages different types of preloads together using a
concept of preloading pipeline. For example, if a site uses a
SpeculationRules for prerender, Chrome first starts a prefetch and
then upgrades it to prerender.
CDP events for them are emitted separately but they share
`PreloadPipelineId`.
*/
export type PreloadPipelineId = string;
/**
* List of FinalStatus reasons for Prerender2.
*/
@ -17198,6 +17373,7 @@ filter out the ones that aren't necessary to the developers.
*/
export type prefetchStatusUpdatedPayload = {
key: PreloadingAttemptKey;
pipelineId: PreloadPipelineId;
/**
* The frame id of the frame initiating prefetch.
*/
@ -17212,6 +17388,7 @@ filter out the ones that aren't necessary to the developers.
*/
export type prerenderStatusUpdatedPayload = {
key: PreloadingAttemptKey;
pipelineId: PreloadPipelineId;
status: PreloadingStatus;
prerenderStatus?: PrerenderFinalStatus;
/**
@ -17922,6 +18099,10 @@ variables as its properties.
* Content hash of the script, SHA-256.
*/
hash: string;
/**
* For Wasm modules, the content of the `build_id` custom section.
*/
buildId: string;
/**
* Embedder-specific auxiliary data likely matching {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string}
*/
@ -17996,6 +18177,10 @@ scripts upon enabling debugger.
* Content hash of the script, SHA-256.
*/
hash: string;
/**
* For Wasm modules, the content of the `build_id` custom section.
*/
buildId: string;
/**
* Embedder-specific auxiliary data likely matching {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string}
*/
@ -20507,9 +20692,13 @@ Error was thrown.
"CSS.disable": CSS.disableParameters;
"CSS.enable": CSS.enableParameters;
"CSS.forcePseudoState": CSS.forcePseudoStateParameters;
"CSS.forceStartingStyle": CSS.forceStartingStyleParameters;
"CSS.getBackgroundColors": CSS.getBackgroundColorsParameters;
"CSS.getComputedStyleForNode": CSS.getComputedStyleForNodeParameters;
"CSS.resolveValues": CSS.resolveValuesParameters;
"CSS.getLonghandProperties": CSS.getLonghandPropertiesParameters;
"CSS.getInlineStylesForNode": CSS.getInlineStylesForNodeParameters;
"CSS.getAnimatedStylesForNode": CSS.getAnimatedStylesForNodeParameters;
"CSS.getMatchedStylesForNode": CSS.getMatchedStylesForNodeParameters;
"CSS.getMediaQueries": CSS.getMediaQueriesParameters;
"CSS.getPlatformFontsForNode": CSS.getPlatformFontsForNodeParameters;
@ -20751,6 +20940,7 @@ Error was thrown.
"Network.getSecurityIsolationStatus": Network.getSecurityIsolationStatusParameters;
"Network.enableReportingApi": Network.enableReportingApiParameters;
"Network.loadNetworkResource": Network.loadNetworkResourceParameters;
"Network.setCookieControls": Network.setCookieControlsParameters;
"Overlay.disable": Overlay.disableParameters;
"Overlay.enable": Overlay.enableParameters;
"Overlay.getHighlightObjectForTest": Overlay.getHighlightObjectForTestParameters;
@ -21119,9 +21309,13 @@ Error was thrown.
"CSS.disable": CSS.disableReturnValue;
"CSS.enable": CSS.enableReturnValue;
"CSS.forcePseudoState": CSS.forcePseudoStateReturnValue;
"CSS.forceStartingStyle": CSS.forceStartingStyleReturnValue;
"CSS.getBackgroundColors": CSS.getBackgroundColorsReturnValue;
"CSS.getComputedStyleForNode": CSS.getComputedStyleForNodeReturnValue;
"CSS.resolveValues": CSS.resolveValuesReturnValue;
"CSS.getLonghandProperties": CSS.getLonghandPropertiesReturnValue;
"CSS.getInlineStylesForNode": CSS.getInlineStylesForNodeReturnValue;
"CSS.getAnimatedStylesForNode": CSS.getAnimatedStylesForNodeReturnValue;
"CSS.getMatchedStylesForNode": CSS.getMatchedStylesForNodeReturnValue;
"CSS.getMediaQueries": CSS.getMediaQueriesReturnValue;
"CSS.getPlatformFontsForNode": CSS.getPlatformFontsForNodeReturnValue;
@ -21363,6 +21557,7 @@ Error was thrown.
"Network.getSecurityIsolationStatus": Network.getSecurityIsolationStatusReturnValue;
"Network.enableReportingApi": Network.enableReportingApiReturnValue;
"Network.loadNetworkResource": Network.loadNetworkResourceReturnValue;
"Network.setCookieControls": Network.setCookieControlsReturnValue;
"Overlay.disable": Overlay.disableReturnValue;
"Overlay.enable": Overlay.enableReturnValue;
"Overlay.getHighlightObjectForTest": Overlay.getHighlightObjectForTestReturnValue;

View file

@ -12429,7 +12429,7 @@ export interface Locator {
/**
* Captures the aria snapshot of the given element. Read more about [aria snapshots](https://playwright.dev/docs/aria-snapshots) and
* [expect(locator).toMatchAriaSnapshot(expected[, options])](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-match-aria-snapshot-2)
* [expect(locator).toMatchAriaSnapshot(expected[, options])](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-match-aria-snapshot-1)
* for the corresponding assertion.
*
* **Usage**

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/experimental-ct-core",
"version": "1.50.0-next",
"version": "1.51.0-next",
"description": "Playwright Component Testing Helpers",
"repository": {
"type": "git",
@ -26,8 +26,8 @@
}
},
"dependencies": {
"playwright-core": "1.50.0-next",
"playwright-core": "1.51.0-next",
"vite": "^5.2.8",
"playwright": "1.50.0-next"
"playwright": "1.51.0-next"
}
}

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/experimental-ct-react",
"version": "1.50.0-next",
"version": "1.51.0-next",
"description": "Playwright Component Testing for React",
"repository": {
"type": "git",
@ -30,7 +30,7 @@
"./package.json": "./package.json"
},
"dependencies": {
"@playwright/experimental-ct-core": "1.50.0-next",
"@playwright/experimental-ct-core": "1.51.0-next",
"@vitejs/plugin-react": "^4.2.1"
},
"bin": {

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/experimental-ct-react17",
"version": "1.50.0-next",
"version": "1.51.0-next",
"description": "Playwright Component Testing for React",
"repository": {
"type": "git",
@ -30,7 +30,7 @@
"./package.json": "./package.json"
},
"dependencies": {
"@playwright/experimental-ct-core": "1.50.0-next",
"@playwright/experimental-ct-core": "1.51.0-next",
"@vitejs/plugin-react": "^4.2.1"
},
"bin": {

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/experimental-ct-svelte",
"version": "1.50.0-next",
"version": "1.51.0-next",
"description": "Playwright Component Testing for Svelte",
"repository": {
"type": "git",
@ -30,7 +30,7 @@
"./package.json": "./package.json"
},
"dependencies": {
"@playwright/experimental-ct-core": "1.50.0-next",
"@playwright/experimental-ct-core": "1.51.0-next",
"@sveltejs/vite-plugin-svelte": "^3.0.1"
},
"devDependencies": {

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/experimental-ct-vue",
"version": "1.50.0-next",
"version": "1.51.0-next",
"description": "Playwright Component Testing for Vue",
"repository": {
"type": "git",
@ -30,7 +30,7 @@
"./package.json": "./package.json"
},
"dependencies": {
"@playwright/experimental-ct-core": "1.50.0-next",
"@playwright/experimental-ct-core": "1.51.0-next",
"@vitejs/plugin-vue": "^5.2.0"
},
"bin": {

View file

@ -1,6 +1,6 @@
{
"name": "playwright-firefox",
"version": "1.50.0-next",
"version": "1.51.0-next",
"description": "A high-level API to automate Firefox",
"repository": {
"type": "git",
@ -30,6 +30,6 @@
"install": "node install.js"
},
"dependencies": {
"playwright-core": "1.50.0-next"
"playwright-core": "1.51.0-next"
}
}

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/test",
"version": "1.50.0-next",
"version": "1.51.0-next",
"description": "A high-level API to automate web browsers",
"repository": {
"type": "git",
@ -30,6 +30,6 @@
},
"scripts": {},
"dependencies": {
"playwright": "1.50.0-next"
"playwright": "1.51.0-next"
}
}

View file

@ -1,6 +1,6 @@
{
"name": "playwright-webkit",
"version": "1.50.0-next",
"version": "1.51.0-next",
"description": "A high-level API to automate WebKit",
"repository": {
"type": "git",
@ -30,6 +30,6 @@
"install": "node install.js"
},
"dependencies": {
"playwright-core": "1.50.0-next"
"playwright-core": "1.51.0-next"
}
}

View file

@ -1,6 +1,6 @@
{
"name": "playwright",
"version": "1.50.0-next",
"version": "1.51.0-next",
"description": "A high-level API to automate web browsers",
"repository": {
"type": "git",
@ -56,7 +56,7 @@
},
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.50.0-next"
"playwright-core": "1.51.0-next"
},
"optionalDependencies": {
"fsevents": "2.3.2"

View file

@ -30,7 +30,6 @@ export function registerESMLoader() {
const { port1, port2 } = new MessageChannel();
// register will wait until the loader is initialized.
require('node:module').register(url.pathToFileURL(require.resolve('../transform/esmLoader')), {
parentURL: url.pathToFileURL(__filename),
data: { port: port2 },
transferList: [port2],
});

View file

@ -57,8 +57,7 @@ export class TestTypeImpl {
test.slow = wrapFunctionWithLocation(this._modifier.bind(this, 'slow'));
test.setTimeout = wrapFunctionWithLocation(this._setTimeout.bind(this));
test.step = this._step.bind(this, 'pass');
test.step.fail = this._step.bind(this, 'fail');
test.step.fixme = this._step.bind(this, 'fixme');
test.step.skip = this._step.bind(this, 'skip');
test.use = wrapFunctionWithLocation(this._use.bind(this));
test.extend = wrapFunctionWithLocation(this._extend.bind(this));
test.info = () => {
@ -259,40 +258,38 @@ export class TestTypeImpl {
suite._use.push({ fixtures, location });
}
async _step<T>(expectation: 'pass'|'fail'|'fixme', title: string, body: () => T | Promise<T>, options: {box?: boolean, location?: Location, timeout?: number } = {}): Promise<T> {
async _step<T>(expectation: 'pass'|'skip', title: string, body: () => T | Promise<T>, options: {box?: boolean, location?: Location, timeout?: number } = {}): Promise<T> {
const testInfo = currentTestInfo();
if (!testInfo)
throw new Error(`test.step() can only be called from a test`);
if (expectation === 'fixme')
if (expectation === 'skip') {
const step = testInfo._addStep({ category: 'test.step.skip', title, location: options.location, box: options.box });
step.complete({});
return undefined as T;
}
const step = testInfo._addStep({ category: 'test.step', title, location: options.location, box: options.box });
return await zones.run('stepZone', step, async () => {
let result;
let error;
try {
result = await raceAgainstDeadline(async () => body(), options.timeout ? monotonicTime() + options.timeout : 0);
} catch (e) {
error = e;
}
if (result?.timedOut) {
const error = new errors.TimeoutError(`Step timeout ${options.timeout}ms exceeded.`);
let result: Awaited<ReturnType<typeof raceAgainstDeadline<T>>> | undefined = undefined;
result = await raceAgainstDeadline(async () => {
try {
return await body();
} catch (e) {
// If the step timed out, the test fixtures will tear down, which in turn
// will abort unfinished actions in the step body. Record such errors here.
if (result?.timedOut)
testInfo._failWithError(e);
throw e;
}
}, options.timeout ? monotonicTime() + options.timeout : 0);
if (result.timedOut)
throw new errors.TimeoutError(`Step timeout of ${options.timeout}ms exceeded.`);
step.complete({});
return result.result;
} catch (error) {
step.complete({ error });
throw error;
}
const expectedToFail = expectation === 'fail';
if (error) {
step.complete({ error });
if (expectedToFail)
return undefined as T;
throw error;
}
if (expectedToFail) {
error = new Error(`Step is expected to fail, but passed`);
step.complete({ error });
throw error;
}
step.complete({});
return result!.result;
});
}

View file

@ -19,7 +19,6 @@ import * as path from 'path';
import type { APIRequestContext, BrowserContext, Browser, BrowserContextOptions, LaunchOptions, Page, Tracing, Video } from 'playwright-core';
import * as playwrightLibrary from 'playwright-core';
import { createGuid, debugMode, addInternalStackPrefix, isString, asLocator, jsonStringifyForceASCII, zones } from 'playwright-core/lib/utils';
import type { ExpectZone } from 'playwright-core/lib/utils';
import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, VideoMode } from '../types/test';
import type { TestInfoImpl, TestStepInternal } from './worker/testInfo';
import { rootTestType } from './common/testType';
@ -264,12 +263,12 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
// Some special calls do not get into steps.
if (!testInfo || data.apiName.includes('setTestIdAttribute') || data.apiName === 'tracing.groupEnd')
return;
const expectZone = zones.zoneData<ExpectZone>('expectZone');
if (expectZone) {
const zone = zones.zoneData<TestStepInternal>('stepZone');
if (zone && zone.category === 'expect') {
// Display the internal locator._expect call under the name of the enclosing expect call,
// and connect it to the existing expect step.
data.apiName = expectZone.title;
data.stepId = expectZone.stepId;
data.apiName = zone.title;
data.stepId = zone.stepId;
return;
}
// In the general case, create a step for each api call and connect them through the stepId.

View file

@ -19,7 +19,6 @@ import {
createGuid,
isString,
pollAgainstDeadline } from 'playwright-core/lib/utils';
import type { ExpectZone } from 'playwright-core/lib/utils';
import {
toBeAttached,
toBeChecked,
@ -315,9 +314,10 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
// out all the frames that belong to the test runner from caught runtime errors.
const stackFrames = filteredStackTrace(captureRawStack());
// Enclose toPass in a step to maintain async stacks, toPass matcher is always async.
// toPass and poll matchers can contain other steps, expects and API calls,
// so they behave like a retriable step.
const stepInfo = {
category: 'expect',
category: (matcherName === 'toPass' || this._info.poll) ? 'step' : 'expect',
title: trimLongString(title, 1024),
params: args[0] ? { expected: args[0] } : undefined,
infectParentStepsWithError: this._info.isSoft,
@ -345,11 +345,7 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
try {
const callback = () => matcher.call(target, ...args);
// toPass and poll matchers can contain other steps, expects and API calls,
// so they behave like a retriable step.
const result = (matcherName === 'toPass' || this._info.poll) ?
zones.run('stepZone', step, callback) :
zones.run<ExpectZone, any>('expectZone', { title, stepId: step.stepId }, callback);
const result = zones.run('stepZone', step, callback);
if (result instanceof Promise)
return result.then(finalizer).catch(reportStepError);
finalizer();

View file

@ -196,20 +196,13 @@ export function toHaveAccessibleDescription(
export function toHaveAccessibleName(
this: ExpectMatcherState,
locator: LocatorEx,
expected: string | RegExp | (string | RegExp)[],
options: { timeout?: number, ignoreCase?: boolean, normalizeWhiteSpace?: boolean } = {}
expected: string | RegExp,
options?: { timeout?: number, ignoreCase?: boolean },
) {
if (Array.isArray(expected)) {
return toEqual.call(this, 'toHaveAccessibleName', locator, 'Locator', async (isNot, timeout) => {
const expectedText = serializeExpectedTextValues(expected, { ignoreCase: options?.ignoreCase, normalizeWhiteSpace: true });
return await locator._expect('to.have.accessible.name.array', { expectedText, isNot, timeout });
}, expected, options);
} else {
return toMatchText.call(this, 'toHaveAccessibleName', locator, 'Locator', async (isNot, timeout) => {
const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase, normalizeWhiteSpace: true });
return await locator._expect('to.have.accessible.name', { expectedText, isNot, timeout });
}, expected, options);
}
return toMatchText.call(this, 'toHaveAccessibleName', locator, 'Locator', async (isNot, timeout) => {
const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase, normalizeWhiteSpace: true });
return await locator._expect('to.have.accessible.name', { expectedText, isNot, timeout });
}, expected, options);
}
export function toHaveAccessibleErrorMessage(

View file

@ -59,9 +59,7 @@ export async function toMatchAriaSnapshot(
if (isString(expectedParam)) {
expected = expectedParam;
} else {
if (expectedParam?.path) {
expectedPath = expectedParam.path;
} else if (expectedParam?.name) {
if (expectedParam?.name) {
expectedPath = testInfo.snapshotPath(sanitizeFilePathBeforeExtension(expectedParam.name));
} else {
let snapshotNames = (testInfo as any)[snapshotNamesSymbol] as SnapshotNames;

View file

@ -517,6 +517,7 @@ class HtmlBuilder {
private _createTestStep(dedupedStep: DedupedStep, result: api.TestResult): TestStep {
const { step, duration, count } = dedupedStep;
const skipped = dedupedStep.step.category === 'test.step.skip';
const testStep: TestStep = {
title: step.title,
startTime: step.startTime.toISOString(),
@ -530,7 +531,8 @@ class HtmlBuilder {
}),
location: this._relativeLocation(step.location),
error: step.error?.message,
count
count,
skipped
};
if (step.location)
this._stepsInFile.set(step.location.file, testStep);

View file

@ -17,7 +17,6 @@
import fs from 'fs';
import path from 'path';
import { captureRawStack, monotonicTime, zones, sanitizeForFilePath, stringifyStackFrames } from 'playwright-core/lib/utils';
import type { ExpectZone } from 'playwright-core/lib/utils';
import type { TestInfo, TestStatus, FullProject } from '../../types/test';
import type { AttachmentPayload, StepBeginPayload, StepEndPayload, TestInfoErrorImpl, WorkerInitParams } from '../common/ipc';
import type { TestCase } from '../common/test';
@ -35,7 +34,7 @@ export interface TestStepInternal {
attachmentIndices: number[];
stepId: string;
title: string;
category: 'hook' | 'fixture' | 'test.step' | 'expect' | 'attach' | string;
category: string;
location?: Location;
boxedStack?: StackFrame[];
steps: TestStepInternal[];
@ -195,7 +194,7 @@ export class TestInfoImpl implements TestInfo {
this._attachmentsPush = this.attachments.push.bind(this.attachments);
this.attachments.push = (...attachments: TestInfo['attachments']) => {
for (const a of attachments)
this._attach(a, this._expectStepId() ?? this._parentStep()?.stepId);
this._attach(a, this._parentStep()?.stepId);
return this.attachments.length;
};
@ -245,10 +244,6 @@ export class TestInfoImpl implements TestInfo {
?? this._findLastStageStep(this._steps); // If no parent step on stack, assume the current stage as parent.
}
private _expectStepId() {
return zones.zoneData<ExpectZone>('expectZone')?.stepId;
}
_addStep(data: Omit<TestStepInternal, 'complete' | 'stepId' | 'steps' | 'attachmentIndices'>, parentStep?: TestStepInternal): TestStepInternal {
const stepId = `${data.category}@${++this._lastStepId}`;

View file

@ -1689,10 +1689,11 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
updateSnapshots?: "all"|"changed"|"missing"|"none";
/**
* Defines how to update the source code snapshots.
* - `'overwrite'` - Overwrite the source code snapshot with the actual result.
* - `'3way'` - Use a three-way merge to update the source code snapshot.
* - `'patch'` - Use a patch to update the source code snapshot. This is the default.
* Defines how to update snapshots in the source code.
* - `'patch'` - Create a unified diff file that can be used to update the source code later. This is the default.
* - `'3way'` - Generate merge conflict markers in source code. This allows user to manually pick relevant changes,
* as if they are resolving a merge conflict in the IDE.
* - `'overwrite'` - Overwrite the source code with the new snapshot values.
*/
updateSourceMethod?: "overwrite"|"3way"|"patch";
@ -5712,18 +5713,19 @@ export interface TestType<TestArgs extends {}, WorkerArgs extends {}> {
*/
<T>(title: string, body: () => T | Promise<T>, options?: { box?: boolean, location?: Location, timeout?: number }): Promise<T>;
/**
* Mark a test step as "fixme", with the intention to fix it. Playwright will not run the step.
* Mark a test step as "skip" to temporarily disable its execution, useful for steps that are currently failing and
* planned for a near-term fix. Playwright will not run the step.
*
* **Usage**
*
* You can declare a test step as failing, so that Playwright ensures it actually fails.
* You can declare a skipped step, and Playwright will not run it.
*
* ```js
* import { test, expect } from '@playwright/test';
*
* test('my test', async ({ page }) => {
* // ...
* await test.step.fixme('not yet ready', async () => {
* await test.step.skip('not yet ready', async () => {
* // ...
* });
* });
@ -5733,34 +5735,7 @@ export interface TestType<TestArgs extends {}, WorkerArgs extends {}> {
* @param body Step body.
* @param options
*/
fixme(title: string, body: () => any | Promise<any>, options?: { box?: boolean, location?: Location, timeout?: number }): Promise<void>;
/**
* Marks a test step as "should fail". Playwright runs this test step and ensures that it actually fails. This is
* useful for documentation purposes to acknowledge that some functionality is broken until it is fixed.
*
* **NOTE** If the step exceeds the timeout, a [TimeoutError](https://playwright.dev/docs/api/class-timeouterror) is
* thrown. This indicates the step did not fail as expected.
*
* **Usage**
*
* You can declare a test step as failing, so that Playwright ensures it actually fails.
*
* ```js
* import { test, expect } from '@playwright/test';
*
* test('my test', async ({ page }) => {
* // ...
* await test.step.fail('currently failing', async () => {
* // ...
* });
* });
* ```
*
* @param title Step name.
* @param body Step body.
* @param options
*/
fail(title: string, body: () => any | Promise<any>, options?: { box?: boolean, location?: Location, timeout?: number }): Promise<void>;
skip(title: string, body: () => any | Promise<any>, options?: { box?: boolean, location?: Location, timeout?: number }): Promise<void>;
}
/**
* `expect` function can be used to create test assertions. Read more about [test assertions](https://playwright.dev/docs/test-assertions).
@ -8167,7 +8142,7 @@ interface LocatorAssertions {
* @param name Expected accessible name.
* @param options
*/
toHaveAccessibleName(name: string|RegExp|ReadonlyArray<string|RegExp>, options?: {
toHaveAccessibleName(name: string|RegExp, options?: {
/**
* Whether to perform case-insensitive match.
* [`ignoreCase`](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-have-accessible-name-option-ignore-case)
@ -8232,21 +8207,24 @@ interface LocatorAssertions {
/**
* Ensures the [Locator](https://playwright.dev/docs/api/class-locator) points to an element with given CSS classes.
* This needs to be a full match or using a relaxed regular expression.
* When a string is provided, it must fully match the element's `class` attribute. To match individual classes or
* perform partial matches, use a regular expression:
*
* **Usage**
*
* ```html
* <div class='selected row' id='component'></div>
* <div class='middle selected row' id='component'></div>
* ```
*
* ```js
* const locator = page.locator('#component');
* await expect(locator).toHaveClass(/selected/);
* await expect(locator).toHaveClass('selected row');
* await expect(locator).toHaveClass('middle selected row');
* await expect(locator).toHaveClass(/(^|\s)selected(\s|$)/);
* ```
*
* Note that if array is passed as an expected value, entire lists of elements can be asserted:
* When an array is passed, the method asserts that the list of elements located matches the corresponding list of
* expected class values. Each element's class attribute is matched against the corresponding string or regular
* expression in the array:
*
* ```js
* const locator = page.locator('list > .component');
@ -8687,25 +8665,17 @@ interface LocatorAssertions {
* **Usage**
*
* ```js
* await expect(page.locator('body')).toMatchAriaSnapshot();
* await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'snapshot' });
* await expect(page.locator('body')).toMatchAriaSnapshot({ path: '/path/to/snapshot.yml' });
* await page.goto('https://demo.playwright.dev/todomvc/');
* await expect(page.locator('body')).toMatchAriaSnapshot(`
* - heading "todos"
* - textbox "What needs to be done?"
* `);
* ```
*
* @param expected
* @param options
*/
toMatchAriaSnapshot(options?: {
/**
* Name of the snapshot to store in the snapshot folder corresponding to this test. Generates ordinal name if not
* specified.
*/
name?: string;
/**
* Path to the YAML snapshot file.
*/
path?: string;
toMatchAriaSnapshot(expected: string, options?: {
/**
* Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`.
*/
@ -8718,17 +8688,20 @@ interface LocatorAssertions {
* **Usage**
*
* ```js
* await page.goto('https://demo.playwright.dev/todomvc/');
* await expect(page.locator('body')).toMatchAriaSnapshot(`
* - heading "todos"
* - textbox "What needs to be done?"
* `);
* await expect(page.locator('body')).toMatchAriaSnapshot();
* await expect(page.locator('body')).toMatchAriaSnapshot({ name: 'snapshot' });
* await expect(page.locator('body')).toMatchAriaSnapshot({ path: '/path/to/snapshot.yml' });
* ```
*
* @param expected
* @param options
*/
toMatchAriaSnapshot(expected: string, options?: {
toMatchAriaSnapshot(options?: {
/**
* Name of the snapshot to store in the snapshot (screenshot) folder corresponding to this test. Generates sequential
* names if not specified.
*/
name?: string;
/**
* Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`.
*/

View file

@ -1772,7 +1772,6 @@ export type BrowserContextPauseResult = void;
export type BrowserContextEnableRecorderParams = {
language?: string,
mode?: 'inspecting' | 'recording',
codegenMode?: 'actions' | 'trace-events',
pauseOnNextStatement?: boolean,
testIdAttributeName?: string,
launchOptions?: any,
@ -1786,7 +1785,6 @@ export type BrowserContextEnableRecorderParams = {
export type BrowserContextEnableRecorderOptions = {
language?: string,
mode?: 'inspecting' | 'recording',
codegenMode?: 'actions' | 'trace-events',
pauseOnNextStatement?: boolean,
testIdAttributeName?: string,
launchOptions?: any,

View file

@ -1198,11 +1198,6 @@ BrowserContext:
literals:
- inspecting
- recording
codegenMode:
type: enum?
literals:
- actions
- trace-events
pauseOnNextStatement: boolean?
testIdAttributeName: string?
launchOptions: json?

View file

@ -1,28 +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.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="/playwright-logo.svg" type="image/svg+xml">
<title>Playwright Recorder</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/recorder.tsx"></script>
</body>
</html>

View file

@ -6,7 +6,3 @@ ui/
[sw-main.ts]
sw/**
[recorder.tsx]
ui/recorder/**

View file

@ -1,41 +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 '@web/common.css';
import { applyTheme } from '@web/theme';
import '@web/third_party/vscode/codicon.css';
import * as ReactDOM from 'react-dom/client';
import { RecorderView } from './ui/recorder/recorderView';
(async () => {
applyTheme();
if (window.location.protocol !== 'file:') {
if (!navigator.serviceWorker)
throw new Error(`Service workers are not supported.\nMake sure to serve the Recorder (${window.location}) via HTTPS or localhost.`);
navigator.serviceWorker.register('sw.bundle.js');
if (!navigator.serviceWorker.controller) {
await new Promise<void>(f => {
navigator.serviceWorker.oncontrollerchange = () => f();
});
}
// Keep SW running.
setInterval(function() { fetch('ping'); }, 10000);
}
ReactDOM.createRoot(document.querySelector('#root')!).render(<RecorderView />);
})();

View file

@ -70,13 +70,20 @@
flex: none;
}
.action-selector {
.action-parameter {
display: inline;
flex: none;
padding-left: 5px;
}
.action-locator-parameter {
color: var(--vscode-charts-orange);
}
.action-generic-parameter {
color: var(--vscode-charts-purple);
}
.action-url {
display: inline;
flex: none;

View file

@ -19,8 +19,7 @@ import { msToString } from '@web/uiUtils';
import * as React from 'react';
import './actionList.css';
import * as modelUtil from './modelUtil';
import { asLocator } from '@isomorphic/locatorGenerators';
import type { Language } from '@isomorphic/locatorGenerators';
import { asLocator, type Language } from '@isomorphic/locatorGenerators';
import type { TreeState } from '@web/components/treeView';
import { TreeView } from '@web/components/treeView';
import type { ActionTraceEventInContext, ActionTreeItem } from './modelUtil';
@ -116,9 +115,10 @@ export const renderAction = (
}) => {
const { sdkLanguage, revealConsole, revealAttachment, isLive, showDuration, showBadges } = options;
const { errors, warnings } = modelUtil.stats(action);
const locator = action.params.selector ? asLocator(sdkLanguage || 'javascript', action.params.selector) : undefined;
const showAttachments = !!action.attachments?.length && !!revealAttachment;
const parameterString = actionParameterDisplayString(action, sdkLanguage || 'javascript');
let time: string = '';
if (action.endTime)
time = msToString(action.endTime - action.startTime);
@ -129,7 +129,23 @@ export const renderAction = (
return <>
<div className='action-title' title={action.apiName}>
<span>{action.apiName}</span>
{locator && <div className='action-selector' title={locator}>{locator}</div>}
{parameterString &&
(parameterString.type === 'locator' ? (
<>
<span className='action-parameter action-locator-parameter'>
{parameterString.value}
</span>
{parameterString.childDisplayString && (
<span className='action-parameter action-generic-parameter'>
{parameterString.childDisplayString.value}
</span>
)}
</>
) : (
<span className='action-parameter action-generic-parameter'>
{parameterString.value}
</span>
))}
{action.method === 'goto' && action.params.url && <div className='action-url' title={action.params.url}>{action.params.url}</div>}
{action.class === 'APIRequestContext' && action.params.url && <div className='action-url' title={action.params.url}>{excludeOrigin(action.params.url)}</div>}
</div>
@ -151,3 +167,154 @@ function excludeOrigin(url: string): string {
return url;
}
}
type ActionParameterDisplayString =
| {
type: 'generic';
value: string;
}
| {
type: 'locator';
value: string;
childDisplayString?: ActionParameterDisplayString;
};
const clockDisplayString = (
action: ActionTraceEvent,
): ActionParameterDisplayString | undefined => {
switch (action.method) {
case 'clockPauseAt':
case 'clockSetFixedTime':
case 'clockSetSystemTime': {
if (
action.params.timeString === undefined &&
action.params.timeNumber === undefined
)
return undefined;
return {
type: 'generic',
value: new Date(
action.params.timeString ?? action.params.timeNumber,
).toLocaleString(undefined, { timeZone: 'UTC' }),
};
}
case 'clockFastForward':
case 'clockRunFor': {
if (
action.params.ticksNumber === undefined &&
action.params.ticksString === undefined
)
return undefined;
return {
type: 'generic',
value: action.params.ticksString ?? `${action.params.ticksNumber}ms`,
};
}
}
return undefined;
};
const keyboardDisplayString = (
action: ActionTraceEvent,
): ActionParameterDisplayString | undefined => {
switch (action.method) {
case 'press':
case 'keyboardPress':
case 'keyboardDown':
case 'keyboardUp': {
if (action.params.key === undefined)
return undefined;
return { type: 'generic', value: action.params.key };
}
case 'type':
case 'fill':
case 'keyboardType':
case 'keyboardInsertText': {
const string = action.params.text ?? action.params.value;
if (string === undefined)
return undefined;
return { type: 'generic', value: `"${string}"` };
}
}
};
const mouseDisplayString = (
action: ActionTraceEvent,
): ActionParameterDisplayString | undefined => {
switch (action.method) {
case 'click':
case 'dblclick':
case 'mouseClick':
case 'mouseMove': {
if (action.params.x === undefined || action.params.y === undefined)
return undefined;
return {
type: 'generic',
value: `(${action.params.x}, ${action.params.y})`,
};
}
case 'mouseWheel': {
if (
action.params.deltaX === undefined ||
action.params.deltaY === undefined
)
return undefined;
return {
type: 'generic',
value: `(${action.params.deltaX}, ${action.params.deltaY})`,
};
}
}
};
const touchscreenDisplayString = (
action: ActionTraceEvent,
): ActionParameterDisplayString | undefined => {
switch (action.method) {
case 'tap': {
if (action.params.x === undefined || action.params.y === undefined)
return undefined;
return {
type: 'generic',
value: `(${action.params.x}, ${action.params.y})`,
};
}
}
};
const actionParameterDisplayString = (
action: ActionTraceEvent,
sdkLanguage: Language,
ignoreLocator: boolean = false,
): ActionParameterDisplayString | undefined => {
const params = action.params;
// Locators have many possible classes, so follow existing logic and use `selector` presence
if (!ignoreLocator && params.selector !== undefined) {
return {
type: 'locator',
value: asLocator(sdkLanguage, params.selector),
childDisplayString: actionParameterDisplayString(
action,
sdkLanguage,
true,
),
};
}
switch (action.class.toLowerCase()) {
case 'browsercontext':
return clockDisplayString(action);
case 'page':
case 'frame':
case 'elementhandle':
return (
keyboardDisplayString(action) ??
mouseDisplayString(action) ??
touchscreenDisplayString(action)
);
}
return undefined;
};

View file

@ -55,3 +55,11 @@
a.codicon-cloud-download:hover{
background-color: var(--vscode-list-inactiveSelectionBackground)
}
.yellow-flash {
animation: yellowflash-bg 2s;
}
@keyframes yellowflash-bg {
from { background: var(--vscode-peekViewEditor-matchHighlightBackground); }
to { background: transparent; }
}

View file

@ -17,36 +17,38 @@
import * as React from 'react';
import './attachmentsTab.css';
import { ImageDiffView } from '@web/shared/imageDiffView';
import type { ActionTraceEventInContext, MultiTraceModel } from './modelUtil';
import type { MultiTraceModel } from './modelUtil';
import { PlaceholderPanel } from './placeholderPanel';
import type { AfterActionTraceEventAttachment } from '@trace/trace';
import { CodeMirrorWrapper, lineHeight } from '@web/components/codeMirrorWrapper';
import { isTextualMimeType } from '@isomorphic/mimeType';
import { Expandable } from '@web/components/expandable';
import { linkifyText } from '@web/renderUtils';
import { clsx } from '@web/uiUtils';
import { clsx, useFlash } from '@web/uiUtils';
type Attachment = AfterActionTraceEventAttachment & { traceUrl: string };
type ExpandableAttachmentProps = {
attachment: Attachment;
reveal: boolean;
highlight: boolean;
reveal?: any;
};
const ExpandableAttachment: React.FunctionComponent<ExpandableAttachmentProps> = ({ attachment, reveal, highlight }) => {
const ExpandableAttachment: React.FunctionComponent<ExpandableAttachmentProps> = ({ attachment, reveal }) => {
const [expanded, setExpanded] = React.useState(false);
const [attachmentText, setAttachmentText] = React.useState<string | null>(null);
const [placeholder, setPlaceholder] = React.useState<string | null>(null);
const [flash, triggerFlash] = useFlash();
const ref = React.useRef<HTMLSpanElement>(null);
const isTextAttachment = isTextualMimeType(attachment.contentType);
const hasContent = !!attachment.sha1 || !!attachment.path;
React.useEffect(() => {
if (reveal)
if (reveal) {
ref.current?.scrollIntoView({ behavior: 'smooth' });
}, [reveal]);
return triggerFlash();
}
}, [reveal, triggerFlash]);
React.useEffect(() => {
if (expanded && attachmentText === null && placeholder === null) {
@ -66,14 +68,14 @@ const ExpandableAttachment: React.FunctionComponent<ExpandableAttachmentProps> =
}, [attachmentText]);
const title = <span style={{ marginLeft: 5 }} ref={ref} aria-label={attachment.name}>
<span className={clsx(highlight && 'attachment-title-highlight')}>{linkifyText(attachment.name)}</span>
<span>{linkifyText(attachment.name)}</span>
{hasContent && <a style={{ marginLeft: 5 }} href={downloadURL(attachment)}>download</a>}
</span>;
if (!isTextAttachment || !hasContent)
return <div style={{ marginLeft: 20 }}>{title}</div>;
return <>
return <div className={clsx(flash && 'yellow-flash')}>
<Expandable title={title} expanded={expanded} setExpanded={setExpanded} expandOnTitleClick={true}>
{placeholder && <i>{placeholder}</i>}
</Expandable>
@ -87,14 +89,13 @@ const ExpandableAttachment: React.FunctionComponent<ExpandableAttachmentProps> =
wrapLines={false}>
</CodeMirrorWrapper>
</div>}
</>;
</div>;
};
export const AttachmentsTab: React.FunctionComponent<{
model: MultiTraceModel | undefined,
selectedAction: ActionTraceEventInContext | undefined,
revealedAttachment?: AfterActionTraceEventAttachment,
}> = ({ model, selectedAction, revealedAttachment }) => {
revealedAttachment?: [AfterActionTraceEventAttachment, number],
}> = ({ model, revealedAttachment }) => {
const { diffMap, screenshots, attachments } = React.useMemo(() => {
const attachments = new Set<Attachment>();
const screenshots = new Set<Attachment>();
@ -153,8 +154,7 @@ export const AttachmentsTab: React.FunctionComponent<{
return <div className='attachment-item' key={attachmentKey(a, i)}>
<ExpandableAttachment
attachment={a}
highlight={selectedAction?.attachments?.some(selected => isEqualAttachment(a, selected)) ?? false}
reveal={!!revealedAttachment && isEqualAttachment(a, revealedAttachment)}
reveal={(!!revealedAttachment && isEqualAttachment(a, revealedAttachment[0])) ? revealedAttachment : undefined}
/>
</div>;
})}

View file

@ -47,27 +47,34 @@ export const InspectorTab: React.FunctionComponent<{
setIsInspecting(false);
}, [highlightedElement, setHighlightedElement, setIsInspecting]);
return <div className='vbox' style={{ backgroundColor: 'var(--vscode-sideBar-background)' }}>
<div style={{ margin: '10px 0px 10px 10px', color: 'var(--vscode-editorCodeLens-foreground)', flex: 'none' }}>Locator</div>
<div style={{ margin: '0 10px 10px', flex: 'auto' }}>
return <div style={{ flex: 'auto', backgroundColor: 'var(--vscode-sideBar-background)', padding: '0 10px 10px 10px', overflow: 'auto' }}>
<div className='hbox' style={{ lineHeight: '28px', color: 'var(--vscode-editorCodeLens-foreground)' }}>
<div style={{ flex: 'auto' }}>Locator</div>
<ToolbarButton icon='files' title='Copy locator' onClick={() => {
copy(highlightedElement.locator || '');
}}></ToolbarButton>
</div>
<div style={{ height: 50 }}>
<CodeMirrorWrapper text={highlightedElement.locator || ''} language={sdkLanguage} isFocused={true} wrapLines={true} onChange={text => {
// Updating text needs to go first - react can squeeze a render between the state updates.
setHighlightedElement({ ...highlightedElement, locator: text, lastEdited: 'locator' });
setIsInspecting(false);
}} />
</div>
<div style={{ margin: '10px 0px 10px 10px', color: 'var(--vscode-editorCodeLens-foreground)', flex: 'none' }}>Aria</div>
<div style={{ margin: '0 10px 10px', flex: 'auto' }}>
<div className='hbox' style={{ lineHeight: '28px', color: 'var(--vscode-editorCodeLens-foreground)' }}>
<div style={{ flex: 'auto' }}>Aria snapshot</div>
<ToolbarButton icon='files' title='Copy snapshot' onClick={() => {
copy(highlightedElement.ariaSnapshot || '');
}}></ToolbarButton>
</div>
<div style={{ height: 150 }}>
<CodeMirrorWrapper
text={highlightedElement.ariaSnapshot || ''}
language='yaml'
wrapLines={false}
highlight={ariaSnapshotErrors}
onChange={onAriaEditorChange} />
</div>
<div style={{ position: 'absolute', right: 5, top: 5 }}>
<ToolbarButton icon='files' title='Copy locator' onClick={() => {
copy(highlightedElement.locator || '');
}}></ToolbarButton>
</div>
</div>;
};

View file

@ -1,5 +0,0 @@
[*]
@isomorphic/**
@trace/**
@web/**
../**

View file

@ -1,62 +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 * as actionTypes from '@recorder/actions';
import { ListView } from '@web/components/listView';
import * as React from 'react';
import '../actionList.css';
import { traceParamsForAction } from '@isomorphic/recorderUtils';
import { asLocator } from '@isomorphic/locatorGenerators';
import type { Language } from '@isomorphic/locatorGenerators';
const ActionList = ListView<actionTypes.ActionInContext>;
export const ActionListView: React.FC<{
sdkLanguage: Language,
actions: actionTypes.ActionInContext[],
selectedAction: actionTypes.ActionInContext | undefined,
onSelectedAction: (action: actionTypes.ActionInContext | undefined) => void,
}> = ({
sdkLanguage,
actions,
selectedAction,
onSelectedAction,
}) => {
const render = React.useCallback((action: actionTypes.ActionInContext) => {
return renderAction(sdkLanguage, action);
}, [sdkLanguage]);
return <div className='vbox'>
<ActionList
name='actions'
items={actions}
selectedItem={selectedAction}
onSelected={onSelectedAction}
render={render} />
</div>;
};
export const renderAction = (sdkLanguage: Language, action: actionTypes.ActionInContext) => {
const { method, apiName, params } = traceParamsForAction(action);
const locator = params.selector ? asLocator(sdkLanguage || 'javascript', params.selector) : undefined;
return <>
<div className='action-title' title={apiName}>
<span>{apiName}</span>
{locator && <div className='action-selector' title={locator}>{locator}</div>}
{method === 'goto' && params.url && <div className='action-url' title={params.url}>{params.url}</div>}
</div>
</>;
};

View file

@ -1,118 +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 * as actionTypes from '@recorder/actions';
import type { Mode, Source } from '@recorder/recorderTypes';
import * as React from 'react';
export const BackendContext = React.createContext<Backend | undefined>(undefined);
export const BackendProvider: React.FunctionComponent<React.PropsWithChildren<{
guid: string,
}>> = ({ guid, children }) => {
const [connection, setConnection] = React.useState<Connection | undefined>(undefined);
const [mode, setMode] = React.useState<Mode>('none');
const [actions, setActions] = React.useState<{ actions: actionTypes.ActionInContext[], sources: Source[] }>({ actions: [], sources: [] });
const callbacks = React.useRef({ setMode, setActions });
React.useEffect(() => {
const wsURL = new URL(`../${guid}`, window.location.toString());
wsURL.protocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:');
const webSocket = new WebSocket(wsURL.toString());
setConnection(new Connection(webSocket, callbacks.current));
return () => {
webSocket.close();
};
}, [guid]);
const backend = React.useMemo(() => {
return connection ? { mode, actions: actions.actions, sources: actions.sources, connection } : undefined;
}, [actions, mode, connection]);
return <BackendContext.Provider value={backend}>
{children}
</BackendContext.Provider>;
};
export type Backend = {
actions: actionTypes.ActionInContext[],
sources: Source[],
connection: Connection,
};
type ConnectionCallbacks = {
setMode: (mode: Mode) => void;
setActions: (data: { actions: actionTypes.ActionInContext[], sources: Source[] }) => void;
};
class Connection {
private _lastId = 0;
private _webSocket: WebSocket;
private _callbacks = new Map<number, { resolve: (arg: any) => void, reject: (arg: Error) => void }>();
private _options: ConnectionCallbacks;
constructor(webSocket: WebSocket, options: ConnectionCallbacks) {
this._webSocket = webSocket;
this._callbacks = new Map();
this._options = options;
this._webSocket.addEventListener('message', event => {
const message = JSON.parse(event.data);
const { id, result, error, method, params } = message;
if (id) {
const callback = this._callbacks.get(id);
if (!callback)
return;
this._callbacks.delete(id);
if (error)
callback.reject(new Error(error));
else
callback.resolve(result);
} else {
this._dispatchEvent(method, params);
}
});
}
setMode(mode: Mode) {
this._sendMessageNoReply('setMode', { mode });
}
private async _sendMessage(method: string, params?: any): Promise<any> {
const id = ++this._lastId;
const message = { id, method, params };
this._webSocket.send(JSON.stringify(message));
return new Promise((resolve, reject) => {
this._callbacks.set(id, { resolve, reject });
});
}
private _sendMessageNoReply(method: string, params?: any) {
this._sendMessage(method, params).catch(() => { });
}
private _dispatchEvent(method: string, params?: any) {
if (method === 'setMode') {
const { mode } = params as { mode: Mode };
this._options.setMode(mode);
}
if (method === 'setActions') {
const { actions, sources } = params as { actions: actionTypes.ActionInContext[], sources: Source[] };
this._options.setActions({ actions: actions.filter(a => a.action.name !== 'openPage' && a.action.name !== 'closePage'), sources });
(window as any).playwrightSourcesEchoForTest = sources;
}
}
}

View file

@ -1,71 +0,0 @@
/*
Copyright (c) Microsoft Corporation.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { sha1 } from '@web/uiUtils';
import * as React from 'react';
import type { ContextEntry } from '../../types/entries';
import { MultiTraceModel } from '../modelUtil';
export const ModelContext = React.createContext<MultiTraceModel | undefined>(undefined);
export const ModelProvider: React.FunctionComponent<React.PropsWithChildren<{
trace: string,
}>> = ({ trace, children }) => {
const [model, setModel] = React.useState<{ model: MultiTraceModel, sha1: string } | undefined>();
const [counter, setCounter] = React.useState(0);
const pollTimer = React.useRef<NodeJS.Timeout | null>(null);
React.useEffect(() => {
if (pollTimer.current)
clearTimeout(pollTimer.current);
// Start polling running test.
pollTimer.current = setTimeout(async () => {
try {
const result = await loadSingleTraceFile(trace);
if (result.sha1 !== model?.sha1)
setModel(result);
} catch {
setModel(undefined);
} finally {
setCounter(counter + 1);
}
}, 500);
return () => {
if (pollTimer.current)
clearTimeout(pollTimer.current);
};
}, [counter, model, trace]);
return <ModelContext.Provider value={model?.model}>
{children}
</ModelContext.Provider>;
};
async function loadSingleTraceFile(url: string): Promise<{ model: MultiTraceModel, sha1: string }> {
const params = new URLSearchParams();
params.set('trace', url);
params.set('limit', '1');
const response = await fetch(`contexts?${params.toString()}`);
const contextEntries = await response.json() as ContextEntry[];
const tokens: string[] = [];
for (const entry of contextEntries) {
entry.actions.forEach(a => tokens.push(a.type + '@' + a.startTime + '-' + a.endTime));
entry.events.forEach(e => tokens.push(e.type + '@' + e.time));
}
return { model: new MultiTraceModel(contextEntries), sha1: await sha1(tokens.join('|')) };
}

View file

@ -1,15 +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.
*/

View file

@ -1,299 +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 * as actionTypes from '@recorder/actions';
import { SourceChooser } from '@web/components/sourceChooser';
import { SplitView } from '@web/components/splitView';
import type { TabbedPaneTabModel } from '@web/components/tabbedPane';
import { TabbedPane } from '@web/components/tabbedPane';
import { Toolbar } from '@web/components/toolbar';
import { ToolbarButton, ToolbarSeparator } from '@web/components/toolbarButton';
import { copy, useSetting } from '@web/uiUtils';
import * as React from 'react';
import { ConsoleTab, useConsoleTabModel } from '../consoleTab';
import type { Boundaries } from '../geometry';
import { InspectorTab } from '../inspectorTab';
import type * as modelUtil from '../modelUtil';
import type { SourceLocation } from '../modelUtil';
import { NetworkTab, useNetworkTabModel } from '../networkTab';
import { collectSnapshots, extendSnapshot, SnapshotView } from '../snapshotTab';
import { SourceTab } from '../sourceTab';
import { ModelContext, ModelProvider } from './modelContext';
import './recorderView.css';
import { ActionListView } from './actionListView';
import { BackendContext, BackendProvider } from './backendContext';
import type { Language } from '@isomorphic/locatorGenerators';
import { SettingsToolbarButton } from '../settingsToolbarButton';
import type { HighlightedElement } from '../snapshotTab';
export const RecorderView: React.FunctionComponent = () => {
const searchParams = new URLSearchParams(window.location.search);
const guid = searchParams.get('ws')!;
const trace = searchParams.get('trace') + '.json';
return <BackendProvider guid={guid}>
<ModelProvider trace={trace}>
<Workbench />
</ModelProvider>
</BackendProvider>;
};
export const Workbench: React.FunctionComponent = () => {
const backend = React.useContext(BackendContext);
const model = React.useContext(ModelContext);
const [fileId, setFileId] = React.useState<string | undefined>();
const [selectedStartTime, setSelectedStartTime] = React.useState<number | undefined>(undefined);
const [isInspecting, setIsInspecting] = React.useState(false);
const [highlightedElementInProperties, setHighlightedElementInProperties] = React.useState<HighlightedElement>({ lastEdited: 'none' });
const [highlightedElementInTrace, setHighlightedElementInTrace] = React.useState<HighlightedElement>({ lastEdited: 'none' });
const [traceCallId, setTraceCallId] = React.useState<string | undefined>();
const setSelectedAction = React.useCallback((action: actionTypes.ActionInContext | undefined) => {
setSelectedStartTime(action?.startTime);
}, []);
const selectedAction = React.useMemo(() => {
return backend?.actions.find(a => a.startTime === selectedStartTime);
}, [backend?.actions, selectedStartTime]);
React.useEffect(() => {
const callId = model?.actions.find(a => a.endTime && a.endTime === selectedAction?.endTime)?.callId;
if (callId)
setTraceCallId(callId);
}, [model, selectedAction]);
const source = React.useMemo(() => backend?.sources.find(s => s.id === fileId) || backend?.sources[0], [backend?.sources, fileId]);
const sourceLocation = React.useMemo(() => {
if (!source)
return undefined;
const sourceLocation: SourceLocation = {
file: '',
line: 0,
column: 0,
source: {
errors: [],
content: source.text
}
};
return sourceLocation;
}, [source]);
const sdkLanguage: Language = source?.language || 'javascript';
const { boundaries } = React.useMemo(() => {
const boundaries = { minimum: model?.startTime || 0, maximum: model?.endTime || 30000 };
if (boundaries.minimum > boundaries.maximum) {
boundaries.minimum = 0;
boundaries.maximum = 30000;
}
// Leave some nice free space on the right hand side.
boundaries.maximum += (boundaries.maximum - boundaries.minimum) / 20;
return { boundaries };
}, [model]);
const elementPickedInTrace = React.useCallback((element: HighlightedElement) => {
setHighlightedElementInProperties(element);
setHighlightedElementInTrace({ lastEdited: 'none' });
setIsInspecting(false);
}, []);
const elementTypedInProperties = React.useCallback((element: HighlightedElement) => {
setHighlightedElementInTrace(element);
setHighlightedElementInProperties(element);
}, []);
const actionList = <ActionListView
sdkLanguage={sdkLanguage}
actions={backend?.actions || []}
selectedAction={selectedAction}
onSelectedAction={setSelectedAction}
/>;
const actionsTab: TabbedPaneTabModel = {
id: 'actions',
title: 'Actions',
component: actionList,
};
const toolbar = <Toolbar sidebarBackground>
<div style={{ width: 4 }}></div>
<ToolbarButton icon='inspect' title='Pick locator' toggled={isInspecting} onClick={() => {
setIsInspecting(!isInspecting);
}} />
<ToolbarButton icon='eye' title='Assert visibility' onClick={() => {
}} />
<ToolbarButton icon='whole-word' title='Assert text' onClick={() => {
}} />
<ToolbarButton icon='symbol-constant' title='Assert value' onClick={() => {
}} />
<ToolbarSeparator />
<ToolbarButton icon='files' title='Copy' disabled={!source || !source.text} onClick={() => {
if (source?.text)
copy(source.text);
}}></ToolbarButton>
<div style={{ flex: 'auto' }}></div>
<div>Target:</div>
<SourceChooser fileId={fileId} sources={backend?.sources || []} setFileId={fileId => {
setFileId(fileId);
}} />
<SettingsToolbarButton />
</Toolbar>;
const sidebarTabbedPane = <TabbedPane tabs={[actionsTab]} />;
const traceView = <TraceView
sdkLanguage={sdkLanguage}
callId={traceCallId}
isInspecting={isInspecting}
setIsInspecting={setIsInspecting}
highlightedElement={highlightedElementInTrace}
setHighlightedElement={elementPickedInTrace} />;
const propertiesView = <PropertiesView
sdkLanguage={sdkLanguage}
boundaries={boundaries}
setIsInspecting={setIsInspecting}
highlightedElement={highlightedElementInProperties}
setHighlightedElement={elementTypedInProperties}
sourceLocation={sourceLocation} />;
return <div className='vbox workbench'>
<SplitView
sidebarSize={250}
orientation={'horizontal'}
settingName='recorderActionListSidebar'
sidebarIsFirst
main={<SplitView
sidebarSize={250}
orientation='vertical'
settingName='recorderPropertiesSidebar'
main={<div className='vbox'>
{toolbar}
{traceView}
</div>}
sidebar={propertiesView}
/>}
sidebar={sidebarTabbedPane}
/>
</div>;
};
const PropertiesView: React.FunctionComponent<{
sdkLanguage: Language,
boundaries: Boundaries,
setIsInspecting: (value: boolean) => void,
highlightedElement: HighlightedElement,
setHighlightedElement: (element: HighlightedElement) => void,
sourceLocation: modelUtil.SourceLocation | undefined,
}> = ({
sdkLanguage,
boundaries,
setIsInspecting,
highlightedElement,
setHighlightedElement,
sourceLocation,
}) => {
const model = React.useContext(ModelContext);
const consoleModel = useConsoleTabModel(model, boundaries);
const networkModel = useNetworkTabModel(model, boundaries);
const sourceModel = React.useRef(new Map<string, modelUtil.SourceModel>());
const [selectedPropertiesTab, setSelectedPropertiesTab] = useSetting<string>('recorderPropertiesTab', 'source');
const inspectorTab: TabbedPaneTabModel = {
id: 'inspector',
title: 'Locator',
render: () => <InspectorTab
sdkLanguage={sdkLanguage}
setIsInspecting={setIsInspecting}
highlightedElement={highlightedElement}
setHighlightedElement={setHighlightedElement} />,
};
const sourceTab: TabbedPaneTabModel = {
id: 'source',
title: 'Source',
render: () => <SourceTab
sources={sourceModel.current}
stackFrameLocation={'right'}
fallbackLocation={sourceLocation}
/>
};
const consoleTab: TabbedPaneTabModel = {
id: 'console',
title: 'Console',
count: consoleModel.entries.length,
render: () => <ConsoleTab boundaries={boundaries} consoleModel={consoleModel} />
};
const networkTab: TabbedPaneTabModel = {
id: 'network',
title: 'Network',
count: networkModel.resources.length,
render: () => <NetworkTab boundaries={boundaries} networkModel={networkModel} sdkLanguage={sdkLanguage} />
};
const tabs: TabbedPaneTabModel[] = [
sourceTab,
inspectorTab,
consoleTab,
networkTab,
];
return <TabbedPane
tabs={tabs}
selectedTab={selectedPropertiesTab}
setSelectedTab={setSelectedPropertiesTab}
/>;
};
const TraceView: React.FunctionComponent<{
sdkLanguage: Language,
callId: string | undefined,
isInspecting: boolean;
setIsInspecting: (value: boolean) => void;
highlightedElement: HighlightedElement;
setHighlightedElement: (element: HighlightedElement) => void;
}> = ({
sdkLanguage,
callId,
isInspecting,
setIsInspecting,
highlightedElement,
setHighlightedElement,
}) => {
const model = React.useContext(ModelContext);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [shouldPopulateCanvasFromScreenshot, _] = useSetting('shouldPopulateCanvasFromScreenshot', false);
const action = React.useMemo(() => {
return model?.actions.find(a => a.callId === callId);
}, [model, callId]);
const snapshot = React.useMemo(() => {
const snapshot = collectSnapshots(action);
return snapshot.action || snapshot.after || snapshot.before;
}, [action]);
const snapshotUrls = React.useMemo(() => {
return snapshot ? extendSnapshot(snapshot, shouldPopulateCanvasFromScreenshot) : undefined;
}, [snapshot, shouldPopulateCanvasFromScreenshot]);
return <SnapshotView
sdkLanguage={sdkLanguage}
testIdAttributeName='data-testid'
isInspecting={isInspecting}
setIsInspecting={setIsInspecting}
highlightedElement={highlightedElement}
setHighlightedElement={setHighlightedElement}
snapshotUrls={snapshotUrls} />;
};

View file

@ -59,7 +59,7 @@ export const Workbench: React.FunctionComponent<{
}> = ({ model, showSourcesFirst, rootDir, fallbackLocation, isLive, hideTimeline, status, annotations, inert, onOpenExternally, revealSource }) => {
const [selectedCallId, setSelectedCallId] = React.useState<string | undefined>(undefined);
const [revealedError, setRevealedError] = React.useState<ErrorDescription | undefined>(undefined);
const [revealedAttachment, setRevealedAttachment] = React.useState<AfterActionTraceEventAttachment | undefined>(undefined);
const [revealedAttachment, setRevealedAttachment] = React.useState<[attachment: AfterActionTraceEventAttachment, renderCounter: number] | undefined>(undefined);
const [highlightedCallId, setHighlightedCallId] = React.useState<string | undefined>();
const [highlightedEntry, setHighlightedEntry] = React.useState<Entry | undefined>();
const [highlightedConsoleMessage, setHighlightedConsoleMessage] = React.useState<ConsoleEntry | undefined>();
@ -148,7 +148,12 @@ export const Workbench: React.FunctionComponent<{
const revealAttachment = React.useCallback((attachment: AfterActionTraceEventAttachment) => {
selectPropertiesTab('attachments');
setRevealedAttachment(attachment);
setRevealedAttachment(currentValue => {
if (!currentValue)
return [attachment, 0];
const revealCounter = currentValue[1];
return [attachment, revealCounter + 1];
});
}, [selectPropertiesTab]);
React.useEffect(() => {
@ -238,7 +243,7 @@ export const Workbench: React.FunctionComponent<{
id: 'attachments',
title: 'Attachments',
count: attachments.length,
render: () => <AttachmentsTab model={model} selectedAction={selectedAction} revealedAttachment={revealedAttachment} />
render: () => <AttachmentsTab model={model} revealedAttachment={revealedAttachment} />
};
const tabs: TabbedPaneTabModel[] = [

View file

@ -46,7 +46,6 @@ export default defineConfig({
input: {
index: path.resolve(__dirname, 'index.html'),
uiMode: path.resolve(__dirname, 'uiMode.html'),
recorder: path.resolve(__dirname, 'recorder.html'),
snapshot: path.resolve(__dirname, 'snapshot.html'),
},
output: {

View file

@ -14,6 +14,7 @@
limitations under the License.
*/
import type { EffectCallback } from 'react';
import React from 'react';
// Recalculates the value when dependencies change.
@ -224,3 +225,26 @@ export function scrollIntoViewIfNeeded(element: Element | undefined) {
const kControlCodesRe = '\\u0000-\\u0020\\u007f-\\u009f';
export const kWebLinkRe = new RegExp('(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}:\\/\\/|www\\.)[^\\s' + kControlCodesRe + '"]{2,}[^\\s' + kControlCodesRe + '"\')}\\],:;.!?]', 'ug');
/**
* Manages flash animation state.
* Calling `trigger` will turn `flash` to true for a second, and then back to false.
* If `trigger` is called while a flash is ongoing, the ongoing flash will be cancelled and after 50ms a new flash is started.
* @returns [flash, trigger]
*/
export function useFlash(): [boolean, EffectCallback] {
const [flash, setFlash] = React.useState(false);
const trigger = React.useCallback<React.EffectCallback>(() => {
const timeouts: any[] = [];
setFlash(currentlyFlashing => {
timeouts.push(setTimeout(() => setFlash(false), 1000));
if (!currentlyFlashing)
return true;
timeouts.push(setTimeout(() => setFlash(true), 50));
return false;
});
return () => timeouts.forEach(clearTimeout);
}, [setFlash]);
return [flash, trigger];
}

View file

@ -101,7 +101,6 @@ library/inspector/cli-codegen-3.spec.ts cli codegen should generate fram
library/page-clock.spec.ts popup should run time before popup [timeout]
library/page-clock.spec.ts popup should tick after popup [timeout]
library/page-clock.spec.ts popup should tick before popup [timeout]
library/popup.spec.ts should not throttle rAF in the opener page [timeout]
library/popup.spec.ts should not throw when click closes popup [timeout]
library/popup.spec.ts should use viewport size from window features [timeout]
library/trace-viewer.spec.ts should serve css without content-type [timeout]

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