Compare commits

...

25 commits

Author SHA1 Message Date
Yury Semikhatsky 0e130fa8ed
chore: mark v1.45.3 (#31799) 2024-07-22 10:29:01 -07:00
Yury Semikhatsky 2f17a270b6
cherry-pick(#31781): chore: show error when opening newer trace with … (#31797)
…old viewer

Reference: https://github.com/microsoft/playwright-java/issues/1617
2024-07-22 10:01:28 -07:00
Yury Semikhatsky b129abab04
cherry-pick(#31768): fix(trace viewer): library-only trace should not merge actions (#31780)
This PR cherry-picks two trace viewer fixes: 
cherry-pick(#31768): fix(trace viewer): library-only trace should not
merge actions
cherry-pick(#31564): fix(trace): do not corrupt test runner actions when
no library trace is present

Reference: https://github.com/microsoft/playwright-java/issues/1617
2024-07-22 10:00:20 -07:00
Max Schmitt d8a5f3b331
chore: mark v1.45.2 (#31702) 2024-07-16 12:45:01 +02:00
Dmitry Gozman 51fa0fffbd
Revert "chore: move artifacts recording to TestLifecycleInstrumentation (#30935)" (#31694)
This reverts commit ba5b460444.
2024-07-15 08:13:33 -07:00
Max Schmitt 80f44f3c16
cherry-pick(#31666): Revert "fix(chromium): pass --enable-gpu when running headless (#30937)"
This reverts commit d0b052e1e0.
2024-07-12 20:13:09 +02:00
Max Schmitt 4bd2d2502e cherry-pick(#31511): docs(clock): update time types in Python/.NET 2024-07-03 10:48:18 +02:00
Max Schmitt e8989f83d9
chore: mark v1.45.1 (#31516) 2024-07-02 19:27:06 +02:00
Max Schmitt 1d94caa103 cherry-pick(#31496): docs(release-notes): fix .NET snippets 2024-07-01 19:21:31 +02:00
Max Schmitt 5be51684fb cherry-pick(#31504): fix(ct): export package.json 2024-07-01 19:21:11 +02:00
Max Schmitt 0fe7a102f3 cherry-pick(#31437): fix(electron): tracing with @playwright/test 2024-07-01 19:20:53 +02:00
Max Schmitt 4ec4ccf7f3 cherry-pick(#31401): chore: .NET generator fixes 2024-07-01 09:44:49 +02:00
Pavel Feldman 3b4d32e731 cherry-pick(#31458): fix(utility): create utility world when web security is disabled 2024-06-27 09:30:20 -07:00
Pavel Feldman 4ccaef69be cherry-pick(#31426): fix(runner): do not run beforeEach hooks upon skip modifier 2024-06-26 16:16:16 -07:00
Dmitry Gozman 4f3f6eecae cherry-pick(#31421): docs: release notes for 1.45 2024-06-24 12:24:29 -07:00
Yury Semikhatsky d557b7b256
cherry-pick(#31420): docs(java): correctly parse time (#31422) 2024-06-24 12:24:13 -07:00
Dmitry Gozman 1368bca737 cherry-pick(#31419): docs: deprecate handle option in exposeBinding 2024-06-24 11:30:17 -07:00
Playwright Service 6c3fc49cf3 cherry-pick(#31404): feat(chromium): roll to r1124
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2024-06-21 14:20:38 +02:00
Max Schmitt 4ae151f42a cherry-pick(#31340): docs: add guide for print dialogs 2024-06-19 18:12:10 +02:00
Max Schmitt a11585f0d6 cherry-pick(#31331): docs(test-parameterize): improve forEach example 2024-06-19 18:11:48 +02:00
Max Schmitt afcf8d296c cherry-pick(#31350): docs: fix typo in 1.45 release notes 2024-06-19 09:27:21 +02:00
Max Schmitt 599d074f71 cherry-pick(#31356): fix(clock): throw for invalid date
Fixes https://github.com/microsoft/playwright/issues/31354
2024-06-19 09:26:47 +02:00
Max Schmitt 245179103e cherry-pick(#31357): fix(clock): under reused context
We uninstall all the setInitScript but forgot to mark `installed` as
`false`.

Fixes https://github.com/microsoft/playwright/issues/31353
2024-06-19 09:26:26 +02:00
Yury Semikhatsky 56ca1e13e1
cherry-pick(#31369): docs: use long for time in milliseconds (#31371)
In Java and .NET int is not enough to store millis since epoch.
2024-06-18 11:02:07 -07:00
Dmitry Gozman 816c003189
chore: set version to v1.45.0 (#31327) 2024-06-14 19:43:52 -07:00
70 changed files with 956 additions and 704 deletions

View file

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

View file

@ -748,83 +748,6 @@ await page.SetContentAsync("<script>\n" +
await page.GetByRole(AriaRole.Button).ClickAsync();
```
An example of passing an element handle:
```js
await context.exposeBinding('clicked', async (source, element) => {
console.log(await element.textContent());
}, { handle: true });
await page.setContent(`
<script>
document.addEventListener('click', event => window.clicked(event.target));
</script>
<div>Click me</div>
<div>Or click me</div>
`);
```
```java
context.exposeBinding("clicked", (source, args) -> {
ElementHandle element = (ElementHandle) args[0];
System.out.println(element.textContent());
return null;
}, new BrowserContext.ExposeBindingOptions().setHandle(true));
page.setContent("" +
"<script>\n" +
" document.addEventListener('click', event => window.clicked(event.target));\n" +
"</script>\n" +
"<div>Click me</div>\n" +
"<div>Or click me</div>\n");
```
```python async
async def print(source, element):
print(await element.text_content())
await context.expose_binding("clicked", print, handle=true)
await page.set_content("""
<script>
document.addEventListener('click', event => window.clicked(event.target));
</script>
<div>Click me</div>
<div>Or click me</div>
""")
```
```python sync
def print(source, element):
print(element.text_content())
context.expose_binding("clicked", print, handle=true)
page.set_content("""
<script>
document.addEventListener('click', event => window.clicked(event.target));
</script>
<div>Click me</div>
<div>Or click me</div>
""")
```
```csharp
var result = new TaskCompletionSource<string>();
var page = await Context.NewPageAsync();
await Context.ExposeBindingAsync("clicked", async (BindingSource _, IJSHandle t) =>
{
return result.TrySetResult(await t.AsElement().TextContentAsync());
});
await page.SetContentAsync("<script>\n" +
" document.addEventListener('click', event => window.clicked(event.target));\n" +
"</script>\n" +
"<div>Click me</div>\n" +
"<div>Or click me</div>\n");
await page.ClickAsync("div");
// Note: it makes sense to await the result here, because otherwise, the context
// gets closed and the binding function will throw an exception.
Assert.AreEqual("Click me", await result.Task);
```
### param: BrowserContext.exposeBinding.name
* since: v1.8
- `name` <[string]>
@ -839,6 +762,7 @@ Callback function that will be called in the Playwright's context.
### option: BrowserContext.exposeBinding.handle
* since: v1.8
* deprecated: This option will be removed in the future.
- `handle` <[boolean]>
Whether to pass the argument as a handle, instead of passing by value. When passing a handle, only one argument is

View file

@ -41,7 +41,7 @@ await page.Clock.FastForwardAsync("30:00");
### param: Clock.fastForward.ticks
* since: v1.45
- `ticks` <[int]|[string]>
- `ticks` <[long]|[string]>
Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.
@ -64,8 +64,23 @@ Install fake implementations for the following time-related functions:
Fake timers are used to manually control the flow of time in tests. They allow you to advance time, fire timers, and control the behavior of time-dependent functions. See [`method: Clock.runFor`] and [`method: Clock.fastForward`] for more information.
### option: Clock.install.time
* langs: js, java
* since: v1.45
- `time` <[int]|[string]|[Date]>
- `time` <[long]|[string]|[Date]>
Time to initialize with, current system time by default.
### option: Clock.install.time
* langs: python
* since: v1.45
- `time` <[float]|[string]|[Date]>
Time to initialize with, current system time by default.
### option: Clock.install.time
* langs: csharp
* since: v1.45
- `time` <[string]|[Date]>
Time to initialize with, current system time by default.
@ -103,7 +118,7 @@ await page.Clock.RunForAsync("30:00");
### param: Clock.runFor.ticks
* since: v1.45
- `ticks` <[int]|[string]>
- `ticks` <[long]|[string]>
Time may be the number of milliseconds to advance the clock by or a human-readable string. Valid string formats are "08" for eight seconds, "01:00" for one minute and "02:34:10" for two hours, 34 minutes and ten seconds.
@ -136,7 +151,8 @@ page.clock.pause_at("2020-02-02")
```
```java
page.clock().pauseAt(Instant.parse("2020-02-02"));
SimpleDateFormat format = new SimpleDateFormat("yyy-MM-dd");
page.clock().pauseAt(format.parse("2020-02-02"));
page.clock().pauseAt("2020-02-02");
```
@ -146,9 +162,25 @@ await page.Clock.PauseAtAsync("2020-02-02");
```
### param: Clock.pauseAt.time
* langs: js, java
* since: v1.45
- `time` <[int]|[string]|[Date]>
- `time` <[long]|[string]|[Date]>
Time to pause at.
### param: Clock.pauseAt.time
* langs: python
* since: v1.45
- `time` <[float]|[string]|[Date]>
Time to pause at.
### param: Clock.pauseAt.time
* langs: csharp
* since: v1.45
- `time` <[Date]|[string]>
Time to pause at.
## async method: Clock.resume
* since: v1.45
@ -182,8 +214,8 @@ page.clock.set_fixed_time("2020-02-02")
```
```java
page.clock().setFixedTime(Instant.now());
page.clock().setFixedTime(Instant.parse("2020-02-02"));
page.clock().setFixedTime(new Date());
page.clock().setFixedTime(new SimpleDateFormat("yyy-MM-dd").parse("2020-02-02"));
page.clock().setFixedTime("2020-02-02");
```
@ -194,8 +226,23 @@ await page.Clock.SetFixedTimeAsync("2020-02-02");
```
### param: Clock.setFixedTime.time
* langs: js, java
* since: v1.45
- `time` <[int]|[string]|[Date]>
- `time` <[long]|[string]|[Date]>
Time to be set in milliseconds.
### param: Clock.setFixedTime.time
* langs: python
* since: v1.45
- `time` <[float]|[string]|[Date]>
Time to be set.
### param: Clock.setFixedTime.time
* langs: csharp
* since: v1.45
- `time` <[string]|[Date]>
Time to be set.
@ -225,8 +272,8 @@ page.clock.set_system_time("2020-02-02")
```
```java
page.clock().setSystemTime(Instant.now());
page.clock().setSystemTime(Instant.parse("2020-02-02"));
page.clock().setSystemTime(new Date());
page.clock().setSystemTime(new SimpleDateFormat("yyy-MM-dd").parse("2020-02-02"));
page.clock().setSystemTime("2020-02-02");
```
@ -237,5 +284,22 @@ await page.Clock.SetSystemTimeAsync("2020-02-02");
```
### param: Clock.setSystemTime.time
* langs: js, java
* since: v1.45
- `time` <[int]|[string]|[Date]>
- `time` <[long]|[string]|[Date]>
Time to be set in milliseconds.
### param: Clock.setSystemTime.time
* langs: python
* since: v1.45
- `time` <[float]|[string]|[Date]>
Time to be set.
### param: Clock.setSystemTime.time
* langs: csharp
* since: v1.45
- `time` <[string]|[Date]>
Time to be set.

View file

@ -1817,80 +1817,6 @@ class PageExamples
}
```
An example of passing an element handle:
```js
await page.exposeBinding('clicked', async (source, element) => {
console.log(await element.textContent());
}, { handle: true });
await page.setContent(`
<script>
document.addEventListener('click', event => window.clicked(event.target));
</script>
<div>Click me</div>
<div>Or click me</div>
`);
```
```java
page.exposeBinding("clicked", (source, args) -> {
ElementHandle element = (ElementHandle) args[0];
System.out.println(element.textContent());
return null;
}, new Page.ExposeBindingOptions().setHandle(true));
page.setContent("" +
"<script>\n" +
" document.addEventListener('click', event => window.clicked(event.target));\n" +
"</script>\n" +
"<div>Click me</div>\n" +
"<div>Or click me</div>\n");
```
```python async
async def print(source, element):
print(await element.text_content())
await page.expose_binding("clicked", print, handle=true)
await page.set_content("""
<script>
document.addEventListener('click', event => window.clicked(event.target));
</script>
<div>Click me</div>
<div>Or click me</div>
""")
```
```python sync
def print(source, element):
print(element.text_content())
page.expose_binding("clicked", print, handle=true)
page.set_content("""
<script>
document.addEventListener('click', event => window.clicked(event.target));
</script>
<div>Click me</div>
<div>Or click me</div>
""")
```
```csharp
var result = new TaskCompletionSource<string>();
await page.ExposeBindingAsync("clicked", async (BindingSource _, IJSHandle t) =>
{
return result.TrySetResult(await t.AsElement().TextContentAsync());
});
await page.SetContentAsync("<script>\n" +
" document.addEventListener('click', event => window.clicked(event.target));\n" +
"</script>\n" +
"<div>Click me</div>\n" +
"<div>Or click me</div>\n");
await page.ClickAsync("div");
Console.WriteLine(await result.Task);
```
### param: Page.exposeBinding.name
* since: v1.8
- `name` <[string]>
@ -1905,6 +1831,7 @@ Callback function that will be called in the Playwright's context.
### option: Page.exposeBinding.handle
* since: v1.8
* deprecated: This option will be removed in the future.
- `handle` <[boolean]>
Whether to pass the argument as a handle, instead of passing by value. When passing a handle, only one argument is

View file

@ -118,13 +118,14 @@ expect(page.get_by_test_id("current-time")).to_have_text("2/2/2024, 10:30:00 AM"
```java
// Initialize clock with some time before the test time and let the page load
// naturally. `Date.now` will progress as the timers fire.
page.clock().install(new Clock.InstallOptions().setTime(Instant.parse("2024-02-02T08:00:00")));
SimpleDateFormat format = new SimpleDateFormat("yyy-MM-dd'T'HH:mm:ss");
page.clock().install(new Clock.InstallOptions().setTime(format.parse("2024-02-02T08:00:00")));
page.navigate("http://localhost:3333");
Locator locator = page.getByTestId("current-time");
// Pretend that the user closed the laptop lid and opened it again at 10am.
// Pause the time once reached that point.
page.clock().pauseAt(Instant.parse("2024-02-02T10:00:00"));
page.clock().pauseAt(format.parse("2024-02-02T10:00:00"));
// Assert the page state.
assertThat(locator).hasText("2/2/2024, 10:00:00 AM");
@ -315,15 +316,16 @@ expect(locator).to_have_text("2/2/2024, 10:00:02 AM")
```
```java
SimpleDateFormat format = new SimpleDateFormat("yyy-MM-dd'T'HH:mm:ss");
// Initialize clock with a specific time, let the page load naturally.
page.clock().install(new Clock.InstallOptions()
.setTime(Instant.parse("2024-02-02T08:00:00")));
.setTime(format.parse("2024-02-02T08:00:00")));
page.navigate("http://localhost:3333");
Locator locator = page.getByTestId("current-time");
// Pause the time flow, stop the timers, you now have manual control
// over the page time.
page.clock().pauseAt(Instant.parse("2024-02-02T10:00:00"));
page.clock().pauseAt(format.parse("2024-02-02T10:00:00"));
assertThat(locator).hasText("2/2/2024, 10:00:00 AM");
// Tick through time manually, firing all timers in the process.

View file

@ -5,7 +5,7 @@ title: "Dialogs"
## Introduction
Playwright can interact with the web page dialogs such as [`alert`](https://developer.mozilla.org/en-US/docs/Web/API/Window/alert), [`confirm`](https://developer.mozilla.org/en-US/docs/Web/API/Window/confirm), [`prompt`](https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt) as well as [`beforeunload`](https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event) confirmation.
Playwright can interact with the web page dialogs such as [`alert`](https://developer.mozilla.org/en-US/docs/Web/API/Window/alert), [`confirm`](https://developer.mozilla.org/en-US/docs/Web/API/Window/confirm), [`prompt`](https://developer.mozilla.org/en-US/docs/Web/API/Window/prompt) as well as [`beforeunload`](https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event) confirmation. For print dialogs, see [Print](#print-dialogs).
## alert(), confirm(), prompt() dialogs
@ -126,3 +126,55 @@ Page.Dialog += async (_, dialog) =>
};
await Page.CloseAsync(new() { RunBeforeUnload = true });
```
## Print dialogs
In order to assert that a print dialog via [`window.print`](https://developer.mozilla.org/en-US/docs/Web/API/Window/print) was triggered, you can use the following snippet:
```js
await page.goto('<url>');
await page.evaluate('(() => {window.waitForPrintDialog = new Promise(f => window.print = f);})()');
await page.getByText('Print it!').click();
await page.waitForFunction('window.waitForPrintDialog');
```
```java
page.navigate("<url>");
page.evaluate("(() => {window.waitForPrintDialog = new Promise(f => window.print = f);})()");
page.getByText("Print it!").click();
page.waitForFunction("window.waitForPrintDialog");
```
```python async
await page.goto("<url>")
await page.evaluate("(() => {window.waitForPrintDialog = new Promise(f => window.print = f);})()")
await page.get_by_text("Print it!").click()
await page.wait_for_function("window.waitForPrintDialog")
```
```python sync
page.goto("<url>")
page.evaluate("(() => {window.waitForPrintDialog = new Promise(f => window.print = f);})()")
page.get_by_text("Print it!").click()
page.wait_for_function("window.waitForPrintDialog")
```
```csharp
await Page.GotoAsync("<url>");
await Page.EvaluateAsync("(() => {window.waitForPrintDialog = new Promise(f => window.print = f);})()");
await Page.GetByText("Print it!").ClickAsync();
await Page.WaitForFunctionAsync("window.waitForPrintDialog");
```
This will wait for the print dialog to be opened after the button is clicked.
Make sure to evaluate the script before clicking the button / after the page is loaded.

View file

@ -4,6 +4,69 @@ title: "Release notes"
toc_max_heading_level: 2
---
## Version 1.45
### Clock
Utilizing the new [Clock] API allows to manipulate and control time within tests to verify time-related behavior. This API covers many common scenarios, including:
* testing with predefined time;
* keeping consistent time and timers;
* monitoring inactivity;
* ticking through time manually.
```csharp
// Initialize clock with some time before the test time and let the page load naturally.
// `Date.now` will progress as the timers fire.
await Page.Clock.InstallAsync(new()
{
TimeDate = new DateTime(2024, 2, 2, 8, 0, 0)
});
await Page.GotoAsync("http://localhost:3333");
// Pretend that the user closed the laptop lid and opened it again at 10am.
// Pause the time once reached that point.
await Page.Clock.PauseAtAsync(new DateTime(2024, 2, 2, 10, 0, 0));
// Assert the page state.
await Expect(Page.GetByTestId("current-time")).ToHaveTextAsync("2/2/2024, 10:00:00 AM");
// Close the laptop lid again and open it at 10:30am.
await Page.Clock.FastForwardAsync("30:00");
await Expect(Page.GetByTestId("current-time")).ToHaveTextAsync("2/2/2024, 10:30:00 AM");
```
See [the clock guide](./clock.md) for more details.
### Miscellaneous
- Method [`method: Locator.setInputFiles`] now supports uploading a directory for `<input type=file webkitdirectory>` elements.
```csharp
await page.GetByLabel("Upload directory").SetInputFilesAsync("mydir");
```
- Multiple methods like [`method: Locator.click`] or [`method: Locator.press`] now support a `ControlOrMeta` modifier key. This key maps to `Meta` on macOS and maps to `Control` on Windows and Linux.
```csharp
// Press the common keyboard shortcut Control+S or Meta+S to trigger a "Save" operation.
await page.Keyboard.PressAsync("ControlOrMeta+S");
```
- New property `httpCredentials.send` in [`method: APIRequest.newContext`] that allows to either always send the `Authorization` header or only send it in response to `401 Unauthorized`.
- Playwright now supports Chromium, Firefox and WebKit on Ubuntu 24.04.
- v1.45 is the last release to receive WebKit update for macOS 12 Monterey. Please update macOS to keep using the latest WebKit.
### Browser Versions
* Chromium 127.0.6533.5
* Mozilla Firefox 127.0
* WebKit 17.4
This version was also tested against the following stable channels:
* Google Chrome 126
* Microsoft Edge 126
## Version 1.44
### New APIs

View file

@ -4,6 +4,67 @@ title: "Release notes"
toc_max_heading_level: 2
---
## Version 1.45
### Clock
Utilizing the new [Clock] API allows to manipulate and control time within tests to verify time-related behavior. This API covers many common scenarios, including:
* testing with predefined time;
* keeping consistent time and timers;
* monitoring inactivity;
* ticking through time manually.
```java
// Initialize clock with some time before the test time and let the page load
// naturally. `Date.now` will progress as the timers fire.
page.clock().install(new Clock.InstallOptions().setTime("2024-02-02T08:00:00"));
page.navigate("http://localhost:3333");
Locator locator = page.getByTestId("current-time");
// Pretend that the user closed the laptop lid and opened it again at 10am.
// Pause the time once reached that point.
page.clock().pauseAt("2024-02-02T10:00:00");
// Assert the page state.
assertThat(locator).hasText("2/2/2024, 10:00:00 AM");
// Close the laptop lid again and open it at 10:30am.
page.clock().fastForward("30:00");
assertThat(locator).hasText("2/2/2024, 10:30:00 AM");
```
See [the clock guide](./clock.md) for more details.
### Miscellaneous
- Method [`method: Locator.setInputFiles`] now supports uploading a directory for `<input type=file webkitdirectory>` elements.
```java
page.getByLabel("Upload directory").setInputFiles(Paths.get("mydir"));
```
- Multiple methods like [`method: Locator.click`] or [`method: Locator.press`] now support a `ControlOrMeta` modifier key. This key maps to `Meta` on macOS and maps to `Control` on Windows and Linux.
```java
// Press the common keyboard shortcut Control+S or Meta+S to trigger a "Save" operation.
page.keyboard.press("ControlOrMeta+S");
```
- New property `httpCredentials.send` in [`method: APIRequest.newContext`] that allows to either always send the `Authorization` header or only send it in response to `401 Unauthorized`.
- Playwright now supports Chromium, Firefox and WebKit on Ubuntu 24.04.
- v1.45 is the last release to receive WebKit update for macOS 12 Monterey. Please update macOS to keep using the latest WebKit.
### Browser Versions
* Chromium 127.0.6533.5
* Mozilla Firefox 127.0
* WebKit 17.4
This version was also tested against the following stable channels:
* Google Chrome 126
* Microsoft Edge 126
## Version 1.44
### New APIs

View file

@ -43,10 +43,10 @@ See [the clock guide](./clock.md) for more details.
```sh
# Avoid TTY features that output ANSI control sequences
PLAYWRIGHT_FORCE_TTY=0 npx playwrigh test
PLAYWRIGHT_FORCE_TTY=0 npx playwright test
# Enable TTY features, assuming a terminal width 80
PLAYWRIGHT_FORCE_TTY=80 npx playwrigh test
PLAYWRIGHT_FORCE_TTY=80 npx playwright test
```
- New options [`property: TestConfig.respectGitIgnore`] and [`property: TestProject.respectGitIgnore`] control whether files matching `.gitignore` patterns are excluded when searching for tests.

View file

@ -4,6 +4,66 @@ title: "Release notes"
toc_max_heading_level: 2
---
## Version 1.45
### Clock
Utilizing the new [Clock] API allows to manipulate and control time within tests to verify time-related behavior. This API covers many common scenarios, including:
* testing with predefined time;
* keeping consistent time and timers;
* monitoring inactivity;
* ticking through time manually.
```python
# Initialize clock with some time before the test time and let the page load
# naturally. `Date.now` will progress as the timers fire.
page.clock.install(time=datetime.datetime(2024, 2, 2, 8, 0, 0))
page.goto("http://localhost:3333")
# Pretend that the user closed the laptop lid and opened it again at 10am.
# Pause the time once reached that point.
page.clock.pause_at(datetime.datetime(2024, 2, 2, 10, 0, 0))
# Assert the page state.
expect(page.get_by_test_id("current-time")).to_have_text("2/2/2024, 10:00:00 AM")
# Close the laptop lid again and open it at 10:30am.
page.clock.fast_forward("30:00")
expect(page.get_by_test_id("current-time")).to_have_text("2/2/2024, 10:30:00 AM")
```
See [the clock guide](./clock.md) for more details.
### Miscellaneous
- Method [`method: Locator.setInputFiles`] now supports uploading a directory for `<input type=file webkitdirectory>` elements.
```python
page.get_by_label("Upload directory").set_input_files('mydir')
```
- Multiple methods like [`method: Locator.click`] or [`method: Locator.press`] now support a `ControlOrMeta` modifier key. This key maps to `Meta` on macOS and maps to `Control` on Windows and Linux.
```python
# Press the common keyboard shortcut Control+S or Meta+S to trigger a "Save" operation.
page.keyboard.press("ControlOrMeta+S")
```
- New property `httpCredentials.send` in [`method: APIRequest.newContext`] that allows to either always send the `Authorization` header or only send it in response to `401 Unauthorized`.
- Playwright now supports Chromium, Firefox and WebKit on Ubuntu 24.04.
- v1.45 is the last release to receive WebKit update for macOS 12 Monterey. Please update macOS to keep using the latest WebKit.
### Browser Versions
* Chromium 127.0.6533.5
* Mozilla Firefox 127.0
* WebKit 17.4
This version was also tested against the following stable channels:
* Google Chrome 126
* Microsoft Edge 126
## Version 1.44
### New APIs

View file

@ -9,13 +9,61 @@ You can either parameterize tests on a test level or on a project level.
## Parameterized Tests
```js title="example.spec.ts"
const people = ['Alice', 'Bob'];
for (const name of people) {
test(`testing with ${name}`, async () => {
// ...
});
[
{ name: 'Alice', expected: 'Hello, Alice!' },
{ name: 'Bob', expected: 'Hello, Bob!' },
{ name: 'Charlie', expected: 'Hello, Charlie!' },
].forEach(({ name, expected }) => {
// You can also do it with test.describe() or with multiple tests as long the test name is unique.
}
test(`testing with ${name}`, async ({ page }) => {
await page.goto(`https://example.com/greet?name=${name}`);
await expect(page.getByRole('heading')).toHaveText(expected);
});
});
```
### Before and after hooks
Most of the time you should put `beforeEach`, `beforeAll`, `afterEach` and `afterAll` hooks outside of `forEach`, so that hooks are executed just once:
```js title="example.spec.ts"
test.beforeEach(async ({ page }) => {
// ...
});
test.afterEach(async ({ page }) => {
// ...
});
[
{ name: 'Alice', expected: 'Hello, Alice!' },
{ name: 'Bob', expected: 'Hello, Bob!' },
{ name: 'Charlie', expected: 'Hello, Charlie!' },
].forEach(({ name, expected }) => {
test(`testing with ${name}`, async ({ page }) => {
await page.goto(`https://example.com/greet?name=${name}`);
await expect(page.getByRole('heading')).toHaveText(expected);
});
});
```
If you want to have hooks for each test, you can put them inside a `describe()` - so they are executed for each iteration / each invidual test:
```js title="example.spec.ts"
[
{ name: 'Alice', expected: 'Hello, Alice!' },
{ name: 'Bob', expected: 'Hello, Bob!' },
{ name: 'Charlie', expected: 'Hello, Charlie!' },
].forEach(({ name, expected }) => {
test.describe(() => {
test.beforeEach(async ({ page }) => {
await page.goto(`https://example.com/greet?name=${name}`);
});
test(`testing with ${expected}`, async ({ page }) => {
await expect(page.getByRole('heading')).toHaveText(expected);
});
});
});
```
## Parameterized Projects

68
package-lock.json generated
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,9 +3,9 @@
"browsers": [
{
"name": "chromium",
"revision": "1123",
"revision": "1124",
"installByDefault": true,
"browserVersion": "127.0.6533.5"
"browserVersion": "127.0.6533.17"
},
{
"name": "chromium-tip-of-tree",

View file

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

View file

@ -58,6 +58,8 @@ function parseTime(time: string | number | Date): { timeNumber?: number, timeStr
return { timeNumber: time };
if (typeof time === 'string')
return { timeString: time };
if (!isFinite(time.getTime()))
throw new Error(`Invalid date: ${time}`);
return { timeNumber: time.getTime() };
}

View file

@ -34,8 +34,8 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
}
async start(options: { name?: string, title?: string, snapshots?: boolean, screenshots?: boolean, sources?: boolean, _live?: boolean } = {}) {
await this._wrapApiCall(async () => {
this._includeSources = !!options.sources;
this._includeSources = !!options.sources;
const traceName = await this._wrapApiCall(async () => {
await this._channel.tracingStart({
name: options.name,
snapshots: options.snapshots,
@ -43,15 +43,14 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
live: options._live,
});
const response = await this._channel.tracingStartChunk({ name: options.name, title: options.title });
await this._startCollectingStacks(response.traceName);
return response.traceName;
}, true);
await this._startCollectingStacks(traceName);
}
async startChunk(options: { name?: string, title?: string } = {}) {
await this._wrapApiCall(async () => {
const { traceName } = await this._channel.tracingStartChunk(options);
await this._startCollectingStacks(traceName);
}, true);
const { traceName } = await this._channel.tracingStartChunk(options);
await this._startCollectingStacks(traceName);
}
private async _startCollectingStacks(traceName: string) {

View file

@ -24,6 +24,7 @@ import type { Download } from './download';
import type * as frames from './frames';
import { helper } from './helper';
import * as network from './network';
import { InitScript } from './page';
import type { PageDelegate } from './page';
import { Page, PageBinding } from './page';
import type { Progress, ProgressController } from './progress';
@ -84,7 +85,7 @@ export abstract class BrowserContext extends SdkObject {
private _customCloseHandler?: () => Promise<any>;
readonly _tempDirs: string[] = [];
private _settingStorageState = false;
readonly initScripts: string[] = [];
readonly initScripts: InitScript[] = [];
private _routesInFlight = new Set<network.Route>();
private _debugger!: Debugger;
_closeReason: string | undefined;
@ -216,6 +217,7 @@ export abstract class BrowserContext extends SdkObject {
await this._resetStorage();
await this._removeExposedBindings();
await this._removeInitScripts();
this.clock.markAsUninstalled();
// TODO: following can be optimized to not perform noops.
if (this._options.permissions)
await this.grantPermissions(this._options.permissions);
@ -265,7 +267,7 @@ export abstract class BrowserContext extends SdkObject {
protected abstract doGrantPermissions(origin: string, permissions: string[]): Promise<void>;
protected abstract doClearPermissions(): Promise<void>;
protected abstract doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise<void>;
protected abstract doAddInitScript(expression: string): Promise<void>;
protected abstract doAddInitScript(initScript: InitScript): Promise<void>;
protected abstract doRemoveInitScripts(): Promise<void>;
protected abstract doExposeBinding(binding: PageBinding): Promise<void>;
protected abstract doRemoveExposedBindings(): Promise<void>;
@ -402,9 +404,10 @@ export abstract class BrowserContext extends SdkObject {
this._options.httpCredentials = { username, password: password || '' };
}
async addInitScript(script: string) {
this.initScripts.push(script);
await this.doAddInitScript(script);
async addInitScript(source: string) {
const initScript = new InitScript(source);
this.initScripts.push(initScript);
await this.doAddInitScript(initScript);
}
async _removeInitScripts(): Promise<void> {

View file

@ -305,12 +305,9 @@ export class Chromium extends BrowserType {
if (os.platform() === 'darwin') {
// See https://github.com/microsoft/playwright/issues/7362
chromeArguments.push('--enable-use-zoom-for-dsf=false');
}
if (options.headless) {
// See https://bugs.chromium.org/p/chromium/issues/detail?id=1407025.
// See also https://github.com/microsoft/playwright/issues/30585
// and chromium fix at https://issues.chromium.org/issues/338414704.
chromeArguments.push('--enable-gpu');
if (options.headless)
chromeArguments.push('--use-angle');
}
if (options.devtools)

View file

@ -21,7 +21,7 @@ import { Browser } from '../browser';
import { assertBrowserContextIsNotOwned, BrowserContext, verifyGeolocation } from '../browserContext';
import { assert, createGuid } from '../../utils';
import * as network from '../network';
import type { PageBinding, PageDelegate, Worker } from '../page';
import type { InitScript, PageBinding, PageDelegate, Worker } from '../page';
import { Page } from '../page';
import { Frame } from '../frames';
import type { Dialog } from '../dialog';
@ -486,9 +486,9 @@ export class CRBrowserContext extends BrowserContext {
await (sw as CRServiceWorker).updateHttpCredentials();
}
async doAddInitScript(source: string) {
async doAddInitScript(initScript: InitScript) {
for (const page of this.pages())
await (page._delegate as CRPage).addInitScript(source);
await (page._delegate as CRPage).addInitScript(initScript);
}
async doRemoveInitScripts() {

View file

@ -26,7 +26,7 @@ import * as dom from '../dom';
import * as frames from '../frames';
import { helper } from '../helper';
import * as network from '../network';
import type { PageBinding, PageDelegate } from '../page';
import type { InitScript, PageBinding, PageDelegate } from '../page';
import { Page, Worker } from '../page';
import type { Progress } from '../progress';
import type * as types from '../types';
@ -256,8 +256,8 @@ export class CRPage implements PageDelegate {
return this._go(+1);
}
async addInitScript(source: string, world: types.World = 'main'): Promise<void> {
await this._forAllFrameSessions(frame => frame._evaluateOnNewDocument(source, world));
async addInitScript(initScript: InitScript, world: types.World = 'main'): Promise<void> {
await this._forAllFrameSessions(frame => frame._evaluateOnNewDocument(initScript, world));
}
async removeInitScripts() {
@ -511,6 +511,20 @@ class FrameSession {
this._addRendererListeners();
}
const localFrames = this._isMainFrame() ? this._page.frames() : [this._page._frameManager.frame(this._targetId)!];
for (const frame of localFrames) {
// Note: frames might be removed before we send these.
this._client._sendMayFail('Page.createIsolatedWorld', {
frameId: frame._id,
grantUniveralAccess: true,
worldName: UTILITY_WORLD_NAME,
});
for (const binding of this._crPage._browserContext._pageBindings.values())
frame.evaluateExpression(binding.source).catch(e => {});
for (const initScript of this._crPage._browserContext.initScripts)
frame.evaluateExpression(initScript.source).catch(e => {});
}
const isInitialEmptyPage = this._isMainFrame() && this._page.mainFrame().url() === ':';
if (isInitialEmptyPage) {
// Ignore lifecycle events, worlds and bindings for the initial empty page. It is never the final page
@ -520,20 +534,6 @@ class FrameSession {
this._eventListeners.push(eventsHelper.addEventListener(this._client, 'Page.lifecycleEvent', event => this._onLifecycleEvent(event)));
});
} else {
const localFrames = this._isMainFrame() ? this._page.frames() : [this._page._frameManager.frame(this._targetId)!];
for (const frame of localFrames) {
// Note: frames might be removed before we send these.
this._client._sendMayFail('Page.createIsolatedWorld', {
frameId: frame._id,
grantUniveralAccess: true,
worldName: UTILITY_WORLD_NAME,
});
for (const binding of this._crPage._browserContext._pageBindings.values())
frame.evaluateExpression(binding.source).catch(e => {});
for (const source of this._crPage._browserContext.initScripts)
frame.evaluateExpression(source).catch(e => {});
}
this._firstNonInitialNavigationCommittedFulfill();
this._eventListeners.push(eventsHelper.addEventListener(this._client, 'Page.lifecycleEvent', event => this._onLifecycleEvent(event)));
}
@ -575,10 +575,10 @@ class FrameSession {
promises.push(this._updateFileChooserInterception(true));
for (const binding of this._crPage._page.allBindings())
promises.push(this._initBinding(binding));
for (const source of this._crPage._browserContext.initScripts)
promises.push(this._evaluateOnNewDocument(source, 'main'));
for (const source of this._crPage._page.initScripts)
promises.push(this._evaluateOnNewDocument(source, 'main'));
for (const initScript of this._crPage._browserContext.initScripts)
promises.push(this._evaluateOnNewDocument(initScript, 'main'));
for (const initScript of this._crPage._page.initScripts)
promises.push(this._evaluateOnNewDocument(initScript, 'main'));
if (screencastOptions)
promises.push(this._startVideoRecording(screencastOptions));
}
@ -1099,9 +1099,9 @@ class FrameSession {
await this._client.send('Page.setInterceptFileChooserDialog', { enabled }).catch(() => {}); // target can be closed.
}
async _evaluateOnNewDocument(source: string, world: types.World): Promise<void> {
async _evaluateOnNewDocument(initScript: InitScript, world: types.World): Promise<void> {
const worldName = world === 'utility' ? UTILITY_WORLD_NAME : undefined;
const { identifier } = await this._client.send('Page.addScriptToEvaluateOnNewDocument', { source, worldName });
const { identifier } = await this._client.send('Page.addScriptToEvaluateOnNewDocument', { source: initScript.source, worldName });
this._evaluateOnNewDocumentIdentifiers.push(identifier);
}

View file

@ -26,6 +26,10 @@ export class Clock {
this._browserContext = browserContext;
}
markAsUninstalled() {
this._scriptInstalled = false;
}
async fastForward(ticks: number | string) {
await this._installIfNeeded();
const ticksMillis = parseTicks(ticks);
@ -144,5 +148,8 @@ function parseTime(epoch: string | number | undefined): number {
return 0;
if (typeof epoch === 'number')
return epoch;
return new Date(epoch).getTime();
const parsed = new Date(epoch);
if (!isFinite(parsed.getTime()))
throw new Error(`Invalid date: ${epoch}`);
return parsed.getTime();
}

View file

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

View file

@ -21,7 +21,7 @@ import type { BrowserOptions } from '../browser';
import { Browser } from '../browser';
import { assertBrowserContextIsNotOwned, BrowserContext, verifyGeolocation } from '../browserContext';
import * as network from '../network';
import type { Page, PageBinding, PageDelegate } from '../page';
import type { InitScript, Page, PageBinding, PageDelegate } from '../page';
import type { ConnectionTransport } from '../transport';
import type * as types from '../types';
import type * as channels from '@protocol/channels';
@ -352,8 +352,8 @@ export class FFBrowserContext extends BrowserContext {
await this._browser.session.send('Browser.setHTTPCredentials', { browserContextId: this._browserContextId, credentials });
}
async doAddInitScript(source: string) {
await this._browser.session.send('Browser.setInitScripts', { browserContextId: this._browserContextId, scripts: this.initScripts.map(script => ({ script })) });
async doAddInitScript(initScript: InitScript) {
await this._browser.session.send('Browser.setInitScripts', { browserContextId: this._browserContextId, scripts: this.initScripts.map(script => ({ script: script.source })) });
}
async doRemoveInitScripts() {

View file

@ -21,6 +21,7 @@ import type * as frames from '../frames';
import type { RegisteredListener } from '../../utils/eventsHelper';
import { eventsHelper } from '../../utils/eventsHelper';
import type { PageBinding, PageDelegate } from '../page';
import { InitScript } from '../page';
import { Page, Worker } from '../page';
import type * as types from '../types';
import { getAccessibilityTree } from './ffAccessibility';
@ -56,7 +57,7 @@ export class FFPage implements PageDelegate {
private _eventListeners: RegisteredListener[];
private _workers = new Map<string, { frameId: string, session: FFSession }>();
private _screencastId: string | undefined;
private _initScripts: { script: string, worldName?: string }[] = [];
private _initScripts: { initScript: InitScript, worldName?: string }[] = [];
constructor(session: FFSession, browserContext: FFBrowserContext, opener: FFPage | null) {
this._session = session;
@ -113,7 +114,7 @@ export class FFPage implements PageDelegate {
});
// Ideally, we somehow ensure that utility world is created before Page.ready arrives, but currently it is racy.
// Therefore, we can end up with an initialized page without utility world, although very unlikely.
this.addInitScript('', UTILITY_WORLD_NAME).catch(e => this._markAsError(e));
this.addInitScript(new InitScript(''), UTILITY_WORLD_NAME).catch(e => this._markAsError(e));
}
potentiallyUninitializedPage(): Page {
@ -406,9 +407,9 @@ export class FFPage implements PageDelegate {
return success;
}
async addInitScript(script: string, worldName?: string): Promise<void> {
this._initScripts.push({ script, worldName });
await this._session.send('Page.setInitScripts', { scripts: this._initScripts });
async addInitScript(initScript: InitScript, worldName?: string): Promise<void> {
this._initScripts.push({ initScript, worldName });
await this._session.send('Page.setInitScripts', { scripts: this._initScripts.map(s => ({ script: s.initScript.source, worldName: s.worldName })) });
}
async removeInitScripts() {

View file

@ -31,7 +31,7 @@ import * as accessibility from './accessibility';
import { FileChooser } from './fileChooser';
import type { Progress } from './progress';
import { ProgressController } from './progress';
import { LongStandingScope, assert, isError } from '../utils';
import { LongStandingScope, assert, createGuid, isError } from '../utils';
import { ManualPromise } from '../utils/manualPromise';
import { debugLogger } from '../utils/debugLogger';
import type { ImageComparatorOptions } from '../utils/comparators';
@ -56,7 +56,7 @@ export interface PageDelegate {
goForward(): Promise<boolean>;
exposeBinding(binding: PageBinding): Promise<void>;
removeExposedBindings(): Promise<void>;
addInitScript(source: string): Promise<void>;
addInitScript(initScript: InitScript): Promise<void>;
removeInitScripts(): Promise<void>;
closePage(runBeforeUnload: boolean): Promise<void>;
potentiallyUninitializedPage(): Page;
@ -154,7 +154,7 @@ export class Page extends SdkObject {
private _emulatedMedia: Partial<EmulatedMedia> = {};
private _interceptFileChooser = false;
private readonly _pageBindings = new Map<string, PageBinding>();
readonly initScripts: string[] = [];
readonly initScripts: InitScript[] = [];
readonly _screenshotter: Screenshotter;
readonly _frameManager: frames.FrameManager;
readonly accessibility: accessibility.Accessibility;
@ -527,8 +527,9 @@ export class Page extends SdkObject {
}
async addInitScript(source: string) {
this.initScripts.push(source);
await this._delegate.addInitScript(source);
const initScript = new InitScript(source);
this.initScripts.push(initScript);
await this._delegate.addInitScript(initScript);
}
async _removeInitScripts() {
@ -905,6 +906,22 @@ function addPageBinding(bindingName: string, needsHandle: boolean, utilityScript
(globalThis as any)[bindingName].__installed = true;
}
export class InitScript {
readonly source: string;
constructor(source: string) {
const guid = createGuid();
this.source = `(() => {
globalThis.__pwInitScripts = globalThis.__pwInitScripts || {};
const hasInitScript = globalThis.__pwInitScripts[${JSON.stringify(guid)}];
if (hasInitScript)
return;
globalThis.__pwInitScripts[${JSON.stringify(guid)}] = true;
${source}
})();`;
}
}
class FrameThrottler {
private _acks: (() => void)[] = [];
private _defaultInterval: number;

View file

@ -22,7 +22,7 @@ import type { RegisteredListener } from '../../utils/eventsHelper';
import { assert } from '../../utils';
import { eventsHelper } from '../../utils/eventsHelper';
import * as network from '../network';
import type { Page, PageBinding, PageDelegate } from '../page';
import type { InitScript, Page, PageBinding, PageDelegate } from '../page';
import type { ConnectionTransport } from '../transport';
import type * as types from '../types';
import type * as channels from '@protocol/channels';
@ -315,7 +315,7 @@ export class WKBrowserContext extends BrowserContext {
await (page._delegate as WKPage).updateHttpCredentials();
}
async doAddInitScript(source: string) {
async doAddInitScript(initScript: InitScript) {
for (const page of this.pages())
await (page._delegate as WKPage)._updateBootstrapScript();
}

View file

@ -30,7 +30,7 @@ import { eventsHelper } from '../../utils/eventsHelper';
import { helper } from '../helper';
import type { JSHandle } from '../javascript';
import * as network from '../network';
import type { PageBinding, PageDelegate } from '../page';
import type { InitScript, PageBinding, PageDelegate } from '../page';
import { Page } from '../page';
import type { Progress } from '../progress';
import type * as types from '../types';
@ -777,7 +777,7 @@ export class WKPage implements PageDelegate {
await this._updateBootstrapScript();
}
async addInitScript(script: string): Promise<void> {
async addInitScript(initScript: InitScript): Promise<void> {
await this._updateBootstrapScript();
}
@ -797,8 +797,8 @@ export class WKPage implements PageDelegate {
for (const binding of this._page.allBindings())
scripts.push(binding.source);
scripts.push(...this._browserContext.initScripts);
scripts.push(...this._page.initScripts);
scripts.push(...this._browserContext.initScripts.map(s => s.source));
scripts.push(...this._page.initScripts.map(s => s.source));
return scripts.join(';\n');
}

View file

@ -814,21 +814,6 @@ export interface Page {
* })();
* ```
*
* An example of passing an element handle:
*
* ```js
* await page.exposeBinding('clicked', async (source, element) => {
* console.log(await element.textContent());
* }, { handle: true });
* await page.setContent(`
* <script>
* document.addEventListener('click', event => window.clicked(event.target));
* </script>
* <div>Click me</div>
* <div>Or click me</div>
* `);
* ```
*
* @param name Name of the function on the window object.
* @param callback Callback function that will be called in the Playwright's context.
* @param options
@ -875,21 +860,6 @@ export interface Page {
* })();
* ```
*
* An example of passing an element handle:
*
* ```js
* await page.exposeBinding('clicked', async (source, element) => {
* console.log(await element.textContent());
* }, { handle: true });
* await page.setContent(`
* <script>
* document.addEventListener('click', event => window.clicked(event.target));
* </script>
* <div>Click me</div>
* <div>Or click me</div>
* `);
* ```
*
* @param name Name of the function on the window object.
* @param callback Callback function that will be called in the Playwright's context.
* @param options
@ -7637,21 +7607,6 @@ export interface BrowserContext {
* })();
* ```
*
* An example of passing an element handle:
*
* ```js
* await context.exposeBinding('clicked', async (source, element) => {
* console.log(await element.textContent());
* }, { handle: true });
* await page.setContent(`
* <script>
* document.addEventListener('click', event => window.clicked(event.target));
* </script>
* <div>Click me</div>
* <div>Or click me</div>
* `);
* ```
*
* @param name Name of the function on the window object.
* @param callback Callback function that will be called in the Playwright's context.
* @param options
@ -7693,21 +7648,6 @@ export interface BrowserContext {
* })();
* ```
*
* An example of passing an element handle:
*
* ```js
* await context.exposeBinding('clicked', async (source, element) => {
* console.log(await element.textContent());
* }, { handle: true });
* await page.setContent(`
* <script>
* document.addEventListener('click', event => window.clicked(event.target));
* </script>
* <div>Click me</div>
* <div>Or click me</div>
* `);
* ```
*
* @param name Name of the function on the window object.
* @param callback Callback function that will be called in the Playwright's context.
* @param options
@ -17312,7 +17252,7 @@ export interface Clock {
* await page.clock.pauseAt('2020-02-02');
* ```
*
* @param time
* @param time Time to pause at.
*/
pauseAt(time: number|string|Date): Promise<void>;
@ -17347,7 +17287,7 @@ export interface Clock {
* await page.clock.setFixedTime('2020-02-02');
* ```
*
* @param time Time to be set.
* @param time Time to be set in milliseconds.
*/
setFixedTime(time: number|string|Date): Promise<void>;
@ -17362,7 +17302,7 @@ export interface Clock {
* await page.clock.setSystemTime('2020-02-02');
* ```
*
* @param time
* @param time Time to be set in milliseconds.
*/
setSystemTime(time: number|string|Date): Promise<void>;
}

View file

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

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/experimental-ct-react",
"version": "1.45.0-next",
"version": "1.45.3",
"description": "Playwright Component Testing for React",
"repository": {
"type": "git",
@ -26,10 +26,11 @@
"./hooks": {
"types": "./hooks.d.ts",
"default": "./hooks.mjs"
}
},
"./package.json": "./package.json"
},
"dependencies": {
"@playwright/experimental-ct-core": "1.45.0-next",
"@playwright/experimental-ct-core": "1.45.3",
"@vitejs/plugin-react": "^4.2.1"
},
"bin": {

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/experimental-ct-react17",
"version": "1.45.0-next",
"version": "1.45.3",
"description": "Playwright Component Testing for React",
"repository": {
"type": "git",
@ -26,10 +26,11 @@
"./hooks": {
"types": "./hooks.d.ts",
"default": "./hooks.mjs"
}
},
"./package.json": "./package.json"
},
"dependencies": {
"@playwright/experimental-ct-core": "1.45.0-next",
"@playwright/experimental-ct-core": "1.45.3",
"@vitejs/plugin-react": "^4.2.1"
},
"bin": {

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/experimental-ct-solid",
"version": "1.45.0-next",
"version": "1.45.3",
"description": "Playwright Component Testing for Solid",
"repository": {
"type": "git",
@ -26,10 +26,11 @@
"./hooks": {
"types": "./hooks.d.ts",
"default": "./hooks.mjs"
}
},
"./package.json": "./package.json"
},
"dependencies": {
"@playwright/experimental-ct-core": "1.45.0-next",
"@playwright/experimental-ct-core": "1.45.3",
"vite-plugin-solid": "^2.7.0"
},
"devDependencies": {

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/experimental-ct-svelte",
"version": "1.45.0-next",
"version": "1.45.3",
"description": "Playwright Component Testing for Svelte",
"repository": {
"type": "git",
@ -26,10 +26,11 @@
"./hooks": {
"types": "./hooks.d.ts",
"default": "./hooks.mjs"
}
},
"./package.json": "./package.json"
},
"dependencies": {
"@playwright/experimental-ct-core": "1.45.0-next",
"@playwright/experimental-ct-core": "1.45.3",
"@sveltejs/vite-plugin-svelte": "^3.0.1"
},
"devDependencies": {

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/experimental-ct-vue",
"version": "1.45.0-next",
"version": "1.45.3",
"description": "Playwright Component Testing for Vue",
"repository": {
"type": "git",
@ -26,10 +26,11 @@
"./hooks": {
"types": "./hooks.d.ts",
"default": "./hooks.mjs"
}
},
"./package.json": "./package.json"
},
"dependencies": {
"@playwright/experimental-ct-core": "1.45.0-next",
"@playwright/experimental-ct-core": "1.45.3",
"@vitejs/plugin-vue": "^4.2.1"
},
"bin": {

View file

@ -1,6 +1,6 @@
{
"name": "@playwright/experimental-ct-vue2",
"version": "1.45.0-next",
"version": "1.45.3",
"description": "Playwright Component Testing for Vue2",
"repository": {
"type": "git",
@ -26,10 +26,11 @@
"./hooks": {
"types": "./hooks.d.ts",
"default": "./hooks.mjs"
}
},
"./package.json": "./package.json"
},
"dependencies": {
"@playwright/experimental-ct-core": "1.45.0-next",
"@playwright/experimental-ct-core": "1.45.3",
"@vitejs/plugin-vue2": "^2.2.0"
},
"devDependencies": {

View file

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

View file

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

View file

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

View file

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

View file

@ -42,19 +42,3 @@ export function setIsWorkerProcess() {
export function isWorkerProcess() {
return _isWorkerProcess;
}
export interface TestLifecycleInstrumentation {
onTestBegin?(): Promise<void>;
onTestFunctionEnd?(): Promise<void>;
onTestEnd?(): Promise<void>;
}
let _testLifecycleInstrumentation: TestLifecycleInstrumentation | undefined;
export function setTestLifecycleInstrumentation(instrumentation: TestLifecycleInstrumentation | undefined) {
_testLifecycleInstrumentation = instrumentation;
}
export function testLifecycleInstrumentation() {
return _testLifecycleInstrumentation;
}

View file

@ -16,14 +16,15 @@
import * as fs from 'fs';
import * as path from 'path';
import type { APIRequestContext, BrowserContext, Browser, BrowserContextOptions, LaunchOptions, Page, Tracing, Video, PageScreenshotOptions } from 'playwright-core';
import type { APIRequestContext, BrowserContext, Browser, BrowserContextOptions, LaunchOptions, Page, Tracing, Video } from 'playwright-core';
import * as playwrightLibrary from 'playwright-core';
import { createGuid, debugMode, addInternalStackPrefix, isString, asLocator, jsonStringifyForceASCII } from 'playwright-core/lib/utils';
import type { Fixtures, PlaywrightTestArgs, PlaywrightTestOptions, PlaywrightWorkerArgs, PlaywrightWorkerOptions, ScreenshotMode, TestInfo, TestType, VideoMode } from '../types/test';
import type { TestInfoImpl } from './worker/testInfo';
import { rootTestType } from './common/testType';
import type { ContextReuseMode } from './common/config';
import type { ClientInstrumentation, ClientInstrumentationListener } from '../../playwright-core/src/client/clientInstrumentation';
import { currentTestInfo, setTestLifecycleInstrumentation, type TestLifecycleInstrumentation } from './common/globals';
import { currentTestInfo } from './common/globals';
export { expect } from './matchers/expect';
export const _baseTest: TestType<{}, {}> = rootTestType.test;
@ -44,12 +45,11 @@ if ((process as any)['__pw_initiator__']) {
type TestFixtures = PlaywrightTestArgs & PlaywrightTestOptions & {
_combinedContextOptions: BrowserContextOptions,
_setupContextOptions: void;
_setupArtifacts: void;
_contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>;
};
type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & {
// Same as "playwright", but exposed so that our internal tests can override it.
_playwrightImpl: PlaywrightWorkerArgs['playwright'];
_browserOptions: LaunchOptions;
_optionContextReuseMode: ContextReuseMode,
_optionConnectOptions: PlaywrightWorkerOptions['connectOptions'],
@ -59,14 +59,9 @@ type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & {
const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
defaultBrowserType: ['chromium', { scope: 'worker', option: true }],
browserName: [({ defaultBrowserType }, use) => use(defaultBrowserType), { scope: 'worker', option: true }],
_playwrightImpl: [({}, use) => use(require('playwright-core')), { scope: 'worker' }],
playwright: [async ({ _playwrightImpl, screenshot }, use) => {
await connector.setPlaywright(_playwrightImpl, screenshot);
await use(_playwrightImpl);
await connector.setPlaywright(undefined, screenshot);
playwright: [async ({}, use) => {
await use(require('playwright-core'));
}, { scope: 'worker', _hideStep: true } as any],
headless: [({ launchOptions }, use) => use(launchOptions.headless ?? true), { scope: 'worker', option: true }],
channel: [({ launchOptions }, use) => use(launchOptions.channel), { scope: 'worker', option: true }],
launchOptions: [{}, { scope: 'worker', option: true }],
@ -227,7 +222,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
_setupContextOptions: [async ({ playwright, _combinedContextOptions, actionTimeout, navigationTimeout, testIdAttribute }, use, testInfo) => {
if (testIdAttribute)
playwright.selectors.setTestIdAttribute(testIdAttribute);
playwrightLibrary.selectors.setTestIdAttribute(testIdAttribute);
testInfo.snapshotSuffix = process.platform;
if (debugMode())
testInfo.setTimeout(0);
@ -248,6 +243,58 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
}
}, { auto: 'all-hooks-included', _title: 'context configuration' } as any],
_setupArtifacts: [async ({ playwright, screenshot }, use, testInfo) => {
const artifactsRecorder = new ArtifactsRecorder(playwright, tracing().artifactsDir(), screenshot);
await artifactsRecorder.willStartTest(testInfo as TestInfoImpl);
const csiListener: ClientInstrumentationListener = {
onApiCallBegin: (apiName: string, params: Record<string, any>, frames: StackFrame[], userData: any, out: { stepId?: string }) => {
const testInfo = currentTestInfo();
if (!testInfo || apiName.includes('setTestIdAttribute'))
return { userObject: null };
const step = testInfo._addStep({
location: frames[0] as any,
category: 'pw:api',
title: renderApiCall(apiName, params),
apiName,
params,
});
userData.userObject = step;
out.stepId = step.stepId;
},
onApiCallEnd: (userData: any, error?: Error) => {
const step = userData.userObject;
step?.complete({ error });
},
onWillPause: () => {
currentTestInfo()?.setTimeout(0);
},
runAfterCreateBrowserContext: async (context: BrowserContext) => {
await artifactsRecorder?.didCreateBrowserContext(context);
const testInfo = currentTestInfo();
if (testInfo)
attachConnectedHeaderIfNeeded(testInfo, context.browser());
},
runAfterCreateRequestContext: async (context: APIRequestContext) => {
await artifactsRecorder?.didCreateRequestContext(context);
},
runBeforeCloseBrowserContext: async (context: BrowserContext) => {
await artifactsRecorder?.willCloseBrowserContext(context);
},
runBeforeCloseRequestContext: async (context: APIRequestContext) => {
await artifactsRecorder?.willCloseRequestContext(context);
},
};
const clientInstrumentation = (playwright as any)._instrumentation as ClientInstrumentation;
clientInstrumentation.addListener(csiListener);
await use();
clientInstrumentation.removeListener(csiListener);
await artifactsRecorder.didFinishTest();
}, { auto: 'all-hooks-included', _title: 'trace recording' } as any],
_contextFactory: [async ({ browser, video, _reuseContext }, use, testInfo) => {
const testInfoImpl = testInfo as TestInfoImpl;
const videoMode = normalizeVideoMode(video);
@ -424,7 +471,7 @@ class ArtifactsRecorder {
private _playwright: Playwright;
private _artifactsDir: string;
private _screenshotMode: ScreenshotMode;
private _screenshotOptions: { mode: ScreenshotMode } & Pick<PageScreenshotOptions, 'fullPage' | 'omitBackground'> | undefined;
private _screenshotOptions: { mode: ScreenshotMode } & Pick<playwrightLibrary.PageScreenshotOptions, 'fullPage' | 'omitBackground'> | undefined;
private _temporaryScreenshots: string[] = [];
private _temporaryArtifacts: string[] = [];
private _reusedContexts = new Set<BrowserContext>();
@ -449,6 +496,7 @@ class ArtifactsRecorder {
async willStartTest(testInfo: TestInfoImpl) {
this._testInfo = testInfo;
testInfo._onDidFinishTestFunction = () => this.didFinishTestFunction();
// Since beforeAll(s), test and afterAll(s) reuse the same TestInfo, make sure we do not
// overwrite previous screenshots.
@ -597,7 +645,7 @@ class ArtifactsRecorder {
if ((tracing as any)[this._startedCollectingArtifacts])
return;
(tracing as any)[this._startedCollectingArtifacts] = true;
if (this._testInfo._tracing.traceOptions())
if (this._testInfo._tracing.traceOptions() && (tracing as any)[kTracingStarted])
await tracing.stopChunk({ path: this._testInfo._tracing.generateNextTraceRecordingPath() });
}
}
@ -630,101 +678,6 @@ function tracing() {
return (test.info() as TestInfoImpl)._tracing;
}
class InstrumentationConnector implements TestLifecycleInstrumentation, ClientInstrumentationListener {
private _playwright: PlaywrightWorkerArgs['playwright'] | undefined;
private _screenshot: ScreenshotOption = 'off';
private _artifactsRecorder: ArtifactsRecorder | undefined;
private _testIsRunning = false;
constructor() {
setTestLifecycleInstrumentation(this);
}
async setPlaywright(playwright: PlaywrightWorkerArgs['playwright'] | undefined, screenshot: ScreenshotOption) {
if (this._playwright) {
if (this._testIsRunning) {
// When "playwright" is destroyed during a test, collect artifacts immediately.
await this.onTestEnd();
}
const clientInstrumentation = (this._playwright as any)._instrumentation as ClientInstrumentation;
clientInstrumentation.removeListener(this);
}
this._playwright = playwright;
this._screenshot = screenshot;
if (this._playwright) {
const clientInstrumentation = (this._playwright as any)._instrumentation as ClientInstrumentation;
clientInstrumentation.addListener(this);
if (this._testIsRunning) {
// When "playwright" is created during a test, wire it up immediately.
await this.onTestBegin();
}
}
}
async onTestBegin() {
this._testIsRunning = true;
if (this._playwright) {
this._artifactsRecorder = new ArtifactsRecorder(this._playwright, tracing().artifactsDir(), this._screenshot);
await this._artifactsRecorder.willStartTest(currentTestInfo() as TestInfoImpl);
}
}
async onTestFunctionEnd() {
await this._artifactsRecorder?.didFinishTestFunction();
}
async onTestEnd() {
await this._artifactsRecorder?.didFinishTest();
this._artifactsRecorder = undefined;
this._testIsRunning = false;
}
onApiCallBegin(apiName: string, params: Record<string, any>, frames: StackFrame[], userData: any, out: { stepId?: string }) {
const testInfo = currentTestInfo();
if (!testInfo || apiName.includes('setTestIdAttribute'))
return { userObject: null };
const step = testInfo._addStep({
location: frames[0] as any,
category: 'pw:api',
title: renderApiCall(apiName, params),
apiName,
params,
});
userData.userObject = step;
out.stepId = step.stepId;
}
onApiCallEnd(userData: any, error?: Error) {
const step = userData.userObject;
step?.complete({ error });
}
onWillPause() {
currentTestInfo()?.setTimeout(0);
}
async runAfterCreateBrowserContext(context: BrowserContext) {
await this._artifactsRecorder?.didCreateBrowserContext(context);
const testInfo = currentTestInfo();
if (testInfo)
attachConnectedHeaderIfNeeded(testInfo, context.browser());
}
async runAfterCreateRequestContext(context: APIRequestContext) {
await this._artifactsRecorder?.didCreateRequestContext(context);
}
async runBeforeCloseBrowserContext(context: BrowserContext) {
await this._artifactsRecorder?.willCloseBrowserContext(context);
}
async runBeforeCloseRequestContext(context: APIRequestContext) {
await this._artifactsRecorder?.willCloseRequestContext(context);
}
}
const connector = new InstrumentationConnector();
export const test = _baseTest.extend<TestFixtures, WorkerFixtures>(playwrightFixtures);
export { defineConfig } from './common/configLoader';

View file

@ -68,6 +68,7 @@ export class TestInfoImpl implements TestInfo {
readonly _projectInternal: FullProjectInternal;
readonly _configInternal: FullConfigInternal;
private readonly _steps: TestStepInternal[] = [];
_onDidFinishTestFunction: (() => Promise<void>) | undefined;
private readonly _stages: TestStage[] = [];
_hasNonRetriableError = false;
_hasUnhandledError = false;

View file

@ -17,7 +17,7 @@
import { colors } from 'playwright-core/lib/utilsBundle';
import { debugTest, relativeFilePath, serializeError } from '../util';
import { type TestBeginPayload, type TestEndPayload, type RunPayload, type DonePayload, type WorkerInitParams, type TeardownErrorsPayload, stdioChunkToParams } from '../common/ipc';
import { setCurrentTestInfo, setIsWorkerProcess, testLifecycleInstrumentation } from '../common/globals';
import { setCurrentTestInfo, setIsWorkerProcess } from '../common/globals';
import { deserializeConfig } from '../common/configLoader';
import type { Suite, TestCase } from '../common/test';
import type { Annotation, FullConfigInternal, FullProjectInternal } from '../common/config';
@ -304,11 +304,10 @@ export class WorkerMain extends ProcessRunner {
if (this._lastRunningTests.length > 10)
this._lastRunningTests.shift();
let shouldRunAfterEachHooks = false;
const tracingSlot = { timeout: this._project.project.timeout, elapsed: 0 };
testInfo._allowSkips = true;
await testInfo._runAsStage({ title: 'setup and test' }, async () => {
await testInfo._runAsStage({ title: 'start tracing', runnable: { type: 'test', slot: tracingSlot } }, async () => {
await testInfo._runAsStage({ title: 'start tracing', runnable: { type: 'test' } }, async () => {
// Ideally, "trace" would be an config-level option belonging to the
// test runner instead of a fixture belonging to Playwright.
// However, for backwards compatibility, we have to read it from a fixture today.
@ -319,7 +318,6 @@ export class WorkerMain extends ProcessRunner {
if (typeof traceFixtureRegistration.fn === 'function')
throw new Error(`"trace" option cannot be a function`);
await testInfo._tracing.startIfNeeded(traceFixtureRegistration.fn);
await testLifecycleInstrumentation()?.onTestBegin?.();
});
if (this._isStopped || isSkipped) {
@ -374,10 +372,10 @@ export class WorkerMain extends ProcessRunner {
try {
// Run "immediately upon test function finish" callback.
await testInfo._runAsStage({ title: 'on-test-function-finish', runnable: { type: 'test', slot: tracingSlot } }, async () => {
await testLifecycleInstrumentation()?.onTestFunctionEnd?.();
});
await testInfo._runAsStage({ title: 'on-test-function-finish', runnable: { type: 'test', slot: afterHooksSlot } }, async () => testInfo._onDidFinishTestFunction?.());
} catch (error) {
if (error instanceof TimeoutManagerError)
didTimeoutInAfterHooks = true;
firstAfterHooksError = firstAfterHooksError ?? error;
}
@ -460,8 +458,8 @@ export class WorkerMain extends ProcessRunner {
}).catch(() => {}); // Ignore the top-level error, it is already inside TestInfo.errors.
}
const tracingSlot = { timeout: this._project.project.timeout, elapsed: 0 };
await testInfo._runAsStage({ title: 'stop tracing', runnable: { type: 'test', slot: tracingSlot } }, async () => {
await testLifecycleInstrumentation()?.onTestEnd?.();
await testInfo._tracing.stopIfNeeded();
}).catch(() => {}); // Ignore the top-level error, it is already inside TestInfo.errors.
@ -570,6 +568,9 @@ export class WorkerMain extends ProcessRunner {
if (error instanceof TimeoutManagerError)
throw error;
firstError = firstError ?? error;
// Skip in modifier prevents others from running.
if (error instanceof SkipError)
break;
}
}
if (firstError)

View file

@ -19,6 +19,7 @@ import { unwrapPopoutUrl } from './snapshotRenderer';
import { SnapshotServer } from './snapshotServer';
import { TraceModel } from './traceModel';
import { FetchTraceModelBackend, ZipTraceModelBackend } from './traceModelBackends';
import { TraceVersionError } from './traceModernizer';
// @ts-ignore
declare const self: ServiceWorkerGlobalScope;
@ -57,6 +58,8 @@ async function loadTrace(traceUrl: string, traceFileName: string | null, clientI
console.error(error);
if (error?.message?.includes('Cannot find .trace file') && await traceModel.hasEntry('index.html'))
throw new Error('Could not load trace. Did you upload a Playwright HTML report instead? Make sure to extract the archive first and then double-click the index.html file or put it on a web server.');
if (error instanceof TraceVersionError)
throw new Error(`Could not load trace from ${traceFileName || traceUrl}. ${error.message}`);
if (traceFileName)
throw new Error(`Could not load trace from ${traceFileName}. Make sure to upload a valid Playwright trace.`);
throw new Error(`Could not load trace from ${traceUrl}. Make sure a valid Playwright Trace is accessible over this url.`);

View file

@ -22,6 +22,15 @@ import type * as traceV6 from './versions/traceV6';
import type { ActionEntry, ContextEntry, PageEntry } from './entries';
import type { SnapshotStorage } from './snapshotStorage';
export class TraceVersionError extends Error {
constructor(message: string) {
super(message);
this.name = 'TraceVersionError';
}
}
const latestVersion: trace.VERSION = 7;
export class TraceModernizer {
private _contextEntry: ContextEntry;
private _snapshotStorage: SnapshotStorage;
@ -71,6 +80,8 @@ export class TraceModernizer {
const contextEntry = this._contextEntry;
switch (event.type) {
case 'context-options': {
if (event.version > latestVersion)
throw new TraceVersionError('The trace was created by a newer version of Playwright and is not supported by this version of the viewer. Please use latest Playwright to open the trace.');
this._version = event.version;
contextEntry.origin = event.origin;
contextEntry.browserName = event.browserName;
@ -181,9 +192,8 @@ export class TraceModernizer {
let version = this._version || event.version;
if (version === undefined)
return [event];
const lastVersion: trace.VERSION = 7;
let events = [event];
for (; version < lastVersion; ++version)
for (; version < latestVersion; ++version)
events = (this as any)[`_modernize_${version}_to_${version + 1}`].call(this, events);
return events;
}

View file

@ -183,8 +183,8 @@ function mergeActionsAndUpdateTiming(contexts: ContextEntry[]) {
if (traceFileToContexts.size > 1)
makeCallIdsUniqueAcrossTraceFiles(contexts, ++traceFileId);
// Align action times across runner and library contexts within each trace file.
const map = mergeActionsAndUpdateTimingSameTrace(contexts);
result.push(...map.values());
const actions = mergeActionsAndUpdateTimingSameTrace(contexts);
result.push(...actions);
}
result.sort((a1, a2) => {
if (a2.parentId === a1.callId)
@ -211,12 +211,19 @@ function makeCallIdsUniqueAcrossTraceFiles(contexts: ContextEntry[], traceFileId
}
}
function mergeActionsAndUpdateTimingSameTrace(contexts: ContextEntry[]) {
function mergeActionsAndUpdateTimingSameTrace(contexts: ContextEntry[]): ActionTraceEventInContext[] {
const map = new Map<string, ActionTraceEventInContext>();
const libraryContexts = contexts.filter(context => context.origin === 'library');
const testRunnerContexts = contexts.filter(context => context.origin === 'testRunner');
// With library-only or test-runner-only traces there is nothing to match.
if (!testRunnerContexts.length || !libraryContexts.length) {
return contexts.map(context => {
return context.actions.map(action => ({ ...action, context }));
}).flat();
}
// Library actions are replaced with corresponding test runner steps. Matching with
// the test runner steps enables us to find parent steps.
// - In the newer versions the actions are matched by explicit step id stored in the
@ -264,7 +271,7 @@ function mergeActionsAndUpdateTimingSameTrace(contexts: ContextEntry[]) {
map.set(key, { ...action, context });
}
}
return map;
return [...map.values()];
}
function adjustMonotonicTime(contexts: ContextEntry[], monotonicTimeDelta: number) {

Binary file not shown.

Binary file not shown.

View file

@ -30,12 +30,11 @@ export type TestModeTestFixtures = {
export type TestModeWorkerFixtures = {
toImplInWorkerScope: (rpcObject?: any) => any;
playwright: typeof import('@playwright/test');
_playwrightImpl: typeof import('@playwright/test');
};
export const testModeTest = test.extend<TestModeTestFixtures, TestModeWorkerOptions & TestModeWorkerFixtures>({
mode: ['default', { scope: 'worker', option: true }],
_playwrightImpl: [async ({ mode }, run) => {
playwright: [async ({ mode }, run) => {
const testMode = {
'default': new DefaultTestMode(),
'service': new DefaultTestMode(),

View file

@ -15,6 +15,7 @@
*/
import { test } from './npmTest';
import fs from 'fs';
import { expect } from 'packages/playwright-test';
import path from 'path';
test('electron should work', async ({ exec, tsc, writeFiles }) => {
@ -39,3 +40,46 @@ test('electron should work with special characters in path', async ({ exec, tmpW
cwd: path.join(folderName)
});
});
test('should work when wrapped inside @playwright/test and trace is enabled', async ({ exec, tmpWorkspace, writeFiles }) => {
await exec('npm i -D @playwright/test electron@31');
await writeFiles({
'electron-with-tracing.spec.ts': `
import { test, expect, _electron } from '@playwright/test';
test('should work', async ({ trace }) => {
const electronApp = await _electron.launch({ args: [${JSON.stringify(path.join(__dirname, '../electron/electron-window-app.js'))}] });
const window = await electronApp.firstWindow();
if (trace)
await window.context().tracing.start({ screenshots: true, snapshots: true });
await window.goto('data:text/html,<title>Playwright</title><h1>Playwright</h1>');
await expect(window).toHaveTitle(/Playwright/);
await expect(window.getByRole('heading')).toHaveText('Playwright');
const path = test.info().outputPath('electron-trace.zip');
if (trace) {
await window.context().tracing.stop({ path });
test.info().attachments.push({ name: 'trace', path, contentType: 'application/zip' });
}
await electronApp.close();
});
`,
});
const jsonOutputName = test.info().outputPath('report.json');
await exec('npx playwright test --trace=on --reporter=json electron-with-tracing.spec.ts', {
env: { PLAYWRIGHT_JSON_OUTPUT_NAME: jsonOutputName }
});
const traces = [
// our actual trace.
path.join(tmpWorkspace, 'test-results', 'electron-with-tracing-should-work', 'electron-trace.zip'),
// contains the expect() calls
path.join(tmpWorkspace, 'test-results', 'electron-with-tracing-should-work', 'trace.zip'),
];
for (const trace of traces)
expect(fs.existsSync(trace)).toBe(true);
const report = JSON.parse(fs.readFileSync(jsonOutputName, 'utf-8'));
expect(new Set(['trace'])).toEqual(new Set(report.suites[0].specs[0].tests[0].results[0].attachments.map(a => a.name)));
expect(new Set(traces.map(p => fs.realpathSync(p)))).toEqual(new Set(report.suites[0].specs[0].tests[0].results[0].attachments.map(a => a.path)));
});

View file

@ -435,7 +435,7 @@ it('should return error with wrong credentials', async ({ context, server }) =>
expect(response2.status()).toBe(401);
});
it('should support HTTPCredentials.sendImmediately for newContext', async ({ contextFactory, server }) => {
it('should support HTTPCredentials.send for newContext', async ({ contextFactory, server }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30534' });
const context = await contextFactory({
httpCredentials: { username: 'user', password: 'pass', origin: server.PREFIX.toUpperCase(), send: 'always' }
@ -459,7 +459,7 @@ it('should support HTTPCredentials.sendImmediately for newContext', async ({ con
}
});
it('should support HTTPCredentials.sendImmediately for browser.newPage', async ({ contextFactory, server, browser }) => {
it('should support HTTPCredentials.send for browser.newPage', async ({ contextFactory, server, browser }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30534' });
const page = await browser.newPage({
httpCredentials: { username: 'user', password: 'pass', origin: server.PREFIX.toUpperCase(), send: 'always' }

View file

@ -252,6 +252,19 @@ test('should reset tracing', async ({ reusedContext, trace }, testInfo) => {
expect(error.message).toContain('Must start tracing before stopping');
});
test('should work with clock emulation', async ({ reusedContext, trace }, testInfo) => {
let context = await reusedContext();
let page = await context.newPage();
await page.clock.setFixedTime(new Date('2020-01-01T00:00:00.000Z'));
expect(await page.evaluate('new Date().toISOString()')).toBe('2020-01-01T00:00:00.000Z');
context = await reusedContext();
page = context.pages()[0];
await page.clock.setFixedTime(new Date('2020-01-01T00:00:00Z'));
expect(await page.evaluate('new Date().toISOString()')).toBe('2020-01-01T00:00:00.000Z');
});
test('should continue issuing events after closing the reused page', async ({ reusedContext, server }) => {
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/24574' });

View file

@ -110,6 +110,7 @@ it('should play audio @smoke', async ({ page, server, browserName, platform }) =
});
it('should support webgl @smoke', async ({ page, browserName, platform }) => {
it.fixme(browserName === 'chromium' && platform === 'darwin' && os.arch() === 'arm64', 'SwiftShader is not available on macOS-arm64 - https://github.com/microsoft/playwright/issues/28216');
const hasWebGL = await page.evaluate(() => {
const canvas = document.createElement('canvas');
return !!canvas.getContext('webgl');
@ -118,7 +119,10 @@ it('should support webgl @smoke', async ({ page, browserName, platform }) => {
});
it('should support webgl 2 @smoke', async ({ page, browserName, headless, isWindows, platform }) => {
it.skip(browserName === 'webkit', 'WebKit doesn\'t have webgl2 enabled yet upstream.');
it.fixme(browserName === 'firefox' && isWindows);
it.fixme(browserName === 'chromium' && !headless, 'chromium doesn\'t like webgl2 when running under xvfb');
it.fixme(browserName === 'chromium' && platform === 'darwin' && os.arch() === 'arm64', 'SwiftShader is not available on macOS-arm64 - https://github.com/microsoft/playwright/issues/28216');
const hasWebGL2 = await page.evaluate(() => {
const canvas = document.createElement('canvas');

View file

@ -0,0 +1,68 @@
/**
* Copyright 2018 Google Inc. All rights reserved.
* Modifications copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { contextTest as it, expect } from '../../config/browserTest';
it.use({
launchOptions: async ({ launchOptions }, use) => {
await use({ ...launchOptions, args: ['--disable-web-security'] });
}
});
it('test utility world in popup w/ --disable-web-security', async ({ page, server }) => {
server.setRoute('/main.html', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/html'
});
res.end(`<a href="${server.PREFIX}/target.html" target="_blank">Click me</a>`);
});
server.setRoute('/target.html', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/html'
});
res.end(`<html></html>`);
});
await page.goto(server.PREFIX + '/main.html');
const page1Promise = page.context().waitForEvent('page');
await page.getByRole('link', { name: 'Click me' }).click();
const page1 = await page1Promise;
await expect(page1).toHaveURL(/target/);
});
it('test init script w/ --disable-web-security', async ({ page, server }) => {
server.setRoute('/main.html', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/html'
});
res.end(`<a href="${server.PREFIX}/target.html" target="_blank">Click me</a>`);
});
server.setRoute('/target.html', (req, res) => {
res.writeHead(200, {
'Content-Type': 'text/html'
});
res.end(`<html></html>`);
});
await page.context().addInitScript('window.injected = 123');
await page.goto(server.PREFIX + '/main.html');
const page1Promise = page.context().waitForEvent('page');
await page.getByRole('link', { name: 'Click me' }).click();
const page1 = await page1Promise;
const value = await page1.evaluate('window.injected');
expect(value).toBe(123);
});

View file

@ -154,7 +154,7 @@ it('should support WWW-Authenticate: Basic', async ({ playwright, server }) => {
expect(credentials).toBe('user:pass');
});
it('should support HTTPCredentials.sendImmediately', async ({ playwright, server }) => {
it('should support HTTPCredentials.send', async ({ playwright, server }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30534' });
const request = await playwright.request.newContext({
httpCredentials: { username: 'user', password: 'pass', origin: server.PREFIX.toUpperCase(), send: 'always' }

View file

@ -120,6 +120,11 @@ test('should open simple trace viewer', async ({ showTraceViewer }) => {
]);
});
test('should complain about newer version of trace in old viewer', async ({ showTraceViewer, asset }, testInfo) => {
const traceViewer = await showTraceViewer([asset('trace-from-the-future.zip')]);
await expect(traceViewer.page.getByText('The trace was created by a newer version of Playwright and is not supported by this version of the viewer.')).toBeVisible();
});
test('should contain action info', async ({ showTraceViewer }) => {
const traceViewer = await showTraceViewer([traceFile]);
await traceViewer.selectAction('locator.click');
@ -1236,3 +1241,14 @@ test('should open snapshot in new browser context', async ({ browser, page, runA
await expect(newPage.getByText('hello')).toBeVisible();
await newPage.close();
});
test('should show similar actions from library-only trace', async ({ showTraceViewer, asset }) => {
const traceViewer = await showTraceViewer([asset('trace-library-1.46.zip')]);
await expect(traceViewer.actionTitles).toHaveText([
/page.setContent/,
/locator.getAttributelocator\('div'\)/,
/locator.isVisiblelocator\('div'\)/,
/locator.getAttributelocator\('div'\)/,
/locator.isVisiblelocator\('div'\)/,
]);
});

View file

@ -245,6 +245,11 @@ it.describe('stubTimers', () => {
expect(await page.evaluate(() => Date.now())).toBe(1400);
});
it('should throw for invalid date', async ({ page }) => {
await expect(page.clock.setSystemTime(new Date('invalid'))).rejects.toThrow('Invalid date: Invalid Date');
await expect(page.clock.setSystemTime('invalid')).rejects.toThrow('clock.setSystemTime: Invalid date: invalid');
});
it('replaces global setTimeout', async ({ page, calls }) => {
await page.evaluate(async () => {
setTimeout(window.stub, 1000);

View file

@ -151,8 +151,10 @@ test('should work with screenshot: on', async ({ runInlineTest }, testInfo) => {
' test-finished-1.png',
'artifacts-shared-shared-failing',
' test-failed-1.png',
' test-failed-2.png',
'artifacts-shared-shared-passing',
' test-finished-1.png',
' test-finished-2.png',
'artifacts-two-contexts',
' test-finished-1.png',
' test-finished-2.png',
@ -183,6 +185,7 @@ test('should work with screenshot: only-on-failure', async ({ runInlineTest }, t
' test-failed-1.png',
'artifacts-shared-shared-failing',
' test-failed-1.png',
' test-failed-2.png',
'artifacts-two-contexts-failing',
' test-failed-1.png',
' test-failed-2.png',

View file

@ -569,19 +569,14 @@ test('should opt out of attachments', async ({ runInlineTest, server }, testInfo
expect([...trace.resources.keys()].filter(f => f.startsWith('resources/'))).toHaveLength(0);
});
test('should record with custom page fixture that closes the context', async ({ runInlineTest }, testInfo) => {
// Note that original issue did not close the context, but we do not support such usecase.
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/23220' });
test('should record with custom page fixture', async ({ runInlineTest }, testInfo) => {
const result = await runInlineTest({
'a.spec.ts': `
import { test as base, expect } from '@playwright/test';
const test = base.extend({
myPage: async ({ browser }, use) => {
const page = await browser.newPage();
await use(page);
await page.close();
await use(await browser.newPage());
},
});
@ -1118,120 +1113,68 @@ test('trace:retain-on-first-failure should create trace if request context is di
expect(result.failed).toBe(1);
});
test('should record trace in workerStorageState', async ({ runInlineTest }) => {
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30287' });
test('should not corrupt actions when no library trace is present', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.spec.ts': `
import { test as base, expect } from '@playwright/test';
const test = base.extend({
storageState: ({ workerStorageState }, use) => use(workerStorageState),
workerStorageState: [async ({ browser }, use) => {
const page = await browser.newPage({ storageState: undefined });
await page.setContent('<div>hello</div>');
await page.close();
await use(undefined);
}, { scope: 'worker' }],
})
test('pass', async ({ page }) => {
await page.goto('data:text/html,<div>hi</div>');
foo: async ({}, use) => {
expect(1).toBe(1);
await use();
expect(2).toBe(2);
},
});
test('fail', async ({ foo }) => {
expect(1).toBe(2);
});
`,
}, { trace: 'on' });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
const tracePath = test.info().outputPath('test-results', 'a-pass', 'trace.zip');
const trace = await parseTrace(tracePath);
expect(trace.actionTree).toEqual([
'Before Hooks',
' fixture: browser',
' browserType.launch',
' fixture: workerStorageState',
' browser.newPage',
' page.setContent',
' page.close',
' fixture: context',
' browser.newContext',
' fixture: page',
' browserContext.newPage',
'page.goto',
'After Hooks',
' fixture: page',
' fixture: context',
]);
});
test('should record trace after fixture teardown timeout', async ({ runInlineTest }) => {
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30718' });
const result = await runInlineTest({
'a.spec.ts': `
import { test as base, expect } from '@playwright/test';
const test = base.extend({
fixture: async ({}, use) => {
await use('foo');
await new Promise(() => {});
},
})
test('fails', async ({ fixture, page }) => {
await page.evaluate(() => console.log('from the page'));
});
`,
}, { trace: 'on', timeout: '4000' });
expect(result.exitCode).toBe(1);
expect(result.failed).toBe(1);
const tracePath = test.info().outputPath('test-results', 'a-fails', 'trace.zip');
const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.zip');
const trace = await parseTrace(tracePath);
expect(trace.actionTree).toEqual([
'Before Hooks',
' fixture: foo',
' expect.toBe',
'expect.toBe',
'After Hooks',
' fixture: foo',
' expect.toBe',
'Worker Cleanup',
]);
});
test('should record trace for manually created context in a failed test', async ({ runInlineTest }) => {
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/31541' });
const result = await runInlineTest({
'a.spec.ts': `
import { test, expect } from '@playwright/test';
test('fail', async ({ browser }) => {
const page = await browser.newPage();
await page.setContent('<script>console.log("from the page");</script>');
expect(1).toBe(2);
});
`,
}, { trace: 'on' });
expect(result.exitCode).toBe(1);
expect(result.failed).toBe(1);
const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.zip');
const trace = await parseTrace(tracePath);
expect(trace.actionTree).toEqual([
'Before Hooks',
' fixture: fixture',
' fixture: browser',
' browserType.launch',
' fixture: context',
' browser.newContext',
' fixture: page',
' browserContext.newPage',
'page.evaluate',
'browser.newPage',
'page.setContent',
'expect.toBe',
'After Hooks',
' fixture: page',
' fixture: context',
' fixture: fixture',
'Worker Cleanup',
' fixture: browser',
]);
// Check console events to make sure that library trace is recorded.
expect(trace.events).toContainEqual(expect.objectContaining({ type: 'console', text: 'from the page' }));
});
test('should take a screenshot-on-failure in workerStorageState', async ({ runInlineTest }) => {
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30959' });
const result = await runInlineTest({
'playwright.config.ts': `
export default {
use: {
screenshot: 'only-on-failure',
},
};
`,
'a.spec.ts': `
import { test as base, expect } from '@playwright/test';
const test = base.extend({
storageState: ({ workerStorageState }, use) => use(workerStorageState),
workerStorageState: [async ({ browser }, use) => {
const page = await browser.newPage({ storageState: undefined });
await page.setContent('hello world!');
throw new Error('Failed!');
await use(undefined);
}, { scope: 'worker' }],
})
test('fail', async ({ page }) => {
});
`,
});
expect(result.exitCode).toBe(1);
expect(result.failed).toBe(1);
expect(fs.existsSync(test.info().outputPath('test-results', 'a-fail', 'test-failed-1.png'))).toBeTruthy();
});

View file

@ -690,3 +690,25 @@ test('static modifiers should be added in serial mode', async ({ runInlineTest }
expect(result.report.suites[0].specs[2].tests[0].annotations).toEqual([{ type: 'skip' }]);
expect(result.report.suites[0].specs[3].tests[0].annotations).toEqual([]);
});
test('should skip beforeEach hooks upon modifiers', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.ts': `
import { test } from '@playwright/test';
test('top', () => {});
test.describe(() => {
test.skip(({ viewport }) => true);
test.beforeEach(() => { throw new Error(); });
test.describe(() => {
test.beforeEach(() => { throw new Error(); });
test('test', () => {});
});
});
`,
});
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1);
expect(result.skipped).toBe(1);
});

View file

@ -167,7 +167,7 @@ class Documentation {
renderLinksInNodes(nodes, classOrMember) {
if (classOrMember instanceof Member) {
classOrMember.discouraged = classOrMember.discouraged ? this.renderLinksInText(classOrMember.discouraged, classOrMember) : undefined;
classOrMember.deprecated = classOrMember.deprecated ? this.renderLinksInText(classOrMember.deprecated, classOrMember) : undefined
classOrMember.deprecated = classOrMember.deprecated ? this.renderLinksInText(classOrMember.deprecated, classOrMember) : undefined;
}
md.visitAll(nodes, node => {
if (!node.text)
@ -208,7 +208,7 @@ class Documentation {
}
}
class Class {
class Class {
/**
* @param {Metainfo} metainfo
* @param {string} name
@ -322,7 +322,7 @@ class Documentation {
for (const e of this.eventsArray)
e.visit(visitor);
}
};
}
class Member {
/**
@ -345,7 +345,7 @@ class Member {
this.spec = spec;
this.argsArray = argsArray;
this.required = required;
this.comment = '';
this.comment = '';
/** @type {!Map<string, !Member>} */
this.args = new Map();
this.index();
@ -473,8 +473,10 @@ class Member {
this.type.visit(visitor);
for (const arg of this.argsArray)
arg.visit(visitor);
for (const lang in this.langs.overrides || {})
this.langs.overrides?.[lang].visit(visitor);
}
};
}
class Type {
/**
@ -509,9 +511,9 @@ class Type {
* @return {Type}
*/
static fromParsedType(parsedType, inUnion = false) {
if (!inUnion && !parsedType.unionName && isStringUnion(parsedType) ) {
if (!inUnion && !parsedType.unionName && isStringUnion(parsedType))
throw new Error('Enum must have a name:\n' + JSON.stringify(parsedType, null, 2));
}
if (!inUnion && (parsedType.union || parsedType.unionName)) {
const type = new Type(parsedType.unionName || '');
@ -556,15 +558,15 @@ class Type {
/** @type {Member[] | undefined} */
this.properties = this.name === 'Object' ? properties : undefined;
/** @type {Type[] | undefined} */
this.union;
this.union = undefined;
/** @type {Type[] | undefined} */
this.args;
this.args = undefined;
/** @type {Type | undefined} */
this.returnType;
this.returnType = undefined;
/** @type {Type[] | undefined} */
this.templates;
this.templates = undefined;
/** @type {string | undefined} */
this.expression;
this.expression = undefined;
}
visit(visitor) {
@ -645,7 +647,7 @@ class Type {
if (this.returnType)
this.returnType._collectAllTypes(result);
}
};
}
/**
* @param {ParsedType | null} type
@ -866,6 +868,7 @@ function csharpOptionOverloadSuffix(option, type) {
case 'Buffer': return 'Byte';
case 'Serializable': return 'Object';
case 'int': return 'Int';
case 'long': return 'Int64';
case 'Date': return 'Date';
}
throw new Error(`CSharp option "${option}" has unsupported type overload "${type}"`);
@ -930,7 +933,7 @@ function processCodeGroups(spec, language, transformer) {
* @param {string} codeLang
* @return {{ highlighter: string, language: string|undefined, codeGroup: string|undefined}}
*/
function parseCodeLang(codeLang) {
function parseCodeLang(codeLang) {
if (codeLang === 'python async')
return { highlighter: 'py', codeGroup: 'python-async', language: 'python' };
if (codeLang === 'python sync')

View file

@ -17,7 +17,6 @@
// @ts-check
const path = require('path');
const fs = require('fs');
const Documentation = require('./documentation');
const { parseApi } = require('./api_parser');
const PROJECT_DIR = path.join(__dirname, '..', '..');
@ -73,7 +72,7 @@ function serializeMember(member) {
sanitize(result);
result.args = member.argsArray.map(serializeProperty);
if (member.type)
result.type = serializeType(member.type)
result.type = serializeType(member.type);
return result;
}
@ -81,7 +80,7 @@ function serializeProperty(arg) {
const result = { ...arg };
sanitize(result);
if (arg.type)
result.type = serializeType(arg.type, arg.name === 'options')
result.type = serializeType(arg.type, arg.name === 'options');
return result;
}

View file

@ -82,6 +82,7 @@ classNameMap.set('boolean', 'bool');
classNameMap.set('any', 'object');
classNameMap.set('Buffer', 'byte[]');
classNameMap.set('path', 'string');
classNameMap.set('Date', 'DateTime');
classNameMap.set('URL', 'string');
classNameMap.set('RegExp', 'Regex');
classNameMap.set('Readable', 'Stream');
@ -829,7 +830,7 @@ function translateType(type, parent, generateNameCallback = t => t.name, optiona
* @param {Documentation.Type} type
*/
function registerModelType(typeName, type) {
if (['object', 'string', 'int'].includes(typeName))
if (['object', 'string', 'int', 'long'].includes(typeName))
return;
if (typeName.endsWith('Option'))
return;

View file

@ -427,7 +427,7 @@ class TypesGenerator {
return `{ [key: ${keyType}]: ${valueType}; }`;
}
let out = type.name;
if (out === 'int' || out === 'float')
if (out === 'int' || out === 'long' || out === 'float')
out = 'number';
if (out === 'Array' && direction === 'in')
out = 'ReadonlyArray';

View file

@ -124,7 +124,7 @@ function buildTree(lines) {
const headerStack = [root];
/** @type {{ indent: string, node: MarkdownNode }[]} */
let sectionStack = [];
const sectionStack = [];
/**
* @param {string} indent
@ -133,7 +133,7 @@ function buildTree(lines) {
const appendNode = (indent, node) => {
while (sectionStack.length && sectionStack[0].indent.length >= indent.length)
sectionStack.shift();
const parentNode = sectionStack.length ? sectionStack[0].node :headerStack[0];
const parentNode = sectionStack.length ? sectionStack[0].node : headerStack[0];
if (!parentNode.children)
parentNode.children = [];
parentNode.children.push(node);
@ -176,7 +176,7 @@ function buildTree(lines) {
line = lines[++i];
while (!line.trim().startsWith('```')) {
if (line && !line.startsWith(indent)) {
const from = Math.max(0, i - 5)
const from = Math.max(0, i - 5);
const to = Math.min(lines.length, from + 10);
const snippet = lines.slice(from, to);
throw new Error(`Bad code block: ${snippet.join('\n')}`);
@ -200,7 +200,7 @@ function buildTree(lines) {
const tokens = [];
while (!line.trim().startsWith(':::')) {
if (!line.startsWith(indent)) {
const from = Math.max(0, i - 5)
const from = Math.max(0, i - 5);
const to = Math.min(lines.length, from + 10);
const snippet = lines.slice(from, to);
throw new Error(`Bad comment block: ${snippet.join('\n')}`);
@ -279,7 +279,7 @@ function parse(content) {
function render(nodes, options) {
const result = [];
let lastNode;
for (let node of nodes) {
for (const node of nodes) {
if (node.type === 'null')
continue;
innerRenderMdNode('', node, /** @type {MarkdownNode} */ (lastNode), result, options);
@ -322,7 +322,7 @@ function innerRenderMdNode(indent, node, lastNode, result, options) {
const bothLinks = node.text.match(/\[[^\]]+\]:/) && lastNode && lastNode.type === 'text' && lastNode.text.match(/\[[^\]]+\]:/);
if (!bothTables && !bothGen && !bothComments && !bothLinks && lastNode && lastNode.text)
newLine();
result.push(wrapText(node.text, options, indent));
result.push(wrapText(node.text, options, indent));
return;
}
@ -391,15 +391,15 @@ function tokenizeNoBreakLinks(text) {
* @param {string} prefix
* @returns {string}
*/
function wrapText(text, options, prefix) {
function wrapText(text, options, prefix) {
if (options?.flattenText)
text = text.replace(/↵/g, ' ');
const lines = text.split(/[\n↵]/);
const result = /** @type {string[]} */([]);
const indent = ' '.repeat(prefix.length);
for (const line of lines) {
for (const line of lines)
result.push(wrapLine(line, options?.maxColumns, result.length ? indent : prefix));
}
return result.join('\n');
}