Merge branch 'main' into clip-screenshot-into-canvas

This commit is contained in:
Simon Knott 2024-10-22 12:50:44 +02:00
commit 4560b96067
No known key found for this signature in database
GPG key ID: 8CEDC00028084AEC
141 changed files with 5121 additions and 1402 deletions

View file

@ -8,14 +8,11 @@ test/assets/modernizr.js
/packages/playwright-ct-core/src/generated/* /packages/playwright-ct-core/src/generated/*
/index.d.ts /index.d.ts
node_modules/ node_modules/
browser_patches/*/checkout/
browser_patches/chromium/output/
**/*.d.ts **/*.d.ts
output/ output/
test-results/ test-results/
tests/components/ /tests/components/
tests/installation/fixture-scripts/ /tests/installation/fixture-scripts/
examples/
DEPS DEPS
.cache/ .cache/
utils/ /utils/

View file

@ -16,7 +16,7 @@ env:
jobs: jobs:
doc-and-lint: doc-and-lint:
name: "docs & lint" name: "docs & lint"
runs-on: ubuntu-20.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4

View file

@ -1,4 +1,4 @@
export default { export default {
testDir: '../../tests', testDir: '../../tests',
reporter: [['markdown'], ['html']] reporter: [[require.resolve('../../packages/playwright/lib/reporters/markdown')], ['html']]
}; };

View file

