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);
|
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}) {
|
async ['Network.getResponseBody']({requestId}) {
|
||||||
return this._pageNetwork.getResponseBody(requestId);
|
return this._pageNetwork.getResponseBody(requestId);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -487,6 +487,17 @@ const Browser = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const Heap = {
|
||||||
|
targets: ['page'],
|
||||||
|
types: {},
|
||||||
|
events: {},
|
||||||
|
methods: {
|
||||||
|
'collectGarbage': {
|
||||||
|
params: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const Network = {
|
const Network = {
|
||||||
targets: ['page'],
|
targets: ['page'],
|
||||||
types: networkTypes,
|
types: networkTypes,
|
||||||
|
|
@ -1002,7 +1013,7 @@ const Accessibility = {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.protocol = {
|
this.protocol = {
|
||||||
domains: {Browser, Page, Runtime, Network, Accessibility},
|
domains: {Browser, Heap, Page, Runtime, Network, Accessibility},
|
||||||
};
|
};
|
||||||
this.checkScheme = checkScheme;
|
this.checkScheme = checkScheme;
|
||||||
this.EXPORTED_SYMBOLS = ['protocol', '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-%%
|
### option: APIRequestContext.delete.data = %%-js-python-csharp-fetch-option-data-%%
|
||||||
* since: v1.17
|
* 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
|
* since: v1.17
|
||||||
|
|
||||||
### option: APIRequestContext.delete.form = %%-csharp-fetch-option-form-%%
|
### 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-%%
|
### option: APIRequestContext.fetch.data = %%-js-python-csharp-fetch-option-data-%%
|
||||||
* since: v1.16
|
* 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
|
* since: v1.16
|
||||||
|
|
||||||
### option: APIRequestContext.fetch.form = %%-csharp-fetch-option-form-%%
|
### 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-%%
|
### option: APIRequestContext.get.data = %%-js-python-csharp-fetch-option-data-%%
|
||||||
* since: v1.26
|
* 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
|
* since: v1.26
|
||||||
|
|
||||||
### option: APIRequestContext.get.form = %%-csharp-fetch-option-form-%%
|
### 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-%%
|
### option: APIRequestContext.head.data = %%-js-python-csharp-fetch-option-data-%%
|
||||||
* since: v1.26
|
* 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
|
* since: v1.26
|
||||||
|
|
||||||
### option: APIRequestContext.head.form = %%-csharp-fetch-option-form-%%
|
### 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-%%
|
### option: APIRequestContext.patch.data = %%-js-python-csharp-fetch-option-data-%%
|
||||||
* since: v1.16
|
* 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
|
* since: v1.16
|
||||||
|
|
||||||
### option: APIRequestContext.patch.form = %%-csharp-fetch-option-form-%%
|
### 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-%%
|
### option: APIRequestContext.post.data = %%-js-python-csharp-fetch-option-data-%%
|
||||||
* since: v1.16
|
* 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
|
* since: v1.16
|
||||||
|
|
||||||
### option: APIRequestContext.post.form = %%-csharp-fetch-option-form-%%
|
### 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-%%
|
### option: APIRequestContext.put.data = %%-js-python-csharp-fetch-option-data-%%
|
||||||
* since: v1.16
|
* 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
|
* since: v1.16
|
||||||
|
|
||||||
### option: APIRequestContext.put.form = %%-csharp-fetch-option-form-%%
|
### 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.
|
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-%%
|
### option: Page.goForward.waitUntil = %%-navigation-wait-until-%%
|
||||||
* since: v1.8
|
* since: v1.8
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ const context = await browser.newContext();
|
||||||
await context.tracing.start({ screenshots: true, snapshots: true });
|
await context.tracing.start({ screenshots: true, snapshots: true });
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
await page.goto('https://playwright.dev');
|
await page.goto('https://playwright.dev');
|
||||||
await context.tracing.stop({ path: 'trace.zip' });
|
await context.tracing.stop({ path: 'trace.pwtrace.zip' });
|
||||||
```
|
```
|
||||||
|
|
||||||
```java
|
```java
|
||||||
|
|
@ -23,7 +23,7 @@ context.tracing().start(new Tracing.StartOptions()
|
||||||
Page page = context.newPage();
|
Page page = context.newPage();
|
||||||
page.navigate("https://playwright.dev");
|
page.navigate("https://playwright.dev");
|
||||||
context.tracing().stop(new Tracing.StopOptions()
|
context.tracing().stop(new Tracing.StopOptions()
|
||||||
.setPath(Paths.get("trace.zip")));
|
.setPath(Paths.get("trace.pwtrace.zip")));
|
||||||
```
|
```
|
||||||
|
|
||||||
```python async
|
```python async
|
||||||
|
|
@ -32,7 +32,7 @@ context = await browser.new_context()
|
||||||
await context.tracing.start(screenshots=True, snapshots=True)
|
await context.tracing.start(screenshots=True, snapshots=True)
|
||||||
page = await context.new_page()
|
page = await context.new_page()
|
||||||
await page.goto("https://playwright.dev")
|
await page.goto("https://playwright.dev")
|
||||||
await context.tracing.stop(path = "trace.zip")
|
await context.tracing.stop(path = "trace.pwtrace.zip")
|
||||||
```
|
```
|
||||||
|
|
||||||
```python sync
|
```python sync
|
||||||
|
|
@ -41,7 +41,7 @@ context = browser.new_context()
|
||||||
context.tracing.start(screenshots=True, snapshots=True)
|
context.tracing.start(screenshots=True, snapshots=True)
|
||||||
page = context.new_page()
|
page = context.new_page()
|
||||||
page.goto("https://playwright.dev")
|
page.goto("https://playwright.dev")
|
||||||
context.tracing.stop(path = "trace.zip")
|
context.tracing.stop(path = "trace.pwtrace.zip")
|
||||||
```
|
```
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
|
|
@ -57,7 +57,7 @@ var page = await context.NewPageAsync();
|
||||||
await page.GotoAsync("https://playwright.dev");
|
await page.GotoAsync("https://playwright.dev");
|
||||||
await context.Tracing.StopAsync(new()
|
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 });
|
await context.tracing.start({ screenshots: true, snapshots: true });
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
await page.goto('https://playwright.dev');
|
await page.goto('https://playwright.dev');
|
||||||
await context.tracing.stop({ path: 'trace.zip' });
|
await context.tracing.stop({ path: 'trace.pwtrace.zip' });
|
||||||
```
|
```
|
||||||
|
|
||||||
```java
|
```java
|
||||||
|
|
@ -82,21 +82,21 @@ context.tracing().start(new Tracing.StartOptions()
|
||||||
Page page = context.newPage();
|
Page page = context.newPage();
|
||||||
page.navigate("https://playwright.dev");
|
page.navigate("https://playwright.dev");
|
||||||
context.tracing().stop(new Tracing.StopOptions()
|
context.tracing().stop(new Tracing.StopOptions()
|
||||||
.setPath(Paths.get("trace.zip")));
|
.setPath(Paths.get("trace.pwtrace.zip")));
|
||||||
```
|
```
|
||||||
|
|
||||||
```python async
|
```python async
|
||||||
await context.tracing.start(screenshots=True, snapshots=True)
|
await context.tracing.start(screenshots=True, snapshots=True)
|
||||||
page = await context.new_page()
|
page = await context.new_page()
|
||||||
await page.goto("https://playwright.dev")
|
await page.goto("https://playwright.dev")
|
||||||
await context.tracing.stop(path = "trace.zip")
|
await context.tracing.stop(path = "trace.pwtrace.zip")
|
||||||
```
|
```
|
||||||
|
|
||||||
```python sync
|
```python sync
|
||||||
context.tracing.start(screenshots=True, snapshots=True)
|
context.tracing.start(screenshots=True, snapshots=True)
|
||||||
page = context.new_page()
|
page = context.new_page()
|
||||||
page.goto("https://playwright.dev")
|
page.goto("https://playwright.dev")
|
||||||
context.tracing.stop(path = "trace.zip")
|
context.tracing.stop(path = "trace.pwtrace.zip")
|
||||||
```
|
```
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
|
|
@ -112,7 +112,7 @@ var page = await context.NewPageAsync();
|
||||||
await page.GotoAsync("https://playwright.dev");
|
await page.GotoAsync("https://playwright.dev");
|
||||||
await context.Tracing.StopAsync(new()
|
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 context.tracing.startChunk();
|
||||||
await page.getByText('Get Started').click();
|
await page.getByText('Get Started').click();
|
||||||
// Everything between startChunk and stopChunk will be recorded in the trace.
|
// 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 context.tracing.startChunk();
|
||||||
await page.goto('http://example.com');
|
await page.goto('http://example.com');
|
||||||
// Save a second trace file with different actions.
|
// Save a second trace file with different actions.
|
||||||
await context.tracing.stopChunk({ path: 'trace2.zip' });
|
await context.tracing.stopChunk({ path: 'trace2.pwtrace.zip' });
|
||||||
```
|
```
|
||||||
|
|
||||||
```java
|
```java
|
||||||
|
|
@ -196,13 +196,13 @@ context.tracing().startChunk();
|
||||||
page.getByText("Get Started").click();
|
page.getByText("Get Started").click();
|
||||||
// Everything between startChunk and stopChunk will be recorded in the trace.
|
// Everything between startChunk and stopChunk will be recorded in the trace.
|
||||||
context.tracing().stopChunk(new Tracing.StopChunkOptions()
|
context.tracing().stopChunk(new Tracing.StopChunkOptions()
|
||||||
.setPath(Paths.get("trace1.zip")));
|
.setPath(Paths.get("trace1.pwtrace.zip")));
|
||||||
|
|
||||||
context.tracing().startChunk();
|
context.tracing().startChunk();
|
||||||
page.navigate("http://example.com");
|
page.navigate("http://example.com");
|
||||||
// Save a second trace file with different actions.
|
// Save a second trace file with different actions.
|
||||||
context.tracing().stopChunk(new Tracing.StopChunkOptions()
|
context.tracing().stopChunk(new Tracing.StopChunkOptions()
|
||||||
.setPath(Paths.get("trace2.zip")));
|
.setPath(Paths.get("trace2.pwtrace.zip")));
|
||||||
```
|
```
|
||||||
|
|
||||||
```python async
|
```python async
|
||||||
|
|
@ -213,12 +213,12 @@ await page.goto("https://playwright.dev")
|
||||||
await context.tracing.start_chunk()
|
await context.tracing.start_chunk()
|
||||||
await page.get_by_text("Get Started").click()
|
await page.get_by_text("Get Started").click()
|
||||||
# Everything between start_chunk and stop_chunk will be recorded in the trace.
|
# 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 context.tracing.start_chunk()
|
||||||
await page.goto("http://example.com")
|
await page.goto("http://example.com")
|
||||||
# Save a second trace file with different actions.
|
# 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
|
```python sync
|
||||||
|
|
@ -229,12 +229,12 @@ page.goto("https://playwright.dev")
|
||||||
context.tracing.start_chunk()
|
context.tracing.start_chunk()
|
||||||
page.get_by_text("Get Started").click()
|
page.get_by_text("Get Started").click()
|
||||||
# Everything between start_chunk and stop_chunk will be recorded in the trace.
|
# 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()
|
context.tracing.start_chunk()
|
||||||
page.goto("http://example.com")
|
page.goto("http://example.com")
|
||||||
# Save a second trace file with different actions.
|
# Save a second trace file with different actions.
|
||||||
context.tracing.stop_chunk(path = "trace2.zip")
|
context.tracing.stop_chunk(path = "trace2.pwtrace.zip")
|
||||||
```
|
```
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
|
|
@ -254,7 +254,7 @@ await page.GetByText("Get Started").ClickAsync();
|
||||||
// Everything between StartChunkAsync and StopChunkAsync will be recorded in the trace.
|
// Everything between StartChunkAsync and StopChunkAsync will be recorded in the trace.
|
||||||
await context.Tracing.StopChunkAsync(new()
|
await context.Tracing.StopChunkAsync(new()
|
||||||
{
|
{
|
||||||
Path = "trace1.zip"
|
Path = "trace1.pwtrace.zip"
|
||||||
});
|
});
|
||||||
|
|
||||||
await context.Tracing.StartChunkAsync();
|
await context.Tracing.StartChunkAsync();
|
||||||
|
|
@ -262,7 +262,7 @@ await page.GotoAsync("http://example.com");
|
||||||
// Save a second trace file with different actions.
|
// Save a second trace file with different actions.
|
||||||
await context.Tracing.StopChunkAsync(new()
|
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
|
Whether to throw on response codes other than 2xx and 3xx. By default response object is returned
|
||||||
for all status codes.
|
for all status codes.
|
||||||
|
|
||||||
## js-python-fetch-option-form
|
## js-fetch-option-form
|
||||||
* langs: js, python
|
* 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]>>
|
- `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
|
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
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: mcr.microsoft.com/playwright:v%%VERSION%%-jammy
|
image: mcr.microsoft.com/playwright:v%%VERSION%%-jammy
|
||||||
|
options: --user 1001
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
|
|
@ -218,8 +219,6 @@ jobs:
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Run your tests
|
- name: Run your tests
|
||||||
run: npx playwright test
|
run: npx playwright test
|
||||||
env:
|
|
||||||
HOME: /root
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```yml python title=".github/workflows/playwright.yml"
|
```yml python title=".github/workflows/playwright.yml"
|
||||||
|
|
@ -235,6 +234,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: mcr.microsoft.com/playwright/python:v%%VERSION%%-jammy
|
image: mcr.microsoft.com/playwright/python:v%%VERSION%%-jammy
|
||||||
|
options: --user 1001
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
|
|
@ -248,8 +248,6 @@ jobs:
|
||||||
pip install -e .
|
pip install -e .
|
||||||
- name: Run your tests
|
- name: Run your tests
|
||||||
run: pytest
|
run: pytest
|
||||||
env:
|
|
||||||
HOME: /root
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```yml java title=".github/workflows/playwright.yml"
|
```yml java title=".github/workflows/playwright.yml"
|
||||||
|
|
@ -265,6 +263,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: mcr.microsoft.com/playwright/java:v%%VERSION%%-jammy
|
image: mcr.microsoft.com/playwright/java:v%%VERSION%%-jammy
|
||||||
|
options: --user 1001
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-java@v3
|
- uses: actions/setup-java@v3
|
||||||
|
|
@ -275,8 +274,6 @@ jobs:
|
||||||
run: mvn -B install -D skipTests --no-transfer-progress
|
run: mvn -B install -D skipTests --no-transfer-progress
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: mvn test
|
run: mvn test
|
||||||
env:
|
|
||||||
HOME: /root
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```yml csharp title=".github/workflows/playwright.yml"
|
```yml csharp title=".github/workflows/playwright.yml"
|
||||||
|
|
@ -292,6 +289,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: mcr.microsoft.com/playwright/dotnet:v%%VERSION%%-jammy
|
image: mcr.microsoft.com/playwright/dotnet:v%%VERSION%%-jammy
|
||||||
|
options: --user 1001
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Setup dotnet
|
- name: Setup dotnet
|
||||||
|
|
@ -301,8 +299,6 @@ jobs:
|
||||||
- run: dotnet build
|
- run: dotnet build
|
||||||
- name: Run your tests
|
- name: Run your tests
|
||||||
run: dotnet test
|
run: dotnet test
|
||||||
env:
|
|
||||||
HOME: /root
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### On deployment
|
#### On deployment
|
||||||
|
|
|
||||||
|
|
@ -238,12 +238,12 @@ async function globalSetup(config: FullConfig) {
|
||||||
await page.getByText('Sign in').click();
|
await page.getByText('Sign in').click();
|
||||||
await context.storageState({ path: storageState as string });
|
await context.storageState({ path: storageState as string });
|
||||||
await context.tracing.stop({
|
await context.tracing.stop({
|
||||||
path: './test-results/setup-trace.zip',
|
path: './test-results/setup-trace.pwtrace.zip',
|
||||||
});
|
});
|
||||||
await browser.close();
|
await browser.close();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await context.tracing.stop({
|
await context.tracing.stop({
|
||||||
path: './test-results/failed-setup-trace.zip',
|
path: './test-results/failed-setup-trace.pwtrace.zip',
|
||||||
});
|
});
|
||||||
await browser.close();
|
await browser.close();
|
||||||
throw error;
|
throw error;
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ Options for tracing are:
|
||||||
- `off`: Do not record trace. (default)
|
- `off`: Do not record trace. (default)
|
||||||
- `retain-on-failure`: Record trace for each test, but remove all traces from successful test runs.
|
- `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>
|
<details>
|
||||||
<summary>If you are not using Pytest, click here to learn how to record traces.</summary>
|
<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")
|
await page.goto("https://playwright.dev")
|
||||||
|
|
||||||
# Stop tracing and export it into a zip archive.
|
# 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
|
```python sync
|
||||||
|
|
@ -55,7 +55,7 @@ page = context.new_page()
|
||||||
page.goto("https://playwright.dev")
|
page.goto("https://playwright.dev")
|
||||||
|
|
||||||
# Stop tracing and export it into a zip archive.
|
# Stop tracing and export it into a zip archive.
|
||||||
context.tracing.stop(path = "trace.zip")
|
context.tracing.stop(path = "trace.pwtrace.zip")
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
@ -80,22 +80,22 @@ page.navigate("https://playwright.dev");
|
||||||
|
|
||||||
// Stop tracing and export it into a zip archive.
|
// Stop tracing and export it into a zip archive.
|
||||||
context.tracing().stop(new Tracing.StopOptions()
|
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
|
## 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.
|
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
|
```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
|
```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
|
## 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"
|
```js title="playwright.config.ts"
|
||||||
import { defineConfig } from '@playwright/test';
|
import { defineConfig } from '@playwright/test';
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,7 @@ npx playwright show-report
|
||||||
* langs: js
|
* langs: js
|
||||||
|
|
||||||
Traces should be run on continuous integration on the first retry of a failed test
|
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"
|
```js tab=js-test title="playwright.config.ts"
|
||||||
import { defineConfig } from '@playwright/test';
|
import { defineConfig } from '@playwright/test';
|
||||||
|
|
@ -155,7 +155,7 @@ const page = await context.newPage();
|
||||||
await page.goto('https://playwright.dev');
|
await page.goto('https://playwright.dev');
|
||||||
|
|
||||||
// Stop tracing and export it into a zip archive.
|
// 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:
|
Available options to record a trace:
|
||||||
|
|
@ -185,7 +185,7 @@ Options for tracing are:
|
||||||
- `off`: Do not record trace. (default)
|
- `off`: Do not record trace. (default)
|
||||||
- `retain-on-failure`: Record trace for each test, but remove all traces from successful test runs.
|
- `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>
|
<details>
|
||||||
<summary>If you are not using Pytest, click here to learn how to record traces.</summary>
|
<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")
|
await page.goto("https://playwright.dev")
|
||||||
|
|
||||||
# Stop tracing and export it into a zip archive.
|
# 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
|
```python sync
|
||||||
|
|
@ -215,7 +215,7 @@ page = context.new_page()
|
||||||
page.goto("https://playwright.dev")
|
page.goto("https://playwright.dev")
|
||||||
|
|
||||||
# Stop tracing and export it into a zip archive.
|
# Stop tracing and export it into a zip archive.
|
||||||
context.tracing.stop(path = "trace.zip")
|
context.tracing.stop(path = "trace.pwtrace.zip")
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
@ -240,10 +240,10 @@ page.navigate("https://playwright.dev");
|
||||||
|
|
||||||
// Stop tracing and export it into a zip archive.
|
// Stop tracing and export it into a zip archive.
|
||||||
context.tracing().stop(new Tracing.StopOptions()
|
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
|
## Recording a trace
|
||||||
* langs: csharp
|
* langs: csharp
|
||||||
|
|
@ -466,22 +466,22 @@ public class ExampleTest : PageTest
|
||||||
|
|
||||||
## Opening the trace
|
## 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
|
```bash js
|
||||||
npx playwright show-trace path/to/trace.zip
|
npx playwright show-trace path/to/trace.pwtrace.zip
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash java
|
```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
|
```bash python
|
||||||
playwright show-trace trace.zip
|
playwright show-trace trace.pwtrace.zip
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash csharp
|
```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)
|
## 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.
|
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
|
```bash js
|
||||||
npx playwright show-trace https://example.com/trace.zip
|
npx playwright show-trace https://example.com/trace.pwtrace.zip
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash java
|
```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
|
```bash python
|
||||||
playwright show-trace https://example.com/trace.zip
|
playwright show-trace https://example.com/trace.pwtrace.zip
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash csharp
|
```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",
|
"name": "firefox",
|
||||||
"revision": "1463",
|
"revision": "1464",
|
||||||
"installByDefault": true,
|
"installByDefault": true,
|
||||||
"browserVersion": "130.0"
|
"browserVersion": "130.0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "firefox-beta",
|
"name": "firefox-beta",
|
||||||
"revision": "1463",
|
"revision": "1464",
|
||||||
"installByDefault": false,
|
"installByDefault": false,
|
||||||
"browserVersion": "131.0b2"
|
"browserVersion": "131.0b2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "webkit",
|
"name": "webkit",
|
||||||
"revision": "2073",
|
"revision": "2075",
|
||||||
"installByDefault": true,
|
"installByDefault": true,
|
||||||
"revisionOverrides": {
|
"revisionOverrides": {
|
||||||
"mac10.14": "1446",
|
"mac10.14": "1446",
|
||||||
|
|
|
||||||
|
|
@ -318,7 +318,7 @@ program
|
||||||
}).addHelpText('afterAll', `
|
}).addHelpText('afterAll', `
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
$ show-trace https://example.com/trace.zip`);
|
$ show-trace https://example.com/trace.pwtrace.zip`);
|
||||||
|
|
||||||
type Options = {
|
type Options = {
|
||||||
browser: string;
|
browser: string;
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,8 @@ export type FetchOptions = {
|
||||||
method?: string,
|
method?: string,
|
||||||
headers?: Headers,
|
headers?: Headers,
|
||||||
data?: string | Buffer | Serializable,
|
data?: string | Buffer | Serializable,
|
||||||
form?: { [key: string]: string|number|boolean; };
|
form?: { [key: string]: string|number|boolean; } | FormData;
|
||||||
multipart?: { [key: string]: string|number|boolean|fs.ReadStream|FilePayload; };
|
multipart?: { [key: string]: string|number|boolean|fs.ReadStream|FilePayload; } | FormData;
|
||||||
timeout?: number,
|
timeout?: number,
|
||||||
failOnStatusCode?: boolean,
|
failOnStatusCode?: boolean,
|
||||||
ignoreHTTPSErrors?: boolean,
|
ignoreHTTPSErrors?: boolean,
|
||||||
|
|
@ -202,7 +202,16 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
|
||||||
throw new Error(`Unexpected 'data' type`);
|
throw new Error(`Unexpected 'data' type`);
|
||||||
}
|
}
|
||||||
} else if (options.form) {
|
} 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) {
|
} else if (options.multipart) {
|
||||||
multipartData = [];
|
multipartData = [];
|
||||||
if (globalThis.FormData && options.multipart instanceof FormData) {
|
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);
|
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 } = {}) {
|
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({
|
await this._channel.emulateMedia({
|
||||||
media: options.media === null ? 'no-override' : options.media,
|
media: options.media === null ? 'no-override' : options.media,
|
||||||
|
|
|
||||||
|
|
@ -1122,6 +1122,8 @@ scheme.PageGoForwardParams = tObject({
|
||||||
scheme.PageGoForwardResult = tObject({
|
scheme.PageGoForwardResult = tObject({
|
||||||
response: tOptional(tChannel(['Response'])),
|
response: tOptional(tChannel(['Response'])),
|
||||||
});
|
});
|
||||||
|
scheme.PageForceGarbageCollectionParams = tOptional(tObject({}));
|
||||||
|
scheme.PageForceGarbageCollectionResult = tOptional(tObject({}));
|
||||||
scheme.PageRegisterLocatorHandlerParams = tObject({
|
scheme.PageRegisterLocatorHandlerParams = tObject({
|
||||||
selector: tString,
|
selector: tString,
|
||||||
noWaitAfter: tOptional(tBoolean),
|
noWaitAfter: tOptional(tBoolean),
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import { validateBrowserContextOptions } from '../browserContext';
|
||||||
import { ProgressController } from '../progress';
|
import { ProgressController } from '../progress';
|
||||||
import { CRBrowser } from '../chromium/crBrowser';
|
import { CRBrowser } from '../chromium/crBrowser';
|
||||||
import { helper } from '../helper';
|
import { helper } from '../helper';
|
||||||
|
import type * as types from '../types';
|
||||||
import { PipeTransport } from '../../protocol/transport';
|
import { PipeTransport } from '../../protocol/transport';
|
||||||
import { RecentLogsCollector } from '../../utils/debugLogger';
|
import { RecentLogsCollector } from '../../utils/debugLogger';
|
||||||
import { gracefullyCloseSet } from '../../utils/processLauncher';
|
import { gracefullyCloseSet } from '../../utils/processLauncher';
|
||||||
|
|
@ -309,7 +310,7 @@ export class AndroidDevice extends SdkObject {
|
||||||
return await this._connectToBrowser(socketName);
|
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 socket = await this._waitForLocalAbstract(socketName);
|
||||||
const androidBrowser = new AndroidBrowser(this, socket);
|
const androidBrowser = new AndroidBrowser(this, socket);
|
||||||
await androidBrowser._init();
|
await androidBrowser._init();
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ export class BidiBrowser extends Browser {
|
||||||
this._didClose();
|
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 { userContext } = await this._browserSession.send('browser.createUserContext', {});
|
||||||
const context = new BidiBrowserContext(this, userContext, options);
|
const context = new BidiBrowserContext(this, userContext, options);
|
||||||
await context._initialize();
|
await context._initialize();
|
||||||
|
|
@ -190,7 +190,7 @@ export class BidiBrowser extends Browser {
|
||||||
export class BidiBrowserContext extends BrowserContext {
|
export class BidiBrowserContext extends BrowserContext {
|
||||||
declare readonly _browser: BidiBrowser;
|
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);
|
super(browser, options, browserContextId);
|
||||||
this._authenticateProxyViaHeader();
|
this._authenticateProxyViaHeader();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ export class BidiChromium extends BrowserType {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _innerDefaultArgs(options: types.LaunchOptions): string[] {
|
private _innerDefaultArgs(options: types.LaunchOptions): string[] {
|
||||||
const { args = [], proxy } = options;
|
const { args = [] } = options;
|
||||||
const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir'));
|
const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir'));
|
||||||
if (userDataDirArg)
|
if (userDataDirArg)
|
||||||
throw this._createUserDataDirArgMisuseError('--user-data-dir');
|
throw this._createUserDataDirArgMisuseError('--user-data-dir');
|
||||||
|
|
@ -125,6 +125,7 @@ export class BidiChromium extends BrowserType {
|
||||||
}
|
}
|
||||||
if (options.chromiumSandbox !== true)
|
if (options.chromiumSandbox !== true)
|
||||||
chromeArguments.push('--no-sandbox');
|
chromeArguments.push('--no-sandbox');
|
||||||
|
const proxy = options.proxyOverride || options.proxy;
|
||||||
if (proxy) {
|
if (proxy) {
|
||||||
const proxyURL = new URL(proxy.server);
|
const proxyURL = new URL(proxy.server);
|
||||||
const isSocks = proxyURL.protocol === 'socks5:';
|
const isSocks = proxyURL.protocol === 'socks5:';
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import type * as frames from '../frames';
|
||||||
import type * as types from '../types';
|
import type * as types from '../types';
|
||||||
import * as bidi from './third_party/bidiProtocol';
|
import * as bidi from './third_party/bidiProtocol';
|
||||||
import type { BidiSession } from './bidiConnection';
|
import type { BidiSession } from './bidiConnection';
|
||||||
|
import { parseRawCookie } from '../cookieStore';
|
||||||
|
|
||||||
|
|
||||||
export class BidiNetworkManager {
|
export class BidiNetworkManager {
|
||||||
|
|
@ -68,7 +69,7 @@ export class BidiNetworkManager {
|
||||||
if (redirectedFrom) {
|
if (redirectedFrom) {
|
||||||
this._session.sendMayFail('network.continueRequest', {
|
this._session.sendMayFail('network.continueRequest', {
|
||||||
request: param.request.request,
|
request: param.request.request,
|
||||||
headers: redirectedFrom._originalRequestRoute?._alreadyContinuedHeaders,
|
...(redirectedFrom._originalRequestRoute?._alreadyContinuedHeaders || {}),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
route = new BidiRouteImpl(this._session, param.request.request);
|
route = new BidiRouteImpl(this._session, param.request.request);
|
||||||
|
|
@ -245,7 +246,7 @@ class BidiRouteImpl implements network.RouteDelegate {
|
||||||
private _requestId: bidi.Network.Request;
|
private _requestId: bidi.Network.Request;
|
||||||
private _session: BidiSession;
|
private _session: BidiSession;
|
||||||
private _request!: network.Request;
|
private _request!: network.Request;
|
||||||
_alreadyContinuedHeaders: bidi.Network.Header[] | undefined;
|
_alreadyContinuedHeaders: types.HeadersArray | undefined;
|
||||||
|
|
||||||
constructor(session: BidiSession, requestId: bidi.Network.Request) {
|
constructor(session: BidiSession, requestId: bidi.Network.Request) {
|
||||||
this._session = session;
|
this._session = session;
|
||||||
|
|
@ -266,13 +267,12 @@ class BidiRouteImpl implements network.RouteDelegate {
|
||||||
return header;
|
return header;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this._alreadyContinuedHeaders = toBidiHeaders(headers);
|
this._alreadyContinuedHeaders = headers;
|
||||||
await this._session.sendMayFail('network.continueRequest', {
|
await this._session.sendMayFail('network.continueRequest', {
|
||||||
request: this._requestId,
|
request: this._requestId,
|
||||||
url: overrides.url,
|
url: overrides.url,
|
||||||
method: overrides.method,
|
method: overrides.method,
|
||||||
// TODO: cookies!
|
...toBidiRequestHeaders(this._alreadyContinuedHeaders),
|
||||||
headers: this._alreadyContinuedHeaders,
|
|
||||||
body: overrides.postData ? { type: 'base64', value: Buffer.from(overrides.postData).toString('base64') } : undefined,
|
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,
|
request: this._requestId,
|
||||||
statusCode: response.status,
|
statusCode: response.status,
|
||||||
reasonPhrase: network.statusText(response.status),
|
reasonPhrase: network.statusText(response.status),
|
||||||
headers: toBidiHeaders(response.headers),
|
...toBidiResponseHeaders(response.headers),
|
||||||
body: { type: 'base64', value: base64body },
|
body: { type: 'base64', value: base64body },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -302,6 +302,27 @@ function fromBidiHeaders(bidiHeaders: bidi.Network.Header[]): types.HeadersArray
|
||||||
return result;
|
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[] {
|
function toBidiHeaders(headers: types.HeadersArray): bidi.Network.Header[] {
|
||||||
return headers.map(({ name, value }) => ({ name, value: { type: 'string', value } }));
|
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;
|
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.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async forceGarbageCollection(): Promise<void> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
async addInitScript(initScript: InitScript): Promise<void> {
|
async addInitScript(initScript: InitScript): Promise<void> {
|
||||||
const { script } = await this._session.send('script.addPreloadScript', {
|
const { script } = await this._session.send('script.addPreloadScript', {
|
||||||
// TODO: remove function call from the source.
|
// TODO: remove function call from the source.
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ export type BrowserOptions = {
|
||||||
downloadsPath: string,
|
downloadsPath: string,
|
||||||
tracesDir: string,
|
tracesDir: string,
|
||||||
headful?: boolean,
|
headful?: boolean,
|
||||||
persistent?: channels.BrowserNewContextParams, // Undefined means no persistent context.
|
persistent?: types.BrowserContextOptions, // Undefined means no persistent context.
|
||||||
browserProcess: BrowserProcess,
|
browserProcess: BrowserProcess,
|
||||||
customExecutablePath?: string;
|
customExecutablePath?: string;
|
||||||
proxy?: ProxySettings,
|
proxy?: ProxySettings,
|
||||||
|
|
@ -74,15 +74,19 @@ export abstract class Browser extends SdkObject {
|
||||||
this.instrumentation.onBrowserOpen(this);
|
this.instrumentation.onBrowserOpen(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract doCreateNewContext(options: channels.BrowserNewContextParams): Promise<BrowserContext>;
|
abstract doCreateNewContext(options: types.BrowserContextOptions): Promise<BrowserContext>;
|
||||||
abstract contexts(): BrowserContext[];
|
abstract contexts(): BrowserContext[];
|
||||||
abstract isConnected(): boolean;
|
abstract isConnected(): boolean;
|
||||||
abstract version(): string;
|
abstract version(): string;
|
||||||
abstract userAgent(): 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);
|
validateBrowserContextOptions(options, this.options);
|
||||||
const clientCertificatesProxy = await createClientCertificatesProxyIfNeeded(options, this.options);
|
const clientCertificatesProxy = await createClientCertificatesProxyIfNeeded(options, this.options);
|
||||||
|
if (clientCertificatesProxy) {
|
||||||
|
options.proxyOverride = await clientCertificatesProxy.listen();
|
||||||
|
options.internalIgnoreHTTPSErrors = true;
|
||||||
|
}
|
||||||
let context;
|
let context;
|
||||||
try {
|
try {
|
||||||
context = await this.doCreateNewContext(options);
|
context = await this.doCreateNewContext(options);
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ export abstract class BrowserContext extends SdkObject {
|
||||||
readonly _timeoutSettings = new TimeoutSettings();
|
readonly _timeoutSettings = new TimeoutSettings();
|
||||||
readonly _pageBindings = new Map<string, PageBinding>();
|
readonly _pageBindings = new Map<string, PageBinding>();
|
||||||
readonly _activeProgressControllers = new Set<ProgressController>();
|
readonly _activeProgressControllers = new Set<ProgressController>();
|
||||||
readonly _options: channels.BrowserNewContextParams;
|
readonly _options: types.BrowserContextOptions;
|
||||||
_requestInterceptor?: network.RouteHandler;
|
_requestInterceptor?: network.RouteHandler;
|
||||||
private _isPersistentContext: boolean;
|
private _isPersistentContext: boolean;
|
||||||
private _closedStatus: 'open' | 'closing' | 'closed' = 'open';
|
private _closedStatus: 'open' | 'closing' | 'closed' = 'open';
|
||||||
|
|
@ -93,7 +93,7 @@ export abstract class BrowserContext extends SdkObject {
|
||||||
readonly clock: Clock;
|
readonly clock: Clock;
|
||||||
_clientCertificatesProxy: ClientCertificatesProxy | undefined;
|
_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');
|
super(browser, 'browser-context');
|
||||||
this.attribution.context = this;
|
this.attribution.context = this;
|
||||||
this._browser = browser;
|
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)
|
if (!options.clientCertificates?.length)
|
||||||
return;
|
return;
|
||||||
if ((options.proxy?.server && options.proxy?.server !== 'per-context') || (browserOptions?.proxy?.server && browserOptions?.proxy?.server !== 'http://per-context'))
|
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');
|
throw new Error('Cannot specify both proxy and clientCertificates');
|
||||||
verifyClientCertificates(options.clientCertificates);
|
verifyClientCertificates(options.clientCertificates);
|
||||||
const clientCertificatesProxy = new ClientCertificatesProxy(options);
|
return new ClientCertificatesProxy(options);
|
||||||
options.proxy = { server: await clientCertificatesProxy.listen() };
|
|
||||||
options.ignoreHTTPSErrors = true;
|
|
||||||
return clientCertificatesProxy;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateBrowserContextOptions(options: channels.BrowserNewContextParams, browserOptions: BrowserOptions) {
|
export function validateBrowserContextOptions(options: types.BrowserContextOptions, browserOptions: BrowserOptions) {
|
||||||
if (options.noDefaultViewport && options.deviceScaleFactor !== undefined)
|
if (options.noDefaultViewport && options.deviceScaleFactor !== undefined)
|
||||||
throw new Error(`"deviceScaleFactor" option is not supported with null "viewport"`);
|
throw new Error(`"deviceScaleFactor" option is not supported with null "viewport"`);
|
||||||
if (options.noDefaultViewport && !!options.isMobile)
|
if (options.noDefaultViewport && !!options.isMobile)
|
||||||
|
|
@ -720,7 +717,7 @@ export function verifyGeolocation(geolocation?: types.Geolocation) {
|
||||||
throw new Error(`geolocation.accuracy: precondition 0 <= ACCURACY failed.`);
|
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)
|
if (!clientCertificates)
|
||||||
return;
|
return;
|
||||||
for (const cert of clientCertificates) {
|
for (const cert of clientCertificates) {
|
||||||
|
|
|
||||||
|
|
@ -92,27 +92,26 @@ export abstract class BrowserType extends SdkObject {
|
||||||
return browser;
|
return browser;
|
||||||
}
|
}
|
||||||
|
|
||||||
async launchPersistentContext(metadata: CallMetadata, userDataDir: string, options: channels.BrowserTypeLaunchPersistentContextOptions & { useWebSocket?: boolean }): Promise<BrowserContext> {
|
async launchPersistentContext(metadata: CallMetadata, userDataDir: string, persistentContextOptions: channels.BrowserTypeLaunchPersistentContextOptions & { useWebSocket?: boolean }): Promise<BrowserContext> {
|
||||||
options = this._validateLaunchOptions(options);
|
const launchOptions = this._validateLaunchOptions(persistentContextOptions);
|
||||||
if (this._useBidi)
|
if (this._useBidi)
|
||||||
options.useWebSocket = true;
|
launchOptions.useWebSocket = true;
|
||||||
const controller = new ProgressController(metadata, this);
|
const controller = new ProgressController(metadata, this);
|
||||||
const persistent: channels.BrowserNewContextParams = { ...options };
|
|
||||||
controller.setLogName('browser');
|
controller.setLogName('browser');
|
||||||
const browser = await controller.run(async progress => {
|
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.
|
// 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)
|
if (clientCertificatesProxy)
|
||||||
options.proxy = persistent.proxy;
|
launchOptions.proxyOverride = await clientCertificatesProxy?.listen();
|
||||||
progress.cleanupWhenAborted(() => clientCertificatesProxy?.close());
|
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;
|
browser._defaultContext!._clientCertificatesProxy = clientCertificatesProxy;
|
||||||
return browser;
|
return browser;
|
||||||
}, TimeoutSettings.launchTimeout(options));
|
}, TimeoutSettings.launchTimeout(launchOptions));
|
||||||
return browser._defaultContext!;
|
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 {
|
try {
|
||||||
return await this._innerLaunch(progress, options, persistent, protocolLogger, userDataDir);
|
return await this._innerLaunch(progress, options, persistent, protocolLogger, userDataDir);
|
||||||
} catch (error) {
|
} 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;
|
options.proxy = options.proxy ? normalizeProxySettings(options.proxy) : undefined;
|
||||||
const browserLogsCollector = new RecentLogsCollector();
|
const browserLogsCollector = new RecentLogsCollector();
|
||||||
const { browserProcess, userDataDir, artifactsDir, transport } = await this._launchProcess(progress, options, !!persistent, browserLogsCollector, maybeUserDataDir);
|
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');
|
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;
|
const { devtools = false } = options;
|
||||||
let { headless = !devtools, downloadsPath, proxy } = options;
|
let { headless = !devtools, downloadsPath, proxy } = options;
|
||||||
if (debugMode())
|
if (debugMode())
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ import { CRDevTools } from './crDevTools';
|
||||||
import type { BrowserOptions, BrowserProcess } from '../browser';
|
import type { BrowserOptions, BrowserProcess } from '../browser';
|
||||||
import { Browser } from '../browser';
|
import { Browser } from '../browser';
|
||||||
import type * as types from '../types';
|
import type * as types from '../types';
|
||||||
import type * as channels from '@protocol/channels';
|
|
||||||
import type { HTTPRequestParams } from '../../utils/network';
|
import type { HTTPRequestParams } from '../../utils/network';
|
||||||
import { fetchData } from '../../utils/network';
|
import { fetchData } from '../../utils/network';
|
||||||
import { getUserAgent } from '../../utils/userAgent';
|
import { getUserAgent } from '../../utils/userAgent';
|
||||||
|
|
@ -98,7 +97,7 @@ export class Chromium extends BrowserType {
|
||||||
await cleanedUp;
|
await cleanedUp;
|
||||||
};
|
};
|
||||||
const browserProcess: BrowserProcess = { close: doClose, kill: doClose };
|
const browserProcess: BrowserProcess = { close: doClose, kill: doClose };
|
||||||
const persistent: channels.BrowserNewContextParams = { noDefaultViewport: true };
|
const persistent: types.BrowserContextOptions = { noDefaultViewport: true };
|
||||||
const browserOptions: BrowserOptions = {
|
const browserOptions: BrowserOptions = {
|
||||||
slowMo: options.slowMo,
|
slowMo: options.slowMo,
|
||||||
name: 'chromium',
|
name: 'chromium',
|
||||||
|
|
@ -287,7 +286,7 @@ export class Chromium extends BrowserType {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _innerDefaultArgs(options: types.LaunchOptions): string[] {
|
private _innerDefaultArgs(options: types.LaunchOptions): string[] {
|
||||||
const { args = [], proxy } = options;
|
const { args = [] } = options;
|
||||||
const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir'));
|
const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir'));
|
||||||
if (userDataDirArg)
|
if (userDataDirArg)
|
||||||
throw this._createUserDataDirArgMisuseError('--user-data-dir');
|
throw this._createUserDataDirArgMisuseError('--user-data-dir');
|
||||||
|
|
@ -321,6 +320,7 @@ export class Chromium extends BrowserType {
|
||||||
}
|
}
|
||||||
if (options.chromiumSandbox !== true)
|
if (options.chromiumSandbox !== true)
|
||||||
chromeArguments.push('--no-sandbox');
|
chromeArguments.push('--no-sandbox');
|
||||||
|
const proxy = options.proxyOverride || options.proxy;
|
||||||
if (proxy) {
|
if (proxy) {
|
||||||
const proxyURL = new URL(proxy.server);
|
const proxyURL = new URL(proxy.server);
|
||||||
const isSocks = proxyURL.protocol === 'socks5:';
|
const isSocks = proxyURL.protocol === 'socks5:';
|
||||||
|
|
|
||||||
|
|
@ -100,18 +100,19 @@ export class CRBrowser extends Browser {
|
||||||
this._session.on('Browser.downloadProgress', this._onDownloadProgress.bind(this));
|
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;
|
let proxyBypassList = undefined;
|
||||||
if (options.proxy) {
|
if (proxy) {
|
||||||
if (process.env.PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK)
|
if (process.env.PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK)
|
||||||
proxyBypassList = options.proxy.bypass;
|
proxyBypassList = proxy.bypass;
|
||||||
else
|
else
|
||||||
proxyBypassList = '<-loopback>' + (options.proxy.bypass ? `,${options.proxy.bypass}` : '');
|
proxyBypassList = '<-loopback>' + (proxy.bypass ? `,${proxy.bypass}` : '');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { browserContextId } = await this._session.send('Target.createBrowserContext', {
|
const { browserContextId } = await this._session.send('Target.createBrowserContext', {
|
||||||
disposeOnDetach: true,
|
disposeOnDetach: true,
|
||||||
proxyServer: options.proxy ? options.proxy.server : undefined,
|
proxyServer: proxy ? proxy.server : undefined,
|
||||||
proxyBypassList,
|
proxyBypassList,
|
||||||
});
|
});
|
||||||
const context = new CRBrowserContext(this, browserContextId, options);
|
const context = new CRBrowserContext(this, browserContextId, options);
|
||||||
|
|
@ -340,7 +341,7 @@ export class CRBrowserContext extends BrowserContext {
|
||||||
|
|
||||||
declare readonly _browser: CRBrowser;
|
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);
|
super(browser, options, browserContextId);
|
||||||
this._authenticateProxyViaCredentials();
|
this._authenticateProxyViaCredentials();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -247,6 +247,10 @@ export class CRPage implements PageDelegate {
|
||||||
return this._go(+1);
|
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> {
|
async addInitScript(initScript: InitScript, world: types.World = 'main'): Promise<void> {
|
||||||
await this._forAllFrameSessions(frame => frame._evaluateOnNewDocument(initScript, world));
|
await this._forAllFrameSessions(frame => frame._evaluateOnNewDocument(initScript, world));
|
||||||
}
|
}
|
||||||
|
|
@ -543,7 +547,7 @@ class FrameSession {
|
||||||
const options = this._crPage._browserContext._options;
|
const options = this._crPage._browserContext._options;
|
||||||
if (options.bypassCSP)
|
if (options.bypassCSP)
|
||||||
promises.push(this._client.send('Page.setBypassCSP', { enabled: true }));
|
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 }));
|
promises.push(this._client.send('Security.setIgnoreCertificateErrors', { ignore: true }));
|
||||||
if (this._isMainFrame())
|
if (this._isMainFrame())
|
||||||
promises.push(this._updateViewport());
|
promises.push(this._updateViewport());
|
||||||
|
|
@ -1213,7 +1217,7 @@ async function emulateTimezone(session: CRSession, timezoneId: string) {
|
||||||
const contextDelegateSymbol = Symbol('delegate');
|
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
|
// 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;
|
const ua = options.userAgent;
|
||||||
if (!ua)
|
if (!ua)
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type * as channels from '@protocol/channels';
|
import type * as channels from '@protocol/channels';
|
||||||
|
import { kMaxCookieExpiresDateInSeconds } from './network';
|
||||||
|
|
||||||
class Cookie {
|
class Cookie {
|
||||||
private _raw: channels.NetworkCookie;
|
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 {
|
export function domainMatches(value: string, domain: string): boolean {
|
||||||
if (value === domain)
|
if (value === domain)
|
||||||
return true;
|
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)) };
|
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> {
|
async registerLocatorHandler(params: channels.PageRegisterLocatorHandlerParams, metadata: CallMetadata): Promise<channels.PageRegisterLocatorHandlerResult> {
|
||||||
const uid = this._page.registerLocatorHandler(params.selector, params.noWaitAfter);
|
const uid = this._page.registerLocatorHandler(params.selector, params.noWaitAfter);
|
||||||
return { uid };
|
return { uid };
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ import type { BrowserWindow } from 'electron';
|
||||||
import type { Progress } from '../progress';
|
import type { Progress } from '../progress';
|
||||||
import { ProgressController } from '../progress';
|
import { ProgressController } from '../progress';
|
||||||
import { helper } from '../helper';
|
import { helper } from '../helper';
|
||||||
|
import type * as types from '../types';
|
||||||
import { eventsHelper } from '../../utils/eventsHelper';
|
import { eventsHelper } from '../../utils/eventsHelper';
|
||||||
import type { BrowserOptions, BrowserProcess } from '../browser';
|
import type { BrowserOptions, BrowserProcess } from '../browser';
|
||||||
import type { Playwright } from '../playwright';
|
import type { Playwright } from '../playwright';
|
||||||
|
|
@ -265,7 +266,7 @@ export class Electron extends SdkObject {
|
||||||
close: gracefullyClose,
|
close: gracefullyClose,
|
||||||
kill
|
kill
|
||||||
};
|
};
|
||||||
const contextOptions: channels.BrowserNewContextParams = {
|
const contextOptions: types.BrowserContextOptions = {
|
||||||
...options,
|
...options,
|
||||||
noDefaultViewport: true,
|
noDefaultViewport: true,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ import { getUserAgent } from '../utils/userAgent';
|
||||||
import { assert, createGuid, monotonicTime } from '../utils';
|
import { assert, createGuid, monotonicTime } from '../utils';
|
||||||
import { HttpsProxyAgent, SocksProxyAgent } from '../utilsBundle';
|
import { HttpsProxyAgent, SocksProxyAgent } from '../utilsBundle';
|
||||||
import { BrowserContext, verifyClientCertificates } from './browserContext';
|
import { BrowserContext, verifyClientCertificates } from './browserContext';
|
||||||
import { CookieStore, domainMatches } from './cookieStore';
|
import { CookieStore, domainMatches, parseRawCookie } from './cookieStore';
|
||||||
import { MultipartFormData } from './formData';
|
import { MultipartFormData } from './formData';
|
||||||
import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent } from '../utils/happy-eyeballs';
|
import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent } from '../utils/happy-eyeballs';
|
||||||
import type { CallMetadata } from './instrumentation';
|
import type { CallMetadata } from './instrumentation';
|
||||||
|
|
@ -39,7 +39,6 @@ import { ProgressController } from './progress';
|
||||||
import { Tracing } from './trace/recorder/tracing';
|
import { Tracing } from './trace/recorder/tracing';
|
||||||
import type * as types from './types';
|
import type * as types from './types';
|
||||||
import type { HeadersArray, ProxySettings } from './types';
|
import type { HeadersArray, ProxySettings } from './types';
|
||||||
import { kMaxCookieExpiresDateInSeconds } from './network';
|
|
||||||
import { getMatchingTLSOptionsForOrigin, rewriteOpenSSLErrorIfNeeded } from './socksClientCertificatesInterceptor';
|
import { getMatchingTLSOptionsForOrigin, rewriteOpenSSLErrorIfNeeded } from './socksClientCertificatesInterceptor';
|
||||||
|
|
||||||
type FetchRequestOptions = {
|
type FetchRequestOptions = {
|
||||||
|
|
@ -50,7 +49,7 @@ type FetchRequestOptions = {
|
||||||
timeoutSettings: TimeoutSettings;
|
timeoutSettings: TimeoutSettings;
|
||||||
ignoreHTTPSErrors?: boolean;
|
ignoreHTTPSErrors?: boolean;
|
||||||
baseURL?: string;
|
baseURL?: string;
|
||||||
clientCertificates?: channels.BrowserNewContextOptions['clientCertificates'];
|
clientCertificates?: types.BrowserContextOptions['clientCertificates'];
|
||||||
};
|
};
|
||||||
|
|
||||||
type HeadersObject = Readonly<{ [name: string]: string }>;
|
type HeadersObject = Readonly<{ [name: string]: string }>;
|
||||||
|
|
@ -640,27 +639,10 @@ function toHeadersArray(rawHeaders: string[]): types.HeadersArray {
|
||||||
const redirectStatus = [301, 302, 303, 307, 308];
|
const redirectStatus = [301, 302, 303, 307, 308];
|
||||||
|
|
||||||
function parseCookie(header: string): channels.NetworkCookie | null {
|
function parseCookie(header: string): channels.NetworkCookie | null {
|
||||||
const pairs = header.split(';').filter(s => s.trim().length > 0).map(p => {
|
const raw = parseRawCookie(header);
|
||||||
let key = '';
|
if (!raw)
|
||||||
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;
|
return null;
|
||||||
const [name, value] = pairs[0];
|
|
||||||
const cookie: channels.NetworkCookie = {
|
const cookie: channels.NetworkCookie = {
|
||||||
name,
|
|
||||||
value,
|
|
||||||
domain: '',
|
domain: '',
|
||||||
path: '',
|
path: '',
|
||||||
expires: -1,
|
expires: -1,
|
||||||
|
|
@ -668,62 +650,9 @@ function parseCookie(header: string): channels.NetworkCookie | null {
|
||||||
secure: false,
|
secure: false,
|
||||||
// From https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
|
// 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.
|
// 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;
|
return cookie;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,8 +58,9 @@ export class FFBrowser extends Browser {
|
||||||
browser._defaultContext = new FFBrowserContext(browser, undefined, options.persistent);
|
browser._defaultContext = new FFBrowserContext(browser, undefined, options.persistent);
|
||||||
promises.push((browser._defaultContext as FFBrowserContext)._initialize());
|
promises.push((browser._defaultContext as FFBrowserContext)._initialize());
|
||||||
}
|
}
|
||||||
if (options.proxy)
|
const proxy = options.originalLaunchOptions.proxyOverride || options.proxy;
|
||||||
promises.push(browser.session.send('Browser.setBrowserProxy', toJugglerProxyOptions(options.proxy)));
|
if (proxy)
|
||||||
|
promises.push(browser.session.send('Browser.setBrowserProxy', toJugglerProxyOptions(proxy)));
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
return browser;
|
return browser;
|
||||||
}
|
}
|
||||||
|
|
@ -88,7 +89,7 @@ export class FFBrowser extends Browser {
|
||||||
return !this._connection._closed;
|
return !this._connection._closed;
|
||||||
}
|
}
|
||||||
|
|
||||||
async doCreateNewContext(options: channels.BrowserNewContextParams): Promise<BrowserContext> {
|
async doCreateNewContext(options: types.BrowserContextOptions): Promise<BrowserContext> {
|
||||||
if (options.isMobile)
|
if (options.isMobile)
|
||||||
throw new Error('options.isMobile is not supported in Firefox');
|
throw new Error('options.isMobile is not supported in Firefox');
|
||||||
const { browserContextId } = await this.session.send('Browser.createBrowserContext', { removeOnDetach: true });
|
const { browserContextId } = await this.session.send('Browser.createBrowserContext', { removeOnDetach: true });
|
||||||
|
|
@ -172,7 +173,7 @@ export class FFBrowser extends Browser {
|
||||||
export class FFBrowserContext extends BrowserContext {
|
export class FFBrowserContext extends BrowserContext {
|
||||||
declare readonly _browser: FFBrowser;
|
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);
|
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 }));
|
promises.push(this._browser.session.send('Browser.setUserAgentOverride', { browserContextId, userAgent: this._options.userAgent }));
|
||||||
if (this._options.bypassCSP)
|
if (this._options.bypassCSP)
|
||||||
promises.push(this._browser.session.send('Browser.setBypassCSP', { browserContextId, bypassCSP: true }));
|
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 }));
|
promises.push(this._browser.session.send('Browser.setIgnoreHTTPSErrors', { browserContextId, ignoreHTTPSErrors: true }));
|
||||||
if (this._options.javaScriptEnabled === false)
|
if (this._options.javaScriptEnabled === false)
|
||||||
promises.push(this._browser.session.send('Browser.setJavaScriptDisabled', { browserContextId, javaScriptDisabled: true }));
|
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', {
|
promises.push(this._browser.session.send('Browser.setContextProxy', {
|
||||||
browserContextId: this._browserContextId,
|
browserContextId: this._browserContextId,
|
||||||
...toJugglerProxyOptions(this._options.proxy)
|
...toJugglerProxyOptions(proxy)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -400,6 +400,10 @@ export class FFPage implements PageDelegate {
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async forceGarbageCollection(): Promise<void> {
|
||||||
|
await this._session.send('Heap.collectGarbage');
|
||||||
|
}
|
||||||
|
|
||||||
async addInitScript(initScript: InitScript, worldName?: string): Promise<void> {
|
async addInitScript(initScript: InitScript, worldName?: string): Promise<void> {
|
||||||
this._initScripts.push({ initScript, worldName });
|
this._initScripts.push({ initScript, worldName });
|
||||||
await this._session.send('Page.setInitScripts', { scripts: this._initScripts.map(s => ({ script: s.initScript.source, worldName: s.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 type cancelDownloadReturnValue = void;
|
||||||
}
|
}
|
||||||
|
export module Heap {
|
||||||
|
export type collectGarbageParameters = {
|
||||||
|
};
|
||||||
|
export type collectGarbageReturnValue = void;
|
||||||
|
}
|
||||||
export module Page {
|
export module Page {
|
||||||
export type DOMPoint = {
|
export type DOMPoint = {
|
||||||
x: number;
|
x: number;
|
||||||
|
|
@ -1124,6 +1129,7 @@ export module Protocol {
|
||||||
"Browser.setForcedColors": Browser.setForcedColorsParameters;
|
"Browser.setForcedColors": Browser.setForcedColorsParameters;
|
||||||
"Browser.setVideoRecordingOptions": Browser.setVideoRecordingOptionsParameters;
|
"Browser.setVideoRecordingOptions": Browser.setVideoRecordingOptionsParameters;
|
||||||
"Browser.cancelDownload": Browser.cancelDownloadParameters;
|
"Browser.cancelDownload": Browser.cancelDownloadParameters;
|
||||||
|
"Heap.collectGarbage": Heap.collectGarbageParameters;
|
||||||
"Page.close": Page.closeParameters;
|
"Page.close": Page.closeParameters;
|
||||||
"Page.setFileInputFiles": Page.setFileInputFilesParameters;
|
"Page.setFileInputFiles": Page.setFileInputFilesParameters;
|
||||||
"Page.addBinding": Page.addBindingParameters;
|
"Page.addBinding": Page.addBindingParameters;
|
||||||
|
|
@ -1204,6 +1210,7 @@ export module Protocol {
|
||||||
"Browser.setForcedColors": Browser.setForcedColorsReturnValue;
|
"Browser.setForcedColors": Browser.setForcedColorsReturnValue;
|
||||||
"Browser.setVideoRecordingOptions": Browser.setVideoRecordingOptionsReturnValue;
|
"Browser.setVideoRecordingOptions": Browser.setVideoRecordingOptionsReturnValue;
|
||||||
"Browser.cancelDownload": Browser.cancelDownloadReturnValue;
|
"Browser.cancelDownload": Browser.cancelDownloadReturnValue;
|
||||||
|
"Heap.collectGarbage": Heap.collectGarbageReturnValue;
|
||||||
"Page.close": Page.closeReturnValue;
|
"Page.close": Page.closeReturnValue;
|
||||||
"Page.setFileInputFiles": Page.setFileInputFilesReturnValue;
|
"Page.setFileInputFiles": Page.setFileInputFilesReturnValue;
|
||||||
"Page.addBinding": Page.addBindingReturnValue;
|
"Page.addBinding": Page.addBindingReturnValue;
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ export interface PageDelegate {
|
||||||
reload(): Promise<void>;
|
reload(): Promise<void>;
|
||||||
goBack(): Promise<boolean>;
|
goBack(): Promise<boolean>;
|
||||||
goForward(): Promise<boolean>;
|
goForward(): Promise<boolean>;
|
||||||
|
forceGarbageCollection(): Promise<void>;
|
||||||
addInitScript(initScript: InitScript): Promise<void>;
|
addInitScript(initScript: InitScript): Promise<void>;
|
||||||
removeNonInternalInitScripts(): Promise<void>;
|
removeNonInternalInitScripts(): Promise<void>;
|
||||||
closePage(runBeforeUnload: boolean): Promise<void>;
|
closePage(runBeforeUnload: boolean): Promise<void>;
|
||||||
|
|
@ -430,6 +431,10 @@ export class Page extends SdkObject {
|
||||||
}), this._timeoutSettings.navigationTimeout(options));
|
}), this._timeoutSettings.navigationTimeout(options));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
forceGarbageCollection(): Promise<void> {
|
||||||
|
return this._delegate.forceGarbageCollection();
|
||||||
|
}
|
||||||
|
|
||||||
registerLocatorHandler(selector: string, noWaitAfter: boolean | undefined) {
|
registerLocatorHandler(selector: string, noWaitAfter: boolean | undefined) {
|
||||||
const uid = ++this._lastLocatorHandlerUid;
|
const uid = ++this._lastLocatorHandlerUid;
|
||||||
this._locatorHandlers.set(uid, { selector, noWaitAfter });
|
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 { escapeHTML, generateSelfSignedCertificate, ManualPromise, rewriteErrorMessage } from '../utils';
|
||||||
import type { SocksSocketClosedPayload, SocksSocketDataPayload, SocksSocketRequestedPayload } from '../common/socksProxy';
|
import type { SocksSocketClosedPayload, SocksSocketDataPayload, SocksSocketRequestedPayload } from '../common/socksProxy';
|
||||||
import { SocksProxy } 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';
|
import { debugLogger } from '../utils/debugLogger';
|
||||||
|
|
||||||
let dummyServerTlsOptions: tls.TlsOptions | undefined = undefined;
|
let dummyServerTlsOptions: tls.TlsOptions | undefined = undefined;
|
||||||
|
|
@ -235,7 +235,7 @@ export class ClientCertificatesProxy {
|
||||||
alpnCache: ALPNCache;
|
alpnCache: ALPNCache;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
contextOptions: Pick<channels.BrowserNewContextOptions, 'clientCertificates' | 'ignoreHTTPSErrors'>
|
contextOptions: Pick<types.BrowserContextOptions, 'clientCertificates' | 'ignoreHTTPSErrors'>
|
||||||
) {
|
) {
|
||||||
this.alpnCache = new ALPNCache();
|
this.alpnCache = new ALPNCache();
|
||||||
this.ignoreHTTPSErrors = contextOptions.ignoreHTTPSErrors;
|
this.ignoreHTTPSErrors = contextOptions.ignoreHTTPSErrors;
|
||||||
|
|
@ -261,9 +261,9 @@ export class ClientCertificatesProxy {
|
||||||
loadDummyServerCertsIfNeeded();
|
loadDummyServerCertsIfNeeded();
|
||||||
}
|
}
|
||||||
|
|
||||||
_initSecureContexts(clientCertificates: channels.BrowserNewContextOptions['clientCertificates']) {
|
_initSecureContexts(clientCertificates: types.BrowserContextOptions['clientCertificates']) {
|
||||||
// Step 1. Group certificates by origin.
|
// 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 || []) {
|
for (const cert of clientCertificates || []) {
|
||||||
const origin = normalizeOrigin(cert.origin);
|
const origin = normalizeOrigin(cert.origin);
|
||||||
const certs = origin2certs.get(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');
|
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() {
|
public async close() {
|
||||||
|
|
@ -301,7 +301,7 @@ function normalizeOrigin(origin: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertClientCertificatesToTLSOptions(
|
function convertClientCertificatesToTLSOptions(
|
||||||
clientCertificates: channels.BrowserNewContextOptions['clientCertificates']
|
clientCertificates: types.BrowserContextOptions['clientCertificates']
|
||||||
): Pick<https.RequestOptions, 'pfx' | 'key' | 'cert'> | undefined {
|
): Pick<https.RequestOptions, 'pfx' | 'key' | 'cert'> | undefined {
|
||||||
if (!clientCertificates || !clientCertificates.length)
|
if (!clientCertificates || !clientCertificates.length)
|
||||||
return;
|
return;
|
||||||
|
|
@ -322,7 +322,7 @@ function convertClientCertificatesToTLSOptions(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMatchingTLSOptionsForOrigin(
|
export function getMatchingTLSOptionsForOrigin(
|
||||||
clientCertificates: channels.BrowserNewContextOptions['clientCertificates'],
|
clientCertificates: types.BrowserContextOptions['clientCertificates'],
|
||||||
origin: string
|
origin: string
|
||||||
): Pick<https.RequestOptions, 'pfx' | 'key' | 'cert'> | undefined {
|
): Pick<https.RequestOptions, 'pfx' | 'key' | 'cert'> | undefined {
|
||||||
const matchingCerts = clientCertificates?.filter(c =>
|
const matchingCerts = clientCertificates?.filter(c =>
|
||||||
|
|
|
||||||
|
|
@ -310,7 +310,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||||
|
|
||||||
this._fs.copyFile(this._state.networkFile, newNetworkFile);
|
this._fs.copyFile(this._state.networkFile, newNetworkFile);
|
||||||
|
|
||||||
const zipFileName = this._state.traceFile + '.zip';
|
const zipFileName = this._state.traceFile + '.pwtrace.zip';
|
||||||
if (params.mode === 'archive')
|
if (params.mode === 'archive')
|
||||||
this._fs.zip(entries, zipFileName);
|
this._fs.zip(entries, zipFileName);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,15 @@ export type NormalizedContinueOverrides = {
|
||||||
|
|
||||||
export type EmulatedSize = { viewport: Size, screen: Size };
|
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;
|
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[] {
|
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'));
|
const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir'));
|
||||||
if (userDataDirArg)
|
if (userDataDirArg)
|
||||||
throw this._createUserDataDirArgMisuseError('--user-data-dir');
|
throw this._createUserDataDirArgMisuseError('--user-data-dir');
|
||||||
|
|
@ -68,6 +68,7 @@ export class WebKit extends BrowserType {
|
||||||
webkitArguments.push(`--user-data-dir=${userDataDir}`);
|
webkitArguments.push(`--user-data-dir=${userDataDir}`);
|
||||||
else
|
else
|
||||||
webkitArguments.push(`--no-startup-window`);
|
webkitArguments.push(`--no-startup-window`);
|
||||||
|
const proxy = options.proxyOverride || options.proxy;
|
||||||
if (proxy) {
|
if (proxy) {
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
webkitArguments.push(`--proxy=${proxy.server}`);
|
webkitArguments.push(`--proxy=${proxy.server}`);
|
||||||
|
|
|
||||||
|
|
@ -81,12 +81,13 @@ export class WKBrowser extends Browser {
|
||||||
this._didClose();
|
this._didClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
async doCreateNewContext(options: channels.BrowserNewContextParams): Promise<BrowserContext> {
|
async doCreateNewContext(options: types.BrowserContextOptions): Promise<BrowserContext> {
|
||||||
const createOptions = options.proxy ? {
|
const proxy = options.proxyOverride || options.proxy;
|
||||||
// Enable socks5 hostname resolution on Windows. Workaround can be removed once fixed upstream.
|
const createOptions = proxy ? {
|
||||||
|
// Enable socks5 hostname resolution on Windows.
|
||||||
// See https://github.com/microsoft/playwright/issues/20451
|
// See https://github.com/microsoft/playwright/issues/20451
|
||||||
proxyServer: process.platform === 'win32' ? options.proxy.server.replace(/^socks5:\/\//, 'socks5h://') : options.proxy.server,
|
proxyServer: process.platform === 'win32' ? proxy.server.replace(/^socks5:\/\//, 'socks5h://') : proxy.server,
|
||||||
proxyBypassList: options.proxy.bypass
|
proxyBypassList: proxy.bypass
|
||||||
} : undefined;
|
} : undefined;
|
||||||
const { browserContextId } = await this._browserSession.send('Playwright.createContext', createOptions);
|
const { browserContextId } = await this._browserSession.send('Playwright.createContext', createOptions);
|
||||||
options.userAgent = options.userAgent || DEFAULT_USER_AGENT;
|
options.userAgent = options.userAgent || DEFAULT_USER_AGENT;
|
||||||
|
|
@ -206,7 +207,7 @@ export class WKBrowser extends Browser {
|
||||||
export class WKBrowserContext extends BrowserContext {
|
export class WKBrowserContext extends BrowserContext {
|
||||||
declare readonly _browser: WKBrowser;
|
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);
|
super(browser, options, browserContextId);
|
||||||
this._validateEmulatedViewport(options.viewport);
|
this._validateEmulatedViewport(options.viewport);
|
||||||
this._authenticateProxyViaHeader();
|
this._authenticateProxyViaHeader();
|
||||||
|
|
@ -221,7 +222,7 @@ export class WKBrowserContext extends BrowserContext {
|
||||||
downloadPath: this._browser.options.downloadsPath,
|
downloadPath: this._browser.options.downloadsPath,
|
||||||
browserContextId
|
browserContextId
|
||||||
}));
|
}));
|
||||||
if (this._options.ignoreHTTPSErrors)
|
if (this._options.ignoreHTTPSErrors || this._options.internalIgnoreHTTPSErrors)
|
||||||
promises.push(this._browser._browserSession.send('Playwright.setIgnoreCertificateErrors', { browserContextId, ignore: true }));
|
promises.push(this._browser._browserSession.send('Playwright.setIgnoreCertificateErrors', { browserContextId, ignore: true }));
|
||||||
if (this._options.locale)
|
if (this._options.locale)
|
||||||
promises.push(this._browser._browserSession.send('Playwright.setLanguages', { browserContextId, languages: [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> {
|
async addInitScript(initScript: InitScript): Promise<void> {
|
||||||
await this._updateBootstrapScript();
|
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;
|
timeout?: number;
|
||||||
}): Promise<void>;
|
}): 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.
|
* 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
|
* as this request body. If this parameter is specified `content-type` header will be set to
|
||||||
* `application/x-www-form-urlencoded` unless explicitly provided.
|
* `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
|
* 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
|
* as this request body. If this parameter is specified `content-type` header will be set to
|
||||||
* `application/x-www-form-urlencoded` unless explicitly provided.
|
* `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
|
* 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
|
* as this request body. If this parameter is specified `content-type` header will be set to
|
||||||
* `application/x-www-form-urlencoded` unless explicitly provided.
|
* `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
|
* 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
|
* as this request body. If this parameter is specified `content-type` header will be set to
|
||||||
* `application/x-www-form-urlencoded` unless explicitly provided.
|
* `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
|
* 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
|
* as this request body. If this parameter is specified `content-type` header will be set to
|
||||||
* `application/x-www-form-urlencoded` unless explicitly provided.
|
* `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
|
* 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
|
* as this request body. If this parameter is specified `content-type` header will be set to
|
||||||
* `application/x-www-form-urlencoded` unless explicitly provided.
|
* `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
|
* 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
|
* as this request body. If this parameter is specified `content-type` header will be set to
|
||||||
* `application/x-www-form-urlencoded` unless explicitly provided.
|
* `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
|
* 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 });
|
* await context.tracing.start({ screenshots: true, snapshots: true });
|
||||||
* const page = await context.newPage();
|
* const page = await context.newPage();
|
||||||
* await page.goto('https://playwright.dev');
|
* 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 });
|
* await context.tracing.start({ screenshots: true, snapshots: true });
|
||||||
* const page = await context.newPage();
|
* const page = await context.newPage();
|
||||||
* await page.goto('https://playwright.dev');
|
* await page.goto('https://playwright.dev');
|
||||||
* await context.tracing.stop({ path: 'trace.zip' });
|
* await context.tracing.stop({ path: 'trace.pwtrace.zip' });
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @param options
|
* @param options
|
||||||
|
|
@ -19994,12 +19999,12 @@ export interface Tracing {
|
||||||
* await context.tracing.startChunk();
|
* await context.tracing.startChunk();
|
||||||
* await page.getByText('Get Started').click();
|
* await page.getByText('Get Started').click();
|
||||||
* // Everything between startChunk and stopChunk will be recorded in the trace.
|
* // 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 context.tracing.startChunk();
|
||||||
* await page.goto('http://example.com');
|
* await page.goto('http://example.com');
|
||||||
* // Save a second trace file with different actions.
|
* // 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
|
* @param options
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ export interface TestServerInterface {
|
||||||
closeOnDisconnect?: boolean,
|
closeOnDisconnect?: boolean,
|
||||||
interceptStdio?: boolean,
|
interceptStdio?: boolean,
|
||||||
watchTestDirs?: boolean,
|
watchTestDirs?: boolean,
|
||||||
|
populateDependenciesOnList?: boolean,
|
||||||
}): Promise<void>;
|
}): Promise<void>;
|
||||||
|
|
||||||
ping(params: {}): Promise<void>;
|
ping(params: {}): Promise<void>;
|
||||||
|
|
|
||||||
|
|
@ -121,10 +121,10 @@ function createExpect(info: ExpectMetaInfo, prefix: string[], customMatchers: Re
|
||||||
const [actual, messageOrOptions] = argumentsList;
|
const [actual, messageOrOptions] = argumentsList;
|
||||||
const message = isString(messageOrOptions) ? messageOrOptions : messageOrOptions?.message || info.message;
|
const message = isString(messageOrOptions) ? messageOrOptions : messageOrOptions?.message || info.message;
|
||||||
const newInfo = { ...info, message };
|
const newInfo = { ...info, message };
|
||||||
if (newInfo.isPoll) {
|
if (newInfo.poll) {
|
||||||
if (typeof actual !== 'function')
|
if (typeof actual !== 'function')
|
||||||
throw new Error('`expect.poll()` accepts only function as a first argument');
|
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);
|
return createMatchers(actual, newInfo, prefix);
|
||||||
},
|
},
|
||||||
|
|
@ -189,10 +189,10 @@ function createExpect(info: ExpectMetaInfo, prefix: string[], customMatchers: Re
|
||||||
if ('soft' in configuration)
|
if ('soft' in configuration)
|
||||||
newInfo.isSoft = configuration.soft;
|
newInfo.isSoft = configuration.soft;
|
||||||
if ('_poll' in configuration) {
|
if ('_poll' in configuration) {
|
||||||
newInfo.isPoll = !!configuration._poll;
|
newInfo.poll = configuration._poll ? { ...info.poll, generator: () => {} } : undefined;
|
||||||
if (typeof configuration._poll === 'object') {
|
if (typeof configuration._poll === 'object') {
|
||||||
newInfo.pollTimeout = configuration._poll.timeout;
|
newInfo.poll!.timeout = configuration._poll.timeout ?? newInfo.poll!.timeout;
|
||||||
newInfo.pollIntervals = configuration._poll.intervals;
|
newInfo.poll!.intervals = configuration._poll.intervals ?? newInfo.poll!.intervals;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return createExpect(newInfo, prefix, customMatchers);
|
return createExpect(newInfo, prefix, customMatchers);
|
||||||
|
|
@ -249,11 +249,12 @@ type ExpectMetaInfo = {
|
||||||
message?: string;
|
message?: string;
|
||||||
isNot?: boolean;
|
isNot?: boolean;
|
||||||
isSoft?: boolean;
|
isSoft?: boolean;
|
||||||
isPoll?: boolean;
|
poll?: {
|
||||||
|
timeout?: number;
|
||||||
|
intervals?: number[];
|
||||||
|
generator: Generator;
|
||||||
|
};
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
pollTimeout?: number;
|
|
||||||
pollIntervals?: number[];
|
|
||||||
generator?: Generator;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
||||||
|
|
@ -287,10 +288,10 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
||||||
this._info.isNot = !this._info.isNot;
|
this._info.isNot = !this._info.isNot;
|
||||||
return new Proxy(matcher, this);
|
return new Proxy(matcher, this);
|
||||||
}
|
}
|
||||||
if (this._info.isPoll) {
|
if (this._info.poll) {
|
||||||
if ((customAsyncMatchers as any)[matcherName] || matcherName === 'resolves' || matcherName === 'rejects')
|
if ((customAsyncMatchers as any)[matcherName] || matcherName === 'resolves' || matcherName === 'rejects')
|
||||||
throw new Error(`\`expect.poll()\` does not support "${matcherName}" matcher.`);
|
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[]) => {
|
return (...args: any[]) => {
|
||||||
const testInfo = currentTestInfo();
|
const testInfo = currentTestInfo();
|
||||||
|
|
@ -302,7 +303,7 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
||||||
const customMessage = this._info.message || '';
|
const customMessage = this._info.message || '';
|
||||||
const argsSuffix = computeArgsSuffix(matcherName, args);
|
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;
|
const title = customMessage || defaultTitle;
|
||||||
|
|
||||||
// This looks like it is unnecessary, but it isn't - we need to filter
|
// 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);
|
const callback = () => matcher.call(target, ...args);
|
||||||
// toPass and poll matchers can contain other steps, expects and API calls,
|
// toPass and poll matchers can contain other steps, expects and API calls,
|
||||||
// so they behave like a retriable step.
|
// 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('stepZone', step, callback) :
|
||||||
zones.run<ExpectZone, any>('expectZone', { title, stepId: step.stepId }, callback);
|
zones.run<ExpectZone, any>('expectZone', { title, stepId: step.stepId }, callback);
|
||||||
if (result instanceof Promise)
|
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 testInfo = currentTestInfo();
|
||||||
|
const poll = info.poll!;
|
||||||
|
const timeout = poll.timeout ?? currentExpectTimeout();
|
||||||
const { deadline, timeoutMessage } = testInfo ? testInfo._deadlineForMatcher(timeout) : TestInfoImpl._defaultDeadlineForMatcher(timeout);
|
const { deadline, timeoutMessage } = testInfo ? testInfo._deadlineForMatcher(timeout) : TestInfoImpl._defaultDeadlineForMatcher(timeout);
|
||||||
|
|
||||||
const result = await pollAgainstDeadline<Error|undefined>(async () => {
|
const result = await pollAgainstDeadline<Error|undefined>(async () => {
|
||||||
if (testInfo && currentTestInfo() !== testInfo)
|
if (testInfo && currentTestInfo() !== testInfo)
|
||||||
return { continuePolling: false, result: undefined };
|
return { continuePolling: false, result: undefined };
|
||||||
|
|
||||||
const value = await generator();
|
const innerInfo: ExpectMetaInfo = {
|
||||||
let expectInstance = expectLibrary(value) as any;
|
...info,
|
||||||
if (isNot)
|
isSoft: false, // soft is outside of poll, not inside
|
||||||
expectInstance = expectInstance.not;
|
poll: undefined,
|
||||||
|
};
|
||||||
|
const value = await poll.generator();
|
||||||
try {
|
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 };
|
return { continuePolling: false, result: undefined };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { continuePolling: true, result: error };
|
return { continuePolling: true, result: error };
|
||||||
}
|
}
|
||||||
}, deadline, pollIntervals ?? [100, 250, 500, 1000]);
|
}, deadline, poll.intervals ?? [100, 250, 500, 1000]);
|
||||||
|
|
||||||
if (result.timedOut) {
|
if (result.timedOut) {
|
||||||
const message = result.result ? [
|
const message = result.result ? [
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,7 @@ Examples:
|
||||||
}
|
}
|
||||||
|
|
||||||
function addMergeReportsCommand(program: Command) {
|
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.description('merge multiple blob reports (for sharded tests) into a single report');
|
||||||
command.action(async (dir, options) => {
|
command.action(async (dir, options) => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,7 @@ export class TestServerDispatcher implements TestServerInterface {
|
||||||
private _serializer = require.resolve('./uiModeReporter');
|
private _serializer = require.resolve('./uiModeReporter');
|
||||||
private _watchTestDirs = false;
|
private _watchTestDirs = false;
|
||||||
private _closeOnDisconnect = false;
|
private _closeOnDisconnect = false;
|
||||||
|
private _populateDependenciesOnList = false;
|
||||||
|
|
||||||
constructor(configLocation: ConfigLocation) {
|
constructor(configLocation: ConfigLocation) {
|
||||||
this._configLocation = configLocation;
|
this._configLocation = configLocation;
|
||||||
|
|
@ -113,6 +114,7 @@ export class TestServerDispatcher implements TestServerInterface {
|
||||||
this._closeOnDisconnect = !!params.closeOnDisconnect;
|
this._closeOnDisconnect = !!params.closeOnDisconnect;
|
||||||
await this._setInterceptStdio(!!params.interceptStdio);
|
await this._setInterceptStdio(!!params.interceptStdio);
|
||||||
this._watchTestDirs = !!params.watchTestDirs;
|
this._watchTestDirs = !!params.watchTestDirs;
|
||||||
|
this._populateDependenciesOnList = !!params.populateDependenciesOnList;
|
||||||
}
|
}
|
||||||
|
|
||||||
async ping() {}
|
async ping() {}
|
||||||
|
|
@ -252,7 +254,7 @@ export class TestServerDispatcher implements TestServerInterface {
|
||||||
config.cliListOnly = true;
|
config.cliListOnly = true;
|
||||||
|
|
||||||
const status = await runTasks(new TestRun(config, reporter), [
|
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(),
|
createReportBeginTask(),
|
||||||
]);
|
]);
|
||||||
return { config, report, reporter, status };
|
return { config, report, reporter, status };
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,7 @@ export async function runWatchModeLoop(configLocation: ConfigLocation, initialOp
|
||||||
});
|
});
|
||||||
testServerConnection.onReport(report => teleSuiteUpdater.processTestReportEvent(report));
|
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({});
|
await testServerConnection.runGlobalSetup({});
|
||||||
|
|
||||||
const { report } = await testServerConnection.listTests({});
|
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 lastRun: { type: 'changed' | 'regular' | 'failed', failedTestIds?: string[], dirtyTestIds?: string[] } = { type: 'regular' };
|
||||||
let result: FullResult['status'] = 'passed';
|
let result: FullResult['status'] = 'passed';
|
||||||
|
|
||||||
// Enter the watch loop.
|
|
||||||
await runTests(options, testServerConnection);
|
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
printPrompt();
|
printPrompt();
|
||||||
const readCommandPromise = readCommand();
|
const readCommandPromise = readCommand();
|
||||||
|
|
@ -330,7 +327,7 @@ Change settings
|
||||||
|
|
||||||
let showBrowserServer: PlaywrightServer | undefined;
|
let showBrowserServer: PlaywrightServer | undefined;
|
||||||
let connectWsEndpoint: string | undefined = undefined;
|
let connectWsEndpoint: string | undefined = undefined;
|
||||||
let seq = 0;
|
let seq = 1;
|
||||||
|
|
||||||
function printConfiguration(options: WatchModeOptions, title?: string) {
|
function printConfiguration(options: WatchModeOptions, title?: string) {
|
||||||
const packageManagerCommand = getPackageManagerExecCommand();
|
const packageManagerCommand = getPackageManagerExecCommand();
|
||||||
|
|
@ -344,9 +341,7 @@ function printConfiguration(options: WatchModeOptions, title?: string) {
|
||||||
tokens.push(...options.files.map(a => colors.bold(a)));
|
tokens.push(...options.files.map(a => colors.bold(a)));
|
||||||
if (title)
|
if (title)
|
||||||
tokens.push(colors.dim(`(${title})`));
|
tokens.push(colors.dim(`(${title})`));
|
||||||
if (seq)
|
tokens.push(colors.dim(`#${seq++}`));
|
||||||
tokens.push(colors.dim(`#${seq}`));
|
|
||||||
++seq;
|
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
const sep = separator();
|
const sep = separator();
|
||||||
lines.push('\x1Bc' + sep);
|
lines.push('\x1Bc' + sep);
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@ export class TestTracing {
|
||||||
}
|
}
|
||||||
|
|
||||||
generateNextTraceRecordingPath() {
|
generateNextTraceRecordingPath() {
|
||||||
const file = path.join(this._artifactsDir, createGuid() + '.zip');
|
const file = path.join(this._artifactsDir, createGuid() + '.pwtrace.zip');
|
||||||
this._temporaryTraceFiles.push(file);
|
this._temporaryTraceFiles.push(file);
|
||||||
return 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);
|
await mergeTraceFiles(tracePath, this._temporaryTraceFiles);
|
||||||
this._testInfo.attachments.push({ name: 'trace', path: tracePath, contentType: 'application/zip' });
|
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>;
|
exposeBinding(params: PageExposeBindingParams, metadata?: CallMetadata): Promise<PageExposeBindingResult>;
|
||||||
goBack(params: PageGoBackParams, metadata?: CallMetadata): Promise<PageGoBackResult>;
|
goBack(params: PageGoBackParams, metadata?: CallMetadata): Promise<PageGoBackResult>;
|
||||||
goForward(params: PageGoForwardParams, metadata?: CallMetadata): Promise<PageGoForwardResult>;
|
goForward(params: PageGoForwardParams, metadata?: CallMetadata): Promise<PageGoForwardResult>;
|
||||||
|
forceGarbageCollection(params?: PageForceGarbageCollectionParams, metadata?: CallMetadata): Promise<PageForceGarbageCollectionResult>;
|
||||||
registerLocatorHandler(params: PageRegisterLocatorHandlerParams, metadata?: CallMetadata): Promise<PageRegisterLocatorHandlerResult>;
|
registerLocatorHandler(params: PageRegisterLocatorHandlerParams, metadata?: CallMetadata): Promise<PageRegisterLocatorHandlerResult>;
|
||||||
resolveLocatorHandlerNoReply(params: PageResolveLocatorHandlerNoReplyParams, metadata?: CallMetadata): Promise<PageResolveLocatorHandlerNoReplyResult>;
|
resolveLocatorHandlerNoReply(params: PageResolveLocatorHandlerNoReplyParams, metadata?: CallMetadata): Promise<PageResolveLocatorHandlerNoReplyResult>;
|
||||||
unregisterLocatorHandler(params: PageUnregisterLocatorHandlerParams, metadata?: CallMetadata): Promise<PageUnregisterLocatorHandlerResult>;
|
unregisterLocatorHandler(params: PageUnregisterLocatorHandlerParams, metadata?: CallMetadata): Promise<PageUnregisterLocatorHandlerResult>;
|
||||||
|
|
@ -2070,6 +2071,9 @@ export type PageGoForwardOptions = {
|
||||||
export type PageGoForwardResult = {
|
export type PageGoForwardResult = {
|
||||||
response?: ResponseChannel,
|
response?: ResponseChannel,
|
||||||
};
|
};
|
||||||
|
export type PageForceGarbageCollectionParams = {};
|
||||||
|
export type PageForceGarbageCollectionOptions = {};
|
||||||
|
export type PageForceGarbageCollectionResult = void;
|
||||||
export type PageRegisterLocatorHandlerParams = {
|
export type PageRegisterLocatorHandlerParams = {
|
||||||
selector: string,
|
selector: string,
|
||||||
noWaitAfter?: boolean,
|
noWaitAfter?: boolean,
|
||||||
|
|
|
||||||
|
|
@ -1430,6 +1430,8 @@ Page:
|
||||||
slowMo: true
|
slowMo: true
|
||||||
snapshot: true
|
snapshot: true
|
||||||
|
|
||||||
|
forceGarbageCollection:
|
||||||
|
|
||||||
registerLocatorHandler:
|
registerLocatorHandler:
|
||||||
parameters:
|
parameters:
|
||||||
selector: string
|
selector: string
|
||||||
|
|
|
||||||
|
|
@ -62,21 +62,21 @@ const test = baseTest.extend<BrowserTestTestFixtures, BrowserTestWorkerFixtures>
|
||||||
await run(playwright[browserName]);
|
await run(playwright[browserName]);
|
||||||
}, { scope: 'worker' }],
|
}, { scope: 'worker' }],
|
||||||
|
|
||||||
allowsThirdParty: [async ({ browserName, browserMajorVersion, channel }, run) => {
|
allowsThirdParty: [async ({ browserName }, run) => {
|
||||||
if (browserName === 'firefox')
|
if (browserName === 'firefox')
|
||||||
await run(true);
|
await run(true);
|
||||||
else
|
else
|
||||||
await run(false);
|
await run(false);
|
||||||
}, { scope: 'worker' }],
|
}, { scope: 'worker' }],
|
||||||
|
|
||||||
defaultSameSiteCookieValue: [async ({ browserName, browserMajorVersion, channel, isLinux }, run) => {
|
defaultSameSiteCookieValue: [async ({ browserName, isLinux }, run) => {
|
||||||
if (browserName === 'chromium')
|
if (browserName === 'chromium' || browserName as any === '_bidiChromium')
|
||||||
await run('Lax');
|
await run('Lax');
|
||||||
else if (browserName === 'webkit' && isLinux)
|
else if (browserName === 'webkit' && isLinux)
|
||||||
await run('Lax');
|
await run('Lax');
|
||||||
else if (browserName === 'webkit' && !isLinux)
|
else if (browserName === 'webkit' && !isLinux)
|
||||||
await run('None');
|
await run('None');
|
||||||
else if (browserName === 'firefox')
|
else if (browserName === 'firefox' || browserName as any === '_bidiFirefox')
|
||||||
await run('None');
|
await run('None');
|
||||||
else
|
else
|
||||||
throw new Error('unknown browser - ' + browserName);
|
throw new Error('unknown browser - ' + browserName);
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,7 @@ export const traceViewerFixtures: Fixtures<TraceViewerFixtures, {}, BaseTestFixt
|
||||||
|
|
||||||
runAndTrace: async ({ context, showTraceViewer }, use, testInfo) => {
|
runAndTrace: async ({ context, showTraceViewer }, use, testInfo) => {
|
||||||
await use(async (body: () => Promise<void>, optsOverrides = {}) => {
|
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 context.tracing.start({ snapshots: true, screenshots: true, sources: true, ...optsOverrides });
|
||||||
await body();
|
await body();
|
||||||
await context.tracing.stop({ path: traceFile });
|
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).toHaveTitle(/Playwright/);
|
||||||
await expect(window.getByRole('heading')).toHaveText('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) {
|
if (trace) {
|
||||||
await window.context().tracing.stop({ path });
|
await window.context().tracing.stop({ path });
|
||||||
test.info().attachments.push({ name: 'trace', path, contentType: 'application/zip' });
|
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 = [
|
const traces = [
|
||||||
// our actual trace.
|
// 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
|
// 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)
|
for (const trace of traces)
|
||||||
expect(fs.existsSync(trace)).toBe(true);
|
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');
|
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 }) {
|
it('should encode to application/json by default', async function({ context, page, server }) {
|
||||||
const data = {
|
const data = {
|
||||||
firstName: 'John',
|
firstName: 'John',
|
||||||
|
|
|
||||||
|
|
@ -248,7 +248,7 @@ test('should reset tracing', async ({ reusedContext, trace }, testInfo) => {
|
||||||
page = context.pages()[0];
|
page = context.pages()[0];
|
||||||
await page.evaluate('2 + 2');
|
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');
|
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 });
|
await context.tracing.start({ screenshots: true, snapshots: true });
|
||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
await page.evaluate(() => 2 + 2);
|
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 context.tracing.stop({ path: traceZip });
|
||||||
await cdpBrowser.close();
|
await cdpBrowser.close();
|
||||||
expect(fs.existsSync(traceZip)).toBe(true);
|
expect(fs.existsSync(traceZip)).toBe(true);
|
||||||
|
|
|
||||||
|
|
@ -546,6 +546,7 @@ test.describe('browser', () => {
|
||||||
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
||||||
};
|
};
|
||||||
const page = await browser.newPage({
|
const page = await browser.newPage({
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
clientCertificates: [{
|
clientCertificates: [{
|
||||||
origin: new URL(serverURL).origin,
|
origin: new URL(serverURL).origin,
|
||||||
...baseOptions,
|
...baseOptions,
|
||||||
|
|
|
||||||
|
|
@ -491,7 +491,7 @@ await page1.GotoAsync("about:blank?foo");`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should --save-trace', async ({ runCLI }, testInfo) => {
|
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}`], {
|
const cli = runCLI([`--save-trace=${traceFileName}`], {
|
||||||
autoExitWhen: ' ',
|
autoExitWhen: ' ',
|
||||||
});
|
});
|
||||||
|
|
@ -502,7 +502,7 @@ await page1.GotoAsync("about:blank?foo");`);
|
||||||
test('should save assets via SIGINT', async ({ runCLI, platform }, testInfo) => {
|
test('should save assets via SIGINT', async ({ runCLI, platform }, testInfo) => {
|
||||||
test.skip(platform === 'win32', 'SIGINT not supported on Windows');
|
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 storageFileName = testInfo.outputPath('auth.json');
|
||||||
const harFileName = testInfo.outputPath('har.har');
|
const harFileName = testInfo.outputPath('har.har');
|
||||||
const cli = runCLI([`--save-trace=${traceFileName}`, `--save-storage=${storageFileName}`, `--save-har=${harFileName}`]);
|
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 () => {
|
runBeforeCloseBrowserContext: async () => {
|
||||||
await page.hover('body');
|
await page.hover('body');
|
||||||
await page.close();
|
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 });
|
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 }) => {
|
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 });
|
const page = await browser.newPage({ deviceScaleFactor: 3 });
|
||||||
await page.context().tracing.start({ snapshots: true, screenshots: true, sources: true });
|
await page.context().tracing.start({ snapshots: true, screenshots: true, sources: true });
|
||||||
await page.setViewportSize({ width: 300, height: 300 });
|
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 }) => {
|
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 });
|
const page = await browser.newPage({ javaScriptEnabled: false });
|
||||||
await page.context().tracing.start({ snapshots: true, screenshots: true, sources: true });
|
await page.context().tracing.start({ snapshots: true, screenshots: true, sources: true });
|
||||||
await page.goto(server.EMPTY_PAGE);
|
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();
|
await expect(frame.getByText('javascript is disabled!')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should remove noscript by default', async ({ browser, server, showTraceViewer, browserType }) => {
|
test('should remove noscript by default', 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: undefined });
|
const page = await browser.newPage({ javaScriptEnabled: undefined });
|
||||||
await page.context().tracing.start({ snapshots: true, screenshots: true, sources: true });
|
await page.context().tracing.start({ snapshots: true, screenshots: true, sources: true });
|
||||||
await page.goto(server.EMPTY_PAGE);
|
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();
|
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 }) => {
|
test('should remove noscript when javaScriptEnabled is set to true', 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: true });
|
const page = await browser.newPage({ javaScriptEnabled: true });
|
||||||
await page.context().tracing.start({ snapshots: true, screenshots: true, sources: true });
|
await page.context().tracing.start({ snapshots: true, screenshots: true, sources: true });
|
||||||
await page.goto(server.EMPTY_PAGE);
|
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.locator('input[type="file"]').setInputFiles(asset('file-to-upload.txt'));
|
||||||
await page.waitForTimeout(2000); // Give it some time to produce screenshots.
|
await page.waitForTimeout(2000); // Give it some time to produce screenshots.
|
||||||
await page.close();
|
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(events[0].type).toBe('context-options');
|
||||||
expect(actions).toEqual([
|
expect(actions).toEqual([
|
||||||
'page.goto',
|
'page.goto',
|
||||||
|
|
@ -81,8 +81,8 @@ test('should use the correct apiName for event driven callbacks', async ({ conte
|
||||||
});
|
});
|
||||||
await page.evaluate(() => alert('yo'));
|
await page.evaluate(() => alert('yo'));
|
||||||
|
|
||||||
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(events[0].type).toBe('context-options');
|
||||||
expect(actions).toEqual([
|
expect(actions).toEqual([
|
||||||
'page.route',
|
'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.setContent('<button>Click</button>');
|
||||||
await page.click('"Click"');
|
await page.click('"Click"');
|
||||||
await page.close();
|
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 === 'frame-snapshot')).toBeFalsy();
|
||||||
expect(events.some(e => e.type === 'resource-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 context.tracing.start({ snapshots: true });
|
||||||
await page.goto(server.PREFIX + '/empty.html');
|
await page.goto(server.PREFIX + '/empty.html');
|
||||||
await page.screenshot();
|
await page.screenshot();
|
||||||
await context.tracing.stop({ path: testInfo.outputPath('trace.zip') });
|
await context.tracing.stop({ path: testInfo.outputPath('trace.pwtrace.zip') });
|
||||||
const { actionObjects } = await parseTraceRaw(testInfo.outputPath('trace.zip'));
|
const { actionObjects } = await parseTraceRaw(testInfo.outputPath('trace.pwtrace.zip'));
|
||||||
const screenshotEvent = actionObjects.find(a => a.apiName === 'page.screenshot');
|
const screenshotEvent = actionObjects.find(a => a.apiName === 'page.screenshot');
|
||||||
expect(screenshotEvent.beforeSnapshot).toBeTruthy();
|
expect(screenshotEvent.beforeSnapshot).toBeTruthy();
|
||||||
expect(screenshotEvent.afterSnapshot).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.tracing.start();
|
||||||
await context.storageState();
|
await context.storageState();
|
||||||
await page.close();
|
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();
|
const pageIds = new Set();
|
||||||
trace.events.forEach(e => {
|
trace.events.forEach(e => {
|
||||||
const pageId = e.pageId;
|
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) => {
|
test('should include context API requests', async ({ browserName, context, page, server }, testInfo) => {
|
||||||
await context.tracing.start({ snapshots: true });
|
await context.tracing.start({ snapshots: true });
|
||||||
await page.request.post(server.PREFIX + '/simple.json', { data: { foo: 'bar' } });
|
await page.request.post(server.PREFIX + '/simple.json', { data: { foo: 'bar' } });
|
||||||
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'));
|
||||||
const postEvent = events.find(e => e.apiName === 'apiRequestContext.post');
|
const postEvent = events.find(e => e.apiName === 'apiRequestContext.post');
|
||||||
expect(postEvent).toBeTruthy();
|
expect(postEvent).toBeTruthy();
|
||||||
const harEntry = events.find(e => e.type === 'resource-snapshot');
|
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.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 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');
|
const frames = events.filter(e => e.type === 'screencast-frame');
|
||||||
|
|
||||||
// Check all frame sizes.
|
// 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.goto(server.EMPTY_PAGE);
|
||||||
await page.setContent('<button>Click</button>');
|
await page.setContent('<button>Click</button>');
|
||||||
page.click('"ClickNoButton"').catch(() => {});
|
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();
|
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');
|
const clickEvent = events.find(e => e.apiName === 'page.click');
|
||||||
expect(clickEvent).toBeTruthy();
|
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) => {
|
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');
|
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"');
|
await page.click('"Click"');
|
||||||
page.click('"ClickNoButton"', { timeout: 0 }).catch(() => {});
|
page.click('"ClickNoButton"', { timeout: 0 }).catch(() => {});
|
||||||
await page.evaluate(() => {});
|
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 context.tracing.startChunk();
|
||||||
await page.hover('"Click"');
|
await page.hover('"Click"');
|
||||||
|
|
@ -502,7 +502,7 @@ test('should work with multiple chunks', async ({ context, page, server }, testI
|
||||||
await page.click('"Click"');
|
await page.click('"Click"');
|
||||||
await context.tracing.stopChunk(); // Should stop without a path.
|
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.events[0].type).toBe('context-options');
|
||||||
expect(trace1.actions).toEqual([
|
expect(trace1.actions).toEqual([
|
||||||
'page.setContent',
|
'page.setContent',
|
||||||
|
|
@ -533,7 +533,7 @@ test('should export trace concurrently to second navigation', async ({ context,
|
||||||
await page.waitForTimeout(timeout);
|
await page.waitForTimeout(timeout);
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
promise,
|
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 context.tracing.start({ screenshots: true, snapshots: true });
|
||||||
await page.click('button');
|
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([
|
expect(trace.actions).toEqual([
|
||||||
'page.click',
|
'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.setContent(`<div onclick='window.alert(123)'>Click me</div>`);
|
||||||
await page.click('div');
|
await page.click('div');
|
||||||
await evalPromise;
|
await evalPromise;
|
||||||
const tracePath = testInfo.outputPath('trace.zip');
|
const tracePath = testInfo.outputPath('trace.pwtrace.zip');
|
||||||
await context.tracing.stop({ path: tracePath });
|
await context.tracing.stop({ path: tracePath });
|
||||||
|
|
||||||
const trace = await parseTraceRaw(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 page.click('div');
|
||||||
await expect(page.locator('div')).toBeVisible();
|
await expect(page.locator('div')).toBeVisible();
|
||||||
await expectPromise;
|
await expectPromise;
|
||||||
const tracePath = testInfo.outputPath('trace.zip');
|
const tracePath = testInfo.outputPath('trace.pwtrace.zip');
|
||||||
await context.tracing.stop({ path: tracePath });
|
await context.tracing.stop({ path: tracePath });
|
||||||
|
|
||||||
const trace = await parseTraceRaw(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 });
|
await (request as any)._tracing.start({ snapshots: true });
|
||||||
const url = server.PREFIX + '/simple.json';
|
const url = server.PREFIX + '/simple.json';
|
||||||
await request.get(url);
|
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 });
|
await (request as any)._tracing.stop({ path: tracePath });
|
||||||
|
|
||||||
const trace = await parseTraceRaw(tracePath);
|
const trace = await parseTraceRaw(tracePath);
|
||||||
|
|
@ -649,7 +649,7 @@ test('should store global request traces separately', async ({ request, server,
|
||||||
request.get(url),
|
request.get(url),
|
||||||
request2.post(url)
|
request2.post(url)
|
||||||
]);
|
]);
|
||||||
const tracePath = testInfo.outputPath('trace.zip');
|
const tracePath = testInfo.outputPath('trace.pwtrace.zip');
|
||||||
const trace2Path = testInfo.outputPath('trace2.zip');
|
const trace2Path = testInfo.outputPath('trace2.zip');
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
(request as any)._tracing.stop({ path: tracePath }),
|
(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, {
|
await request.post(url, {
|
||||||
data: 'test'
|
data: 'test'
|
||||||
});
|
});
|
||||||
const tracePath = testInfo.outputPath('trace.zip');
|
const tracePath = testInfo.outputPath('trace.pwtrace.zip');
|
||||||
await (request as any)._tracing.stop({ path: tracePath });
|
await (request as any)._tracing.stop({ path: tracePath });
|
||||||
|
|
||||||
const trace = await parseTraceRaw(tracePath);
|
const trace = await parseTraceRaw(tracePath);
|
||||||
|
|
@ -755,7 +755,7 @@ test('should flush console events on tracing stop', async ({ context, page }, te
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await promise;
|
await promise;
|
||||||
const tracePath = testInfo.outputPath('trace.zip');
|
const tracePath = testInfo.outputPath('trace.pwtrace.zip');
|
||||||
await context.tracing.stop({ path: tracePath });
|
await context.tracing.stop({ path: tracePath });
|
||||||
const trace = await parseTraceRaw(tracePath);
|
const trace = await parseTraceRaw(tracePath);
|
||||||
const events = trace.events.filter(e => e.type === 'console');
|
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');
|
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 size = { width: 500, height: 400 };
|
||||||
const traceFile = testInfo.outputPath('trace.zip');
|
const traceFile = testInfo.outputPath('trace.pwtrace.zip');
|
||||||
|
|
||||||
const context = await browser.newContext({
|
const context = await browser.newContext({
|
||||||
recordVideo: {
|
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'>;
|
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';
|
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 };
|
video: VideoMode | { mode: VideoMode, size: ViewportSize };
|
||||||
browserName: 'chromium' | 'firefox' | 'webkit';
|
browserName: 'chromium' | 'firefox' | 'webkit' | '_bidiFirefox' | '_bidiChromium';
|
||||||
browserVersion: string;
|
browserVersion: string;
|
||||||
browserMajorVersion: number;
|
browserMajorVersion: number;
|
||||||
electronMajorVersion: number;
|
electronMajorVersion: number;
|
||||||
|
|
|
||||||
|
|
@ -262,4 +262,20 @@ test('should propagate string exception from async arrow function', { annotation
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.output).toContain('some error');
|
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([
|
expect(listFiles(testInfo.outputPath('test-results'))).toEqual([
|
||||||
'.last-run.json',
|
'.last-run.json',
|
||||||
'artifacts-failing',
|
'artifacts-failing',
|
||||||
' trace.zip',
|
' trace.pwtrace.zip',
|
||||||
'artifacts-own-context-failing',
|
'artifacts-own-context-failing',
|
||||||
' trace.zip',
|
' trace.pwtrace.zip',
|
||||||
'artifacts-own-context-passing',
|
'artifacts-own-context-passing',
|
||||||
' trace.zip',
|
' trace.pwtrace.zip',
|
||||||
'artifacts-passing',
|
'artifacts-passing',
|
||||||
' trace.zip',
|
' trace.pwtrace.zip',
|
||||||
'artifacts-persistent-failing',
|
'artifacts-persistent-failing',
|
||||||
' trace.zip',
|
' trace.pwtrace.zip',
|
||||||
'artifacts-persistent-passing',
|
'artifacts-persistent-passing',
|
||||||
' trace.zip',
|
' trace.pwtrace.zip',
|
||||||
'artifacts-shared-shared-failing',
|
'artifacts-shared-shared-failing',
|
||||||
' trace.zip',
|
' trace.pwtrace.zip',
|
||||||
'artifacts-shared-shared-passing',
|
'artifacts-shared-shared-passing',
|
||||||
' trace.zip',
|
' trace.pwtrace.zip',
|
||||||
'artifacts-two-contexts',
|
'artifacts-two-contexts',
|
||||||
' trace.zip',
|
' trace.pwtrace.zip',
|
||||||
'artifacts-two-contexts-failing',
|
'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([
|
expect(listFiles(testInfo.outputPath('test-results'))).toEqual([
|
||||||
'.last-run.json',
|
'.last-run.json',
|
||||||
'artifacts-failing',
|
'artifacts-failing',
|
||||||
' trace.zip',
|
' trace.pwtrace.zip',
|
||||||
'artifacts-own-context-failing',
|
'artifacts-own-context-failing',
|
||||||
' trace.zip',
|
' trace.pwtrace.zip',
|
||||||
'artifacts-persistent-failing',
|
'artifacts-persistent-failing',
|
||||||
' trace.zip',
|
' trace.pwtrace.zip',
|
||||||
'artifacts-shared-shared-failing',
|
'artifacts-shared-shared-failing',
|
||||||
' trace.zip',
|
' trace.pwtrace.zip',
|
||||||
'artifacts-two-contexts-failing',
|
'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([
|
expect(listFiles(testInfo.outputPath('test-results'))).toEqual([
|
||||||
'.last-run.json',
|
'.last-run.json',
|
||||||
'artifacts-failing-retry1',
|
'artifacts-failing-retry1',
|
||||||
' trace.zip',
|
' trace.pwtrace.zip',
|
||||||
'artifacts-own-context-failing-retry1',
|
'artifacts-own-context-failing-retry1',
|
||||||
' trace.zip',
|
' trace.pwtrace.zip',
|
||||||
'artifacts-persistent-failing-retry1',
|
'artifacts-persistent-failing-retry1',
|
||||||
' trace.zip',
|
' trace.pwtrace.zip',
|
||||||
'artifacts-shared-shared-failing-retry1',
|
'artifacts-shared-shared-failing-retry1',
|
||||||
' trace.zip',
|
' trace.pwtrace.zip',
|
||||||
'artifacts-two-contexts-failing-retry1',
|
'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([
|
expect(listFiles(testInfo.outputPath('test-results'))).toEqual([
|
||||||
'.last-run.json',
|
'.last-run.json',
|
||||||
'artifacts-failing-retry1',
|
'artifacts-failing-retry1',
|
||||||
' trace.zip',
|
' trace.pwtrace.zip',
|
||||||
'artifacts-failing-retry2',
|
'artifacts-failing-retry2',
|
||||||
' trace.zip',
|
' trace.pwtrace.zip',
|
||||||
'artifacts-own-context-failing-retry1',
|
'artifacts-own-context-failing-retry1',
|
||||||
' trace.zip',
|
' trace.pwtrace.zip',
|
||||||
'artifacts-own-context-failing-retry2',
|
'artifacts-own-context-failing-retry2',
|
||||||
' trace.zip',
|
' trace.pwtrace.zip',
|
||||||
'artifacts-persistent-failing-retry1',
|
'artifacts-persistent-failing-retry1',
|
||||||
' trace.zip',
|
' trace.pwtrace.zip',
|
||||||
'artifacts-persistent-failing-retry2',
|
'artifacts-persistent-failing-retry2',
|
||||||
' trace.zip',
|
' trace.pwtrace.zip',
|
||||||
'artifacts-shared-shared-failing-retry1',
|
'artifacts-shared-shared-failing-retry1',
|
||||||
' trace.zip',
|
' trace.pwtrace.zip',
|
||||||
'artifacts-shared-shared-failing-retry2',
|
'artifacts-shared-shared-failing-retry2',
|
||||||
' trace.zip',
|
' trace.pwtrace.zip',
|
||||||
'artifacts-two-contexts-failing-retry1',
|
'artifacts-two-contexts-failing-retry1',
|
||||||
' trace.zip',
|
' trace.pwtrace.zip',
|
||||||
'artifacts-two-contexts-failing-retry2',
|
'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([
|
expect(listFiles(testInfo.outputPath('test-results'))).toEqual([
|
||||||
'.last-run.json',
|
'.last-run.json',
|
||||||
'artifacts-failing',
|
'artifacts-failing',
|
||||||
' trace.zip',
|
' trace.pwtrace.zip',
|
||||||
'artifacts-own-context-failing',
|
'artifacts-own-context-failing',
|
||||||
' trace.zip',
|
' trace.pwtrace.zip',
|
||||||
'artifacts-persistent-failing',
|
'artifacts-persistent-failing',
|
||||||
' trace.zip',
|
' trace.pwtrace.zip',
|
||||||
'artifacts-shared-shared-failing',
|
'artifacts-shared-shared-failing',
|
||||||
' trace.zip',
|
' trace.pwtrace.zip',
|
||||||
'artifacts-two-contexts-failing',
|
'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.exitCode).toBe(0);
|
||||||
expect(result.passed).toBe(2);
|
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([
|
expect(trace1.actionTree).toEqual([
|
||||||
'Before Hooks',
|
'Before Hooks',
|
||||||
' fixture: browser',
|
' 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(trace1.traceModel.storage().snapshotsForTest().length).toBeGreaterThan(0);
|
||||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'reuse-one', 'trace-1.zip'))).toBe(false);
|
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([
|
expect(trace2.actionTree).toEqual([
|
||||||
'Before Hooks',
|
'Before Hooks',
|
||||||
' fixture: context',
|
' fixture: context',
|
||||||
|
|
@ -533,6 +533,6 @@ test('should survive serial mode with tracing and reuse', async ({ runInlineTest
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
expect(result.passed).toBe(2);
|
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-one', 'trace.pwtrace.zip'))).toBe(true);
|
||||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'reuse-two', 'trace.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.exitCode).toBe(0);
|
||||||
expect(result.passed).toBe(1);
|
expect(result.passed).toBe(1);
|
||||||
expect(result.flaky).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) => {
|
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.passed).toBe(2);
|
||||||
expect(result.failed).toBe(1);
|
expect(result.failed).toBe(1);
|
||||||
// One trace file for request context and one for each APIRequestContext
|
// 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([
|
expect(trace1.actionTree).toEqual([
|
||||||
'Before Hooks',
|
'Before Hooks',
|
||||||
' fixture: request',
|
' fixture: request',
|
||||||
|
|
@ -105,14 +105,14 @@ test('should record api trace', async ({ runInlineTest, server }, testInfo) => {
|
||||||
' fixture: request',
|
' fixture: request',
|
||||||
' apiRequestContext.dispose',
|
' 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([
|
expect(trace2.actionTree).toEqual([
|
||||||
'Before Hooks',
|
'Before Hooks',
|
||||||
'apiRequest.newContext',
|
'apiRequest.newContext',
|
||||||
'apiRequestContext.get',
|
'apiRequestContext.get',
|
||||||
'After Hooks',
|
'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([
|
expect(trace3.actionTree).toEqual([
|
||||||
'Before Hooks',
|
'Before Hooks',
|
||||||
' fixture: request',
|
' fixture: request',
|
||||||
|
|
@ -204,7 +204,7 @@ test('should not mixup network files between contexts', async ({ runInlineTest,
|
||||||
}, { workers: 1, timeout: 15000 });
|
}, { workers: 1, timeout: 15000 });
|
||||||
expect(result.exitCode).toEqual(0);
|
expect(result.exitCode).toEqual(0);
|
||||||
expect(result.passed).toBe(1);
|
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) => {
|
test('should save sources when requested', async ({ runInlineTest }, testInfo) => {
|
||||||
|
|
@ -224,7 +224,7 @@ test('should save sources when requested', async ({ runInlineTest }, testInfo) =
|
||||||
`,
|
`,
|
||||||
}, { workers: 1 });
|
}, { workers: 1 });
|
||||||
expect(result.exitCode).toEqual(0);
|
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);
|
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 });
|
}, { workers: 1 });
|
||||||
expect(result.exitCode).toEqual(0);
|
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);
|
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.exitCode).toBe(1);
|
||||||
expect(result.passed).toBe(1);
|
expect(result.passed).toBe(1);
|
||||||
expect(result.failed).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-passes', 'trace.pwtrace.zip'))).toBeFalsy();
|
||||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'a-serial-fails', 'trace.zip'))).toBeTruthy();
|
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) => {
|
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.exitCode).toBe(1);
|
||||||
expect(result.passed).toBe(1);
|
expect(result.passed).toBe(1);
|
||||||
expect(result.failed).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([
|
expect(trace1.actionTree).toEqual([
|
||||||
'Before Hooks',
|
'Before Hooks',
|
||||||
|
|
@ -338,7 +338,7 @@ test('should not override trace file in afterAll', async ({ runInlineTest, serve
|
||||||
]);
|
]);
|
||||||
expect(trace1.errors).toEqual([`'oh no!'`]);
|
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();
|
expect(error).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -366,8 +366,8 @@ test('should retain traces for interrupted tests', async ({ runInlineTest }, tes
|
||||||
expect(result.exitCode).toBe(1);
|
expect(result.exitCode).toBe(1);
|
||||||
expect(result.failed).toBe(1);
|
expect(result.failed).toBe(1);
|
||||||
expect(result.interrupted).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', 'a-test-1', 'trace.pwtrace.zip'))).toBeTruthy();
|
||||||
expect(fs.existsSync(testInfo.outputPath('test-results', 'b-test-2', 'trace.zip'))).toBeTruthy();
|
expect(fs.existsSync(testInfo.outputPath('test-results', 'b-test-2', 'trace.pwtrace.zip'))).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should respect --trace', async ({ runInlineTest }, testInfo) => {
|
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.exitCode).toBe(0);
|
||||||
expect(result.passed).toBe(1);
|
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) => {
|
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.exitCode).toBe(0);
|
||||||
expect(result.passed).toBe(1);
|
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']) {
|
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' });
|
}, { 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);
|
const trace = await parseTrace(tracePath);
|
||||||
expect(trace.apiNames).toContain('page.goto');
|
expect(trace.apiNames).toContain('page.goto');
|
||||||
expect(result.failed).toBe(1);
|
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' });
|
}, { 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);
|
const trace = await parseTrace(tracePath);
|
||||||
expect(trace.apiNames).toContain('page.goto');
|
expect(trace.apiNames).toContain('page.goto');
|
||||||
expect(result.failed).toBe(1);
|
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' });
|
}, { 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);
|
const trace = await parseTrace(tracePath);
|
||||||
expect(trace.apiNames).toContain('apiRequestContext.get');
|
expect(trace.apiNames).toContain('apiRequestContext.get');
|
||||||
expect(result.failed).toBe(1);
|
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.exitCode).toBe(0);
|
||||||
expect(result.passed).toBe(1);
|
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([
|
expect(trace.apiNames).toEqual([
|
||||||
'Before Hooks',
|
'Before Hooks',
|
||||||
`attach "foo"`,
|
`attach "foo"`,
|
||||||
|
|
@ -559,7 +559,7 @@ test('should opt out of attachments', async ({ runInlineTest, server }, testInfo
|
||||||
|
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
expect(result.passed).toBe(1);
|
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([
|
expect(trace.apiNames).toEqual([
|
||||||
'Before Hooks',
|
'Before Hooks',
|
||||||
`attach "foo"`,
|
`attach "foo"`,
|
||||||
|
|
@ -592,7 +592,7 @@ test('should record with custom page fixture', async ({ runInlineTest }, testInf
|
||||||
expect(result.exitCode).toBe(1);
|
expect(result.exitCode).toBe(1);
|
||||||
expect(result.failed).toBe(1);
|
expect(result.failed).toBe(1);
|
||||||
expect(result.output).toContain('failure!');
|
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({
|
expect(trace.events).toContainEqual(expect.objectContaining({
|
||||||
type: 'frame-snapshot',
|
type: 'frame-snapshot',
|
||||||
}));
|
}));
|
||||||
|
|
@ -617,7 +617,7 @@ test('should expand expect.toPass', async ({ runInlineTest }, testInfo) => {
|
||||||
|
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
expect(result.passed).toBe(1);
|
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([
|
expect(trace.actionTree).toEqual([
|
||||||
'Before Hooks',
|
'Before Hooks',
|
||||||
' fixture: browser',
|
' fixture: browser',
|
||||||
|
|
@ -656,7 +656,7 @@ test('should show non-expect error in trace', async ({ runInlineTest }, testInfo
|
||||||
|
|
||||||
expect(result.exitCode).toBe(1);
|
expect(result.exitCode).toBe(1);
|
||||||
expect(result.failed).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([
|
expect(trace.actionTree).toEqual([
|
||||||
'Before Hooks',
|
'Before Hooks',
|
||||||
' fixture: browser',
|
' fixture: browser',
|
||||||
|
|
@ -692,7 +692,7 @@ test('should show error from beforeAll in trace', async ({ runInlineTest }, test
|
||||||
|
|
||||||
expect(result.exitCode).toBe(1);
|
expect(result.exitCode).toBe(1);
|
||||||
expect(result.failed).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!']);
|
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.exitCode).toBe(0);
|
||||||
expect(result.passed).toBe(1);
|
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"');
|
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.exitCode).toBe(0);
|
||||||
expect(result.passed).toBe(1);
|
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"`);
|
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.
|
// One screenshot for the page, no screenshot for pdf page since it should have failed.
|
||||||
expect(attachedScreenshots.length).toBe(1);
|
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.exitCode).toBe(0);
|
||||||
expect(result.passed).toBe(1);
|
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([
|
expect(trace.actionTree).toEqual([
|
||||||
'Before Hooks',
|
'Before Hooks',
|
||||||
' fixture: browser',
|
' fixture: browser',
|
||||||
|
|
@ -837,7 +837,7 @@ test('should not throw when merging traces multiple times', async ({ runInlineTe
|
||||||
|
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
expect(result.passed).toBe(1);
|
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) => {
|
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.exitCode).toBe(1);
|
||||||
expect(result.failed).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([
|
expect(trace.actionTree).toEqual([
|
||||||
'Before Hooks',
|
'Before Hooks',
|
||||||
' beforeAll hook',
|
' 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.exitCode).toBe(1);
|
||||||
expect(result.passed).toBe(1);
|
expect(result.passed).toBe(1);
|
||||||
expect(result.failed).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([
|
expect(trace1.actionTree).toEqual([
|
||||||
'Before Hooks',
|
'Before Hooks',
|
||||||
' fixture: foo',
|
' fixture: foo',
|
||||||
' step in foo setup',
|
' step in foo setup',
|
||||||
'After Hooks',
|
'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([
|
expect(trace2.actionTree).toEqual([
|
||||||
'Before Hooks',
|
'Before Hooks',
|
||||||
'After 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 });
|
}, { 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);
|
const retryTraceExists = fs.existsSync(retryTracePath);
|
||||||
expect(retryTraceExists).toBe(false);
|
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);
|
const trace = await parseTrace(tracePath);
|
||||||
expect(trace.apiNames).toContain('page.goto');
|
expect(trace.apiNames).toContain('page.goto');
|
||||||
expect(result.failed).toBe(1);
|
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' });
|
}, { 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);
|
const trace = await parseTrace(tracePath);
|
||||||
expect(trace.apiNames).toContain('page.goto');
|
expect(trace.apiNames).toContain('page.goto');
|
||||||
expect(result.failed).toBe(1);
|
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' });
|
}, { 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);
|
const trace = await parseTrace(tracePath);
|
||||||
expect(trace.apiNames).toContain('page.goto');
|
expect(trace.apiNames).toContain('page.goto');
|
||||||
expect(result.failed).toBe(1);
|
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' });
|
}, { 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);
|
const trace = await parseTrace(tracePath);
|
||||||
expect(trace.apiNames).toContain('apiRequestContext.get');
|
expect(trace.apiNames).toContain('apiRequestContext.get');
|
||||||
expect(result.failed).toBe(1);
|
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.exitCode).toBe(1);
|
||||||
expect(result.failed).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);
|
const trace = await parseTrace(tracePath);
|
||||||
expect(trace.actionTree).toEqual([
|
expect(trace.actionTree).toEqual([
|
||||||
'Before Hooks',
|
'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.exitCode).toBe(1);
|
||||||
expect(result.failed).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);
|
const trace = await parseTrace(tracePath);
|
||||||
expect(trace.actionTree).toEqual([
|
expect(trace.actionTree).toEqual([
|
||||||
'Before Hooks',
|
'Before Hooks',
|
||||||
|
|
@ -1204,7 +1204,7 @@ test('should not nest top level expect into unfinished api calls ', {
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
expect(result.failed).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);
|
const trace = await parseTrace(tracePath);
|
||||||
expect(trace.actionTree).toEqual([
|
expect(trace.actionTree).toEqual([
|
||||||
'Before Hooks',
|
'Before Hooks',
|
||||||
|
|
@ -1246,7 +1246,7 @@ test('should record trace after fixture teardown timeout', {
|
||||||
expect(result.exitCode).toBe(1);
|
expect(result.exitCode).toBe(1);
|
||||||
expect(result.failed).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);
|
const trace = await parseTrace(tracePath);
|
||||||
expect(trace.actionTree).toEqual([
|
expect(trace.actionTree).toEqual([
|
||||||
'Before Hooks',
|
'Before Hooks',
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ test('render trace attachment', async ({ runInlineTest }) => {
|
||||||
test('one', async ({}, testInfo) => {
|
test('one', async ({}, testInfo) => {
|
||||||
testInfo.attachments.push({
|
testInfo.attachments.push({
|
||||||
name: 'trace',
|
name: 'trace',
|
||||||
path: testInfo.outputPath('my dir with space', 'trace.zip'),
|
path: testInfo.outputPath('my dir with space', 'trace.pwtrace.zip'),
|
||||||
contentType: 'application/zip'
|
contentType: 'application/zip'
|
||||||
});
|
});
|
||||||
expect(1).toBe(0);
|
expect(1).toBe(0);
|
||||||
|
|
@ -75,8 +75,8 @@ test('render trace attachment', async ({ runInlineTest }) => {
|
||||||
}, { reporter: 'line' });
|
}, { reporter: 'line' });
|
||||||
const text = result.output.replace(/\\/g, '/');
|
const text = result.output.replace(/\\/g, '/');
|
||||||
expect(text).toContain(' attachment #1: trace (application/zip) ─────────────────────────────────────────────────────────');
|
expect(text).toContain(' attachment #1: trace (application/zip) ─────────────────────────────────────────────────────────');
|
||||||
expect(text).toContain(' 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.zip"');
|
expect(text).toContain('npx playwright show-trace "test-results/a-one/my dir with space/trace.pwtrace.zip"');
|
||||||
expect(text).toContain(' ────────────────────────────────────────────────────────────────────────────────────────────────');
|
expect(text).toContain(' ────────────────────────────────────────────────────────────────────────────────────────────────');
|
||||||
expect(result.exitCode).toBe(1);
|
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
|
pw:api | page.goto(about:blank) @ a.test.ts:7
|
||||||
test.step | inner step attempt: 0 @ a.test.ts:8
|
test.step | inner step attempt: 0 @ a.test.ts:8
|
||||||
expect | expect.toBe @ a.test.ts:10
|
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
|
pw:api | page.goto(about:blank) @ a.test.ts:7
|
||||||
test.step | inner step attempt: 1 @ a.test.ts:8
|
test.step | inner step attempt: 1 @ a.test.ts:8
|
||||||
expect | expect.toBe @ a.test.ts:10
|
expect | expect.toBe @ a.test.ts:10
|
||||||
|
expect | expect.toHaveLength @ a.test.ts:6
|
||||||
hook |After Hooks
|
hook |After Hooks
|
||||||
fixture | fixture: page
|
fixture | fixture: page
|
||||||
fixture | fixture: context
|
fixture | fixture: context
|
||||||
|
|
@ -1036,9 +1039,12 @@ expect |expect.poll.toBe @ a.test.ts:13
|
||||||
expect | expect.toHaveText @ a.test.ts:7
|
expect | expect.toHaveText @ a.test.ts:7
|
||||||
test.step | iteration 1 @ a.test.ts:9
|
test.step | iteration 1 @ a.test.ts:9
|
||||||
expect | expect.toBeVisible @ a.test.ts:10
|
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
|
expect | expect.toHaveText @ a.test.ts:7
|
||||||
test.step | iteration 2 @ a.test.ts:9
|
test.step | iteration 2 @ a.test.ts:9
|
||||||
expect | expect.toBeVisible @ a.test.ts:10
|
expect | expect.toBeVisible @ a.test.ts:10
|
||||||
|
expect | expect.toBe @ a.test.ts:6
|
||||||
hook |After Hooks
|
hook |After Hooks
|
||||||
fixture | fixture: page
|
fixture | fixture: page
|
||||||
fixture | fixture: context
|
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({
|
const testProcess = await runWatchTest({
|
||||||
'a.test.ts': `
|
'a.test.ts': `
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
test('passes', () => {});
|
test('passes', () => {});
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
|
||||||
await testProcess.waitForOutput('Waiting for file changes.');
|
await testProcess.waitForOutput('Waiting for file changes.');
|
||||||
|
|
||||||
|
expect(testProcess.output).not.toContain('a.test.ts');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should quit on Q', async ({ runWatchTest }) => {
|
test('should quit on Q', async ({ runWatchTest }) => {
|
||||||
|
|
@ -206,7 +207,6 @@ test('should run tests on Enter', async ({ runWatchTest }) => {
|
||||||
test('passes', () => {});
|
test('passes', () => {});
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
|
||||||
await testProcess.waitForOutput('Waiting for file changes.');
|
await testProcess.waitForOutput('Waiting for file changes.');
|
||||||
testProcess.clearOutput();
|
testProcess.clearOutput();
|
||||||
testProcess.write('\r\n');
|
testProcess.write('\r\n');
|
||||||
|
|
@ -222,7 +222,6 @@ test('should run tests on R', async ({ runWatchTest }) => {
|
||||||
test('passes', () => {});
|
test('passes', () => {});
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
|
||||||
await testProcess.waitForOutput('Waiting for file changes.');
|
await testProcess.waitForOutput('Waiting for file changes.');
|
||||||
testProcess.clearOutput();
|
testProcess.clearOutput();
|
||||||
testProcess.write('r');
|
testProcess.write('r');
|
||||||
|
|
@ -246,6 +245,10 @@ test('should run failed tests on F', async ({ runWatchTest }) => {
|
||||||
test('fails', () => { expect(1).toBe(2); });
|
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('a.test.ts:3:11 › passes');
|
||||||
await testProcess.waitForOutput('b.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('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.');
|
await testProcess.waitForOutput('Waiting for file changes.');
|
||||||
testProcess.clearOutput();
|
testProcess.clearOutput();
|
||||||
testProcess.write('f');
|
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');
|
await testProcess.waitForOutput('c.test.ts:3:11 › fails');
|
||||||
expect(testProcess.output).not.toContain('a.test.ts:3:11');
|
expect(testProcess.output).not.toContain('a.test.ts:3:11');
|
||||||
});
|
});
|
||||||
|
|
@ -269,8 +272,6 @@ test('should respect file filter P', async ({ runWatchTest }) => {
|
||||||
test('passes', () => {});
|
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.');
|
await testProcess.waitForOutput('Waiting for file changes.');
|
||||||
testProcess.clearOutput();
|
testProcess.clearOutput();
|
||||||
testProcess.write('p');
|
testProcess.write('p');
|
||||||
|
|
@ -294,6 +295,11 @@ test('should respect project filter C', async ({ runWatchTest, writeFiles }) =>
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
const testProcess = await runWatchTest(files, { project: 'foo' });
|
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('[foo] › a.test.ts:3:11 › passes');
|
||||||
await testProcess.waitForOutput('Waiting for file changes.');
|
await testProcess.waitForOutput('Waiting for file changes.');
|
||||||
testProcess.clearOutput();
|
testProcess.clearOutput();
|
||||||
|
|
@ -303,7 +309,7 @@ test('should respect project filter C', async ({ runWatchTest, writeFiles }) =>
|
||||||
await testProcess.waitForOutput('bar');
|
await testProcess.waitForOutput('bar');
|
||||||
testProcess.write(' ');
|
testProcess.write(' ');
|
||||||
testProcess.write('\r\n');
|
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');
|
await testProcess.waitForOutput('[foo] › a.test.ts:3:11 › passes');
|
||||||
expect(testProcess.output).not.toContain('[bar] › 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', () => {});
|
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.');
|
await testProcess.waitForOutput('Waiting for file changes.');
|
||||||
testProcess.clearOutput();
|
testProcess.clearOutput();
|
||||||
testProcess.write('p');
|
testProcess.write('p');
|
||||||
|
|
@ -353,8 +357,6 @@ test('should respect title filter T', async ({ runWatchTest }) => {
|
||||||
test('title 2', () => {});
|
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.');
|
await testProcess.waitForOutput('Waiting for file changes.');
|
||||||
testProcess.clearOutput();
|
testProcess.clearOutput();
|
||||||
testProcess.write('t');
|
testProcess.write('t');
|
||||||
|
|
@ -381,6 +383,11 @@ test('should re-run failed tests on F > R', async ({ runWatchTest }) => {
|
||||||
test('fails', () => { expect(1).toBe(2); });
|
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('a.test.ts:3:11 › passes');
|
||||||
await testProcess.waitForOutput('b.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('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.');
|
await testProcess.waitForOutput('Waiting for file changes.');
|
||||||
testProcess.clearOutput();
|
testProcess.clearOutput();
|
||||||
testProcess.write('f');
|
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');
|
await testProcess.waitForOutput('c.test.ts:3:11 › fails');
|
||||||
expect(testProcess.output).not.toContain('a.test.ts:3:11');
|
expect(testProcess.output).not.toContain('a.test.ts:3:11');
|
||||||
testProcess.clearOutput();
|
testProcess.clearOutput();
|
||||||
testProcess.write('r');
|
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');
|
await testProcess.waitForOutput('c.test.ts:3:11 › fails');
|
||||||
expect(testProcess.output).not.toContain('a.test.ts:3:11');
|
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); });
|
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.');
|
await testProcess.waitForOutput('Waiting for file changes.');
|
||||||
testProcess.clearOutput();
|
testProcess.clearOutput();
|
||||||
await writeFiles({
|
await writeFiles({
|
||||||
|
|
@ -457,9 +460,6 @@ test('should run on changed deps', async ({ runWatchTest, writeFiles }) => {
|
||||||
console.log('old helper');
|
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.');
|
await testProcess.waitForOutput('Waiting for file changes.');
|
||||||
testProcess.clearOutput();
|
testProcess.clearOutput();
|
||||||
await writeFiles({
|
await writeFiles({
|
||||||
|
|
@ -490,9 +490,6 @@ test('should run on changed deps in ESM', async ({ runWatchTest, writeFiles }) =
|
||||||
console.log('old helper');
|
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.');
|
await testProcess.waitForOutput('Waiting for file changes.');
|
||||||
testProcess.clearOutput();
|
testProcess.clearOutput();
|
||||||
await writeFiles({
|
await writeFiles({
|
||||||
|
|
@ -521,10 +518,6 @@ test('should re-run changed files on R', async ({ runWatchTest, writeFiles }) =>
|
||||||
test('fails', () => { expect(1).toBe(2); });
|
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.');
|
await testProcess.waitForOutput('Waiting for file changes.');
|
||||||
testProcess.clearOutput();
|
testProcess.clearOutput();
|
||||||
await writeFiles({
|
await writeFiles({
|
||||||
|
|
@ -556,8 +549,6 @@ test('should not trigger on changes to non-tests', async ({ runWatchTest, writeF
|
||||||
test('passes', () => {});
|
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.');
|
await testProcess.waitForOutput('Waiting for file changes.');
|
||||||
|
|
||||||
testProcess.clearOutput();
|
testProcess.clearOutput();
|
||||||
|
|
@ -582,6 +573,10 @@ test('should only watch selected projects', async ({ runWatchTest, writeFiles })
|
||||||
test('passes', () => {});
|
test('passes', () => {});
|
||||||
`,
|
`,
|
||||||
}, undefined, undefined, { additionalArgs: ['--project=foo'] });
|
}, 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('npx playwright test --project foo');
|
||||||
await testProcess.waitForOutput('[foo] › a.test.ts:3:11 › passes');
|
await testProcess.waitForOutput('[foo] › a.test.ts:3:11 › passes');
|
||||||
expect(testProcess.output).not.toContain('[bar]');
|
expect(testProcess.output).not.toContain('[bar]');
|
||||||
|
|
@ -612,6 +607,10 @@ test('should watch filtered files', async ({ runWatchTest, writeFiles }) => {
|
||||||
test('passes', () => {});
|
test('passes', () => {});
|
||||||
`,
|
`,
|
||||||
}, undefined, undefined, { additionalArgs: ['a.test.ts'] });
|
}, 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('npx playwright test a.test.ts');
|
||||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||||
expect(testProcess.output).not.toContain('b.test');
|
expect(testProcess.output).not.toContain('b.test');
|
||||||
|
|
@ -640,6 +639,10 @@ test('should not watch unfiltered files', async ({ runWatchTest, writeFiles }) =
|
||||||
test('passes', () => {});
|
test('passes', () => {});
|
||||||
`,
|
`,
|
||||||
}, undefined, undefined, { additionalArgs: ['a.test.ts'] });
|
}, 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('npx playwright test a.test.ts');
|
||||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||||
expect(testProcess.output).not.toContain('b.test');
|
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.');
|
await testProcess.waitForOutput('Waiting for file changes.');
|
||||||
testProcess.clearOutput();
|
|
||||||
await writeFiles({
|
await writeFiles({
|
||||||
'src/button.tsx': `
|
'src/button.tsx': `
|
||||||
export const Button = () => <button>Button 2</button>;
|
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.');
|
await testProcess.waitForOutput('Waiting for file changes.');
|
||||||
testProcess.clearOutput();
|
|
||||||
await writeFiles({
|
await writeFiles({
|
||||||
'src/button.css': `
|
'src/button.css': `
|
||||||
button { color: blue; }
|
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.');
|
await testProcess.waitForOutput('Waiting for file changes.');
|
||||||
testProcess.clearOutput();
|
|
||||||
await writeFiles({
|
await writeFiles({
|
||||||
'src/button.css': `
|
'src/button.css': `
|
||||||
button { color: blue; }
|
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('a.test.ts:3:11 › passes');
|
||||||
await testProcess.waitForOutput('Waiting for file changes.');
|
await testProcess.waitForOutput('Waiting for file changes.');
|
||||||
testProcess.write('\x1B');
|
testProcess.write('\x1B');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue