Compare commits

...

18 commits

Author SHA1 Message Date
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
54 changed files with 691 additions and 420 deletions

View file

@ -1,6 +1,6 @@
# 🎭 Playwright # 🎭 Playwright
[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[![Chromium version](https://img.shields.io/badge/chromium-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) ## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
@ -8,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
| | Linux | macOS | Windows | | | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: | | :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->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: | | 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: | | 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(); 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 ### param: BrowserContext.exposeBinding.name
* since: v1.8 * since: v1.8
- `name` <[string]> - `name` <[string]>
@ -839,6 +762,7 @@ Callback function that will be called in the Playwright's context.
### option: BrowserContext.exposeBinding.handle ### option: BrowserContext.exposeBinding.handle
* since: v1.8 * since: v1.8
* deprecated: This option will be removed in the future.
- `handle` <[boolean]> - `handle` <[boolean]>
Whether to pass the argument as a handle, instead of passing by value. When passing a handle, only one argument is 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 ### param: Clock.fastForward.ticks
* since: v1.45 * 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. 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.
@ -65,7 +65,7 @@ Fake timers are used to manually control the flow of time in tests. They allow y
### option: Clock.install.time ### option: Clock.install.time
* since: v1.45 * since: v1.45
- `time` <[int]|[string]|[Date]> - `time` <[long]|[string]|[Date]>
Time to initialize with, current system time by default. Time to initialize with, current system time by default.
@ -103,7 +103,7 @@ await page.Clock.RunForAsync("30:00");
### param: Clock.runFor.ticks ### param: Clock.runFor.ticks
* since: v1.45 * 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. 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 +136,8 @@ page.clock.pause_at("2020-02-02")
``` ```
```java ```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"); page.clock().pauseAt("2020-02-02");
``` ```
@ -147,7 +148,7 @@ await page.Clock.PauseAtAsync("2020-02-02");
### param: Clock.pauseAt.time ### param: Clock.pauseAt.time
* since: v1.45 * since: v1.45
- `time` <[int]|[string]|[Date]> - `time` <[long]|[string]|[Date]>
## async method: Clock.resume ## async method: Clock.resume
@ -182,8 +183,8 @@ page.clock.set_fixed_time("2020-02-02")
``` ```
```java ```java
page.clock().setFixedTime(Instant.now()); page.clock().setFixedTime(new Date());
page.clock().setFixedTime(Instant.parse("2020-02-02")); page.clock().setFixedTime(new SimpleDateFormat("yyy-MM-dd").parse("2020-02-02"));
page.clock().setFixedTime("2020-02-02"); page.clock().setFixedTime("2020-02-02");
``` ```
@ -195,7 +196,7 @@ await page.Clock.SetFixedTimeAsync("2020-02-02");
### param: Clock.setFixedTime.time ### param: Clock.setFixedTime.time
* since: v1.45 * since: v1.45
- `time` <[int]|[string]|[Date]> - `time` <[long]|[string]|[Date]>
Time to be set. Time to be set.
@ -225,8 +226,8 @@ page.clock.set_system_time("2020-02-02")
``` ```
```java ```java
page.clock().setSystemTime(Instant.now()); page.clock().setSystemTime(new Date());
page.clock().setSystemTime(Instant.parse("2020-02-02")); page.clock().setSystemTime(new SimpleDateFormat("yyy-MM-dd").parse("2020-02-02"));
page.clock().setSystemTime("2020-02-02"); page.clock().setSystemTime("2020-02-02");
``` ```
@ -238,4 +239,4 @@ await page.Clock.SetSystemTimeAsync("2020-02-02");
### param: Clock.setSystemTime.time ### param: Clock.setSystemTime.time
* since: v1.45 * since: v1.45
- `time` <[int]|[string]|[Date]> - `time` <[long]|[string]|[Date]>

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 ### param: Page.exposeBinding.name
* since: v1.8 * since: v1.8
- `name` <[string]> - `name` <[string]>
@ -1905,6 +1831,7 @@ Callback function that will be called in the Playwright's context.
### option: Page.exposeBinding.handle ### option: Page.exposeBinding.handle
* since: v1.8 * since: v1.8
* deprecated: This option will be removed in the future.
- `handle` <[boolean]> - `handle` <[boolean]>
Whether to pass the argument as a handle, instead of passing by value. When passing a handle, only one argument is 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 ```java
// Initialize clock with some time before the test time and let the page load // Initialize clock with some time before the test time and let the page load
// naturally. `Date.now` will progress as the timers fire. // 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"); page.navigate("http://localhost:3333");
Locator locator = page.getByTestId("current-time"); Locator locator = page.getByTestId("current-time");
// Pretend that the user closed the laptop lid and opened it again at 10am. // Pretend that the user closed the laptop lid and opened it again at 10am.
// Pause the time once reached that point. // 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. // Assert the page state.
assertThat(locator).hasText("2/2/2024, 10:00:00 AM"); 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 ```java
SimpleDateFormat format = new SimpleDateFormat("yyy-MM-dd'T'HH:mm:ss");
// Initialize clock with a specific time, let the page load naturally. // Initialize clock with a specific time, let the page load naturally.
page.clock().install(new Clock.InstallOptions() 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"); page.navigate("http://localhost:3333");
Locator locator = page.getByTestId("current-time"); Locator locator = page.getByTestId("current-time");
// Pause the time flow, stop the timers, you now have manual control // Pause the time flow, stop the timers, you now have manual control
// over the page time. // 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"); assertThat(locator).hasText("2/2/2024, 10:00:00 AM");
// Tick through time manually, firing all timers in the process. // Tick through time manually, firing all timers in the process.

View file

@ -5,7 +5,7 @@ title: "Dialogs"
## Introduction ## 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 ## alert(), confirm(), prompt() dialogs
@ -126,3 +126,55 @@ Page.Dialog += async (_, dialog) =>
}; };
await Page.CloseAsync(new() { RunBeforeUnload = true }); 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 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 ## Version 1.44
### New APIs ### New APIs
@ -129,7 +192,7 @@ This version was also tested against the following stable channels:
### New Locator Handler ### New Locator Handler
New method [`method: Page.addLocatorHandler`] registers a callback that will be invoked when specified element becomes visible and may block Playwright actions. The callback can get rid of the overlay. Here is an example that closes a cookie dialog when it appears. New method [`method: Page.addLocatorHandler`] registers a callback that will be invoked when specified element becomes visible and may block Playwright actions. The callback can get rid of the overlay. Here is an example that closes a cookie dialog when it appears.
```csharp ```csharp
// Setup the handler. // Setup the handler.
await Page.AddLocatorHandlerAsync( await Page.AddLocatorHandlerAsync(

View file

@ -4,6 +4,67 @@ title: "Release notes"
toc_max_heading_level: 2 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 ## Version 1.44
### New APIs ### New APIs
@ -201,7 +262,7 @@ Learn more about the fixtures in our [JUnit guide](./junit.md).
### New Locator Handler ### New Locator Handler
New method [`method: Page.addLocatorHandler`] registers a callback that will be invoked when specified element becomes visible and may block Playwright actions. The callback can get rid of the overlay. Here is an example that closes a cookie dialog when it appears. New method [`method: Page.addLocatorHandler`] registers a callback that will be invoked when specified element becomes visible and may block Playwright actions. The callback can get rid of the overlay. Here is an example that closes a cookie dialog when it appears.
```java ```java
// Setup the handler. // Setup the handler.
page.addLocatorHandler( page.addLocatorHandler(

View file

@ -43,10 +43,10 @@ See [the clock guide](./clock.md) for more details.
```sh ```sh
# Avoid TTY features that output ANSI control sequences # 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 # 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. - 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 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 ## Version 1.44
### New APIs ### New APIs
@ -109,7 +169,7 @@ This version was also tested against the following stable channels:
### New Locator Handler ### New Locator Handler
New method [`method: Page.addLocatorHandler`] registers a callback that will be invoked when specified element becomes visible and may block Playwright actions. The callback can get rid of the overlay. Here is an example that closes a cookie dialog when it appears. New method [`method: Page.addLocatorHandler`] registers a callback that will be invoked when specified element becomes visible and may block Playwright actions. The callback can get rid of the overlay. Here is an example that closes a cookie dialog when it appears.
```python ```python
# Setup the handler. # Setup the handler.
page.add_locator_handler( page.add_locator_handler(

View file

@ -9,13 +9,61 @@ You can either parameterize tests on a test level or on a project level.
## Parameterized Tests ## Parameterized Tests
```js title="example.spec.ts" ```js title="example.spec.ts"
const people = ['Alice', 'Bob']; [
for (const name of people) { { name: 'Alice', expected: 'Hello, Alice!' },
test(`testing with ${name}`, async () => { { 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. // 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 ## Parameterized Projects

68
package-lock.json generated
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -26,7 +26,7 @@ import * as dom from '../dom';
import * as frames from '../frames'; import * as frames from '../frames';
import { helper } from '../helper'; import { helper } from '../helper';
import * as network from '../network'; import * as network from '../network';
import type { PageBinding, PageDelegate } from '../page'; import type { InitScript, PageBinding, PageDelegate } from '../page';
import { Page, Worker } from '../page'; import { Page, Worker } from '../page';
import type { Progress } from '../progress'; import type { Progress } from '../progress';
import type * as types from '../types'; import type * as types from '../types';
@ -256,8 +256,8 @@ export class CRPage implements PageDelegate {
return this._go(+1); return this._go(+1);
} }
async addInitScript(source: string, world: types.World = 'main'): Promise<void> { async addInitScript(initScript: InitScript, world: types.World = 'main'): Promise<void> {
await this._forAllFrameSessions(frame => frame._evaluateOnNewDocument(source, world)); await this._forAllFrameSessions(frame => frame._evaluateOnNewDocument(initScript, world));
} }
async removeInitScripts() { async removeInitScripts() {
@ -511,6 +511,20 @@ class FrameSession {
this._addRendererListeners(); 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() === ':'; const isInitialEmptyPage = this._isMainFrame() && this._page.mainFrame().url() === ':';
if (isInitialEmptyPage) { if (isInitialEmptyPage) {
// Ignore lifecycle events, worlds and bindings for the initial empty page. It is never the final page // 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))); this._eventListeners.push(eventsHelper.addEventListener(this._client, 'Page.lifecycleEvent', event => this._onLifecycleEvent(event)));
}); });
} else { } 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._firstNonInitialNavigationCommittedFulfill();
this._eventListeners.push(eventsHelper.addEventListener(this._client, 'Page.lifecycleEvent', event => this._onLifecycleEvent(event))); this._eventListeners.push(eventsHelper.addEventListener(this._client, 'Page.lifecycleEvent', event => this._onLifecycleEvent(event)));
} }
@ -575,10 +575,10 @@ class FrameSession {
promises.push(this._updateFileChooserInterception(true)); promises.push(this._updateFileChooserInterception(true));
for (const binding of this._crPage._page.allBindings()) for (const binding of this._crPage._page.allBindings())
promises.push(this._initBinding(binding)); promises.push(this._initBinding(binding));
for (const source of this._crPage._browserContext.initScripts) for (const initScript of this._crPage._browserContext.initScripts)
promises.push(this._evaluateOnNewDocument(source, 'main')); promises.push(this._evaluateOnNewDocument(initScript, 'main'));
for (const source of this._crPage._page.initScripts) for (const initScript of this._crPage._page.initScripts)
promises.push(this._evaluateOnNewDocument(source, 'main')); promises.push(this._evaluateOnNewDocument(initScript, 'main'));
if (screencastOptions) if (screencastOptions)
promises.push(this._startVideoRecording(screencastOptions)); promises.push(this._startVideoRecording(screencastOptions));
} }
@ -1099,9 +1099,9 @@ class FrameSession {
await this._client.send('Page.setInterceptFileChooserDialog', { enabled }).catch(() => {}); // target can be closed. 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 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); this._evaluateOnNewDocumentIdentifiers.push(identifier);
} }

View file

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

View file

@ -21,7 +21,7 @@ import type { BrowserOptions } from '../browser';
import { Browser } from '../browser'; import { Browser } from '../browser';
import { assertBrowserContextIsNotOwned, BrowserContext, verifyGeolocation } from '../browserContext'; import { assertBrowserContextIsNotOwned, BrowserContext, verifyGeolocation } from '../browserContext';
import * as network from '../network'; 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 { ConnectionTransport } from '../transport';
import type * as types from '../types'; import type * as types from '../types';
import type * as channels from '@protocol/channels'; 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 }); await this._browser.session.send('Browser.setHTTPCredentials', { browserContextId: this._browserContextId, credentials });
} }
async doAddInitScript(source: string) { async doAddInitScript(initScript: InitScript) {
await this._browser.session.send('Browser.setInitScripts', { browserContextId: this._browserContextId, scripts: this.initScripts.map(script => ({ script })) }); await this._browser.session.send('Browser.setInitScripts', { browserContextId: this._browserContextId, scripts: this.initScripts.map(script => ({ script: script.source })) });
} }
async doRemoveInitScripts() { async doRemoveInitScripts() {

View file

@ -21,6 +21,7 @@ import type * as frames from '../frames';
import type { RegisteredListener } from '../../utils/eventsHelper'; import type { RegisteredListener } from '../../utils/eventsHelper';
import { eventsHelper } from '../../utils/eventsHelper'; import { eventsHelper } from '../../utils/eventsHelper';
import type { PageBinding, PageDelegate } from '../page'; import type { PageBinding, PageDelegate } from '../page';
import { InitScript } from '../page';
import { Page, Worker } from '../page'; import { Page, Worker } from '../page';
import type * as types from '../types'; import type * as types from '../types';
import { getAccessibilityTree } from './ffAccessibility'; import { getAccessibilityTree } from './ffAccessibility';
@ -56,7 +57,7 @@ export class FFPage implements PageDelegate {
private _eventListeners: RegisteredListener[]; private _eventListeners: RegisteredListener[];
private _workers = new Map<string, { frameId: string, session: FFSession }>(); private _workers = new Map<string, { frameId: string, session: FFSession }>();
private _screencastId: string | undefined; private _screencastId: string | undefined;
private _initScripts: { script: string, worldName?: string }[] = []; private _initScripts: { initScript: InitScript, worldName?: string }[] = [];
constructor(session: FFSession, browserContext: FFBrowserContext, opener: FFPage | null) { constructor(session: FFSession, browserContext: FFBrowserContext, opener: FFPage | null) {
this._session = session; 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. // 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. // 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 { potentiallyUninitializedPage(): Page {
@ -406,9 +407,9 @@ export class FFPage implements PageDelegate {
return success; return success;
} }
async addInitScript(script: string, worldName?: string): Promise<void> { async addInitScript(initScript: InitScript, worldName?: string): Promise<void> {
this._initScripts.push({ script, worldName }); this._initScripts.push({ initScript, worldName });
await this._session.send('Page.setInitScripts', { scripts: this._initScripts }); await this._session.send('Page.setInitScripts', { scripts: this._initScripts.map(s => ({ script: s.initScript.source, worldName: s.worldName })) });
} }
async removeInitScripts() { async removeInitScripts() {

View file

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

View file

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

View file

@ -30,7 +30,7 @@ import { eventsHelper } from '../../utils/eventsHelper';
import { helper } from '../helper'; import { helper } from '../helper';
import type { JSHandle } from '../javascript'; import type { JSHandle } from '../javascript';
import * as network from '../network'; import * as network from '../network';
import type { PageBinding, PageDelegate } from '../page'; import type { InitScript, PageBinding, PageDelegate } from '../page';
import { Page } from '../page'; import { Page } from '../page';
import type { Progress } from '../progress'; import type { Progress } from '../progress';
import type * as types from '../types'; import type * as types from '../types';
@ -777,7 +777,7 @@ export class WKPage implements PageDelegate {
await this._updateBootstrapScript(); await this._updateBootstrapScript();
} }
async addInitScript(script: string): Promise<void> { async addInitScript(initScript: InitScript): Promise<void> {
await this._updateBootstrapScript(); await this._updateBootstrapScript();
} }
@ -797,8 +797,8 @@ export class WKPage implements PageDelegate {
for (const binding of this._page.allBindings()) for (const binding of this._page.allBindings())
scripts.push(binding.source); scripts.push(binding.source);
scripts.push(...this._browserContext.initScripts); scripts.push(...this._browserContext.initScripts.map(s => s.source));
scripts.push(...this._page.initScripts); scripts.push(...this._page.initScripts.map(s => s.source));
return scripts.join(';\n'); 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 name Name of the function on the window object.
* @param callback Callback function that will be called in the Playwright's context. * @param callback Callback function that will be called in the Playwright's context.
* @param options * @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 name Name of the function on the window object.
* @param callback Callback function that will be called in the Playwright's context. * @param callback Callback function that will be called in the Playwright's context.
* @param options * @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 name Name of the function on the window object.
* @param callback Callback function that will be called in the Playwright's context. * @param callback Callback function that will be called in the Playwright's context.
* @param options * @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 name Name of the function on the window object.
* @param callback Callback function that will be called in the Playwright's context. * @param callback Callback function that will be called in the Playwright's context.
* @param options * @param options

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -597,7 +597,7 @@ class ArtifactsRecorder {
if ((tracing as any)[this._startedCollectingArtifacts]) if ((tracing as any)[this._startedCollectingArtifacts])
return; return;
(tracing as any)[this._startedCollectingArtifacts] = true; (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() }); await tracing.stopChunk({ path: this._testInfo._tracing.generateNextTraceRecordingPath() });
} }
} }

View file

@ -570,6 +570,9 @@ export class WorkerMain extends ProcessRunner {
if (error instanceof TimeoutManagerError) if (error instanceof TimeoutManagerError)
throw error; throw error;
firstError = firstError ?? error; firstError = firstError ?? error;
// Skip in modifier prevents others from running.
if (error instanceof SkipError)
break;
} }
} }
if (firstError) if (firstError)

View file

@ -15,6 +15,7 @@
*/ */
import { test } from './npmTest'; import { test } from './npmTest';
import fs from 'fs'; import fs from 'fs';
import { expect } from 'packages/playwright-test';
import path from 'path'; import path from 'path';
test('electron should work', async ({ exec, tsc, writeFiles }) => { 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) 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); 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' }); it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30534' });
const context = await contextFactory({ const context = await contextFactory({
httpCredentials: { username: 'user', password: 'pass', origin: server.PREFIX.toUpperCase(), send: 'always' } 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' }); it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30534' });
const page = await browser.newPage({ const page = await browser.newPage({
httpCredentials: { username: 'user', password: 'pass', origin: server.PREFIX.toUpperCase(), send: 'always' } 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'); 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('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' }); test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/24574' });

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'); 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' }); it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30534' });
const request = await playwright.request.newContext({ const request = await playwright.request.newContext({
httpCredentials: { username: 'user', password: 'pass', origin: server.PREFIX.toUpperCase(), send: 'always' } httpCredentials: { username: 'user', password: 'pass', origin: server.PREFIX.toUpperCase(), send: 'always' }

View file

@ -245,6 +245,11 @@ it.describe('stubTimers', () => {
expect(await page.evaluate(() => Date.now())).toBe(1400); 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 }) => { it('replaces global setTimeout', async ({ page, calls }) => {
await page.evaluate(async () => { await page.evaluate(async () => {
setTimeout(window.stub, 1000); setTimeout(window.stub, 1000);

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[2].tests[0].annotations).toEqual([{ type: 'skip' }]);
expect(result.report.suites[0].specs[3].tests[0].annotations).toEqual([]); 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

@ -866,6 +866,7 @@ function csharpOptionOverloadSuffix(option, type) {
case 'Buffer': return 'Byte'; case 'Buffer': return 'Byte';
case 'Serializable': return 'Object'; case 'Serializable': return 'Object';
case 'int': return 'Int'; case 'int': return 'Int';
case 'long': return 'Int64';
case 'Date': return 'Date'; case 'Date': return 'Date';
} }
throw new Error(`CSharp option "${option}" has unsupported type overload "${type}"`); throw new Error(`CSharp option "${option}" has unsupported type overload "${type}"`);

View file

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

View file

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