@ -12,7 +12,7 @@ on:
jobs: jobs:
check: check:
name: Check name: Check
runs-on: ubuntu-20.04 runs-on: ubuntu-24.04
if: github.repository == 'microsoft/playwright' if: github.repository == 'microsoft/playwright'
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -27,7 +27,7 @@ jobs:
repo: context.repo.repo, repo: context.repo.repo,
commit_sha: context.sha, commit_sha: context.sha,
}); });
const commitHeader = data.message.split('\n')[0]; const commitHeader = data.message.split('\n')[0].replace(/#(\d+)/g, 'https://github.com/microsoft/playwright/pull/$1');
const title = '[Ports]: Backport client side changes for ' + currentPlaywrightVersion; const title = '[Ports]: Backport client side changes for ' + currentPlaywrightVersion;
for (const repo of ['playwright-python', 'playwright-java', 'playwright-dotnet']) { for (const repo of ['playwright-python', 'playwright-java', 'playwright-dotnet']) {

View file

@ -65,7 +65,7 @@ jobs:
publish-trace-viewer: publish-trace-viewer:
name: "publish Trace Viewer to trace.playwright.dev" name: "publish Trace Viewer to trace.playwright.dev"
runs-on: ubuntu-20.04 runs-on: ubuntu-24.04
if: github.repository == 'microsoft/playwright' if: github.repository == 'microsoft/playwright'
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4

View file

@ -10,7 +10,7 @@ env:
jobs: jobs:
publish-npm-release: publish-npm-release:
name: "publish to NPM" name: "publish to NPM"
runs-on: ubuntu-20.04 runs-on: ubuntu-24.04
if: github.repository == 'microsoft/playwright' if: github.repository == 'microsoft/playwright'
permissions: permissions:
contents: read contents: read

View file

@ -7,7 +7,7 @@ on:
jobs: jobs:
publish-trace-viewer: publish-trace-viewer:
name: "publish Trace Viewer to trace.playwright.dev" name: "publish Trace Viewer to trace.playwright.dev"
runs-on: ubuntu-20.04 runs-on: ubuntu-24.04
if: github.repository == 'microsoft/playwright' if: github.repository == 'microsoft/playwright'
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4

View file

@ -12,7 +12,7 @@ permissions:
jobs: jobs:
roll: roll:
runs-on: ubuntu-20.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4

View file

@ -9,7 +9,7 @@ on:
jobs: jobs:
trigger: trigger:
name: "trigger" name: "trigger"
runs-on: ubuntu-20.04 runs-on: ubuntu-24.04
steps: steps:
- run: | - run: |
curl -X POST \ curl -X POST \

View file

@ -1,92 +1,77 @@
# Contributing # Contributing
- [How to Contribute](#how-to-contribute) ## Choose an issue
* [Getting Code](#getting-code)
* [Code reviews](#code-reviews)
* [Code Style](#code-style)
* [API guidelines](#api-guidelines)
* [Commit Messages](#commit-messages)
* [Writing Documentation](#writing-documentation)
* [Adding New Dependencies](#adding-new-dependencies)
* [Running & Writing Tests](#running--writing-tests)
* [Public API Coverage](#public-api-coverage)
- [Contributor License Agreement](#contributor-license-agreement)
* [Code of Conduct](#code-of-conduct)
## How to Contribute Playwright **requires an issue** for every contribution, except for minor documentation updates. We strongly recommend to pick an issue labeled `open-to-a-pull-request` for your first contribution to the project.
We strongly recommend that you open an issue before beginning any code modifications. This is particularly important if the changes involve complex logic or if the existing code isn't immediately clear. By doing so, we can discuss and agree upon the best approach to address a bug or implement a feature, ensuring that our efforts are aligned. If you are passioned about a bug/feature, but cannot find an issue describing it, **file an issue first**. This will facilitate the discussion and you might get some early feedback from project maintainers before spending your time on creating a pull request.
### Getting Code ## Make a change
Make sure you're running Node.js 20 to verify and upgrade NPM do:
Make sure you're running Node.js 20 or later.
```bash ```bash
node --version node --version
npm --version
npm i -g npm@latest
``` ```
1. Clone this repository Clone the repository. If you plan to send a pull request, it might be better to [fork the repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo) first.
```bash ```bash
git clone https://github.com/microsoft/playwright git clone https://github.com/microsoft/playwright
cd playwright cd playwright
``` ```
2. Install dependencies Install dependencies and run the build in watch mode.
```bash ```bash
npm ci npm ci
npm run watch
npx playwright install
``` ```
3. Build Playwright Playwright is a multi-package repository that uses npm workspaces. For browser APIs, look at [`packages/playwright-core`](https://github.com/microsoft/playwright/blob/main/packages/playwright-core). For test runner, see [`packages/playwright`](https://github.com/microsoft/playwright/blob/main/packages/playwright).
Note that some files are generated by the build, so the watch process might override your changes if done in the wrong file. For example, TypeScript types for the API are generated from the [`docs/src`](https://github.com/microsoft/playwright/blob/main/docs/src).
Coding style is fully defined in [.eslintrc](https://github.com/microsoft/playwright/blob/main/.eslintrc.js). Before creating a pull request, or at any moment during development, run linter to check all kinds of things:
```bash ```bash
npm run build npm run lint
``` ```
4. Run tests Comments should be generally avoided. If the code would not be understood without comments, consider re-writing the code to make it self-explanatory.
This will run a test on line `23` in `page-fill.spec.ts`: ### Write documentation
Every part of the public API should be documented in [`docs/src`](https://github.com/microsoft/playwright/blob/main/docs/src), in the same change that adds/changes the API. We use markdown files with custom structure to specify the API. Take a look around for an example.
Various other files are generated from the API specification. If you are running `npm run watch`, these will be re-generated automatically.
Larger changes will require updates to the documentation guides as well. This will be made clear during the code review.
## Add a test
Playwright requires a test for almost any new or modified functionality. An exception would be a pure refactoring, but chances are you are doing more than that.
There are multiple [test suites](https://github.com/microsoft/playwright/blob/main/tests) in Playwright that will be executed on the CI. The two most important that you need to run locally are:
- Library tests cover APIs not related to the test runner.
```bash ```bash
npm run ctest -- page-fill:23 # fast path runs all tests in Chromium
npm run ctest
# slow path runs all tests in three browsers
npm run test
``` ```
See [here](#running--writing-tests) for more information about running and writing tests. - Test runner tests.
### Code reviews
All submissions, including submissions by project members, require review. We
use GitHub pull requests for this purpose. Consult
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
information on using pull requests.
### Code Style
- Coding style is fully defined in [.eslintrc](https://github.com/microsoft/playwright/blob/main/.eslintrc.js)
- Comments should be generally avoided. If the code would not be understood without comments, consider re-writing the code to make it self-explanatory.
To run code linter, use:
```bash ```bash
npm run eslint npm run ttest
``` ```
### API guidelines Since Playwright tests are using Playwright under the hood, everything from our documentation applies, for example [this guide on running and debugging tests](https://playwright.dev/docs/running-tests#running-tests).
When authoring new API methods, consider the following: Note that tests should be *hermetic*, and not depend on external services. Tests should work on all three platforms: macOS, Linux and Windows.
- Expose as little information as needed. When in doubt, dont expose new information. ## Write a commit message
- Methods are used in favor of getters/setters.
- The only exception is namespaces, e.g. `page.keyboard` and `page.coverage`
- All string literals must be lowercase. This includes event names and option values.
- Avoid adding "sugar" API (API that is trivially implementable in user-space) unless they're **very** common.
### Commit Messages Commit messages should follow the [Semantic Commit Messages](https://www.conventionalcommits.org/en/v1.0.0/) format:
Commit messages should follow the Semantic Commit Messages format:
``` ```
label(namespace): title label(namespace): title
@ -97,131 +82,57 @@ footer
``` ```
1. *label* is one of the following: 1. *label* is one of the following:
- `fix` - playwright bug fixes. - `fix` - bug fixes
- `feat` - playwright features. - `feat` - new features
- `docs` - changes to docs, e.g. `docs(api): ..` to change documentation. - `docs` - documentation-only changes
- `test` - changes to playwright tests infrastructure. - `test` - test-only changes
- `devops` - build-related work, e.g. CI related patches and general changes to the browser build infrastructure - `devops` - changes to the CI or build
- `chore` - everything that doesn't fall under previous categories - `chore` - everything that doesn't fall under previous categories
2. *namespace* is put in parenthesis after label and is optional. Must be lowercase. 1. *namespace* is put in parenthesis after label and is optional. Must be lowercase.
3. *title* is a brief summary of changes. 1. *title* is a brief summary of changes.
4. *description* is **optional**, new-line separated from title and is in present tense. 1. *description* is **optional**, new-line separated from title and is in present tense.
5. *footer* is **optional**, new-line separated from *description* and contains "fixes" / "references" attribution to github issues. 1. *footer* is **optional**, new-line separated from *description* and contains "fixes" / "references" attribution to github issues.
Example: Example:
``` ```
fix(firefox): make sure session cookies work feat(trace viewer): network panel filtering
This patch fixes session cookies in the firefox browser. This patch adds a filtering toolbar to the network panel.
<link to a screenshot>
Fixes #123, fixes #234 Fixes #123, references #234.
``` ```
### Writing Documentation ## Send a pull request
All API classes, methods, and events should have a description in [`docs/src`](https://github.com/microsoft/playwright/blob/main/docs/src). There's a [documentation linter](https://github.com/microsoft/playwright/tree/main/utils/doclint) which makes sure documentation is aligned with the codebase. All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more information on using pull requests.
To run the documentation linter, use: After a successful code review, one of the maintainers will merge your pull request. Congratulations!
```bash ## More details
npm run doc
```
To build the documentation site locally and test how your changes will look in practice: **No new dependencies**
1. Clone the [microsoft/playwright.dev](https://github.com/microsoft/playwright.dev) repo There is a very high bar for new dependencies, including updating to a new version of an existing dependency. We recommend to explicitly discuss this in an issue and get a green light from a maintainer, before creating a pull request that updates dependencies.
1. Follow [the playwright.dev README instructions to "roll docs"](https://github.com/microsoft/playwright.dev/#roll-docs) against your local `playwright` repo with your changes in progress
1. Follow [the playwright.dev README instructions to "run dev server"](https://github.com/microsoft/playwright.dev/#run-dev-server) to view your changes
### Adding New Dependencies **Custom browser build**
For all dependencies (both installation and development):
- **Do not add** a dependency if the desired functionality is easily implementable.
- If adding a dependency, it should be well-maintained and trustworthy.
A barrier for introducing new installation dependencies is especially high:
- **Do not add** installation dependency unless it's critical to project success.
### Running & Writing Tests
- Every feature should be accompanied by a test.
- Every public api event/method should be accompanied by a test.
- Tests should be *hermetic*. Tests should not depend on external services.
- Tests should work on all three platforms: Mac, Linux and Win. This is especially important for screenshot tests.
Playwright tests are located in [`tests`](https://github.com/microsoft/playwright/blob/main/tests) and use `@playwright/test` test runner.
These are integration tests, making sure public API methods and events work as expected.
- To run all tests:
```bash
npx playwright install
npm run test
```
Be sure to run `npm run build` or let `npm run watch` run before you re-run the
tests after making your changes to check them.
- To run tests in Chromium
```bash
npm run ctest # also `ftest` for firefox and `wtest` for WebKit
npm run ctest -- page-fill:23 # runs line 23 of page-fill.spec.ts
```
- To run tests in WebKit / Firefox, use `wtest` or `ftest`.
- To run the Playwright test runner tests
```bash
npm run ttest
npm run ttest -- --grep "specific test"
```
- To run a specific test, substitute `it` with `it.only`, or use the `--grep 'My test'` CLI parameter:
```js
...
// Using "it.only" to run a specific test
it.only('should work', async ({server, page}) => {
const response = await page.goto(server.EMPTY_PAGE);
expect(response.ok).toBe(true);
});
// or
playwright test --config=xxx --grep 'should work'
```
- To disable a specific test, substitute `it` with `it.skip`:
```js
...
// Using "it.skip" to skip a specific test
it.skip('should work', async ({server, page}) => {
const response = await page.goto(server.EMPTY_PAGE);
expect(response.ok).toBe(true);
});
```
- To run tests in non-headless (headed) mode:
```bash
npm run ctest -- --headed
```
- To run tests with custom browser executable, specify `CRPATH`, `WKPATH` or `FFPATH` env variable that points to browser executable:
To run tests with custom browser executable, specify `CRPATH`, `WKPATH` or `FFPATH` env variable that points to browser executable:
```bash ```bash
CRPATH=<path-to-executable> npm run ctest CRPATH=<path-to-executable> npm run ctest
``` ```
- When should a test be marked with `skip` or `fixme`? You will also find `DEBUG=pw:browser` useful for debugging custom builds.
- **`skip(condition)`**: This test *should ***never*** work* for `condition` **Building documentation site**
where `condition` is usually something like: `test.skip(browserName === 'chromium', 'This does not work because of ...')`.
- **`fixme(condition)`**: This test *should ***eventually*** work* for `condition` The [playwright.dev](https://playwright.dev/) documentation site lives in a separate repository, and documentation from [`docs/src`](https://github.com/microsoft/playwright/blob/main/docs/src) is frequently rolled there.
where `condition` is usually something like: `test.fixme(browserName === 'chromium', 'We are waiting for version x')`.
Most of the time this should not concern you. However, if you are doing something unusual in the docs, you can build locally and test how your changes will look in practice:
1. Clone the [microsoft/playwright.dev](https://github.com/microsoft/playwright.dev) repo.
1. Follow [the playwright.dev README instructions to "roll docs"](https://github.com/microsoft/playwright.dev/#roll-docs) against your local `playwright` repo with your changes in progress.
1. Follow [the playwright.dev README instructions to "run dev server"](https://github.com/microsoft/playwright.dev/#run-dev-server) to view your changes.
## Contributor License Agreement ## Contributor License Agreement

View file

@ -1,6 +1,6 @@
# 🎭 Playwright # 🎭 Playwright
[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-130.0.6723.44-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-131.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[![WebKit version](https://img.shields.io/badge/webkit-18.0-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop --> [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord) [![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-131.0.6778.3-blue.svg?logo=google-chrome)](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[![Firefox version](https://img.shields.io/badge/firefox-131.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[![WebKit version](https://img.shields.io/badge/webkit-18.0-blue.svg?logo=safari)](https://webkit.org/)<!-- GEN:stop --> [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord)
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright) ## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
@ -8,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
| | Linux | macOS | Windows | | | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: | | :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->130.0.6723.44<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Chromium <!-- GEN:chromium-version -->131.0.6778.3<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->18.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: | | WebKit <!-- GEN:webkit-version -->18.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Firefox <!-- GEN:firefox-version -->131.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Firefox <!-- GEN:firefox-version -->131.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
@ -46,7 +46,6 @@ npx playwright install
You can optionally install only selected browsers, see [install browsers](https://playwright.dev/docs/cli#install-browsers) for more details. Or you can install no browsers at all and use existing [browser channels](https://playwright.dev/docs/browsers). You can optionally install only selected browsers, see [install browsers](https://playwright.dev/docs/cli#install-browsers) for more details. Or you can install no browsers at all and use existing [browser channels](https://playwright.dev/docs/browsers).
* [Getting started](https://playwright.dev/docs/intro) * [Getting started](https://playwright.dev/docs/intro)
* [Installation configuration](https://playwright.dev/docs/installation)
* [API reference](https://playwright.dev/docs/api/class-playwright) * [API reference](https://playwright.dev/docs/api/class-playwright)
## Capabilities ## Capabilities
@ -163,7 +162,7 @@ test('Intercept network requests', async ({ page }) => {
## Resources ## Resources
* [Documentation](https://playwright.dev/docs/intro) * [Documentation](https://playwright.dev)
* [API reference](https://playwright.dev/docs/api/class-playwright/) * [API reference](https://playwright.dev/docs/api/class-playwright/)
* [Contribution guide](CONTRIBUTING.md) * [Contribution guide](CONTRIBUTING.md)
* [Changelog](https://github.com/microsoft/playwright/releases) * [Changelog](https://github.com/microsoft/playwright/releases)

View file

@ -150,6 +150,63 @@ var button = page.GetByRole(AriaRole.Button).And(page.GetByTitle("Subscribe"));
Additional locator to match. Additional locator to match.
## async method: Locator.ariaSnapshot
* since: v1.49
- returns: <[string]>
Captures the aria snapshot of the given element. See [`method: LocatorAssertions.toMatchAriaSnapshot`] for the corresponding assertion.
**Usage**
```js
await page.getByRole('link').ariaSnapshot();
```
```java
page.getByRole(AriaRole.LINK).ariaSnapshot();
```
```python async
await page.get_by_role("link").aria_snapshot()
```
```python sync
page.get_by_role("link").aria_snapshot()
```
```csharp
await page.GetByRole(AriaRole.Link).AriaSnapshotAsync();
```
**Details**
This method captures the aria snapshot of the given element. The snapshot is a string that represents the state of the element and its children.
The snapshot can be used to assert the state of the element in the test, or to compare it to state in the future.
The ARIA snapshot is represented using [YAML](https://yaml.org/spec/1.2.2/) markup language:
* The keys of the objects are the roles and optional accessible names of the elements.
* The values are either text content or an array of child elements.
* Generic static text can be represented with the `text` key.
Below is the HTML markup and the respective ARIA snapshot:
```html
<ul aria-label="Links">
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<ul>
```
```yml
- list "Links":
- listitem:
- link "Home"
- listitem:
- link "About"
```
### option: Locator.ariaSnapshot.timeout = %%-input-timeout-js-%%
* since: v1.49
## async method: Locator.blur ## async method: Locator.blur
* since: v1.28 * since: v1.28

View file

@ -2106,15 +2106,14 @@ Expected options currently selected.
## async method: LocatorAssertions.toMatchAriaSnapshot ## async method: LocatorAssertions.toMatchAriaSnapshot
* since: v1.49 * since: v1.49
* langs: js * langs:
- alias-java: matchesAriaSnapshot
Asserts that the target element matches the given accessibility snapshot. Asserts that the target element matches the given accessibility snapshot.
**Usage** **Usage**
```js ```js
import { role as x } from '@playwright/test';
// ...
await page.goto('https://demo.playwright.dev/todomvc/'); await page.goto('https://demo.playwright.dev/todomvc/');
await expect(page.locator('body')).toMatchAriaSnapshot(` await expect(page.locator('body')).toMatchAriaSnapshot(`
- heading "todos" - heading "todos"
@ -2122,11 +2121,41 @@ await expect(page.locator('body')).toMatchAriaSnapshot(`
`); `);
``` ```
```python async
await page.goto('https://demo.playwright.dev/todomvc/')
await expect(page.locator('body')).to_match_aria_snapshot('''
- heading "todos"
- textbox "What needs to be done?"
''')
```
```python sync
page.goto('https://demo.playwright.dev/todomvc/')
expect(page.locator('body')).to_match_aria_snapshot('''
- heading "todos"
- textbox "What needs to be done?"
''')
```
```csharp
await page.GotoAsync("https://demo.playwright.dev/todomvc/");
await Expect(page.Locator("body")).ToMatchAriaSnapshotAsync(@"
- heading ""todos""
- textbox ""What needs to be done?""
");
```
```java
page.navigate("https://demo.playwright.dev/todomvc/");
assertThat(page.locator("body")).matchesAriaSnapshot("""
- heading "todos"
- textbox "What needs to be done?"
""");
```
### param: LocatorAssertions.toMatchAriaSnapshot.expected ### param: LocatorAssertions.toMatchAriaSnapshot.expected
* since: v1.49 * since: v1.49
* langs: js
- `expected` <string> - `expected` <string>
### option: LocatorAssertions.toMatchAriaSnapshot.timeout = %%-js-assertions-timeout-%% ### option: LocatorAssertions.toMatchAriaSnapshot.timeout = %%-js-assertions-timeout-%%
* since: v1.49 * since: v1.49
* langs: js

View file

@ -3691,8 +3691,8 @@ await page.routeWebSocket('/ws', ws => {
```java ```java
page.routeWebSocket("/ws", ws -> { page.routeWebSocket("/ws", ws -> {
ws.onMessage(message -> { ws.onMessage(frame -> {
if ("request".equals(message)) if ("request".equals(frame.text()))
ws.send("response"); ws.send("response");
}); });
}); });
@ -3722,8 +3722,8 @@ page.route_web_socket("/ws", handler)
```csharp ```csharp
await page.RouteWebSocketAsync("/ws", ws => { await page.RouteWebSocketAsync("/ws", ws => {
ws.OnMessage(message => { ws.OnMessage(frame => {
if (message == "request") if (frame.Text == "request")
ws.Send("response"); ws.Send("response");
}); });
}); });

View file

@ -18,8 +18,8 @@ await page.routeWebSocket('wss://example.com/ws', ws => {
```java ```java
page.routeWebSocket("wss://example.com/ws", ws -> { page.routeWebSocket("wss://example.com/ws", ws -> {
ws.onMessage(message -> { ws.onMessage(frame -> {
if ("request".equals(message)) if ("request".equals(frame.text()))
ws.send("response"); ws.send("response");
}); });
}); });
@ -47,8 +47,8 @@ page.route_web_socket("wss://example.com/ws", lambda ws: ws.on_message(
```csharp ```csharp
await page.RouteWebSocketAsync("wss://example.com/ws", ws => { await page.RouteWebSocketAsync("wss://example.com/ws", ws => {
ws.OnMessage(message => { ws.OnMessage(frame => {
if (message == "request") if (frame.Text == "request")
ws.Send("response"); ws.Send("response");
}); });
}); });
@ -70,8 +70,8 @@ await page.routeWebSocket('wss://example.com/ws', ws => {
```java ```java
page.routeWebSocket("wss://example.com/ws", ws -> { page.routeWebSocket("wss://example.com/ws", ws -> {
ws.onMessage(message -> { ws.onMessage(frame -> {
JsonObject json = new JsonParser().parse(message).getAsJsonObject(); JsonObject json = new JsonParser().parse(frame.text()).getAsJsonObject();
if ("question".equals(json.get("request").getAsString())) { if ("question".equals(json.get("request").getAsString())) {
Map<String, String> result = new HashMap(); Map<String, String> result = new HashMap();
result.put("response", "answer"); result.put("response", "answer");
@ -105,8 +105,8 @@ page.route_web_socket("wss://example.com/ws", lambda ws: ws.on_message(
```csharp ```csharp
await page.RouteWebSocketAsync("wss://example.com/ws", ws => { await page.RouteWebSocketAsync("wss://example.com/ws", ws => {
ws.OnMessage(message => { ws.OnMessage(frame => {
using var jsonDoc = JsonDocument.Parse(message); using var jsonDoc = JsonDocument.Parse(frame.Text);
JsonElement root = jsonDoc.RootElement; JsonElement root = jsonDoc.RootElement;
if (root.TryGetProperty("request", out JsonElement requestElement) && requestElement.GetString() == "question") if (root.TryGetProperty("request", out JsonElement requestElement) && requestElement.GetString() == "question")
{ {
@ -140,11 +140,11 @@ await page.routeWebSocket('/ws', ws => {
```java ```java
page.routeWebSocket("/ws", ws -> { page.routeWebSocket("/ws", ws -> {
WebSocketRoute server = ws.connectToServer(); WebSocketRoute server = ws.connectToServer();
ws.onMessage(message -> { ws.onMessage(frame -> {
if ("request".equals(message)) if ("request".equals(frame.text()))
server.send("request2"); server.send("request2");
else else
server.send(message); server.send(frame.text());
}); });
}); });
``` ```
@ -180,11 +180,11 @@ page.route_web_socket("/ws", handler)
```csharp ```csharp
await page.RouteWebSocketAsync("/ws", ws => { await page.RouteWebSocketAsync("/ws", ws => {
var server = ws.ConnectToServer(); var server = ws.ConnectToServer();
ws.OnMessage(message => { ws.OnMessage(frame => {
if (message == "request") if (frame.Text == "request")
server.Send("request2"); server.Send("request2");
else else
server.Send(message); server.Send(frame.Text);
}); });
}); });
``` ```
@ -215,13 +215,13 @@ await page.routeWebSocket('/ws', ws => {
```java ```java
page.routeWebSocket("/ws", ws -> { page.routeWebSocket("/ws", ws -> {
WebSocketRoute server = ws.connectToServer(); WebSocketRoute server = ws.connectToServer();
ws.onMessage(message -> { ws.onMessage(frame -> {
if (!"blocked-from-the-page".equals(message)) if (!"blocked-from-the-page".equals(frame.text()))
server.send(message); server.send(frame.text());
}); });
server.onMessage(message -> { server.onMessage(frame -> {
if (!"blocked-from-the-server".equals(message)) if (!"blocked-from-the-server".equals(frame.text()))
ws.send(message); ws.send(frame.text());
}); });
}); });
``` ```
@ -263,13 +263,13 @@ page.route_web_socket("/ws", handler)
```csharp ```csharp
await page.RouteWebSocketAsync("/ws", ws => { await page.RouteWebSocketAsync("/ws", ws => {
var server = ws.ConnectToServer(); var server = ws.ConnectToServer();
ws.OnMessage(message => { ws.OnMessage(frame => {
if (message != "blocked-from-the-page") if (frame.Text != "blocked-from-the-page")
server.Send(message); server.Send(frame.Text);
}); });
server.OnMessage(message => { server.OnMessage(frame => {
if (message != "blocked-from-the-server") if (frame.Text != "blocked-from-the-server")
ws.Send(message); ws.Send(frame.Text);
}); });
}); });
``` ```

View file

@ -5,7 +5,7 @@ title: "Docker"
## Introduction ## Introduction
[Dockerfile.jammy] can be used to run Playwright scripts in Docker environment. This image includes the [Playwright browsers](./browsers.md#install-browsers) and [browser system dependencies](./browsers.md#install-system-dependencies). The Playwright package/dependency is not included in the image and should be installed separately. [Dockerfile.noble] can be used to run Playwright scripts in Docker environment. This image includes the [Playwright browsers](./browsers.md#install-browsers) and [browser system dependencies](./browsers.md#install-system-dependencies). The Playwright package/dependency is not included in the image and should be installed separately.
## Usage ## Usage
@ -111,7 +111,6 @@ We currently publish images with the following tags:
- `:v%%VERSION%%` - Playwright v%%VERSION%% release docker image based on Ubuntu 24.04 LTS (Noble Numbat). - `:v%%VERSION%%` - Playwright v%%VERSION%% release docker image based on Ubuntu 24.04 LTS (Noble Numbat).
- `:v%%VERSION%%-noble` - Playwright v%%VERSION%% release docker image based on Ubuntu 24.04 LTS (Noble Numbat). - `:v%%VERSION%%-noble` - Playwright v%%VERSION%% release docker image based on Ubuntu 24.04 LTS (Noble Numbat).
- `:v%%VERSION%%-jammy` - Playwright v%%VERSION%% release docker image based on Ubuntu 22.04 LTS (Jammy Jellyfish). - `:v%%VERSION%%-jammy` - Playwright v%%VERSION%% release docker image based on Ubuntu 22.04 LTS (Jammy Jellyfish).
- `:v%%VERSION%%-focal` - Playwright v%%VERSION%% release docker image based on Ubuntu 20.04 LTS (Focal Fossa).
:::note :::note
It is recommended to always pin your Docker image to a specific version if possible. If the Playwright version in your Docker image does not match the version in your project/tests, Playwright will be unable to locate browser executables. It is recommended to always pin your Docker image to a specific version if possible. If the Playwright version in your Docker image does not match the version in your project/tests, Playwright will be unable to locate browser executables.
@ -122,7 +121,6 @@ It is recommended to always pin your Docker image to a specific version if possi
We currently publish images based on the following [Ubuntu](https://hub.docker.com/_/ubuntu) versions: We currently publish images based on the following [Ubuntu](https://hub.docker.com/_/ubuntu) versions:
- **Ubuntu 24.04 LTS** (Noble Numbat), image tags include `noble` - **Ubuntu 24.04 LTS** (Noble Numbat), image tags include `noble`
- **Ubuntu 22.04 LTS** (Jammy Jellyfish), image tags include `jammy` - **Ubuntu 22.04 LTS** (Jammy Jellyfish), image tags include `jammy`
- **Ubuntu 20.04 LTS** (Focal Fossa), image tags include `focal`
#### Alpine #### Alpine

View file

@ -180,8 +180,8 @@ See our doc on [Running and Debugging Tests](./running-tests.md) to learn more a
- Playwright is distributed as a .NET Standard 2.0 library. We recommend .NET 8. - Playwright is distributed as a .NET Standard 2.0 library. We recommend .NET 8.
- Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL). - Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL).
- macOS 13 Ventura, or macOS 14 Sonoma. - macOS 13 Ventura, or later.
- Debian 11, Debian 12, Ubuntu 20.04 or Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture. - Debian 12, Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture.
## What's next ## What's next

View file

@ -130,8 +130,8 @@ By default browsers launched with Playwright run headless, meaning no browser UI
- Java 8 or higher. - Java 8 or higher.
- Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL). - Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL).
- macOS 13 Ventura, or macOS 14 Sonoma. - macOS 13 Ventura, or later.
- Debian 11, Debian 12, Ubuntu 20.04 or Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture. - Debian 12, Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture.
## What's next ## What's next

View file

@ -288,8 +288,8 @@ pnpm exec playwright --version
- Node.js 18+ - Node.js 18+
- Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL). - Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL).
- macOS 13 Ventura, or macOS 14 Sonoma. - macOS 13 Ventura, or later.
- Debian 11, Debian 12, Ubuntu 20.04 or Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture. - Debian 12, Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture.
## What's next ## What's next

View file

@ -101,8 +101,8 @@ pip install pytest-playwright playwright -U
- Python 3.8 or higher. - Python 3.8 or higher.
- Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL). - Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL).
- macOS 13 Ventura, or macOS 14 Sonoma. - macOS 13 Ventura, or later.
- Debian 11, Debian 12, Ubuntu 20.04 or Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture. - Debian 12, Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture.
## What's next ## What's next

View file

@ -62,11 +62,11 @@ expect(page.get_by_text("Welcome, John!")).to_be_visible()
``` ```
```csharp ```csharp
await page.GetByLabel("User Name").FillAsync("John"); await Page.GetByLabel("User Name").FillAsync("John");
await page.GetByLabel("Password").FillAsync("secret-password"); await Page.GetByLabel("Password").FillAsync("secret-password");
await page.GetByRole(AriaRole.Button, new() { Name = "Sign in" }).ClickAsync(); await Page.GetByRole(AriaRole.Button, new() { Name = "Sign in" }).ClickAsync();
await Expect(Page.GetByText("Welcome, John!")).ToBeVisibleAsync(); await Expect(Page.GetByText("Welcome, John!")).ToBeVisibleAsync();
``` ```
@ -101,7 +101,7 @@ page.get_by_role("button", name="Sign in").click()
``` ```
```csharp ```csharp
await page.GetByRole(AriaRole.Button, new() { Name = "Sign in" }).ClickAsync(); await Page.GetByRole(AriaRole.Button, new() { Name = "Sign in" }).ClickAsync();
``` ```
:::note :::note
@ -143,7 +143,7 @@ locator.click()
``` ```
```csharp ```csharp
var locator = page.GetByRole(AriaRole.Button, new() { Name = "Sign in" }); var locator = Page.GetByRole(AriaRole.Button, new() { Name = "Sign in" });
await locator.HoverAsync(); await locator.HoverAsync();
await locator.ClickAsync(); await locator.ClickAsync();
@ -180,7 +180,7 @@ locator.click()
``` ```
```csharp ```csharp
var locator = page var locator = Page
.FrameLocator("#my-frame") .FrameLocator("#my-frame")
.GetByRole(AriaRole.Button, new() { Name = "Sign in" }); .GetByRole(AriaRole.Button, new() { Name = "Sign in" });
@ -249,11 +249,11 @@ await Expect(Page
.GetByRole(AriaRole.Heading, new() { Name = "Sign up" })) .GetByRole(AriaRole.Heading, new() { Name = "Sign up" }))
.ToBeVisibleAsync(); .ToBeVisibleAsync();
await page await Page
.GetByRole(AriaRole.Checkbox, new() { Name = "Subscribe" }) .GetByRole(AriaRole.Checkbox, new() { Name = "Subscribe" })
.CheckAsync(); .CheckAsync();
await page await Page
.GetByRole(AriaRole.Button, new() { .GetByRole(AriaRole.Button, new() {
NameRegex = new Regex("submit", RegexOptions.IgnoreCase) NameRegex = new Regex("submit", RegexOptions.IgnoreCase)
}) })
@ -298,7 +298,7 @@ page.get_by_label("Password").fill("secret")
``` ```
```csharp ```csharp
await page.GetByLabel("Password").FillAsync("secret"); await Page.GetByLabel("Password").FillAsync("secret");
``` ```
:::note[When to use label locators] :::note[When to use label locators]
@ -335,7 +335,7 @@ page.get_by_placeholder("name@example.com").fill("playwright@microsoft.com")
``` ```
```csharp ```csharp
await page await Page
.GetByPlaceholder("name@example.com") .GetByPlaceholder("name@example.com")
.FillAsync("playwright@microsoft.com"); .FillAsync("playwright@microsoft.com");
``` ```
@ -468,7 +468,7 @@ page.get_by_alt_text("playwright logo").click()
``` ```
```csharp ```csharp
await page.GetByAltText("playwright logo").ClickAsync(); await Page.GetByAltText("playwright logo").ClickAsync();
``` ```
:::note[When to use alt locators] :::note[When to use alt locators]
@ -540,7 +540,7 @@ page.get_by_test_id("directions").click()
``` ```
```csharp ```csharp
await page.GetByTestId("directions").ClickAsync(); await Page.GetByTestId("directions").ClickAsync();
``` ```
:::note[When to use testid locators] :::note[When to use testid locators]
@ -604,7 +604,7 @@ page.get_by_test_id("directions").click()
``` ```
```csharp ```csharp
await page.GetByTestId("directions").ClickAsync(); await Page.GetByTestId("directions").ClickAsync();
``` ```
### Locate by CSS or XPath ### Locate by CSS or XPath
@ -644,11 +644,11 @@ page.locator("//button").click()
``` ```
```csharp ```csharp
await page.Locator("css=button").ClickAsync(); await Page.Locator("css=button").ClickAsync();
await page.Locator("xpath=//button").ClickAsync(); await Page.Locator("xpath=//button").ClickAsync();
await page.Locator("button").ClickAsync(); await Page.Locator("button").ClickAsync();
await page.Locator("//button").ClickAsync(); await Page.Locator("//button").ClickAsync();
``` ```
XPath and CSS selectors can be tied to the DOM structure or implementation. These selectors can break when the DOM structure changes. Long CSS or XPath chains below are an example of a **bad practice** that leads to unstable tests: XPath and CSS selectors can be tied to the DOM structure or implementation. These selectors can break when the DOM structure changes. Long CSS or XPath chains below are an example of a **bad practice** that leads to unstable tests:
@ -688,9 +688,9 @@ page.locator('//*[@id="tsf"]/div[2]/div[1]/div[1]/div/div[2]/input').click()
``` ```
```csharp ```csharp
await page.Locator("#tsf > div:nth-child(2) > div.A8SBwf > div.RNNXgb > div > div.a4bIc > input").ClickAsync(); await Page.Locator("#tsf > div:nth-child(2) > div.A8SBwf > div.RNNXgb > div > div.a4bIc > input").ClickAsync();
await page.Locator("//*[@id='tsf']/div[2]/div[1]/div[1]/div/div[2]/input").ClickAsync(); await Page.Locator("//*[@id='tsf']/div[2]/div[1]/div[1]/div/div[2]/input").ClickAsync();
``` ```
:::note[When to use this] :::note[When to use this]
@ -1218,7 +1218,7 @@ var button = page.GetByRole(AriaRole.Button).And(page.GetByTitle("Subscribe"));
### Matching one of the two alternative locators ### Matching one of the two alternative locators
If you'd like to target one of the two or more elements, and you don't know which one it will be, use [`method: Locator.or`] to create a locator that matches all of the alternatives. If you'd like to target one of the two or more elements, and you don't know which one it will be, use [`method: Locator.or`] to create a locator that matches any one or both of the alternatives.
For example, consider a scenario where you'd like to click on a "New email" button, but sometimes a security settings dialog shows up instead. In this case, you can wait for either a "New email" button, or a dialog and act accordingly. For example, consider a scenario where you'd like to click on a "New email" button, but sometimes a security settings dialog shows up instead. In this case, you can wait for either a "New email" button, or a dialog and act accordingly.

View file

@ -451,8 +451,8 @@ await page.routeWebSocket('wss://example.com/ws', ws => {
```java ```java
page.routeWebSocket("wss://example.com/ws", ws -> { page.routeWebSocket("wss://example.com/ws", ws -> {
ws.onMessage(message -> { ws.onMessage(frame -> {
if ("request".equals(message)) if ("request".equals(frame.text()))
ws.send("response"); ws.send("response");
}); });
}); });
@ -480,8 +480,8 @@ page.route_web_socket("wss://example.com/ws", lambda ws: ws.on_message(
```csharp ```csharp
await page.RouteWebSocketAsync("wss://example.com/ws", ws => { await page.RouteWebSocketAsync("wss://example.com/ws", ws => {
ws.OnMessage(message => { ws.OnMessage(frame => {
if (message == "request") if (frame.Text == "request")
ws.Send("response"); ws.Send("response");
}); });
}); });
@ -504,11 +504,11 @@ await page.routeWebSocket('wss://example.com/ws', ws => {
```java ```java
page.routeWebSocket("wss://example.com/ws", ws -> { page.routeWebSocket("wss://example.com/ws", ws -> {
WebSocketRoute server = ws.connectToServer(); WebSocketRoute server = ws.connectToServer();
ws.onMessage(message -> { ws.onMessage(frame -> {
if ("request".equals(message)) if ("request".equals(frame.text()))
server.send("request2"); server.send("request2");
else else
server.send(message); server.send(frame.text());
}); });
}); });
``` ```
@ -544,11 +544,11 @@ page.route_web_socket("wss://example.com/ws", handler)
```csharp ```csharp
await page.RouteWebSocketAsync("wss://example.com/ws", ws => { await page.RouteWebSocketAsync("wss://example.com/ws", ws => {
var server = ws.ConnectToServer(); var server = ws.ConnectToServer();
ws.OnMessage(message => { ws.OnMessage(frame => {
if (message == "request") if (frame.Text == "request")
server.Send("request2"); server.Send("request2");
else else
server.Send(message); server.Send(frame.Text);
}); });
}); });
``` ```

View file

@ -13,8 +13,8 @@ New methods [`method: Page.routeWebSocket`] and [`method: BrowserContext.routeWe
```csharp ```csharp
await page.RouteWebSocketAsync("/ws", ws => { await page.RouteWebSocketAsync("/ws", ws => {
ws.OnMessage(message => { ws.OnMessage(frame => {
if (message == "request") if (frame.Text == "request")
ws.Send("response"); ws.Send("response");
}); });
}); });

View file

@ -12,8 +12,8 @@ New methods [`method: Page.routeWebSocket`] and [`method: BrowserContext.routeWe
```java ```java
page.routeWebSocket("/ws", ws -> { page.routeWebSocket("/ws", ws -> {
ws.onMessage(message -> { ws.onMessage(frame -> {
if ("request".equals(message)) if ("request".equals(frame.text()))
ws.send("response"); ws.send("response");
}); });
}); });

View file

@ -8,6 +8,11 @@ import LiteYouTube from '@site/src/components/LiteYouTube';
## Version 1.48 ## Version 1.48
<LiteYouTube
id="VGlkSBkMVCQ"
title="Playwright 1.48"
/>
### WebSocket routing ### WebSocket routing
New methods [`method: Page.routeWebSocket`] and [`method: BrowserContext.routeWebSocket`] allow to intercept, modify and mock WebSocket connections initiated in the page. Below is a simple example that mocks WebSocket communication by responding to a `"request"` with a `"response"`. New methods [`method: Page.routeWebSocket`] and [`method: BrowserContext.routeWebSocket`] allow to intercept, modify and mock WebSocket connections initiated in the page. Below is a simple example that mocks WebSocket communication by responding to a `"request"` with a `"response"`.

View file

@ -1138,6 +1138,57 @@ Optional description that will be reflected in a test report.
## method: Test.fail.only
* since: v1.49
You can use `test.fail.only` to focus on a specific test that is expected to fail. This is particularly useful when debugging a failing test or working on a specific issue.
To declare a focused "failing" test:
* `test.fail.only(title, body)`
* `test.fail.only(title, details, body)`
**Usage**
You can declare a focused failing test, so that Playwright runs only this test and ensures it actually fails.
```js
import { test, expect } from '@playwright/test';
test.fail.only('focused failing test', async ({ page }) => {
// This test is expected to fail
});
test('not in the focused group', async ({ page }) => {
// This test will not run
});
```
### param: Test.fail.only.title
* since: v1.49
- `title` ?<[string]>
Test title.
### param: Test.fail.only.details
* since: v1.49
- `details` ?<[Object]>
- `tag` ?<[string]|[Array]<[string]>>
- `annotation` ?<[Object]|[Array]<[Object]>>
- `type` <[string]>
- `description` ?<[string]>
See [`method: Test.describe`] for test details description.
### param: Test.fail.only.body
* since: v1.49
- `body` ?<[function]\([Fixtures], [TestInfo]\)>
Test body that takes one or two arguments: an object with fixtures and optional [TestInfo].
## method: Test.fixme ## method: Test.fixme
* since: v1.10 * since: v1.10

View file

@ -110,9 +110,9 @@ export default defineConfig({
## property: TestConfig.globalSetup ## property: TestConfig.globalSetup
* since: v1.10 * since: v1.10
- type: ?<[string]> - type: ?<[string]|[Array]<[string]>>
Path to the global setup file. This file will be required and run before all the tests. It must export a single function that takes a [FullConfig] argument. Path to the global setup file. This file will be required and run before all the tests. It must export a single function that takes a [FullConfig] argument. Pass an array of paths to specify multiple global setup files.
Learn more about [global setup and teardown](../test-global-setup-teardown.md). Learn more about [global setup and teardown](../test-global-setup-teardown.md).
@ -128,9 +128,9 @@ export default defineConfig({
## property: TestConfig.globalTeardown ## property: TestConfig.globalTeardown
* since: v1.10 * since: v1.10
- type: ?<[string]> - type: ?<[string]|[Array]<[string]>>
Path to the global teardown file. This file will be required and run after all the tests. It must export a single function. See also [`property: TestConfig.globalSetup`]. Path to the global teardown file. This file will be required and run after all the tests. It must export a single function. See also [`property: TestConfig.globalSetup`]. Pass an array of paths to specify multiple global teardown files.
Learn more about [global setup and teardown](../test-global-setup-teardown.md). Learn more about [global setup and teardown](../test-global-setup-teardown.md).

View file

@ -4,18 +4,54 @@
Information about an error thrown during test execution. Information about an error thrown during test execution.
## property: TestError.expected
* since: v1.49
- type: ?<[string]>
Expected value formatted as a human-readable string.
## property: TestError.locator
* since: v1.49
- type: ?<[string]>
Receiver's locator.
## property: TestError.log
* since: v1.49
- type: ?<[Array]<[string]>>
Call log.
## property: TestError.matcherName
* since: v1.49
- type: ?<[string]>
Expect matcher name.
## property: TestError.message ## property: TestError.message
* since: v1.10 * since: v1.10
- type: ?<[string]> - type: ?<[string]>
Error message. Set when [Error] (or its subclass) has been thrown. Error message. Set when [Error] (or its subclass) has been thrown.
## property: TestError.received
* since: v1.49
- type: ?<[string]>
Received value formatted as a human-readable string.
## property: TestError.stack ## property: TestError.stack
* since: v1.10 * since: v1.10
- type: ?<[string]> - type: ?<[string]>
Error stack. Set when [Error] (or its subclass) has been thrown. Error stack. Set when [Error] (or its subclass) has been thrown.
## property: TestError.timeout
* since: v1.49
- type: ?<[int]>
Timeout in milliseconds, if the error was caused by a timeout.
## property: TestError.value ## property: TestError.value
* since: v1.10 * since: v1.10
- type: ?<[string]> - type: ?<[string]>

View file

@ -22,9 +22,10 @@ import { ImageDiffView } from '@web/shared/imageDiffView';
export const TestErrorView: React.FC<{ export const TestErrorView: React.FC<{
error: string; error: string;
}> = ({ error }) => { testId?: string;
}> = ({ error, testId }) => {
const html = React.useMemo(() => ansiErrorToHtml(error), [error]); const html = React.useMemo(() => ansiErrorToHtml(error), [error]);
return <div className='test-error-view test-error-text' dangerouslySetInnerHTML={{ __html: html || '' }}></div>; return <div className='test-error-view test-error-text' data-testId={testId} dangerouslySetInnerHTML={{ __html: html || '' }}></div>;
}; };
export const TestScreenshotErrorView: React.FC<{ export const TestScreenshotErrorView: React.FC<{

View file

@ -186,7 +186,7 @@ const StepTreeItem: React.FC<{
</span>} loadChildren={step.steps.length + (step.snippet ? 1 : 0) ? () => { </span>} loadChildren={step.steps.length + (step.snippet ? 1 : 0) ? () => {
const children = step.steps.map((s, i) => <StepTreeItem key={i} step={s} depth={depth + 1}></StepTreeItem>); const children = step.steps.map((s, i) => <StepTreeItem key={i} step={s} depth={depth + 1}></StepTreeItem>);
if (step.snippet) if (step.snippet)
children.unshift(<TestErrorView key='line' error={step.snippet}></TestErrorView>); children.unshift(<TestErrorView testId='test-snippet' key='line' error={step.snippet}></TestErrorView>);
return children; return children;
} : undefined} depth={depth}></TreeItem>; } : undefined} depth={depth}></TreeItem>;
}; };

View file

@ -48,6 +48,7 @@ This project incorporates components from the projects listed below. The origina
- stack-utils@2.0.5 (https://github.com/tapjs/stack-utils) - stack-utils@2.0.5 (https://github.com/tapjs/stack-utils)
- wrappy@1.0.2 (https://github.com/npm/wrappy) - wrappy@1.0.2 (https://github.com/npm/wrappy)
- ws@8.17.1 (https://github.com/websockets/ws) - ws@8.17.1 (https://github.com/websockets/ws)
- yaml@2.6.0 (https://github.com/eemeli/yaml)
- yauzl@2.10.0 (https://github.com/thejoshwolfe/yauzl) - yauzl@2.10.0 (https://github.com/thejoshwolfe/yauzl)
- yazl@2.5.1 (https://github.com/thejoshwolfe/yazl) - yazl@2.5.1 (https://github.com/thejoshwolfe/yazl)
@ -1121,6 +1122,24 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
========================================= =========================================
END OF ws@8.17.1 AND INFORMATION END OF ws@8.17.1 AND INFORMATION
%% yaml@2.6.0 NOTICES AND INFORMATION BEGIN HERE
=========================================
Copyright Eemeli Aro <eemeli@gmail.com>
Permission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted, provided that the above copyright notice
and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.
=========================================
END OF yaml@2.6.0 AND INFORMATION
%% yauzl@2.10.0 NOTICES AND INFORMATION BEGIN HERE %% yauzl@2.10.0 NOTICES AND INFORMATION BEGIN HERE
========================================= =========================================
The MIT License (MIT) The MIT License (MIT)
@ -1175,6 +1194,6 @@ END OF yazl@2.5.1 AND INFORMATION
SUMMARY BEGIN HERE SUMMARY BEGIN HERE
========================================= =========================================
Total Packages: 46 Total Packages: 47
========================================= =========================================
END OF SUMMARY END OF SUMMARY

View file

@ -3,15 +3,15 @@
"browsers": [ "browsers": [
{ {
"name": "chromium", "name": "chromium",
"revision": "1142", "revision": "1143",
"installByDefault": true, "installByDefault": true,
"browserVersion": "130.0.6723.44" "browserVersion": "131.0.6778.3"
}, },
{ {
"name": "chromium-tip-of-tree", "name": "chromium-tip-of-tree",
"revision": "1268", "revision": "1269",
"installByDefault": false, "installByDefault": false,
"browserVersion": "131.0.6768.0" "browserVersion": "131.0.6778.0"
}, },
{ {
"name": "firefox", "name": "firefox",
@ -21,13 +21,13 @@
}, },
{ {
"name": "firefox-beta", "name": "firefox-beta",
"revision": "1464", "revision": "1465",
"installByDefault": false, "installByDefault": false,
"browserVersion": "131.0b2" "browserVersion": "132.0b8"
}, },
{ {
"name": "webkit", "name": "webkit",
"revision": "2092", "revision": "2094",
"installByDefault": true, "installByDefault": true,
"revisionOverrides": { "revisionOverrides": {
"mac10.14": "1446", "mac10.14": "1446",
@ -35,7 +35,9 @@
"mac11": "1816", "mac11": "1816",
"mac11-arm64": "1816", "mac11-arm64": "1816",
"mac12": "2009", "mac12": "2009",
"mac12-arm64": "2009" "mac12-arm64": "2009",
"ubuntu20.04-x64": "2092",
"ubuntu20.04-arm64": "2092"
}, },
"browserVersion": "18.0" "browserVersion": "18.0"
}, },

View file

@ -25,7 +25,8 @@
"signal-exit": "3.0.7", "signal-exit": "3.0.7",
"socks-proxy-agent": "8.0.4", "socks-proxy-agent": "8.0.4",
"stack-utils": "2.0.5", "stack-utils": "2.0.5",
"ws": "8.17.1" "ws": "8.17.1",
"yaml": "^2.5.1"
}, },
"devDependencies": { "devDependencies": {
"@types/debug": "^4.1.7", "@types/debug": "^4.1.7",
@ -432,6 +433,17 @@
"optional": true "optional": true
} }
} }
},
"node_modules/yaml": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz",
"integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14"
}
} }
}, },
"dependencies": { "dependencies": {
@ -726,6 +738,11 @@
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"requires": {} "requires": {}
},
"yaml": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz",
"integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ=="
} }
} }
} }

View file

@ -26,6 +26,7 @@
"signal-exit": "3.0.7", "signal-exit": "3.0.7",
"socks-proxy-agent": "8.0.4", "socks-proxy-agent": "8.0.4",
"stack-utils": "2.0.5", "stack-utils": "2.0.5",
"yaml": "^2.5.1",
"ws": "8.17.1" "ws": "8.17.1"
}, },
"devDependencies": { "devDependencies": {

View file

@ -54,6 +54,9 @@ export { SocksProxyAgent } from 'socks-proxy-agent';
import StackUtilsLibrary from 'stack-utils'; import StackUtilsLibrary from 'stack-utils';
export const StackUtils = StackUtilsLibrary; export const StackUtils = StackUtilsLibrary;
import yamlLibrary from 'yaml';
export const yaml = yamlLibrary;
// @ts-ignore // @ts-ignore
import wsLibrary, { WebSocketServer, Receiver, Sender } from 'ws'; import wsLibrary, { WebSocketServer, Receiver, Sender } from 'ws';
export const ws = wsLibrary; export const ws = wsLibrary;

View file

@ -288,6 +288,11 @@ export class Locator implements api.Locator {
return await this._withElement((h, timeout) => h.screenshot({ ...options, timeout }), options.timeout); return await this._withElement((h, timeout) => h.screenshot({ ...options, timeout }), options.timeout);
} }
async ariaSnapshot(options?: TimeoutOptions): Promise<string> {
const result = await this._frame._channel.ariaSnapshot({ ...options, selector: this._selector });
return result.snapshot;
}
async scrollIntoViewIfNeeded(options: channels.ElementHandleScrollIntoViewIfNeededOptions = {}) { async scrollIntoViewIfNeeded(options: channels.ElementHandleScrollIntoViewIfNeededOptions = {}) {
return await this._withElement((h, timeout) => h.scrollIntoViewIfNeeded({ ...options, timeout }), options.timeout); return await this._withElement((h, timeout) => h.scrollIntoViewIfNeeded({ ...options, timeout }), options.timeout);
} }

View file

@ -1424,6 +1424,13 @@ scheme.FrameAddStyleTagParams = tObject({
scheme.FrameAddStyleTagResult = tObject({ scheme.FrameAddStyleTagResult = tObject({
element: tChannel(['ElementHandle']), element: tChannel(['ElementHandle']),
}); });
scheme.FrameAriaSnapshotParams = tObject({
selector: tString,
timeout: tOptional(tNumber),
});
scheme.FrameAriaSnapshotResult = tObject({
snapshot: tString,
});
scheme.FrameBlurParams = tObject({ scheme.FrameBlurParams = tObject({
selector: tString, selector: tString,
strict: tOptional(tBoolean), strict: tOptional(tBoolean),

View file

@ -0,0 +1,140 @@
/**
* 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 { AriaTemplateNode } from './injected/ariaSnapshot';
import { yaml } from '../utilsBundle';
import type { AriaRole } from '@injected/roleUtils';
import { assert } from '../utils';
export function parseAriaSnapshot(text: string): AriaTemplateNode {
const fragment = yaml.parse(text) as any[];
const result: AriaTemplateNode = { role: 'fragment' };
populateNode(result, fragment);
return result;
}
function populateNode(node: AriaTemplateNode, container: any[]) {
for (const object of container) {
if (typeof object === 'string') {
const childNode = parseKey(object);
node.children = node.children || [];
node.children.push(childNode);
continue;
}
for (const key of Object.keys(object)) {
const childNode = parseKey(key);
const value = object[key];
node.children = node.children || [];
if (childNode.role === 'text') {
node.children.push(valueOrRegex(value));
continue;
}
if (typeof value === 'string') {
node.children.push({ ...childNode, children: [valueOrRegex(value)] });
continue;
}
node.children.push(childNode);
populateNode(childNode, value);
}
}
}
function applyAttribute(node: AriaTemplateNode, key: string, value: string) {
if (key === 'checked') {
assert(value === 'true' || value === 'false' || value === 'mixed', 'Value of "disabled" attribute must be a boolean or "mixed"');
node.checked = value === 'true' ? true : value === 'false' ? false : 'mixed';
return;
}
if (key === 'disabled') {
assert(value === 'true' || value === 'false', 'Value of "disabled" attribute must be a boolean');
node.disabled = value === 'true';
return;
}
if (key === 'expanded') {
assert(value === 'true' || value === 'false', 'Value of "expanded" attribute must be a boolean');
node.expanded = value === 'true';
return;
}
if (key === 'level') {
assert(!isNaN(Number(value)), 'Value of "level" attribute must be a number');
node.level = Number(value);
return;
}
if (key === 'pressed') {
assert(value === 'true' || value === 'false' || value === 'mixed', 'Value of "pressed" attribute must be a boolean or "mixed"');
node.pressed = value === 'true' ? true : value === 'false' ? false : 'mixed';
return;
}
if (key === 'selected') {
assert(value === 'true' || value === 'false', 'Value of "selected" attribute must be a boolean');
node.selected = value === 'true';
return;
}
throw new Error(`Unsupported attribute [${key}] `);
}
function parseKey(key: string): AriaTemplateNode {
const tokenRegex = /\s*([a-z]+|"(?:[^"]*)"|\/(?:[^\/]*)\/|\[.*?\])/g;
let match;
const tokens = [];
while ((match = tokenRegex.exec(key)) !== null)
tokens.push(match[1]);
if (tokens.length === 0)
throw new Error(`Invalid key ${key}`);
const role = tokens[0] as AriaRole | 'text';
let name: string | RegExp = '';
let index = 1;
if (tokens.length > 1 && (tokens[1].startsWith('"') || tokens[1].startsWith('/'))) {
const nameToken = tokens[1];
if (nameToken.startsWith('"')) {
name = nameToken.slice(1, -1);
} else {
const pattern = nameToken.slice(1, -1);
name = new RegExp(pattern);
}
index = 2;
}
const result: AriaTemplateNode = { role, name };
for (; index < tokens.length; index++) {
const attrToken = tokens[index];
if (attrToken.startsWith('[') && attrToken.endsWith(']')) {
const attrContent = attrToken.slice(1, -1).trim();
const [attrName, attrValue] = attrContent.split('=', 2);
const value = attrValue !== undefined ? attrValue.trim() : 'true';
applyAttribute(result, attrName, value);
} else {
throw new Error(`Invalid attribute token ${attrToken} in key ${key}`);
}
}
return result;
}
function normalizeWhitespace(text: string) {
return text.replace(/[\r\n\s\t]+/g, ' ').trim();
}
function valueOrRegex(value: string): string | RegExp {
return value.startsWith('/') && value.endsWith('/') ? new RegExp(value.slice(1, -1)) : normalizeWhitespace(value);
}

View file

@ -695,7 +695,7 @@ percentage [0 - 100] for scroll driven animations
frameId: Page.FrameId; frameId: Page.FrameId;
} }
export type CookieExclusionReason = "ExcludeSameSiteUnspecifiedTreatedAsLax"|"ExcludeSameSiteNoneInsecure"|"ExcludeSameSiteLax"|"ExcludeSameSiteStrict"|"ExcludeInvalidSameParty"|"ExcludeSamePartyCrossPartyContext"|"ExcludeDomainNonASCII"|"ExcludeThirdPartyCookieBlockedInFirstPartySet"|"ExcludeThirdPartyPhaseout"; export type CookieExclusionReason = "ExcludeSameSiteUnspecifiedTreatedAsLax"|"ExcludeSameSiteNoneInsecure"|"ExcludeSameSiteLax"|"ExcludeSameSiteStrict"|"ExcludeInvalidSameParty"|"ExcludeSamePartyCrossPartyContext"|"ExcludeDomainNonASCII"|"ExcludeThirdPartyCookieBlockedInFirstPartySet"|"ExcludeThirdPartyPhaseout";
export type CookieWarningReason = "WarnSameSiteUnspecifiedCrossSiteContext"|"WarnSameSiteNoneInsecure"|"WarnSameSiteUnspecifiedLaxAllowUnsafe"|"WarnSameSiteStrictLaxDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeLax"|"WarnSameSiteLaxCrossDowngradeStrict"|"WarnSameSiteLaxCrossDowngradeLax"|"WarnAttributeValueExceedsMaxSize"|"WarnDomainNonASCII"|"WarnThirdPartyPhaseout"|"WarnCrossSiteRedirectDowngradeChangesInclusion"; export type CookieWarningReason = "WarnSameSiteUnspecifiedCrossSiteContext"|"WarnSameSiteNoneInsecure"|"WarnSameSiteUnspecifiedLaxAllowUnsafe"|"WarnSameSiteStrictLaxDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeLax"|"WarnSameSiteLaxCrossDowngradeStrict"|"WarnSameSiteLaxCrossDowngradeLax"|"WarnAttributeValueExceedsMaxSize"|"WarnDomainNonASCII"|"WarnThirdPartyPhaseout"|"WarnCrossSiteRedirectDowngradeChangesInclusion"|"WarnDeprecationTrialMetadata"|"WarnThirdPartyCookieHeuristic";
export type CookieOperation = "SetCookie"|"ReadCookie"; export type CookieOperation = "SetCookie"|"ReadCookie";
/** /**
* This information is currently necessary, as the front-end has a difficult * This information is currently necessary, as the front-end has a difficult
@ -934,7 +934,7 @@ Should be updated alongside RequestIdTokenStatus in
third_party/blink/public/mojom/devtools/inspector_issue.mojom to include third_party/blink/public/mojom/devtools/inspector_issue.mojom to include
all cases except for success. all cases except for success.
*/ */
export type FederatedAuthRequestIssueReason = "ShouldEmbargo"|"TooManyRequests"|"WellKnownHttpNotFound"|"WellKnownNoResponse"|"WellKnownInvalidResponse"|"WellKnownListEmpty"|"WellKnownInvalidContentType"|"ConfigNotInWellKnown"|"WellKnownTooBig"|"ConfigHttpNotFound"|"ConfigNoResponse"|"ConfigInvalidResponse"|"ConfigInvalidContentType"|"ClientMetadataHttpNotFound"|"ClientMetadataNoResponse"|"ClientMetadataInvalidResponse"|"ClientMetadataInvalidContentType"|"IdpNotPotentiallyTrustworthy"|"DisabledInSettings"|"DisabledInFlags"|"ErrorFetchingSignin"|"InvalidSigninResponse"|"AccountsHttpNotFound"|"AccountsNoResponse"|"AccountsInvalidResponse"|"AccountsListEmpty"|"AccountsInvalidContentType"|"IdTokenHttpNotFound"|"IdTokenNoResponse"|"IdTokenInvalidResponse"|"IdTokenIdpErrorResponse"|"IdTokenCrossSiteIdpErrorResponse"|"IdTokenInvalidRequest"|"IdTokenInvalidContentType"|"ErrorIdToken"|"Canceled"|"RpPageNotVisible"|"SilentMediationFailure"|"ThirdPartyCookiesBlocked"|"NotSignedInWithIdp"|"MissingTransientUserActivation"|"ReplacedByButtonMode"|"InvalidFieldsSpecified"|"RelyingPartyOriginIsOpaque"|"TypeNotMatching"; export type FederatedAuthRequestIssueReason = "ShouldEmbargo"|"TooManyRequests"|"WellKnownHttpNotFound"|"WellKnownNoResponse"|"WellKnownInvalidResponse"|"WellKnownListEmpty"|"WellKnownInvalidContentType"|"ConfigNotInWellKnown"|"WellKnownTooBig"|"ConfigHttpNotFound"|"ConfigNoResponse"|"ConfigInvalidResponse"|"ConfigInvalidContentType"|"ClientMetadataHttpNotFound"|"ClientMetadataNoResponse"|"ClientMetadataInvalidResponse"|"ClientMetadataInvalidContentType"|"IdpNotPotentiallyTrustworthy"|"DisabledInSettings"|"DisabledInFlags"|"ErrorFetchingSignin"|"InvalidSigninResponse"|"AccountsHttpNotFound"|"AccountsNoResponse"|"AccountsInvalidResponse"|"AccountsListEmpty"|"AccountsInvalidContentType"|"IdTokenHttpNotFound"|"IdTokenNoResponse"|"IdTokenInvalidResponse"|"IdTokenIdpErrorResponse"|"IdTokenCrossSiteIdpErrorResponse"|"IdTokenInvalidRequest"|"IdTokenInvalidContentType"|"ErrorIdToken"|"Canceled"|"RpPageNotVisible"|"SilentMediationFailure"|"ThirdPartyCookiesBlocked"|"NotSignedInWithIdp"|"MissingTransientUserActivation"|"ReplacedByActiveMode"|"InvalidFieldsSpecified"|"RelyingPartyOriginIsOpaque"|"TypeNotMatching";
export interface FederatedAuthUserInfoRequestIssueDetails { export interface FederatedAuthUserInfoRequestIssueDetails {
federatedAuthUserInfoRequestIssueReason: FederatedAuthUserInfoRequestIssueReason; federatedAuthUserInfoRequestIssueReason: FederatedAuthUserInfoRequestIssueReason;
} }
@ -5989,7 +5989,7 @@ Missing optional values will be filled in by the target with what it would norma
* Used to specify sensor types to emulate. * Used to specify sensor types to emulate.
See https://w3c.github.io/sensors/#automation for more information. See https://w3c.github.io/sensors/#automation for more information.
*/ */
export type SensorType = "absolute-orientation"|"accelerometer"|"ambient-light"|"gravity"|"gyroscope"|"linear-acceleration"|"magnetometer"|"proximity"|"relative-orientation"; export type SensorType = "absolute-orientation"|"accelerometer"|"ambient-light"|"gravity"|"gyroscope"|"linear-acceleration"|"magnetometer"|"relative-orientation";
export interface SensorMetadata { export interface SensorMetadata {
available?: boolean; available?: boolean;
minimumFrequency?: number; minimumFrequency?: number;
@ -11397,7 +11397,7 @@ Backend then generates 'inspectNodeRequested' event upon element selection.
export type setShowHitTestBordersReturnValue = { export type setShowHitTestBordersReturnValue = {
} }
/** /**
* Request that backend shows an overlay with web vital metrics. * Deprecated, no longer has any effect.
*/ */
export type setShowWebVitalsParameters = { export type setShowWebVitalsParameters = {
show: boolean; show: boolean;
@ -11498,7 +11498,7 @@ as an ad.
* All Permissions Policy features. This enum should match the one defined * All Permissions Policy features. This enum should match the one defined
in third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5. in third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5.
*/ */
export type PermissionsPolicyFeature = "accelerometer"|"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"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"media-playback-while-not-visible"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"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"|"digital-credentials-get"|"direct-sockets"|"direct-sockets-private"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"media-playback-while-not-visible"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"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. * Reason for a permissions policy feature to be disabled.
*/ */
@ -12086,7 +12086,7 @@ https://github.com/WICG/manifest-incubations/blob/gh-pages/scope_extensions-expl
/** /**
* List of not restored reasons for back-forward cache. * List of not restored reasons for back-forward cache.
*/ */
export type BackForwardCacheNotRestoredReason = "NotPrimaryMainFrame"|"BackForwardCacheDisabled"|"RelatedActiveContentsExist"|"HTTPStatusNotOK"|"SchemeNotHTTPOrHTTPS"|"Loading"|"WasGrantedMediaAccess"|"DisableForRenderFrameHostCalled"|"DomainNotAllowed"|"HTTPMethodNotGET"|"SubframeIsNavigating"|"Timeout"|"CacheLimit"|"JavaScriptExecution"|"RendererProcessKilled"|"RendererProcessCrashed"|"SchedulerTrackedFeatureUsed"|"ConflictingBrowsingInstance"|"CacheFlushed"|"ServiceWorkerVersionActivation"|"SessionRestored"|"ServiceWorkerPostMessage"|"EnteredBackForwardCacheBeforeServiceWorkerHostAdded"|"RenderFrameHostReused_SameSite"|"RenderFrameHostReused_CrossSite"|"ServiceWorkerClaim"|"IgnoreEventAndEvict"|"HaveInnerContents"|"TimeoutPuttingInCache"|"BackForwardCacheDisabledByLowMemory"|"BackForwardCacheDisabledByCommandLine"|"NetworkRequestDatapipeDrainedAsBytesConsumer"|"NetworkRequestRedirected"|"NetworkRequestTimeout"|"NetworkExceedsBufferLimit"|"NavigationCancelledWhileRestoring"|"NotMostRecentNavigationEntry"|"BackForwardCacheDisabledForPrerender"|"UserAgentOverrideDiffers"|"ForegroundCacheLimit"|"BrowsingInstanceNotSwapped"|"BackForwardCacheDisabledForDelegate"|"UnloadHandlerExistsInMainFrame"|"UnloadHandlerExistsInSubFrame"|"ServiceWorkerUnregistration"|"CacheControlNoStore"|"CacheControlNoStoreCookieModified"|"CacheControlNoStoreHTTPOnlyCookieModified"|"NoResponseHead"|"Unknown"|"ActivationNavigationsDisallowedForBug1234857"|"ErrorDocument"|"FencedFramesEmbedder"|"CookieDisabled"|"HTTPAuthRequired"|"CookieFlushed"|"BroadcastChannelOnMessage"|"WebViewSettingsChanged"|"WebViewJavaScriptObjectChanged"|"WebViewMessageListenerInjected"|"WebViewSafeBrowsingAllowlistChanged"|"WebViewDocumentStartJavascriptChanged"|"WebSocket"|"WebTransport"|"WebRTC"|"MainResourceHasCacheControlNoStore"|"MainResourceHasCacheControlNoCache"|"SubresourceHasCacheControlNoStore"|"SubresourceHasCacheControlNoCache"|"ContainsPlugins"|"DocumentLoaded"|"OutstandingNetworkRequestOthers"|"RequestedMIDIPermission"|"RequestedAudioCapturePermission"|"RequestedVideoCapturePermission"|"RequestedBackForwardCacheBlockedSensors"|"RequestedBackgroundWorkPermission"|"BroadcastChannel"|"WebXR"|"SharedWorker"|"WebLocks"|"WebHID"|"WebShare"|"RequestedStorageAccessGrant"|"WebNfc"|"OutstandingNetworkRequestFetch"|"OutstandingNetworkRequestXHR"|"AppBanner"|"Printing"|"WebDatabase"|"PictureInPicture"|"SpeechRecognizer"|"IdleManager"|"PaymentManager"|"SpeechSynthesis"|"KeyboardLock"|"WebOTPService"|"OutstandingNetworkRequestDirectSocket"|"InjectedJavascript"|"InjectedStyleSheet"|"KeepaliveRequest"|"IndexedDBEvent"|"Dummy"|"JsNetworkRequestReceivedCacheControlNoStoreResource"|"WebRTCSticky"|"WebTransportSticky"|"WebSocketSticky"|"SmartCard"|"LiveMediaStreamTrack"|"UnloadHandler"|"ParserAborted"|"ContentSecurityHandler"|"ContentWebAuthenticationAPI"|"ContentFileChooser"|"ContentSerial"|"ContentFileSystemAccess"|"ContentMediaDevicesDispatcherHost"|"ContentWebBluetooth"|"ContentWebUSB"|"ContentMediaSessionService"|"ContentScreenReader"|"ContentDiscarded"|"EmbedderPopupBlockerTabHelper"|"EmbedderSafeBrowsingTriggeredPopupBlocker"|"EmbedderSafeBrowsingThreatDetails"|"EmbedderAppBannerManager"|"EmbedderDomDistillerViewerSource"|"EmbedderDomDistillerSelfDeletingRequestDelegate"|"EmbedderOomInterventionTabHelper"|"EmbedderOfflinePage"|"EmbedderChromePasswordManagerClientBindCredentialManager"|"EmbedderPermissionRequestManager"|"EmbedderModalDialog"|"EmbedderExtensions"|"EmbedderExtensionMessaging"|"EmbedderExtensionMessagingForOpenPort"|"EmbedderExtensionSentMessageToCachedFrame"|"RequestedByWebViewClient"; export type BackForwardCacheNotRestoredReason = "NotPrimaryMainFrame"|"BackForwardCacheDisabled"|"RelatedActiveContentsExist"|"HTTPStatusNotOK"|"SchemeNotHTTPOrHTTPS"|"Loading"|"WasGrantedMediaAccess"|"DisableForRenderFrameHostCalled"|"DomainNotAllowed"|"HTTPMethodNotGET"|"SubframeIsNavigating"|"Timeout"|"CacheLimit"|"JavaScriptExecution"|"RendererProcessKilled"|"RendererProcessCrashed"|"SchedulerTrackedFeatureUsed"|"ConflictingBrowsingInstance"|"CacheFlushed"|"ServiceWorkerVersionActivation"|"SessionRestored"|"ServiceWorkerPostMessage"|"EnteredBackForwardCacheBeforeServiceWorkerHostAdded"|"RenderFrameHostReused_SameSite"|"RenderFrameHostReused_CrossSite"|"ServiceWorkerClaim"|"IgnoreEventAndEvict"|"HaveInnerContents"|"TimeoutPuttingInCache"|"BackForwardCacheDisabledByLowMemory"|"BackForwardCacheDisabledByCommandLine"|"NetworkRequestDatapipeDrainedAsBytesConsumer"|"NetworkRequestRedirected"|"NetworkRequestTimeout"|"NetworkExceedsBufferLimit"|"NavigationCancelledWhileRestoring"|"NotMostRecentNavigationEntry"|"BackForwardCacheDisabledForPrerender"|"UserAgentOverrideDiffers"|"ForegroundCacheLimit"|"BrowsingInstanceNotSwapped"|"BackForwardCacheDisabledForDelegate"|"UnloadHandlerExistsInMainFrame"|"UnloadHandlerExistsInSubFrame"|"ServiceWorkerUnregistration"|"CacheControlNoStore"|"CacheControlNoStoreCookieModified"|"CacheControlNoStoreHTTPOnlyCookieModified"|"NoResponseHead"|"Unknown"|"ActivationNavigationsDisallowedForBug1234857"|"ErrorDocument"|"FencedFramesEmbedder"|"CookieDisabled"|"HTTPAuthRequired"|"CookieFlushed"|"BroadcastChannelOnMessage"|"WebViewSettingsChanged"|"WebViewJavaScriptObjectChanged"|"WebViewMessageListenerInjected"|"WebViewSafeBrowsingAllowlistChanged"|"WebViewDocumentStartJavascriptChanged"|"WebSocket"|"WebTransport"|"WebRTC"|"MainResourceHasCacheControlNoStore"|"MainResourceHasCacheControlNoCache"|"SubresourceHasCacheControlNoStore"|"SubresourceHasCacheControlNoCache"|"ContainsPlugins"|"DocumentLoaded"|"OutstandingNetworkRequestOthers"|"RequestedMIDIPermission"|"RequestedAudioCapturePermission"|"RequestedVideoCapturePermission"|"RequestedBackForwardCacheBlockedSensors"|"RequestedBackgroundWorkPermission"|"BroadcastChannel"|"WebXR"|"SharedWorker"|"WebLocks"|"WebHID"|"WebShare"|"RequestedStorageAccessGrant"|"WebNfc"|"OutstandingNetworkRequestFetch"|"OutstandingNetworkRequestXHR"|"AppBanner"|"Printing"|"WebDatabase"|"PictureInPicture"|"SpeechRecognizer"|"IdleManager"|"PaymentManager"|"SpeechSynthesis"|"KeyboardLock"|"WebOTPService"|"OutstandingNetworkRequestDirectSocket"|"InjectedJavascript"|"InjectedStyleSheet"|"KeepaliveRequest"|"IndexedDBEvent"|"Dummy"|"JsNetworkRequestReceivedCacheControlNoStoreResource"|"WebRTCSticky"|"WebTransportSticky"|"WebSocketSticky"|"SmartCard"|"LiveMediaStreamTrack"|"UnloadHandler"|"ParserAborted"|"ContentSecurityHandler"|"ContentWebAuthenticationAPI"|"ContentFileChooser"|"ContentSerial"|"ContentFileSystemAccess"|"ContentMediaDevicesDispatcherHost"|"ContentWebBluetooth"|"ContentWebUSB"|"ContentMediaSessionService"|"ContentScreenReader"|"ContentDiscarded"|"EmbedderPopupBlockerTabHelper"|"EmbedderSafeBrowsingTriggeredPopupBlocker"|"EmbedderSafeBrowsingThreatDetails"|"EmbedderAppBannerManager"|"EmbedderDomDistillerViewerSource"|"EmbedderDomDistillerSelfDeletingRequestDelegate"|"EmbedderOomInterventionTabHelper"|"EmbedderOfflinePage"|"EmbedderChromePasswordManagerClientBindCredentialManager"|"EmbedderPermissionRequestManager"|"EmbedderModalDialog"|"EmbedderExtensions"|"EmbedderExtensionMessaging"|"EmbedderExtensionMessagingForOpenPort"|"EmbedderExtensionSentMessageToCachedFrame"|"RequestedByWebViewClient"|"PostMessageByWebViewClient";
/** /**
* Types of not restored reasons for back-forward cache. * Types of not restored reasons for back-forward cache.
*/ */
@ -16634,6 +16634,17 @@ flag set to this value. Defaults to the authenticator's
defaultBackupState value. defaultBackupState value.
*/ */
backupState?: boolean; backupState?: boolean;
/**
* The credential's user.name property. Equivalent to empty if not set.
https://w3c.github.io/webauthn/#dom-publickeycredentialentity-name
*/
userName?: string;
/**
* The credential's user.displayName property. Equivalent to empty if
not set.
https://w3c.github.io/webauthn/#dom-publickeycredentialuserentity-displayname
*/
userDisplayName?: string;
} }
/** /**
@ -16643,6 +16654,22 @@ defaultBackupState value.
authenticatorId: AuthenticatorId; authenticatorId: AuthenticatorId;
credential: Credential; credential: Credential;
} }
/**
* Triggered when a credential is deleted, e.g. through
PublicKeyCredential.signalUnknownCredential().
*/
export type credentialDeletedPayload = {
authenticatorId: AuthenticatorId;
credentialId: binary;
}
/**
* Triggered when a credential is updated, e.g. through
PublicKeyCredential.signalCurrentUserDetails().
*/
export type credentialUpdatedPayload = {
authenticatorId: AuthenticatorId;
credential: Credential;
}
/** /**
* Triggered when a credential is used in a webauthn assertion. * Triggered when a credential is used in a webauthn assertion.
*/ */
@ -17076,7 +17103,7 @@ possible for multiple rule sets and links to trigger a single attempt.
/** /**
* List of FinalStatus reasons for Prerender2. * List of FinalStatus reasons for Prerender2.
*/ */
export type PrerenderFinalStatus = "Activated"|"Destroyed"|"LowEndDevice"|"InvalidSchemeRedirect"|"InvalidSchemeNavigation"|"NavigationRequestBlockedByCsp"|"MainFrameNavigation"|"MojoBinderPolicy"|"RendererProcessCrashed"|"RendererProcessKilled"|"Download"|"TriggerDestroyed"|"NavigationNotCommitted"|"NavigationBadHttpStatus"|"ClientCertRequested"|"NavigationRequestNetworkError"|"CancelAllHostsForTesting"|"DidFailLoad"|"Stop"|"SslCertificateError"|"LoginAuthRequested"|"UaChangeRequiresReload"|"BlockedByClient"|"AudioOutputDeviceRequested"|"MixedContent"|"TriggerBackgrounded"|"MemoryLimitExceeded"|"DataSaverEnabled"|"TriggerUrlHasEffectiveUrl"|"ActivatedBeforeStarted"|"InactivePageRestriction"|"StartFailed"|"TimeoutBackgrounded"|"CrossSiteRedirectInInitialNavigation"|"CrossSiteNavigationInInitialNavigation"|"SameSiteCrossOriginRedirectNotOptInInInitialNavigation"|"SameSiteCrossOriginNavigationNotOptInInInitialNavigation"|"ActivationNavigationParameterMismatch"|"ActivatedInBackground"|"EmbedderHostDisallowed"|"ActivationNavigationDestroyedBeforeSuccess"|"TabClosedByUserGesture"|"TabClosedWithoutUserGesture"|"PrimaryMainFrameRendererProcessCrashed"|"PrimaryMainFrameRendererProcessKilled"|"ActivationFramePolicyNotCompatible"|"PreloadingDisabled"|"BatterySaverEnabled"|"ActivatedDuringMainFrameNavigation"|"PreloadingUnsupportedByWebContents"|"CrossSiteRedirectInMainFrameNavigation"|"CrossSiteNavigationInMainFrameNavigation"|"SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation"|"SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation"|"MemoryPressureOnTrigger"|"MemoryPressureAfterTriggered"|"PrerenderingDisabledByDevTools"|"SpeculationRuleRemoved"|"ActivatedWithAuxiliaryBrowsingContexts"|"MaxNumOfRunningEagerPrerendersExceeded"|"MaxNumOfRunningNonEagerPrerendersExceeded"|"MaxNumOfRunningEmbedderPrerendersExceeded"|"PrerenderingUrlHasEffectiveUrl"|"RedirectedPrerenderingUrlHasEffectiveUrl"|"ActivationUrlHasEffectiveUrl"|"JavaScriptInterfaceAdded"|"JavaScriptInterfaceRemoved"|"AllPrerenderingCanceled"|"WindowClosed"|"SlowNetwork"|"OtherPrerenderedPageActivated"; export type PrerenderFinalStatus = "Activated"|"Destroyed"|"LowEndDevice"|"InvalidSchemeRedirect"|"InvalidSchemeNavigation"|"NavigationRequestBlockedByCsp"|"MainFrameNavigation"|"MojoBinderPolicy"|"RendererProcessCrashed"|"RendererProcessKilled"|"Download"|"TriggerDestroyed"|"NavigationNotCommitted"|"NavigationBadHttpStatus"|"ClientCertRequested"|"NavigationRequestNetworkError"|"CancelAllHostsForTesting"|"DidFailLoad"|"Stop"|"SslCertificateError"|"LoginAuthRequested"|"UaChangeRequiresReload"|"BlockedByClient"|"AudioOutputDeviceRequested"|"MixedContent"|"TriggerBackgrounded"|"MemoryLimitExceeded"|"DataSaverEnabled"|"TriggerUrlHasEffectiveUrl"|"ActivatedBeforeStarted"|"InactivePageRestriction"|"StartFailed"|"TimeoutBackgrounded"|"CrossSiteRedirectInInitialNavigation"|"CrossSiteNavigationInInitialNavigation"|"SameSiteCrossOriginRedirectNotOptInInInitialNavigation"|"SameSiteCrossOriginNavigationNotOptInInInitialNavigation"|"ActivationNavigationParameterMismatch"|"ActivatedInBackground"|"EmbedderHostDisallowed"|"ActivationNavigationDestroyedBeforeSuccess"|"TabClosedByUserGesture"|"TabClosedWithoutUserGesture"|"PrimaryMainFrameRendererProcessCrashed"|"PrimaryMainFrameRendererProcessKilled"|"ActivationFramePolicyNotCompatible"|"PreloadingDisabled"|"BatterySaverEnabled"|"ActivatedDuringMainFrameNavigation"|"PreloadingUnsupportedByWebContents"|"CrossSiteRedirectInMainFrameNavigation"|"CrossSiteNavigationInMainFrameNavigation"|"SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation"|"SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation"|"MemoryPressureOnTrigger"|"MemoryPressureAfterTriggered"|"PrerenderingDisabledByDevTools"|"SpeculationRuleRemoved"|"ActivatedWithAuxiliaryBrowsingContexts"|"MaxNumOfRunningEagerPrerendersExceeded"|"MaxNumOfRunningNonEagerPrerendersExceeded"|"MaxNumOfRunningEmbedderPrerendersExceeded"|"PrerenderingUrlHasEffectiveUrl"|"RedirectedPrerenderingUrlHasEffectiveUrl"|"ActivationUrlHasEffectiveUrl"|"JavaScriptInterfaceAdded"|"JavaScriptInterfaceRemoved"|"AllPrerenderingCanceled"|"WindowClosed"|"SlowNetwork"|"OtherPrerenderedPageActivated"|"V8OptimizerDisabled"|"PrerenderFailedDuringPrefetch";
/** /**
* Preloading status values, see also PreloadingTriggeringOutcome. This * Preloading status values, see also PreloadingTriggeringOutcome. This
status is shared by prefetchStatusUpdated and prerenderStatusUpdated. status is shared by prefetchStatusUpdated and prerenderStatusUpdated.
@ -17751,7 +17778,7 @@ variables as its properties.
/** /**
* Type of the debug symbols. * Type of the debug symbols.
*/ */
type: "None"|"SourceMap"|"EmbeddedDWARF"|"ExternalDWARF"; type: "SourceMap"|"EmbeddedDWARF"|"ExternalDWARF";
/** /**
* URL of the external symbol source. * URL of the external symbol source.
*/ */
@ -17955,9 +17982,9 @@ scripts upon enabling debugger.
*/ */
scriptLanguage?: Debugger.ScriptLanguage; scriptLanguage?: Debugger.ScriptLanguage;
/** /**
* If the scriptLanguage is WebASsembly, the source of debug symbols for the module. * If the scriptLanguage is WebAssembly, the source of debug symbols for the module.
*/ */
debugSymbols?: Debugger.DebugSymbols; debugSymbols?: Debugger.DebugSymbols[];
/** /**
* The name the embedder supplied for this script. * The name the embedder supplied for this script.
*/ */
@ -18280,6 +18307,19 @@ call stacks (default).
} }
export type setAsyncCallStackDepthReturnValue = { export type setAsyncCallStackDepthReturnValue = {
} }
/**
* Replace previous blackbox execution contexts with passed ones. Forces backend to skip
stepping/pausing in scripts in these execution contexts. VM will try to leave blackboxed script by
performing 'step in' several times, finally resorting to 'step out' if unsuccessful.
*/
export type setBlackboxExecutionContextsParameters = {
/**
* Array of execution context unique ids for the debugger to ignore.
*/
uniqueIds: string[];
}
export type setBlackboxExecutionContextsReturnValue = {
}
/** /**
* Replace previous blackbox patterns with passed ones. Forces backend to skip stepping/pausing in * Replace previous blackbox patterns with passed ones. Forces backend to skip stepping/pausing in
scripts with url matching one of the patterns. VM will try to leave blackboxed script by scripts with url matching one of the patterns. VM will try to leave blackboxed script by
@ -18290,6 +18330,10 @@ performing 'step in' several times, finally resorting to 'step out' if unsuccess
* Array of regexps that will be used to check script url for blackbox state. * Array of regexps that will be used to check script url for blackbox state.
*/ */
patterns: string[]; patterns: string[];
/**
* If true, also ignore scripts with no source url.
*/
skipAnonymous?: boolean;
} }
export type setBlackboxPatternsReturnValue = { export type setBlackboxPatternsReturnValue = {
} }
@ -20310,6 +20354,8 @@ Error was thrown.
"WebAudio.nodeParamConnected": WebAudio.nodeParamConnectedPayload; "WebAudio.nodeParamConnected": WebAudio.nodeParamConnectedPayload;
"WebAudio.nodeParamDisconnected": WebAudio.nodeParamDisconnectedPayload; "WebAudio.nodeParamDisconnected": WebAudio.nodeParamDisconnectedPayload;
"WebAuthn.credentialAdded": WebAuthn.credentialAddedPayload; "WebAuthn.credentialAdded": WebAuthn.credentialAddedPayload;
"WebAuthn.credentialDeleted": WebAuthn.credentialDeletedPayload;
"WebAuthn.credentialUpdated": WebAuthn.credentialUpdatedPayload;
"WebAuthn.credentialAsserted": WebAuthn.credentialAssertedPayload; "WebAuthn.credentialAsserted": WebAuthn.credentialAssertedPayload;
"Media.playerPropertiesChanged": Media.playerPropertiesChangedPayload; "Media.playerPropertiesChanged": Media.playerPropertiesChangedPayload;
"Media.playerEventsAdded": Media.playerEventsAddedPayload; "Media.playerEventsAdded": Media.playerEventsAddedPayload;
@ -20897,6 +20943,7 @@ Error was thrown.
"Debugger.resume": Debugger.resumeParameters; "Debugger.resume": Debugger.resumeParameters;
"Debugger.searchInContent": Debugger.searchInContentParameters; "Debugger.searchInContent": Debugger.searchInContentParameters;
"Debugger.setAsyncCallStackDepth": Debugger.setAsyncCallStackDepthParameters; "Debugger.setAsyncCallStackDepth": Debugger.setAsyncCallStackDepthParameters;
"Debugger.setBlackboxExecutionContexts": Debugger.setBlackboxExecutionContextsParameters;
"Debugger.setBlackboxPatterns": Debugger.setBlackboxPatternsParameters; "Debugger.setBlackboxPatterns": Debugger.setBlackboxPatternsParameters;
"Debugger.setBlackboxedRanges": Debugger.setBlackboxedRangesParameters; "Debugger.setBlackboxedRanges": Debugger.setBlackboxedRangesParameters;
"Debugger.setBreakpoint": Debugger.setBreakpointParameters; "Debugger.setBreakpoint": Debugger.setBreakpointParameters;
@ -21507,6 +21554,7 @@ Error was thrown.
"Debugger.resume": Debugger.resumeReturnValue; "Debugger.resume": Debugger.resumeReturnValue;
"Debugger.searchInContent": Debugger.searchInContentReturnValue; "Debugger.searchInContent": Debugger.searchInContentReturnValue;
"Debugger.setAsyncCallStackDepth": Debugger.setAsyncCallStackDepthReturnValue; "Debugger.setAsyncCallStackDepth": Debugger.setAsyncCallStackDepthReturnValue;
"Debugger.setBlackboxExecutionContexts": Debugger.setBlackboxExecutionContextsReturnValue;
"Debugger.setBlackboxPatterns": Debugger.setBlackboxPatternsReturnValue; "Debugger.setBlackboxPatterns": Debugger.setBlackboxPatternsReturnValue;
"Debugger.setBlackboxedRanges": Debugger.setBlackboxedRangesReturnValue; "Debugger.setBlackboxedRanges": Debugger.setBlackboxedRangesReturnValue;
"Debugger.setBreakpoint": Debugger.setBreakpointReturnValue; "Debugger.setBreakpoint": Debugger.setBreakpointReturnValue;

View file

@ -146,6 +146,8 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
const assertion = action.value ? `ToHaveValueAsync(${quote(action.value)})` : `ToBeEmptyAsync()`; const assertion = action.value ? `ToHaveValueAsync(${quote(action.value)})` : `ToBeEmptyAsync()`;
return `await Expect(${subject}.${this._asLocator(action.selector)}).${assertion};`; return `await Expect(${subject}.${this._asLocator(action.selector)}).${assertion};`;
} }
case 'assertSnapshot':
return `await Expect(${subject}.${this._asLocator(action.selector)}).ToMatchAriaSnapshotAsync(${quote(action.snapshot)});`;
} }
} }

View file

@ -133,6 +133,8 @@ export class JavaLanguageGenerator implements LanguageGenerator {
const assertion = action.value ? `hasValue(${quote(action.value)})` : `isEmpty()`; const assertion = action.value ? `hasValue(${quote(action.value)})` : `isEmpty()`;
return `assertThat(${subject}.${this._asLocator(action.selector, inFrameLocator)}).${assertion};`; return `assertThat(${subject}.${this._asLocator(action.selector, inFrameLocator)}).${assertion};`;
} }
case 'assertSnapshot':
return `assertThat(${subject}.${this._asLocator(action.selector, inFrameLocator)}).matchesAriaSnapshot(${quote(action.snapshot)});`;
} }
} }

View file

@ -117,6 +117,8 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
const assertion = action.value ? `toHaveValue(${quote(action.value)})` : `toBeEmpty()`; const assertion = action.value ? `toHaveValue(${quote(action.value)})` : `toBeEmpty()`;
return `${this._isTest ? '' : '// '}await expect(${subject}.${this._asLocator(action.selector)}).${assertion};`; return `${this._isTest ? '' : '// '}await expect(${subject}.${this._asLocator(action.selector)}).${assertion};`;
} }
case 'assertSnapshot':
return `${this._isTest ? '' : '// '}await expect(${subject}.${this._asLocator(action.selector)}).toMatchAriaSnapshot(${quoteMultiline(action.snapshot)});`;
} }
} }
@ -228,11 +230,13 @@ export class JavaScriptFormatter {
} }
prepend(text: string) { prepend(text: string) {
this._lines = text.trim().split('\n').map(line => line.trim()).concat(this._lines); const trim = isMultilineString(text) ? (line: string) => line : (line: string) => line.trim();
this._lines = text.trim().split('\n').map(trim).concat(this._lines);
} }
add(text: string) { add(text: string) {
this._lines.push(...text.trim().split('\n').map(line => line.trim())); const trim = isMultilineString(text) ? (line: string) => line : (line: string) => line.trim();
this._lines.push(...text.trim().split('\n').map(trim));
} }
newLine() { newLine() {
@ -269,3 +273,14 @@ function wrapWithStep(description: string | undefined, body: string) {
${body} ${body}
});` : body; });` : body;
} }
export function quoteMultiline(text: string, indent = ' ') {
const lines = text.split('\n');
if (lines.length === 1)
return '`' + text.replace(/`/g, '\\`').replace(/\${/g, '\\${') + '`';
return '`\n' + lines.map(line => indent + line.replace(/`/g, '\\`').replace(/\${/g, '\\${')).join('\n') + `\n${indent}\``;
}
function isMultilineString(text: string) {
return text.match(/`[\S\s]*`/)?.[0].includes('\n');
}

View file

@ -126,6 +126,8 @@ export class PythonLanguageGenerator implements LanguageGenerator {
const assertion = action.value ? `to_have_value(${quote(action.value)})` : `to_be_empty()`; const assertion = action.value ? `to_have_value(${quote(action.value)})` : `to_be_empty()`;
return `expect(${subject}.${this._asLocator(action.selector)}).${assertion};`; return `expect(${subject}.${this._asLocator(action.selector)}).${assertion};`;
} }
case 'assertSnapshot':
return `expect(${subject}.${this._asLocator(action.selector)}).to_match_aria_snapshot(${quote(action.snapshot)})`;
} }
} }

View file

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

View file

@ -26,6 +26,7 @@ import type { CallMetadata } from '../instrumentation';
import type { BrowserContextDispatcher } from './browserContextDispatcher'; import type { BrowserContextDispatcher } from './browserContextDispatcher';
import type { PageDispatcher } from './pageDispatcher'; import type { PageDispatcher } from './pageDispatcher';
import { debugAssert } from '../../utils'; import { debugAssert } from '../../utils';
import { parseAriaSnapshot } from '../ariaSnapshot';
export class FrameDispatcher extends Dispatcher<Frame, channels.FrameChannel, BrowserContextDispatcher | PageDispatcher> implements channels.FrameChannel { export class FrameDispatcher extends Dispatcher<Frame, channels.FrameChannel, BrowserContextDispatcher | PageDispatcher> implements channels.FrameChannel {
_type_Frame = true; _type_Frame = true;
@ -258,10 +259,16 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameChannel, Br
async expect(params: channels.FrameExpectParams, metadata: CallMetadata): Promise<channels.FrameExpectResult> { async expect(params: channels.FrameExpectParams, metadata: CallMetadata): Promise<channels.FrameExpectResult> {
metadata.potentiallyClosesScope = true; metadata.potentiallyClosesScope = true;
const expectedValue = params.expectedValue ? parseArgument(params.expectedValue) : undefined; let expectedValue = params.expectedValue ? parseArgument(params.expectedValue) : undefined;
if (params.expression === 'to.match.aria' && expectedValue)
expectedValue = parseAriaSnapshot(expectedValue);
const result = await this._frame.expect(metadata, params.selector, { ...params, expectedValue }); const result = await this._frame.expect(metadata, params.selector, { ...params, expectedValue });
if (result.received !== undefined) if (result.received !== undefined)
result.received = serializeResult(result.received); result.received = serializeResult(result.received);
return result; return result;
} }
async ariaSnapshot(params: channels.FrameAriaSnapshotParams, metadata: CallMetadata): Promise<channels.FrameAriaSnapshotResult> {
return { snapshot: await this._frame.ariaSnapshot(metadata, params.selector, params) };
}
} }

View file

@ -789,6 +789,10 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return this._page._delegate.getBoundingBox(this); return this._page._delegate.getBoundingBox(this);
} }
async ariaSnapshot(): Promise<string> {
return await this.evaluateInUtility(([injected, element]) => injected.ariaSnapshot(element), {});
}
async screenshot(metadata: CallMetadata, options: ScreenshotOptions & TimeoutOptions = {}): Promise<Buffer> { async screenshot(metadata: CallMetadata, options: ScreenshotOptions & TimeoutOptions = {}): Promise<Buffer> {
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
return controller.run( return controller.run(

View file

@ -183,7 +183,7 @@ const causeToResourceType: {[key: string]: string} = {
TYPE_XSLT: 'other', TYPE_XSLT: 'other',
TYPE_BEACON: 'other', TYPE_BEACON: 'other',
TYPE_FETCH: 'fetch', TYPE_FETCH: 'fetch',
TYPE_IMAGESET: 'images', TYPE_IMAGESET: 'image',
TYPE_WEB_MANIFEST: 'manifest', TYPE_WEB_MANIFEST: 'manifest',
}; };

View file

@ -1405,6 +1405,13 @@ export class Frame extends SdkObject {
}); });
} }
async ariaSnapshot(metadata: CallMetadata, selector: string, options: types.TimeoutOptions = {}): Promise<string> {
const controller = new ProgressController(metadata, this);
return controller.run(async progress => {
return await this._retryWithProgressIfNotConnected(progress, selector, true /* strict */, true /* performActionPreChecks */, handle => handle.ariaSnapshot());
}, this._page._timeoutSettings.timeout(options));
}
async expect(metadata: CallMetadata, selector: string, options: FrameExpectParams): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }> { async expect(metadata: CallMetadata, selector: string, options: FrameExpectParams): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }> {
const result = await this._expectImpl(metadata, selector, options); const result = await this._expectImpl(metadata, selector, options);
// Library mode special case for the expect errors which are return values, not exceptions. // Library mode special case for the expect errors which are return values, not exceptions.

View file

@ -15,39 +15,37 @@
*/ */
import { escapeWithQuotes } from '@isomorphic/stringUtils'; import { escapeWithQuotes } from '@isomorphic/stringUtils';
import { beginAriaCaches, endAriaCaches, getAriaRole, getElementAccessibleName, isElementIgnoredForAria } from './roleUtils'; import * as roleUtils from './roleUtils';
import { isElementVisible, isElementStyleVisibilityVisible } from './domUtils'; import { isElementVisible, isElementStyleVisibilityVisible, getElementComputedStyle } from './domUtils';
import type { AriaRole } from './roleUtils';
type AriaNode = { type AriaProps = {
role: string; checked?: boolean | 'mixed';
name?: string; disabled?: boolean;
children?: (AriaNode | string)[]; expanded?: boolean;
level?: number;
pressed?: boolean | 'mixed';
selected?: boolean;
}; };
export type AriaTemplateNode = { type AriaNode = AriaProps & {
role: string; role: AriaRole | 'fragment' | 'text';
name: string;
children: (AriaNode | string)[];
};
export type AriaTemplateNode = AriaProps & {
role: AriaRole | 'fragment' | 'text';
name?: RegExp | string; name?: RegExp | string;
children?: (AriaTemplateNode | string | RegExp)[]; children?: (AriaTemplateNode | string | RegExp)[];
}; };
export function generateAriaTree(rootElement: Element): AriaNode { export function generateAriaTree(rootElement: Element): AriaNode {
const toAriaNode = (element: Element): { ariaNode: AriaNode, isLeaf: boolean } | null => {
const role = getAriaRole(element);
if (!role)
return null;
const name = role ? getElementAccessibleName(element, false) || undefined : undefined;
const isLeaf = leafRoles.has(role);
const result: AriaNode = { role, name };
if (isLeaf && !name && element.textContent)
result.children = [element.textContent];
return { isLeaf, ariaNode: result };
};
const visit = (ariaNode: AriaNode, node: Node) => { const visit = (ariaNode: AriaNode, node: Node) => {
if (node.nodeType === Node.TEXT_NODE && node.nodeValue) { if (node.nodeType === Node.TEXT_NODE && node.nodeValue) {
ariaNode.children = ariaNode.children || []; const text = node.nodeValue;
ariaNode.children.push(node.nodeValue); if (text)
ariaNode.children.push(node.nodeValue || '');
return; return;
} }
@ -55,7 +53,7 @@ export function generateAriaTree(rootElement: Element): AriaNode {
return; return;
const element = node as Element; const element = node as Element;
if (isElementIgnoredForAria(element)) if (roleUtils.isElementIgnoredForAria(element))
return; return;
const visible = isElementVisible(element); const visible = isElementVisible(element);
@ -67,10 +65,8 @@ export function generateAriaTree(rootElement: Element): AriaNode {
if (visible) { if (visible) {
const childAriaNode = toAriaNode(element); const childAriaNode = toAriaNode(element);
const isHiddenContainer = childAriaNode && hiddenContainerRoles.has(childAriaNode.ariaNode.role); const isHiddenContainer = childAriaNode && hiddenContainerRoles.has(childAriaNode.ariaNode.role);
if (childAriaNode && !isHiddenContainer) { if (childAriaNode && !isHiddenContainer)
ariaNode.children = ariaNode.children || [];
ariaNode.children.push(childAriaNode.ariaNode); ariaNode.children.push(childAriaNode.ariaNode);
}
if (isHiddenContainer || !childAriaNode?.isLeaf) if (isHiddenContainer || !childAriaNode?.isLeaf)
processChildNodes(childAriaNode?.ariaNode || ariaNode, element); processChildNodes(childAriaNode?.ariaNode || ariaNode, element);
} else { } else {
@ -79,29 +75,81 @@ export function generateAriaTree(rootElement: Element): AriaNode {
}; };
function processChildNodes(ariaNode: AriaNode, element: Element) { function processChildNodes(ariaNode: AriaNode, element: Element) {
// Process light DOM children // Surround every element with spaces for the sake of concatenated text nodes.
for (let child = element.firstChild; child; child = child.nextSibling) const display = getElementComputedStyle(element)?.display || 'inline';
const treatAsBlock = (display !== 'inline' || element.nodeName === 'BR') ? ' ' : '';
if (treatAsBlock)
ariaNode.children.push(treatAsBlock);
ariaNode.children.push(roleUtils.getPseudoContent(element, '::before'));
const assignedNodes = element.nodeName === 'SLOT' ? (element as HTMLSlotElement).assignedNodes() : [];
if (assignedNodes.length) {
for (const child of assignedNodes)
visit(ariaNode, child); visit(ariaNode, child);
// Process shadow DOM children, if any } else {
for (let child = element.firstChild; child; child = child.nextSibling) {
if (!(child as Element | Text).assignedSlot)
visit(ariaNode, child);
}
if (element.shadowRoot) { if (element.shadowRoot) {
for (let child = element.shadowRoot.firstChild; child; child = child.nextSibling) for (let child = element.shadowRoot.firstChild; child; child = child.nextSibling)
visit(ariaNode, child); visit(ariaNode, child);
} }
} }
beginAriaCaches(); ariaNode.children.push(roleUtils.getPseudoContent(element, '::after'));
const result = toAriaNode(rootElement);
const ariaRoot = result?.ariaNode || { role: '' }; if (treatAsBlock)
ariaNode.children.push(treatAsBlock);
}
roleUtils.beginAriaCaches();
const ariaRoot: AriaNode = { role: 'fragment', name: '', children: [] };
try { try {
visit(ariaRoot, rootElement); visit(ariaRoot, rootElement);
} finally { } finally {
endAriaCaches(); roleUtils.endAriaCaches();
} }
normalizeStringChildren(ariaRoot); normalizeStringChildren(ariaRoot);
return ariaRoot; return ariaRoot;
} }
function toAriaNode(element: Element): { ariaNode: AriaNode, isLeaf: boolean } | null {
const role = roleUtils.getAriaRole(element);
if (!role)
return null;
const name = roleUtils.getElementAccessibleName(element, false) || '';
const isLeaf = leafRoles.has(role);
const result: AriaNode = { role, name, children: [] };
if (isLeaf && !name) {
const text = roleUtils.accumulatedElementText(element);
if (text)
result.children = [text];
}
if (roleUtils.kAriaCheckedRoles.includes(role))
result.checked = roleUtils.getAriaChecked(element);
if (roleUtils.kAriaDisabledRoles.includes(role))
result.disabled = roleUtils.getAriaDisabled(element);
if (roleUtils.kAriaExpandedRoles.includes(role))
result.expanded = roleUtils.getAriaExpanded(element);
if (roleUtils.kAriaLevelRoles.includes(role))
result.level = roleUtils.getAriaLevel(element);
if (roleUtils.kAriaPressedRoles.includes(role))
result.pressed = roleUtils.getAriaPressed(element);
if (roleUtils.kAriaSelectedRoles.includes(role))
result.selected = roleUtils.getAriaSelected(element);
return { isLeaf, ariaNode: result };
}
export function renderedAriaTree(rootElement: Element): string { export function renderedAriaTree(rootElement: Element): string {
return renderAriaTree(generateAriaTree(rootElement)); return renderAriaTree(generateAriaTree(rootElement));
} }
@ -129,14 +177,14 @@ function normalizeStringChildren(rootA11yNode: AriaNode) {
} }
} }
flushChildren(buffer, normalizedChildren); flushChildren(buffer, normalizedChildren);
ariaNode.children = normalizedChildren.length ? normalizedChildren : undefined; ariaNode.children = normalizedChildren.length ? normalizedChildren : [];
}; };
visit(rootA11yNode); visit(rootA11yNode);
} }
const hiddenContainerRoles = new Set(['none', 'presentation']); const hiddenContainerRoles = new Set(['none', 'presentation']);
const leafRoles = new Set([ const leafRoles = new Set<AriaRole>([
'alert', 'blockquote', 'button', 'caption', 'checkbox', 'code', 'columnheader', 'alert', 'blockquote', 'button', 'caption', 'checkbox', 'code', 'columnheader',
'definition', 'deletion', 'emphasis', 'generic', 'heading', 'img', 'insertion', 'definition', 'deletion', 'emphasis', 'generic', 'heading', 'img', 'insertion',
'link', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'meter', 'option', 'link', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'meter', 'option',
@ -145,7 +193,7 @@ const leafRoles = new Set([
'textbox', 'time', 'tooltip' 'textbox', 'time', 'tooltip'
]); ]);
const normalizeWhitespaceWithin = (text: string) => text.replace(/[\s\n]+/g, ' '); const normalizeWhitespaceWithin = (text: string) => text.replace(/[\s\t\r\n]+/g, ' ');
function matchesText(text: string | undefined, template: RegExp | string | undefined) { function matchesText(text: string | undefined, template: RegExp | string | undefined) {
if (!template) if (!template)
@ -159,8 +207,8 @@ function matchesText(text: string | undefined, template: RegExp | string | undef
export function matchesAriaTree(rootElement: Element, template: AriaTemplateNode): { matches: boolean, received: string } { export function matchesAriaTree(rootElement: Element, template: AriaTemplateNode): { matches: boolean, received: string } {
const root = generateAriaTree(rootElement); const root = generateAriaTree(rootElement);
const matches = nodeMatches(root, template); const matches = matchesNodeDeep(root, template);
return { matches, received: renderAriaTree(root) }; return { matches, received: renderAriaTree(root, { noText: true }) };
} }
function matchesNode(node: AriaNode | string, template: AriaTemplateNode | RegExp | string, depth: number): boolean { function matchesNode(node: AriaNode | string, template: AriaTemplateNode | RegExp | string, depth: number): boolean {
@ -168,7 +216,19 @@ function matchesNode(node: AriaNode | string, template: AriaTemplateNode | RegEx
return matchesText(node, template); return matchesText(node, template);
if (typeof node === 'object' && typeof template === 'object' && !(template instanceof RegExp)) { if (typeof node === 'object' && typeof template === 'object' && !(template instanceof RegExp)) {
if (template.role && template.role !== node.role) if (template.role !== 'fragment' && template.role !== node.role)
return false;
if (template.checked !== undefined && template.checked !== node.checked)
return false;
if (template.disabled !== undefined && template.disabled !== node.disabled)
return false;
if (template.expanded !== undefined && template.expanded !== node.expanded)
return false;
if (template.level !== undefined && template.level !== node.level)
return false;
if (template.pressed !== undefined && template.pressed !== node.pressed)
return false;
if (template.selected !== undefined && template.selected !== node.selected)
return false; return false;
if (!matchesText(node.name, template.name)) if (!matchesText(node.name, template.name))
return false; return false;
@ -197,7 +257,7 @@ function containsList(children: (AriaNode | string)[], template: (AriaTemplateNo
return true; return true;
} }
function nodeMatches(root: AriaNode, template: AriaTemplateNode): boolean { function matchesNodeDeep(root: AriaNode, template: AriaTemplateNode): boolean {
const results: (AriaNode | string)[] = []; const results: (AriaNode | string)[] = [];
const visit = (node: AriaNode | string): boolean => { const visit = (node: AriaNode | string): boolean => {
if (matchesNode(node, template, 0)) { if (matchesNode(node, template, 0)) {
@ -216,29 +276,55 @@ function nodeMatches(root: AriaNode, template: AriaTemplateNode): boolean {
return !!results.length; return !!results.length;
} }
export function renderAriaTree(ariaNode: AriaNode): string { export function renderAriaTree(ariaNode: AriaNode, options?: { noText?: boolean }): string {
const lines: string[] = []; const lines: string[] = [];
const visit = (ariaNode: AriaNode, indent: string) => { const visit = (ariaNode: AriaNode | string, indent: string) => {
if (typeof ariaNode === 'string') {
if (!options?.noText)
lines.push(indent + '- text: ' + escapeYamlString(ariaNode));
return;
}
let line = `${indent}- ${ariaNode.role}`; let line = `${indent}- ${ariaNode.role}`;
if (ariaNode.name) if (ariaNode.name)
line += ` ${escapeWithQuotes(ariaNode.name, '"')}`; line += ` ${escapeWithQuotes(ariaNode.name, '"')}`;
const noChild = !ariaNode.name && !ariaNode.children?.length;
const oneChild = !ariaNode.name && ariaNode.children?.length === 1 && typeof ariaNode.children[0] === 'string'; if (ariaNode.checked === 'mixed')
if (noChild || oneChild) { line += ` [checked=mixed]`;
if (oneChild) if (ariaNode.checked === true)
line += ` [checked]`;
if (ariaNode.disabled)
line += ` [disabled]`;
if (ariaNode.expanded)
line += ` [expanded]`;
if (ariaNode.level)
line += ` [level=${ariaNode.level}]`;
if (ariaNode.pressed === 'mixed')
line += ` [pressed=mixed]`;
if (ariaNode.pressed === true)
line += ` [pressed]`;
if (ariaNode.selected === true)
line += ` [selected]`;
const stringValue = !ariaNode.children.length || (ariaNode.children?.length === 1 && typeof ariaNode.children[0] === 'string');
if (stringValue) {
if (!options?.noText && ariaNode.children.length)
line += ': ' + escapeYamlString(ariaNode.children?.[0] as string); line += ': ' + escapeYamlString(ariaNode.children?.[0] as string);
lines.push(line); lines.push(line);
return; return;
} }
lines.push(line + (ariaNode.children ? ':' : ''));
for (const child of ariaNode.children || []) { lines.push(line + ':');
if (typeof child === 'string') for (const child of ariaNode.children || [])
lines.push(indent + ' - text: ' + escapeYamlString(child));
else
visit(child, indent + ' '); visit(child, indent + ' ');
}
}; };
if (ariaNode.role === 'fragment') {
// Render fragment.
for (const child of ariaNode.children || [])
visit(child, '');
} else {
visit(ariaNode, ''); visit(ariaNode, '');
}
return lines.join('\n'); return lines.join('\n');
} }

View file

@ -20,7 +20,6 @@ import { escapeForTextSelector } from '../../utils/isomorphic/stringUtils';
import { asLocator } from '../../utils/isomorphic/locatorGenerators'; import { asLocator } from '../../utils/isomorphic/locatorGenerators';
import type { Language } from '../../utils/isomorphic/locatorGenerators'; import type { Language } from '../../utils/isomorphic/locatorGenerators';
import type { InjectedScript } from './injectedScript'; import type { InjectedScript } from './injectedScript';
import { renderedAriaTree } from './ariaSnapshot';
const selectorSymbol = Symbol('selector'); const selectorSymbol = Symbol('selector');
@ -86,7 +85,7 @@ class ConsoleAPI {
inspect: (selector: string) => this._inspect(selector), inspect: (selector: string) => this._inspect(selector),
selector: (element: Element) => this._selector(element), selector: (element: Element) => this._selector(element),
generateLocator: (element: Element, language?: Language) => this._generateLocator(element, language), generateLocator: (element: Element, language?: Language) => this._generateLocator(element, language),
ariaSnapshot: (element?: Element) => renderedAriaTree(element || this._injectedScript.document.body), ariaSnapshot: (element?: Element) => this._injectedScript.ariaSnapshot(element || this._injectedScript.document.body),
resume: () => this._resume(), resume: () => this._resume(),
...new Locator(injectedScript, ''), ...new Locator(injectedScript, ''),
}; };

View file

@ -220,6 +220,11 @@ x-pw-tool-item.value > x-div {
clip-path: url(#icon-symbol-constant); clip-path: url(#icon-symbol-constant);
} }
x-pw-tool-item.snapshot > x-div {
/* codicon: eye */
clip-path: url(#icon-gist);
}
x-pw-tool-item.accept > x-div { x-pw-tool-item.accept > x-div {
clip-path: url(#icon-check); clip-path: url(#icon-check);
} }

View file

@ -34,7 +34,7 @@ import { kLayoutSelectorNames, type LayoutSelectorName, layoutSelectorScore } fr
import { asLocator } from '../../utils/isomorphic/locatorGenerators'; import { asLocator } from '../../utils/isomorphic/locatorGenerators';
import type { Language } from '../../utils/isomorphic/locatorGenerators'; import type { Language } from '../../utils/isomorphic/locatorGenerators';
import { cacheNormalizedWhitespaces, normalizeWhiteSpace, trimStringWithEllipsis } from '../../utils/isomorphic/stringUtils'; import { cacheNormalizedWhitespaces, normalizeWhiteSpace, trimStringWithEllipsis } from '../../utils/isomorphic/stringUtils';
import { matchesAriaTree } from './ariaSnapshot'; import { matchesAriaTree, renderedAriaTree } from './ariaSnapshot';
export type FrameExpectParams = Omit<channels.FrameExpectParams, 'expectedValue'> & { expectedValue?: any }; export type FrameExpectParams = Omit<channels.FrameExpectParams, 'expectedValue'> & { expectedValue?: any };
@ -206,6 +206,12 @@ export class InjectedScript {
return new Set<Element>(result.map(r => r.element)); return new Set<Element>(result.map(r => r.element));
} }
ariaSnapshot(node: Node): string {
if (node.nodeType !== Node.ELEMENT_NODE)
throw this.createStacklessError('Can only capture aria snapshot of Element nodes.');
return renderedAriaTree(node as Element);
}
querySelectorAll(selector: ParsedSelector, root: Node): Element[] { querySelectorAll(selector: ParsedSelector, root: Node): Element[] {
if (selector.capture !== undefined) { if (selector.capture !== undefined) {
if (selector.parts.some(part => part.name === 'nth')) if (selector.parts.some(part => part.name === 'nth'))

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.57 1.14l3.28 3.3.15.36v9.7l-.5.5h-11l-.5-.5v-13l.5-.5h7.72l.35.14zM10 5h3l-3-3v3zM3 2v12h10V6H9.5L9 5.5V2H3zm2.062 7.533l1.817-1.828L6.17 7 4 9.179v.707l2.171 2.174.707-.707-1.816-1.82zM8.8 7.714l.7-.709 2.189 2.175v.709L9.5 12.062l-.705-.709 1.831-1.82L8.8 7.714z"/></svg>

After

Width:  |  Height:  |  Size: 429 B

View file

@ -608,9 +608,9 @@ class TextAssertionTool implements RecorderTool {
private _action: actions.AssertAction | null = null; private _action: actions.AssertAction | null = null;
private _dialog: Dialog; private _dialog: Dialog;
private _textCache = new Map<Element | ShadowRoot, ElementText>(); private _textCache = new Map<Element | ShadowRoot, ElementText>();
private _kind: 'text' | 'value'; private _kind: 'text' | 'value' | 'snapshot';
constructor(recorder: Recorder, kind: 'text' | 'value') { constructor(recorder: Recorder, kind: 'text' | 'value' | 'snapshot') {
this._recorder = recorder; this._recorder = recorder;
this._kind = kind; this._kind = kind;
this._dialog = new Dialog(recorder); this._dialog = new Dialog(recorder);
@ -656,7 +656,7 @@ class TextAssertionTool implements RecorderTool {
const target = this._recorder.deepEventTarget(event); const target = this._recorder.deepEventTarget(event);
if (this._hoverHighlight?.elements[0] === target) if (this._hoverHighlight?.elements[0] === target)
return; return;
if (this._kind === 'text') if (this._kind === 'text' || this._kind === 'snapshot')
this._hoverHighlight = this._recorder.injectedScript.utils.elementText(this._textCache, target).full ? { elements: [target], selector: '' } : null; this._hoverHighlight = this._recorder.injectedScript.utils.elementText(this._textCache, target).full ? { elements: [target], selector: '' } : null;
else else
this._hoverHighlight = this._elementHasValue(target) ? this._recorder.injectedScript.generateSelector(target, { testIdAttributeName: this._recorder.state.testIdAttributeName }) : null; this._hoverHighlight = this._elementHasValue(target) ? this._recorder.injectedScript.generateSelector(target, { testIdAttributeName: this._recorder.state.testIdAttributeName }) : null;
@ -704,6 +704,18 @@ class TextAssertionTool implements RecorderTool {
value: (target as (HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement)).value, value: (target as (HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement)).value,
}; };
} }
} else if (this._kind === 'snapshot') {
this._hoverHighlight = this._recorder.injectedScript.generateSelector(target, { testIdAttributeName: this._recorder.state.testIdAttributeName, forTextExpect: true });
this._hoverHighlight.color = '#8acae480';
// forTextExpect can update the target, re-highlight it.
this._recorder.updateHighlight(this._hoverHighlight, true);
return {
name: 'assertSnapshot',
selector: this._hoverHighlight.selector,
signals: [],
snapshot: this._recorder.injectedScript.ariaSnapshot(target),
};
} else { } else {
this._hoverHighlight = this._recorder.injectedScript.generateSelector(target, { testIdAttributeName: this._recorder.state.testIdAttributeName, forTextExpect: true }); this._hoverHighlight = this._recorder.injectedScript.generateSelector(target, { testIdAttributeName: this._recorder.state.testIdAttributeName, forTextExpect: true });
this._hoverHighlight.color = '#8acae480'; this._hoverHighlight.color = '#8acae480';
@ -727,6 +739,8 @@ class TextAssertionTool implements RecorderTool {
return String(action.checked); return String(action.checked);
if (action?.name === 'assertValue') if (action?.name === 'assertValue')
return action.value; return action.value;
if (action?.name === 'assertSnapshot')
return action.snapshot;
return ''; return '';
} }
@ -742,13 +756,19 @@ class TextAssertionTool implements RecorderTool {
if (!this._hoverHighlight?.elements[0]) if (!this._hoverHighlight?.elements[0])
return; return;
this._action = this._generateAction(); this._action = this._generateAction();
if (!this._action || this._action.name !== 'assertText') if (this._action?.name === 'assertText') {
return; this._showTextDialog(this._action);
} else if (this._action?.name === 'assertSnapshot') {
this._recorder.recordAction(this._action);
this._recorder.setMode('recording');
this._recorder.overlay?.flashToolSucceeded('assertingSnapshot');
}
}
const action = this._action; private _showTextDialog(action: actions.AssertTextAction) {
const textElement = this._recorder.document.createElement('textarea'); const textElement = this._recorder.document.createElement('textarea');
textElement.setAttribute('spellcheck', 'false'); textElement.setAttribute('spellcheck', 'false');
textElement.value = this._renderValue(this._action); textElement.value = this._renderValue(action);
textElement.classList.add('text-editor'); textElement.classList.add('text-editor');
const updateAndValidate = () => { const updateAndValidate = () => {
@ -796,6 +816,7 @@ class Overlay {
private _assertVisibilityToggle: HTMLElement; private _assertVisibilityToggle: HTMLElement;
private _assertTextToggle: HTMLElement; private _assertTextToggle: HTMLElement;
private _assertValuesToggle: HTMLElement; private _assertValuesToggle: HTMLElement;
private _assertSnapshotToggle: HTMLElement;
private _offsetX = 0; private _offsetX = 0;
private _dragState: { offsetX: number, dragStart: { x: number, y: number } } | undefined; private _dragState: { offsetX: number, dragStart: { x: number, y: number } } | undefined;
private _measure: { width: number, height: number } = { width: 0, height: 0 }; private _measure: { width: number, height: number } = { width: 0, height: 0 };
@ -842,6 +863,12 @@ class Overlay {
this._assertValuesToggle.appendChild(this._recorder.document.createElement('x-div')); this._assertValuesToggle.appendChild(this._recorder.document.createElement('x-div'));
toolsListElement.appendChild(this._assertValuesToggle); toolsListElement.appendChild(this._assertValuesToggle);
this._assertSnapshotToggle = this._recorder.document.createElement('x-pw-tool-item');
this._assertSnapshotToggle.title = 'Assert snapshot';
this._assertSnapshotToggle.classList.add('snapshot');
this._assertSnapshotToggle.appendChild(this._recorder.document.createElement('x-div'));
toolsListElement.appendChild(this._assertSnapshotToggle);
this._updateVisualPosition(); this._updateVisualPosition();
this._refreshListeners(); this._refreshListeners();
} }
@ -865,6 +892,7 @@ class Overlay {
'assertingText': 'recording-inspecting', 'assertingText': 'recording-inspecting',
'assertingVisibility': 'recording-inspecting', 'assertingVisibility': 'recording-inspecting',
'assertingValue': 'recording-inspecting', 'assertingValue': 'recording-inspecting',
'assertingSnapshot': 'recording-inspecting',
}; };
this._recorder.setMode(newMode[this._recorder.state.mode]); this._recorder.setMode(newMode[this._recorder.state.mode]);
}), }),
@ -880,6 +908,10 @@ class Overlay {
if (!this._assertValuesToggle.classList.contains('disabled')) if (!this._assertValuesToggle.classList.contains('disabled'))
this._recorder.setMode(this._recorder.state.mode === 'assertingValue' ? 'recording' : 'assertingValue'); this._recorder.setMode(this._recorder.state.mode === 'assertingValue' ? 'recording' : 'assertingValue');
}), }),
addEventListener(this._assertSnapshotToggle, 'click', () => {
if (!this._assertSnapshotToggle.classList.contains('disabled'))
this._recorder.setMode(this._recorder.state.mode === 'assertingSnapshot' ? 'recording' : 'assertingSnapshot');
}),
]; ];
} }
@ -902,6 +934,8 @@ class Overlay {
this._assertTextToggle.classList.toggle('disabled', state.mode === 'none' || state.mode === 'standby' || state.mode === 'inspecting'); this._assertTextToggle.classList.toggle('disabled', state.mode === 'none' || state.mode === 'standby' || state.mode === 'inspecting');
this._assertValuesToggle.classList.toggle('active', state.mode === 'assertingValue'); this._assertValuesToggle.classList.toggle('active', state.mode === 'assertingValue');
this._assertValuesToggle.classList.toggle('disabled', state.mode === 'none' || state.mode === 'standby' || state.mode === 'inspecting'); this._assertValuesToggle.classList.toggle('disabled', state.mode === 'none' || state.mode === 'standby' || state.mode === 'inspecting');
this._assertSnapshotToggle.classList.toggle('active', state.mode === 'assertingSnapshot');
this._assertSnapshotToggle.classList.toggle('disabled', state.mode === 'none' || state.mode === 'standby' || state.mode === 'inspecting');
if (this._offsetX !== state.overlay.offsetX) { if (this._offsetX !== state.overlay.offsetX) {
this._offsetX = state.overlay.offsetX; this._offsetX = state.overlay.offsetX;
this._updateVisualPosition(); this._updateVisualPosition();
@ -912,8 +946,14 @@ class Overlay {
this._showOverlay(); this._showOverlay();
} }
flashToolSucceeded(tool: 'assertingVisibility' | 'assertingValue') { flashToolSucceeded(tool: 'assertingVisibility' | 'assertingSnapshot' | 'assertingValue') {
const element = tool === 'assertingVisibility' ? this._assertVisibilityToggle : this._assertValuesToggle; let element: Element;
if (tool === 'assertingVisibility')
element = this._assertVisibilityToggle;
else if (tool === 'assertingSnapshot')
element = this._assertSnapshotToggle;
else
element = this._assertValuesToggle;
element.classList.add('succeeded'); element.classList.add('succeeded');
this._recorder.injectedScript.builtinSetTimeout(() => element.classList.remove('succeeded'), 2000); this._recorder.injectedScript.builtinSetTimeout(() => element.classList.remove('succeeded'), 2000);
} }
@ -1004,6 +1044,7 @@ export class Recorder {
'assertingText': new TextAssertionTool(this, 'text'), 'assertingText': new TextAssertionTool(this, 'text'),
'assertingVisibility': new InspectTool(this, true), 'assertingVisibility': new InspectTool(this, true),
'assertingValue': new TextAssertionTool(this, 'value'), 'assertingValue': new TextAssertionTool(this, 'value'),
'assertingSnapshot': new TextAssertionTool(this, 'snapshot'),
}; };
this._currentTool = this._tools.none; this._currentTool = this._tools.none;
if (injectedScript.window.top === injectedScript.window) { if (injectedScript.window.top === injectedScript.window) {

View file

@ -82,7 +82,7 @@ function isNativelyFocusable(element: Element) {
// https://w3c.github.io/html-aam/#html-element-role-mappings // https://w3c.github.io/html-aam/#html-element-role-mappings
// https://www.w3.org/TR/html-aria/#docconformance // https://www.w3.org/TR/html-aria/#docconformance
const kImplicitRoleByTagName: { [tagName: string]: (e: Element) => string | null } = { const kImplicitRoleByTagName: { [tagName: string]: (e: Element) => AriaRole | null } = {
'A': (e: Element) => { 'A': (e: Element) => {
return e.hasAttribute('href') ? 'link' : null; return e.hasAttribute('href') ? 'link' : null;
}, },
@ -127,17 +127,8 @@ const kImplicitRoleByTagName: { [tagName: string]: (e: Element) => string | null
return (list && elementSafeTagName(list) === 'DATALIST') ? 'combobox' : 'textbox'; return (list && elementSafeTagName(list) === 'DATALIST') ? 'combobox' : 'textbox';
} }
if (type === 'hidden') if (type === 'hidden')
return ''; return null;
return { return inputTypeToRole[type] || 'textbox';
'button': 'button',
'checkbox': 'checkbox',
'image': 'button',
'number': 'spinbutton',
'radio': 'radio',
'range': 'slider',
'reset': 'button',
'submit': 'button',
}[type] || 'textbox';
}, },
'INS': () => 'insertion', 'INS': () => 'insertion',
'LI': () => 'listitem', 'LI': () => 'listitem',
@ -200,7 +191,7 @@ const kPresentationInheritanceParents: { [tagName: string]: string[] } = {
'TR': ['THEAD', 'TBODY', 'TFOOT', 'TABLE'], 'TR': ['THEAD', 'TBODY', 'TFOOT', 'TABLE'],
}; };
function getImplicitAriaRole(element: Element): string | null { function getImplicitAriaRole(element: Element): AriaRole | null {
const implicitRole = kImplicitRoleByTagName[elementSafeTagName(element)]?.(element) || ''; const implicitRole = kImplicitRoleByTagName[elementSafeTagName(element)]?.(element) || '';
if (!implicitRole) if (!implicitRole)
return null; return null;
@ -221,23 +212,29 @@ function getImplicitAriaRole(element: Element): string | null {
} }
// https://www.w3.org/TR/wai-aria-1.2/#role_definitions // https://www.w3.org/TR/wai-aria-1.2/#role_definitions
const allRoles = [
'alert', 'alertdialog', 'application', 'article', 'banner', 'blockquote', 'button', 'caption', 'cell', 'checkbox', 'code', 'columnheader', 'combobox', 'command',
'complementary', 'composite', 'contentinfo', 'definition', 'deletion', 'dialog', 'directory', 'document', 'emphasis', 'feed', 'figure', 'form', 'generic', 'grid',
'gridcell', 'group', 'heading', 'img', 'input', 'insertion', 'landmark', 'link', 'list', 'listbox', 'listitem', 'log', 'main', 'marquee', 'math', 'meter', 'menu',
'menubar', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'navigation', 'none', 'note', 'option', 'paragraph', 'presentation', 'progressbar', 'radio', 'radiogroup',
'range', 'region', 'roletype', 'row', 'rowgroup', 'rowheader', 'scrollbar', 'search', 'searchbox', 'section', 'sectionhead', 'select', 'separator', 'slider',
'spinbutton', 'status', 'strong', 'structure', 'subscript', 'superscript', 'switch', 'tab', 'table', 'tablist', 'tabpanel', 'term', 'textbox', 'time', 'timer',
'toolbar', 'tooltip', 'tree', 'treegrid', 'treeitem', 'widget', 'window'
];
// https://www.w3.org/TR/wai-aria-1.2/#abstract_roles // https://www.w3.org/TR/wai-aria-1.2/#abstract_roles
const abstractRoles = ['command', 'composite', 'input', 'landmark', 'range', 'roletype', 'section', 'sectionhead', 'select', 'structure', 'widget', 'window']; // type AbstractRoles = 'command' | 'composite' | 'input' | 'landmark' | 'range' | 'roletype' | 'section' | 'sectionhead' | 'select' | 'structure' | 'widget' | 'window';
const validRoles = allRoles.filter(role => !abstractRoles.includes(role));
function getExplicitAriaRole(element: Element): string | null { export type AriaRole = 'alert' | 'alertdialog' | 'application' | 'article' | 'banner' | 'blockquote' | 'button' | 'caption' | 'cell' | 'checkbox' | 'code' | 'columnheader' | 'combobox' |
'complementary' | 'contentinfo' | 'definition' | 'deletion' | 'dialog' | 'directory' | 'document' | 'emphasis' | 'feed' | 'figure' | 'form' | 'generic' | 'grid' |
'gridcell' | 'group' | 'heading' | 'img' | 'insertion' | 'link' | 'list' | 'listbox' | 'listitem' | 'log' | 'main' | 'mark' | 'marquee' | 'math' | 'meter' | 'menu' |
'menubar' | 'menuitem' | 'menuitemcheckbox' | 'menuitemradio' | 'navigation' | 'none' | 'note' | 'option' | 'paragraph' | 'presentation' | 'progressbar' | 'radio' | 'radiogroup' |
'region' | 'row' | 'rowgroup' | 'rowheader' | 'scrollbar' | 'search' | 'searchbox' | 'separator' | 'slider' |
'spinbutton' | 'status' | 'strong' | 'subscript' | 'superscript' | 'switch' | 'tab' | 'table' | 'tablist' | 'tabpanel' | 'term' | 'textbox' | 'time' | 'timer' |
'toolbar' | 'tooltip' | 'tree' | 'treegrid' | 'treeitem';
const validRoles: AriaRole[] = ['alert', 'alertdialog', 'application', 'article', 'banner', 'blockquote', 'button', 'caption', 'cell', 'checkbox', 'code', 'columnheader', 'combobox',
'complementary', 'contentinfo', 'definition', 'deletion', 'dialog', 'directory', 'document', 'emphasis', 'feed', 'figure', 'form', 'generic', 'grid',
'gridcell', 'group', 'heading', 'img', 'insertion', 'link', 'list', 'listbox', 'listitem', 'log', 'main', 'mark', 'marquee', 'math', 'meter', 'menu',
'menubar', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'navigation', 'none', 'note', 'option', 'paragraph', 'presentation', 'progressbar', 'radio', 'radiogroup',
'region', 'row', 'rowgroup', 'rowheader', 'scrollbar', 'search', 'searchbox', 'separator', 'slider',
'spinbutton', 'status', 'strong', 'subscript', 'superscript', 'switch', 'tab', 'table', 'tablist', 'tabpanel', 'term', 'textbox', 'time', 'timer',
'toolbar', 'tooltip', 'tree', 'treegrid', 'treeitem'];
function getExplicitAriaRole(element: Element): AriaRole | null {
// https://www.w3.org/TR/wai-aria-1.2/#document-handling_author-errors_roles // https://www.w3.org/TR/wai-aria-1.2/#document-handling_author-errors_roles
const roles = (element.getAttribute('role') || '').split(' ').map(role => role.trim()); const roles = (element.getAttribute('role') || '').split(' ').map(role => role.trim());
return roles.find(role => validRoles.includes(role)) || null; return roles.find(role => validRoles.includes(role as any)) as AriaRole || null;
} }
function hasPresentationConflictResolution(element: Element, role: string | null) { function hasPresentationConflictResolution(element: Element, role: string | null) {
@ -245,7 +242,7 @@ function hasPresentationConflictResolution(element: Element, role: string | null
return hasGlobalAriaAttribute(element, role) || isFocusable(element); return hasGlobalAriaAttribute(element, role) || isFocusable(element);
} }
export function getAriaRole(element: Element): string | null { export function getAriaRole(element: Element): AriaRole | null {
const explicitRole = getExplicitAriaRole(element); const explicitRole = getExplicitAriaRole(element);
if (!explicitRole) if (!explicitRole)
return getImplicitAriaRole(element); return getImplicitAriaRole(element);
@ -363,7 +360,7 @@ function queryInAriaOwned(element: Element, selector: string): Element[] {
return result; return result;
} }
function getPseudoContent(element: Element, pseudo: '::before' | '::after') { export function getPseudoContent(element: Element, pseudo: '::before' | '::after') {
const cache = pseudo === '::before' ? cachePseudoContentBefore : cachePseudoContentAfter; const cache = pseudo === '::before' ? cachePseudoContentBefore : cachePseudoContentAfter;
if (cache?.has(element)) if (cache?.has(element))
return cache?.get(element) || ''; return cache?.get(element) || '';
@ -430,10 +427,6 @@ export function getElementAccessibleName(element: Element, includeHidden: boolea
accessibleName = asFlatString(getTextAlternativeInternal(element, { accessibleName = asFlatString(getTextAlternativeInternal(element, {
includeHidden, includeHidden,
visitedElements: new Set(), visitedElements: new Set(),
embeddedInDescribedBy: undefined,
embeddedInLabelledBy: undefined,
embeddedInLabel: undefined,
embeddedInNativeTextAlternative: undefined,
embeddedInTargetElement: 'self', embeddedInTargetElement: 'self',
})); }));
} }
@ -458,10 +451,6 @@ export function getElementAccessibleDescription(element: Element, includeHidden:
accessibleDescription = asFlatString(describedBy.map(ref => getTextAlternativeInternal(ref, { accessibleDescription = asFlatString(describedBy.map(ref => getTextAlternativeInternal(ref, {
includeHidden, includeHidden,
visitedElements: new Set(), visitedElements: new Set(),
embeddedInLabelledBy: undefined,
embeddedInLabel: undefined,
embeddedInNativeTextAlternative: undefined,
embeddedInTargetElement: 'none',
embeddedInDescribedBy: { element: ref, hidden: isElementHiddenForAria(ref) }, embeddedInDescribedBy: { element: ref, hidden: isElementHiddenForAria(ref) },
})).join(' ')); })).join(' '));
} else if (element.hasAttribute('aria-description')) { } else if (element.hasAttribute('aria-description')) {
@ -480,13 +469,13 @@ export function getElementAccessibleDescription(element: Element, includeHidden:
} }
type AccessibleNameOptions = { type AccessibleNameOptions = {
includeHidden: boolean,
visitedElements: Set<Element>, visitedElements: Set<Element>,
embeddedInDescribedBy: { element: Element, hidden: boolean } | undefined, includeHidden?: boolean,
embeddedInLabelledBy: { element: Element, hidden: boolean } | undefined, embeddedInDescribedBy?: { element: Element, hidden: boolean },
embeddedInLabel: { element: Element, hidden: boolean } | undefined, embeddedInLabelledBy?: { element: Element, hidden: boolean },
embeddedInNativeTextAlternative: { element: Element, hidden: boolean } | undefined, embeddedInLabel?: { element: Element, hidden: boolean },
embeddedInTargetElement: 'none' | 'self' | 'descendant', embeddedInNativeTextAlternative?: { element: Element, hidden: boolean },
embeddedInTargetElement?: 'self' | 'descendant',
}; };
function getTextAlternativeInternal(element: Element, options: AccessibleNameOptions): string { function getTextAlternativeInternal(element: Element, options: AccessibleNameOptions): string {
@ -525,7 +514,7 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
...options, ...options,
embeddedInLabelledBy: { element: ref, hidden: isElementHiddenForAria(ref) }, embeddedInLabelledBy: { element: ref, hidden: isElementHiddenForAria(ref) },
embeddedInDescribedBy: undefined, embeddedInDescribedBy: undefined,
embeddedInTargetElement: 'none', embeddedInTargetElement: undefined,
embeddedInLabel: undefined, embeddedInLabel: undefined,
embeddedInNativeTextAlternative: undefined, embeddedInNativeTextAlternative: undefined,
})).join(' '); })).join(' ');
@ -778,13 +767,35 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
!!options.embeddedInLabelledBy || !!options.embeddedInDescribedBy || !!options.embeddedInLabelledBy || !!options.embeddedInDescribedBy ||
!!options.embeddedInLabel || !!options.embeddedInNativeTextAlternative) { !!options.embeddedInLabel || !!options.embeddedInNativeTextAlternative) {
options.visitedElements.add(element); options.visitedElements.add(element);
const accessibleName = innerAccumulatedElementText(element, childOptions);
// Spec says "Return the accumulated text if it is not the empty string". However, that is not really
// compatible with the real browser behavior and wpt tests, where an element with empty contents will fallback to the title.
// So we follow the spec everywhere except for the target element itself. This can probably be improved.
const maybeTrimmedAccessibleName = options.embeddedInTargetElement === 'self' ? trimFlatString(accessibleName) : accessibleName;
if (maybeTrimmedAccessibleName)
return accessibleName;
}
// step 2i.
if (!['presentation', 'none'].includes(role) || tagName === 'IFRAME') {
options.visitedElements.add(element);
const title = element.getAttribute('title') || '';
if (trimFlatString(title))
return title;
}
options.visitedElements.add(element);
return '';
}
function innerAccumulatedElementText(element: Element, options: AccessibleNameOptions): string {
const tokens: string[] = []; const tokens: string[] = [];
const visit = (node: Node, skipSlotted: boolean) => { const visit = (node: Node, skipSlotted: boolean) => {
if (skipSlotted && (node as Element | Text).assignedSlot) if (skipSlotted && (node as Element | Text).assignedSlot)
return; return;
if (node.nodeType === 1 /* Node.ELEMENT_NODE */) { if (node.nodeType === 1 /* Node.ELEMENT_NODE */) {
const display = getElementComputedStyle(node as Element)?.display || 'inline'; const display = getElementComputedStyle(node as Element)?.display || 'inline';
let token = getTextAlternativeInternal(node as Element, childOptions); let token = getTextAlternativeInternal(node as Element, options);
// SPEC DIFFERENCE. // SPEC DIFFERENCE.
// Spec says "append the result to the accumulated text", assuming "with space". // Spec says "append the result to the accumulated text", assuming "with space".
// However, multiple tests insist that inline elements do not add a space. // However, multiple tests insist that inline elements do not add a space.
@ -813,25 +824,12 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
visit(owned, true); visit(owned, true);
} }
tokens.push(getPseudoContent(element, '::after')); tokens.push(getPseudoContent(element, '::after'));
const accessibleName = tokens.join(''); return tokens.join('');
// Spec says "Return the accumulated text if it is not the empty string". However, that is not really
// compatible with the real browser behavior and wpt tests, where an element with empty contents will fallback to the title.
// So we follow the spec everywhere except for the target element itself. This can probably be improved.
const maybeTrimmedAccessibleName = options.embeddedInTargetElement === 'self' ? trimFlatString(accessibleName) : accessibleName;
if (maybeTrimmedAccessibleName)
return accessibleName;
} }
// step 2i. export function accumulatedElementText(element: Element): string {
if (!['presentation', 'none'].includes(role) || tagName === 'IFRAME') { const visitedElements = new Set<Element>();
options.visitedElements.add(element); return asFlatString(innerAccumulatedElementText(element, { visitedElements })).trim();
const title = element.getAttribute('title') || '';
if (trimFlatString(title))
return title;
}
options.visitedElements.add(element);
return '';
} }
export const kAriaSelectedRoles = ['gridcell', 'option', 'row', 'tab', 'rowheader', 'columnheader', 'treeitem']; export const kAriaSelectedRoles = ['gridcell', 'option', 'row', 'tab', 'rowheader', 'columnheader', 'treeitem'];
@ -883,7 +881,7 @@ export function getAriaPressed(element: Element): boolean | 'mixed' {
} }
export const kAriaExpandedRoles = ['application', 'button', 'checkbox', 'combobox', 'gridcell', 'link', 'listbox', 'menuitem', 'row', 'rowheader', 'tab', 'treeitem', 'columnheader', 'menuitemcheckbox', 'menuitemradio', 'rowheader', 'switch']; export const kAriaExpandedRoles = ['application', 'button', 'checkbox', 'combobox', 'gridcell', 'link', 'listbox', 'menuitem', 'row', 'rowheader', 'tab', 'treeitem', 'columnheader', 'menuitemcheckbox', 'menuitemradio', 'rowheader', 'switch'];
export function getAriaExpanded(element: Element): boolean | 'none' { export function getAriaExpanded(element: Element): boolean | undefined {
// https://www.w3.org/TR/wai-aria-1.2/#aria-expanded // https://www.w3.org/TR/wai-aria-1.2/#aria-expanded
// https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings // https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings
if (elementSafeTagName(element) === 'DETAILS') if (elementSafeTagName(element) === 'DETAILS')
@ -891,12 +889,12 @@ export function getAriaExpanded(element: Element): boolean | 'none' {
if (kAriaExpandedRoles.includes(getAriaRole(element) || '')) { if (kAriaExpandedRoles.includes(getAriaRole(element) || '')) {
const expanded = element.getAttribute('aria-expanded'); const expanded = element.getAttribute('aria-expanded');
if (expanded === null) if (expanded === null)
return 'none'; return undefined;
if (expanded === 'true') if (expanded === 'true')
return true; return true;
return false; return false;
} }
return 'none'; return undefined;
} }
export const kAriaLevelRoles = ['heading', 'listitem', 'row', 'treeitem']; export const kAriaLevelRoles = ['heading', 'listitem', 'row', 'treeitem'];
@ -958,7 +956,7 @@ function getAccessibleNameFromAssociatedLabels(labels: Iterable<HTMLLabelElement
embeddedInNativeTextAlternative: undefined, embeddedInNativeTextAlternative: undefined,
embeddedInLabelledBy: undefined, embeddedInLabelledBy: undefined,
embeddedInDescribedBy: undefined, embeddedInDescribedBy: undefined,
embeddedInTargetElement: 'none', embeddedInTargetElement: undefined,
})).filter(accessibleName => !!accessibleName).join(' '); })).filter(accessibleName => !!accessibleName).join(' ');
} }
@ -993,3 +991,14 @@ export function endAriaCaches() {
cachePseudoContentAfter = undefined; cachePseudoContentAfter = undefined;
} }
} }
const inputTypeToRole: Record<string, AriaRole> = {
'button': 'button',
'checkbox': 'checkbox',
'image': 'button',
'number': 'spinbutton',
'radio': 'radio',
'range': 'slider',
'reset': 'button',
'submit': 'button',
};

View file

@ -216,7 +216,7 @@ export class Recorder implements InstrumentationListener, IRecorder {
this._highlightedSelector = ''; this._highlightedSelector = '';
this._mode = mode; this._mode = mode;
this._recorderApp?.setMode(this._mode); this._recorderApp?.setMode(this._mode);
this._contextRecorder.setEnabled(this._mode === 'recording' || this._mode === 'assertingText' || this._mode === 'assertingVisibility' || this._mode === 'assertingValue'); this._contextRecorder.setEnabled(this._mode === 'recording' || this._mode === 'assertingText' || this._mode === 'assertingVisibility' || this._mode === 'assertingValue' || this._mode === 'assertingSnapshot');
this._debugger.setMuted(this._mode === 'recording' || this._mode === 'assertingText' || this._mode === 'assertingVisibility' || this._mode === 'assertingValue'); this._debugger.setMuted(this._mode === 'recording' || this._mode === 'assertingText' || this._mode === 'assertingVisibility' || this._mode === 'assertingValue');
if (this._mode !== 'none' && this._mode !== 'standby' && this._context.pages().length === 1) if (this._mode !== 'none' && this._mode !== 'standby' && this._context.pages().length === 1)
this._context.pages()[0].bringToFront().catch(() => {}); this._context.pages()[0].bringToFront().catch(() => {});

View file

@ -33,11 +33,12 @@ function encodeBase128(value: number): Buffer {
do { do {
let byte = value & 0x7f; let byte = value & 0x7f;
value >>>= 7; value >>>= 7;
if (bytes.length > 0) byte |= 0x80; if (bytes.length > 0)
byte |= 0x80;
bytes.push(byte); bytes.push(byte);
} while (value > 0); } while (value > 0);
return Buffer.from(bytes.reverse()); return Buffer.from(bytes.reverse());
}; }
// ASN1/DER Speficiation: https://www.itu.int/rec/T-REC-X.680-X.693-202102-I/en // ASN1/DER Speficiation: https://www.itu.int/rec/T-REC-X.680-X.693-202102-I/en
class DER { class DER {
@ -49,13 +50,13 @@ class DER {
return this._encode(0x02, Buffer.from([data])); return this._encode(0x02, Buffer.from([data]));
} }
static encodeObjectIdentifier(oid: string): Buffer { static encodeObjectIdentifier(oid: string): Buffer {
const parts = oid.split('.').map((v) => Number(v)); const parts = oid.split('.').map(v => Number(v));
// Encode the second part, which could be large, using base-128 encoding if necessary // Encode the second part, which could be large, using base-128 encoding if necessary
const output = [encodeBase128(40 * parts[0] + parts[1])]; const output = [encodeBase128(40 * parts[0] + parts[1])];
for (let i = 2; i < parts.length; i++) { for (let i = 2; i < parts.length; i++)
output.push(encodeBase128(parts[i])); output.push(encodeBase128(parts[i]));
}
return this._encode(0x06, Buffer.concat(output)); return this._encode(0x06, Buffer.concat(output));
} }

View file

@ -29,8 +29,8 @@ import { monotonicTime } from './time';
// Same as in Chromium (https://source.chromium.org/chromium/chromium/src/+/5666ff4f5077a7e2f72902f3a95f5d553ea0d88d:net/socket/transport_connect_job.cc;l=102) // Same as in Chromium (https://source.chromium.org/chromium/chromium/src/+/5666ff4f5077a7e2f72902f3a95f5d553ea0d88d:net/socket/transport_connect_job.cc;l=102)
const connectionAttemptDelayMs = 300; const connectionAttemptDelayMs = 300;
const kDNSLookupAt = Symbol('kDNSLookupAt') const kDNSLookupAt = Symbol('kDNSLookupAt');
const kTCPConnectionAt = Symbol('kTCPConnectionAt') const kTCPConnectionAt = Symbol('kTCPConnectionAt');
class HttpHappyEyeballsAgent extends http.Agent { class HttpHappyEyeballsAgent extends http.Agent {
createConnection(options: http.ClientRequestArgs, oncreate?: (err: Error | null, socket?: net.Socket) => void): net.Socket | undefined { createConnection(options: http.ClientRequestArgs, oncreate?: (err: Error | null, socket?: net.Socket) => void): net.Socket | undefined {
@ -75,7 +75,7 @@ export async function createTLSSocket(options: tls.ConnectionOptions): Promise<t
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
assert(options.host, 'host is required'); assert(options.host, 'host is required');
if (net.isIP(options.host)) { if (net.isIP(options.host)) {
const socket = tls.connect(options) const socket = tls.connect(options);
socket.on('secureConnect', () => resolve(socket)); socket.on('secureConnect', () => resolve(socket));
socket.on('error', error => reject(error)); socket.on('error', error => reject(error));
} else { } else {
@ -202,5 +202,5 @@ export function timingForSocket(socket: net.Socket | tls.TLSSocket) {
return { return {
dnsLookupAt: (socket as any)[kDNSLookupAt] as number | undefined, dnsLookupAt: (socket as any)[kDNSLookupAt] as number | undefined,
tcpConnectionAt: (socket as any)[kTCPConnectionAt] as number | undefined, tcpConnectionAt: (socket as any)[kTCPConnectionAt] as number | undefined,
} };
} }

View file

@ -163,7 +163,7 @@ function innerAsLocators(factory: LocatorFactory, parsed: ParsedSelector, isFram
continue; continue;
} }
let locatorType: LocatorType = 'default'; const locatorType: LocatorType = 'default';
const nextPart = parts[index + 1]; const nextPart = parts[index + 1];

View file

@ -130,6 +130,15 @@ export function traceParamsForAction(actionInContext: recorderActions.ActionInCo
}; };
return { method: 'expect', params }; return { method: 'expect', params };
} }
case 'assertSnapshot': {
const params: channels.FrameExpectParams = {
selector,
expression: 'to.match.snapshot',
expectedText: [],
isNot: false,
};
return { method: 'expect', params };
}
} }
} }

View file

@ -0,0 +1,66 @@
/**
* 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.
*/
export function findRepeatedSubsequences(s: string[]): { sequence: string[]; count: number }[] {
const n = s.length;
const result = [];
let i = 0;
const arraysEqual = (a1: string[], a2: string[]) => {
if (a1.length !== a2.length)
return false;
for (let j = 0; j < a1.length; j++) {
if (a1[j] !== a2[j])
return false;
}
return true;
};
while (i < n) {
let maxRepeatCount = 1;
let maxRepeatSubstr = [s[i]]; // Initialize with the element at index i
let maxRepeatLength = 1;
// Try substrings of length from 1 to the remaining length of the array
for (let p = 1; p <= n - i; p++) {
const substr = s.slice(i, i + p); // Extract substring as array
let k = 1;
// Count how many times the substring repeats consecutively
while (
i + p * k <= n &&
arraysEqual(s.slice(i + p * (k - 1), i + p * k), substr)
)
k += 1;
k -= 1; // Adjust k since it increments one extra time in the loop
// Update the maximal repeating substring if necessary
if (k > 1 && (k * p) > (maxRepeatCount * maxRepeatLength)) {
maxRepeatCount = k;
maxRepeatSubstr = substr;
maxRepeatLength = p;
}
}
// Record the substring and its count
result.push({ sequence: maxRepeatSubstr, count: maxRepeatCount });
i += maxRepeatLength * maxRepeatCount; // Move index forward
}
return result;
}

View file

@ -16,9 +16,9 @@
import path from 'path'; import path from 'path';
import { parseStackTraceLine } from '../utilsBundle'; import { parseStackTraceLine } from '../utilsBundle';
import { isUnderTest } from './';
import type { StackFrame } from '@protocol/channels'; import type { StackFrame } from '@protocol/channels';
import { colors } from '../utilsBundle'; import { colors } from '../utilsBundle';
import { findRepeatedSubsequences } from './sequence';
export function rewriteErrorMessage<E extends Error>(e: E, newMessage: string): E { export function rewriteErrorMessage<E extends Error>(e: E, newMessage: string): E {
const lines: string[] = (e.stack?.split('\n') || []).filter(l => l.startsWith(' at ')); const lines: string[] = (e.stack?.split('\n') || []).filter(l => l.startsWith(' at '));
@ -50,7 +50,6 @@ export function captureRawStack(): RawStack {
export function captureLibraryStackTrace(): { frames: StackFrame[], apiName: string } { export function captureLibraryStackTrace(): { frames: StackFrame[], apiName: string } {
const stack = captureRawStack(); const stack = captureRawStack();
const isTesting = isUnderTest();
type ParsedFrame = { type ParsedFrame = {
frame: StackFrame; frame: StackFrame;
frameText: string; frameText: string;
@ -132,9 +131,26 @@ export function splitErrorMessage(message: string): { name: string, message: str
export function formatCallLog(log: string[] | undefined): string { export function formatCallLog(log: string[] | undefined): string {
if (!log || !log.some(l => !!l)) if (!log || !log.some(l => !!l))
return ''; return '';
const lines: string[] = [];
for (const block of findRepeatedSubsequences(log)) {
for (let i = 0; i < block.sequence.length; i++) {
const line = block.sequence[i];
const leadingWhitespace = line.match(/^\s*/);
const whitespacePrefix = ' ' + leadingWhitespace?.[0] || '';
const countPrefix = `${block.count} × `;
if (block.count > 1 && i === 0)
lines.push(whitespacePrefix + countPrefix + line.trim());
else if (block.count > 1)
lines.push(whitespacePrefix + ' '.repeat(countPrefix.length - 2) + '- ' + line.trim());
else
lines.push(whitespacePrefix + '- ' + line.trim());
}
}
return ` return `
Call log: Call log:
${colors.dim('- ' + (log || []).join('\n - '))} ${colors.dim(lines.join('\n'))}
`; `;
} }

View file

@ -48,6 +48,7 @@ class ZoneManager {
zones.push(str); zones.push(str);
} }
// eslint-disable-next-line no-console
console.log('zones: ', zones.join(' -> ')); console.log('zones: ', zones.join(' -> '));
} }
} }

View file

@ -31,6 +31,7 @@ export const PNG: typeof import('../bundles/utils/node_modules/@types/pngjs').PN
export const program: typeof import('../bundles/utils/node_modules/commander').program = require('./utilsBundleImpl').program; export const program: typeof import('../bundles/utils/node_modules/commander').program = require('./utilsBundleImpl').program;
export const progress: typeof import('../bundles/utils/node_modules/@types/progress') = require('./utilsBundleImpl').progress; export const progress: typeof import('../bundles/utils/node_modules/@types/progress') = require('./utilsBundleImpl').progress;
export const SocksProxyAgent: typeof import('../bundles/utils/node_modules/socks-proxy-agent').SocksProxyAgent = require('./utilsBundleImpl').SocksProxyAgent; export const SocksProxyAgent: typeof import('../bundles/utils/node_modules/socks-proxy-agent').SocksProxyAgent = require('./utilsBundleImpl').SocksProxyAgent;
export const yaml: typeof import('../bundles/utils/node_modules/yaml') = require('./utilsBundleImpl').yaml;
export const ws: typeof import('../bundles/utils/node_modules/@types/ws') = require('./utilsBundleImpl').ws; export const ws: typeof import('../bundles/utils/node_modules/@types/ws') = require('./utilsBundleImpl').ws;
export const wsServer: typeof import('../bundles/utils/node_modules/@types/ws').WebSocketServer = require('./utilsBundleImpl').wsServer; export const wsServer: typeof import('../bundles/utils/node_modules/@types/ws').WebSocketServer = require('./utilsBundleImpl').wsServer;
export const wsReceiver = require('./utilsBundleImpl').wsReceiver; export const wsReceiver = require('./utilsBundleImpl').wsReceiver;

View file

@ -695,7 +695,7 @@ percentage [0 - 100] for scroll driven animations
frameId: Page.FrameId; frameId: Page.FrameId;
} }
export type CookieExclusionReason = "ExcludeSameSiteUnspecifiedTreatedAsLax"|"ExcludeSameSiteNoneInsecure"|"ExcludeSameSiteLax"|"ExcludeSameSiteStrict"|"ExcludeInvalidSameParty"|"ExcludeSamePartyCrossPartyContext"|"ExcludeDomainNonASCII"|"ExcludeThirdPartyCookieBlockedInFirstPartySet"|"ExcludeThirdPartyPhaseout"; export type CookieExclusionReason = "ExcludeSameSiteUnspecifiedTreatedAsLax"|"ExcludeSameSiteNoneInsecure"|"ExcludeSameSiteLax"|"ExcludeSameSiteStrict"|"ExcludeInvalidSameParty"|"ExcludeSamePartyCrossPartyContext"|"ExcludeDomainNonASCII"|"ExcludeThirdPartyCookieBlockedInFirstPartySet"|"ExcludeThirdPartyPhaseout";
export type CookieWarningReason = "WarnSameSiteUnspecifiedCrossSiteContext"|"WarnSameSiteNoneInsecure"|"WarnSameSiteUnspecifiedLaxAllowUnsafe"|"WarnSameSiteStrictLaxDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeLax"|"WarnSameSiteLaxCrossDowngradeStrict"|"WarnSameSiteLaxCrossDowngradeLax"|"WarnAttributeValueExceedsMaxSize"|"WarnDomainNonASCII"|"WarnThirdPartyPhaseout"|"WarnCrossSiteRedirectDowngradeChangesInclusion"; export type CookieWarningReason = "WarnSameSiteUnspecifiedCrossSiteContext"|"WarnSameSiteNoneInsecure"|"WarnSameSiteUnspecifiedLaxAllowUnsafe"|"WarnSameSiteStrictLaxDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeLax"|"WarnSameSiteLaxCrossDowngradeStrict"|"WarnSameSiteLaxCrossDowngradeLax"|"WarnAttributeValueExceedsMaxSize"|"WarnDomainNonASCII"|"WarnThirdPartyPhaseout"|"WarnCrossSiteRedirectDowngradeChangesInclusion"|"WarnDeprecationTrialMetadata"|"WarnThirdPartyCookieHeuristic";
export type CookieOperation = "SetCookie"|"ReadCookie"; export type CookieOperation = "SetCookie"|"ReadCookie";
/** /**
* This information is currently necessary, as the front-end has a difficult * This information is currently necessary, as the front-end has a difficult
@ -934,7 +934,7 @@ Should be updated alongside RequestIdTokenStatus in
third_party/blink/public/mojom/devtools/inspector_issue.mojom to include third_party/blink/public/mojom/devtools/inspector_issue.mojom to include
all cases except for success. all cases except for success.
*/ */
export type FederatedAuthRequestIssueReason = "ShouldEmbargo"|"TooManyRequests"|"WellKnownHttpNotFound"|"WellKnownNoResponse"|"WellKnownInvalidResponse"|"WellKnownListEmpty"|"WellKnownInvalidContentType"|"ConfigNotInWellKnown"|"WellKnownTooBig"|"ConfigHttpNotFound"|"ConfigNoResponse"|"ConfigInvalidResponse"|"ConfigInvalidContentType"|"ClientMetadataHttpNotFound"|"ClientMetadataNoResponse"|"ClientMetadataInvalidResponse"|"ClientMetadataInvalidContentType"|"IdpNotPotentiallyTrustworthy"|"DisabledInSettings"|"DisabledInFlags"|"ErrorFetchingSignin"|"InvalidSigninResponse"|"AccountsHttpNotFound"|"AccountsNoResponse"|"AccountsInvalidResponse"|"AccountsListEmpty"|"AccountsInvalidContentType"|"IdTokenHttpNotFound"|"IdTokenNoResponse"|"IdTokenInvalidResponse"|"IdTokenIdpErrorResponse"|"IdTokenCrossSiteIdpErrorResponse"|"IdTokenInvalidRequest"|"IdTokenInvalidContentType"|"ErrorIdToken"|"Canceled"|"RpPageNotVisible"|"SilentMediationFailure"|"ThirdPartyCookiesBlocked"|"NotSignedInWithIdp"|"MissingTransientUserActivation"|"ReplacedByButtonMode"|"InvalidFieldsSpecified"|"RelyingPartyOriginIsOpaque"|"TypeNotMatching"; export type FederatedAuthRequestIssueReason = "ShouldEmbargo"|"TooManyRequests"|"WellKnownHttpNotFound"|"WellKnownNoResponse"|"WellKnownInvalidResponse"|"WellKnownListEmpty"|"WellKnownInvalidContentType"|"ConfigNotInWellKnown"|"WellKnownTooBig"|"ConfigHttpNotFound"|"ConfigNoResponse"|"ConfigInvalidResponse"|"ConfigInvalidContentType"|"ClientMetadataHttpNotFound"|"ClientMetadataNoResponse"|"ClientMetadataInvalidResponse"|"ClientMetadataInvalidContentType"|"IdpNotPotentiallyTrustworthy"|"DisabledInSettings"|"DisabledInFlags"|"ErrorFetchingSignin"|"InvalidSigninResponse"|"AccountsHttpNotFound"|"AccountsNoResponse"|"AccountsInvalidResponse"|"AccountsListEmpty"|"AccountsInvalidContentType"|"IdTokenHttpNotFound"|"IdTokenNoResponse"|"IdTokenInvalidResponse"|"IdTokenIdpErrorResponse"|"IdTokenCrossSiteIdpErrorResponse"|"IdTokenInvalidRequest"|"IdTokenInvalidContentType"|"ErrorIdToken"|"Canceled"|"RpPageNotVisible"|"SilentMediationFailure"|"ThirdPartyCookiesBlocked"|"NotSignedInWithIdp"|"MissingTransientUserActivation"|"ReplacedByActiveMode"|"InvalidFieldsSpecified"|"RelyingPartyOriginIsOpaque"|"TypeNotMatching";
export interface FederatedAuthUserInfoRequestIssueDetails { export interface FederatedAuthUserInfoRequestIssueDetails {
federatedAuthUserInfoRequestIssueReason: FederatedAuthUserInfoRequestIssueReason; federatedAuthUserInfoRequestIssueReason: FederatedAuthUserInfoRequestIssueReason;
} }
@ -5989,7 +5989,7 @@ Missing optional values will be filled in by the target with what it would norma
* Used to specify sensor types to emulate. * Used to specify sensor types to emulate.
See https://w3c.github.io/sensors/#automation for more information. See https://w3c.github.io/sensors/#automation for more information.
*/ */
export type SensorType = "absolute-orientation"|"accelerometer"|"ambient-light"|"gravity"|"gyroscope"|"linear-acceleration"|"magnetometer"|"proximity"|"relative-orientation"; export type SensorType = "absolute-orientation"|"accelerometer"|"ambient-light"|"gravity"|"gyroscope"|"linear-acceleration"|"magnetometer"|"relative-orientation";
export interface SensorMetadata { export interface SensorMetadata {
available?: boolean; available?: boolean;
minimumFrequency?: number; minimumFrequency?: number;
@ -11397,7 +11397,7 @@ Backend then generates 'inspectNodeRequested' event upon element selection.
export type setShowHitTestBordersReturnValue = { export type setShowHitTestBordersReturnValue = {
} }
/** /**
* Request that backend shows an overlay with web vital metrics. * Deprecated, no longer has any effect.
*/ */
export type setShowWebVitalsParameters = { export type setShowWebVitalsParameters = {
show: boolean; show: boolean;
@ -11498,7 +11498,7 @@ as an ad.
* All Permissions Policy features. This enum should match the one defined * All Permissions Policy features. This enum should match the one defined
in third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5. in third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5.
*/ */
export type PermissionsPolicyFeature = "accelerometer"|"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"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"media-playback-while-not-visible"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"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"|"digital-credentials-get"|"direct-sockets"|"direct-sockets-private"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"media-playback-while-not-visible"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"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. * Reason for a permissions policy feature to be disabled.
*/ */
@ -12086,7 +12086,7 @@ https://github.com/WICG/manifest-incubations/blob/gh-pages/scope_extensions-expl
/** /**
* List of not restored reasons for back-forward cache. * List of not restored reasons for back-forward cache.
*/ */
export type BackForwardCacheNotRestoredReason = "NotPrimaryMainFrame"|"BackForwardCacheDisabled"|"RelatedActiveContentsExist"|"HTTPStatusNotOK"|"SchemeNotHTTPOrHTTPS"|"Loading"|"WasGrantedMediaAccess"|"DisableForRenderFrameHostCalled"|"DomainNotAllowed"|"HTTPMethodNotGET"|"SubframeIsNavigating"|"Timeout"|"CacheLimit"|"JavaScriptExecution"|"RendererProcessKilled"|"RendererProcessCrashed"|"SchedulerTrackedFeatureUsed"|"ConflictingBrowsingInstance"|"CacheFlushed"|"ServiceWorkerVersionActivation"|"SessionRestored"|"ServiceWorkerPostMessage"|"EnteredBackForwardCacheBeforeServiceWorkerHostAdded"|"RenderFrameHostReused_SameSite"|"RenderFrameHostReused_CrossSite"|"ServiceWorkerClaim"|"IgnoreEventAndEvict"|"HaveInnerContents"|"TimeoutPuttingInCache"|"BackForwardCacheDisabledByLowMemory"|"BackForwardCacheDisabledByCommandLine"|"NetworkRequestDatapipeDrainedAsBytesConsumer"|"NetworkRequestRedirected"|"NetworkRequestTimeout"|"NetworkExceedsBufferLimit"|"NavigationCancelledWhileRestoring"|"NotMostRecentNavigationEntry"|"BackForwardCacheDisabledForPrerender"|"UserAgentOverrideDiffers"|"ForegroundCacheLimit"|"BrowsingInstanceNotSwapped"|"BackForwardCacheDisabledForDelegate"|"UnloadHandlerExistsInMainFrame"|"UnloadHandlerExistsInSubFrame"|"ServiceWorkerUnregistration"|"CacheControlNoStore"|"CacheControlNoStoreCookieModified"|"CacheControlNoStoreHTTPOnlyCookieModified"|"NoResponseHead"|"Unknown"|"ActivationNavigationsDisallowedForBug1234857"|"ErrorDocument"|"FencedFramesEmbedder"|"CookieDisabled"|"HTTPAuthRequired"|"CookieFlushed"|"BroadcastChannelOnMessage"|"WebViewSettingsChanged"|"WebViewJavaScriptObjectChanged"|"WebViewMessageListenerInjected"|"WebViewSafeBrowsingAllowlistChanged"|"WebViewDocumentStartJavascriptChanged"|"WebSocket"|"WebTransport"|"WebRTC"|"MainResourceHasCacheControlNoStore"|"MainResourceHasCacheControlNoCache"|"SubresourceHasCacheControlNoStore"|"SubresourceHasCacheControlNoCache"|"ContainsPlugins"|"DocumentLoaded"|"OutstandingNetworkRequestOthers"|"RequestedMIDIPermission"|"RequestedAudioCapturePermission"|"RequestedVideoCapturePermission"|"RequestedBackForwardCacheBlockedSensors"|"RequestedBackgroundWorkPermission"|"BroadcastChannel"|"WebXR"|"SharedWorker"|"WebLocks"|"WebHID"|"WebShare"|"RequestedStorageAccessGrant"|"WebNfc"|"OutstandingNetworkRequestFetch"|"OutstandingNetworkRequestXHR"|"AppBanner"|"Printing"|"WebDatabase"|"PictureInPicture"|"SpeechRecognizer"|"IdleManager"|"PaymentManager"|"SpeechSynthesis"|"KeyboardLock"|"WebOTPService"|"OutstandingNetworkRequestDirectSocket"|"InjectedJavascript"|"InjectedStyleSheet"|"KeepaliveRequest"|"IndexedDBEvent"|"Dummy"|"JsNetworkRequestReceivedCacheControlNoStoreResource"|"WebRTCSticky"|"WebTransportSticky"|"WebSocketSticky"|"SmartCard"|"LiveMediaStreamTrack"|"UnloadHandler"|"ParserAborted"|"ContentSecurityHandler"|"ContentWebAuthenticationAPI"|"ContentFileChooser"|"ContentSerial"|"ContentFileSystemAccess"|"ContentMediaDevicesDispatcherHost"|"ContentWebBluetooth"|"ContentWebUSB"|"ContentMediaSessionService"|"ContentScreenReader"|"ContentDiscarded"|"EmbedderPopupBlockerTabHelper"|"EmbedderSafeBrowsingTriggeredPopupBlocker"|"EmbedderSafeBrowsingThreatDetails"|"EmbedderAppBannerManager"|"EmbedderDomDistillerViewerSource"|"EmbedderDomDistillerSelfDeletingRequestDelegate"|"EmbedderOomInterventionTabHelper"|"EmbedderOfflinePage"|"EmbedderChromePasswordManagerClientBindCredentialManager"|"EmbedderPermissionRequestManager"|"EmbedderModalDialog"|"EmbedderExtensions"|"EmbedderExtensionMessaging"|"EmbedderExtensionMessagingForOpenPort"|"EmbedderExtensionSentMessageToCachedFrame"|"RequestedByWebViewClient"; export type BackForwardCacheNotRestoredReason = "NotPrimaryMainFrame"|"BackForwardCacheDisabled"|"RelatedActiveContentsExist"|"HTTPStatusNotOK"|"SchemeNotHTTPOrHTTPS"|"Loading"|"WasGrantedMediaAccess"|"DisableForRenderFrameHostCalled"|"DomainNotAllowed"|"HTTPMethodNotGET"|"SubframeIsNavigating"|"Timeout"|"CacheLimit"|"JavaScriptExecution"|"RendererProcessKilled"|"RendererProcessCrashed"|"SchedulerTrackedFeatureUsed"|"ConflictingBrowsingInstance"|"CacheFlushed"|"ServiceWorkerVersionActivation"|"SessionRestored"|"ServiceWorkerPostMessage"|"EnteredBackForwardCacheBeforeServiceWorkerHostAdded"|"RenderFrameHostReused_SameSite"|"RenderFrameHostReused_CrossSite"|"ServiceWorkerClaim"|"IgnoreEventAndEvict"|"HaveInnerContents"|"TimeoutPuttingInCache"|"BackForwardCacheDisabledByLowMemory"|"BackForwardCacheDisabledByCommandLine"|"NetworkRequestDatapipeDrainedAsBytesConsumer"|"NetworkRequestRedirected"|"NetworkRequestTimeout"|"NetworkExceedsBufferLimit"|"NavigationCancelledWhileRestoring"|"NotMostRecentNavigationEntry"|"BackForwardCacheDisabledForPrerender"|"UserAgentOverrideDiffers"|"ForegroundCacheLimit"|"BrowsingInstanceNotSwapped"|"BackForwardCacheDisabledForDelegate"|"UnloadHandlerExistsInMainFrame"|"UnloadHandlerExistsInSubFrame"|"ServiceWorkerUnregistration"|"CacheControlNoStore"|"CacheControlNoStoreCookieModified"|"CacheControlNoStoreHTTPOnlyCookieModified"|"NoResponseHead"|"Unknown"|"ActivationNavigationsDisallowedForBug1234857"|"ErrorDocument"|"FencedFramesEmbedder"|"CookieDisabled"|"HTTPAuthRequired"|"CookieFlushed"|"BroadcastChannelOnMessage"|"WebViewSettingsChanged"|"WebViewJavaScriptObjectChanged"|"WebViewMessageListenerInjected"|"WebViewSafeBrowsingAllowlistChanged"|"WebViewDocumentStartJavascriptChanged"|"WebSocket"|"WebTransport"|"WebRTC"|"MainResourceHasCacheControlNoStore"|"MainResourceHasCacheControlNoCache"|"SubresourceHasCacheControlNoStore"|"SubresourceHasCacheControlNoCache"|"ContainsPlugins"|"DocumentLoaded"|"OutstandingNetworkRequestOthers"|"RequestedMIDIPermission"|"RequestedAudioCapturePermission"|"RequestedVideoCapturePermission"|"RequestedBackForwardCacheBlockedSensors"|"RequestedBackgroundWorkPermission"|"BroadcastChannel"|"WebXR"|"SharedWorker"|"WebLocks"|"WebHID"|"WebShare"|"RequestedStorageAccessGrant"|"WebNfc"|"OutstandingNetworkRequestFetch"|"OutstandingNetworkRequestXHR"|"AppBanner"|"Printing"|"WebDatabase"|"PictureInPicture"|"SpeechRecognizer"|"IdleManager"|"PaymentManager"|"SpeechSynthesis"|"KeyboardLock"|"WebOTPService"|"OutstandingNetworkRequestDirectSocket"|"InjectedJavascript"|"InjectedStyleSheet"|"KeepaliveRequest"|"IndexedDBEvent"|"Dummy"|"JsNetworkRequestReceivedCacheControlNoStoreResource"|"WebRTCSticky"|"WebTransportSticky"|"WebSocketSticky"|"SmartCard"|"LiveMediaStreamTrack"|"UnloadHandler"|"ParserAborted"|"ContentSecurityHandler"|"ContentWebAuthenticationAPI"|"ContentFileChooser"|"ContentSerial"|"ContentFileSystemAccess"|"ContentMediaDevicesDispatcherHost"|"ContentWebBluetooth"|"ContentWebUSB"|"ContentMediaSessionService"|"ContentScreenReader"|"ContentDiscarded"|"EmbedderPopupBlockerTabHelper"|"EmbedderSafeBrowsingTriggeredPopupBlocker"|"EmbedderSafeBrowsingThreatDetails"|"EmbedderAppBannerManager"|"EmbedderDomDistillerViewerSource"|"EmbedderDomDistillerSelfDeletingRequestDelegate"|"EmbedderOomInterventionTabHelper"|"EmbedderOfflinePage"|"EmbedderChromePasswordManagerClientBindCredentialManager"|"EmbedderPermissionRequestManager"|"EmbedderModalDialog"|"EmbedderExtensions"|"EmbedderExtensionMessaging"|"EmbedderExtensionMessagingForOpenPort"|"EmbedderExtensionSentMessageToCachedFrame"|"RequestedByWebViewClient"|"PostMessageByWebViewClient";
/** /**
* Types of not restored reasons for back-forward cache. * Types of not restored reasons for back-forward cache.
*/ */
@ -16634,6 +16634,17 @@ flag set to this value. Defaults to the authenticator's
defaultBackupState value. defaultBackupState value.
*/ */
backupState?: boolean; backupState?: boolean;
/**
* The credential's user.name property. Equivalent to empty if not set.
https://w3c.github.io/webauthn/#dom-publickeycredentialentity-name
*/
userName?: string;
/**
* The credential's user.displayName property. Equivalent to empty if
not set.
https://w3c.github.io/webauthn/#dom-publickeycredentialuserentity-displayname
*/
userDisplayName?: string;
} }
/** /**
@ -16643,6 +16654,22 @@ defaultBackupState value.
authenticatorId: AuthenticatorId; authenticatorId: AuthenticatorId;
credential: Credential; credential: Credential;
} }
/**
* Triggered when a credential is deleted, e.g. through
PublicKeyCredential.signalUnknownCredential().
*/
export type credentialDeletedPayload = {
authenticatorId: AuthenticatorId;
credentialId: binary;
}
/**
* Triggered when a credential is updated, e.g. through
PublicKeyCredential.signalCurrentUserDetails().
*/
export type credentialUpdatedPayload = {
authenticatorId: AuthenticatorId;
credential: Credential;
}
/** /**
* Triggered when a credential is used in a webauthn assertion. * Triggered when a credential is used in a webauthn assertion.
*/ */
@ -17076,7 +17103,7 @@ possible for multiple rule sets and links to trigger a single attempt.
/** /**
* List of FinalStatus reasons for Prerender2. * List of FinalStatus reasons for Prerender2.
*/ */
export type PrerenderFinalStatus = "Activated"|"Destroyed"|"LowEndDevice"|"InvalidSchemeRedirect"|"InvalidSchemeNavigation"|"NavigationRequestBlockedByCsp"|"MainFrameNavigation"|"MojoBinderPolicy"|"RendererProcessCrashed"|"RendererProcessKilled"|"Download"|"TriggerDestroyed"|"NavigationNotCommitted"|"NavigationBadHttpStatus"|"ClientCertRequested"|"NavigationRequestNetworkError"|"CancelAllHostsForTesting"|"DidFailLoad"|"Stop"|"SslCertificateError"|"LoginAuthRequested"|"UaChangeRequiresReload"|"BlockedByClient"|"AudioOutputDeviceRequested"|"MixedContent"|"TriggerBackgrounded"|"MemoryLimitExceeded"|"DataSaverEnabled"|"TriggerUrlHasEffectiveUrl"|"ActivatedBeforeStarted"|"InactivePageRestriction"|"StartFailed"|"TimeoutBackgrounded"|"CrossSiteRedirectInInitialNavigation"|"CrossSiteNavigationInInitialNavigation"|"SameSiteCrossOriginRedirectNotOptInInInitialNavigation"|"SameSiteCrossOriginNavigationNotOptInInInitialNavigation"|"ActivationNavigationParameterMismatch"|"ActivatedInBackground"|"EmbedderHostDisallowed"|"ActivationNavigationDestroyedBeforeSuccess"|"TabClosedByUserGesture"|"TabClosedWithoutUserGesture"|"PrimaryMainFrameRendererProcessCrashed"|"PrimaryMainFrameRendererProcessKilled"|"ActivationFramePolicyNotCompatible"|"PreloadingDisabled"|"BatterySaverEnabled"|"ActivatedDuringMainFrameNavigation"|"PreloadingUnsupportedByWebContents"|"CrossSiteRedirectInMainFrameNavigation"|"CrossSiteNavigationInMainFrameNavigation"|"SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation"|"SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation"|"MemoryPressureOnTrigger"|"MemoryPressureAfterTriggered"|"PrerenderingDisabledByDevTools"|"SpeculationRuleRemoved"|"ActivatedWithAuxiliaryBrowsingContexts"|"MaxNumOfRunningEagerPrerendersExceeded"|"MaxNumOfRunningNonEagerPrerendersExceeded"|"MaxNumOfRunningEmbedderPrerendersExceeded"|"PrerenderingUrlHasEffectiveUrl"|"RedirectedPrerenderingUrlHasEffectiveUrl"|"ActivationUrlHasEffectiveUrl"|"JavaScriptInterfaceAdded"|"JavaScriptInterfaceRemoved"|"AllPrerenderingCanceled"|"WindowClosed"|"SlowNetwork"|"OtherPrerenderedPageActivated"; export type PrerenderFinalStatus = "Activated"|"Destroyed"|"LowEndDevice"|"InvalidSchemeRedirect"|"InvalidSchemeNavigation"|"NavigationRequestBlockedByCsp"|"MainFrameNavigation"|"MojoBinderPolicy"|"RendererProcessCrashed"|"RendererProcessKilled"|"Download"|"TriggerDestroyed"|"NavigationNotCommitted"|"NavigationBadHttpStatus"|"ClientCertRequested"|"NavigationRequestNetworkError"|"CancelAllHostsForTesting"|"DidFailLoad"|"Stop"|"SslCertificateError"|"LoginAuthRequested"|"UaChangeRequiresReload"|"BlockedByClient"|"AudioOutputDeviceRequested"|"MixedContent"|"TriggerBackgrounded"|"MemoryLimitExceeded"|"DataSaverEnabled"|"TriggerUrlHasEffectiveUrl"|"ActivatedBeforeStarted"|"InactivePageRestriction"|"StartFailed"|"TimeoutBackgrounded"|"CrossSiteRedirectInInitialNavigation"|"CrossSiteNavigationInInitialNavigation"|"SameSiteCrossOriginRedirectNotOptInInInitialNavigation"|"SameSiteCrossOriginNavigationNotOptInInInitialNavigation"|"ActivationNavigationParameterMismatch"|"ActivatedInBackground"|"EmbedderHostDisallowed"|"ActivationNavigationDestroyedBeforeSuccess"|"TabClosedByUserGesture"|"TabClosedWithoutUserGesture"|"PrimaryMainFrameRendererProcessCrashed"|"PrimaryMainFrameRendererProcessKilled"|"ActivationFramePolicyNotCompatible"|"PreloadingDisabled"|"BatterySaverEnabled"|"ActivatedDuringMainFrameNavigation"|"PreloadingUnsupportedByWebContents"|"CrossSiteRedirectInMainFrameNavigation"|"CrossSiteNavigationInMainFrameNavigation"|"SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation"|"SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation"|"MemoryPressureOnTrigger"|"MemoryPressureAfterTriggered"|"PrerenderingDisabledByDevTools"|"SpeculationRuleRemoved"|"ActivatedWithAuxiliaryBrowsingContexts"|"MaxNumOfRunningEagerPrerendersExceeded"|"MaxNumOfRunningNonEagerPrerendersExceeded"|"MaxNumOfRunningEmbedderPrerendersExceeded"|"PrerenderingUrlHasEffectiveUrl"|"RedirectedPrerenderingUrlHasEffectiveUrl"|"ActivationUrlHasEffectiveUrl"|"JavaScriptInterfaceAdded"|"JavaScriptInterfaceRemoved"|"AllPrerenderingCanceled"|"WindowClosed"|"SlowNetwork"|"OtherPrerenderedPageActivated"|"V8OptimizerDisabled"|"PrerenderFailedDuringPrefetch";
/** /**
* Preloading status values, see also PreloadingTriggeringOutcome. This * Preloading status values, see also PreloadingTriggeringOutcome. This
status is shared by prefetchStatusUpdated and prerenderStatusUpdated. status is shared by prefetchStatusUpdated and prerenderStatusUpdated.
@ -17751,7 +17778,7 @@ variables as its properties.
/** /**
* Type of the debug symbols. * Type of the debug symbols.
*/ */
type: "None"|"SourceMap"|"EmbeddedDWARF"|"ExternalDWARF"; type: "SourceMap"|"EmbeddedDWARF"|"ExternalDWARF";
/** /**
* URL of the external symbol source. * URL of the external symbol source.
*/ */
@ -17955,9 +17982,9 @@ scripts upon enabling debugger.
*/ */
scriptLanguage?: Debugger.ScriptLanguage; scriptLanguage?: Debugger.ScriptLanguage;
/** /**
* If the scriptLanguage is WebASsembly, the source of debug symbols for the module. * If the scriptLanguage is WebAssembly, the source of debug symbols for the module.
*/ */
debugSymbols?: Debugger.DebugSymbols; debugSymbols?: Debugger.DebugSymbols[];
/** /**
* The name the embedder supplied for this script. * The name the embedder supplied for this script.
*/ */
@ -18280,6 +18307,19 @@ call stacks (default).
} }
export type setAsyncCallStackDepthReturnValue = { export type setAsyncCallStackDepthReturnValue = {
} }
/**
* Replace previous blackbox execution contexts with passed ones. Forces backend to skip
stepping/pausing in scripts in these execution contexts. VM will try to leave blackboxed script by
performing 'step in' several times, finally resorting to 'step out' if unsuccessful.
*/
export type setBlackboxExecutionContextsParameters = {
/**
* Array of execution context unique ids for the debugger to ignore.
*/
uniqueIds: string[];
}
export type setBlackboxExecutionContextsReturnValue = {
}
/** /**
* Replace previous blackbox patterns with passed ones. Forces backend to skip stepping/pausing in * Replace previous blackbox patterns with passed ones. Forces backend to skip stepping/pausing in
scripts with url matching one of the patterns. VM will try to leave blackboxed script by scripts with url matching one of the patterns. VM will try to leave blackboxed script by
@ -18290,6 +18330,10 @@ performing 'step in' several times, finally resorting to 'step out' if unsuccess
* Array of regexps that will be used to check script url for blackbox state. * Array of regexps that will be used to check script url for blackbox state.
*/ */
patterns: string[]; patterns: string[];
/**
* If true, also ignore scripts with no source url.
*/
skipAnonymous?: boolean;
} }
export type setBlackboxPatternsReturnValue = { export type setBlackboxPatternsReturnValue = {
} }
@ -20310,6 +20354,8 @@ Error was thrown.
"WebAudio.nodeParamConnected": WebAudio.nodeParamConnectedPayload; "WebAudio.nodeParamConnected": WebAudio.nodeParamConnectedPayload;
"WebAudio.nodeParamDisconnected": WebAudio.nodeParamDisconnectedPayload; "WebAudio.nodeParamDisconnected": WebAudio.nodeParamDisconnectedPayload;
"WebAuthn.credentialAdded": WebAuthn.credentialAddedPayload; "WebAuthn.credentialAdded": WebAuthn.credentialAddedPayload;
"WebAuthn.credentialDeleted": WebAuthn.credentialDeletedPayload;
"WebAuthn.credentialUpdated": WebAuthn.credentialUpdatedPayload;
"WebAuthn.credentialAsserted": WebAuthn.credentialAssertedPayload; "WebAuthn.credentialAsserted": WebAuthn.credentialAssertedPayload;
"Media.playerPropertiesChanged": Media.playerPropertiesChangedPayload; "Media.playerPropertiesChanged": Media.playerPropertiesChangedPayload;
"Media.playerEventsAdded": Media.playerEventsAddedPayload; "Media.playerEventsAdded": Media.playerEventsAddedPayload;
@ -20897,6 +20943,7 @@ Error was thrown.
"Debugger.resume": Debugger.resumeParameters; "Debugger.resume": Debugger.resumeParameters;
"Debugger.searchInContent": Debugger.searchInContentParameters; "Debugger.searchInContent": Debugger.searchInContentParameters;
"Debugger.setAsyncCallStackDepth": Debugger.setAsyncCallStackDepthParameters; "Debugger.setAsyncCallStackDepth": Debugger.setAsyncCallStackDepthParameters;
"Debugger.setBlackboxExecutionContexts": Debugger.setBlackboxExecutionContextsParameters;
"Debugger.setBlackboxPatterns": Debugger.setBlackboxPatternsParameters; "Debugger.setBlackboxPatterns": Debugger.setBlackboxPatternsParameters;
"Debugger.setBlackboxedRanges": Debugger.setBlackboxedRangesParameters; "Debugger.setBlackboxedRanges": Debugger.setBlackboxedRangesParameters;
"Debugger.setBreakpoint": Debugger.setBreakpointParameters; "Debugger.setBreakpoint": Debugger.setBreakpointParameters;
@ -21507,6 +21554,7 @@ Error was thrown.
"Debugger.resume": Debugger.resumeReturnValue; "Debugger.resume": Debugger.resumeReturnValue;
"Debugger.searchInContent": Debugger.searchInContentReturnValue; "Debugger.searchInContent": Debugger.searchInContentReturnValue;
"Debugger.setAsyncCallStackDepth": Debugger.setAsyncCallStackDepthReturnValue; "Debugger.setAsyncCallStackDepth": Debugger.setAsyncCallStackDepthReturnValue;
"Debugger.setBlackboxExecutionContexts": Debugger.setBlackboxExecutionContextsReturnValue;
"Debugger.setBlackboxPatterns": Debugger.setBlackboxPatternsReturnValue; "Debugger.setBlackboxPatterns": Debugger.setBlackboxPatternsReturnValue;
"Debugger.setBlackboxedRanges": Debugger.setBlackboxedRangesReturnValue; "Debugger.setBlackboxedRanges": Debugger.setBlackboxedRangesReturnValue;
"Debugger.setBreakpoint": Debugger.setBreakpointReturnValue; "Debugger.setBreakpoint": Debugger.setBreakpointReturnValue;

View file

@ -12424,6 +12424,57 @@ export interface Locator {
*/ */
and(locator: Locator): Locator; and(locator: Locator): Locator;
/**
* Captures the aria snapshot of the given element. See
* [expect(locator).toMatchAriaSnapshot(expected[, options])](https://playwright.dev/docs/api/class-locatorassertions#locator-assertions-to-match-aria-snapshot)
* for the corresponding assertion.
*
* **Usage**
*
* ```js
* await page.getByRole('link').ariaSnapshot();
* ```
*
* **Details**
*
* This method captures the aria snapshot of the given element. The snapshot is a string that represents the state of
* the element and its children. The snapshot can be used to assert the state of the element in the test, or to
* compare it to state in the future.
*
* The ARIA snapshot is represented using [YAML](https://yaml.org/spec/1.2.2/) markup language:
* - The keys of the objects are the roles and optional accessible names of the elements.
* - The values are either text content or an array of child elements.
* - Generic static text can be represented with the `text` key.
*
* Below is the HTML markup and the respective ARIA snapshot:
*
* ```html
* <ul aria-label="Links">
* <li><a href="/">Home</a></li>
* <li><a href="/about">About</a></li>
* <ul>
* ```
*
* ```yml
* - list "Links":
* - listitem:
* - link "Home"
* - listitem:
* - link "About"
* ```
*
* @param options
*/
ariaSnapshot(options?: {
/**
* Maximum time in milliseconds. Defaults to `0` - no timeout. The default value can be changed via `actionTimeout`
* option in the config, or by using the
* [browserContext.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout)
* or [page.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-timeout) methods.
*/
timeout?: number;
}): Promise<string>;
/** /**
* Calls [blur](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/blur) on the element. * Calls [blur](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/blur) on the element.
* @param options * @param options

View file

@ -155,7 +155,6 @@ This project incorporates components from the projects listed below. The origina
- undici-types@6.19.8 (https://github.com/nodejs/undici) - undici-types@6.19.8 (https://github.com/nodejs/undici)
- update-browserslist-db@1.0.13 (https://github.com/browserslist/update-db) - update-browserslist-db@1.0.13 (https://github.com/browserslist/update-db)
- yallist@3.1.1 (https://github.com/isaacs/yallist) - yallist@3.1.1 (https://github.com/isaacs/yallist)
- yaml@2.5.1 (https://github.com/eemeli/yaml)
%% @ampproject/remapping@2.2.1 NOTICES AND INFORMATION BEGIN HERE %% @ampproject/remapping@2.2.1 NOTICES AND INFORMATION BEGIN HERE
========================================= =========================================
@ -4398,26 +4397,8 @@ IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
========================================= =========================================
END OF yallist@3.1.1 AND INFORMATION END OF yallist@3.1.1 AND INFORMATION
%% yaml@2.5.1 NOTICES AND INFORMATION BEGIN HERE
=========================================
Copyright Eemeli Aro <eemeli@gmail.com>
Permission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted, provided that the above copyright notice
and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.
=========================================
END OF yaml@2.5.1 AND INFORMATION
SUMMARY BEGIN HERE SUMMARY BEGIN HERE
========================================= =========================================
Total Packages: 152 Total Packages: 151
========================================= =========================================
END OF SUMMARY END OF SUMMARY

View file

@ -13,8 +13,7 @@
"json5": "2.2.3", "json5": "2.2.3",
"pirates": "4.0.4", "pirates": "4.0.4",
"source-map-support": "0.5.21", "source-map-support": "0.5.21",
"stoppable": "1.1.0", "stoppable": "1.1.0"
"yaml": "^2.5.1"
}, },
"devDependencies": { "devDependencies": {
"@types/source-map-support": "^0.5.4", "@types/source-map-support": "^0.5.4",
@ -281,17 +280,6 @@
"engines": { "engines": {
"node": ">=8.0" "node": ">=8.0"
} }
},
"node_modules/yaml": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz",
"integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14"
}
} }
}, },
"dependencies": { "dependencies": {
@ -476,11 +464,6 @@
"requires": { "requires": {
"is-number": "^7.0.0" "is-number": "^7.0.0"
} }
},
"yaml": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz",
"integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q=="
} }
} }
} }

View file

@ -14,8 +14,7 @@
"json5": "2.2.3", "json5": "2.2.3",
"pirates": "4.0.4", "pirates": "4.0.4",
"source-map-support": "0.5.21", "source-map-support": "0.5.21",
"stoppable": "1.1.0", "stoppable": "1.1.0"
"yaml": "^2.5.1"
}, },
"devDependencies": { "devDependencies": {
"@types/source-map-support": "^0.5.4", "@types/source-map-support": "^0.5.4",

View file

@ -31,6 +31,3 @@ export const enquirer = enquirerLibrary;
import chokidarLibrary from 'chokidar'; import chokidarLibrary from 'chokidar';
export const chokidar = chokidarLibrary; export const chokidar = chokidarLibrary;
import yamlLibrary from 'yaml';
export const yaml = yamlLibrary;

View file

@ -58,6 +58,9 @@ export class FullConfigInternal {
testIdMatcher?: Matcher; testIdMatcher?: Matcher;
defineConfigWasUsed = false; defineConfigWasUsed = false;
globalSetups: string[] = [];
globalTeardowns: string[] = [];
constructor(location: ConfigLocation, userConfig: Config, configCLIOverrides: ConfigCLIOverrides) { constructor(location: ConfigLocation, userConfig: Config, configCLIOverrides: ConfigCLIOverrides) {
if (configCLIOverrides.projects && userConfig.projects) if (configCLIOverrides.projects && userConfig.projects)
throw new Error(`Cannot use --browser option when configuration file defines projects. Specify browserName in the projects instead.`); throw new Error(`Cannot use --browser option when configuration file defines projects. Specify browserName in the projects instead.`);
@ -72,13 +75,16 @@ export class FullConfigInternal {
this.plugins = (privateConfiguration?.plugins || []).map((p: any) => ({ factory: p })); this.plugins = (privateConfiguration?.plugins || []).map((p: any) => ({ factory: p }));
this.singleTSConfigPath = pathResolve(configDir, userConfig.tsconfig); this.singleTSConfigPath = pathResolve(configDir, userConfig.tsconfig);
this.globalSetups = (Array.isArray(userConfig.globalSetup) ? userConfig.globalSetup : [userConfig.globalSetup]).map(s => resolveScript(s, configDir)).filter(script => script !== undefined);
this.globalTeardowns = (Array.isArray(userConfig.globalTeardown) ? userConfig.globalTeardown : [userConfig.globalTeardown]).map(s => resolveScript(s, configDir)).filter(script => script !== undefined);
this.config = { this.config = {
configFile: resolvedConfigFile, configFile: resolvedConfigFile,
rootDir: pathResolve(configDir, userConfig.testDir) || configDir, rootDir: pathResolve(configDir, userConfig.testDir) || configDir,
forbidOnly: takeFirst(configCLIOverrides.forbidOnly, userConfig.forbidOnly, false), forbidOnly: takeFirst(configCLIOverrides.forbidOnly, userConfig.forbidOnly, false),
fullyParallel: takeFirst(configCLIOverrides.fullyParallel, userConfig.fullyParallel, false), fullyParallel: takeFirst(configCLIOverrides.fullyParallel, userConfig.fullyParallel, false),
globalSetup: takeFirst(resolveScript(userConfig.globalSetup, configDir), null), globalSetup: this.globalSetups[0] ?? null,
globalTeardown: takeFirst(resolveScript(userConfig.globalTeardown, configDir), null), globalTeardown: this.globalTeardowns[0] ?? null,
globalTimeout: takeFirst(configCLIOverrides.globalTimeout, userConfig.globalTimeout, 0), globalTimeout: takeFirst(configCLIOverrides.globalTimeout, userConfig.globalTimeout, 0),
grep: takeFirst(userConfig.grep, defaultGrep), grep: takeFirst(userConfig.grep, defaultGrep),
grepInvert: takeFirst(userConfig.grepInvert, null), grepInvert: takeFirst(userConfig.grepInvert, null),
@ -270,7 +276,7 @@ export function toReporters(reporters: BuiltInReporter | ReporterDescription[] |
return reporters; return reporters;
} }
export const builtInReporters = ['list', 'line', 'dot', 'json', 'junit', 'null', 'github', 'html', 'blob', 'markdown'] as const; export const builtInReporters = ['list', 'line', 'dot', 'json', 'junit', 'null', 'github', 'html', 'blob'] as const;
export type BuiltInReporter = typeof builtInReporters[number]; export type BuiltInReporter = typeof builtInReporters[number];
export type ContextReuseMode = 'none' | 'when-possible'; export type ContextReuseMode = 'none' | 'when-possible';

View file

@ -139,14 +139,26 @@ function validateConfig(file: string, config: Config) {
} }
if ('globalSetup' in config && config.globalSetup !== undefined) { if ('globalSetup' in config && config.globalSetup !== undefined) {
if (typeof config.globalSetup !== 'string') if (Array.isArray(config.globalSetup)) {
config.globalSetup.forEach((item, index) => {
if (typeof item !== 'string')
throw errorWithFile(file, `config.globalSetup[${index}] must be a string`);
});
} else if (typeof config.globalSetup !== 'string') {
throw errorWithFile(file, `config.globalSetup must be a string`); throw errorWithFile(file, `config.globalSetup must be a string`);
} }
}
if ('globalTeardown' in config && config.globalTeardown !== undefined) { if ('globalTeardown' in config && config.globalTeardown !== undefined) {
if (typeof config.globalTeardown !== 'string') if (Array.isArray(config.globalTeardown)) {
config.globalTeardown.forEach((item, index) => {
if (typeof item !== 'string')
throw errorWithFile(file, `config.globalTeardown[${index}] must be a string`);
});
} else if (typeof config.globalTeardown !== 'string') {
throw errorWithFile(file, `config.globalTeardown must be a string`); throw errorWithFile(file, `config.globalTeardown must be a string`);
} }
}
if ('globalTimeout' in config && config.globalTimeout !== undefined) { if ('globalTimeout' in config && config.globalTimeout !== undefined) {
if (typeof config.globalTimeout !== 'number' || config.globalTimeout < 0) if (typeof config.globalTimeout !== 'number' || config.globalTimeout < 0)

View file

@ -52,6 +52,7 @@ export class TestTypeImpl {
test.skip = wrapFunctionWithLocation(this._modifier.bind(this, 'skip')); test.skip = wrapFunctionWithLocation(this._modifier.bind(this, 'skip'));
test.fixme = wrapFunctionWithLocation(this._modifier.bind(this, 'fixme')); test.fixme = wrapFunctionWithLocation(this._modifier.bind(this, 'fixme'));
test.fail = wrapFunctionWithLocation(this._modifier.bind(this, 'fail')); test.fail = wrapFunctionWithLocation(this._modifier.bind(this, 'fail'));
test.fail.only = wrapFunctionWithLocation(this._createTest.bind(this, 'fail.only'));
test.slow = wrapFunctionWithLocation(this._modifier.bind(this, 'slow')); test.slow = wrapFunctionWithLocation(this._modifier.bind(this, 'slow'));
test.setTimeout = wrapFunctionWithLocation(this._setTimeout.bind(this)); test.setTimeout = wrapFunctionWithLocation(this._setTimeout.bind(this));
test.step = this._step.bind(this); test.step = this._step.bind(this);
@ -81,7 +82,7 @@ export class TestTypeImpl {
return suite; return suite;
} }
private _createTest(type: 'default' | 'only' | 'skip' | 'fixme' | 'fail', location: Location, title: string, fnOrDetails: Function | TestDetails, fn?: Function) { private _createTest(type: 'default' | 'only' | 'skip' | 'fixme' | 'fail' | 'fail.only', location: Location, title: string, fnOrDetails: Function | TestDetails, fn?: Function) {
throwIfRunningInsideJest(); throwIfRunningInsideJest();
const suite = this._currentSuite(location, 'test()'); const suite = this._currentSuite(location, 'test()');
if (!suite) if (!suite)
@ -104,10 +105,12 @@ export class TestTypeImpl {
test._tags.push(...validatedDetails.tags); test._tags.push(...validatedDetails.tags);
suite._addTest(test); suite._addTest(test);
if (type === 'only') if (type === 'only' || type === 'fail.only')
test._only = true; test._only = true;
if (type === 'skip' || type === 'fixme' || type === 'fail') if (type === 'skip' || type === 'fixme' || type === 'fail')
test._staticAnnotations.push({ type }); test._staticAnnotations.push({ type });
else if (type === 'fail.only')
test._staticAnnotations.push({ type: 'fail' });
} }
private _describe(type: 'default' | 'only' | 'serial' | 'serial.only' | 'parallel' | 'parallel.only' | 'skip' | 'fixme', location: Location, titleOrFn: string | Function, fnOrDetails?: TestDetails | Function, fn?: Function) { private _describe(type: 'default' | 'only' | 'serial' | 'serial.only' | 'parallel' | 'parallel.only' | 'skip' | 'fixme', location: Location, titleOrFn: string | Function, fnOrDetails?: TestDetails | Function, fn?: Function) {

View file

@ -39,6 +39,10 @@ export type MatcherResult<E, A> = {
actual?: A; actual?: A;
log?: string[]; log?: string[];
timeout?: number; timeout?: number;
locator?: string;
printedReceived?: string;
printedExpected?: string;
printedDiff?: string;
}; };
export class ExpectError extends Error { export class ExpectError extends Error {

View file

@ -39,22 +39,41 @@ export async function toBeTruthy(
}; };
const timeout = options.timeout ?? this.timeout; const timeout = options.timeout ?? this.timeout;
const { matches, log, timedOut, received } = await query(!!this.isNot, timeout); const { matches: pass, log, timedOut, received } = await query(!!this.isNot, timeout);
if (pass === !this.isNot) {
return {
name: matcherName,
message: () => '',
pass,
expected
};
}
const notFound = received === kNoElementsFoundError ? received : undefined; const notFound = received === kNoElementsFoundError ? received : undefined;
const actual = matches ? expected : unexpected; const actual = pass ? expected : unexpected;
let printedReceived: string | undefined;
let printedExpected: string | undefined;
if (pass) {
printedExpected = `Expected: not ${expected}`;
printedReceived = `Received: ${notFound ? kNoElementsFoundError : expected}`;
} else {
printedExpected = `Expected: ${expected}`;
printedReceived = `Received: ${notFound ? kNoElementsFoundError : unexpected}`;
}
const message = () => { const message = () => {
const header = matcherHint(this, receiver, matcherName, 'locator', arg, matcherOptions, timedOut ? timeout : undefined); const header = matcherHint(this, receiver, matcherName, 'locator', arg, matcherOptions, timedOut ? timeout : undefined);
const logText = callLogText(log); const logText = callLogText(log);
return matches ? `${header}Expected: not ${expected}\nReceived: ${notFound ? kNoElementsFoundError : expected}${logText}` : return `${header}${printedExpected}\n${printedReceived}${logText}`;
`${header}Expected: ${expected}\nReceived: ${notFound ? kNoElementsFoundError : unexpected}${logText}`;
}; };
return { return {
message, message,
pass: matches, pass,
actual, actual,
name: matcherName, name: matcherName,
expected, expected,
log, log,
timeout: timedOut ? timeout : undefined, timeout: timedOut ? timeout : undefined,
...(printedReceived ? { printedReceived } : {}),
...(printedExpected ? { printedExpected } : {}),
}; };
} }

View file

@ -44,22 +44,35 @@ export async function toEqual<T>(
const timeout = options.timeout ?? this.timeout; const timeout = options.timeout ?? this.timeout;
const { matches: pass, received, log, timedOut } = await query(!!this.isNot, timeout); const { matches: pass, received, log, timedOut } = await query(!!this.isNot, timeout);
if (pass === !this.isNot) {
return {
name: matcherName,
message: () => '',
pass,
expected
};
}
const message = pass let printedReceived: string | undefined;
? () => let printedExpected: string | undefined;
matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined) + let printedDiff: string | undefined;
`Expected: not ${this.utils.printExpected(expected)}\n` + if (pass) {
`Received: ${this.utils.printReceived(received)}` + callLogText(log) printedExpected = `Expected: not ${this.utils.printExpected(expected)}`;
: () => printedReceived = `Received: ${this.utils.printReceived(received)}`;
matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined) + } else {
this.utils.printDiffOrStringify( printedDiff = this.utils.printDiffOrStringify(
expected, expected,
received, received,
EXPECTED_LABEL, EXPECTED_LABEL,
RECEIVED_LABEL, RECEIVED_LABEL,
false, false,
) + callLogText(log); );
}
const message = () => {
const header = matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined);
const details = printedDiff || `${printedExpected}\n${printedReceived}`;
return `${header}${details}${callLogText(log)}`;
};
// Passing the actual and expected objects so that a custom reporter // Passing the actual and expected objects so that a custom reporter
// could access them, for example in order to display a custom visual diff, // could access them, for example in order to display a custom visual diff,
// or create a different error message // or create a different error message
@ -70,5 +83,8 @@ export async function toEqual<T>(
pass, pass,
log, log,
timeout: timedOut ? timeout : undefined, timeout: timedOut ? timeout : undefined,
...(printedReceived ? { printedReceived } : {}),
...(printedExpected ? { printedExpected } : {}),
...(printedDiff ? { printedDiff } : {}),
}; };
} }

View file

@ -18,8 +18,6 @@
import type { LocatorEx } from './matchers'; import type { LocatorEx } from './matchers';
import type { ExpectMatcherState } from '../../types/test'; import type { ExpectMatcherState } from '../../types/test';
import { kNoElementsFoundError, matcherHint, type MatcherResult } from './matcherHint'; import { kNoElementsFoundError, matcherHint, type MatcherResult } from './matcherHint';
import type { AriaTemplateNode } from 'playwright-core/lib/server/injected/ariaSnapshot';
import { yaml } from '../utilsBundle';
import { colors } from 'playwright-core/lib/utilsBundle'; import { colors } from 'playwright-core/lib/utilsBundle';
import { EXPECTED_COLOR } from '../common/expectBundle'; import { EXPECTED_COLOR } from '../common/expectBundle';
import { callLogText } from '../util'; import { callLogText } from '../util';
@ -46,23 +44,24 @@ export async function toMatchAriaSnapshot(
].join('\n\n')); ].join('\n\n'));
} }
const ariaTree = toAriaTree(expected) as AriaTemplateNode;
const timeout = options.timeout ?? this.timeout; const timeout = options.timeout ?? this.timeout;
const { matches: pass, received, log, timedOut } = await receiver._expect('to.match.aria', { expectedValue: ariaTree, isNot: this.isNot, timeout }); const { matches: pass, received, log, timedOut } = await receiver._expect('to.match.aria', { expectedValue: expected, isNot: this.isNot, timeout });
const messagePrefix = matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined); const messagePrefix = matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined);
const notFound = received === kNoElementsFoundError; const notFound = received === kNoElementsFoundError;
const escapedExpected = unshift(escapePrivateUsePoints(expected));
const escapedReceived = unshift(escapePrivateUsePoints(received));
const message = () => { const message = () => {
if (pass) { if (pass) {
if (notFound) if (notFound)
return messagePrefix + `Expected: not ${this.utils.printExpected(expected)}\nReceived: ${received}` + callLogText(log); return messagePrefix + `Expected: not ${this.utils.printExpected(escapedExpected)}\nReceived: ${escapedReceived}` + callLogText(log);
const printedReceived = printReceivedStringContainExpectedSubstring(received, received.indexOf(expected), expected.length); const printedReceived = printReceivedStringContainExpectedSubstring(escapedReceived, escapedReceived.indexOf(escapedExpected), escapedExpected.length);
return messagePrefix + `Expected: not ${this.utils.printExpected(expected)}\nReceived string: ${printedReceived}` + callLogText(log); return messagePrefix + `Expected: not ${this.utils.printExpected(escapedExpected)}\nReceived string: ${printedReceived}` + callLogText(log);
} else { } else {
const labelExpected = `Expected`; const labelExpected = `Expected`;
if (notFound) if (notFound)
return messagePrefix + `${labelExpected}: ${this.utils.printExpected(expected)}\nReceived: ${received}` + callLogText(log); return messagePrefix + `${labelExpected}: ${this.utils.printExpected(escapedExpected)}\nReceived: ${escapedReceived}` + callLogText(log);
return messagePrefix + this.utils.printDiffOrStringify(expected, received, labelExpected, 'Received string', false) + callLogText(log); return messagePrefix + this.utils.printDiffOrStringify(escapedExpected, escapedReceived, labelExpected, 'Received string', false) + callLogText(log);
} }
}; };
@ -77,58 +76,20 @@ export async function toMatchAriaSnapshot(
}; };
} }
function parseKey(key: string): AriaTemplateNode { function escapePrivateUsePoints(str: string) {
if (!key) return str.replace(/[\uE000-\uF8FF]/g, char => `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`);
return { role: '' };
const match = key.match(/^([a-z]+)(?:\s+(?:"([^"]*)"|\/([^\/]*)\/))?$/);
if (!match)
throw new Error(`Invalid key ${key}`);
const role = match[1];
if (role && role !== 'text' && !allRoles.includes(role))
throw new Error(`Invalid role ${role}`);
if (match[2])
return { role, name: match[2] };
if (match[3])
return { role, name: new RegExp(match[3]) };
return { role };
} }
function valueOrRegex(value: string): string | RegExp { function unshift(snapshot: string): string {
return value.startsWith('/') && value.endsWith('/') ? new RegExp(value.slice(1, -1)) : value; const lines = snapshot.split('\n');
let whitespacePrefixLength = 100;
for (const line of lines) {
if (!line.trim())
continue;
const match = line.match(/^(\s*)/);
if (match && match[1].length < whitespacePrefixLength)
whitespacePrefixLength = match[1].length;
break;
} }
return lines.filter(t => t.trim()).map(line => line.substring(whitespacePrefixLength)).join('\n');
type YamlNode = Record<string, Array<YamlNode> | string>;
function toAriaTree(text: string): AriaTemplateNode {
const convert = (object: YamlNode | string): AriaTemplateNode | RegExp | string => {
const key = typeof object === 'string' ? object : Object.keys(object)[0];
const value = typeof object === 'string' ? undefined : object[key];
const parsed = parseKey(key);
if (parsed.role === 'text') {
if (typeof value !== 'string')
throw new Error(`Generic role must have a text value`);
return valueOrRegex(value as string);
} }
if (Array.isArray(value))
parsed.children = value.map(convert);
else if (value)
parsed.children = [valueOrRegex(value)];
return parsed;
};
const fragment = yaml.parse(text) as YamlNode[];
return convert({ '': fragment }) as AriaTemplateNode;
}
const allRoles = [
'alert', 'alertdialog', 'application', 'article', 'banner', 'blockquote', 'button', 'caption', 'cell', 'checkbox', 'code', 'columnheader', 'combobox', 'command',
'complementary', 'composite', 'contentinfo', 'definition', 'deletion', 'dialog', 'directory', 'document', 'emphasis', 'feed', 'figure', 'form', 'generic', 'grid',
'gridcell', 'group', 'heading', 'img', 'input', 'insertion', 'landmark', 'link', 'list', 'listbox', 'listitem', 'log', 'main', 'marquee', 'math', 'meter', 'menu',
'menubar', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'navigation', 'none', 'note', 'option', 'paragraph', 'presentation', 'progressbar', 'radio', 'radiogroup',
'range', 'region', 'roletype', 'row', 'rowgroup', 'rowheader', 'scrollbar', 'search', 'searchbox', 'section', 'sectionhead', 'select', 'separator', 'slider',
'spinbutton', 'status', 'strong', 'structure', 'subscript', 'superscript', 'switch', 'tab', 'table', 'tablist', 'tabpanel', 'term', 'textbox', 'time', 'timer',
'toolbar', 'tooltip', 'tree', 'treegrid', 'treeitem', 'widget', 'window'
];

View file

@ -194,6 +194,10 @@ class SnapshotHelper {
pass, pass,
message: () => message, message: () => message,
log, log,
// eslint-disable-next-line @typescript-eslint/no-base-to-string
...(this.locator ? { locator: this.locator.toString() } : {}),
printedExpected: this.expectedPath,
printedReceived: this.actualPath,
}; };
return Object.fromEntries(Object.entries(unfiltered).filter(([_, v]) => v !== undefined)) as ImageMatcherResult; return Object.fromEntries(Object.entries(unfiltered).filter(([_, v]) => v !== undefined)) as ImageMatcherResult;
} }

View file

@ -58,29 +58,56 @@ export async function toMatchText(
const timeout = options.timeout ?? this.timeout; const timeout = options.timeout ?? this.timeout;
const { matches: pass, received, log, timedOut } = await query(!!this.isNot, timeout); const { matches: pass, received, log, timedOut } = await query(!!this.isNot, timeout);
if (pass === !this.isNot) {
return {
name: matcherName,
message: () => '',
pass,
expected
};
}
const stringSubstring = options.matchSubstring ? 'substring' : 'string'; const stringSubstring = options.matchSubstring ? 'substring' : 'string';
const receivedString = received || ''; const receivedString = received || '';
const messagePrefix = matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined); const messagePrefix = matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined);
const notFound = received === kNoElementsFoundError; const notFound = received === kNoElementsFoundError;
const message = () => {
let printedReceived: string | undefined;
let printedExpected: string | undefined;
let printedDiff: string | undefined;
if (pass) { if (pass) {
if (typeof expected === 'string') { if (typeof expected === 'string') {
if (notFound) if (notFound) {
return messagePrefix + `Expected ${stringSubstring}: not ${this.utils.printExpected(expected)}\nReceived: ${received}` + callLogText(log); printedExpected = `Expected ${stringSubstring}: not ${this.utils.printExpected(expected)}`;
const printedReceived = printReceivedStringContainExpectedSubstring(receivedString, receivedString.indexOf(expected), expected.length); printedReceived = `Received: ${received}`;
return messagePrefix + `Expected ${stringSubstring}: not ${this.utils.printExpected(expected)}\nReceived string: ${printedReceived}` + callLogText(log);
} else { } else {
if (notFound) printedExpected = `Expected ${stringSubstring}: not ${this.utils.printExpected(expected)}`;
return messagePrefix + `Expected pattern: not ${this.utils.printExpected(expected)}\nReceived: ${received}` + callLogText(log); const formattedReceived = printReceivedStringContainExpectedSubstring(receivedString, receivedString.indexOf(expected), expected.length);
const printedReceived = printReceivedStringContainExpectedResult(receivedString, typeof expected.exec === 'function' ? expected.exec(receivedString) : null); printedReceived = `Received string: ${formattedReceived}`;
return messagePrefix + `Expected pattern: not ${this.utils.printExpected(expected)}\nReceived string: ${printedReceived}` + callLogText(log); }
} else {
if (notFound) {
printedExpected = `Expected pattern: not ${this.utils.printExpected(expected)}`;
printedReceived = `Received: ${received}`;
} else {
printedExpected = `Expected pattern: not ${this.utils.printExpected(expected)}`;
const formattedReceived = printReceivedStringContainExpectedResult(receivedString, typeof expected.exec === 'function' ? expected.exec(receivedString) : null);
printedReceived = `Received string: ${formattedReceived}`;
}
} }
} else { } else {
const labelExpected = `Expected ${typeof expected === 'string' ? stringSubstring : 'pattern'}`; const labelExpected = `Expected ${typeof expected === 'string' ? stringSubstring : 'pattern'}`;
if (notFound) if (notFound) {
return messagePrefix + `${labelExpected}: ${this.utils.printExpected(expected)}\nReceived: ${received}` + callLogText(log); printedExpected = `${labelExpected}: ${this.utils.printExpected(expected)}`;
return messagePrefix + this.utils.printDiffOrStringify(expected, receivedString, labelExpected, 'Received string', false) + callLogText(log); printedReceived = `Received: ${received}`;
} else {
printedDiff = this.utils.printDiffOrStringify(expected, receivedString, labelExpected, 'Received string', false);
} }
}
const message = () => {
const resultDetails = printedDiff ? printedDiff : printedExpected + '\n' + printedReceived;
return messagePrefix + resultDetails + callLogText(log);
}; };
return { return {
@ -91,5 +118,10 @@ export async function toMatchText(
actual: received, actual: received,
log, log,
timeout: timedOut ? timeout : undefined, timeout: timedOut ? timeout : undefined,
// eslint-disable-next-line @typescript-eslint/no-base-to-string
locator: receiver.toString(),
...(printedReceived ? { printedReceived } : {}),
...(printedExpected ? { printedExpected } : {}),
...(printedDiff ? { printedDiff } : {}),
}; };
} }

View file

@ -32,6 +32,13 @@ type Annotation = {
type ErrorDetails = { type ErrorDetails = {
message: string; message: string;
location?: Location; location?: Location;
timeout?: number;
matcherName?: string;
locator?: string;
expected?: string;
received?: string;
log?: string[];
snippet?: string;
}; };
type TestSummary = { type TestSummary = {
@ -383,6 +390,13 @@ export function formatResultFailure(test: TestCase, result: TestResult, initialI
errorDetails.push({ errorDetails.push({
message: indent(formattedError.message, initialIndent), message: indent(formattedError.message, initialIndent),
location: formattedError.location, location: formattedError.location,
timeout: error.timeout,
matcherName: error.matcherName,
locator: error.locator,
expected: error.expected,
received: error.received,
log: error.log,
snippet: error.snippet,
}); });
} }
return errorDetails; return errorDetails;

View file

@ -505,8 +505,8 @@ class HtmlBuilder {
error: step.error?.message, error: step.error?.message,
count count
}; };
if (result.location) if (step.location)
this._stepsInFile.set(result.location.file, result); this._stepsInFile.set(step.location.file, result);
return result; return result;
} }

View file

@ -25,7 +25,6 @@ import JSONReporter from '../reporters/json';
import JUnitReporter from '../reporters/junit'; import JUnitReporter from '../reporters/junit';
import LineReporter from '../reporters/line'; import LineReporter from '../reporters/line';
import ListReporter from '../reporters/list'; import ListReporter from '../reporters/list';
import MarkdownReporter from '../reporters/markdown';
import type { Suite } from '../common/test'; import type { Suite } from '../common/test';
import type { BuiltInReporter, FullConfigInternal } from '../common/config'; import type { BuiltInReporter, FullConfigInternal } from '../common/config';
import { loadReporter } from './loadUtils'; import { loadReporter } from './loadUtils';
@ -45,7 +44,6 @@ export async function createReporters(config: FullConfigInternal, mode: 'list' |
junit: JUnitReporter, junit: JUnitReporter,
null: EmptyReporter, null: EmptyReporter,
html: HtmlReporter, html: HtmlReporter,
markdown: MarkdownReporter,
}; };
const reporters: ReporterV2[] = []; const reporters: ReporterV2[] = [];
descriptions ??= config.config.reporter; descriptions ??= config.config.reporter;

View file

@ -98,8 +98,11 @@ export function createGlobalSetupTasks(config: FullConfigInternal) {
if (!config.configCLIOverrides.preserveOutputDir && !process.env.PW_TEST_NO_REMOVE_OUTPUT_DIRS) if (!config.configCLIOverrides.preserveOutputDir && !process.env.PW_TEST_NO_REMOVE_OUTPUT_DIRS)
tasks.push(createRemoveOutputDirsTask()); tasks.push(createRemoveOutputDirsTask());
tasks.push(...createPluginSetupTasks(config)); tasks.push(...createPluginSetupTasks(config));
if (config.config.globalSetup || config.config.globalTeardown) if (config.globalSetups.length || config.globalTeardowns.length) {
tasks.push(createGlobalSetupTask()); const length = Math.max(config.globalSetups.length, config.globalTeardowns.length);
for (let i = 0; i < length; i++)
tasks.push(createGlobalSetupTask(i, length));
}
return tasks; return tasks;
} }
@ -161,15 +164,20 @@ function createPluginBeginTask(plugin: TestRunnerPluginRegistration): Task<TestR
}; };
} }
function createGlobalSetupTask(): Task<TestRun> { function createGlobalSetupTask(index: number, length: number): Task<TestRun> {
let globalSetupResult: any; let globalSetupResult: any;
let globalSetupFinished = false; let globalSetupFinished = false;
let teardownHook: any; let teardownHook: any;
let title = 'global setup';
if (length > 1)
title += ` #${index}`;
return { return {
title: 'global setup', title,
setup: async ({ config }) => { setup: async ({ config }) => {
const setupHook = config.config.globalSetup ? await loadGlobalHook(config, config.config.globalSetup) : undefined; const setupHook = config.globalSetups[index] ? await loadGlobalHook(config, config.globalSetups[index]) : undefined;
teardownHook = config.config.globalTeardown ? await loadGlobalHook(config, config.config.globalTeardown) : undefined; teardownHook = config.globalTeardowns[index] ? await loadGlobalHook(config, config.globalTeardowns[index]) : undefined;
globalSetupResult = setupHook ? await setupHook(config.config) : undefined; globalSetupResult = setupHook ? await setupHook(config.config) : undefined;
globalSetupFinished = true; globalSetupFinished = true;
}, },

View file

@ -20,4 +20,3 @@ export const sourceMapSupport: typeof import('../bundles/utils/node_modules/@typ
export const stoppable: typeof import('../bundles/utils/node_modules/@types/stoppable') = require('./utilsBundleImpl').stoppable; export const stoppable: typeof import('../bundles/utils/node_modules/@types/stoppable') = require('./utilsBundleImpl').stoppable;
export const enquirer: typeof import('../bundles/utils/node_modules/enquirer') = require('./utilsBundleImpl').enquirer; export const enquirer: typeof import('../bundles/utils/node_modules/enquirer') = require('./utilsBundleImpl').enquirer;
export const chokidar: typeof import('../bundles/utils/node_modules/chokidar') = require('./utilsBundleImpl').chokidar; export const chokidar: typeof import('../bundles/utils/node_modules/chokidar') = require('./utilsBundleImpl').chokidar;
export const yaml: typeof import('../bundles/utils/node_modules/yaml') = require('./utilsBundleImpl').yaml;

View file

@ -24,10 +24,11 @@ import { TimeoutManager, TimeoutManagerError, kMaxDeadline } from './timeoutMana
import type { RunnableDescription } from './timeoutManager'; import type { RunnableDescription } from './timeoutManager';
import type { Annotation, FullConfigInternal, FullProjectInternal } from '../common/config'; import type { Annotation, FullConfigInternal, FullProjectInternal } from '../common/config';
import type { FullConfig, Location } from '../../types/testReporter'; import type { FullConfig, Location } from '../../types/testReporter';
import { debugTest, filteredStackTrace, formatLocation, getContainedPath, normalizeAndSaveAttachment, serializeError, trimLongString, windowsFilesystemFriendlyLength } from '../util'; import { debugTest, filteredStackTrace, formatLocation, getContainedPath, normalizeAndSaveAttachment, trimLongString, windowsFilesystemFriendlyLength } from '../util';
import { TestTracing } from './testTracing'; import { TestTracing } from './testTracing';
import type { Attachment } from './testTracing'; import type { Attachment } from './testTracing';
import type { StackFrame } from '@protocol/channels'; import type { StackFrame } from '@protocol/channels';
import { serializeWorkerError } from './util';
export interface TestStepInternal { export interface TestStepInternal {
complete(result: { error?: Error | unknown, attachments?: Attachment[] }): void; complete(result: { error?: Error | unknown, attachments?: Attachment[] }): void;
@ -272,7 +273,7 @@ export class TestInfoImpl implements TestInfo {
if (result.error) { if (result.error) {
if (typeof result.error === 'object' && !(result.error as any)?.[stepSymbol]) if (typeof result.error === 'object' && !(result.error as any)?.[stepSymbol])
(result.error as any)[stepSymbol] = step; (result.error as any)[stepSymbol] = step;
const error = serializeError(result.error); const error = serializeWorkerError(result.error);
if (data.boxedStack) if (data.boxedStack)
error.stack = `${error.message}\n${stringifyStackFrames(data.boxedStack).join('\n')}`; error.stack = `${error.message}\n${stringifyStackFrames(data.boxedStack).join('\n')}`;
step.error = error; step.error = error;
@ -330,7 +331,7 @@ export class TestInfoImpl implements TestInfo {
_failWithError(error: Error | unknown) { _failWithError(error: Error | unknown) {
if (this.status === 'passed' || this.status === 'skipped') if (this.status === 'passed' || this.status === 'skipped')
this.status = error instanceof TimeoutManagerError ? 'timedOut' : 'failed'; this.status = error instanceof TimeoutManagerError ? 'timedOut' : 'failed';
const serialized = serializeError(error); const serialized = serializeWorkerError(error);
const step: TestStepInternal | undefined = typeof error === 'object' ? (error as any)?.[stepSymbol] : undefined; const step: TestStepInternal | undefined = typeof error === 'object' ? (error as any)?.[stepSymbol] : undefined;
if (step && step.boxedStack) if (step && step.boxedStack)
serialized.stack = `${(error as Error).name}: ${(error as Error).message}\n${stringifyStackFrames(step.boxedStack).join('\n')}`; serialized.stack = `${(error as Error).name}: ${(error as Error).message}\n${stringifyStackFrames(step.boxedStack).join('\n')}`;

View file

@ -0,0 +1,45 @@
/**
* 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 { TestError } from '../../types/testReporter';
import type { TestInfoError } from '../../types/test';
import type { MatcherResult } from '../matchers/matcherHint';
import { serializeError } from '../util';
type MatcherResultDetails = Pick<TestError, 'timeout'|'matcherName'|'locator'|'expected'|'received'|'log'>;
export function serializeWorkerError(error: Error | any): TestInfoError & MatcherResultDetails {
return {
...serializeError(error),
...serializeExpectDetails(error),
};
}
function serializeExpectDetails(e: Error): MatcherResultDetails {
const matcherResult = (e as any).matcherResult as MatcherResult<unknown, unknown>;
if (!matcherResult)
return {};
return {
timeout: matcherResult.timeout,
matcherName: matcherResult.name,
locator: matcherResult.locator,
expected: matcherResult.printedExpected,
received: matcherResult.printedReceived,
log: matcherResult.log,
};
}

View file

@ -15,7 +15,7 @@
*/ */
import { colors } from 'playwright-core/lib/utilsBundle'; import { colors } from 'playwright-core/lib/utilsBundle';
import { debugTest, relativeFilePath, serializeError } from '../util'; import { debugTest, relativeFilePath } from '../util';
import { type TestBeginPayload, type TestEndPayload, type RunPayload, type DonePayload, type WorkerInitParams, type TeardownErrorsPayload, stdioChunkToParams } from '../common/ipc'; import { type TestBeginPayload, type TestEndPayload, type RunPayload, type DonePayload, type WorkerInitParams, type TeardownErrorsPayload, stdioChunkToParams } from '../common/ipc';
import { setCurrentTestInfo, setIsWorkerProcess } from '../common/globals'; import { setCurrentTestInfo, setIsWorkerProcess } from '../common/globals';
import { deserializeConfig } from '../common/configLoader'; import { deserializeConfig } from '../common/configLoader';
@ -32,6 +32,7 @@ import type { TestInfoError } from '../../types/test';
import type { Location } from '../../types/testReporter'; import type { Location } from '../../types/testReporter';
import { inheritFixtureNames } from '../common/fixtures'; import { inheritFixtureNames } from '../common/fixtures';
import { type TimeSlot } from './timeoutManager'; import { type TimeSlot } from './timeoutManager';
import { serializeWorkerError } from './util';
export class WorkerMain extends ProcessRunner { export class WorkerMain extends ProcessRunner {
private _params: WorkerInitParams; private _params: WorkerInitParams;
@ -112,7 +113,7 @@ export class WorkerMain extends ProcessRunner {
await fakeTestInfo._runAsStage({ title: 'worker cleanup', runnable }, () => gracefullyCloseAll()).catch(() => {}); await fakeTestInfo._runAsStage({ title: 'worker cleanup', runnable }, () => gracefullyCloseAll()).catch(() => {});
this._fatalErrors.push(...fakeTestInfo.errors); this._fatalErrors.push(...fakeTestInfo.errors);
} catch (e) { } catch (e) {
this._fatalErrors.push(serializeError(e)); this._fatalErrors.push(serializeWorkerError(e));
} }
if (this._fatalErrors.length) { if (this._fatalErrors.length) {
@ -153,7 +154,7 @@ export class WorkerMain extends ProcessRunner {
// No current test - fatal error. // No current test - fatal error.
if (!this._currentTest) { if (!this._currentTest) {
if (!this._fatalErrors.length) if (!this._fatalErrors.length)
this._fatalErrors.push(serializeError(error)); this._fatalErrors.push(serializeWorkerError(error));
void this._stop(); void this._stop();
return; return;
} }
@ -224,7 +225,7 @@ export class WorkerMain extends ProcessRunner {
// In theory, we should run above code without any errors. // In theory, we should run above code without any errors.
// However, in the case we screwed up, or loadTestFile failed in the worker // However, in the case we screwed up, or loadTestFile failed in the worker
// but not in the runner, let's do a fatal error. // but not in the runner, let's do a fatal error.
this._fatalErrors.push(serializeError(e)); this._fatalErrors.push(serializeWorkerError(e));
void this._stop(); void this._stop();
} finally { } finally {
const donePayload: DonePayload = { const donePayload: DonePayload = {

File diff suppressed because it is too large Load diff

View file

@ -554,16 +554,41 @@ export interface TestCase {
* Information about an error thrown during test execution. * Information about an error thrown during test execution.
*/ */
export interface TestError { export interface TestError {
/**
* Expected value formatted as a human-readable string.
*/
expected?: string;
/** /**
* Error location in the source code. * Error location in the source code.
*/ */
location?: Location; location?: Location;
/**
* Receiver's locator.
*/
locator?: string;
/**
* Call log.
*/
log?: Array<string>;
/**
* Expect matcher name.
*/
matcherName?: string;
/** /**
* Error message. Set when [Error] (or its subclass) has been thrown. * Error message. Set when [Error] (or its subclass) has been thrown.
*/ */
message?: string; message?: string;
/**
* Received value formatted as a human-readable string.
*/
received?: string;
/** /**
* Source code snippet with highlighted error. * Source code snippet with highlighted error.
*/ */
@ -574,6 +599,11 @@ export interface TestError {
*/ */
stack?: string; stack?: string;
/**
* Timeout in milliseconds, if the error was caused by a timeout.
*/
timeout?: number;
/** /**
* The value that was thrown. Set when anything except the [Error] (or its subclass) has been thrown. * The value that was thrown. Set when anything except the [Error] (or its subclass) has been thrown.
*/ */

View file

@ -2509,6 +2509,7 @@ export interface FrameChannel extends FrameEventTarget, Channel {
evalOnSelectorAll(params: FrameEvalOnSelectorAllParams, metadata?: CallMetadata): Promise<FrameEvalOnSelectorAllResult>; evalOnSelectorAll(params: FrameEvalOnSelectorAllParams, metadata?: CallMetadata): Promise<FrameEvalOnSelectorAllResult>;
addScriptTag(params: FrameAddScriptTagParams, metadata?: CallMetadata): Promise<FrameAddScriptTagResult>; addScriptTag(params: FrameAddScriptTagParams, metadata?: CallMetadata): Promise<FrameAddScriptTagResult>;
addStyleTag(params: FrameAddStyleTagParams, metadata?: CallMetadata): Promise<FrameAddStyleTagResult>; addStyleTag(params: FrameAddStyleTagParams, metadata?: CallMetadata): Promise<FrameAddStyleTagResult>;
ariaSnapshot(params: FrameAriaSnapshotParams, metadata?: CallMetadata): Promise<FrameAriaSnapshotResult>;
blur(params: FrameBlurParams, metadata?: CallMetadata): Promise<FrameBlurResult>; blur(params: FrameBlurParams, metadata?: CallMetadata): Promise<FrameBlurResult>;
check(params: FrameCheckParams, metadata?: CallMetadata): Promise<FrameCheckResult>; check(params: FrameCheckParams, metadata?: CallMetadata): Promise<FrameCheckResult>;
click(params: FrameClickParams, metadata?: CallMetadata): Promise<FrameClickResult>; click(params: FrameClickParams, metadata?: CallMetadata): Promise<FrameClickResult>;
@ -2613,6 +2614,16 @@ export type FrameAddStyleTagOptions = {
export type FrameAddStyleTagResult = { export type FrameAddStyleTagResult = {
element: ElementHandleChannel, element: ElementHandleChannel,
}; };
export type FrameAriaSnapshotParams = {
selector: string,
timeout?: number,
};
export type FrameAriaSnapshotOptions = {
timeout?: number,
};
export type FrameAriaSnapshotResult = {
snapshot: string,
};
export type FrameBlurParams = { export type FrameBlurParams = {
selector: string, selector: string,
strict?: boolean, strict?: boolean,

View file

@ -1875,6 +1875,13 @@ Frame:
flags: flags:
snapshot: true snapshot: true
ariaSnapshot:
parameters:
selector: string
timeout: number?
returns:
snapshot: string
blur: blur:
parameters: parameters:
selector: string selector: string

View file

@ -30,7 +30,8 @@ export type ActionName =
'assertText' | 'assertText' |
'assertValue' | 'assertValue' |
'assertChecked' | 'assertChecked' |
'assertVisible'; 'assertVisible' |
'assertSnapshot';
export type ActionBase = { export type ActionBase = {
name: ActionName, name: ActionName,
@ -113,8 +114,13 @@ export type AssertVisibleAction = ActionWithSelector & {
name: 'assertVisible', name: 'assertVisible',
}; };
export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageAction | UncheckAction | FillAction | NavigateAction | PressAction | SelectAction | SetInputFilesAction | AssertTextAction | AssertValueAction | AssertCheckedAction | AssertVisibleAction; export type AssertSnapshotAction = ActionWithSelector & {
export type AssertAction = AssertCheckedAction | AssertValueAction | AssertTextAction | AssertVisibleAction; name: 'assertSnapshot',
snapshot: string,
};
export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageAction | UncheckAction | FillAction | NavigateAction | PressAction | SelectAction | SetInputFilesAction | AssertTextAction | AssertValueAction | AssertCheckedAction | AssertVisibleAction | AssertSnapshotAction;
export type AssertAction = AssertCheckedAction | AssertValueAction | AssertTextAction | AssertVisibleAction | AssertSnapshotAction;
export type PerformOnRecordAction = ClickAction | CheckAction | UncheckAction | PressAction | SelectAction; export type PerformOnRecordAction = ClickAction | CheckAction | UncheckAction | PressAction | SelectAction;
// Signals. // Signals.

View file

@ -116,6 +116,7 @@ export const Recorder: React.FC<RecorderProps> = ({
'assertingText': 'recording-inspecting', 'assertingText': 'recording-inspecting',
'assertingVisibility': 'recording-inspecting', 'assertingVisibility': 'recording-inspecting',
'assertingValue': 'recording-inspecting', 'assertingValue': 'recording-inspecting',
'assertingSnapshot': 'recording-inspecting',
}[mode]; }[mode];
window.dispatch({ event: 'setMode', params: { mode: newMode } }).catch(() => { }); window.dispatch({ event: 'setMode', params: { mode: newMode } }).catch(() => { });
}}></ToolbarButton> }}></ToolbarButton>

View file

@ -26,7 +26,8 @@ export type Mode =
| 'recording-inspecting' | 'recording-inspecting'
| 'standby' | 'standby'
| 'assertingVisibility' | 'assertingVisibility'
| 'assertingValue'; | 'assertingValue'
| 'assertingSnapshot';
export type EventData = { export type EventData = {
event: event:

View file

@ -59,6 +59,30 @@ export const ActionList: React.FC<ActionListProps> = ({
return { selectedItem }; return { selectedItem };
}, [itemMap, selectedAction]); }, [itemMap, selectedAction]);
const isError = React.useCallback((item: ActionTreeItem) => {
return !!item.action?.error?.message;
}, []);
const onAccepted = React.useCallback((item: ActionTreeItem) => {
return setSelectedTime({ minimum: item.action!.startTime, maximum: item.action!.endTime });
}, [setSelectedTime]);
const render = React.useCallback((item: ActionTreeItem) => {
return renderAction(item.action!, { sdkLanguage, revealConsole, isLive, showDuration: true, showBadges: true });
}, [isLive, revealConsole, sdkLanguage]);
const isVisible = React.useCallback((item: ActionTreeItem) => {
return !selectedTime || !item.action || (item.action!.startTime <= selectedTime.maximum && item.action!.endTime >= selectedTime.minimum);
}, [selectedTime]);
const onSelectedAction = React.useCallback((item: ActionTreeItem) => {
onSelected?.(item.action!);
}, [onSelected]);
const onHighlightedAction = React.useCallback((item: ActionTreeItem | undefined) => {
onHighlighted?.(item?.action);
}, [onHighlighted]);
return <div className='vbox'> return <div className='vbox'>
{selectedTime && <div className='action-list-show-all' onClick={() => setSelectedTime(undefined)}><span className='codicon codicon-triangle-left'></span>Show all</div>} {selectedTime && <div className='action-list-show-all' onClick={() => setSelectedTime(undefined)}><span className='codicon codicon-triangle-left'></span>Show all</div>}
<ActionTreeView <ActionTreeView
@ -67,12 +91,12 @@ export const ActionList: React.FC<ActionListProps> = ({
treeState={treeState} treeState={treeState}
setTreeState={setTreeState} setTreeState={setTreeState}
selectedItem={selectedItem} selectedItem={selectedItem}
onSelected={item => onSelected?.(item.action!)} onSelected={onSelectedAction}
onHighlighted={item => onHighlighted?.(item?.action)} onHighlighted={onHighlightedAction}
onAccepted={item => setSelectedTime({ minimum: item.action!.startTime, maximum: item.action!.endTime })} onAccepted={onAccepted}
isError={item => !!item.action?.error?.message} isError={isError}
isVisible={item => !selectedTime || (item.action!.startTime <= selectedTime.maximum && item.action!.endTime >= selectedTime.minimum)} isVisible={isVisible}
render={item => renderAction(item.action!, { sdkLanguage, revealConsole, isLive, showDuration: true, showBadges: true })} render={render}
/> />
</div>; </div>;
}; };

View file

@ -14,28 +14,28 @@
limitations under the License. limitations under the License.
*/ */
.ui-mode-list-item { .ui-mode-tree-item {
flex: auto; flex: auto;
} }
.ui-mode-list-item-title { .ui-mode-tree-item-title {
flex: auto; flex: auto;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
} }
.ui-mode-list-item-time { .ui-mode-tree-item-time {
flex: none; flex: none;
color: var(--vscode-editorCodeLens-foreground); color: var(--vscode-editorCodeLens-foreground);
margin: 0 4px; margin: 0 4px;
user-select: none; user-select: none;
} }
.tests-list-view .list-view-entry.selected .ui-mode-list-item-time, .tests-tree-view .tree-view-entry.selected .ui-mode-tree-item-time,
.tests-list-view .list-view-entry.highlighted .ui-mode-list-item-time { .tests-tree-view .tree-view-entry.highlighted .ui-mode-tree-item-time {
display: none; display: none;
} }
.tests-list-view .list-view-entry:not(.highlighted):not(.selected) .toolbar-button:not(.toggled) { .tests-tree-view .tree-view-entry:not(.highlighted):not(.selected) .toolbar-button:not(.toggled) {
display: none; display: none;
} }

View file

@ -159,12 +159,15 @@ export const TestListView: React.FC<{
rootItem={testTree.rootItem} rootItem={testTree.rootItem}
dataTestId='test-tree' dataTestId='test-tree'
render={treeItem => { render={treeItem => {
return <div className='hbox ui-mode-list-item'> const prefixId = treeItem.id.replace(/[^\w\d-_]/g, '-');
<div className='ui-mode-list-item-title'> const labelId = prefixId + '-label';
<span title={treeItem.title}>{treeItem.title}</span> const timeId = prefixId + '-time';
return <div className='hbox ui-mode-tree-item' aria-labelledby={`${labelId} ${timeId}`}>
<div id={labelId} className='ui-mode-tree-item-title'>
<span>{treeItem.title}</span>
{treeItem.kind === 'case' ? treeItem.tags.map(tag => <TagView key={tag} tag={tag.slice(1)} onClick={e => handleTagClick(e, tag)} />) : null} {treeItem.kind === 'case' ? treeItem.tags.map(tag => <TagView key={tag} tag={tag.slice(1)} onClick={e => handleTagClick(e, tag)} />) : null}
</div> </div>
{!!treeItem.duration && treeItem.status !== 'skipped' && <div className='ui-mode-list-item-time'>{msToString(treeItem.duration)}</div>} {!!treeItem.duration && treeItem.status !== 'skipped' && <div id={timeId} className='ui-mode-tree-item-time'>{msToString(treeItem.duration)}</div>}
<Toolbar noMinHeight={true} noShadow={true}> <Toolbar noMinHeight={true} noShadow={true}>
<ToolbarButton icon='play' title='Run' onClick={() => runTreeItem(treeItem)} disabled={!!runningState && !runningState.completed}></ToolbarButton> <ToolbarButton icon='play' title='Run' onClick={() => runTreeItem(treeItem)} disabled={!!runningState && !runningState.completed}></ToolbarButton>
<ToolbarButton icon='go-to-file' title='Show source' onClick={onRevealSource} style={(treeItem.kind === 'group' && treeItem.subKind === 'folder') ? { visibility: 'hidden' } : {}}></ToolbarButton> <ToolbarButton icon='go-to-file' title='Show source' onClick={onRevealSource} style={(treeItem.kind === 'group' && treeItem.subKind === 'folder') ? { visibility: 'hidden' } : {}}></ToolbarButton>
@ -179,6 +182,7 @@ export const TestListView: React.FC<{
</div>; </div>;
}} }}
icon={treeItem => testStatusIcon(treeItem.status)} icon={treeItem => testStatusIcon(treeItem.status)}
title={treeItem => treeItem.title}
selectedItem={selectedTreeItem} selectedItem={selectedTreeItem}
onAccepted={runTreeItem} onAccepted={runTreeItem}
onSelected={treeItem => { onSelected={treeItem => {

View file

@ -110,15 +110,12 @@ export function GridView<T>(model: GridViewProps<T>) {
</>; </>;
}} }}
icon={model.icon} icon={model.icon}
indent={model.indent}
isError={model.isError} isError={model.isError}
isWarning={model.isWarning} isWarning={model.isWarning}
isInfo={model.isInfo} isInfo={model.isInfo}
selectedItem={model.selectedItem} selectedItem={model.selectedItem}
onAccepted={model.onAccepted} onAccepted={model.onAccepted}
onSelected={model.onSelected} onSelected={model.onSelected}
onLeftArrow={model.onLeftArrow}
onRightArrow={model.onRightArrow}
onHighlighted={model.onHighlighted} onHighlighted={model.onHighlighted}
onIconClicked={model.onIconClicked} onIconClicked={model.onIconClicked}
noItemsMessage={model.noItemsMessage} noItemsMessage={model.noItemsMessage}

View file

@ -16,7 +16,7 @@
import * as React from 'react'; import * as React from 'react';
import './listView.css'; import './listView.css';
import { clsx } from '@web/uiUtils'; import { clsx, scrollIntoViewIfNeeded } from '@web/uiUtils';
export type ListViewProps<T> = { export type ListViewProps<T> = {
name: string, name: string,
@ -24,15 +24,12 @@ export type ListViewProps<T> = {
id?: (item: T, index: number) => string, id?: (item: T, index: number) => string,
render: (item: T, index: number) => React.ReactNode, render: (item: T, index: number) => React.ReactNode,
icon?: (item: T, index: number) => string | undefined, icon?: (item: T, index: number) => string | undefined,
indent?: (item: T, index: number) => number | undefined,
isError?: (item: T, index: number) => boolean, isError?: (item: T, index: number) => boolean,
isWarning?: (item: T, index: number) => boolean, isWarning?: (item: T, index: number) => boolean,
isInfo?: (item: T, index: number) => boolean, isInfo?: (item: T, index: number) => boolean,
selectedItem?: T, selectedItem?: T,
onAccepted?: (item: T, index: number) => void, onAccepted?: (item: T, index: number) => void,
onSelected?: (item: T, index: number) => void, onSelected?: (item: T, index: number) => void,
onLeftArrow?: (item: T, index: number) => void,
onRightArrow?: (item: T, index: number) => void,
onHighlighted?: (item: T | undefined) => void, onHighlighted?: (item: T | undefined) => void,
onIconClicked?: (item: T, index: number) => void, onIconClicked?: (item: T, index: number) => void,
noItemsMessage?: string, noItemsMessage?: string,
@ -51,12 +48,9 @@ export function ListView<T>({
isError, isError,
isWarning, isWarning,
isInfo, isInfo,
indent,
selectedItem, selectedItem,
onAccepted, onAccepted,
onSelected, onSelected,
onLeftArrow,
onRightArrow,
onHighlighted, onHighlighted,
onIconClicked, onIconClicked,
noItemsMessage, noItemsMessage,
@ -95,21 +89,12 @@ export function ListView<T>({
onAccepted?.(selectedItem, items.indexOf(selectedItem)); onAccepted?.(selectedItem, items.indexOf(selectedItem));
return; return;
} }
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp' && event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp')
return; return;
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
if (selectedItem && event.key === 'ArrowLeft') {
onLeftArrow?.(selectedItem, items.indexOf(selectedItem));
return;
}
if (selectedItem && event.key === 'ArrowRight') {
onRightArrow?.(selectedItem, items.indexOf(selectedItem));
return;
}
const index = selectedItem ? items.indexOf(selectedItem) : -1; const index = selectedItem ? items.indexOf(selectedItem) : -1;
let newIndex = index; let newIndex = index;
if (event.key === 'ArrowDown') { if (event.key === 'ArrowDown') {
@ -135,7 +120,6 @@ export function ListView<T>({
> >
{noItemsMessage && items.length === 0 && <div className='list-view-empty'>{noItemsMessage}</div>} {noItemsMessage && items.length === 0 && <div className='list-view-empty'>{noItemsMessage}</div>}
{items.map((item, index) => { {items.map((item, index) => {
const indentation = indent?.(item, index) || 0;
const rendered = render(item, index); const rendered = render(item, index);
return <div return <div
key={id?.(item, index) || index} key={id?.(item, index) || index}
@ -152,8 +136,6 @@ export function ListView<T>({
onMouseEnter={() => setHighlightedItem(item)} onMouseEnter={() => setHighlightedItem(item)}
onMouseLeave={() => setHighlightedItem(undefined)} onMouseLeave={() => setHighlightedItem(undefined)}
> >
{/* eslint-disable-next-line react/jsx-key */}
{indentation ? new Array(indentation).fill(0).map(() => <div className='list-view-indent'></div>) : undefined}
{icon && <div {icon && <div
className={'codicon ' + (icon(item, index) || 'codicon-blank')} className={'codicon ' + (icon(item, index) || 'codicon-blank')}
style={{ minWidth: 16, marginRight: 4 }} style={{ minWidth: 16, marginRight: 4 }}
@ -173,12 +155,3 @@ export function ListView<T>({
</div> </div>
</div>; </div>;
} }
function scrollIntoViewIfNeeded(element: Element | undefined) {
if (!element)
return;
if ((element as any)?.scrollIntoViewIfNeeded)
(element as any).scrollIntoViewIfNeeded(false);
else
element?.scrollIntoView();
}

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