Merge branch 'main' into fix-iframe-locator-generator
This commit is contained in:
commit
6be03bd02f
|
|
@ -8,14 +8,11 @@ test/assets/modernizr.js
|
||||||
/packages/playwright-ct-core/src/generated/*
|
/packages/playwright-ct-core/src/generated/*
|
||||||
/index.d.ts
|
/index.d.ts
|
||||||
node_modules/
|
node_modules/
|
||||||
browser_patches/*/checkout/
|
|
||||||
browser_patches/chromium/output/
|
|
||||||
**/*.d.ts
|
**/*.d.ts
|
||||||
output/
|
output/
|
||||||
test-results/
|
test-results/
|
||||||
tests/components/
|
/tests/components/
|
||||||
tests/installation/fixture-scripts/
|
/tests/installation/fixture-scripts/
|
||||||
examples/
|
|
||||||
DEPS
|
DEPS
|
||||||
.cache/
|
.cache/
|
||||||
utils/
|
/utils/
|
||||||
|
|
|
||||||
|
|
@ -3691,8 +3691,8 @@ await page.routeWebSocket('/ws', ws => {
|
||||||
|
|
||||||
```java
|
```java
|
||||||
page.routeWebSocket("/ws", ws -> {
|
page.routeWebSocket("/ws", ws -> {
|
||||||
ws.onMessage(message -> {
|
ws.onMessage(frame -> {
|
||||||
if ("request".equals(message))
|
if ("request".equals(frame.text()))
|
||||||
ws.send("response");
|
ws.send("response");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -3722,8 +3722,8 @@ page.route_web_socket("/ws", handler)
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
await page.RouteWebSocketAsync("/ws", ws => {
|
await page.RouteWebSocketAsync("/ws", ws => {
|
||||||
ws.OnMessage(message => {
|
ws.OnMessage(frame => {
|
||||||
if (message == "request")
|
if (frame.Text == "request")
|
||||||
ws.Send("response");
|
ws.Send("response");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ await page.routeWebSocket('wss://example.com/ws', ws => {
|
||||||
|
|
||||||
```java
|
```java
|
||||||
page.routeWebSocket("wss://example.com/ws", ws -> {
|
page.routeWebSocket("wss://example.com/ws", ws -> {
|
||||||
ws.onMessage(message -> {
|
ws.onMessage(frame -> {
|
||||||
if ("request".equals(message))
|
if ("request".equals(frame.text()))
|
||||||
ws.send("response");
|
ws.send("response");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -47,8 +47,8 @@ page.route_web_socket("wss://example.com/ws", lambda ws: ws.on_message(
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
await page.RouteWebSocketAsync("wss://example.com/ws", ws => {
|
await page.RouteWebSocketAsync("wss://example.com/ws", ws => {
|
||||||
ws.OnMessage(message => {
|
ws.OnMessage(frame => {
|
||||||
if (message == "request")
|
if (frame.Text == "request")
|
||||||
ws.Send("response");
|
ws.Send("response");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -70,8 +70,8 @@ await page.routeWebSocket('wss://example.com/ws', ws => {
|
||||||
|
|
||||||
```java
|
```java
|
||||||
page.routeWebSocket("wss://example.com/ws", ws -> {
|
page.routeWebSocket("wss://example.com/ws", ws -> {
|
||||||
ws.onMessage(message -> {
|
ws.onMessage(frame -> {
|
||||||
JsonObject json = new JsonParser().parse(message).getAsJsonObject();
|
JsonObject json = new JsonParser().parse(frame.text()).getAsJsonObject();
|
||||||
if ("question".equals(json.get("request").getAsString())) {
|
if ("question".equals(json.get("request").getAsString())) {
|
||||||
Map<String, String> result = new HashMap();
|
Map<String, String> result = new HashMap();
|
||||||
result.put("response", "answer");
|
result.put("response", "answer");
|
||||||
|
|
@ -105,8 +105,8 @@ page.route_web_socket("wss://example.com/ws", lambda ws: ws.on_message(
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
await page.RouteWebSocketAsync("wss://example.com/ws", ws => {
|
await page.RouteWebSocketAsync("wss://example.com/ws", ws => {
|
||||||
ws.OnMessage(message => {
|
ws.OnMessage(frame => {
|
||||||
using var jsonDoc = JsonDocument.Parse(message);
|
using var jsonDoc = JsonDocument.Parse(frame.Text);
|
||||||
JsonElement root = jsonDoc.RootElement;
|
JsonElement root = jsonDoc.RootElement;
|
||||||
if (root.TryGetProperty("request", out JsonElement requestElement) && requestElement.GetString() == "question")
|
if (root.TryGetProperty("request", out JsonElement requestElement) && requestElement.GetString() == "question")
|
||||||
{
|
{
|
||||||
|
|
@ -140,11 +140,11 @@ await page.routeWebSocket('/ws', ws => {
|
||||||
```java
|
```java
|
||||||
page.routeWebSocket("/ws", ws -> {
|
page.routeWebSocket("/ws", ws -> {
|
||||||
WebSocketRoute server = ws.connectToServer();
|
WebSocketRoute server = ws.connectToServer();
|
||||||
ws.onMessage(message -> {
|
ws.onMessage(frame -> {
|
||||||
if ("request".equals(message))
|
if ("request".equals(frame.text()))
|
||||||
server.send("request2");
|
server.send("request2");
|
||||||
else
|
else
|
||||||
server.send(message);
|
server.send(frame.text());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
@ -180,11 +180,11 @@ page.route_web_socket("/ws", handler)
|
||||||
```csharp
|
```csharp
|
||||||
await page.RouteWebSocketAsync("/ws", ws => {
|
await page.RouteWebSocketAsync("/ws", ws => {
|
||||||
var server = ws.ConnectToServer();
|
var server = ws.ConnectToServer();
|
||||||
ws.OnMessage(message => {
|
ws.OnMessage(frame => {
|
||||||
if (message == "request")
|
if (frame.Text == "request")
|
||||||
server.Send("request2");
|
server.Send("request2");
|
||||||
else
|
else
|
||||||
server.Send(message);
|
server.Send(frame.Text);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
@ -215,13 +215,13 @@ await page.routeWebSocket('/ws', ws => {
|
||||||
```java
|
```java
|
||||||
page.routeWebSocket("/ws", ws -> {
|
page.routeWebSocket("/ws", ws -> {
|
||||||
WebSocketRoute server = ws.connectToServer();
|
WebSocketRoute server = ws.connectToServer();
|
||||||
ws.onMessage(message -> {
|
ws.onMessage(frame -> {
|
||||||
if (!"blocked-from-the-page".equals(message))
|
if (!"blocked-from-the-page".equals(frame.text()))
|
||||||
server.send(message);
|
server.send(frame.text());
|
||||||
});
|
});
|
||||||
server.onMessage(message -> {
|
server.onMessage(frame -> {
|
||||||
if (!"blocked-from-the-server".equals(message))
|
if (!"blocked-from-the-server".equals(frame.text()))
|
||||||
ws.send(message);
|
ws.send(frame.text());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
@ -263,13 +263,13 @@ page.route_web_socket("/ws", handler)
|
||||||
```csharp
|
```csharp
|
||||||
await page.RouteWebSocketAsync("/ws", ws => {
|
await page.RouteWebSocketAsync("/ws", ws => {
|
||||||
var server = ws.ConnectToServer();
|
var server = ws.ConnectToServer();
|
||||||
ws.OnMessage(message => {
|
ws.OnMessage(frame => {
|
||||||
if (message != "blocked-from-the-page")
|
if (frame.Text != "blocked-from-the-page")
|
||||||
server.Send(message);
|
server.Send(frame.Text);
|
||||||
});
|
});
|
||||||
server.OnMessage(message => {
|
server.OnMessage(frame => {
|
||||||
if (message != "blocked-from-the-server")
|
if (frame.Text != "blocked-from-the-server")
|
||||||
ws.Send(message);
|
ws.Send(frame.Text);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ title: "Docker"
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
[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.
|
[Dockerfile.noble] 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
|
## Usage
|
||||||
|
|
||||||
|
|
@ -111,7 +111,6 @@ We currently publish images with the following tags:
|
||||||
- `:v%%VERSION%%` - Playwright v%%VERSION%% release docker image based on Ubuntu 24.04 LTS (Noble Numbat).
|
- `:v%%VERSION%%` - Playwright v%%VERSION%% release docker image based on Ubuntu 24.04 LTS (Noble Numbat).
|
||||||
- `:v%%VERSION%%-noble` - Playwright v%%VERSION%% release docker image based on Ubuntu 24.04 LTS (Noble Numbat).
|
- `:v%%VERSION%%-noble` - Playwright v%%VERSION%% release docker image based on Ubuntu 24.04 LTS (Noble Numbat).
|
||||||
- `:v%%VERSION%%-jammy` - Playwright v%%VERSION%% release docker image based on Ubuntu 22.04 LTS (Jammy Jellyfish).
|
- `:v%%VERSION%%-jammy` - Playwright v%%VERSION%% release docker image based on Ubuntu 22.04 LTS (Jammy Jellyfish).
|
||||||
- `:v%%VERSION%%-focal` - Playwright v%%VERSION%% release docker image based on Ubuntu 20.04 LTS (Focal Fossa).
|
|
||||||
|
|
||||||
:::note
|
:::note
|
||||||
It is recommended to always pin your Docker image to a specific version if possible. If the Playwright version in your Docker image does not match the version in your project/tests, Playwright will be unable to locate browser executables.
|
It is recommended to always pin your Docker image to a specific version if possible. If the Playwright version in your Docker image does not match the version in your project/tests, Playwright will be unable to locate browser executables.
|
||||||
|
|
@ -122,7 +121,6 @@ It is recommended to always pin your Docker image to a specific version if possi
|
||||||
We currently publish images based on the following [Ubuntu](https://hub.docker.com/_/ubuntu) versions:
|
We currently publish images based on the following [Ubuntu](https://hub.docker.com/_/ubuntu) versions:
|
||||||
- **Ubuntu 24.04 LTS** (Noble Numbat), image tags include `noble`
|
- **Ubuntu 24.04 LTS** (Noble Numbat), image tags include `noble`
|
||||||
- **Ubuntu 22.04 LTS** (Jammy Jellyfish), image tags include `jammy`
|
- **Ubuntu 22.04 LTS** (Jammy Jellyfish), image tags include `jammy`
|
||||||
- **Ubuntu 20.04 LTS** (Focal Fossa), image tags include `focal`
|
|
||||||
|
|
||||||
#### Alpine
|
#### Alpine
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -180,8 +180,8 @@ See our doc on [Running and Debugging Tests](./running-tests.md) to learn more a
|
||||||
|
|
||||||
- Playwright is distributed as a .NET Standard 2.0 library. We recommend .NET 8.
|
- Playwright is distributed as a .NET Standard 2.0 library. We recommend .NET 8.
|
||||||
- Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL).
|
- Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL).
|
||||||
- macOS 13 Ventura, or macOS 14 Sonoma.
|
- macOS 13 Ventura, or later.
|
||||||
- Debian 11, Debian 12, Ubuntu 20.04 or Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture.
|
- Debian 12, Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture.
|
||||||
|
|
||||||
## What's next
|
## What's next
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -130,8 +130,8 @@ By default browsers launched with Playwright run headless, meaning no browser UI
|
||||||
|
|
||||||
- Java 8 or higher.
|
- Java 8 or higher.
|
||||||
- Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL).
|
- Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL).
|
||||||
- macOS 13 Ventura, or macOS 14 Sonoma.
|
- macOS 13 Ventura, or later.
|
||||||
- Debian 11, Debian 12, Ubuntu 20.04 or Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture.
|
- Debian 12, Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture.
|
||||||
|
|
||||||
## What's next
|
## What's next
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -288,8 +288,8 @@ pnpm exec playwright --version
|
||||||
|
|
||||||
- Node.js 18+
|
- Node.js 18+
|
||||||
- Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL).
|
- Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL).
|
||||||
- macOS 13 Ventura, or macOS 14 Sonoma.
|
- macOS 13 Ventura, or later.
|
||||||
- Debian 11, Debian 12, Ubuntu 20.04 or Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture.
|
- Debian 12, Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture.
|
||||||
|
|
||||||
## What's next
|
## What's next
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -101,8 +101,8 @@ pip install pytest-playwright playwright -U
|
||||||
|
|
||||||
- Python 3.8 or higher.
|
- Python 3.8 or higher.
|
||||||
- Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL).
|
- Windows 10+, Windows Server 2016+ or Windows Subsystem for Linux (WSL).
|
||||||
- macOS 13 Ventura, or macOS 14 Sonoma.
|
- macOS 13 Ventura, or later.
|
||||||
- Debian 11, Debian 12, Ubuntu 20.04 or Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture.
|
- Debian 12, Ubuntu 22.04, Ubuntu 24.04, on x86-64 and arm64 architecture.
|
||||||
|
|
||||||
## What's next
|
## What's next
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -451,8 +451,8 @@ await page.routeWebSocket('wss://example.com/ws', ws => {
|
||||||
|
|
||||||
```java
|
```java
|
||||||
page.routeWebSocket("wss://example.com/ws", ws -> {
|
page.routeWebSocket("wss://example.com/ws", ws -> {
|
||||||
ws.onMessage(message -> {
|
ws.onMessage(frame -> {
|
||||||
if ("request".equals(message))
|
if ("request".equals(frame.text()))
|
||||||
ws.send("response");
|
ws.send("response");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -480,8 +480,8 @@ page.route_web_socket("wss://example.com/ws", lambda ws: ws.on_message(
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
await page.RouteWebSocketAsync("wss://example.com/ws", ws => {
|
await page.RouteWebSocketAsync("wss://example.com/ws", ws => {
|
||||||
ws.OnMessage(message => {
|
ws.OnMessage(frame => {
|
||||||
if (message == "request")
|
if (frame.Text == "request")
|
||||||
ws.Send("response");
|
ws.Send("response");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -504,11 +504,11 @@ await page.routeWebSocket('wss://example.com/ws', ws => {
|
||||||
```java
|
```java
|
||||||
page.routeWebSocket("wss://example.com/ws", ws -> {
|
page.routeWebSocket("wss://example.com/ws", ws -> {
|
||||||
WebSocketRoute server = ws.connectToServer();
|
WebSocketRoute server = ws.connectToServer();
|
||||||
ws.onMessage(message -> {
|
ws.onMessage(frame -> {
|
||||||
if ("request".equals(message))
|
if ("request".equals(frame.text()))
|
||||||
server.send("request2");
|
server.send("request2");
|
||||||
else
|
else
|
||||||
server.send(message);
|
server.send(frame.text());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
@ -544,11 +544,11 @@ page.route_web_socket("wss://example.com/ws", handler)
|
||||||
```csharp
|
```csharp
|
||||||
await page.RouteWebSocketAsync("wss://example.com/ws", ws => {
|
await page.RouteWebSocketAsync("wss://example.com/ws", ws => {
|
||||||
var server = ws.ConnectToServer();
|
var server = ws.ConnectToServer();
|
||||||
ws.OnMessage(message => {
|
ws.OnMessage(frame => {
|
||||||
if (message == "request")
|
if (frame.Text == "request")
|
||||||
server.Send("request2");
|
server.Send("request2");
|
||||||
else
|
else
|
||||||
server.Send(message);
|
server.Send(frame.Text);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ New methods [`method: Page.routeWebSocket`] and [`method: BrowserContext.routeWe
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
await page.RouteWebSocketAsync("/ws", ws => {
|
await page.RouteWebSocketAsync("/ws", ws => {
|
||||||
ws.OnMessage(message => {
|
ws.OnMessage(frame => {
|
||||||
if (message == "request")
|
if (frame.Text == "request")
|
||||||
ws.Send("response");
|
ws.Send("response");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@ New methods [`method: Page.routeWebSocket`] and [`method: BrowserContext.routeWe
|
||||||
|
|
||||||
```java
|
```java
|
||||||
page.routeWebSocket("/ws", ws -> {
|
page.routeWebSocket("/ws", ws -> {
|
||||||
ws.onMessage(message -> {
|
ws.onMessage(frame -> {
|
||||||
if ("request".equals(message))
|
if ("request".equals(frame.text()))
|
||||||
ws.send("response");
|
ws.send("response");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,54 @@
|
||||||
|
|
||||||
Information about an error thrown during test execution.
|
Information about an error thrown during test execution.
|
||||||
|
|
||||||
|
## property: TestError.expected
|
||||||
|
* since: v1.49
|
||||||
|
- type: ?<[string]>
|
||||||
|
|
||||||
|
Expected value formatted as a human-readable string.
|
||||||
|
|
||||||
|
## property: TestError.locator
|
||||||
|
* since: v1.49
|
||||||
|
- type: ?<[string]>
|
||||||
|
|
||||||
|
Receiver's locator.
|
||||||
|
|
||||||
|
## property: TestError.log
|
||||||
|
* since: v1.49
|
||||||
|
- type: ?<[Array]<[string]>>
|
||||||
|
|
||||||
|
Call log.
|
||||||
|
|
||||||
|
## property: TestError.matcherName
|
||||||
|
* since: v1.49
|
||||||
|
- type: ?<[string]>
|
||||||
|
|
||||||
|
Expect matcher name.
|
||||||
|
|
||||||
## property: TestError.message
|
## property: TestError.message
|
||||||
* since: v1.10
|
* since: v1.10
|
||||||
- type: ?<[string]>
|
- type: ?<[string]>
|
||||||
|
|
||||||
Error message. Set when [Error] (or its subclass) has been thrown.
|
Error message. Set when [Error] (or its subclass) has been thrown.
|
||||||
|
|
||||||
|
## property: TestError.received
|
||||||
|
* since: v1.49
|
||||||
|
- type: ?<[string]>
|
||||||
|
|
||||||
|
Received value formatted as a human-readable string.
|
||||||
|
|
||||||
## property: TestError.stack
|
## property: TestError.stack
|
||||||
* since: v1.10
|
* since: v1.10
|
||||||
- type: ?<[string]>
|
- type: ?<[string]>
|
||||||
|
|
||||||
Error stack. Set when [Error] (or its subclass) has been thrown.
|
Error stack. Set when [Error] (or its subclass) has been thrown.
|
||||||
|
|
||||||
|
## property: TestError.timeout
|
||||||
|
* since: v1.49
|
||||||
|
- type: ?<[int]>
|
||||||
|
|
||||||
|
Timeout in milliseconds, if the error was caused by a timeout.
|
||||||
|
|
||||||
## property: TestError.value
|
## property: TestError.value
|
||||||
* since: v1.10
|
* since: v1.10
|
||||||
- type: ?<[string]>
|
- type: ?<[string]>
|
||||||
|
|
|
||||||
|
|
@ -208,7 +208,7 @@ function matchesText(text: string | undefined, template: RegExp | string | undef
|
||||||
export function matchesAriaTree(rootElement: Element, template: AriaTemplateNode): { matches: boolean, received: string } {
|
export function matchesAriaTree(rootElement: Element, template: AriaTemplateNode): { matches: boolean, received: string } {
|
||||||
const root = generateAriaTree(rootElement);
|
const root = generateAriaTree(rootElement);
|
||||||
const matches = matchesNodeDeep(root, template);
|
const matches = matchesNodeDeep(root, template);
|
||||||
return { matches, received: renderAriaTree(root) };
|
return { matches, received: renderAriaTree(root, { noText: true }) };
|
||||||
}
|
}
|
||||||
|
|
||||||
function matchesNode(node: AriaNode | string, template: AriaTemplateNode | RegExp | string, depth: number): boolean {
|
function matchesNode(node: AriaNode | string, template: AriaTemplateNode | RegExp | string, depth: number): boolean {
|
||||||
|
|
@ -276,11 +276,12 @@ function matchesNodeDeep(root: AriaNode, template: AriaTemplateNode): boolean {
|
||||||
return !!results.length;
|
return !!results.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderAriaTree(ariaNode: AriaNode): string {
|
export function renderAriaTree(ariaNode: AriaNode, options?: { noText?: boolean }): string {
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
const visit = (ariaNode: AriaNode | string, indent: string) => {
|
const visit = (ariaNode: AriaNode | string, indent: string) => {
|
||||||
if (typeof ariaNode === 'string') {
|
if (typeof ariaNode === 'string') {
|
||||||
lines.push(indent + '- text: ' + escapeYamlString(ariaNode));
|
if (!options?.noText)
|
||||||
|
lines.push(indent + '- text: ' + escapeYamlString(ariaNode));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let line = `${indent}- ${ariaNode.role}`;
|
let line = `${indent}- ${ariaNode.role}`;
|
||||||
|
|
@ -301,10 +302,12 @@ export function renderAriaTree(ariaNode: AriaNode): string {
|
||||||
line += ` [pressed=mixed]`;
|
line += ` [pressed=mixed]`;
|
||||||
if (ariaNode.pressed === true)
|
if (ariaNode.pressed === true)
|
||||||
line += ` [pressed]`;
|
line += ` [pressed]`;
|
||||||
|
if (ariaNode.selected === true)
|
||||||
|
line += ` [selected]`;
|
||||||
|
|
||||||
const stringValue = !ariaNode.children.length || (ariaNode.children?.length === 1 && typeof ariaNode.children[0] === 'string');
|
const stringValue = !ariaNode.children.length || (ariaNode.children?.length === 1 && typeof ariaNode.children[0] === 'string');
|
||||||
if (stringValue) {
|
if (stringValue) {
|
||||||
if (ariaNode.children.length)
|
if (!options?.noText && ariaNode.children.length)
|
||||||
line += ': ' + escapeYamlString(ariaNode.children?.[0] as string);
|
line += ': ' + escapeYamlString(ariaNode.children?.[0] as string);
|
||||||
lines.push(line);
|
lines.push(line);
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -33,11 +33,12 @@ function encodeBase128(value: number): Buffer {
|
||||||
do {
|
do {
|
||||||
let byte = value & 0x7f;
|
let byte = value & 0x7f;
|
||||||
value >>>= 7;
|
value >>>= 7;
|
||||||
if (bytes.length > 0) byte |= 0x80;
|
if (bytes.length > 0)
|
||||||
|
byte |= 0x80;
|
||||||
bytes.push(byte);
|
bytes.push(byte);
|
||||||
} while (value > 0);
|
} while (value > 0);
|
||||||
return Buffer.from(bytes.reverse());
|
return Buffer.from(bytes.reverse());
|
||||||
};
|
}
|
||||||
|
|
||||||
// ASN1/DER Speficiation: https://www.itu.int/rec/T-REC-X.680-X.693-202102-I/en
|
// ASN1/DER Speficiation: https://www.itu.int/rec/T-REC-X.680-X.693-202102-I/en
|
||||||
class DER {
|
class DER {
|
||||||
|
|
@ -49,13 +50,13 @@ class DER {
|
||||||
return this._encode(0x02, Buffer.from([data]));
|
return this._encode(0x02, Buffer.from([data]));
|
||||||
}
|
}
|
||||||
static encodeObjectIdentifier(oid: string): Buffer {
|
static encodeObjectIdentifier(oid: string): Buffer {
|
||||||
const parts = oid.split('.').map((v) => Number(v));
|
const parts = oid.split('.').map(v => Number(v));
|
||||||
// Encode the second part, which could be large, using base-128 encoding if necessary
|
// Encode the second part, which could be large, using base-128 encoding if necessary
|
||||||
const output = [encodeBase128(40 * parts[0] + parts[1])];
|
const output = [encodeBase128(40 * parts[0] + parts[1])];
|
||||||
|
|
||||||
for (let i = 2; i < parts.length; i++) {
|
for (let i = 2; i < parts.length; i++)
|
||||||
output.push(encodeBase128(parts[i]));
|
output.push(encodeBase128(parts[i]));
|
||||||
}
|
|
||||||
|
|
||||||
return this._encode(0x06, Buffer.concat(output));
|
return this._encode(0x06, Buffer.concat(output));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,8 @@ import { monotonicTime } from './time';
|
||||||
// Same as in Chromium (https://source.chromium.org/chromium/chromium/src/+/5666ff4f5077a7e2f72902f3a95f5d553ea0d88d:net/socket/transport_connect_job.cc;l=102)
|
// Same as in Chromium (https://source.chromium.org/chromium/chromium/src/+/5666ff4f5077a7e2f72902f3a95f5d553ea0d88d:net/socket/transport_connect_job.cc;l=102)
|
||||||
const connectionAttemptDelayMs = 300;
|
const connectionAttemptDelayMs = 300;
|
||||||
|
|
||||||
const kDNSLookupAt = Symbol('kDNSLookupAt')
|
const kDNSLookupAt = Symbol('kDNSLookupAt');
|
||||||
const kTCPConnectionAt = Symbol('kTCPConnectionAt')
|
const kTCPConnectionAt = Symbol('kTCPConnectionAt');
|
||||||
|
|
||||||
class HttpHappyEyeballsAgent extends http.Agent {
|
class HttpHappyEyeballsAgent extends http.Agent {
|
||||||
createConnection(options: http.ClientRequestArgs, oncreate?: (err: Error | null, socket?: net.Socket) => void): net.Socket | undefined {
|
createConnection(options: http.ClientRequestArgs, oncreate?: (err: Error | null, socket?: net.Socket) => void): net.Socket | undefined {
|
||||||
|
|
@ -75,7 +75,7 @@ export async function createTLSSocket(options: tls.ConnectionOptions): Promise<t
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
assert(options.host, 'host is required');
|
assert(options.host, 'host is required');
|
||||||
if (net.isIP(options.host)) {
|
if (net.isIP(options.host)) {
|
||||||
const socket = tls.connect(options)
|
const socket = tls.connect(options);
|
||||||
socket.on('secureConnect', () => resolve(socket));
|
socket.on('secureConnect', () => resolve(socket));
|
||||||
socket.on('error', error => reject(error));
|
socket.on('error', error => reject(error));
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -202,5 +202,5 @@ export function timingForSocket(socket: net.Socket | tls.TLSSocket) {
|
||||||
return {
|
return {
|
||||||
dnsLookupAt: (socket as any)[kDNSLookupAt] as number | undefined,
|
dnsLookupAt: (socket as any)[kDNSLookupAt] as number | undefined,
|
||||||
tcpConnectionAt: (socket as any)[kTCPConnectionAt] as number | undefined,
|
tcpConnectionAt: (socket as any)[kTCPConnectionAt] as number | undefined,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@ export function createHttpsServer(...args: any[]): https.Server {
|
||||||
return server;
|
return server;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createHttp2Server( onRequestHandler?: (request: http2.Http2ServerRequest, response: http2.Http2ServerResponse) => void,): http2.Http2SecureServer;
|
export function createHttp2Server(onRequestHandler?: (request: http2.Http2ServerRequest, response: http2.Http2ServerResponse) => void,): http2.Http2SecureServer;
|
||||||
export function createHttp2Server(options: http2.SecureServerOptions, onRequestHandler?: (request: http2.Http2ServerRequest, response: http2.Http2ServerResponse) => void,): http2.Http2SecureServer;
|
export function createHttp2Server(options: http2.SecureServerOptions, onRequestHandler?: (request: http2.Http2ServerRequest, response: http2.Http2ServerResponse) => void,): http2.Http2SecureServer;
|
||||||
export function createHttp2Server(...args: any[]): http2.Http2SecureServer {
|
export function createHttp2Server(...args: any[]): http2.Http2SecureServer {
|
||||||
const server = http2.createSecureServer(...args);
|
const server = http2.createSecureServer(...args);
|
||||||
|
|
|
||||||
66
packages/playwright-core/src/utils/sequence.ts
Normal file
66
packages/playwright-core/src/utils/sequence.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function findRepeatedSubsequences(s: string[]): { sequence: string[]; count: number }[] {
|
||||||
|
const n = s.length;
|
||||||
|
const result = [];
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
const arraysEqual = (a1: string[], a2: string[]) => {
|
||||||
|
if (a1.length !== a2.length)
|
||||||
|
return false;
|
||||||
|
for (let j = 0; j < a1.length; j++) {
|
||||||
|
if (a1[j] !== a2[j])
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
while (i < n) {
|
||||||
|
let maxRepeatCount = 1;
|
||||||
|
let maxRepeatSubstr = [s[i]]; // Initialize with the element at index i
|
||||||
|
let maxRepeatLength = 1;
|
||||||
|
|
||||||
|
// Try substrings of length from 1 to the remaining length of the array
|
||||||
|
for (let p = 1; p <= n - i; p++) {
|
||||||
|
const substr = s.slice(i, i + p); // Extract substring as array
|
||||||
|
let k = 1;
|
||||||
|
|
||||||
|
// Count how many times the substring repeats consecutively
|
||||||
|
while (
|
||||||
|
i + p * k <= n &&
|
||||||
|
arraysEqual(s.slice(i + p * (k - 1), i + p * k), substr)
|
||||||
|
)
|
||||||
|
k += 1;
|
||||||
|
|
||||||
|
k -= 1; // Adjust k since it increments one extra time in the loop
|
||||||
|
|
||||||
|
// Update the maximal repeating substring if necessary
|
||||||
|
if (k > 1 && (k * p) > (maxRepeatCount * maxRepeatLength)) {
|
||||||
|
maxRepeatCount = k;
|
||||||
|
maxRepeatSubstr = substr;
|
||||||
|
maxRepeatLength = p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record the substring and its count
|
||||||
|
result.push({ sequence: maxRepeatSubstr, count: maxRepeatCount });
|
||||||
|
i += maxRepeatLength * maxRepeatCount; // Move index forward
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
@ -16,9 +16,9 @@
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { parseStackTraceLine } from '../utilsBundle';
|
import { parseStackTraceLine } from '../utilsBundle';
|
||||||
import { isUnderTest } from './';
|
|
||||||
import type { StackFrame } from '@protocol/channels';
|
import type { StackFrame } from '@protocol/channels';
|
||||||
import { colors } from '../utilsBundle';
|
import { colors } from '../utilsBundle';
|
||||||
|
import { findRepeatedSubsequences } from './sequence';
|
||||||
|
|
||||||
export function rewriteErrorMessage<E extends Error>(e: E, newMessage: string): E {
|
export function rewriteErrorMessage<E extends Error>(e: E, newMessage: string): E {
|
||||||
const lines: string[] = (e.stack?.split('\n') || []).filter(l => l.startsWith(' at '));
|
const lines: string[] = (e.stack?.split('\n') || []).filter(l => l.startsWith(' at '));
|
||||||
|
|
@ -50,7 +50,6 @@ export function captureRawStack(): RawStack {
|
||||||
export function captureLibraryStackTrace(): { frames: StackFrame[], apiName: string } {
|
export function captureLibraryStackTrace(): { frames: StackFrame[], apiName: string } {
|
||||||
const stack = captureRawStack();
|
const stack = captureRawStack();
|
||||||
|
|
||||||
const isTesting = isUnderTest();
|
|
||||||
type ParsedFrame = {
|
type ParsedFrame = {
|
||||||
frame: StackFrame;
|
frame: StackFrame;
|
||||||
frameText: string;
|
frameText: string;
|
||||||
|
|
@ -132,9 +131,26 @@ export function splitErrorMessage(message: string): { name: string, message: str
|
||||||
export function formatCallLog(log: string[] | undefined): string {
|
export function formatCallLog(log: string[] | undefined): string {
|
||||||
if (!log || !log.some(l => !!l))
|
if (!log || !log.some(l => !!l))
|
||||||
return '';
|
return '';
|
||||||
|
|
||||||
|
const lines: string[] = [];
|
||||||
|
|
||||||
|
for (const block of findRepeatedSubsequences(log)) {
|
||||||
|
for (let i = 0; i < block.sequence.length; i++) {
|
||||||
|
const line = block.sequence[i];
|
||||||
|
const leadingWhitespace = line.match(/^\s*/);
|
||||||
|
const whitespacePrefix = ' ' + leadingWhitespace?.[0] || '';
|
||||||
|
const countPrefix = `${block.count} × `;
|
||||||
|
if (block.count > 1 && i === 0)
|
||||||
|
lines.push(whitespacePrefix + countPrefix + line.trim());
|
||||||
|
else if (block.count > 1)
|
||||||
|
lines.push(whitespacePrefix + ' '.repeat(countPrefix.length - 2) + '- ' + line.trim());
|
||||||
|
else
|
||||||
|
lines.push(whitespacePrefix + '- ' + line.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
return `
|
return `
|
||||||
Call log:
|
Call log:
|
||||||
${colors.dim('- ' + (log || []).join('\n - '))}
|
${colors.dim(lines.join('\n'))}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ class ZoneManager {
|
||||||
zones.push(str);
|
zones.push(str);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.log('zones: ', zones.join(' -> '));
|
console.log('zones: ', zones.join(' -> '));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,10 @@ export type MatcherResult<E, A> = {
|
||||||
actual?: A;
|
actual?: A;
|
||||||
log?: string[];
|
log?: string[];
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
|
locator?: string;
|
||||||
|
printedReceived?: string;
|
||||||
|
printedExpected?: string;
|
||||||
|
printedDiff?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ExpectError extends Error {
|
export class ExpectError extends Error {
|
||||||
|
|
|
||||||
|
|
@ -39,22 +39,41 @@ export async function toBeTruthy(
|
||||||
};
|
};
|
||||||
|
|
||||||
const timeout = options.timeout ?? this.timeout;
|
const timeout = options.timeout ?? this.timeout;
|
||||||
const { matches, log, timedOut, received } = await query(!!this.isNot, timeout);
|
const { matches: pass, log, timedOut, received } = await query(!!this.isNot, timeout);
|
||||||
|
if (pass === !this.isNot) {
|
||||||
|
return {
|
||||||
|
name: matcherName,
|
||||||
|
message: () => '',
|
||||||
|
pass,
|
||||||
|
expected
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const notFound = received === kNoElementsFoundError ? received : undefined;
|
const notFound = received === kNoElementsFoundError ? received : undefined;
|
||||||
const actual = matches ? expected : unexpected;
|
const actual = pass ? expected : unexpected;
|
||||||
|
let printedReceived: string | undefined;
|
||||||
|
let printedExpected: string | undefined;
|
||||||
|
if (pass) {
|
||||||
|
printedExpected = `Expected: not ${expected}`;
|
||||||
|
printedReceived = `Received: ${notFound ? kNoElementsFoundError : expected}`;
|
||||||
|
} else {
|
||||||
|
printedExpected = `Expected: ${expected}`;
|
||||||
|
printedReceived = `Received: ${notFound ? kNoElementsFoundError : unexpected}`;
|
||||||
|
}
|
||||||
const message = () => {
|
const message = () => {
|
||||||
const header = matcherHint(this, receiver, matcherName, 'locator', arg, matcherOptions, timedOut ? timeout : undefined);
|
const header = matcherHint(this, receiver, matcherName, 'locator', arg, matcherOptions, timedOut ? timeout : undefined);
|
||||||
const logText = callLogText(log);
|
const logText = callLogText(log);
|
||||||
return matches ? `${header}Expected: not ${expected}\nReceived: ${notFound ? kNoElementsFoundError : expected}${logText}` :
|
return `${header}${printedExpected}\n${printedReceived}${logText}`;
|
||||||
`${header}Expected: ${expected}\nReceived: ${notFound ? kNoElementsFoundError : unexpected}${logText}`;
|
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
message,
|
message,
|
||||||
pass: matches,
|
pass,
|
||||||
actual,
|
actual,
|
||||||
name: matcherName,
|
name: matcherName,
|
||||||
expected,
|
expected,
|
||||||
log,
|
log,
|
||||||
timeout: timedOut ? timeout : undefined,
|
timeout: timedOut ? timeout : undefined,
|
||||||
|
...(printedReceived ? { printedReceived } : {}),
|
||||||
|
...(printedExpected ? { printedExpected } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,22 +44,35 @@ export async function toEqual<T>(
|
||||||
const timeout = options.timeout ?? this.timeout;
|
const timeout = options.timeout ?? this.timeout;
|
||||||
|
|
||||||
const { matches: pass, received, log, timedOut } = await query(!!this.isNot, timeout);
|
const { matches: pass, received, log, timedOut } = await query(!!this.isNot, timeout);
|
||||||
|
if (pass === !this.isNot) {
|
||||||
|
return {
|
||||||
|
name: matcherName,
|
||||||
|
message: () => '',
|
||||||
|
pass,
|
||||||
|
expected
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const message = pass
|
let printedReceived: string | undefined;
|
||||||
? () =>
|
let printedExpected: string | undefined;
|
||||||
matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined) +
|
let printedDiff: string | undefined;
|
||||||
`Expected: not ${this.utils.printExpected(expected)}\n` +
|
if (pass) {
|
||||||
`Received: ${this.utils.printReceived(received)}` + callLogText(log)
|
printedExpected = `Expected: not ${this.utils.printExpected(expected)}`;
|
||||||
: () =>
|
printedReceived = `Received: ${this.utils.printReceived(received)}`;
|
||||||
matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined) +
|
} else {
|
||||||
this.utils.printDiffOrStringify(
|
printedDiff = this.utils.printDiffOrStringify(
|
||||||
expected,
|
expected,
|
||||||
received,
|
received,
|
||||||
EXPECTED_LABEL,
|
EXPECTED_LABEL,
|
||||||
RECEIVED_LABEL,
|
RECEIVED_LABEL,
|
||||||
false,
|
false,
|
||||||
) + callLogText(log);
|
);
|
||||||
|
}
|
||||||
|
const message = () => {
|
||||||
|
const header = matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined);
|
||||||
|
const details = printedDiff || `${printedExpected}\n${printedReceived}`;
|
||||||
|
return `${header}${details}${callLogText(log)}`;
|
||||||
|
};
|
||||||
// Passing the actual and expected objects so that a custom reporter
|
// Passing the actual and expected objects so that a custom reporter
|
||||||
// could access them, for example in order to display a custom visual diff,
|
// could access them, for example in order to display a custom visual diff,
|
||||||
// or create a different error message
|
// or create a different error message
|
||||||
|
|
@ -70,5 +83,8 @@ export async function toEqual<T>(
|
||||||
pass,
|
pass,
|
||||||
log,
|
log,
|
||||||
timeout: timedOut ? timeout : undefined,
|
timeout: timedOut ? timeout : undefined,
|
||||||
|
...(printedReceived ? { printedReceived } : {}),
|
||||||
|
...(printedExpected ? { printedExpected } : {}),
|
||||||
|
...(printedDiff ? { printedDiff } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -194,6 +194,10 @@ class SnapshotHelper {
|
||||||
pass,
|
pass,
|
||||||
message: () => message,
|
message: () => message,
|
||||||
log,
|
log,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||||
|
...(this.locator ? { locator: this.locator.toString() } : {}),
|
||||||
|
printedExpected: this.expectedPath,
|
||||||
|
printedReceived: this.actualPath,
|
||||||
};
|
};
|
||||||
return Object.fromEntries(Object.entries(unfiltered).filter(([_, v]) => v !== undefined)) as ImageMatcherResult;
|
return Object.fromEntries(Object.entries(unfiltered).filter(([_, v]) => v !== undefined)) as ImageMatcherResult;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,29 +58,56 @@ export async function toMatchText(
|
||||||
const timeout = options.timeout ?? this.timeout;
|
const timeout = options.timeout ?? this.timeout;
|
||||||
|
|
||||||
const { matches: pass, received, log, timedOut } = await query(!!this.isNot, timeout);
|
const { matches: pass, received, log, timedOut } = await query(!!this.isNot, timeout);
|
||||||
|
if (pass === !this.isNot) {
|
||||||
|
return {
|
||||||
|
name: matcherName,
|
||||||
|
message: () => '',
|
||||||
|
pass,
|
||||||
|
expected
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const stringSubstring = options.matchSubstring ? 'substring' : 'string';
|
const stringSubstring = options.matchSubstring ? 'substring' : 'string';
|
||||||
const receivedString = received || '';
|
const receivedString = received || '';
|
||||||
const messagePrefix = matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined);
|
const messagePrefix = matcherHint(this, receiver, matcherName, 'locator', undefined, matcherOptions, timedOut ? timeout : undefined);
|
||||||
const notFound = received === kNoElementsFoundError;
|
const notFound = received === kNoElementsFoundError;
|
||||||
const message = () => {
|
|
||||||
if (pass) {
|
let printedReceived: string | undefined;
|
||||||
if (typeof expected === 'string') {
|
let printedExpected: string | undefined;
|
||||||
if (notFound)
|
let printedDiff: string | undefined;
|
||||||
return messagePrefix + `Expected ${stringSubstring}: not ${this.utils.printExpected(expected)}\nReceived: ${received}` + callLogText(log);
|
if (pass) {
|
||||||
const printedReceived = printReceivedStringContainExpectedSubstring(receivedString, receivedString.indexOf(expected), expected.length);
|
if (typeof expected === 'string') {
|
||||||
return messagePrefix + `Expected ${stringSubstring}: not ${this.utils.printExpected(expected)}\nReceived string: ${printedReceived}` + callLogText(log);
|
if (notFound) {
|
||||||
|
printedExpected = `Expected ${stringSubstring}: not ${this.utils.printExpected(expected)}`;
|
||||||
|
printedReceived = `Received: ${received}`;
|
||||||
} else {
|
} else {
|
||||||
if (notFound)
|
printedExpected = `Expected ${stringSubstring}: not ${this.utils.printExpected(expected)}`;
|
||||||
return messagePrefix + `Expected pattern: not ${this.utils.printExpected(expected)}\nReceived: ${received}` + callLogText(log);
|
const formattedReceived = printReceivedStringContainExpectedSubstring(receivedString, receivedString.indexOf(expected), expected.length);
|
||||||
const printedReceived = printReceivedStringContainExpectedResult(receivedString, typeof expected.exec === 'function' ? expected.exec(receivedString) : null);
|
printedReceived = `Received string: ${formattedReceived}`;
|
||||||
return messagePrefix + `Expected pattern: not ${this.utils.printExpected(expected)}\nReceived string: ${printedReceived}` + callLogText(log);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const labelExpected = `Expected ${typeof expected === 'string' ? stringSubstring : 'pattern'}`;
|
if (notFound) {
|
||||||
if (notFound)
|
printedExpected = `Expected pattern: not ${this.utils.printExpected(expected)}`;
|
||||||
return messagePrefix + `${labelExpected}: ${this.utils.printExpected(expected)}\nReceived: ${received}` + callLogText(log);
|
printedReceived = `Received: ${received}`;
|
||||||
return messagePrefix + this.utils.printDiffOrStringify(expected, receivedString, labelExpected, 'Received string', false) + callLogText(log);
|
} else {
|
||||||
|
printedExpected = `Expected pattern: not ${this.utils.printExpected(expected)}`;
|
||||||
|
const formattedReceived = printReceivedStringContainExpectedResult(receivedString, typeof expected.exec === 'function' ? expected.exec(receivedString) : null);
|
||||||
|
printedReceived = `Received string: ${formattedReceived}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
const labelExpected = `Expected ${typeof expected === 'string' ? stringSubstring : 'pattern'}`;
|
||||||
|
if (notFound) {
|
||||||
|
printedExpected = `${labelExpected}: ${this.utils.printExpected(expected)}`;
|
||||||
|
printedReceived = `Received: ${received}`;
|
||||||
|
} else {
|
||||||
|
printedDiff = this.utils.printDiffOrStringify(expected, receivedString, labelExpected, 'Received string', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = () => {
|
||||||
|
const resultDetails = printedDiff ? printedDiff : printedExpected + '\n' + printedReceived;
|
||||||
|
return messagePrefix + resultDetails + callLogText(log);
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -91,5 +118,10 @@ export async function toMatchText(
|
||||||
actual: received,
|
actual: received,
|
||||||
log,
|
log,
|
||||||
timeout: timedOut ? timeout : undefined,
|
timeout: timedOut ? timeout : undefined,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||||
|
locator: receiver.toString(),
|
||||||
|
...(printedReceived ? { printedReceived } : {}),
|
||||||
|
...(printedExpected ? { printedExpected } : {}),
|
||||||
|
...(printedDiff ? { printedDiff } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,13 @@ type Annotation = {
|
||||||
type ErrorDetails = {
|
type ErrorDetails = {
|
||||||
message: string;
|
message: string;
|
||||||
location?: Location;
|
location?: Location;
|
||||||
|
timeout?: number;
|
||||||
|
matcherName?: string;
|
||||||
|
locator?: string;
|
||||||
|
expected?: string;
|
||||||
|
received?: string;
|
||||||
|
log?: string[];
|
||||||
|
snippet?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TestSummary = {
|
type TestSummary = {
|
||||||
|
|
@ -383,6 +390,13 @@ export function formatResultFailure(test: TestCase, result: TestResult, initialI
|
||||||
errorDetails.push({
|
errorDetails.push({
|
||||||
message: indent(formattedError.message, initialIndent),
|
message: indent(formattedError.message, initialIndent),
|
||||||
location: formattedError.location,
|
location: formattedError.location,
|
||||||
|
timeout: error.timeout,
|
||||||
|
matcherName: error.matcherName,
|
||||||
|
locator: error.locator,
|
||||||
|
expected: error.expected,
|
||||||
|
received: error.received,
|
||||||
|
log: error.log,
|
||||||
|
snippet: error.snippet,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return errorDetails;
|
return errorDetails;
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,11 @@ import { TimeoutManager, TimeoutManagerError, kMaxDeadline } from './timeoutMana
|
||||||
import type { RunnableDescription } from './timeoutManager';
|
import type { RunnableDescription } from './timeoutManager';
|
||||||
import type { Annotation, FullConfigInternal, FullProjectInternal } from '../common/config';
|
import type { Annotation, FullConfigInternal, FullProjectInternal } from '../common/config';
|
||||||
import type { FullConfig, Location } from '../../types/testReporter';
|
import type { FullConfig, Location } from '../../types/testReporter';
|
||||||
import { debugTest, filteredStackTrace, formatLocation, getContainedPath, normalizeAndSaveAttachment, serializeError, trimLongString, windowsFilesystemFriendlyLength } from '../util';
|
import { debugTest, filteredStackTrace, formatLocation, getContainedPath, normalizeAndSaveAttachment, trimLongString, windowsFilesystemFriendlyLength } from '../util';
|
||||||
import { TestTracing } from './testTracing';
|
import { TestTracing } from './testTracing';
|
||||||
import type { Attachment } from './testTracing';
|
import type { Attachment } from './testTracing';
|
||||||
import type { StackFrame } from '@protocol/channels';
|
import type { StackFrame } from '@protocol/channels';
|
||||||
|
import { serializeWorkerError } from './util';
|
||||||
|
|
||||||
export interface TestStepInternal {
|
export interface TestStepInternal {
|
||||||
complete(result: { error?: Error | unknown, attachments?: Attachment[] }): void;
|
complete(result: { error?: Error | unknown, attachments?: Attachment[] }): void;
|
||||||
|
|
@ -272,7 +273,7 @@ export class TestInfoImpl implements TestInfo {
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
if (typeof result.error === 'object' && !(result.error as any)?.[stepSymbol])
|
if (typeof result.error === 'object' && !(result.error as any)?.[stepSymbol])
|
||||||
(result.error as any)[stepSymbol] = step;
|
(result.error as any)[stepSymbol] = step;
|
||||||
const error = serializeError(result.error);
|
const error = serializeWorkerError(result.error);
|
||||||
if (data.boxedStack)
|
if (data.boxedStack)
|
||||||
error.stack = `${error.message}\n${stringifyStackFrames(data.boxedStack).join('\n')}`;
|
error.stack = `${error.message}\n${stringifyStackFrames(data.boxedStack).join('\n')}`;
|
||||||
step.error = error;
|
step.error = error;
|
||||||
|
|
@ -330,7 +331,7 @@ export class TestInfoImpl implements TestInfo {
|
||||||
_failWithError(error: Error | unknown) {
|
_failWithError(error: Error | unknown) {
|
||||||
if (this.status === 'passed' || this.status === 'skipped')
|
if (this.status === 'passed' || this.status === 'skipped')
|
||||||
this.status = error instanceof TimeoutManagerError ? 'timedOut' : 'failed';
|
this.status = error instanceof TimeoutManagerError ? 'timedOut' : 'failed';
|
||||||
const serialized = serializeError(error);
|
const serialized = serializeWorkerError(error);
|
||||||
const step: TestStepInternal | undefined = typeof error === 'object' ? (error as any)?.[stepSymbol] : undefined;
|
const step: TestStepInternal | undefined = typeof error === 'object' ? (error as any)?.[stepSymbol] : undefined;
|
||||||
if (step && step.boxedStack)
|
if (step && step.boxedStack)
|
||||||
serialized.stack = `${(error as Error).name}: ${(error as Error).message}\n${stringifyStackFrames(step.boxedStack).join('\n')}`;
|
serialized.stack = `${(error as Error).name}: ${(error as Error).message}\n${stringifyStackFrames(step.boxedStack).join('\n')}`;
|
||||||
|
|
|
||||||
45
packages/playwright/src/worker/util.ts
Normal file
45
packages/playwright/src/worker/util.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
/**
|
||||||
|
* 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 type { TestError } from '../../types/testReporter';
|
||||||
|
import type { TestInfoError } from '../../types/test';
|
||||||
|
import type { MatcherResult } from '../matchers/matcherHint';
|
||||||
|
import { serializeError } from '../util';
|
||||||
|
|
||||||
|
|
||||||
|
type MatcherResultDetails = Pick<TestError, 'timeout'|'matcherName'|'locator'|'expected'|'received'|'log'>;
|
||||||
|
|
||||||
|
export function serializeWorkerError(error: Error | any): TestInfoError & MatcherResultDetails {
|
||||||
|
return {
|
||||||
|
...serializeError(error),
|
||||||
|
...serializeExpectDetails(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeExpectDetails(e: Error): MatcherResultDetails {
|
||||||
|
const matcherResult = (e as any).matcherResult as MatcherResult<unknown, unknown>;
|
||||||
|
if (!matcherResult)
|
||||||
|
return {};
|
||||||
|
return {
|
||||||
|
timeout: matcherResult.timeout,
|
||||||
|
matcherName: matcherResult.name,
|
||||||
|
locator: matcherResult.locator,
|
||||||
|
expected: matcherResult.printedExpected,
|
||||||
|
received: matcherResult.printedReceived,
|
||||||
|
log: matcherResult.log,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { colors } from 'playwright-core/lib/utilsBundle';
|
import { colors } from 'playwright-core/lib/utilsBundle';
|
||||||
import { debugTest, relativeFilePath, serializeError } from '../util';
|
import { debugTest, relativeFilePath } from '../util';
|
||||||
import { type TestBeginPayload, type TestEndPayload, type RunPayload, type DonePayload, type WorkerInitParams, type TeardownErrorsPayload, stdioChunkToParams } from '../common/ipc';
|
import { type TestBeginPayload, type TestEndPayload, type RunPayload, type DonePayload, type WorkerInitParams, type TeardownErrorsPayload, stdioChunkToParams } from '../common/ipc';
|
||||||
import { setCurrentTestInfo, setIsWorkerProcess } from '../common/globals';
|
import { setCurrentTestInfo, setIsWorkerProcess } from '../common/globals';
|
||||||
import { deserializeConfig } from '../common/configLoader';
|
import { deserializeConfig } from '../common/configLoader';
|
||||||
|
|
@ -32,6 +32,7 @@ import type { TestInfoError } from '../../types/test';
|
||||||
import type { Location } from '../../types/testReporter';
|
import type { Location } from '../../types/testReporter';
|
||||||
import { inheritFixtureNames } from '../common/fixtures';
|
import { inheritFixtureNames } from '../common/fixtures';
|
||||||
import { type TimeSlot } from './timeoutManager';
|
import { type TimeSlot } from './timeoutManager';
|
||||||
|
import { serializeWorkerError } from './util';
|
||||||
|
|
||||||
export class WorkerMain extends ProcessRunner {
|
export class WorkerMain extends ProcessRunner {
|
||||||
private _params: WorkerInitParams;
|
private _params: WorkerInitParams;
|
||||||
|
|
@ -112,7 +113,7 @@ export class WorkerMain extends ProcessRunner {
|
||||||
await fakeTestInfo._runAsStage({ title: 'worker cleanup', runnable }, () => gracefullyCloseAll()).catch(() => {});
|
await fakeTestInfo._runAsStage({ title: 'worker cleanup', runnable }, () => gracefullyCloseAll()).catch(() => {});
|
||||||
this._fatalErrors.push(...fakeTestInfo.errors);
|
this._fatalErrors.push(...fakeTestInfo.errors);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this._fatalErrors.push(serializeError(e));
|
this._fatalErrors.push(serializeWorkerError(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._fatalErrors.length) {
|
if (this._fatalErrors.length) {
|
||||||
|
|
@ -153,7 +154,7 @@ export class WorkerMain extends ProcessRunner {
|
||||||
// No current test - fatal error.
|
// No current test - fatal error.
|
||||||
if (!this._currentTest) {
|
if (!this._currentTest) {
|
||||||
if (!this._fatalErrors.length)
|
if (!this._fatalErrors.length)
|
||||||
this._fatalErrors.push(serializeError(error));
|
this._fatalErrors.push(serializeWorkerError(error));
|
||||||
void this._stop();
|
void this._stop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -224,7 +225,7 @@ export class WorkerMain extends ProcessRunner {
|
||||||
// In theory, we should run above code without any errors.
|
// In theory, we should run above code without any errors.
|
||||||
// However, in the case we screwed up, or loadTestFile failed in the worker
|
// However, in the case we screwed up, or loadTestFile failed in the worker
|
||||||
// but not in the runner, let's do a fatal error.
|
// but not in the runner, let's do a fatal error.
|
||||||
this._fatalErrors.push(serializeError(e));
|
this._fatalErrors.push(serializeWorkerError(e));
|
||||||
void this._stop();
|
void this._stop();
|
||||||
} finally {
|
} finally {
|
||||||
const donePayload: DonePayload = {
|
const donePayload: DonePayload = {
|
||||||
|
|
|
||||||
30
packages/playwright/types/testReporter.d.ts
vendored
30
packages/playwright/types/testReporter.d.ts
vendored
|
|
@ -554,16 +554,41 @@ export interface TestCase {
|
||||||
* Information about an error thrown during test execution.
|
* Information about an error thrown during test execution.
|
||||||
*/
|
*/
|
||||||
export interface TestError {
|
export interface TestError {
|
||||||
|
/**
|
||||||
|
* Expected value formatted as a human-readable string.
|
||||||
|
*/
|
||||||
|
expected?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error location in the source code.
|
* Error location in the source code.
|
||||||
*/
|
*/
|
||||||
location?: Location;
|
location?: Location;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receiver's locator.
|
||||||
|
*/
|
||||||
|
locator?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call log.
|
||||||
|
*/
|
||||||
|
log?: Array<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expect matcher name.
|
||||||
|
*/
|
||||||
|
matcherName?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error message. Set when [Error] (or its subclass) has been thrown.
|
* Error message. Set when [Error] (or its subclass) has been thrown.
|
||||||
*/
|
*/
|
||||||
message?: string;
|
message?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Received value formatted as a human-readable string.
|
||||||
|
*/
|
||||||
|
received?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Source code snippet with highlighted error.
|
* Source code snippet with highlighted error.
|
||||||
*/
|
*/
|
||||||
|
|
@ -574,6 +599,11 @@ export interface TestError {
|
||||||
*/
|
*/
|
||||||
stack?: string;
|
stack?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timeout in milliseconds, if the error was caused by a timeout.
|
||||||
|
*/
|
||||||
|
timeout?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The value that was thrown. Set when anything except the [Error] (or its subclass) has been thrown.
|
* The value that was thrown. Set when anything except the [Error] (or its subclass) has been thrown.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -159,12 +159,15 @@ export const TestListView: React.FC<{
|
||||||
rootItem={testTree.rootItem}
|
rootItem={testTree.rootItem}
|
||||||
dataTestId='test-tree'
|
dataTestId='test-tree'
|
||||||
render={treeItem => {
|
render={treeItem => {
|
||||||
return <div className='hbox ui-mode-tree-item'>
|
const prefixId = treeItem.id.replace(/[^\w\d-_]/g, '-');
|
||||||
<div className='ui-mode-tree-item-title'>
|
const labelId = prefixId + '-label';
|
||||||
|
const timeId = prefixId + '-time';
|
||||||
|
return <div className='hbox ui-mode-tree-item' aria-labelledby={`${labelId} ${timeId}`}>
|
||||||
|
<div id={labelId} className='ui-mode-tree-item-title'>
|
||||||
<span>{treeItem.title}</span>
|
<span>{treeItem.title}</span>
|
||||||
{treeItem.kind === 'case' ? treeItem.tags.map(tag => <TagView key={tag} tag={tag.slice(1)} onClick={e => handleTagClick(e, tag)} />) : null}
|
{treeItem.kind === 'case' ? treeItem.tags.map(tag => <TagView key={tag} tag={tag.slice(1)} onClick={e => handleTagClick(e, tag)} />) : null}
|
||||||
</div>
|
</div>
|
||||||
{!!treeItem.duration && treeItem.status !== 'skipped' && <div className='ui-mode-tree-item-time'>{msToString(treeItem.duration)}</div>}
|
{!!treeItem.duration && treeItem.status !== 'skipped' && <div id={timeId} className='ui-mode-tree-item-time'>{msToString(treeItem.duration)}</div>}
|
||||||
<Toolbar noMinHeight={true} noShadow={true}>
|
<Toolbar noMinHeight={true} noShadow={true}>
|
||||||
<ToolbarButton icon='play' title='Run' onClick={() => runTreeItem(treeItem)} disabled={!!runningState && !runningState.completed}></ToolbarButton>
|
<ToolbarButton icon='play' title='Run' onClick={() => runTreeItem(treeItem)} disabled={!!runningState && !runningState.completed}></ToolbarButton>
|
||||||
<ToolbarButton icon='go-to-file' title='Show source' onClick={onRevealSource} style={(treeItem.kind === 'group' && treeItem.subKind === 'folder') ? { visibility: 'hidden' } : {}}></ToolbarButton>
|
<ToolbarButton icon='go-to-file' title='Show source' onClick={onRevealSource} style={(treeItem.kind === 'group' && treeItem.subKind === 'folder') ? { visibility: 'hidden' } : {}}></ToolbarButton>
|
||||||
|
|
|
||||||
|
|
@ -249,8 +249,9 @@ export function TreeItemHeader<T extends TreeItem>({
|
||||||
const rendered = render(item);
|
const rendered = render(item);
|
||||||
const children = expanded && item.children.length ? item.children as T[] : [];
|
const children = expanded && item.children.length ? item.children as T[] : [];
|
||||||
const titled = title?.(item);
|
const titled = title?.(item);
|
||||||
|
const iconed = icon?.(item) || 'codicon-blank';
|
||||||
|
|
||||||
return <div ref={itemRef} role='treeitem' aria-selected={item === selectedItem} aria-expanded={expanded} aria-label={titled} title={titled} className='vbox' style={{ flex: 'none' }}>
|
return <div ref={itemRef} role='treeitem' aria-selected={item === selectedItem} aria-expanded={expanded} title={titled} className='vbox' style={{ flex: 'none' }}>
|
||||||
<div
|
<div
|
||||||
onDoubleClick={() => onAccepted?.(item)}
|
onDoubleClick={() => onAccepted?.(item)}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
|
@ -277,10 +278,10 @@ export function TreeItemHeader<T extends TreeItem>({
|
||||||
toggleExpanded(item);
|
toggleExpanded(item);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{icon && <div className={'codicon ' + (icon(item) || 'codicon-blank')} style={{ minWidth: 16, marginRight: 4 }} aria-hidden='true'></div>}
|
{icon && <div className={'codicon ' + iconed} style={{ minWidth: 16, marginRight: 4 }} aria-label={'[' + iconed.replace('codicon', 'icon') + ']'}></div>}
|
||||||
{typeof rendered === 'string' ? <div style={{ textOverflow: 'ellipsis', overflow: 'hidden' }}>{rendered}</div> : rendered}
|
{typeof rendered === 'string' ? <div style={{ textOverflow: 'ellipsis', overflow: 'hidden' }}>{rendered}</div> : rendered}
|
||||||
</div>
|
</div>
|
||||||
{!!children.length && <div aria-label='group'>
|
{!!children.length && <div role='group'>
|
||||||
{children.map(child => {
|
{children.map(child => {
|
||||||
const itemData = treeItems.get(child);
|
const itemData = treeItems.get(child);
|
||||||
return itemData && <TreeItemHeader
|
return itemData && <TreeItemHeader
|
||||||
|
|
|
||||||
157
tests/library/sequence.spec.ts
Normal file
157
tests/library/sequence.spec.ts
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
/**
|
||||||
|
* 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 { test as it, expect } from '@playwright/test';
|
||||||
|
import { findRepeatedSubsequences } from '../../packages/playwright-core/lib/utils/sequence';
|
||||||
|
|
||||||
|
it('should return an empty array when the input is empty', () => {
|
||||||
|
const input = [];
|
||||||
|
const expectedOutput = [];
|
||||||
|
expect(findRepeatedSubsequences(input)).toEqual(expectedOutput);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle a single-element array', () => {
|
||||||
|
const input = ['a'];
|
||||||
|
const expectedOutput = [{ sequence: ['a'], count: 1 }];
|
||||||
|
expect(findRepeatedSubsequences(input)).toEqual(expectedOutput);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle an array with no repeats', () => {
|
||||||
|
const input = ['a', 'b', 'c'];
|
||||||
|
const expectedOutput = [
|
||||||
|
{ sequence: ['a'], count: 1 },
|
||||||
|
{ sequence: ['b'], count: 1 },
|
||||||
|
{ sequence: ['c'], count: 1 },
|
||||||
|
];
|
||||||
|
expect(findRepeatedSubsequences(input)).toEqual(expectedOutput);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle contiguous repeats of single elements', () => {
|
||||||
|
const input = ['a', 'a', 'a', 'b', 'b', 'c'];
|
||||||
|
const expectedOutput = [
|
||||||
|
{ sequence: ['a'], count: 3 },
|
||||||
|
{ sequence: ['b'], count: 2 },
|
||||||
|
{ sequence: ['c'], count: 1 },
|
||||||
|
];
|
||||||
|
expect(findRepeatedSubsequences(input)).toEqual(expectedOutput);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect longer repeating substrings', () => {
|
||||||
|
const input = ['a', 'b', 'a', 'b', 'a', 'b'];
|
||||||
|
const expectedOutput = [{ sequence: ['a', 'b'], count: 3 }];
|
||||||
|
expect(findRepeatedSubsequences(input)).toEqual(expectedOutput);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple repeating substrings', () => {
|
||||||
|
const input = ['a', 'a', 'b', 'b', 'a', 'a', 'b', 'b'];
|
||||||
|
const expectedOutput = [
|
||||||
|
{ sequence: ['a', 'a', 'b', 'b'], count: 2 },
|
||||||
|
];
|
||||||
|
expect(findRepeatedSubsequences(input)).toEqual(expectedOutput);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle complex cases with overlapping repeats', () => {
|
||||||
|
const input = ['a', 'a', 'a', 'a'];
|
||||||
|
const expectedOutput = [{ sequence: ['a'], count: 4 }];
|
||||||
|
expect(findRepeatedSubsequences(input)).toEqual(expectedOutput);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle complex acceptance cases with multiple possible repeats', () => {
|
||||||
|
const input = ['a', 'a', 'b', 'b', 'a', 'a', 'b', 'b', 'c', 'c', 'c', 'c'];
|
||||||
|
const expectedOutput = [
|
||||||
|
{ sequence: ['a', 'a', 'b', 'b'], count: 2 },
|
||||||
|
{ sequence: ['c'], count: 4 },
|
||||||
|
];
|
||||||
|
expect(findRepeatedSubsequences(input)).toEqual(expectedOutput);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle non-repeating sequences correctly', () => {
|
||||||
|
const input = ['a', 'b', 'c', 'd', 'e'];
|
||||||
|
const expectedOutput = [
|
||||||
|
{ sequence: ['a'], count: 1 },
|
||||||
|
{ sequence: ['b'], count: 1 },
|
||||||
|
{ sequence: ['c'], count: 1 },
|
||||||
|
{ sequence: ['d'], count: 1 },
|
||||||
|
{ sequence: ['e'], count: 1 },
|
||||||
|
];
|
||||||
|
expect(findRepeatedSubsequences(input)).toEqual(expectedOutput);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle a case where the entire array is a repeating sequence', () => {
|
||||||
|
const input = ['x', 'y', 'x', 'y', 'x', 'y'];
|
||||||
|
const expectedOutput = [{ sequence: ['x', 'y'], count: 3 }];
|
||||||
|
expect(findRepeatedSubsequences(input)).toEqual(expectedOutput);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly identify the maximal repeating substring', () => {
|
||||||
|
const input = ['a', 'b', 'a', 'b', 'a', 'b', 'c', 'c', 'c', 'c'];
|
||||||
|
const expectedOutput = [
|
||||||
|
{ sequence: ['a', 'b'], count: 3 },
|
||||||
|
{ sequence: ['c'], count: 4 },
|
||||||
|
];
|
||||||
|
expect(findRepeatedSubsequences(input)).toEqual(expectedOutput);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle repeats with varying lengths', () => {
|
||||||
|
const input = ['a', 'a', 'b', 'b', 'b', 'b', 'a', 'a'];
|
||||||
|
const expectedOutput = [
|
||||||
|
{ sequence: ['a'], count: 2 },
|
||||||
|
{ sequence: ['b'], count: 4 },
|
||||||
|
{ sequence: ['a'], count: 2 },
|
||||||
|
];
|
||||||
|
expect(findRepeatedSubsequences(input)).toEqual(expectedOutput);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly handle a repeat count of one (k adjustment to zero)', () => {
|
||||||
|
const input = ['a', 'b', 'a', 'b', 'c'];
|
||||||
|
const expectedOutput = [
|
||||||
|
{ sequence: ['a', 'b'], count: 2 },
|
||||||
|
{ sequence: ['c'], count: 1 },
|
||||||
|
];
|
||||||
|
expect(findRepeatedSubsequences(input)).toEqual(expectedOutput);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly handle repeats at the end of the array', () => {
|
||||||
|
const input = ['x', 'y', 'x', 'y', 'x', 'y', 'z'];
|
||||||
|
const expectedOutput = [
|
||||||
|
{ sequence: ['x', 'y'], count: 3 },
|
||||||
|
{ sequence: ['z'], count: 1 },
|
||||||
|
];
|
||||||
|
expect(findRepeatedSubsequences(input)).toEqual(expectedOutput);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not overcount repeats when the last potential repeat is incomplete', () => {
|
||||||
|
const input = ['m', 'n', 'm', 'n', 'm'];
|
||||||
|
const expectedOutput = [
|
||||||
|
{ sequence: ['m', 'n'], count: 2 },
|
||||||
|
{ sequence: ['m'], count: 1 },
|
||||||
|
];
|
||||||
|
expect(findRepeatedSubsequences(input)).toEqual(expectedOutput);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle single repeats correctly when the substring length is greater than one', () => {
|
||||||
|
const input = ['a', 'b', 'c', 'a', 'b', 'd'];
|
||||||
|
const expectedOutput = [
|
||||||
|
{ sequence: ['a'], count: 1 },
|
||||||
|
{ sequence: ['b'], count: 1 },
|
||||||
|
{ sequence: ['c'], count: 1 },
|
||||||
|
{ sequence: ['a'], count: 1 },
|
||||||
|
{ sequence: ['b'], count: 1 },
|
||||||
|
{ sequence: ['d'], count: 1 },
|
||||||
|
];
|
||||||
|
expect(findRepeatedSubsequences(input)).toEqual(expectedOutput);
|
||||||
|
});
|
||||||
|
|
@ -24,12 +24,16 @@ test('toMatchText-based assertions should have matcher result', async ({ page })
|
||||||
{
|
{
|
||||||
const e = await expect(locator).toHaveText(/Text2/, { timeout: 1 }).catch(e => e);
|
const e = await expect(locator).toHaveText(/Text2/, { timeout: 1 }).catch(e => e);
|
||||||
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
||||||
|
e.matcherResult.printedDiff = stripAnsi(e.matcherResult.printedDiff);
|
||||||
expect.soft(e.matcherResult).toEqual({
|
expect.soft(e.matcherResult).toEqual({
|
||||||
actual: 'Text content',
|
actual: 'Text content',
|
||||||
expected: /Text2/,
|
expected: /Text2/,
|
||||||
message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).toHaveText(expected)`),
|
message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).toHaveText(expected)`),
|
||||||
name: 'toHaveText',
|
name: 'toHaveText',
|
||||||
pass: false,
|
pass: false,
|
||||||
|
locator: `locator('#node')`,
|
||||||
|
printedDiff: `Expected pattern: /Text2/
|
||||||
|
Received string: \"Text content\"`,
|
||||||
log: expect.any(Array),
|
log: expect.any(Array),
|
||||||
timeout: 1,
|
timeout: 1,
|
||||||
});
|
});
|
||||||
|
|
@ -46,12 +50,17 @@ Call log`);
|
||||||
{
|
{
|
||||||
const e = await expect(locator).not.toHaveText(/Text/, { timeout: 1 }).catch(e => e);
|
const e = await expect(locator).not.toHaveText(/Text/, { timeout: 1 }).catch(e => e);
|
||||||
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
||||||
|
e.matcherResult.printedExpected = stripAnsi(e.matcherResult.printedExpected);
|
||||||
|
e.matcherResult.printedReceived = stripAnsi(e.matcherResult.printedReceived);
|
||||||
expect.soft(e.matcherResult).toEqual({
|
expect.soft(e.matcherResult).toEqual({
|
||||||
actual: 'Text content',
|
actual: 'Text content',
|
||||||
expected: /Text/,
|
expected: /Text/,
|
||||||
message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).not.toHaveText(expected)`),
|
message: expect.stringContaining(`Timed out 1ms waiting for expect(locator).not.toHaveText(expected)`),
|
||||||
name: 'toHaveText',
|
name: 'toHaveText',
|
||||||
pass: true,
|
pass: true,
|
||||||
|
locator: `locator('#node')`,
|
||||||
|
printedExpected: 'Expected pattern: not /Text/',
|
||||||
|
printedReceived: `Received string: \"Text content\"`,
|
||||||
log: expect.any(Array),
|
log: expect.any(Array),
|
||||||
timeout: 1,
|
timeout: 1,
|
||||||
});
|
});
|
||||||
|
|
@ -79,6 +88,8 @@ test('toBeTruthy-based assertions should have matcher result', async ({ page })
|
||||||
name: 'toBeVisible',
|
name: 'toBeVisible',
|
||||||
pass: false,
|
pass: false,
|
||||||
log: expect.any(Array),
|
log: expect.any(Array),
|
||||||
|
printedExpected: 'Expected: visible',
|
||||||
|
printedReceived: 'Received: <element(s) not found>',
|
||||||
timeout: 1,
|
timeout: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -101,6 +112,8 @@ Call log`);
|
||||||
name: 'toBeVisible',
|
name: 'toBeVisible',
|
||||||
pass: true,
|
pass: true,
|
||||||
log: expect.any(Array),
|
log: expect.any(Array),
|
||||||
|
printedExpected: 'Expected: not visible',
|
||||||
|
printedReceived: 'Received: visible',
|
||||||
timeout: 1,
|
timeout: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -120,6 +133,7 @@ test('toEqual-based assertions should have matcher result', async ({ page }) =>
|
||||||
{
|
{
|
||||||
const e = await expect(page.locator('#node2')).toHaveCount(1, { timeout: 1 }).catch(e => e);
|
const e = await expect(page.locator('#node2')).toHaveCount(1, { timeout: 1 }).catch(e => e);
|
||||||
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
||||||
|
e.matcherResult.printedDiff = stripAnsi(e.matcherResult.printedDiff);
|
||||||
expect.soft(e.matcherResult).toEqual({
|
expect.soft(e.matcherResult).toEqual({
|
||||||
actual: 0,
|
actual: 0,
|
||||||
expected: 1,
|
expected: 1,
|
||||||
|
|
@ -127,6 +141,8 @@ test('toEqual-based assertions should have matcher result', async ({ page }) =>
|
||||||
name: 'toHaveCount',
|
name: 'toHaveCount',
|
||||||
pass: false,
|
pass: false,
|
||||||
log: expect.any(Array),
|
log: expect.any(Array),
|
||||||
|
printedDiff: `Expected: 1
|
||||||
|
Received: 0`,
|
||||||
timeout: 1,
|
timeout: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -141,6 +157,8 @@ Call log`);
|
||||||
{
|
{
|
||||||
const e = await expect(page.locator('#node')).not.toHaveCount(1, { timeout: 1 }).catch(e => e);
|
const e = await expect(page.locator('#node')).not.toHaveCount(1, { timeout: 1 }).catch(e => e);
|
||||||
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
e.matcherResult.message = stripAnsi(e.matcherResult.message);
|
||||||
|
e.matcherResult.printedExpected = stripAnsi(e.matcherResult.printedExpected);
|
||||||
|
e.matcherResult.printedReceived = stripAnsi(e.matcherResult.printedReceived);
|
||||||
expect.soft(e.matcherResult).toEqual({
|
expect.soft(e.matcherResult).toEqual({
|
||||||
actual: 1,
|
actual: 1,
|
||||||
expected: 1,
|
expected: 1,
|
||||||
|
|
@ -148,6 +166,8 @@ Call log`);
|
||||||
name: 'toHaveCount',
|
name: 'toHaveCount',
|
||||||
pass: true,
|
pass: true,
|
||||||
log: expect.any(Array),
|
log: expect.any(Array),
|
||||||
|
printedExpected: `Expected: not 1`,
|
||||||
|
printedReceived: `Received: 1`,
|
||||||
timeout: 1,
|
timeout: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -177,6 +197,8 @@ test('toBeChecked({ checked: false }) should have expected: false', async ({ pag
|
||||||
name: 'toBeChecked',
|
name: 'toBeChecked',
|
||||||
pass: false,
|
pass: false,
|
||||||
log: expect.any(Array),
|
log: expect.any(Array),
|
||||||
|
printedExpected: 'Expected: checked',
|
||||||
|
printedReceived: 'Received: unchecked',
|
||||||
timeout: 1,
|
timeout: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -199,6 +221,8 @@ Call log`);
|
||||||
name: 'toBeChecked',
|
name: 'toBeChecked',
|
||||||
pass: true,
|
pass: true,
|
||||||
log: expect.any(Array),
|
log: expect.any(Array),
|
||||||
|
printedExpected: 'Expected: not checked',
|
||||||
|
printedReceived: 'Received: checked',
|
||||||
timeout: 1,
|
timeout: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -221,6 +245,8 @@ Call log`);
|
||||||
name: 'toBeChecked',
|
name: 'toBeChecked',
|
||||||
pass: false,
|
pass: false,
|
||||||
log: expect.any(Array),
|
log: expect.any(Array),
|
||||||
|
printedExpected: 'Expected: unchecked',
|
||||||
|
printedReceived: 'Received: checked',
|
||||||
timeout: 1,
|
timeout: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -243,6 +269,8 @@ Call log`);
|
||||||
name: 'toBeChecked',
|
name: 'toBeChecked',
|
||||||
pass: true,
|
pass: true,
|
||||||
log: expect.any(Array),
|
log: expect.any(Array),
|
||||||
|
printedExpected: 'Expected: not unchecked',
|
||||||
|
printedReceived: 'Received: unchecked',
|
||||||
timeout: 1,
|
timeout: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -271,6 +299,8 @@ test('toHaveScreenshot should populate matcherResult', async ({ page, server, is
|
||||||
name: 'toHaveScreenshot',
|
name: 'toHaveScreenshot',
|
||||||
pass: false,
|
pass: false,
|
||||||
log: expect.any(Array),
|
log: expect.any(Array),
|
||||||
|
printedExpected: expect.stringContaining('screenshot-sanity-'),
|
||||||
|
printedReceived: expect.stringContaining('screenshot-sanity-actual'),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect.soft(stripAnsi(e.toString())).toContain(`Error: Screenshot comparison failed:
|
expect.soft(stripAnsi(e.toString())).toContain(`Error: Screenshot comparison failed:
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,15 @@
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/test": "1.49.0-alpha-2024-10-17"
|
"@playwright/test": "1.49.0-alpha-2024-10-20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@playwright/test": {
|
"node_modules/@playwright/test": {
|
||||||
"version": "1.49.0-alpha-2024-10-17",
|
"version": "1.49.0-alpha-2024-10-20",
|
||||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-alpha-2024-10-17.tgz",
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-alpha-2024-10-20.tgz",
|
||||||
"integrity": "sha512-HLZY3sM6xt9Wi8K09zPwjJQtcUBZNBcNSIVoMZhtJM3+TikCKx4SiJ3P8vbSlk7Tm3s2oqlS+wA181IxhbTGBA==",
|
"integrity": "sha512-lSagJ8KSD636T/TNfSJRh+vuBBssCL5xJgYmsvsF37cDMATTdVf2OVozVK91V9MAL7CxP4F5sQFVq/8rqu23WA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright": "1.49.0-alpha-2024-10-17"
|
"playwright": "1.49.0-alpha-2024-10-20"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
|
|
@ -36,11 +36,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright": {
|
"node_modules/playwright": {
|
||||||
"version": "1.49.0-alpha-2024-10-17",
|
"version": "1.49.0-alpha-2024-10-20",
|
||||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-alpha-2024-10-17.tgz",
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-alpha-2024-10-20.tgz",
|
||||||
"integrity": "sha512-IgcLunnpocVS/AEq2lcftVOu0DGQzFm1Qt25SCJsrVvKVe83ElKXZYskPz7yA0HeuOVxQyN69EDWI09ph7lfoQ==",
|
"integrity": "sha512-lkZXCaLoVKaa3eVu8qJJiLym6SkjXD+ilE4XZJx3AIE0o4vqMEYVB8tjLzAcl4UZx8wVcCps/WcCvTWhOSIXRA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.49.0-alpha-2024-10-17"
|
"playwright-core": "1.49.0-alpha-2024-10-20"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
|
|
@ -53,9 +53,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright-core": {
|
"node_modules/playwright-core": {
|
||||||
"version": "1.49.0-alpha-2024-10-17",
|
"version": "1.49.0-alpha-2024-10-20",
|
||||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-alpha-2024-10-17.tgz",
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-alpha-2024-10-20.tgz",
|
||||||
"integrity": "sha512-XLTKmPBm2ZIOXBckXtiimSOIjQsYy8MqEP9CsHSgytsP0E+j/44v1BuwHOOMaG8sfjcuZLZ1QdFidnl07A9wSg==",
|
"integrity": "sha512-TeQNA7vsGVrHaArr+giPyiWPAV27+wIcuMLrAJXzUB0leVA9bkXbNQ5lA5+G4OhqlmYAbMOpJMtN+TREDv4nXA==",
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright-core": "cli.js"
|
"playwright-core": "cli.js"
|
||||||
},
|
},
|
||||||
|
|
@ -66,11 +66,11 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/test": {
|
"@playwright/test": {
|
||||||
"version": "1.49.0-alpha-2024-10-17",
|
"version": "1.49.0-alpha-2024-10-20",
|
||||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-alpha-2024-10-17.tgz",
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.0-alpha-2024-10-20.tgz",
|
||||||
"integrity": "sha512-HLZY3sM6xt9Wi8K09zPwjJQtcUBZNBcNSIVoMZhtJM3+TikCKx4SiJ3P8vbSlk7Tm3s2oqlS+wA181IxhbTGBA==",
|
"integrity": "sha512-lSagJ8KSD636T/TNfSJRh+vuBBssCL5xJgYmsvsF37cDMATTdVf2OVozVK91V9MAL7CxP4F5sQFVq/8rqu23WA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"playwright": "1.49.0-alpha-2024-10-17"
|
"playwright": "1.49.0-alpha-2024-10-20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fsevents": {
|
"fsevents": {
|
||||||
|
|
@ -80,18 +80,18 @@
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"playwright": {
|
"playwright": {
|
||||||
"version": "1.49.0-alpha-2024-10-17",
|
"version": "1.49.0-alpha-2024-10-20",
|
||||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-alpha-2024-10-17.tgz",
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.0-alpha-2024-10-20.tgz",
|
||||||
"integrity": "sha512-IgcLunnpocVS/AEq2lcftVOu0DGQzFm1Qt25SCJsrVvKVe83ElKXZYskPz7yA0HeuOVxQyN69EDWI09ph7lfoQ==",
|
"integrity": "sha512-lkZXCaLoVKaa3eVu8qJJiLym6SkjXD+ilE4XZJx3AIE0o4vqMEYVB8tjLzAcl4UZx8wVcCps/WcCvTWhOSIXRA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"fsevents": "2.3.2",
|
"fsevents": "2.3.2",
|
||||||
"playwright-core": "1.49.0-alpha-2024-10-17"
|
"playwright-core": "1.49.0-alpha-2024-10-20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"playwright-core": {
|
"playwright-core": {
|
||||||
"version": "1.49.0-alpha-2024-10-17",
|
"version": "1.49.0-alpha-2024-10-20",
|
||||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-alpha-2024-10-17.tgz",
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.0-alpha-2024-10-20.tgz",
|
||||||
"integrity": "sha512-XLTKmPBm2ZIOXBckXtiimSOIjQsYy8MqEP9CsHSgytsP0E+j/44v1BuwHOOMaG8sfjcuZLZ1QdFidnl07A9wSg=="
|
"integrity": "sha512-TeQNA7vsGVrHaArr+giPyiWPAV27+wIcuMLrAJXzUB0leVA9bkXbNQ5lA5+G4OhqlmYAbMOpJMtN+TREDv4nXA=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/test": "1.49.0-alpha-2024-10-17"
|
"@playwright/test": "1.49.0-alpha-2024-10-20"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,22 +61,25 @@ test('should run visible', async ({ runUITest }) => {
|
||||||
⊘ skipped
|
⊘ skipped
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
|
await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
|
||||||
// - tree:
|
- tree:
|
||||||
// - treeitem "a.test.ts" [expanded]:
|
- treeitem "[icon-error] a.test.ts" [expanded]:
|
||||||
// - treeitem "passes"
|
- group:
|
||||||
// - treeitem "fails" [selected]:
|
- treeitem ${/\[icon-check\] passes \d+ms/}
|
||||||
// - button "Run"
|
- treeitem ${/\[icon-error\] fails \d+ms/} [selected]:
|
||||||
// - button "Show source"
|
- button "Run"
|
||||||
// - button "Watch"
|
- button "Show source"
|
||||||
// - treeitem "suite"
|
- button "Watch"
|
||||||
// - treeitem "b.test.ts" [expanded]:
|
- treeitem "[icon-error] suite"
|
||||||
// - treeitem "passes"
|
- treeitem "[icon-error] b.test.ts" [expanded]:
|
||||||
// - treeitem "fails"
|
- group:
|
||||||
// - treeitem "c.test.ts" [expanded]:
|
- treeitem ${/\[icon-check\] passes \d+ms/}
|
||||||
// - treeitem "passes"
|
- treeitem ${/\[icon-error\] fails \d+ms/}
|
||||||
// - treeitem "skipped"
|
- treeitem "[icon-check] c.test.ts" [expanded]:
|
||||||
// `);
|
- group:
|
||||||
|
- treeitem ${/\[icon-check\] passes \d+ms/}
|
||||||
|
- treeitem "[icon-circle-slash] skipped"
|
||||||
|
`);
|
||||||
|
|
||||||
await expect(page.getByTestId('status-line')).toHaveText('4/8 passed (50%)');
|
await expect(page.getByTestId('status-line')).toHaveText('4/8 passed (50%)');
|
||||||
});
|
});
|
||||||
|
|
@ -117,6 +120,17 @@ test('should run on hover', async ({ runUITest }) => {
|
||||||
✅ passes <=
|
✅ passes <=
|
||||||
◯ fails
|
◯ fails
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
|
||||||
|
- tree:
|
||||||
|
- treeitem "[icon-circle-outline] a.test.ts" [expanded]:
|
||||||
|
- group:
|
||||||
|
- treeitem ${/\[icon-check\] passes \d+ms/}:
|
||||||
|
- button "Run"
|
||||||
|
- button "Show source"
|
||||||
|
- button "Watch"
|
||||||
|
- treeitem "[icon-circle-outline] fails"
|
||||||
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should run on double click', async ({ runUITest }) => {
|
test('should run on double click', async ({ runUITest }) => {
|
||||||
|
|
@ -135,6 +149,17 @@ test('should run on double click', async ({ runUITest }) => {
|
||||||
✅ passes <=
|
✅ passes <=
|
||||||
◯ fails
|
◯ fails
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
|
||||||
|
- tree:
|
||||||
|
- treeitem "[icon-circle-outline] a.test.ts" [expanded]:
|
||||||
|
- group:
|
||||||
|
- treeitem ${/\[icon-check\] passes/} [selected]:
|
||||||
|
- button "Run"
|
||||||
|
- button "Show source"
|
||||||
|
- button "Watch"
|
||||||
|
- treeitem "[icon-circle-outline] fails"
|
||||||
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should run on Enter', async ({ runUITest }) => {
|
test('should run on Enter', async ({ runUITest }) => {
|
||||||
|
|
@ -154,6 +179,17 @@ test('should run on Enter', async ({ runUITest }) => {
|
||||||
◯ passes
|
◯ passes
|
||||||
❌ fails <=
|
❌ fails <=
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
|
||||||
|
- tree:
|
||||||
|
- treeitem "[icon-error] a.test.ts" [expanded]:
|
||||||
|
- group:
|
||||||
|
- treeitem "[icon-circle-outline] passes"
|
||||||
|
- treeitem ${/\[icon-error\] fails \d+ms/} [selected]:
|
||||||
|
- button "Run"
|
||||||
|
- button "Show source"
|
||||||
|
- button "Watch"
|
||||||
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should run by project', async ({ runUITest }) => {
|
test('should run by project', async ({ runUITest }) => {
|
||||||
|
|
@ -185,6 +221,26 @@ test('should run by project', async ({ runUITest }) => {
|
||||||
⊘ skipped
|
⊘ skipped
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
|
||||||
|
- tree:
|
||||||
|
- treeitem "[icon-error] a.test.ts" [expanded]:
|
||||||
|
- group:
|
||||||
|
- treeitem ${/\[icon-check\] passes \d+ms/}
|
||||||
|
- treeitem ${/\[icon-error\] fails \d+ms/} [selected]:
|
||||||
|
- button "Run"
|
||||||
|
- button "Show source"
|
||||||
|
- button "Watch"
|
||||||
|
- treeitem "[icon-error] suite"
|
||||||
|
- treeitem "[icon-error] b.test.ts" [expanded]:
|
||||||
|
- group:
|
||||||
|
- treeitem ${/\[icon-check\] passes \d+ms/}
|
||||||
|
- treeitem ${/\[icon-error\] fails \d+ms/}
|
||||||
|
- treeitem "[icon-check] c.test.ts" [expanded]:
|
||||||
|
- group:
|
||||||
|
- treeitem ${/\[icon-check\] passes \d+ms/}
|
||||||
|
- treeitem "[icon-circle-slash] skipped"
|
||||||
|
`);
|
||||||
|
|
||||||
await page.getByText('Status:').click();
|
await page.getByText('Status:').click();
|
||||||
await page.getByLabel('bar').setChecked(true);
|
await page.getByLabel('bar').setChecked(true);
|
||||||
|
|
||||||
|
|
@ -203,6 +259,29 @@ test('should run by project', async ({ runUITest }) => {
|
||||||
► ◯ skipped
|
► ◯ skipped
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
|
||||||
|
- tree:
|
||||||
|
- treeitem "[icon-error] a.test.ts" [expanded]:
|
||||||
|
- group:
|
||||||
|
- treeitem ${/\[icon-circle-outline\] passes/}
|
||||||
|
- treeitem ${/\[icon-error\] fails/}:
|
||||||
|
- group:
|
||||||
|
- treeitem ${/\[icon-error\] foo/} [selected]:
|
||||||
|
- button "Run"
|
||||||
|
- button "Show source"
|
||||||
|
- button "Watch"
|
||||||
|
- treeitem "[icon-circle-outline] bar"
|
||||||
|
- treeitem "[icon-error] suite"
|
||||||
|
- treeitem "[icon-error] b.test.ts" [expanded]:
|
||||||
|
- group:
|
||||||
|
- treeitem ${/\[icon-circle-outline\] passes/}
|
||||||
|
- treeitem ${/\[icon-error\] fails/}
|
||||||
|
- treeitem "[icon-circle-outline] c.test.ts" [expanded]:
|
||||||
|
- group:
|
||||||
|
- treeitem ${/\[icon-circle-outline\] passes/}
|
||||||
|
- treeitem ${/\[icon-circle-outline\] skipped/}
|
||||||
|
`);
|
||||||
|
|
||||||
await page.getByText('Status:').click();
|
await page.getByText('Status:').click();
|
||||||
|
|
||||||
await page.getByTestId('test-tree').getByText('passes').first().click();
|
await page.getByTestId('test-tree').getByText('passes').first().click();
|
||||||
|
|
@ -216,6 +295,20 @@ test('should run by project', async ({ runUITest }) => {
|
||||||
► ❌ fails
|
► ❌ fails
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
|
||||||
|
- tree:
|
||||||
|
- treeitem "[icon-error] a.test.ts" [expanded]:
|
||||||
|
- group:
|
||||||
|
- treeitem ${/\[icon-circle-outline\] passes \d+ms/} [expanded] [selected]:
|
||||||
|
- button "Run"
|
||||||
|
- button "Show source"
|
||||||
|
- button "Watch"
|
||||||
|
- group:
|
||||||
|
- treeitem ${/\[icon-check\] foo \d+ms/}
|
||||||
|
- treeitem ${/\[icon-circle-outline\] bar/}
|
||||||
|
- treeitem ${/\[icon-error\] fails \d+ms/}
|
||||||
|
`);
|
||||||
|
|
||||||
await expect(page.getByText('Projects: foo bar')).toBeVisible();
|
await expect(page.getByText('Projects: foo bar')).toBeVisible();
|
||||||
|
|
||||||
await page.getByTitle('Run all').click();
|
await page.getByTitle('Run all').click();
|
||||||
|
|
@ -235,6 +328,32 @@ test('should run by project', async ({ runUITest }) => {
|
||||||
► ✅ passes
|
► ✅ passes
|
||||||
► ⊘ skipped
|
► ⊘ skipped
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
|
||||||
|
- tree:
|
||||||
|
- treeitem "[icon-error] a.test.ts" [expanded]:
|
||||||
|
- group:
|
||||||
|
- treeitem ${/\[icon-check\] passes \d+ms/} [expanded]:
|
||||||
|
- group:
|
||||||
|
- treeitem ${/\[icon-check\] foo \d+ms/}
|
||||||
|
- treeitem ${/\[icon-check\] bar \d+ms/}
|
||||||
|
- treeitem ${/\[icon-error\] fails \d+ms/} [expanded]:
|
||||||
|
- group:
|
||||||
|
- treeitem ${/\[icon-error\] foo \d+ms/} [selected]:
|
||||||
|
- button "Run"
|
||||||
|
- button "Show source"
|
||||||
|
- button "Watch"
|
||||||
|
- treeitem ${/\[icon-error\] bar \d+ms/}
|
||||||
|
- treeitem ${/\[icon-error\] suite/}
|
||||||
|
- treeitem "[icon-error] b.test.ts" [expanded]:
|
||||||
|
- group:
|
||||||
|
- treeitem ${/\[icon-check\] passes/}
|
||||||
|
- treeitem ${/\[icon-error\] fails/}
|
||||||
|
- treeitem "[icon-check] c.test.ts" [expanded]:
|
||||||
|
- group:
|
||||||
|
- treeitem ${/\[icon-check\] passes/}
|
||||||
|
- treeitem ${/\[icon-circle-slash\] skipped/}
|
||||||
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should stop', async ({ runUITest }) => {
|
test('should stop', async ({ runUITest }) => {
|
||||||
|
|
@ -261,6 +380,16 @@ test('should stop', async ({ runUITest }) => {
|
||||||
🕦 test 3
|
🕦 test 3
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
|
||||||
|
- tree:
|
||||||
|
- treeitem "[icon-loading] a.test.ts" [expanded]:
|
||||||
|
- group:
|
||||||
|
- treeitem "[icon-circle-slash] test 0"
|
||||||
|
- treeitem ${/\[icon-check\] test 1 \d+ms/}
|
||||||
|
- treeitem ${/\[icon-loading\] test 2/}
|
||||||
|
- treeitem ${/\[icon-clock\] test 3/}
|
||||||
|
`);
|
||||||
|
|
||||||
await expect(page.getByTitle('Run all')).toBeDisabled();
|
await expect(page.getByTitle('Run all')).toBeDisabled();
|
||||||
await expect(page.getByTitle('Stop')).toBeEnabled();
|
await expect(page.getByTitle('Stop')).toBeEnabled();
|
||||||
|
|
||||||
|
|
@ -273,6 +402,16 @@ test('should stop', async ({ runUITest }) => {
|
||||||
◯ test 2
|
◯ test 2
|
||||||
◯ test 3
|
◯ test 3
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
|
||||||
|
- tree:
|
||||||
|
- treeitem "[icon-circle-outline] a.test.ts" [expanded]:
|
||||||
|
- group:
|
||||||
|
- treeitem "[icon-circle-slash] test 0"
|
||||||
|
- treeitem ${/\[icon-check\] test 1 \d+ms/}
|
||||||
|
- treeitem ${/\[icon-circle-outline\] test 2/}
|
||||||
|
- treeitem ${/\[icon-circle-outline\] test 3/}
|
||||||
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should run folder', async ({ runUITest }) => {
|
test('should run folder', async ({ runUITest }) => {
|
||||||
|
|
@ -301,6 +440,17 @@ test('should run folder', async ({ runUITest }) => {
|
||||||
▼ ◯ in-a.test.ts
|
▼ ◯ in-a.test.ts
|
||||||
◯ passes
|
◯ passes
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
|
||||||
|
- tree:
|
||||||
|
- treeitem "[icon-check] folder-b" [expanded] [selected]:
|
||||||
|
- group:
|
||||||
|
- treeitem "[icon-check] folder-c"
|
||||||
|
- treeitem "[icon-check] in-b.test.ts"
|
||||||
|
- treeitem "[icon-circle-outline] in-a.test.ts" [expanded]:
|
||||||
|
- group:
|
||||||
|
- treeitem "[icon-circle-outline] passes"
|
||||||
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show time', async ({ runUITest }) => {
|
test('should show time', async ({ runUITest }) => {
|
||||||
|
|
@ -324,6 +474,26 @@ test('should show time', async ({ runUITest }) => {
|
||||||
⊘ skipped
|
⊘ skipped
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
|
||||||
|
- tree:
|
||||||
|
- treeitem "[icon-error] a.test.ts" [expanded]:
|
||||||
|
- group:
|
||||||
|
- treeitem ${/\[icon-check\] passes \d+ms/}
|
||||||
|
- treeitem ${/\[icon-error\] fails \d+ms/} [selected]:
|
||||||
|
- button "Run"
|
||||||
|
- button "Show source"
|
||||||
|
- button "Watch"
|
||||||
|
- treeitem "[icon-error] suite"
|
||||||
|
- treeitem "[icon-error] b.test.ts" [expanded]:
|
||||||
|
- group:
|
||||||
|
- treeitem ${/\[icon-check\] passes \d+ms/}
|
||||||
|
- treeitem ${/\[icon-error\] fails \d+ms/}
|
||||||
|
- treeitem "[icon-check] c.test.ts" [expanded]:
|
||||||
|
- group:
|
||||||
|
- treeitem ${/\[icon-check\] passes \d+ms/}
|
||||||
|
- treeitem "[icon-circle-slash] skipped"
|
||||||
|
`);
|
||||||
|
|
||||||
await expect(page.getByTestId('status-line')).toHaveText('4/8 passed (50%)');
|
await expect(page.getByTestId('status-line')).toHaveText('4/8 passed (50%)');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -348,6 +518,13 @@ test('should show test.fail as passing', async ({ runUITest }) => {
|
||||||
✅ should fail XXms
|
✅ should fail XXms
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
|
||||||
|
- tree:
|
||||||
|
- treeitem "[icon-check] a.test.ts" [expanded]:
|
||||||
|
- group:
|
||||||
|
- treeitem ${/\[icon-check\] should fail \d+ms/}
|
||||||
|
`);
|
||||||
|
|
||||||
await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
|
await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -377,6 +554,13 @@ test('should ignore repeatEach', async ({ runUITest }) => {
|
||||||
✅ should pass
|
✅ should pass
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
|
||||||
|
- tree:
|
||||||
|
- treeitem "[icon-check] a.test.ts" [expanded]:
|
||||||
|
- group:
|
||||||
|
- treeitem ${/\[icon-check\] should pass \d+ms/}
|
||||||
|
`);
|
||||||
|
|
||||||
await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
|
await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -404,6 +588,14 @@ test('should remove output folder before test run', async ({ runUITest }) => {
|
||||||
▼ ✅ a.test.ts
|
▼ ✅ a.test.ts
|
||||||
✅ should pass
|
✅ should pass
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
|
||||||
|
- tree:
|
||||||
|
- treeitem "[icon-check] a.test.ts" [expanded]:
|
||||||
|
- group:
|
||||||
|
- treeitem ${/\[icon-check\] should pass \d+ms/}
|
||||||
|
`);
|
||||||
|
|
||||||
await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
|
await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
|
||||||
|
|
||||||
await page.getByTitle('Run all').click();
|
await page.getByTitle('Run all').click();
|
||||||
|
|
@ -411,6 +603,14 @@ test('should remove output folder before test run', async ({ runUITest }) => {
|
||||||
▼ ✅ a.test.ts
|
▼ ✅ a.test.ts
|
||||||
✅ should pass
|
✅ should pass
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
|
||||||
|
- tree:
|
||||||
|
- treeitem "[icon-check] a.test.ts" [expanded]:
|
||||||
|
- group:
|
||||||
|
- treeitem ${/\[icon-check\] should pass \d+ms/}
|
||||||
|
`);
|
||||||
|
|
||||||
await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
|
await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -451,6 +651,18 @@ test('should show proper total when using deps', async ({ runUITest }) => {
|
||||||
✅ run @setup <=
|
✅ run @setup <=
|
||||||
◯ run @chromium
|
◯ run @chromium
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
|
||||||
|
- tree:
|
||||||
|
- treeitem "[icon-circle-outline] a.test.ts" [expanded]:
|
||||||
|
- group:
|
||||||
|
- treeitem ${/\[icon-check\] run @setup setup \d+ms/} [selected]:
|
||||||
|
- button "Run"
|
||||||
|
- button "Show source"
|
||||||
|
- button "Watch"
|
||||||
|
- treeitem "[icon-circle-outline] run @chromium chromium"
|
||||||
|
`);
|
||||||
|
|
||||||
await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
|
await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
|
||||||
|
|
||||||
await page.getByTitle('run @chromium').dblclick();
|
await page.getByTitle('run @chromium').dblclick();
|
||||||
|
|
@ -459,6 +671,18 @@ test('should show proper total when using deps', async ({ runUITest }) => {
|
||||||
✅ run @setup
|
✅ run @setup
|
||||||
✅ run @chromium <=
|
✅ run @chromium <=
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
|
||||||
|
- tree:
|
||||||
|
- treeitem "[icon-check] a.test.ts" [expanded]:
|
||||||
|
- group:
|
||||||
|
- treeitem ${/\[icon-check\] run @setup setup \d+ms/}
|
||||||
|
- treeitem ${/\[icon-check\] run @chromium chromium \d+ms/} [selected]:
|
||||||
|
- button "Run"
|
||||||
|
- button "Show source"
|
||||||
|
- button "Watch"
|
||||||
|
`);
|
||||||
|
|
||||||
await expect(page.getByTestId('status-line')).toHaveText('2/2 passed (100%)');
|
await expect(page.getByTestId('status-line')).toHaveText('2/2 passed (100%)');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -518,6 +742,13 @@ test('should respect --tsconfig option', {
|
||||||
✅ test
|
✅ test
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
|
||||||
|
- tree:
|
||||||
|
- treeitem "[icon-check] a.test.ts" [expanded]:
|
||||||
|
- group:
|
||||||
|
- treeitem ${/\[icon-check\] test \d+ms/}
|
||||||
|
`);
|
||||||
|
|
||||||
await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
|
await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -539,4 +770,11 @@ test('should respect --ignore-snapshots option', {
|
||||||
▼ ✅ a.test.ts
|
▼ ✅ a.test.ts
|
||||||
✅ snapshot
|
✅ snapshot
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
await expect(page.getByTestId('test-tree')).toMatchAriaSnapshot(`
|
||||||
|
- tree:
|
||||||
|
- treeitem "[icon-check] a.test.ts" [expanded]:
|
||||||
|
- group:
|
||||||
|
- treeitem ${/\[icon-check\] snapshot \d+ms/}
|
||||||
|
`);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
FROM ubuntu:focal
|
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
|
||||||
ARG TZ=America/Los_Angeles
|
|
||||||
ARG DOCKER_IMAGE_NAME_TEMPLATE="mcr.microsoft.com/playwright:v%version%-focal"
|
|
||||||
|
|
||||||
ENV LANG=C.UTF-8
|
|
||||||
ENV LC_ALL=C.UTF-8
|
|
||||||
|
|
||||||
# === INSTALL Node.js ===
|
|
||||||
|
|
||||||
RUN apt-get update && \
|
|
||||||
# Install Node.js
|
|
||||||
apt-get install -y curl wget gpg ca-certificates && \
|
|
||||||
mkdir -p /etc/apt/keyrings && \
|
|
||||||
curl -sL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
|
|
||||||
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" >> /etc/apt/sources.list.d/nodesource.list && \
|
|
||||||
apt-get update && \
|
|
||||||
apt-get install -y nodejs && \
|
|
||||||
# Feature-parity with node.js base images.
|
|
||||||
apt-get install -y --no-install-recommends git openssh-client && \
|
|
||||||
npm install -g yarn && \
|
|
||||||
# clean apt cache
|
|
||||||
rm -rf /var/lib/apt/lists/* && \
|
|
||||||
# Create the pwuser
|
|
||||||
adduser pwuser
|
|
||||||
|
|
||||||
# === BAKE BROWSERS INTO IMAGE ===
|
|
||||||
|
|
||||||
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
|
|
||||||
|
|
||||||
# 1. Add tip-of-tree Playwright package to install its browsers.
|
|
||||||
# The package should be built beforehand from tip-of-tree Playwright.
|
|
||||||
COPY ./playwright-core.tar.gz /tmp/playwright-core.tar.gz
|
|
||||||
|
|
||||||
# 2. Bake in Playwright Agent.
|
|
||||||
# Playwright Agent is used to bake in browsers and browser dependencies,
|
|
||||||
# and run docker server later on.
|
|
||||||
# Browsers will be downloaded in `/ms-playwright`.
|
|
||||||
# Note: make sure to set 777 to the registry so that any user can access
|
|
||||||
# registry.
|
|
||||||
RUN mkdir /ms-playwright && \
|
|
||||||
mkdir /ms-playwright-agent && \
|
|
||||||
cd /ms-playwright-agent && npm init -y && \
|
|
||||||
npm i /tmp/playwright-core.tar.gz && \
|
|
||||||
npm exec --no -- playwright-core mark-docker-image "${DOCKER_IMAGE_NAME_TEMPLATE}" && \
|
|
||||||
npm exec --no -- playwright-core install --with-deps && rm -rf /var/lib/apt/lists/* && \
|
|
||||||
rm /tmp/playwright-core.tar.gz && \
|
|
||||||
rm -rf /ms-playwright-agent && \
|
|
||||||
rm -rf ~/.npm/ && \
|
|
||||||
chmod -R 777 /ms-playwright
|
|
||||||
|
|
@ -3,12 +3,12 @@ set -e
|
||||||
set +x
|
set +x
|
||||||
|
|
||||||
if [[ ($1 == '--help') || ($1 == '-h') || ($1 == '') || ($2 == '') ]]; then
|
if [[ ($1 == '--help') || ($1 == '-h') || ($1 == '') || ($2 == '') ]]; then
|
||||||
echo "usage: $(basename $0) {--arm64,--amd64} {focal,jammy} playwright:localbuild-focal"
|
echo "usage: $(basename $0) {--arm64,--amd64} {jammy,noble} playwright:localbuild-noble"
|
||||||
echo
|
echo
|
||||||
echo "Build Playwright docker image and tag it as 'playwright:localbuild-focal'."
|
echo "Build Playwright docker image and tag it as 'playwright:localbuild-noble'."
|
||||||
echo "Once image is built, you can run it with"
|
echo "Once image is built, you can run it with"
|
||||||
echo ""
|
echo ""
|
||||||
echo " docker run --rm -it playwright:localbuild-focal /bin/bash"
|
echo " docker run --rm -it playwright:localbuild-noble /bin/bash"
|
||||||
echo ""
|
echo ""
|
||||||
echo "NOTE: this requires on Playwright dependencies to be installed with 'npm install'"
|
echo "NOTE: this requires on Playwright dependencies to be installed with 'npm install'"
|
||||||
echo " and Playwright itself being built with 'npm run build'"
|
echo " and Playwright itself being built with 'npm run build'"
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,6 @@ else
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Ubuntu 20.04
|
|
||||||
FOCAL_TAGS=(
|
|
||||||
"v${PW_VERSION}-focal"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Ubuntu 22.04
|
# Ubuntu 22.04
|
||||||
JAMMY_TAGS=(
|
JAMMY_TAGS=(
|
||||||
"v${PW_VERSION}-jammy"
|
"v${PW_VERSION}-jammy"
|
||||||
|
|
@ -69,14 +64,12 @@ install_oras_if_needed() {
|
||||||
publish_docker_images_with_arch_suffix() {
|
publish_docker_images_with_arch_suffix() {
|
||||||
local FLAVOR="$1"
|
local FLAVOR="$1"
|
||||||
local TAGS=()
|
local TAGS=()
|
||||||
if [[ "$FLAVOR" == "focal" ]]; then
|
if [[ "$FLAVOR" == "jammy" ]]; then
|
||||||
TAGS=("${FOCAL_TAGS[@]}")
|
|
||||||
elif [[ "$FLAVOR" == "jammy" ]]; then
|
|
||||||
TAGS=("${JAMMY_TAGS[@]}")
|
TAGS=("${JAMMY_TAGS[@]}")
|
||||||
elif [[ "$FLAVOR" == "noble" ]]; then
|
elif [[ "$FLAVOR" == "noble" ]]; then
|
||||||
TAGS=("${NOBLE_TAGS[@]}")
|
TAGS=("${NOBLE_TAGS[@]}")
|
||||||
else
|
else
|
||||||
echo "ERROR: unknown flavor - $FLAVOR. Must be either 'focal', 'jammy', or 'noble'"
|
echo "ERROR: unknown flavor - $FLAVOR. Must be either 'jammy', or 'noble'"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
local ARCH="$2"
|
local ARCH="$2"
|
||||||
|
|
@ -97,14 +90,12 @@ publish_docker_images_with_arch_suffix() {
|
||||||
publish_docker_manifest () {
|
publish_docker_manifest () {
|
||||||
local FLAVOR="$1"
|
local FLAVOR="$1"
|
||||||
local TAGS=()
|
local TAGS=()
|
||||||
if [[ "$FLAVOR" == "focal" ]]; then
|
if [[ "$FLAVOR" == "jammy" ]]; then
|
||||||
TAGS=("${FOCAL_TAGS[@]}")
|
|
||||||
elif [[ "$FLAVOR" == "jammy" ]]; then
|
|
||||||
TAGS=("${JAMMY_TAGS[@]}")
|
TAGS=("${JAMMY_TAGS[@]}")
|
||||||
elif [[ "$FLAVOR" == "noble" ]]; then
|
elif [[ "$FLAVOR" == "noble" ]]; then
|
||||||
TAGS=("${NOBLE_TAGS[@]}")
|
TAGS=("${NOBLE_TAGS[@]}")
|
||||||
else
|
else
|
||||||
echo "ERROR: unknown flavor - $FLAVOR. Must be either 'focal', 'jammy', or 'noble'"
|
echo "ERROR: unknown flavor - $FLAVOR. Must be either 'jammy', or 'noble'"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -123,11 +114,6 @@ publish_docker_manifest () {
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
# Ubuntu 20.04
|
|
||||||
publish_docker_images_with_arch_suffix focal amd64
|
|
||||||
publish_docker_images_with_arch_suffix focal arm64
|
|
||||||
publish_docker_manifest focal amd64 arm64
|
|
||||||
|
|
||||||
# Ubuntu 22.04
|
# Ubuntu 22.04
|
||||||
publish_docker_images_with_arch_suffix jammy amd64
|
publish_docker_images_with_arch_suffix jammy amd64
|
||||||
publish_docker_images_with_arch_suffix jammy arm64
|
publish_docker_images_with_arch_suffix jammy arm64
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue