Compare commits

...

14 commits

Author SHA1 Message Date
Playwright Service 50769b4e23
cherry-pick(#30964): docs: add release video (#30975)
This PR cherry-picks the following commits:

- 7ead708902

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2024-05-23 10:33:51 +02:00
Max Schmitt afacb84a52
chore: mark v1.44.1 (#30942) 2024-05-23 09:29:52 +02:00
Dmitry Gozman be133650f6
cherry-pick(#30853): chore: print friendly localhost address from http server (#30881) 2024-05-21 08:40:49 -07:00
Max Schmitt 67b85e6ace
docs: cherry-pick dotnet docs enhancements + release-notes (#30927)
Co-authored-by: Debbie O'Brien <debs-obrien@users.noreply.github.com>
2024-05-21 12:04:31 +02:00
Pavel Feldman 32bde52512 cherry-pick(#30832): chore(testServer): accept video parameter when running tests 2024-05-15 12:47:58 -07:00
Pavel Feldman 5d2623030d cherry-pick(#30807): chore: do not close the reused context when video is on 2024-05-15 12:47:16 -07:00
Playwright Service 3867d5581b
cherry-pick(#30820): fix(electron): allow launching with spaces in path (#30830)
This PR cherry-picks the following commits:

- 90765a226f

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2024-05-15 18:46:48 +02:00
Playwright Service 01bf93cda4
cherry-pick(#30800): Revert "fix(highlight): highlight Top Layer elements (#30001)" (#30801)
This PR cherry-picks the following commits:

- b06c1dfff1

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2024-05-14 18:00:51 +01:00
Playwright Service 1b2de3f7a7
cherry-pick(#30708): docs(python): roll fixes (#30709)
This PR cherry-picks the following commits:

- 5babb37f19

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2024-05-08 18:24:51 +01:00
Yury Semikhatsky a6aa50b0c7
chore: set version to 1.44.0 (#30680) 2024-05-06 11:26:54 -07:00
Yury Semikhatsky 54c157dec5
cherry-pick(#30677): chore: print resolved host in the http server te… (#30679)
…rminal
2024-05-06 11:07:45 -07:00
Playwright Service 2d437e86e2
cherry-pick(#30646): feat(chromium): roll to r1117 (#30652)
This PR cherry-picks the following commits:

- ce69236510

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2024-05-03 16:16:44 +00:00
Dmitry Gozman 7637399834
cherry-pick(#30636): fix(role): extract tagName safely (#30639)
Fixes #30616.
2024-05-02 11:19:58 -07:00
Yury Semikhatsky 9e091e74bd
cherry-pick(#30611): chore: add common env vars for junit and json re… (#30624)
…porters
2024-05-01 11:22:08 -07:00
60 changed files with 1172 additions and 755 deletions

View file

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

View file

@ -232,9 +232,9 @@ The opposite of [`method: LocatorAssertions.toHaveAccessibleDescription`].
### param: LocatorAssertions.NotToHaveAccessibleDescription.name ### param: LocatorAssertions.NotToHaveAccessibleDescription.name
* since: v1.44 * since: v1.44
- `name` <[string]|[RegExp]> - `description` <[string]|[RegExp]>
Expected accessible name. Expected accessible description.
### option: LocatorAssertions.NotToHaveAccessibleDescription.ignoreCase = %%-assertions-ignore-case-%% ### option: LocatorAssertions.NotToHaveAccessibleDescription.ignoreCase = %%-assertions-ignore-case-%%
* since: v1.44 * since: v1.44
@ -380,11 +380,11 @@ Property value.
The opposite of [`method: LocatorAssertions.toHaveRole`]. The opposite of [`method: LocatorAssertions.toHaveRole`].
### param: LocatorAssertions.NotToHaveRole.name ### param: LocatorAssertions.NotToHaveRole.role = %%-get-by-role-to-have-role-role-%%
* since: v1.44 * since: v1.44
- `name` <[string]|[RegExp]>
Expected accessible name. ### option: LocatorAssertions.NotToHaveRole.timeout = %%-js-assertions-timeout-%%
* since: v1.44
### option: LocatorAssertions.NotToHaveRole.timeout = %%-csharp-java-python-assertions-timeout-%% ### option: LocatorAssertions.NotToHaveRole.timeout = %%-csharp-java-python-assertions-timeout-%%
* since: v1.44 * since: v1.44

View file

@ -118,7 +118,6 @@ Expected URL string or RegExp.
### option: PageAssertions.NotToHaveURL.ignoreCase ### option: PageAssertions.NotToHaveURL.ignoreCase
* since: v1.44 * since: v1.44
* langs: js
- `ignoreCase` <[boolean]> - `ignoreCase` <[boolean]>
Whether to perform case-insensitive match. [`option: ignoreCase`] option takes precedence over the corresponding regular expression flag if specified. Whether to perform case-insensitive match. [`option: ignoreCase`] option takes precedence over the corresponding regular expression flag if specified.

View file

@ -148,7 +148,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Setup dotnet - name: Setup dotnet
uses: actions/setup-dotnet@v3 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: 8.0.x dotnet-version: 8.0.x
- run: dotnet build - run: dotnet build
@ -266,7 +266,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Setup dotnet - name: Setup dotnet
uses: actions/setup-dotnet@v3 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: 8.0.x dotnet-version: 8.0.x
- run: dotnet build - run: dotnet build
@ -370,7 +370,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Setup dotnet - name: Setup dotnet
uses: actions/setup-dotnet@v3 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: 8.0.x dotnet-version: 8.0.x
- run: dotnet build - run: dotnet build
@ -388,23 +388,49 @@ jobs:
Once you have your [GitHub actions workflow](#setting-up-github-actions) setup then all you need to do is [Create a repo on GitHub](https://docs.github.com/en/get-started/quickstart/create-a-repo) or push your code to an existing repository. Follow the instructions on GitHub and don't forget to [initialize a git repository](https://github.com/git-guides/git-init) using the `git init` command so you can [add](https://github.com/git-guides/git-add), [commit](https://github.com/git-guides/git-commit) and [push](https://github.com/git-guides/git-push) your code. Once you have your [GitHub actions workflow](#setting-up-github-actions) setup then all you need to do is [Create a repo on GitHub](https://docs.github.com/en/get-started/quickstart/create-a-repo) or push your code to an existing repository. Follow the instructions on GitHub and don't forget to [initialize a git repository](https://github.com/git-guides/git-init) using the `git init` command so you can [add](https://github.com/git-guides/git-add), [commit](https://github.com/git-guides/git-commit) and [push](https://github.com/git-guides/git-push) your code.
######
* langs: js, java, python
<img width="861" alt="Create a Repo and Push to GitHub" src="https://user-images.githubusercontent.com/13063165/183423254-d2735278-a2ab-4d63-bb99-48d8e5e447bc.png"/> <img width="861" alt="Create a Repo and Push to GitHub" src="https://user-images.githubusercontent.com/13063165/183423254-d2735278-a2ab-4d63-bb99-48d8e5e447bc.png"/>
######
* langs: csharp
![dotnet repo on github](https://github.com/microsoft/playwright/assets/13063165/4f1b4cc3-b850-4d60-a99e-24057eaf91ad)
## Opening the Workflows ## Opening the Workflows
Click on the **Actions** tab to see the workflows. Here you will see if your tests have passed or failed. Click on the **Actions** tab to see the workflows. Here you will see if your tests have passed or failed.
<img width="847" alt="Opening the Workflows" src="https://user-images.githubusercontent.com/13063165/183423584-2ea18038-cd49-4daa-a20c-2205352f0933.png"/> ######
* langs: js, python, java
![opening the workflow](https://user-images.githubusercontent.com/13063165/183423783-58bf2008-514e-4f96-9c12-c9a55703960c.png)
######
* langs: csharp
![opening the workflow](https://github.com/microsoft/playwright/assets/13063165/71793c09-0815-4faa-866b-85684a1f87e5)
On Pull Requests you can also click on the **Details** link in the [PR status check](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/about-status-checks). On Pull Requests you can also click on the **Details** link in the [PR status check](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/about-status-checks).
<img width="645" alt="pr status checked" src="https://user-images.githubusercontent.com/13063165/183722462-17a985db-0e10-4205-b16c-8aaac36117b9.png" /> <img width="645" alt="pr status checked" src="https://user-images.githubusercontent.com/13063165/183722462-17a985db-0e10-4205-b16c-8aaac36117b9.png" />
## Viewing Test Logs ## Viewing Test Logs
Clicking on the workflow run will show you the all the actions that GitHub performed and clicking on **Run Playwright tests** will show the error messages, what was expected and what was received as well as the call log. Clicking on the workflow run will show you the all the actions that GitHub performed and clicking on **Run Playwright tests** will show the error messages, what was expected and what was received as well as the call log.
<img width="839" alt="Viewing Test Logs" src="https://user-images.githubusercontent.com/13063165/183423783-58bf2008-514e-4f96-9c12-c9a55703960c.png"/> ######
* langs: js, python, java
![Viewing Test Logs](https://user-images.githubusercontent.com/13063165/183423783-58bf2008-514e-4f96-9c12-c9a55703960c.png)
######
* langs: csharp
![viewing the test logs](https://github.com/microsoft/playwright/assets/13063165/ba2d8d7b-ffce-42de-95e0-bcb35c421975)
## HTML Report ## HTML Report
@ -441,12 +467,22 @@ Once you have served the report using `npx playwright show-report`, click on the
![playwright trace viewer](https://github.com/microsoft/playwright/assets/13063165/10fe3585-8401-4051-b1c2-b2e92ac4c274) ![playwright trace viewer](https://github.com/microsoft/playwright/assets/13063165/10fe3585-8401-4051-b1c2-b2e92ac4c274)
## Viewing the Trace ## Viewing the Trace
* langs: python, java, csharp * langs: python, java
[trace.playwright.dev](https://trace.playwright.dev) is a statically hosted variant of the Trace Viewer. You can upload trace files using drag and drop. [trace.playwright.dev](https://trace.playwright.dev) is a statically hosted variant of the Trace Viewer. You can upload trace files using drag and drop.
![playwright trace viewer](https://github.com/microsoft/playwright/assets/13063165/6d5885dc-d511-4c20-b728-040a7ef6cea4) ![playwright trace viewer](https://github.com/microsoft/playwright/assets/13063165/6d5885dc-d511-4c20-b728-040a7ef6cea4)
## Viewing the Trace
* langs: csharp
You can upload Traces which get created on your CI like GitHub Actions as artifacts. This requires [starting and stopping the trace](./trace-viewer-intro#recording-a-trace). We recommend only recording traces for failing tests. Once your traces have been uploaded to CI, they can then be downloaded and opened using [trace.playwright.dev](https://trace.playwright.dev), which is a statically hosted variant of the Trace Viewer. You can upload trace files using drag and drop.
######
* langs: csharp
![playwright trace viewer](https://github.com/microsoft/playwright/assets/13063165/84150084-5019-470a-8449-b61d206bfbb0)
## Publishing report on the web ## Publishing report on the web
* langs: js * langs: js

View file

@ -29,7 +29,7 @@ playwright codegen demo.playwright.dev/todomvc
``` ```
```bash csharp ```bash csharp
pwsh bin/Debug/netX/playwright.ps1 codegen demo.playwright.dev/todomvc pwsh bin/Debug/net8.0/playwright.ps1 codegen demo.playwright.dev/todomvc
``` ```
### Recording a test ### Recording a test

View file

@ -69,13 +69,13 @@ dotnet add package Microsoft.Playwright.MSTest
dotnet build dotnet build
``` ```
4. Install required browsers by replacing `netX` with the actual output folder name, e.g. `net8.0`: 1. Install required browsers. This example uses `net8.0`, if you are using a different version of .NET you will need to adjust the command and change `net8.0` to your version.
```bash ```bash
pwsh bin/Debug/netX/playwright.ps1 install pwsh bin/Debug/net8.0/playwright.ps1 install
``` ```
If `pwsh` is not available, you have to [install PowerShell](https://docs.microsoft.com/powershell/scripting/install/installing-powershell). If `pwsh` is not available, you will have to [install PowerShell](https://docs.microsoft.com/powershell/scripting/install/installing-powershell).
## Add Example Tests ## Add Example Tests
@ -102,27 +102,27 @@ namespace PlaywrightTests;
[Parallelizable(ParallelScope.Self)] [Parallelizable(ParallelScope.Self)]
[TestFixture] [TestFixture]
public class Tests : PageTest public class ExampleTest : PageTest
{ {
[Test] [Test]
public async Task HomepageHasPlaywrightInTitleAndGetStartedLinkLinkingtoTheIntroPage() public async Task HasTitle()
{ {
await Page.GotoAsync("https://playwright.dev"); await Page.GotoAsync("https://playwright.dev");
// Expect a title "to contain" a substring. // Expect a title "to contain" a substring.
await Expect(Page).ToHaveTitleAsync(new Regex("Playwright")); await Expect(Page).ToHaveTitleAsync(new Regex("Playwright"));
}
// create a locator [Test]
var getStarted = Page.GetByRole(AriaRole.Link, new() { Name = "Get started" }); public async Task GetStartedLink()
{
// Expect an attribute "to be strictly equal" to the value. await Page.GotoAsync("https://playwright.dev");
await Expect(getStarted).ToHaveAttributeAsync("href", "/docs/intro");
// Click the get started link. // Click the get started link.
await getStarted.ClickAsync(); await Page.GetByRole(AriaRole.Link, new() { Name = "Get started" }).ClickAsync();
// Expects the URL to contain intro. // Expects page to have a heading with the name of Installation.
await Expect(Page).ToHaveURLAsync(new Regex(".*intro")); await Expect(Page.GetByRole(AriaRole.Heading, new() { Name = "Installation" })).ToBeVisibleAsync();
} }
} }
``` ```
@ -140,27 +140,27 @@ using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace PlaywrightTests; namespace PlaywrightTests;
[TestClass] [TestClass]
public class UnitTest1 : PageTest public class ExampleTest : PageTest
{ {
[TestMethod] [TestMethod]
public async Task HomepageHasPlaywrightInTitleAndGetStartedLinkLinkingtoTheIntroPage() public async Task HasTitle()
{ {
await Page.GotoAsync("https://playwright.dev"); await Page.GotoAsync("https://playwright.dev");
// Expect a title "to contain" a substring. // Expect a title "to contain" a substring.
await Expect(Page).ToHaveTitleAsync(new Regex("Playwright")); await Expect(Page).ToHaveTitleAsync(new Regex("Playwright"));
}
// create a locator [TestMethod]
var getStarted = Page.GetByRole(AriaRole.Link, new() { Name = "Get started" }); public async Task GetStartedLink()
{
// Expect an attribute "to be strictly equal" to the value. await Page.GotoAsync("https://playwright.dev");
await Expect(getStarted).ToHaveAttributeAsync("href", "/docs/intro");
// Click the get started link. // Click the get started link.
await getStarted.ClickAsync(); await Page.GetByRole(AriaRole.Link, new() { Name = "Get started" }).ClickAsync();
// Expects the URL to contain intro. // Expects page to have a heading with the name of Installation.
await Expect(Page).ToHaveURLAsync(new Regex(".*intro")); await Expect(Page.GetByRole(AriaRole.Heading, new() { Name = "Installation" })).ToBeVisibleAsync();
} }
} }
``` ```
@ -170,33 +170,13 @@ public class UnitTest1 : PageTest
## Running the Example Tests ## Running the Example Tests
By default tests will be run on Chromium. This can be configured via the `BROWSER` environment variable, or by adjusting the [launch configuration options](./test-runners.md). Tests are run in headless mode meaning no browser will open up when running the tests. Results of the tests and test logs will be shown in the terminal. By default tests will be run on Chromium. This can be configured via the `BROWSER` environment variable, or by adjusting the [launch configuration options](./running-tests.md). Tests are run in headless mode meaning no browser will open up when running the tests. Results of the tests and test logs will be shown in the terminal.
<Tabs
groupId="test-runners"
defaultValue="nunit"
values={[
{label: 'NUnit', value: 'nunit'},
{label: 'MSTest', value: 'mstest'}
]
}>
<TabItem value="nunit">
```bash ```bash
dotnet test -- NUnit.NumberOfTestWorkers=5 dotnet test
``` ```
</TabItem> See our doc on [Running and Debugging Tests](./running-tests.md) to learn more about running tests in headed mode, running multiple tests, running specific configurations etc.
<TabItem value="mstest">
```bash
dotnet test -- MSTest.Parallelize.Workers=5
```
</TabItem>
</Tabs>
See our doc on [Test Runners](./test-runners.md) to learn more about running tests in headed mode, running multiple tests, running specific configurations etc.
## System requirements ## System requirements
@ -209,7 +189,7 @@ See our doc on [Test Runners](./test-runners.md) to learn more about running tes
- [Write tests using web first assertions, page fixtures and locators](./writing-tests.md) - [Write tests using web first assertions, page fixtures and locators](./writing-tests.md)
- [Run single test, multiple tests, headed mode](./running-tests.md) - [Run single test, multiple tests, headed mode](./running-tests.md)
- [Learn more about the NUnit and MSTest base classes](./test-runners.md) - [Generate tests with Codegen](./codegen-intro.md)
- [Generate tests with Codegen](./codegen.md)
- [See a trace of your tests](./trace-viewer-intro.md) - [See a trace of your tests](./trace-viewer-intro.md)
- [Using Playwright as library](./library.md) - [Run tests on CI](./ci-intro.md)
- [Learn more about the NUnit and MSTest base classes](./test-runners.md)

View file

@ -4,6 +4,82 @@ title: "Release notes"
toc_max_heading_level: 2 toc_max_heading_level: 2
--- ---
## Version 1.44
### New APIs
**Accessibility assertions**
- [`method: LocatorAssertions.toHaveAccessibleName`] checks if the element has the specified accessible name:
```csharp
var locator = Page.GetByRole(AriaRole.Button);
await Expect(locator).ToHaveAccessibleNameAsync("Submit");
```
- [`method: LocatorAssertions.toHaveAccessibleDescription`] checks if the element has the specified accessible description:
```csharp
var locator = Page.GetByRole(AriaRole.Button);
await Expect(locator).ToHaveAccessibleDescriptionAsync("Upload a photo");
```
- [`method: LocatorAssertions.toHaveRole`] checks if the element has the specified ARIA role:
```csharp
var locator = Page.GetByTestId("save-button");
await Expect(locator).ToHaveRoleAsync(AriaRole.Button);
```
**Locator handler**
- After executing the handler added with [`method: Page.addLocatorHandler`], Playwright will now wait until the overlay that triggered the handler is not visible anymore. You can opt-out of this behavior with the new `NoWaitAfter` option.
- You can use new `Times` option in [`method: Page.addLocatorHandler`] to specify maximum number of times the handler should be run.
- The handler in [`method: Page.addLocatorHandler`] now accepts the locator as argument.
- New [`method: Page.removeLocatorHandler`] method for removing previously added locator handlers.
```csharp
var locator = Page.GetByText("This interstitial covers the button");
await Page.AddLocatorHandlerAsync(locator, async (overlay) =>
{
await overlay.Locator("#close").ClickAsync();
}, new() { Times = 3, NoWaitAfter = true });
// Run your tests that can be interrupted by the overlay.
// ...
await Page.RemoveLocatorHandlerAsync(locator);
```
**Miscellaneous options**
- New method [`method: FormData.append`] allows to specify repeating fields with the same name in [`Multipart`](./api/class-apirequestcontext#api-request-context-fetch-option-multipart) option in `APIRequestContext.FetchAsync()`:
- ```
```csharp
var formData = Context.APIRequest.CreateFormData();
formData.Append("file", new FilePayload()
{
Name = "f1.js",
MimeType = "text/javascript",
Buffer = System.Text.Encoding.UTF8.GetBytes("var x = 2024;")
});
formData.Append("file", new FilePayload()
{
Name = "f2.txt",
MimeType = "text/plain",
Buffer = System.Text.Encoding.UTF8.GetBytes("hello")
});
var response = await Context.APIRequest.PostAsync("https://example.com/uploadFiles", new() { Multipart = formData });
```
- [`method: PageAssertions.toHaveURL`] now supports `IgnoreCase` [option](./api/class-pageassertions#page-assertions-to-have-url-option-ignore-case).
### Browser Versions
* Chromium 125.0.6422.14
* Mozilla Firefox 125.0.1
* WebKit 17.4
This version was also tested against the following stable channels:
* Google Chrome 124
* Microsoft Edge 124
## Version 1.43 ## Version 1.43
### New APIs ### New APIs

View file

@ -4,6 +4,72 @@ title: "Release notes"
toc_max_heading_level: 2 toc_max_heading_level: 2
--- ---
## Version 1.44
### New APIs
**Accessibility assertions**
- [`method: LocatorAssertions.toHaveAccessibleName`] checks if the element has the specified accessible name:
```java
Locator locator = page.getByRole(AriaRole.BUTTON);
assertThat(locator).hasAccessibleName("Submit");
```
- [`method: LocatorAssertions.toHaveAccessibleDescription`] checks if the element has the specified accessible description:
```java
Locator locator = page.getByRole(AriaRole.BUTTON);
assertThat(locator).hasAccessibleDescription("Upload a photo");
```
- [`method: LocatorAssertions.toHaveRole`] checks if the element has the specified ARIA role:
```java
Locator locator = page.getByTestId("save-button");
assertThat(locator).hasRole(AriaRole.BUTTON);
```
**Locator handler**
- After executing the handler added with [`method: Page.addLocatorHandler`], Playwright will now wait until the overlay that triggered the handler is not visible anymore. You can opt-out of this behavior with the new `setNoWaitAfter` option.
- You can use new `setTimes` option in [`method: Page.addLocatorHandler`] to specify maximum number of times the handler should be run.
- The handler in [`method: Page.addLocatorHandler`] now accepts the locator as argument.
- New [`method: Page.removeLocatorHandler`] method for removing previously added locator handlers.
```java
Locator locator = page.getByText("This interstitial covers the button");
page.addLocatorHandler(locator, overlay -> {
overlay.locator("#close").click();
}, new Page.AddLocatorHandlerOptions().setTimes(3).setNoWaitAfter(true));
// Run your tests that can be interrupted by the overlay.
// ...
page.removeLocatorHandler(locator);
```
**Miscellaneous options**
- New method [`method: FormData.append`] allows to specify repeating fields with the same name in [`setMultipart`](./api/class-requestoptions#request-options-set-multipart) option in `RequestOptions`:
```java
FormData formData = FormData.create();
formData.append("file", new FilePayload("f1.js", "text/javascript",
"var x = 2024;".getBytes(StandardCharsets.UTF_8)));
formData.append("file", new FilePayload("f2.txt", "text/plain",
"hello".getBytes(StandardCharsets.UTF_8)));
APIResponse response = context.request().post("https://example.com/uploadFile", RequestOptions.create().setMultipart(formData));
```
- `expect(page).toHaveURL(url)` now supports `setIgnoreCase` [option](./api/class-pageassertions#page-assertions-to-have-url-option-ignore-case).
### Browser Versions
* Chromium 125.0.6422.14
* Mozilla Firefox 125.0.1
* WebKit 17.4
This version was also tested against the following stable channels:
* Google Chrome 124
* Microsoft Edge 124
## Version 1.43 ## Version 1.43
### New APIs ### New APIs

View file

@ -8,6 +8,11 @@ import LiteYouTube from '@site/src/components/LiteYouTube';
## Version 1.44 ## Version 1.44
<LiteYouTube
id="avjSahFWdCI"
title="Playwright 1.44"
/>
### New APIs ### New APIs
**Accessibility assertions** **Accessibility assertions**
@ -21,7 +26,7 @@ import LiteYouTube from '@site/src/components/LiteYouTube';
- [`method: LocatorAssertions.toHaveAccessibleDescription`] checks if the element has the specified accessible description: - [`method: LocatorAssertions.toHaveAccessibleDescription`] checks if the element has the specified accessible description:
```js ```js
const locator = page.getByRole('button'); const locator = page.getByRole('button');
await expect(locator).toHaveAccessibleName('Upload the photo'); await expect(locator).toHaveAccessibleDescription('Upload a photo');
``` ```
- [`method: LocatorAssertions.toHaveRole`] checks if the element has the specified ARIA role: - [`method: LocatorAssertions.toHaveRole`] checks if the element has the specified ARIA role:
@ -59,7 +64,7 @@ await page.removeLocatorHandler(locator);
}); });
``` ```
- `expect(callback).toPass({ intervals })` can now be configured by `expect.toPass.inervals` option globally in [`property: TestConfig.expect`] or per project in [`property: TestProject.expect`]. - `expect(callback).toPass({ intervals })` can now be configured by `expect.toPass.intervals` option globally in [`property: TestConfig.expect`] or per project in [`property: TestProject.expect`].
- `expect(page).toHaveURL(url)` now supports `ignoreCase` [option](./api/class-pageassertions#page-assertions-to-have-url-option-ignore-case). - `expect(page).toHaveURL(url)` now supports `ignoreCase` [option](./api/class-pageassertions#page-assertions-to-have-url-option-ignore-case).
- [`property: TestProject.ignoreSnapshots`](./api/class-testproject#test-project-ignore-snapshots) allows to configure per project whether to skip screenshot expectations. - [`property: TestProject.ignoreSnapshots`](./api/class-testproject#test-project-ignore-snapshots) allows to configure per project whether to skip screenshot expectations.

View file

@ -4,6 +4,60 @@ title: "Release notes"
toc_max_heading_level: 2 toc_max_heading_level: 2
--- ---
## Version 1.44
### New APIs
**Accessibility assertions**
- [`method: LocatorAssertions.toHaveAccessibleName`] checks if the element has the specified accessible name:
```python
locator = page.get_by_role("button")
expect(locator).to_have_accessible_name("Submit")
```
- [`method: LocatorAssertions.toHaveAccessibleDescription`] checks if the element has the specified accessible description:
```python
locator = page.get_by_role("button")
expect(locator).to_have_accessible_description("Upload a photo")
```
- [`method: LocatorAssertions.toHaveRole`] checks if the element has the specified ARIA role:
```python
locator = page.get_by_test_id("save-button")
expect(locator).to_have_role("button")
```
**Locator handler**
- After executing the handler added with [`method: Page.addLocatorHandler`], Playwright will now wait until the overlay that triggered the handler is not visible anymore. You can opt-out of this behavior with the new `no_wait_after` option.
- You can use new `times` option in [`method: Page.addLocatorHandler`] to specify maximum number of times the handler should be run.
- The handler in [`method: Page.addLocatorHandler`] now accepts the locator as argument.
- New [`method: Page.removeLocatorHandler`] method for removing previously added locator handlers.
```python
locator = page.get_by_text("This interstitial covers the button")
page.add_locator_handler(locator, lambda overlay: overlay.locator("#close").click(), times=3, no_wait_after=True)
# Run your tests that can be interrupted by the overlay.
# ...
page.remove_locator_handler(locator)
```
**Miscellaneous options**
- [`method: PageAssertions.toHaveURL`] now supports `ignore_case` [option](./api/class-pageassertions#page-assertions-to-have-url-option-ignore-case).
### Browser Versions
* Chromium 125.0.6422.14
* Mozilla Firefox 125.0.1
* WebKit 17.4
This version was also tested against the following stable channels:
* Google Chrome 124
* Microsoft Edge 124
## Version 1.43 ## Version 1.43
### New APIs ### New APIs

View file

@ -7,57 +7,130 @@ title: "Running and debugging tests"
You can run a single test, a set of tests or all tests. Tests can be run on different browsers. By default, tests are run in a headless manner, meaning no browser window will be opened while running the tests and results will be seen in the terminal. If you prefer, you can run your tests in headed mode by using the `headless` test run parameter. You can run a single test, a set of tests or all tests. Tests can be run on different browsers. By default, tests are run in a headless manner, meaning no browser window will be opened while running the tests and results will be seen in the terminal. If you prefer, you can run your tests in headed mode by using the `headless` test run parameter.
- Running all tests **You will learn**
```bash - [How to run tests](#running-tests)
dotnet test - [How to debug tests](#debugging-tests)
```
- Running a single test file ## Running tests
```bash ### Run all tests
dotnet test --filter "MyClassName"
```
- Run a set of test files Use the following command to run all tests.
```bash ```bash
dotnet test --filter "MyClassName1|MyClassName2" dotnet test
``` ```
- Run the test with the title ### Run your tests in headed mode
```bash Use the following command to run your tests in headed mode opening a browser window for each test.
dotnet test --filter "Name~TestMethod1"
```
- Running Tests on specific browsers ```bash tab=bash-bash lang=csharp
HEADED=1 dotnet test
```
```bash ```batch tab=bash-batch lang=csharp
dotnet test -- Playwright.BrowserName=webkit set HEADED=1
``` dotnet test
```
- Running Tests on multiple browsers ```powershell tab=bash-powershell lang=csharp
$env:HEADED="1"
dotnet test
```
To run your test on multiple browsers or configurations, you need to invoke the `dotnet test` command multiple times. There you can then either specify the `BROWSER` environment variable or set the `Playwright.BrowserName` via the runsettings file: ### Run tests on different browsers: Browser env
```bash Specify which browser you would like to run your tests on via the `BROWSER` environment variable.
dotnet test --settings:chromium.runsettings
dotnet test --settings:firefox.runsettings
dotnet test --settings:webkit.runsettings
```
```xml ```bash tab=bash-bash lang=csharp
<?xml version="1.0" encoding="utf-8"?> BROWSER=webkit dotnet test
```
```batch tab=bash-batch lang=csharp
set BROWSER=webkit
dotnet test
```
```powershell tab=bash-powershell lang=csharp
$env:BROWSER="webkit"
dotnet test
```
### Run tests on different browsers: launch configuration
Specify which browser you would like to run your tests on by adjusting the launch configuration options:
```bash
dotnet test -- Playwright.BrowserName=webkit
```
To run your test on multiple browsers or configurations, you need to invoke the `dotnet test` command multiple times. There you can then either specify the `BROWSER` environment variable or set the `Playwright.BrowserName` via the runsettings file:
```bash
dotnet test --settings:chromium.runsettings
dotnet test --settings:firefox.runsettings
dotnet test --settings:webkit.runsettings
```
```xml
<?xml version="1.0" encoding="utf-8"?>
<RunSettings> <RunSettings>
<Playwright> <Playwright>
<BrowserName>chromium</BrowserName> <BrowserName>chromium</BrowserName>
</Playwright> </Playwright>
</RunSettings> </RunSettings>
``` ```
For more information see [selective unit tests](https://docs.microsoft.com/en-us/dotnet/core/testing/selective-unit-tests?pivots=mstest) in the Microsoft docs. For more information see [selective unit tests](https://docs.microsoft.com/en-us/dotnet/core/testing/selective-unit-tests?pivots=mstest) in the Microsoft docs.
### Run specific tests
To run a single test file, use the filter flag followed by the class name of the test you want to run.
```bash
dotnet test --filter "ExampleTest"
```
To run a set of test files, use the filter flag followed by the class names of the tests you want to run.
```bash
dotnet test --filter "ExampleTest1|ExampleTest2"
```
To run a test with a specific title use the filter flag followed by *Name~* and the title of the test.
```bash
dotnet test --filter "Name~GetStartedLink"
```
### Run tests with multiple workers:
<Tabs
groupId="test-runners"
defaultValue="nunit"
values={[
{label: 'NUnit', value: 'nunit'},
{label: 'MSTest', value: 'mstest'}
]
}>
<TabItem value="nunit">
```bash
dotnet test -- NUnit.NumberOfTestWorkers=5
```
</TabItem>
<TabItem value="mstest">
```bash
dotnet test -- MSTest.Parallelize.Workers=5
```
</TabItem>
</Tabs>
## Debugging Tests ## Debugging Tests
Since Playwright runs in .NET, you can debug it with your debugger of choice in e.g. Visual Studio Code or Visual Studio. Playwright comes with the Playwright Inspector which allows you to step through Playwright API calls, see their debug logs and explore [locators](./locators.md). Since Playwright runs in .NET, you can debug it with your debugger of choice in e.g. Visual Studio Code or Visual Studio. Playwright comes with the Playwright Inspector which allows you to step through Playwright API calls, see their debug logs and explore [locators](./locators.md).
@ -76,12 +149,14 @@ $env:PWDEBUG=1
dotnet test dotnet test
``` ```
<img width="712" alt="Playwright Inspector" src="https://user-images.githubusercontent.com/883973/108614092-8c478a80-73ac-11eb-9597-67dfce110e00.png"></img> ![debugging tests with playwright inspector](https://github.com/microsoft/playwright/assets/13063165/a1e758d3-d379-414f-be0b-7339f12bb635)
Check out our [debugging guide](./debug.md) to learn more about the [Playwright Inspector](./debug.md#playwright-inspector) as well as debugging with [Browser Developer tools](./debug.md#browser-developer-tools). Check out our [debugging guide](./debug.md) to learn more about the [Playwright Inspector](./debug.md#playwright-inspector) as well as debugging with [Browser Developer tools](./debug.md#browser-developer-tools).
## What's Next ## What's Next
- [Generate tests with Codegen](./codegen.md) - [Generate tests with Codegen](./codegen-intro.md)
- [See a trace of your tests](./trace-viewer-intro.md) - [See a trace of your tests](./trace-viewer-intro.md)
- [Run tests on CI](./ci-intro.md)
- [Learn more about the NUnit and MSTest base classes](./test-runners.md)

View file

@ -231,7 +231,7 @@ Blob report supports following configuration options and environment variables:
|---|---|---|---| |---|---|---|---|
| `PLAYWRIGHT_BLOB_OUTPUT_DIR` | `outputDir` | Directory to save the output. Existing content is deleted before writing the new report. | `blob-report` | `PLAYWRIGHT_BLOB_OUTPUT_DIR` | `outputDir` | Directory to save the output. Existing content is deleted before writing the new report. | `blob-report`
| `PLAYWRIGHT_BLOB_OUTPUT_NAME` | `fileName` | Report file name. | `report-<project>-<hash>-<shard_number>.zip` | `PLAYWRIGHT_BLOB_OUTPUT_NAME` | `fileName` | Report file name. | `report-<project>-<hash>-<shard_number>.zip`
| `PLAYWRIGHT_BLOB_OUTPUT_FILE` | `outputFile` | Full path for the output. If defined, `outputDir` and `fileName` will be ignored. | `undefined` | `PLAYWRIGHT_BLOB_OUTPUT_FILE` | `outputFile` | Full path to the output file. If defined, `outputDir` and `fileName` will be ignored. | `undefined`
### JSON reporter ### JSON reporter
@ -267,7 +267,9 @@ JSON report supports following configuration options and environment variables:
| Environment Variable Name | Reporter Config Option| Description | Default | Environment Variable Name | Reporter Config Option| Description | Default
|---|---|---|---| |---|---|---|---|
| `PLAYWRIGHT_JUNIT_OUTPUT_NAME` | `outputFile` | Report file path. | JSON report is printed to stdout. | `PLAYWRIGHT_JSON_OUTPUT_DIR` | | Directory to save the output file. Ignored if output file is specified. | `cwd` or config directory.
| `PLAYWRIGHT_JSON_OUTPUT_NAME` | `outputFile` | Base file name for the output, relative to the output dir. | JSON report is printed to the stdout.
| `PLAYWRIGHT_JSON_OUTPUT_FILE` | `outputFile` | Full path to the output file. If defined, `PLAYWRIGHT_JSON_OUTPUT_DIR` and `PLAYWRIGHT_JSON_OUTPUT_NAME` will be ignored. | JSON report is printed to the stdout.
### JUnit reporter ### JUnit reporter
@ -303,7 +305,9 @@ JUnit report supports following configuration options and environment variables:
| Environment Variable Name | Reporter Config Option| Description | Default | Environment Variable Name | Reporter Config Option| Description | Default
|---|---|---|---| |---|---|---|---|
| `PLAYWRIGHT_JUNIT_OUTPUT_NAME` | `outputFile` | Report file path. | JUnit report is printed to stdout. | `PLAYWRIGHT_JUNIT_OUTPUT_DIR` | | Directory to save the output file. Ignored if output file is not specified. | `cwd` or config directory.
| `PLAYWRIGHT_JUNIT_OUTPUT_NAME` | `outputFile` | Base file name for the output, relative to the output dir. | JUnit report is printed to the stdout.
| `PLAYWRIGHT_JUNIT_OUTPUT_FILE` | `outputFile` | Full path to the output file. If defined, `PLAYWRIGHT_JUNIT_OUTPUT_DIR` and `PLAYWRIGHT_JUNIT_OUTPUT_NAME` will be ignored. | JUnit report is printed to the stdout.
| | `stripANSIControlSequences` | Whether to remove ANSI control sequences from the text before writing it in the report. | By default output text is added as is. | | `stripANSIControlSequences` | Whether to remove ANSI control sequences from the text before writing it in the report. | By default output text is added as is.
| | `includeProjectInTestName` | Whether to include Playwright project name in every test case as a name prefix. | By default not included. | | `includeProjectInTestName` | Whether to include Playwright project name in every test case as a name prefix. | By default not included.
| `PLAYWRIGHT_JUNIT_SUITE_ID` | | Value of the `id` attribute on the root `<testsuites/>` report entry. | Empty string. | `PLAYWRIGHT_JUNIT_SUITE_ID` | | Value of the `id` attribute on the root `<testsuites/>` report entry. | Empty string.

View file

@ -5,107 +5,18 @@ title: "Test Runners"
## Introduction ## Introduction
While Playwright for .NET isn't tied to a particular test runner or testing framework, in our experience While Playwright for .NET isn't tied to a particular test runner or testing framework, in our experience it works best with the built-in .NET test runner, and using NUnit as the test framework. NUnit is also what we use internally for [our tests](https://github.com/microsoft/playwright-dotnet/tree/main/src/Playwright.Tests).
it works best with the built-in .NET test runner, and using NUnit as the test framework. NUnit is
also what we use internally for [our tests](https://github.com/microsoft/playwright-dotnet/tree/main/src/Playwright.Tests).
Playwright and Browser instances can be reused between tests for better performance. We Playwright and Browser instances can be reused between tests for better performance. We
recommend running each test case in a new BrowserContext, this way browser state will be recommend running each test case in a new BrowserContext, this way browser state will be
isolated between the tests. isolated between the tests.
<!-- TOC -->
## NUnit ## NUnit
Playwright provides base classes to write tests with NUnit via the [`Microsoft.Playwright.NUnit`](https://www.nuget.org/packages/Microsoft.Playwright.NUnit) package. Playwright provides base classes to write tests with NUnit via the [`Microsoft.Playwright.NUnit`](https://www.nuget.org/packages/Microsoft.Playwright.NUnit) package.
Check out the [installation guide](./intro.md) to get started.
### Creating an NUnit project
```bash
# Create a new project
dotnet new nunit -n PlaywrightTests
cd PlaywrightTests
# Add the required reference
dotnet add package Microsoft.Playwright.NUnit
dotnet build
# Install the required browsers and operating system dependencies
pwsh bin/Debug/netX/playwright.ps1 install --with-deps
```
Modify the UnitTest1.cs:
```csharp
using Microsoft.Playwright.NUnit;
namespace PlaywrightTests;
[Parallelizable(ParallelScope.Self)]
[TestFixture]
public class MyTest : PageTest
{
[Test]
public async Task ShouldHaveTheCorrectSlogan()
{
await Page.GotoAsync("https://playwright.dev");
await Expect(Page.Locator("text=enables reliable end-to-end testing for modern web apps")).ToBeVisibleAsync();
}
[Test]
public async Task ShouldHaveTheCorrectTitle()
{
await Page.GotoAsync("https://playwright.dev");
var title = Page.Locator(".navbar__inner .navbar__title");
await Expect(title).ToHaveTextAsync("Playwright");
}
}
```
Run your tests against Chromium
```bash
dotnet test
```
Run your tests against WebKit
```bash tab=bash-bash lang=csharp
BROWSER=webkit dotnet test
```
```batch tab=bash-batch lang=csharp
set BROWSER=webkit
dotnet test
```
```powershell tab=bash-powershell lang=csharp
$env:BROWSER="webkit"
dotnet test
```
Run your tests with GUI
```bash tab=bash-bash lang=csharp
HEADED=1 dotnet test
```
```batch tab=bash-batch lang=csharp
set HEADED=1
dotnet test
```
```powershell tab=bash-powershell lang=csharp
$env:HEADED="1"
dotnet test
```
You can also choose specifically which tests to run, using the [filtering capabilities](https://docs.microsoft.com/en-us/dotnet/core/testing/selective-unit-tests?pivots=nunit):
```bash
dotnet test --filter "Name~Slogan"
```
### Running NUnit tests in Parallel ### Running NUnit tests in Parallel
@ -114,6 +25,10 @@ Only `ParallelScope.Self` is supported.
For CPU-bound tests, we recommend using as many workers as there are cores on your system, divided by 2. For IO-bound tests you can use as many workers as you have cores. For CPU-bound tests, we recommend using as many workers as there are cores on your system, divided by 2. For IO-bound tests you can use as many workers as you have cores.
```bash
dotnet test -- NUnit.NumberOfTestWorkers=5
```
### Customizing [BrowserContext] options ### Customizing [BrowserContext] options
To customize context options, you can override the `ContextOptions` method of your test class derived from `Microsoft.Playwright.MSTest.PageTest` or `Microsoft.Playwright.MSTest.ContextTest`. See the following example: To customize context options, you can override the `ContextOptions` method of your test class derived from `Microsoft.Playwright.MSTest.PageTest` or `Microsoft.Playwright.MSTest.ContextTest`. See the following example:
@ -223,91 +138,7 @@ There are a few base classes available to you in `Microsoft.Playwright.NUnit` na
Playwright provides base classes to write tests with MSTest via the [`Microsoft.Playwright.MSTest`](https://www.nuget.org/packages/Microsoft.Playwright.MSTest) package. Playwright provides base classes to write tests with MSTest via the [`Microsoft.Playwright.MSTest`](https://www.nuget.org/packages/Microsoft.Playwright.MSTest) package.
### Creating an MSTest project Check out the [installation guide](./intro.md) to get started.
```bash
# Create a new project
dotnet new mstest -n PlaywrightTests
cd PlaywrightTests
# Add the required reference
dotnet add package Microsoft.Playwright.MSTest
dotnet build
# Install the required browsers and operating system dependencies
pwsh bin/Debug/netX/playwright.ps1 install --with-deps
```
Modify the UnitTest1.cs:
```csharp
using Microsoft.Playwright.MSTest;
namespace PlaywrightTests;
[TestClass]
public class UnitTest1: PageTest
{
[TestMethod]
public async Task ShouldHaveTheCorrectSlogan()
{
await Page.GotoAsync("https://playwright.dev");
await Expect(Page.Locator("text=enables reliable end-to-end testing for modern web apps")).ToBeVisibleAsync();
}
[TestMethod]
public async Task ShouldHaveTheCorrectTitle()
{
await Page.GotoAsync("https://playwright.dev");
var title = Page.Locator(".navbar__inner .navbar__title");
await Expect(title).ToHaveTextAsync("Playwright");
}
}
```
Run your tests against Chromium
```bash
dotnet test
```
Run your tests against WebKit
```bash tab=bash-bash lang=csharp
BROWSER=webkit dotnet test
```
```batch tab=bash-batch lang=csharp
set BROWSER=webkit
dotnet test
```
```powershell tab=bash-powershell lang=csharp
$env:BROWSER="webkit"
dotnet test
```
Run your tests with GUI
```bash tab=bash-bash lang=csharp
HEADED=1 dotnet test
```
```batch tab=bash-batch lang=csharp
set HEADED=1
dotnet test
```
```powershell tab=bash-powershell lang=csharp
$env:HEADED="1"
dotnet test
```
You can also choose specifically which tests to run, using the [filtering capabilities](https://docs.microsoft.com/en-us/dotnet/core/testing/selective-unit-tests?pivots=mstest):
```bash
dotnet test --filter "Name~Slogan"
```
### Running MSTest tests in Parallel ### Running MSTest tests in Parallel
@ -331,7 +162,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace PlaywrightTests; namespace PlaywrightTests;
[TestClass] [TestClass]
public class UnitTest1 : PageTest public class ExampleTest : PageTest
{ {
[TestMethod] [TestMethod]
public async Task TestWithCustomContextOptions() public async Task TestWithCustomContextOptions()

View file

@ -0,0 +1,137 @@
---
id: trace-viewer-intro
title: "Trace viewer"
---
## Introduction
Playwright Trace Viewer is a GUI tool that lets you explore recorded Playwright traces of your tests meaning you can go back and forward though each action of your test and visually see what was happening during each action.
**You will learn**
- How to record a trace
- How to open the trace viewer
## Recording a trace
Traces can be recorded using the [`property: BrowserContext.tracing`] API as follows:
<Tabs
groupId="test-runners"
defaultValue="nunit"
values={[
{label: 'NUnit', value: 'nunit'},
{label: 'MSTest', value: 'mstest'}
]
}>
<TabItem value="nunit">
```csharp
namespace PlaywrightTests;
[Parallelizable(ParallelScope.Self)]
[TestFixture]
public class Tests : PageTest
{
[SetUp]
public async Task Setup()
{
await Context.Tracing.StartAsync(new()
{
Title = $"{TestContext.CurrentContext.Test.ClassName}.{TestContext.CurrentContext.Test.Name}",
Screenshots = true,
Snapshots = true,
Sources = true
});
}
[TearDown]
public async Task TearDown()
{
await Context.Tracing.StopAsync(new()
{
Path = Path.Combine(
TestContext.CurrentContext.WorkDirectory,
"playwright-traces",
$"{TestContext.CurrentContext.Test.ClassName}.{TestContext.CurrentContext.Test.Name}.zip"
)
});
}
[Test]
public async Task GetStartedLink()
{
// ..
}
}
```
</TabItem>
<TabItem value="mstest">
```csharp
using System.Text.RegularExpressions;
using Microsoft.Playwright;
using Microsoft.Playwright.MSTest;
namespace PlaywrightTests;
[TestClass]
public class ExampleTest : PageTest
{
[TestInitialize]
public async Task TestInitialize()
{
await Context.Tracing.StartAsync(new()
{
Title = $"{TestContext.FullyQualifiedTestClassName}.{TestContext.TestName}",
Screenshots = true,
Snapshots = true,
Sources = true
});
}
[TestCleanup]
public async Task TestCleanup()
{
await Context.Tracing.StopAsync(new()
{
Path = Path.Combine(
Environment.CurrentDirectory,
"playwright-traces",
$"{TestContext.FullyQualifiedTestClassName}.{TestContext.TestName}.zip"
)
});
}
[TestMethod]
public async Task GetStartedLink()
{
// ...
}
}
```
</TabItem>
</Tabs>
This will record a zip file for each test, e.g. `PlaywrightTests.ExampleTest.GetStartedLink.zip` and place it into the `bin/Debug/net8.0/playwright-traces/` directory.
## Opening the trace
You can open the saved trace using the Playwright CLI or in your browser on [`trace.playwright.dev`](https://trace.playwright.dev). Make sure to add the full path to where your trace's zip file is located. Once opened you can click on each action or use the timeline to see the state of the page before and after each action. You can also inspect the log, source and network during each step of the test. The trace viewer creates a DOM snapshot so you can fully interact with it, open devtools etc.
```bash csharp
pwsh bin/Debug/net8.0/playwright.ps1 show-trace bin/Debug/net8.0/playwright-traces/PlaywrightTests.ExampleTest.GetStartedLink.zip
```
![playwright trace viewer dotnet](https://github.com/microsoft/playwright/assets/13063165/4372d661-5bfa-4e1f-be65-0d2fe165a75c)
Check out our detailed guide on [Trace Viewer](/trace-viewer.md) to learn more about the trace viewer and how to setup your tests to record a trace only when the test fails.
## What's next
- [Run tests on CI with GitHub Actions](/ci-intro.md)
- [Learn more about the NUnit and MSTest base classes](./test-runners.md)

View file

@ -10,7 +10,6 @@ Playwright Trace Viewer is a GUI tool that lets you explore recorded Playwright
**You will learn** **You will learn**
- How to record a trace - How to record a trace
- How to open the HTML report
- How to open the trace viewer - How to open the trace viewer
## Recording a trace ## Recording a trace
@ -87,119 +86,9 @@ context.tracing().stop(new Tracing.StopOptions()
This will record the trace and place it into the file named `trace.zip`. This will record the trace and place it into the file named `trace.zip`.
## Recording a trace
* langs: csharp
Traces can be recorded using the [`property: BrowserContext.tracing`] API as follows:
<Tabs
groupId="test-runners"
defaultValue="nunit"
values={[
{label: 'NUnit', value: 'nunit'},
{label: 'MSTest', value: 'mstest'}
]
}>
<TabItem value="nunit">
```csharp
namespace PlaywrightTests;
[Parallelizable(ParallelScope.Self)]
[TestFixture]
public class Tests : PageTest
{
[SetUp]
public async Task Setup()
{
await Context.Tracing.StartAsync(new()
{
Title = TestContext.CurrentContext.Test.ClassName + "." + TestContext.CurrentContext.Test.Name,
Screenshots = true,
Snapshots = true,
Sources = true
});
}
[TearDown]
public async Task TearDown()
{
// This will produce e.g.:
// bin/Debug/net8.0/playwright-traces/PlaywrightTests.Tests.Test1.zip
await Context.Tracing.StopAsync(new()
{
Path = Path.Combine(
TestContext.CurrentContext.WorkDirectory,
"playwright-traces",
$"{TestContext.CurrentContext.Test.ClassName}.{TestContext.CurrentContext.Test.Name}.zip"
)
});
}
[Test]
public async Task TestYourOnlineShop()
{
// ..
}
}
```
</TabItem>
<TabItem value="mstest">
```csharp
using System.Text.RegularExpressions;
using Microsoft.Playwright;
using Microsoft.Playwright.MSTest;
namespace PlaywrightTestsMSTest;
[TestClass]
public class UnitTest1 : PageTest
{
[TestInitialize]
public async Task TestInitialize()
{
await Context.Tracing.StartAsync(new()
{
Title = TestContext.TestName,
Screenshots = true,
Snapshots = true,
Sources = true
});
}
[TestCleanup]
public async Task TestCleanup()
{
// This will produce e.g.:
// bin/Debug/net8.0/playwright-traces/PlaywrightTests.UnitTest1.zip
await Context.Tracing.StopAsync(new()
{
Path = Path.Combine(
Environment.CurrentDirectory,
"playwright-traces",
$"{TestContext.FullyQualifiedTestClassName}.zip"
)
});
}
[TestMethod]
public async Task TestYourOnlineShop()
{
// ...
}
}
```
</TabItem>
</Tabs>
This will record the trace and place it into the `bin/Debug/net8.0/playwright-traces/` directory.
## Opening the trace ## Opening the trace
You can open the saved trace using the Playwright CLI or in your browser on [`trace.playwright.dev`](https://trace.playwright.dev). Make sure to add the full path to where your `trace.zip` file is located. This should include the `test-results` directory followed by the test name and then `trace.zip`. You can open the saved trace using the Playwright CLI or in your browser on [`trace.playwright.dev`](https://trace.playwright.dev). Make sure to add the full path to where your trace's zip file is located. Once opened you can click on each action or use the timeline to see the state of the page before and after each action. You can also inspect the log, source and network during each step of the test. The trace viewer creates a DOM snapshot so you can fully interact with it, open devtools etc.
```bash java ```bash java
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="show-trace trace.zip" mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="show-trace trace.zip"
@ -209,15 +98,12 @@ mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="sh
playwright show-trace trace.zip playwright show-trace trace.zip
``` ```
```bash csharp ######
pwsh bin/Debug/netX/playwright.ps1 show-trace trace.zip * langs: python, java
```
## Viewing the trace
View traces of your test by clicking through each action or hovering using the timeline and see the state of the page before and after the action. Inspect the log, source and network during each step of the test. The trace viewer creates a DOM snapshot so you can fully interact with it, open devtools etc.
![playwright trace viewer](https://github.com/microsoft/playwright/assets/13063165/10fe3585-8401-4051-b1c2-b2e92ac4c274) ![playwright trace viewer](https://github.com/microsoft/playwright/assets/13063165/10fe3585-8401-4051-b1c2-b2e92ac4c274)
To learn more check out our detailed guide on [Trace Viewer](/trace-viewer.md). To learn more check out our detailed guide on [Trace Viewer](/trace-viewer.md).
## What's next ## What's next

View file

@ -345,9 +345,118 @@ public class UnitTest1 : PageTest
This will record the trace and place it into the `bin/Debug/net8.0/playwright-traces/` directory. This will record the trace and place it into the `bin/Debug/net8.0/playwright-traces/` directory.
## Run trace only on failure
* langs: csharp
Setup your tests to record a trace only when the test fails:
<Tabs
groupId="test-runners"
defaultValue="nunit"
values={[
{label: 'NUnit', value: 'nunit'},
{label: 'MSTest', value: 'mstest'}
]
}>
<TabItem value="nunit">
```csharp
namespace PlaywrightTests;
[Parallelizable(ParallelScope.Self)]
[TestFixture]
public class ExampleTest : PageTest
{
[SetUp]
public async Task Setup()
{
await Context.Tracing.StartAsync(new()
{
Title = $"{TestContext.CurrentContext.Test.ClassName}.{TestContext.CurrentContext.Test.Name}",
Screenshots = true,
Snapshots = true,
Sources = true
});
}
[TearDown]
public async Task TearDown()
{
var failed = TestContext.CurrentContext.Result.Outcome == NUnit.Framework.Interfaces.ResultState.Error
|| TestContext.CurrentContext.Result.Outcome == NUnit.Framework.Interfaces.ResultState.Failure;
await Context.Tracing.StopAsync(new()
{
Path = failed ? Path.Combine(
TestContext.CurrentContext.WorkDirectory,
"playwright-traces",
$"{TestContext.CurrentContext.Test.ClassName}.{TestContext.CurrentContext.Test.Name}.zip"
) : null,
});
}
[Test]
public async Task GetStartedLink()
{
// ..
}
}
```
</TabItem>
<TabItem value="mstest">
```csharp
using System.Text.RegularExpressions;
using Microsoft.Playwright;
using Microsoft.Playwright.MSTest;
namespace PlaywrightTests;
[TestClass]
public class ExampleTest : PageTest
{
[TestInitialize]
public async Task TestInitialize()
{
await Context.Tracing.StartAsync(new()
{
Title = $"{TestContext.FullyQualifiedTestClassName}.{TestContext.TestName}",
Screenshots = true,
Snapshots = true,
Sources = true
});
}
[TestCleanup]
public async Task TestCleanup()
{
var failed = new[] { UnitTestOutcome.Failed, UnitTestOutcome.Error, UnitTestOutcome.Timeout, UnitTestOutcome.Aborted }.Contains(TestContext.CurrentTestOutcome);
await Context.Tracing.StopAsync(new()
{
Path = failed ? Path.Combine(
Environment.CurrentDirectory,
"playwright-traces",
$"{TestContext.FullyQualifiedTestClassName}.{TestContext.TestName}.zip"
) : null,
});
}
[TestMethod]
public async Task GetStartedLink()
{
// ...
}
}
```
</TabItem>
</Tabs>
## Opening the trace ## Opening the trace
You can open the saved trace using the Playwright CLI or in your browser on [`trace.playwright.dev`](https://trace.playwright.dev). Make sure to add the full path to where your `trace.zip` file is located. This should include the full path to your `trace.zip` file. You can open the saved trace using the Playwright CLI or in your browser on [`trace.playwright.dev`](https://trace.playwright.dev). Make sure to add the full path to where your `trace.zip` file is located.
```bash js ```bash js
npx playwright show-trace path/to/trace.zip npx playwright show-trace path/to/trace.zip
@ -399,4 +508,3 @@ You can also pass the URL of your uploaded trace (e.g. inside your CI) from some
https://trace.playwright.dev/?trace=https://demo.playwright.dev/reports/todomvc/data/cb0fa77ebd9487a5c899f3ae65a7ffdbac681182.zip https://trace.playwright.dev/?trace=https://demo.playwright.dev/reports/todomvc/data/cb0fa77ebd9487a5c899f3ae65a7ffdbac681182.zip
``` ```

View file

@ -5,9 +5,33 @@ title: "Writing tests"
## Introduction ## Introduction
Playwright assertions are created specifically for the dynamic web. Checks are automatically retried until the necessary conditions are met. Playwright comes with [auto-wait](./actionability.md) built in meaning it waits for elements to be actionable prior to performing actions. Playwright provides the [Expect](./test-assertions) function to write assertions. Playwright tests are simple, they
Take a look at the example test below to see how to write a test using using [locators](/locators.md) and web first assertions. - **perform actions**, and
- **assert the state** against expectations.
There is no need to wait for anything prior to performing an action: Playwright
automatically waits for the wide range of [actionability](./actionability.md)
checks to pass prior to performing each action.
There is also no need to deal with the race conditions when performing the checks -
Playwright assertions are designed in a way that they describe the expectations
that need to be eventually met.
That's it! These design choices allow Playwright users to forget about flaky
timeouts and racy checks in their tests altogether.
**You will learn**
- [How to write the first test](/writing-tests.md#first-test)
- [How to perform actions](/writing-tests.md#actions)
- [How to use assertions](/writing-tests.md#assertions)
- [How tests run in isolation](/writing-tests.md#test-isolation)
- [How to use test hooks](/writing-tests.md#using-test-hooks)
## First test
Take a look at the following example to see how to write a test.
<Tabs <Tabs
groupId="test-runners" groupId="test-runners"
@ -30,29 +54,27 @@ namespace PlaywrightTests;
[Parallelizable(ParallelScope.Self)] [Parallelizable(ParallelScope.Self)]
[TestFixture] [TestFixture]
public class Tests : PageTest public class ExampleTest : PageTest
{ {
[Test] [Test]
public async Task HomepageHasPlaywrightInTitleAndGetStartedLinkLinkingtoTheIntroPage() public async Task HasTitle()
{ {
await Page.GotoAsync("https://playwright.dev"); await Page.GotoAsync("https://playwright.dev");
// Expect a title "to contain" a substring. // Expect a title "to contain" a substring.
await Expect(Page).ToHaveTitleAsync(new Regex("Playwright")); await Expect(Page).ToHaveTitleAsync(new Regex("Playwright"));
}
// create a locator [Test]
var getStarted = Page.GetByRole(AriaRole.Link, new() { Name = "Get started" }); public async Task GetStartedLink()
{
// Expect an attribute "to be strictly equal" to the value. await Page.GotoAsync("https://playwright.dev");
await Expect(getStarted).ToHaveAttributeAsync("href", "/docs/intro");
// Click the get started link. // Click the get started link.
await getStarted.ClickAsync(); await Page.GetByRole(AriaRole.Link, new() { Name = "Get started" }).ClickAsync();
// Expects page to have a heading with the name of Installation. // Expects page to have a heading with the name of Installation.
await Expect(Page await Expect(Page.GetByRole(AriaRole.Heading, new() { Name = "Installation" })).ToBeVisibleAsync();
.GetByRole(AriaRole.Heading, new() { Name = "Installation" }))
.ToBeVisibleAsync();
} }
} }
``` ```
@ -70,27 +92,27 @@ using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace PlaywrightTests; namespace PlaywrightTests;
[TestClass] [TestClass]
public class UnitTest1 : PageTest public class ExampleTest : PageTest
{ {
[TestMethod] [TestMethod]
public async Task HomepageHasPlaywrightInTitleAndGetStartedLinkLinkingtoTheIntroPage() public async Task HasTitle()
{ {
await Page.GotoAsync("https://playwright.dev"); await Page.GotoAsync("https://playwright.dev");
// Expect a title "to contain" a substring. // Expect a title "to contain" a substring.
await Expect(Page).ToHaveTitleAsync(new Regex("Playwright")); await Expect(Page).ToHaveTitleAsync(new Regex("Playwright"));
}
// create a locator [TestMethod]
var getStarted = Page.GetByRole(AriaRole.Link, new() { Name = "Get started" }); public async Task GetStartedLink()
{
// Expect an attribute "to be strictly equal" to the value. await Page.GotoAsync("https://playwright.dev");
await Expect(getStarted).ToHaveAttributeAsync("href", "/docs/intro");
// Click the get started link. // Click the get started link.
await getStarted.ClickAsync(); await Page.GetByRole(AriaRole.Link, new() { Name = "Get started" }).ClickAsync();
// Expects the URL to contain intro. // Expects page to have a heading with the name of Installation.
await Expect(Page).ToHaveURLAsync(new Regex(".*intro")); await Expect(Page.GetByRole(AriaRole.Heading, new() { Name = "Installation" })).ToBeVisibleAsync();
} }
} }
``` ```
@ -98,7 +120,57 @@ public class UnitTest1 : PageTest
</TabItem> </TabItem>
</Tabs> </Tabs>
### Assertions ## Actions
### Navigation
Most of the tests will start by navigating the page to a URL. After that, the test
will be able to interact with the page elements.
```csharp
await Page.GotoAsync("https://playwright.dev");
```
Playwright will wait for the page to reach the load state prior to moving forward.
Learn more about the [`method: Page.goto`] options.
### Interactions
Performing actions starts with locating the elements. Playwright uses [Locators API](./locators.md) for that. Locators represent a way to find element(s) on the page at any moment, learn more about the [different types](./locators.md) of locators available. Playwright will wait for the element to be [actionable](./actionability.md) prior to performing the action, so there is no need to wait for it to become available.
```csharp
// Create a locator.
var getStarted = Page.GetByRole(AriaRole.Link, new() { Name = "Get started" });
// Click it.
await getStarted.ClickAsync();
```
In most cases, it'll be written in one line:
```csharp
await Page.GetByRole(AriaRole.Link, new() { Name = "Get started" }).ClickAsync();
```
### Basic actions
This is the list of the most popular Playwright actions. Note that there are many more, so make sure to check the [Locator API](./api/class-locator.md) section to
learn more about them.
| Action | Description |
| :- | :- |
| [`method: Locator.check`] | Check the input checkbox |
| [`method: Locator.click`] | Click the element |
| [`method: Locator.uncheck`] | Uncheck the input checkbox |
| [`method: Locator.hover`] | Hover mouse over the element |
| [`method: Locator.fill`] | Fill the form field, input text |
| [`method: Locator.focus`] | Focus the element |
| [`method: Locator.press`] | Press single key |
| [`method: Locator.setInputFiles`] | Pick files to upload |
| [`method: Locator.selectOption`] | Select option in the drop down |
## Assertions
Playwright provides an async function called [Expect](./test-assertions) to assert and wait until the expected condition is met. Playwright provides an async function called [Expect](./test-assertions) to assert and wait until the expected condition is met.
@ -106,19 +178,23 @@ Playwright provides an async function called [Expect](./test-assertions) to asse
await Expect(Page).ToHaveTitleAsync(new Regex("Playwright")); await Expect(Page).ToHaveTitleAsync(new Regex("Playwright"));
``` ```
Here is the list of the most popular async assertions. Note that there are [many more](./test-assertions.md) to get familiar with:
### Locators | Assertion | Description |
| :- | :- |
| [`method: LocatorAssertions.toBeChecked`] | Checkbox is checked |
| [`method: LocatorAssertions.toBeEnabled`] | Control is enabled |
| [`method: LocatorAssertions.toBeVisible`] | Element is visible |
| [`method: LocatorAssertions.toContainText`] | Element contains text |
| [`method: LocatorAssertions.toHaveAttribute`] | Element has attribute |
| [`method: LocatorAssertions.toHaveCount`] | List of elements has given length |
| [`method: LocatorAssertions.toHaveText`] | Element matches text |
| [`method: LocatorAssertions.toHaveValue`] | Input element has value |
| [`method: PageAssertions.toHaveTitle`] | Page has title |
| [`method: PageAssertions.toHaveURL`] | Page has URL |
[Locators](./locators.md) are the central piece of Playwright's auto-waiting and retry-ability. Locators represent a way to find element(s) on the page at any moment and are used to perform actions on elements such as `.ClickAsync` `.FillAsync` etc.
```csharp ## Test Isolation
var getStarted = Page.GetByRole(AriaRole.Link, new() { Name = "Get started" });
await Expect(getStarted).ToHaveAttributeAsync("href", "/docs/installation");
await getStarted.ClickAsync();
```
### Test Isolation
The Playwright NUnit and MSTest test framework base classes will isolate each test from each other by providing a separate `Page` instance. Pages are isolated between tests due to the Browser Context, which is equivalent to a brand new browser profile, where every test gets a fresh environment, even when multiple tests run in a single Browser. The Playwright NUnit and MSTest test framework base classes will isolate each test from each other by providing a separate `Page` instance. Pages are isolated between tests due to the Browser Context, which is equivalent to a brand new browser profile, where every test gets a fresh environment, even when multiple tests run in a single Browser.
@ -141,7 +217,7 @@ namespace PlaywrightTests;
[Parallelizable(ParallelScope.Self)] [Parallelizable(ParallelScope.Self)]
[TestFixture] [TestFixture]
public class Tests : PageTest public class ExampleTest : PageTest
{ {
[Test] [Test]
public async Task BasicTest() public async Task BasicTest()
@ -162,7 +238,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace PlaywrightTests; namespace PlaywrightTests;
[TestClass] [TestClass]
public class UnitTest1 : PageTest public class ExampleTest : PageTest
{ {
[TestMethod] [TestMethod]
public async Task BasicTest() public async Task BasicTest()
@ -175,7 +251,7 @@ public class UnitTest1 : PageTest
</TabItem> </TabItem>
</Tabs> </Tabs>
### Using Test Hooks ## Using Test Hooks
You can use `SetUp`/`TearDown` in NUnit or `TestInitialize`/`TestCleanup` in MSTest to prepare and clean up your test environment: You can use `SetUp`/`TearDown` in NUnit or `TestInitialize`/`TestCleanup` in MSTest to prepare and clean up your test environment:
@ -198,7 +274,7 @@ namespace PlaywrightTests;
[Parallelizable(ParallelScope.Self)] [Parallelizable(ParallelScope.Self)]
[TestFixture] [TestFixture]
public class Tests : PageTest public class ExampleTest : PageTest
{ {
[Test] [Test]
public async Task MainNavigation() public async Task MainNavigation()
@ -226,7 +302,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace PlaywrightTests; namespace PlaywrightTests;
[TestClass] [TestClass]
public class UnitTest1 : PageTest public class ExampleTest : PageTest
{ {
[TestMethod] [TestMethod]
public async Task MainNavigation() public async Task MainNavigation()
@ -249,5 +325,7 @@ public class UnitTest1 : PageTest
## What's Next ## What's Next
- [Run single test, multiple tests, headed mode](./running-tests.md) - [Run single test, multiple tests, headed mode](./running-tests.md)
- [Generate tests with Codegen](./codegen.md) - [Generate tests with Codegen](./codegen-intro.md)
- [See a trace of your tests](./trace-viewer-intro.md) - [See a trace of your tests](./trace-viewer-intro.md)
- [Run tests on CI](./ci-intro.md)
- [Learn more about the NUnit and MSTest base classes](./test-runners.md)

68
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "playwright-internal", "name": "playwright-internal",
"version": "1.44.0-next", "version": "1.44.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "playwright-internal", "name": "playwright-internal",
"version": "1.44.0-next", "version": "1.44.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
@ -8162,10 +8162,10 @@
} }
}, },
"packages/playwright": { "packages/playwright": {
"version": "1.44.0-next", "version": "1.44.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.44.0-next" "playwright-core": "1.44.1"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -8179,11 +8179,11 @@
}, },
"packages/playwright-browser-chromium": { "packages/playwright-browser-chromium": {
"name": "@playwright/browser-chromium", "name": "@playwright/browser-chromium",
"version": "1.44.0-next", "version": "1.44.1",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.44.0-next" "playwright-core": "1.44.1"
}, },
"engines": { "engines": {
"node": ">=16" "node": ">=16"
@ -8191,11 +8191,11 @@
}, },
"packages/playwright-browser-firefox": { "packages/playwright-browser-firefox": {
"name": "@playwright/browser-firefox", "name": "@playwright/browser-firefox",
"version": "1.44.0-next", "version": "1.44.1",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.44.0-next" "playwright-core": "1.44.1"
}, },
"engines": { "engines": {
"node": ">=16" "node": ">=16"
@ -8203,22 +8203,22 @@
}, },
"packages/playwright-browser-webkit": { "packages/playwright-browser-webkit": {
"name": "@playwright/browser-webkit", "name": "@playwright/browser-webkit",
"version": "1.44.0-next", "version": "1.44.1",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.44.0-next" "playwright-core": "1.44.1"
}, },
"engines": { "engines": {
"node": ">=16" "node": ">=16"
} }
}, },
"packages/playwright-chromium": { "packages/playwright-chromium": {
"version": "1.44.0-next", "version": "1.44.1",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.44.0-next" "playwright-core": "1.44.1"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -8228,7 +8228,7 @@
} }
}, },
"packages/playwright-core": { "packages/playwright-core": {
"version": "1.44.0-next", "version": "1.44.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"playwright-core": "cli.js" "playwright-core": "cli.js"
@ -8239,11 +8239,11 @@
}, },
"packages/playwright-ct-core": { "packages/playwright-ct-core": {
"name": "@playwright/experimental-ct-core", "name": "@playwright/experimental-ct-core",
"version": "1.44.0-next", "version": "1.44.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright": "1.44.0-next", "playwright": "1.44.1",
"playwright-core": "1.44.0-next", "playwright-core": "1.44.1",
"vite": "^5.2.8" "vite": "^5.2.8"
}, },
"engines": { "engines": {
@ -8252,10 +8252,10 @@
}, },
"packages/playwright-ct-react": { "packages/playwright-ct-react": {
"name": "@playwright/experimental-ct-react", "name": "@playwright/experimental-ct-react",
"version": "1.44.0-next", "version": "1.44.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.44.0-next", "@playwright/experimental-ct-core": "1.44.1",
"@vitejs/plugin-react": "^4.2.1" "@vitejs/plugin-react": "^4.2.1"
}, },
"bin": { "bin": {
@ -8267,10 +8267,10 @@
}, },
"packages/playwright-ct-react17": { "packages/playwright-ct-react17": {
"name": "@playwright/experimental-ct-react17", "name": "@playwright/experimental-ct-react17",
"version": "1.44.0-next", "version": "1.44.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.44.0-next", "@playwright/experimental-ct-core": "1.44.1",
"@vitejs/plugin-react": "^4.2.1" "@vitejs/plugin-react": "^4.2.1"
}, },
"bin": { "bin": {
@ -8282,10 +8282,10 @@
}, },
"packages/playwright-ct-solid": { "packages/playwright-ct-solid": {
"name": "@playwright/experimental-ct-solid", "name": "@playwright/experimental-ct-solid",
"version": "1.44.0-next", "version": "1.44.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.44.0-next", "@playwright/experimental-ct-core": "1.44.1",
"vite-plugin-solid": "^2.7.0" "vite-plugin-solid": "^2.7.0"
}, },
"bin": { "bin": {
@ -8300,10 +8300,10 @@
}, },
"packages/playwright-ct-svelte": { "packages/playwright-ct-svelte": {
"name": "@playwright/experimental-ct-svelte", "name": "@playwright/experimental-ct-svelte",
"version": "1.44.0-next", "version": "1.44.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.44.0-next", "@playwright/experimental-ct-core": "1.44.1",
"@sveltejs/vite-plugin-svelte": "^3.0.1" "@sveltejs/vite-plugin-svelte": "^3.0.1"
}, },
"bin": { "bin": {
@ -8318,10 +8318,10 @@
}, },
"packages/playwright-ct-vue": { "packages/playwright-ct-vue": {
"name": "@playwright/experimental-ct-vue", "name": "@playwright/experimental-ct-vue",
"version": "1.44.0-next", "version": "1.44.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.44.0-next", "@playwright/experimental-ct-core": "1.44.1",
"@vitejs/plugin-vue": "^4.2.1" "@vitejs/plugin-vue": "^4.2.1"
}, },
"bin": { "bin": {
@ -8333,10 +8333,10 @@
}, },
"packages/playwright-ct-vue2": { "packages/playwright-ct-vue2": {
"name": "@playwright/experimental-ct-vue2", "name": "@playwright/experimental-ct-vue2",
"version": "1.44.0-next", "version": "1.44.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.44.0-next", "@playwright/experimental-ct-core": "1.44.1",
"@vitejs/plugin-vue2": "^2.2.0" "@vitejs/plugin-vue2": "^2.2.0"
}, },
"bin": { "bin": {
@ -8385,11 +8385,11 @@
} }
}, },
"packages/playwright-firefox": { "packages/playwright-firefox": {
"version": "1.44.0-next", "version": "1.44.1",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.44.0-next" "playwright-core": "1.44.1"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -8400,10 +8400,10 @@
}, },
"packages/playwright-test": { "packages/playwright-test": {
"name": "@playwright/test", "name": "@playwright/test",
"version": "1.44.0-next", "version": "1.44.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright": "1.44.0-next" "playwright": "1.44.1"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@ -8413,11 +8413,11 @@
} }
}, },
"packages/playwright-webkit": { "packages/playwright-webkit": {
"version": "1.44.0-next", "version": "1.44.1",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.44.0-next" "playwright-core": "1.44.1"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,9 +3,9 @@
"browsers": [ "browsers": [
{ {
"name": "chromium", "name": "chromium",
"revision": "1116", "revision": "1117",
"installByDefault": true, "installByDefault": true,
"browserVersion": "125.0.6422.14" "browserVersion": "125.0.6422.26"
}, },
{ {
"name": "chromium-tip-of-tree", "name": "chromium-tip-of-tree",

View file

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

View file

@ -110,7 +110,7 @@
"defaultBrowserType": "webkit" "defaultBrowserType": "webkit"
}, },
"Galaxy S5": { "Galaxy S5": {
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 360, "width": 360,
"height": 640 "height": 640
@ -121,7 +121,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Galaxy S5 landscape": { "Galaxy S5 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 640, "width": 640,
"height": 360 "height": 360
@ -132,7 +132,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Galaxy S8": { "Galaxy S8": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 360, "width": 360,
"height": 740 "height": 740
@ -143,7 +143,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Galaxy S8 landscape": { "Galaxy S8 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 740, "width": 740,
"height": 360 "height": 360
@ -154,7 +154,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Galaxy S9+": { "Galaxy S9+": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 320, "width": 320,
"height": 658 "height": 658
@ -165,7 +165,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Galaxy S9+ landscape": { "Galaxy S9+ landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 658, "width": 658,
"height": 320 "height": 320
@ -176,7 +176,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Galaxy Tab S4": { "Galaxy Tab S4": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Safari/537.36",
"viewport": { "viewport": {
"width": 712, "width": 712,
"height": 1138 "height": 1138
@ -187,7 +187,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Galaxy Tab S4 landscape": { "Galaxy Tab S4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Safari/537.36",
"viewport": { "viewport": {
"width": 1138, "width": 1138,
"height": 712 "height": 712
@ -978,7 +978,7 @@
"defaultBrowserType": "webkit" "defaultBrowserType": "webkit"
}, },
"LG Optimus L70": { "LG Optimus L70": {
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/125.0.6422.14 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 384, "width": 384,
"height": 640 "height": 640
@ -989,7 +989,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"LG Optimus L70 landscape": { "LG Optimus L70 landscape": {
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/125.0.6422.14 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 640, "width": 640,
"height": 384 "height": 384
@ -1000,7 +1000,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Microsoft Lumia 550": { "Microsoft Lumia 550": {
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Mobile Safari/537.36 Edge/14.14263", "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36 Edge/14.14263",
"viewport": { "viewport": {
"width": 640, "width": 640,
"height": 360 "height": 360
@ -1011,7 +1011,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Microsoft Lumia 550 landscape": { "Microsoft Lumia 550 landscape": {
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Mobile Safari/537.36 Edge/14.14263", "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36 Edge/14.14263",
"viewport": { "viewport": {
"width": 360, "width": 360,
"height": 640 "height": 640
@ -1022,7 +1022,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Microsoft Lumia 950": { "Microsoft Lumia 950": {
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Mobile Safari/537.36 Edge/14.14263", "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36 Edge/14.14263",
"viewport": { "viewport": {
"width": 360, "width": 360,
"height": 640 "height": 640
@ -1033,7 +1033,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Microsoft Lumia 950 landscape": { "Microsoft Lumia 950 landscape": {
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Mobile Safari/537.36 Edge/14.14263", "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36 Edge/14.14263",
"viewport": { "viewport": {
"width": 640, "width": 640,
"height": 360 "height": 360
@ -1044,7 +1044,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 10": { "Nexus 10": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Safari/537.36",
"viewport": { "viewport": {
"width": 800, "width": 800,
"height": 1280 "height": 1280
@ -1055,7 +1055,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 10 landscape": { "Nexus 10 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Safari/537.36",
"viewport": { "viewport": {
"width": 1280, "width": 1280,
"height": 800 "height": 800
@ -1066,7 +1066,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 4": { "Nexus 4": {
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 384, "width": 384,
"height": 640 "height": 640
@ -1077,7 +1077,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 4 landscape": { "Nexus 4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 640, "width": 640,
"height": 384 "height": 384
@ -1088,7 +1088,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 5": { "Nexus 5": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 360, "width": 360,
"height": 640 "height": 640
@ -1099,7 +1099,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 5 landscape": { "Nexus 5 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 640, "width": 640,
"height": 360 "height": 360
@ -1110,7 +1110,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 5X": { "Nexus 5X": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 412, "width": 412,
"height": 732 "height": 732
@ -1121,7 +1121,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 5X landscape": { "Nexus 5X landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 732, "width": 732,
"height": 412 "height": 412
@ -1132,7 +1132,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 6": { "Nexus 6": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 412, "width": 412,
"height": 732 "height": 732
@ -1143,7 +1143,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 6 landscape": { "Nexus 6 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 732, "width": 732,
"height": 412 "height": 412
@ -1154,7 +1154,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 6P": { "Nexus 6P": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 412, "width": 412,
"height": 732 "height": 732
@ -1165,7 +1165,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 6P landscape": { "Nexus 6P landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 732, "width": 732,
"height": 412 "height": 412
@ -1176,7 +1176,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 7": { "Nexus 7": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Safari/537.36",
"viewport": { "viewport": {
"width": 600, "width": 600,
"height": 960 "height": 960
@ -1187,7 +1187,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Nexus 7 landscape": { "Nexus 7 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Safari/537.36",
"viewport": { "viewport": {
"width": 960, "width": 960,
"height": 600 "height": 600
@ -1242,7 +1242,7 @@
"defaultBrowserType": "webkit" "defaultBrowserType": "webkit"
}, },
"Pixel 2": { "Pixel 2": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 411, "width": 411,
"height": 731 "height": 731
@ -1253,7 +1253,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 2 landscape": { "Pixel 2 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 731, "width": 731,
"height": 411 "height": 411
@ -1264,7 +1264,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 2 XL": { "Pixel 2 XL": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 411, "width": 411,
"height": 823 "height": 823
@ -1275,7 +1275,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 2 XL landscape": { "Pixel 2 XL landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 823, "width": 823,
"height": 411 "height": 411
@ -1286,7 +1286,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 3": { "Pixel 3": {
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 393, "width": 393,
"height": 786 "height": 786
@ -1297,7 +1297,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 3 landscape": { "Pixel 3 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 786, "width": 786,
"height": 393 "height": 393
@ -1308,7 +1308,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 4": { "Pixel 4": {
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 353, "width": 353,
"height": 745 "height": 745
@ -1319,7 +1319,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 4 landscape": { "Pixel 4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 745, "width": 745,
"height": 353 "height": 353
@ -1330,7 +1330,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 4a (5G)": { "Pixel 4a (5G)": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"screen": { "screen": {
"width": 412, "width": 412,
"height": 892 "height": 892
@ -1345,7 +1345,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 4a (5G) landscape": { "Pixel 4a (5G) landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"screen": { "screen": {
"height": 892, "height": 892,
"width": 412 "width": 412
@ -1360,7 +1360,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 5": { "Pixel 5": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"screen": { "screen": {
"width": 393, "width": 393,
"height": 851 "height": 851
@ -1375,7 +1375,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 5 landscape": { "Pixel 5 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"screen": { "screen": {
"width": 851, "width": 851,
"height": 393 "height": 393
@ -1390,7 +1390,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 7": { "Pixel 7": {
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"screen": { "screen": {
"width": 412, "width": 412,
"height": 915 "height": 915
@ -1405,7 +1405,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Pixel 7 landscape": { "Pixel 7 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"screen": { "screen": {
"width": 915, "width": 915,
"height": 412 "height": 412
@ -1420,7 +1420,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Moto G4": { "Moto G4": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 360, "width": 360,
"height": 640 "height": 640
@ -1431,7 +1431,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Moto G4 landscape": { "Moto G4 landscape": {
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Mobile Safari/537.36", "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Mobile Safari/537.36",
"viewport": { "viewport": {
"width": 640, "width": 640,
"height": 360 "height": 360
@ -1442,7 +1442,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Desktop Chrome HiDPI": { "Desktop Chrome HiDPI": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Safari/537.36", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Safari/537.36",
"screen": { "screen": {
"width": 1792, "width": 1792,
"height": 1120 "height": 1120
@ -1457,7 +1457,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Desktop Edge HiDPI": { "Desktop Edge HiDPI": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Safari/537.36 Edg/125.0.6422.14", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Safari/537.36 Edg/125.0.6422.26",
"screen": { "screen": {
"width": 1792, "width": 1792,
"height": 1120 "height": 1120
@ -1502,7 +1502,7 @@
"defaultBrowserType": "webkit" "defaultBrowserType": "webkit"
}, },
"Desktop Chrome": { "Desktop Chrome": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Safari/537.36", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Safari/537.36",
"screen": { "screen": {
"width": 1920, "width": 1920,
"height": 1080 "height": 1080
@ -1517,7 +1517,7 @@
"defaultBrowserType": "chromium" "defaultBrowserType": "chromium"
}, },
"Desktop Edge": { "Desktop Edge": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.14 Safari/537.36 Edg/125.0.6422.14", "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.26 Safari/537.36 Edg/125.0.6422.26",
"screen": { "screen": {
"width": 1920, "width": 1920,
"height": 1080 "height": 1080

View file

@ -161,7 +161,7 @@ export class Electron extends SdkObject {
return controller.run(async progress => { return controller.run(async progress => {
let app: ElectronApplication | undefined = undefined; let app: ElectronApplication | undefined = undefined;
// --remote-debugging-port=0 must be the last playwright's argument, loader.ts relies on it. // --remote-debugging-port=0 must be the last playwright's argument, loader.ts relies on it.
const electronArguments = ['--inspect=0', '--remote-debugging-port=0', ...args]; let electronArguments = ['--inspect=0', '--remote-debugging-port=0', ...args];
if (os.platform() === 'linux') { if (os.platform() === 'linux') {
const runningAsRoot = process.geteuid && process.geteuid() === 0; const runningAsRoot = process.geteuid && process.geteuid() === 0;
@ -195,6 +195,16 @@ export class Electron extends SdkObject {
// Packaged apps might have their own command line handling. // Packaged apps might have their own command line handling.
electronArguments.unshift('-r', require.resolve('./loader')); electronArguments.unshift('-r', require.resolve('./loader'));
} }
let shell = false;
if (process.platform === 'win32') {
// On Windows in order to run .cmd files, shell: true is required.
// https://github.com/nodejs/node/issues/52554
shell = true;
// On Windows, we need to quote the executable path due to shell: true.
command = `"${command}"`;
// On Windows, we need to quote the arguments due to shell: true.
electronArguments = electronArguments.map(arg => `"${arg}"`);
}
// When debugging Playwright test that runs Electron, NODE_OPTIONS // When debugging Playwright test that runs Electron, NODE_OPTIONS
// will make the debugger attach to Electron's Node. But Playwright // will make the debugger attach to Electron's Node. But Playwright
@ -208,9 +218,7 @@ export class Electron extends SdkObject {
progress.log(message); progress.log(message);
browserLogsCollector.log(message); browserLogsCollector.log(message);
}, },
// On Windows in order to run .cmd files, shell: true is required. shell,
// https://github.com/nodejs/node/issues/52554
shell: process.platform === 'win32',
stdio: 'pipe', stdio: 'pipe',
cwd: options.cwd, cwd: options.cwd,
tempDirectories: [artifactsDir], tempDirectories: [artifactsDir],

View file

@ -125,3 +125,12 @@ export function isVisibleTextNode(node: Text) {
const rect = range.getBoundingClientRect(); const rect = range.getBoundingClientRect();
return rect.width > 0 && rect.height > 0; return rect.width > 0 && rect.height > 0;
} }
export function elementSafeTagName(element: Element) {
// Named inputs, e.g. <input name=tagName>, will be exposed as fields on the parent <form>
// and override its properties.
if (element instanceof HTMLFormElement)
return 'FORM';
// Elements from the svg namespace do not have uppercase tagName right away.
return element.tagName.toUpperCase();
}

View file

@ -55,7 +55,6 @@ export class Highlight {
const document = injectedScript.document; const document = injectedScript.document;
this._isUnderTest = injectedScript.isUnderTest; this._isUnderTest = injectedScript.isUnderTest;
this._glassPaneElement = document.createElement('x-pw-glass'); this._glassPaneElement = document.createElement('x-pw-glass');
this._glassPaneElement.popover = 'manual';
this._glassPaneElement.style.position = 'fixed'; this._glassPaneElement.style.position = 'fixed';
this._glassPaneElement.style.top = '0'; this._glassPaneElement.style.top = '0';
this._glassPaneElement.style.right = '0'; this._glassPaneElement.style.right = '0';
@ -65,12 +64,6 @@ export class Highlight {
this._glassPaneElement.style.pointerEvents = 'none'; this._glassPaneElement.style.pointerEvents = 'none';
this._glassPaneElement.style.display = 'flex'; this._glassPaneElement.style.display = 'flex';
this._glassPaneElement.style.backgroundColor = 'transparent'; this._glassPaneElement.style.backgroundColor = 'transparent';
this._glassPaneElement.style.width = 'inherit';
this._glassPaneElement.style.height = 'inherit';
this._glassPaneElement.style.padding = '0';
this._glassPaneElement.style.margin = '0';
this._glassPaneElement.style.border = 'none';
this._glassPaneElement.style.overflow = 'hidden';
for (const eventName of ['click', 'auxclick', 'dragstart', 'input', 'keydown', 'keyup', 'pointerdown', 'pointerup', 'mousedown', 'mouseup', 'mouseleave', 'focus', 'scroll']) { for (const eventName of ['click', 'auxclick', 'dragstart', 'input', 'keydown', 'keyup', 'pointerdown', 'pointerup', 'mousedown', 'mouseup', 'mouseleave', 'focus', 'scroll']) {
this._glassPaneElement.addEventListener(eventName, e => { this._glassPaneElement.addEventListener(eventName, e => {
e.stopPropagation(); e.stopPropagation();
@ -98,8 +91,6 @@ export class Highlight {
install() { install() {
this._injectedScript.document.documentElement.appendChild(this._glassPaneElement); this._injectedScript.document.documentElement.appendChild(this._glassPaneElement);
// Popover is not supported in WebKit-macOS < 14.0
this._glassPaneElement.showPopover?.();
} }
setLanguage(language: Language) { setLanguage(language: Language) {
@ -116,8 +107,6 @@ export class Highlight {
uninstall() { uninstall() {
if (this._rafRequest) if (this._rafRequest)
cancelAnimationFrame(this._rafRequest); cancelAnimationFrame(this._rafRequest);
// Popover is not supported in WebKit-macOS < 14.0
this._glassPaneElement.hidePopover?.();
this._glassPaneElement.remove(); this._glassPaneElement.remove();
} }

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { closestCrossShadow, enclosingShadowRootOrDocument, getElementComputedStyle, isElementStyleVisibilityVisible, isVisibleTextNode, parentElementOrShadowHost } from './domUtils'; import { closestCrossShadow, elementSafeTagName, enclosingShadowRootOrDocument, getElementComputedStyle, isElementStyleVisibilityVisible, isVisibleTextNode, parentElementOrShadowHost } from './domUtils';
function hasExplicitAccessibleName(e: Element) { function hasExplicitAccessibleName(e: Element) {
return e.hasAttribute('aria-label') || e.hasAttribute('aria-labelledby'); return e.hasAttribute('aria-label') || e.hasAttribute('aria-labelledby');
@ -70,7 +70,7 @@ function isFocusable(element: Element) {
} }
function isNativelyFocusable(element: Element) { function isNativelyFocusable(element: Element) {
const tagName = element.tagName.toUpperCase(); const tagName = elementSafeTagName(element);
if (['BUTTON', 'DETAILS', 'SELECT', 'TEXTAREA'].includes(tagName)) if (['BUTTON', 'DETAILS', 'SELECT', 'TEXTAREA'].includes(tagName))
return true; return true;
if (tagName === 'A' || tagName === 'AREA') if (tagName === 'A' || tagName === 'AREA')
@ -124,7 +124,7 @@ const kImplicitRoleByTagName: { [tagName: string]: (e: Element) => string | null
if (['email', 'tel', 'text', 'url', ''].includes(type)) { if (['email', 'tel', 'text', 'url', ''].includes(type)) {
// https://html.spec.whatwg.org/multipage/input.html#concept-input-list // https://html.spec.whatwg.org/multipage/input.html#concept-input-list
const list = getIdRefs(e, e.getAttribute('list'))[0]; const list = getIdRefs(e, e.getAttribute('list'))[0];
return (list && list.tagName === 'DATALIST') ? 'combobox' : 'textbox'; return (list && elementSafeTagName(list) === 'DATALIST') ? 'combobox' : 'textbox';
} }
if (type === 'hidden') if (type === 'hidden')
return ''; return '';
@ -201,8 +201,7 @@ const kPresentationInheritanceParents: { [tagName: string]: string[] } = {
}; };
function getImplicitAriaRole(element: Element): string | null { function getImplicitAriaRole(element: Element): string | null {
// Elements from the svg namespace do not have uppercase tagName. const implicitRole = kImplicitRoleByTagName[elementSafeTagName(element)]?.(element) || '';
const implicitRole = kImplicitRoleByTagName[element.tagName.toUpperCase()]?.(element) || '';
if (!implicitRole) if (!implicitRole)
return null; return null;
// Inherit presentation role when required. // Inherit presentation role when required.
@ -210,8 +209,8 @@ function getImplicitAriaRole(element: Element): string | null {
let ancestor: Element | null = element; let ancestor: Element | null = element;
while (ancestor) { while (ancestor) {
const parent = parentElementOrShadowHost(ancestor); const parent = parentElementOrShadowHost(ancestor);
const parents = kPresentationInheritanceParents[ancestor.tagName]; const parents = kPresentationInheritanceParents[elementSafeTagName(ancestor)];
if (!parents || !parent || !parents.includes(parent.tagName)) if (!parents || !parent || !parents.includes(elementSafeTagName(parent)))
break; break;
const parentExplicitRole = getExplicitAriaRole(parent); const parentExplicitRole = getExplicitAriaRole(parent);
if ((parentExplicitRole === 'none' || parentExplicitRole === 'presentation') && !hasPresentationConflictResolution(parent, parentExplicitRole)) if ((parentExplicitRole === 'none' || parentExplicitRole === 'presentation') && !hasPresentationConflictResolution(parent, parentExplicitRole))
@ -267,7 +266,7 @@ function getAriaBoolean(attr: string | null) {
// `Any descendants of elements that have the characteristic "Children Presentational: True"` // `Any descendants of elements that have the characteristic "Children Presentational: True"`
// https://www.w3.org/TR/wai-aria-1.2/#aria-hidden // https://www.w3.org/TR/wai-aria-1.2/#aria-hidden
export function isElementHiddenForAria(element: Element): boolean { export function isElementHiddenForAria(element: Element): boolean {
if (['STYLE', 'SCRIPT', 'NOSCRIPT', 'TEMPLATE'].includes(element.tagName)) if (['STYLE', 'SCRIPT', 'NOSCRIPT', 'TEMPLATE'].includes(elementSafeTagName(element)))
return true; return true;
const style = getElementComputedStyle(element); const style = getElementComputedStyle(element);
const isSlot = element.nodeName === 'SLOT'; const isSlot = element.nodeName === 'SLOT';
@ -527,6 +526,7 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
} }
const role = getAriaRole(element) || ''; const role = getAriaRole(element) || '';
const tagName = elementSafeTagName(element);
// step 2c: // step 2c:
// if the current node is a control embedded within the label (e.g. any element directly referenced by aria-labelledby) for another widget... // if the current node is a control embedded within the label (e.g. any element directly referenced by aria-labelledby) for another widget...
@ -542,14 +542,14 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
if (!isOwnLabel && !isOwnLabelledBy) { if (!isOwnLabel && !isOwnLabelledBy) {
if (role === 'textbox') { if (role === 'textbox') {
options.visitedElements.add(element); options.visitedElements.add(element);
if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') if (tagName === 'INPUT' || tagName === 'TEXTAREA')
return (element as HTMLInputElement | HTMLTextAreaElement).value; return (element as HTMLInputElement | HTMLTextAreaElement).value;
return element.textContent || ''; return element.textContent || '';
} }
if (['combobox', 'listbox'].includes(role)) { if (['combobox', 'listbox'].includes(role)) {
options.visitedElements.add(element); options.visitedElements.add(element);
let selectedOptions: Element[]; let selectedOptions: Element[];
if (element.tagName === 'SELECT') { if (tagName === 'SELECT') {
selectedOptions = [...(element as HTMLSelectElement).selectedOptions]; selectedOptions = [...(element as HTMLSelectElement).selectedOptions];
if (!selectedOptions.length && (element as HTMLSelectElement).options.length) if (!selectedOptions.length && (element as HTMLSelectElement).options.length)
selectedOptions.push((element as HTMLSelectElement).options[0]); selectedOptions.push((element as HTMLSelectElement).options[0]);
@ -557,7 +557,7 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
const listbox = role === 'combobox' ? queryInAriaOwned(element, '*').find(e => getAriaRole(e) === 'listbox') : element; const listbox = role === 'combobox' ? queryInAriaOwned(element, '*').find(e => getAriaRole(e) === 'listbox') : element;
selectedOptions = listbox ? queryInAriaOwned(listbox, '[aria-selected="true"]').filter(e => getAriaRole(e) === 'option') : []; selectedOptions = listbox ? queryInAriaOwned(listbox, '[aria-selected="true"]').filter(e => getAriaRole(e) === 'option') : [];
} }
if (!selectedOptions.length && element.tagName === 'INPUT') { if (!selectedOptions.length && tagName === 'INPUT') {
// SPEC DIFFERENCE: // SPEC DIFFERENCE:
// This fallback is not explicitly mentioned in the spec, but all browsers and // This fallback is not explicitly mentioned in the spec, but all browsers and
// wpt test name_heading-combobox-focusable-alternative-manual.html do this. // wpt test name_heading-combobox-focusable-alternative-manual.html do this.
@ -596,7 +596,7 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
// Spec says to ignore this when aria-labelledby is defined. // Spec says to ignore this when aria-labelledby is defined.
// WebKit follows the spec, while Chromium and Firefox do not. // WebKit follows the spec, while Chromium and Firefox do not.
// We align with Chromium and Firefox here. // We align with Chromium and Firefox here.
if (element.tagName === 'INPUT' && ['button', 'submit', 'reset'].includes((element as HTMLInputElement).type)) { if (tagName === 'INPUT' && ['button', 'submit', 'reset'].includes((element as HTMLInputElement).type)) {
options.visitedElements.add(element); options.visitedElements.add(element);
const value = (element as HTMLInputElement).value || ''; const value = (element as HTMLInputElement).value || '';
if (trimFlatString(value)) if (trimFlatString(value))
@ -613,7 +613,7 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
// //
// SPEC DIFFERENCE. // SPEC DIFFERENCE.
// Spec says to ignore this when aria-labelledby is defined, but all browsers take it into account. // Spec says to ignore this when aria-labelledby is defined, but all browsers take it into account.
if (element.tagName === 'INPUT' && (element as HTMLInputElement).type === 'image') { if (tagName === 'INPUT' && (element as HTMLInputElement).type === 'image') {
options.visitedElements.add(element); options.visitedElements.add(element);
const labels = (element as HTMLInputElement).labels || []; const labels = (element as HTMLInputElement).labels || [];
if (labels.length && !options.embeddedInLabelledBy) if (labels.length && !options.embeddedInLabelledBy)
@ -630,7 +630,7 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
} }
// https://w3c.github.io/html-aam/#button-element-accessible-name-computation // https://w3c.github.io/html-aam/#button-element-accessible-name-computation
if (!labelledBy && element.tagName === 'BUTTON') { if (!labelledBy && tagName === 'BUTTON') {
options.visitedElements.add(element); options.visitedElements.add(element);
const labels = (element as HTMLButtonElement).labels || []; const labels = (element as HTMLButtonElement).labels || [];
if (labels.length) if (labels.length)
@ -639,7 +639,7 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
} }
// https://w3c.github.io/html-aam/#output-element-accessible-name-computation // https://w3c.github.io/html-aam/#output-element-accessible-name-computation
if (!labelledBy && element.tagName === 'OUTPUT') { if (!labelledBy && tagName === 'OUTPUT') {
options.visitedElements.add(element); options.visitedElements.add(element);
const labels = (element as HTMLOutputElement).labels || []; const labels = (element as HTMLOutputElement).labels || [];
if (labels.length) if (labels.length)
@ -652,13 +652,13 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
// For "other form elements", we count select and any other input. // For "other form elements", we count select and any other input.
// //
// Note: WebKit does not follow the spec and uses placeholder when aria-labelledby is present. // Note: WebKit does not follow the spec and uses placeholder when aria-labelledby is present.
if (!labelledBy && (element.tagName === 'TEXTAREA' || element.tagName === 'SELECT' || element.tagName === 'INPUT')) { if (!labelledBy && (tagName === 'TEXTAREA' || tagName === 'SELECT' || tagName === 'INPUT')) {
options.visitedElements.add(element); options.visitedElements.add(element);
const labels = (element as (HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement)).labels || []; const labels = (element as (HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement)).labels || [];
if (labels.length) if (labels.length)
return getAccessibleNameFromAssociatedLabels(labels, options); return getAccessibleNameFromAssociatedLabels(labels, options);
const usePlaceholder = (element.tagName === 'INPUT' && ['text', 'password', 'search', 'tel', 'email', 'url'].includes((element as HTMLInputElement).type)) || element.tagName === 'TEXTAREA'; const usePlaceholder = (tagName === 'INPUT' && ['text', 'password', 'search', 'tel', 'email', 'url'].includes((element as HTMLInputElement).type)) || tagName === 'TEXTAREA';
const placeholder = element.getAttribute('placeholder') || ''; const placeholder = element.getAttribute('placeholder') || '';
const title = element.getAttribute('title') || ''; const title = element.getAttribute('title') || '';
if (!usePlaceholder || title) if (!usePlaceholder || title)
@ -667,10 +667,10 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
} }
// https://w3c.github.io/html-aam/#fieldset-and-legend-elements // https://w3c.github.io/html-aam/#fieldset-and-legend-elements
if (!labelledBy && element.tagName === 'FIELDSET') { if (!labelledBy && tagName === 'FIELDSET') {
options.visitedElements.add(element); options.visitedElements.add(element);
for (let child = element.firstElementChild; child; child = child.nextElementSibling) { for (let child = element.firstElementChild; child; child = child.nextElementSibling) {
if (child.tagName === 'LEGEND') { if (elementSafeTagName(child) === 'LEGEND') {
return getTextAlternativeInternal(child, { return getTextAlternativeInternal(child, {
...childOptions, ...childOptions,
embeddedInNativeTextAlternative: { element: child, hidden: isElementHiddenForAria(child) }, embeddedInNativeTextAlternative: { element: child, hidden: isElementHiddenForAria(child) },
@ -682,10 +682,10 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
} }
// https://w3c.github.io/html-aam/#figure-and-figcaption-elements // https://w3c.github.io/html-aam/#figure-and-figcaption-elements
if (!labelledBy && element.tagName === 'FIGURE') { if (!labelledBy && tagName === 'FIGURE') {
options.visitedElements.add(element); options.visitedElements.add(element);
for (let child = element.firstElementChild; child; child = child.nextElementSibling) { for (let child = element.firstElementChild; child; child = child.nextElementSibling) {
if (child.tagName === 'FIGCAPTION') { if (elementSafeTagName(child) === 'FIGCAPTION') {
return getTextAlternativeInternal(child, { return getTextAlternativeInternal(child, {
...childOptions, ...childOptions,
embeddedInNativeTextAlternative: { element: child, hidden: isElementHiddenForAria(child) }, embeddedInNativeTextAlternative: { element: child, hidden: isElementHiddenForAria(child) },
@ -700,7 +700,7 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
// //
// SPEC DIFFERENCE. // SPEC DIFFERENCE.
// Spec says to ignore this when aria-labelledby is defined, but all browsers take it into account. // Spec says to ignore this when aria-labelledby is defined, but all browsers take it into account.
if (element.tagName === 'IMG') { if (tagName === 'IMG') {
options.visitedElements.add(element); options.visitedElements.add(element);
const alt = element.getAttribute('alt') || ''; const alt = element.getAttribute('alt') || '';
if (trimFlatString(alt)) if (trimFlatString(alt))
@ -710,10 +710,10 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
} }
// https://w3c.github.io/html-aam/#table-element // https://w3c.github.io/html-aam/#table-element
if (element.tagName === 'TABLE') { if (tagName === 'TABLE') {
options.visitedElements.add(element); options.visitedElements.add(element);
for (let child = element.firstElementChild; child; child = child.nextElementSibling) { for (let child = element.firstElementChild; child; child = child.nextElementSibling) {
if (child.tagName === 'CAPTION') { if (elementSafeTagName(child) === 'CAPTION') {
return getTextAlternativeInternal(child, { return getTextAlternativeInternal(child, {
...childOptions, ...childOptions,
embeddedInNativeTextAlternative: { element: child, hidden: isElementHiddenForAria(child) }, embeddedInNativeTextAlternative: { element: child, hidden: isElementHiddenForAria(child) },
@ -731,7 +731,7 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
} }
// https://w3c.github.io/html-aam/#area-element // https://w3c.github.io/html-aam/#area-element
if (element.tagName === 'AREA') { if (tagName === 'AREA') {
options.visitedElements.add(element); options.visitedElements.add(element);
const alt = element.getAttribute('alt') || ''; const alt = element.getAttribute('alt') || '';
if (trimFlatString(alt)) if (trimFlatString(alt))
@ -741,10 +741,10 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
} }
// https://www.w3.org/TR/svg-aam-1.0/#mapping_additional_nd // https://www.w3.org/TR/svg-aam-1.0/#mapping_additional_nd
if (element.tagName.toUpperCase() === 'SVG' || (element as SVGElement).ownerSVGElement) { if (tagName === 'SVG' || (element as SVGElement).ownerSVGElement) {
options.visitedElements.add(element); options.visitedElements.add(element);
for (let child = element.firstElementChild; child; child = child.nextElementSibling) { for (let child = element.firstElementChild; child; child = child.nextElementSibling) {
if (child.tagName.toUpperCase() === 'TITLE' && (child as SVGElement).ownerSVGElement) { if (elementSafeTagName(child) === 'TITLE' && (child as SVGElement).ownerSVGElement) {
return getTextAlternativeInternal(child, { return getTextAlternativeInternal(child, {
...childOptions, ...childOptions,
embeddedInLabelledBy: { element: child, hidden: isElementHiddenForAria(child) }, embeddedInLabelledBy: { element: child, hidden: isElementHiddenForAria(child) },
@ -752,7 +752,7 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
} }
} }
} }
if ((element as SVGElement).ownerSVGElement && element.tagName.toUpperCase() === 'A') { if ((element as SVGElement).ownerSVGElement && tagName === 'A') {
const title = element.getAttribute('xlink:title') || ''; const title = element.getAttribute('xlink:title') || '';
if (trimFlatString(title)) { if (trimFlatString(title)) {
options.visitedElements.add(element); options.visitedElements.add(element);
@ -762,7 +762,7 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
} }
// See https://w3c.github.io/html-aam/#summary-element-accessible-name-computation for "summary"-specific check. // See https://w3c.github.io/html-aam/#summary-element-accessible-name-computation for "summary"-specific check.
const shouldNameFromContentForSummary = element.tagName === 'SUMMARY' && !['presentation', 'none'].includes(role); const shouldNameFromContentForSummary = tagName === 'SUMMARY' && !['presentation', 'none'].includes(role);
// step 2f + step 2h. // step 2f + step 2h.
if (allowsNameFromContent(role, options.embeddedInTargetElement === 'descendant') || if (allowsNameFromContent(role, options.embeddedInTargetElement === 'descendant') ||
@ -815,7 +815,7 @@ function getTextAlternativeInternal(element: Element, options: AccessibleNameOpt
} }
// step 2i. // step 2i.
if (!['presentation', 'none'].includes(role) || element.tagName === 'IFRAME') { if (!['presentation', 'none'].includes(role) || tagName === 'IFRAME') {
options.visitedElements.add(element); options.visitedElements.add(element);
const title = element.getAttribute('title') || ''; const title = element.getAttribute('title') || '';
if (trimFlatString(title)) if (trimFlatString(title))
@ -830,7 +830,7 @@ export const kAriaSelectedRoles = ['gridcell', 'option', 'row', 'tab', 'rowheade
export function getAriaSelected(element: Element): boolean { export function getAriaSelected(element: Element): boolean {
// https://www.w3.org/TR/wai-aria-1.2/#aria-selected // https://www.w3.org/TR/wai-aria-1.2/#aria-selected
// https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings // https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings
if (element.tagName === 'OPTION') if (elementSafeTagName(element) === 'OPTION')
return (element as HTMLOptionElement).selected; return (element as HTMLOptionElement).selected;
if (kAriaSelectedRoles.includes(getAriaRole(element) || '')) if (kAriaSelectedRoles.includes(getAriaRole(element) || ''))
return getAriaBoolean(element.getAttribute('aria-selected')) === true; return getAriaBoolean(element.getAttribute('aria-selected')) === true;
@ -843,11 +843,12 @@ export function getAriaChecked(element: Element): boolean | 'mixed' {
return result === 'error' ? false : result; return result === 'error' ? false : result;
} }
export function getChecked(element: Element, allowMixed: boolean): boolean | 'mixed' | 'error' { export function getChecked(element: Element, allowMixed: boolean): boolean | 'mixed' | 'error' {
const tagName = elementSafeTagName(element);
// https://www.w3.org/TR/wai-aria-1.2/#aria-checked // https://www.w3.org/TR/wai-aria-1.2/#aria-checked
// https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings // https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings
if (allowMixed && element.tagName === 'INPUT' && (element as HTMLInputElement).indeterminate) if (allowMixed && tagName === 'INPUT' && (element as HTMLInputElement).indeterminate)
return 'mixed'; return 'mixed';
if (element.tagName === 'INPUT' && ['checkbox', 'radio'].includes((element as HTMLInputElement).type)) if (tagName === 'INPUT' && ['checkbox', 'radio'].includes((element as HTMLInputElement).type))
return (element as HTMLInputElement).checked; return (element as HTMLInputElement).checked;
if (kAriaCheckedRoles.includes(getAriaRole(element) || '')) { if (kAriaCheckedRoles.includes(getAriaRole(element) || '')) {
const checked = element.getAttribute('aria-checked'); const checked = element.getAttribute('aria-checked');
@ -877,7 +878,7 @@ export const kAriaExpandedRoles = ['application', 'button', 'checkbox', 'combobo
export function getAriaExpanded(element: Element): boolean | 'none' { export function getAriaExpanded(element: Element): boolean | 'none' {
// https://www.w3.org/TR/wai-aria-1.2/#aria-expanded // https://www.w3.org/TR/wai-aria-1.2/#aria-expanded
// https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings // https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings
if (element.tagName === 'DETAILS') if (elementSafeTagName(element) === 'DETAILS')
return (element as HTMLDetailsElement).open; return (element as HTMLDetailsElement).open;
if (kAriaExpandedRoles.includes(getAriaRole(element) || '')) { if (kAriaExpandedRoles.includes(getAriaRole(element) || '')) {
const expanded = element.getAttribute('aria-expanded'); const expanded = element.getAttribute('aria-expanded');
@ -894,7 +895,7 @@ export const kAriaLevelRoles = ['heading', 'listitem', 'row', 'treeitem'];
export function getAriaLevel(element: Element): number { export function getAriaLevel(element: Element): number {
// https://www.w3.org/TR/wai-aria-1.2/#aria-level // https://www.w3.org/TR/wai-aria-1.2/#aria-level
// https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings // https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings
const native = { 'H1': 1, 'H2': 2, 'H3': 3, 'H4': 4, 'H5': 5, 'H6': 6 }[element.tagName]; const native = { 'H1': 1, 'H2': 2, 'H3': 3, 'H4': 4, 'H5': 5, 'H6': 6 }[elementSafeTagName(element)];
if (native) if (native)
return native; return native;
if (kAriaLevelRoles.includes(getAriaRole(element) || '')) { if (kAriaLevelRoles.includes(getAriaRole(element) || '')) {
@ -922,7 +923,7 @@ function isNativelyDisabled(element: Element) {
function belongsToDisabledFieldSet(element: Element | null): boolean { function belongsToDisabledFieldSet(element: Element | null): boolean {
if (!element) if (!element)
return false; return false;
if (element.tagName === 'FIELDSET' && element.hasAttribute('disabled')) if (elementSafeTagName(element) === 'FIELDSET' && element.hasAttribute('disabled'))
return true; return true;
// fieldset does not work across shadow boundaries. // fieldset does not work across shadow boundaries.
return belongsToDisabledFieldSet(element.parentElement); return belongsToDisabledFieldSet(element.parentElement);

View file

@ -145,7 +145,7 @@ export async function runTraceViewerApp(traceUrls: string[], browserName: string
validateTraceUrls(traceUrls); validateTraceUrls(traceUrls);
const server = await startTraceViewerServer(options); const server = await startTraceViewerServer(options);
await installRootRedirect(server, traceUrls, options); await installRootRedirect(server, traceUrls, options);
const page = await openTraceViewerApp(server.urlPrefix(), browserName, options); const page = await openTraceViewerApp(server.urlPrefix('precise'), browserName, options);
if (exitOnClose) if (exitOnClose)
page.on('close', () => gracefullyProcessExitDoNotHang(0)); page.on('close', () => gracefullyProcessExitDoNotHang(0));
return page; return page;
@ -155,7 +155,7 @@ export async function runTraceInBrowser(traceUrls: string[], options: TraceViewe
validateTraceUrls(traceUrls); validateTraceUrls(traceUrls);
const server = await startTraceViewerServer(options); const server = await startTraceViewerServer(options);
await installRootRedirect(server, traceUrls, options); await installRootRedirect(server, traceUrls, options);
await openTraceInBrowser(server.urlPrefix()); await openTraceInBrowser(server.urlPrefix('human-readable'));
} }
export async function openTraceViewerApp(url: string, browserName: string, options?: TraceViewerAppOptions): Promise<Page> { export async function openTraceViewerApp(url: string, browserName: string, options?: TraceViewerAppOptions): Promise<Page> {

View file

@ -34,14 +34,14 @@ export type Transport = {
export class HttpServer { export class HttpServer {
private _server: http.Server; private _server: http.Server;
private _urlPrefix: string; private _urlPrefixPrecise: string = '';
private _urlPrefixHumanReadable: string = '';
private _port: number = 0; private _port: number = 0;
private _started = false; private _started = false;
private _routes: { prefix?: string, exact?: string, handler: ServerRouteHandler }[] = []; private _routes: { prefix?: string, exact?: string, handler: ServerRouteHandler }[] = [];
private _wsGuid: string | undefined; private _wsGuid: string | undefined;
constructor(address: string = '') { constructor() {
this._urlPrefix = address;
this._server = createHttpServer(this._onRequest.bind(this)); this._server = createHttpServer(this._onRequest.bind(this));
} }
@ -102,7 +102,7 @@ export class HttpServer {
return this._wsGuid; return this._wsGuid;
} }
async start(options: { port?: number, preferredPort?: number, host?: string } = {}): Promise<string> { async start(options: { port?: number, preferredPort?: number, host?: string } = {}): Promise<void> {
assert(!this._started, 'server already started'); assert(!this._started, 'server already started');
this._started = true; this._started = true;
@ -121,23 +121,23 @@ export class HttpServer {
const address = this._server.address(); const address = this._server.address();
assert(address, 'Could not bind server socket'); assert(address, 'Could not bind server socket');
if (!this._urlPrefix) { if (typeof address === 'string') {
if (typeof address === 'string') { this._urlPrefixPrecise = address;
this._urlPrefix = address; this._urlPrefixHumanReadable = address;
} else { } else {
this._port = address.port; this._port = address.port;
this._urlPrefix = `http://${host}:${address.port}`; const resolvedHost = address.family === 'IPv4' ? address.address : `[${address.address}]`;
} this._urlPrefixPrecise = `http://${resolvedHost}:${address.port}`;
this._urlPrefixHumanReadable = `http://${host}:${address.port}`;
} }
return this._urlPrefix;
} }
async stop() { async stop() {
await new Promise(cb => this._server!.close(cb)); await new Promise(cb => this._server!.close(cb));
} }
urlPrefix(): string { urlPrefix(purpose: 'human-readable' | 'precise'): string {
return this._urlPrefix; return purpose === 'human-readable' ? this._urlPrefixHumanReadable : this._urlPrefixPrecise;
} }
serveFile(request: http.IncomingMessage, response: http.ServerResponse, absoluteFilePath: string, headers?: { [name: string]: string }): boolean { serveFile(request: http.IncomingMessage, response: http.ServerResponse, absoluteFilePath: string, headers?: { [name: string]: string }): boolean {

View file

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

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/experimental-ct-react", "name": "@playwright/experimental-ct-react",
"version": "1.44.0-next", "version": "1.44.1",
"description": "Playwright Component Testing for React", "description": "Playwright Component Testing for React",
"repository": { "repository": {
"type": "git", "type": "git",
@ -29,7 +29,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.44.0-next", "@playwright/experimental-ct-core": "1.44.1",
"@vitejs/plugin-react": "^4.2.1" "@vitejs/plugin-react": "^4.2.1"
}, },
"bin": { "bin": {

View file

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

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/experimental-ct-solid", "name": "@playwright/experimental-ct-solid",
"version": "1.44.0-next", "version": "1.44.1",
"description": "Playwright Component Testing for Solid", "description": "Playwright Component Testing for Solid",
"repository": { "repository": {
"type": "git", "type": "git",
@ -29,7 +29,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.44.0-next", "@playwright/experimental-ct-core": "1.44.1",
"vite-plugin-solid": "^2.7.0" "vite-plugin-solid": "^2.7.0"
}, },
"devDependencies": { "devDependencies": {

View file

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

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/experimental-ct-vue", "name": "@playwright/experimental-ct-vue",
"version": "1.44.0-next", "version": "1.44.1",
"description": "Playwright Component Testing for Vue", "description": "Playwright Component Testing for Vue",
"repository": { "repository": {
"type": "git", "type": "git",
@ -29,7 +29,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.44.0-next", "@playwright/experimental-ct-core": "1.44.1",
"@vitejs/plugin-vue": "^4.2.1" "@vitejs/plugin-vue": "^4.2.1"
}, },
"bin": { "bin": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/experimental-ct-vue2", "name": "@playwright/experimental-ct-vue2",
"version": "1.44.0-next", "version": "1.44.1",
"description": "Playwright Component Testing for Vue2", "description": "Playwright Component Testing for Vue2",
"repository": { "repository": {
"type": "git", "type": "git",
@ -29,7 +29,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@playwright/experimental-ct-core": "1.44.0-next", "@playwright/experimental-ct-core": "1.44.1",
"@vitejs/plugin-vue2": "^2.2.0" "@vitejs/plugin-vue2": "^2.2.0"
}, },
"devDependencies": { "devDependencies": {

View file

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

View file

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

View file

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

View file

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

View file

@ -265,7 +265,7 @@ export function toReporters(reporters: BuiltInReporter | ReporterDescription[] |
export const builtInReporters = ['list', 'line', 'dot', 'json', 'junit', 'null', 'github', 'html', 'blob', 'markdown'] as const; export const builtInReporters = ['list', 'line', 'dot', 'json', 'junit', 'null', 'github', 'html', 'blob', 'markdown'] as const;
export type BuiltInReporter = typeof builtInReporters[number]; export type BuiltInReporter = typeof builtInReporters[number];
export type ContextReuseMode = 'none' | 'force' | 'when-possible'; export type ContextReuseMode = 'none' | 'when-possible';
function resolveScript(id: string | undefined, rootDir: string): string | undefined { function resolveScript(id: string | undefined, rootDir: string): string | undefined {
if (!id) if (!id)

View file

@ -356,8 +356,8 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
_reuseContext: [async ({ video, _optionContextReuseMode }, use) => { _reuseContext: [async ({ video, _optionContextReuseMode }, use) => {
let mode = _optionContextReuseMode; let mode = _optionContextReuseMode;
if (process.env.PW_TEST_REUSE_CONTEXT) if (process.env.PW_TEST_REUSE_CONTEXT)
mode = process.env.PW_TEST_REUSE_CONTEXT === 'when-possible' ? 'when-possible' : (process.env.PW_TEST_REUSE_CONTEXT ? 'force' : 'none'); mode = 'when-possible';
const reuse = mode === 'force' || (mode === 'when-possible' && normalizeVideoMode(video) === 'off'); const reuse = mode === 'when-possible' && normalizeVideoMode(video) === 'off';
await use(reuse); await use(reuse);
}, { scope: 'worker', _title: 'context' } as any], }, { scope: 'worker', _title: 'context' } as any],

View file

@ -94,6 +94,7 @@ export interface TestServerInterface {
timeout?: number, timeout?: number,
reporters?: string[], reporters?: string[],
trace?: 'on' | 'off'; trace?: 'on' | 'off';
video?: 'on' | 'off';
projects?: string[]; projects?: string[];
reuseContext?: boolean; reuseContext?: boolean;
connectWsEndpoint?: string; connectWsEndpoint?: string;

View file

@ -19,6 +19,7 @@ import path from 'path';
import type { FullConfig, TestCase, Suite, TestResult, TestError, FullResult, TestStep, Location } from '../../types/testReporter'; import type { FullConfig, TestCase, Suite, TestResult, TestError, FullResult, TestStep, Location } from '../../types/testReporter';
import { getPackageManagerExecCommand } from 'playwright-core/lib/utils'; import { getPackageManagerExecCommand } from 'playwright-core/lib/utils';
import type { ReporterV2 } from './reporterV2'; import type { ReporterV2 } from './reporterV2';
import { resolveReporterOutputPath } from '../util';
export type TestResultOutput = { chunk: string | Buffer, type: 'stdout' | 'stderr' }; export type TestResultOutput = { chunk: string | Buffer, type: 'stdout' | 'stderr' };
export const kOutputSymbol = Symbol('output'); export const kOutputSymbol = Symbol('output');
@ -547,3 +548,49 @@ function fitToWidth(line: string, width: number, prefix?: string): string {
function belongsToNodeModules(file: string) { function belongsToNodeModules(file: string) {
return file.includes(`${path.sep}node_modules${path.sep}`); return file.includes(`${path.sep}node_modules${path.sep}`);
} }
function resolveFromEnv(name: string): string | undefined {
const value = process.env[name];
if (value)
return path.resolve(process.cwd(), value);
return undefined;
}
// In addition to `outputFile` the function returns `outputDir` which should
// be cleaned up if present by some reporters contract.
export function resolveOutputFile(reporterName: string, options: {
configDir: string,
outputDir?: string,
fileName?: string,
outputFile?: string,
default?: {
fileName: string,
outputDir: string,
}
}): { outputFile: string, outputDir?: string } |undefined {
const name = reporterName.toUpperCase();
let outputFile;
if (options.outputFile)
outputFile = path.resolve(options.configDir, options.outputFile);
if (!outputFile)
outputFile = resolveFromEnv(`PLAYWRIGHT_${name}_OUTPUT_FILE`);
// Return early to avoid deleting outputDir.
if (outputFile)
return { outputFile };
let outputDir;
if (options.outputDir)
outputDir = path.resolve(options.configDir, options.outputDir);
if (!outputDir)
outputDir = resolveFromEnv(`PLAYWRIGHT_${name}_OUTPUT_DIR`);
if (!outputDir && options.default)
outputDir = resolveReporterOutputPath(options.default.outputDir, options.configDir, undefined);
if (!outputFile) {
const reportName = options.fileName ?? process.env[`PLAYWRIGHT_${name}_OUTPUT_NAME`] ?? options.default?.fileName;
if (!reportName)
return undefined;
outputFile = path.resolve(outputDir ?? process.cwd(), reportName);
}
return { outputFile, outputDir };
}

View file

@ -24,7 +24,7 @@ import type { FullConfig, FullResult, TestResult } from '../../types/testReporte
import type { JsonAttachment, JsonEvent } from '../isomorphic/teleReceiver'; import type { JsonAttachment, JsonEvent } from '../isomorphic/teleReceiver';
import { TeleReporterEmitter } from './teleEmitter'; import { TeleReporterEmitter } from './teleEmitter';
import { yazl } from 'playwright-core/lib/zipBundle'; import { yazl } from 'playwright-core/lib/zipBundle';
import { resolveReporterOutputPath } from '../util'; import { resolveOutputFile } from './base';
type BlobReporterOptions = { type BlobReporterOptions = {
configDir: string; configDir: string;
@ -107,17 +107,15 @@ export class BlobReporter extends TeleReporterEmitter {
} }
private async _prepareOutputFile() { private async _prepareOutputFile() {
let outputFile = reportOutputFileFromEnv(); const { outputFile, outputDir } = resolveOutputFile('BLOB', {
if (!outputFile && this._options.outputFile) ...this._options,
outputFile = path.resolve(this._options.configDir, this._options.outputFile); default: {
// Explicit `outputFile` overrides `outputDir` and `fileName` options. fileName: this._defaultReportName(this._config),
if (!outputFile) { outputDir: 'blob-report',
const reportName = this._options.fileName || process.env[`PLAYWRIGHT_BLOB_OUTPUT_NAME`] || this._defaultReportName(this._config); }
const outputDir = resolveReporterOutputPath('blob-report', this._options.configDir, this._options.outputDir ?? reportOutputDirFromEnv()); })!;
if (!process.env.PWTEST_BLOB_DO_NOT_REMOVE) if (!process.env.PWTEST_BLOB_DO_NOT_REMOVE)
await removeFolders([outputDir]); await removeFolders([outputDir!]);
outputFile = path.resolve(outputDir, reportName);
}
await fs.promises.mkdir(path.dirname(outputFile), { recursive: true }); await fs.promises.mkdir(path.dirname(outputFile), { recursive: true });
return outputFile; return outputFile;
} }
@ -149,15 +147,3 @@ export class BlobReporter extends TeleReporterEmitter {
}); });
} }
} }
function reportOutputDirFromEnv(): string | undefined {
if (process.env[`PLAYWRIGHT_BLOB_OUTPUT_DIR`])
return path.resolve(process.cwd(), process.env[`PLAYWRIGHT_BLOB_OUTPUT_DIR`]);
return undefined;
}
function reportOutputFileFromEnv(): string | undefined {
if (process.env[`PLAYWRIGHT_BLOB_OUTPUT_FILE`])
return path.resolve(process.cwd(), process.env[`PLAYWRIGHT_BLOB_OUTPUT_FILE`]);
return undefined;
}

View file

@ -177,7 +177,8 @@ export async function showHTMLReport(reportFolder: string | undefined, host: str
return; return;
} }
const server = startHtmlReportServer(folder); const server = startHtmlReportServer(folder);
let url = await server.start({ port, host, preferredPort: port ? undefined : 9323 }); await server.start({ port, host, preferredPort: port ? undefined : 9323 });
let url = server.urlPrefix('human-readable');
console.log(''); console.log('');
console.log(colors.cyan(` Serving HTML report at ${url}. Press Ctrl+C to quit.`)); console.log(colors.cyan(` Serving HTML report at ${url}. Press Ctrl+C to quit.`));
if (testId) if (testId)

View file

@ -17,24 +17,29 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import type { FullConfig, TestCase, Suite, TestResult, TestError, TestStep, FullResult, Location, JSONReport, JSONReportSuite, JSONReportSpec, JSONReportTest, JSONReportTestResult, JSONReportTestStep, JSONReportError } from '../../types/testReporter'; import type { FullConfig, TestCase, Suite, TestResult, TestError, TestStep, FullResult, Location, JSONReport, JSONReportSuite, JSONReportSpec, JSONReportTest, JSONReportTestResult, JSONReportTestStep, JSONReportError } from '../../types/testReporter';
import { formatError, prepareErrorStack } from './base'; import { formatError, prepareErrorStack, resolveOutputFile } from './base';
import { MultiMap, assert, toPosixPath } from 'playwright-core/lib/utils'; import { MultiMap, toPosixPath } from 'playwright-core/lib/utils';
import { getProjectId } from '../common/config'; import { getProjectId } from '../common/config';
import EmptyReporter from './empty'; import EmptyReporter from './empty';
type JSONOptions = {
outputFile?: string,
configDir: string,
};
class JSONReporter extends EmptyReporter { class JSONReporter extends EmptyReporter {
config!: FullConfig; config!: FullConfig;
suite!: Suite; suite!: Suite;
private _errors: TestError[] = []; private _errors: TestError[] = [];
private _outputFile: string | undefined; private _resolvedOutputFile: string | undefined;
constructor(options: { outputFile?: string } = {}) { constructor(options: JSONOptions) {
super(); super();
this._outputFile = options.outputFile || reportOutputNameFromEnv(); this._resolvedOutputFile = resolveOutputFile('JSON', options)?.outputFile;
} }
override printsToStdio() { override printsToStdio() {
return !this._outputFile; return !this._resolvedOutputFile;
} }
override onConfigure(config: FullConfig) { override onConfigure(config: FullConfig) {
@ -50,7 +55,7 @@ class JSONReporter extends EmptyReporter {
} }
override async onEnd(result: FullResult) { override async onEnd(result: FullResult) {
await outputReport(this._serializeReport(result), this.config, this._outputFile); await outputReport(this._serializeReport(result), this._resolvedOutputFile);
} }
private _serializeReport(result: FullResult): JSONReport { private _serializeReport(result: FullResult): JSONReport {
@ -228,13 +233,11 @@ class JSONReporter extends EmptyReporter {
} }
} }
async function outputReport(report: JSONReport, config: FullConfig, outputFile: string | undefined) { async function outputReport(report: JSONReport, resolvedOutputFile: string | undefined) {
const reportString = JSON.stringify(report, undefined, 2); const reportString = JSON.stringify(report, undefined, 2);
if (outputFile) { if (resolvedOutputFile) {
assert(config.configFile || path.isAbsolute(outputFile), 'Expected fully resolved path if not using config file.'); await fs.promises.mkdir(path.dirname(resolvedOutputFile), { recursive: true });
outputFile = config.configFile ? path.resolve(path.dirname(config.configFile), outputFile) : outputFile; await fs.promises.writeFile(resolvedOutputFile, reportString);
await fs.promises.mkdir(path.dirname(outputFile), { recursive: true });
await fs.promises.writeFile(outputFile, reportString);
} else { } else {
console.log(reportString); console.log(reportString);
} }
@ -250,12 +253,6 @@ function removePrivateFields(config: FullConfig): FullConfig {
return Object.fromEntries(Object.entries(config).filter(([name, value]) => !name.startsWith('_'))) as FullConfig; return Object.fromEntries(Object.entries(config).filter(([name, value]) => !name.startsWith('_'))) as FullConfig;
} }
function reportOutputNameFromEnv(): string | undefined {
if (process.env[`PLAYWRIGHT_JSON_OUTPUT_NAME`])
return path.resolve(process.cwd(), process.env[`PLAYWRIGHT_JSON_OUTPUT_NAME`]);
return undefined;
}
export function serializePatterns(patterns: string | RegExp | (string | RegExp)[]): string[] { export function serializePatterns(patterns: string | RegExp | (string | RegExp)[]): string[] {
if (!Array.isArray(patterns)) if (!Array.isArray(patterns))
patterns = [patterns]; patterns = [patterns];

View file

@ -17,7 +17,7 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import type { FullConfig, FullResult, Suite, TestCase } from '../../types/testReporter'; import type { FullConfig, FullResult, Suite, TestCase } from '../../types/testReporter';
import { formatFailure, stripAnsiEscapes } from './base'; import { formatFailure, resolveOutputFile, stripAnsiEscapes } from './base';
import EmptyReporter from './empty'; import EmptyReporter from './empty';
type JUnitOptions = { type JUnitOptions = {
@ -25,7 +25,7 @@ type JUnitOptions = {
stripANSIControlSequences?: boolean, stripANSIControlSequences?: boolean,
includeProjectInTestName?: boolean, includeProjectInTestName?: boolean,
configDir?: string, configDir: string,
}; };
class JUnitReporter extends EmptyReporter { class JUnitReporter extends EmptyReporter {
@ -40,14 +40,12 @@ class JUnitReporter extends EmptyReporter {
private stripANSIControlSequences = false; private stripANSIControlSequences = false;
private includeProjectInTestName = false; private includeProjectInTestName = false;
constructor(options: JUnitOptions = {}) { constructor(options: JUnitOptions) {
super(); super();
this.stripANSIControlSequences = options.stripANSIControlSequences || false; this.stripANSIControlSequences = options.stripANSIControlSequences || false;
this.includeProjectInTestName = options.includeProjectInTestName || false; this.includeProjectInTestName = options.includeProjectInTestName || false;
this.configDir = options.configDir || ''; this.configDir = options.configDir;
const outputFile = options.outputFile || reportOutputNameFromEnv(); this.resolvedOutputFile = resolveOutputFile('JUNIT', options)?.outputFile;
if (outputFile)
this.resolvedOutputFile = path.resolve(this.configDir, outputFile);
} }
override printsToStdio() { override printsToStdio() {
@ -261,10 +259,4 @@ function escape(text: string, stripANSIControlSequences: boolean, isCharacterDat
return text; return text;
} }
function reportOutputNameFromEnv(): string | undefined {
if (process.env[`PLAYWRIGHT_JUNIT_OUTPUT_NAME`])
return path.resolve(process.cwd(), process.env[`PLAYWRIGHT_JUNIT_OUTPUT_NAME`]);
return undefined;
}
export default JUnitReporter; export default JUnitReporter;

View file

@ -308,6 +308,7 @@ class TestServerDispatcher implements TestServerInterface {
reporter: params.reporters ? params.reporters.map(r => [r]) : undefined, reporter: params.reporters ? params.reporters.map(r => [r]) : undefined,
use: { use: {
trace: params.trace === 'on' ? { mode: 'on', sources: false, _live: true } : (params.trace === 'off' ? 'off' : undefined), trace: params.trace === 'on' ? { mode: 'on', sources: false, _live: true } : (params.trace === 'off' ? 'off' : undefined),
video: params.video === 'on' ? 'on' : (params.video === 'off' ? 'off' : undefined),
headless: params.headed ? false : undefined, headless: params.headed ? false : undefined,
_optionContextReuseMode: params.reuseContext ? 'when-possible' : undefined, _optionContextReuseMode: params.reuseContext ? 'when-possible' : undefined,
_optionConnectOptions: params.connectWsEndpoint ? { wsEndpoint: params.connectWsEndpoint } : undefined, _optionConnectOptions: params.connectWsEndpoint ? { wsEndpoint: params.connectWsEndpoint } : undefined,
@ -417,9 +418,9 @@ export async function runUIMode(configFile: string | undefined, options: TraceVi
return await innerRunTestServer(configLocation, options, async (server: HttpServer, cancelPromise: ManualPromise<void>) => { return await innerRunTestServer(configLocation, options, async (server: HttpServer, cancelPromise: ManualPromise<void>) => {
await installRootRedirect(server, [], { ...options, webApp: 'uiMode.html' }); await installRootRedirect(server, [], { ...options, webApp: 'uiMode.html' });
if (options.host !== undefined || options.port !== undefined) { if (options.host !== undefined || options.port !== undefined) {
await openTraceInBrowser(server.urlPrefix()); await openTraceInBrowser(server.urlPrefix('human-readable'));
} else { } else {
const page = await openTraceViewerApp(server.urlPrefix(), 'chromium', { const page = await openTraceViewerApp(server.urlPrefix('precise'), 'chromium', {
headless: isUnderTest() && process.env.PWTEST_HEADED_FOR_TEST !== '1', headless: isUnderTest() && process.env.PWTEST_HEADED_FOR_TEST !== '1',
persistentContextOptions: { persistentContextOptions: {
handleSIGINT: false, handleSIGINT: false,
@ -434,7 +435,7 @@ export async function runTestServer(configFile: string | undefined, options: { h
const configLocation = resolveConfigLocation(configFile); const configLocation = resolveConfigLocation(configFile);
return await innerRunTestServer(configLocation, options, async server => { return await innerRunTestServer(configLocation, options, async server => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('Listening on ' + server.urlPrefix().replace('http:', 'ws:') + '/' + server.wsGuid()); console.log('Listening on ' + server.urlPrefix('precise').replace('http:', 'ws:') + '/' + server.wsGuid());
}); });
} }

View file

@ -14,6 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
import { test } from './npmTest'; import { test } from './npmTest';
import fs from 'fs';
import path from 'path';
test('electron should work', async ({ exec, tsc, writeFiles }) => { test('electron should work', async ({ exec, tsc, writeFiles }) => {
await exec('npm i playwright electron@19.0.11'); await exec('npm i playwright electron@19.0.11');
@ -24,3 +26,16 @@ test('electron should work', async ({ exec, tsc, writeFiles }) => {
}); });
await tsc('test.ts'); await tsc('test.ts');
}); });
test('electron should work with special characters in path', async ({ exec, tmpWorkspace }) => {
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30755' });
const folderName = path.join(tmpWorkspace, '!@#$% тест with spaces and 😊');
await exec('npm i playwright electron@19.0.11');
await fs.promises.mkdir(folderName);
for (const file of ['electron-app.js', 'sanity-electron.js'])
await fs.promises.copyFile(path.join(tmpWorkspace, file), path.join(folderName, file));
await exec('node sanity-electron.js', {
cwd: path.join(folderName)
});
});

View file

@ -450,6 +450,18 @@ test('svg role=presentation', async ({ page }) => {
expect.soft(await getNameAndRole(page, 'svg')).toEqual({ role: 'presentation', name: '' }); expect.soft(await getNameAndRole(page, 'svg')).toEqual({ role: 'presentation', name: '' });
}); });
test('should work with form and tricky input names', async ({ page }) => {
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30616' });
await page.setContent(`
<form aria-label="my form">
<input name="tagName" value="hello" title="tagName input">
<input name="localName" value="hello" title="localName input">
</form>
`);
expect.soft(await getNameAndRole(page, 'form')).toEqual({ role: 'form', name: 'my form' });
});
function toArray(x: any): any[] { function toArray(x: any): any[] {
return Array.isArray(x) ? x : [x]; return Array.isArray(x) ? x : [x];
} }

View file

@ -482,36 +482,6 @@ it.describe('page screenshot', () => {
})).toMatchSnapshot('should-mask-inside-iframe.png'); })).toMatchSnapshot('should-mask-inside-iframe.png');
}); });
it('should mask inside <dialog />', async ({ page, server, browserName, isElectron }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/29878' });
it.skip(browserName === 'webkit' && process.platform === 'darwin' && parseInt(os.release().split('.')[0], 10) < 23, 'SDKAlignedBehavior::PopoverAttributeEnabled is only enabled in macOS 14.0+');
it.fixme(isElectron, 'Requires a more recent Electron version');
await page.setViewportSize({ width: 500, height: 500 });
await page.goto(server.PREFIX + '/grid.html');
await page.evaluate(() => {
const elements = document.body.innerHTML;
document.body.innerHTML = '';
// Move all the elements of the body (grid elements) into a <dialog /> which lives on the Top-Layer.
const dialog = document.createElement('dialog');
dialog.style.padding = '0';
dialog.style.margin = '0';
dialog.style.border = 'none';
dialog.style.maxWidth = 'inherit';
dialog.style.maxHeight = 'inherit';
dialog.style.outline = 'none';
document.body.appendChild(dialog);
dialog.innerHTML = elements;
dialog.showModal();
});
expect(await page.screenshot({
mask: [
page.locator('div').nth(5),
page.frameLocator('#frame1').locator('div').nth(12),
],
})).toMatchSnapshot('should-mask-inside-iframe.png');
});
it('should mask in parallel', async ({ page, server }) => { it('should mask in parallel', async ({ page, server }) => {
await page.setViewportSize({ width: 500, height: 500 }); await page.setViewportSize({ width: 500, height: 500 });
await attachFrame(page, 'frame1', server.PREFIX + '/grid.html'); await attachFrame(page, 'frame1', server.PREFIX + '/grid.html');

View file

@ -85,35 +85,6 @@ test('should not reuse context with video if mode=when-possible', async ({ runIn
expect(fs.existsSync(testInfo.outputPath('test-results', 'reuse-two', 'video.webm'))).toBeFalsy(); expect(fs.existsSync(testInfo.outputPath('test-results', 'reuse-two', 'video.webm'))).toBeFalsy();
}); });
test('should reuse context and disable video if mode=force', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'playwright.config.ts': `
export default {
use: { video: 'on' },
};
`,
'reuse.test.ts': `
import { test, expect } from '@playwright/test';
let lastContextGuid;
test('one', async ({ context, page }) => {
lastContextGuid = context._guid;
await page.waitForTimeout(2000);
});
test('two', async ({ context, page }) => {
expect(context._guid).toBe(lastContextGuid);
await page.waitForTimeout(2000);
});
`,
}, { workers: 1 }, { PW_TEST_REUSE_CONTEXT: '1' });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(2);
expect(fs.existsSync(testInfo.outputPath('test-results', 'reuse-one', 'video.webm'))).toBeFalsy();
expect(fs.existsSync(testInfo.outputPath('test-results', 'reuse-two', 'video.webm'))).toBeFalsy();
});
test('should reuse context with trace if mode=when-possible', async ({ runInlineTest }, testInfo) => { test('should reuse context with trace if mode=when-possible', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({ const result = await runInlineTest({
'playwright.config.ts': ` 'playwright.config.ts': `

View file

@ -38,8 +38,8 @@ const test = baseTest.extend<{
await use(async (reportFolder?: string) => { await use(async (reportFolder?: string) => {
reportFolder ??= test.info().outputPath('playwright-report'); reportFolder ??= test.info().outputPath('playwright-report');
server = startHtmlReportServer(reportFolder) as HttpServer; server = startHtmlReportServer(reportFolder) as HttpServer;
const location = await server.start(); await server.start();
await page.goto(location); await page.goto(server.urlPrefix('precise'));
}); });
await server?.stop(); await server?.stop();
} }
@ -1292,12 +1292,18 @@ test('support PLAYWRIGHT_BLOB_OUTPUT_FILE environment variable', async ({ runInl
test('math 1 @smoke', async ({}) => {}); test('math 1 @smoke', async ({}) => {});
`, `,
}; };
const defaultDir = test.info().outputPath('blob-report');
fs.mkdirSync(defaultDir, { recursive: true });
const file = path.join(defaultDir, 'some.file');
fs.writeFileSync(file, 'content');
await runInlineTest(files, { shard: `1/2` }, { PLAYWRIGHT_BLOB_OUTPUT_FILE: 'subdir/report-one.zip' }); await runInlineTest(files, { shard: `1/2` }, { PLAYWRIGHT_BLOB_OUTPUT_FILE: 'subdir/report-one.zip' });
await runInlineTest(files, { shard: `2/2` }, { PLAYWRIGHT_BLOB_OUTPUT_FILE: test.info().outputPath('subdir/report-two.zip') }); await runInlineTest(files, { shard: `2/2` }, { PLAYWRIGHT_BLOB_OUTPUT_FILE: test.info().outputPath('subdir/report-two.zip') });
const reportDir = test.info().outputPath('subdir'); const reportDir = test.info().outputPath('subdir');
const reportFiles = await fs.promises.readdir(reportDir); const reportFiles = await fs.promises.readdir(reportDir);
expect(reportFiles.sort()).toEqual(['report-one.zip', 'report-two.zip']); expect(reportFiles.sort()).toEqual(['report-one.zip', 'report-two.zip']);
expect(fs.existsSync(file), 'Default directory should not be cleaned up if output file is specified.').toBe(true);
}); });
test('keep projects with same name different bot name separate', async ({ runInlineTest, mergeReports, showReport, page }) => { test('keep projects with same name different bot name separate', async ({ runInlineTest, mergeReports, showReport, page }) => {

View file

@ -29,8 +29,8 @@ const test = baseTest.extend<{ showReport: (reportFolder?: string) => Promise<vo
await use(async (reportFolder?: string) => { await use(async (reportFolder?: string) => {
reportFolder ??= testInfo.outputPath('playwright-report'); reportFolder ??= testInfo.outputPath('playwright-report');
server = startHtmlReportServer(reportFolder) as HttpServer; server = startHtmlReportServer(reportFolder) as HttpServer;
const location = await server.start(); await server.start();
await page.goto(location); await page.goto(server.urlPrefix('precise'));
}); });
await server?.stop(); await server?.stop();
} }

View file

@ -288,4 +288,42 @@ test.describe('report location', () => {
expect(result.passed).toBe(1); expect(result.passed).toBe(1);
expect(fs.existsSync(testInfo.outputPath('foo', 'bar', 'baz', 'my-report.json'))).toBe(true); expect(fs.existsSync(testInfo.outputPath('foo', 'bar', 'baz', 'my-report.json'))).toBe(true);
}); });
test('support PLAYWRIGHT_JSON_OUTPUT_FILE', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'foo/package.json': `{ "name": "foo" }`,
// unused config along "search path"
'foo/bar/playwright.config.js': `
module.exports = { projects: [ {} ] };
`,
'foo/bar/baz/tests/a.spec.js': `
import { test, expect } from '@playwright/test';
const fs = require('fs');
test('pass', ({}, testInfo) => {
});
`
}, { 'reporter': 'json' }, { 'PW_TEST_HTML_REPORT_OPEN': 'never', 'PLAYWRIGHT_JSON_OUTPUT_FILE': '../my-report.json' }, {
cwd: 'foo/bar/baz/tests',
});
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
expect(fs.existsSync(testInfo.outputPath('foo', 'bar', 'baz', 'my-report.json'))).toBe(true);
});
test('support PLAYWRIGHT_JSON_OUTPUT_DIR and PLAYWRIGHT_JSON_OUTPUT_NAME', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'playwright.config.js': `
module.exports = { projects: [ {} ] };
`,
'tests/a.spec.js': `
import { test, expect } from '@playwright/test';
const fs = require('fs');
test('pass', ({}, testInfo) => {
});
`
}, { 'reporter': 'json' }, { 'PLAYWRIGHT_JSON_OUTPUT_DIR': 'foo/bar', 'PLAYWRIGHT_JSON_OUTPUT_NAME': 'baz/my-report.json' });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
expect(fs.existsSync(testInfo.outputPath('foo', 'bar', 'baz', 'my-report.json'))).toBe(true);
});
}); });

View file

@ -504,6 +504,44 @@ for (const useIntermediateMergeReport of [false, true] as const) {
expect(result.passed).toBe(1); expect(result.passed).toBe(1);
expect(fs.existsSync(testInfo.outputPath('foo', 'bar', 'baz', 'my-report.xml'))).toBe(true); expect(fs.existsSync(testInfo.outputPath('foo', 'bar', 'baz', 'my-report.xml'))).toBe(true);
}); });
test('support PLAYWRIGHT_JUNIT_OUTPUT_FILE', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'foo/package.json': `{ "name": "foo" }`,
// unused config along "search path"
'foo/bar/playwright.config.js': `
module.exports = { projects: [ {} ] };
`,
'foo/bar/baz/tests/a.spec.js': `
import { test, expect } from '@playwright/test';
const fs = require('fs');
test('pass', ({}, testInfo) => {
});
`
}, { 'reporter': 'junit,line' }, { 'PLAYWRIGHT_JUNIT_OUTPUT_FILE': '../my-report.xml' }, {
cwd: 'foo/bar/baz/tests',
});
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
expect(fs.existsSync(testInfo.outputPath('foo', 'bar', 'baz', 'my-report.xml'))).toBe(true);
});
test('support PLAYWRIGHT_JUNIT_OUTPUT_DIR and PLAYWRIGHT_JUNIT_OUTPUT_NAME', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'playwright.config.js': `
module.exports = { projects: [ {} ] };
`,
'tests/a.spec.js': `
import { test, expect } from '@playwright/test';
const fs = require('fs');
test('pass', ({}, testInfo) => {
});
`
}, { 'reporter': 'junit,line' }, { 'PLAYWRIGHT_JUNIT_OUTPUT_DIR': 'foo/bar', 'PLAYWRIGHT_JUNIT_OUTPUT_NAME': 'baz/my-report.xml' });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
expect(fs.existsSync(testInfo.outputPath('foo', 'bar', 'baz', 'my-report.xml'))).toBe(true);
});
}); });
test('testsuites time is test run wall time', async ({ runInlineTest }) => { test('testsuites time is test run wall time', async ({ runInlineTest }) => {