Merge branch 'main' into sharding-algorithm
This commit is contained in:
commit
29f67f3522
|
|
@ -1,6 +1,6 @@
|
|||
# 🎭 Playwright
|
||||
|
||||
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop -->
|
||||
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop -->
|
||||
|
||||
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
|
||||
|
||||
|
|
@ -8,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
|
|||
|
||||
| | Linux | macOS | Windows |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| Chromium <!-- GEN:chromium-version -->127.0.6533.17<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| Chromium <!-- GEN:chromium-version -->127.0.6533.26<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| WebKit <!-- GEN:webkit-version -->17.4<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| Firefox <!-- GEN:firefox-version -->127.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
|
||||
|
|
|
|||
|
|
@ -357,18 +357,14 @@ await context.AddCookiesAsync(new[] { cookie1, cookie2 });
|
|||
- `cookies` <[Array]<[Object]>>
|
||||
- `name` <[string]>
|
||||
- `value` <[string]>
|
||||
- `url` ?<[string]> either url or domain / path are required. Optional.
|
||||
- `domain` ?<[string]> either url or domain / path are required Optional.
|
||||
- `path` ?<[string]> either url or domain / path are required Optional.
|
||||
- `url` ?<[string]> Either url or domain / path are required. Optional.
|
||||
- `domain` ?<[string]> 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.
|
||||
- `path` ?<[string]> Either url or domain / path are required Optional.
|
||||
- `expires` ?<[float]> Unix time in seconds. Optional.
|
||||
- `httpOnly` ?<[boolean]> Optional.
|
||||
- `secure` ?<[boolean]> Optional.
|
||||
- `sameSite` ?<[SameSiteAttribute]<"Strict"|"Lax"|"None">> Optional.
|
||||
|
||||
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".
|
||||
|
||||
## async method: BrowserContext.addInitScript
|
||||
* since: v1.8
|
||||
|
||||
|
|
@ -748,83 +744,6 @@ await page.SetContentAsync("<script>\n" +
|
|||
await page.GetByRole(AriaRole.Button).ClickAsync();
|
||||
```
|
||||
|
||||
An example of passing an element handle:
|
||||
|
||||
```js
|
||||
await context.exposeBinding('clicked', async (source, element) => {
|
||||
console.log(await element.textContent());
|
||||
}, { handle: true });
|
||||
await page.setContent(`
|
||||
<script>
|
||||
document.addEventListener('click', event => window.clicked(event.target));
|
||||
</script>
|
||||
<div>Click me</div>
|
||||
<div>Or click me</div>
|
||||
`);
|
||||
```
|
||||
|
||||
```java
|
||||
context.exposeBinding("clicked", (source, args) -> {
|
||||
ElementHandle element = (ElementHandle) args[0];
|
||||
System.out.println(element.textContent());
|
||||
return null;
|
||||
}, new BrowserContext.ExposeBindingOptions().setHandle(true));
|
||||
page.setContent("" +
|
||||
"<script>\n" +
|
||||
" document.addEventListener('click', event => window.clicked(event.target));\n" +
|
||||
"</script>\n" +
|
||||
"<div>Click me</div>\n" +
|
||||
"<div>Or click me</div>\n");
|
||||
```
|
||||
|
||||
```python async
|
||||
async def print(source, element):
|
||||
print(await element.text_content())
|
||||
|
||||
await context.expose_binding("clicked", print, handle=true)
|
||||
await page.set_content("""
|
||||
<script>
|
||||
document.addEventListener('click', event => window.clicked(event.target));
|
||||
</script>
|
||||
<div>Click me</div>
|
||||
<div>Or click me</div>
|
||||
""")
|
||||
```
|
||||
|
||||
```python sync
|
||||
def print(source, element):
|
||||
print(element.text_content())
|
||||
|
||||
context.expose_binding("clicked", print, handle=true)
|
||||
page.set_content("""
|
||||
<script>
|
||||
document.addEventListener('click', event => window.clicked(event.target));
|
||||
</script>
|
||||
<div>Click me</div>
|
||||
<div>Or click me</div>
|
||||
""")
|
||||
```
|
||||
|
||||
```csharp
|
||||
var result = new TaskCompletionSource<string>();
|
||||
var page = await Context.NewPageAsync();
|
||||
await Context.ExposeBindingAsync("clicked", async (BindingSource _, IJSHandle t) =>
|
||||
{
|
||||
return result.TrySetResult(await t.AsElement().TextContentAsync());
|
||||
});
|
||||
|
||||
await page.SetContentAsync("<script>\n" +
|
||||
" document.addEventListener('click', event => window.clicked(event.target));\n" +
|
||||
"</script>\n" +
|
||||
"<div>Click me</div>\n" +
|
||||
"<div>Or click me</div>\n");
|
||||
|
||||
await page.ClickAsync("div");
|
||||
// Note: it makes sense to await the result here, because otherwise, the context
|
||||
// gets closed and the binding function will throw an exception.
|
||||
Assert.AreEqual("Click me", await result.Task);
|
||||
```
|
||||
|
||||
### param: BrowserContext.exposeBinding.name
|
||||
* since: v1.8
|
||||
- `name` <[string]>
|
||||
|
|
@ -839,6 +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
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -1817,80 +1817,6 @@ class PageExamples
|
|||
}
|
||||
```
|
||||
|
||||
An example of passing an element handle:
|
||||
|
||||
```js
|
||||
await page.exposeBinding('clicked', async (source, element) => {
|
||||
console.log(await element.textContent());
|
||||
}, { handle: true });
|
||||
await page.setContent(`
|
||||
<script>
|
||||
document.addEventListener('click', event => window.clicked(event.target));
|
||||
</script>
|
||||
<div>Click me</div>
|
||||
<div>Or click me</div>
|
||||
`);
|
||||
```
|
||||
|
||||
```java
|
||||
page.exposeBinding("clicked", (source, args) -> {
|
||||
ElementHandle element = (ElementHandle) args[0];
|
||||
System.out.println(element.textContent());
|
||||
return null;
|
||||
}, new Page.ExposeBindingOptions().setHandle(true));
|
||||
page.setContent("" +
|
||||
"<script>\n" +
|
||||
" document.addEventListener('click', event => window.clicked(event.target));\n" +
|
||||
"</script>\n" +
|
||||
"<div>Click me</div>\n" +
|
||||
"<div>Or click me</div>\n");
|
||||
```
|
||||
|
||||
```python async
|
||||
async def print(source, element):
|
||||
print(await element.text_content())
|
||||
|
||||
await page.expose_binding("clicked", print, handle=true)
|
||||
await page.set_content("""
|
||||
<script>
|
||||
document.addEventListener('click', event => window.clicked(event.target));
|
||||
</script>
|
||||
<div>Click me</div>
|
||||
<div>Or click me</div>
|
||||
""")
|
||||
```
|
||||
|
||||
```python sync
|
||||
def print(source, element):
|
||||
print(element.text_content())
|
||||
|
||||
page.expose_binding("clicked", print, handle=true)
|
||||
page.set_content("""
|
||||
<script>
|
||||
document.addEventListener('click', event => window.clicked(event.target));
|
||||
</script>
|
||||
<div>Click me</div>
|
||||
<div>Or click me</div>
|
||||
""")
|
||||
```
|
||||
|
||||
```csharp
|
||||
var result = new TaskCompletionSource<string>();
|
||||
await page.ExposeBindingAsync("clicked", async (BindingSource _, IJSHandle t) =>
|
||||
{
|
||||
return result.TrySetResult(await t.AsElement().TextContentAsync());
|
||||
});
|
||||
|
||||
await page.SetContentAsync("<script>\n" +
|
||||
" document.addEventListener('click', event => window.clicked(event.target));\n" +
|
||||
"</script>\n" +
|
||||
"<div>Click me</div>\n" +
|
||||
"<div>Or click me</div>\n");
|
||||
|
||||
await page.ClickAsync("div");
|
||||
Console.WriteLine(await result.Task);
|
||||
```
|
||||
|
||||
### param: Page.exposeBinding.name
|
||||
* since: v1.8
|
||||
- `name` <[string]>
|
||||
|
|
@ -1905,6 +1831,7 @@ Callback function that will be called in the Playwright's context.
|
|||
|
||||
### option: Page.exposeBinding.handle
|
||||
* since: v1.8
|
||||
* deprecated: This option will be removed in the future.
|
||||
- `handle` <[boolean]>
|
||||
|
||||
Whether to pass the argument as a handle, instead of passing by value. When passing a handle, only one argument is
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -601,6 +601,23 @@ steps:
|
|||
- 'CI=true'
|
||||
```
|
||||
|
||||
### Drone
|
||||
* langs: js
|
||||
|
||||
To run Playwright tests on Drone, use our public Docker image ([see Dockerfile](./docker.md)).
|
||||
|
||||
```yml
|
||||
kind: pipeline
|
||||
name: default
|
||||
type: docker
|
||||
|
||||
steps:
|
||||
- name: test
|
||||
image: mcr.microsoft.com/playwright:v%%VERSION%%-jammy
|
||||
commands:
|
||||
- npx playwright test
|
||||
```
|
||||
|
||||
## Caching browsers
|
||||
|
||||
Caching browser binaries is not recommended, since the amount of time it takes to restore the cache is comparable to the time it takes to download the binaries. Especially under Linux, [operating system dependencies](./browsers.md#install-system-dependencies) need to be installed, which are not cacheable.
|
||||
|
|
@ -633,7 +650,7 @@ By default, Playwright launches browsers in headless mode. See in our [Running t
|
|||
On Linux agents, headed execution requires [Xvfb](https://en.wikipedia.org/wiki/Xvfb) to be installed. Our [Docker image](./docker.md) and GitHub Action have Xvfb pre-installed. To run browsers in headed mode with Xvfb, add `xvfb-run` before the actual command.
|
||||
|
||||
```bash js
|
||||
xvfb-run npx playwrght test
|
||||
xvfb-run npx playwright test
|
||||
```
|
||||
```bash python
|
||||
xvfb-run pytest
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
<LiteYouTube
|
||||
id="54_aC-rVKHg"
|
||||
title="Playwright 1.45"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -1444,14 +1444,6 @@ page.getByText("orange").click();
|
|||
await page.GetByText("orange").ClickAsync();
|
||||
```
|
||||
|
||||
```html card
|
||||
<ul>
|
||||
<li>apple</li>
|
||||
<li>banana</li>
|
||||
<li>orange</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
#### Filter by text
|
||||
Use the [`method: Locator.filter`] to locate a specific item in a list.
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,69 @@ title: "Release notes"
|
|||
toc_max_heading_level: 2
|
||||
---
|
||||
|
||||
## Version 1.45
|
||||
|
||||
### Clock
|
||||
|
||||
Utilizing the new [Clock] API allows to manipulate and control time within tests to verify time-related behavior. This API covers many common scenarios, including:
|
||||
* testing with predefined time;
|
||||
* keeping consistent time and timers;
|
||||
* monitoring inactivity;
|
||||
* ticking through time manually.
|
||||
|
||||
```csharp
|
||||
// Initialize clock with some time before the test time and let the page load naturally.
|
||||
// `Date.now` will progress as the timers fire.
|
||||
await Page.Clock.InstallAsync(new()
|
||||
{
|
||||
TimeDate = new DateTime(2024, 2, 2, 8, 0, 0)
|
||||
});
|
||||
await Page.GotoAsync("http://localhost:3333");
|
||||
|
||||
// Pretend that the user closed the laptop lid and opened it again at 10am.
|
||||
// Pause the time once reached that point.
|
||||
await Page.Clock.PauseAtAsync(new DateTime(2024, 2, 2, 10, 0, 0));
|
||||
|
||||
// Assert the page state.
|
||||
await Expect(Page.GetByTestId("current-time")).ToHaveTextAsync("2/2/2024, 10:00:00 AM");
|
||||
|
||||
// Close the laptop lid again and open it at 10:30am.
|
||||
await Page.Clock.FastForwardAsync("30:00");
|
||||
await Expect(Page.GetByTestId("current-time")).ToHaveTextAsync("2/2/2024, 10:30:00 AM");
|
||||
```
|
||||
|
||||
See [the clock guide](./clock.md) for more details.
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
- Method [`method: Locator.setInputFiles`] now supports uploading a directory for `<input type=file webkitdirectory>` elements.
|
||||
```csharp
|
||||
await page.GetByLabel("Upload directory").SetInputFilesAsync("mydir");
|
||||
```
|
||||
|
||||
- Multiple methods like [`method: Locator.click`] or [`method: Locator.press`] now support a `ControlOrMeta` modifier key. This key maps to `Meta` on macOS and maps to `Control` on Windows and Linux.
|
||||
```csharp
|
||||
// Press the common keyboard shortcut Control+S or Meta+S to trigger a "Save" operation.
|
||||
await page.Keyboard.PressAsync("ControlOrMeta+S");
|
||||
```
|
||||
|
||||
- New property `httpCredentials.send` in [`method: APIRequest.newContext`] that allows to either always send the `Authorization` header or only send it in response to `401 Unauthorized`.
|
||||
|
||||
- Playwright now supports Chromium, Firefox and WebKit on Ubuntu 24.04.
|
||||
|
||||
- v1.45 is the last release to receive WebKit update for macOS 12 Monterey. Please update macOS to keep using the latest WebKit.
|
||||
|
||||
### Browser Versions
|
||||
|
||||
* Chromium 127.0.6533.5
|
||||
* Mozilla Firefox 127.0
|
||||
* WebKit 17.4
|
||||
|
||||
This version was also tested against the following stable channels:
|
||||
|
||||
* Google Chrome 126
|
||||
* Microsoft Edge 126
|
||||
|
||||
## Version 1.44
|
||||
|
||||
### New APIs
|
||||
|
|
|
|||
|
|
@ -4,6 +4,67 @@ title: "Release notes"
|
|||
toc_max_heading_level: 2
|
||||
---
|
||||
|
||||
## Version 1.45
|
||||
|
||||
### Clock
|
||||
|
||||
Utilizing the new [Clock] API allows to manipulate and control time within tests to verify time-related behavior. This API covers many common scenarios, including:
|
||||
* testing with predefined time;
|
||||
* keeping consistent time and timers;
|
||||
* monitoring inactivity;
|
||||
* ticking through time manually.
|
||||
|
||||
```java
|
||||
// Initialize clock with some time before the test time and let the page load
|
||||
// naturally. `Date.now` will progress as the timers fire.
|
||||
page.clock().install(new Clock.InstallOptions().setTime("2024-02-02T08:00:00"));
|
||||
page.navigate("http://localhost:3333");
|
||||
Locator locator = page.getByTestId("current-time");
|
||||
|
||||
// Pretend that the user closed the laptop lid and opened it again at 10am.
|
||||
// Pause the time once reached that point.
|
||||
page.clock().pauseAt("2024-02-02T10:00:00");
|
||||
|
||||
// Assert the page state.
|
||||
assertThat(locator).hasText("2/2/2024, 10:00:00 AM");
|
||||
|
||||
// Close the laptop lid again and open it at 10:30am.
|
||||
page.clock().fastForward("30:00");
|
||||
assertThat(locator).hasText("2/2/2024, 10:30:00 AM");
|
||||
```
|
||||
|
||||
See [the clock guide](./clock.md) for more details.
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
- Method [`method: Locator.setInputFiles`] now supports uploading a directory for `<input type=file webkitdirectory>` elements.
|
||||
```java
|
||||
page.getByLabel("Upload directory").setInputFiles(Paths.get("mydir"));
|
||||
```
|
||||
|
||||
- Multiple methods like [`method: Locator.click`] or [`method: Locator.press`] now support a `ControlOrMeta` modifier key. This key maps to `Meta` on macOS and maps to `Control` on Windows and Linux.
|
||||
```java
|
||||
// Press the common keyboard shortcut Control+S or Meta+S to trigger a "Save" operation.
|
||||
page.keyboard.press("ControlOrMeta+S");
|
||||
```
|
||||
|
||||
- New property `httpCredentials.send` in [`method: APIRequest.newContext`] that allows to either always send the `Authorization` header or only send it in response to `401 Unauthorized`.
|
||||
|
||||
- Playwright now supports Chromium, Firefox and WebKit on Ubuntu 24.04.
|
||||
|
||||
- v1.45 is the last release to receive WebKit update for macOS 12 Monterey. Please update macOS to keep using the latest WebKit.
|
||||
|
||||
### Browser Versions
|
||||
|
||||
* Chromium 127.0.6533.5
|
||||
* Mozilla Firefox 127.0
|
||||
* WebKit 17.4
|
||||
|
||||
This version was also tested against the following stable channels:
|
||||
|
||||
* Google Chrome 126
|
||||
* Microsoft Edge 126
|
||||
|
||||
## Version 1.44
|
||||
|
||||
### New APIs
|
||||
|
|
|
|||
|
|
@ -8,6 +8,11 @@ import LiteYouTube from '@site/src/components/LiteYouTube';
|
|||
|
||||
## Version 1.45
|
||||
|
||||
<LiteYouTube
|
||||
id="54_aC-rVKHg"
|
||||
title="Playwright 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:
|
||||
|
|
|
|||
|
|
@ -4,6 +4,66 @@ title: "Release notes"
|
|||
toc_max_heading_level: 2
|
||||
---
|
||||
|
||||
## Version 1.45
|
||||
|
||||
### Clock
|
||||
|
||||
Utilizing the new [Clock] API allows to manipulate and control time within tests to verify time-related behavior. This API covers many common scenarios, including:
|
||||
* testing with predefined time;
|
||||
* keeping consistent time and timers;
|
||||
* monitoring inactivity;
|
||||
* ticking through time manually.
|
||||
|
||||
```python
|
||||
# Initialize clock with some time before the test time and let the page load
|
||||
# naturally. `Date.now` will progress as the timers fire.
|
||||
page.clock.install(time=datetime.datetime(2024, 2, 2, 8, 0, 0))
|
||||
page.goto("http://localhost:3333")
|
||||
|
||||
# Pretend that the user closed the laptop lid and opened it again at 10am.
|
||||
# Pause the time once reached that point.
|
||||
page.clock.pause_at(datetime.datetime(2024, 2, 2, 10, 0, 0))
|
||||
|
||||
# Assert the page state.
|
||||
expect(page.get_by_test_id("current-time")).to_have_text("2/2/2024, 10:00:00 AM")
|
||||
|
||||
# Close the laptop lid again and open it at 10:30am.
|
||||
page.clock.fast_forward("30:00")
|
||||
expect(page.get_by_test_id("current-time")).to_have_text("2/2/2024, 10:30:00 AM")
|
||||
```
|
||||
|
||||
See [the clock guide](./clock.md) for more details.
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
- Method [`method: Locator.setInputFiles`] now supports uploading a directory for `<input type=file webkitdirectory>` elements.
|
||||
```python
|
||||
page.get_by_label("Upload directory").set_input_files('mydir')
|
||||
```
|
||||
|
||||
- Multiple methods like [`method: Locator.click`] or [`method: Locator.press`] now support a `ControlOrMeta` modifier key. This key maps to `Meta` on macOS and maps to `Control` on Windows and Linux.
|
||||
```python
|
||||
# Press the common keyboard shortcut Control+S or Meta+S to trigger a "Save" operation.
|
||||
page.keyboard.press("ControlOrMeta+S")
|
||||
```
|
||||
|
||||
- New property `httpCredentials.send` in [`method: APIRequest.newContext`] that allows to either always send the `Authorization` header or only send it in response to `401 Unauthorized`.
|
||||
|
||||
- Playwright now supports Chromium, Firefox and WebKit on Ubuntu 24.04.
|
||||
|
||||
- v1.45 is the last release to receive WebKit update for macOS 12 Monterey. Please update macOS to keep using the latest WebKit.
|
||||
|
||||
### Browser Versions
|
||||
|
||||
* Chromium 127.0.6533.5
|
||||
* Mozilla Firefox 127.0
|
||||
* WebKit 17.4
|
||||
|
||||
This version was also tested against the following stable channels:
|
||||
|
||||
* Google Chrome 126
|
||||
* Microsoft Edge 126
|
||||
|
||||
## Version 1.44
|
||||
|
||||
### New APIs
|
||||
|
|
|
|||
|
|
@ -43,48 +43,7 @@ Here is how typical test environment setup differs between traditional test styl
|
|||
<summary>Click to expand the code for the <code>TodoPage</code></summary>
|
||||
<div>
|
||||
|
||||
```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:
|
|||
<summary>Click to expand the code for the <code>TodoPage</code></summary>
|
||||
<div>
|
||||
|
||||
```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 {
|
||||
|
|
@ -246,34 +164,7 @@ export class TodoPage {
|
|||
</div>
|
||||
</details>
|
||||
|
||||
```js tab=js-js title="todo.spec.js"
|
||||
const base = require('@playwright/test');
|
||||
const { TodoPage } = require('./todo-page');
|
||||
|
||||
// Extend basic test by providing a "todoPage" fixture.
|
||||
const test = base.test.extend({
|
||||
todoPage: async ({ page }, use) => {
|
||||
const todoPage = new TodoPage(page);
|
||||
await todoPage.goto();
|
||||
await todoPage.addToDo('item1');
|
||||
await todoPage.addToDo('item2');
|
||||
await use(todoPage);
|
||||
await todoPage.removeAll();
|
||||
},
|
||||
});
|
||||
|
||||
test('should add an item', async ({ todoPage }) => {
|
||||
await todoPage.addToDo('my item');
|
||||
// ...
|
||||
});
|
||||
|
||||
test('should remove an item', async ({ todoPage }) => {
|
||||
await todoPage.remove('item1');
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
```js tab=js-ts title="example.spec.ts"
|
||||
```js title="example.spec.ts"
|
||||
import { test as base } from '@playwright/test';
|
||||
import { TodoPage } from './todo-page';
|
||||
|
||||
|
|
@ -309,48 +200,8 @@ Below we create two fixtures `todoPage` and `settingsPage` that follow the [Page
|
|||
<details>
|
||||
<summary>Click to expand the code for the <code>TodoPage</code> and <code>SettingsPage</code></summary>
|
||||
<div>
|
||||
```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 {
|
||||
|
|
@ -388,22 +239,7 @@ export class TodoPage {
|
|||
|
||||
SettingsPage is similar:
|
||||
|
||||
```js tab=js-js title="settings-page.js"
|
||||
export class SettingsPage {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
constructor(page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
async switchToDarkMode() {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```js tab=js-ts title="settings-page.ts"
|
||||
```js title="settings-page.ts"
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
export class SettingsPage {
|
||||
|
|
@ -419,36 +255,7 @@ export class SettingsPage {
|
|||
</div>
|
||||
</details>
|
||||
|
||||
```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
|
|||
<summary>Click to expand the code for the <code>TodoPage</code></summary>
|
||||
<div>
|
||||
|
||||
```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 {
|
||||
|
|
@ -832,28 +505,7 @@ export class TodoPage {
|
|||
</div>
|
||||
</details>
|
||||
|
||||
```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' }],
|
||||
});
|
||||
```
|
||||
|
|
|
|||
|
|
@ -3,15 +3,15 @@
|
|||
"browsers": [
|
||||
{
|
||||
"name": "chromium",
|
||||
"revision": "1124",
|
||||
"revision": "1125",
|
||||
"installByDefault": true,
|
||||
"browserVersion": "127.0.6533.17"
|
||||
"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": "2037",
|
||||
"revision": "2041",
|
||||
"installByDefault": true,
|
||||
"revisionOverrides": {
|
||||
"mac10.14": "1446",
|
||||
|
|
|
|||
|
|
@ -85,16 +85,6 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> 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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import type { Page } from './page';
|
|||
import { ConsoleMessage } from './consoleMessage';
|
||||
import type { Env, WaitForEventOptions, Headers, BrowserContextOptions } from './types';
|
||||
import { Waiter } from './waiter';
|
||||
import { TargetClosedError } from './errors';
|
||||
import { TargetClosedError, isTargetClosedError } from './errors';
|
||||
|
||||
type ElectronOptions = Omit<channels.ElectronLaunchOptions, 'env'|'extraHTTPHeaders'|'recordHar'|'colorScheme'|'acceptDownloads'> & {
|
||||
env?: Env,
|
||||
|
|
@ -116,7 +116,13 @@ export class ElectronApplication extends ChannelOwner<channels.ElectronApplicati
|
|||
}
|
||||
|
||||
async close() {
|
||||
await this._context.close().catch(() => {});
|
||||
try {
|
||||
await this._context.close();
|
||||
} catch (e) {
|
||||
if (isTargetClosedError(e))
|
||||
return;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions = {}): Promise<any> {
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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'])),
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import type { Download } from './download';
|
|||
import type * as frames from './frames';
|
||||
import { helper } from './helper';
|
||||
import * as network from './network';
|
||||
import { InitScript } from './page';
|
||||
import type { PageDelegate } from './page';
|
||||
import { Page, PageBinding } from './page';
|
||||
import type { Progress, ProgressController } from './progress';
|
||||
|
|
@ -84,7 +85,7 @@ export abstract class BrowserContext extends SdkObject {
|
|||
private _customCloseHandler?: () => Promise<any>;
|
||||
readonly _tempDirs: string[] = [];
|
||||
private _settingStorageState = false;
|
||||
readonly initScripts: string[] = [];
|
||||
readonly initScripts: InitScript[] = [];
|
||||
private _routesInFlight = new Set<network.Route>();
|
||||
private _debugger!: Debugger;
|
||||
_closeReason: string | undefined;
|
||||
|
|
@ -266,7 +267,7 @@ export abstract class BrowserContext extends SdkObject {
|
|||
protected abstract doGrantPermissions(origin: string, permissions: string[]): Promise<void>;
|
||||
protected abstract doClearPermissions(): Promise<void>;
|
||||
protected abstract doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise<void>;
|
||||
protected abstract doAddInitScript(expression: string): Promise<void>;
|
||||
protected abstract doAddInitScript(initScript: InitScript): Promise<void>;
|
||||
protected abstract doRemoveInitScripts(): Promise<void>;
|
||||
protected abstract doExposeBinding(binding: PageBinding): Promise<void>;
|
||||
protected abstract doRemoveExposedBindings(): Promise<void>;
|
||||
|
|
@ -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<void> {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import { Browser } from '../browser';
|
|||
import { assertBrowserContextIsNotOwned, BrowserContext, verifyGeolocation } from '../browserContext';
|
||||
import { assert, createGuid } from '../../utils';
|
||||
import * as network from '../network';
|
||||
import type { PageBinding, PageDelegate, Worker } from '../page';
|
||||
import type { InitScript, PageBinding, PageDelegate, Worker } from '../page';
|
||||
import { Page } from '../page';
|
||||
import { Frame } from '../frames';
|
||||
import type { Dialog } from '../dialog';
|
||||
|
|
@ -486,9 +486,9 @@ export class CRBrowserContext extends BrowserContext {
|
|||
await (sw as CRServiceWorker).updateHttpCredentials();
|
||||
}
|
||||
|
||||
async doAddInitScript(source: string) {
|
||||
async doAddInitScript(initScript: InitScript) {
|
||||
for (const page of this.pages())
|
||||
await (page._delegate as CRPage).addInitScript(source);
|
||||
await (page._delegate as CRPage).addInitScript(initScript);
|
||||
}
|
||||
|
||||
async doRemoveInitScripts() {
|
||||
|
|
|
|||
|
|
@ -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<types.KeyboardModifier>) {
|
||||
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)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -359,11 +359,11 @@ export class CRNetworkManager {
|
|||
});
|
||||
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(headersOverride ?? headersObjectToArray(requestPausedEvent.request.headers, '\n'));
|
||||
request.request.setRawRequestHeaders(headersObjectToArray(requestPausedEvent!.request.headers, '\n'));
|
||||
}
|
||||
(this._page?._frameManager || this._serviceWorker)!.requestStarted(request.request, route || undefined);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import * as dom from '../dom';
|
|||
import * as frames from '../frames';
|
||||
import { helper } from '../helper';
|
||||
import * as network from '../network';
|
||||
import type { PageBinding, PageDelegate } from '../page';
|
||||
import type { InitScript, PageBinding, PageDelegate } from '../page';
|
||||
import { Page, Worker } from '../page';
|
||||
import type { Progress } from '../progress';
|
||||
import type * as types from '../types';
|
||||
|
|
@ -256,8 +256,8 @@ export class CRPage implements PageDelegate {
|
|||
return this._go(+1);
|
||||
}
|
||||
|
||||
async addInitScript(source: string, world: types.World = 'main'): Promise<void> {
|
||||
await this._forAllFrameSessions(frame => frame._evaluateOnNewDocument(source, world));
|
||||
async addInitScript(initScript: InitScript, world: types.World = 'main'): Promise<void> {
|
||||
await this._forAllFrameSessions(frame => frame._evaluateOnNewDocument(initScript, world));
|
||||
}
|
||||
|
||||
async removeInitScripts() {
|
||||
|
|
@ -511,15 +511,6 @@ class FrameSession {
|
|||
this._addRendererListeners();
|
||||
}
|
||||
|
||||
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
|
||||
// hence we are going to get more lifecycle updates after the actual navigation has
|
||||
// started (even if the target url is about:blank).
|
||||
lifecycleEventsEnabled.catch(e => {}).then(() => {
|
||||
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.
|
||||
|
|
@ -530,10 +521,19 @@ class FrameSession {
|
|||
});
|
||||
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 => {});
|
||||
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
|
||||
// hence we are going to get more lifecycle updates after the actual navigation has
|
||||
// started (even if the target url is about:blank).
|
||||
lifecycleEventsEnabled.catch(e => {}).then(() => {
|
||||
this._eventListeners.push(eventsHelper.addEventListener(this._client, 'Page.lifecycleEvent', event => this._onLifecycleEvent(event)));
|
||||
});
|
||||
} else {
|
||||
this._firstNonInitialNavigationCommittedFulfill();
|
||||
this._eventListeners.push(eventsHelper.addEventListener(this._client, 'Page.lifecycleEvent', event => this._onLifecycleEvent(event)));
|
||||
}
|
||||
|
|
@ -575,10 +575,10 @@ class FrameSession {
|
|||
promises.push(this._updateFileChooserInterception(true));
|
||||
for (const binding of this._crPage._page.allBindings())
|
||||
promises.push(this._initBinding(binding));
|
||||
for (const source of this._crPage._browserContext.initScripts)
|
||||
promises.push(this._evaluateOnNewDocument(source, 'main'));
|
||||
for (const source of this._crPage._page.initScripts)
|
||||
promises.push(this._evaluateOnNewDocument(source, 'main'));
|
||||
for (const initScript of this._crPage._browserContext.initScripts)
|
||||
promises.push(this._evaluateOnNewDocument(initScript, 'main'));
|
||||
for (const initScript of this._crPage._page.initScripts)
|
||||
promises.push(this._evaluateOnNewDocument(initScript, 'main'));
|
||||
if (screencastOptions)
|
||||
promises.push(this._startVideoRecording(screencastOptions));
|
||||
}
|
||||
|
|
@ -1099,9 +1099,9 @@ class FrameSession {
|
|||
await this._client.send('Page.setInterceptFileChooserDialog', { enabled }).catch(() => {}); // target can be closed.
|
||||
}
|
||||
|
||||
async _evaluateOnNewDocument(source: string, world: types.World): Promise<void> {
|
||||
async _evaluateOnNewDocument(initScript: InitScript, world: types.World): Promise<void> {
|
||||
const worldName = world === 'utility' ? UTILITY_WORLD_NAME : undefined;
|
||||
const { identifier } = await this._client.send('Page.addScriptToEvaluateOnNewDocument', { source, worldName });
|
||||
const { identifier } = await this._client.send('Page.addScriptToEvaluateOnNewDocument', { source: initScript.source, worldName });
|
||||
this._evaluateOnNewDocumentIdentifiers.push(identifier);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 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.17 Safari/537.36 Edg/127.0.6533.17",
|
||||
"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.17 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.17 Safari/537.36 Edg/127.0.6533.17",
|
||||
"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
|
||||
|
|
|
|||
|
|
@ -265,6 +265,10 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
|
|||
await this._page.touchscreen.tap(params.x, params.y, metadata);
|
||||
}
|
||||
|
||||
async touchscreenTouch(params: channels.PageTouchscreenTouchParams, metadata: CallMetadata) {
|
||||
await this._page.touchscreen.touch(params.type, params.touchPoints, metadata);
|
||||
}
|
||||
|
||||
async accessibilitySnapshot(params: channels.PageAccessibilitySnapshotParams, metadata: CallMetadata): Promise<channels.PageAccessibilitySnapshotResult> {
|
||||
const rootAXNode = await this._page.accessibility.snapshot({
|
||||
interestingOnly: params.interestingOnly,
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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<types.KeyboardModifier>) {
|
||||
throw new Error('Not implemented yet.');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import type * as frames from '../frames';
|
|||
import type { RegisteredListener } from '../../utils/eventsHelper';
|
||||
import { eventsHelper } from '../../utils/eventsHelper';
|
||||
import type { PageBinding, PageDelegate } from '../page';
|
||||
import { InitScript } from '../page';
|
||||
import { Page, Worker } from '../page';
|
||||
import type * as types from '../types';
|
||||
import { getAccessibilityTree } from './ffAccessibility';
|
||||
|
|
@ -56,7 +57,7 @@ export class FFPage implements PageDelegate {
|
|||
private _eventListeners: RegisteredListener[];
|
||||
private _workers = new Map<string, { frameId: string, session: FFSession }>();
|
||||
private _screencastId: string | undefined;
|
||||
private _initScripts: { script: string, worldName?: string }[] = [];
|
||||
private _initScripts: { initScript: InitScript, worldName?: string }[] = [];
|
||||
|
||||
constructor(session: FFSession, browserContext: FFBrowserContext, opener: FFPage | null) {
|
||||
this._session = session;
|
||||
|
|
@ -113,7 +114,7 @@ export class FFPage implements PageDelegate {
|
|||
});
|
||||
// Ideally, we somehow ensure that utility world is created before Page.ready arrives, but currently it is racy.
|
||||
// Therefore, we can end up with an initialized page without utility world, although very unlikely.
|
||||
this.addInitScript('', UTILITY_WORLD_NAME).catch(e => this._markAsError(e));
|
||||
this.addInitScript(new InitScript(''), UTILITY_WORLD_NAME).catch(e => this._markAsError(e));
|
||||
}
|
||||
|
||||
potentiallyUninitializedPage(): Page {
|
||||
|
|
@ -406,9 +407,9 @@ export class FFPage implements PageDelegate {
|
|||
return success;
|
||||
}
|
||||
|
||||
async addInitScript(script: string, worldName?: string): Promise<void> {
|
||||
this._initScripts.push({ script, worldName });
|
||||
await this._session.send('Page.setInitScripts', { scripts: this._initScripts });
|
||||
async addInitScript(initScript: InitScript, worldName?: string): Promise<void> {
|
||||
this._initScripts.push({ initScript, worldName });
|
||||
await this._session.send('Page.setInitScripts', { scripts: this._initScripts.map(s => ({ script: s.initScript.source, worldName: s.worldName })) });
|
||||
}
|
||||
|
||||
async removeInitScripts() {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -308,6 +308,7 @@ function buildLayoutClosure(layout: keyboardLayout.KeyboardLayout): Map<string,
|
|||
|
||||
export interface RawTouchscreen {
|
||||
tap(x: number, y: number, modifiers: Set<types.KeyboardModifier>): Promise<void>;
|
||||
touch(type: 'touchstart'|'touchmove'|'touchend'|'touchcancel', touchPoints: { x: number, y: number, id?: number }[], modifiers: Set<types.KeyboardModifier>): Promise<void>;
|
||||
}
|
||||
|
||||
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<number>();
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import * as accessibility from './accessibility';
|
|||
import { FileChooser } from './fileChooser';
|
||||
import type { Progress } from './progress';
|
||||
import { ProgressController } from './progress';
|
||||
import { LongStandingScope, assert, isError } from '../utils';
|
||||
import { LongStandingScope, assert, createGuid, isError } from '../utils';
|
||||
import { ManualPromise } from '../utils/manualPromise';
|
||||
import { debugLogger } from '../utils/debugLogger';
|
||||
import type { ImageComparatorOptions } from '../utils/comparators';
|
||||
|
|
@ -56,7 +56,7 @@ export interface PageDelegate {
|
|||
goForward(): Promise<boolean>;
|
||||
exposeBinding(binding: PageBinding): Promise<void>;
|
||||
removeExposedBindings(): Promise<void>;
|
||||
addInitScript(source: string): Promise<void>;
|
||||
addInitScript(initScript: InitScript): Promise<void>;
|
||||
removeInitScripts(): Promise<void>;
|
||||
closePage(runBeforeUnload: boolean): Promise<void>;
|
||||
potentiallyUninitializedPage(): Page;
|
||||
|
|
@ -154,7 +154,7 @@ export class Page extends SdkObject {
|
|||
private _emulatedMedia: Partial<EmulatedMedia> = {};
|
||||
private _interceptFileChooser = false;
|
||||
private readonly _pageBindings = new Map<string, PageBinding>();
|
||||
readonly initScripts: string[] = [];
|
||||
readonly initScripts: InitScript[] = [];
|
||||
readonly _screenshotter: Screenshotter;
|
||||
readonly _frameManager: frames.FrameManager;
|
||||
readonly accessibility: accessibility.Accessibility;
|
||||
|
|
@ -527,8 +527,9 @@ export class Page extends SdkObject {
|
|||
}
|
||||
|
||||
async addInitScript(source: string) {
|
||||
this.initScripts.push(source);
|
||||
await this._delegate.addInitScript(source);
|
||||
const initScript = new InitScript(source);
|
||||
this.initScripts.push(initScript);
|
||||
await this._delegate.addInitScript(initScript);
|
||||
}
|
||||
|
||||
async _removeInitScripts() {
|
||||
|
|
@ -905,6 +906,22 @@ function addPageBinding(bindingName: string, needsHandle: boolean, utilityScript
|
|||
(globalThis as any)[bindingName].__installed = true;
|
||||
}
|
||||
|
||||
export class InitScript {
|
||||
readonly source: string;
|
||||
|
||||
constructor(source: string) {
|
||||
const guid = createGuid();
|
||||
this.source = `(() => {
|
||||
globalThis.__pwInitScripts = globalThis.__pwInitScripts || {};
|
||||
const hasInitScript = globalThis.__pwInitScripts[${JSON.stringify(guid)}];
|
||||
if (hasInitScript)
|
||||
return;
|
||||
globalThis.__pwInitScripts[${JSON.stringify(guid)}] = true;
|
||||
${source}
|
||||
})();`;
|
||||
}
|
||||
}
|
||||
|
||||
class FrameThrottler {
|
||||
private _acks: (() => void)[] = [];
|
||||
private _defaultInterval: number;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<types.KeyboardModifier>) {
|
||||
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)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import { eventsHelper } from '../../utils/eventsHelper';
|
|||
import { helper } from '../helper';
|
||||
import type { JSHandle } from '../javascript';
|
||||
import * as network from '../network';
|
||||
import type { PageBinding, PageDelegate } from '../page';
|
||||
import type { InitScript, PageBinding, PageDelegate } from '../page';
|
||||
import { Page } from '../page';
|
||||
import type { Progress } from '../progress';
|
||||
import type * as types from '../types';
|
||||
|
|
@ -777,7 +777,7 @@ export class WKPage implements PageDelegate {
|
|||
await this._updateBootstrapScript();
|
||||
}
|
||||
|
||||
async addInitScript(script: string): Promise<void> {
|
||||
async addInitScript(initScript: InitScript): Promise<void> {
|
||||
await this._updateBootstrapScript();
|
||||
}
|
||||
|
||||
|
|
@ -797,8 +797,8 @@ export class WKPage implements PageDelegate {
|
|||
|
||||
for (const binding of this._page.allBindings())
|
||||
scripts.push(binding.source);
|
||||
scripts.push(...this._browserContext.initScripts);
|
||||
scripts.push(...this._page.initScripts);
|
||||
scripts.push(...this._browserContext.initScripts.map(s => s.source));
|
||||
scripts.push(...this._page.initScripts.map(s => s.source));
|
||||
return scripts.join(';\n');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<net.Socket> {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
|
|||
94
packages/playwright-core/types/types.d.ts
vendored
94
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -814,21 +814,6 @@ export interface Page {
|
|||
* })();
|
||||
* ```
|
||||
*
|
||||
* An example of passing an element handle:
|
||||
*
|
||||
* ```js
|
||||
* await page.exposeBinding('clicked', async (source, element) => {
|
||||
* console.log(await element.textContent());
|
||||
* }, { handle: true });
|
||||
* await page.setContent(`
|
||||
* <script>
|
||||
* document.addEventListener('click', event => window.clicked(event.target));
|
||||
* </script>
|
||||
* <div>Click me</div>
|
||||
* <div>Or click me</div>
|
||||
* `);
|
||||
* ```
|
||||
*
|
||||
* @param name Name of the function on the window object.
|
||||
* @param callback Callback function that will be called in the Playwright's context.
|
||||
* @param options
|
||||
|
|
@ -875,21 +860,6 @@ export interface Page {
|
|||
* })();
|
||||
* ```
|
||||
*
|
||||
* An example of passing an element handle:
|
||||
*
|
||||
* ```js
|
||||
* await page.exposeBinding('clicked', async (source, element) => {
|
||||
* console.log(await element.textContent());
|
||||
* }, { handle: true });
|
||||
* await page.setContent(`
|
||||
* <script>
|
||||
* document.addEventListener('click', event => window.clicked(event.target));
|
||||
* </script>
|
||||
* <div>Click me</div>
|
||||
* <div>Or click me</div>
|
||||
* `);
|
||||
* ```
|
||||
*
|
||||
* @param name Name of the function on the window object.
|
||||
* @param callback Callback function that will be called in the Playwright's context.
|
||||
* @param options
|
||||
|
|
@ -7637,21 +7607,6 @@ export interface BrowserContext {
|
|||
* })();
|
||||
* ```
|
||||
*
|
||||
* An example of passing an element handle:
|
||||
*
|
||||
* ```js
|
||||
* await context.exposeBinding('clicked', async (source, element) => {
|
||||
* console.log(await element.textContent());
|
||||
* }, { handle: true });
|
||||
* await page.setContent(`
|
||||
* <script>
|
||||
* document.addEventListener('click', event => window.clicked(event.target));
|
||||
* </script>
|
||||
* <div>Click me</div>
|
||||
* <div>Or click me</div>
|
||||
* `);
|
||||
* ```
|
||||
*
|
||||
* @param name Name of the function on the window object.
|
||||
* @param callback Callback function that will be called in the Playwright's context.
|
||||
* @param options
|
||||
|
|
@ -7693,21 +7648,6 @@ export interface BrowserContext {
|
|||
* })();
|
||||
* ```
|
||||
*
|
||||
* An example of passing an element handle:
|
||||
*
|
||||
* ```js
|
||||
* await context.exposeBinding('clicked', async (source, element) => {
|
||||
* console.log(await element.textContent());
|
||||
* }, { handle: true });
|
||||
* await page.setContent(`
|
||||
* <script>
|
||||
* document.addEventListener('click', event => window.clicked(event.target));
|
||||
* </script>
|
||||
* <div>Click me</div>
|
||||
* <div>Or click me</div>
|
||||
* `);
|
||||
* ```
|
||||
*
|
||||
* @param name Name of the function on the window object.
|
||||
* @param callback Callback function that will be called in the Playwright's context.
|
||||
* @param options
|
||||
|
|
@ -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<void>;
|
||||
|
||||
/**
|
||||
* 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<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@
|
|||
"./hooks": {
|
||||
"types": "./hooks.d.ts",
|
||||
"default": "./hooks.mjs"
|
||||
}
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.46.0-next",
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@
|
|||
"./hooks": {
|
||||
"types": "./hooks.d.ts",
|
||||
"default": "./hooks.mjs"
|
||||
}
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.46.0-next",
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@
|
|||
"./hooks": {
|
||||
"types": "./hooks.d.ts",
|
||||
"default": "./hooks.mjs"
|
||||
}
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.46.0-next",
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@
|
|||
"./hooks": {
|
||||
"types": "./hooks.d.ts",
|
||||
"default": "./hooks.mjs"
|
||||
}
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.46.0-next",
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@
|
|||
"./hooks": {
|
||||
"types": "./hooks.d.ts",
|
||||
"default": "./hooks.mjs"
|
||||
}
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.46.0-next",
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@
|
|||
"./hooks": {
|
||||
"types": "./hooks.d.ts",
|
||||
"default": "./hooks.mjs"
|
||||
}
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.46.0-next",
|
||||
|
|
|
|||
|
|
@ -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<FixtureRegistration, 'visiting' | 'visited'>();
|
||||
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) : '<builtin>';
|
||||
}
|
||||
|
||||
export function fixtureParameterNames(fn: Function | any, location: Location, onError: LoadErrorSink): string[] {
|
||||
if (typeof fn !== 'function')
|
||||
return [];
|
||||
|
|
|
|||
|
|
@ -59,13 +59,13 @@ type WorkerFixtures = PlaywrightWorkerArgs & PlaywrightWorkerOptions & {
|
|||
const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
||||
defaultBrowserType: ['chromium', { scope: 'worker', option: true }],
|
||||
browserName: [({ defaultBrowserType }, use) => use(defaultBrowserType), { scope: 'worker', option: true }],
|
||||
_playwrightImpl: [({}, use) => use(require('playwright-core')), { scope: 'worker' }],
|
||||
_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<TestFixtures, WorkerFixtures> = ({
|
|||
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<TestFixtures, WorkerFixtures> = ({
|
|||
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<TestFixtures, WorkerFixtures> = ({
|
|||
...contextOptions,
|
||||
...options,
|
||||
});
|
||||
},
|
||||
}, { box: true }],
|
||||
|
||||
_setupContextOptions: [async ({ playwright, _combinedContextOptions, actionTimeout, navigationTimeout, testIdAttribute }, use, testInfo) => {
|
||||
if (testIdAttribute)
|
||||
|
|
@ -246,9 +246,9 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
|||
(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<TestFixtures, WorkerFixtures> = ({
|
|||
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<TestFixtures, WorkerFixtures> = ({
|
|||
}
|
||||
}));
|
||||
|
||||
}, { 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<TestFixtures, WorkerFixtures> = ({
|
|||
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);
|
||||
|
|
@ -597,7 +609,7 @@ class ArtifactsRecorder {
|
|||
if ((tracing as any)[this._startedCollectingArtifacts])
|
||||
return;
|
||||
(tracing as any)[this._startedCollectingArtifacts] = true;
|
||||
if (this._testInfo._tracing.traceOptions())
|
||||
if (this._testInfo._tracing.traceOptions() && (tracing as any)[kTracingStarted])
|
||||
await tracing.stopChunk({ path: this._testInfo._tracing.generateNextTraceRecordingPath() });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
8
packages/playwright/types/test.d.ts
vendored
8
packages/playwright/types/test.d.ts
vendored
|
|
@ -4824,13 +4824,13 @@ export type WorkerFixture<R, Args extends KeyValue> = (args: Args, use: (r: R) =
|
|||
type TestFixtureValue<R, Args extends KeyValue> = Exclude<R, Function> | TestFixture<R, Args>;
|
||||
type WorkerFixtureValue<R, Args extends KeyValue> = Exclude<R, Function> | WorkerFixture<R, Args>;
|
||||
export type Fixtures<T extends KeyValue = {}, W extends KeyValue = {}, PT extends KeyValue = {}, PW extends KeyValue = {}> = {
|
||||
[K in keyof PW]?: WorkerFixtureValue<PW[K], W & PW> | [WorkerFixtureValue<PW[K], W & PW>, { scope: 'worker', timeout?: number | undefined }];
|
||||
[K in keyof PW]?: WorkerFixtureValue<PW[K], W & PW> | [WorkerFixtureValue<PW[K], W & PW>, { scope: 'worker', timeout?: number | undefined, title?: string, box?: boolean }];
|
||||
} & {
|
||||
[K in keyof PT]?: TestFixtureValue<PT[K], T & W & PT & PW> | [TestFixtureValue<PT[K], T & W & PT & PW>, { scope: 'test', timeout?: number | undefined }];
|
||||
[K in keyof PT]?: TestFixtureValue<PT[K], T & W & PT & PW> | [TestFixtureValue<PT[K], T & W & PT & PW>, { scope: 'test', timeout?: number | undefined, title?: string, box?: boolean }];
|
||||
} & {
|
||||
[K in keyof W]?: [WorkerFixtureValue<W[K], W & PW>, { scope: 'worker', auto?: boolean, option?: boolean, timeout?: number | undefined }];
|
||||
[K in keyof W]?: [WorkerFixtureValue<W[K], W & PW>, { scope: 'worker', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }];
|
||||
} & {
|
||||
[K in keyof T]?: TestFixtureValue<T[K], T & W & PT & PW> | [TestFixtureValue<T[K], T & W & PT & PW>, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined }];
|
||||
[K in keyof T]?: TestFixtureValue<T[K], T & W & PT & PW> | [TestFixtureValue<T[K], T & W & PT & PW>, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }];
|
||||
};
|
||||
|
||||
type BrowserName = 'chromium' | 'firefox' | 'webkit';
|
||||
|
|
|
|||
|
|
@ -1890,6 +1890,7 @@ export interface PageChannel extends PageEventTarget, EventTargetChannel {
|
|||
mouseClick(params: PageMouseClickParams, metadata?: CallMetadata): Promise<PageMouseClickResult>;
|
||||
mouseWheel(params: PageMouseWheelParams, metadata?: CallMetadata): Promise<PageMouseWheelResult>;
|
||||
touchscreenTap(params: PageTouchscreenTapParams, metadata?: CallMetadata): Promise<PageTouchscreenTapResult>;
|
||||
touchscreenTouch(params: PageTouchscreenTouchParams, metadata?: CallMetadata): Promise<PageTouchscreenTouchResult>;
|
||||
accessibilitySnapshot(params: PageAccessibilitySnapshotParams, metadata?: CallMetadata): Promise<PageAccessibilitySnapshotResult>;
|
||||
pdf(params: PagePdfParams, metadata?: CallMetadata): Promise<PagePdfResult>;
|
||||
startJSCoverage(params: PageStartJSCoverageParams, metadata?: CallMetadata): Promise<PageStartJSCoverageResult>;
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
27
packages/trace-viewer/embedded.html
Normal file
27
packages/trace-viewer/embedded.html
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<!--
|
||||
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.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Playwright Trace Viewer for VS Code</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/embedded.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
61
packages/trace-viewer/src/embedded.tsx
Normal file
61
packages/trace-viewer/src/embedded.tsx
Normal file
|
|
@ -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<void>(f => {
|
||||
navigator.serviceWorker.oncontrollerchange = () => f();
|
||||
});
|
||||
}
|
||||
|
||||
// Keep SW running.
|
||||
setInterval(function() { fetch('ping'); }, 10000);
|
||||
}
|
||||
|
||||
ReactDOM.render(<EmbeddedWorkbenchLoader />, document.querySelector('#root'));
|
||||
})();
|
||||
68
packages/trace-viewer/src/ui/embeddedWorkbenchLoader.css
Normal file
68
packages/trace-viewer/src/ui/embeddedWorkbenchLoader.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
96
packages/trace-viewer/src/ui/embeddedWorkbenchLoader.tsx
Normal file
96
packages/trace-viewer/src/ui/embeddedWorkbenchLoader.tsx
Normal file
|
|
@ -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<string[]>([]);
|
||||
const [model, setModel] = React.useState<MultiTraceModel>(emptyModel);
|
||||
const [progress, setProgress] = React.useState<{ done: number, total: number }>({ done: 0, total: 0 });
|
||||
const [processingErrorMessage, setProcessingErrorMessage] = React.useState<string | null>(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 <div className='vbox workbench-loader'>
|
||||
<div className='progress'>
|
||||
<div className='inner-progress' style={{ width: progress.total ? (100 * progress.done / progress.total) + '%' : 0 }}></div>
|
||||
</div>
|
||||
<Workbench model={model} openPage={openPage} />
|
||||
{!traceURLs.length && <div className='empty-state'>
|
||||
<div className='title'>Select test to see the trace</div>
|
||||
</div>}
|
||||
</div>;
|
||||
};
|
||||
|
||||
export const emptyModel = new MultiTraceModel([]);
|
||||
|
|
@ -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<HTMLDivElement>();
|
||||
const [snapshotTab, setSnapshotTab] = React.useState<'action'|'before'|'after'>('action');
|
||||
|
||||
|
|
@ -190,7 +191,9 @@ export const SnapshotTab: React.FunctionComponent<{
|
|||
})}
|
||||
<div style={{ flex: 'auto' }}></div>
|
||||
<ToolbarButton icon='link-external' title='Open snapshot in a new tab' disabled={!popoutUrl} onClick={() => {
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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<ActionTraceEventInContext | undefined>(undefined);
|
||||
const [revealedStack, setRevealedStack] = React.useState<StackFrame[] | undefined>(undefined);
|
||||
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEventInContext | undefined>();
|
||||
|
|
@ -234,7 +235,8 @@ export const Workbench: React.FunctionComponent<{
|
|||
isInspecting={isInspecting}
|
||||
setIsInspecting={setIsInspecting}
|
||||
highlightedLocator={highlightedLocator}
|
||||
setHighlightedLocator={locatorPicked} />
|
||||
setHighlightedLocator={locatorPicked}
|
||||
openPage={openPage} />
|
||||
<TabbedPane
|
||||
tabs={[
|
||||
{
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ export default defineConfig({
|
|||
input: {
|
||||
index: path.resolve(__dirname, 'index.html'),
|
||||
uiMode: path.resolve(__dirname, 'uiMode.html'),
|
||||
embedded: path.resolve(__dirname, 'embedded.html'),
|
||||
snapshot: path.resolve(__dirname, 'snapshot.html'),
|
||||
},
|
||||
output: {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
import { test } from './npmTest';
|
||||
import fs from 'fs';
|
||||
import { expect } from 'packages/playwright-test';
|
||||
import path from 'path';
|
||||
|
||||
test('electron should work', async ({ exec, tsc, writeFiles }) => {
|
||||
|
|
@ -39,3 +40,46 @@ test('electron should work with special characters in path', async ({ exec, tmpW
|
|||
cwd: path.join(folderName)
|
||||
});
|
||||
});
|
||||
|
||||
test('should work when wrapped inside @playwright/test and trace is enabled', async ({ exec, tmpWorkspace, writeFiles }) => {
|
||||
await exec('npm i -D @playwright/test electron@31');
|
||||
await writeFiles({
|
||||
'electron-with-tracing.spec.ts': `
|
||||
import { test, expect, _electron } from '@playwright/test';
|
||||
|
||||
test('should work', async ({ trace }) => {
|
||||
const electronApp = await _electron.launch({ args: [${JSON.stringify(path.join(__dirname, '../electron/electron-window-app.js'))}] });
|
||||
|
||||
const window = await electronApp.firstWindow();
|
||||
if (trace)
|
||||
await window.context().tracing.start({ screenshots: true, snapshots: true });
|
||||
|
||||
await window.goto('data:text/html,<title>Playwright</title><h1>Playwright</h1>');
|
||||
await expect(window).toHaveTitle(/Playwright/);
|
||||
await expect(window.getByRole('heading')).toHaveText('Playwright');
|
||||
|
||||
const path = test.info().outputPath('electron-trace.zip');
|
||||
if (trace) {
|
||||
await window.context().tracing.stop({ path });
|
||||
test.info().attachments.push({ name: 'trace', path, contentType: 'application/zip' });
|
||||
}
|
||||
await electronApp.close();
|
||||
});
|
||||
`,
|
||||
});
|
||||
const jsonOutputName = test.info().outputPath('report.json');
|
||||
await exec('npx playwright test --trace=on --reporter=json electron-with-tracing.spec.ts', {
|
||||
env: { PLAYWRIGHT_JSON_OUTPUT_NAME: jsonOutputName }
|
||||
});
|
||||
const traces = [
|
||||
// our actual trace.
|
||||
path.join(tmpWorkspace, 'test-results', 'electron-with-tracing-should-work', 'electron-trace.zip'),
|
||||
// contains the expect() calls
|
||||
path.join(tmpWorkspace, 'test-results', 'electron-with-tracing-should-work', 'trace.zip'),
|
||||
];
|
||||
for (const trace of traces)
|
||||
expect(fs.existsSync(trace)).toBe(true);
|
||||
const report = JSON.parse(fs.readFileSync(jsonOutputName, 'utf-8'));
|
||||
expect(new Set(['trace'])).toEqual(new Set(report.suites[0].specs[0].tests[0].results[0].attachments.map(a => a.name)));
|
||||
expect(new Set(traces.map(p => fs.realpathSync(p)))).toEqual(new Set(report.suites[0].specs[0].tests[0].results[0].attachments.map(a => a.path)));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 }) {
|
||||
|
|
|
|||
|
|
@ -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')),
|
||||
// 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.
|
||||
new Promise(r => setTimeout(r, 1000))
|
||||
]);
|
||||
await page.waitForTimeout(3_000);
|
||||
});
|
||||
|
||||
it('should not crash on storage.getDirectory()', async ({ page, server, browserName, isMac }) => {
|
||||
|
|
|
|||
68
tests/library/chromium/disable-web-security.spec.ts
Normal file
68
tests/library/chromium/disable-web-security.spec.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
/**
|
||||
* Copyright 2018 Google Inc. All rights reserved.
|
||||
* Modifications copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { contextTest as it, expect } from '../../config/browserTest';
|
||||
|
||||
it.use({
|
||||
launchOptions: async ({ launchOptions }, use) => {
|
||||
await use({ ...launchOptions, args: ['--disable-web-security'] });
|
||||
}
|
||||
});
|
||||
|
||||
it('test utility world in popup w/ --disable-web-security', async ({ page, server }) => {
|
||||
server.setRoute('/main.html', (req, res) => {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/html'
|
||||
});
|
||||
res.end(`<a href="${server.PREFIX}/target.html" target="_blank">Click me</a>`);
|
||||
});
|
||||
server.setRoute('/target.html', (req, res) => {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/html'
|
||||
});
|
||||
res.end(`<html></html>`);
|
||||
});
|
||||
|
||||
await page.goto(server.PREFIX + '/main.html');
|
||||
const page1Promise = page.context().waitForEvent('page');
|
||||
await page.getByRole('link', { name: 'Click me' }).click();
|
||||
const page1 = await page1Promise;
|
||||
await expect(page1).toHaveURL(/target/);
|
||||
});
|
||||
|
||||
it('test init script w/ --disable-web-security', async ({ page, server }) => {
|
||||
server.setRoute('/main.html', (req, res) => {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/html'
|
||||
});
|
||||
res.end(`<a href="${server.PREFIX}/target.html" target="_blank">Click me</a>`);
|
||||
});
|
||||
server.setRoute('/target.html', (req, res) => {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/html'
|
||||
});
|
||||
res.end(`<html></html>`);
|
||||
});
|
||||
|
||||
await page.context().addInitScript('window.injected = 123');
|
||||
await page.goto(server.PREFIX + '/main.html');
|
||||
const page1Promise = page.context().waitForEvent('page');
|
||||
await page.getByRole('link', { name: 'Click me' }).click();
|
||||
const page1 = await page1Promise;
|
||||
const value = await page1.evaluate('window.injected');
|
||||
expect(value).toBe(123);
|
||||
});
|
||||
|
|
@ -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(`
|
||||
<select id=first><option value=foo1>Foo1</option><option value=bar1>Bar1</option></select>
|
||||
<select id=second disabled><option value=foo2>Foo2</option><option value=bar2 selected>Bar2</option></select>
|
||||
`);
|
||||
|
||||
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();
|
||||
|
||||
|
|
|
|||
76
tests/library/touch.spec.ts
Normal file
76
tests/library/touch.spec.ts
Normal file
|
|
@ -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(`<div id="a" style="background: lightblue; width: 200px; height: 200px">a</div>`);
|
||||
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 };
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
*/
|
||||
|
||||
import { test as it, expect } from './pageTest';
|
||||
import { attachFrame, chromiumVersionLessThan } from '../config/utils';
|
||||
import { attachFrame } from '../config/utils';
|
||||
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
|
@ -37,7 +37,7 @@ it('should upload the file', async ({ page, server, asset }) => {
|
|||
}, input)).toBe('contents of the file');
|
||||
});
|
||||
|
||||
it('should upload a folder', async ({ page, server, browserName, headless, browserVersion, isAndroid }) => {
|
||||
it('should upload a folder', async ({ page, server, browserName, headless, browserMajorVersion, isAndroid }) => {
|
||||
it.skip(isAndroid);
|
||||
it.skip(os.platform() === 'darwin' && parseInt(os.release().split('.')[0], 10) <= 21, 'WebKit on macOS-12 is frozen');
|
||||
|
||||
|
|
@ -54,7 +54,7 @@ it('should upload a folder', async ({ page, server, browserName, headless, brows
|
|||
await input.setInputFiles(dir);
|
||||
expect(new Set(await page.evaluate(e => [...e.files].map(f => f.webkitRelativePath), input))).toEqual(new Set([
|
||||
// https://issues.chromium.org/issues/345393164
|
||||
...((browserName === 'chromium' && headless && !process.env.PLAYWRIGHT_CHROMIUM_USE_HEADLESS_NEW && chromiumVersionLessThan(browserVersion, '127.0.6533.0')) ? [] : ['file-upload-test/sub-dir/really.txt']),
|
||||
...((browserName === 'chromium' && headless && !process.env.PLAYWRIGHT_CHROMIUM_USE_HEADLESS_NEW && browserMajorVersion < 127) ? [] : ['file-upload-test/sub-dir/really.txt']),
|
||||
'file-upload-test/file1.txt',
|
||||
'file-upload-test/file2',
|
||||
]));
|
||||
|
|
|
|||
|
|
@ -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: <builtin> -> x.spec.ts:3:25 -> <builtin>');
|
||||
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': `
|
||||
|
|
|
|||
|
|
@ -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 }} };
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
8
utils/generate_types/overrides-test.d.ts
vendored
8
utils/generate_types/overrides-test.d.ts
vendored
|
|
@ -140,13 +140,13 @@ export type WorkerFixture<R, Args extends KeyValue> = (args: Args, use: (r: R) =
|
|||
type TestFixtureValue<R, Args extends KeyValue> = Exclude<R, Function> | TestFixture<R, Args>;
|
||||
type WorkerFixtureValue<R, Args extends KeyValue> = Exclude<R, Function> | WorkerFixture<R, Args>;
|
||||
export type Fixtures<T extends KeyValue = {}, W extends KeyValue = {}, PT extends KeyValue = {}, PW extends KeyValue = {}> = {
|
||||
[K in keyof PW]?: WorkerFixtureValue<PW[K], W & PW> | [WorkerFixtureValue<PW[K], W & PW>, { scope: 'worker', timeout?: number | undefined }];
|
||||
[K in keyof PW]?: WorkerFixtureValue<PW[K], W & PW> | [WorkerFixtureValue<PW[K], W & PW>, { scope: 'worker', timeout?: number | undefined, title?: string, box?: boolean }];
|
||||
} & {
|
||||
[K in keyof PT]?: TestFixtureValue<PT[K], T & W & PT & PW> | [TestFixtureValue<PT[K], T & W & PT & PW>, { scope: 'test', timeout?: number | undefined }];
|
||||
[K in keyof PT]?: TestFixtureValue<PT[K], T & W & PT & PW> | [TestFixtureValue<PT[K], T & W & PT & PW>, { scope: 'test', timeout?: number | undefined, title?: string, box?: boolean }];
|
||||
} & {
|
||||
[K in keyof W]?: [WorkerFixtureValue<W[K], W & PW>, { scope: 'worker', auto?: boolean, option?: boolean, timeout?: number | undefined }];
|
||||
[K in keyof W]?: [WorkerFixtureValue<W[K], W & PW>, { scope: 'worker', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }];
|
||||
} & {
|
||||
[K in keyof T]?: TestFixtureValue<T[K], T & W & PT & PW> | [TestFixtureValue<T[K], T & W & PT & PW>, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined }];
|
||||
[K in keyof T]?: TestFixtureValue<T[K], T & W & PT & PW> | [TestFixtureValue<T[K], T & W & PT & PW>, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }];
|
||||
};
|
||||
|
||||
type BrowserName = 'chromium' | 'firefox' | 'webkit';
|
||||
|
|
|
|||
Loading…
Reference in a new issue