\n");
-
-await page.ClickAsync("div");
-// Note: it makes sense to await the result here, because otherwise, the context
-// gets closed and the binding function will throw an exception.
-Assert.AreEqual("Click me", await result.Task);
-```
-
### param: BrowserContext.exposeBinding.name
* since: v1.8
- `name` <[string]>
@@ -839,6 +758,7 @@ Callback function that will be called in the Playwright's context.
### option: BrowserContext.exposeBinding.handle
* since: v1.8
+* deprecated: This option will be removed in the future.
- `handle` <[boolean]>
Whether to pass the argument as a handle, instead of passing by value. When passing a handle, only one argument is
diff --git a/docs/src/api/class-clock.md b/docs/src/api/class-clock.md
index 1a87cdffa0..6f70e72a02 100644
--- a/docs/src/api/class-clock.md
+++ b/docs/src/api/class-clock.md
@@ -136,7 +136,8 @@ page.clock.pause_at("2020-02-02")
```
```java
-page.clock().pauseAt(Instant.parse("2020-02-02"));
+SimpleDateFormat format = new SimpleDateFormat("yyy-MM-dd");
+page.clock().pauseAt(format.parse("2020-02-02"));
page.clock().pauseAt("2020-02-02");
```
@@ -182,8 +183,8 @@ page.clock.set_fixed_time("2020-02-02")
```
```java
-page.clock().setFixedTime(Instant.now());
-page.clock().setFixedTime(Instant.parse("2020-02-02"));
+page.clock().setFixedTime(new Date());
+page.clock().setFixedTime(new SimpleDateFormat("yyy-MM-dd").parse("2020-02-02"));
page.clock().setFixedTime("2020-02-02");
```
@@ -225,8 +226,8 @@ page.clock.set_system_time("2020-02-02")
```
```java
-page.clock().setSystemTime(Instant.now());
-page.clock().setSystemTime(Instant.parse("2020-02-02"));
+page.clock().setSystemTime(new Date());
+page.clock().setSystemTime(new SimpleDateFormat("yyy-MM-dd").parse("2020-02-02"));
page.clock().setSystemTime("2020-02-02");
```
diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md
index d087b78c9f..3d667bb186 100644
--- a/docs/src/api/class-page.md
+++ b/docs/src/api/class-page.md
@@ -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(`
-
-
\n");
-
-await page.ClickAsync("div");
-Console.WriteLine(await result.Task);
-```
-
### param: Page.exposeBinding.name
* since: v1.8
- `name` <[string]>
@@ -1905,6 +1831,7 @@ Callback function that will be called in the Playwright's context.
### option: Page.exposeBinding.handle
* since: v1.8
+* deprecated: This option will be removed in the future.
- `handle` <[boolean]>
Whether to pass the argument as a handle, instead of passing by value. When passing a handle, only one argument is
diff --git a/docs/src/api/class-touchscreen.md b/docs/src/api/class-touchscreen.md
index 90ea8cd759..79fd134dca 100644
--- a/docs/src/api/class-touchscreen.md
+++ b/docs/src/api/class-touchscreen.md
@@ -20,3 +20,23 @@ Dispatches a `touchstart` and `touchend` event with a single touch at the positi
### param: Touchscreen.tap.y
* since: v1.8
- `y` <[float]>
+
+## async method: Touchscreen.touch
+* since: v1.46
+
+Synthesizes a touch event.
+
+### param: Touchscreen.touch.type
+* since: v1.46
+- `type` <[TouchType]<"touchstart"|"touchend"|"touchmove"|"touchcancel">>
+
+Type of the touch event.
+
+### param: Touchscreen.touch.touches
+* since: v1.46
+- `touchPoints` <[Array]<[Object]>>
+ - `x` <[float]> x coordinate of the event in CSS pixels.
+ - `y` <[float]> y coordinate of the event in CSS pixels.
+ - `id` ?<[int]> Identifier used to track the touch point between events, must be unique within an event. Optional.
+
+List of touch points for this event. `id` is a unique identifier of a touch point that helps identify it between touch events for the duration of its movement around the surface.
\ No newline at end of file
diff --git a/docs/src/clock.md b/docs/src/clock.md
index b89dfd93d6..ec3aaf8bd5 100644
--- a/docs/src/clock.md
+++ b/docs/src/clock.md
@@ -2,11 +2,24 @@
id: clock
title: "Clock"
---
+import LiteYouTube from '@site/src/components/LiteYouTube';
## Introduction
Accurately simulating time-dependent behavior is essential for verifying the correctness of applications. Utilizing [Clock] functionality allows developers to manipulate and control time within tests, enabling the precise validation of features such as rendering time, timeouts, scheduled tasks without the delays and variability of real-time execution.
+The [Clock] API provides the following methods to control time:
+- `setFixedTime`: Sets the fixed time for `Date.now()` and `new Date()`.
+- `install`: initializes the clock and allows you to:
+ - `pauseAt`: Pauses the time at a specific time.
+ - `fastForward`: Fast forwards the time.
+ - `runFor`: Runs the time for a specific duration.
+ - `resume`: Resumes the time.
+- `setSystemTime`: Sets the current system time.
+
+The recommended approach is to use `setFixedTime` to set the time to a specific value. If that doesn't work for your use case, you can use `install` which allows you to pause time later on, fast forward it, tick it, etc. `setSystemTime` is only recommended for advanced use cases.
+
+:::note
[`property: Page.clock`] overrides native global classes and functions related to time allowing them to be manually controlled:
- `Date`
- `setTimeout`
@@ -18,6 +31,7 @@ Accurately simulating time-dependent behavior is essential for verifying the cor
- `requestIdleCallback`
- `cancelIdleCallback`
- `performance`
+:::
## Test with predefined time
@@ -118,13 +132,14 @@ expect(page.get_by_test_id("current-time")).to_have_text("2/2/2024, 10:30:00 AM"
```java
// Initialize clock with some time before the test time and let the page load
// naturally. `Date.now` will progress as the timers fire.
-page.clock().install(new Clock.InstallOptions().setTime(Instant.parse("2024-02-02T08:00:00")));
+SimpleDateFormat format = new SimpleDateFormat("yyy-MM-dd'T'HH:mm:ss");
+page.clock().install(new Clock.InstallOptions().setTime(format.parse("2024-02-02T08:00:00")));
page.navigate("http://localhost:3333");
Locator locator = page.getByTestId("current-time");
// Pretend that the user closed the laptop lid and opened it again at 10am.
// Pause the time once reached that point.
-page.clock().pauseAt(Instant.parse("2024-02-02T10:00:00"));
+page.clock().pauseAt(format.parse("2024-02-02T10:00:00"));
// Assert the page state.
assertThat(locator).hasText("2/2/2024, 10:00:00 AM");
@@ -315,15 +330,16 @@ expect(locator).to_have_text("2/2/2024, 10:00:02 AM")
```
```java
+SimpleDateFormat format = new SimpleDateFormat("yyy-MM-dd'T'HH:mm:ss");
// Initialize clock with a specific time, let the page load naturally.
page.clock().install(new Clock.InstallOptions()
- .setTime(Instant.parse("2024-02-02T08:00:00")));
+ .setTime(format.parse("2024-02-02T08:00:00")));
page.navigate("http://localhost:3333");
Locator locator = page.getByTestId("current-time");
// Pause the time flow, stop the timers, you now have manual control
// over the page time.
-page.clock().pauseAt(Instant.parse("2024-02-02T10:00:00"));
+page.clock().pauseAt(format.parse("2024-02-02T10:00:00"));
assertThat(locator).hasText("2/2/2024, 10:00:00 AM");
// Tick through time manually, firing all timers in the process.
@@ -351,3 +367,10 @@ await Expect(locator).ToHaveTextAsync("2/2/2024, 10:00:00 AM");
await Page.Clock.RunForAsync(2000);
await Expect(locator).ToHaveTextAsync("2/2/2024, 10:00:02 AM");
```
+
+## Related Videos
+
+
diff --git a/docs/src/docker.md b/docs/src/docker.md
index 11553a578a..0946c2f68b 100644
--- a/docs/src/docker.md
+++ b/docs/src/docker.md
@@ -5,7 +5,7 @@ title: "Docker"
## Introduction
-[Dockerfile.jammy] can be used to run Playwright scripts in Docker environment. These image includes the [Playwright browsers](./browsers.md#install-browsers) and [browser system dependencies](./browsers.md#install-system-dependencies). The Playwright package/dependency is not included in the image and should be installed separately.
+[Dockerfile.jammy] can be used to run Playwright scripts in Docker environment. This image includes the [Playwright browsers](./browsers.md#install-browsers) and [browser system dependencies](./browsers.md#install-system-dependencies). The Playwright package/dependency is not included in the image and should be installed separately.
## Usage
diff --git a/docs/src/locators.md b/docs/src/locators.md
index 6d5926b579..052894a172 100644
--- a/docs/src/locators.md
+++ b/docs/src/locators.md
@@ -1444,14 +1444,6 @@ page.getByText("orange").click();
await page.GetByText("orange").ClickAsync();
```
-```html card
-
-
apple
-
banana
-
orange
-
-```
-
#### Filter by text
Use the [`method: Locator.filter`] to locate a specific item in a list.
diff --git a/docs/src/release-notes-csharp.md b/docs/src/release-notes-csharp.md
index 9a3301a764..97b0c9cd50 100644
--- a/docs/src/release-notes-csharp.md
+++ b/docs/src/release-notes-csharp.md
@@ -4,6 +4,69 @@ title: "Release notes"
toc_max_heading_level: 2
---
+## Version 1.45
+
+### Clock
+
+Utilizing the new [Clock] API allows to manipulate and control time within tests to verify time-related behavior. This API covers many common scenarios, including:
+* testing with predefined time;
+* keeping consistent time and timers;
+* monitoring inactivity;
+* ticking through time manually.
+
+```csharp
+// Initialize clock with some time before the test time and let the page load naturally.
+// `Date.now` will progress as the timers fire.
+await Page.Clock.InstallAsync(new
+{
+ Time = 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")).ToHaveText("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")).ToHaveText("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 `` elements.
+ ```csharp
+ await page.GetByLabel("Upload directory").SetInputFilesAsync("mydir");
+ ```
+
+- Multiple methods like [`method: Locator.click`] or [`method: Locator.press`] now support a `ControlOrMeta` modifier key. This key maps to `Meta` on macOS and maps to `Control` on Windows and Linux.
+ ```csharp
+ // Press the common keyboard shortcut Control+S or Meta+S to trigger a "Save" operation.
+ await page.Keyboard.PressAsync("ControlOrMeta+S");
+ ```
+
+- New property `httpCredentials.send` in [`method: APIRequest.newContext`] that allows to either always send the `Authorization` header or only send it in response to `401 Unauthorized`.
+
+- Playwright now supports Chromium, Firefox and WebKit on Ubuntu 24.04.
+
+- v1.45 is the last release to receive WebKit update for macOS 12 Monterey. Please update macOS to keep using the latest WebKit.
+
+### Browser Versions
+
+* Chromium 127.0.6533.5
+* Mozilla Firefox 127.0
+* WebKit 17.4
+
+This version was also tested against the following stable channels:
+
+* Google Chrome 126
+* Microsoft Edge 126
+
## Version 1.44
### New APIs
@@ -129,7 +192,7 @@ This version was also tested against the following stable channels:
### 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.
-
+
```csharp
// Setup the handler.
await Page.AddLocatorHandlerAsync(
diff --git a/docs/src/release-notes-java.md b/docs/src/release-notes-java.md
index 4a46556f68..6dce675b8f 100644
--- a/docs/src/release-notes-java.md
+++ b/docs/src/release-notes-java.md
@@ -4,6 +4,67 @@ title: "Release notes"
toc_max_heading_level: 2
---
+## Version 1.45
+
+### Clock
+
+Utilizing the new [Clock] API allows to manipulate and control time within tests to verify time-related behavior. This API covers many common scenarios, including:
+* testing with predefined time;
+* keeping consistent time and timers;
+* monitoring inactivity;
+* ticking through time manually.
+
+```java
+// Initialize clock with some time before the test time and let the page load
+// naturally. `Date.now` will progress as the timers fire.
+page.clock().install(new Clock.InstallOptions().setTime("2024-02-02T08:00:00"));
+page.navigate("http://localhost:3333");
+Locator locator = page.getByTestId("current-time");
+
+// Pretend that the user closed the laptop lid and opened it again at 10am.
+// Pause the time once reached that point.
+page.clock().pauseAt("2024-02-02T10:00:00");
+
+// Assert the page state.
+assertThat(locator).hasText("2/2/2024, 10:00:00 AM");
+
+// Close the laptop lid again and open it at 10:30am.
+page.clock().fastForward("30:00");
+assertThat(locator).hasText("2/2/2024, 10:30:00 AM");
+```
+
+See [the clock guide](./clock.md) for more details.
+
+### Miscellaneous
+
+- Method [`method: Locator.setInputFiles`] now supports uploading a directory for `` elements.
+ ```java
+ page.getByLabel("Upload directory").setInputFiles(Paths.get("mydir"));
+ ```
+
+- Multiple methods like [`method: Locator.click`] or [`method: Locator.press`] now support a `ControlOrMeta` modifier key. This key maps to `Meta` on macOS and maps to `Control` on Windows and Linux.
+ ```java
+ // Press the common keyboard shortcut Control+S or Meta+S to trigger a "Save" operation.
+ page.keyboard.press("ControlOrMeta+S");
+ ```
+
+- New property `httpCredentials.send` in [`method: APIRequest.newContext`] that allows to either always send the `Authorization` header or only send it in response to `401 Unauthorized`.
+
+- Playwright now supports Chromium, Firefox and WebKit on Ubuntu 24.04.
+
+- v1.45 is the last release to receive WebKit update for macOS 12 Monterey. Please update macOS to keep using the latest WebKit.
+
+### Browser Versions
+
+* Chromium 127.0.6533.5
+* Mozilla Firefox 127.0
+* WebKit 17.4
+
+This version was also tested against the following stable channels:
+
+* Google Chrome 126
+* Microsoft Edge 126
+
## Version 1.44
### New APIs
@@ -201,7 +262,7 @@ Learn more about the fixtures in our [JUnit guide](./junit.md).
### 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.
-
+
```java
// Setup the handler.
page.addLocatorHandler(
diff --git a/docs/src/release-notes-js.md b/docs/src/release-notes-js.md
index 839a4ab07b..5bcea8962b 100644
--- a/docs/src/release-notes-js.md
+++ b/docs/src/release-notes-js.md
@@ -8,6 +8,11 @@ import LiteYouTube from '@site/src/components/LiteYouTube';
## 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:
diff --git a/docs/src/release-notes-python.md b/docs/src/release-notes-python.md
index 590a4edb88..e7a4341a7e 100644
--- a/docs/src/release-notes-python.md
+++ b/docs/src/release-notes-python.md
@@ -4,6 +4,66 @@ title: "Release notes"
toc_max_heading_level: 2
---
+## Version 1.45
+
+### Clock
+
+Utilizing the new [Clock] API allows to manipulate and control time within tests to verify time-related behavior. This API covers many common scenarios, including:
+* testing with predefined time;
+* keeping consistent time and timers;
+* monitoring inactivity;
+* ticking through time manually.
+
+```python
+# Initialize clock with some time before the test time and let the page load
+# naturally. `Date.now` will progress as the timers fire.
+page.clock.install(time=datetime.datetime(2024, 2, 2, 8, 0, 0))
+page.goto("http://localhost:3333")
+
+# Pretend that the user closed the laptop lid and opened it again at 10am.
+# Pause the time once reached that point.
+page.clock.pause_at(datetime.datetime(2024, 2, 2, 10, 0, 0))
+
+# Assert the page state.
+expect(page.get_by_test_id("current-time")).to_have_text("2/2/2024, 10:00:00 AM")
+
+# Close the laptop lid again and open it at 10:30am.
+page.clock.fast_forward("30:00")
+expect(page.get_by_test_id("current-time")).to_have_text("2/2/2024, 10:30:00 AM")
+```
+
+See [the clock guide](./clock.md) for more details.
+
+### Miscellaneous
+
+- Method [`method: Locator.setInputFiles`] now supports uploading a directory for `` elements.
+ ```python
+ page.get_by_label("Upload directory").set_input_files('mydir')
+ ```
+
+- Multiple methods like [`method: Locator.click`] or [`method: Locator.press`] now support a `ControlOrMeta` modifier key. This key maps to `Meta` on macOS and maps to `Control` on Windows and Linux.
+ ```python
+ # Press the common keyboard shortcut Control+S or Meta+S to trigger a "Save" operation.
+ page.keyboard.press("ControlOrMeta+S")
+ ```
+
+- New property `httpCredentials.send` in [`method: APIRequest.newContext`] that allows to either always send the `Authorization` header or only send it in response to `401 Unauthorized`.
+
+- Playwright now supports Chromium, Firefox and WebKit on Ubuntu 24.04.
+
+- v1.45 is the last release to receive WebKit update for macOS 12 Monterey. Please update macOS to keep using the latest WebKit.
+
+### Browser Versions
+
+* Chromium 127.0.6533.5
+* Mozilla Firefox 127.0
+* WebKit 17.4
+
+This version was also tested against the following stable channels:
+
+* Google Chrome 126
+* Microsoft Edge 126
+
## Version 1.44
### New APIs
@@ -109,7 +169,7 @@ This version was also tested against the following stable channels:
### 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.
-
+
```python
# Setup the handler.
page.add_locator_handler(
diff --git a/docs/src/test-fixtures-js.md b/docs/src/test-fixtures-js.md
index 0971922d35..57334aa2ef 100644
--- a/docs/src/test-fixtures-js.md
+++ b/docs/src/test-fixtures-js.md
@@ -43,48 +43,7 @@ Here is how typical test environment setup differs between traditional test styl
Click to expand the code for the TodoPage
-```js tab=js-js title="todo-page.js"
-export class TodoPage {
- /**
- * @param {import('@playwright/test').Page} page
- */
- constructor(page) {
- this.page = page;
- this.inputBox = this.page.locator('input.new-todo');
- this.todoItems = this.page.getByTestId('todo-item');
- }
-
- async goto() {
- await this.page.goto('https://demo.playwright.dev/todomvc/');
- }
-
- /**
- * @param {string} text
- */
- async addToDo(text) {
- await this.inputBox.fill(text);
- await this.inputBox.press('Enter');
- }
-
- /**
- * @param {string} text
- */
- async remove(text) {
- const todo = this.todoItems.filter({ hasText: text });
- await todo.hover();
- await todo.getByLabel('Delete').click();
- }
-
- async removeAll() {
- while ((await this.todoItems.count()) > 0) {
- await this.todoItems.first().hover();
- await this.todoItems.getByLabel('Delete').first().click();
- }
- }
-}
-```
-
-```js tab=js-ts title="todo-page.ts"
+```js title="todo-page.ts"
import type { Page, Locator } from '@playwright/test';
export class TodoPage {
@@ -167,48 +126,7 @@ Fixtures have a number of advantages over before/after hooks:
Click to expand the code for the TodoPage
-```js tab=js-js title="my-test.js"
-const base = require('@playwright/test');
-const { TodoPage } = require('./todo-page');
-const { SettingsPage } = require('./settings-page');
-
-// Extend base test by providing "todoPage" and "settingsPage".
-// This new "test" can be used in multiple test files, and each of them will get the fixtures.
-exports.test = base.test.extend({
- todoPage: async ({ page }, use) => {
- // Set up the fixture.
- const todoPage = new TodoPage(page);
- await todoPage.goto();
- await todoPage.addToDo('item1');
- await todoPage.addToDo('item2');
-
- // Use the fixture value in the test.
- await use(todoPage);
-
- // Clean up the fixture.
- await todoPage.removeAll();
- },
-
- settingsPage: async ({ page }, use) => {
- await use(new SettingsPage(page));
- },
-});
-exports.expect = base.expect;
-```
-
-```js tab=js-ts title="my-test.ts"
+```js title="my-test.ts"
import { test as base } from '@playwright/test';
import { TodoPage } from './todo-page';
import { SettingsPage } from './settings-page';
@@ -493,20 +300,7 @@ Just mention fixture in your test function argument, and test runner will take c
Below we use the `todoPage` and `settingsPage` fixtures defined above.
-```js tab=js-js
-const { test, expect } = require('./my-test');
-
-test.beforeEach(async ({ settingsPage }) => {
- await settingsPage.switchToDarkMode();
-});
-
-test('basic test', async ({ todoPage, page }) => {
- await todoPage.addToDo('something nice');
- await expect(page.getByTestId('todo-title')).toContainText(['something nice']);
-});
-```
-
-```js tab=js-ts
+```js
import { test, expect } from './my-test';
test.beforeEach(async ({ settingsPage }) => {
@@ -560,47 +354,7 @@ Playwright Test uses [worker processes](./test-parallel.md) to run test files. S
Below we'll create an `account` fixture that will be shared by all tests in the same worker, and override the `page` fixture to login into this account for each test. To generate unique accounts, we'll use the [`property: WorkerInfo.workerIndex`] that is available to any test or fixture. Note the tuple-like syntax for the worker fixture - we have to pass `{scope: 'worker'}` so that test runner sets up this fixture once per worker.
-```js tab=js-js title="my-test.js"
-const base = require('@playwright/test');
-
-exports.test = base.test.extend({
- account: [async ({ browser }, use, workerInfo) => {
- // Unique username.
- const username = 'user' + workerInfo.workerIndex;
- const password = 'verysecure';
-
- // Create the account with Playwright.
- const page = await browser.newPage();
- await page.goto('/signup');
- await page.getByLabel('User Name').fill(username);
- await page.getByLabel('Password').fill(password);
- await page.getByText('Sign up').click();
- // Make sure everything is ok.
- await expect(page.locator('#result')).toHaveText('Success');
- // Do not forget to cleanup.
- await page.close();
-
- // Use the account value.
- await use({ username, password });
- }, { scope: 'worker' }],
-
- page: async ({ page, account }, use) => {
- // Sign in with our account.
- const { username, password } = account;
- await page.goto('/signin');
- await page.getByLabel('User Name').fill(username);
- await page.getByLabel('Password').fill(password);
- await page.getByText('Sign in').click();
- await expect(page.getByTestId('userinfo')).toHaveText(username);
-
- // Use signed-in page in the test.
- await use(page);
- },
-});
-exports.expect = base.expect;
-```
-
-```js tab=js-ts title="my-test.ts"
+```js title="my-test.ts"
import { test as base } from '@playwright/test';
type Account = {
@@ -652,32 +406,7 @@ Automatic fixtures are set up for each test/worker, even when the test does not
Here is an example fixture that automatically attaches debug logs when the test fails, so we can later review the logs in the reporter. Note how it uses [TestInfo] object that is available in each test/fixture to retrieve metadata about the test being run.
-```js tab=js-js title="my-test.js"
-const debug = require('debug');
-const fs = require('fs');
-const base = require('@playwright/test');
-
-exports.test = base.test.extend({
- saveLogs: [async ({}, use, testInfo) => {
- // Collecting logs during the test.
- const logs = [];
- debug.log = (...args) => logs.push(args.map(String).join(''));
- debug.enable('myserver');
-
- await use();
-
- // After the test we can check whether the test passed or failed.
- if (testInfo.status !== testInfo.expectedStatus) {
- // outputPath() API guarantees a unique file name.
- const logFile = testInfo.outputPath('logs.txt');
- await fs.promises.writeFile(logFile, logs.join('\n'), 'utf8');
- testInfo.attachments.push({ name: 'logs', contentType: 'text/plain', path: logFile });
- }
- }, { auto: true }],
-});
-```
-
-```js tab=js-ts title="my-test.ts"
+```js title="my-test.ts"
import * as debug from 'debug';
import * as fs from 'fs';
import { test as base } from '@playwright/test';
@@ -707,22 +436,7 @@ export { expect } from '@playwright/test';
By default, fixture shares timeout with the test. However, for slow fixtures, especially [worker-scoped](#worker-scoped-fixtures) ones, it is convenient to have a separate timeout. This way you can keep the overall test timeout small, and give the slow fixture more time.
-```js tab=js-js
-const { test: base, expect } = require('@playwright/test');
-
-const test = base.extend({
- slowFixture: [async ({}, use) => {
- // ... perform a slow operation ...
- await use('hello');
- }, { timeout: 60000 }]
-});
-
-test('example test', async ({ slowFixture }) => {
- // ...
-});
-```
-
-```js tab=js-ts
+```js
import { test as base, expect } from '@playwright/test';
const test = base.extend<{ slowFixture: string }>({
@@ -752,48 +466,7 @@ Below we'll create a `defaultItem` option in addition to the `todoPage` fixture
Click to expand the code for the TodoPage
-```js tab=js-js title="my-test.js"
-const base = require('@playwright/test');
-const { TodoPage } = require('./todo-page');
-
-exports.test = base.test.extend({
- // Define an option and provide a default value.
- // We can later override it in the config.
- defaultItem: ['Something nice', { option: true }],
-
- // Our "todoPage" fixture depends on the option.
- todoPage: async ({ page, defaultItem }, use) => {
- const todoPage = new TodoPage(page);
- await todoPage.goto();
- await todoPage.addToDo(defaultItem);
- await use(todoPage);
- await todoPage.removeAll();
- },
-});
-exports.expect = base.expect;
-```
-
-```js tab=js-ts title="my-test.ts"
+```js title="my-test.ts"
import { test as base } from '@playwright/test';
import { TodoPage } from './todo-page';
@@ -885,25 +537,7 @@ export { expect } from '@playwright/test';
We can now use `todoPage` fixture as usual, and set the `defaultItem` option in the config file.
-```js tab=js-js title="playwright.config.ts"
-// @ts-check
-
-const { defineConfig } = require('@playwright/test');
-module.exports = defineConfig({
- projects: [
- {
- name: 'shopping',
- use: { defaultItem: 'Buy milk' },
- },
- {
- name: 'wellbeing',
- use: { defaultItem: 'Exercise!' },
- },
- ]
-});
-```
-
-```js tab=js-ts title="playwright.config.ts"
+```js title="playwright.config.ts"
import { defineConfig } from '@playwright/test';
import type { MyOptions } from './my-test';
@@ -932,50 +566,7 @@ Fixtures follow these rules to determine the execution order:
Consider the following example:
-```js tab=js-js
-const { test: base } = require('@playwright/test');
-
-const test = base.extend({
- workerFixture: [async ({ browser }) => {
- // workerFixture setup...
- await use('workerFixture');
- // workerFixture teardown...
- }, { scope: 'worker' }],
-
- autoWorkerFixture: [async ({ browser }) => {
- // autoWorkerFixture setup...
- await use('autoWorkerFixture');
- // autoWorkerFixture teardown...
- }, { scope: 'worker', auto: true }],
-
- testFixture: [async ({ page, workerFixture }) => {
- // testFixture setup...
- await use('testFixture');
- // testFixture teardown...
- }, { scope: 'test' }],
-
- autoTestFixture: [async () => {
- // autoTestFixture setup...
- await use('autoTestFixture');
- // autoTestFixture teardown...
- }, { scope: 'test', auto: true }],
-
- unusedFixture: [async ({ page }) => {
- // unusedFixture setup...
- await use('unusedFixture');
- // unusedFixture teardown...
- }, { scope: 'test' }],
-});
-
-test.beforeAll(async () => { /* ... */ });
-test.beforeEach(async ({ page }) => { /* ... */ });
-test('first test', async ({ page }) => { /* ... */ });
-test('second test', async ({ testFixture }) => { /* ... */ });
-test.afterEach(async () => { /* ... */ });
-test.afterAll(async () => { /* ... */ });
-```
-
-```js tab=js-ts
+```js
import { test as base } from '@playwright/test';
const test = base.extend<{
@@ -1081,3 +672,30 @@ test('passes', async ({ database, page, a11y }) => {
// use database and a11y fixtures.
});
```
+
+## Box fixtures
+
+You can minimize the fixture exposure to the reporters UI and error messages via boxing it:
+
+```js
+import { test as base } from '@playwright/test';
+
+export const test = base.extend({
+ _helperFixture: [async ({}, use, testInfo) => {
+ }, { box: true }],
+});
+```
+
+## Custom fixture title
+
+You can assign a custom title to a fixture to be used in error messages and in the
+reporters UI:
+
+```js
+import { test as base } from '@playwright/test';
+
+export const test = base.extend({
+ _innerFixture: [async ({}, use, testInfo) => {
+ }, { title: 'my fixture' }],
+});
+```
diff --git a/docs/src/test-sharding-js.md b/docs/src/test-sharding-js.md
index 068b7172d0..d0312db811 100644
--- a/docs/src/test-sharding-js.md
+++ b/docs/src/test-sharding-js.md
@@ -5,7 +5,11 @@ title: "Sharding"
## Introduction
-By default, Playwright runs test files in [parallel](./test-parallel.md) and strives for optimal utilization of CPU cores on your machine. In order to achieve even greater parallelisation, you can further scale Playwright test execution by running tests on multiple machines simultaneously. We call this mode of operation "sharding".
+By default, Playwright runs test files in [parallel](./test-parallel.md) and strives for optimal utilization of CPU cores on your machine. In order to achieve even greater parallelisation, you can further scale Playwright test execution by running tests on multiple machines simultaneously. We call this mode of operation "sharding". Sharding in Playwright means splitting your tests into smaller parts called "shards". Each shard is like a separate job that can run independently. The whole purpose is to divide your tests to speed up test runtime.
+
+When you shard your tests, each shard can run on its own, utilizing the available CPU cores. This helps speed up the testing process by doing tasks simultaneously.
+
+In a CI pipeline, each shard can run as a separate job, making use of the hardware resources available in your CI pipeline, like CPU cores, to run tests faster.
## Sharding tests between multiple machines
@@ -18,7 +22,7 @@ npx playwright test --shard=3/4
npx playwright test --shard=4/4
```
-Now, if you run these shards in parallel on different computers, your test suite completes four times faster.
+Now, if you run these shards in parallel on different jobs, your test suite completes four times faster.
Note that Playwright can only shard tests that can be run in parallel. By default, this means Playwright will shard test files. Learn about other options in the [parallelism guide](./test-parallel.md).
diff --git a/package-lock.json b/package-lock.json
index acd9467df5..b63578e29e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -44,7 +44,7 @@
"concurrently": "^6.2.1",
"cross-env": "^7.0.3",
"dotenv": "^16.0.0",
- "electron": "19.0.11",
+ "electron": "^30.1.2",
"enquirer": "^2.3.6",
"esbuild": "^0.18.11",
"eslint": "^8.55.0",
@@ -831,25 +831,24 @@
}
},
"node_modules/@electron/get": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/@electron/get/-/get-1.14.1.tgz",
- "integrity": "sha512-BrZYyL/6m0ZXz/lDxy/nlVhQz+WF+iPS6qXolEU8atw7h6v1aYkjwJZ63m+bJMBTxDE66X+r2tPS4a/8C82sZw==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz",
+ "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==",
"dev": true,
"dependencies": {
"debug": "^4.1.1",
"env-paths": "^2.2.0",
"fs-extra": "^8.1.0",
- "got": "^9.6.0",
+ "got": "^11.8.5",
"progress": "^2.0.3",
"semver": "^6.2.0",
"sumchecker": "^3.0.1"
},
"engines": {
- "node": ">=8.6"
+ "node": ">=12"
},
"optionalDependencies": {
- "global-agent": "^3.0.0",
- "global-tunnel-ng": "^2.7.1"
+ "global-agent": "^3.0.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
@@ -1698,12 +1697,15 @@
]
},
"node_modules/@sindresorhus/is": {
- "version": "0.14.0",
- "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
- "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==",
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
+ "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
"dev": true,
"engines": {
- "node": ">=6"
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/is?sponsor=1"
}
},
"node_modules/@sveltejs/vite-plugin-svelte": {
@@ -1744,15 +1746,15 @@
}
},
"node_modules/@szmarczak/http-timer": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz",
- "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==",
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
+ "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==",
"dev": true,
"dependencies": {
- "defer-to-connect": "^1.0.1"
+ "defer-to-connect": "^2.0.0"
},
"engines": {
- "node": ">=6"
+ "node": ">=10"
}
},
"node_modules/@types/babel__core": {
@@ -1792,6 +1794,18 @@
"@babel/types": "^7.20.7"
}
},
+ "node_modules/@types/cacheable-request": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
+ "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==",
+ "dev": true,
+ "dependencies": {
+ "@types/http-cache-semantics": "*",
+ "@types/keyv": "^3.1.4",
+ "@types/node": "*",
+ "@types/responselike": "^1.0.0"
+ }
+ },
"node_modules/@types/codemirror": {
"version": "5.60.15",
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.15.tgz",
@@ -1815,12 +1829,27 @@
"@types/node": "*"
}
},
+ "node_modules/@types/http-cache-semantics": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz",
+ "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==",
+ "dev": true
+ },
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true
},
+ "node_modules/@types/keyv": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz",
+ "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/node": {
"version": "18.19.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.8.tgz",
@@ -1862,6 +1891,15 @@
"integrity": "sha512-cNw5iH8JkMkb3QkCoe7DaZiawbDQEUX8t7iuQaRTyLOyQCR2h+ibBD4GJt7p5yhUHrlOeL7ZtbxNHeipqNsBzQ==",
"dev": true
},
+ "node_modules/@types/responselike": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz",
+ "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/scheduler": {
"version": "0.16.8",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
@@ -1901,6 +1939,16 @@
"@types/node": "*"
}
},
+ "node_modules/@types/yauzl": {
+ "version": "2.10.3",
+ "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
+ "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "6.19.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.19.0.tgz",
@@ -2773,69 +2821,33 @@
"node": "*"
}
},
- "node_modules/buffer-from": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
- "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
- "dev": true
+ "node_modules/cacheable-lookup": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz",
+ "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10.6.0"
+ }
},
"node_modules/cacheable-request": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz",
- "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==",
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz",
+ "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==",
"dev": true,
"dependencies": {
"clone-response": "^1.0.2",
"get-stream": "^5.1.0",
"http-cache-semantics": "^4.0.0",
- "keyv": "^3.0.0",
+ "keyv": "^4.0.0",
"lowercase-keys": "^2.0.0",
- "normalize-url": "^4.1.0",
- "responselike": "^1.0.2"
+ "normalize-url": "^6.0.1",
+ "responselike": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
- "node_modules/cacheable-request/node_modules/get-stream": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
- "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
- "dev": true,
- "dependencies": {
- "pump": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/cacheable-request/node_modules/json-buffer": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz",
- "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==",
- "dev": true
- },
- "node_modules/cacheable-request/node_modules/keyv": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz",
- "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==",
- "dev": true,
- "dependencies": {
- "json-buffer": "3.0.0"
- }
- },
- "node_modules/cacheable-request/node_modules/lowercase-keys": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
- "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
- "dev": true,
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/call-bind": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
@@ -3048,21 +3060,6 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
},
- "node_modules/concat-stream": {
- "version": "1.6.2",
- "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
- "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
- "dev": true,
- "engines": [
- "node >= 0.8"
- ],
- "dependencies": {
- "buffer-from": "^1.0.0",
- "inherits": "^2.0.3",
- "readable-stream": "^2.2.2",
- "typedarray": "^0.0.6"
- }
- },
"node_modules/concurrently": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-6.5.1.tgz",
@@ -3170,28 +3167,11 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
- "node_modules/config-chain": {
- "version": "1.1.13",
- "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
- "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==",
- "dev": true,
- "optional": true,
- "dependencies": {
- "ini": "^1.3.4",
- "proto-list": "~1.2.1"
- }
- },
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
},
- "node_modules/core-util-is": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
- "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
- "dev": true
- },
"node_modules/cross-env": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
@@ -3293,15 +3273,30 @@
}
},
"node_modules/decompress-response": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz",
- "integrity": "sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==",
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"dev": true,
"dependencies": {
- "mimic-response": "^1.0.0"
+ "mimic-response": "^3.1.0"
},
"engines": {
- "node": ">=4"
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/decompress-response/node_modules/mimic-response": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/deep-is": {
@@ -3319,10 +3314,13 @@
}
},
"node_modules/defer-to-connect": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz",
- "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==",
- "dev": true
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
+ "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
},
"node_modules/define-data-property": {
"version": "1.1.1",
@@ -3416,28 +3414,22 @@
"url": "https://github.com/motdotla/dotenv?sponsor=1"
}
},
- "node_modules/duplexer3": {
- "version": "0.1.5",
- "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz",
- "integrity": "sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==",
- "dev": true
- },
"node_modules/electron": {
- "version": "19.0.11",
- "resolved": "https://registry.npmjs.org/electron/-/electron-19.0.11.tgz",
- "integrity": "sha512-GPM6C1Ze17/gR4koTE171MxrI5unYfFRgXQdkMdpWM2Cd55LMUrVa0QHCsfKpsaloufv9T65lsOn0uZuzCw5UA==",
+ "version": "30.1.2",
+ "resolved": "https://registry.npmjs.org/electron/-/electron-30.1.2.tgz",
+ "integrity": "sha512-A5CFGwbA+HSXnzwjc8fP2GIezBcAb0uN/VbNGLOW8DHOYn07rvJ/1bAJECHUUzt5zbfohveG3hpMQiYpbktuDw==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
- "@electron/get": "^1.14.1",
- "@types/node": "^16.11.26",
- "extract-zip": "^1.0.3"
+ "@electron/get": "^2.0.0",
+ "@types/node": "^20.9.0",
+ "extract-zip": "^2.0.1"
},
"bin": {
"electron": "cli.js"
},
"engines": {
- "node": ">= 8.6"
+ "node": ">= 12.20.55"
}
},
"node_modules/electron-to-chromium": {
@@ -3446,10 +3438,13 @@
"integrity": "sha512-CkKf3ZUVZchr+zDpAlNLEEy2NJJ9T64ULWaDgy3THXXlPVPkLu3VOs9Bac44nebVtdwl2geSj6AxTtGDOxoXhg=="
},
"node_modules/electron/node_modules/@types/node": {
- "version": "16.18.71",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.71.tgz",
- "integrity": "sha512-ARO+458bNJQeNEFuPyT6W+q9ULotmsQzhV3XABsFSxEvRMUYENcBsNAHWYPlahU+UHa5gCVwyKT1Z3f1Wwr26Q==",
- "dev": true
+ "version": "20.12.13",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.13.tgz",
+ "integrity": "sha512-gBGeanV41c1L171rR7wjbMiEpEI/l5XFQdLLfhr/REwpgDy/4U8y89+i8kRiLzDyZdOkXh+cRaTetUnCYutoXA==",
+ "dev": true,
+ "dependencies": {
+ "undici-types": "~5.26.4"
+ }
},
"node_modules/emoji-regex": {
"version": "8.0.0",
@@ -3457,16 +3452,6 @@
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
},
- "node_modules/encodeurl": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
- "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
- "dev": true,
- "optional": true,
- "engines": {
- "node": ">= 0.8"
- }
- },
"node_modules/end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
@@ -4067,35 +4052,25 @@
}
},
"node_modules/extract-zip": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz",
- "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
+ "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
"dev": true,
"dependencies": {
- "concat-stream": "^1.6.2",
- "debug": "^2.6.9",
- "mkdirp": "^0.5.4",
+ "debug": "^4.1.1",
+ "get-stream": "^5.1.0",
"yauzl": "^2.10.0"
},
"bin": {
"extract-zip": "cli.js"
+ },
+ "engines": {
+ "node": ">= 10.17.0"
+ },
+ "optionalDependencies": {
+ "@types/yauzl": "^2.9.1"
}
},
- "node_modules/extract-zip/node_modules/debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "dev": true,
- "dependencies": {
- "ms": "2.0.0"
- }
- },
- "node_modules/extract-zip/node_modules/ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
- "dev": true
- },
"node_modules/eyes": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz",
@@ -4355,15 +4330,18 @@
}
},
"node_modules/get-stream": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
- "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+ "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
"dev": true,
"dependencies": {
"pump": "^3.0.0"
},
"engines": {
- "node": ">=6"
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-symbol-description": {
@@ -4468,9 +4446,9 @@
}
},
"node_modules/global-agent/node_modules/semver": {
- "version": "7.5.4",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
- "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
+ "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
"dev": true,
"optional": true,
"dependencies": {
@@ -4490,22 +4468,6 @@
"dev": true,
"optional": true
},
- "node_modules/global-tunnel-ng": {
- "version": "2.7.1",
- "resolved": "https://registry.npmjs.org/global-tunnel-ng/-/global-tunnel-ng-2.7.1.tgz",
- "integrity": "sha512-4s+DyciWBV0eK148wqXxcmVAbFVPqtc3sEtUE/GTQfuU80rySLcMhUmHKSHI7/LDj8q0gDYI1lIhRRB7ieRAqg==",
- "dev": true,
- "optional": true,
- "dependencies": {
- "encodeurl": "^1.0.2",
- "lodash": "^4.17.10",
- "npm-conf": "^1.1.3",
- "tunnel": "^0.0.6"
- },
- "engines": {
- "node": ">=0.10"
- }
- },
"node_modules/globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
@@ -4571,25 +4533,28 @@
}
},
"node_modules/got": {
- "version": "9.6.0",
- "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz",
- "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==",
+ "version": "11.8.6",
+ "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz",
+ "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==",
"dev": true,
"dependencies": {
- "@sindresorhus/is": "^0.14.0",
- "@szmarczak/http-timer": "^1.1.2",
- "cacheable-request": "^6.0.0",
- "decompress-response": "^3.3.0",
- "duplexer3": "^0.1.4",
- "get-stream": "^4.1.0",
- "lowercase-keys": "^1.0.1",
- "mimic-response": "^1.0.1",
- "p-cancelable": "^1.0.0",
- "to-readable-stream": "^1.0.0",
- "url-parse-lax": "^3.0.0"
+ "@sindresorhus/is": "^4.0.0",
+ "@szmarczak/http-timer": "^4.0.5",
+ "@types/cacheable-request": "^6.0.1",
+ "@types/responselike": "^1.0.0",
+ "cacheable-lookup": "^5.0.3",
+ "cacheable-request": "^7.0.2",
+ "decompress-response": "^6.0.0",
+ "http2-wrapper": "^1.0.0-beta.5.2",
+ "lowercase-keys": "^2.0.0",
+ "p-cancelable": "^2.0.0",
+ "responselike": "^2.0.0"
},
"engines": {
- "node": ">=8.6"
+ "node": ">=10.19.0"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/got?sponsor=1"
}
},
"node_modules/graceful-fs": {
@@ -4714,6 +4679,19 @@
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
"dev": true
},
+ "node_modules/http2-wrapper": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
+ "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==",
+ "dev": true,
+ "dependencies": {
+ "quick-lru": "^5.1.1",
+ "resolve-alpn": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=10.19.0"
+ }
+ },
"node_modules/ignore": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz",
@@ -4764,13 +4742,6 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
- "node_modules/ini": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
- "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
- "dev": true,
- "optional": true
- },
"node_modules/internal-slot": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz",
@@ -5139,12 +5110,6 @@
"url": "https://github.com/sponsors/mesqueeb"
}
},
- "node_modules/isarray": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
- "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
- "dev": true
- },
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -5378,12 +5343,12 @@
}
},
"node_modules/lowercase-keys": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz",
- "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
+ "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
"dev": true,
"engines": {
- "node": ">=0.10.0"
+ "node": ">=8"
}
},
"node_modules/lru-cache": {
@@ -5670,36 +5635,15 @@
}
},
"node_modules/normalize-url": {
- "version": "4.5.1",
- "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz",
- "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==",
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz",
+ "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==",
"dev": true,
"engines": {
- "node": ">=8"
- }
- },
- "node_modules/npm-conf": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz",
- "integrity": "sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==",
- "dev": true,
- "optional": true,
- "dependencies": {
- "config-chain": "^1.1.11",
- "pify": "^3.0.0"
+ "node": ">=10"
},
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/npm-conf/node_modules/pify": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
- "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==",
- "dev": true,
- "optional": true,
- "engines": {
- "node": ">=4"
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/npm-normalize-package-bin": {
@@ -5869,12 +5813,12 @@
}
},
"node_modules/p-cancelable": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz",
- "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==",
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
+ "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==",
"dev": true,
"engines": {
- "node": ">=6"
+ "node": ">=8"
}
},
"node_modules/p-limit": {
@@ -6068,15 +6012,6 @@
"node": ">= 0.8.0"
}
},
- "node_modules/prepend-http": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
- "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==",
- "dev": true,
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/prettier": {
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
@@ -6092,12 +6027,6 @@
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
- "node_modules/process-nextick-args": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
- "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
- "dev": true
- },
"node_modules/progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
@@ -6118,13 +6047,6 @@
"react-is": "^16.13.1"
}
},
- "node_modules/proto-list": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
- "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
- "dev": true,
- "optional": true
- },
"node_modules/pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
@@ -6179,6 +6101,18 @@
}
]
},
+ "node_modules/quick-lru": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
+ "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/react": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
@@ -6256,21 +6190,6 @@
"npm-normalize-package-bin": "^1.0.0"
}
},
- "node_modules/readable-stream": {
- "version": "2.3.8",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
- "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
- "dev": true,
- "dependencies": {
- "core-util-is": "~1.0.0",
- "inherits": "~2.0.3",
- "isarray": "~1.0.0",
- "process-nextick-args": "~2.0.0",
- "safe-buffer": "~5.1.1",
- "string_decoder": "~1.1.1",
- "util-deprecate": "~1.0.1"
- }
- },
"node_modules/readdir-scoped-modules": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz",
@@ -6369,6 +6288,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/resolve-alpn": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
+ "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
+ "dev": true
+ },
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -6379,12 +6304,15 @@
}
},
"node_modules/responselike": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz",
- "integrity": "sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz",
+ "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==",
"dev": true,
"dependencies": {
- "lowercase-keys": "^1.0.0"
+ "lowercase-keys": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/reusify": {
@@ -6522,12 +6450,6 @@
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
"dev": true
},
- "node_modules/safe-buffer": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
- "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
- "dev": true
- },
"node_modules/safe-regex-test": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz",
@@ -6861,15 +6783,6 @@
"node": "*"
}
},
- "node_modules/string_decoder": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
- "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
- "dev": true,
- "dependencies": {
- "safe-buffer": "~5.1.0"
- }
- },
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@@ -7063,15 +6976,6 @@
"node": ">=4"
}
},
- "node_modules/to-readable-stream": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz",
- "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==",
- "dev": true,
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -7223,12 +7127,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/typedarray": {
- "version": "0.0.6",
- "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
- "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
- "dev": true
- },
"node_modules/typescript": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
@@ -7322,24 +7220,6 @@
"punycode": "^2.1.0"
}
},
- "node_modules/url-parse-lax": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
- "integrity": "sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==",
- "dev": true,
- "dependencies": {
- "prepend-http": "^2.0.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/util-deprecate": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
- "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
- "dev": true
- },
"node_modules/util-extend": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/util-extend/-/util-extend-1.0.3.tgz",
diff --git a/package.json b/package.json
index 125abb0b7c..6caf9c748d 100644
--- a/package.json
+++ b/package.json
@@ -82,7 +82,7 @@
"concurrently": "^6.2.1",
"cross-env": "^7.0.3",
"dotenv": "^16.0.0",
- "electron": "19.0.11",
+ "electron": "^30.1.2",
"enquirer": "^2.3.6",
"esbuild": "^0.18.11",
"eslint": "^8.55.0",
diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json
index d21d1c67e7..761a95ce6b 100644
--- a/packages/playwright-core/browsers.json
+++ b/packages/playwright-core/browsers.json
@@ -3,15 +3,15 @@
"browsers": [
{
"name": "chromium",
- "revision": "1123",
+ "revision": "1125",
"installByDefault": true,
- "browserVersion": "127.0.6533.5"
+ "browserVersion": "127.0.6533.26"
},
{
"name": "chromium-tip-of-tree",
- "revision": "1231",
+ "revision": "1236",
"installByDefault": false,
- "browserVersion": "128.0.6536.0"
+ "browserVersion": "128.0.6561.0"
},
{
"name": "firefox",
@@ -27,7 +27,7 @@
},
{
"name": "webkit",
- "revision": "2036",
+ "revision": "2039",
"installByDefault": true,
"revisionOverrides": {
"mac10.14": "1446",
diff --git a/packages/playwright-core/src/client/browser.ts b/packages/playwright-core/src/client/browser.ts
index 7c353656cb..be47ddeb51 100644
--- a/packages/playwright-core/src/client/browser.ts
+++ b/packages/playwright-core/src/client/browser.ts
@@ -85,16 +85,6 @@ export class Browser extends ChannelOwner implements ap
const response = forReuse ? await this._channel.newContextForReuse(contextOptions) : await this._channel.newContext(contextOptions);
const context = BrowserContext.from(response.context);
await this._browserType._didCreateContext(context, contextOptions, this._options, options.logger || this._logger);
- if (!forReuse && process.env.PW_CLOCK === 'frozen') {
- await this._wrapApiCall(async () => {
- await context.clock.install({ time: 0 });
- await context.clock.pauseAt(1000);
- }, true);
- } else if (!forReuse && process.env.PW_CLOCK === 'realtime') {
- await this._wrapApiCall(async () => {
- await context.clock.install({ time: 0 });
- }, true);
- }
return context;
}
diff --git a/packages/playwright-core/src/client/input.ts b/packages/playwright-core/src/client/input.ts
index e06b0e3e4a..0cc841619f 100644
--- a/packages/playwright-core/src/client/input.ts
+++ b/packages/playwright-core/src/client/input.ts
@@ -89,4 +89,8 @@ export class Touchscreen implements api.Touchscreen {
async tap(x: number, y: number) {
await this._page._channel.touchscreenTap({ x, y });
}
+
+ async touch(type: 'touchstart'|'touchmove'|'touchend'|'touchcancel', touchPoints: { x: number, y: number, id?: number }[]) {
+ await this._page._channel.touchscreenTouch({ type, touchPoints });
+ }
}
diff --git a/packages/playwright-core/src/protocol/debug.ts b/packages/playwright-core/src/protocol/debug.ts
index 4f44f5941e..f8e1f6c4b2 100644
--- a/packages/playwright-core/src/protocol/debug.ts
+++ b/packages/playwright-core/src/protocol/debug.ts
@@ -31,6 +31,7 @@ export const slowMoActions = new Set([
'Page.mouseClick',
'Page.mouseWheel',
'Page.touchscreenTap',
+ 'Page.touchscreenTouch',
'Frame.blur',
'Frame.check',
'Frame.click',
@@ -89,6 +90,7 @@ export const commandsWithTracingSnapshots = new Set([
'Page.mouseClick',
'Page.mouseWheel',
'Page.touchscreenTap',
+ 'Page.touchscreenTouch',
'Frame.evalOnSelector',
'Frame.evalOnSelectorAll',
'Frame.addScriptTag',
diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts
index 9187dc13d2..63679d1993 100644
--- a/packages/playwright-core/src/protocol/validator.ts
+++ b/packages/playwright-core/src/protocol/validator.ts
@@ -1237,6 +1237,15 @@ scheme.PageTouchscreenTapParams = tObject({
y: tNumber,
});
scheme.PageTouchscreenTapResult = tOptional(tObject({}));
+scheme.PageTouchscreenTouchParams = tObject({
+ type: tEnum(['touchstart', 'touchend', 'touchmove', 'touchcancel']),
+ touchPoints: tArray(tObject({
+ x: tNumber,
+ y: tNumber,
+ id: tOptional(tNumber),
+ })),
+});
+scheme.PageTouchscreenTouchResult = tOptional(tObject({}));
scheme.PageAccessibilitySnapshotParams = tObject({
interestingOnly: tOptional(tBoolean),
root: tOptional(tChannel(['ElementHandle'])),
diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts
index 404f94fe4f..92f3f8a20b 100644
--- a/packages/playwright-core/src/server/browserContext.ts
+++ b/packages/playwright-core/src/server/browserContext.ts
@@ -24,6 +24,7 @@ import type { Download } from './download';
import type * as frames from './frames';
import { helper } from './helper';
import * as network from './network';
+import { InitScript } from './page';
import type { PageDelegate } from './page';
import { Page, PageBinding } from './page';
import type { Progress, ProgressController } from './progress';
@@ -84,7 +85,7 @@ export abstract class BrowserContext extends SdkObject {
private _customCloseHandler?: () => Promise;
readonly _tempDirs: string[] = [];
private _settingStorageState = false;
- readonly initScripts: string[] = [];
+ readonly initScripts: InitScript[] = [];
private _routesInFlight = new Set();
private _debugger!: Debugger;
_closeReason: string | undefined;
@@ -266,7 +267,7 @@ export abstract class BrowserContext extends SdkObject {
protected abstract doGrantPermissions(origin: string, permissions: string[]): Promise;
protected abstract doClearPermissions(): Promise;
protected abstract doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise;
- protected abstract doAddInitScript(expression: string): Promise;
+ protected abstract doAddInitScript(initScript: InitScript): Promise;
protected abstract doRemoveInitScripts(): Promise;
protected abstract doExposeBinding(binding: PageBinding): Promise;
protected abstract doRemoveExposedBindings(): Promise;
@@ -403,9 +404,10 @@ export abstract class BrowserContext extends SdkObject {
this._options.httpCredentials = { username, password: password || '' };
}
- async addInitScript(script: string) {
- this.initScripts.push(script);
- await this.doAddInitScript(script);
+ async addInitScript(source: string) {
+ const initScript = new InitScript(source);
+ this.initScripts.push(initScript);
+ await this.doAddInitScript(initScript);
}
async _removeInitScripts(): Promise {
@@ -654,8 +656,13 @@ export function validateBrowserContextOptions(options: channels.BrowserNewContex
throw new Error(`"deviceScaleFactor" option is not supported with null "viewport"`);
if (options.noDefaultViewport && !!options.isMobile)
throw new Error(`"isMobile" option is not supported with null "viewport"`);
- if (options.acceptDownloads === undefined)
+ if (options.acceptDownloads === undefined && browserOptions.name !== 'electron')
options.acceptDownloads = 'accept';
+ // Electron requires explicit acceptDownloads: true since we wait for
+ // https://github.com/electron/electron/pull/41718 to be widely shipped.
+ // In 6-12 months, we can remove this check.
+ else if (options.acceptDownloads === undefined && browserOptions.name === 'electron')
+ options.acceptDownloads = 'internal-browser-default';
if (!options.viewport && !options.noDefaultViewport)
options.viewport = { width: 1280, height: 720 };
if (options.recordVideo) {
diff --git a/packages/playwright-core/src/server/chromium/chromium.ts b/packages/playwright-core/src/server/chromium/chromium.ts
index 6200977473..6a369c918d 100644
--- a/packages/playwright-core/src/server/chromium/chromium.ts
+++ b/packages/playwright-core/src/server/chromium/chromium.ts
@@ -319,7 +319,7 @@ export class Chromium extends BrowserType {
if (process.env.PLAYWRIGHT_CHROMIUM_USE_HEADLESS_NEW)
chromeArguments.push('--headless=new');
else
- chromeArguments.push('--headless');
+ chromeArguments.push('--headless=old');
chromeArguments.push(
'--hide-scrollbars',
diff --git a/packages/playwright-core/src/server/chromium/crBrowser.ts b/packages/playwright-core/src/server/chromium/crBrowser.ts
index 09a24c1d1b..777ff2eee8 100644
--- a/packages/playwright-core/src/server/chromium/crBrowser.ts
+++ b/packages/playwright-core/src/server/chromium/crBrowser.ts
@@ -21,7 +21,7 @@ import { Browser } from '../browser';
import { assertBrowserContextIsNotOwned, BrowserContext, verifyGeolocation } from '../browserContext';
import { assert, createGuid } from '../../utils';
import * as network from '../network';
-import type { PageBinding, PageDelegate, Worker } from '../page';
+import type { InitScript, PageBinding, PageDelegate, Worker } from '../page';
import { Page } from '../page';
import { Frame } from '../frames';
import type { Dialog } from '../dialog';
@@ -348,7 +348,7 @@ export class CRBrowserContext extends BrowserContext {
override async _initialize() {
assert(!Array.from(this._browser._crPages.values()).some(page => page._browserContext === this));
const promises: Promise[] = [super._initialize()];
- if (this._browser.options.name !== 'electron' && this._browser.options.name !== 'clank' && this._options.acceptDownloads !== 'internal-browser-default') {
+ if (this._browser.options.name !== 'clank' && this._options.acceptDownloads !== 'internal-browser-default') {
promises.push(this._browser._session.send('Browser.setDownloadBehavior', {
behavior: this._options.acceptDownloads === 'accept' ? 'allowAndName' : 'deny',
browserContextId: this._browserContextId,
@@ -486,9 +486,9 @@ export class CRBrowserContext extends BrowserContext {
await (sw as CRServiceWorker).updateHttpCredentials();
}
- async doAddInitScript(source: string) {
+ async doAddInitScript(initScript: InitScript) {
for (const page of this.pages())
- await (page._delegate as CRPage).addInitScript(source);
+ await (page._delegate as CRPage).addInitScript(initScript);
}
async doRemoveInitScripts() {
diff --git a/packages/playwright-core/src/server/chromium/crInput.ts b/packages/playwright-core/src/server/chromium/crInput.ts
index bbfd973d10..47e7cc53e9 100644
--- a/packages/playwright-core/src/server/chromium/crInput.ts
+++ b/packages/playwright-core/src/server/chromium/crInput.ts
@@ -179,4 +179,19 @@ export class RawTouchscreenImpl implements input.RawTouchscreen {
}),
]);
}
+ async touch(eventType: 'touchstart'|'touchmove'|'touchend'|'touchcancel', touchPoints: { x: number, y: number, id?: number }[], modifiers: Set) {
+ let type: 'touchStart' | 'touchMove' | 'touchEnd' | 'touchCancel';
+ switch (eventType) {
+ case 'touchstart': type = 'touchStart'; break;
+ case 'touchmove': type = 'touchMove'; break;
+ case 'touchend': type = 'touchEnd'; break;
+ case 'touchcancel': type = 'touchCancel'; break;
+ default: throw new Error('Invalid eventType: ' + eventType);
+ }
+ await this._client.send('Input.dispatchTouchEvent', {
+ type,
+ touchPoints,
+ modifiers: toModifiersMask(modifiers)
+ });
+ }
}
diff --git a/packages/playwright-core/src/server/chromium/crNetworkManager.ts b/packages/playwright-core/src/server/chromium/crNetworkManager.ts
index 3752a5c06f..850f466afa 100644
--- a/packages/playwright-core/src/server/chromium/crNetworkManager.ts
+++ b/packages/playwright-core/src/server/chromium/crNetworkManager.ts
@@ -332,12 +332,13 @@ export class CRNetworkManager {
}
let route = null;
+ let headersOverride: types.HeadersArray | undefined;
if (requestPausedEvent) {
// We do not support intercepting redirects.
if (redirectedFrom || (!this._userRequestInterceptionEnabled && this._protocolRequestInterceptionEnabled)) {
// Chromium does not preserve header overrides between redirects, so we have to do it ourselves.
- const headers = redirectedFrom?._originalRequestRoute?._alreadyContinuedParams?.headers;
- requestPausedSessionInfo!.session._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent.requestId, headers });
+ headersOverride = redirectedFrom?._originalRequestRoute?._alreadyContinuedParams?.headers;
+ requestPausedSessionInfo!.session._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent.requestId, headers: headersOverride });
} else {
route = new RouteImpl(requestPausedSessionInfo!.session, requestPausedEvent.requestId);
}
@@ -353,15 +354,16 @@ export class CRNetworkManager {
route,
requestWillBeSentEvent,
requestPausedEvent,
- redirectedFrom
+ redirectedFrom,
+ headersOverride: headersOverride || null,
});
this._requestIdToRequest.set(requestWillBeSentEvent.requestId, request);
- if (requestPausedEvent) {
- // We will not receive extra info when intercepting the request.
+ if (route) {
+ // We may not receive extra info when intercepting the request.
// Use the headers from the Fetch.requestPausedPayload and release the allHeaders()
// right away, so that client can call it from the route handler.
- request.request.setRawRequestHeaders(headersObjectToArray(requestPausedEvent.request.headers, '\n'));
+ request.request.setRawRequestHeaders(headersObjectToArray(requestPausedEvent!.request.headers, '\n'));
}
(this._page?._frameManager || this._serviceWorker)!.requestStarted(request.request, route || undefined);
}
@@ -568,8 +570,9 @@ class InterceptableRequest {
requestWillBeSentEvent: Protocol.Network.requestWillBeSentPayload;
requestPausedEvent: Protocol.Fetch.requestPausedPayload | undefined;
redirectedFrom: InterceptableRequest | null;
+ headersOverride: types.HeadersArray | null;
}) {
- const { session, context, frame, documentId, route, requestWillBeSentEvent, requestPausedEvent, redirectedFrom, serviceWorker } = options;
+ const { session, context, frame, documentId, route, requestWillBeSentEvent, requestPausedEvent, redirectedFrom, serviceWorker, headersOverride } = options;
this.session = session;
this._timestamp = requestWillBeSentEvent.timestamp;
this._wallTime = requestWillBeSentEvent.wallTime;
@@ -591,7 +594,7 @@ class InterceptableRequest {
if (entries && entries.length)
postDataBuffer = Buffer.concat(entries.map(entry => Buffer.from(entry.bytes!, 'base64')));
- this.request = new network.Request(context, frame, serviceWorker, redirectedFrom?.request || null, documentId, url, type, method, postDataBuffer, headersObjectToArray(headers));
+ this.request = new network.Request(context, frame, serviceWorker, redirectedFrom?.request || null, documentId, url, type, method, postDataBuffer, headersOverride || headersObjectToArray(headers));
}
}
diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts
index 34d1c53cd4..53b96ad403 100644
--- a/packages/playwright-core/src/server/chromium/crPage.ts
+++ b/packages/playwright-core/src/server/chromium/crPage.ts
@@ -26,7 +26,7 @@ import * as dom from '../dom';
import * as frames from '../frames';
import { helper } from '../helper';
import * as network from '../network';
-import type { PageBinding, PageDelegate } from '../page';
+import type { InitScript, PageBinding, PageDelegate } from '../page';
import { Page, Worker } from '../page';
import type { Progress } from '../progress';
import type * as types from '../types';
@@ -256,8 +256,8 @@ export class CRPage implements PageDelegate {
return this._go(+1);
}
- async addInitScript(source: string, world: types.World = 'main'): Promise {
- await this._forAllFrameSessions(frame => frame._evaluateOnNewDocument(source, world));
+ async addInitScript(initScript: InitScript, world: types.World = 'main'): Promise {
+ await this._forAllFrameSessions(frame => frame._evaluateOnNewDocument(initScript, world));
}
async removeInitScripts() {
@@ -511,6 +511,20 @@ class FrameSession {
this._addRendererListeners();
}
+ const localFrames = this._isMainFrame() ? this._page.frames() : [this._page._frameManager.frame(this._targetId)!];
+ for (const frame of localFrames) {
+ // Note: frames might be removed before we send these.
+ this._client._sendMayFail('Page.createIsolatedWorld', {
+ frameId: frame._id,
+ grantUniveralAccess: true,
+ worldName: UTILITY_WORLD_NAME,
+ });
+ for (const binding of this._crPage._browserContext._pageBindings.values())
+ frame.evaluateExpression(binding.source).catch(e => {});
+ for (const initScript of this._crPage._browserContext.initScripts)
+ frame.evaluateExpression(initScript.source).catch(e => {});
+ }
+
const isInitialEmptyPage = this._isMainFrame() && this._page.mainFrame().url() === ':';
if (isInitialEmptyPage) {
// Ignore lifecycle events, worlds and bindings for the initial empty page. It is never the final page
@@ -520,20 +534,6 @@ class FrameSession {
this._eventListeners.push(eventsHelper.addEventListener(this._client, 'Page.lifecycleEvent', event => this._onLifecycleEvent(event)));
});
} else {
- const localFrames = this._isMainFrame() ? this._page.frames() : [this._page._frameManager.frame(this._targetId)!];
- for (const frame of localFrames) {
- // Note: frames might be removed before we send these.
- this._client._sendMayFail('Page.createIsolatedWorld', {
- frameId: frame._id,
- grantUniveralAccess: true,
- worldName: UTILITY_WORLD_NAME,
- });
- for (const binding of this._crPage._browserContext._pageBindings.values())
- frame.evaluateExpression(binding.source).catch(e => {});
- for (const source of this._crPage._browserContext.initScripts)
- frame.evaluateExpression(source).catch(e => {});
- }
-
this._firstNonInitialNavigationCommittedFulfill();
this._eventListeners.push(eventsHelper.addEventListener(this._client, 'Page.lifecycleEvent', event => this._onLifecycleEvent(event)));
}
@@ -575,10 +575,10 @@ class FrameSession {
promises.push(this._updateFileChooserInterception(true));
for (const binding of this._crPage._page.allBindings())
promises.push(this._initBinding(binding));
- for (const source of this._crPage._browserContext.initScripts)
- promises.push(this._evaluateOnNewDocument(source, 'main'));
- for (const source of this._crPage._page.initScripts)
- promises.push(this._evaluateOnNewDocument(source, 'main'));
+ for (const initScript of this._crPage._browserContext.initScripts)
+ promises.push(this._evaluateOnNewDocument(initScript, 'main'));
+ for (const initScript of this._crPage._page.initScripts)
+ promises.push(this._evaluateOnNewDocument(initScript, 'main'));
if (screencastOptions)
promises.push(this._startVideoRecording(screencastOptions));
}
@@ -1099,9 +1099,9 @@ class FrameSession {
await this._client.send('Page.setInterceptFileChooserDialog', { enabled }).catch(() => {}); // target can be closed.
}
- async _evaluateOnNewDocument(source: string, world: types.World): Promise {
+ async _evaluateOnNewDocument(initScript: InitScript, world: types.World): Promise {
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);
}
diff --git a/packages/playwright-core/src/server/chromium/crProtocolHelper.ts b/packages/playwright-core/src/server/chromium/crProtocolHelper.ts
index 36bc712aa6..9458bed124 100644
--- a/packages/playwright-core/src/server/chromium/crProtocolHelper.ts
+++ b/packages/playwright-core/src/server/chromium/crProtocolHelper.ts
@@ -37,7 +37,7 @@ export function getExceptionMessage(exceptionDetails: Protocol.Runtime.Exception
}
export async function releaseObject(client: CRSession, objectId: string) {
- await client.send('Runtime.releaseObject', { objectId }).catch(error => {});
+ await client.send('Runtime.releaseObject', { objectId }).catch(error => { });
}
export async function saveProtocolStream(client: CRSession, handle: string, path: string) {
@@ -91,7 +91,8 @@ export function exceptionToError(exceptionDetails: Protocol.Runtime.ExceptionDet
const err = new Error(message);
err.stack = stack;
- err.name = name;
+ const nameOverride = exceptionDetails.exception?.preview?.properties.find(o => o.name === 'name');
+ err.name = nameOverride ? nameOverride.value ?? 'Error' : name;
return err;
}
diff --git a/packages/playwright-core/src/server/deviceDescriptorsSource.json b/packages/playwright-core/src/server/deviceDescriptorsSource.json
index db818f07e7..ee4e655e5a 100644
--- a/packages/playwright-core/src/server/deviceDescriptorsSource.json
+++ b/packages/playwright-core/src/server/deviceDescriptorsSource.json
@@ -110,7 +110,7 @@
"defaultBrowserType": "webkit"
},
"Galaxy S5": {
- "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36",
"viewport": {
"width": 360,
"height": 640
@@ -121,7 +121,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S5 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 360
@@ -132,7 +132,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S8": {
- "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36",
"viewport": {
"width": 360,
"height": 740
@@ -143,7 +143,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S8 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36",
"viewport": {
"width": 740,
"height": 360
@@ -154,7 +154,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S9+": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36",
"viewport": {
"width": 320,
"height": 658
@@ -165,7 +165,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy S9+ landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36",
"viewport": {
"width": 658,
"height": 320
@@ -176,7 +176,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy Tab S4": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Safari/537.36",
"viewport": {
"width": 712,
"height": 1138
@@ -187,7 +187,7 @@
"defaultBrowserType": "chromium"
},
"Galaxy Tab S4 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Safari/537.36",
"viewport": {
"width": 1138,
"height": 712
@@ -978,7 +978,7 @@
"defaultBrowserType": "webkit"
},
"LG Optimus L70": {
- "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/127.0.6533.5 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/127.0.6533.26 Mobile Safari/537.36",
"viewport": {
"width": 384,
"height": 640
@@ -989,7 +989,7 @@
"defaultBrowserType": "chromium"
},
"LG Optimus L70 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/127.0.6533.5 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/127.0.6533.26 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 384
@@ -1000,7 +1000,7 @@
"defaultBrowserType": "chromium"
},
"Microsoft Lumia 550": {
- "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Mobile Safari/537.36 Edge/14.14263",
+ "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36 Edge/14.14263",
"viewport": {
"width": 640,
"height": 360
@@ -1011,7 +1011,7 @@
"defaultBrowserType": "chromium"
},
"Microsoft Lumia 550 landscape": {
- "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Mobile Safari/537.36 Edge/14.14263",
+ "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36 Edge/14.14263",
"viewport": {
"width": 360,
"height": 640
@@ -1022,7 +1022,7 @@
"defaultBrowserType": "chromium"
},
"Microsoft Lumia 950": {
- "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Mobile Safari/537.36 Edge/14.14263",
+ "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36 Edge/14.14263",
"viewport": {
"width": 360,
"height": 640
@@ -1033,7 +1033,7 @@
"defaultBrowserType": "chromium"
},
"Microsoft Lumia 950 landscape": {
- "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Mobile Safari/537.36 Edge/14.14263",
+ "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36 Edge/14.14263",
"viewport": {
"width": 640,
"height": 360
@@ -1044,7 +1044,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 10": {
- "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Safari/537.36",
"viewport": {
"width": 800,
"height": 1280
@@ -1055,7 +1055,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 10 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Safari/537.36",
"viewport": {
"width": 1280,
"height": 800
@@ -1066,7 +1066,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 4": {
- "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36",
"viewport": {
"width": 384,
"height": 640
@@ -1077,7 +1077,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 4 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 384
@@ -1088,7 +1088,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5": {
- "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36",
"viewport": {
"width": 360,
"height": 640
@@ -1099,7 +1099,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 360
@@ -1110,7 +1110,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5X": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36",
"viewport": {
"width": 412,
"height": 732
@@ -1121,7 +1121,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 5X landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36",
"viewport": {
"width": 732,
"height": 412
@@ -1132,7 +1132,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6": {
- "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36",
"viewport": {
"width": 412,
"height": 732
@@ -1143,7 +1143,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36",
"viewport": {
"width": 732,
"height": 412
@@ -1154,7 +1154,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6P": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36",
"viewport": {
"width": 412,
"height": 732
@@ -1165,7 +1165,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 6P landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36",
"viewport": {
"width": 732,
"height": 412
@@ -1176,7 +1176,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 7": {
- "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Safari/537.36",
"viewport": {
"width": 600,
"height": 960
@@ -1187,7 +1187,7 @@
"defaultBrowserType": "chromium"
},
"Nexus 7 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Safari/537.36",
"viewport": {
"width": 960,
"height": 600
@@ -1242,7 +1242,7 @@
"defaultBrowserType": "webkit"
},
"Pixel 2": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36",
"viewport": {
"width": 411,
"height": 731
@@ -1253,7 +1253,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 2 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36",
"viewport": {
"width": 731,
"height": 411
@@ -1264,7 +1264,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 2 XL": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36",
"viewport": {
"width": 411,
"height": 823
@@ -1275,7 +1275,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 2 XL landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36",
"viewport": {
"width": 823,
"height": 411
@@ -1286,7 +1286,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 3": {
- "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36",
"viewport": {
"width": 393,
"height": 786
@@ -1297,7 +1297,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 3 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36",
"viewport": {
"width": 786,
"height": 393
@@ -1308,7 +1308,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4": {
- "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36",
"viewport": {
"width": 353,
"height": 745
@@ -1319,7 +1319,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36",
"viewport": {
"width": 745,
"height": 353
@@ -1330,7 +1330,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4a (5G)": {
- "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36",
"screen": {
"width": 412,
"height": 892
@@ -1345,7 +1345,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 4a (5G) landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36",
"screen": {
"height": 892,
"width": 412
@@ -1360,7 +1360,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 5": {
- "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36",
"screen": {
"width": 393,
"height": 851
@@ -1375,7 +1375,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 5 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36",
"screen": {
"width": 851,
"height": 393
@@ -1390,7 +1390,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 7": {
- "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36",
"screen": {
"width": 412,
"height": 915
@@ -1405,7 +1405,7 @@
"defaultBrowserType": "chromium"
},
"Pixel 7 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36",
"screen": {
"width": 915,
"height": 412
@@ -1420,7 +1420,7 @@
"defaultBrowserType": "chromium"
},
"Moto G4": {
- "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36",
"viewport": {
"width": 360,
"height": 640
@@ -1431,7 +1431,7 @@
"defaultBrowserType": "chromium"
},
"Moto G4 landscape": {
- "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Mobile Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Mobile Safari/537.36",
"viewport": {
"width": 640,
"height": 360
@@ -1442,7 +1442,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Chrome HiDPI": {
- "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Safari/537.36",
"screen": {
"width": 1792,
"height": 1120
@@ -1457,7 +1457,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Edge HiDPI": {
- "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Safari/537.36 Edg/127.0.6533.5",
+ "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Safari/537.36 Edg/127.0.6533.26",
"screen": {
"width": 1792,
"height": 1120
@@ -1502,7 +1502,7 @@
"defaultBrowserType": "webkit"
},
"Desktop Chrome": {
- "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Safari/537.36",
+ "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Safari/537.36",
"screen": {
"width": 1920,
"height": 1080
@@ -1517,7 +1517,7 @@
"defaultBrowserType": "chromium"
},
"Desktop Edge": {
- "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.5 Safari/537.36 Edg/127.0.6533.5",
+ "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.26 Safari/537.36 Edg/127.0.6533.26",
"screen": {
"width": 1920,
"height": 1080
diff --git a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts
index 3101cd051d..9acc7378b2 100644
--- a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts
+++ b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts
@@ -265,6 +265,10 @@ export class PageDispatcher extends Dispatcher {
const rootAXNode = await this._page.accessibility.snapshot({
interestingOnly: params.interestingOnly,
diff --git a/packages/playwright-core/src/server/firefox/ffBrowser.ts b/packages/playwright-core/src/server/firefox/ffBrowser.ts
index ae26d0994a..7ed2d8cb2f 100644
--- a/packages/playwright-core/src/server/firefox/ffBrowser.ts
+++ b/packages/playwright-core/src/server/firefox/ffBrowser.ts
@@ -21,7 +21,7 @@ import type { BrowserOptions } from '../browser';
import { Browser } from '../browser';
import { assertBrowserContextIsNotOwned, BrowserContext, verifyGeolocation } from '../browserContext';
import * as network from '../network';
-import type { Page, PageBinding, PageDelegate } from '../page';
+import type { InitScript, Page, PageBinding, PageDelegate } from '../page';
import type { ConnectionTransport } from '../transport';
import type * as types from '../types';
import type * as channels from '@protocol/channels';
@@ -352,8 +352,8 @@ export class FFBrowserContext extends BrowserContext {
await this._browser.session.send('Browser.setHTTPCredentials', { browserContextId: this._browserContextId, credentials });
}
- async doAddInitScript(source: string) {
- await this._browser.session.send('Browser.setInitScripts', { browserContextId: this._browserContextId, scripts: this.initScripts.map(script => ({ script })) });
+ async doAddInitScript(initScript: InitScript) {
+ await this._browser.session.send('Browser.setInitScripts', { browserContextId: this._browserContextId, scripts: this.initScripts.map(script => ({ script: script.source })) });
}
async doRemoveInitScripts() {
diff --git a/packages/playwright-core/src/server/firefox/ffInput.ts b/packages/playwright-core/src/server/firefox/ffInput.ts
index 66f35399a5..fcf53ee9fc 100644
--- a/packages/playwright-core/src/server/firefox/ffInput.ts
+++ b/packages/playwright-core/src/server/firefox/ffInput.ts
@@ -166,4 +166,8 @@ export class RawTouchscreenImpl implements input.RawTouchscreen {
modifiers: toModifiersMask(modifiers),
});
}
+
+ async touch(eventType: 'touchstart'|'touchmove'|'touchend'|'touchcancel', touchPoints: { x: number, y: number, id?: number }[], modifiers: Set) {
+ throw new Error('Not implemented yet.');
+ }
}
diff --git a/packages/playwright-core/src/server/firefox/ffPage.ts b/packages/playwright-core/src/server/firefox/ffPage.ts
index 77fcd480bb..aac22d5e50 100644
--- a/packages/playwright-core/src/server/firefox/ffPage.ts
+++ b/packages/playwright-core/src/server/firefox/ffPage.ts
@@ -21,6 +21,7 @@ import type * as frames from '../frames';
import type { RegisteredListener } from '../../utils/eventsHelper';
import { eventsHelper } from '../../utils/eventsHelper';
import type { PageBinding, PageDelegate } from '../page';
+import { InitScript } from '../page';
import { Page, Worker } from '../page';
import type * as types from '../types';
import { getAccessibilityTree } from './ffAccessibility';
@@ -56,7 +57,7 @@ export class FFPage implements PageDelegate {
private _eventListeners: RegisteredListener[];
private _workers = new Map();
private _screencastId: string | undefined;
- private _initScripts: { script: string, worldName?: string }[] = [];
+ private _initScripts: { initScript: InitScript, worldName?: string }[] = [];
constructor(session: FFSession, browserContext: FFBrowserContext, opener: FFPage | null) {
this._session = session;
@@ -113,7 +114,7 @@ export class FFPage implements PageDelegate {
});
// Ideally, we somehow ensure that utility world is created before Page.ready arrives, but currently it is racy.
// Therefore, we can end up with an initialized page without utility world, although very unlikely.
- this.addInitScript('', UTILITY_WORLD_NAME).catch(e => this._markAsError(e));
+ this.addInitScript(new InitScript(''), UTILITY_WORLD_NAME).catch(e => this._markAsError(e));
}
potentiallyUninitializedPage(): Page {
@@ -406,9 +407,9 @@ export class FFPage implements PageDelegate {
return success;
}
- async addInitScript(script: string, worldName?: string): Promise {
- this._initScripts.push({ script, worldName });
- await this._session.send('Page.setInitScripts', { scripts: this._initScripts });
+ async addInitScript(initScript: InitScript, worldName?: string): Promise {
+ this._initScripts.push({ initScript, worldName });
+ await this._session.send('Page.setInitScripts', { scripts: this._initScripts.map(s => ({ script: s.initScript.source, worldName: s.worldName })) });
}
async removeInitScripts() {
diff --git a/packages/playwright-core/src/server/injected/recorder/recorder.ts b/packages/playwright-core/src/server/injected/recorder/recorder.ts
index 261a0eab21..4cfa512a4f 100644
--- a/packages/playwright-core/src/server/injected/recorder/recorder.ts
+++ b/packages/playwright-core/src/server/injected/recorder/recorder.ts
@@ -578,8 +578,8 @@ class TextAssertionTool implements RecorderTool {
onPointerUp(event: PointerEvent) {
const target = this._hoverHighlight?.elements[0];
- if (this._kind === 'value' && target && target.nodeName === 'INPUT' && (target as HTMLInputElement).disabled) {
- // Click on a disabled input does not produce a "click" event, but we still want
+ if (this._kind === 'value' && target && (target.nodeName === 'INPUT' || target.nodeName === 'SELECT') && (target as HTMLInputElement).disabled) {
+ // Click on a disabled input (or select) does not produce a "click" event, but we still want
// to assert the value.
this._commitAssertValue();
}
diff --git a/packages/playwright-core/src/server/input.ts b/packages/playwright-core/src/server/input.ts
index f09f91b86f..501874322c 100644
--- a/packages/playwright-core/src/server/input.ts
+++ b/packages/playwright-core/src/server/input.ts
@@ -308,6 +308,7 @@ function buildLayoutClosure(layout: keyboardLayout.KeyboardLayout): Map): Promise;
+ touch(type: 'touchstart'|'touchmove'|'touchend'|'touchcancel', touchPoints: { x: number, y: number, id?: number }[], modifiers: Set): Promise;
}
export class Touchscreen {
@@ -326,4 +327,19 @@ export class Touchscreen {
throw new Error('hasTouch must be enabled on the browser context before using the touchscreen.');
await this._raw.tap(x, y, this._page.keyboard._modifiers());
}
+ async touch(type: 'touchstart'|'touchmove'|'touchend'|'touchcancel', touchPoints: { x: number, y: number, id?: number }[], metadata?: CallMetadata) {
+ if (metadata && touchPoints.length === 1)
+ metadata.point = { x: touchPoints[0].x, y: touchPoints[0].y };
+ if (!this._page._browserContext._options.hasTouch)
+ throw new Error('hasTouch must be enabled on the browser context before using the touchscreen.');
+ const ids = new Set();
+ for (const point of touchPoints) {
+ if (point.id !== undefined) {
+ if (ids.has(point.id))
+ throw new Error(`Duplicate touch point id: ${point.id}`);
+ ids.add(point.id);
+ }
+ }
+ await this._raw.touch(type, touchPoints, this._page.keyboard._modifiers());
+ }
}
diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts
index 28ff203a3c..da9063821c 100644
--- a/packages/playwright-core/src/server/page.ts
+++ b/packages/playwright-core/src/server/page.ts
@@ -31,7 +31,7 @@ import * as accessibility from './accessibility';
import { FileChooser } from './fileChooser';
import type { Progress } from './progress';
import { ProgressController } from './progress';
-import { LongStandingScope, assert, isError } from '../utils';
+import { LongStandingScope, assert, createGuid, isError } from '../utils';
import { ManualPromise } from '../utils/manualPromise';
import { debugLogger } from '../utils/debugLogger';
import type { ImageComparatorOptions } from '../utils/comparators';
@@ -56,7 +56,7 @@ export interface PageDelegate {
goForward(): Promise;
exposeBinding(binding: PageBinding): Promise;
removeExposedBindings(): Promise;
- addInitScript(source: string): Promise;
+ addInitScript(initScript: InitScript): Promise;
removeInitScripts(): Promise;
closePage(runBeforeUnload: boolean): Promise;
potentiallyUninitializedPage(): Page;
@@ -154,7 +154,7 @@ export class Page extends SdkObject {
private _emulatedMedia: Partial = {};
private _interceptFileChooser = false;
private readonly _pageBindings = new Map();
- readonly initScripts: string[] = [];
+ readonly initScripts: InitScript[] = [];
readonly _screenshotter: Screenshotter;
readonly _frameManager: frames.FrameManager;
readonly accessibility: accessibility.Accessibility;
@@ -527,8 +527,9 @@ export class Page extends SdkObject {
}
async addInitScript(source: string) {
- this.initScripts.push(source);
- await this._delegate.addInitScript(source);
+ const initScript = new InitScript(source);
+ this.initScripts.push(initScript);
+ await this._delegate.addInitScript(initScript);
}
async _removeInitScripts() {
@@ -905,6 +906,22 @@ function addPageBinding(bindingName: string, needsHandle: boolean, utilityScript
(globalThis as any)[bindingName].__installed = true;
}
+export class InitScript {
+ readonly source: string;
+
+ constructor(source: string) {
+ const guid = createGuid();
+ this.source = `(() => {
+ globalThis.__pwInitScripts = globalThis.__pwInitScripts || {};
+ const hasInitScript = globalThis.__pwInitScripts[${JSON.stringify(guid)}];
+ if (hasInitScript)
+ return;
+ globalThis.__pwInitScripts[${JSON.stringify(guid)}] = true;
+ ${source}
+ })();`;
+ }
+}
+
class FrameThrottler {
private _acks: (() => void)[] = [];
private _defaultInterval: number;
diff --git a/packages/playwright-core/src/server/webkit/protocol.d.ts b/packages/playwright-core/src/server/webkit/protocol.d.ts
index ba9ba9e5d3..9e71534a17 100644
--- a/packages/playwright-core/src/server/webkit/protocol.d.ts
+++ b/packages/playwright-core/src/server/webkit/protocol.d.ts
@@ -5013,6 +5013,23 @@ might return multiple quads for inline nodes.
* UTC time in seconds, counted from January 1, 1970.
*/
export type TimeSinceEpoch = number;
+ /**
+ * Touch point.
+ */
+ export interface TouchPoint {
+ /**
+ * X coordinate of the event relative to the main frame's viewport in CSS pixels.
+ */
+ x: number;
+ /**
+ * Y coordinate of the event relative to the main frame's viewport in CSS pixels.
+ */
+ y: number;
+ /**
+ * Identifier used to track touch sources between events, must be unique within an event.
+ */
+ id: number;
+ }
/**
@@ -5169,6 +5186,26 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the
}
export type dispatchTapEventReturnValue = {
}
+ /**
+ * Dispatches a touch event to the page.
+ */
+ export type dispatchTouchEventParameters = {
+ /**
+ * Type of the touch event.
+ */
+ type: "touchStart"|"touchMove"|"touchEnd"|"touchCancel";
+ /**
+ * Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8
+(default: 0).
+ */
+ modifiers?: number;
+ /**
+ * List of touch points
+ */
+ touchPoints?: TouchPoint[];
+ }
+ export type dispatchTouchEventReturnValue = {
+ }
}
export module Inspector {
@@ -9532,6 +9569,7 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the
"Input.dispatchMouseEvent": Input.dispatchMouseEventParameters;
"Input.dispatchWheelEvent": Input.dispatchWheelEventParameters;
"Input.dispatchTapEvent": Input.dispatchTapEventParameters;
+ "Input.dispatchTouchEvent": Input.dispatchTouchEventParameters;
"Inspector.enable": Inspector.enableParameters;
"Inspector.disable": Inspector.disableParameters;
"Inspector.initialized": Inspector.initializedParameters;
@@ -9843,6 +9881,7 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the
"Input.dispatchMouseEvent": Input.dispatchMouseEventReturnValue;
"Input.dispatchWheelEvent": Input.dispatchWheelEventReturnValue;
"Input.dispatchTapEvent": Input.dispatchTapEventReturnValue;
+ "Input.dispatchTouchEvent": Input.dispatchTouchEventReturnValue;
"Inspector.enable": Inspector.enableReturnValue;
"Inspector.disable": Inspector.disableReturnValue;
"Inspector.initialized": Inspector.initializedReturnValue;
diff --git a/packages/playwright-core/src/server/webkit/wkBrowser.ts b/packages/playwright-core/src/server/webkit/wkBrowser.ts
index a8541e73cf..9eedec410b 100644
--- a/packages/playwright-core/src/server/webkit/wkBrowser.ts
+++ b/packages/playwright-core/src/server/webkit/wkBrowser.ts
@@ -22,7 +22,7 @@ import type { RegisteredListener } from '../../utils/eventsHelper';
import { assert } from '../../utils';
import { eventsHelper } from '../../utils/eventsHelper';
import * as network from '../network';
-import type { Page, PageBinding, PageDelegate } from '../page';
+import type { InitScript, Page, PageBinding, PageDelegate } from '../page';
import type { ConnectionTransport } from '../transport';
import type * as types from '../types';
import type * as channels from '@protocol/channels';
@@ -315,7 +315,7 @@ export class WKBrowserContext extends BrowserContext {
await (page._delegate as WKPage).updateHttpCredentials();
}
- async doAddInitScript(source: string) {
+ async doAddInitScript(initScript: InitScript) {
for (const page of this.pages())
await (page._delegate as WKPage)._updateBootstrapScript();
}
diff --git a/packages/playwright-core/src/server/webkit/wkInput.ts b/packages/playwright-core/src/server/webkit/wkInput.ts
index 0732f246d1..88afa774c8 100644
--- a/packages/playwright-core/src/server/webkit/wkInput.ts
+++ b/packages/playwright-core/src/server/webkit/wkInput.ts
@@ -182,4 +182,20 @@ export class RawTouchscreenImpl implements input.RawTouchscreen {
modifiers: toModifiersMask(modifiers),
});
}
+
+ async touch(eventType: 'touchstart'|'touchmove'|'touchend'|'touchcancel', touchPoints: { x: number, y: number, id?: number }[], modifiers: Set) {
+ let type: 'touchStart' | 'touchMove' | 'touchEnd' | 'touchCancel';
+ switch (eventType) {
+ case 'touchstart': type = 'touchStart'; break;
+ case 'touchmove': type = 'touchMove'; break;
+ case 'touchend': type = 'touchEnd'; break;
+ case 'touchcancel': type = 'touchCancel'; break;
+ default: throw new Error('Invalid eventType: ' + eventType);
+ }
+ await this._pageProxySession.send('Input.dispatchTouchEvent', {
+ type,
+ touchPoints: touchPoints.map(p => ({ ...p, id: p.id || 0 })),
+ modifiers: toModifiersMask(modifiers)
+ });
+ }
}
diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts
index 9507f187e5..adfd06c05e 100644
--- a/packages/playwright-core/src/server/webkit/wkPage.ts
+++ b/packages/playwright-core/src/server/webkit/wkPage.ts
@@ -30,7 +30,7 @@ import { eventsHelper } from '../../utils/eventsHelper';
import { helper } from '../helper';
import type { JSHandle } from '../javascript';
import * as network from '../network';
-import type { PageBinding, PageDelegate } from '../page';
+import type { InitScript, PageBinding, PageDelegate } from '../page';
import { Page } from '../page';
import type { Progress } from '../progress';
import type * as types from '../types';
@@ -777,7 +777,7 @@ export class WKPage implements PageDelegate {
await this._updateBootstrapScript();
}
- async addInitScript(script: string): Promise {
+ async addInitScript(initScript: InitScript): Promise {
await this._updateBootstrapScript();
}
@@ -797,8 +797,8 @@ export class WKPage implements PageDelegate {
for (const binding of this._page.allBindings())
scripts.push(binding.source);
- scripts.push(...this._browserContext.initScripts);
- scripts.push(...this._page.initScripts);
+ scripts.push(...this._browserContext.initScripts.map(s => s.source));
+ scripts.push(...this._page.initScripts.map(s => s.source));
return scripts.join(';\n');
}
diff --git a/packages/playwright-core/src/utils/happy-eyeballs.ts b/packages/playwright-core/src/utils/happy-eyeballs.ts
index 4576da7899..37d5c1b271 100644
--- a/packages/playwright-core/src/utils/happy-eyeballs.ts
+++ b/packages/playwright-core/src/utils/happy-eyeballs.ts
@@ -45,8 +45,9 @@ class HttpsHappyEyeballsAgent extends https.Agent {
}
}
-export const httpsHappyEyeballsAgent = new HttpsHappyEyeballsAgent();
-export const httpHappyEyeballsAgent = new HttpHappyEyeballsAgent();
+// These options are aligned with the default Node.js globalAgent options.
+export const httpsHappyEyeballsAgent = new HttpsHappyEyeballsAgent({ keepAlive: true });
+export const httpHappyEyeballsAgent = new HttpHappyEyeballsAgent({ keepAlive: true });
export async function createSocket(host: string, port: number): Promise {
return new Promise((resolve, reject) => {
diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts
index 67e92a917a..3152c067a3 100644
--- a/packages/playwright-core/types/types.d.ts
+++ b/packages/playwright-core/types/types.d.ts
@@ -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(`
- *
- *
Click me
- *
Or click me
- * `);
- * ```
- *
* @param name Name of the function on the window object.
* @param callback Callback function that will be called in the Playwright's context.
* @param options
@@ -875,21 +860,6 @@ export interface Page {
* })();
* ```
*
- * An example of passing an element handle:
- *
- * ```js
- * await page.exposeBinding('clicked', async (source, element) => {
- * console.log(await element.textContent());
- * }, { handle: true });
- * await page.setContent(`
- *
- *
Click me
- *
Or click me
- * `);
- * ```
- *
* @param name Name of the function on the window object.
* @param callback Callback function that will be called in the Playwright's context.
* @param options
@@ -7637,21 +7607,6 @@ export interface BrowserContext {
* })();
* ```
*
- * An example of passing an element handle:
- *
- * ```js
- * await context.exposeBinding('clicked', async (source, element) => {
- * console.log(await element.textContent());
- * }, { handle: true });
- * await page.setContent(`
- *
- *
Click me
- *
Or click me
- * `);
- * ```
- *
* @param name Name of the function on the window object.
* @param callback Callback function that will be called in the Playwright's context.
* @param options
@@ -7693,21 +7648,6 @@ export interface BrowserContext {
* })();
* ```
*
- * An example of passing an element handle:
- *
- * ```js
- * await context.exposeBinding('clicked', async (source, element) => {
- * console.log(await element.textContent());
- * }, { handle: true });
- * await page.setContent(`
- *
- *
Click me
- *
Or click me
- * `);
- * ```
- *
* @param name Name of the function on the window object.
* @param callback Callback function that will be called in the Playwright's context.
* @param options
@@ -8346,9 +8286,7 @@ export interface BrowserContext {
* await browserContext.addCookies([cookieObject1, cookieObject2]);
* ```
*
- * @param cookies Adds cookies to the browser context.
- *
- * For the cookie to apply to all subdomains as well, prefix domain with a dot, like this: ".example.com".
+ * @param cookies
*/
addCookies(cookies: ReadonlyArray<{
name: string;
@@ -8356,17 +8294,18 @@ export interface BrowserContext {
value: string;
/**
- * either url or domain / path are required. Optional.
+ * Either url or domain / path are required. Optional.
*/
url?: string;
/**
- * either url or domain / path are required Optional.
+ * For the cookie to apply to all subdomains as well, prefix domain with a dot, like this: ".example.com". Either url
+ * or domain / path are required. Optional.
*/
domain?: string;
/**
- * either url or domain / path are required Optional.
+ * Either url or domain / path are required Optional.
*/
path?: string;
@@ -19743,6 +19682,29 @@ export interface Touchscreen {
* @param y
*/
tap(x: number, y: number): Promise;
+
+ /**
+ * Synthesizes a touch event.
+ * @param type Type of the touch event.
+ * @param touchPoints List of touch points for this event. `id` is a unique identifier of a touch point that helps identify it between
+ * touch events for the duration of its movement around the surface.
+ */
+ touch(type: "touchstart"|"touchend"|"touchmove"|"touchcancel", touchPoints: ReadonlyArray<{
+ /**
+ * x coordinate of the event in CSS pixels.
+ */
+ x: number;
+
+ /**
+ * y coordinate of the event in CSS pixels.
+ */
+ y: number;
+
+ /**
+ * Identifier used to track the touch point between events, must be unique within an event. Optional.
+ */
+ id?: number;
+ }>): Promise;
}
/**
diff --git a/packages/playwright/src/common/fixtures.ts b/packages/playwright/src/common/fixtures.ts
index aa3886718c..6b50947ab5 100644
--- a/packages/playwright/src/common/fixtures.ts
+++ b/packages/playwright/src/common/fixtures.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { formatLocation } from '../util';
+import { filterStackFile, formatLocation } from '../util';
import * as crypto from 'crypto';
import type { Fixtures } from '../../types/test';
import type { Location } from '../../types/testReporter';
@@ -23,7 +23,7 @@ import type { FixturesWithLocation } from './config';
export type FixtureScope = 'test' | 'worker';
type FixtureAuto = boolean | 'all-hooks-included';
const kScopeOrder: FixtureScope[] = ['test', 'worker'];
-type FixtureOptions = { auto?: FixtureAuto, scope?: FixtureScope, option?: boolean, timeout?: number | undefined };
+type FixtureOptions = { auto?: FixtureAuto, scope?: FixtureScope, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean };
type FixtureTuple = [ value: any, options: FixtureOptions ];
export type FixtureRegistration = {
// Fixture registration location.
@@ -49,8 +49,8 @@ export type FixtureRegistration = {
super?: FixtureRegistration;
// Whether this fixture is an option override value set from the config.
optionOverride?: boolean;
- // Do not generate the step for this fixture.
- hideStep?: boolean;
+ // Do not generate the step for this fixture, consider it internal.
+ box?: boolean;
};
export type LoadError = {
message: string;
@@ -63,7 +63,7 @@ type OptionOverrides = {
};
function isFixtureTuple(value: any): value is FixtureTuple {
- return Array.isArray(value) && typeof value[1] === 'object' && ('scope' in value[1] || 'auto' in value[1] || 'option' in value[1] || 'timeout' in value[1]);
+ return Array.isArray(value) && typeof value[1] === 'object';
}
function isFixtureOption(value: any): value is FixtureTuple {
@@ -103,15 +103,15 @@ export class FixturePool {
for (const entry of Object.entries(fixtures)) {
const name = entry[0];
let value = entry[1];
- let options: { auto: FixtureAuto, scope: FixtureScope, option: boolean, timeout: number | undefined, customTitle: string | undefined, hideStep: boolean | undefined } | undefined;
+ let options: { auto: FixtureAuto, scope: FixtureScope, option: boolean, timeout: number | undefined, customTitle?: string, box?: boolean } | undefined;
if (isFixtureTuple(value)) {
options = {
auto: value[1].auto ?? false,
scope: value[1].scope || 'test',
option: !!value[1].option,
timeout: value[1].timeout,
- customTitle: (value[1] as any)._title,
- hideStep: (value[1] as any)._hideStep,
+ customTitle: value[1].title,
+ box: value[1].box,
};
value = value[0];
}
@@ -128,9 +128,9 @@ export class FixturePool {
continue;
}
} else if (previous) {
- options = { auto: previous.auto, scope: previous.scope, option: previous.option, timeout: previous.timeout, customTitle: previous.customTitle, hideStep: undefined };
+ options = { auto: previous.auto, scope: previous.scope, option: previous.option, timeout: previous.timeout, customTitle: previous.customTitle, box: previous.box };
} else if (!options) {
- options = { auto: false, scope: 'test', option: false, timeout: undefined, customTitle: undefined, hideStep: undefined };
+ options = { auto: false, scope: 'test', option: false, timeout: undefined };
}
if (!kScopeOrder.includes(options.scope)) {
@@ -152,7 +152,7 @@ export class FixturePool {
}
const deps = fixtureParameterNames(fn, location, e => this._onLoadError(e));
- const registration: FixtureRegistration = { id: '', name, location, scope: options.scope, fn, auto: options.auto, option: options.option, timeout: options.timeout, customTitle: options.customTitle, hideStep: options.hideStep, deps, super: previous, optionOverride: isOptionsOverride };
+ const registration: FixtureRegistration = { id: '', name, location, scope: options.scope, fn, auto: options.auto, option: options.option, timeout: options.timeout, customTitle: options.customTitle, box: options.box, deps, super: previous, optionOverride: isOptionsOverride };
registrationId(registration);
this._registrations.set(name, registration);
}
@@ -161,29 +161,36 @@ export class FixturePool {
private validate() {
const markers = new Map();
const stack: FixtureRegistration[] = [];
- const visit = (registration: FixtureRegistration) => {
+ let hasDependencyErrors = false;
+ const addDependencyError = (message: string, location: Location) => {
+ hasDependencyErrors = true;
+ this._addLoadError(message, location);
+ };
+ const visit = (registration: FixtureRegistration, boxedOnly: boolean) => {
markers.set(registration, 'visiting');
stack.push(registration);
for (const name of registration.deps) {
const dep = this.resolve(name, registration);
if (!dep) {
if (name === registration.name)
- this._addLoadError(`Fixture "${registration.name}" references itself, but does not have a base implementation.`, registration.location);
+ addDependencyError(`Fixture "${registration.name}" references itself, but does not have a base implementation.`, registration.location);
else
- this._addLoadError(`Fixture "${registration.name}" has unknown parameter "${name}".`, registration.location);
+ addDependencyError(`Fixture "${registration.name}" has unknown parameter "${name}".`, registration.location);
continue;
}
if (kScopeOrder.indexOf(registration.scope) > kScopeOrder.indexOf(dep.scope)) {
- this._addLoadError(`${registration.scope} fixture "${registration.name}" cannot depend on a ${dep.scope} fixture "${name}" defined in ${formatLocation(dep.location)}.`, registration.location);
+ addDependencyError(`${registration.scope} fixture "${registration.name}" cannot depend on a ${dep.scope} fixture "${name}" defined in ${formatPotentiallyInternalLocation(dep.location)}.`, registration.location);
continue;
}
if (!markers.has(dep)) {
- visit(dep);
+ visit(dep, boxedOnly);
} else if (markers.get(dep) === 'visiting') {
const index = stack.indexOf(dep);
- const regs = stack.slice(index, stack.length);
+ const allRegs = stack.slice(index, stack.length);
+ const filteredRegs = allRegs.filter(r => !r.box);
+ const regs = boxedOnly ? filteredRegs : allRegs;
const names = regs.map(r => `"${r.name}"`);
- this._addLoadError(`Fixtures ${names.join(' -> ')} -> "${dep.name}" form a dependency cycle: ${regs.map(r => formatLocation(r.location)).join(' -> ')}`, dep.location);
+ addDependencyError(`Fixtures ${names.join(' -> ')} -> "${dep.name}" form a dependency cycle: ${regs.map(r => formatPotentiallyInternalLocation(r.location)).join(' -> ')} -> ${formatPotentiallyInternalLocation(dep.location)}`, dep.location);
continue;
}
}
@@ -191,11 +198,27 @@ export class FixturePool {
stack.pop();
};
- const hash = crypto.createHash('sha1');
const names = Array.from(this._registrations.keys()).sort();
+
+ // First iterate over non-boxed fixtures to provide clear error messages.
+ for (const name of names) {
+ const registration = this._registrations.get(name)!;
+ if (!registration.box)
+ visit(registration, true);
+ }
+
+ // If no errors found, iterate over boxed fixtures
+ if (!hasDependencyErrors) {
+ for (const name of names) {
+ const registration = this._registrations.get(name)!;
+ if (registration.box)
+ visit(registration, false);
+ }
+ }
+
+ const hash = crypto.createHash('sha1');
for (const name of names) {
const registration = this._registrations.get(name)!;
- visit(registration);
if (registration.scope === 'worker')
hash.update(registration.id + ';');
}
@@ -227,6 +250,11 @@ export class FixturePool {
const signatureSymbol = Symbol('signature');
+export function formatPotentiallyInternalLocation(location: Location): string {
+ const isUserFixture = location && filterStackFile(location.file);
+ return isUserFixture ? formatLocation(location) : '';
+}
+
export function fixtureParameterNames(fn: Function | any, location: Location, onError: LoadErrorSink): string[] {
if (typeof fn !== 'function')
return [];
diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts
index 3fbaf33585..a258cbcad5 100644
--- a/packages/playwright/src/index.ts
+++ b/packages/playwright/src/index.ts
@@ -59,13 +59,13 @@ type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & {
const playwrightFixtures: Fixtures = ({
defaultBrowserType: ['chromium', { scope: 'worker', option: true }],
browserName: [({ defaultBrowserType }, use) => use(defaultBrowserType), { scope: 'worker', option: true }],
- _playwrightImpl: [({}, use) => use(require('playwright-core')), { scope: 'worker' }],
+ _playwrightImpl: [({}, use) => use(require('playwright-core')), { scope: 'worker', box: true }],
playwright: [async ({ _playwrightImpl, screenshot }, use) => {
await connector.setPlaywright(_playwrightImpl, screenshot);
await use(_playwrightImpl);
await connector.setPlaywright(undefined, screenshot);
- }, { scope: 'worker', _hideStep: true } as any],
+ }, { scope: 'worker', box: true }],
headless: [({ launchOptions }, use) => use(launchOptions.headless ?? true), { scope: 'worker', option: true }],
channel: [({ launchOptions }, use) => use(launchOptions.channel), { scope: 'worker', option: true }],
@@ -93,7 +93,7 @@ const playwrightFixtures: Fixtures = ({
await use(options);
for (const browserType of [playwright.chromium, playwright.firefox, playwright.webkit])
(browserType as any)._defaultLaunchOptions = undefined;
- }, { scope: 'worker', auto: true }],
+ }, { scope: 'worker', auto: true, box: true }],
browser: [async ({ playwright, browserName, _browserOptions, connectOptions, _reuseContext }, use, testInfo) => {
if (!['chromium', 'firefox', 'webkit'].includes(browserName))
@@ -152,7 +152,7 @@ const playwrightFixtures: Fixtures = ({
serviceWorkers: [({ contextOptions }, use) => use(contextOptions.serviceWorkers ?? 'allow'), { option: true }],
contextOptions: [{}, { option: true }],
- _combinedContextOptions: async ({
+ _combinedContextOptions: [async ({
acceptDownloads,
bypassCSP,
colorScheme,
@@ -223,7 +223,7 @@ const playwrightFixtures: Fixtures = ({
...contextOptions,
...options,
});
- },
+ }, { box: true }],
_setupContextOptions: [async ({ playwright, _combinedContextOptions, actionTimeout, navigationTimeout, testIdAttribute }, use, testInfo) => {
if (testIdAttribute)
@@ -246,9 +246,9 @@ const playwrightFixtures: Fixtures = ({
(browserType as any)._defaultContextTimeout = undefined;
(browserType as any)._defaultContextNavigationTimeout = undefined;
}
- }, { auto: 'all-hooks-included', _title: 'context configuration' } as any],
+ }, { auto: 'all-hooks-included', title: 'context configuration', box: true } as any],
- _contextFactory: [async ({ browser, video, _reuseContext }, use, testInfo) => {
+ _contextFactory: [async ({ browser, video, _reuseContext, _combinedContextOptions /** mitigate dep-via-auto lack of traceability */ }, use, testInfo) => {
const testInfoImpl = testInfo as TestInfoImpl;
const videoMode = normalizeVideoMode(video);
const captureVideo = shouldCaptureVideo(videoMode, testInfo) && !_reuseContext;
@@ -274,6 +274,18 @@ const playwrightFixtures: Fixtures = ({
contexts.set(context, contextData);
if (captureVideo)
context.on('page', page => contextData.pagesWithVideo.push(page));
+
+ if (process.env.PW_CLOCK === 'frozen') {
+ await (context as any)._wrapApiCall(async () => {
+ await context.clock.install({ time: 0 });
+ await context.clock.pauseAt(1000);
+ }, true);
+ } else if (process.env.PW_CLOCK === 'realtime') {
+ await (context as any)._wrapApiCall(async () => {
+ await context.clock.install({ time: 0 });
+ }, true);
+ }
+
return context;
});
@@ -301,7 +313,7 @@ const playwrightFixtures: Fixtures = ({
}
}));
- }, { scope: 'test', _title: 'context' } as any],
+ }, { scope: 'test', title: 'context', box: true }],
_optionContextReuseMode: ['none', { scope: 'worker', option: true }],
_optionConnectOptions: [undefined, { scope: 'worker', option: true }],
@@ -312,7 +324,7 @@ const playwrightFixtures: Fixtures = ({
mode = 'when-possible';
const reuse = mode === 'when-possible' && normalizeVideoMode(video) === 'off';
await use(reuse);
- }, { scope: 'worker', _title: 'context' } as any],
+ }, { scope: 'worker', title: 'context', box: true }],
context: async ({ playwright, browser, _reuseContext, _contextFactory }, use, testInfo) => {
attachConnectedHeaderIfNeeded(testInfo, browser);
diff --git a/packages/playwright/src/worker/fixtureRunner.ts b/packages/playwright/src/worker/fixtureRunner.ts
index 8832acf8e2..0d94f40631 100644
--- a/packages/playwright/src/worker/fixtureRunner.ts
+++ b/packages/playwright/src/worker/fixtureRunner.ts
@@ -40,10 +40,10 @@ class Fixture {
this.runner = runner;
this.registration = registration;
this.value = null;
- const shouldGenerateStep = !this.registration.hideStep && !this.registration.name.startsWith('_') && !this.registration.option;
- const isInternalFixture = this.registration.location && filterStackFile(this.registration.location.file);
+ const shouldGenerateStep = !this.registration.box && !this.registration.option;
+ const isUserFixture = this.registration.location && filterStackFile(this.registration.location.file);
const title = this.registration.customTitle || this.registration.name;
- const location = isInternalFixture ? this.registration.location : undefined;
+ const location = isUserFixture ? this.registration.location : undefined;
this._stepInfo = shouldGenerateStep ? { category: 'fixture', location } : undefined;
this._setupDescription = {
title,
diff --git a/packages/playwright/src/worker/workerMain.ts b/packages/playwright/src/worker/workerMain.ts
index 3237104520..2f0dcc424d 100644
--- a/packages/playwright/src/worker/workerMain.ts
+++ b/packages/playwright/src/worker/workerMain.ts
@@ -261,7 +261,7 @@ export class WorkerMain extends ProcessRunner {
testInfo.expectedStatus = 'failed';
break;
case 'slow':
- testInfo.slow();
+ testInfo._timeoutManager.slow();
break;
}
};
@@ -570,6 +570,9 @@ export class WorkerMain extends ProcessRunner {
if (error instanceof TimeoutManagerError)
throw error;
firstError = firstError ?? error;
+ // Skip in modifier prevents others from running.
+ if (error instanceof SkipError)
+ break;
}
}
if (firstError)
diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts
index 2fd171e3bd..5a18eded3f 100644
--- a/packages/playwright/types/test.d.ts
+++ b/packages/playwright/types/test.d.ts
@@ -4811,13 +4811,13 @@ export type WorkerFixture = (args: Args, use: (r: R) =
type TestFixtureValue = Exclude | TestFixture;
type WorkerFixtureValue = Exclude | WorkerFixture;
export type Fixtures = {
- [K in keyof PW]?: WorkerFixtureValue | [WorkerFixtureValue, { scope: 'worker', timeout?: number | undefined }];
+ [K in keyof PW]?: WorkerFixtureValue | [WorkerFixtureValue, { scope: 'worker', timeout?: number | undefined, title?: string, box?: boolean }];
} & {
- [K in keyof PT]?: TestFixtureValue | [TestFixtureValue, { scope: 'test', timeout?: number | undefined }];
+ [K in keyof PT]?: TestFixtureValue | [TestFixtureValue, { scope: 'test', timeout?: number | undefined, title?: string, box?: boolean }];
} & {
- [K in keyof W]?: [WorkerFixtureValue, { scope: 'worker', auto?: boolean, option?: boolean, timeout?: number | undefined }];
+ [K in keyof W]?: [WorkerFixtureValue, { scope: 'worker', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }];
} & {
- [K in keyof T]?: TestFixtureValue | [TestFixtureValue, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined }];
+ [K in keyof T]?: TestFixtureValue | [TestFixtureValue, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }];
};
type BrowserName = 'chromium' | 'firefox' | 'webkit';
diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts
index ffb591556c..a1ae5a13e0 100644
--- a/packages/protocol/src/channels.ts
+++ b/packages/protocol/src/channels.ts
@@ -1890,6 +1890,7 @@ export interface PageChannel extends PageEventTarget, EventTargetChannel {
mouseClick(params: PageMouseClickParams, metadata?: CallMetadata): Promise;
mouseWheel(params: PageMouseWheelParams, metadata?: CallMetadata): Promise;
touchscreenTap(params: PageTouchscreenTapParams, metadata?: CallMetadata): Promise;
+ touchscreenTouch(params: PageTouchscreenTouchParams, metadata?: CallMetadata): Promise;
accessibilitySnapshot(params: PageAccessibilitySnapshotParams, metadata?: CallMetadata): Promise;
pdf(params: PagePdfParams, metadata?: CallMetadata): Promise;
startJSCoverage(params: PageStartJSCoverageParams, metadata?: CallMetadata): Promise;
@@ -2257,6 +2258,18 @@ export type PageTouchscreenTapOptions = {
};
export type PageTouchscreenTapResult = void;
+export type PageTouchscreenTouchParams = {
+ type: 'touchstart' | 'touchend' | 'touchmove' | 'touchcancel',
+ touchPoints: {
+ x: number,
+ y: number,
+ id?: number,
+ }[],
+};
+export type PageTouchscreenTouchOptions = {
+
+};
+export type PageTouchscreenTouchResult = void;
export type PageAccessibilitySnapshotParams = {
interestingOnly?: boolean,
root?: ElementHandleChannel,
diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml
index 14799ef17e..54f356102b 100644
--- a/packages/protocol/src/protocol.yml
+++ b/packages/protocol/src/protocol.yml
@@ -1603,6 +1603,27 @@ Page:
slowMo: true
snapshot: true
+ touchscreenTouch:
+ parameters:
+ type:
+ type: enum
+ literals:
+ - touchstart
+ - touchend
+ - touchmove
+ - touchcancel
+ touchPoints:
+ type: array
+ items:
+ type: object
+ properties:
+ x: number
+ y: number
+ id: number?
+ flags:
+ slowMo: true
+ snapshot: true
+
accessibilitySnapshot:
parameters:
interestingOnly: boolean?
diff --git a/packages/trace-viewer/embedded.html b/packages/trace-viewer/embedded.html
new file mode 100644
index 0000000000..7d0fd2f175
--- /dev/null
+++ b/packages/trace-viewer/embedded.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+ Playwright Trace Viewer for VS Code
+
+
+
+
+
+
diff --git a/packages/trace-viewer/src/embedded.tsx b/packages/trace-viewer/src/embedded.tsx
new file mode 100644
index 0000000000..8f0a09e560
--- /dev/null
+++ b/packages/trace-viewer/src/embedded.tsx
@@ -0,0 +1,61 @@
+/**
+ * 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 '@web/common.css';
+import { applyTheme } from '@web/theme';
+import '@web/third_party/vscode/codicon.css';
+import React from 'react';
+import * as ReactDOM from 'react-dom';
+import { EmbeddedWorkbenchLoader } from './ui/embeddedWorkbenchLoader';
+
+(async () => {
+ applyTheme();
+
+ // workaround to send keystrokes back to vscode webview to keep triggering key bindings there
+ const handleKeyEvent = (e: KeyboardEvent) => {
+ if (!e.isTrusted)
+ return;
+ window.parent?.postMessage({
+ type: e.type,
+ key: e.key,
+ keyCode: e.keyCode,
+ code: e.code,
+ shiftKey: e.shiftKey,
+ altKey: e.altKey,
+ ctrlKey: e.ctrlKey,
+ metaKey: e.metaKey,
+ repeat: e.repeat,
+ }, '*');
+ };
+ window.addEventListener('keydown', handleKeyEvent);
+ window.addEventListener('keyup', handleKeyEvent);
+
+ if (window.location.protocol !== 'file:') {
+ if (!navigator.serviceWorker)
+ throw new Error(`Service workers are not supported.\nMake sure to serve the Trace Viewer (${window.location}) via HTTPS or localhost.`);
+ navigator.serviceWorker.register('sw.bundle.js');
+ if (!navigator.serviceWorker.controller) {
+ await new Promise(f => {
+ navigator.serviceWorker.oncontrollerchange = () => f();
+ });
+ }
+
+ // Keep SW running.
+ setInterval(function() { fetch('ping'); }, 10000);
+ }
+
+ ReactDOM.render(, document.querySelector('#root'));
+})();
diff --git a/packages/trace-viewer/src/ui/embeddedWorkbenchLoader.css b/packages/trace-viewer/src/ui/embeddedWorkbenchLoader.css
new file mode 100644
index 0000000000..2274355322
--- /dev/null
+++ b/packages/trace-viewer/src/ui/embeddedWorkbenchLoader.css
@@ -0,0 +1,68 @@
+/*
+ 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.
+*/
+
+.empty-state {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex: auto;
+ flex-direction: column;
+ background-color: var(--vscode-editor-background);
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 100;
+ line-height: 24px;
+}
+
+body .empty-state {
+ background: rgba(255, 255, 255, 0.8);
+}
+
+body.dark-mode .empty-state {
+ background: rgba(0, 0, 0, 0.8);
+}
+
+.empty-state .title {
+ font-size: 24px;
+ font-weight: bold;
+ margin-bottom: 30px;
+}
+
+.progress {
+ flex: none;
+ width: 100%;
+ height: 3px;
+ z-index: 10;
+}
+
+.inner-progress {
+ background-color: var(--vscode-progressBar-background);
+ height: 100%;
+}
+
+.workbench-loader {
+ contain: size;
+}
+
+/* Limit to a reasonable minimum viewport */
+html, body {
+ min-width: 550px;
+ min-height: 450px;
+ overflow: auto;
+}
diff --git a/packages/trace-viewer/src/ui/embeddedWorkbenchLoader.tsx b/packages/trace-viewer/src/ui/embeddedWorkbenchLoader.tsx
new file mode 100644
index 0000000000..17a2211c41
--- /dev/null
+++ b/packages/trace-viewer/src/ui/embeddedWorkbenchLoader.tsx
@@ -0,0 +1,96 @@
+/*
+ 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 * as React from 'react';
+import type { ContextEntry } from '../entries';
+import { MultiTraceModel } from './modelUtil';
+import './embeddedWorkbenchLoader.css';
+import { Workbench } from './workbench';
+import { currentTheme, toggleTheme } from '@web/theme';
+
+function openPage(url: string, target?: string) {
+ if (url)
+ window.parent!.postMessage({ command: 'openExternal', params: { url, target } }, '*');
+}
+
+export const EmbeddedWorkbenchLoader: React.FunctionComponent = () => {
+ const [traceURLs, setTraceURLs] = React.useState([]);
+ const [model, setModel] = React.useState(emptyModel);
+ const [progress, setProgress] = React.useState<{ done: number, total: number }>({ done: 0, total: 0 });
+ const [processingErrorMessage, setProcessingErrorMessage] = React.useState(null);
+
+ React.useEffect(() => {
+ window.addEventListener('message', async ({ data: { method, params } }) => {
+ if (method === 'loadTraceRequested') {
+ setTraceURLs(params.traceUrl ? [params.traceUrl] : []);
+ setProcessingErrorMessage(null);
+ } else if (method === 'applyTheme') {
+ if (currentTheme() !== params.theme)
+ toggleTheme();
+ }
+ });
+ // notify vscode that it is now listening to its messages
+ window.parent!.postMessage({ type: 'loaded' }, '*');
+ }, []);
+
+ React.useEffect(() => {
+ (async () => {
+ if (traceURLs.length) {
+ const swListener = (event: any) => {
+ if (event.data.method === 'progress')
+ setProgress(event.data.params);
+ };
+ navigator.serviceWorker.addEventListener('message', swListener);
+ setProgress({ done: 0, total: 1 });
+ const contextEntries: ContextEntry[] = [];
+ for (let i = 0; i < traceURLs.length; i++) {
+ const url = traceURLs[i];
+ const params = new URLSearchParams();
+ params.set('trace', url);
+ const response = await fetch(`contexts?${params.toString()}`);
+ if (!response.ok) {
+ setProcessingErrorMessage((await response.json()).error);
+ return;
+ }
+ contextEntries.push(...(await response.json()));
+ }
+ navigator.serviceWorker.removeEventListener('message', swListener);
+ const model = new MultiTraceModel(contextEntries);
+ setProgress({ done: 0, total: 0 });
+ setModel(model);
+ } else {
+ setModel(emptyModel);
+ }
+ })();
+ }, [traceURLs]);
+
+ React.useEffect(() => {
+ if (processingErrorMessage)
+ window.parent?.postMessage({ method: 'showErrorMessage', params: { message: processingErrorMessage } }, '*');
+ }, [processingErrorMessage]);
+
+ return
+
+
+
+
+ {!traceURLs.length &&
+
Select test to see the trace
+
}
+
;
+};
+
+export const emptyModel = new MultiTraceModel([]);
diff --git a/packages/trace-viewer/src/ui/snapshotTab.tsx b/packages/trace-viewer/src/ui/snapshotTab.tsx
index bdb955c13c..4bc2abc9eb 100644
--- a/packages/trace-viewer/src/ui/snapshotTab.tsx
+++ b/packages/trace-viewer/src/ui/snapshotTab.tsx
@@ -38,7 +38,8 @@ export const SnapshotTab: React.FunctionComponent<{
setIsInspecting: (isInspecting: boolean) => void,
highlightedLocator: string,
setHighlightedLocator: (locator: string) => void,
-}> = ({ action, sdkLanguage, testIdAttributeName, isInspecting, setIsInspecting, highlightedLocator, setHighlightedLocator }) => {
+ openPage?: (url: string, target?: string) => Window | any,
+}> = ({ action, sdkLanguage, testIdAttributeName, isInspecting, setIsInspecting, highlightedLocator, setHighlightedLocator, openPage }) => {
const [measure, ref] = useMeasure();
const [snapshotTab, setSnapshotTab] = React.useState<'action'|'before'|'after'>('action');
@@ -190,7 +191,9 @@ export const SnapshotTab: React.FunctionComponent<{
})}
{
- const win = window.open(popoutUrl || '', '_blank');
+ if (!openPage)
+ openPage = window.open;
+ const win = openPage(popoutUrl || '', '_blank');
win?.addEventListener('DOMContentLoaded', () => {
const injectedScript = new InjectedScript(win as any, false, sdkLanguage, testIdAttributeName, 1, 'chromium', []);
new ConsoleAPI(injectedScript);
diff --git a/packages/trace-viewer/src/ui/workbench.tsx b/packages/trace-viewer/src/ui/workbench.tsx
index c50b345b75..afc003c674 100644
--- a/packages/trace-viewer/src/ui/workbench.tsx
+++ b/packages/trace-viewer/src/ui/workbench.tsx
@@ -51,7 +51,8 @@ export const Workbench: React.FunctionComponent<{
isLive?: boolean,
status?: UITestStatus,
inert?: boolean,
-}> = ({ model, showSourcesFirst, rootDir, fallbackLocation, initialSelection, onSelectionChanged, isLive, status, inert }) => {
+ openPage?: (url: string, target?: string) => Window | any,
+}> = ({ model, showSourcesFirst, rootDir, fallbackLocation, initialSelection, onSelectionChanged, isLive, status, inert, openPage }) => {
const [selectedAction, setSelectedActionImpl] = React.useState(undefined);
const [revealedStack, setRevealedStack] = React.useState(undefined);
const [highlightedAction, setHighlightedAction] = React.useState();
@@ -234,7 +235,8 @@ export const Workbench: React.FunctionComponent<{
isInspecting={isInspecting}
setIsInspecting={setIsInspecting}
highlightedLocator={highlightedLocator}
- setHighlightedLocator={locatorPicked} />
+ setHighlightedLocator={locatorPicked}
+ openPage={openPage} />
{
diff --git a/tests/components/ct-vue-vite/package.json b/tests/components/ct-vue-vite/package.json
index 64f695ac7c..608a128763 100644
--- a/tests/components/ct-vue-vite/package.json
+++ b/tests/components/ct-vue-vite/package.json
@@ -13,8 +13,8 @@
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.1.0",
- "@vue/tsconfig": "^0.1.3",
+ "@vue/tsconfig": "^0.5.1",
"vite": "^5.2.8",
- "vue-tsc": "^1.0.0"
+ "vue-tsc": "^2.0.21"
}
}
diff --git a/tests/components/ct-vue-vite/tsconfig.json b/tests/components/ct-vue-vite/tsconfig.json
index cda3ce98be..b74b79834f 100644
--- a/tests/components/ct-vue-vite/tsconfig.json
+++ b/tests/components/ct-vue-vite/tsconfig.json
@@ -1,5 +1,5 @@
{
- "extends": "@vue/tsconfig/tsconfig.web.json",
+ "extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue", "tests/**/*", "playwright"],
"compilerOptions": {
"baseUrl": ".",
@@ -8,11 +8,5 @@
"*": ["_"],
},
"ignoreDeprecations": "5.0"
- },
-
- "references": [
- {
- "path": "./tsconfig.node.json"
- }
- ]
+ }
}
diff --git a/tests/components/ct-vue-vite/tsconfig.node.json b/tests/components/ct-vue-vite/tsconfig.node.json
index 7924803d11..43cc105313 100644
--- a/tests/components/ct-vue-vite/tsconfig.node.json
+++ b/tests/components/ct-vue-vite/tsconfig.node.json
@@ -1,5 +1,5 @@
{
- "extends": "@vue/tsconfig/tsconfig.node.json",
+ "extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["vite.config.*", "playwright.config.*"],
"compilerOptions": {
"composite": true,
diff --git a/tests/config/browserTest.ts b/tests/config/browserTest.ts
index 8b4fdc27b9..01b574f105 100644
--- a/tests/config/browserTest.ts
+++ b/tests/config/browserTest.ts
@@ -88,6 +88,7 @@ const test = baseTest.extend
isAndroid: [false, { scope: 'worker' }],
isElectron: [false, { scope: 'worker' }],
+ electronMajorVersion: [0, { scope: 'worker' }],
isWebView2: [false, { scope: 'worker' }],
contextFactory: async ({ _contextFactory }: any, run) => {
diff --git a/tests/electron/electron-app.spec.ts b/tests/electron/electron-app.spec.ts
index 37bb69ea9e..c725752e37 100644
--- a/tests/electron/electron-app.spec.ts
+++ b/tests/electron/electron-app.spec.ts
@@ -314,3 +314,27 @@ test('should return app name / version from manifest', async ({ launchElectronAp
version: '1.0.0'
});
});
+
+test('should report downloads', async ({ launchElectronApp, electronMajorVersion, server }) => {
+ test.skip(electronMajorVersion < 30, 'Depends on https://github.com/electron/electron/pull/41718');
+
+ server.setRoute('/download', (req, res) => {
+ res.setHeader('Content-Type', 'application/octet-stream');
+ res.setHeader('Content-Disposition', 'attachment');
+ res.end(`Hello world`);
+ });
+
+ const app = await launchElectronApp('electron-window-app.js', [], {
+ acceptDownloads: true,
+ });
+ const window = await app.firstWindow();
+ await window.setContent(`download`);
+ const [download] = await Promise.all([
+ window.waitForEvent('download'),
+ window.click('a')
+ ]);
+ const path = await download.path();
+ expect(fs.existsSync(path)).toBeTruthy();
+ expect(fs.readFileSync(path).toString()).toBe('Hello world');
+ await app.close();
+});
diff --git a/tests/electron/electronTest.ts b/tests/electron/electronTest.ts
index fb353905d3..33210e9116 100644
--- a/tests/electron/electronTest.ts
+++ b/tests/electron/electronTest.ts
@@ -31,6 +31,7 @@ type ElectronTestFixtures = PageTestFixtures & {
export const electronTest = baseTest.extend(traceViewerFixtures).extend({
browserVersion: [({}, use) => use(process.env.ELECTRON_CHROMIUM_VERSION), { scope: 'worker' }],
browserMajorVersion: [({}, use) => use(Number(process.env.ELECTRON_CHROMIUM_VERSION.split('.')[0])), { scope: 'worker' }],
+ electronMajorVersion: [({}, use) => use(parseInt(require('electron/package.json').version.split('.')[0], 10)), { scope: 'worker' }],
isAndroid: [false, { scope: 'worker' }],
isElectron: [true, { scope: 'worker' }],
isWebView2: [false, { scope: 'worker' }],
diff --git a/tests/library/browsercontext-fetch.spec.ts b/tests/library/browsercontext-fetch.spec.ts
index a8aaeb389f..f8c0327c7d 100644
--- a/tests/library/browsercontext-fetch.spec.ts
+++ b/tests/library/browsercontext-fetch.spec.ts
@@ -435,7 +435,7 @@ it('should return error with wrong credentials', async ({ context, server }) =>
expect(response2.status()).toBe(401);
});
-it('should support HTTPCredentials.sendImmediately for newContext', async ({ contextFactory, server }) => {
+it('should support HTTPCredentials.send for newContext', async ({ contextFactory, server }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30534' });
const context = await contextFactory({
httpCredentials: { username: 'user', password: 'pass', origin: server.PREFIX.toUpperCase(), send: 'always' }
@@ -459,7 +459,7 @@ it('should support HTTPCredentials.sendImmediately for newContext', async ({ con
}
});
-it('should support HTTPCredentials.sendImmediately for browser.newPage', async ({ contextFactory, server, browser }) => {
+it('should support HTTPCredentials.send for browser.newPage', async ({ contextFactory, server, browser }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30534' });
const page = await browser.newPage({
httpCredentials: { username: 'user', password: 'pass', origin: server.PREFIX.toUpperCase(), send: 'always' }
@@ -853,7 +853,7 @@ it('should not hang on a brotli encoded Range request', async ({ context, server
headers: {
range: 'bytes=0-2',
},
- })).rejects.toThrow(/(failed to decompress 'br' encoding: Error: unexpected end of file|Parse Error: Data after \`Connection: close\`)/);
+ })).rejects.toThrow(/Parse Error: Expected HTTP/);
});
it('should dispose', async function({ context, server }) {
diff --git a/tests/library/browsercontext-storage-state.spec.ts b/tests/library/browsercontext-storage-state.spec.ts
index 7bd2581c9a..89bb84d2c3 100644
--- a/tests/library/browsercontext-storage-state.spec.ts
+++ b/tests/library/browsercontext-storage-state.spec.ts
@@ -223,9 +223,9 @@ it('should serialize storageState with lone surrogates', async ({ page, context,
expect(storageState.origins[0].localStorage[0].value).toBe(String.fromCharCode(55934));
});
-it('should work when service worker is intefering', async ({ page, context, server, isAndroid, isElectron }) => {
+it('should work when service worker is intefering', async ({ page, context, server, isAndroid, isElectron, electronMajorVersion }) => {
it.skip(isAndroid);
- it.skip(isElectron);
+ it.skip(isElectron && electronMajorVersion < 30, 'error: Browser context management is not supported.');
server.setRoute('/', (req, res) => {
res.writeHead(200, { 'content-type': 'text/html' });
diff --git a/tests/library/capabilities.spec.ts b/tests/library/capabilities.spec.ts
index 66fc9058e9..863a3bafca 100644
--- a/tests/library/capabilities.spec.ts
+++ b/tests/library/capabilities.spec.ts
@@ -139,15 +139,14 @@ it('should not crash on showDirectoryPicker', async ({ page, server, browserName
it.skip(browserName === 'chromium' && browserMajorVersion < 99, 'Fixed in Chromium r956769');
it.skip(browserName !== 'chromium', 'showDirectoryPicker is only available in Chromium');
await page.goto(server.EMPTY_PAGE);
- await Promise.race([
- page.evaluate(async () => {
- const dir = await (window as any).showDirectoryPicker();
- return dir.name;
- }).catch(e => expect(e.message).toContain('DOMException: The user aborted a request')),
- // The dialog will not be accepted, so we just wait for some time to
- // to give the browser a chance to crash.
- new Promise(r => setTimeout(r, 1000))
- ]);
+ page.evaluate(async () => {
+ const dir = await (window as any).showDirectoryPicker();
+ return dir.name;
+ // In headless it throws (aborted), in headed it stalls (Test ended) and waits for the picker to be accepted.
+ }).catch(e => expect(e.message).toMatch(/((DOMException|AbortError): The user aborted a request|Test ended)/));
+ // The dialog will not be accepted, so we just wait for some time to
+ // to give the browser a chance to crash.
+ await page.waitForTimeout(3_000);
});
it('should not crash on storage.getDirectory()', async ({ page, server, browserName, isMac }) => {
diff --git a/tests/library/chromium/disable-web-security.spec.ts b/tests/library/chromium/disable-web-security.spec.ts
new file mode 100644
index 0000000000..fab5599a76
--- /dev/null
+++ b/tests/library/chromium/disable-web-security.spec.ts
@@ -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(`Click me`);
+ });
+ server.setRoute('/target.html', (req, res) => {
+ res.writeHead(200, {
+ 'Content-Type': 'text/html'
+ });
+ res.end(``);
+ });
+
+ 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(`Click me`);
+ });
+ server.setRoute('/target.html', (req, res) => {
+ res.writeHead(200, {
+ 'Content-Type': 'text/html'
+ });
+ res.end(``);
+ });
+
+ 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);
+});
diff --git a/tests/library/global-fetch.spec.ts b/tests/library/global-fetch.spec.ts
index 264df5c619..5915272621 100644
--- a/tests/library/global-fetch.spec.ts
+++ b/tests/library/global-fetch.spec.ts
@@ -154,7 +154,7 @@ it('should support WWW-Authenticate: Basic', async ({ playwright, server }) => {
expect(credentials).toBe('user:pass');
});
-it('should support HTTPCredentials.sendImmediately', async ({ playwright, server }) => {
+it('should support HTTPCredentials.send', async ({ playwright, server }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30534' });
const request = await playwright.request.newContext({
httpCredentials: { username: 'user', password: 'pass', origin: server.PREFIX.toUpperCase(), send: 'always' }
diff --git a/tests/library/inspector/cli-codegen-3.spec.ts b/tests/library/inspector/cli-codegen-3.spec.ts
index 96a6295f13..2c0fff974a 100644
--- a/tests/library/inspector/cli-codegen-3.spec.ts
+++ b/tests/library/inspector/cli-codegen-3.spec.ts
@@ -631,6 +631,27 @@ await page.GetByLabel("Coun\\"try").ClickAsync();`);
expect.soft(sources2.get('C#')!.text).toContain(`await Expect(page.Locator("#second")).ToHaveValueAsync("bar")`);
});
+ test('should assert value on disabled select', async ({ openRecorder, browserName }) => {
+ const recorder = await openRecorder();
+
+ await recorder.setContentAndWait(`
+
+
+ `);
+
+ await recorder.page.click('x-pw-tool-item.value');
+ await recorder.hoverOverElement('#second');
+ const [sources2] = await Promise.all([
+ recorder.waitForOutput('JavaScript', '#second'),
+ recorder.trustedClick(),
+ ]);
+ expect.soft(sources2.get('JavaScript')!.text).toContain(`await expect(page.locator('#second')).toHaveValue('bar2')`);
+ expect.soft(sources2.get('Python')!.text).toContain(`expect(page.locator("#second")).to_have_value("bar2")`);
+ expect.soft(sources2.get('Python Async')!.text).toContain(`await expect(page.locator("#second")).to_have_value("bar2")`);
+ expect.soft(sources2.get('Java')!.text).toContain(`assertThat(page.locator("#second")).hasValue("bar2")`);
+ expect.soft(sources2.get('C#')!.text).toContain(`await Expect(page.Locator("#second")).ToHaveValueAsync("bar2")`);
+ });
+
test('should assert visibility', async ({ openRecorder }) => {
const recorder = await openRecorder();
diff --git a/tests/library/page-event-crash.spec.ts b/tests/library/page-event-crash.spec.ts
index ea90023630..1bf9c396dd 100644
--- a/tests/library/page-event-crash.spec.ts
+++ b/tests/library/page-event-crash.spec.ts
@@ -70,9 +70,8 @@ test('should cancel navigation when page crashes', async ({ server, page, crash
expect(error.message).toContain('page.goto: Page crashed');
});
-test('should be able to close context when page crashes', async ({ isAndroid, isElectron, isWebView2, page, crash }) => {
+test('should be able to close context when page crashes', async ({ isAndroid, isWebView2, page, crash }) => {
test.skip(isAndroid);
- test.skip(isElectron);
test.skip(isWebView2, 'Page.close() is not supported in WebView2');
await page.setContent(`
This page should crash
`);
diff --git a/tests/library/touch.spec.ts b/tests/library/touch.spec.ts
new file mode 100644
index 0000000000..f842beb207
--- /dev/null
+++ b/tests/library/touch.spec.ts
@@ -0,0 +1,76 @@
+/**
+ * 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';
+import type { Locator } from 'playwright-core';
+
+it.use({ hasTouch: true });
+
+it.fixme(({ browserName }) => browserName === 'firefox');
+
+it('slow swipe events @smoke', async ({ page }) => {
+ it.fixme();
+ await page.setContent(`
a
`);
+ const eventsHandle = await trackEvents(await page.locator('#a'));
+ const center = await centerPoint(page.locator('#a'));
+ await page.touchscreen.touch('touchstart', [{ ...center, id: 1 }]);
+ expect.soft(await eventsHandle.jsonValue()).toEqual([
+ 'pointerover',
+ 'pointerenter',
+ 'pointerdown',
+ 'touchstart',
+ ]);
+
+ await eventsHandle.evaluate(events => events.length = 0);
+ await page.touchscreen.touch('touchmove', [{ x: center.x + 10, y: center.y + 10, id: 1 }]);
+ await page.touchscreen.touch('touchmove', [{ x: center.x + 20, y: center.y + 20, id: 1 }]);
+ expect.soft(await eventsHandle.jsonValue()).toEqual([
+ 'pointermove',
+ 'touchmove',
+ 'pointermove',
+ 'touchmove',
+ ]);
+
+ await eventsHandle.evaluate(events => events.length = 0);
+ await page.touchscreen.touch('touchend', [{ x: center.x + 20, y: center.y + 20, id: 1 }]);
+ expect.soft(await eventsHandle.jsonValue()).toEqual([
+ 'pointerup',
+ 'pointerout',
+ 'pointerleave',
+ 'touchend',
+ ]);
+});
+
+
+async function trackEvents(target: Locator) {
+ const eventsHandle = await target.evaluateHandle(target => {
+ const events: string[] = [];
+ for (const event of [
+ 'mousedown', 'mouseenter', 'mouseleave', 'mousemove', 'mouseout', 'mouseover', 'mouseup', 'click',
+ 'pointercancel', 'pointerdown', 'pointerenter', 'pointerleave', 'pointermove', 'pointerout', 'pointerover', 'pointerup',
+ 'touchstart', 'touchend', 'touchmove', 'touchcancel',])
+ target.addEventListener(event, () => events.push(event), { passive: false });
+ return events;
+ });
+ return eventsHandle;
+}
+
+async function centerPoint(e: Locator) {
+ const box = await e.boundingBox();
+ if (!box)
+ throw new Error('Element is not visible');
+ return { x: box.x + box.width / 2, y: box.y + box.height / 2 };
+}
\ No newline at end of file
diff --git a/tests/page/expect-boolean.spec.ts b/tests/page/expect-boolean.spec.ts
index 719fa33877..a22657bda6 100644
--- a/tests/page/expect-boolean.spec.ts
+++ b/tests/page/expect-boolean.spec.ts
@@ -455,7 +455,7 @@ test('should print selector syntax error', async ({ page }) => {
test.describe(() => {
test.skip(({ isAndroid }) => isAndroid, 'server.EMPTY_PAGE is the emulator address 10.0.2.2');
- test.skip(({ isElectron }) => isElectron, 'Protocol error (Storage.getCookies): Browser context management is not supported.');
+ test.skip(({ isElectron, electronMajorVersion }) => isElectron && electronMajorVersion < 30, 'Protocol error (Storage.getCookies): Browser context management is not supported.');
test('toBeOK', async ({ page, server }) => {
const res = await page.request.get(server.EMPTY_PAGE);
diff --git a/tests/page/interception.spec.ts b/tests/page/interception.spec.ts
index c381e9f6e3..d71675cc39 100644
--- a/tests/page/interception.spec.ts
+++ b/tests/page/interception.spec.ts
@@ -33,9 +33,8 @@ it('should work with navigation @smoke', async ({ page, server }) => {
expect(requests.get('style.css').isNavigationRequest()).toBe(false);
});
-it('should intercept after a service worker', async ({ page, server, browserName, isAndroid, isElectron }) => {
+it('should intercept after a service worker', async ({ page, server, browserName, isAndroid }) => {
it.skip(isAndroid);
- it.skip(isElectron);
await page.goto(server.PREFIX + '/serviceworkers/fetchdummy/sw.html');
await page.evaluate(() => window['activationPromise']);
diff --git a/tests/page/page-clock.frozen.spec.ts b/tests/page/page-clock.frozen.spec.ts
index 9b70936377..b9b319cd63 100644
--- a/tests/page/page-clock.frozen.spec.ts
+++ b/tests/page/page-clock.frozen.spec.ts
@@ -23,5 +23,5 @@ it('clock should be frozen', async ({ page }) => {
it('clock should be realtime', async ({ page }) => {
it.skip(process.env.PW_CLOCK !== 'realtime');
- expect(await page.evaluate('Date.now()')).toBeLessThan(1000);
+ expect(await page.evaluate('Date.now()')).toBeLessThan(10000);
});
diff --git a/tests/page/page-event-request.spec.ts b/tests/page/page-event-request.spec.ts
index 07c1fcc3ce..cfc2f7a1e2 100644
--- a/tests/page/page-event-request.spec.ts
+++ b/tests/page/page-event-request.spec.ts
@@ -107,9 +107,8 @@ it('should report requests and responses handled by service worker with routing'
expect(interceptedUrls).toEqual(expectedUrls);
});
-it('should report navigation requests and responses handled by service worker', async ({ page, server, isAndroid, isElectron, browserName }) => {
+it('should report navigation requests and responses handled by service worker', async ({ page, server, isAndroid, browserName }) => {
it.fixme(isAndroid);
- it.fixme(isElectron);
await page.goto(server.PREFIX + '/serviceworkers/stub/sw.html');
await page.evaluate(() => window['activationPromise']);
@@ -136,9 +135,8 @@ it('should report navigation requests and responses handled by service worker',
}
});
-it('should report navigation requests and responses handled by service worker with routing', async ({ page, server, isAndroid, isElectron, browserName }) => {
+it('should report navigation requests and responses handled by service worker with routing', async ({ page, server, isAndroid, browserName }) => {
it.fixme(isAndroid);
- it.fixme(isElectron);
await page.route('**/*', route => route.continue());
await page.goto(server.PREFIX + '/serviceworkers/stub/sw.html');
diff --git a/tests/page/page-goto.spec.ts b/tests/page/page-goto.spec.ts
index f0a700429d..5ca9ee6e46 100644
--- a/tests/page/page-goto.spec.ts
+++ b/tests/page/page-goto.spec.ts
@@ -685,8 +685,7 @@ it('should fail when canceled by another navigation', async ({ page, server }) =
expect(error.message).toBeTruthy();
});
-it('should work with lazy loading iframes', async ({ page, server, isElectron, isAndroid }) => {
- it.fixme(isElectron);
+it('should work with lazy loading iframes', async ({ page, server, isAndroid }) => {
it.fixme(isAndroid);
await page.goto(server.PREFIX + '/frames/lazy-frame.html');
diff --git a/tests/page/page-request-continue.spec.ts b/tests/page/page-request-continue.spec.ts
index 2fbac43081..dd7ed68b49 100644
--- a/tests/page/page-request-continue.spec.ts
+++ b/tests/page/page-request-continue.spec.ts
@@ -412,6 +412,27 @@ it('continue should propagate headers to redirects', async ({ page, server, brow
expect(serverRequest.headers['custom']).toBe('value');
});
+it('redirected requests should report overridden headers', {
+ annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/31351' }
+}, async ({ page, server, browserName }) => {
+ it.fixme(browserName === 'firefox');
+ await server.setRedirect('/redirect', '/empty.html');
+ await page.route('**/redirect', route => {
+ const headers = route.request().headers();
+ headers['custom'] = 'value';
+ void route.fallback({ headers });
+ });
+
+ const [serverRequest, response] = await Promise.all([
+ server.waitForRequest('/empty.html'),
+ page.goto(server.PREFIX + '/redirect')
+ ]);
+ expect(serverRequest.headers['custom']).toBe('value');
+ expect(response.request().url()).toBe(server.EMPTY_PAGE);
+ expect(response.request().headers()['custom']).toBe('value');
+ expect((await response.request().allHeaders())['custom']).toBe('value');
+});
+
it('continue should delete headers on redirects', async ({ page, server, browserName }) => {
it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/13106' });
it.fixme(browserName === 'firefox');
diff --git a/tests/page/page-request-fulfill.spec.ts b/tests/page/page-request-fulfill.spec.ts
index 7b78c07625..ebde0dbc56 100644
--- a/tests/page/page-request-fulfill.spec.ts
+++ b/tests/page/page-request-fulfill.spec.ts
@@ -140,10 +140,10 @@ it('should allow mocking binary responses', async ({ page, server, browserName,
expect(await img.screenshot()).toMatchSnapshot('mock-binary-response.png');
});
-it('should allow mocking svg with charset', async ({ page, server, browserName, headless, isAndroid, isElectron, mode }) => {
+it('should allow mocking svg with charset', async ({ page, server, browserName, headless, isAndroid, isElectron, electronMajorVersion }) => {
it.skip(browserName === 'firefox' && !headless, 'Firefox headed produces a different image.');
it.skip(isAndroid);
- it.skip(isElectron, 'Protocol error (Storage.getCookies): Browser context management is not supported');
+ it.skip(isElectron && electronMajorVersion < 30, 'Protocol error (Storage.getCookies): Browser context management is not supported');
await page.route('**/*', route => {
void route.fulfill({
@@ -253,8 +253,8 @@ it('should include the origin header', async ({ page, server, isAndroid }) => {
expect(interceptedRequest.headers()['origin']).toEqual(server.PREFIX);
});
-it('should fulfill with global fetch result', async ({ playwright, page, server, isElectron, rewriteAndroidLoopbackURL }) => {
- it.fixme(isElectron, 'error: Browser context management is not supported.');
+it('should fulfill with global fetch result', async ({ playwright, page, server, isElectron, electronMajorVersion, rewriteAndroidLoopbackURL }) => {
+ it.skip(isElectron && electronMajorVersion < 30, 'error: Browser context management is not supported.');
await page.route('**/*', async route => {
const request = await playwright.request.newContext();
const response = await request.get(rewriteAndroidLoopbackURL(server.PREFIX + '/simple.json'));
@@ -265,8 +265,8 @@ it('should fulfill with global fetch result', async ({ playwright, page, server,
expect(await response.json()).toEqual({ 'foo': 'bar' });
});
-it('should fulfill with fetch result', async ({ page, server, isElectron, rewriteAndroidLoopbackURL }) => {
- it.fixme(isElectron, 'error: Browser context management is not supported.');
+it('should fulfill with fetch result', async ({ page, server, isElectron, electronMajorVersion, rewriteAndroidLoopbackURL }) => {
+ it.skip(isElectron && electronMajorVersion < 30, 'error: Browser context management is not supported.');
await page.route('**/*', async route => {
const response = await page.request.get(rewriteAndroidLoopbackURL(server.PREFIX + '/simple.json'));
void route.fulfill({ response });
@@ -276,8 +276,8 @@ it('should fulfill with fetch result', async ({ page, server, isElectron, rewrit
expect(await response.json()).toEqual({ 'foo': 'bar' });
});
-it('should fulfill with fetch result and overrides', async ({ page, server, isElectron, rewriteAndroidLoopbackURL }) => {
- it.fixme(isElectron, 'error: Browser context management is not supported.');
+it('should fulfill with fetch result and overrides', async ({ page, server, isElectron, electronMajorVersion, rewriteAndroidLoopbackURL }) => {
+ it.skip(isElectron && electronMajorVersion < 30, 'error: Browser context management is not supported.');
await page.route('**/*', async route => {
const response = await page.request.get(rewriteAndroidLoopbackURL(server.PREFIX + '/simple.json'));
void route.fulfill({
@@ -295,8 +295,8 @@ it('should fulfill with fetch result and overrides', async ({ page, server, isEl
expect(await response.json()).toEqual({ 'foo': 'bar' });
});
-it('should fetch original request and fulfill', async ({ page, server, isElectron, isAndroid }) => {
- it.fixme(isElectron, 'error: Browser context management is not supported.');
+it('should fetch original request and fulfill', async ({ page, server, isElectron, electronMajorVersion, isAndroid }) => {
+ it.skip(isElectron && electronMajorVersion < 30, 'error: Browser context management is not supported.');
it.skip(isAndroid, 'The internal Android localhost (10.0.0.2) != the localhost on the host');
await page.route('**/*', async route => {
const response = await page.request.fetch(route.request());
@@ -309,8 +309,8 @@ it('should fetch original request and fulfill', async ({ page, server, isElectro
expect(await page.title()).toEqual('Woof-Woof');
});
-it('should fulfill with multiple set-cookie', async ({ page, server, isElectron }) => {
- it.fixme(isElectron, 'Electron 14+ is required');
+it('should fulfill with multiple set-cookie', async ({ page, server, isElectron, electronMajorVersion }) => {
+ it.skip(isElectron && electronMajorVersion < 14, 'Electron 14+ is required');
const cookies = ['a=b', 'c=d'];
await page.route('**/empty.html', async route => {
void route.fulfill({
@@ -442,9 +442,9 @@ it('should fulfill json', async ({ page, server }) => {
it('should fulfill with gzip and readback', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/29261' },
-}, async ({ page, server, isAndroid, isElectron }) => {
+}, async ({ page, server, isAndroid, isElectron, electronMajorVersion }) => {
it.skip(isAndroid, 'The internal Android localhost (10.0.0.2) != the localhost on the host');
- it.fixme(isElectron, 'error: Browser context management is not supported.');
+ it.skip(isElectron && electronMajorVersion < 30, 'error: Browser context management is not supported.');
server.enableGzip('/one-style.html');
await page.route('**/one-style.html', async route => {
const response = await route.fetch();
diff --git a/tests/page/page-request-intercept.spec.ts b/tests/page/page-request-intercept.spec.ts
index 8b0b82bedf..33516606e7 100644
--- a/tests/page/page-request-intercept.spec.ts
+++ b/tests/page/page-request-intercept.spec.ts
@@ -33,8 +33,8 @@ const it = base.extend<{ rewriteAndroidLoopbackURL(url: string): string }>({
})
});
-it('should fulfill intercepted response', async ({ page, server, isElectron, isAndroid }) => {
- it.fixme(isElectron, 'error: Browser context management is not supported.');
+it('should fulfill intercepted response', async ({ page, server, isElectron, electronMajorVersion, isAndroid }) => {
+ it.skip(isElectron && electronMajorVersion < 30, 'error: Browser context management is not supported.');
it.skip(isAndroid, 'The internal Android localhost (10.0.0.2) != the localhost on the host');
await page.route('**/*', async route => {
const response = await page.request.fetch(route.request());
@@ -55,10 +55,10 @@ it('should fulfill intercepted response', async ({ page, server, isElectron, isA
expect(await page.evaluate(() => document.body.textContent)).toBe('Yo, page!');
});
-it('should fulfill response with empty body', async ({ page, server, isAndroid, isElectron, browserName, browserMajorVersion }) => {
+it('should fulfill response with empty body', async ({ page, server, isAndroid, isElectron, electronMajorVersion, browserName, browserMajorVersion }) => {
it.skip(browserName === 'chromium' && browserMajorVersion <= 91, 'Fails in Electron that uses old Chromium');
it.skip(isAndroid, 'The internal Android localhost (10.0.0.2) != the localhost on the host');
- it.skip(isElectron, 'Protocol error (Storage.getCookies): Browser context management is not supported.');
+ it.skip(isElectron && electronMajorVersion < 30, 'error: Browser context management is not supported.');
await page.route('**/*', async route => {
const response = await page.request.fetch(route.request());
await route.fulfill({
@@ -73,8 +73,8 @@ it('should fulfill response with empty body', async ({ page, server, isAndroid,
expect(await response.text()).toBe('');
});
-it('should override with defaults when intercepted response not provided', async ({ page, server, browserName, isElectron, isAndroid }) => {
- it.fixme(isElectron, 'error: Browser context management is not supported.');
+it('should override with defaults when intercepted response not provided', async ({ page, server, browserName, isElectron, electronMajorVersion, isAndroid }) => {
+ it.skip(isElectron && electronMajorVersion < 30, 'error: Browser context management is not supported.');
it.skip(isAndroid, 'The internal Android localhost (10.0.0.2) != the localhost on the host');
server.setRoute('/empty.html', (req, res) => {
res.setHeader('foo', 'bar');
@@ -95,8 +95,8 @@ it('should override with defaults when intercepted response not provided', async
expect(response.headers()).toEqual({ });
});
-it('should fulfill with any response', async ({ page, server, isElectron, rewriteAndroidLoopbackURL }) => {
- it.fixme(isElectron, 'error: Browser context management is not supported.');
+it('should fulfill with any response', async ({ page, server, isElectron, electronMajorVersion, rewriteAndroidLoopbackURL }) => {
+ it.skip(isElectron && electronMajorVersion < 30, 'error: Browser context management is not supported.');
server.setRoute('/sample', (req, res) => {
res.setHeader('foo', 'bar');
@@ -117,8 +117,8 @@ it('should fulfill with any response', async ({ page, server, isElectron, rewrit
expect(response.headers()['foo']).toBe('bar');
});
-it('should support fulfill after intercept', async ({ page, server, isElectron, isAndroid }) => {
- it.fixme(isElectron, 'error: Browser context management is not supported.');
+it('should support fulfill after intercept', async ({ page, server, isElectron, electronMajorVersion, isAndroid }) => {
+ it.skip(isElectron && electronMajorVersion < 30, 'error: Browser context management is not supported.');
it.skip(isAndroid, 'The internal Android localhost (10.0.0.2) != the localhost on the host');
const requestPromise = server.waitForRequest('/title.html');
await page.route('**', async route => {
@@ -132,8 +132,8 @@ it('should support fulfill after intercept', async ({ page, server, isElectron,
expect(await response.text()).toBe(original);
});
-it('should give access to the intercepted response', async ({ page, server, isElectron, isAndroid }) => {
- it.fixme(isElectron, 'error: Browser context management is not supported.');
+it('should give access to the intercepted response', async ({ page, server, isElectron, electronMajorVersion, isAndroid }) => {
+ it.skip(isElectron && electronMajorVersion < 30, 'error: Browser context management is not supported.');
it.skip(isAndroid, 'The internal Android localhost (10.0.0.2) != the localhost on the host');
await page.goto(server.EMPTY_PAGE);
@@ -156,8 +156,8 @@ it('should give access to the intercepted response', async ({ page, server, isEl
await Promise.all([route.fulfill({ response }), evalPromise]);
});
-it('should give access to the intercepted response body', async ({ page, server, isElectron, isAndroid }) => {
- it.fixme(isElectron, 'error: Browser context management is not supported.');
+it('should give access to the intercepted response body', async ({ page, server, isAndroid, isElectron, electronMajorVersion }) => {
+ it.skip(isElectron && electronMajorVersion < 30, 'error: Browser context management is not supported.');
it.skip(isAndroid, 'The internal Android localhost (10.0.0.2) != the localhost on the host');
await page.goto(server.EMPTY_PAGE);
@@ -194,8 +194,8 @@ it('should intercept multipart/form-data request body', async ({ page, server, a
expect(request.postData()).toContain(fs.readFileSync(filePath, 'utf8'));
});
-it('should fulfill intercepted response using alias', async ({ page, server, isElectron, isAndroid }) => {
- it.fixme(isElectron, 'error: Browser context management is not supported.');
+it('should fulfill intercepted response using alias', async ({ page, server, isElectron, electronMajorVersion, isAndroid }) => {
+ it.skip(isElectron && electronMajorVersion < 30, 'error: Browser context management is not supported.');
it.skip(isAndroid, 'The internal Android localhost (10.0.0.2) != the localhost on the host');
await page.route('**/*', async route => {
const response = await route.fetch();
@@ -206,8 +206,8 @@ it('should fulfill intercepted response using alias', async ({ page, server, isE
expect(response.headers()['content-type']).toContain('text/html');
});
-it('should support timeout option in route.fetch', async ({ page, server, isElectron, isAndroid }) => {
- it.fixme(isElectron, 'error: Browser context management is not supported.');
+it('should support timeout option in route.fetch', async ({ page, server, isElectron, electronMajorVersion, isAndroid }) => {
+ it.skip(isElectron && electronMajorVersion < 30, 'error: Browser context management is not supported.');
it.skip(isAndroid, 'The internal Android localhost (10.0.0.2) != the localhost on the host');
server.setRoute('/slow', (req, res) => {
@@ -224,8 +224,8 @@ it('should support timeout option in route.fetch', async ({ page, server, isElec
expect(error.message).toContain(`Timeout 2000ms exceeded`);
});
-it('should not follow redirects when maxRedirects is set to 0 in route.fetch', async ({ page, server, isAndroid, isElectron }) => {
- it.fixme(isElectron, 'error: Browser context management is not supported.');
+it('should not follow redirects when maxRedirects is set to 0 in route.fetch', async ({ page, server, isAndroid, isElectron, electronMajorVersion }) => {
+ it.skip(isElectron && electronMajorVersion < 30, 'error: Browser context management is not supported.');
it.skip(isAndroid, 'The internal Android localhost (10.0.0.2) != the localhost on the host');
server.setRedirect('/foo', '/empty.html');
@@ -239,8 +239,8 @@ it('should not follow redirects when maxRedirects is set to 0 in route.fetch', a
expect(await page.content()).toContain('hello');
});
-it('should intercept with url override', async ({ page, server, isElectron, isAndroid }) => {
- it.fixme(isElectron, 'error: Browser context management is not supported.');
+it('should intercept with url override', async ({ page, server, isElectron, electronMajorVersion, isAndroid }) => {
+ it.skip(isElectron && electronMajorVersion < 30, 'error: Browser context management is not supported.');
it.skip(isAndroid, 'The internal Android localhost (10.0.0.2) != the localhost on the host');
await page.route('**/*.html', async route => {
const response = await route.fetch({ url: server.PREFIX + '/one-style.html' });
@@ -251,8 +251,8 @@ it('should intercept with url override', async ({ page, server, isElectron, isAn
expect((await response.body()).toString()).toContain('one-style.css');
});
-it('should intercept with post data override', async ({ page, server, isElectron, isAndroid }) => {
- it.fixme(isElectron, 'error: Browser context management is not supported.');
+it('should intercept with post data override', async ({ page, server, isElectron, electronMajorVersion, isAndroid }) => {
+ it.skip(isElectron && electronMajorVersion < 30, 'error: Browser context management is not supported.');
it.skip(isAndroid, 'The internal Android localhost (10.0.0.2) != the localhost on the host');
const requestPromise = server.waitForRequest('/empty.html');
await page.route('**/*.html', async route => {
@@ -266,8 +266,8 @@ it('should intercept with post data override', async ({ page, server, isElectron
expect((await request.postBody).toString()).toBe(JSON.stringify({ 'foo': 'bar' }));
});
-it('should fulfill popup main request using alias', async ({ page, server, isElectron, isAndroid }) => {
- it.fixme(isElectron, 'error: Browser context management is not supported.');
+it('should fulfill popup main request using alias', async ({ page, server, isElectron, electronMajorVersion, isAndroid }) => {
+ it.skip(isElectron && electronMajorVersion < 30, 'error: Browser context management is not supported.');
it.skip(isAndroid, 'The internal Android localhost (10.0.0.2) != the localhost on the host');
await page.context().route('**/*', async route => {
@@ -284,8 +284,8 @@ it('should fulfill popup main request using alias', async ({ page, server, isEle
it('request.postData is not null when fetching FormData with a Blob', {
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/24077' }
-}, async ({ server, page, browserName, isElectron }) => {
- it.fixme(isElectron, 'error: Browser context management is not supported.');
+}, async ({ server, page, browserName, isElectron, electronMajorVersion }) => {
+ it.skip(isElectron && electronMajorVersion < 31);
it.fixme(browserName === 'webkit', 'The body is empty in WebKit when intercepting');
await page.goto(server.EMPTY_PAGE);
await page.setContent(`
diff --git a/tests/page/page-route.spec.ts b/tests/page/page-route.spec.ts
index ebde1617ea..911521018e 100644
--- a/tests/page/page-route.spec.ts
+++ b/tests/page/page-route.spec.ts
@@ -152,9 +152,9 @@ it('should contain referer header', async ({ page, server }) => {
expect(requests[1].headers().referer).toContain('/one-style.html');
});
-it('should properly return navigation response when URL has cookies', async ({ page, server, isElectron, isAndroid }) => {
+it('should properly return navigation response when URL has cookies', async ({ page, server, isAndroid, isElectron, electronMajorVersion }) => {
it.skip(isAndroid, 'No isolated context');
- it.fixme(isElectron, 'error: Browser context management is not supported.');
+ it.skip(isElectron && electronMajorVersion < 30, 'error: Browser context management is not supported.');
// Setup cookie.
await page.goto(server.EMPTY_PAGE);
diff --git a/tests/page/pageTestApi.ts b/tests/page/pageTestApi.ts
index a7b124a84b..cf497e76c0 100644
--- a/tests/page/pageTestApi.ts
+++ b/tests/page/pageTestApi.ts
@@ -32,6 +32,7 @@ export type PageWorkerFixtures = {
browserName: 'chromium' | 'firefox' | 'webkit';
browserVersion: string;
browserMajorVersion: number;
+ electronMajorVersion: number;
isAndroid: boolean;
isElectron: boolean;
isWebView2: boolean;
diff --git a/tests/playwright-test/fixture-errors.spec.ts b/tests/playwright-test/fixture-errors.spec.ts
index c17db65bce..51c928f156 100644
--- a/tests/playwright-test/fixture-errors.spec.ts
+++ b/tests/playwright-test/fixture-errors.spec.ts
@@ -256,6 +256,41 @@ test('should detect fixture dependency cycle', async ({ runInlineTest }) => {
expect(result.exitCode).toBe(1);
});
+test('should hide boxed fixtures in dependency cycle', async ({ runInlineTest }) => {
+ const result = await runInlineTest({
+ 'x.spec.ts': `
+ import { test as base } from '@playwright/test';
+ const test = base.extend({
+ storageState: async ({ context, storageState }, use) => {
+ await use(storageState);
+ }
+ });
+ test('failed', async ({ page }) => {});
+ `,
+ });
+ expect(result.output).toContain('Fixtures "context" -> "storageState" -> "context" form a dependency cycle: -> x.spec.ts:3:25 -> ');
+ expect(result.exitCode).toBe(1);
+});
+
+test('should show boxed fixtures in dependency cycle if there are no public fixtures', async ({ runInlineTest }) => {
+ const result = await runInlineTest({
+ 'x.spec.ts': `
+ import { test as base } from '@playwright/test';
+ const test = base.extend({
+ f1: [async ({ f2 }, use) => {
+ await use(f2);
+ }, { box: true }],
+ f2: [async ({ f1 }, use) => {
+ await use(f1);
+ }, { box: true }],
+ });
+ test('failed', async ({ f1, f2 }) => {});
+ `,
+ });
+ expect(result.output).toContain('Fixtures "f1" -> "f2" -> "f1" form a dependency cycle: x.spec.ts:3:25 -> x.spec.ts:3:25 -> x.spec.ts:3:25');
+ expect(result.exitCode).toBe(1);
+});
+
test('should not reuse fixtures from one file in another one', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.spec.ts': `
diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts
index 83992c0bad..e416cd05c1 100644
--- a/tests/playwright-test/reporter-html.spec.ts
+++ b/tests/playwright-test/reporter-html.spec.ts
@@ -294,6 +294,7 @@ for (const useIntermediateMergeReport of [false] as const) {
});
test('should include image diff when screenshot failed to generate due to animation', async ({ runInlineTest, page, showReport }) => {
+ test.skip(process.env.PW_CLOCK === 'frozen', 'Assumes Date.now() changes');
const result = await runInlineTest({
'playwright.config.ts': `
module.exports = { use: { viewport: { width: 200, height: 200 }} };
diff --git a/tests/playwright-test/test-modifiers.spec.ts b/tests/playwright-test/test-modifiers.spec.ts
index 2b206b448a..0dd41bd0ab 100644
--- a/tests/playwright-test/test-modifiers.spec.ts
+++ b/tests/playwright-test/test-modifiers.spec.ts
@@ -690,3 +690,50 @@ test('static modifiers should be added in serial mode', async ({ runInlineTest }
expect(result.report.suites[0].specs[2].tests[0].annotations).toEqual([{ type: 'skip' }]);
expect(result.report.suites[0].specs[3].tests[0].annotations).toEqual([]);
});
+
+test('should contain only one slow modifier', async ({ runInlineTest }) => {
+ const result = await runInlineTest({
+ 'slow.test.ts': `
+ import { test } from '@playwright/test';
+ test.slow();
+ test('pass', { annotation: { type: 'issue', description: 'my-value' } }, () => {});
+ `,
+ 'skip.test.ts': `
+ import { test } from '@playwright/test';
+ test.skip();
+ test('pass', { annotation: { type: 'issue', description: 'my-value' } }, () => {});
+ `,
+ 'fixme.test.ts': `
+ import { test } from '@playwright/test';
+ test.fixme();
+ test('pass', { annotation: { type: 'issue', description: 'my-value' } }, () => {});
+`,
+ });
+ expect(result.exitCode).toBe(0);
+ expect(result.passed).toBe(1);
+ expect(result.report.suites[0].specs[0].tests[0].annotations).toEqual([{ type: 'fixme' }, { type: 'issue', description: 'my-value' }]);
+ expect(result.report.suites[1].specs[0].tests[0].annotations).toEqual([{ type: 'skip' }, { type: 'issue', description: 'my-value' }]);
+ expect(result.report.suites[2].specs[0].tests[0].annotations).toEqual([{ type: 'slow' }, { type: 'issue', description: 'my-value' }]);
+});
+
+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);
+});
diff --git a/tests/playwright-test/timeout.spec.ts b/tests/playwright-test/timeout.spec.ts
index 50069b3280..a079c3f70b 100644
--- a/tests/playwright-test/timeout.spec.ts
+++ b/tests/playwright-test/timeout.spec.ts
@@ -182,7 +182,7 @@ test('should respect fixture timeout', async ({ runInlineTest }) => {
slowSetup: [async ({}, use) => {
await new Promise(f => setTimeout(f, 2000));
await use('hey');
- }, { timeout: 500, _title: 'custom title' }],
+ }, { timeout: 500, title: 'custom title' }],
slowTeardown: [async ({}, use) => {
await use('hey');
await new Promise(f => setTimeout(f, 2000));
@@ -227,7 +227,7 @@ test('should respect test.setTimeout in the worker fixture', async ({ runInlineT
slowTeardown: [async ({}, use) => {
await use('hey');
await new Promise(f => setTimeout(f, 2000));
- }, { scope: 'worker', timeout: 400, _title: 'custom title' }],
+ }, { scope: 'worker', timeout: 400, title: 'custom title' }],
});
test('test ok', async ({ fixture, noTimeout }) => {
await new Promise(f => setTimeout(f, 1000));
diff --git a/tests/webview2/webView2Test.ts b/tests/webview2/webView2Test.ts
index b72f57ce93..dcadcebe6d 100644
--- a/tests/webview2/webView2Test.ts
+++ b/tests/webview2/webView2Test.ts
@@ -30,6 +30,7 @@ export const webView2Test = baseTest.extend(traceViewerFixt
browserMajorVersion: [({ browserVersion }, use) => use(Number(browserVersion.split('.')[0])), { scope: 'worker' }],
isAndroid: [false, { scope: 'worker' }],
isElectron: [false, { scope: 'worker' }],
+ electronMajorVersion: [0, { scope: 'worker' }],
isWebView2: [true, { scope: 'worker' }],
browser: [async ({ playwright }, use, testInfo) => {
diff --git a/utils/build/build-playwright-driver.sh b/utils/build/build-playwright-driver.sh
index 975c66446b..c0864681bc 100755
--- a/utils/build/build-playwright-driver.sh
+++ b/utils/build/build-playwright-driver.sh
@@ -4,7 +4,7 @@ set -x
trap "cd $(pwd -P)" EXIT
SCRIPT_PATH="$(cd "$(dirname "$0")" ; pwd -P)"
-NODE_VERSION="20.14.0" # autogenerated via ./update-playwright-driver-version.mjs
+NODE_VERSION="20.15.0" # autogenerated via ./update-playwright-driver-version.mjs
cd "$(dirname "$0")"
PACKAGE_VERSION=$(node -p "require('../../package.json').version")
diff --git a/utils/doclint/generateDotnetApi.js b/utils/doclint/generateDotnetApi.js
index db2204617b..9c094026f9 100644
--- a/utils/doclint/generateDotnetApi.js
+++ b/utils/doclint/generateDotnetApi.js
@@ -82,6 +82,7 @@ classNameMap.set('boolean', 'bool');
classNameMap.set('any', 'object');
classNameMap.set('Buffer', 'byte[]');
classNameMap.set('path', 'string');
+classNameMap.set('Date', 'DateTime');
classNameMap.set('URL', 'string');
classNameMap.set('RegExp', 'Regex');
classNameMap.set('Readable', 'Stream');
diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts
index 0367f3259c..80880f71a7 100644
--- a/utils/generate_types/overrides-test.d.ts
+++ b/utils/generate_types/overrides-test.d.ts
@@ -140,13 +140,13 @@ export type WorkerFixture = (args: Args, use: (r: R) =
type TestFixtureValue = Exclude | TestFixture;
type WorkerFixtureValue = Exclude | WorkerFixture;
export type Fixtures = {
- [K in keyof PW]?: WorkerFixtureValue | [WorkerFixtureValue, { scope: 'worker', timeout?: number | undefined }];
+ [K in keyof PW]?: WorkerFixtureValue | [WorkerFixtureValue, { scope: 'worker', timeout?: number | undefined, title?: string, box?: boolean }];
} & {
- [K in keyof PT]?: TestFixtureValue | [TestFixtureValue, { scope: 'test', timeout?: number | undefined }];
+ [K in keyof PT]?: TestFixtureValue | [TestFixtureValue, { scope: 'test', timeout?: number | undefined, title?: string, box?: boolean }];
} & {
- [K in keyof W]?: [WorkerFixtureValue, { scope: 'worker', auto?: boolean, option?: boolean, timeout?: number | undefined }];
+ [K in keyof W]?: [WorkerFixtureValue, { scope: 'worker', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }];
} & {
- [K in keyof T]?: TestFixtureValue | [TestFixtureValue, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined }];
+ [K in keyof T]?: TestFixtureValue | [TestFixtureValue, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }];
};
type BrowserName = 'chromium' | 'firefox' | 'webkit';