Merge branch 'main' into html-reporter-attachments
This commit is contained in:
commit
a0ba3510e9
269
CONTRIBUTING.md
269
CONTRIBUTING.md
|
|
@ -1,92 +1,77 @@
|
|||
# Contributing
|
||||
|
||||
- [How to Contribute](#how-to-contribute)
|
||||
* [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)
|
||||
## Choose an issue
|
||||
|
||||
## 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 sure you're running Node.js 20 to verify and upgrade NPM do:
|
||||
## Make a change
|
||||
|
||||
Make sure you're running Node.js 20 or later.
|
||||
```bash
|
||||
node --version
|
||||
npm --version
|
||||
npm i -g npm@latest
|
||||
```
|
||||
|
||||
1. Clone this repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/microsoft/playwright
|
||||
cd playwright
|
||||
```
|
||||
|
||||
2. Install dependencies
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
```
|
||||
|
||||
3. Build Playwright
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
4. Run tests
|
||||
|
||||
This will run a test on line `23` in `page-fill.spec.ts`:
|
||||
|
||||
```bash
|
||||
npm run ctest -- page-fill:23
|
||||
```
|
||||
|
||||
See [here](#running--writing-tests) for more information about running and writing 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:
|
||||
|
||||
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
|
||||
npm run eslint
|
||||
git clone https://github.com/microsoft/playwright
|
||||
cd playwright
|
||||
```
|
||||
|
||||
### API guidelines
|
||||
Install dependencies and run the build in watch mode.
|
||||
```bash
|
||||
npm ci
|
||||
npm run watch
|
||||
npx playwright install
|
||||
```
|
||||
|
||||
When authoring new API methods, consider the following:
|
||||
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).
|
||||
|
||||
- Expose as little information as needed. When in doubt, don’t expose new information.
|
||||
- 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.
|
||||
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).
|
||||
|
||||
### Commit Messages
|
||||
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
|
||||
npm run lint
|
||||
```
|
||||
|
||||
Commit messages should follow the Semantic Commit Messages format:
|
||||
Comments should be generally avoided. If the code would not be understood without comments, consider re-writing the code to make it self-explanatory.
|
||||
|
||||
### 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
|
||||
# fast path runs all tests in Chromium
|
||||
npm run ctest
|
||||
|
||||
# slow path runs all tests in three browsers
|
||||
npm run test
|
||||
```
|
||||
|
||||
- Test runner tests.
|
||||
```bash
|
||||
npm run ttest
|
||||
```
|
||||
|
||||
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).
|
||||
|
||||
Note that tests should be *hermetic*, and not depend on external services. Tests should work on all three platforms: macOS, Linux and Windows.
|
||||
|
||||
## Write a commit message
|
||||
|
||||
Commit messages should follow the [Semantic Commit Messages](https://www.conventionalcommits.org/en/v1.0.0/) format:
|
||||
|
||||
```
|
||||
label(namespace): title
|
||||
|
|
@ -97,131 +82,57 @@ footer
|
|||
```
|
||||
|
||||
1. *label* is one of the following:
|
||||
- `fix` - playwright bug fixes.
|
||||
- `feat` - playwright features.
|
||||
- `docs` - changes to docs, e.g. `docs(api): ..` to change documentation.
|
||||
- `test` - changes to playwright tests infrastructure.
|
||||
- `devops` - build-related work, e.g. CI related patches and general changes to the browser build infrastructure
|
||||
- `fix` - bug fixes
|
||||
- `feat` - new features
|
||||
- `docs` - documentation-only changes
|
||||
- `test` - test-only changes
|
||||
- `devops` - changes to the CI or build
|
||||
- `chore` - everything that doesn't fall under previous categories
|
||||
2. *namespace* is put in parenthesis after label and is optional. Must be lowercase.
|
||||
3. *title* is a brief summary of changes.
|
||||
4. *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. *namespace* is put in parenthesis after label and is optional. Must be lowercase.
|
||||
1. *title* is a brief summary of changes.
|
||||
1. *description* is **optional**, new-line separated from title and is in present tense.
|
||||
1. *footer* is **optional**, new-line separated from *description* and contains "fixes" / "references" attribution to github issues.
|
||||
|
||||
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!
|
||||
|
||||
## More details
|
||||
|
||||
**No new dependencies**
|
||||
|
||||
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.
|
||||
|
||||
**Custom browser build**
|
||||
|
||||
To run tests with custom browser executable, specify `CRPATH`, `WKPATH` or `FFPATH` env variable that points to browser executable:
|
||||
```bash
|
||||
npm run doc
|
||||
CRPATH=<path-to-executable> npm run ctest
|
||||
```
|
||||
|
||||
To build the documentation site locally and test how your changes will look in practice:
|
||||
You will also find `DEBUG=pw:browser` useful for debugging custom builds.
|
||||
|
||||
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
|
||||
**Building documentation site**
|
||||
|
||||
### Adding New Dependencies
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
```bash
|
||||
CRPATH=<path-to-executable> npm run ctest
|
||||
```
|
||||
|
||||
- When should a test be marked with `skip` or `fixme`?
|
||||
|
||||
- **`skip(condition)`**: This test *should ***never*** work* for `condition`
|
||||
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`
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
||||
* [Getting started](https://playwright.dev/docs/intro)
|
||||
* [Installation configuration](https://playwright.dev/docs/installation)
|
||||
* [API reference](https://playwright.dev/docs/api/class-playwright)
|
||||
|
||||
## Capabilities
|
||||
|
|
@ -163,7 +162,7 @@ test('Intercept network requests', async ({ page }) => {
|
|||
|
||||
## Resources
|
||||
|
||||
* [Documentation](https://playwright.dev/docs/intro)
|
||||
* [Documentation](https://playwright.dev)
|
||||
* [API reference](https://playwright.dev/docs/api/class-playwright/)
|
||||
* [Contribution guide](CONTRIBUTING.md)
|
||||
* [Changelog](https://github.com/microsoft/playwright/releases)
|
||||
|
|
|
|||
|
|
@ -62,11 +62,11 @@ expect(page.get_by_text("Welcome, John!")).to_be_visible()
|
|||
```
|
||||
|
||||
```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();
|
||||
```
|
||||
|
|
@ -101,7 +101,7 @@ page.get_by_role("button", name="Sign in").click()
|
|||
```
|
||||
|
||||
```csharp
|
||||
await page.GetByRole(AriaRole.Button, new() { Name = "Sign in" }).ClickAsync();
|
||||
await Page.GetByRole(AriaRole.Button, new() { Name = "Sign in" }).ClickAsync();
|
||||
```
|
||||
|
||||
:::note
|
||||
|
|
@ -143,7 +143,7 @@ locator.click()
|
|||
```
|
||||
|
||||
```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.ClickAsync();
|
||||
|
|
@ -180,7 +180,7 @@ locator.click()
|
|||
```
|
||||
|
||||
```csharp
|
||||
var locator = page
|
||||
var locator = Page
|
||||
.FrameLocator("#my-frame")
|
||||
.GetByRole(AriaRole.Button, new() { Name = "Sign in" });
|
||||
|
||||
|
|
@ -249,11 +249,11 @@ await Expect(Page
|
|||
.GetByRole(AriaRole.Heading, new() { Name = "Sign up" }))
|
||||
.ToBeVisibleAsync();
|
||||
|
||||
await page
|
||||
await Page
|
||||
.GetByRole(AriaRole.Checkbox, new() { Name = "Subscribe" })
|
||||
.CheckAsync();
|
||||
|
||||
await page
|
||||
await Page
|
||||
.GetByRole(AriaRole.Button, new() {
|
||||
NameRegex = new Regex("submit", RegexOptions.IgnoreCase)
|
||||
})
|
||||
|
|
@ -298,7 +298,7 @@ page.get_by_label("Password").fill("secret")
|
|||
```
|
||||
|
||||
```csharp
|
||||
await page.GetByLabel("Password").FillAsync("secret");
|
||||
await Page.GetByLabel("Password").FillAsync("secret");
|
||||
```
|
||||
|
||||
:::note[When to use label locators]
|
||||
|
|
@ -335,7 +335,7 @@ page.get_by_placeholder("name@example.com").fill("playwright@microsoft.com")
|
|||
```
|
||||
|
||||
```csharp
|
||||
await page
|
||||
await Page
|
||||
.GetByPlaceholder("name@example.com")
|
||||
.FillAsync("playwright@microsoft.com");
|
||||
```
|
||||
|
|
@ -468,7 +468,7 @@ page.get_by_alt_text("playwright logo").click()
|
|||
```
|
||||
|
||||
```csharp
|
||||
await page.GetByAltText("playwright logo").ClickAsync();
|
||||
await Page.GetByAltText("playwright logo").ClickAsync();
|
||||
```
|
||||
|
||||
:::note[When to use alt locators]
|
||||
|
|
@ -540,7 +540,7 @@ page.get_by_test_id("directions").click()
|
|||
```
|
||||
|
||||
```csharp
|
||||
await page.GetByTestId("directions").ClickAsync();
|
||||
await Page.GetByTestId("directions").ClickAsync();
|
||||
```
|
||||
|
||||
:::note[When to use testid locators]
|
||||
|
|
@ -604,7 +604,7 @@ page.get_by_test_id("directions").click()
|
|||
```
|
||||
|
||||
```csharp
|
||||
await page.GetByTestId("directions").ClickAsync();
|
||||
await Page.GetByTestId("directions").ClickAsync();
|
||||
```
|
||||
|
||||
### Locate by CSS or XPath
|
||||
|
|
@ -644,11 +644,11 @@ page.locator("//button").click()
|
|||
```
|
||||
|
||||
```csharp
|
||||
await page.Locator("css=button").ClickAsync();
|
||||
await page.Locator("xpath=//button").ClickAsync();
|
||||
await Page.Locator("css=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:
|
||||
|
|
@ -688,9 +688,9 @@ page.locator('//*[@id="tsf"]/div[2]/div[1]/div[1]/div/div[2]/input').click()
|
|||
```
|
||||
|
||||
```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]
|
||||
|
|
|
|||
|
|
@ -8,6 +8,11 @@ import LiteYouTube from '@site/src/components/LiteYouTube';
|
|||
|
||||
## Version 1.48
|
||||
|
||||
<LiteYouTube
|
||||
id="VGlkSBkMVCQ"
|
||||
title="Playwright 1.48"
|
||||
/>
|
||||
|
||||
### 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"`.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
* since: v1.10
|
||||
|
||||
|
|
|
|||
|
|
@ -110,9 +110,9 @@ export default defineConfig({
|
|||
|
||||
## property: TestConfig.globalSetup
|
||||
* 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).
|
||||
|
||||
|
|
@ -128,9 +128,9 @@ export default defineConfig({
|
|||
|
||||
## property: TestConfig.globalTeardown
|
||||
* 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).
|
||||
|
||||
|
|
|
|||
|
|
@ -22,9 +22,10 @@ import { ImageDiffView } from '@web/shared/imageDiffView';
|
|||
|
||||
export const TestErrorView: React.FC<{
|
||||
error: string;
|
||||
}> = ({ error }) => {
|
||||
testId?: string;
|
||||
}> = ({ error, testId }) => {
|
||||
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<{
|
||||
|
|
|
|||
|
|
@ -215,7 +215,7 @@ const StepTreeItem: React.FC<{
|
|||
return <AttachmentLink key={`attachment-${a}`} attachment={attachment} depth={depth + 1} openInNewTab={getAttachmentCategory(attachment) === 'html'}/>;
|
||||
}));
|
||||
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;
|
||||
} : undefined} depth={depth}></TreeItem>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -40,8 +40,12 @@ export function parseAriaSnapshot(text: string): AriaTemplateNode {
|
|||
return { role };
|
||||
};
|
||||
|
||||
const normalizeWhitespace = (text: string) => {
|
||||
return text.replace(/[\r\n\s\t]+/g, ' ').trim();
|
||||
};
|
||||
|
||||
const valueOrRegex = (value: string): string | RegExp => {
|
||||
return value.startsWith('/') && value.endsWith('/') ? new RegExp(value.slice(1, -1)) : value;
|
||||
return value.startsWith('/') && value.endsWith('/') ? new RegExp(value.slice(1, -1)) : normalizeWhitespace(value);
|
||||
};
|
||||
|
||||
const convert = (object: YamlNode | string): AriaTemplateNode | RegExp | string => {
|
||||
|
|
|
|||
|
|
@ -15,13 +15,13 @@
|
|||
*/
|
||||
|
||||
import { escapeWithQuotes } from '@isomorphic/stringUtils';
|
||||
import { beginAriaCaches, endAriaCaches, getAriaRole, getElementAccessibleName, isElementIgnoredForAria } from './roleUtils';
|
||||
import { isElementVisible, isElementStyleVisibilityVisible } from './domUtils';
|
||||
import { accumulatedElementText, beginAriaCaches, endAriaCaches, getAriaRole, getElementAccessibleName, getPseudoContent, isElementIgnoredForAria } from './roleUtils';
|
||||
import { isElementVisible, isElementStyleVisibilityVisible, getElementComputedStyle } from './domUtils';
|
||||
|
||||
type AriaNode = {
|
||||
role: string;
|
||||
name?: string;
|
||||
children?: (AriaNode | string)[];
|
||||
children: (AriaNode | string)[];
|
||||
};
|
||||
|
||||
export type AriaTemplateNode = {
|
||||
|
|
@ -38,16 +38,20 @@ export function generateAriaTree(rootElement: Element): AriaNode {
|
|||
|
||||
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];
|
||||
const result: AriaNode = { role, name, children: [] };
|
||||
if (isLeaf && !name) {
|
||||
const text = accumulatedElementText(element);
|
||||
if (text)
|
||||
result.children = [text];
|
||||
}
|
||||
return { isLeaf, ariaNode: result };
|
||||
};
|
||||
|
||||
const visit = (ariaNode: AriaNode, node: Node) => {
|
||||
if (node.nodeType === Node.TEXT_NODE && node.nodeValue) {
|
||||
ariaNode.children = ariaNode.children || [];
|
||||
ariaNode.children.push(node.nodeValue);
|
||||
const text = node.nodeValue;
|
||||
if (text)
|
||||
ariaNode.children.push(node.nodeValue || '');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -67,10 +71,8 @@ export function generateAriaTree(rootElement: Element): AriaNode {
|
|||
if (visible) {
|
||||
const childAriaNode = toAriaNode(element);
|
||||
const isHiddenContainer = childAriaNode && hiddenContainerRoles.has(childAriaNode.ariaNode.role);
|
||||
if (childAriaNode && !isHiddenContainer) {
|
||||
ariaNode.children = ariaNode.children || [];
|
||||
if (childAriaNode && !isHiddenContainer)
|
||||
ariaNode.children.push(childAriaNode.ariaNode);
|
||||
}
|
||||
if (isHiddenContainer || !childAriaNode?.isLeaf)
|
||||
processChildNodes(childAriaNode?.ariaNode || ariaNode, element);
|
||||
} else {
|
||||
|
|
@ -79,18 +81,36 @@ export function generateAriaTree(rootElement: Element): AriaNode {
|
|||
};
|
||||
|
||||
function processChildNodes(ariaNode: AriaNode, element: Element) {
|
||||
// Process light DOM children
|
||||
for (let child = element.firstChild; child; child = child.nextSibling)
|
||||
// Surround every element with spaces for the sake of concatenated text nodes.
|
||||
const display = getElementComputedStyle(element)?.display || 'inline';
|
||||
const treatAsBlock = (display !== 'inline' || element.nodeName === 'BR') ? ' ' : '';
|
||||
if (treatAsBlock)
|
||||
ariaNode.children.push(treatAsBlock);
|
||||
|
||||
ariaNode.children.push(getPseudoContent(element, '::before'));
|
||||
const assignedNodes = element.nodeName === 'SLOT' ? (element as HTMLSlotElement).assignedNodes() : [];
|
||||
if (assignedNodes.length) {
|
||||
for (const child of assignedNodes)
|
||||
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) {
|
||||
for (let child = element.shadowRoot.firstChild; child; child = child.nextSibling)
|
||||
visit(ariaNode, child);
|
||||
}
|
||||
}
|
||||
|
||||
ariaNode.children.push(getPseudoContent(element, '::after'));
|
||||
|
||||
if (treatAsBlock)
|
||||
ariaNode.children.push(treatAsBlock);
|
||||
}
|
||||
|
||||
beginAriaCaches();
|
||||
const ariaRoot: AriaNode = { role: '' };
|
||||
const ariaRoot: AriaNode = { role: '', children: [] };
|
||||
try {
|
||||
visit(ariaRoot, rootElement);
|
||||
} finally {
|
||||
|
|
@ -128,7 +148,7 @@ function normalizeStringChildren(rootA11yNode: AriaNode) {
|
|||
}
|
||||
}
|
||||
flushChildren(buffer, normalizedChildren);
|
||||
ariaNode.children = normalizedChildren.length ? normalizedChildren : undefined;
|
||||
ariaNode.children = normalizedChildren.length ? normalizedChildren : [];
|
||||
};
|
||||
visit(rootA11yNode);
|
||||
}
|
||||
|
|
@ -144,7 +164,7 @@ const leafRoles = new Set([
|
|||
'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) {
|
||||
if (!template)
|
||||
|
|
@ -233,7 +253,7 @@ export function renderAriaTree(ariaNode: AriaNode): string {
|
|||
lines.push(line);
|
||||
return;
|
||||
}
|
||||
lines.push(line + (ariaNode.children ? ':' : ''));
|
||||
lines.push(line + (ariaNode.children.length ? ':' : ''));
|
||||
for (const child of ariaNode.children || [])
|
||||
visit(child, indent + ' ');
|
||||
};
|
||||
|
|
|
|||
|
|
@ -363,7 +363,7 @@ function queryInAriaOwned(element: Element, selector: string): Element[] {
|
|||
return result;
|
||||
}
|
||||
|
||||
function getPseudoContent(element: Element, pseudo: '::before' | '::after') {
|
||||
export function getPseudoContent(element: Element, pseudo: '::before' | '::after') {
|
||||
const cache = pseudo === '::before' ? cachePseudoContentBefore : cachePseudoContentAfter;
|
||||
if (cache?.has(element))
|
||||
return cache?.get(element) || '';
|
||||
|
|
@ -430,10 +430,6 @@ export function getElementAccessibleName(element: Element, includeHidden: boolea
|
|||
accessibleName = asFlatString(getTextAlternativeInternal(element, {
|
||||
includeHidden,
|
||||
visitedElements: new Set(),
|
||||
embeddedInDescribedBy: undefined,
|
||||
embeddedInLabelledBy: undefined,
|
||||
embeddedInLabel: undefined,
|
||||
embeddedInNativeTextAlternative: undefined,
|
||||
embeddedInTargetElement: 'self',
|
||||
}));
|
||||
}
|
||||
|
|
@ -458,10 +454,6 @@ export function getElementAccessibleDescription(element: Element, includeHidden:
|
|||
accessibleDescription = asFlatString(describedBy.map(ref => getTextAlternativeInternal(ref, {
|
||||
includeHidden,
|
||||
visitedElements: new Set(),
|
||||
embeddedInLabelledBy: undefined,
|
||||
embeddedInLabel: undefined,
|
||||
embeddedInNativeTextAlternative: undefined,
|
||||
embeddedInTargetElement: 'none',
|
||||
embeddedInDescribedBy: { element: ref, hidden: isElementHiddenForAria(ref) },
|
||||
})).join(' '));
|
||||
} else if (element.hasAttribute('aria-description')) {
|
||||
|
|
@ -480,13 +472,13 @@ export function getElementAccessibleDescription(element: Element, includeHidden:
|
|||
}
|
||||
|
||||
type AccessibleNameOptions = {
|
||||
includeHidden: boolean,
|
||||
visitedElements: Set<Element>,
|
||||
embeddedInDescribedBy: { element: Element, hidden: boolean } | undefined,
|
||||
embeddedInLabelledBy: { element: Element, hidden: boolean } | undefined,
|
||||
embeddedInLabel: { element: Element, hidden: boolean } | undefined,
|
||||
embeddedInNativeTextAlternative: { element: Element, hidden: boolean } | undefined,
|
||||
embeddedInTargetElement: 'none' | 'self' | 'descendant',
|
||||
includeHidden?: boolean,
|
||||
embeddedInDescribedBy?: { element: Element, hidden: boolean },
|
||||
embeddedInLabelledBy?: { element: Element, hidden: boolean },
|
||||
embeddedInLabel?: { element: Element, hidden: boolean },
|
||||
embeddedInNativeTextAlternative?: { element: Element, hidden: boolean },
|
||||
embeddedInTargetElement?: 'self' | 'descendant',
|
||||
};
|
||||
|
||||
function getTextAlternativeInternal(element: Element, options: AccessibleNameOptions): string {
|
||||
|
|
@ -525,7 +517,7 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
|
|||
...options,
|
||||
embeddedInLabelledBy: { element: ref, hidden: isElementHiddenForAria(ref) },
|
||||
embeddedInDescribedBy: undefined,
|
||||
embeddedInTargetElement: 'none',
|
||||
embeddedInTargetElement: undefined,
|
||||
embeddedInLabel: undefined,
|
||||
embeddedInNativeTextAlternative: undefined,
|
||||
})).join(' ');
|
||||
|
|
@ -778,13 +770,35 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
|
|||
!!options.embeddedInLabelledBy || !!options.embeddedInDescribedBy ||
|
||||
!!options.embeddedInLabel || !!options.embeddedInNativeTextAlternative) {
|
||||
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 visit = (node: Node, skipSlotted: boolean) => {
|
||||
if (skipSlotted && (node as Element | Text).assignedSlot)
|
||||
return;
|
||||
if (node.nodeType === 1 /* Node.ELEMENT_NODE */) {
|
||||
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 says "append the result to the accumulated text", assuming "with space".
|
||||
// However, multiple tests insist that inline elements do not add a space.
|
||||
|
|
@ -813,25 +827,12 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
|
|||
visit(owned, true);
|
||||
}
|
||||
tokens.push(getPseudoContent(element, '::after'));
|
||||
const accessibleName = 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;
|
||||
}
|
||||
return tokens.join('');
|
||||
}
|
||||
|
||||
// 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 '';
|
||||
export function accumulatedElementText(element: Element): string {
|
||||
const visitedElements = new Set<Element>();
|
||||
return asFlatString(innerAccumulatedElementText(element, { visitedElements })).trim();
|
||||
}
|
||||
|
||||
export const kAriaSelectedRoles = ['gridcell', 'option', 'row', 'tab', 'rowheader', 'columnheader', 'treeitem'];
|
||||
|
|
@ -958,7 +959,7 @@ function getAccessibleNameFromAssociatedLabels(labels: Iterable<HTMLLabelElement
|
|||
embeddedInNativeTextAlternative: undefined,
|
||||
embeddedInLabelledBy: undefined,
|
||||
embeddedInDescribedBy: undefined,
|
||||
embeddedInTargetElement: 'none',
|
||||
embeddedInTargetElement: undefined,
|
||||
})).filter(accessibleName => !!accessibleName).join(' ');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,9 @@ export class FullConfigInternal {
|
|||
testIdMatcher?: Matcher;
|
||||
defineConfigWasUsed = false;
|
||||
|
||||
globalSetups: string[] = [];
|
||||
globalTeardowns: string[] = [];
|
||||
|
||||
constructor(location: ConfigLocation, userConfig: Config, configCLIOverrides: ConfigCLIOverrides) {
|
||||
if (configCLIOverrides.projects && userConfig.projects)
|
||||
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.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 = {
|
||||
configFile: resolvedConfigFile,
|
||||
rootDir: pathResolve(configDir, userConfig.testDir) || configDir,
|
||||
forbidOnly: takeFirst(configCLIOverrides.forbidOnly, userConfig.forbidOnly, false),
|
||||
fullyParallel: takeFirst(configCLIOverrides.fullyParallel, userConfig.fullyParallel, false),
|
||||
globalSetup: takeFirst(resolveScript(userConfig.globalSetup, configDir), null),
|
||||
globalTeardown: takeFirst(resolveScript(userConfig.globalTeardown, configDir), null),
|
||||
globalSetup: this.globalSetups[0] ?? null,
|
||||
globalTeardown: this.globalTeardowns[0] ?? null,
|
||||
globalTimeout: takeFirst(configCLIOverrides.globalTimeout, userConfig.globalTimeout, 0),
|
||||
grep: takeFirst(userConfig.grep, defaultGrep),
|
||||
grepInvert: takeFirst(userConfig.grepInvert, null),
|
||||
|
|
|
|||
|
|
@ -139,14 +139,26 @@ function validateConfig(file: string, config: Config) {
|
|||
}
|
||||
|
||||
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`);
|
||||
}
|
||||
}
|
||||
|
||||
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`);
|
||||
}
|
||||
}
|
||||
|
||||
if ('globalTimeout' in config && config.globalTimeout !== undefined) {
|
||||
if (typeof config.globalTimeout !== 'number' || config.globalTimeout < 0)
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ export class TestTypeImpl {
|
|||
test.skip = wrapFunctionWithLocation(this._modifier.bind(this, 'skip'));
|
||||
test.fixme = wrapFunctionWithLocation(this._modifier.bind(this, 'fixme'));
|
||||
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.setTimeout = wrapFunctionWithLocation(this._setTimeout.bind(this));
|
||||
test.step = this._step.bind(this);
|
||||
|
|
@ -81,7 +82,7 @@ export class TestTypeImpl {
|
|||
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();
|
||||
const suite = this._currentSuite(location, 'test()');
|
||||
if (!suite)
|
||||
|
|
@ -104,10 +105,12 @@ export class TestTypeImpl {
|
|||
test._tags.push(...validatedDetails.tags);
|
||||
suite._addTest(test);
|
||||
|
||||
if (type === 'only')
|
||||
if (type === 'only' || type === 'fail.only')
|
||||
test._only = true;
|
||||
if (type === 'skip' || type === 'fixme' || type === 'fail')
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -49,17 +49,19 @@ export async function toMatchAriaSnapshot(
|
|||
|
||||
const messagePrefix = matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined);
|
||||
const notFound = received === kNoElementsFoundError;
|
||||
const escapedExpected = escapePrivateUsePoints(expected);
|
||||
const escapedReceived = escapePrivateUsePoints(received);
|
||||
const message = () => {
|
||||
if (pass) {
|
||||
if (notFound)
|
||||
return messagePrefix + `Expected: not ${this.utils.printExpected(expected)}\nReceived: ${received}` + callLogText(log);
|
||||
const printedReceived = printReceivedStringContainExpectedSubstring(received, received.indexOf(expected), expected.length);
|
||||
return messagePrefix + `Expected: not ${this.utils.printExpected(expected)}\nReceived string: ${printedReceived}` + callLogText(log);
|
||||
return messagePrefix + `Expected: not ${this.utils.printExpected(escapedExpected)}\nReceived: ${escapedReceived}` + callLogText(log);
|
||||
const printedReceived = printReceivedStringContainExpectedSubstring(escapedReceived, escapedReceived.indexOf(escapedExpected), escapedExpected.length);
|
||||
return messagePrefix + `Expected: not ${this.utils.printExpected(escapedExpected)}\nReceived string: ${printedReceived}` + callLogText(log);
|
||||
} else {
|
||||
const labelExpected = `Expected`;
|
||||
if (notFound)
|
||||
return messagePrefix + `${labelExpected}: ${this.utils.printExpected(expected)}\nReceived: ${received}` + callLogText(log);
|
||||
return messagePrefix + this.utils.printDiffOrStringify(expected, received, labelExpected, 'Received string', false) + callLogText(log);
|
||||
return messagePrefix + `${labelExpected}: ${this.utils.printExpected(escapedExpected)}\nReceived: ${escapedReceived}` + callLogText(log);
|
||||
return messagePrefix + this.utils.printDiffOrStringify(escapedExpected, escapedReceived, labelExpected, 'Received string', false) + callLogText(log);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -73,3 +75,7 @@ export async function toMatchAriaSnapshot(
|
|||
timeout: timedOut ? timeout : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function escapePrivateUsePoints(str: string) {
|
||||
return str.replace(/[\uE000-\uF8FF]/g, char => `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -512,8 +512,8 @@ class HtmlBuilder {
|
|||
error: step.error?.message,
|
||||
count
|
||||
};
|
||||
if (testStep.location)
|
||||
this._stepsInFile.set(testStep.location.file, testStep);
|
||||
if (step.location)
|
||||
this._stepsInFile.set(step.location.file, testStep);
|
||||
return testStep;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -98,8 +98,11 @@ export function createGlobalSetupTasks(config: FullConfigInternal) {
|
|||
if (!config.configCLIOverrides.preserveOutputDir && !process.env.PW_TEST_NO_REMOVE_OUTPUT_DIRS)
|
||||
tasks.push(createRemoveOutputDirsTask());
|
||||
tasks.push(...createPluginSetupTasks(config));
|
||||
if (config.config.globalSetup || config.config.globalTeardown)
|
||||
tasks.push(createGlobalSetupTask());
|
||||
if (config.globalSetups.length || config.globalTeardowns.length) {
|
||||
const length = Math.max(config.globalSetups.length, config.globalTeardowns.length);
|
||||
for (let i = 0; i < length; i++)
|
||||
tasks.push(createGlobalSetupTask(i, length));
|
||||
}
|
||||
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 globalSetupFinished = false;
|
||||
let teardownHook: any;
|
||||
|
||||
let title = 'global setup';
|
||||
if (length > 1)
|
||||
title += ` #${index}`;
|
||||
|
||||
return {
|
||||
title: 'global setup',
|
||||
title,
|
||||
setup: async ({ config }) => {
|
||||
const setupHook = config.config.globalSetup ? await loadGlobalHook(config, config.config.globalSetup) : undefined;
|
||||
teardownHook = config.config.globalTeardown ? await loadGlobalHook(config, config.config.globalTeardown) : undefined;
|
||||
const setupHook = config.globalSetups[index] ? await loadGlobalHook(config, config.globalSetups[index]) : undefined;
|
||||
teardownHook = config.globalTeardowns[index] ? await loadGlobalHook(config, config.globalTeardowns[index]) : undefined;
|
||||
globalSetupResult = setupHook ? await setupHook(config.config) : undefined;
|
||||
globalSetupFinished = true;
|
||||
},
|
||||
|
|
|
|||
1398
packages/playwright/types/test.d.ts
vendored
1398
packages/playwright/types/test.d.ts
vendored
File diff suppressed because it is too large
Load diff
|
|
@ -14,28 +14,28 @@
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.ui-mode-list-item {
|
||||
.ui-mode-tree-item {
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.ui-mode-list-item-title {
|
||||
.ui-mode-tree-item-title {
|
||||
flex: auto;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ui-mode-list-item-time {
|
||||
.ui-mode-tree-item-time {
|
||||
flex: none;
|
||||
color: var(--vscode-editorCodeLens-foreground);
|
||||
margin: 0 4px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tests-list-view .list-view-entry.selected .ui-mode-list-item-time,
|
||||
.tests-list-view .list-view-entry.highlighted .ui-mode-list-item-time {
|
||||
.tests-tree-view .tree-view-entry.selected .ui-mode-tree-item-time,
|
||||
.tests-tree-view .tree-view-entry.highlighted .ui-mode-tree-item-time {
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -159,12 +159,12 @@ export const TestListView: React.FC<{
|
|||
rootItem={testTree.rootItem}
|
||||
dataTestId='test-tree'
|
||||
render={treeItem => {
|
||||
return <div className='hbox ui-mode-list-item'>
|
||||
<div className='ui-mode-list-item-title'>
|
||||
return <div className='hbox ui-mode-tree-item'>
|
||||
<div className='ui-mode-tree-item-title'>
|
||||
<span title={treeItem.title}>{treeItem.title}</span>
|
||||
{treeItem.kind === 'case' ? treeItem.tags.map(tag => <TagView key={tag} tag={tag.slice(1)} onClick={e => handleTagClick(e, tag)} />) : null}
|
||||
</div>
|
||||
{!!treeItem.duration && treeItem.status !== 'skipped' && <div className='ui-mode-list-item-time'>{msToString(treeItem.duration)}</div>}
|
||||
{!!treeItem.duration && treeItem.status !== 'skipped' && <div className='ui-mode-tree-item-time'>{msToString(treeItem.duration)}</div>}
|
||||
<Toolbar noMinHeight={true} noShadow={true}>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -110,15 +110,12 @@ export function GridView<T>(model: GridViewProps<T>) {
|
|||
</>;
|
||||
}}
|
||||
icon={model.icon}
|
||||
indent={model.indent}
|
||||
isError={model.isError}
|
||||
isWarning={model.isWarning}
|
||||
isInfo={model.isInfo}
|
||||
selectedItem={model.selectedItem}
|
||||
onAccepted={model.onAccepted}
|
||||
onSelected={model.onSelected}
|
||||
onLeftArrow={model.onLeftArrow}
|
||||
onRightArrow={model.onRightArrow}
|
||||
onHighlighted={model.onHighlighted}
|
||||
onIconClicked={model.onIconClicked}
|
||||
noItemsMessage={model.noItemsMessage}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
import * as React from 'react';
|
||||
import './listView.css';
|
||||
import { clsx } from '@web/uiUtils';
|
||||
import { clsx, scrollIntoViewIfNeeded } from '@web/uiUtils';
|
||||
|
||||
export type ListViewProps<T> = {
|
||||
name: string,
|
||||
|
|
@ -24,15 +24,12 @@ export type ListViewProps<T> = {
|
|||
id?: (item: T, index: number) => string,
|
||||
render: (item: T, index: number) => React.ReactNode,
|
||||
icon?: (item: T, index: number) => string | undefined,
|
||||
indent?: (item: T, index: number) => number | undefined,
|
||||
isError?: (item: T, index: number) => boolean,
|
||||
isWarning?: (item: T, index: number) => boolean,
|
||||
isInfo?: (item: T, index: number) => boolean,
|
||||
selectedItem?: T,
|
||||
onAccepted?: (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,
|
||||
onIconClicked?: (item: T, index: number) => void,
|
||||
noItemsMessage?: string,
|
||||
|
|
@ -51,12 +48,9 @@ export function ListView<T>({
|
|||
isError,
|
||||
isWarning,
|
||||
isInfo,
|
||||
indent,
|
||||
selectedItem,
|
||||
onAccepted,
|
||||
onSelected,
|
||||
onLeftArrow,
|
||||
onRightArrow,
|
||||
onHighlighted,
|
||||
onIconClicked,
|
||||
noItemsMessage,
|
||||
|
|
@ -95,21 +89,12 @@ export function ListView<T>({
|
|||
onAccepted?.(selectedItem, items.indexOf(selectedItem));
|
||||
return;
|
||||
}
|
||||
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp' && event.key !== 'ArrowLeft' && event.key !== 'ArrowRight')
|
||||
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp')
|
||||
return;
|
||||
|
||||
event.stopPropagation();
|
||||
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;
|
||||
let newIndex = index;
|
||||
if (event.key === 'ArrowDown') {
|
||||
|
|
@ -135,7 +120,6 @@ export function ListView<T>({
|
|||
>
|
||||
{noItemsMessage && items.length === 0 && <div className='list-view-empty'>{noItemsMessage}</div>}
|
||||
{items.map((item, index) => {
|
||||
const indentation = indent?.(item, index) || 0;
|
||||
const rendered = render(item, index);
|
||||
return <div
|
||||
key={id?.(item, index) || index}
|
||||
|
|
@ -152,8 +136,6 @@ export function ListView<T>({
|
|||
onMouseEnter={() => setHighlightedItem(item)}
|
||||
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
|
||||
className={'codicon ' + (icon(item, index) || 'codicon-blank')}
|
||||
style={{ minWidth: 16, marginRight: 4 }}
|
||||
|
|
@ -173,12 +155,3 @@ export function ListView<T>({
|
|||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function scrollIntoViewIfNeeded(element: Element | undefined) {
|
||||
if (!element)
|
||||
return;
|
||||
if ((element as any)?.scrollIntoViewIfNeeded)
|
||||
(element as any).scrollIntoViewIfNeeded(false);
|
||||
else
|
||||
element?.scrollIntoView();
|
||||
}
|
||||
|
|
|
|||
91
packages/web/src/components/treeView.css
Normal file
91
packages/web/src/components/treeView.css
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
.tree-view-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: auto;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
overflow: hidden auto;
|
||||
outline: 1px solid transparent;
|
||||
}
|
||||
|
||||
.tree-view-entry {
|
||||
display: flex;
|
||||
flex: none;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
line-height: 28px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.tree-view-content.not-selectable > .tree-view-entry {
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
.tree-view-entry.highlighted:not(.selected) {
|
||||
background-color: var(--vscode-list-inactiveSelectionBackground) !important;
|
||||
}
|
||||
|
||||
.tree-view-entry.selected {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.tree-view-indent {
|
||||
min-width: 16px;
|
||||
}
|
||||
|
||||
.tree-view-content:focus .tree-view-entry.selected {
|
||||
background-color: var(--vscode-list-activeSelectionBackground);
|
||||
color: var(--vscode-list-activeSelectionForeground);
|
||||
outline: 1px solid var(--vscode-focusBorder);
|
||||
}
|
||||
|
||||
.tree-view-content .tree-view-entry.selected {
|
||||
background-color: var(--vscode-list-inactiveSelectionBackground);
|
||||
}
|
||||
|
||||
.tree-view-content:focus .tree-view-entry.selected * {
|
||||
color: var(--vscode-list-activeSelectionForeground) !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.tree-view-content:focus .tree-view-entry.selected .codicon {
|
||||
color: var(--vscode-list-activeSelectionForeground) !important;
|
||||
}
|
||||
|
||||
.tree-view-empty {
|
||||
flex: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tree-view-entry.error {
|
||||
color: var(--vscode-list-errorForeground);
|
||||
background-color: var(--vscode-inputValidation-errorBackground);
|
||||
}
|
||||
|
||||
.tree-view-entry.warning {
|
||||
color: var(--vscode-list-warningForeground);
|
||||
background-color: var(--vscode-inputValidation-warningBackground);
|
||||
}
|
||||
|
||||
.tree-view-entry.info {
|
||||
background-color: var(--vscode-inputValidation-infoBackground);
|
||||
}
|
||||
|
|
@ -15,7 +15,8 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { ListView } from './listView';
|
||||
import { clsx, scrollIntoViewIfNeeded } from '@web/uiUtils';
|
||||
import './treeView.css';
|
||||
|
||||
export type TreeItem = {
|
||||
id: string,
|
||||
|
|
@ -45,7 +46,7 @@ export type TreeViewProps<T> = {
|
|||
autoExpandDepth?: number,
|
||||
};
|
||||
|
||||
const TreeListView = ListView<TreeItem>;
|
||||
const scrollPositions = new Map<string, number>();
|
||||
|
||||
export function TreeView<T extends TreeItem>({
|
||||
name,
|
||||
|
|
@ -97,46 +98,31 @@ export function TreeView<T extends TreeItem>({
|
|||
return result;
|
||||
}, [treeItems, isVisible]);
|
||||
|
||||
return <TreeListView
|
||||
name={name}
|
||||
items={visibleItems}
|
||||
id={item => item.id}
|
||||
dataTestId={dataTestId || (name + '-tree')}
|
||||
render={item => {
|
||||
const rendered = render(item as T);
|
||||
return <>
|
||||
{icon && <div className={'codicon ' + (icon(item as T) || 'blank')} style={{ minWidth: 16, marginRight: 4 }}></div>}
|
||||
{typeof rendered === 'string' ? <div style={{ textOverflow: 'ellipsis', overflow: 'hidden' }}>{rendered}</div> : rendered}
|
||||
</>;
|
||||
}}
|
||||
icon={item => {
|
||||
const expanded = treeItems.get(item as T)!.expanded;
|
||||
if (typeof expanded === 'boolean')
|
||||
return expanded ? 'codicon-chevron-down' : 'codicon-chevron-right';
|
||||
}}
|
||||
isError={item => isError?.(item as T) || false}
|
||||
indent={item => treeItems.get(item as T)!.depth}
|
||||
selectedItem={selectedItem}
|
||||
onAccepted={item => onAccepted?.(item as T)}
|
||||
onSelected={item => onSelected?.(item as T)}
|
||||
onHighlighted={item => onHighlighted?.(item as T)}
|
||||
onLeftArrow={item => {
|
||||
const { expanded, parent } = treeItems.get(item as T)!;
|
||||
if (expanded) {
|
||||
treeState.expandedItems.set(item.id, false);
|
||||
setTreeState({ ...treeState });
|
||||
} else if (parent) {
|
||||
onSelected?.(parent as T);
|
||||
}
|
||||
}}
|
||||
onRightArrow={item => {
|
||||
if (item.children.length) {
|
||||
treeState.expandedItems.set(item.id, true);
|
||||
setTreeState({ ...treeState });
|
||||
}
|
||||
}}
|
||||
onIconClicked={item => {
|
||||
const { expanded } = treeItems.get(item as T)!;
|
||||
const itemListRef = React.useRef<HTMLDivElement>(null);
|
||||
const [highlightedItem, setHighlightedItem] = React.useState<any>();
|
||||
|
||||
React.useEffect(() => {
|
||||
onHighlighted?.(highlightedItem);
|
||||
}, [onHighlighted, highlightedItem]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const treeElem = itemListRef.current;
|
||||
if (!treeElem)
|
||||
return;
|
||||
const saveScrollPosition = () => {
|
||||
scrollPositions.set(name, treeElem.scrollTop);
|
||||
};
|
||||
treeElem.addEventListener('scroll', saveScrollPosition, { passive: true });
|
||||
return () => treeElem.removeEventListener('scroll', saveScrollPosition);
|
||||
}, [name]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (itemListRef.current)
|
||||
itemListRef.current.scrollTop = scrollPositions.get(name) || 0;
|
||||
}, [name]);
|
||||
|
||||
const toggleExpanded = React.useCallback((item: T) => {
|
||||
const { expanded } = treeItems.get(item)!;
|
||||
if (expanded) {
|
||||
// Move nested selection up.
|
||||
for (let i: TreeItem | undefined = selectedItem; i; i = i.parent) {
|
||||
|
|
@ -150,8 +136,147 @@ export function TreeView<T extends TreeItem>({
|
|||
treeState.expandedItems.set(item.id, true);
|
||||
}
|
||||
setTreeState({ ...treeState });
|
||||
}, [treeItems, selectedItem, onSelected, treeState, setTreeState]);
|
||||
|
||||
return <div className={clsx(`tree-view vbox`, name + '-tree-view')} role={'tree'} data-testid={dataTestId || (name + '-tree')}>
|
||||
<div
|
||||
className={clsx('tree-view-content')}
|
||||
tabIndex={0}
|
||||
onKeyDown={event => {
|
||||
if (selectedItem && event.key === 'Enter') {
|
||||
onAccepted?.(selectedItem);
|
||||
return;
|
||||
}
|
||||
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp' && event.key !== 'ArrowLeft' && event.key !== 'ArrowRight')
|
||||
return;
|
||||
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
if (selectedItem && event.key === 'ArrowLeft') {
|
||||
const { expanded, parent } = treeItems.get(selectedItem)!;
|
||||
if (expanded) {
|
||||
treeState.expandedItems.set(selectedItem.id, false);
|
||||
setTreeState({ ...treeState });
|
||||
} else if (parent) {
|
||||
onSelected?.(parent as T);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (selectedItem && event.key === 'ArrowRight') {
|
||||
if (selectedItem.children.length) {
|
||||
treeState.expandedItems.set(selectedItem.id, true);
|
||||
setTreeState({ ...treeState });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const index = selectedItem ? visibleItems.indexOf(selectedItem) : -1;
|
||||
let newIndex = index;
|
||||
if (event.key === 'ArrowDown') {
|
||||
if (index === -1)
|
||||
newIndex = 0;
|
||||
else
|
||||
newIndex = Math.min(index + 1, visibleItems.length - 1);
|
||||
}
|
||||
if (event.key === 'ArrowUp') {
|
||||
if (index === -1)
|
||||
newIndex = visibleItems.length - 1;
|
||||
else
|
||||
newIndex = Math.max(index - 1, 0);
|
||||
}
|
||||
|
||||
const element = itemListRef.current?.children.item(newIndex);
|
||||
scrollIntoViewIfNeeded(element || undefined);
|
||||
onHighlighted?.(undefined);
|
||||
onSelected?.(visibleItems[newIndex]);
|
||||
setHighlightedItem(undefined);
|
||||
}}
|
||||
noItemsMessage={noItemsMessage} />;
|
||||
ref={itemListRef}
|
||||
>
|
||||
{noItemsMessage && visibleItems.length === 0 && <div className='tree-view-empty'>{noItemsMessage}</div>}
|
||||
{visibleItems.map(item => {
|
||||
return <div key={item.id} role='treeitem' aria-selected={item === selectedItem}>
|
||||
<TreeItemHeader
|
||||
item={item}
|
||||
itemData={treeItems.get(item)!}
|
||||
selectedItem={selectedItem}
|
||||
onSelected={onSelected}
|
||||
onAccepted={onAccepted}
|
||||
isError={isError}
|
||||
toggleExpanded={toggleExpanded}
|
||||
highlightedItem={highlightedItem}
|
||||
setHighlightedItem={setHighlightedItem}
|
||||
render={render}
|
||||
icon={icon} />
|
||||
</div>;
|
||||
})}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
type TreeItemHeaderProps<T> = {
|
||||
item: T,
|
||||
itemData: TreeItemData,
|
||||
selectedItem: T | undefined,
|
||||
onSelected?: (item: T) => void,
|
||||
toggleExpanded: (item: T) => void,
|
||||
highlightedItem: T | undefined,
|
||||
isError?: (item: T) => boolean,
|
||||
onAccepted?: (item: T) => void,
|
||||
setHighlightedItem: (item: T | undefined) => void,
|
||||
render: (item: T) => React.ReactNode,
|
||||
icon?: (item: T) => string | undefined,
|
||||
};
|
||||
|
||||
export function TreeItemHeader<T extends TreeItem>({
|
||||
item,
|
||||
itemData,
|
||||
selectedItem,
|
||||
onSelected,
|
||||
highlightedItem,
|
||||
setHighlightedItem,
|
||||
isError,
|
||||
onAccepted,
|
||||
toggleExpanded,
|
||||
render,
|
||||
icon }: TreeItemHeaderProps<T>) {
|
||||
|
||||
const indentation = itemData.depth;
|
||||
const expanded = itemData.expanded;
|
||||
let expandIcon = 'codicon-blank';
|
||||
if (typeof expanded === 'boolean')
|
||||
expandIcon = expanded ? 'codicon-chevron-down' : 'codicon-chevron-right';
|
||||
const rendered = render(item);
|
||||
|
||||
return <div
|
||||
onDoubleClick={() => onAccepted?.(item)}
|
||||
className={clsx(
|
||||
'tree-view-entry',
|
||||
selectedItem === item && 'selected',
|
||||
highlightedItem === item && 'highlighted',
|
||||
isError?.(item) && 'error')}
|
||||
onClick={() => onSelected?.(item)}
|
||||
onMouseEnter={() => setHighlightedItem(item)}
|
||||
onMouseLeave={() => setHighlightedItem(undefined)}
|
||||
>
|
||||
{indentation ? new Array(indentation).fill(0).map((_, i) => <div key={'indent-' + i} className='tree-view-indent'></div>) : undefined}
|
||||
<div
|
||||
className={'codicon ' + expandIcon}
|
||||
style={{ minWidth: 16, marginRight: 4 }}
|
||||
onDoubleClick={e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleExpanded(item);
|
||||
}}
|
||||
/>
|
||||
{icon && <div className={'codicon ' + (icon(item) || 'codicon-blank')} style={{ minWidth: 16, marginRight: 4 }}></div>}
|
||||
{typeof rendered === 'string' ? <div style={{ textOverflow: 'ellipsis', overflow: 'hidden' }}>{rendered}</div> : rendered}
|
||||
</div>;
|
||||
}
|
||||
|
||||
type TreeItemData = {
|
||||
|
|
@ -160,7 +285,12 @@ type TreeItemData = {
|
|||
parent: TreeItem | null,
|
||||
};
|
||||
|
||||
function flattenTree<T extends TreeItem>(rootItem: T, selectedItem: T | undefined, expandedItems: Map<string, boolean | undefined>, autoExpandDepth: number): Map<T, TreeItemData> {
|
||||
function flattenTree<T extends TreeItem>(
|
||||
rootItem: T,
|
||||
selectedItem: T | undefined,
|
||||
expandedItems: Map<string, boolean | undefined>,
|
||||
autoExpandDepth: number): Map<T, TreeItemData> {
|
||||
|
||||
const result = new Map<T, TreeItemData>();
|
||||
const temporaryExpanded = new Set<string>();
|
||||
for (let item: TreeItem | undefined = selectedItem?.parent; item; item = item.parent)
|
||||
|
|
|
|||
|
|
@ -208,5 +208,14 @@ export async function sha1(str: string): Promise<string> {
|
|||
return Array.from(new Uint8Array(await crypto.subtle.digest('SHA-1', buffer))).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
export function scrollIntoViewIfNeeded(element: Element | undefined) {
|
||||
if (!element)
|
||||
return;
|
||||
if ((element as any)?.scrollIntoViewIfNeeded)
|
||||
(element as any).scrollIntoViewIfNeeded(false);
|
||||
else
|
||||
element?.scrollIntoView();
|
||||
}
|
||||
|
||||
const kControlCodesRe = '\\u0000-\\u0020\\u007f-\\u009f';
|
||||
export const kWebLinkRe = new RegExp('(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}:\\/\\/|www\\.)[^\\s' + kControlCodesRe + '"]{2,}[^\\s' + kControlCodesRe + '"\')}\\],:;.!?]', 'ug');
|
||||
|
|
|
|||
596
tests/assets/codicon.css
Normal file
596
tests/assets/codicon.css
Normal file
|
|
@ -0,0 +1,596 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
@font-face {
|
||||
font-family: "codicon";
|
||||
src: url("codicon.ttf") format("truetype");
|
||||
}
|
||||
|
||||
.codicon {
|
||||
font: normal normal normal 16px/1 codicon;
|
||||
flex: none;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
text-rendering: auto;
|
||||
text-align: center;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.codicon-add:before { content: '\ea60'; }
|
||||
.codicon-plus:before { content: '\ea60'; }
|
||||
.codicon-gist-new:before { content: '\ea60'; }
|
||||
.codicon-repo-create:before { content: '\ea60'; }
|
||||
.codicon-lightbulb:before { content: '\ea61'; }
|
||||
.codicon-light-bulb:before { content: '\ea61'; }
|
||||
.codicon-repo:before { content: '\ea62'; }
|
||||
.codicon-repo-delete:before { content: '\ea62'; }
|
||||
.codicon-gist-fork:before { content: '\ea63'; }
|
||||
.codicon-repo-forked:before { content: '\ea63'; }
|
||||
.codicon-git-pull-request:before { content: '\ea64'; }
|
||||
.codicon-git-pull-request-abandoned:before { content: '\ea64'; }
|
||||
.codicon-record-keys:before { content: '\ea65'; }
|
||||
.codicon-keyboard:before { content: '\ea65'; }
|
||||
.codicon-tag:before { content: '\ea66'; }
|
||||
.codicon-git-pull-request-label:before { content: '\ea66'; }
|
||||
.codicon-tag-add:before { content: '\ea66'; }
|
||||
.codicon-tag-remove:before { content: '\ea66'; }
|
||||
.codicon-person:before { content: '\ea67'; }
|
||||
.codicon-person-follow:before { content: '\ea67'; }
|
||||
.codicon-person-outline:before { content: '\ea67'; }
|
||||
.codicon-person-filled:before { content: '\ea67'; }
|
||||
.codicon-git-branch:before { content: '\ea68'; }
|
||||
.codicon-git-branch-create:before { content: '\ea68'; }
|
||||
.codicon-git-branch-delete:before { content: '\ea68'; }
|
||||
.codicon-source-control:before { content: '\ea68'; }
|
||||
.codicon-mirror:before { content: '\ea69'; }
|
||||
.codicon-mirror-public:before { content: '\ea69'; }
|
||||
.codicon-star:before { content: '\ea6a'; }
|
||||
.codicon-star-add:before { content: '\ea6a'; }
|
||||
.codicon-star-delete:before { content: '\ea6a'; }
|
||||
.codicon-star-empty:before { content: '\ea6a'; }
|
||||
.codicon-comment:before { content: '\ea6b'; }
|
||||
.codicon-comment-add:before { content: '\ea6b'; }
|
||||
.codicon-alert:before { content: '\ea6c'; }
|
||||
.codicon-warning:before { content: '\ea6c'; }
|
||||
.codicon-search:before { content: '\ea6d'; }
|
||||
.codicon-search-save:before { content: '\ea6d'; }
|
||||
.codicon-log-out:before { content: '\ea6e'; }
|
||||
.codicon-sign-out:before { content: '\ea6e'; }
|
||||
.codicon-log-in:before { content: '\ea6f'; }
|
||||
.codicon-sign-in:before { content: '\ea6f'; }
|
||||
.codicon-eye:before { content: '\ea70'; }
|
||||
.codicon-eye-unwatch:before { content: '\ea70'; }
|
||||
.codicon-eye-watch:before { content: '\ea70'; }
|
||||
.codicon-circle-filled:before { content: '\ea71'; }
|
||||
.codicon-primitive-dot:before { content: '\ea71'; }
|
||||
.codicon-close-dirty:before { content: '\ea71'; }
|
||||
.codicon-debug-breakpoint:before { content: '\ea71'; }
|
||||
.codicon-debug-breakpoint-disabled:before { content: '\ea71'; }
|
||||
.codicon-debug-hint:before { content: '\ea71'; }
|
||||
.codicon-terminal-decoration-success:before { content: '\ea71'; }
|
||||
.codicon-primitive-square:before { content: '\ea72'; }
|
||||
.codicon-edit:before { content: '\ea73'; }
|
||||
.codicon-pencil:before { content: '\ea73'; }
|
||||
.codicon-info:before { content: '\ea74'; }
|
||||
.codicon-issue-opened:before { content: '\ea74'; }
|
||||
.codicon-gist-private:before { content: '\ea75'; }
|
||||
.codicon-git-fork-private:before { content: '\ea75'; }
|
||||
.codicon-lock:before { content: '\ea75'; }
|
||||
.codicon-mirror-private:before { content: '\ea75'; }
|
||||
.codicon-close:before { content: '\ea76'; }
|
||||
.codicon-remove-close:before { content: '\ea76'; }
|
||||
.codicon-x:before { content: '\ea76'; }
|
||||
.codicon-repo-sync:before { content: '\ea77'; }
|
||||
.codicon-sync:before { content: '\ea77'; }
|
||||
.codicon-clone:before { content: '\ea78'; }
|
||||
.codicon-desktop-download:before { content: '\ea78'; }
|
||||
.codicon-beaker:before { content: '\ea79'; }
|
||||
.codicon-microscope:before { content: '\ea79'; }
|
||||
.codicon-vm:before { content: '\ea7a'; }
|
||||
.codicon-device-desktop:before { content: '\ea7a'; }
|
||||
.codicon-file:before { content: '\ea7b'; }
|
||||
.codicon-file-text:before { content: '\ea7b'; }
|
||||
.codicon-more:before { content: '\ea7c'; }
|
||||
.codicon-ellipsis:before { content: '\ea7c'; }
|
||||
.codicon-kebab-horizontal:before { content: '\ea7c'; }
|
||||
.codicon-mail-reply:before { content: '\ea7d'; }
|
||||
.codicon-reply:before { content: '\ea7d'; }
|
||||
.codicon-organization:before { content: '\ea7e'; }
|
||||
.codicon-organization-filled:before { content: '\ea7e'; }
|
||||
.codicon-organization-outline:before { content: '\ea7e'; }
|
||||
.codicon-new-file:before { content: '\ea7f'; }
|
||||
.codicon-file-add:before { content: '\ea7f'; }
|
||||
.codicon-new-folder:before { content: '\ea80'; }
|
||||
.codicon-file-directory-create:before { content: '\ea80'; }
|
||||
.codicon-trash:before { content: '\ea81'; }
|
||||
.codicon-trashcan:before { content: '\ea81'; }
|
||||
.codicon-history:before { content: '\ea82'; }
|
||||
.codicon-clock:before { content: '\ea82'; }
|
||||
.codicon-folder:before { content: '\ea83'; }
|
||||
.codicon-file-directory:before { content: '\ea83'; }
|
||||
.codicon-symbol-folder:before { content: '\ea83'; }
|
||||
.codicon-logo-github:before { content: '\ea84'; }
|
||||
.codicon-mark-github:before { content: '\ea84'; }
|
||||
.codicon-github:before { content: '\ea84'; }
|
||||
.codicon-terminal:before { content: '\ea85'; }
|
||||
.codicon-console:before { content: '\ea85'; }
|
||||
.codicon-repl:before { content: '\ea85'; }
|
||||
.codicon-zap:before { content: '\ea86'; }
|
||||
.codicon-symbol-event:before { content: '\ea86'; }
|
||||
.codicon-error:before { content: '\ea87'; }
|
||||
.codicon-stop:before { content: '\ea87'; }
|
||||
.codicon-variable:before { content: '\ea88'; }
|
||||
.codicon-symbol-variable:before { content: '\ea88'; }
|
||||
.codicon-array:before { content: '\ea8a'; }
|
||||
.codicon-symbol-array:before { content: '\ea8a'; }
|
||||
.codicon-symbol-module:before { content: '\ea8b'; }
|
||||
.codicon-symbol-package:before { content: '\ea8b'; }
|
||||
.codicon-symbol-namespace:before { content: '\ea8b'; }
|
||||
.codicon-symbol-object:before { content: '\ea8b'; }
|
||||
.codicon-symbol-method:before { content: '\ea8c'; }
|
||||
.codicon-symbol-function:before { content: '\ea8c'; }
|
||||
.codicon-symbol-constructor:before { content: '\ea8c'; }
|
||||
.codicon-symbol-boolean:before { content: '\ea8f'; }
|
||||
.codicon-symbol-null:before { content: '\ea8f'; }
|
||||
.codicon-symbol-numeric:before { content: '\ea90'; }
|
||||
.codicon-symbol-number:before { content: '\ea90'; }
|
||||
.codicon-symbol-structure:before { content: '\ea91'; }
|
||||
.codicon-symbol-struct:before { content: '\ea91'; }
|
||||
.codicon-symbol-parameter:before { content: '\ea92'; }
|
||||
.codicon-symbol-type-parameter:before { content: '\ea92'; }
|
||||
.codicon-symbol-key:before { content: '\ea93'; }
|
||||
.codicon-symbol-text:before { content: '\ea93'; }
|
||||
.codicon-symbol-reference:before { content: '\ea94'; }
|
||||
.codicon-go-to-file:before { content: '\ea94'; }
|
||||
.codicon-symbol-enum:before { content: '\ea95'; }
|
||||
.codicon-symbol-value:before { content: '\ea95'; }
|
||||
.codicon-symbol-ruler:before { content: '\ea96'; }
|
||||
.codicon-symbol-unit:before { content: '\ea96'; }
|
||||
.codicon-activate-breakpoints:before { content: '\ea97'; }
|
||||
.codicon-archive:before { content: '\ea98'; }
|
||||
.codicon-arrow-both:before { content: '\ea99'; }
|
||||
.codicon-arrow-down:before { content: '\ea9a'; }
|
||||
.codicon-arrow-left:before { content: '\ea9b'; }
|
||||
.codicon-arrow-right:before { content: '\ea9c'; }
|
||||
.codicon-arrow-small-down:before { content: '\ea9d'; }
|
||||
.codicon-arrow-small-left:before { content: '\ea9e'; }
|
||||
.codicon-arrow-small-right:before { content: '\ea9f'; }
|
||||
.codicon-arrow-small-up:before { content: '\eaa0'; }
|
||||
.codicon-arrow-up:before { content: '\eaa1'; }
|
||||
.codicon-bell:before { content: '\eaa2'; }
|
||||
.codicon-bold:before { content: '\eaa3'; }
|
||||
.codicon-book:before { content: '\eaa4'; }
|
||||
.codicon-bookmark:before { content: '\eaa5'; }
|
||||
.codicon-debug-breakpoint-conditional-unverified:before { content: '\eaa6'; }
|
||||
.codicon-debug-breakpoint-conditional:before { content: '\eaa7'; }
|
||||
.codicon-debug-breakpoint-conditional-disabled:before { content: '\eaa7'; }
|
||||
.codicon-debug-breakpoint-data-unverified:before { content: '\eaa8'; }
|
||||
.codicon-debug-breakpoint-data:before { content: '\eaa9'; }
|
||||
.codicon-debug-breakpoint-data-disabled:before { content: '\eaa9'; }
|
||||
.codicon-debug-breakpoint-log-unverified:before { content: '\eaaa'; }
|
||||
.codicon-debug-breakpoint-log:before { content: '\eaab'; }
|
||||
.codicon-debug-breakpoint-log-disabled:before { content: '\eaab'; }
|
||||
.codicon-briefcase:before { content: '\eaac'; }
|
||||
.codicon-broadcast:before { content: '\eaad'; }
|
||||
.codicon-browser:before { content: '\eaae'; }
|
||||
.codicon-bug:before { content: '\eaaf'; }
|
||||
.codicon-calendar:before { content: '\eab0'; }
|
||||
.codicon-case-sensitive:before { content: '\eab1'; }
|
||||
.codicon-check:before { content: '\eab2'; }
|
||||
.codicon-checklist:before { content: '\eab3'; }
|
||||
.codicon-chevron-down:before { content: '\eab4'; }
|
||||
.codicon-chevron-left:before { content: '\eab5'; }
|
||||
.codicon-chevron-right:before { content: '\eab6'; }
|
||||
.codicon-chevron-up:before { content: '\eab7'; }
|
||||
.codicon-chrome-close:before { content: '\eab8'; }
|
||||
.codicon-chrome-maximize:before { content: '\eab9'; }
|
||||
.codicon-chrome-minimize:before { content: '\eaba'; }
|
||||
.codicon-chrome-restore:before { content: '\eabb'; }
|
||||
.codicon-circle-outline:before { content: '\eabc'; }
|
||||
.codicon-circle:before { content: '\eabc'; }
|
||||
.codicon-debug-breakpoint-unverified:before { content: '\eabc'; }
|
||||
.codicon-terminal-decoration-incomplete:before { content: '\eabc'; }
|
||||
.codicon-circle-slash:before { content: '\eabd'; }
|
||||
.codicon-circuit-board:before { content: '\eabe'; }
|
||||
.codicon-clear-all:before { content: '\eabf'; }
|
||||
.codicon-clippy:before { content: '\eac0'; }
|
||||
.codicon-close-all:before { content: '\eac1'; }
|
||||
.codicon-cloud-download:before { content: '\eac2'; }
|
||||
.codicon-cloud-upload:before { content: '\eac3'; }
|
||||
.codicon-code:before { content: '\eac4'; }
|
||||
.codicon-collapse-all:before { content: '\eac5'; }
|
||||
.codicon-color-mode:before { content: '\eac6'; }
|
||||
.codicon-comment-discussion:before { content: '\eac7'; }
|
||||
.codicon-credit-card:before { content: '\eac9'; }
|
||||
.codicon-dash:before { content: '\eacc'; }
|
||||
.codicon-dashboard:before { content: '\eacd'; }
|
||||
.codicon-database:before { content: '\eace'; }
|
||||
.codicon-debug-continue:before { content: '\eacf'; }
|
||||
.codicon-debug-disconnect:before { content: '\ead0'; }
|
||||
.codicon-debug-pause:before { content: '\ead1'; }
|
||||
.codicon-debug-restart:before { content: '\ead2'; }
|
||||
.codicon-debug-start:before { content: '\ead3'; }
|
||||
.codicon-debug-step-into:before { content: '\ead4'; }
|
||||
.codicon-debug-step-out:before { content: '\ead5'; }
|
||||
.codicon-debug-step-over:before { content: '\ead6'; }
|
||||
.codicon-debug-stop:before { content: '\ead7'; }
|
||||
.codicon-debug:before { content: '\ead8'; }
|
||||
.codicon-device-camera-video:before { content: '\ead9'; }
|
||||
.codicon-device-camera:before { content: '\eada'; }
|
||||
.codicon-device-mobile:before { content: '\eadb'; }
|
||||
.codicon-diff-added:before { content: '\eadc'; }
|
||||
.codicon-diff-ignored:before { content: '\eadd'; }
|
||||
.codicon-diff-modified:before { content: '\eade'; }
|
||||
.codicon-diff-removed:before { content: '\eadf'; }
|
||||
.codicon-diff-renamed:before { content: '\eae0'; }
|
||||
.codicon-diff:before { content: '\eae1'; }
|
||||
.codicon-diff-sidebyside:before { content: '\eae1'; }
|
||||
.codicon-discard:before { content: '\eae2'; }
|
||||
.codicon-editor-layout:before { content: '\eae3'; }
|
||||
.codicon-empty-window:before { content: '\eae4'; }
|
||||
.codicon-exclude:before { content: '\eae5'; }
|
||||
.codicon-extensions:before { content: '\eae6'; }
|
||||
.codicon-eye-closed:before { content: '\eae7'; }
|
||||
.codicon-file-binary:before { content: '\eae8'; }
|
||||
.codicon-file-code:before { content: '\eae9'; }
|
||||
.codicon-file-media:before { content: '\eaea'; }
|
||||
.codicon-file-pdf:before { content: '\eaeb'; }
|
||||
.codicon-file-submodule:before { content: '\eaec'; }
|
||||
.codicon-file-symlink-directory:before { content: '\eaed'; }
|
||||
.codicon-file-symlink-file:before { content: '\eaee'; }
|
||||
.codicon-file-zip:before { content: '\eaef'; }
|
||||
.codicon-files:before { content: '\eaf0'; }
|
||||
.codicon-filter:before { content: '\eaf1'; }
|
||||
.codicon-flame:before { content: '\eaf2'; }
|
||||
.codicon-fold-down:before { content: '\eaf3'; }
|
||||
.codicon-fold-up:before { content: '\eaf4'; }
|
||||
.codicon-fold:before { content: '\eaf5'; }
|
||||
.codicon-folder-active:before { content: '\eaf6'; }
|
||||
.codicon-folder-opened:before { content: '\eaf7'; }
|
||||
.codicon-gear:before { content: '\eaf8'; }
|
||||
.codicon-gift:before { content: '\eaf9'; }
|
||||
.codicon-gist-secret:before { content: '\eafa'; }
|
||||
.codicon-gist:before { content: '\eafb'; }
|
||||
.codicon-git-commit:before { content: '\eafc'; }
|
||||
.codicon-git-compare:before { content: '\eafd'; }
|
||||
.codicon-compare-changes:before { content: '\eafd'; }
|
||||
.codicon-git-merge:before { content: '\eafe'; }
|
||||
.codicon-github-action:before { content: '\eaff'; }
|
||||
.codicon-github-alt:before { content: '\eb00'; }
|
||||
.codicon-globe:before { content: '\eb01'; }
|
||||
.codicon-grabber:before { content: '\eb02'; }
|
||||
.codicon-graph:before { content: '\eb03'; }
|
||||
.codicon-gripper:before { content: '\eb04'; }
|
||||
.codicon-heart:before { content: '\eb05'; }
|
||||
.codicon-home:before { content: '\eb06'; }
|
||||
.codicon-horizontal-rule:before { content: '\eb07'; }
|
||||
.codicon-hubot:before { content: '\eb08'; }
|
||||
.codicon-inbox:before { content: '\eb09'; }
|
||||
.codicon-issue-reopened:before { content: '\eb0b'; }
|
||||
.codicon-issues:before { content: '\eb0c'; }
|
||||
.codicon-italic:before { content: '\eb0d'; }
|
||||
.codicon-jersey:before { content: '\eb0e'; }
|
||||
.codicon-json:before { content: '\eb0f'; }
|
||||
.codicon-kebab-vertical:before { content: '\eb10'; }
|
||||
.codicon-key:before { content: '\eb11'; }
|
||||
.codicon-law:before { content: '\eb12'; }
|
||||
.codicon-lightbulb-autofix:before { content: '\eb13'; }
|
||||
.codicon-link-external:before { content: '\eb14'; }
|
||||
.codicon-link:before { content: '\eb15'; }
|
||||
.codicon-list-ordered:before { content: '\eb16'; }
|
||||
.codicon-list-unordered:before { content: '\eb17'; }
|
||||
.codicon-live-share:before { content: '\eb18'; }
|
||||
.codicon-loading:before { content: '\eb19'; }
|
||||
.codicon-location:before { content: '\eb1a'; }
|
||||
.codicon-mail-read:before { content: '\eb1b'; }
|
||||
.codicon-mail:before { content: '\eb1c'; }
|
||||
.codicon-markdown:before { content: '\eb1d'; }
|
||||
.codicon-megaphone:before { content: '\eb1e'; }
|
||||
.codicon-mention:before { content: '\eb1f'; }
|
||||
.codicon-milestone:before { content: '\eb20'; }
|
||||
.codicon-git-pull-request-milestone:before { content: '\eb20'; }
|
||||
.codicon-mortar-board:before { content: '\eb21'; }
|
||||
.codicon-move:before { content: '\eb22'; }
|
||||
.codicon-multiple-windows:before { content: '\eb23'; }
|
||||
.codicon-mute:before { content: '\eb24'; }
|
||||
.codicon-no-newline:before { content: '\eb25'; }
|
||||
.codicon-note:before { content: '\eb26'; }
|
||||
.codicon-octoface:before { content: '\eb27'; }
|
||||
.codicon-open-preview:before { content: '\eb28'; }
|
||||
.codicon-package:before { content: '\eb29'; }
|
||||
.codicon-paintcan:before { content: '\eb2a'; }
|
||||
.codicon-pin:before { content: '\eb2b'; }
|
||||
.codicon-play:before { content: '\eb2c'; }
|
||||
.codicon-run:before { content: '\eb2c'; }
|
||||
.codicon-plug:before { content: '\eb2d'; }
|
||||
.codicon-preserve-case:before { content: '\eb2e'; }
|
||||
.codicon-preview:before { content: '\eb2f'; }
|
||||
.codicon-project:before { content: '\eb30'; }
|
||||
.codicon-pulse:before { content: '\eb31'; }
|
||||
.codicon-question:before { content: '\eb32'; }
|
||||
.codicon-quote:before { content: '\eb33'; }
|
||||
.codicon-radio-tower:before { content: '\eb34'; }
|
||||
.codicon-reactions:before { content: '\eb35'; }
|
||||
.codicon-references:before { content: '\eb36'; }
|
||||
.codicon-refresh:before { content: '\eb37'; }
|
||||
.codicon-regex:before { content: '\eb38'; }
|
||||
.codicon-remote-explorer:before { content: '\eb39'; }
|
||||
.codicon-remote:before { content: '\eb3a'; }
|
||||
.codicon-remove:before { content: '\eb3b'; }
|
||||
.codicon-replace-all:before { content: '\eb3c'; }
|
||||
.codicon-replace:before { content: '\eb3d'; }
|
||||
.codicon-repo-clone:before { content: '\eb3e'; }
|
||||
.codicon-repo-force-push:before { content: '\eb3f'; }
|
||||
.codicon-repo-pull:before { content: '\eb40'; }
|
||||
.codicon-repo-push:before { content: '\eb41'; }
|
||||
.codicon-report:before { content: '\eb42'; }
|
||||
.codicon-request-changes:before { content: '\eb43'; }
|
||||
.codicon-rocket:before { content: '\eb44'; }
|
||||
.codicon-root-folder-opened:before { content: '\eb45'; }
|
||||
.codicon-root-folder:before { content: '\eb46'; }
|
||||
.codicon-rss:before { content: '\eb47'; }
|
||||
.codicon-ruby:before { content: '\eb48'; }
|
||||
.codicon-save-all:before { content: '\eb49'; }
|
||||
.codicon-save-as:before { content: '\eb4a'; }
|
||||
.codicon-save:before { content: '\eb4b'; }
|
||||
.codicon-screen-full:before { content: '\eb4c'; }
|
||||
.codicon-screen-normal:before { content: '\eb4d'; }
|
||||
.codicon-search-stop:before { content: '\eb4e'; }
|
||||
.codicon-server:before { content: '\eb50'; }
|
||||
.codicon-settings-gear:before { content: '\eb51'; }
|
||||
.codicon-settings:before { content: '\eb52'; }
|
||||
.codicon-shield:before { content: '\eb53'; }
|
||||
.codicon-smiley:before { content: '\eb54'; }
|
||||
.codicon-sort-precedence:before { content: '\eb55'; }
|
||||
.codicon-split-horizontal:before { content: '\eb56'; }
|
||||
.codicon-split-vertical:before { content: '\eb57'; }
|
||||
.codicon-squirrel:before { content: '\eb58'; }
|
||||
.codicon-star-full:before { content: '\eb59'; }
|
||||
.codicon-star-half:before { content: '\eb5a'; }
|
||||
.codicon-symbol-class:before { content: '\eb5b'; }
|
||||
.codicon-symbol-color:before { content: '\eb5c'; }
|
||||
.codicon-symbol-constant:before { content: '\eb5d'; }
|
||||
.codicon-symbol-enum-member:before { content: '\eb5e'; }
|
||||
.codicon-symbol-field:before { content: '\eb5f'; }
|
||||
.codicon-symbol-file:before { content: '\eb60'; }
|
||||
.codicon-symbol-interface:before { content: '\eb61'; }
|
||||
.codicon-symbol-keyword:before { content: '\eb62'; }
|
||||
.codicon-symbol-misc:before { content: '\eb63'; }
|
||||
.codicon-symbol-operator:before { content: '\eb64'; }
|
||||
.codicon-symbol-property:before { content: '\eb65'; }
|
||||
.codicon-wrench:before { content: '\eb65'; }
|
||||
.codicon-wrench-subaction:before { content: '\eb65'; }
|
||||
.codicon-symbol-snippet:before { content: '\eb66'; }
|
||||
.codicon-tasklist:before { content: '\eb67'; }
|
||||
.codicon-telescope:before { content: '\eb68'; }
|
||||
.codicon-text-size:before { content: '\eb69'; }
|
||||
.codicon-three-bars:before { content: '\eb6a'; }
|
||||
.codicon-thumbsdown:before { content: '\eb6b'; }
|
||||
.codicon-thumbsup:before { content: '\eb6c'; }
|
||||
.codicon-tools:before { content: '\eb6d'; }
|
||||
.codicon-triangle-down:before { content: '\eb6e'; }
|
||||
.codicon-triangle-left:before { content: '\eb6f'; }
|
||||
.codicon-triangle-right:before { content: '\eb70'; }
|
||||
.codicon-triangle-up:before { content: '\eb71'; }
|
||||
.codicon-twitter:before { content: '\eb72'; }
|
||||
.codicon-unfold:before { content: '\eb73'; }
|
||||
.codicon-unlock:before { content: '\eb74'; }
|
||||
.codicon-unmute:before { content: '\eb75'; }
|
||||
.codicon-unverified:before { content: '\eb76'; }
|
||||
.codicon-verified:before { content: '\eb77'; }
|
||||
.codicon-versions:before { content: '\eb78'; }
|
||||
.codicon-vm-active:before { content: '\eb79'; }
|
||||
.codicon-vm-outline:before { content: '\eb7a'; }
|
||||
.codicon-vm-running:before { content: '\eb7b'; }
|
||||
.codicon-watch:before { content: '\eb7c'; }
|
||||
.codicon-whitespace:before { content: '\eb7d'; }
|
||||
.codicon-whole-word:before { content: '\eb7e'; }
|
||||
.codicon-window:before { content: '\eb7f'; }
|
||||
.codicon-word-wrap:before { content: '\eb80'; }
|
||||
.codicon-zoom-in:before { content: '\eb81'; }
|
||||
.codicon-zoom-out:before { content: '\eb82'; }
|
||||
.codicon-list-filter:before { content: '\eb83'; }
|
||||
.codicon-list-flat:before { content: '\eb84'; }
|
||||
.codicon-list-selection:before { content: '\eb85'; }
|
||||
.codicon-selection:before { content: '\eb85'; }
|
||||
.codicon-list-tree:before { content: '\eb86'; }
|
||||
.codicon-debug-breakpoint-function-unverified:before { content: '\eb87'; }
|
||||
.codicon-debug-breakpoint-function:before { content: '\eb88'; }
|
||||
.codicon-debug-breakpoint-function-disabled:before { content: '\eb88'; }
|
||||
.codicon-debug-stackframe-active:before { content: '\eb89'; }
|
||||
.codicon-circle-small-filled:before { content: '\eb8a'; }
|
||||
.codicon-debug-stackframe-dot:before { content: '\eb8a'; }
|
||||
.codicon-terminal-decoration-mark:before { content: '\eb8a'; }
|
||||
.codicon-debug-stackframe:before { content: '\eb8b'; }
|
||||
.codicon-debug-stackframe-focused:before { content: '\eb8b'; }
|
||||
.codicon-debug-breakpoint-unsupported:before { content: '\eb8c'; }
|
||||
.codicon-symbol-string:before { content: '\eb8d'; }
|
||||
.codicon-debug-reverse-continue:before { content: '\eb8e'; }
|
||||
.codicon-debug-step-back:before { content: '\eb8f'; }
|
||||
.codicon-debug-restart-frame:before { content: '\eb90'; }
|
||||
.codicon-debug-alt:before { content: '\eb91'; }
|
||||
.codicon-call-incoming:before { content: '\eb92'; }
|
||||
.codicon-call-outgoing:before { content: '\eb93'; }
|
||||
.codicon-menu:before { content: '\eb94'; }
|
||||
.codicon-expand-all:before { content: '\eb95'; }
|
||||
.codicon-feedback:before { content: '\eb96'; }
|
||||
.codicon-git-pull-request-reviewer:before { content: '\eb96'; }
|
||||
.codicon-group-by-ref-type:before { content: '\eb97'; }
|
||||
.codicon-ungroup-by-ref-type:before { content: '\eb98'; }
|
||||
.codicon-account:before { content: '\eb99'; }
|
||||
.codicon-git-pull-request-assignee:before { content: '\eb99'; }
|
||||
.codicon-bell-dot:before { content: '\eb9a'; }
|
||||
.codicon-debug-console:before { content: '\eb9b'; }
|
||||
.codicon-library:before { content: '\eb9c'; }
|
||||
.codicon-output:before { content: '\eb9d'; }
|
||||
.codicon-run-all:before { content: '\eb9e'; }
|
||||
.codicon-sync-ignored:before { content: '\eb9f'; }
|
||||
.codicon-pinned:before { content: '\eba0'; }
|
||||
.codicon-github-inverted:before { content: '\eba1'; }
|
||||
.codicon-server-process:before { content: '\eba2'; }
|
||||
.codicon-server-environment:before { content: '\eba3'; }
|
||||
.codicon-pass:before { content: '\eba4'; }
|
||||
.codicon-issue-closed:before { content: '\eba4'; }
|
||||
.codicon-stop-circle:before { content: '\eba5'; }
|
||||
.codicon-play-circle:before { content: '\eba6'; }
|
||||
.codicon-record:before { content: '\eba7'; }
|
||||
.codicon-debug-alt-small:before { content: '\eba8'; }
|
||||
.codicon-vm-connect:before { content: '\eba9'; }
|
||||
.codicon-cloud:before { content: '\ebaa'; }
|
||||
.codicon-merge:before { content: '\ebab'; }
|
||||
.codicon-export:before { content: '\ebac'; }
|
||||
.codicon-graph-left:before { content: '\ebad'; }
|
||||
.codicon-magnet:before { content: '\ebae'; }
|
||||
.codicon-notebook:before { content: '\ebaf'; }
|
||||
.codicon-redo:before { content: '\ebb0'; }
|
||||
.codicon-check-all:before { content: '\ebb1'; }
|
||||
.codicon-pinned-dirty:before { content: '\ebb2'; }
|
||||
.codicon-pass-filled:before { content: '\ebb3'; }
|
||||
.codicon-circle-large-filled:before { content: '\ebb4'; }
|
||||
.codicon-circle-large:before { content: '\ebb5'; }
|
||||
.codicon-circle-large-outline:before { content: '\ebb5'; }
|
||||
.codicon-combine:before { content: '\ebb6'; }
|
||||
.codicon-gather:before { content: '\ebb6'; }
|
||||
.codicon-table:before { content: '\ebb7'; }
|
||||
.codicon-variable-group:before { content: '\ebb8'; }
|
||||
.codicon-type-hierarchy:before { content: '\ebb9'; }
|
||||
.codicon-type-hierarchy-sub:before { content: '\ebba'; }
|
||||
.codicon-type-hierarchy-super:before { content: '\ebbb'; }
|
||||
.codicon-git-pull-request-create:before { content: '\ebbc'; }
|
||||
.codicon-run-above:before { content: '\ebbd'; }
|
||||
.codicon-run-below:before { content: '\ebbe'; }
|
||||
.codicon-notebook-template:before { content: '\ebbf'; }
|
||||
.codicon-debug-rerun:before { content: '\ebc0'; }
|
||||
.codicon-workspace-trusted:before { content: '\ebc1'; }
|
||||
.codicon-workspace-untrusted:before { content: '\ebc2'; }
|
||||
.codicon-workspace-unknown:before { content: '\ebc3'; }
|
||||
.codicon-terminal-cmd:before { content: '\ebc4'; }
|
||||
.codicon-terminal-debian:before { content: '\ebc5'; }
|
||||
.codicon-terminal-linux:before { content: '\ebc6'; }
|
||||
.codicon-terminal-powershell:before { content: '\ebc7'; }
|
||||
.codicon-terminal-tmux:before { content: '\ebc8'; }
|
||||
.codicon-terminal-ubuntu:before { content: '\ebc9'; }
|
||||
.codicon-terminal-bash:before { content: '\ebca'; }
|
||||
.codicon-arrow-swap:before { content: '\ebcb'; }
|
||||
.codicon-copy:before { content: '\ebcc'; }
|
||||
.codicon-person-add:before { content: '\ebcd'; }
|
||||
.codicon-filter-filled:before { content: '\ebce'; }
|
||||
.codicon-wand:before { content: '\ebcf'; }
|
||||
.codicon-debug-line-by-line:before { content: '\ebd0'; }
|
||||
.codicon-inspect:before { content: '\ebd1'; }
|
||||
.codicon-layers:before { content: '\ebd2'; }
|
||||
.codicon-layers-dot:before { content: '\ebd3'; }
|
||||
.codicon-layers-active:before { content: '\ebd4'; }
|
||||
.codicon-compass:before { content: '\ebd5'; }
|
||||
.codicon-compass-dot:before { content: '\ebd6'; }
|
||||
.codicon-compass-active:before { content: '\ebd7'; }
|
||||
.codicon-azure:before { content: '\ebd8'; }
|
||||
.codicon-issue-draft:before { content: '\ebd9'; }
|
||||
.codicon-git-pull-request-closed:before { content: '\ebda'; }
|
||||
.codicon-git-pull-request-draft:before { content: '\ebdb'; }
|
||||
.codicon-debug-all:before { content: '\ebdc'; }
|
||||
.codicon-debug-coverage:before { content: '\ebdd'; }
|
||||
.codicon-run-errors:before { content: '\ebde'; }
|
||||
.codicon-folder-library:before { content: '\ebdf'; }
|
||||
.codicon-debug-continue-small:before { content: '\ebe0'; }
|
||||
.codicon-beaker-stop:before { content: '\ebe1'; }
|
||||
.codicon-graph-line:before { content: '\ebe2'; }
|
||||
.codicon-graph-scatter:before { content: '\ebe3'; }
|
||||
.codicon-pie-chart:before { content: '\ebe4'; }
|
||||
.codicon-bracket:before { content: '\eb0f'; }
|
||||
.codicon-bracket-dot:before { content: '\ebe5'; }
|
||||
.codicon-bracket-error:before { content: '\ebe6'; }
|
||||
.codicon-lock-small:before { content: '\ebe7'; }
|
||||
.codicon-azure-devops:before { content: '\ebe8'; }
|
||||
.codicon-verified-filled:before { content: '\ebe9'; }
|
||||
.codicon-newline:before { content: '\ebea'; }
|
||||
.codicon-layout:before { content: '\ebeb'; }
|
||||
.codicon-layout-activitybar-left:before { content: '\ebec'; }
|
||||
.codicon-layout-activitybar-right:before { content: '\ebed'; }
|
||||
.codicon-layout-panel-left:before { content: '\ebee'; }
|
||||
.codicon-layout-panel-center:before { content: '\ebef'; }
|
||||
.codicon-layout-panel-justify:before { content: '\ebf0'; }
|
||||
.codicon-layout-panel-right:before { content: '\ebf1'; }
|
||||
.codicon-layout-panel:before { content: '\ebf2'; }
|
||||
.codicon-layout-sidebar-left:before { content: '\ebf3'; }
|
||||
.codicon-layout-sidebar-right:before { content: '\ebf4'; }
|
||||
.codicon-layout-statusbar:before { content: '\ebf5'; }
|
||||
.codicon-layout-menubar:before { content: '\ebf6'; }
|
||||
.codicon-layout-centered:before { content: '\ebf7'; }
|
||||
.codicon-target:before { content: '\ebf8'; }
|
||||
.codicon-indent:before { content: '\ebf9'; }
|
||||
.codicon-record-small:before { content: '\ebfa'; }
|
||||
.codicon-error-small:before { content: '\ebfb'; }
|
||||
.codicon-terminal-decoration-error:before { content: '\ebfb'; }
|
||||
.codicon-arrow-circle-down:before { content: '\ebfc'; }
|
||||
.codicon-arrow-circle-left:before { content: '\ebfd'; }
|
||||
.codicon-arrow-circle-right:before { content: '\ebfe'; }
|
||||
.codicon-arrow-circle-up:before { content: '\ebff'; }
|
||||
.codicon-layout-sidebar-right-off:before { content: '\ec00'; }
|
||||
.codicon-layout-panel-off:before { content: '\ec01'; }
|
||||
.codicon-layout-sidebar-left-off:before { content: '\ec02'; }
|
||||
.codicon-blank:before { content: '\ec03'; }
|
||||
.codicon-heart-filled:before { content: '\ec04'; }
|
||||
.codicon-map:before { content: '\ec05'; }
|
||||
.codicon-map-horizontal:before { content: '\ec05'; }
|
||||
.codicon-fold-horizontal:before { content: '\ec05'; }
|
||||
.codicon-map-filled:before { content: '\ec06'; }
|
||||
.codicon-map-horizontal-filled:before { content: '\ec06'; }
|
||||
.codicon-fold-horizontal-filled:before { content: '\ec06'; }
|
||||
.codicon-circle-small:before { content: '\ec07'; }
|
||||
.codicon-bell-slash:before { content: '\ec08'; }
|
||||
.codicon-bell-slash-dot:before { content: '\ec09'; }
|
||||
.codicon-comment-unresolved:before { content: '\ec0a'; }
|
||||
.codicon-git-pull-request-go-to-changes:before { content: '\ec0b'; }
|
||||
.codicon-git-pull-request-new-changes:before { content: '\ec0c'; }
|
||||
.codicon-search-fuzzy:before { content: '\ec0d'; }
|
||||
.codicon-comment-draft:before { content: '\ec0e'; }
|
||||
.codicon-send:before { content: '\ec0f'; }
|
||||
.codicon-sparkle:before { content: '\ec10'; }
|
||||
.codicon-insert:before { content: '\ec11'; }
|
||||
.codicon-mic:before { content: '\ec12'; }
|
||||
.codicon-thumbsdown-filled:before { content: '\ec13'; }
|
||||
.codicon-thumbsup-filled:before { content: '\ec14'; }
|
||||
.codicon-coffee:before { content: '\ec15'; }
|
||||
.codicon-snake:before { content: '\ec16'; }
|
||||
.codicon-game:before { content: '\ec17'; }
|
||||
.codicon-vr:before { content: '\ec18'; }
|
||||
.codicon-chip:before { content: '\ec19'; }
|
||||
.codicon-piano:before { content: '\ec1a'; }
|
||||
.codicon-music:before { content: '\ec1b'; }
|
||||
.codicon-mic-filled:before { content: '\ec1c'; }
|
||||
.codicon-repo-fetch:before { content: '\ec1d'; }
|
||||
.codicon-copilot:before { content: '\ec1e'; }
|
||||
.codicon-lightbulb-sparkle:before { content: '\ec1f'; }
|
||||
.codicon-robot:before { content: '\ec20'; }
|
||||
.codicon-sparkle-filled:before { content: '\ec21'; }
|
||||
.codicon-diff-single:before { content: '\ec22'; }
|
||||
.codicon-diff-multiple:before { content: '\ec23'; }
|
||||
.codicon-surround-with:before { content: '\ec24'; }
|
||||
.codicon-share:before { content: '\ec25'; }
|
||||
.codicon-git-stash:before { content: '\ec26'; }
|
||||
.codicon-git-stash-apply:before { content: '\ec27'; }
|
||||
.codicon-git-stash-pop:before { content: '\ec28'; }
|
||||
.codicon-vscode:before { content: '\ec29'; }
|
||||
.codicon-vscode-insiders:before { content: '\ec2a'; }
|
||||
.codicon-code-oss:before { content: '\ec2b'; }
|
||||
.codicon-run-coverage:before { content: '\ec2c'; }
|
||||
.codicon-run-all-coverage:before { content: '\ec2d'; }
|
||||
.codicon-coverage:before { content: '\ec2e'; }
|
||||
.codicon-github-project:before { content: '\ec2f'; }
|
||||
.codicon-map-vertical:before { content: '\ec30'; }
|
||||
.codicon-fold-vertical:before { content: '\ec30'; }
|
||||
.codicon-map-vertical-filled:before { content: '\ec31'; }
|
||||
.codicon-fold-vertical-filled:before { content: '\ec31'; }
|
||||
.codicon-go-to-search:before { content: '\ec32'; }
|
||||
.codicon-percentage:before { content: '\ec33'; }
|
||||
.codicon-sort-percentage:before { content: '\ec33'; }
|
||||
.codicon-attach:before { content: '\ec34'; }
|
||||
.codicon-git-fetch:before { content: '\f101'; }
|
||||
BIN
tests/assets/codicon.ttf
Normal file
BIN
tests/assets/codicon.ttf
Normal file
Binary file not shown.
|
|
@ -62,13 +62,13 @@ class TraceViewerPage {
|
|||
}
|
||||
|
||||
async actionIconsText(action: string) {
|
||||
const entry = await this.page.waitForSelector(`.list-view-entry:has-text("${action}")`);
|
||||
const entry = await this.page.waitForSelector(`.tree-view-entry:has-text("${action}")`);
|
||||
await entry.waitForSelector('.action-icon-value:visible');
|
||||
return await entry.$$eval('.action-icon-value:visible', ee => ee.map(e => e.textContent));
|
||||
}
|
||||
|
||||
async actionIcons(action: string) {
|
||||
return await this.page.waitForSelector(`.list-view-entry:has-text("${action}") .action-icons`);
|
||||
return await this.page.waitForSelector(`.tree-view-entry:has-text("${action}") .action-icons`);
|
||||
}
|
||||
|
||||
@step
|
||||
|
|
|
|||
|
|
@ -15,26 +15,373 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Locator } from '@playwright/test';
|
||||
import { test as it, expect } from './pageTest';
|
||||
|
||||
it('should snapshot the check box @smoke', async ({ page }) => {
|
||||
await page.setContent(`<input id='checkbox' type='checkbox'></input>`);
|
||||
expect(await page.locator('body').ariaSnapshot()).toBe('- checkbox');
|
||||
function unshift(snapshot: string): string {
|
||||
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');
|
||||
}
|
||||
|
||||
async function checkAndMatchSnapshot(locator: Locator, snapshot: string) {
|
||||
expect.soft(await locator.ariaSnapshot()).toBe(unshift(snapshot));
|
||||
await expect.soft(locator).toMatchAriaSnapshot(snapshot);
|
||||
}
|
||||
|
||||
it('should snapshot', async ({ page }) => {
|
||||
await page.setContent(`<h1>title</h1>`);
|
||||
await checkAndMatchSnapshot(page.locator('body'), `
|
||||
- heading "title"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should snapshot nested element', async ({ page }) => {
|
||||
it('should snapshot list', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<div>
|
||||
<input id='checkbox' type='checkbox'></input>
|
||||
</div>`);
|
||||
expect(await page.locator('body').ariaSnapshot()).toBe('- checkbox');
|
||||
<h1>title</h1>
|
||||
<h1>title 2</h1>
|
||||
`);
|
||||
await checkAndMatchSnapshot(page.locator('body'), `
|
||||
- heading "title"
|
||||
- heading "title 2"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should snapshot fragment', async ({ page }) => {
|
||||
it('should snapshot list with accessible name', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<div>
|
||||
<a href="about:blank">Link</a>
|
||||
<a href="about:blank">Link</a>
|
||||
</div>`);
|
||||
expect(await page.locator('body').ariaSnapshot()).toBe(`- link "Link"\n- link "Link"`);
|
||||
<ul aria-label="my list">
|
||||
<li>one</li>
|
||||
<li>two</li>
|
||||
</ul>
|
||||
`);
|
||||
await checkAndMatchSnapshot(page.locator('body'), `
|
||||
- list "my list":
|
||||
- listitem: one
|
||||
- listitem: two
|
||||
`);
|
||||
});
|
||||
|
||||
it('should snapshot complex', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<ul>
|
||||
<li>
|
||||
<a href='about:blank'>link</a>
|
||||
</li>
|
||||
</ul>
|
||||
`);
|
||||
await checkAndMatchSnapshot(page.locator('body'), `
|
||||
- list:
|
||||
- listitem:
|
||||
- link "link"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should allow text nodes', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<h1>Microsoft</h1>
|
||||
<div>Open source projects and samples from Microsoft</div>
|
||||
`);
|
||||
|
||||
await checkAndMatchSnapshot(page.locator('body'), `
|
||||
- heading "Microsoft"
|
||||
- text: Open source projects and samples from Microsoft
|
||||
`);
|
||||
});
|
||||
|
||||
it('should snapshot details visibility', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<details>
|
||||
<summary>Summary</summary>
|
||||
<div>Details</div>
|
||||
</details>
|
||||
`);
|
||||
|
||||
await checkAndMatchSnapshot(page.locator('body'), `
|
||||
- group: Summary
|
||||
`);
|
||||
});
|
||||
|
||||
it('should snapshot integration', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<h1>Microsoft</h1>
|
||||
<div>Open source projects and samples from Microsoft</div>
|
||||
<ul>
|
||||
<li>
|
||||
<details>
|
||||
<summary>
|
||||
Verified
|
||||
</summary>
|
||||
<div>
|
||||
<div>
|
||||
<p>
|
||||
We've verified that the organization <strong>microsoft</strong> controls the domain:
|
||||
</p>
|
||||
<ul>
|
||||
<li class="mb-1">
|
||||
<strong>opensource.microsoft.com</strong>
|
||||
</li>
|
||||
</ul>
|
||||
<div>
|
||||
<a href="about: blank">Learn more about verified organizations</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</li>
|
||||
<li>
|
||||
<a href="about:blank">
|
||||
<summary title="Label: GitHub Sponsor">Sponsor</summary>
|
||||
</a>
|
||||
</li>
|
||||
</ul>`);
|
||||
|
||||
await checkAndMatchSnapshot(page.locator('body'), `
|
||||
- heading "Microsoft"
|
||||
- text: Open source projects and samples from Microsoft
|
||||
- list:
|
||||
- listitem:
|
||||
- group: Verified
|
||||
- listitem:
|
||||
- link "Sponsor"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should support multiline text', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<p>
|
||||
Line 1
|
||||
Line 2
|
||||
Line 3
|
||||
</p>
|
||||
`);
|
||||
|
||||
await checkAndMatchSnapshot(page.locator('body'), `
|
||||
- paragraph: Line 1 Line 2 Line 3
|
||||
`);
|
||||
await expect(page.locator('body')).toMatchAriaSnapshot(`
|
||||
- paragraph: |
|
||||
Line 1
|
||||
Line 2
|
||||
Line 3
|
||||
`);
|
||||
});
|
||||
|
||||
it('should concatenate span text', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<span>One</span> <span>Two</span> <span>Three</span>
|
||||
`);
|
||||
|
||||
await checkAndMatchSnapshot(page.locator('body'), `
|
||||
- text: One Two Three
|
||||
`);
|
||||
});
|
||||
|
||||
it('should concatenate span text 2', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<span>One </span><span>Two </span><span>Three</span>
|
||||
`);
|
||||
|
||||
await checkAndMatchSnapshot(page.locator('body'), `
|
||||
- text: One Two Three
|
||||
`);
|
||||
});
|
||||
|
||||
it('should concatenate div text with spaces', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<div>One</div><div>Two</div><div>Three</div>
|
||||
`);
|
||||
|
||||
await checkAndMatchSnapshot(page.locator('body'), `
|
||||
- text: One Two Three
|
||||
`);
|
||||
});
|
||||
|
||||
it('should include pseudo in text', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<style>
|
||||
span:before {
|
||||
content: 'world';
|
||||
}
|
||||
div:after {
|
||||
content: 'bye';
|
||||
}
|
||||
</style>
|
||||
<a href="about:blank">
|
||||
<span>hello</span>
|
||||
<div>hello</div>
|
||||
</a>
|
||||
`);
|
||||
|
||||
await checkAndMatchSnapshot(page.locator('body'), `
|
||||
- link "worldhello hellobye"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not include hidden pseudo in text', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<style>
|
||||
span:before {
|
||||
content: 'world';
|
||||
display: none;
|
||||
}
|
||||
div:after {
|
||||
content: 'bye';
|
||||
visibility: hidden;
|
||||
}
|
||||
</style>
|
||||
<a href="about:blank">
|
||||
<span>hello</span>
|
||||
<div>hello</div>
|
||||
</a>
|
||||
`);
|
||||
|
||||
await checkAndMatchSnapshot(page.locator('body'), `
|
||||
- link "hello hello"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should include new line for block pseudo', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<style>
|
||||
span:before {
|
||||
content: 'world';
|
||||
display: block;
|
||||
}
|
||||
div:after {
|
||||
content: 'bye';
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
<a href="about:blank">
|
||||
<span>hello</span>
|
||||
<div>hello</div>
|
||||
</a>
|
||||
`);
|
||||
|
||||
await checkAndMatchSnapshot(page.locator('body'), `
|
||||
- link "world hello hello bye"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should work with slots', async ({ page }) => {
|
||||
// Text "foo" is assigned to the slot, should not be used twice.
|
||||
await page.setContent(`
|
||||
<button><div>foo</div></button>
|
||||
<script>
|
||||
(() => {
|
||||
const container = document.querySelector('div');
|
||||
const shadow = container.attachShadow({ mode: 'open' });
|
||||
const slot = document.createElement('slot');
|
||||
shadow.appendChild(slot);
|
||||
})();
|
||||
</script>
|
||||
`);
|
||||
await checkAndMatchSnapshot(page.locator('body'), `
|
||||
- button "foo"
|
||||
`);
|
||||
|
||||
// Text "foo" is assigned to the slot, should be used instead of slot content.
|
||||
await page.setContent(`
|
||||
<div>foo</div>
|
||||
<script>
|
||||
(() => {
|
||||
const container = document.querySelector('div');
|
||||
const shadow = container.attachShadow({ mode: 'open' });
|
||||
const button = document.createElement('button');
|
||||
shadow.appendChild(button);
|
||||
const slot = document.createElement('slot');
|
||||
button.appendChild(slot);
|
||||
const span = document.createElement('span');
|
||||
span.textContent = 'pre';
|
||||
slot.appendChild(span);
|
||||
})();
|
||||
</script>
|
||||
`);
|
||||
await checkAndMatchSnapshot(page.locator('body'), `
|
||||
- button "foo"
|
||||
`);
|
||||
|
||||
// Nothing is assigned to the slot, should use slot content.
|
||||
await page.setContent(`
|
||||
<div></div>
|
||||
<script>
|
||||
(() => {
|
||||
const container = document.querySelector('div');
|
||||
const shadow = container.attachShadow({ mode: 'open' });
|
||||
const button = document.createElement('button');
|
||||
shadow.appendChild(button);
|
||||
const slot = document.createElement('slot');
|
||||
button.appendChild(slot);
|
||||
const span = document.createElement('span');
|
||||
span.textContent = 'pre';
|
||||
slot.appendChild(span);
|
||||
})();
|
||||
</script>
|
||||
`);
|
||||
await checkAndMatchSnapshot(page.locator('body'), `
|
||||
- button "pre"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should snapshot inner text', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<div role="listitem">
|
||||
<div>
|
||||
<div>
|
||||
<span title="a.test.ts">a.test.ts</span>
|
||||
</div>
|
||||
<div>
|
||||
<button title="Run"></button>
|
||||
<button title="Show source"></button>
|
||||
<button title="Watch"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div role="listitem">
|
||||
<div>
|
||||
<div>
|
||||
<span title="snapshot">snapshot</span>
|
||||
</div>
|
||||
<div class="ui-mode-list-item-time">30ms</div>
|
||||
<div>
|
||||
<button title="Run"></button>
|
||||
<button title="Show source"></button>
|
||||
<button title="Watch"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
await checkAndMatchSnapshot(page.locator('body'), `
|
||||
- listitem:
|
||||
- text: a.test.ts
|
||||
- button "Run"
|
||||
- button "Show source"
|
||||
- button "Watch"
|
||||
- listitem:
|
||||
- text: snapshot 30ms
|
||||
- button "Run"
|
||||
- button "Show source"
|
||||
- button "Watch"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should include pseudo codepoints', async ({ page, server }) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.setContent(`
|
||||
<link href="codicon.css" rel="stylesheet" />
|
||||
<p class='codicon codicon-check'>hello</p>
|
||||
`);
|
||||
|
||||
await checkAndMatchSnapshot(page.locator('body'), `
|
||||
- paragraph: \ueab2hello
|
||||
`);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ test('should allow text nodes', async ({ page }) => {
|
|||
`);
|
||||
});
|
||||
|
||||
test('details visibility', async ({ page, browserName }) => {
|
||||
test('details visibility', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<details>
|
||||
<summary>Summary</summary>
|
||||
|
|
@ -107,7 +107,7 @@ test('details visibility', async ({ page, browserName }) => {
|
|||
`);
|
||||
});
|
||||
|
||||
test('integration test', async ({ page, browserName }) => {
|
||||
test('integration test', async ({ page }) => {
|
||||
await page.setContent(`
|
||||
<h1>Microsoft</h1>
|
||||
<div>Open source projects and samples from Microsoft</div>
|
||||
|
|
|
|||
|
|
@ -153,6 +153,10 @@ test('should respect focused tests', async ({ runInlineTest }) => {
|
|||
});
|
||||
});
|
||||
|
||||
test.fail.only('focused fail.only test', () => {
|
||||
expect(1 + 1).toBe(3);
|
||||
});
|
||||
|
||||
test.describe('non-focused describe', () => {
|
||||
test('describe test', () => {
|
||||
expect(1 + 1).toBe(3);
|
||||
|
|
@ -172,13 +176,46 @@ test('should respect focused tests', async ({ runInlineTest }) => {
|
|||
test.only('test4', () => {
|
||||
expect(1 + 1).toBe(2);
|
||||
});
|
||||
test.fail.only('test5', () => {
|
||||
expect(1 + 1).toBe(3);
|
||||
});
|
||||
});
|
||||
`
|
||||
});
|
||||
expect(passed).toBe(5);
|
||||
expect(passed).toBe(7);
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test('should respect focused tests with test.fail', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'fail-only.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('test1', () => {
|
||||
console.log('test1 should not run');
|
||||
expect(1 + 1).toBe(2);
|
||||
});
|
||||
|
||||
test.fail.only('test2', () => {
|
||||
console.log('test2 should run and fail');
|
||||
expect(1 + 1).toBe(3);
|
||||
});
|
||||
|
||||
test('test3', () => {
|
||||
console.log('test3 should not run');
|
||||
expect(1 + 1).toBe(2);
|
||||
});
|
||||
`,
|
||||
});
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
expect(result.failed).toBe(0);
|
||||
expect(result.skipped).toBe(0);
|
||||
expect(result.output).toContain('test2 should run and fail');
|
||||
expect(result.output).not.toContain('test1 should not run');
|
||||
expect(result.output).not.toContain('test3 should not run');
|
||||
});
|
||||
|
||||
test('skip should take priority over fail', async ({ runInlineTest }) => {
|
||||
const { passed, skipped, failed } = await runInlineTest({
|
||||
'test.spec.ts': `
|
||||
|
|
@ -550,3 +587,33 @@ test('should support describe.fixme', async ({ runInlineTest }) => {
|
|||
expect(result.skipped).toBe(3);
|
||||
expect(result.output).toContain('heytest4');
|
||||
});
|
||||
|
||||
test('should fail when test.fail.only passes unexpectedly', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'fail-only-pass.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('test1', () => {
|
||||
console.log('test1 should not run');
|
||||
expect(1 + 1).toBe(2);
|
||||
});
|
||||
|
||||
test.fail.only('test2', () => {
|
||||
console.log('test2 should run and pass unexpectedly');
|
||||
expect(1 + 1).toBe(2);
|
||||
});
|
||||
|
||||
test('test3', () => {
|
||||
console.log('test3 should not run');
|
||||
expect(1 + 1).toBe(2);
|
||||
});
|
||||
`,
|
||||
});
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.passed).toBe(0);
|
||||
expect(result.failed).toBe(1);
|
||||
expect(result.skipped).toBe(0);
|
||||
expect(result.output).toContain('should run and pass unexpectedly');
|
||||
expect(result.output).not.toContain('test1 should not run');
|
||||
expect(result.output).not.toContain('test3 should not run');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -386,3 +386,43 @@ test('teardown after error', async ({ runInlineTest }) => {
|
|||
'teardown 1',
|
||||
]);
|
||||
});
|
||||
|
||||
test('globalSetup should support multiple', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'playwright.config.ts': `
|
||||
module.exports = {
|
||||
globalSetup: ['./globalSetup1.ts','./globalSetup2.ts','./globalSetup3.ts','./globalSetup4.ts'],
|
||||
globalTeardown: ['./globalTeardown1.ts', './globalTeardown2.ts'],
|
||||
};
|
||||
`,
|
||||
'globalSetup1.ts': `module.exports = () => { console.log('%%globalSetup1'); return () => { console.log('%%globalSetup1Function'); throw new Error('kaboom'); } };`,
|
||||
'globalSetup2.ts': `module.exports = () => console.log('%%globalSetup2');`,
|
||||
'globalSetup3.ts': `module.exports = () => { console.log('%%globalSetup3'); return () => console.log('%%globalSetup3Function'); }`,
|
||||
'globalSetup4.ts': `module.exports = () => console.log('%%globalSetup4');`,
|
||||
'globalTeardown1.ts': `module.exports = () => console.log('%%globalTeardown1')`,
|
||||
'globalTeardown2.ts': `module.exports = () => { console.log('%%globalTeardown2'); throw new Error('kaboom'); }`,
|
||||
|
||||
'a.test.js': `
|
||||
import { test } from '@playwright/test';
|
||||
test('a', () => console.log('%%test a'));
|
||||
test('b', () => console.log('%%test b'));
|
||||
`,
|
||||
}, { reporter: 'line' });
|
||||
expect(result.passed).toBe(2);
|
||||
|
||||
// behaviour: setups in order, teardowns in reverse order.
|
||||
// setup-returned functions inherit their position, and take precedence over `globalTeardown` scripts.
|
||||
expect(result.outputLines).toEqual([
|
||||
'globalSetup1',
|
||||
'globalSetup2',
|
||||
'globalSetup3',
|
||||
'globalSetup4',
|
||||
'test a',
|
||||
'test b',
|
||||
'globalSetup3Function',
|
||||
'globalTeardown2',
|
||||
'globalSetup1Function',
|
||||
// 'globalTeardown1' is missing, because globalSetup1Function errored out.
|
||||
]);
|
||||
expect(result.output).toContain('Error: kaboom');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ const expect = baseExpect.configure({ timeout: process.env.CI ? 75000 : 25000 })
|
|||
|
||||
test.describe.configure({ mode: 'parallel' });
|
||||
|
||||
for (const useIntermediateMergeReport of [false] as const) {
|
||||
for (const useIntermediateMergeReport of [true, false] as const) {
|
||||
test.describe(`${useIntermediateMergeReport ? 'merged' : 'created'}`, () => {
|
||||
test.use({ useIntermediateMergeReport });
|
||||
|
||||
|
|
@ -612,7 +612,7 @@ for (const useIntermediateMergeReport of [false] as const) {
|
|||
]);
|
||||
});
|
||||
`,
|
||||
}, { reporter: 'html' }, { PLAYWRIGHT_HTML_OPEN: 'never' });
|
||||
}, { reporter: 'html,dot' }, { PLAYWRIGHT_HTML_OPEN: 'never' });
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
|
||||
|
|
@ -727,6 +727,34 @@ for (const useIntermediateMergeReport of [false] as const) {
|
|||
]);
|
||||
});
|
||||
|
||||
test('should show step snippets from non-root', async ({ runInlineTest, page, showReport }) => {
|
||||
const result = await runInlineTest({
|
||||
'playwright.config.js': `
|
||||
export default { testDir: './tests' };
|
||||
`,
|
||||
'tests/a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('example', async ({}) => {
|
||||
await test.step('step title', async () => {
|
||||
expect(1).toBe(1);
|
||||
});
|
||||
});
|
||||
`,
|
||||
}, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' });
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
|
||||
await showReport();
|
||||
await page.click('text=example');
|
||||
await page.click('text=step title');
|
||||
await page.click('text=expect.toBe');
|
||||
await expect(page.getByTestId('test-snippet')).toContainText([
|
||||
`await test.step('step title', async () => {`,
|
||||
'expect(1).toBe(1);',
|
||||
]);
|
||||
});
|
||||
|
||||
test('should render annotations', async ({ runInlineTest, page, showReport }) => {
|
||||
const result = await runInlineTest({
|
||||
'playwright.config.js': `
|
||||
|
|
|
|||
|
|
@ -5,16 +5,15 @@
|
|||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@playwright/test": "1.48.0-beta-1728384960000"
|
||||
"@playwright/test": "1.49.0-alpha-2024-10-17"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.48.0-beta-1728384960000",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.0-beta-1728384960000.tgz",
|
||||
"integrity": "sha512-bqQorY7LKVldgwAsUbjULdwKEoUlZ8OOHRZmM/1XyGiGqJwzTGdr0x8Ss312BvKddAh+5pz8cbaPopw10Rp3Ng==",
|
||||
"license": "Apache-2.0",
|
||||
"version": "1.49.0-alpha-2024-10-17",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-alpha-2024-10-17.tgz",
|
||||
"integrity": "sha512-HLZY3sM6xt9Wi8K09zPwjJQtcUBZNBcNSIVoMZhtJM3+TikCKx4SiJ3P8vbSlk7Tm3s2oqlS+wA181IxhbTGBA==",
|
||||
"dependencies": {
|
||||
"playwright": "1.48.0-beta-1728384960000"
|
||||
"playwright": "1.49.0-alpha-2024-10-17"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -28,7 +27,6 @@
|
|||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
|
|
@ -38,12 +36,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.48.0-beta-1728384960000",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.0-beta-1728384960000.tgz",
|
||||
"integrity": "sha512-5pIZTwoktOGYJL+YpF2RNhGzVUY6rA/ceQAT0lEQSZaL55MKUzraD2FAoZoBnz84cIIks2ZSlXt8j5mJ5xXt8g==",
|
||||
"license": "Apache-2.0",
|
||||
"version": "1.49.0-alpha-2024-10-17",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-alpha-2024-10-17.tgz",
|
||||
"integrity": "sha512-IgcLunnpocVS/AEq2lcftVOu0DGQzFm1Qt25SCJsrVvKVe83ElKXZYskPz7yA0HeuOVxQyN69EDWI09ph7lfoQ==",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.48.0-beta-1728384960000"
|
||||
"playwright-core": "1.49.0-alpha-2024-10-17"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -56,10 +53,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.48.0-beta-1728384960000",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.0-beta-1728384960000.tgz",
|
||||
"integrity": "sha512-atIhpuvqvVEW5luPhwzhdcXsGdPvzOBLXAg3+MvOLY+6Q4JcTfXMTtTmltP+llUV+LAgj38foQz+6tKTzNMlWg==",
|
||||
"license": "Apache-2.0",
|
||||
"version": "1.49.0-alpha-2024-10-17",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-alpha-2024-10-17.tgz",
|
||||
"integrity": "sha512-XLTKmPBm2ZIOXBckXtiimSOIjQsYy8MqEP9CsHSgytsP0E+j/44v1BuwHOOMaG8sfjcuZLZ1QdFidnl07A9wSg==",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
|
|
@ -70,11 +66,11 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@playwright/test": {
|
||||
"version": "1.48.0-beta-1728384960000",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.0-beta-1728384960000.tgz",
|
||||
"integrity": "sha512-bqQorY7LKVldgwAsUbjULdwKEoUlZ8OOHRZmM/1XyGiGqJwzTGdr0x8Ss312BvKddAh+5pz8cbaPopw10Rp3Ng==",
|
||||
"version": "1.49.0-alpha-2024-10-17",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-alpha-2024-10-17.tgz",
|
||||
"integrity": "sha512-HLZY3sM6xt9Wi8K09zPwjJQtcUBZNBcNSIVoMZhtJM3+TikCKx4SiJ3P8vbSlk7Tm3s2oqlS+wA181IxhbTGBA==",
|
||||
"requires": {
|
||||
"playwright": "1.48.0-beta-1728384960000"
|
||||
"playwright": "1.49.0-alpha-2024-10-17"
|
||||
}
|
||||
},
|
||||
"fsevents": {
|
||||
|
|
@ -84,18 +80,18 @@
|
|||
"optional": true
|
||||
},
|
||||
"playwright": {
|
||||
"version": "1.48.0-beta-1728384960000",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.0-beta-1728384960000.tgz",
|
||||
"integrity": "sha512-5pIZTwoktOGYJL+YpF2RNhGzVUY6rA/ceQAT0lEQSZaL55MKUzraD2FAoZoBnz84cIIks2ZSlXt8j5mJ5xXt8g==",
|
||||
"version": "1.49.0-alpha-2024-10-17",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-alpha-2024-10-17.tgz",
|
||||
"integrity": "sha512-IgcLunnpocVS/AEq2lcftVOu0DGQzFm1Qt25SCJsrVvKVe83ElKXZYskPz7yA0HeuOVxQyN69EDWI09ph7lfoQ==",
|
||||
"requires": {
|
||||
"fsevents": "2.3.2",
|
||||
"playwright-core": "1.48.0-beta-1728384960000"
|
||||
"playwright-core": "1.49.0-alpha-2024-10-17"
|
||||
}
|
||||
},
|
||||
"playwright-core": {
|
||||
"version": "1.48.0-beta-1728384960000",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.0-beta-1728384960000.tgz",
|
||||
"integrity": "sha512-atIhpuvqvVEW5luPhwzhdcXsGdPvzOBLXAg3+MvOLY+6Q4JcTfXMTtTmltP+llUV+LAgj38foQz+6tKTzNMlWg=="
|
||||
"version": "1.49.0-alpha-2024-10-17",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-alpha-2024-10-17.tgz",
|
||||
"integrity": "sha512-XLTKmPBm2ZIOXBckXtiimSOIjQsYy8MqEP9CsHSgytsP0E+j/44v1BuwHOOMaG8sfjcuZLZ1QdFidnl07A9wSg=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@playwright/test": "1.48.0-beta-1728384960000"
|
||||
"@playwright/test": "1.49.0-alpha-2024-10-17"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -279,6 +279,33 @@ test.describe('test modifier annotations', () => {
|
|||
expectTest('focused fixme by suite', 'skipped', 'skipped', ['fixme']);
|
||||
});
|
||||
|
||||
test('should work with fail.only inside describe.only', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe.only("suite", () => {
|
||||
test.skip('focused skip by suite', () => {});
|
||||
test.fixme('focused fixme by suite', () => {});
|
||||
test.fail.only('focused fail by suite', () => { expect(1).toBe(2); });
|
||||
});
|
||||
|
||||
test.describe.skip('not focused', () => {
|
||||
test('no marker', () => {});
|
||||
});
|
||||
`,
|
||||
});
|
||||
const expectTest = expectTestHelper(result);
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
expect(result.failed).toBe(0);
|
||||
expect(result.skipped).toBe(0);
|
||||
expectTest('focused skip by suite', 'skipped', 'skipped', ['skip']);
|
||||
expectTest('focused fixme by suite', 'skipped', 'skipped', ['fixme']);
|
||||
expectTest('focused fail by suite', 'failed', 'expected', ['fail']);
|
||||
});
|
||||
|
||||
test('should not multiple on retry', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.test.ts': `
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ test('basics should work', async ({ runTSC }) => {
|
|||
test.skip('my test', async () => {});
|
||||
test.fixme('my test', async () => {});
|
||||
test.fail('my test', async () => {});
|
||||
test.fail.only('my test', async () => {});
|
||||
});
|
||||
test.describe(() => {
|
||||
test('my test', () => {});
|
||||
|
|
@ -59,6 +60,7 @@ test('basics should work', async ({ runTSC }) => {
|
|||
test.fixme('title', { tag: '@foo' }, () => {});
|
||||
test.only('title', { tag: '@foo' }, () => {});
|
||||
test.fail('title', { tag: '@foo' }, () => {});
|
||||
test.fail.only('title', { tag: '@foo' }, () => {});
|
||||
test.describe('title', { tag: '@foo' }, () => {});
|
||||
test.describe('title', { annotation: { type: 'issue' } }, () => {});
|
||||
// @ts-expect-error
|
||||
|
|
|
|||
|
|
@ -66,16 +66,16 @@ export function dumpTestTree(page: Page, options: { time?: boolean } = {}): () =
|
|||
}
|
||||
|
||||
const result: string[] = [];
|
||||
const listItems = treeElement.querySelectorAll('[role=listitem]');
|
||||
for (const listItem of listItems) {
|
||||
const iconElements = listItem.querySelectorAll('.codicon');
|
||||
const treeItems = treeElement.querySelectorAll('[role=treeitem]');
|
||||
for (const treeItem of treeItems) {
|
||||
const iconElements = treeItem.querySelectorAll('.codicon');
|
||||
const treeIcon = iconName(iconElements[0]);
|
||||
const statusIcon = iconName(iconElements[1]);
|
||||
const indent = listItem.querySelectorAll('.list-view-indent').length;
|
||||
const watch = listItem.querySelector('.toolbar-button.eye.toggled') ? ' 👁' : '';
|
||||
const selected = listItem.classList.contains('selected') ? ' <=' : '';
|
||||
const title = listItem.querySelector('.ui-mode-list-item-title').childNodes[0].textContent;
|
||||
const timeElement = options.time ? listItem.querySelector('.ui-mode-list-item-time') : undefined;
|
||||
const indent = treeItem.querySelectorAll('.tree-view-indent').length;
|
||||
const watch = treeItem.querySelector('.toolbar-button.eye.toggled') ? ' 👁' : '';
|
||||
const selected = treeItem.getAttribute('aria-selected') === 'true' ? ' <=' : '';
|
||||
const title = treeItem.querySelector('.ui-mode-tree-item-title').childNodes[0].textContent;
|
||||
const timeElement = options.time ? treeItem.querySelector('.ui-mode-tree-item-time') : undefined;
|
||||
const time = timeElement ? ' ' + timeElement.textContent.replace(/[.\d]+m?s/, 'XXms') : '';
|
||||
result.push(' ' + ' '.repeat(indent) + treeIcon + ' ' + statusIcon + ' ' + title + time + watch + selected);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ test('should display annotations', async ({ runUITest }) => {
|
|||
});
|
||||
await page.getByTitle('Run all').click();
|
||||
await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
|
||||
await page.getByRole('listitem').filter({ hasText: 'suite' }).locator('.codicon-chevron-right').click();
|
||||
await page.getByRole('treeitem').filter({ hasText: 'suite' }).locator('.codicon-chevron-right').click();
|
||||
await page.getByText('annotation test').click();
|
||||
await page.getByText('Annotations', { exact: true }).click();
|
||||
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ test('should display native tags and filter by them on click', async ({ runUITes
|
|||
test('pwt', { tag: '@smoke' }, () => {});
|
||||
`,
|
||||
});
|
||||
await page.locator('.ui-mode-list-item-title').getByText('smoke').click();
|
||||
await page.locator('.ui-mode-tree-item-title').getByText('smoke').click();
|
||||
await expect(page.getByPlaceholder('Filter')).toHaveValue('@smoke');
|
||||
await expect.poll(dumpTestTree(page)).toBe(`
|
||||
▼ ◯ a.test.ts
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ test('should update trace live', async ({ runUITest, server }) => {
|
|||
await page.getByText('live test').dblclick();
|
||||
|
||||
// It should halt on loading one.html.
|
||||
const listItem = page.getByTestId('actions-tree').getByRole('listitem');
|
||||
const listItem = page.getByTestId('actions-tree').getByRole('treeitem');
|
||||
await expect(
|
||||
listItem,
|
||||
'action list'
|
||||
|
|
@ -57,11 +57,11 @@ test('should update trace live', async ({ runUITest, server }) => {
|
|||
]);
|
||||
|
||||
await expect(
|
||||
listItem.locator(':scope.selected'),
|
||||
listItem.locator(':scope[aria-selected="true"]'),
|
||||
'last action to be selected'
|
||||
).toHaveText(/page.goto/);
|
||||
await expect(
|
||||
listItem.locator(':scope.selected .codicon.codicon-loading'),
|
||||
listItem.locator(':scope[aria-selected="true"] .codicon.codicon-loading'),
|
||||
'spinner'
|
||||
).toBeVisible();
|
||||
|
||||
|
|
@ -83,11 +83,11 @@ test('should update trace live', async ({ runUITest, server }) => {
|
|||
/page.gotohttp:\/\/localhost:\d+\/two.html/
|
||||
]);
|
||||
await expect(
|
||||
listItem.locator(':scope.selected'),
|
||||
listItem.locator(':scope[aria-selected="true"]'),
|
||||
'last action to be selected'
|
||||
).toHaveText(/page.goto/);
|
||||
await expect(
|
||||
listItem.locator(':scope.selected .codicon.codicon-loading'),
|
||||
listItem.locator(':scope[aria-selected="true"] .codicon.codicon-loading'),
|
||||
'spinner'
|
||||
).toBeVisible();
|
||||
|
||||
|
|
@ -132,7 +132,7 @@ test('should preserve action list selection upon live trace update', async ({ ru
|
|||
await page.getByText('live test').dblclick();
|
||||
|
||||
// It should wait on the latch.
|
||||
const listItem = page.getByTestId('actions-tree').getByRole('listitem');
|
||||
const listItem = page.getByTestId('actions-tree').getByRole('treeitem');
|
||||
await expect(
|
||||
listItem,
|
||||
'action list'
|
||||
|
|
@ -157,7 +157,7 @@ test('should preserve action list selection upon live trace update', async ({ ru
|
|||
/page.setContent[\d.]+m?s/,
|
||||
]);
|
||||
await expect(
|
||||
listItem.locator(':scope.selected'),
|
||||
listItem.locator(':scope[aria-selected="true"]'),
|
||||
'selected action stays the same'
|
||||
).toHaveText(/page.goto/);
|
||||
});
|
||||
|
|
@ -193,7 +193,7 @@ test('should update tracing network live', async ({ runUITest, server }) => {
|
|||
await page.getByText('live test').dblclick();
|
||||
|
||||
// It should wait on the latch.
|
||||
const listItem = page.getByTestId('actions-tree').getByRole('listitem');
|
||||
const listItem = page.getByTestId('actions-tree').getByRole('treeitem');
|
||||
await expect(
|
||||
listItem,
|
||||
'action list'
|
||||
|
|
@ -233,7 +233,7 @@ test('should show trace w/ multiple contexts', async ({ runUITest, server, creat
|
|||
await page.getByText('live test').dblclick();
|
||||
|
||||
// It should wait on the latch.
|
||||
const listItem = page.getByTestId('actions-tree').getByRole('listitem');
|
||||
const listItem = page.getByTestId('actions-tree').getByRole('treeitem');
|
||||
await expect(
|
||||
listItem,
|
||||
'action list'
|
||||
|
|
@ -278,7 +278,7 @@ test('should show live trace for serial', async ({ runUITest, server, createLatc
|
|||
await page.getByText('two', { exact: true }).click();
|
||||
await page.getByTitle('Run all').click();
|
||||
|
||||
const listItem = page.getByTestId('actions-tree').getByRole('listitem');
|
||||
const listItem = page.getByTestId('actions-tree').getByRole('treeitem');
|
||||
await expect(
|
||||
listItem,
|
||||
'action list'
|
||||
|
|
@ -318,7 +318,7 @@ test('should show live trace from hooks', async ({ runUITest, createLatch }) =>
|
|||
`);
|
||||
await page.getByText('test one').dblclick();
|
||||
|
||||
const listItem = page.getByTestId('actions-tree').getByRole('listitem');
|
||||
const listItem = page.getByTestId('actions-tree').getByRole('treeitem');
|
||||
await expect(
|
||||
listItem,
|
||||
'action list'
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ test('should run on hover', async ({ runUITest }) => {
|
|||
});
|
||||
|
||||
await page.getByText('passes').hover();
|
||||
await page.getByRole('listitem').filter({ hasText: 'passes' }).getByTitle('Run').click();
|
||||
await page.getByRole('treeitem').filter({ hasText: 'passes' }).getByTitle('Run').click();
|
||||
|
||||
await expect.poll(dumpTestTree(page)).toBe(`
|
||||
▼ ◯ a.test.ts
|
||||
|
|
@ -275,7 +275,7 @@ test('should run folder', async ({ runUITest }) => {
|
|||
});
|
||||
|
||||
await page.getByText('folder-b').hover();
|
||||
await page.getByRole('listitem').filter({ hasText: 'folder-b' }).getByTitle('Run').click();
|
||||
await page.getByRole('treeitem').filter({ hasText: 'folder-b' }).getByTitle('Run').click();
|
||||
|
||||
await expect.poll(dumpTestTree(page)).toContain(`
|
||||
▼ ✅ folder-b <=
|
||||
|
|
|
|||
|
|
@ -211,7 +211,7 @@ test('should run part of the setup only', async ({ runUITest }) => {
|
|||
await page.getByLabel('test').setChecked(true);
|
||||
|
||||
await page.getByText('setup.ts').hover();
|
||||
await page.getByRole('listitem').filter({ hasText: 'setup.ts' }).getByTitle('Run').click();
|
||||
await page.getByRole('treeitem').filter({ hasText: 'setup.ts' }).getByTitle('Run').click();
|
||||
|
||||
await expect.poll(dumpTestTree(page)).toBe(`
|
||||
▼ ✅ setup.ts <=
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ test('should not loose run information after execution if test wrote into testDi
|
|||
await page.getByTitle('Run all').click();
|
||||
await page.waitForTimeout(5_000);
|
||||
await expect(page.getByText('Did not run')).toBeHidden();
|
||||
const listItem = page.getByTestId('actions-tree').getByRole('listitem');
|
||||
const listItem = page.getByTestId('actions-tree').getByRole('treeitem');
|
||||
await expect(
|
||||
listItem,
|
||||
'action list'
|
||||
|
|
@ -215,7 +215,7 @@ test('should update test locations', async ({ runUITest, writeFiles }) => {
|
|||
const messages: any[] = [];
|
||||
await page.exposeBinding('__logForTest', (source, arg) => messages.push(arg));
|
||||
|
||||
const passesItemLocator = page.getByRole('listitem').filter({ hasText: 'passes' });
|
||||
const passesItemLocator = page.getByRole('treeitem').filter({ hasText: 'passes' });
|
||||
await passesItemLocator.hover();
|
||||
await passesItemLocator.getByTitle('Show source').click();
|
||||
await page.getByTitle('Open in VS Code').click();
|
||||
|
|
|
|||
|
|
@ -28,14 +28,14 @@ test('should watch files', async ({ runUITest, writeFiles }) => {
|
|||
});
|
||||
|
||||
await page.getByText('fails').click();
|
||||
await page.getByRole('listitem').filter({ hasText: 'fails' }).getByTitle('Watch').click();
|
||||
await page.getByRole('treeitem').filter({ hasText: 'fails' }).getByTitle('Watch').click();
|
||||
await expect.poll(dumpTestTree(page)).toBe(`
|
||||
▼ ◯ a.test.ts
|
||||
◯ passes
|
||||
◯ fails 👁 <=
|
||||
`);
|
||||
|
||||
await page.getByRole('listitem').filter({ hasText: 'fails' }).getByTitle('Run').click();
|
||||
await page.getByRole('treeitem').filter({ hasText: 'fails' }).getByTitle('Run').click();
|
||||
|
||||
await expect.poll(dumpTestTree(page)).toBe(`
|
||||
▼ ❌ a.test.ts
|
||||
|
|
@ -75,7 +75,7 @@ test('should watch e2e deps', async ({ runUITest, writeFiles }) => {
|
|||
});
|
||||
|
||||
await page.getByText('answer').click();
|
||||
await page.getByRole('listitem').filter({ hasText: 'answer' }).getByTitle('Watch').click();
|
||||
await page.getByRole('treeitem').filter({ hasText: 'answer' }).getByTitle('Watch').click();
|
||||
await expect.poll(dumpTestTree(page)).toBe(`
|
||||
▼ ◯ a.test.ts
|
||||
◯ answer 👁 <=
|
||||
|
|
@ -102,13 +102,13 @@ test('should batch watch updates', async ({ runUITest, writeFiles }) => {
|
|||
});
|
||||
|
||||
await page.getByText('a.test.ts').click();
|
||||
await page.getByRole('listitem').filter({ hasText: 'a.test.ts' }).getByTitle('Watch').click();
|
||||
await page.getByRole('treeitem').filter({ hasText: 'a.test.ts' }).getByTitle('Watch').click();
|
||||
await page.getByText('b.test.ts').click();
|
||||
await page.getByRole('listitem').filter({ hasText: 'b.test.ts' }).getByTitle('Watch').click();
|
||||
await page.getByRole('treeitem').filter({ hasText: 'b.test.ts' }).getByTitle('Watch').click();
|
||||
await page.getByText('c.test.ts').click();
|
||||
await page.getByRole('listitem').filter({ hasText: 'c.test.ts' }).getByTitle('Watch').click();
|
||||
await page.getByRole('treeitem').filter({ hasText: 'c.test.ts' }).getByTitle('Watch').click();
|
||||
await page.getByText('d.test.ts').click();
|
||||
await page.getByRole('listitem').filter({ hasText: 'd.test.ts' }).getByTitle('Watch').click();
|
||||
await page.getByRole('treeitem').filter({ hasText: 'd.test.ts' }).getByTitle('Watch').click();
|
||||
|
||||
await expect.poll(dumpTestTree(page)).toBe(`
|
||||
▼ ◯ a.test.ts 👁
|
||||
|
|
@ -229,7 +229,7 @@ test('should run added test in watched file', async ({ runUITest, writeFiles })
|
|||
});
|
||||
|
||||
await page.getByText('a.test.ts').click();
|
||||
await page.getByRole('listitem').filter({ hasText: 'a.test.ts' }).getByTitle('Watch').click();
|
||||
await page.getByRole('treeitem').filter({ hasText: 'a.test.ts' }).getByTitle('Watch').click();
|
||||
|
||||
await expect.poll(dumpTestTree(page)).toBe(`
|
||||
▼ ◯ a.test.ts 👁 <=
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ test('should merge trace events', async ({ runUITest }) => {
|
|||
|
||||
await page.getByText('trace test').dblclick();
|
||||
|
||||
const listItem = page.getByTestId('actions-tree').getByRole('listitem');
|
||||
const listItem = page.getByTestId('actions-tree').getByRole('treeitem');
|
||||
await expect(
|
||||
listItem,
|
||||
'action list'
|
||||
|
|
@ -61,7 +61,7 @@ test('should merge web assertion events', async ({ runUITest }, testInfo) => {
|
|||
|
||||
await page.getByText('trace test').dblclick();
|
||||
|
||||
const listItem = page.getByTestId('actions-tree').getByRole('listitem');
|
||||
const listItem = page.getByTestId('actions-tree').getByRole('treeitem');
|
||||
await expect(
|
||||
listItem,
|
||||
'action list'
|
||||
|
|
@ -86,7 +86,7 @@ test('should merge screenshot assertions', async ({ runUITest }, testInfo) => {
|
|||
|
||||
await page.getByText('trace test').dblclick();
|
||||
|
||||
const listItem = page.getByTestId('actions-tree').getByRole('listitem');
|
||||
const listItem = page.getByTestId('actions-tree').getByRole('treeitem');
|
||||
await expect(
|
||||
listItem,
|
||||
'action list'
|
||||
|
|
@ -134,7 +134,7 @@ test('should show snapshots for sync assertions', async ({ runUITest }) => {
|
|||
|
||||
await page.getByText('trace test').dblclick();
|
||||
|
||||
const listItem = page.getByTestId('actions-tree').getByRole('listitem');
|
||||
const listItem = page.getByTestId('actions-tree').getByRole('treeitem');
|
||||
await expect(
|
||||
listItem,
|
||||
'action list'
|
||||
|
|
@ -214,7 +214,7 @@ test('should not fail on internal page logs', async ({ runUITest, server }) => {
|
|||
});
|
||||
|
||||
await page.getByText('pass').dblclick();
|
||||
const listItem = page.getByTestId('actions-tree').getByRole('listitem');
|
||||
const listItem = page.getByTestId('actions-tree').getByRole('treeitem');
|
||||
|
||||
await expect(
|
||||
listItem,
|
||||
|
|
@ -241,7 +241,7 @@ test('should not show caught errors in the errors tab', async ({ runUITest }, te
|
|||
});
|
||||
|
||||
await page.getByText('pass').dblclick();
|
||||
const listItem = page.getByTestId('actions-tree').getByRole('listitem');
|
||||
const listItem = page.getByTestId('actions-tree').getByRole('treeitem');
|
||||
|
||||
await expect(
|
||||
listItem,
|
||||
|
|
@ -272,7 +272,7 @@ test('should reveal errors in the sourcetab', async ({ runUITest }) => {
|
|||
});
|
||||
|
||||
await page.getByText('pass').dblclick();
|
||||
const listItem = page.getByTestId('actions-tree').getByRole('listitem');
|
||||
const listItem = page.getByTestId('actions-tree').getByRole('treeitem');
|
||||
|
||||
await expect(
|
||||
listItem,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ FROM ubuntu:noble
|
|||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ARG TZ=America/Los_Angeles
|
||||
ARG DOCKER_IMAGE_NAME_TEMPLATE="mcr.microsoft.com/playwright:v%version%-jammy"
|
||||
ARG DOCKER_IMAGE_NAME_TEMPLATE="mcr.microsoft.com/playwright:v%version%-noble"
|
||||
|
||||
ENV LANG=C.UTF-8
|
||||
ENV LC_ALL=C.UTF-8
|
||||
|
|
|
|||
|
|
@ -93,25 +93,8 @@ class TypesGenerator {
|
|||
handledClasses.add(className);
|
||||
return this.writeComment(docClass.comment, '') + '\n';
|
||||
}, (className, methodName, overloadIndex) => {
|
||||
if (className === 'SuiteFunction' && methodName === '__call') {
|
||||
const cls = this.documentation.classes.get('Test');
|
||||
if (!cls)
|
||||
throw new Error(`Unknown class "Test"`);
|
||||
const method = cls.membersArray.find(m => m.alias === 'describe');
|
||||
if (!method)
|
||||
throw new Error(`Unknown method "Test.describe"`);
|
||||
return this.memberJSDOC(method, ' ').trimLeft();
|
||||
}
|
||||
if (className === 'TestFunction' && methodName === '__call') {
|
||||
const cls = this.documentation.classes.get('Test');
|
||||
if (!cls)
|
||||
throw new Error(`Unknown class "Test"`);
|
||||
const method = cls.membersArray.find(m => m.alias === '(call)');
|
||||
if (!method)
|
||||
throw new Error(`Unknown method "Test.(call)"`);
|
||||
return this.memberJSDOC(method, ' ').trimLeft();
|
||||
}
|
||||
|
||||
if (methodName === '__call')
|
||||
methodName = '(call)';
|
||||
const docClass = this.docClassForName(className);
|
||||
let method;
|
||||
if (docClass) {
|
||||
|
|
@ -591,8 +574,6 @@ class TypesGenerator {
|
|||
'PlaywrightWorkerArgs.playwright',
|
||||
'PlaywrightWorkerOptions.defaultBrowserType',
|
||||
'Project',
|
||||
'SuiteFunction',
|
||||
'TestFunction',
|
||||
]),
|
||||
doNotExportClassNames: assertionClasses,
|
||||
});
|
||||
|
|
|
|||
90
utils/generate_types/overrides-test.d.ts
vendored
90
utils/generate_types/overrides-test.d.ts
vendored
|
|
@ -75,49 +75,83 @@ export type TestDetails = {
|
|||
annotation?: TestDetailsAnnotation | TestDetailsAnnotation[];
|
||||
}
|
||||
|
||||
interface SuiteFunction {
|
||||
type TestBody<TestArgs> = (args: TestArgs, testInfo: TestInfo) => Promise<void> | void;
|
||||
type ConditionBody<TestArgs> = (args: TestArgs) => boolean;
|
||||
|
||||
export interface TestType<TestArgs extends KeyValue, WorkerArgs extends KeyValue> {
|
||||
(title: string, body: TestBody<TestArgs & WorkerArgs>): void;
|
||||
(title: string, details: TestDetails, body: TestBody<TestArgs & WorkerArgs>): void;
|
||||
|
||||
only(title: string, body: TestBody<TestArgs & WorkerArgs>): void;
|
||||
only(title: string, details: TestDetails, body: TestBody<TestArgs & WorkerArgs>): void;
|
||||
|
||||
describe: {
|
||||
(title: string, callback: () => void): void;
|
||||
(callback: () => void): void;
|
||||
(title: string, details: TestDetails, callback: () => void): void;
|
||||
}
|
||||
|
||||
interface TestFunction<TestArgs> {
|
||||
(title: string, body: (args: TestArgs, testInfo: TestInfo) => Promise<void> | void): void;
|
||||
(title: string, details: TestDetails, body: (args: TestArgs, testInfo: TestInfo) => Promise<void> | void): void;
|
||||
}
|
||||
only(title: string, callback: () => void): void;
|
||||
only(callback: () => void): void;
|
||||
only(title: string, details: TestDetails, callback: () => void): void;
|
||||
|
||||
export interface TestType<TestArgs extends KeyValue, WorkerArgs extends KeyValue> extends TestFunction<TestArgs & WorkerArgs> {
|
||||
only: TestFunction<TestArgs & WorkerArgs>;
|
||||
describe: SuiteFunction & {
|
||||
only: SuiteFunction;
|
||||
skip: SuiteFunction;
|
||||
fixme: SuiteFunction;
|
||||
serial: SuiteFunction & {
|
||||
only: SuiteFunction;
|
||||
skip(title: string, callback: () => void): void;
|
||||
skip(callback: () => void): void;
|
||||
skip(title: string, details: TestDetails, callback: () => void): void;
|
||||
|
||||
fixme(title: string, callback: () => void): void;
|
||||
fixme(callback: () => void): void;
|
||||
fixme(title: string, details: TestDetails, callback: () => void): void;
|
||||
|
||||
serial: {
|
||||
(title: string, callback: () => void): void;
|
||||
(callback: () => void): void;
|
||||
(title: string, details: TestDetails, callback: () => void): void;
|
||||
|
||||
only(title: string, callback: () => void): void;
|
||||
only(callback: () => void): void;
|
||||
only(title: string, details: TestDetails, callback: () => void): void;
|
||||
};
|
||||
parallel: SuiteFunction & {
|
||||
only: SuiteFunction;
|
||||
|
||||
parallel: {
|
||||
(title: string, callback: () => void): void;
|
||||
(callback: () => void): void;
|
||||
(title: string, details: TestDetails, callback: () => void): void;
|
||||
|
||||
only(title: string, callback: () => void): void;
|
||||
only(callback: () => void): void;
|
||||
only(title: string, details: TestDetails, callback: () => void): void;
|
||||
};
|
||||
|
||||
configure: (options: { mode?: 'default' | 'parallel' | 'serial', retries?: number, timeout?: number }) => void;
|
||||
};
|
||||
skip(title: string, body: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise<void> | void): void;
|
||||
skip(title: string, details: TestDetails, body: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise<void> | void): void;
|
||||
|
||||
skip(title: string, body: TestBody<TestArgs & WorkerArgs>): void;
|
||||
skip(title: string, details: TestDetails, body: TestBody<TestArgs & WorkerArgs>): void;
|
||||
skip(): void;
|
||||
skip(condition: boolean, description?: string): void;
|
||||
skip(callback: (args: TestArgs & WorkerArgs) => boolean, description?: string): void;
|
||||
fixme(title: string, body: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise<void> | void): void;
|
||||
fixme(title: string, details: TestDetails, body: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise<void> | void): void;
|
||||
skip(callback: ConditionBody<TestArgs & WorkerArgs>, description?: string): void;
|
||||
|
||||
fixme(title: string, body: TestBody<TestArgs & WorkerArgs>): void;
|
||||
fixme(title: string, details: TestDetails, body: TestBody<TestArgs & WorkerArgs>): void;
|
||||
fixme(): void;
|
||||
fixme(condition: boolean, description?: string): void;
|
||||
fixme(callback: (args: TestArgs & WorkerArgs) => boolean, description?: string): void;
|
||||
fail(title: string, body: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise<void> | void): void;
|
||||
fail(title: string, details: TestDetails, body: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise<void> | void): void;
|
||||
fail(condition: boolean, description?: string): void;
|
||||
fail(callback: (args: TestArgs & WorkerArgs) => boolean, description?: string): void;
|
||||
fail(): void;
|
||||
fixme(callback: ConditionBody<TestArgs & WorkerArgs>, description?: string): void;
|
||||
|
||||
fail: {
|
||||
(title: string, body: TestBody<TestArgs & WorkerArgs>): void;
|
||||
(title: string, details: TestDetails, body: TestBody<TestArgs & WorkerArgs>): void;
|
||||
(condition: boolean, description?: string): void;
|
||||
(callback: ConditionBody<TestArgs & WorkerArgs>, description?: string): void;
|
||||
(): void;
|
||||
|
||||
only(title: string, body: TestBody<TestArgs & WorkerArgs>): void;
|
||||
only(title: string, details: TestDetails, body: TestBody<TestArgs & WorkerArgs>): void;
|
||||
}
|
||||
|
||||
slow(): void;
|
||||
slow(condition: boolean, description?: string): void;
|
||||
slow(callback: (args: TestArgs & WorkerArgs) => boolean, description?: string): void;
|
||||
slow(callback: ConditionBody<TestArgs & WorkerArgs>, description?: string): void;
|
||||
|
||||
setTimeout(timeout: number): void;
|
||||
beforeEach(inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise<any> | any): void;
|
||||
beforeEach(title: string, inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise<any> | any): void;
|
||||
|
|
|
|||
|
|
@ -101,9 +101,9 @@ async function parseOverrides(filePath, commentForClass, commentForMethod, extra
|
|||
* @param {ts.Node} node
|
||||
*/
|
||||
function visitProperties(className, prefix, node) {
|
||||
// This function supports structs like "a: { b: string; c: number, (): void }"
|
||||
// and inserts comments for "a.b", "a.c", a.
|
||||
if (ts.isPropertySignature(node)) {
|
||||
// This function supports structs like "a: { b: string; c: number, (): void, d(): void }"
|
||||
// and inserts comments for "a.b", "a.c", "a", "a.d".
|
||||
if (ts.isPropertySignature(node) || ts.isMethodSignature(node)) {
|
||||
const name = checker.getSymbolAtLocation(node.name).getName();
|
||||
const pos = node.getStart(file, false);
|
||||
replacers.push({
|
||||
|
|
|
|||
Loading…
Reference in a new issue