Merge branch 'main' into sharding-algorithm
This commit is contained in:
commit
b3b568bc31
|
|
@ -256,6 +256,13 @@ class PageHandler {
|
|||
return await this._contentPage.send('disposeObject', options);
|
||||
}
|
||||
|
||||
async ['Heap.collectGarbage']() {
|
||||
Services.obs.notifyObservers(null, "child-gc-request");
|
||||
Cu.forceGC();
|
||||
Services.obs.notifyObservers(null, "child-cc-request");
|
||||
Cu.forceCC();
|
||||
}
|
||||
|
||||
async ['Network.getResponseBody']({requestId}) {
|
||||
return this._pageNetwork.getResponseBody(requestId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -487,6 +487,17 @@ const Browser = {
|
|||
},
|
||||
};
|
||||
|
||||
const Heap = {
|
||||
targets: ['page'],
|
||||
types: {},
|
||||
events: {},
|
||||
methods: {
|
||||
'collectGarbage': {
|
||||
params: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Network = {
|
||||
targets: ['page'],
|
||||
types: networkTypes,
|
||||
|
|
@ -1002,7 +1013,7 @@ const Accessibility = {
|
|||
}
|
||||
|
||||
this.protocol = {
|
||||
domains: {Browser, Page, Runtime, Network, Accessibility},
|
||||
domains: {Browser, Heap, Page, Runtime, Network, Accessibility},
|
||||
};
|
||||
this.checkScheme = checkScheme;
|
||||
this.EXPORTED_SYMBOLS = ['protocol', 'checkScheme'];
|
||||
|
|
|
|||
|
|
@ -159,7 +159,10 @@ context cookies from the response. The method will automatically follow redirect
|
|||
### option: APIRequestContext.delete.data = %%-js-python-csharp-fetch-option-data-%%
|
||||
* since: v1.17
|
||||
|
||||
### option: APIRequestContext.delete.form = %%-js-python-fetch-option-form-%%
|
||||
### option: APIRequestContext.delete.form = %%-js-fetch-option-form-%%
|
||||
* since: v1.17
|
||||
|
||||
### option: APIRequestContext.delete.form = %%-python-fetch-option-form-%%
|
||||
* since: v1.17
|
||||
|
||||
### option: APIRequestContext.delete.form = %%-csharp-fetch-option-form-%%
|
||||
|
|
@ -332,7 +335,10 @@ If set changes the fetch method (e.g. [PUT](https://developer.mozilla.org/en-US/
|
|||
### option: APIRequestContext.fetch.data = %%-js-python-csharp-fetch-option-data-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.fetch.form = %%-js-python-fetch-option-form-%%
|
||||
### option: APIRequestContext.fetch.form = %%-js-fetch-option-form-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.fetch.form = %%-python-fetch-option-form-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.fetch.form = %%-csharp-fetch-option-form-%%
|
||||
|
|
@ -442,7 +448,10 @@ await request.GetAsync("https://example.com/api/getText", new() { Params = query
|
|||
### option: APIRequestContext.get.data = %%-js-python-csharp-fetch-option-data-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.get.form = %%-js-python-fetch-option-form-%%
|
||||
### option: APIRequestContext.get.form = %%-js-fetch-option-form-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.get.form = %%-python-fetch-option-form-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.get.form = %%-csharp-fetch-option-form-%%
|
||||
|
|
@ -504,7 +513,10 @@ context cookies from the response. The method will automatically follow redirect
|
|||
### option: APIRequestContext.head.data = %%-js-python-csharp-fetch-option-data-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.head.form = %%-js-python-fetch-option-form-%%
|
||||
### option: APIRequestContext.head.form = %%-python-fetch-option-form-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.head.form = %%-js-fetch-option-form-%%
|
||||
* since: v1.26
|
||||
|
||||
### option: APIRequestContext.head.form = %%-csharp-fetch-option-form-%%
|
||||
|
|
@ -566,7 +578,10 @@ context cookies from the response. The method will automatically follow redirect
|
|||
### option: APIRequestContext.patch.data = %%-js-python-csharp-fetch-option-data-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.patch.form = %%-js-python-fetch-option-form-%%
|
||||
### option: APIRequestContext.patch.form = %%-js-fetch-option-form-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.patch.form = %%-python-fetch-option-form-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.patch.form = %%-csharp-fetch-option-form-%%
|
||||
|
|
@ -749,7 +764,10 @@ await request.PostAsync("https://example.com/api/uploadScript", new() { Multipar
|
|||
### option: APIRequestContext.post.data = %%-js-python-csharp-fetch-option-data-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.post.form = %%-js-python-fetch-option-form-%%
|
||||
### option: APIRequestContext.post.form = %%-js-fetch-option-form-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.post.form = %%-python-fetch-option-form-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.post.form = %%-csharp-fetch-option-form-%%
|
||||
|
|
@ -811,7 +829,10 @@ context cookies from the response. The method will automatically follow redirect
|
|||
### option: APIRequestContext.put.data = %%-js-python-csharp-fetch-option-data-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.put.form = %%-js-python-fetch-option-form-%%
|
||||
### option: APIRequestContext.put.form = %%-python-fetch-option-form-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.put.form = %%-js-fetch-option-form-%%
|
||||
* since: v1.16
|
||||
|
||||
### option: APIRequestContext.put.form = %%-csharp-fetch-option-form-%%
|
||||
|
|
|
|||
|
|
@ -2333,6 +2333,11 @@ last redirect. If cannot go forward, returns `null`.
|
|||
|
||||
Navigate to the next page in history.
|
||||
|
||||
## async method: Page.forceGarbageCollection
|
||||
* since: v1.47
|
||||
|
||||
Force the browser to perform garbage collection.
|
||||
|
||||
### option: Page.goForward.waitUntil = %%-navigation-wait-until-%%
|
||||
* since: v1.8
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const context = await browser.newContext();
|
|||
await context.tracing.start({ screenshots: true, snapshots: true });
|
||||
const page = await context.newPage();
|
||||
await page.goto('https://playwright.dev');
|
||||
await context.tracing.stop({ path: 'trace.zip' });
|
||||
await context.tracing.stop({ path: 'trace.pwtrace.zip' });
|
||||
```
|
||||
|
||||
```java
|
||||
|
|
@ -23,7 +23,7 @@ context.tracing().start(new Tracing.StartOptions()
|
|||
Page page = context.newPage();
|
||||
page.navigate("https://playwright.dev");
|
||||
context.tracing().stop(new Tracing.StopOptions()
|
||||
.setPath(Paths.get("trace.zip")));
|
||||
.setPath(Paths.get("trace.pwtrace.zip")));
|
||||
```
|
||||
|
||||
```python async
|
||||
|
|
@ -32,7 +32,7 @@ context = await browser.new_context()
|
|||
await context.tracing.start(screenshots=True, snapshots=True)
|
||||
page = await context.new_page()
|
||||
await page.goto("https://playwright.dev")
|
||||
await context.tracing.stop(path = "trace.zip")
|
||||
await context.tracing.stop(path = "trace.pwtrace.zip")
|
||||
```
|
||||
|
||||
```python sync
|
||||
|
|
@ -41,7 +41,7 @@ context = browser.new_context()
|
|||
context.tracing.start(screenshots=True, snapshots=True)
|
||||
page = context.new_page()
|
||||
page.goto("https://playwright.dev")
|
||||
context.tracing.stop(path = "trace.zip")
|
||||
context.tracing.stop(path = "trace.pwtrace.zip")
|
||||
```
|
||||
|
||||
```csharp
|
||||
|
|
@ -57,7 +57,7 @@ var page = await context.NewPageAsync();
|
|||
await page.GotoAsync("https://playwright.dev");
|
||||
await context.Tracing.StopAsync(new()
|
||||
{
|
||||
Path = "trace.zip"
|
||||
Path = "trace.pwtrace.zip"
|
||||
});
|
||||
```
|
||||
|
||||
|
|
@ -72,7 +72,7 @@ Start tracing.
|
|||
await context.tracing.start({ screenshots: true, snapshots: true });
|
||||
const page = await context.newPage();
|
||||
await page.goto('https://playwright.dev');
|
||||
await context.tracing.stop({ path: 'trace.zip' });
|
||||
await context.tracing.stop({ path: 'trace.pwtrace.zip' });
|
||||
```
|
||||
|
||||
```java
|
||||
|
|
@ -82,21 +82,21 @@ context.tracing().start(new Tracing.StartOptions()
|
|||
Page page = context.newPage();
|
||||
page.navigate("https://playwright.dev");
|
||||
context.tracing().stop(new Tracing.StopOptions()
|
||||
.setPath(Paths.get("trace.zip")));
|
||||
.setPath(Paths.get("trace.pwtrace.zip")));
|
||||
```
|
||||
|
||||
```python async
|
||||
await context.tracing.start(screenshots=True, snapshots=True)
|
||||
page = await context.new_page()
|
||||
await page.goto("https://playwright.dev")
|
||||
await context.tracing.stop(path = "trace.zip")
|
||||
await context.tracing.stop(path = "trace.pwtrace.zip")
|
||||
```
|
||||
|
||||
```python sync
|
||||
context.tracing.start(screenshots=True, snapshots=True)
|
||||
page = context.new_page()
|
||||
page.goto("https://playwright.dev")
|
||||
context.tracing.stop(path = "trace.zip")
|
||||
context.tracing.stop(path = "trace.pwtrace.zip")
|
||||
```
|
||||
|
||||
```csharp
|
||||
|
|
@ -112,7 +112,7 @@ var page = await context.NewPageAsync();
|
|||
await page.GotoAsync("https://playwright.dev");
|
||||
await context.Tracing.StopAsync(new()
|
||||
{
|
||||
Path = "trace.zip"
|
||||
Path = "trace.pwtrace.zip"
|
||||
});
|
||||
```
|
||||
|
||||
|
|
@ -177,12 +177,12 @@ await page.goto('https://playwright.dev');
|
|||
await context.tracing.startChunk();
|
||||
await page.getByText('Get Started').click();
|
||||
// Everything between startChunk and stopChunk will be recorded in the trace.
|
||||
await context.tracing.stopChunk({ path: 'trace1.zip' });
|
||||
await context.tracing.stopChunk({ path: 'trace1.pwtrace.zip' });
|
||||
|
||||
await context.tracing.startChunk();
|
||||
await page.goto('http://example.com');
|
||||
// Save a second trace file with different actions.
|
||||
await context.tracing.stopChunk({ path: 'trace2.zip' });
|
||||
await context.tracing.stopChunk({ path: 'trace2.pwtrace.zip' });
|
||||
```
|
||||
|
||||
```java
|
||||
|
|
@ -196,13 +196,13 @@ context.tracing().startChunk();
|
|||
page.getByText("Get Started").click();
|
||||
// Everything between startChunk and stopChunk will be recorded in the trace.
|
||||
context.tracing().stopChunk(new Tracing.StopChunkOptions()
|
||||
.setPath(Paths.get("trace1.zip")));
|
||||
.setPath(Paths.get("trace1.pwtrace.zip")));
|
||||
|
||||
context.tracing().startChunk();
|
||||
page.navigate("http://example.com");
|
||||
// Save a second trace file with different actions.
|
||||
context.tracing().stopChunk(new Tracing.StopChunkOptions()
|
||||
.setPath(Paths.get("trace2.zip")));
|
||||
.setPath(Paths.get("trace2.pwtrace.zip")));
|
||||
```
|
||||
|
||||
```python async
|
||||
|
|
@ -213,12 +213,12 @@ await page.goto("https://playwright.dev")
|
|||
await context.tracing.start_chunk()
|
||||
await page.get_by_text("Get Started").click()
|
||||
# Everything between start_chunk and stop_chunk will be recorded in the trace.
|
||||
await context.tracing.stop_chunk(path = "trace1.zip")
|
||||
await context.tracing.stop_chunk(path = "trace1.pwtrace.zip")
|
||||
|
||||
await context.tracing.start_chunk()
|
||||
await page.goto("http://example.com")
|
||||
# Save a second trace file with different actions.
|
||||
await context.tracing.stop_chunk(path = "trace2.zip")
|
||||
await context.tracing.stop_chunk(path = "trace2.pwtrace.zip")
|
||||
```
|
||||
|
||||
```python sync
|
||||
|
|
@ -229,12 +229,12 @@ page.goto("https://playwright.dev")
|
|||
context.tracing.start_chunk()
|
||||
page.get_by_text("Get Started").click()
|
||||
# Everything between start_chunk and stop_chunk will be recorded in the trace.
|
||||
context.tracing.stop_chunk(path = "trace1.zip")
|
||||
context.tracing.stop_chunk(path = "trace1.pwtrace.zip")
|
||||
|
||||
context.tracing.start_chunk()
|
||||
page.goto("http://example.com")
|
||||
# Save a second trace file with different actions.
|
||||
context.tracing.stop_chunk(path = "trace2.zip")
|
||||
context.tracing.stop_chunk(path = "trace2.pwtrace.zip")
|
||||
```
|
||||
|
||||
```csharp
|
||||
|
|
@ -254,7 +254,7 @@ await page.GetByText("Get Started").ClickAsync();
|
|||
// Everything between StartChunkAsync and StopChunkAsync will be recorded in the trace.
|
||||
await context.Tracing.StopChunkAsync(new()
|
||||
{
|
||||
Path = "trace1.zip"
|
||||
Path = "trace1.pwtrace.zip"
|
||||
});
|
||||
|
||||
await context.Tracing.StartChunkAsync();
|
||||
|
|
@ -262,7 +262,7 @@ await page.GotoAsync("http://example.com");
|
|||
// Save a second trace file with different actions.
|
||||
await context.Tracing.StopChunkAsync(new()
|
||||
{
|
||||
Path = "trace2.zip"
|
||||
Path = "trace2.pwtrace.zip"
|
||||
});
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -405,8 +405,16 @@ Request timeout in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to d
|
|||
Whether to throw on response codes other than 2xx and 3xx. By default response object is returned
|
||||
for all status codes.
|
||||
|
||||
## js-python-fetch-option-form
|
||||
* langs: js, python
|
||||
## js-fetch-option-form
|
||||
* langs: js
|
||||
- `form` <[Object]<[string], [string]|[float]|[boolean]>|[FormData]>
|
||||
|
||||
Provides an object that will be serialized as html form using `application/x-www-form-urlencoded` encoding and sent as
|
||||
this request body. If this parameter is specified `content-type` header will be set to `application/x-www-form-urlencoded`
|
||||
unless explicitly provided.
|
||||
|
||||
## python-fetch-option-form
|
||||
* langs: python
|
||||
- `form` <[Object]<[string], [string]|[float]|[boolean]>>
|
||||
|
||||
Provides an object that will be serialized as html form using `application/x-www-form-urlencoded` encoding and sent as
|
||||
|
|
|
|||
|
|
@ -209,6 +209,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v%%VERSION%%-jammy
|
||||
options: --user 1001
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
|
|
@ -218,8 +219,6 @@ jobs:
|
|||
run: npm ci
|
||||
- name: Run your tests
|
||||
run: npx playwright test
|
||||
env:
|
||||
HOME: /root
|
||||
```
|
||||
|
||||
```yml python title=".github/workflows/playwright.yml"
|
||||
|
|
@ -235,6 +234,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright/python:v%%VERSION%%-jammy
|
||||
options: --user 1001
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
|
|
@ -248,8 +248,6 @@ jobs:
|
|||
pip install -e .
|
||||
- name: Run your tests
|
||||
run: pytest
|
||||
env:
|
||||
HOME: /root
|
||||
```
|
||||
|
||||
```yml java title=".github/workflows/playwright.yml"
|
||||
|
|
@ -265,6 +263,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright/java:v%%VERSION%%-jammy
|
||||
options: --user 1001
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-java@v3
|
||||
|
|
@ -275,8 +274,6 @@ jobs:
|
|||
run: mvn -B install -D skipTests --no-transfer-progress
|
||||
- name: Run tests
|
||||
run: mvn test
|
||||
env:
|
||||
HOME: /root
|
||||
```
|
||||
|
||||
```yml csharp title=".github/workflows/playwright.yml"
|
||||
|
|
@ -292,6 +289,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright/dotnet:v%%VERSION%%-jammy
|
||||
options: --user 1001
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup dotnet
|
||||
|
|
@ -301,8 +299,6 @@ jobs:
|
|||
- run: dotnet build
|
||||
- name: Run your tests
|
||||
run: dotnet test
|
||||
env:
|
||||
HOME: /root
|
||||
```
|
||||
|
||||
#### On deployment
|
||||
|
|
|
|||
|
|
@ -238,12 +238,12 @@ async function globalSetup(config: FullConfig) {
|
|||
await page.getByText('Sign in').click();
|
||||
await context.storageState({ path: storageState as string });
|
||||
await context.tracing.stop({
|
||||
path: './test-results/setup-trace.zip',
|
||||
path: './test-results/setup-trace.pwtrace.zip',
|
||||
});
|
||||
await browser.close();
|
||||
} catch (error) {
|
||||
await context.tracing.stop({
|
||||
path: './test-results/failed-setup-trace.zip',
|
||||
path: './test-results/failed-setup-trace.pwtrace.zip',
|
||||
});
|
||||
await browser.close();
|
||||
throw error;
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ Options for tracing are:
|
|||
- `off`: Do not record trace. (default)
|
||||
- `retain-on-failure`: Record trace for each test, but remove all traces from successful test runs.
|
||||
|
||||
This will record the trace and place it into the file named `trace.zip` in your `test-results` directory.
|
||||
This will record the trace and place it into the file named `trace.pwtrace.zip` in your `test-results` directory.
|
||||
|
||||
<details>
|
||||
<summary>If you are not using Pytest, click here to learn how to record traces.</summary>
|
||||
|
|
@ -41,7 +41,7 @@ page = await context.new_page()
|
|||
await page.goto("https://playwright.dev")
|
||||
|
||||
# Stop tracing and export it into a zip archive.
|
||||
await context.tracing.stop(path = "trace.zip")
|
||||
await context.tracing.stop(path = "trace.pwtrace.zip")
|
||||
```
|
||||
|
||||
```python sync
|
||||
|
|
@ -55,7 +55,7 @@ page = context.new_page()
|
|||
page.goto("https://playwright.dev")
|
||||
|
||||
# Stop tracing and export it into a zip archive.
|
||||
context.tracing.stop(path = "trace.zip")
|
||||
context.tracing.stop(path = "trace.pwtrace.zip")
|
||||
```
|
||||
|
||||
</details>
|
||||
|
|
@ -80,22 +80,22 @@ page.navigate("https://playwright.dev");
|
|||
|
||||
// Stop tracing and export it into a zip archive.
|
||||
context.tracing().stop(new Tracing.StopOptions()
|
||||
.setPath(Paths.get("trace.zip")));
|
||||
.setPath(Paths.get("trace.pwtrace.zip")));
|
||||
```
|
||||
|
||||
|
||||
This will record the trace and place it into the file named `trace.zip`.
|
||||
This will record the trace and place it into the file named `trace.pwtrace.zip`.
|
||||
|
||||
## Opening the trace
|
||||
|
||||
You can open the saved trace using the Playwright CLI or in your browser on [`trace.playwright.dev`](https://trace.playwright.dev). Make sure to add the full path to where your trace's zip file is located. Once opened you can click on each action or use the timeline to see the state of the page before and after each action. You can also inspect the log, source and network during each step of the test. The trace viewer creates a DOM snapshot so you can fully interact with it, open devtools etc.
|
||||
|
||||
```bash java
|
||||
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="show-trace trace.zip"
|
||||
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="show-trace trace.pwtrace.zip"
|
||||
```
|
||||
|
||||
```bash python
|
||||
playwright show-trace trace.zip
|
||||
playwright show-trace trace.pwtrace.zip
|
||||
```
|
||||
|
||||
######
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ Playwright Trace Viewer is a GUI tool that lets you explore recorded Playwright
|
|||
|
||||
## Recording a Trace
|
||||
|
||||
By default the [playwright.config](./trace-viewer.md#recording-a-trace-on-ci) file will contain the configuration needed to create a `trace.zip` file for each test. Traces are setup to run `on-first-retry` meaning they will be run on the first retry of a failed test. Also `retries` are set to 2 when running on CI and 0 locally. This means the traces will be recorded on the first retry of a failed test but not on the first run and not on the second retry.
|
||||
By default the [playwright.config](./trace-viewer.md#recording-a-trace-on-ci) file will contain the configuration needed to create a `trace.pwtrace.zip` file for each test. Traces are setup to run `on-first-retry` meaning they will be run on the first retry of a failed test. Also `retries` are set to 2 when running on CI and 0 locally. This means the traces will be recorded on the first retry of a failed test but not on the first run and not on the second retry.
|
||||
|
||||
```js title="playwright.config.ts"
|
||||
import { defineConfig } from '@playwright/test';
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ npx playwright show-report
|
|||
* langs: js
|
||||
|
||||
Traces should be run on continuous integration on the first retry of a failed test
|
||||
by setting the `trace: 'on-first-retry'` option in the test configuration file. This will produce a `trace.zip` file for each test that was retried.
|
||||
by setting the `trace: 'on-first-retry'` option in the test configuration file. This will produce a `trace.pwtrace.zip` file for each test that was retried.
|
||||
|
||||
```js tab=js-test title="playwright.config.ts"
|
||||
import { defineConfig } from '@playwright/test';
|
||||
|
|
@ -155,7 +155,7 @@ const page = await context.newPage();
|
|||
await page.goto('https://playwright.dev');
|
||||
|
||||
// Stop tracing and export it into a zip archive.
|
||||
await context.tracing.stop({ path: 'trace.zip' });
|
||||
await context.tracing.stop({ path: 'trace.pwtrace.zip' });
|
||||
```
|
||||
|
||||
Available options to record a trace:
|
||||
|
|
@ -185,7 +185,7 @@ Options for tracing are:
|
|||
- `off`: Do not record trace. (default)
|
||||
- `retain-on-failure`: Record trace for each test, but remove all traces from successful test runs.
|
||||
|
||||
This will record the trace and place it into the file named `trace.zip` in your `test-results` directory.
|
||||
This will record the trace and place it into the file named `trace.pwtrace.zip` in your `test-results` directory.
|
||||
|
||||
<details>
|
||||
<summary>If you are not using Pytest, click here to learn how to record traces.</summary>
|
||||
|
|
@ -201,7 +201,7 @@ page = await context.new_page()
|
|||
await page.goto("https://playwright.dev")
|
||||
|
||||
# Stop tracing and export it into a zip archive.
|
||||
await context.tracing.stop(path = "trace.zip")
|
||||
await context.tracing.stop(path = "trace.pwtrace.zip")
|
||||
```
|
||||
|
||||
```python sync
|
||||
|
|
@ -215,7 +215,7 @@ page = context.new_page()
|
|||
page.goto("https://playwright.dev")
|
||||
|
||||
# Stop tracing and export it into a zip archive.
|
||||
context.tracing.stop(path = "trace.zip")
|
||||
context.tracing.stop(path = "trace.pwtrace.zip")
|
||||
```
|
||||
|
||||
</details>
|
||||
|
|
@ -240,10 +240,10 @@ page.navigate("https://playwright.dev");
|
|||
|
||||
// Stop tracing and export it into a zip archive.
|
||||
context.tracing().stop(new Tracing.StopOptions()
|
||||
.setPath(Paths.get("trace.zip")));
|
||||
.setPath(Paths.get("trace.pwtracezip")));
|
||||
```
|
||||
|
||||
This will record the trace and place it into the file named `trace.zip`.
|
||||
This will record the trace and place it into the file named `trace.pwtrace.zip`.
|
||||
|
||||
## Recording a trace
|
||||
* langs: csharp
|
||||
|
|
@ -466,22 +466,22 @@ public class ExampleTest : PageTest
|
|||
|
||||
## Opening the trace
|
||||
|
||||
You can open the saved trace using the Playwright CLI or in your browser on [`trace.playwright.dev`](https://trace.playwright.dev). Make sure to add the full path to where your `trace.zip` file is located.
|
||||
You can open the saved trace using the Playwright CLI or in your browser on [`trace.playwright.dev`](https://trace.playwright.dev). Make sure to add the full path to where your `trace.pwtrace.zip` file is located.
|
||||
|
||||
```bash js
|
||||
npx playwright show-trace path/to/trace.zip
|
||||
npx playwright show-trace path/to/trace.pwtrace.zip
|
||||
```
|
||||
|
||||
```bash java
|
||||
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="show-trace trace.zip"
|
||||
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="show-trace trace.pwtrace.zip"
|
||||
```
|
||||
|
||||
```bash python
|
||||
playwright show-trace trace.zip
|
||||
playwright show-trace trace.pwtrace.zip
|
||||
```
|
||||
|
||||
```bash csharp
|
||||
pwsh bin/Debug/netX/playwright.ps1 show-trace trace.zip
|
||||
pwsh bin/Debug/netX/playwright.ps1 show-trace trace.pwtrace.zip
|
||||
```
|
||||
|
||||
## Using [trace.playwright.dev](https://trace.playwright.dev)
|
||||
|
|
@ -496,19 +496,19 @@ pwsh bin/Debug/netX/playwright.ps1 show-trace trace.zip
|
|||
You can open remote traces using its URL. They could be generated on a CI run which makes it easy to view the remote trace without having to manually download the file.
|
||||
|
||||
```bash js
|
||||
npx playwright show-trace https://example.com/trace.zip
|
||||
npx playwright show-trace https://example.com/trace.pwtrace.zip
|
||||
```
|
||||
|
||||
```bash java
|
||||
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="show-trace https://example.com/trace.zip"
|
||||
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="show-trace https://example.com/trace.pwtrace.zip"
|
||||
```
|
||||
|
||||
```bash python
|
||||
playwright show-trace https://example.com/trace.zip
|
||||
playwright show-trace https://example.com/trace.pwtrace.zip
|
||||
```
|
||||
|
||||
```bash csharp
|
||||
pwsh bin/Debug/netX/playwright.ps1 show-trace https://example.com/trace.zip
|
||||
pwsh bin/Debug/netX/playwright.ps1 show-trace https://example.com/trace.pwtrace.zip
|
||||
```
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -15,19 +15,19 @@
|
|||
},
|
||||
{
|
||||
"name": "firefox",
|
||||
"revision": "1463",
|
||||
"revision": "1464",
|
||||
"installByDefault": true,
|
||||
"browserVersion": "130.0"
|
||||
},
|
||||
{
|
||||
"name": "firefox-beta",
|
||||
"revision": "1463",
|
||||
"revision": "1464",
|
||||
"installByDefault": false,
|
||||
"browserVersion": "131.0b2"
|
||||
},
|
||||
{
|
||||
"name": "webkit",
|
||||
"revision": "2073",
|
||||
"revision": "2075",
|
||||
"installByDefault": true,
|
||||
"revisionOverrides": {
|
||||
"mac10.14": "1446",
|
||||
|
|
|
|||
|
|
@ -318,7 +318,7 @@ program
|
|||
}).addHelpText('afterAll', `
|
||||
Examples:
|
||||
|
||||
$ show-trace https://example.com/trace.zip`);
|
||||
$ show-trace https://example.com/trace.pwtrace.zip`);
|
||||
|
||||
type Options = {
|
||||
browser: string;
|
||||
|
|
|
|||
|
|
@ -36,8 +36,8 @@ export type FetchOptions = {
|
|||
method?: string,
|
||||
headers?: Headers,
|
||||
data?: string | Buffer | Serializable,
|
||||
form?: { [key: string]: string|number|boolean; };
|
||||
multipart?: { [key: string]: string|number|boolean|fs.ReadStream|FilePayload; };
|
||||
form?: { [key: string]: string|number|boolean; } | FormData;
|
||||
multipart?: { [key: string]: string|number|boolean|fs.ReadStream|FilePayload; } | FormData;
|
||||
timeout?: number,
|
||||
failOnStatusCode?: boolean,
|
||||
ignoreHTTPSErrors?: boolean,
|
||||
|
|
@ -202,7 +202,16 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
|
|||
throw new Error(`Unexpected 'data' type`);
|
||||
}
|
||||
} else if (options.form) {
|
||||
formData = objectToArray(options.form);
|
||||
if (globalThis.FormData && options.form instanceof FormData) {
|
||||
formData = [];
|
||||
for (const [name, value] of options.form.entries()) {
|
||||
if (typeof value !== 'string')
|
||||
throw new Error(`Expected string for options.form["${name}"], found File. Please use options.multipart instead.`);
|
||||
formData.push({ name, value });
|
||||
}
|
||||
} else {
|
||||
formData = objectToArray(options.form);
|
||||
}
|
||||
} else if (options.multipart) {
|
||||
multipartData = [];
|
||||
if (globalThis.FormData && options.multipart instanceof FormData) {
|
||||
|
|
|
|||
|
|
@ -468,6 +468,10 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
return Response.fromNullable((await this._channel.goForward({ ...options, waitUntil })).response);
|
||||
}
|
||||
|
||||
async forceGarbageCollection() {
|
||||
await this._channel.forceGarbageCollection();
|
||||
}
|
||||
|
||||
async emulateMedia(options: { media?: 'screen' | 'print' | null, colorScheme?: 'dark' | 'light' | 'no-preference' | null, reducedMotion?: 'reduce' | 'no-preference' | null, forcedColors?: 'active' | 'none' | null } = {}) {
|
||||
await this._channel.emulateMedia({
|
||||
media: options.media === null ? 'no-override' : options.media,
|
||||
|
|
|
|||
|
|
@ -1122,6 +1122,8 @@ scheme.PageGoForwardParams = tObject({
|
|||
scheme.PageGoForwardResult = tObject({
|
||||
response: tOptional(tChannel(['Response'])),
|
||||
});
|
||||
scheme.PageForceGarbageCollectionParams = tOptional(tObject({}));
|
||||
scheme.PageForceGarbageCollectionResult = tOptional(tObject({}));
|
||||
scheme.PageRegisterLocatorHandlerParams = tObject({
|
||||
selector: tString,
|
||||
noWaitAfter: tOptional(tBoolean),
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import { validateBrowserContextOptions } from '../browserContext';
|
|||
import { ProgressController } from '../progress';
|
||||
import { CRBrowser } from '../chromium/crBrowser';
|
||||
import { helper } from '../helper';
|
||||
import type * as types from '../types';
|
||||
import { PipeTransport } from '../../protocol/transport';
|
||||
import { RecentLogsCollector } from '../../utils/debugLogger';
|
||||
import { gracefullyCloseSet } from '../../utils/processLauncher';
|
||||
|
|
@ -309,7 +310,7 @@ export class AndroidDevice extends SdkObject {
|
|||
return await this._connectToBrowser(socketName);
|
||||
}
|
||||
|
||||
private async _connectToBrowser(socketName: string, options: channels.BrowserNewContextParams = {}): Promise<BrowserContext> {
|
||||
private async _connectToBrowser(socketName: string, options: types.BrowserContextOptions = {}): Promise<BrowserContext> {
|
||||
const socket = await this._waitForLocalAbstract(socketName);
|
||||
const androidBrowser = new AndroidBrowser(this, socket);
|
||||
await androidBrowser._init();
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ export class BidiBrowser extends Browser {
|
|||
this._didClose();
|
||||
}
|
||||
|
||||
async doCreateNewContext(options: channels.BrowserNewContextParams): Promise<BrowserContext> {
|
||||
async doCreateNewContext(options: types.BrowserContextOptions): Promise<BrowserContext> {
|
||||
const { userContext } = await this._browserSession.send('browser.createUserContext', {});
|
||||
const context = new BidiBrowserContext(this, userContext, options);
|
||||
await context._initialize();
|
||||
|
|
@ -190,7 +190,7 @@ export class BidiBrowser extends Browser {
|
|||
export class BidiBrowserContext extends BrowserContext {
|
||||
declare readonly _browser: BidiBrowser;
|
||||
|
||||
constructor(browser: BidiBrowser, browserContextId: string | undefined, options: channels.BrowserNewContextParams) {
|
||||
constructor(browser: BidiBrowser, browserContextId: string | undefined, options: types.BrowserContextOptions) {
|
||||
super(browser, options, browserContextId);
|
||||
this._authenticateProxyViaHeader();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ export class BidiChromium extends BrowserType {
|
|||
}
|
||||
|
||||
private _innerDefaultArgs(options: types.LaunchOptions): string[] {
|
||||
const { args = [], proxy } = options;
|
||||
const { args = [] } = options;
|
||||
const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir'));
|
||||
if (userDataDirArg)
|
||||
throw this._createUserDataDirArgMisuseError('--user-data-dir');
|
||||
|
|
@ -125,6 +125,7 @@ export class BidiChromium extends BrowserType {
|
|||
}
|
||||
if (options.chromiumSandbox !== true)
|
||||
chromeArguments.push('--no-sandbox');
|
||||
const proxy = options.proxyOverride || options.proxy;
|
||||
if (proxy) {
|
||||
const proxyURL = new URL(proxy.server);
|
||||
const isSocks = proxyURL.protocol === 'socks5:';
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import type * as frames from '../frames';
|
|||
import type * as types from '../types';
|
||||
import * as bidi from './third_party/bidiProtocol';
|
||||
import type { BidiSession } from './bidiConnection';
|
||||
import { parseRawCookie } from '../cookieStore';
|
||||
|
||||
|
||||
export class BidiNetworkManager {
|
||||
|
|
@ -68,7 +69,7 @@ export class BidiNetworkManager {
|
|||
if (redirectedFrom) {
|
||||
this._session.sendMayFail('network.continueRequest', {
|
||||
request: param.request.request,
|
||||
headers: redirectedFrom._originalRequestRoute?._alreadyContinuedHeaders,
|
||||
...(redirectedFrom._originalRequestRoute?._alreadyContinuedHeaders || {}),
|
||||
});
|
||||
} else {
|
||||
route = new BidiRouteImpl(this._session, param.request.request);
|
||||
|
|
@ -245,7 +246,7 @@ class BidiRouteImpl implements network.RouteDelegate {
|
|||
private _requestId: bidi.Network.Request;
|
||||
private _session: BidiSession;
|
||||
private _request!: network.Request;
|
||||
_alreadyContinuedHeaders: bidi.Network.Header[] | undefined;
|
||||
_alreadyContinuedHeaders: types.HeadersArray | undefined;
|
||||
|
||||
constructor(session: BidiSession, requestId: bidi.Network.Request) {
|
||||
this._session = session;
|
||||
|
|
@ -266,13 +267,12 @@ class BidiRouteImpl implements network.RouteDelegate {
|
|||
return header;
|
||||
});
|
||||
}
|
||||
this._alreadyContinuedHeaders = toBidiHeaders(headers);
|
||||
this._alreadyContinuedHeaders = headers;
|
||||
await this._session.sendMayFail('network.continueRequest', {
|
||||
request: this._requestId,
|
||||
url: overrides.url,
|
||||
method: overrides.method,
|
||||
// TODO: cookies!
|
||||
headers: this._alreadyContinuedHeaders,
|
||||
...toBidiRequestHeaders(this._alreadyContinuedHeaders),
|
||||
body: overrides.postData ? { type: 'base64', value: Buffer.from(overrides.postData).toString('base64') } : undefined,
|
||||
});
|
||||
}
|
||||
|
|
@ -283,7 +283,7 @@ class BidiRouteImpl implements network.RouteDelegate {
|
|||
request: this._requestId,
|
||||
statusCode: response.status,
|
||||
reasonPhrase: network.statusText(response.status),
|
||||
headers: toBidiHeaders(response.headers),
|
||||
...toBidiResponseHeaders(response.headers),
|
||||
body: { type: 'base64', value: base64body },
|
||||
});
|
||||
}
|
||||
|
|
@ -302,6 +302,27 @@ function fromBidiHeaders(bidiHeaders: bidi.Network.Header[]): types.HeadersArray
|
|||
return result;
|
||||
}
|
||||
|
||||
function toBidiRequestHeaders(allHeaders: types.HeadersArray): { cookies: bidi.Network.CookieHeader[], headers: bidi.Network.Header[] } {
|
||||
const bidiHeaders = toBidiHeaders(allHeaders);
|
||||
const cookies = bidiHeaders.filter(h => h.name.toLowerCase() === 'cookie');
|
||||
const headers = bidiHeaders.filter(h => h.name.toLowerCase() !== 'cookie');
|
||||
return { cookies, headers };
|
||||
}
|
||||
|
||||
function toBidiResponseHeaders(headers: types.HeadersArray): { cookies: bidi.Network.SetCookieHeader[], headers: bidi.Network.Header[] } {
|
||||
const setCookieHeaders = headers.filter(h => h.name.toLowerCase() === 'set-cookie');
|
||||
const otherHeaders = headers.filter(h => h.name.toLowerCase() !== 'set-cookie');
|
||||
const rawCookies = setCookieHeaders.map(h => parseRawCookie(h.value));
|
||||
const cookies: bidi.Network.SetCookieHeader[] = rawCookies.filter(Boolean).map(c => {
|
||||
return {
|
||||
...c!,
|
||||
value: { type: 'string', value: c!.value },
|
||||
sameSite: toBidiSameSite(c!.sameSite),
|
||||
};
|
||||
});
|
||||
return { cookies, headers: toBidiHeaders(otherHeaders) };
|
||||
}
|
||||
|
||||
function toBidiHeaders(headers: types.HeadersArray): bidi.Network.Header[] {
|
||||
return headers.map(({ name, value }) => ({ name, value: { type: 'string', value } }));
|
||||
}
|
||||
|
|
@ -314,3 +335,13 @@ export function bidiBytesValueToString(value: bidi.Network.BytesValue): string {
|
|||
return 'unknown value type: ' + (value as any).type;
|
||||
|
||||
}
|
||||
|
||||
function toBidiSameSite(sameSite?: 'Strict' | 'Lax' | 'None'): bidi.Network.SameSite | undefined {
|
||||
if (!sameSite)
|
||||
return undefined;
|
||||
if (sameSite === 'Strict')
|
||||
return bidi.Network.SameSite.Strict;
|
||||
if (sameSite === 'Lax')
|
||||
return bidi.Network.SameSite.Lax;
|
||||
return bidi.Network.SameSite.None;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -323,6 +323,10 @@ export class BidiPage implements PageDelegate {
|
|||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
async forceGarbageCollection(): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
async addInitScript(initScript: InitScript): Promise<void> {
|
||||
const { script } = await this._session.send('script.addPreloadScript', {
|
||||
// TODO: remove function call from the source.
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export type BrowserOptions = {
|
|||
downloadsPath: string,
|
||||
tracesDir: string,
|
||||
headful?: boolean,
|
||||
persistent?: channels.BrowserNewContextParams, // Undefined means no persistent context.
|
||||
persistent?: types.BrowserContextOptions, // Undefined means no persistent context.
|
||||
browserProcess: BrowserProcess,
|
||||
customExecutablePath?: string;
|
||||
proxy?: ProxySettings,
|
||||
|
|
@ -74,15 +74,19 @@ export abstract class Browser extends SdkObject {
|
|||
this.instrumentation.onBrowserOpen(this);
|
||||
}
|
||||
|
||||
abstract doCreateNewContext(options: channels.BrowserNewContextParams): Promise<BrowserContext>;
|
||||
abstract doCreateNewContext(options: types.BrowserContextOptions): Promise<BrowserContext>;
|
||||
abstract contexts(): BrowserContext[];
|
||||
abstract isConnected(): boolean;
|
||||
abstract version(): string;
|
||||
abstract userAgent(): string;
|
||||
|
||||
async newContext(metadata: CallMetadata, options: channels.BrowserNewContextParams): Promise<BrowserContext> {
|
||||
async newContext(metadata: CallMetadata, options: types.BrowserContextOptions): Promise<BrowserContext> {
|
||||
validateBrowserContextOptions(options, this.options);
|
||||
const clientCertificatesProxy = await createClientCertificatesProxyIfNeeded(options, this.options);
|
||||
if (clientCertificatesProxy) {
|
||||
options.proxyOverride = await clientCertificatesProxy.listen();
|
||||
options.internalIgnoreHTTPSErrors = true;
|
||||
}
|
||||
let context;
|
||||
try {
|
||||
context = await this.doCreateNewContext(options);
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ export abstract class BrowserContext extends SdkObject {
|
|||
readonly _timeoutSettings = new TimeoutSettings();
|
||||
readonly _pageBindings = new Map<string, PageBinding>();
|
||||
readonly _activeProgressControllers = new Set<ProgressController>();
|
||||
readonly _options: channels.BrowserNewContextParams;
|
||||
readonly _options: types.BrowserContextOptions;
|
||||
_requestInterceptor?: network.RouteHandler;
|
||||
private _isPersistentContext: boolean;
|
||||
private _closedStatus: 'open' | 'closing' | 'closed' = 'open';
|
||||
|
|
@ -93,7 +93,7 @@ export abstract class BrowserContext extends SdkObject {
|
|||
readonly clock: Clock;
|
||||
_clientCertificatesProxy: ClientCertificatesProxy | undefined;
|
||||
|
||||
constructor(browser: Browser, options: channels.BrowserNewContextParams, browserContextId: string | undefined) {
|
||||
constructor(browser: Browser, options: types.BrowserContextOptions, browserContextId: string | undefined) {
|
||||
super(browser, 'browser-context');
|
||||
this.attribution.context = this;
|
||||
this._browser = browser;
|
||||
|
|
@ -659,19 +659,16 @@ export function assertBrowserContextIsNotOwned(context: BrowserContext) {
|
|||
}
|
||||
}
|
||||
|
||||
export async function createClientCertificatesProxyIfNeeded(options: channels.BrowserNewContextOptions, browserOptions?: BrowserOptions) {
|
||||
export async function createClientCertificatesProxyIfNeeded(options: types.BrowserContextOptions, browserOptions?: BrowserOptions) {
|
||||
if (!options.clientCertificates?.length)
|
||||
return;
|
||||
if ((options.proxy?.server && options.proxy?.server !== 'per-context') || (browserOptions?.proxy?.server && browserOptions?.proxy?.server !== 'http://per-context'))
|
||||
throw new Error('Cannot specify both proxy and clientCertificates');
|
||||
verifyClientCertificates(options.clientCertificates);
|
||||
const clientCertificatesProxy = new ClientCertificatesProxy(options);
|
||||
options.proxy = { server: await clientCertificatesProxy.listen() };
|
||||
options.ignoreHTTPSErrors = true;
|
||||
return clientCertificatesProxy;
|
||||
return new ClientCertificatesProxy(options);
|
||||
}
|
||||
|
||||
export function validateBrowserContextOptions(options: channels.BrowserNewContextParams, browserOptions: BrowserOptions) {
|
||||
export function validateBrowserContextOptions(options: types.BrowserContextOptions, browserOptions: BrowserOptions) {
|
||||
if (options.noDefaultViewport && options.deviceScaleFactor !== undefined)
|
||||
throw new Error(`"deviceScaleFactor" option is not supported with null "viewport"`);
|
||||
if (options.noDefaultViewport && !!options.isMobile)
|
||||
|
|
@ -720,7 +717,7 @@ export function verifyGeolocation(geolocation?: types.Geolocation) {
|
|||
throw new Error(`geolocation.accuracy: precondition 0 <= ACCURACY failed.`);
|
||||
}
|
||||
|
||||
export function verifyClientCertificates(clientCertificates?: channels.BrowserNewContextParams['clientCertificates']) {
|
||||
export function verifyClientCertificates(clientCertificates?: types.BrowserContextOptions['clientCertificates']) {
|
||||
if (!clientCertificates)
|
||||
return;
|
||||
for (const cert of clientCertificates) {
|
||||
|
|
|
|||
|
|
@ -92,27 +92,26 @@ export abstract class BrowserType extends SdkObject {
|
|||
return browser;
|
||||
}
|
||||
|
||||
async launchPersistentContext(metadata: CallMetadata, userDataDir: string, options: channels.BrowserTypeLaunchPersistentContextOptions & { useWebSocket?: boolean }): Promise<BrowserContext> {
|
||||
options = this._validateLaunchOptions(options);
|
||||
async launchPersistentContext(metadata: CallMetadata, userDataDir: string, persistentContextOptions: channels.BrowserTypeLaunchPersistentContextOptions & { useWebSocket?: boolean }): Promise<BrowserContext> {
|
||||
const launchOptions = this._validateLaunchOptions(persistentContextOptions);
|
||||
if (this._useBidi)
|
||||
options.useWebSocket = true;
|
||||
launchOptions.useWebSocket = true;
|
||||
const controller = new ProgressController(metadata, this);
|
||||
const persistent: channels.BrowserNewContextParams = { ...options };
|
||||
controller.setLogName('browser');
|
||||
const browser = await controller.run(async progress => {
|
||||
// Note: Any initial TLS requests will fail since we rely on the Page/Frames initialize which sets ignoreHTTPSErrors.
|
||||
const clientCertificatesProxy = await createClientCertificatesProxyIfNeeded(persistent);
|
||||
const clientCertificatesProxy = await createClientCertificatesProxyIfNeeded(persistentContextOptions);
|
||||
if (clientCertificatesProxy)
|
||||
options.proxy = persistent.proxy;
|
||||
launchOptions.proxyOverride = await clientCertificatesProxy?.listen();
|
||||
progress.cleanupWhenAborted(() => clientCertificatesProxy?.close());
|
||||
const browser = await this._innerLaunchWithRetries(progress, options, persistent, helper.debugProtocolLogger(), userDataDir).catch(e => { throw this._rewriteStartupLog(e); });
|
||||
const browser = await this._innerLaunchWithRetries(progress, launchOptions, persistentContextOptions, helper.debugProtocolLogger(), userDataDir).catch(e => { throw this._rewriteStartupLog(e); });
|
||||
browser._defaultContext!._clientCertificatesProxy = clientCertificatesProxy;
|
||||
return browser;
|
||||
}, TimeoutSettings.launchTimeout(options));
|
||||
}, TimeoutSettings.launchTimeout(launchOptions));
|
||||
return browser._defaultContext!;
|
||||
}
|
||||
|
||||
async _innerLaunchWithRetries(progress: Progress, options: types.LaunchOptions, persistent: channels.BrowserNewContextParams | undefined, protocolLogger: types.ProtocolLogger, userDataDir?: string): Promise<Browser> {
|
||||
async _innerLaunchWithRetries(progress: Progress, options: types.LaunchOptions, persistent: types.BrowserContextOptions | undefined, protocolLogger: types.ProtocolLogger, userDataDir?: string): Promise<Browser> {
|
||||
try {
|
||||
return await this._innerLaunch(progress, options, persistent, protocolLogger, userDataDir);
|
||||
} catch (error) {
|
||||
|
|
@ -126,7 +125,7 @@ export abstract class BrowserType extends SdkObject {
|
|||
}
|
||||
}
|
||||
|
||||
async _innerLaunch(progress: Progress, options: types.LaunchOptions, persistent: channels.BrowserNewContextParams | undefined, protocolLogger: types.ProtocolLogger, maybeUserDataDir?: string): Promise<Browser> {
|
||||
async _innerLaunch(progress: Progress, options: types.LaunchOptions, persistent: types.BrowserContextOptions | undefined, protocolLogger: types.ProtocolLogger, maybeUserDataDir?: string): Promise<Browser> {
|
||||
options.proxy = options.proxy ? normalizeProxySettings(options.proxy) : undefined;
|
||||
const browserLogsCollector = new RecentLogsCollector();
|
||||
const { browserProcess, userDataDir, artifactsDir, transport } = await this._launchProcess(progress, options, !!persistent, browserLogsCollector, maybeUserDataDir);
|
||||
|
|
@ -289,7 +288,7 @@ export abstract class BrowserType extends SdkObject {
|
|||
throw new Error('Connecting to SELENIUM_REMOTE_URL is only supported by Chromium');
|
||||
}
|
||||
|
||||
private _validateLaunchOptions<Options extends types.LaunchOptions>(options: Options): Options {
|
||||
private _validateLaunchOptions(options: types.LaunchOptions): types.LaunchOptions {
|
||||
const { devtools = false } = options;
|
||||
let { headless = !devtools, downloadsPath, proxy } = options;
|
||||
if (debugMode())
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ import { CRDevTools } from './crDevTools';
|
|||
import type { BrowserOptions, BrowserProcess } from '../browser';
|
||||
import { Browser } from '../browser';
|
||||
import type * as types from '../types';
|
||||
import type * as channels from '@protocol/channels';
|
||||
import type { HTTPRequestParams } from '../../utils/network';
|
||||
import { fetchData } from '../../utils/network';
|
||||
import { getUserAgent } from '../../utils/userAgent';
|
||||
|
|
@ -98,7 +97,7 @@ export class Chromium extends BrowserType {
|
|||
await cleanedUp;
|
||||
};
|
||||
const browserProcess: BrowserProcess = { close: doClose, kill: doClose };
|
||||
const persistent: channels.BrowserNewContextParams = { noDefaultViewport: true };
|
||||
const persistent: types.BrowserContextOptions = { noDefaultViewport: true };
|
||||
const browserOptions: BrowserOptions = {
|
||||
slowMo: options.slowMo,
|
||||
name: 'chromium',
|
||||
|
|
@ -287,7 +286,7 @@ export class Chromium extends BrowserType {
|
|||
}
|
||||
|
||||
private _innerDefaultArgs(options: types.LaunchOptions): string[] {
|
||||
const { args = [], proxy } = options;
|
||||
const { args = [] } = options;
|
||||
const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir'));
|
||||
if (userDataDirArg)
|
||||
throw this._createUserDataDirArgMisuseError('--user-data-dir');
|
||||
|
|
@ -321,6 +320,7 @@ export class Chromium extends BrowserType {
|
|||
}
|
||||
if (options.chromiumSandbox !== true)
|
||||
chromeArguments.push('--no-sandbox');
|
||||
const proxy = options.proxyOverride || options.proxy;
|
||||
if (proxy) {
|
||||
const proxyURL = new URL(proxy.server);
|
||||
const isSocks = proxyURL.protocol === 'socks5:';
|
||||
|
|
|
|||
|
|
@ -100,18 +100,19 @@ export class CRBrowser extends Browser {
|
|||
this._session.on('Browser.downloadProgress', this._onDownloadProgress.bind(this));
|
||||
}
|
||||
|
||||
async doCreateNewContext(options: channels.BrowserNewContextParams): Promise<BrowserContext> {
|
||||
async doCreateNewContext(options: types.BrowserContextOptions): Promise<BrowserContext> {
|
||||
const proxy = options.proxyOverride || options.proxy;
|
||||
let proxyBypassList = undefined;
|
||||
if (options.proxy) {
|
||||
if (proxy) {
|
||||
if (process.env.PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK)
|
||||
proxyBypassList = options.proxy.bypass;
|
||||
proxyBypassList = proxy.bypass;
|
||||
else
|
||||
proxyBypassList = '<-loopback>' + (options.proxy.bypass ? `,${options.proxy.bypass}` : '');
|
||||
proxyBypassList = '<-loopback>' + (proxy.bypass ? `,${proxy.bypass}` : '');
|
||||
}
|
||||
|
||||
const { browserContextId } = await this._session.send('Target.createBrowserContext', {
|
||||
disposeOnDetach: true,
|
||||
proxyServer: options.proxy ? options.proxy.server : undefined,
|
||||
proxyServer: proxy ? proxy.server : undefined,
|
||||
proxyBypassList,
|
||||
});
|
||||
const context = new CRBrowserContext(this, browserContextId, options);
|
||||
|
|
@ -340,7 +341,7 @@ export class CRBrowserContext extends BrowserContext {
|
|||
|
||||
declare readonly _browser: CRBrowser;
|
||||
|
||||
constructor(browser: CRBrowser, browserContextId: string | undefined, options: channels.BrowserNewContextParams) {
|
||||
constructor(browser: CRBrowser, browserContextId: string | undefined, options: types.BrowserContextOptions) {
|
||||
super(browser, options, browserContextId);
|
||||
this._authenticateProxyViaCredentials();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -247,6 +247,10 @@ export class CRPage implements PageDelegate {
|
|||
return this._go(+1);
|
||||
}
|
||||
|
||||
async forceGarbageCollection(): Promise<void> {
|
||||
await this._mainFrameSession._client.send('HeapProfiler.collectGarbage');
|
||||
}
|
||||
|
||||
async addInitScript(initScript: InitScript, world: types.World = 'main'): Promise<void> {
|
||||
await this._forAllFrameSessions(frame => frame._evaluateOnNewDocument(initScript, world));
|
||||
}
|
||||
|
|
@ -543,7 +547,7 @@ class FrameSession {
|
|||
const options = this._crPage._browserContext._options;
|
||||
if (options.bypassCSP)
|
||||
promises.push(this._client.send('Page.setBypassCSP', { enabled: true }));
|
||||
if (options.ignoreHTTPSErrors)
|
||||
if (options.ignoreHTTPSErrors || options.internalIgnoreHTTPSErrors)
|
||||
promises.push(this._client.send('Security.setIgnoreCertificateErrors', { ignore: true }));
|
||||
if (this._isMainFrame())
|
||||
promises.push(this._updateViewport());
|
||||
|
|
@ -1213,7 +1217,7 @@ async function emulateTimezone(session: CRSession, timezoneId: string) {
|
|||
const contextDelegateSymbol = Symbol('delegate');
|
||||
|
||||
// Chromium reference: https://source.chromium.org/chromium/chromium/src/+/main:components/embedder_support/user_agent_utils.cc;l=434;drc=70a6711e08e9f9e0d8e4c48e9ba5cab62eb010c2
|
||||
function calculateUserAgentMetadata(options: channels.BrowserNewContextParams) {
|
||||
function calculateUserAgentMetadata(options: types.BrowserContextOptions) {
|
||||
const ua = options.userAgent;
|
||||
if (!ua)
|
||||
return undefined;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
|
||||
import type * as channels from '@protocol/channels';
|
||||
import { kMaxCookieExpiresDateInSeconds } from './network';
|
||||
|
||||
class Cookie {
|
||||
private _raw: channels.NetworkCookie;
|
||||
|
|
@ -115,6 +116,97 @@ export class CookieStore {
|
|||
}
|
||||
}
|
||||
|
||||
type RawCookie = {
|
||||
name: string,
|
||||
value: string,
|
||||
domain?: string,
|
||||
path?: string,
|
||||
expires?: number,
|
||||
httpOnly?: boolean,
|
||||
secure?: boolean,
|
||||
sameSite?: 'Strict' | 'Lax' | 'None',
|
||||
};
|
||||
|
||||
export function parseRawCookie(header: string): RawCookie | null {
|
||||
const pairs = header.split(';').filter(s => s.trim().length > 0).map(p => {
|
||||
let key = '';
|
||||
let value = '';
|
||||
const separatorPos = p.indexOf('=');
|
||||
if (separatorPos === -1) {
|
||||
// If only a key is specified, the value is left undefined.
|
||||
key = p.trim();
|
||||
} else {
|
||||
// Otherwise we assume that the key is the element before the first `=`
|
||||
key = p.slice(0, separatorPos).trim();
|
||||
// And the value is the rest of the string.
|
||||
value = p.slice(separatorPos + 1).trim();
|
||||
}
|
||||
return [key, value];
|
||||
});
|
||||
if (!pairs.length)
|
||||
return null;
|
||||
const [name, value] = pairs[0];
|
||||
const cookie: RawCookie = {
|
||||
name,
|
||||
value,
|
||||
};
|
||||
for (let i = 1; i < pairs.length; i++) {
|
||||
const [name, value] = pairs[i];
|
||||
switch (name.toLowerCase()) {
|
||||
case 'expires':
|
||||
const expiresMs = (+new Date(value));
|
||||
// https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.1
|
||||
if (isFinite(expiresMs)) {
|
||||
if (expiresMs <= 0)
|
||||
cookie.expires = 0;
|
||||
else
|
||||
cookie.expires = Math.min(expiresMs / 1000, kMaxCookieExpiresDateInSeconds);
|
||||
}
|
||||
break;
|
||||
case 'max-age':
|
||||
const maxAgeSec = parseInt(value, 10);
|
||||
if (isFinite(maxAgeSec)) {
|
||||
// From https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.2
|
||||
// If delta-seconds is less than or equal to zero (0), let expiry-time
|
||||
// be the earliest representable date and time.
|
||||
if (maxAgeSec <= 0)
|
||||
cookie.expires = 0;
|
||||
else
|
||||
cookie.expires = Math.min(Date.now() / 1000 + maxAgeSec, kMaxCookieExpiresDateInSeconds);
|
||||
}
|
||||
break;
|
||||
case 'domain':
|
||||
cookie.domain = value.toLocaleLowerCase() || '';
|
||||
if (cookie.domain && !cookie.domain.startsWith('.') && cookie.domain.includes('.'))
|
||||
cookie.domain = '.' + cookie.domain;
|
||||
break;
|
||||
case 'path':
|
||||
cookie.path = value || '';
|
||||
break;
|
||||
case 'secure':
|
||||
cookie.secure = true;
|
||||
break;
|
||||
case 'httponly':
|
||||
cookie.httpOnly = true;
|
||||
break;
|
||||
case 'samesite':
|
||||
switch (value.toLowerCase()) {
|
||||
case 'none':
|
||||
cookie.sameSite = 'None';
|
||||
break;
|
||||
case 'lax':
|
||||
cookie.sameSite = 'Lax';
|
||||
break;
|
||||
case 'strict':
|
||||
cookie.sameSite = 'Strict';
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return cookie;
|
||||
}
|
||||
|
||||
export function domainMatches(value: string, domain: string): boolean {
|
||||
if (value === domain)
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -137,6 +137,10 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
|
|||
return { response: ResponseDispatcher.fromNullable(this.parentScope(), await this._page.goForward(metadata, params)) };
|
||||
}
|
||||
|
||||
async forceGarbageCollection(params: channels.PageForceGarbageCollectionParams, metadata: CallMetadata): Promise<channels.PageForceGarbageCollectionResult> {
|
||||
await this._page.forceGarbageCollection();
|
||||
}
|
||||
|
||||
async registerLocatorHandler(params: channels.PageRegisterLocatorHandlerParams, metadata: CallMetadata): Promise<channels.PageRegisterLocatorHandlerResult> {
|
||||
const uid = this._page.registerLocatorHandler(params.selector, params.noWaitAfter);
|
||||
return { uid };
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import type { BrowserWindow } from 'electron';
|
|||
import type { Progress } from '../progress';
|
||||
import { ProgressController } from '../progress';
|
||||
import { helper } from '../helper';
|
||||
import type * as types from '../types';
|
||||
import { eventsHelper } from '../../utils/eventsHelper';
|
||||
import type { BrowserOptions, BrowserProcess } from '../browser';
|
||||
import type { Playwright } from '../playwright';
|
||||
|
|
@ -265,7 +266,7 @@ export class Electron extends SdkObject {
|
|||
close: gracefullyClose,
|
||||
kill
|
||||
};
|
||||
const contextOptions: channels.BrowserNewContextParams = {
|
||||
const contextOptions: types.BrowserContextOptions = {
|
||||
...options,
|
||||
noDefaultViewport: true,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import { getUserAgent } from '../utils/userAgent';
|
|||
import { assert, createGuid, monotonicTime } from '../utils';
|
||||
import { HttpsProxyAgent, SocksProxyAgent } from '../utilsBundle';
|
||||
import { BrowserContext, verifyClientCertificates } from './browserContext';
|
||||
import { CookieStore, domainMatches } from './cookieStore';
|
||||
import { CookieStore, domainMatches, parseRawCookie } from './cookieStore';
|
||||
import { MultipartFormData } from './formData';
|
||||
import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent } from '../utils/happy-eyeballs';
|
||||
import type { CallMetadata } from './instrumentation';
|
||||
|
|
@ -39,7 +39,6 @@ import { ProgressController } from './progress';
|
|||
import { Tracing } from './trace/recorder/tracing';
|
||||
import type * as types from './types';
|
||||
import type { HeadersArray, ProxySettings } from './types';
|
||||
import { kMaxCookieExpiresDateInSeconds } from './network';
|
||||
import { getMatchingTLSOptionsForOrigin, rewriteOpenSSLErrorIfNeeded } from './socksClientCertificatesInterceptor';
|
||||
|
||||
type FetchRequestOptions = {
|
||||
|
|
@ -50,7 +49,7 @@ type FetchRequestOptions = {
|
|||
timeoutSettings: TimeoutSettings;
|
||||
ignoreHTTPSErrors?: boolean;
|
||||
baseURL?: string;
|
||||
clientCertificates?: channels.BrowserNewContextOptions['clientCertificates'];
|
||||
clientCertificates?: types.BrowserContextOptions['clientCertificates'];
|
||||
};
|
||||
|
||||
type HeadersObject = Readonly<{ [name: string]: string }>;
|
||||
|
|
@ -640,27 +639,10 @@ function toHeadersArray(rawHeaders: string[]): types.HeadersArray {
|
|||
const redirectStatus = [301, 302, 303, 307, 308];
|
||||
|
||||
function parseCookie(header: string): channels.NetworkCookie | null {
|
||||
const pairs = header.split(';').filter(s => s.trim().length > 0).map(p => {
|
||||
let key = '';
|
||||
let value = '';
|
||||
const separatorPos = p.indexOf('=');
|
||||
if (separatorPos === -1) {
|
||||
// If only a key is specified, the value is left undefined.
|
||||
key = p.trim();
|
||||
} else {
|
||||
// Otherwise we assume that the key is the element before the first `=`
|
||||
key = p.slice(0, separatorPos).trim();
|
||||
// And the value is the rest of the string.
|
||||
value = p.slice(separatorPos + 1).trim();
|
||||
}
|
||||
return [key, value];
|
||||
});
|
||||
if (!pairs.length)
|
||||
const raw = parseRawCookie(header);
|
||||
if (!raw)
|
||||
return null;
|
||||
const [name, value] = pairs[0];
|
||||
const cookie: channels.NetworkCookie = {
|
||||
name,
|
||||
value,
|
||||
domain: '',
|
||||
path: '',
|
||||
expires: -1,
|
||||
|
|
@ -668,62 +650,9 @@ function parseCookie(header: string): channels.NetworkCookie | null {
|
|||
secure: false,
|
||||
// From https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
|
||||
// The cookie-sending behavior if SameSite is not specified is SameSite=Lax.
|
||||
sameSite: 'Lax'
|
||||
sameSite: 'Lax',
|
||||
...raw
|
||||
};
|
||||
for (let i = 1; i < pairs.length; i++) {
|
||||
const [name, value] = pairs[i];
|
||||
switch (name.toLowerCase()) {
|
||||
case 'expires':
|
||||
const expiresMs = (+new Date(value));
|
||||
// https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.1
|
||||
if (isFinite(expiresMs)) {
|
||||
if (expiresMs <= 0)
|
||||
cookie.expires = 0;
|
||||
else
|
||||
cookie.expires = Math.min(expiresMs / 1000, kMaxCookieExpiresDateInSeconds);
|
||||
}
|
||||
break;
|
||||
case 'max-age':
|
||||
const maxAgeSec = parseInt(value, 10);
|
||||
if (isFinite(maxAgeSec)) {
|
||||
// From https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.2
|
||||
// If delta-seconds is less than or equal to zero (0), let expiry-time
|
||||
// be the earliest representable date and time.
|
||||
if (maxAgeSec <= 0)
|
||||
cookie.expires = 0;
|
||||
else
|
||||
cookie.expires = Math.min(Date.now() / 1000 + maxAgeSec, kMaxCookieExpiresDateInSeconds);
|
||||
}
|
||||
break;
|
||||
case 'domain':
|
||||
cookie.domain = value.toLocaleLowerCase() || '';
|
||||
if (cookie.domain && !cookie.domain.startsWith('.') && cookie.domain.includes('.'))
|
||||
cookie.domain = '.' + cookie.domain;
|
||||
break;
|
||||
case 'path':
|
||||
cookie.path = value || '';
|
||||
break;
|
||||
case 'secure':
|
||||
cookie.secure = true;
|
||||
break;
|
||||
case 'httponly':
|
||||
cookie.httpOnly = true;
|
||||
break;
|
||||
case 'samesite':
|
||||
switch (value.toLowerCase()) {
|
||||
case 'none':
|
||||
cookie.sameSite = 'None';
|
||||
break;
|
||||
case 'lax':
|
||||
cookie.sameSite = 'Lax';
|
||||
break;
|
||||
case 'strict':
|
||||
cookie.sameSite = 'Strict';
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return cookie;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -58,8 +58,9 @@ export class FFBrowser extends Browser {
|
|||
browser._defaultContext = new FFBrowserContext(browser, undefined, options.persistent);
|
||||
promises.push((browser._defaultContext as FFBrowserContext)._initialize());
|
||||
}
|
||||
if (options.proxy)
|
||||
promises.push(browser.session.send('Browser.setBrowserProxy', toJugglerProxyOptions(options.proxy)));
|
||||
const proxy = options.originalLaunchOptions.proxyOverride || options.proxy;
|
||||
if (proxy)
|
||||
promises.push(browser.session.send('Browser.setBrowserProxy', toJugglerProxyOptions(proxy)));
|
||||
await Promise.all(promises);
|
||||
return browser;
|
||||
}
|
||||
|
|
@ -88,7 +89,7 @@ export class FFBrowser extends Browser {
|
|||
return !this._connection._closed;
|
||||
}
|
||||
|
||||
async doCreateNewContext(options: channels.BrowserNewContextParams): Promise<BrowserContext> {
|
||||
async doCreateNewContext(options: types.BrowserContextOptions): Promise<BrowserContext> {
|
||||
if (options.isMobile)
|
||||
throw new Error('options.isMobile is not supported in Firefox');
|
||||
const { browserContextId } = await this.session.send('Browser.createBrowserContext', { removeOnDetach: true });
|
||||
|
|
@ -172,7 +173,7 @@ export class FFBrowser extends Browser {
|
|||
export class FFBrowserContext extends BrowserContext {
|
||||
declare readonly _browser: FFBrowser;
|
||||
|
||||
constructor(browser: FFBrowser, browserContextId: string | undefined, options: channels.BrowserNewContextParams) {
|
||||
constructor(browser: FFBrowser, browserContextId: string | undefined, options: types.BrowserContextOptions) {
|
||||
super(browser, options, browserContextId);
|
||||
}
|
||||
|
||||
|
|
@ -205,7 +206,7 @@ export class FFBrowserContext extends BrowserContext {
|
|||
promises.push(this._browser.session.send('Browser.setUserAgentOverride', { browserContextId, userAgent: this._options.userAgent }));
|
||||
if (this._options.bypassCSP)
|
||||
promises.push(this._browser.session.send('Browser.setBypassCSP', { browserContextId, bypassCSP: true }));
|
||||
if (this._options.ignoreHTTPSErrors)
|
||||
if (this._options.ignoreHTTPSErrors || this._options.internalIgnoreHTTPSErrors)
|
||||
promises.push(this._browser.session.send('Browser.setIgnoreHTTPSErrors', { browserContextId, ignoreHTTPSErrors: true }));
|
||||
if (this._options.javaScriptEnabled === false)
|
||||
promises.push(this._browser.session.send('Browser.setJavaScriptDisabled', { browserContextId, javaScriptDisabled: true }));
|
||||
|
|
@ -251,10 +252,11 @@ export class FFBrowserContext extends BrowserContext {
|
|||
});
|
||||
}));
|
||||
}
|
||||
if (this._options.proxy) {
|
||||
const proxy = this._options.proxyOverride || this._options.proxy;
|
||||
if (proxy) {
|
||||
promises.push(this._browser.session.send('Browser.setContextProxy', {
|
||||
browserContextId: this._browserContextId,
|
||||
...toJugglerProxyOptions(this._options.proxy)
|
||||
...toJugglerProxyOptions(proxy)
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -400,6 +400,10 @@ export class FFPage implements PageDelegate {
|
|||
return success;
|
||||
}
|
||||
|
||||
async forceGarbageCollection(): Promise<void> {
|
||||
await this._session.send('Heap.collectGarbage');
|
||||
}
|
||||
|
||||
async addInitScript(initScript: InitScript, worldName?: string): Promise<void> {
|
||||
this._initScripts.push({ initScript, worldName });
|
||||
await this._session.send('Page.setInitScripts', { scripts: this._initScripts.map(s => ({ script: s.initScript.source, worldName: s.worldName })) });
|
||||
|
|
|
|||
|
|
@ -315,6 +315,11 @@ export module Protocol {
|
|||
};
|
||||
export type cancelDownloadReturnValue = void;
|
||||
}
|
||||
export module Heap {
|
||||
export type collectGarbageParameters = {
|
||||
};
|
||||
export type collectGarbageReturnValue = void;
|
||||
}
|
||||
export module Page {
|
||||
export type DOMPoint = {
|
||||
x: number;
|
||||
|
|
@ -1124,6 +1129,7 @@ export module Protocol {
|
|||
"Browser.setForcedColors": Browser.setForcedColorsParameters;
|
||||
"Browser.setVideoRecordingOptions": Browser.setVideoRecordingOptionsParameters;
|
||||
"Browser.cancelDownload": Browser.cancelDownloadParameters;
|
||||
"Heap.collectGarbage": Heap.collectGarbageParameters;
|
||||
"Page.close": Page.closeParameters;
|
||||
"Page.setFileInputFiles": Page.setFileInputFilesParameters;
|
||||
"Page.addBinding": Page.addBindingParameters;
|
||||
|
|
@ -1204,6 +1210,7 @@ export module Protocol {
|
|||
"Browser.setForcedColors": Browser.setForcedColorsReturnValue;
|
||||
"Browser.setVideoRecordingOptions": Browser.setVideoRecordingOptionsReturnValue;
|
||||
"Browser.cancelDownload": Browser.cancelDownloadReturnValue;
|
||||
"Heap.collectGarbage": Heap.collectGarbageReturnValue;
|
||||
"Page.close": Page.closeReturnValue;
|
||||
"Page.setFileInputFiles": Page.setFileInputFilesReturnValue;
|
||||
"Page.addBinding": Page.addBindingReturnValue;
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ export interface PageDelegate {
|
|||
reload(): Promise<void>;
|
||||
goBack(): Promise<boolean>;
|
||||
goForward(): Promise<boolean>;
|
||||
forceGarbageCollection(): Promise<void>;
|
||||
addInitScript(initScript: InitScript): Promise<void>;
|
||||
removeNonInternalInitScripts(): Promise<void>;
|
||||
closePage(runBeforeUnload: boolean): Promise<void>;
|
||||
|
|
@ -430,6 +431,10 @@ export class Page extends SdkObject {
|
|||
}), this._timeoutSettings.navigationTimeout(options));
|
||||
}
|
||||
|
||||
forceGarbageCollection(): Promise<void> {
|
||||
return this._delegate.forceGarbageCollection();
|
||||
}
|
||||
|
||||
registerLocatorHandler(selector: string, noWaitAfter: boolean | undefined) {
|
||||
const uid = ++this._lastLocatorHandlerUid;
|
||||
this._locatorHandlers.set(uid, { selector, noWaitAfter });
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import { createSocket, createTLSSocket } from '../utils/happy-eyeballs';
|
|||
import { escapeHTML, generateSelfSignedCertificate, ManualPromise, rewriteErrorMessage } from '../utils';
|
||||
import type { SocksSocketClosedPayload, SocksSocketDataPayload, SocksSocketRequestedPayload } from '../common/socksProxy';
|
||||
import { SocksProxy } from '../common/socksProxy';
|
||||
import type * as channels from '@protocol/channels';
|
||||
import type * as types from './types';
|
||||
import { debugLogger } from '../utils/debugLogger';
|
||||
|
||||
let dummyServerTlsOptions: tls.TlsOptions | undefined = undefined;
|
||||
|
|
@ -235,7 +235,7 @@ export class ClientCertificatesProxy {
|
|||
alpnCache: ALPNCache;
|
||||
|
||||
constructor(
|
||||
contextOptions: Pick<channels.BrowserNewContextOptions, 'clientCertificates' | 'ignoreHTTPSErrors'>
|
||||
contextOptions: Pick<types.BrowserContextOptions, 'clientCertificates' | 'ignoreHTTPSErrors'>
|
||||
) {
|
||||
this.alpnCache = new ALPNCache();
|
||||
this.ignoreHTTPSErrors = contextOptions.ignoreHTTPSErrors;
|
||||
|
|
@ -261,9 +261,9 @@ export class ClientCertificatesProxy {
|
|||
loadDummyServerCertsIfNeeded();
|
||||
}
|
||||
|
||||
_initSecureContexts(clientCertificates: channels.BrowserNewContextOptions['clientCertificates']) {
|
||||
_initSecureContexts(clientCertificates: types.BrowserContextOptions['clientCertificates']) {
|
||||
// Step 1. Group certificates by origin.
|
||||
const origin2certs = new Map<string, channels.BrowserNewContextOptions['clientCertificates']>();
|
||||
const origin2certs = new Map<string, types.BrowserContextOptions['clientCertificates']>();
|
||||
for (const cert of clientCertificates || []) {
|
||||
const origin = normalizeOrigin(cert.origin);
|
||||
const certs = origin2certs.get(origin) || [];
|
||||
|
|
@ -282,9 +282,9 @@ export class ClientCertificatesProxy {
|
|||
}
|
||||
}
|
||||
|
||||
public async listen(): Promise<string> {
|
||||
public async listen() {
|
||||
const port = await this._socksProxy.listen(0, '127.0.0.1');
|
||||
return `socks5://127.0.0.1:${port}`;
|
||||
return { server: `socks5://127.0.0.1:${port}` };
|
||||
}
|
||||
|
||||
public async close() {
|
||||
|
|
@ -301,7 +301,7 @@ function normalizeOrigin(origin: string): string {
|
|||
}
|
||||
|
||||
function convertClientCertificatesToTLSOptions(
|
||||
clientCertificates: channels.BrowserNewContextOptions['clientCertificates']
|
||||
clientCertificates: types.BrowserContextOptions['clientCertificates']
|
||||
): Pick<https.RequestOptions, 'pfx' | 'key' | 'cert'> | undefined {
|
||||
if (!clientCertificates || !clientCertificates.length)
|
||||
return;
|
||||
|
|
@ -322,7 +322,7 @@ function convertClientCertificatesToTLSOptions(
|
|||
}
|
||||
|
||||
export function getMatchingTLSOptionsForOrigin(
|
||||
clientCertificates: channels.BrowserNewContextOptions['clientCertificates'],
|
||||
clientCertificates: types.BrowserContextOptions['clientCertificates'],
|
||||
origin: string
|
||||
): Pick<https.RequestOptions, 'pfx' | 'key' | 'cert'> | undefined {
|
||||
const matchingCerts = clientCertificates?.filter(c =>
|
||||
|
|
|
|||
|
|
@ -310,7 +310,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
|||
|
||||
this._fs.copyFile(this._state.networkFile, newNetworkFile);
|
||||
|
||||
const zipFileName = this._state.traceFile + '.zip';
|
||||
const zipFileName = this._state.traceFile + '.pwtrace.zip';
|
||||
if (params.mode === 'archive')
|
||||
this._fs.zip(entries, zipFileName);
|
||||
|
||||
|
|
|
|||
|
|
@ -150,7 +150,15 @@ export type NormalizedContinueOverrides = {
|
|||
|
||||
export type EmulatedSize = { viewport: Size, screen: Size };
|
||||
|
||||
export type LaunchOptions = channels.BrowserTypeLaunchOptions & { useWebSocket?: boolean };
|
||||
export type LaunchOptions = channels.BrowserTypeLaunchOptions & {
|
||||
useWebSocket?: boolean,
|
||||
proxyOverride?: ProxySettings,
|
||||
};
|
||||
|
||||
export type BrowserContextOptions = channels.BrowserNewContextOptions & {
|
||||
proxyOverride?: ProxySettings;
|
||||
internalIgnoreHTTPSErrors?: boolean;
|
||||
};
|
||||
|
||||
export type ProtocolLogger = (direction: 'send' | 'receive', message: object) => void;
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export class WebKit extends BrowserType {
|
|||
}
|
||||
|
||||
override defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[] {
|
||||
const { args = [], proxy, headless } = options;
|
||||
const { args = [], headless } = options;
|
||||
const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir'));
|
||||
if (userDataDirArg)
|
||||
throw this._createUserDataDirArgMisuseError('--user-data-dir');
|
||||
|
|
@ -68,6 +68,7 @@ export class WebKit extends BrowserType {
|
|||
webkitArguments.push(`--user-data-dir=${userDataDir}`);
|
||||
else
|
||||
webkitArguments.push(`--no-startup-window`);
|
||||
const proxy = options.proxyOverride || options.proxy;
|
||||
if (proxy) {
|
||||
if (process.platform === 'darwin') {
|
||||
webkitArguments.push(`--proxy=${proxy.server}`);
|
||||
|
|
|
|||
|
|
@ -81,12 +81,13 @@ export class WKBrowser extends Browser {
|
|||
this._didClose();
|
||||
}
|
||||
|
||||
async doCreateNewContext(options: channels.BrowserNewContextParams): Promise<BrowserContext> {
|
||||
const createOptions = options.proxy ? {
|
||||
// Enable socks5 hostname resolution on Windows. Workaround can be removed once fixed upstream.
|
||||
async doCreateNewContext(options: types.BrowserContextOptions): Promise<BrowserContext> {
|
||||
const proxy = options.proxyOverride || options.proxy;
|
||||
const createOptions = proxy ? {
|
||||
// Enable socks5 hostname resolution on Windows.
|
||||
// See https://github.com/microsoft/playwright/issues/20451
|
||||
proxyServer: process.platform === 'win32' ? options.proxy.server.replace(/^socks5:\/\//, 'socks5h://') : options.proxy.server,
|
||||
proxyBypassList: options.proxy.bypass
|
||||
proxyServer: process.platform === 'win32' ? proxy.server.replace(/^socks5:\/\//, 'socks5h://') : proxy.server,
|
||||
proxyBypassList: proxy.bypass
|
||||
} : undefined;
|
||||
const { browserContextId } = await this._browserSession.send('Playwright.createContext', createOptions);
|
||||
options.userAgent = options.userAgent || DEFAULT_USER_AGENT;
|
||||
|
|
@ -206,7 +207,7 @@ export class WKBrowser extends Browser {
|
|||
export class WKBrowserContext extends BrowserContext {
|
||||
declare readonly _browser: WKBrowser;
|
||||
|
||||
constructor(browser: WKBrowser, browserContextId: string | undefined, options: channels.BrowserNewContextParams) {
|
||||
constructor(browser: WKBrowser, browserContextId: string | undefined, options: types.BrowserContextOptions) {
|
||||
super(browser, options, browserContextId);
|
||||
this._validateEmulatedViewport(options.viewport);
|
||||
this._authenticateProxyViaHeader();
|
||||
|
|
@ -221,7 +222,7 @@ export class WKBrowserContext extends BrowserContext {
|
|||
downloadPath: this._browser.options.downloadsPath,
|
||||
browserContextId
|
||||
}));
|
||||
if (this._options.ignoreHTTPSErrors)
|
||||
if (this._options.ignoreHTTPSErrors || this._options.internalIgnoreHTTPSErrors)
|
||||
promises.push(this._browser._browserSession.send('Playwright.setIgnoreCertificateErrors', { browserContextId, ignore: true }));
|
||||
if (this._options.locale)
|
||||
promises.push(this._browser._browserSession.send('Playwright.setLanguages', { browserContextId, languages: [this._options.locale] }));
|
||||
|
|
|
|||
|
|
@ -768,6 +768,10 @@ export class WKPage implements PageDelegate {
|
|||
});
|
||||
}
|
||||
|
||||
async forceGarbageCollection(): Promise<void> {
|
||||
await this._session.send('Heap.gc');
|
||||
}
|
||||
|
||||
async addInitScript(initScript: InitScript): Promise<void> {
|
||||
await this._updateBootstrapScript();
|
||||
}
|
||||
|
|
|
|||
27
packages/playwright-core/types/types.d.ts
vendored
27
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -2554,6 +2554,11 @@ export interface Page {
|
|||
timeout?: number;
|
||||
}): Promise<void>;
|
||||
|
||||
/**
|
||||
* Force the browser to perform garbage collection.
|
||||
*/
|
||||
forceGarbageCollection(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Returns frame matching the specified criteria. Either `name` or `url` must be specified.
|
||||
*
|
||||
|
|
@ -16559,7 +16564,7 @@ export interface APIRequestContext {
|
|||
* as this request body. If this parameter is specified `content-type` header will be set to
|
||||
* `application/x-www-form-urlencoded` unless explicitly provided.
|
||||
*/
|
||||
form?: { [key: string]: string|number|boolean; };
|
||||
form?: { [key: string]: string|number|boolean; }|FormData;
|
||||
|
||||
/**
|
||||
* Allows to set HTTP headers. These headers will apply to the fetched request as well as any redirects initiated by
|
||||
|
|
@ -16689,7 +16694,7 @@ export interface APIRequestContext {
|
|||
* as this request body. If this parameter is specified `content-type` header will be set to
|
||||
* `application/x-www-form-urlencoded` unless explicitly provided.
|
||||
*/
|
||||
form?: { [key: string]: string|number|boolean; };
|
||||
form?: { [key: string]: string|number|boolean; }|FormData;
|
||||
|
||||
/**
|
||||
* Allows to set HTTP headers. These headers will apply to the fetched request as well as any redirects initiated by
|
||||
|
|
@ -16807,7 +16812,7 @@ export interface APIRequestContext {
|
|||
* as this request body. If this parameter is specified `content-type` header will be set to
|
||||
* `application/x-www-form-urlencoded` unless explicitly provided.
|
||||
*/
|
||||
form?: { [key: string]: string|number|boolean; };
|
||||
form?: { [key: string]: string|number|boolean; }|FormData;
|
||||
|
||||
/**
|
||||
* Allows to set HTTP headers. These headers will apply to the fetched request as well as any redirects initiated by
|
||||
|
|
@ -16893,7 +16898,7 @@ export interface APIRequestContext {
|
|||
* as this request body. If this parameter is specified `content-type` header will be set to
|
||||
* `application/x-www-form-urlencoded` unless explicitly provided.
|
||||
*/
|
||||
form?: { [key: string]: string|number|boolean; };
|
||||
form?: { [key: string]: string|number|boolean; }|FormData;
|
||||
|
||||
/**
|
||||
* Allows to set HTTP headers. These headers will apply to the fetched request as well as any redirects initiated by
|
||||
|
|
@ -16979,7 +16984,7 @@ export interface APIRequestContext {
|
|||
* as this request body. If this parameter is specified `content-type` header will be set to
|
||||
* `application/x-www-form-urlencoded` unless explicitly provided.
|
||||
*/
|
||||
form?: { [key: string]: string|number|boolean; };
|
||||
form?: { [key: string]: string|number|boolean; }|FormData;
|
||||
|
||||
/**
|
||||
* Allows to set HTTP headers. These headers will apply to the fetched request as well as any redirects initiated by
|
||||
|
|
@ -17107,7 +17112,7 @@ export interface APIRequestContext {
|
|||
* as this request body. If this parameter is specified `content-type` header will be set to
|
||||
* `application/x-www-form-urlencoded` unless explicitly provided.
|
||||
*/
|
||||
form?: { [key: string]: string|number|boolean; };
|
||||
form?: { [key: string]: string|number|boolean; }|FormData;
|
||||
|
||||
/**
|
||||
* Allows to set HTTP headers. These headers will apply to the fetched request as well as any redirects initiated by
|
||||
|
|
@ -17193,7 +17198,7 @@ export interface APIRequestContext {
|
|||
* as this request body. If this parameter is specified `content-type` header will be set to
|
||||
* `application/x-www-form-urlencoded` unless explicitly provided.
|
||||
*/
|
||||
form?: { [key: string]: string|number|boolean; };
|
||||
form?: { [key: string]: string|number|boolean; }|FormData;
|
||||
|
||||
/**
|
||||
* Allows to set HTTP headers. These headers will apply to the fetched request as well as any redirects initiated by
|
||||
|
|
@ -19925,7 +19930,7 @@ export interface Touchscreen {
|
|||
* await context.tracing.start({ screenshots: true, snapshots: true });
|
||||
* const page = await context.newPage();
|
||||
* await page.goto('https://playwright.dev');
|
||||
* await context.tracing.stop({ path: 'trace.zip' });
|
||||
* await context.tracing.stop({ path: 'trace.pwtrace.zip' });
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
|
|
@ -19939,7 +19944,7 @@ export interface Tracing {
|
|||
* await context.tracing.start({ screenshots: true, snapshots: true });
|
||||
* const page = await context.newPage();
|
||||
* await page.goto('https://playwright.dev');
|
||||
* await context.tracing.stop({ path: 'trace.zip' });
|
||||
* await context.tracing.stop({ path: 'trace.pwtrace.zip' });
|
||||
* ```
|
||||
*
|
||||
* @param options
|
||||
|
|
@ -19994,12 +19999,12 @@ export interface Tracing {
|
|||
* await context.tracing.startChunk();
|
||||
* await page.getByText('Get Started').click();
|
||||
* // Everything between startChunk and stopChunk will be recorded in the trace.
|
||||
* await context.tracing.stopChunk({ path: 'trace1.zip' });
|
||||
* await context.tracing.stopChunk({ path: 'trace1.pwtrace.zip' });
|
||||
*
|
||||
* await context.tracing.startChunk();
|
||||
* await page.goto('http://example.com');
|
||||
* // Save a second trace file with different actions.
|
||||
* await context.tracing.stopChunk({ path: 'trace2.zip' });
|
||||
* await context.tracing.stopChunk({ path: 'trace2.pwtrace.zip' });
|
||||
* ```
|
||||
*
|
||||
* @param options
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export interface TestServerInterface {
|
|||
closeOnDisconnect?: boolean,
|
||||
interceptStdio?: boolean,
|
||||
watchTestDirs?: boolean,
|
||||
populateDependenciesOnList?: boolean,
|
||||
}): Promise<void>;
|
||||
|
||||
ping(params: {}): Promise<void>;
|
||||
|
|
|
|||
|
|
@ -121,10 +121,10 @@ function createExpect(info: ExpectMetaInfo, prefix: string[], customMatchers: Re
|
|||
const [actual, messageOrOptions] = argumentsList;
|
||||
const message = isString(messageOrOptions) ? messageOrOptions : messageOrOptions?.message || info.message;
|
||||
const newInfo = { ...info, message };
|
||||
if (newInfo.isPoll) {
|
||||
if (newInfo.poll) {
|
||||
if (typeof actual !== 'function')
|
||||
throw new Error('`expect.poll()` accepts only function as a first argument');
|
||||
newInfo.generator = actual as any;
|
||||
newInfo.poll.generator = actual as any;
|
||||
}
|
||||
return createMatchers(actual, newInfo, prefix);
|
||||
},
|
||||
|
|
@ -189,10 +189,10 @@ function createExpect(info: ExpectMetaInfo, prefix: string[], customMatchers: Re
|
|||
if ('soft' in configuration)
|
||||
newInfo.isSoft = configuration.soft;
|
||||
if ('_poll' in configuration) {
|
||||
newInfo.isPoll = !!configuration._poll;
|
||||
newInfo.poll = configuration._poll ? { ...info.poll, generator: () => {} } : undefined;
|
||||
if (typeof configuration._poll === 'object') {
|
||||
newInfo.pollTimeout = configuration._poll.timeout;
|
||||
newInfo.pollIntervals = configuration._poll.intervals;
|
||||
newInfo.poll!.timeout = configuration._poll.timeout ?? newInfo.poll!.timeout;
|
||||
newInfo.poll!.intervals = configuration._poll.intervals ?? newInfo.poll!.intervals;
|
||||
}
|
||||
}
|
||||
return createExpect(newInfo, prefix, customMatchers);
|
||||
|
|
@ -249,11 +249,12 @@ type ExpectMetaInfo = {
|
|||
message?: string;
|
||||
isNot?: boolean;
|
||||
isSoft?: boolean;
|
||||
isPoll?: boolean;
|
||||
poll?: {
|
||||
timeout?: number;
|
||||
intervals?: number[];
|
||||
generator: Generator;
|
||||
};
|
||||
timeout?: number;
|
||||
pollTimeout?: number;
|
||||
pollIntervals?: number[];
|
||||
generator?: Generator;
|
||||
};
|
||||
|
||||
class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
||||
|
|
@ -287,10 +288,10 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
|||
this._info.isNot = !this._info.isNot;
|
||||
return new Proxy(matcher, this);
|
||||
}
|
||||
if (this._info.isPoll) {
|
||||
if (this._info.poll) {
|
||||
if ((customAsyncMatchers as any)[matcherName] || matcherName === 'resolves' || matcherName === 'rejects')
|
||||
throw new Error(`\`expect.poll()\` does not support "${matcherName}" matcher.`);
|
||||
matcher = (...args: any[]) => pollMatcher(resolvedMatcherName, !!this._info.isNot, this._info.pollIntervals, this._info.pollTimeout ?? currentExpectTimeout(), this._info.generator!, ...args);
|
||||
matcher = (...args: any[]) => pollMatcher(resolvedMatcherName, this._info, this._prefix, ...args);
|
||||
}
|
||||
return (...args: any[]) => {
|
||||
const testInfo = currentTestInfo();
|
||||
|
|
@ -302,7 +303,7 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
|||
const customMessage = this._info.message || '';
|
||||
const argsSuffix = computeArgsSuffix(matcherName, args);
|
||||
|
||||
const defaultTitle = `expect${this._info.isPoll ? '.poll' : ''}${this._info.isSoft ? '.soft' : ''}${this._info.isNot ? '.not' : ''}.${matcherName}${argsSuffix}`;
|
||||
const defaultTitle = `expect${this._info.poll ? '.poll' : ''}${this._info.isSoft ? '.soft' : ''}${this._info.isNot ? '.not' : ''}.${matcherName}${argsSuffix}`;
|
||||
const title = customMessage || defaultTitle;
|
||||
|
||||
// This looks like it is unnecessary, but it isn't - we need to filter
|
||||
|
|
@ -336,7 +337,7 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
|||
const callback = () => matcher.call(target, ...args);
|
||||
// toPass and poll matchers can contain other steps, expects and API calls,
|
||||
// so they behave like a retriable step.
|
||||
const result = (matcherName === 'toPass' || this._info.isPoll) ?
|
||||
const result = (matcherName === 'toPass' || this._info.poll) ?
|
||||
zones.run('stepZone', step, callback) :
|
||||
zones.run<ExpectZone, any>('expectZone', { title, stepId: step.stepId }, callback);
|
||||
if (result instanceof Promise)
|
||||
|
|
@ -350,25 +351,32 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
|||
}
|
||||
}
|
||||
|
||||
async function pollMatcher(qualifiedMatcherName: any, isNot: boolean, pollIntervals: number[] | undefined, timeout: number, generator: () => any, ...args: any[]) {
|
||||
async function pollMatcher(qualifiedMatcherName: string, info: ExpectMetaInfo, prefix: string[], ...args: any[]) {
|
||||
const testInfo = currentTestInfo();
|
||||
const poll = info.poll!;
|
||||
const timeout = poll.timeout ?? currentExpectTimeout();
|
||||
const { deadline, timeoutMessage } = testInfo ? testInfo._deadlineForMatcher(timeout) : TestInfoImpl._defaultDeadlineForMatcher(timeout);
|
||||
|
||||
const result = await pollAgainstDeadline<Error|undefined>(async () => {
|
||||
if (testInfo && currentTestInfo() !== testInfo)
|
||||
return { continuePolling: false, result: undefined };
|
||||
|
||||
const value = await generator();
|
||||
let expectInstance = expectLibrary(value) as any;
|
||||
if (isNot)
|
||||
expectInstance = expectInstance.not;
|
||||
const innerInfo: ExpectMetaInfo = {
|
||||
...info,
|
||||
isSoft: false, // soft is outside of poll, not inside
|
||||
poll: undefined,
|
||||
};
|
||||
const value = await poll.generator();
|
||||
try {
|
||||
expectInstance[qualifiedMatcherName].call(expectInstance, ...args);
|
||||
let matchers = createMatchers(value, innerInfo, prefix);
|
||||
if (info.isNot)
|
||||
matchers = matchers.not;
|
||||
matchers[qualifiedMatcherName](...args);
|
||||
return { continuePolling: false, result: undefined };
|
||||
} catch (error) {
|
||||
return { continuePolling: true, result: error };
|
||||
}
|
||||
}, deadline, pollIntervals ?? [100, 250, 500, 1000]);
|
||||
}, deadline, poll.intervals ?? [100, 250, 500, 1000]);
|
||||
|
||||
if (result.timedOut) {
|
||||
const message = result.result ? [
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ Examples:
|
|||
}
|
||||
|
||||
function addMergeReportsCommand(program: Command) {
|
||||
const command = program.command('merge-reports [dir]', { hidden: true });
|
||||
const command = program.command('merge-reports [dir]');
|
||||
command.description('merge multiple blob reports (for sharded tests) into a single report');
|
||||
command.action(async (dir, options) => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ export class TestServerDispatcher implements TestServerInterface {
|
|||
private _serializer = require.resolve('./uiModeReporter');
|
||||
private _watchTestDirs = false;
|
||||
private _closeOnDisconnect = false;
|
||||
private _populateDependenciesOnList = false;
|
||||
|
||||
constructor(configLocation: ConfigLocation) {
|
||||
this._configLocation = configLocation;
|
||||
|
|
@ -113,6 +114,7 @@ export class TestServerDispatcher implements TestServerInterface {
|
|||
this._closeOnDisconnect = !!params.closeOnDisconnect;
|
||||
await this._setInterceptStdio(!!params.interceptStdio);
|
||||
this._watchTestDirs = !!params.watchTestDirs;
|
||||
this._populateDependenciesOnList = !!params.populateDependenciesOnList;
|
||||
}
|
||||
|
||||
async ping() {}
|
||||
|
|
@ -252,7 +254,7 @@ export class TestServerDispatcher implements TestServerInterface {
|
|||
config.cliListOnly = true;
|
||||
|
||||
const status = await runTasks(new TestRun(config, reporter), [
|
||||
createLoadTask('out-of-process', { failOnLoadErrors: false, filterOnly: false }),
|
||||
createLoadTask('out-of-process', { failOnLoadErrors: false, filterOnly: false, populateDependencies: this._populateDependenciesOnList }),
|
||||
createReportBeginTask(),
|
||||
]);
|
||||
return { config, report, reporter, status };
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ export async function runWatchModeLoop(configLocation: ConfigLocation, initialOp
|
|||
});
|
||||
testServerConnection.onReport(report => teleSuiteUpdater.processTestReportEvent(report));
|
||||
|
||||
await testServerConnection.initialize({ interceptStdio: false, watchTestDirs: true });
|
||||
await testServerConnection.initialize({ interceptStdio: false, watchTestDirs: true, populateDependenciesOnList: true });
|
||||
await testServerConnection.runGlobalSetup({});
|
||||
|
||||
const { report } = await testServerConnection.listTests({});
|
||||
|
|
@ -133,9 +133,6 @@ export async function runWatchModeLoop(configLocation: ConfigLocation, initialOp
|
|||
let lastRun: { type: 'changed' | 'regular' | 'failed', failedTestIds?: string[], dirtyTestIds?: string[] } = { type: 'regular' };
|
||||
let result: FullResult['status'] = 'passed';
|
||||
|
||||
// Enter the watch loop.
|
||||
await runTests(options, testServerConnection);
|
||||
|
||||
while (true) {
|
||||
printPrompt();
|
||||
const readCommandPromise = readCommand();
|
||||
|
|
@ -330,7 +327,7 @@ Change settings
|
|||
|
||||
let showBrowserServer: PlaywrightServer | undefined;
|
||||
let connectWsEndpoint: string | undefined = undefined;
|
||||
let seq = 0;
|
||||
let seq = 1;
|
||||
|
||||
function printConfiguration(options: WatchModeOptions, title?: string) {
|
||||
const packageManagerCommand = getPackageManagerExecCommand();
|
||||
|
|
@ -344,9 +341,7 @@ function printConfiguration(options: WatchModeOptions, title?: string) {
|
|||
tokens.push(...options.files.map(a => colors.bold(a)));
|
||||
if (title)
|
||||
tokens.push(colors.dim(`(${title})`));
|
||||
if (seq)
|
||||
tokens.push(colors.dim(`#${seq}`));
|
||||
++seq;
|
||||
tokens.push(colors.dim(`#${seq++}`));
|
||||
const lines: string[] = [];
|
||||
const sep = separator();
|
||||
lines.push('\x1Bc' + sep);
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ export class TestTracing {
|
|||
}
|
||||
|
||||
generateNextTraceRecordingPath() {
|
||||
const file = path.join(this._artifactsDir, createGuid() + '.zip');
|
||||
const file = path.join(this._artifactsDir, createGuid() + '.pwtrace.zip');
|
||||
this._temporaryTraceFiles.push(file);
|
||||
return file;
|
||||
}
|
||||
|
|
@ -214,7 +214,7 @@ export class TestTracing {
|
|||
});
|
||||
});
|
||||
|
||||
const tracePath = this._testInfo.outputPath('trace.zip');
|
||||
const tracePath = this._testInfo.outputPath('trace.pwtrace.zip');
|
||||
await mergeTraceFiles(tracePath, this._temporaryTraceFiles);
|
||||
this._testInfo.attachments.push({ name: 'trace', path: tracePath, contentType: 'application/zip' });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1933,6 +1933,7 @@ export interface PageChannel extends PageEventTarget, EventTargetChannel {
|
|||
exposeBinding(params: PageExposeBindingParams, metadata?: CallMetadata): Promise<PageExposeBindingResult>;
|
||||
goBack(params: PageGoBackParams, metadata?: CallMetadata): Promise<PageGoBackResult>;
|
||||
goForward(params: PageGoForwardParams, metadata?: CallMetadata): Promise<PageGoForwardResult>;
|
||||
forceGarbageCollection(params?: PageForceGarbageCollectionParams, metadata?: CallMetadata): Promise<PageForceGarbageCollectionResult>;
|
||||
registerLocatorHandler(params: PageRegisterLocatorHandlerParams, metadata?: CallMetadata): Promise<PageRegisterLocatorHandlerResult>;
|
||||
resolveLocatorHandlerNoReply(params: PageResolveLocatorHandlerNoReplyParams, metadata?: CallMetadata): Promise<PageResolveLocatorHandlerNoReplyResult>;
|
||||
unregisterLocatorHandler(params: PageUnregisterLocatorHandlerParams, metadata?: CallMetadata): Promise<PageUnregisterLocatorHandlerResult>;
|
||||
|
|
@ -2070,6 +2071,9 @@ export type PageGoForwardOptions = {
|
|||
export type PageGoForwardResult = {
|
||||
response?: ResponseChannel,
|
||||
};
|
||||
export type PageForceGarbageCollectionParams = {};
|
||||
export type PageForceGarbageCollectionOptions = {};
|
||||
export type PageForceGarbageCollectionResult = void;
|
||||
export type PageRegisterLocatorHandlerParams = {
|
||||
selector: string,
|
||||
noWaitAfter?: boolean,
|
||||
|
|
|
|||
|
|
@ -1430,6 +1430,8 @@ Page:
|
|||
slowMo: true
|
||||
snapshot: true
|
||||
|
||||
forceGarbageCollection:
|
||||
|
||||
registerLocatorHandler:
|
||||
parameters:
|
||||
selector: string
|
||||
|
|
|
|||
|
|
@ -62,21 +62,21 @@ const test = baseTest.extend<BrowserTestTestFixtures, BrowserTestWorkerFixtures>
|
|||
await run(playwright[browserName]);
|
||||
}, { scope: 'worker' }],
|
||||
|
||||
allowsThirdParty: [async ({ browserName, browserMajorVersion, channel }, run) => {
|
||||
allowsThirdParty: [async ({ browserName }, run) => {
|
||||
if (browserName === 'firefox')
|
||||
await run(true);
|
||||
else
|
||||
await run(false);
|
||||
}, { scope: 'worker' }],
|
||||
|
||||
defaultSameSiteCookieValue: [async ({ browserName, browserMajorVersion, channel, isLinux }, run) => {
|
||||
if (browserName === 'chromium')
|
||||
defaultSameSiteCookieValue: [async ({ browserName, isLinux }, run) => {
|
||||
if (browserName === 'chromium' || browserName as any === '_bidiChromium')
|
||||
await run('Lax');
|
||||
else if (browserName === 'webkit' && isLinux)
|
||||
await run('Lax');
|
||||
else if (browserName === 'webkit' && !isLinux)
|
||||
await run('None');
|
||||
else if (browserName === 'firefox')
|
||||
else if (browserName === 'firefox' || browserName as any === '_bidiFirefox')
|
||||
await run('None');
|
||||
else
|
||||
throw new Error('unknown browser - ' + browserName);
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ export const traceViewerFixtures: Fixtures<TraceViewerFixtures, {}, BaseTestFixt
|
|||
|
||||
runAndTrace: async ({ context, showTraceViewer }, use, testInfo) => {
|
||||
await use(async (body: () => Promise<void>, optsOverrides = {}) => {
|
||||
const traceFile = testInfo.outputPath('trace.zip');
|
||||
const traceFile = testInfo.outputPath('trace.pwtrace.zip');
|
||||
await context.tracing.start({ snapshots: true, screenshots: true, sources: true, ...optsOverrides });
|
||||
await body();
|
||||
await context.tracing.stop({ path: traceFile });
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ test('should work when wrapped inside @playwright/test and trace is enabled', as
|
|||
await expect(window).toHaveTitle(/Playwright/);
|
||||
await expect(window.getByRole('heading')).toHaveText('Playwright');
|
||||
|
||||
const path = test.info().outputPath('electron-trace.zip');
|
||||
const path = test.info().outputPath('electron-trace.pwtrace.zip');
|
||||
if (trace) {
|
||||
await window.context().tracing.stop({ path });
|
||||
test.info().attachments.push({ name: 'trace', path, contentType: 'application/zip' });
|
||||
|
|
@ -73,9 +73,9 @@ test('should work when wrapped inside @playwright/test and trace is enabled', as
|
|||
});
|
||||
const traces = [
|
||||
// our actual trace.
|
||||
path.join(tmpWorkspace, 'test-results', 'electron-with-tracing-should-work', 'electron-trace.zip'),
|
||||
path.join(tmpWorkspace, 'test-results', 'electron-with-tracing-should-work', 'electron-trace.pwtrace.zip'),
|
||||
// contains the expect() calls
|
||||
path.join(tmpWorkspace, 'test-results', 'electron-with-tracing-should-work', 'trace.zip'),
|
||||
path.join(tmpWorkspace, 'test-results', 'electron-with-tracing-should-work', 'trace.pwtrace.zip'),
|
||||
];
|
||||
for (const trace of traces)
|
||||
expect(fs.existsSync(trace)).toBe(true);
|
||||
|
|
|
|||
|
|
@ -960,6 +960,22 @@ it('should support application/x-www-form-urlencoded', async function({ context,
|
|||
expect(params.get('file')).toBe('f.js');
|
||||
});
|
||||
|
||||
it('should support application/x-www-form-urlencoded with param lists', async function({ context, page, server }) {
|
||||
const form = new FormData();
|
||||
form.append('foo', '1');
|
||||
form.append('foo', '2');
|
||||
const [req] = await Promise.all([
|
||||
server.waitForRequest('/empty.html'),
|
||||
context.request.post(server.EMPTY_PAGE, { form })
|
||||
]);
|
||||
expect(req.method).toBe('POST');
|
||||
expect(req.headers['content-type']).toBe('application/x-www-form-urlencoded');
|
||||
const body = (await req.postBody).toString('utf8');
|
||||
const params = new URLSearchParams(body);
|
||||
expect(req.headers['content-length']).toBe(String(params.toString().length));
|
||||
expect(params.getAll('foo')).toEqual(['1', '2']);
|
||||
});
|
||||
|
||||
it('should encode to application/json by default', async function({ context, page, server }) {
|
||||
const data = {
|
||||
firstName: 'John',
|
||||
|
|
|
|||
|
|
@ -248,7 +248,7 @@ test('should reset tracing', async ({ reusedContext, trace }, testInfo) => {
|
|||
page = context.pages()[0];
|
||||
await page.evaluate('2 + 2');
|
||||
|
||||
const error = await context.tracing.stopChunk({ path: testInfo.outputPath('trace.zip') }).catch(e => e);
|
||||
const error = await context.tracing.stopChunk({ path: testInfo.outputPath('trace.pwtrace.zip') }).catch(e => e);
|
||||
expect(error.message).toContain('Must start tracing before stopping');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -489,7 +489,7 @@ test('should allow tracing over cdp session', async ({ browserType, trace }, tes
|
|||
await context.tracing.start({ screenshots: true, snapshots: true });
|
||||
const page = await context.newPage();
|
||||
await page.evaluate(() => 2 + 2);
|
||||
const traceZip = testInfo.outputPath('trace.zip');
|
||||
const traceZip = testInfo.outputPath('trace.pwtrace.zip');
|
||||
await context.tracing.stop({ path: traceZip });
|
||||
await cdpBrowser.close();
|
||||
expect(fs.existsSync(traceZip)).toBe(true);
|
||||
|
|
|
|||
|
|
@ -546,6 +546,7 @@ test.describe('browser', () => {
|
|||
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
||||
};
|
||||
const page = await browser.newPage({
|
||||
ignoreHTTPSErrors: true,
|
||||
clientCertificates: [{
|
||||
origin: new URL(serverURL).origin,
|
||||
...baseOptions,
|
||||
|
|
|
|||
|
|
@ -491,7 +491,7 @@ await page1.GotoAsync("about:blank?foo");`);
|
|||
});
|
||||
|
||||
test('should --save-trace', async ({ runCLI }, testInfo) => {
|
||||
const traceFileName = testInfo.outputPath('trace.zip');
|
||||
const traceFileName = testInfo.outputPath('trace.pwtrace.zip');
|
||||
const cli = runCLI([`--save-trace=${traceFileName}`], {
|
||||
autoExitWhen: ' ',
|
||||
});
|
||||
|
|
@ -502,7 +502,7 @@ await page1.GotoAsync("about:blank?foo");`);
|
|||
test('should save assets via SIGINT', async ({ runCLI, platform }, testInfo) => {
|
||||
test.skip(platform === 'win32', 'SIGINT not supported on Windows');
|
||||
|
||||
const traceFileName = testInfo.outputPath('trace.zip');
|
||||
const traceFileName = testInfo.outputPath('trace.pwtrace.zip');
|
||||
const storageFileName = testInfo.outputPath('auth.json');
|
||||
const harFileName = testInfo.outputPath('har.har');
|
||||
const cli = runCLI([`--save-trace=${traceFileName}`, `--save-storage=${storageFileName}`, `--save-har=${harFileName}`]);
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ test.beforeAll(async function recordTrace({ browser, browserName, browserType, s
|
|||
runBeforeCloseBrowserContext: async () => {
|
||||
await page.hover('body');
|
||||
await page.close();
|
||||
traceFile = path.join(workerInfo.project.outputDir, String(workerInfo.workerIndex), browserName, 'trace.zip');
|
||||
traceFile = path.join(workerInfo.project.outputDir, String(workerInfo.workerIndex), browserName, 'trace.pwtrace.zip');
|
||||
await context.tracing.stop({ path: traceFile });
|
||||
}
|
||||
};
|
||||
|
|
@ -698,7 +698,7 @@ test('should handle file URIs', async ({ page, runAndTrace, browserName }) => {
|
|||
});
|
||||
|
||||
test('should preserve currentSrc', async ({ browser, server, showTraceViewer }) => {
|
||||
const traceFile = test.info().outputPath('trace.zip');
|
||||
const traceFile = test.info().outputPath('trace.pwtrace.zip');
|
||||
const page = await browser.newPage({ deviceScaleFactor: 3 });
|
||||
await page.context().tracing.start({ snapshots: true, screenshots: true, sources: true });
|
||||
await page.setViewportSize({ width: 300, height: 300 });
|
||||
|
|
@ -1294,7 +1294,7 @@ test('should highlight locator in iframe while typing', async ({ page, runAndTra
|
|||
});
|
||||
|
||||
test('should preserve noscript when javascript is disabled', async ({ browser, server, showTraceViewer }) => {
|
||||
const traceFile = test.info().outputPath('trace.zip');
|
||||
const traceFile = test.info().outputPath('trace.pwtrace.zip');
|
||||
const page = await browser.newPage({ javaScriptEnabled: false });
|
||||
await page.context().tracing.start({ snapshots: true, screenshots: true, sources: true });
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
|
|
@ -1311,8 +1311,8 @@ test('should preserve noscript when javascript is disabled', async ({ browser, s
|
|||
await expect(frame.getByText('javascript is disabled!')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should remove noscript by default', async ({ browser, server, showTraceViewer, browserType }) => {
|
||||
const traceFile = test.info().outputPath('trace.zip');
|
||||
test('should remove noscript by default', async ({ browser, server, showTraceViewer }) => {
|
||||
const traceFile = test.info().outputPath('trace.pwtrace.zip');
|
||||
const page = await browser.newPage({ javaScriptEnabled: undefined });
|
||||
await page.context().tracing.start({ snapshots: true, screenshots: true, sources: true });
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
|
|
@ -1329,8 +1329,8 @@ test('should remove noscript by default', async ({ browser, server, showTraceVie
|
|||
await expect(frame.getByText('Enable JavaScript to run this app.')).toBeHidden();
|
||||
});
|
||||
|
||||
test('should remove noscript when javaScriptEnabled is set to true', async ({ browser, server, showTraceViewer, browserType }) => {
|
||||
const traceFile = test.info().outputPath('trace.zip');
|
||||
test('should remove noscript when javaScriptEnabled is set to true', async ({ browser, server, showTraceViewer }) => {
|
||||
const traceFile = test.info().outputPath('trace.pwtrace.zip');
|
||||
const page = await browser.newPage({ javaScriptEnabled: true });
|
||||
await page.context().tracing.start({ snapshots: true, screenshots: true, sources: true });
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
|
|
|
|||
|
|
@ -37,9 +37,9 @@ test('should collect trace with resources, but no js', async ({ context, page, s
|
|||
await page.locator('input[type="file"]').setInputFiles(asset('file-to-upload.txt'));
|
||||
await page.waitForTimeout(2000); // Give it some time to produce screenshots.
|
||||
await page.close();
|
||||
await context.tracing.stop({ path: testInfo.outputPath('trace.zip') });
|
||||
await context.tracing.stop({ path: testInfo.outputPath('trace.pwtrace.zip') });
|
||||
|
||||
const { events, actions } = await parseTraceRaw(testInfo.outputPath('trace.zip'));
|
||||
const { events, actions } = await parseTraceRaw(testInfo.outputPath('trace.pwtrace.zip'));
|
||||
expect(events[0].type).toBe('context-options');
|
||||
expect(actions).toEqual([
|
||||
'page.goto',
|
||||
|
|
@ -81,8 +81,8 @@ test('should use the correct apiName for event driven callbacks', async ({ conte
|
|||
});
|
||||
await page.evaluate(() => alert('yo'));
|
||||
|
||||
await context.tracing.stop({ path: testInfo.outputPath('trace.zip') });
|
||||
const { events, actions } = await parseTraceRaw(testInfo.outputPath('trace.zip'));
|
||||
await context.tracing.stop({ path: testInfo.outputPath('trace.pwtrace.zip') });
|
||||
const { events, actions } = await parseTraceRaw(testInfo.outputPath('trace.pwtrace.zip'));
|
||||
expect(events[0].type).toBe('context-options');
|
||||
expect(actions).toEqual([
|
||||
'page.route',
|
||||
|
|
@ -102,9 +102,9 @@ test('should not collect snapshots by default', async ({ context, page, server }
|
|||
await page.setContent('<button>Click</button>');
|
||||
await page.click('"Click"');
|
||||
await page.close();
|
||||
await context.tracing.stop({ path: testInfo.outputPath('trace.zip') });
|
||||
await context.tracing.stop({ path: testInfo.outputPath('trace.pwtrace.zip') });
|
||||
|
||||
const { events } = await parseTraceRaw(testInfo.outputPath('trace.zip'));
|
||||
const { events } = await parseTraceRaw(testInfo.outputPath('trace.pwtrace.zip'));
|
||||
expect(events.some(e => e.type === 'frame-snapshot')).toBeFalsy();
|
||||
expect(events.some(e => e.type === 'resource-snapshot')).toBeFalsy();
|
||||
});
|
||||
|
|
@ -113,8 +113,8 @@ test('should not include buffers in the trace', async ({ context, page, server }
|
|||
await context.tracing.start({ snapshots: true });
|
||||
await page.goto(server.PREFIX + '/empty.html');
|
||||
await page.screenshot();
|
||||
await context.tracing.stop({ path: testInfo.outputPath('trace.zip') });
|
||||
const { actionObjects } = await parseTraceRaw(testInfo.outputPath('trace.zip'));
|
||||
await context.tracing.stop({ path: testInfo.outputPath('trace.pwtrace.zip') });
|
||||
const { actionObjects } = await parseTraceRaw(testInfo.outputPath('trace.pwtrace.zip'));
|
||||
const screenshotEvent = actionObjects.find(a => a.apiName === 'page.screenshot');
|
||||
expect(screenshotEvent.beforeSnapshot).toBeTruthy();
|
||||
expect(screenshotEvent.afterSnapshot).toBeTruthy();
|
||||
|
|
@ -129,9 +129,9 @@ test('should exclude internal pages', async ({ browserName, context, page, serve
|
|||
await context.tracing.start();
|
||||
await context.storageState();
|
||||
await page.close();
|
||||
await context.tracing.stop({ path: testInfo.outputPath('trace.zip') });
|
||||
await context.tracing.stop({ path: testInfo.outputPath('trace.pwtrace.zip') });
|
||||
|
||||
const trace = await parseTraceRaw(testInfo.outputPath('trace.zip'));
|
||||
const trace = await parseTraceRaw(testInfo.outputPath('trace.pwtrace.zip'));
|
||||
const pageIds = new Set();
|
||||
trace.events.forEach(e => {
|
||||
const pageId = e.pageId;
|
||||
|
|
@ -144,8 +144,8 @@ test('should exclude internal pages', async ({ browserName, context, page, serve
|
|||
test('should include context API requests', async ({ browserName, context, page, server }, testInfo) => {
|
||||
await context.tracing.start({ snapshots: true });
|
||||
await page.request.post(server.PREFIX + '/simple.json', { data: { foo: 'bar' } });
|
||||
await context.tracing.stop({ path: testInfo.outputPath('trace.zip') });
|
||||
const { events } = await parseTraceRaw(testInfo.outputPath('trace.zip'));
|
||||
await context.tracing.stop({ path: testInfo.outputPath('trace.pwtrace.zip') });
|
||||
const { events } = await parseTraceRaw(testInfo.outputPath('trace.pwtrace.zip'));
|
||||
const postEvent = events.find(e => e.apiName === 'apiRequestContext.post');
|
||||
expect(postEvent).toBeTruthy();
|
||||
const harEntry = events.find(e => e.type === 'resource-snapshot');
|
||||
|
|
@ -428,9 +428,9 @@ for (const params of [
|
|||
await page.setContent('<body style="box-sizing: border-box; width: 100%; height: 100%; margin:0; background: red; border: 50px solid blue"></body>');
|
||||
await page.evaluate(() => new Promise(window.builtinRequestAnimationFrame));
|
||||
}
|
||||
await context.tracing.stop({ path: testInfo.outputPath('trace.zip') });
|
||||
await context.tracing.stop({ path: testInfo.outputPath('trace.pwtrace.zip') });
|
||||
|
||||
const { events, resources } = await parseTraceRaw(testInfo.outputPath('trace.zip'));
|
||||
const { events, resources } = await parseTraceRaw(testInfo.outputPath('trace.pwtrace.zip'));
|
||||
const frames = events.filter(e => e.type === 'screencast-frame');
|
||||
|
||||
// Check all frame sizes.
|
||||
|
|
@ -460,10 +460,10 @@ test('should include interrupted actions', async ({ context, page, server }, tes
|
|||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.setContent('<button>Click</button>');
|
||||
page.click('"ClickNoButton"').catch(() => {});
|
||||
await context.tracing.stop({ path: testInfo.outputPath('trace.zip') });
|
||||
await context.tracing.stop({ path: testInfo.outputPath('trace.pwtrace.zip') });
|
||||
await context.close();
|
||||
|
||||
const { events } = await parseTraceRaw(testInfo.outputPath('trace.zip'));
|
||||
const { events } = await parseTraceRaw(testInfo.outputPath('trace.pwtrace.zip'));
|
||||
const clickEvent = events.find(e => e.apiName === 'page.click');
|
||||
expect(clickEvent).toBeTruthy();
|
||||
});
|
||||
|
|
@ -475,7 +475,7 @@ test('should throw when starting with different options', async ({ context }) =>
|
|||
});
|
||||
|
||||
test('should throw when stopping without start', async ({ context }, testInfo) => {
|
||||
const error = await context.tracing.stop({ path: testInfo.outputPath('trace.zip') }).catch(e => e);
|
||||
const error = await context.tracing.stop({ path: testInfo.outputPath('trace.pwtrace.zip') }).catch(e => e);
|
||||
expect(error.message).toContain('Must start tracing before stopping');
|
||||
});
|
||||
|
||||
|
|
@ -492,7 +492,7 @@ test('should work with multiple chunks', async ({ context, page, server }, testI
|
|||
await page.click('"Click"');
|
||||
page.click('"ClickNoButton"', { timeout: 0 }).catch(() => {});
|
||||
await page.evaluate(() => {});
|
||||
await context.tracing.stopChunk({ path: testInfo.outputPath('trace.zip') });
|
||||
await context.tracing.stopChunk({ path: testInfo.outputPath('trace.pwtrace.zip') });
|
||||
|
||||
await context.tracing.startChunk();
|
||||
await page.hover('"Click"');
|
||||
|
|
@ -502,7 +502,7 @@ test('should work with multiple chunks', async ({ context, page, server }, testI
|
|||
await page.click('"Click"');
|
||||
await context.tracing.stopChunk(); // Should stop without a path.
|
||||
|
||||
const trace1 = await parseTraceRaw(testInfo.outputPath('trace.zip'));
|
||||
const trace1 = await parseTraceRaw(testInfo.outputPath('trace.pwtrace.zip'));
|
||||
expect(trace1.events[0].type).toBe('context-options');
|
||||
expect(trace1.actions).toEqual([
|
||||
'page.setContent',
|
||||
|
|
@ -533,7 +533,7 @@ test('should export trace concurrently to second navigation', async ({ context,
|
|||
await page.waitForTimeout(timeout);
|
||||
await Promise.all([
|
||||
promise,
|
||||
context.tracing.stop({ path: testInfo.outputPath('trace.zip') }),
|
||||
context.tracing.stop({ path: testInfo.outputPath('trace.pwtrace.zip') }),
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
|
@ -561,9 +561,9 @@ test('should ignore iframes in head', async ({ context, page, server }, testInfo
|
|||
|
||||
await context.tracing.start({ screenshots: true, snapshots: true });
|
||||
await page.click('button');
|
||||
await context.tracing.stopChunk({ path: testInfo.outputPath('trace.zip') });
|
||||
await context.tracing.stopChunk({ path: testInfo.outputPath('trace.pwtrace.zip') });
|
||||
|
||||
const trace = await parseTraceRaw(testInfo.outputPath('trace.zip'));
|
||||
const trace = await parseTraceRaw(testInfo.outputPath('trace.pwtrace.zip'));
|
||||
expect(trace.actions).toEqual([
|
||||
'page.click',
|
||||
]);
|
||||
|
|
@ -581,7 +581,7 @@ test('should hide internal stack frames', async ({ context, page }, testInfo) =>
|
|||
await page.setContent(`<div onclick='window.alert(123)'>Click me</div>`);
|
||||
await page.click('div');
|
||||
await evalPromise;
|
||||
const tracePath = testInfo.outputPath('trace.zip');
|
||||
const tracePath = testInfo.outputPath('trace.pwtrace.zip');
|
||||
await context.tracing.stop({ path: tracePath });
|
||||
|
||||
const trace = await parseTraceRaw(tracePath);
|
||||
|
|
@ -602,7 +602,7 @@ test('should hide internal stack frames in expect', async ({ context, page }, te
|
|||
await page.click('div');
|
||||
await expect(page.locator('div')).toBeVisible();
|
||||
await expectPromise;
|
||||
const tracePath = testInfo.outputPath('trace.zip');
|
||||
const tracePath = testInfo.outputPath('trace.pwtrace.zip');
|
||||
await context.tracing.stop({ path: tracePath });
|
||||
|
||||
const trace = await parseTraceRaw(tracePath);
|
||||
|
|
@ -616,7 +616,7 @@ test('should record global request trace', async ({ request, context, server },
|
|||
await (request as any)._tracing.start({ snapshots: true });
|
||||
const url = server.PREFIX + '/simple.json';
|
||||
await request.get(url);
|
||||
const tracePath = testInfo.outputPath('trace.zip');
|
||||
const tracePath = testInfo.outputPath('trace.pwtrace.zip');
|
||||
await (request as any)._tracing.stop({ path: tracePath });
|
||||
|
||||
const trace = await parseTraceRaw(tracePath);
|
||||
|
|
@ -649,7 +649,7 @@ test('should store global request traces separately', async ({ request, server,
|
|||
request.get(url),
|
||||
request2.post(url)
|
||||
]);
|
||||
const tracePath = testInfo.outputPath('trace.zip');
|
||||
const tracePath = testInfo.outputPath('trace.pwtrace.zip');
|
||||
const trace2Path = testInfo.outputPath('trace2.zip');
|
||||
await Promise.all([
|
||||
(request as any)._tracing.stop({ path: tracePath }),
|
||||
|
|
@ -682,7 +682,7 @@ test('should store postData for global request', async ({ request, server }, tes
|
|||
await request.post(url, {
|
||||
data: 'test'
|
||||
});
|
||||
const tracePath = testInfo.outputPath('trace.zip');
|
||||
const tracePath = testInfo.outputPath('trace.pwtrace.zip');
|
||||
await (request as any)._tracing.stop({ path: tracePath });
|
||||
|
||||
const trace = await parseTraceRaw(tracePath);
|
||||
|
|
@ -755,7 +755,7 @@ test('should flush console events on tracing stop', async ({ context, page }, te
|
|||
});
|
||||
});
|
||||
await promise;
|
||||
const tracePath = testInfo.outputPath('trace.zip');
|
||||
const tracePath = testInfo.outputPath('trace.pwtrace.zip');
|
||||
await context.tracing.stop({ path: tracePath });
|
||||
const trace = await parseTraceRaw(tracePath);
|
||||
const events = trace.events.filter(e => e.type === 'console');
|
||||
|
|
|
|||
|
|
@ -799,7 +799,7 @@ it.describe('screencast', () => {
|
|||
it.fixme(!headless || !!process.env.PLAYWRIGHT_CHROMIUM_USE_HEADLESS_NEW, 'different trace screencast image size on all browsers');
|
||||
|
||||
const size = { width: 500, height: 400 };
|
||||
const traceFile = testInfo.outputPath('trace.zip');
|
||||
const traceFile = testInfo.outputPath('trace.pwtrace.zip');
|
||||
|
||||
const context = await browser.newContext({
|
||||
recordVideo: {
|
||||
|
|
|
|||
27
tests/page/page-force-gc.spec.ts
Normal file
27
tests/page/page-force-gc.spec.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* Copyright 2024 Adobe Inc. All rights reserved.
|
||||
*
|
||||
* 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, expect } from './pageTest';
|
||||
|
||||
test('should work', async ({ page }) => {
|
||||
await page.evaluate(() => {
|
||||
globalThis.objectToDestroy = {};
|
||||
globalThis.weakRef = new WeakRef(globalThis.objectToDestroy);
|
||||
});
|
||||
await page.evaluate(() => globalThis.objectToDestroy = null);
|
||||
await page.forceGarbageCollection();
|
||||
expect(await page.evaluate(() => globalThis.weakRef.deref())).toBe(undefined);
|
||||
});
|
||||
|
|
@ -29,7 +29,7 @@ export type PageWorkerFixtures = {
|
|||
screenshot: ScreenshotMode | { mode: ScreenshotMode } & Pick<PageScreenshotOptions, 'fullPage' | 'omitBackground'>;
|
||||
trace: 'off' | 'on' | 'retain-on-failure' | 'on-first-retry' | 'retain-on-first-failure' | 'on-all-retries' | /** deprecated */ 'retry-with-trace';
|
||||
video: VideoMode | { mode: VideoMode, size: ViewportSize };
|
||||
browserName: 'chromium' | 'firefox' | 'webkit';
|
||||
browserName: 'chromium' | 'firefox' | 'webkit' | '_bidiFirefox' | '_bidiChromium';
|
||||
browserVersion: string;
|
||||
browserMajorVersion: number;
|
||||
electronMajorVersion: number;
|
||||
|
|
|
|||
|
|
@ -263,3 +263,19 @@ test('should propagate string exception from async arrow function', { annotation
|
|||
|
||||
expect(result.output).toContain('some error');
|
||||
});
|
||||
|
||||
test('should show custom message', {
|
||||
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32582' }
|
||||
}, async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('should fail', async () => {
|
||||
await expect.poll(() => 1, { message: 'custom message', timeout: 500 }).toBe(2);
|
||||
});
|
||||
`,
|
||||
});
|
||||
expect(result.output).toContain('Error: custom message');
|
||||
expect(result.output).toContain('Expected: 2');
|
||||
expect(result.output).toContain('Received: 1');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -235,25 +235,25 @@ test('should work with trace: on', async ({ runInlineTest }, testInfo) => {
|
|||
expect(listFiles(testInfo.outputPath('test-results'))).toEqual([
|
||||
'.last-run.json',
|
||||
'artifacts-failing',
|
||||
' trace.zip',
|
||||
' trace.pwtrace.zip',
|
||||
'artifacts-own-context-failing',
|
||||
' trace.zip',
|
||||
' trace.pwtrace.zip',
|
||||
'artifacts-own-context-passing',
|
||||
' trace.zip',
|
||||
' trace.pwtrace.zip',
|
||||
'artifacts-passing',
|
||||
' trace.zip',
|
||||
' trace.pwtrace.zip',
|
||||
'artifacts-persistent-failing',
|
||||
' trace.zip',
|
||||
' trace.pwtrace.zip',
|
||||
'artifacts-persistent-passing',
|
||||
' trace.zip',
|
||||
' trace.pwtrace.zip',
|
||||
'artifacts-shared-shared-failing',
|
||||
' trace.zip',
|
||||
' trace.pwtrace.zip',
|
||||
'artifacts-shared-shared-passing',
|
||||
' trace.zip',
|
||||
' trace.pwtrace.zip',
|
||||
'artifacts-two-contexts',
|
||||
' trace.zip',
|
||||
' trace.pwtrace.zip',
|
||||
'artifacts-two-contexts-failing',
|
||||
' trace.zip',
|
||||
' trace.pwtrace.zip',
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -271,15 +271,15 @@ test('should work with trace: retain-on-failure', async ({ runInlineTest }, test
|
|||
expect(listFiles(testInfo.outputPath('test-results'))).toEqual([
|
||||
'.last-run.json',
|
||||
'artifacts-failing',
|
||||
' trace.zip',
|
||||
' trace.pwtrace.zip',
|
||||
'artifacts-own-context-failing',
|
||||
' trace.zip',
|
||||
' trace.pwtrace.zip',
|
||||
'artifacts-persistent-failing',
|
||||
' trace.zip',
|
||||
' trace.pwtrace.zip',
|
||||
'artifacts-shared-shared-failing',
|
||||
' trace.zip',
|
||||
' trace.pwtrace.zip',
|
||||
'artifacts-two-contexts-failing',
|
||||
' trace.zip',
|
||||
' trace.pwtrace.zip',
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -297,15 +297,15 @@ test('should work with trace: on-first-retry', async ({ runInlineTest }, testInf
|
|||
expect(listFiles(testInfo.outputPath('test-results'))).toEqual([
|
||||
'.last-run.json',
|
||||
'artifacts-failing-retry1',
|
||||
' trace.zip',
|
||||
' trace.pwtrace.zip',
|
||||
'artifacts-own-context-failing-retry1',
|
||||
' trace.zip',
|
||||
' trace.pwtrace.zip',
|
||||
'artifacts-persistent-failing-retry1',
|
||||
' trace.zip',
|
||||
' trace.pwtrace.zip',
|
||||
'artifacts-shared-shared-failing-retry1',
|
||||
' trace.zip',
|
||||
' trace.pwtrace.zip',
|
||||
'artifacts-two-contexts-failing-retry1',
|
||||
' trace.zip',
|
||||
' trace.pwtrace.zip',
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -323,25 +323,25 @@ test('should work with trace: on-all-retries', async ({ runInlineTest }, testInf
|
|||
expect(listFiles(testInfo.outputPath('test-results'))).toEqual([
|
||||
'.last-run.json',
|
||||
'artifacts-failing-retry1',
|
||||
' trace.zip',
|
||||
' trace.pwtrace.zip',
|
||||
'artifacts-failing-retry2',
|
||||
' trace.zip',
|
||||
' trace.pwtrace.zip',
|
||||
'artifacts-own-context-failing-retry1',
|
||||
' trace.zip',
|
||||
' trace.pwtrace.zip',
|
||||
'artifacts-own-context-failing-retry2',
|
||||
' trace.zip',
|
||||
' trace.pwtrace.zip',
|
||||
'artifacts-persistent-failing-retry1',
|
||||
' trace.zip',
|
||||
' trace.pwtrace.zip',
|
||||
'artifacts-persistent-failing-retry2',
|
||||
' trace.zip',
|
||||
' trace.pwtrace.zip',
|
||||
'artifacts-shared-shared-failing-retry1',
|
||||
' trace.zip',
|
||||
' trace.pwtrace.zip',
|
||||
'artifacts-shared-shared-failing-retry2',
|
||||
' trace.zip',
|
||||
' trace.pwtrace.zip',
|
||||
'artifacts-two-contexts-failing-retry1',
|
||||
' trace.zip',
|
||||
' trace.pwtrace.zip',
|
||||
'artifacts-two-contexts-failing-retry2',
|
||||
' trace.zip',
|
||||
' trace.pwtrace.zip',
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
@ -359,15 +359,15 @@ test('should work with trace: retain-on-first-failure', async ({ runInlineTest }
|
|||
expect(listFiles(testInfo.outputPath('test-results'))).toEqual([
|
||||
'.last-run.json',
|
||||
'artifacts-failing',
|
||||
' trace.zip',
|
||||
' trace.pwtrace.zip',
|
||||
'artifacts-own-context-failing',
|
||||
' trace.zip',
|
||||
' trace.pwtrace.zip',
|
||||
'artifacts-persistent-failing',
|
||||
' trace.zip',
|
||||
' trace.pwtrace.zip',
|
||||
'artifacts-shared-shared-failing',
|
||||
' trace.zip',
|
||||
' trace.pwtrace.zip',
|
||||
'artifacts-two-contexts-failing',
|
||||
' trace.zip',
|
||||
' trace.pwtrace.zip',
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ test('should reuse context with trace if mode=when-possible', async ({ runInline
|
|||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(2);
|
||||
|
||||
const trace1 = await parseTrace(testInfo.outputPath('test-results', 'reuse-one', 'trace.zip'));
|
||||
const trace1 = await parseTrace(testInfo.outputPath('test-results', 'reuse-one', 'trace.pwtrace.zip'));
|
||||
expect(trace1.actionTree).toEqual([
|
||||
'Before Hooks',
|
||||
' fixture: browser',
|
||||
|
|
@ -131,7 +131,7 @@ test('should reuse context with trace if mode=when-possible', async ({ runInline
|
|||
expect(trace1.traceModel.storage().snapshotsForTest().length).toBeGreaterThan(0);
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'reuse-one', 'trace-1.zip'))).toBe(false);
|
||||
|
||||
const trace2 = await parseTrace(testInfo.outputPath('test-results', 'reuse-two', 'trace.zip'));
|
||||
const trace2 = await parseTrace(testInfo.outputPath('test-results', 'reuse-two', 'trace.pwtrace.zip'));
|
||||
expect(trace2.actionTree).toEqual([
|
||||
'Before Hooks',
|
||||
' fixture: context',
|
||||
|
|
@ -533,6 +533,6 @@ test('should survive serial mode with tracing and reuse', async ({ runInlineTest
|
|||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(2);
|
||||
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'reuse-one', 'trace.zip'))).toBe(true);
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'reuse-two', 'trace.zip'))).toBe(true);
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'reuse-one', 'trace.pwtrace.zip'))).toBe(true);
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'reuse-two', 'trace.pwtrace.zip'))).toBe(true);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ test('should stop tracing with trace: on-first-retry, when not retrying', async
|
|||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
expect(result.flaky).toBe(1);
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-shared-flaky-retry1', 'trace.zip'))).toBeTruthy();
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-shared-flaky-retry1', 'trace.pwtrace.zip'))).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should record api trace', async ({ runInlineTest, server }, testInfo) => {
|
||||
|
|
@ -86,7 +86,7 @@ test('should record api trace', async ({ runInlineTest, server }, testInfo) => {
|
|||
expect(result.passed).toBe(2);
|
||||
expect(result.failed).toBe(1);
|
||||
// One trace file for request context and one for each APIRequestContext
|
||||
const trace1 = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.zip'));
|
||||
const trace1 = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.pwtrace.zip'));
|
||||
expect(trace1.actionTree).toEqual([
|
||||
'Before Hooks',
|
||||
' fixture: request',
|
||||
|
|
@ -105,14 +105,14 @@ test('should record api trace', async ({ runInlineTest, server }, testInfo) => {
|
|||
' fixture: request',
|
||||
' apiRequestContext.dispose',
|
||||
]);
|
||||
const trace2 = await parseTrace(testInfo.outputPath('test-results', 'a-api-pass', 'trace.zip'));
|
||||
const trace2 = await parseTrace(testInfo.outputPath('test-results', 'a-api-pass', 'trace.pwtrace.zip'));
|
||||
expect(trace2.actionTree).toEqual([
|
||||
'Before Hooks',
|
||||
'apiRequest.newContext',
|
||||
'apiRequestContext.get',
|
||||
'After Hooks',
|
||||
]);
|
||||
const trace3 = await parseTrace(testInfo.outputPath('test-results', 'a-fail', 'trace.zip'));
|
||||
const trace3 = await parseTrace(testInfo.outputPath('test-results', 'a-fail', 'trace.pwtrace.zip'));
|
||||
expect(trace3.actionTree).toEqual([
|
||||
'Before Hooks',
|
||||
' fixture: request',
|
||||
|
|
@ -204,7 +204,7 @@ test('should not mixup network files between contexts', async ({ runInlineTest,
|
|||
}, { workers: 1, timeout: 15000 });
|
||||
expect(result.exitCode).toEqual(0);
|
||||
expect(result.passed).toBe(1);
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-example', 'trace.zip'))).toBe(true);
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-example', 'trace.pwtrace.zip'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should save sources when requested', async ({ runInlineTest }, testInfo) => {
|
||||
|
|
@ -224,7 +224,7 @@ test('should save sources when requested', async ({ runInlineTest }, testInfo) =
|
|||
`,
|
||||
}, { workers: 1 });
|
||||
expect(result.exitCode).toEqual(0);
|
||||
const { resources } = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.zip'));
|
||||
const { resources } = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.pwtrace.zip'));
|
||||
expect([...resources.keys()].filter(name => name.startsWith('resources/src@'))).toHaveLength(1);
|
||||
});
|
||||
|
||||
|
|
@ -248,7 +248,7 @@ test('should not save sources when not requested', async ({ runInlineTest }, tes
|
|||
`,
|
||||
}, { workers: 1 });
|
||||
expect(result.exitCode).toEqual(0);
|
||||
const { resources } = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.zip'));
|
||||
const { resources } = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.pwtrace.zip'));
|
||||
expect([...resources.keys()].filter(name => name.startsWith('resources/src@'))).toHaveLength(0);
|
||||
});
|
||||
|
||||
|
|
@ -283,8 +283,8 @@ test('should work in serial mode', async ({ runInlineTest }, testInfo) => {
|
|||
expect(result.exitCode).toBe(1);
|
||||
expect(result.passed).toBe(1);
|
||||
expect(result.failed).toBe(1);
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-serial-passes', 'trace.zip'))).toBeFalsy();
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-serial-fails', 'trace.zip'))).toBeTruthy();
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-serial-passes', 'trace.pwtrace.zip'))).toBeFalsy();
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-serial-fails', 'trace.pwtrace.zip'))).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should not override trace file in afterAll', async ({ runInlineTest, server }, testInfo) => {
|
||||
|
|
@ -313,7 +313,7 @@ test('should not override trace file in afterAll', async ({ runInlineTest, serve
|
|||
expect(result.exitCode).toBe(1);
|
||||
expect(result.passed).toBe(1);
|
||||
expect(result.failed).toBe(1);
|
||||
const trace1 = await parseTrace(testInfo.outputPath('test-results', 'a-test-1', 'trace.zip'));
|
||||
const trace1 = await parseTrace(testInfo.outputPath('test-results', 'a-test-1', 'trace.pwtrace.zip'));
|
||||
|
||||
expect(trace1.actionTree).toEqual([
|
||||
'Before Hooks',
|
||||
|
|
@ -338,7 +338,7 @@ test('should not override trace file in afterAll', async ({ runInlineTest, serve
|
|||
]);
|
||||
expect(trace1.errors).toEqual([`'oh no!'`]);
|
||||
|
||||
const error = await parseTrace(testInfo.outputPath('test-results', 'a-test-2', 'trace.zip')).catch(e => e);
|
||||
const error = await parseTrace(testInfo.outputPath('test-results', 'a-test-2', 'trace.pwtrace.zip')).catch(e => e);
|
||||
expect(error).toBeTruthy();
|
||||
});
|
||||
|
||||
|
|
@ -366,8 +366,8 @@ test('should retain traces for interrupted tests', async ({ runInlineTest }, tes
|
|||
expect(result.exitCode).toBe(1);
|
||||
expect(result.failed).toBe(1);
|
||||
expect(result.interrupted).toBe(1);
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-test-1', 'trace.zip'))).toBeTruthy();
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'b-test-2', 'trace.zip'))).toBeTruthy();
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-test-1', 'trace.pwtrace.zip'))).toBeTruthy();
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'b-test-2', 'trace.pwtrace.zip'))).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should respect --trace', async ({ runInlineTest }, testInfo) => {
|
||||
|
|
@ -382,7 +382,7 @@ test('should respect --trace', async ({ runInlineTest }, testInfo) => {
|
|||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-test-1', 'trace.zip'))).toBeTruthy();
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-test-1', 'trace.pwtrace.zip'))).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should respect PW_TEST_DISABLE_TRACING', async ({ runInlineTest }, testInfo) => {
|
||||
|
|
@ -400,7 +400,7 @@ test('should respect PW_TEST_DISABLE_TRACING', async ({ runInlineTest }, testInf
|
|||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-test-1', 'trace.zip'))).toBe(false);
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-test-1', 'trace.pwtrace.zip'))).toBe(false);
|
||||
});
|
||||
|
||||
for (const mode of ['off', 'retain-on-failure', 'on-first-retry', 'on-all-retries', 'retain-on-first-failure']) {
|
||||
|
|
@ -465,7 +465,7 @@ test(`trace:retain-on-failure should create trace if context is closed before fa
|
|||
});
|
||||
`,
|
||||
}, { trace: 'retain-on-failure' });
|
||||
const tracePath = test.info().outputPath('test-results', 'a-passing-test', 'trace.zip');
|
||||
const tracePath = test.info().outputPath('test-results', 'a-passing-test', 'trace.pwtrace.zip');
|
||||
const trace = await parseTrace(tracePath);
|
||||
expect(trace.apiNames).toContain('page.goto');
|
||||
expect(result.failed).toBe(1);
|
||||
|
|
@ -487,7 +487,7 @@ test(`trace:retain-on-failure should create trace if context is closed before fa
|
|||
});
|
||||
`,
|
||||
}, { trace: 'retain-on-failure' });
|
||||
const tracePath = test.info().outputPath('test-results', 'a-passing-test', 'trace.zip');
|
||||
const tracePath = test.info().outputPath('test-results', 'a-passing-test', 'trace.pwtrace.zip');
|
||||
const trace = await parseTrace(tracePath);
|
||||
expect(trace.apiNames).toContain('page.goto');
|
||||
expect(result.failed).toBe(1);
|
||||
|
|
@ -507,7 +507,7 @@ test(`trace:retain-on-failure should create trace if request context is disposed
|
|||
});
|
||||
`,
|
||||
}, { trace: 'retain-on-failure' });
|
||||
const tracePath = test.info().outputPath('test-results', 'a-passing-test', 'trace.zip');
|
||||
const tracePath = test.info().outputPath('test-results', 'a-passing-test', 'trace.pwtrace.zip');
|
||||
const trace = await parseTrace(tracePath);
|
||||
expect(trace.apiNames).toContain('apiRequestContext.get');
|
||||
expect(result.failed).toBe(1);
|
||||
|
|
@ -529,7 +529,7 @@ test('should include attachments by default', async ({ runInlineTest, server },
|
|||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
const trace = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.zip'));
|
||||
const trace = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.pwtrace.zip'));
|
||||
expect(trace.apiNames).toEqual([
|
||||
'Before Hooks',
|
||||
`attach "foo"`,
|
||||
|
|
@ -559,7 +559,7 @@ test('should opt out of attachments', async ({ runInlineTest, server }, testInfo
|
|||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
const trace = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.zip'));
|
||||
const trace = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.pwtrace.zip'));
|
||||
expect(trace.apiNames).toEqual([
|
||||
'Before Hooks',
|
||||
`attach "foo"`,
|
||||
|
|
@ -592,7 +592,7 @@ test('should record with custom page fixture', async ({ runInlineTest }, testInf
|
|||
expect(result.exitCode).toBe(1);
|
||||
expect(result.failed).toBe(1);
|
||||
expect(result.output).toContain('failure!');
|
||||
const trace = await parseTraceRaw(testInfo.outputPath('test-results', 'a-fails', 'trace.zip'));
|
||||
const trace = await parseTraceRaw(testInfo.outputPath('test-results', 'a-fails', 'trace.pwtrace.zip'));
|
||||
expect(trace.events).toContainEqual(expect.objectContaining({
|
||||
type: 'frame-snapshot',
|
||||
}));
|
||||
|
|
@ -617,7 +617,7 @@ test('should expand expect.toPass', async ({ runInlineTest }, testInfo) => {
|
|||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
const trace = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.zip'));
|
||||
const trace = await parseTrace(testInfo.outputPath('test-results', 'a-pass', 'trace.pwtrace.zip'));
|
||||
expect(trace.actionTree).toEqual([
|
||||
'Before Hooks',
|
||||
' fixture: browser',
|
||||
|
|
@ -656,7 +656,7 @@ test('should show non-expect error in trace', async ({ runInlineTest }, testInfo
|
|||
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.failed).toBe(1);
|
||||
const trace = await parseTrace(testInfo.outputPath('test-results', 'a-fail', 'trace.zip'));
|
||||
const trace = await parseTrace(testInfo.outputPath('test-results', 'a-fail', 'trace.pwtrace.zip'));
|
||||
expect(trace.actionTree).toEqual([
|
||||
'Before Hooks',
|
||||
' fixture: browser',
|
||||
|
|
@ -692,7 +692,7 @@ test('should show error from beforeAll in trace', async ({ runInlineTest }, test
|
|||
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.failed).toBe(1);
|
||||
const trace = await parseTrace(testInfo.outputPath('test-results', 'a-fail', 'trace.zip'));
|
||||
const trace = await parseTrace(testInfo.outputPath('test-results', 'a-fail', 'trace.pwtrace.zip'));
|
||||
expect(trace.errors).toEqual(['Error: Oh my!']);
|
||||
});
|
||||
|
||||
|
|
@ -730,7 +730,7 @@ test('should not throw when attachment is missing', async ({ runInlineTest }, te
|
|||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
const trace = await parseTrace(testInfo.outputPath('test-results', 'a-passes', 'trace.zip'));
|
||||
const trace = await parseTrace(testInfo.outputPath('test-results', 'a-passes', 'trace.pwtrace.zip'));
|
||||
expect(trace.actionTree).toContain('attach "screenshot"');
|
||||
});
|
||||
|
||||
|
|
@ -754,7 +754,7 @@ test('should not throw when screenshot on failure fails', async ({ runInlineTest
|
|||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
const trace = await parseTrace(testInfo.outputPath('test-results', 'a-has-pdf-page', 'trace.zip'));
|
||||
const trace = await parseTrace(testInfo.outputPath('test-results', 'a-has-pdf-page', 'trace.pwtrace.zip'));
|
||||
const attachedScreenshots = trace.actionTree.filter(s => s.trim() === `attach "screenshot"`);
|
||||
// One screenshot for the page, no screenshot for pdf page since it should have failed.
|
||||
expect(attachedScreenshots.length).toBe(1);
|
||||
|
|
@ -778,7 +778,7 @@ test('should use custom expect message in trace', async ({ runInlineTest }, test
|
|||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
const trace = await parseTrace(testInfo.outputPath('test-results', 'a-fail', 'trace.zip'));
|
||||
const trace = await parseTrace(testInfo.outputPath('test-results', 'a-fail', 'trace.pwtrace.zip'));
|
||||
expect(trace.actionTree).toEqual([
|
||||
'Before Hooks',
|
||||
' fixture: browser',
|
||||
|
|
@ -837,7 +837,7 @@ test('should not throw when merging traces multiple times', async ({ runInlineTe
|
|||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-foo', 'trace.zip'))).toBe(true);
|
||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-foo', 'trace.pwtrace.zip'))).toBe(true);
|
||||
});
|
||||
|
||||
test('should record nested steps, even after timeout', async ({ runInlineTest }, testInfo) => {
|
||||
|
|
@ -928,7 +928,7 @@ test('should record nested steps, even after timeout', async ({ runInlineTest },
|
|||
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.failed).toBe(1);
|
||||
const trace = await parseTrace(testInfo.outputPath('test-results', 'a-example', 'trace.zip'));
|
||||
const trace = await parseTrace(testInfo.outputPath('test-results', 'a-example', 'trace.pwtrace.zip'));
|
||||
expect(trace.actionTree).toEqual([
|
||||
'Before Hooks',
|
||||
' beforeAll hook',
|
||||
|
|
@ -1022,14 +1022,14 @@ test('should attribute worker fixture teardown to the right test', async ({ runI
|
|||
expect(result.exitCode).toBe(1);
|
||||
expect(result.passed).toBe(1);
|
||||
expect(result.failed).toBe(1);
|
||||
const trace1 = await parseTrace(testInfo.outputPath('test-results', 'a-one', 'trace.zip'));
|
||||
const trace1 = await parseTrace(testInfo.outputPath('test-results', 'a-one', 'trace.pwtrace.zip'));
|
||||
expect(trace1.actionTree).toEqual([
|
||||
'Before Hooks',
|
||||
' fixture: foo',
|
||||
' step in foo setup',
|
||||
'After Hooks',
|
||||
]);
|
||||
const trace2 = await parseTrace(testInfo.outputPath('test-results', 'a-two', 'trace.zip'));
|
||||
const trace2 = await parseTrace(testInfo.outputPath('test-results', 'a-two', 'trace.pwtrace.zip'));
|
||||
expect(trace2.actionTree).toEqual([
|
||||
'Before Hooks',
|
||||
'After Hooks',
|
||||
|
|
@ -1050,11 +1050,11 @@ test('trace:retain-on-first-failure should create trace but only on first failur
|
|||
`,
|
||||
}, { trace: 'retain-on-first-failure', retries: 1 });
|
||||
|
||||
const retryTracePath = test.info().outputPath('test-results', 'a-fail-retry1', 'trace.zip');
|
||||
const retryTracePath = test.info().outputPath('test-results', 'a-fail-retry1', 'trace.pwtrace.zip');
|
||||
const retryTraceExists = fs.existsSync(retryTracePath);
|
||||
expect(retryTraceExists).toBe(false);
|
||||
|
||||
const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.zip');
|
||||
const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.pwtrace.zip');
|
||||
const trace = await parseTrace(tracePath);
|
||||
expect(trace.apiNames).toContain('page.goto');
|
||||
expect(result.failed).toBe(1);
|
||||
|
|
@ -1071,7 +1071,7 @@ test('trace:retain-on-first-failure should create trace if context is closed bef
|
|||
});
|
||||
`,
|
||||
}, { trace: 'retain-on-first-failure' });
|
||||
const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.zip');
|
||||
const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.pwtrace.zip');
|
||||
const trace = await parseTrace(tracePath);
|
||||
expect(trace.apiNames).toContain('page.goto');
|
||||
expect(result.failed).toBe(1);
|
||||
|
|
@ -1090,7 +1090,7 @@ test('trace:retain-on-first-failure should create trace if context is closed bef
|
|||
});
|
||||
`,
|
||||
}, { trace: 'retain-on-first-failure' });
|
||||
const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.zip');
|
||||
const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.pwtrace.zip');
|
||||
const trace = await parseTrace(tracePath);
|
||||
expect(trace.apiNames).toContain('page.goto');
|
||||
expect(result.failed).toBe(1);
|
||||
|
|
@ -1107,7 +1107,7 @@ test('trace:retain-on-first-failure should create trace if request context is di
|
|||
});
|
||||
`,
|
||||
}, { trace: 'retain-on-first-failure' });
|
||||
const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.zip');
|
||||
const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.pwtrace.zip');
|
||||
const trace = await parseTrace(tracePath);
|
||||
expect(trace.apiNames).toContain('apiRequestContext.get');
|
||||
expect(result.failed).toBe(1);
|
||||
|
|
@ -1132,7 +1132,7 @@ test('should not corrupt actions when no library trace is present', async ({ run
|
|||
expect(result.exitCode).toBe(1);
|
||||
expect(result.failed).toBe(1);
|
||||
|
||||
const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.zip');
|
||||
const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.pwtrace.zip');
|
||||
const trace = await parseTrace(tracePath);
|
||||
expect(trace.actionTree).toEqual([
|
||||
'Before Hooks',
|
||||
|
|
@ -1162,7 +1162,7 @@ test('should record trace for manually created context in a failed test', async
|
|||
expect(result.exitCode).toBe(1);
|
||||
expect(result.failed).toBe(1);
|
||||
|
||||
const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.zip');
|
||||
const tracePath = test.info().outputPath('test-results', 'a-fail', 'trace.pwtrace.zip');
|
||||
const trace = await parseTrace(tracePath);
|
||||
expect(trace.actionTree).toEqual([
|
||||
'Before Hooks',
|
||||
|
|
@ -1204,7 +1204,7 @@ test('should not nest top level expect into unfinished api calls ', {
|
|||
expect(result.exitCode).toBe(0);
|
||||
expect(result.failed).toBe(0);
|
||||
|
||||
const tracePath = test.info().outputPath('test-results', 'a-pass', 'trace.zip');
|
||||
const tracePath = test.info().outputPath('test-results', 'a-pass', 'trace.pwtrace.zip');
|
||||
const trace = await parseTrace(tracePath);
|
||||
expect(trace.actionTree).toEqual([
|
||||
'Before Hooks',
|
||||
|
|
@ -1246,7 +1246,7 @@ test('should record trace after fixture teardown timeout', {
|
|||
expect(result.exitCode).toBe(1);
|
||||
expect(result.failed).toBe(1);
|
||||
|
||||
const tracePath = test.info().outputPath('test-results', 'a-fails', 'trace.zip');
|
||||
const tracePath = test.info().outputPath('test-results', 'a-fails', 'trace.pwtrace.zip');
|
||||
const trace = await parseTrace(tracePath);
|
||||
expect(trace.actionTree).toEqual([
|
||||
'Before Hooks',
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ test('render trace attachment', async ({ runInlineTest }) => {
|
|||
test('one', async ({}, testInfo) => {
|
||||
testInfo.attachments.push({
|
||||
name: 'trace',
|
||||
path: testInfo.outputPath('my dir with space', 'trace.zip'),
|
||||
path: testInfo.outputPath('my dir with space', 'trace.pwtrace.zip'),
|
||||
contentType: 'application/zip'
|
||||
});
|
||||
expect(1).toBe(0);
|
||||
|
|
@ -75,8 +75,8 @@ test('render trace attachment', async ({ runInlineTest }) => {
|
|||
}, { reporter: 'line' });
|
||||
const text = result.output.replace(/\\/g, '/');
|
||||
expect(text).toContain(' attachment #1: trace (application/zip) ─────────────────────────────────────────────────────────');
|
||||
expect(text).toContain(' test-results/a-one/my dir with space/trace.zip');
|
||||
expect(text).toContain('npx playwright show-trace "test-results/a-one/my dir with space/trace.zip"');
|
||||
expect(text).toContain(' test-results/a-one/my dir with space/trace.pwtrace.zip');
|
||||
expect(text).toContain('npx playwright show-trace "test-results/a-one/my dir with space/trace.pwtrace.zip"');
|
||||
expect(text).toContain(' ────────────────────────────────────────────────────────────────────────────────────────────────');
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -987,9 +987,12 @@ expect |expect.poll.toHaveLength @ a.test.ts:14
|
|||
pw:api | page.goto(about:blank) @ a.test.ts:7
|
||||
test.step | inner step attempt: 0 @ a.test.ts:8
|
||||
expect | expect.toBe @ a.test.ts:10
|
||||
expect | expect.toHaveLength @ a.test.ts:6
|
||||
expect | ↪ error: Error: expect(received).toHaveLength(expected)
|
||||
pw:api | page.goto(about:blank) @ a.test.ts:7
|
||||
test.step | inner step attempt: 1 @ a.test.ts:8
|
||||
expect | expect.toBe @ a.test.ts:10
|
||||
expect | expect.toHaveLength @ a.test.ts:6
|
||||
hook |After Hooks
|
||||
fixture | fixture: page
|
||||
fixture | fixture: context
|
||||
|
|
@ -1036,9 +1039,12 @@ expect |expect.poll.toBe @ a.test.ts:13
|
|||
expect | expect.toHaveText @ a.test.ts:7
|
||||
test.step | iteration 1 @ a.test.ts:9
|
||||
expect | expect.toBeVisible @ a.test.ts:10
|
||||
expect | expect.toBe @ a.test.ts:6
|
||||
expect | ↪ error: Error: expect(received).toBe(expected) // Object.is equality
|
||||
expect | expect.toHaveText @ a.test.ts:7
|
||||
test.step | iteration 2 @ a.test.ts:9
|
||||
expect | expect.toBeVisible @ a.test.ts:10
|
||||
expect | expect.toBe @ a.test.ts:6
|
||||
hook |After Hooks
|
||||
fixture | fixture: page
|
||||
fixture | fixture: context
|
||||
|
|
|
|||
|
|
@ -174,15 +174,16 @@ test('should print dependencies in mixed CJS/ESM mode 2', async ({ runInlineTest
|
|||
});
|
||||
});
|
||||
|
||||
test('should perform initial run', async ({ runWatchTest }) => {
|
||||
test('should not perform initial run', async ({ runWatchTest }) => {
|
||||
const testProcess = await runWatchTest({
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('passes', () => {});
|
||||
`,
|
||||
});
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
|
||||
expect(testProcess.output).not.toContain('a.test.ts');
|
||||
});
|
||||
|
||||
test('should quit on Q', async ({ runWatchTest }) => {
|
||||
|
|
@ -206,7 +207,6 @@ test('should run tests on Enter', async ({ runWatchTest }) => {
|
|||
test('passes', () => {});
|
||||
`,
|
||||
});
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
testProcess.write('\r\n');
|
||||
|
|
@ -222,7 +222,6 @@ test('should run tests on R', async ({ runWatchTest }) => {
|
|||
test('passes', () => {});
|
||||
`,
|
||||
});
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
testProcess.write('r');
|
||||
|
|
@ -246,6 +245,10 @@ test('should run failed tests on F', async ({ runWatchTest }) => {
|
|||
test('fails', () => { expect(1).toBe(2); });
|
||||
`,
|
||||
});
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.write('\r\n');
|
||||
|
||||
await testProcess.waitForOutput('npx playwright test #1');
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('b.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('c.test.ts:3:11 › fails');
|
||||
|
|
@ -253,7 +256,7 @@ test('should run failed tests on F', async ({ runWatchTest }) => {
|
|||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
testProcess.write('f');
|
||||
await testProcess.waitForOutput('npx playwright test (running failed tests) #1');
|
||||
await testProcess.waitForOutput('npx playwright test (running failed tests) #2');
|
||||
await testProcess.waitForOutput('c.test.ts:3:11 › fails');
|
||||
expect(testProcess.output).not.toContain('a.test.ts:3:11');
|
||||
});
|
||||
|
|
@ -269,8 +272,6 @@ test('should respect file filter P', async ({ runWatchTest }) => {
|
|||
test('passes', () => {});
|
||||
`,
|
||||
});
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('b.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
testProcess.write('p');
|
||||
|
|
@ -294,6 +295,11 @@ test('should respect project filter C', async ({ runWatchTest, writeFiles }) =>
|
|||
`,
|
||||
};
|
||||
const testProcess = await runWatchTest(files, { project: 'foo' });
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
testProcess.write('\r\n');
|
||||
|
||||
await testProcess.waitForOutput('npx playwright test --project foo #1');
|
||||
await testProcess.waitForOutput('[foo] › a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
|
|
@ -303,7 +309,7 @@ test('should respect project filter C', async ({ runWatchTest, writeFiles }) =>
|
|||
await testProcess.waitForOutput('bar');
|
||||
testProcess.write(' ');
|
||||
testProcess.write('\r\n');
|
||||
await testProcess.waitForOutput('npx playwright test --project foo #1');
|
||||
await testProcess.waitForOutput('npx playwright test --project foo #2');
|
||||
await testProcess.waitForOutput('[foo] › a.test.ts:3:11 › passes');
|
||||
expect(testProcess.output).not.toContain('[bar] › a.test.ts:3:11 › passes');
|
||||
|
||||
|
|
@ -329,8 +335,6 @@ test('should respect file filter P and split files', async ({ runWatchTest }) =>
|
|||
test('passes', () => {});
|
||||
`,
|
||||
});
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('b.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
testProcess.write('p');
|
||||
|
|
@ -353,8 +357,6 @@ test('should respect title filter T', async ({ runWatchTest }) => {
|
|||
test('title 2', () => {});
|
||||
`,
|
||||
});
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › title 1');
|
||||
await testProcess.waitForOutput('b.test.ts:3:11 › title 2');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
testProcess.write('t');
|
||||
|
|
@ -381,6 +383,11 @@ test('should re-run failed tests on F > R', async ({ runWatchTest }) => {
|
|||
test('fails', () => { expect(1).toBe(2); });
|
||||
`,
|
||||
});
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
testProcess.write('\r\n');
|
||||
|
||||
await testProcess.waitForOutput('npx playwright test #1');
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('b.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('c.test.ts:3:11 › fails');
|
||||
|
|
@ -388,12 +395,12 @@ test('should re-run failed tests on F > R', async ({ runWatchTest }) => {
|
|||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
testProcess.write('f');
|
||||
await testProcess.waitForOutput('npx playwright test (running failed tests) #1');
|
||||
await testProcess.waitForOutput('npx playwright test (running failed tests) #2');
|
||||
await testProcess.waitForOutput('c.test.ts:3:11 › fails');
|
||||
expect(testProcess.output).not.toContain('a.test.ts:3:11');
|
||||
testProcess.clearOutput();
|
||||
testProcess.write('r');
|
||||
await testProcess.waitForOutput('npx playwright test (re-running tests) #2');
|
||||
await testProcess.waitForOutput('npx playwright test (re-running tests) #3');
|
||||
await testProcess.waitForOutput('c.test.ts:3:11 › fails');
|
||||
expect(testProcess.output).not.toContain('a.test.ts:3:11');
|
||||
});
|
||||
|
|
@ -413,10 +420,6 @@ test('should run on changed files', async ({ runWatchTest, writeFiles }) => {
|
|||
test('fails', () => { expect(1).toBe(2); });
|
||||
`,
|
||||
});
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('b.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('c.test.ts:3:11 › fails');
|
||||
await testProcess.waitForOutput('Error: expect(received).toBe(expected)');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
await writeFiles({
|
||||
|
|
@ -457,9 +460,6 @@ test('should run on changed deps', async ({ runWatchTest, writeFiles }) => {
|
|||
console.log('old helper');
|
||||
`,
|
||||
});
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('b.test.ts:4:11 › passes');
|
||||
await testProcess.waitForOutput('old helper');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
await writeFiles({
|
||||
|
|
@ -490,9 +490,6 @@ test('should run on changed deps in ESM', async ({ runWatchTest, writeFiles }) =
|
|||
console.log('old helper');
|
||||
`,
|
||||
});
|
||||
await testProcess.waitForOutput('a.test.ts:3:7 › passes');
|
||||
await testProcess.waitForOutput('b.test.ts:4:7 › passes');
|
||||
await testProcess.waitForOutput('old helper');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
await writeFiles({
|
||||
|
|
@ -521,10 +518,6 @@ test('should re-run changed files on R', async ({ runWatchTest, writeFiles }) =>
|
|||
test('fails', () => { expect(1).toBe(2); });
|
||||
`,
|
||||
});
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('b.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('c.test.ts:3:11 › fails');
|
||||
await testProcess.waitForOutput('Error: expect(received).toBe(expected)');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
await writeFiles({
|
||||
|
|
@ -556,8 +549,6 @@ test('should not trigger on changes to non-tests', async ({ runWatchTest, writeF
|
|||
test('passes', () => {});
|
||||
`,
|
||||
});
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('b.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
|
||||
testProcess.clearOutput();
|
||||
|
|
@ -582,6 +573,10 @@ test('should only watch selected projects', async ({ runWatchTest, writeFiles })
|
|||
test('passes', () => {});
|
||||
`,
|
||||
}, undefined, undefined, { additionalArgs: ['--project=foo'] });
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
testProcess.write('\r\n');
|
||||
|
||||
await testProcess.waitForOutput('npx playwright test --project foo');
|
||||
await testProcess.waitForOutput('[foo] › a.test.ts:3:11 › passes');
|
||||
expect(testProcess.output).not.toContain('[bar]');
|
||||
|
|
@ -612,6 +607,10 @@ test('should watch filtered files', async ({ runWatchTest, writeFiles }) => {
|
|||
test('passes', () => {});
|
||||
`,
|
||||
}, undefined, undefined, { additionalArgs: ['a.test.ts'] });
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
testProcess.write('\r\n');
|
||||
|
||||
await testProcess.waitForOutput('npx playwright test a.test.ts');
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
expect(testProcess.output).not.toContain('b.test');
|
||||
|
|
@ -640,6 +639,10 @@ test('should not watch unfiltered files', async ({ runWatchTest, writeFiles }) =
|
|||
test('passes', () => {});
|
||||
`,
|
||||
}, undefined, undefined, { additionalArgs: ['a.test.ts'] });
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
testProcess.write('\r\n');
|
||||
|
||||
await testProcess.waitForOutput('npx playwright test a.test.ts');
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
expect(testProcess.output).not.toContain('b.test');
|
||||
|
|
@ -684,10 +687,7 @@ test('should run CT on changed deps', async ({ runWatchTest, writeFiles }) => {
|
|||
});
|
||||
`,
|
||||
});
|
||||
await testProcess.waitForOutput('button.spec.tsx:4:11 › pass');
|
||||
await testProcess.waitForOutput('link.spec.tsx:3:11 › pass');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
await writeFiles({
|
||||
'src/button.tsx': `
|
||||
export const Button = () => <button>Button 2</button>;
|
||||
|
|
@ -732,10 +732,7 @@ test('should run CT on indirect deps change', async ({ runWatchTest, writeFiles
|
|||
});
|
||||
`,
|
||||
});
|
||||
await testProcess.waitForOutput('button.spec.tsx:4:11 › pass');
|
||||
await testProcess.waitForOutput('link.spec.tsx:3:11 › pass');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
await writeFiles({
|
||||
'src/button.css': `
|
||||
button { color: blue; }
|
||||
|
|
@ -776,10 +773,7 @@ test('should run CT on indirect deps change ESM mode', async ({ runWatchTest, wr
|
|||
});
|
||||
`,
|
||||
});
|
||||
await testProcess.waitForOutput('button.spec.tsx:4:7 › pass');
|
||||
await testProcess.waitForOutput('link.spec.tsx:3:7 › pass');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
await writeFiles({
|
||||
'src/button.css': `
|
||||
button { color: blue; }
|
||||
|
|
@ -809,6 +803,10 @@ test('should run global teardown before exiting', async ({ runWatchTest }) => {
|
|||
});
|
||||
`,
|
||||
});
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
testProcess.write('\r\n');
|
||||
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.write('\x1B');
|
||||
|
|
|
|||
Loading…
Reference in a new issue