Compare commits
36 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1c861cfa7 | ||
|
|
20b0788101 | ||
|
|
57c324002a | ||
|
|
301f179735 | ||
|
|
b2d6a0916e | ||
|
|
7cf7aec97f | ||
|
|
d78ae0179d | ||
|
|
bd13da4132 | ||
|
|
30684a77e7 | ||
|
|
5e68061d49 | ||
|
|
99a3631057 | ||
|
|
929fef348e | ||
|
|
cf31aa8b4c | ||
|
|
ed9b4d9b9a | ||
|
|
fca1fa0b95 | ||
|
|
ff11273c7b | ||
|
|
4953ac3072 | ||
|
|
4c66f8aeda | ||
|
|
deba37b6b5 | ||
|
|
2cfe733e30 | ||
|
|
5fdf97658e | ||
|
|
29ba72c06b | ||
|
|
b20e154902 | ||
|
|
876e0e4ba9 | ||
|
|
3ab19c6229 | ||
|
|
8d35c1b517 | ||
|
|
71b8e22501 | ||
|
|
71e5eade8c | ||
|
|
7ff46d4596 | ||
|
|
ca9ddff7ca | ||
|
|
dfecfa5be1 | ||
|
|
468b9b1e7a | ||
|
|
64e4a9b0eb | ||
|
|
446de523c4 | ||
|
|
2ea14ca2c4 | ||
|
|
185a2867c6 |
|
|
@ -1,6 +1,6 @@
|
||||||
# 🎭 Playwright
|
# 🎭 Playwright
|
||||||
|
|
||||||
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop -->
|
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop -->
|
||||||
|
|
||||||
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
|
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
|
||||||
|
|
||||||
|
|
@ -8,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
|
||||||
|
|
||||||
| | Linux | macOS | Windows |
|
| | Linux | macOS | Windows |
|
||||||
| :--- | :---: | :---: | :---: |
|
| :--- | :---: | :---: | :---: |
|
||||||
| Chromium <!-- GEN:chromium-version -->128.0.6613.7<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
| Chromium <!-- GEN:chromium-version -->128.0.6613.18<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||||
| WebKit <!-- GEN:webkit-version -->18.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
| WebKit <!-- GEN:webkit-version -->18.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||||
| Firefox <!-- GEN:firefox-version -->128.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
| Firefox <!-- GEN:firefox-version -->128.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -180,6 +180,9 @@ context cookies from the response. The method will automatically follow redirect
|
||||||
### option: APIRequestContext.delete.maxRedirects = %%-js-python-csharp-fetch-option-maxredirects-%%
|
### option: APIRequestContext.delete.maxRedirects = %%-js-python-csharp-fetch-option-maxredirects-%%
|
||||||
* since: v1.26
|
* since: v1.26
|
||||||
|
|
||||||
|
### option: APIRequestContext.delete.maxRetries = %%-js-python-csharp-fetch-option-maxretries-%%
|
||||||
|
* since: v1.46
|
||||||
|
|
||||||
## async method: APIRequestContext.dispose
|
## async method: APIRequestContext.dispose
|
||||||
* since: v1.16
|
* since: v1.16
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -503,6 +503,12 @@ If set changes the request URL. New URL must have same protocol as original one.
|
||||||
Maximum number of request redirects that will be followed automatically. An error will be thrown if the number is exceeded.
|
Maximum number of request redirects that will be followed automatically. An error will be thrown if the number is exceeded.
|
||||||
Defaults to `20`. Pass `0` to not follow redirects.
|
Defaults to `20`. Pass `0` to not follow redirects.
|
||||||
|
|
||||||
|
### option: Route.fetch.maxRetries
|
||||||
|
* since: v1.46
|
||||||
|
- `maxRetries` <[int]>
|
||||||
|
|
||||||
|
Maximum number of times network errors should be retried. Currently only `ECONNRESET` error is retried. Does not retry based on HTTP response codes. An error will be thrown if the limit is exceeded. Defaults to `0` - no retries.
|
||||||
|
|
||||||
### option: Route.fetch.timeout
|
### option: Route.fetch.timeout
|
||||||
* since: v1.33
|
* since: v1.33
|
||||||
- `timeout` <[float]>
|
- `timeout` <[float]>
|
||||||
|
|
|
||||||
|
|
@ -524,9 +524,9 @@ Does not enforce fixed viewport, allows resizing window in the headed mode.
|
||||||
## context-option-clientCertificates
|
## context-option-clientCertificates
|
||||||
- `clientCertificates` <[Array]<[Object]>>
|
- `clientCertificates` <[Array]<[Object]>>
|
||||||
- `origin` <[string]> Exact origin that the certificate is valid for. Origin includes `https` protocol, a hostname and optionally a port.
|
- `origin` <[string]> Exact origin that the certificate is valid for. Origin includes `https` protocol, a hostname and optionally a port.
|
||||||
- `certPath` ?<[string]> Path to the file with the certificate in PEM format.
|
- `certPath` ?<[path]> Path to the file with the certificate in PEM format.
|
||||||
- `keyPath` ?<[string]> Path to the file with the private key in PEM format.
|
- `keyPath` ?<[path]> Path to the file with the private key in PEM format.
|
||||||
- `pfxPath` ?<[string]> Path to the PFX or PKCS12 encoded private key and certificate chain.
|
- `pfxPath` ?<[path]> Path to the PFX or PKCS12 encoded private key and certificate chain.
|
||||||
- `passphrase` ?<[string]> Passphrase for the private key (PEM or PFX).
|
- `passphrase` ?<[string]> Passphrase for the private key (PEM or PFX).
|
||||||
|
|
||||||
TLS Client Authentication allows the server to request a client certificate and verify it.
|
TLS Client Authentication allows the server to request a client certificate and verify it.
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,50 @@ title: "Release notes"
|
||||||
toc_max_heading_level: 2
|
toc_max_heading_level: 2
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Version 1.46
|
||||||
|
|
||||||
|
### TLS Client Certificates
|
||||||
|
|
||||||
|
Playwright now allows to supply client-side certificates, so that server can verify them, as specified by TLS Client Authentication.
|
||||||
|
|
||||||
|
You can provide client certificates as a parameter of [`method: Browser.newContext`] and [`method: APIRequest.newContext`]. The following snippet sets up a client certificate for `https://example.com`:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var context = await Browser.NewContextAsync(new() {
|
||||||
|
ClientCertificates = [
|
||||||
|
new() {
|
||||||
|
Origin = "https://example.com",
|
||||||
|
CertPath = "client-certificates/cert.pem",
|
||||||
|
KeyPath = "client-certificates/key.pem",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Trace Viewer Updates
|
||||||
|
|
||||||
|
- Content of text attachments is now rendered inline in the attachments pane.
|
||||||
|
- New setting to show/hide routing actions like [`method: Route.continue`].
|
||||||
|
- Request method and status are shown in the network details tab.
|
||||||
|
- New button to copy source file location to clipboard.
|
||||||
|
- Metadata pane now displays the `BaseURL`.
|
||||||
|
|
||||||
|
### Miscellaneous
|
||||||
|
|
||||||
|
- New `maxRetries` option in [`method: APIRequestContext.fetch`] which retries on the `ECONNRESET` network error.
|
||||||
|
|
||||||
|
### Browser Versions
|
||||||
|
|
||||||
|
- Chromium 128.0.6613.18
|
||||||
|
- Mozilla Firefox 128.0
|
||||||
|
- WebKit 18.0
|
||||||
|
|
||||||
|
This version was also tested against the following stable channels:
|
||||||
|
|
||||||
|
- Google Chrome 127
|
||||||
|
- Microsoft Edge 127
|
||||||
|
|
||||||
|
|
||||||
## Version 1.45
|
## Version 1.45
|
||||||
|
|
||||||
### Clock
|
### Clock
|
||||||
|
|
@ -112,7 +156,6 @@ await Page.RemoveLocatorHandlerAsync(locator);
|
||||||
**Miscellaneous options**
|
**Miscellaneous options**
|
||||||
|
|
||||||
- New method [`method: FormData.append`] allows to specify repeating fields with the same name in [`Multipart`](./api/class-apirequestcontext#api-request-context-fetch-option-multipart) option in `APIRequestContext.FetchAsync()`:
|
- New method [`method: FormData.append`] allows to specify repeating fields with the same name in [`Multipart`](./api/class-apirequestcontext#api-request-context-fetch-option-multipart) option in `APIRequestContext.FetchAsync()`:
|
||||||
- ```
|
|
||||||
```csharp
|
```csharp
|
||||||
var formData = Context.APIRequest.CreateFormData();
|
var formData = Context.APIRequest.CreateFormData();
|
||||||
formData.Append("file", new FilePayload()
|
formData.Append("file", new FilePayload()
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,45 @@ title: "Release notes"
|
||||||
toc_max_heading_level: 2
|
toc_max_heading_level: 2
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Version 1.46
|
||||||
|
|
||||||
|
### TLS Client Certificates
|
||||||
|
|
||||||
|
Playwright now allows to supply client-side certificates, so that server can verify them, as specified by TLS Client Authentication.
|
||||||
|
|
||||||
|
You can provide client certificates as a parameter of [`method: Browser.newContext`] and [`method: APIRequest.newContext`]. The following snippet sets up a client certificate for `https://example.com`:
|
||||||
|
|
||||||
|
```java
|
||||||
|
BrowserContext context = browser.newContext(new Browser.NewContextOptions()
|
||||||
|
.setClientCertificates(asList(new ClientCertificate("https://example.com")
|
||||||
|
.setCertPath(Paths.get("client-certificates/cert.pem"))
|
||||||
|
.setKeyPath(Paths.get("client-certificates/key.pem")))));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Trace Viewer Updates
|
||||||
|
|
||||||
|
- Content of text attachments is now rendered inline in the attachments pane.
|
||||||
|
- New setting to show/hide routing actions like [`method: Route.continue`].
|
||||||
|
- Request method and status are shown in the network details tab.
|
||||||
|
- New button to copy source file location to clipboard.
|
||||||
|
- Metadata pane now displays the `baseURL`.
|
||||||
|
|
||||||
|
### Miscellaneous
|
||||||
|
|
||||||
|
- New `maxRetries` option in [`method: APIRequestContext.fetch`] which retries on the `ECONNRESET` network error.
|
||||||
|
|
||||||
|
### Browser Versions
|
||||||
|
|
||||||
|
- Chromium 128.0.6613.18
|
||||||
|
- Mozilla Firefox 128.0
|
||||||
|
- WebKit 18.0
|
||||||
|
|
||||||
|
This version was also tested against the following stable channels:
|
||||||
|
|
||||||
|
- Google Chrome 127
|
||||||
|
- Microsoft Edge 127
|
||||||
|
|
||||||
|
|
||||||
## Version 1.45
|
## Version 1.45
|
||||||
|
|
||||||
### Clock
|
### Clock
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,15 @@ import LiteYouTube from '@site/src/components/LiteYouTube';
|
||||||
|
|
||||||
## Version 1.46
|
## Version 1.46
|
||||||
|
|
||||||
|
<LiteYouTube
|
||||||
|
id="tQo7w-QQBsI"
|
||||||
|
title="Playwright 1.46"
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
### TLS Client Certificates
|
### TLS Client Certificates
|
||||||
|
|
||||||
Playwright now allows to supply client-side certificates, so that server can verify them, as specified by TLS Client Authentication.
|
Playwright now allows you to supply client-side certificates, so that server can verify them, as specified by TLS Client Authentication.
|
||||||
|
|
||||||
The following snippet sets up a client certificate for `https://example.com`:
|
The following snippet sets up a client certificate for `https://example.com`:
|
||||||
|
|
||||||
|
|
@ -33,6 +39,18 @@ export default defineConfig({
|
||||||
|
|
||||||
You can also provide client certificates to a particular [test project](./api/class-testproject#test-project-use) or as a parameter of [`method: Browser.newContext`] and [`method: APIRequest.newContext`].
|
You can also provide client certificates to a particular [test project](./api/class-testproject#test-project-use) or as a parameter of [`method: Browser.newContext`] and [`method: APIRequest.newContext`].
|
||||||
|
|
||||||
|
### `--only-changed` cli option
|
||||||
|
|
||||||
|
New CLI option `--only-changed` allows to only run test files that have been changed since the last git commit or from a specific git "ref".
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Only run test files with uncommitted changes
|
||||||
|
npx playwright test --only-changed
|
||||||
|
|
||||||
|
# Only run test files changed relative to the "main" branch
|
||||||
|
npx playwrigh test --only-changed=main
|
||||||
|
```
|
||||||
|
|
||||||
### Component Testing: New `router` fixture
|
### Component Testing: New `router` fixture
|
||||||
|
|
||||||
This release introduces an experimental `router` fixture to intercept and handle network requests in component testing.
|
This release introduces an experimental `router` fixture to intercept and handle network requests in component testing.
|
||||||
|
|
@ -59,29 +77,24 @@ test('example test', async ({ mount }) => {
|
||||||
|
|
||||||
This fixture is only available in [component tests](./test-components#handling-network-requests).
|
This fixture is only available in [component tests](./test-components#handling-network-requests).
|
||||||
|
|
||||||
### Test runner
|
|
||||||
|
|
||||||
- New CLI option `--only-changed` to only run test files that have been changed since the last commit or from a specific git "ref".
|
|
||||||
- New option to [box a fixture](./test-fixtures#box-fixtures) to minimize the fixture exposure in test reports and error messages.
|
|
||||||
- New option to provide a [custom fixture title](./test-fixtures#custom-fixture-title) to be used in test reports and error messages.
|
|
||||||
|
|
||||||
### UI Mode / Trace Viewer Updates
|
### UI Mode / Trace Viewer Updates
|
||||||
|
|
||||||
- New testing options pane in the UI mode to control test execution, for example "single worker" or "headed browser".
|
- Test annotations are now shown in UI mode.
|
||||||
- New setting to show/hide routing actions like `route.continue`.
|
- Content of text attachments is now rendered inline in the attachments pane.
|
||||||
|
- New setting to show/hide routing actions like [`method: Route.continue`].
|
||||||
- Request method and status are shown in the network details tab.
|
- Request method and status are shown in the network details tab.
|
||||||
- New button to copy source file location to clipboard.
|
- New button to copy source file location to clipboard.
|
||||||
- Content of text attachments is now rendered inline in the attachments pane.
|
|
||||||
- Metadata pane now displays the `baseURL`.
|
- Metadata pane now displays the `baseURL`.
|
||||||
|
|
||||||
### Miscellaneous
|
### Miscellaneous
|
||||||
|
|
||||||
- New `maxRetries` option in [`method: APIRequestContext.fetch`] which retries on the `ECONNRESET` network error.
|
- New `maxRetries` option in [`method: APIRequestContext.fetch`] which retries on the `ECONNRESET` network error.
|
||||||
- Improved link rendering inside annotations and attachments in the html report.
|
- New option to [box a fixture](./test-fixtures#box-fixtures) to minimize the fixture exposure in test reports and error messages.
|
||||||
|
- New option to provide a [custom fixture title](./test-fixtures#custom-fixture-title) to be used in test reports and error messages.
|
||||||
|
|
||||||
### Browser Versions
|
### Browser Versions
|
||||||
|
|
||||||
- Chromium 128.0.6613.7
|
- Chromium 128.0.6613.18
|
||||||
- Mozilla Firefox 128.0
|
- Mozilla Firefox 128.0
|
||||||
- WebKit 18.0
|
- WebKit 18.0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,50 @@ title: "Release notes"
|
||||||
toc_max_heading_level: 2
|
toc_max_heading_level: 2
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Version 1.46
|
||||||
|
|
||||||
|
### TLS Client Certificates
|
||||||
|
|
||||||
|
Playwright now allows to supply client-side certificates, so that server can verify them, as specified by TLS Client Authentication.
|
||||||
|
|
||||||
|
You can provide client certificates as a parameter of [`method: Browser.newContext`] and [`method: APIRequest.newContext`]. The following snippet sets up a client certificate for `https://example.com`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
context = browser.new_context(
|
||||||
|
client_certificates=[
|
||||||
|
{
|
||||||
|
"origin": "https://example.com",
|
||||||
|
"certPath": "client-certificates/cert.pem",
|
||||||
|
"keyPath": "client-certificates/key.pem",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Trace Viewer Updates
|
||||||
|
|
||||||
|
- Content of text attachments is now rendered inline in the attachments pane.
|
||||||
|
- New setting to show/hide routing actions like [`method: Route.continue`].
|
||||||
|
- Request method and status are shown in the network details tab.
|
||||||
|
- New button to copy source file location to clipboard.
|
||||||
|
- Metadata pane now displays the `base_url`.
|
||||||
|
|
||||||
|
### Miscellaneous
|
||||||
|
|
||||||
|
- New `maxRetries` option in [`method: APIRequestContext.fetch`] which retries on the `ECONNRESET` network error.
|
||||||
|
|
||||||
|
### Browser Versions
|
||||||
|
|
||||||
|
- Chromium 128.0.6613.18
|
||||||
|
- Mozilla Firefox 128.0
|
||||||
|
- WebKit 18.0
|
||||||
|
|
||||||
|
This version was also tested against the following stable channels:
|
||||||
|
|
||||||
|
- Google Chrome 127
|
||||||
|
- Microsoft Edge 127
|
||||||
|
|
||||||
|
|
||||||
## Version 1.45
|
## Version 1.45
|
||||||
|
|
||||||
### Clock
|
### Clock
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ Playwright Trace Viewer is a GUI tool that lets you explore recorded Playwright
|
||||||
- [How to open and view the trace](/trace-viewer-intro.md#opening-the-trace)
|
- [How to open and view the trace](/trace-viewer-intro.md#opening-the-trace)
|
||||||
|
|
||||||
<LiteYouTube
|
<LiteYouTube
|
||||||
id="lfxjs--9ZQs"
|
id="yP6AnTxC34s"
|
||||||
title="Viewing Playwright Traces"
|
title="Viewing Playwright Traces"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,14 +13,14 @@ Playwright Trace Viewer is a GUI tool that helps you explore recorded Playwright
|
||||||
* langs: js
|
* langs: js
|
||||||
|
|
||||||
<LiteYouTube
|
<LiteYouTube
|
||||||
id="lfxjs--9ZQs"
|
id="yP6AnTxC34s"
|
||||||
title="Viewing Playwright Traces"
|
title="Viewing Playwright Traces"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
## Trace Viewer features
|
## Trace Viewer features
|
||||||
### Actions
|
### Actions
|
||||||
|
|
||||||
In the Actions tab you can see what locator was used for every action and how long each one took to run. Hover over each action of your test and visually see the change in the DOM snapshot. Go back and forward in time and click an action to inspect and debug. Use the Before and After tabs to visually see what happened before and after the action.
|
In the Actions tab you can see what locator was used for every action and how long each one took to run. Hover over each action of your test and visually see the change in the DOM snapshot. Go back and forward in time and click an action to inspect and debug. Use the Before and After tabs to visually see what happened before and after the action.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
@ -31,7 +31,7 @@ In the Actions tab you can see what locator was used for every action and how lo
|
||||||
|
|
||||||
### Screenshots
|
### Screenshots
|
||||||
|
|
||||||
When tracing with the [`option: screenshots`] option turned on (default), each trace records a screencast and renders it as a film strip. You can hover over the film strip to see a magnified image of for each action and state which helps you easily find the action you want to inspect.
|
When tracing with the [`option: screenshots`] option turned on (default), each trace records a screencast and renders it as a film strip. You can hover over the film strip to see a magnified image of for each action and state which helps you easily find the action you want to inspect.
|
||||||
|
|
||||||
Double click on an action to see the time range for that action. You can use the slider in the timeline to increase the actions selected and these will be shown in the Actions tab and all console logs and network logs will be filtered to only show the logs for the actions selected.
|
Double click on an action to see the time range for that action. You can use the slider in the timeline to increase the actions selected and these will be shown in the Actions tab and all console logs and network logs will be filtered to only show the logs for the actions selected.
|
||||||
|
|
||||||
|
|
@ -393,7 +393,7 @@ public class ExampleTest : PageTest
|
||||||
[TearDown]
|
[TearDown]
|
||||||
public async Task TearDown()
|
public async Task TearDown()
|
||||||
{
|
{
|
||||||
var failed = TestContext.CurrentContext.Result.Outcome == NUnit.Framework.Interfaces.ResultState.Error
|
var failed = TestContext.CurrentContext.Result.Outcome == NUnit.Framework.Interfaces.ResultState.Error
|
||||||
|| TestContext.CurrentContext.Result.Outcome == NUnit.Framework.Interfaces.ResultState.Failure;
|
|| TestContext.CurrentContext.Result.Outcome == NUnit.Framework.Interfaces.ResultState.Failure;
|
||||||
|
|
||||||
await Context.Tracing.StopAsync(new()
|
await Context.Tracing.StopAsync(new()
|
||||||
|
|
@ -402,7 +402,7 @@ public class ExampleTest : PageTest
|
||||||
TestContext.CurrentContext.WorkDirectory,
|
TestContext.CurrentContext.WorkDirectory,
|
||||||
"playwright-traces",
|
"playwright-traces",
|
||||||
$"{TestContext.CurrentContext.Test.ClassName}.{TestContext.CurrentContext.Test.Name}.zip"
|
$"{TestContext.CurrentContext.Test.ClassName}.{TestContext.CurrentContext.Test.Name}.zip"
|
||||||
) : null,
|
) : null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
68
package-lock.json
generated
68
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "playwright-internal",
|
"name": "playwright-internal",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "playwright-internal",
|
"name": "playwright-internal",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
@ -7719,10 +7719,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/playwright": {
|
"packages/playwright": {
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.46.0-next"
|
"playwright-core": "1.46.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
|
|
@ -7736,11 +7736,11 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-browser-chromium": {
|
"packages/playwright-browser-chromium": {
|
||||||
"name": "@playwright/browser-chromium",
|
"name": "@playwright/browser-chromium",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.1",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.46.0-next"
|
"playwright-core": "1.46.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
|
|
@ -7748,11 +7748,11 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-browser-firefox": {
|
"packages/playwright-browser-firefox": {
|
||||||
"name": "@playwright/browser-firefox",
|
"name": "@playwright/browser-firefox",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.1",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.46.0-next"
|
"playwright-core": "1.46.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
|
|
@ -7760,22 +7760,22 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-browser-webkit": {
|
"packages/playwright-browser-webkit": {
|
||||||
"name": "@playwright/browser-webkit",
|
"name": "@playwright/browser-webkit",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.1",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.46.0-next"
|
"playwright-core": "1.46.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/playwright-chromium": {
|
"packages/playwright-chromium": {
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.1",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.46.0-next"
|
"playwright-core": "1.46.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
|
|
@ -7785,7 +7785,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/playwright-core": {
|
"packages/playwright-core": {
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright-core": "cli.js"
|
"playwright-core": "cli.js"
|
||||||
|
|
@ -7796,11 +7796,11 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-ct-core": {
|
"packages/playwright-ct-core": {
|
||||||
"name": "@playwright/experimental-ct-core",
|
"name": "@playwright/experimental-ct-core",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright": "1.46.0-next",
|
"playwright": "1.46.1",
|
||||||
"playwright-core": "1.46.0-next",
|
"playwright-core": "1.46.1",
|
||||||
"vite": "^5.2.8"
|
"vite": "^5.2.8"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -7809,10 +7809,10 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-ct-react": {
|
"packages/playwright-ct-react": {
|
||||||
"name": "@playwright/experimental-ct-react",
|
"name": "@playwright/experimental-ct-react",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.46.0-next",
|
"@playwright/experimental-ct-core": "1.46.1",
|
||||||
"@vitejs/plugin-react": "^4.2.1"
|
"@vitejs/plugin-react": "^4.2.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -7824,10 +7824,10 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-ct-react17": {
|
"packages/playwright-ct-react17": {
|
||||||
"name": "@playwright/experimental-ct-react17",
|
"name": "@playwright/experimental-ct-react17",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.46.0-next",
|
"@playwright/experimental-ct-core": "1.46.1",
|
||||||
"@vitejs/plugin-react": "^4.2.1"
|
"@vitejs/plugin-react": "^4.2.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -7839,10 +7839,10 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-ct-solid": {
|
"packages/playwright-ct-solid": {
|
||||||
"name": "@playwright/experimental-ct-solid",
|
"name": "@playwright/experimental-ct-solid",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.46.0-next",
|
"@playwright/experimental-ct-core": "1.46.1",
|
||||||
"vite-plugin-solid": "^2.7.0"
|
"vite-plugin-solid": "^2.7.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -7857,10 +7857,10 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-ct-svelte": {
|
"packages/playwright-ct-svelte": {
|
||||||
"name": "@playwright/experimental-ct-svelte",
|
"name": "@playwright/experimental-ct-svelte",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.46.0-next",
|
"@playwright/experimental-ct-core": "1.46.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.1"
|
"@sveltejs/vite-plugin-svelte": "^3.0.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -7875,10 +7875,10 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-ct-vue": {
|
"packages/playwright-ct-vue": {
|
||||||
"name": "@playwright/experimental-ct-vue",
|
"name": "@playwright/experimental-ct-vue",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.46.0-next",
|
"@playwright/experimental-ct-core": "1.46.1",
|
||||||
"@vitejs/plugin-vue": "^4.2.1"
|
"@vitejs/plugin-vue": "^4.2.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -7890,10 +7890,10 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-ct-vue2": {
|
"packages/playwright-ct-vue2": {
|
||||||
"name": "@playwright/experimental-ct-vue2",
|
"name": "@playwright/experimental-ct-vue2",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.46.0-next",
|
"@playwright/experimental-ct-core": "1.46.1",
|
||||||
"@vitejs/plugin-vue2": "^2.2.0"
|
"@vitejs/plugin-vue2": "^2.2.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -7942,11 +7942,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/playwright-firefox": {
|
"packages/playwright-firefox": {
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.1",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.46.0-next"
|
"playwright-core": "1.46.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
|
|
@ -7957,10 +7957,10 @@
|
||||||
},
|
},
|
||||||
"packages/playwright-test": {
|
"packages/playwright-test": {
|
||||||
"name": "@playwright/test",
|
"name": "@playwright/test",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright": "1.46.0-next"
|
"playwright": "1.46.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
|
|
@ -7970,11 +7970,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/playwright-webkit": {
|
"packages/playwright-webkit": {
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.1",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.46.0-next"
|
"playwright-core": "1.46.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "playwright-internal",
|
"name": "playwright-internal",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.1",
|
||||||
"description": "A high-level API to automate web browsers",
|
"description": "A high-level API to automate web browsers",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import * as icons from './icons';
|
||||||
import { TreeItem } from './treeItem';
|
import { TreeItem } from './treeItem';
|
||||||
import { CopyToClipboard } from './copyToClipboard';
|
import { CopyToClipboard } from './copyToClipboard';
|
||||||
import './links.css';
|
import './links.css';
|
||||||
import { linkifyText } from './renderUtils';
|
import { linkifyText } from '@web/renderUtils';
|
||||||
|
|
||||||
export function navigate(href: string) {
|
export function navigate(href: string) {
|
||||||
window.history.pushState({}, '', href);
|
window.history.pushState({}, '', href);
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import { ProjectLink } from './links';
|
||||||
import { statusIcon } from './statusIcon';
|
import { statusIcon } from './statusIcon';
|
||||||
import './testCaseView.css';
|
import './testCaseView.css';
|
||||||
import { TestResultView } from './testResultView';
|
import { TestResultView } from './testResultView';
|
||||||
import { linkifyText } from './renderUtils';
|
import { linkifyText } from '@web/renderUtils';
|
||||||
import { hashStringToInt, msToString } from './utils';
|
import { hashStringToInt, msToString } from './utils';
|
||||||
|
|
||||||
export const TestCaseView: React.FC<{
|
export const TestCaseView: React.FC<{
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/browser-chromium",
|
"name": "@playwright/browser-chromium",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.1",
|
||||||
"description": "Playwright package that automatically installs Chromium",
|
"description": "Playwright package that automatically installs Chromium",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -27,6 +27,6 @@
|
||||||
"install": "node install.js"
|
"install": "node install.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.46.0-next"
|
"playwright-core": "1.46.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/browser-firefox",
|
"name": "@playwright/browser-firefox",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.1",
|
||||||
"description": "Playwright package that automatically installs Firefox",
|
"description": "Playwright package that automatically installs Firefox",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -27,6 +27,6 @@
|
||||||
"install": "node install.js"
|
"install": "node install.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.46.0-next"
|
"playwright-core": "1.46.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/browser-webkit",
|
"name": "@playwright/browser-webkit",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.1",
|
||||||
"description": "Playwright package that automatically installs WebKit",
|
"description": "Playwright package that automatically installs WebKit",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -27,6 +27,6 @@
|
||||||
"install": "node install.js"
|
"install": "node install.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.46.0-next"
|
"playwright-core": "1.46.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "playwright-chromium",
|
"name": "playwright-chromium",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.1",
|
||||||
"description": "A high-level API to automate Chromium",
|
"description": "A high-level API to automate Chromium",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -30,6 +30,6 @@
|
||||||
"install": "node install.js"
|
"install": "node install.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.46.0-next"
|
"playwright-core": "1.46.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
# Certfificates for Socks Proxy
|
|
||||||
|
|
||||||
These certificates are used when client certificates are used with
|
|
||||||
Playwright. Playwright then creates a Socks proxy, which sits between
|
|
||||||
the browser and the actual target server. The Socks proxy uses this certificiate
|
|
||||||
to talk to the browser and establishes its own secure TLS connection to the server.
|
|
||||||
The certificates are generated via:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
openssl req -new -newkey rsa:2048 -days 3650 -nodes -x509 -keyout key.pem -out cert.pem -subj "/CN=localhost"
|
|
||||||
```
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIDCTCCAfGgAwIBAgIUTcrzEueVL/OuLHr4LBIPWeS4UL0wDQYJKoZIhvcNAQEL
|
|
||||||
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI0MDcwNDA4NDAzNFoXDTM0MDcw
|
|
||||||
MjA4NDAzNFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
|
|
||||||
AAOCAQ8AMIIBCgKCAQEApof+SZVN4UGma4xJDVHhMSpmEJoCdMPr+HFadJJK/brF
|
|
||||||
BNOhA1C5wNk8oD/XYo7enAHQH/EsBnq4MMxv79rXTGnIdXMF+43GdMDh5kh81FQy
|
|
||||||
Esw8Vt4eif9eZkjUxI2GHhR2ovJewmQa7E+SeUB2RzJTqz8QPLhd74JFfgaci+S2
|
|
||||||
8L37ScVjcw55T1PcNflzB4vwsQHBT3yND0MLDhm+8MLzmTl4Mw5PgIOaBl5Jh8Tr
|
|
||||||
wQF4eeeB3FPJoMQhTP8aGBjW1mo+NmSSRAPIAZyhmCAnDeC33yRjAaiHjaL5Pr9f
|
|
||||||
wt5zoF5+U1xWhGXWzGOE6p/VTj62F9a2fOXNHclYJQIDAQABo1MwUTAdBgNVHQ4E
|
|
||||||
FgQU9BoVzGtb5x70KqGO/89N1hyqi5kwHwYDVR0jBBgwFoAU9BoVzGtb5x70KqGO
|
|
||||||
/89N1hyqi5kwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAYcbI
|
|
||||||
wvcfx2p8z0RNN3EA+epKX1SagZyJX4ORIO8kln1sDU+ceHde3n3xnp1dg6HG2qh1
|
|
||||||
a7CZub/fNUaP9R8+6iiV0wPT7Ybkb2NIJcH1yq+/bfSS5OC5DO0yv9SUADdBoDwa
|
|
||||||
zOuBAqdcYW1BHYcbAzsQnniRcejHu06ioaS6SwwJ8150rQnLT4Lh9LAl40W6v4nZ
|
|
||||||
NdTGQETTrbjcgH1ER4IhWTKtVyPOxGF9A/OOawMEdfS8BhUO7YRS4QNFFaQMrJAb
|
|
||||||
MDhDtjSyDogLr8P43xjjWvQWG9a7zTF0kKEsdJ0cEG5HATpg8bPHmrouxbs2HGeH
|
|
||||||
kJXzMykrsYyXsInN3w==
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCmh/5JlU3hQaZr
|
|
||||||
jEkNUeExKmYQmgJ0w+v4cVp0kkr9usUE06EDULnA2TygP9dijt6cAdAf8SwGergw
|
|
||||||
zG/v2tdMach1cwX7jcZ0wOHmSHzUVDISzDxW3h6J/15mSNTEjYYeFHai8l7CZBrs
|
|
||||||
T5J5QHZHMlOrPxA8uF3vgkV+BpyL5LbwvftJxWNzDnlPU9w1+XMHi/CxAcFPfI0P
|
|
||||||
QwsOGb7wwvOZOXgzDk+Ag5oGXkmHxOvBAXh554HcU8mgxCFM/xoYGNbWaj42ZJJE
|
|
||||||
A8gBnKGYICcN4LffJGMBqIeNovk+v1/C3nOgXn5TXFaEZdbMY4Tqn9VOPrYX1rZ8
|
|
||||||
5c0dyVglAgMBAAECggEAB6zX4vNPKhUZAvbtvP/rlZUDLDu05kXLX+F1jk7ZxvTv
|
|
||||||
NKg+UQVM8l7wxN/8YM3944nP2lEGuuu4BoO9mvvmlV6Avy0EdxITNflX0AHCQxT4
|
|
||||||
U9Z253gIR0ruQl+T8tUk+8jsqNjr1iC//ukx8oWujdx7b7aR3IKQzcOeyU6rs2TN
|
|
||||||
lyrVVsEaFVi9+wCw0xyiCmPlobrn+egdigw7Zhp2BRinC6W9eMxuPS2hlhQUhBm/
|
|
||||||
eiD96YWp0RAv/L5qO93reoXIAzrrLdcUgPEnnq1zN7y2xihU2+B2sTph1m/A26+J
|
|
||||||
yPcXd7vQrXlRXQU6PaCa+0oJULlpiAzy3HPbnr4BkQKBgQDdmekTX8dQqiEZPX1C
|
|
||||||
017QRFbx0/x/TDFDSeJbDeauMzzCaGqCO2WVmYmTvFtby2G4/6BYowVtJVHm4uJl
|
|
||||||
XsYk8dWIQGLPIj1Cw7ZieJvb2EVRxgnY2oMaOTOazHzPHFzZV718zwEeZrryT82J
|
|
||||||
881E8wgM8V3DjkS4ye3TbwvimQKBgQDAYa/IdnpAg5z1TREi9Tt8fnoGpmSscAak
|
|
||||||
USgeXVsvoNzXXkE94MiiCOOrX1r68TWYDAzq6MKGDewkWOfLwXWR6D5C2LyE1q9P
|
|
||||||
1pxstgs/nC3ZUTz0yEH47ahSmhywhGlvXXOQEXUSLiVTOdeMCubMqwQW80F1868n
|
|
||||||
aBHcj5/lbQKBgQDIojjsWaNT3TTqbUmj30vQtI8jlBLgDlPr4FEYr5VT0wAH5BHK
|
|
||||||
p4xpzgFJyRfOHG312TuMBM087LUinfjsXsp3WJ1EJ0dO0mk0sY3HyfsTKNRaHTt9
|
|
||||||
Ixnf/DpExS+bNMq73Tyqa6FPrSNFkAtAA4SuEHwRe9aw33ZI+EpjS/8uwQKBgQCi
|
|
||||||
9NwqSLlLVnColEw0uVdXH+cLJPzX19i4bQo3lkp8MJ2ATJWk7XflUPRQoGf3ckQ8
|
|
||||||
c9CpVtoXJUnmi+xkeo21Nu0uQFqHhzZewWIk75rdmdR4ZUjl649+ZQkUVviASNjq
|
|
||||||
fVU7Lp5k9POm6LL9K+rOaPoA2rKTUAQItC2VD4+YjQKBgB6kgvgN6Mz/u0RE3kkV
|
|
||||||
2GOoP5sso71Hxwh7o6JEzUMhR+e/T/LLcBwEjLYcf1FYRySHsXLn2Ar/Uw1J7pAZ
|
|
||||||
ud54/at+7mTDliaT8Ar7S9vcso7ZfmuDX9qB9+c77idPskVBPo2tjJbwvFcB6sww
|
|
||||||
5Elcfmj6tEP4YLJ6Kv3qTPhT
|
|
||||||
-----END PRIVATE KEY-----
|
|
||||||
|
|
@ -3,9 +3,9 @@
|
||||||
"browsers": [
|
"browsers": [
|
||||||
{
|
{
|
||||||
"name": "chromium",
|
"name": "chromium",
|
||||||
"revision": "1128",
|
"revision": "1129",
|
||||||
"installByDefault": true,
|
"installByDefault": true,
|
||||||
"browserVersion": "128.0.6613.7"
|
"browserVersion": "128.0.6613.18"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "chromium-tip-of-tree",
|
"name": "chromium-tip-of-tree",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "playwright-core",
|
"name": "playwright-core",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.1",
|
||||||
"description": "A high-level API to automate web browsers",
|
"description": "A high-level API to automate web browsers",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@
|
||||||
"defaultBrowserType": "webkit"
|
"defaultBrowserType": "webkit"
|
||||||
},
|
},
|
||||||
"Galaxy S5": {
|
"Galaxy S5": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 360,
|
"width": 360,
|
||||||
"height": 640
|
"height": 640
|
||||||
|
|
@ -121,7 +121,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Galaxy S5 landscape": {
|
"Galaxy S5 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 640,
|
"width": 640,
|
||||||
"height": 360
|
"height": 360
|
||||||
|
|
@ -132,7 +132,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Galaxy S8": {
|
"Galaxy S8": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 360,
|
"width": 360,
|
||||||
"height": 740
|
"height": 740
|
||||||
|
|
@ -143,7 +143,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Galaxy S8 landscape": {
|
"Galaxy S8 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 740,
|
"width": 740,
|
||||||
"height": 360
|
"height": 360
|
||||||
|
|
@ -154,7 +154,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Galaxy S9+": {
|
"Galaxy S9+": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 320,
|
"width": 320,
|
||||||
"height": 658
|
"height": 658
|
||||||
|
|
@ -165,7 +165,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Galaxy S9+ landscape": {
|
"Galaxy S9+ landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 658,
|
"width": 658,
|
||||||
"height": 320
|
"height": 320
|
||||||
|
|
@ -176,7 +176,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Galaxy Tab S4": {
|
"Galaxy Tab S4": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 712,
|
"width": 712,
|
||||||
"height": 1138
|
"height": 1138
|
||||||
|
|
@ -187,7 +187,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Galaxy Tab S4 landscape": {
|
"Galaxy Tab S4 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 1138,
|
"width": 1138,
|
||||||
"height": 712
|
"height": 712
|
||||||
|
|
@ -1098,7 +1098,7 @@
|
||||||
"defaultBrowserType": "webkit"
|
"defaultBrowserType": "webkit"
|
||||||
},
|
},
|
||||||
"LG Optimus L70": {
|
"LG Optimus L70": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.7 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.18 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 384,
|
"width": 384,
|
||||||
"height": 640
|
"height": 640
|
||||||
|
|
@ -1109,7 +1109,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"LG Optimus L70 landscape": {
|
"LG Optimus L70 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.7 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.18 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 640,
|
"width": 640,
|
||||||
"height": 384
|
"height": 384
|
||||||
|
|
@ -1120,7 +1120,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Microsoft Lumia 550": {
|
"Microsoft Lumia 550": {
|
||||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Mobile Safari/537.36 Edge/14.14263",
|
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36 Edge/14.14263",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 640,
|
"width": 640,
|
||||||
"height": 360
|
"height": 360
|
||||||
|
|
@ -1131,7 +1131,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Microsoft Lumia 550 landscape": {
|
"Microsoft Lumia 550 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Mobile Safari/537.36 Edge/14.14263",
|
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36 Edge/14.14263",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 360,
|
"width": 360,
|
||||||
"height": 640
|
"height": 640
|
||||||
|
|
@ -1142,7 +1142,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Microsoft Lumia 950": {
|
"Microsoft Lumia 950": {
|
||||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Mobile Safari/537.36 Edge/14.14263",
|
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36 Edge/14.14263",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 360,
|
"width": 360,
|
||||||
"height": 640
|
"height": 640
|
||||||
|
|
@ -1153,7 +1153,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Microsoft Lumia 950 landscape": {
|
"Microsoft Lumia 950 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Mobile Safari/537.36 Edge/14.14263",
|
"userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36 Edge/14.14263",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 640,
|
"width": 640,
|
||||||
"height": 360
|
"height": 360
|
||||||
|
|
@ -1164,7 +1164,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 10": {
|
"Nexus 10": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 800,
|
"width": 800,
|
||||||
"height": 1280
|
"height": 1280
|
||||||
|
|
@ -1175,7 +1175,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 10 landscape": {
|
"Nexus 10 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 1280,
|
"width": 1280,
|
||||||
"height": 800
|
"height": 800
|
||||||
|
|
@ -1186,7 +1186,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 4": {
|
"Nexus 4": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 384,
|
"width": 384,
|
||||||
"height": 640
|
"height": 640
|
||||||
|
|
@ -1197,7 +1197,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 4 landscape": {
|
"Nexus 4 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 640,
|
"width": 640,
|
||||||
"height": 384
|
"height": 384
|
||||||
|
|
@ -1208,7 +1208,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 5": {
|
"Nexus 5": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 360,
|
"width": 360,
|
||||||
"height": 640
|
"height": 640
|
||||||
|
|
@ -1219,7 +1219,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 5 landscape": {
|
"Nexus 5 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 640,
|
"width": 640,
|
||||||
"height": 360
|
"height": 360
|
||||||
|
|
@ -1230,7 +1230,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 5X": {
|
"Nexus 5X": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 412,
|
"width": 412,
|
||||||
"height": 732
|
"height": 732
|
||||||
|
|
@ -1241,7 +1241,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 5X landscape": {
|
"Nexus 5X landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 732,
|
"width": 732,
|
||||||
"height": 412
|
"height": 412
|
||||||
|
|
@ -1252,7 +1252,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 6": {
|
"Nexus 6": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 412,
|
"width": 412,
|
||||||
"height": 732
|
"height": 732
|
||||||
|
|
@ -1263,7 +1263,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 6 landscape": {
|
"Nexus 6 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 732,
|
"width": 732,
|
||||||
"height": 412
|
"height": 412
|
||||||
|
|
@ -1274,7 +1274,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 6P": {
|
"Nexus 6P": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 412,
|
"width": 412,
|
||||||
"height": 732
|
"height": 732
|
||||||
|
|
@ -1285,7 +1285,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 6P landscape": {
|
"Nexus 6P landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 732,
|
"width": 732,
|
||||||
"height": 412
|
"height": 412
|
||||||
|
|
@ -1296,7 +1296,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 7": {
|
"Nexus 7": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 600,
|
"width": 600,
|
||||||
"height": 960
|
"height": 960
|
||||||
|
|
@ -1307,7 +1307,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Nexus 7 landscape": {
|
"Nexus 7 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 960,
|
"width": 960,
|
||||||
"height": 600
|
"height": 600
|
||||||
|
|
@ -1362,7 +1362,7 @@
|
||||||
"defaultBrowserType": "webkit"
|
"defaultBrowserType": "webkit"
|
||||||
},
|
},
|
||||||
"Pixel 2": {
|
"Pixel 2": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 411,
|
"width": 411,
|
||||||
"height": 731
|
"height": 731
|
||||||
|
|
@ -1373,7 +1373,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 2 landscape": {
|
"Pixel 2 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 731,
|
"width": 731,
|
||||||
"height": 411
|
"height": 411
|
||||||
|
|
@ -1384,7 +1384,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 2 XL": {
|
"Pixel 2 XL": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 411,
|
"width": 411,
|
||||||
"height": 823
|
"height": 823
|
||||||
|
|
@ -1395,7 +1395,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 2 XL landscape": {
|
"Pixel 2 XL landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 823,
|
"width": 823,
|
||||||
"height": 411
|
"height": 411
|
||||||
|
|
@ -1406,7 +1406,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 3": {
|
"Pixel 3": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 393,
|
"width": 393,
|
||||||
"height": 786
|
"height": 786
|
||||||
|
|
@ -1417,7 +1417,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 3 landscape": {
|
"Pixel 3 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 786,
|
"width": 786,
|
||||||
"height": 393
|
"height": 393
|
||||||
|
|
@ -1428,7 +1428,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 4": {
|
"Pixel 4": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 353,
|
"width": 353,
|
||||||
"height": 745
|
"height": 745
|
||||||
|
|
@ -1439,7 +1439,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 4 landscape": {
|
"Pixel 4 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 745,
|
"width": 745,
|
||||||
"height": 353
|
"height": 353
|
||||||
|
|
@ -1450,7 +1450,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 4a (5G)": {
|
"Pixel 4a (5G)": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 412,
|
"width": 412,
|
||||||
"height": 892
|
"height": 892
|
||||||
|
|
@ -1465,7 +1465,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 4a (5G) landscape": {
|
"Pixel 4a (5G) landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
||||||
"screen": {
|
"screen": {
|
||||||
"height": 892,
|
"height": 892,
|
||||||
"width": 412
|
"width": 412
|
||||||
|
|
@ -1480,7 +1480,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 5": {
|
"Pixel 5": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 393,
|
"width": 393,
|
||||||
"height": 851
|
"height": 851
|
||||||
|
|
@ -1495,7 +1495,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 5 landscape": {
|
"Pixel 5 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 851,
|
"width": 851,
|
||||||
"height": 393
|
"height": 393
|
||||||
|
|
@ -1510,7 +1510,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 7": {
|
"Pixel 7": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 412,
|
"width": 412,
|
||||||
"height": 915
|
"height": 915
|
||||||
|
|
@ -1525,7 +1525,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Pixel 7 landscape": {
|
"Pixel 7 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 915,
|
"width": 915,
|
||||||
"height": 412
|
"height": 412
|
||||||
|
|
@ -1540,7 +1540,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Moto G4": {
|
"Moto G4": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 360,
|
"width": 360,
|
||||||
"height": 640
|
"height": 640
|
||||||
|
|
@ -1551,7 +1551,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Moto G4 landscape": {
|
"Moto G4 landscape": {
|
||||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Mobile Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Mobile Safari/537.36",
|
||||||
"viewport": {
|
"viewport": {
|
||||||
"width": 640,
|
"width": 640,
|
||||||
"height": 360
|
"height": 360
|
||||||
|
|
@ -1562,7 +1562,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Desktop Chrome HiDPI": {
|
"Desktop Chrome HiDPI": {
|
||||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Safari/537.36",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 1792,
|
"width": 1792,
|
||||||
"height": 1120
|
"height": 1120
|
||||||
|
|
@ -1577,7 +1577,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Desktop Edge HiDPI": {
|
"Desktop Edge HiDPI": {
|
||||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Safari/537.36 Edg/128.0.6613.7",
|
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Safari/537.36 Edg/128.0.6613.18",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 1792,
|
"width": 1792,
|
||||||
"height": 1120
|
"height": 1120
|
||||||
|
|
@ -1622,7 +1622,7 @@
|
||||||
"defaultBrowserType": "webkit"
|
"defaultBrowserType": "webkit"
|
||||||
},
|
},
|
||||||
"Desktop Chrome": {
|
"Desktop Chrome": {
|
||||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Safari/537.36",
|
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Safari/537.36",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 1920,
|
"width": 1920,
|
||||||
"height": 1080
|
"height": 1080
|
||||||
|
|
@ -1637,7 +1637,7 @@
|
||||||
"defaultBrowserType": "chromium"
|
"defaultBrowserType": "chromium"
|
||||||
},
|
},
|
||||||
"Desktop Edge": {
|
"Desktop Edge": {
|
||||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.7 Safari/537.36 Edg/128.0.6613.7",
|
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.18 Safari/537.36 Edg/128.0.6613.18",
|
||||||
"screen": {
|
"screen": {
|
||||||
"width": 1920,
|
"width": 1920,
|
||||||
"height": 1080
|
"height": 1080
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
import type * as channels from '@protocol/channels';
|
import type * as channels from '@protocol/channels';
|
||||||
import type { LookupAddress } from 'dns';
|
import type { LookupAddress } from 'dns';
|
||||||
import http from 'http';
|
import http from 'http';
|
||||||
import fs from 'fs';
|
|
||||||
import https from 'https';
|
import https from 'https';
|
||||||
import type { Readable, TransformCallback } from 'stream';
|
import type { Readable, TransformCallback } from 'stream';
|
||||||
import { pipeline, Transform } from 'stream';
|
import { pipeline, Transform } from 'stream';
|
||||||
|
|
@ -26,7 +25,7 @@ import zlib from 'zlib';
|
||||||
import type { HTTPCredentials } from '../../types/types';
|
import type { HTTPCredentials } from '../../types/types';
|
||||||
import { TimeoutSettings } from '../common/timeoutSettings';
|
import { TimeoutSettings } from '../common/timeoutSettings';
|
||||||
import { getUserAgent } from '../utils/userAgent';
|
import { getUserAgent } from '../utils/userAgent';
|
||||||
import { assert, createGuid, isUnderTest, 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 } from './cookieStore';
|
||||||
|
|
@ -41,7 +40,7 @@ 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 { kMaxCookieExpiresDateInSeconds } from './network';
|
||||||
import { clientCertificatesToTLSOptions } from './socksClientCertificatesInterceptor';
|
import { clientCertificatesToTLSOptions, rewriteOpenSSLErrorIfNeeded } from './socksClientCertificatesInterceptor';
|
||||||
|
|
||||||
type FetchRequestOptions = {
|
type FetchRequestOptions = {
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
|
|
@ -168,7 +167,10 @@ export abstract class APIRequestContext extends SdkObject {
|
||||||
const method = params.method?.toUpperCase() || 'GET';
|
const method = params.method?.toUpperCase() || 'GET';
|
||||||
const proxy = defaults.proxy;
|
const proxy = defaults.proxy;
|
||||||
let agent;
|
let agent;
|
||||||
if (proxy && proxy.server !== 'per-context' && !shouldBypassProxy(requestUrl, proxy.bypass)) {
|
// When `clientCertificates` is present, we set the `proxy` property to our own socks proxy
|
||||||
|
// for the browser to use. However, we don't need it here, because we already respect
|
||||||
|
// `clientCertificates` when fetching from Node.js.
|
||||||
|
if (proxy && !defaults.clientCertificates?.length && proxy.server !== 'per-context' && !shouldBypassProxy(requestUrl, proxy.bypass)) {
|
||||||
const proxyOpts = url.parse(proxy.server);
|
const proxyOpts = url.parse(proxy.server);
|
||||||
if (proxyOpts.protocol?.startsWith('socks')) {
|
if (proxyOpts.protocol?.startsWith('socks')) {
|
||||||
agent = new SocksProxyAgent({
|
agent = new SocksProxyAgent({
|
||||||
|
|
@ -196,8 +198,6 @@ export abstract class APIRequestContext extends SdkObject {
|
||||||
...clientCertificatesToTLSOptions(this._defaultOptions().clientCertificates, requestUrl.origin),
|
...clientCertificatesToTLSOptions(this._defaultOptions().clientCertificates, requestUrl.origin),
|
||||||
__testHookLookup: (params as any).__testHookLookup,
|
__testHookLookup: (params as any).__testHookLookup,
|
||||||
};
|
};
|
||||||
if (process.env.PWTEST_UNSUPPORTED_CUSTOM_CA && isUnderTest())
|
|
||||||
options.ca = [fs.readFileSync(process.env.PWTEST_UNSUPPORTED_CUSTOM_CA)];
|
|
||||||
// rejectUnauthorized = undefined is treated as true in Node.js 12.
|
// rejectUnauthorized = undefined is treated as true in Node.js 12.
|
||||||
if (params.ignoreHTTPSErrors || defaults.ignoreHTTPSErrors)
|
if (params.ignoreHTTPSErrors || defaults.ignoreHTTPSErrors)
|
||||||
options.rejectUnauthorized = false;
|
options.rejectUnauthorized = false;
|
||||||
|
|
@ -444,7 +444,7 @@ export abstract class APIRequestContext extends SdkObject {
|
||||||
body.on('data', chunk => chunks.push(chunk));
|
body.on('data', chunk => chunks.push(chunk));
|
||||||
body.on('end', notifyBodyFinished);
|
body.on('end', notifyBodyFinished);
|
||||||
});
|
});
|
||||||
request.on('error', reject);
|
request.on('error', error => reject(rewriteOpenSSLErrorIfNeeded(error)));
|
||||||
|
|
||||||
const disposeListener = () => {
|
const disposeListener = () => {
|
||||||
reject(new Error('Request context disposed.'));
|
reject(new Error('Request context disposed.'));
|
||||||
|
|
|
||||||
|
|
@ -170,8 +170,16 @@ export function source() {
|
||||||
if (typeof value === 'bigint')
|
if (typeof value === 'bigint')
|
||||||
return { bi: value.toString() };
|
return { bi: value.toString() };
|
||||||
|
|
||||||
if (isError(value))
|
if (isError(value)) {
|
||||||
return { e: { n: value.name, m: value.message, s: value.stack || '' } };
|
let stack;
|
||||||
|
if (value.stack?.startsWith(value.name + ': ' + value.message)) {
|
||||||
|
// v8
|
||||||
|
stack = value.stack;
|
||||||
|
} else {
|
||||||
|
stack = `${value.name}: ${value.message}\n${value.stack}`;
|
||||||
|
}
|
||||||
|
return { e: { n: value.name, m: value.message, s: stack } };
|
||||||
|
}
|
||||||
if (isDate(value))
|
if (isDate(value))
|
||||||
return { d: value.toJSON() };
|
return { d: value.toJSON() };
|
||||||
if (isURL(value))
|
if (isURL(value))
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,8 @@ export async function syncLocalStorageWithSettings(page: Page, appName: string)
|
||||||
// iframes w/ snapshots, etc.
|
// iframes w/ snapshots, etc.
|
||||||
if (location && location.protocol === 'data:')
|
if (location && location.protocol === 'data:')
|
||||||
return;
|
return;
|
||||||
|
if (window.top !== window)
|
||||||
|
return;
|
||||||
Object.entries(settings).map(([k, v]) => localStorage[k] = v);
|
Object.entries(settings).map(([k, v]) => localStorage[k] = v);
|
||||||
(window as any).saveSettings = () => {
|
(window as any).saveSettings = () => {
|
||||||
(window as any)._saveSerializedSettings(JSON.stringify({ ...localStorage }));
|
(window as any)._saveSerializedSettings(JSON.stringify({ ...localStorage }));
|
||||||
|
|
|
||||||
|
|
@ -15,14 +15,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import net from 'net';
|
import net from 'net';
|
||||||
import path from 'path';
|
|
||||||
import http2 from 'http2';
|
import http2 from 'http2';
|
||||||
import type https from 'https';
|
import type https from 'https';
|
||||||
import fs from 'fs';
|
|
||||||
import tls from 'tls';
|
import tls from 'tls';
|
||||||
import stream from 'stream';
|
import stream from 'stream';
|
||||||
import { createSocket, createTLSSocket } from '../utils/happy-eyeballs';
|
import { createSocket, createTLSSocket } from '../utils/happy-eyeballs';
|
||||||
import { isUnderTest, ManualPromise } 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 channels from '@protocol/channels';
|
||||||
|
|
@ -32,10 +30,8 @@ let dummyServerTlsOptions: tls.TlsOptions | undefined = undefined;
|
||||||
function loadDummyServerCertsIfNeeded() {
|
function loadDummyServerCertsIfNeeded() {
|
||||||
if (dummyServerTlsOptions)
|
if (dummyServerTlsOptions)
|
||||||
return;
|
return;
|
||||||
dummyServerTlsOptions = {
|
const { cert, key } = generateSelfSignedCertificate();
|
||||||
key: fs.readFileSync(path.join(__dirname, '../../bin/socks-certs/key.pem')),
|
dummyServerTlsOptions = { key, cert };
|
||||||
cert: fs.readFileSync(path.join(__dirname, '../../bin/socks-certs/cert.pem')),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ALPNCache {
|
class ALPNCache {
|
||||||
|
|
@ -60,11 +56,9 @@ class ALPNCache {
|
||||||
ALPNProtocols: ['h2', 'http/1.1'],
|
ALPNProtocols: ['h2', 'http/1.1'],
|
||||||
rejectUnauthorized: false,
|
rejectUnauthorized: false,
|
||||||
}).then(socket => {
|
}).then(socket => {
|
||||||
socket.on('secureConnect', () => {
|
// The server may not respond with ALPN, in which case we default to http/1.1.
|
||||||
// The server may not respond with ALPN, in which case we default to http/1.1.
|
result.resolve(socket.alpnProtocol || 'http/1.1');
|
||||||
result.resolve(socket.alpnProtocol || 'http/1.1');
|
socket.end();
|
||||||
socket.end();
|
|
||||||
});
|
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
debugLogger.log('client-certificates', `ALPN error: ${error.message}`);
|
debugLogger.log('client-certificates', `ALPN error: ${error.message}`);
|
||||||
result.resolve('http/1.1');
|
result.resolve('http/1.1');
|
||||||
|
|
@ -93,8 +87,8 @@ class SocksProxyConnection {
|
||||||
|
|
||||||
async connect() {
|
async connect() {
|
||||||
this.target = await createSocket(rewriteToLocalhostIfNeeded(this.host), this.port);
|
this.target = await createSocket(rewriteToLocalhostIfNeeded(this.host), this.port);
|
||||||
this.target.on('close', this._targetCloseEventListener);
|
this.target.once('close', this._targetCloseEventListener);
|
||||||
this.target.on('error', error => this.socksProxy._socksProxy.sendSocketError({ uid: this.uid, error: error.message }));
|
this.target.once('error', error => this.socksProxy._socksProxy.sendSocketError({ uid: this.uid, error: error.message }));
|
||||||
this.socksProxy._socksProxy.socketConnected({
|
this.socksProxy._socksProxy.socketConnected({
|
||||||
uid: this.uid,
|
uid: this.uid,
|
||||||
host: this.target.localAddress!,
|
host: this.target.localAddress!,
|
||||||
|
|
@ -138,42 +132,22 @@ class SocksProxyConnection {
|
||||||
...dummyServerTlsOptions,
|
...dummyServerTlsOptions,
|
||||||
ALPNProtocols: alpnProtocolChosenByServer === 'h2' ? ['h2', 'http/1.1'] : ['http/1.1'],
|
ALPNProtocols: alpnProtocolChosenByServer === 'h2' ? ['h2', 'http/1.1'] : ['http/1.1'],
|
||||||
});
|
});
|
||||||
this.internal?.on('close', () => dummyServer.close());
|
this.internal?.once('close', () => dummyServer.close());
|
||||||
dummyServer.emit('connection', this.internal);
|
dummyServer.emit('connection', this.internal);
|
||||||
dummyServer.on('secureConnection', internalTLS => {
|
dummyServer.once('secureConnection', internalTLS => {
|
||||||
debugLogger.log('client-certificates', `Browser->Proxy ${this.host}:${this.port} chooses ALPN ${internalTLS.alpnProtocol}`);
|
debugLogger.log('client-certificates', `Browser->Proxy ${this.host}:${this.port} chooses ALPN ${internalTLS.alpnProtocol}`);
|
||||||
const tlsOptions: tls.ConnectionOptions = {
|
|
||||||
socket: this.target,
|
|
||||||
host: this.host,
|
|
||||||
port: this.port,
|
|
||||||
rejectUnauthorized: !this.socksProxy.ignoreHTTPSErrors,
|
|
||||||
ALPNProtocols: [internalTLS.alpnProtocol || 'http/1.1'],
|
|
||||||
...clientCertificatesToTLSOptions(this.socksProxy.clientCertificates, `https://${this.host}:${this.port}`),
|
|
||||||
};
|
|
||||||
if (!net.isIP(this.host))
|
|
||||||
tlsOptions.servername = this.host;
|
|
||||||
if (process.env.PWTEST_UNSUPPORTED_CUSTOM_CA && isUnderTest())
|
|
||||||
tlsOptions.ca = [fs.readFileSync(process.env.PWTEST_UNSUPPORTED_CUSTOM_CA)];
|
|
||||||
const targetTLS = tls.connect(tlsOptions);
|
|
||||||
|
|
||||||
targetTLS.on('secureConnect', () => {
|
let targetTLS: tls.TLSSocket | undefined = undefined;
|
||||||
internalTLS.pipe(targetTLS);
|
|
||||||
targetTLS.pipe(internalTLS);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle close and errors
|
|
||||||
const closeBothSockets = () => {
|
const closeBothSockets = () => {
|
||||||
internalTLS.end();
|
internalTLS.end();
|
||||||
targetTLS.end();
|
targetTLS?.end();
|
||||||
};
|
};
|
||||||
|
|
||||||
internalTLS.on('end', () => closeBothSockets());
|
const handleError = (error: Error) => {
|
||||||
targetTLS.on('end', () => closeBothSockets());
|
error = rewriteOpenSSLErrorIfNeeded(error);
|
||||||
|
debugLogger.log('client-certificates', `error when connecting to target: ${error.message.replaceAll('\n', ' ')}`);
|
||||||
internalTLS.on('error', () => closeBothSockets());
|
const responseBody = escapeHTML('Playwright client-certificate error: ' + error.message)
|
||||||
targetTLS.on('error', error => {
|
.replaceAll('\n', ' <br>');
|
||||||
debugLogger.log('client-certificates', `error when connecting to target: ${error.message}`);
|
|
||||||
const responseBody = 'Playwright client-certificate error: ' + error.message;
|
|
||||||
if (internalTLS?.alpnProtocol === 'h2') {
|
if (internalTLS?.alpnProtocol === 'h2') {
|
||||||
// This method is available only in Node.js 20+
|
// This method is available only in Node.js 20+
|
||||||
if ('performServerHandshake' in http2) {
|
if ('performServerHandshake' in http2) {
|
||||||
|
|
@ -182,7 +156,7 @@ class SocksProxyConnection {
|
||||||
this.target.removeListener('close', this._targetCloseEventListener);
|
this.target.removeListener('close', this._targetCloseEventListener);
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
const session: http2.ServerHttp2Session = http2.performServerHandshake(internalTLS);
|
const session: http2.ServerHttp2Session = http2.performServerHandshake(internalTLS);
|
||||||
session.on('stream', (stream: http2.ServerHttp2Stream) => {
|
session.once('stream', (stream: http2.ServerHttp2Stream) => {
|
||||||
stream.respond({
|
stream.respond({
|
||||||
'content-type': 'text/html',
|
'content-type': 'text/html',
|
||||||
[http2.constants.HTTP2_HEADER_STATUS]: 503,
|
[http2.constants.HTTP2_HEADER_STATUS]: 503,
|
||||||
|
|
@ -191,7 +165,7 @@ class SocksProxyConnection {
|
||||||
session.close();
|
session.close();
|
||||||
closeBothSockets();
|
closeBothSockets();
|
||||||
});
|
});
|
||||||
stream.on('error', () => closeBothSockets());
|
stream.once('error', () => closeBothSockets());
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
closeBothSockets();
|
closeBothSockets();
|
||||||
|
|
@ -201,12 +175,42 @@ class SocksProxyConnection {
|
||||||
'HTTP/1.1 503 Internal Server Error',
|
'HTTP/1.1 503 Internal Server Error',
|
||||||
'Content-Type: text/html; charset=utf-8',
|
'Content-Type: text/html; charset=utf-8',
|
||||||
'Content-Length: ' + Buffer.byteLength(responseBody),
|
'Content-Length: ' + Buffer.byteLength(responseBody),
|
||||||
'\r\n',
|
'',
|
||||||
responseBody,
|
responseBody,
|
||||||
].join('\r\n'));
|
].join('\r\n'));
|
||||||
closeBothSockets();
|
closeBothSockets();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let secureContext: tls.SecureContext;
|
||||||
|
try {
|
||||||
|
secureContext = tls.createSecureContext(clientCertificatesToTLSOptions(this.socksProxy.clientCertificates, new URL(`https://${this.host}:${this.port}`).origin));
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tlsOptions: tls.ConnectionOptions = {
|
||||||
|
socket: this.target,
|
||||||
|
host: this.host,
|
||||||
|
port: this.port,
|
||||||
|
rejectUnauthorized: !this.socksProxy.ignoreHTTPSErrors,
|
||||||
|
ALPNProtocols: [internalTLS.alpnProtocol || 'http/1.1'],
|
||||||
|
servername: !net.isIP(this.host) ? this.host : undefined,
|
||||||
|
secureContext,
|
||||||
|
};
|
||||||
|
|
||||||
|
targetTLS = tls.connect(tlsOptions);
|
||||||
|
|
||||||
|
targetTLS.once('secureConnect', () => {
|
||||||
|
internalTLS.pipe(targetTLS);
|
||||||
|
targetTLS.pipe(internalTLS);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
internalTLS.once('close', () => closeBothSockets());
|
||||||
|
|
||||||
|
internalTLS.once('error', () => closeBothSockets());
|
||||||
|
targetTLS.once('error', handleError);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -288,3 +292,14 @@ export function clientCertificatesToTLSOptions(
|
||||||
function rewriteToLocalhostIfNeeded(host: string): string {
|
function rewriteToLocalhostIfNeeded(host: string): string {
|
||||||
return host === 'local.playwright' ? 'localhost' : host;
|
return host === 'local.playwright' ? 'localhost' : host;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function rewriteOpenSSLErrorIfNeeded(error: Error): Error {
|
||||||
|
if (error.message !== 'unsupported')
|
||||||
|
return error;
|
||||||
|
return rewriteErrorMessage(error, [
|
||||||
|
'Unsupported TLS certificate.',
|
||||||
|
'Most likely, the security algorithm of the given certificate was deprecated by OpenSSL.',
|
||||||
|
'For more details, see https://github.com/openssl/openssl/blob/master/README-PROVIDERS.md#the-legacy-provider',
|
||||||
|
'You could probably modernize the certificate by following the steps at https://github.com/nodejs/node/issues/40672#issuecomment-1243648223',
|
||||||
|
].join('\n'));
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
import { assert } from './debug';
|
||||||
|
|
||||||
export function createGuid(): string {
|
export function createGuid(): string {
|
||||||
return crypto.randomBytes(16).toString('hex');
|
return crypto.randomBytes(16).toString('hex');
|
||||||
|
|
@ -25,3 +26,170 @@ export function calculateSha1(buffer: Buffer | string): string {
|
||||||
hash.update(buffer);
|
hash.update(buffer);
|
||||||
return hash.digest('hex');
|
return hash.digest('hex');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Variable-length quantity encoding aka. base-128 encoding
|
||||||
|
function encodeBase128(value: number): Buffer {
|
||||||
|
const bytes = [];
|
||||||
|
do {
|
||||||
|
let byte = value & 0x7f;
|
||||||
|
value >>>= 7;
|
||||||
|
if (bytes.length > 0) byte |= 0x80;
|
||||||
|
bytes.push(byte);
|
||||||
|
} while (value > 0);
|
||||||
|
return Buffer.from(bytes.reverse());
|
||||||
|
};
|
||||||
|
|
||||||
|
// ASN1/DER Speficiation: https://www.itu.int/rec/T-REC-X.680-X.693-202102-I/en
|
||||||
|
class DER {
|
||||||
|
static encodeSequence(data: Buffer[]): Buffer {
|
||||||
|
return this._encode(0x30, Buffer.concat(data));
|
||||||
|
}
|
||||||
|
static encodeInteger(data: number): Buffer {
|
||||||
|
assert(data >= -128 && data <= 127);
|
||||||
|
return this._encode(0x02, Buffer.from([data]));
|
||||||
|
}
|
||||||
|
static encodeObjectIdentifier(oid: string): Buffer {
|
||||||
|
const parts = oid.split('.').map((v) => Number(v));
|
||||||
|
// Encode the second part, which could be large, using base-128 encoding if necessary
|
||||||
|
const output = [encodeBase128(40 * parts[0] + parts[1])];
|
||||||
|
|
||||||
|
for (let i = 2; i < parts.length; i++) {
|
||||||
|
output.push(encodeBase128(parts[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._encode(0x06, Buffer.concat(output));
|
||||||
|
}
|
||||||
|
static encodeNull(): Buffer {
|
||||||
|
return Buffer.from([0x05, 0x00]);
|
||||||
|
}
|
||||||
|
static encodeSet(data: Buffer[]): Buffer {
|
||||||
|
assert(data.length === 1, 'Only one item in the set is supported. We\'d need to sort the data to support more.');
|
||||||
|
// We expect the data to be already sorted.
|
||||||
|
return this._encode(0x31, Buffer.concat(data));
|
||||||
|
}
|
||||||
|
static encodeExplicitContextDependent(tag: number, data: Buffer): Buffer {
|
||||||
|
return this._encode(0xa0 + tag, data);
|
||||||
|
}
|
||||||
|
static encodePrintableString(data: string): Buffer {
|
||||||
|
return this._encode(0x13, Buffer.from(data));
|
||||||
|
}
|
||||||
|
static encodeBitString(data: Buffer): Buffer {
|
||||||
|
// The first byte of the content is the number of unused bits at the end
|
||||||
|
const unusedBits = 0; // Assuming all bits are used
|
||||||
|
const content = Buffer.concat([Buffer.from([unusedBits]), data]);
|
||||||
|
return this._encode(0x03, content);
|
||||||
|
}
|
||||||
|
static encodeDate(date: Date): Buffer {
|
||||||
|
const year = date.getUTCFullYear();
|
||||||
|
const isGeneralizedTime = year >= 2050;
|
||||||
|
const parts = [
|
||||||
|
isGeneralizedTime ? year.toString() : year.toString().slice(-2),
|
||||||
|
(date.getUTCMonth() + 1).toString().padStart(2, '0'),
|
||||||
|
date.getUTCDate().toString().padStart(2, '0'),
|
||||||
|
date.getUTCHours().toString().padStart(2, '0'),
|
||||||
|
date.getUTCMinutes().toString().padStart(2, '0'),
|
||||||
|
date.getUTCSeconds().toString().padStart(2, '0')
|
||||||
|
];
|
||||||
|
const encodedDate = parts.join('') + 'Z';
|
||||||
|
const tag = isGeneralizedTime ? 0x18 : 0x17; // 0x18 for GeneralizedTime, 0x17 for UTCTime
|
||||||
|
return this._encode(tag, Buffer.from(encodedDate));
|
||||||
|
}
|
||||||
|
private static _encode(tag: number, data: Buffer): Buffer {
|
||||||
|
const lengthBytes = this._encodeLength(data.length);
|
||||||
|
return Buffer.concat([Buffer.from([tag]), lengthBytes, data]);
|
||||||
|
}
|
||||||
|
private static _encodeLength(length: number): Buffer {
|
||||||
|
if (length < 128) {
|
||||||
|
return Buffer.from([length]);
|
||||||
|
} else {
|
||||||
|
const lengthBytes = [];
|
||||||
|
while (length > 0) {
|
||||||
|
lengthBytes.unshift(length & 0xFF);
|
||||||
|
length >>= 8;
|
||||||
|
}
|
||||||
|
return Buffer.from([0x80 | lengthBytes.length, ...lengthBytes]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// X.509 Specification: https://datatracker.ietf.org/doc/html/rfc2459#section-4.1
|
||||||
|
export function generateSelfSignedCertificate() {
|
||||||
|
const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 });
|
||||||
|
const publicKeyDer = publicKey.export({ type: 'pkcs1', format: 'der' });
|
||||||
|
|
||||||
|
const oneYearInMilliseconds = 365 * 24 * 60 * 60 * 1_000;
|
||||||
|
const notBefore = new Date(new Date().getTime() - oneYearInMilliseconds);
|
||||||
|
const notAfter = new Date(new Date().getTime() + oneYearInMilliseconds);
|
||||||
|
|
||||||
|
// List of fields / structure: https://datatracker.ietf.org/doc/html/rfc2459#section-4.1
|
||||||
|
const tbsCertificate = DER.encodeSequence([
|
||||||
|
DER.encodeExplicitContextDependent(0, DER.encodeInteger(1)), // version
|
||||||
|
DER.encodeInteger(1), // serialNumber
|
||||||
|
DER.encodeSequence([
|
||||||
|
DER.encodeObjectIdentifier('1.2.840.113549.1.1.11'), // sha256WithRSAEncryption PKCS #1
|
||||||
|
DER.encodeNull()
|
||||||
|
]), // signature
|
||||||
|
DER.encodeSequence([
|
||||||
|
DER.encodeSet([
|
||||||
|
DER.encodeSequence([
|
||||||
|
DER.encodeObjectIdentifier('2.5.4.3'), // commonName X.520 DN component
|
||||||
|
DER.encodePrintableString('localhost')
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
DER.encodeSet([
|
||||||
|
DER.encodeSequence([
|
||||||
|
DER.encodeObjectIdentifier('2.5.4.10'), // organizationName X.520 DN component
|
||||||
|
DER.encodePrintableString('Playwright Client Certificate Support')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]), // issuer
|
||||||
|
DER.encodeSequence([
|
||||||
|
DER.encodeDate(notBefore), // notBefore
|
||||||
|
DER.encodeDate(notAfter), // notAfter
|
||||||
|
]), // validity
|
||||||
|
DER.encodeSequence([
|
||||||
|
DER.encodeSet([
|
||||||
|
DER.encodeSequence([
|
||||||
|
DER.encodeObjectIdentifier('2.5.4.3'), // commonName X.520 DN component
|
||||||
|
DER.encodePrintableString('localhost')
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
DER.encodeSet([
|
||||||
|
DER.encodeSequence([
|
||||||
|
DER.encodeObjectIdentifier('2.5.4.10'), // organizationName X.520 DN component
|
||||||
|
DER.encodePrintableString('Playwright Client Certificate Support')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]), // subject
|
||||||
|
DER.encodeSequence([
|
||||||
|
DER.encodeSequence([
|
||||||
|
DER.encodeObjectIdentifier('1.2.840.113549.1.1.1'), // rsaEncryption PKCS #1
|
||||||
|
DER.encodeNull()
|
||||||
|
]),
|
||||||
|
DER.encodeBitString(publicKeyDer)
|
||||||
|
]), // SubjectPublicKeyInfo
|
||||||
|
]);
|
||||||
|
|
||||||
|
const signature = crypto.sign('sha256', tbsCertificate, privateKey);
|
||||||
|
|
||||||
|
const certificate = DER.encodeSequence([
|
||||||
|
tbsCertificate,
|
||||||
|
DER.encodeSequence([
|
||||||
|
DER.encodeObjectIdentifier('1.2.840.113549.1.1.11'), // sha256WithRSAEncryption PKCS #1
|
||||||
|
DER.encodeNull()
|
||||||
|
]),
|
||||||
|
DER.encodeBitString(signature)
|
||||||
|
]);
|
||||||
|
|
||||||
|
const certPem = [
|
||||||
|
'-----BEGIN CERTIFICATE-----',
|
||||||
|
// Split the base64 string into lines of 64 characters
|
||||||
|
certificate.toString('base64').match(/.{1,64}/g)!.join('\n'),
|
||||||
|
'-----END CERTIFICATE-----'
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
return {
|
||||||
|
cert: certPem,
|
||||||
|
key: privateKey.export({ type: 'pkcs1', format: 'pem' }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,14 +72,16 @@ export async function createTLSSocket(options: tls.ConnectionOptions): Promise<t
|
||||||
assert(options.host, 'host is required');
|
assert(options.host, 'host is required');
|
||||||
if (net.isIP(options.host)) {
|
if (net.isIP(options.host)) {
|
||||||
const socket = tls.connect(options)
|
const socket = tls.connect(options)
|
||||||
socket.on('connect', () => resolve(socket));
|
socket.on('secureConnect', () => resolve(socket));
|
||||||
socket.on('error', error => reject(error));
|
socket.on('error', error => reject(error));
|
||||||
} else {
|
} else {
|
||||||
createConnectionAsync(options, (err, socket) => {
|
createConnectionAsync(options, (err, socket) => {
|
||||||
if (err)
|
if (err)
|
||||||
reject(err);
|
reject(err);
|
||||||
if (socket)
|
if (socket) {
|
||||||
resolve(socket);
|
socket.on('secureConnect', () => resolve(socket));
|
||||||
|
socket.on('error', error => reject(error));
|
||||||
|
}
|
||||||
}, true).catch(err => reject(err));
|
}, true).catch(err => reject(err));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -132,3 +132,11 @@ export function escapeRegExp(s: string) {
|
||||||
// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
|
// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
|
||||||
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const escaped = { '&': '&', '<': '<', '>': '>', '"': '"', '\'': ''' };
|
||||||
|
export function escapeHTMLAttribute(s: string): string {
|
||||||
|
return s.replace(/[&<>"']/ug, char => (escaped as any)[char]);
|
||||||
|
}
|
||||||
|
export function escapeHTML(s: string): string {
|
||||||
|
return s.replace(/[&<]/ug, char => (escaped as any)[char]);
|
||||||
|
}
|
||||||
|
|
|
||||||
12
packages/playwright-core/types/types.d.ts
vendored
12
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -15807,6 +15807,12 @@ export interface APIRequestContext {
|
||||||
*/
|
*/
|
||||||
maxRedirects?: number;
|
maxRedirects?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum number of times network errors should be retried. Currently only `ECONNRESET` error is retried. Does not
|
||||||
|
* retry based on HTTP response codes. An error will be thrown if the limit is exceeded. Defaults to `0` - no retries.
|
||||||
|
*/
|
||||||
|
maxRetries?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides an object that will be serialized as html form using `multipart/form-data` encoding and sent as this
|
* Provides an object that will be serialized as html form using `multipart/form-data` encoding and sent as this
|
||||||
* request body. If this parameter is specified `content-type` header will be set to `multipart/form-data` unless
|
* request body. If this parameter is specified `content-type` header will be set to `multipart/form-data` unless
|
||||||
|
|
@ -19564,6 +19570,12 @@ export interface Route {
|
||||||
*/
|
*/
|
||||||
maxRedirects?: number;
|
maxRedirects?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum number of times network errors should be retried. Currently only `ECONNRESET` error is retried. Does not
|
||||||
|
* retry based on HTTP response codes. An error will be thrown if the limit is exceeded. Defaults to `0` - no retries.
|
||||||
|
*/
|
||||||
|
maxRetries?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If set changes the request method (e.g. GET or POST).
|
* If set changes the request method (e.g. GET or POST).
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/experimental-ct-core",
|
"name": "@playwright/experimental-ct-core",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.1",
|
||||||
"description": "Playwright Component Testing Helpers",
|
"description": "Playwright Component Testing Helpers",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -26,8 +26,8 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.46.0-next",
|
"playwright-core": "1.46.1",
|
||||||
"vite": "^5.2.8",
|
"vite": "^5.2.8",
|
||||||
"playwright": "1.46.0-next"
|
"playwright": "1.46.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/experimental-ct-react",
|
"name": "@playwright/experimental-ct-react",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.1",
|
||||||
"description": "Playwright Component Testing for React",
|
"description": "Playwright Component Testing for React",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
"./package.json": "./package.json"
|
"./package.json": "./package.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.46.0-next",
|
"@playwright/experimental-ct-core": "1.46.1",
|
||||||
"@vitejs/plugin-react": "^4.2.1"
|
"@vitejs/plugin-react": "^4.2.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/experimental-ct-react17",
|
"name": "@playwright/experimental-ct-react17",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.1",
|
||||||
"description": "Playwright Component Testing for React",
|
"description": "Playwright Component Testing for React",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
"./package.json": "./package.json"
|
"./package.json": "./package.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.46.0-next",
|
"@playwright/experimental-ct-core": "1.46.1",
|
||||||
"@vitejs/plugin-react": "^4.2.1"
|
"@vitejs/plugin-react": "^4.2.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/experimental-ct-solid",
|
"name": "@playwright/experimental-ct-solid",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.1",
|
||||||
"description": "Playwright Component Testing for Solid",
|
"description": "Playwright Component Testing for Solid",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
"./package.json": "./package.json"
|
"./package.json": "./package.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.46.0-next",
|
"@playwright/experimental-ct-core": "1.46.1",
|
||||||
"vite-plugin-solid": "^2.7.0"
|
"vite-plugin-solid": "^2.7.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/experimental-ct-svelte",
|
"name": "@playwright/experimental-ct-svelte",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.1",
|
||||||
"description": "Playwright Component Testing for Svelte",
|
"description": "Playwright Component Testing for Svelte",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
"./package.json": "./package.json"
|
"./package.json": "./package.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.46.0-next",
|
"@playwright/experimental-ct-core": "1.46.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.1"
|
"@sveltejs/vite-plugin-svelte": "^3.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/experimental-ct-vue",
|
"name": "@playwright/experimental-ct-vue",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.1",
|
||||||
"description": "Playwright Component Testing for Vue",
|
"description": "Playwright Component Testing for Vue",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
"./package.json": "./package.json"
|
"./package.json": "./package.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.46.0-next",
|
"@playwright/experimental-ct-core": "1.46.1",
|
||||||
"@vitejs/plugin-vue": "^4.2.1"
|
"@vitejs/plugin-vue": "^4.2.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/experimental-ct-vue2",
|
"name": "@playwright/experimental-ct-vue2",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.1",
|
||||||
"description": "Playwright Component Testing for Vue2",
|
"description": "Playwright Component Testing for Vue2",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
"./package.json": "./package.json"
|
"./package.json": "./package.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.46.0-next",
|
"@playwright/experimental-ct-core": "1.46.1",
|
||||||
"@vitejs/plugin-vue2": "^2.2.0"
|
"@vitejs/plugin-vue2": "^2.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "playwright-firefox",
|
"name": "playwright-firefox",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.1",
|
||||||
"description": "A high-level API to automate Firefox",
|
"description": "A high-level API to automate Firefox",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -30,6 +30,6 @@
|
||||||
"install": "node install.js"
|
"install": "node install.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.46.0-next"
|
"playwright-core": "1.46.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/test",
|
"name": "@playwright/test",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.1",
|
||||||
"description": "A high-level API to automate web browsers",
|
"description": "A high-level API to automate web browsers",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -30,6 +30,6 @@
|
||||||
},
|
},
|
||||||
"scripts": {},
|
"scripts": {},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright": "1.46.0-next"
|
"playwright": "1.46.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "playwright-webkit",
|
"name": "playwright-webkit",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.1",
|
||||||
"description": "A high-level API to automate WebKit",
|
"description": "A high-level API to automate WebKit",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -30,6 +30,6 @@
|
||||||
"install": "node install.js"
|
"install": "node install.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.46.0-next"
|
"playwright-core": "1.46.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "playwright",
|
"name": "playwright",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.1",
|
||||||
"description": "A high-level API to automate web browsers",
|
"description": "A high-level API to automate web browsers",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -58,7 +58,7 @@
|
||||||
},
|
},
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.46.0-next"
|
"playwright-core": "1.46.1"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"fsevents": "2.3.2"
|
"fsevents": "2.3.2"
|
||||||
|
|
|
||||||
|
|
@ -261,6 +261,7 @@ const playwrightFixtures: Fixtures<TestFixtures, WorkerFixtures> = ({
|
||||||
title: renderApiCall(apiName, params),
|
title: renderApiCall(apiName, params),
|
||||||
apiName,
|
apiName,
|
||||||
params,
|
params,
|
||||||
|
canNestByTime: true,
|
||||||
});
|
});
|
||||||
userData.userObject = step;
|
userData.userObject = step;
|
||||||
out.stepId = step.stepId;
|
out.stepId = step.stepId;
|
||||||
|
|
|
||||||
|
|
@ -136,10 +136,10 @@ export async function createRootSuite(testRun: TestRun, errors: TestError[], sho
|
||||||
|
|
||||||
// Filter file suites for all projects.
|
// Filter file suites for all projects.
|
||||||
for (const [project, fileSuites] of testRun.projectSuites) {
|
for (const [project, fileSuites] of testRun.projectSuites) {
|
||||||
const filteredFileSuites = additionalFileMatcher ? fileSuites.filter(fileSuite => additionalFileMatcher(fileSuite.location!.file)) : fileSuites;
|
const projectSuite = createProjectSuite(project, fileSuites);
|
||||||
const projectSuite = createProjectSuite(project, filteredFileSuites);
|
|
||||||
projectSuites.set(project, projectSuite);
|
projectSuites.set(project, projectSuite);
|
||||||
const filteredProjectSuite = filterProjectSuite(projectSuite, { cliFileFilters, cliTitleMatcher, testIdMatcher: config.testIdMatcher });
|
|
||||||
|
const filteredProjectSuite = filterProjectSuite(projectSuite, { cliFileFilters, cliTitleMatcher, testIdMatcher: config.testIdMatcher, additionalFileMatcher });
|
||||||
filteredProjectSuites.set(project, filteredProjectSuite);
|
filteredProjectSuites.set(project, filteredProjectSuite);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -200,8 +200,8 @@ export async function createRootSuite(testRun: TestRun, errors: TestError[], sho
|
||||||
const projectClosure = new Map(buildProjectsClosure(rootSuite.suites.map(suite => suite._fullProject!)));
|
const projectClosure = new Map(buildProjectsClosure(rootSuite.suites.map(suite => suite._fullProject!)));
|
||||||
|
|
||||||
// Clone file suites for dependency projects.
|
// Clone file suites for dependency projects.
|
||||||
for (const project of projectClosure.keys()) {
|
for (const [project, level] of projectClosure.entries()) {
|
||||||
if (projectClosure.get(project) === 'dependency')
|
if (level === 'dependency')
|
||||||
rootSuite._prependSuite(buildProjectSuite(project, projectSuites.get(project)!));
|
rootSuite._prependSuite(buildProjectSuite(project, projectSuites.get(project)!));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -225,9 +225,9 @@ function createProjectSuite(project: FullProjectInternal, fileSuites: Suite[]):
|
||||||
return projectSuite;
|
return projectSuite;
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterProjectSuite(projectSuite: Suite, options: { cliFileFilters: TestFileFilter[], cliTitleMatcher?: Matcher, testIdMatcher?: Matcher }): Suite {
|
function filterProjectSuite(projectSuite: Suite, options: { cliFileFilters: TestFileFilter[], cliTitleMatcher?: Matcher, testIdMatcher?: Matcher, additionalFileMatcher?: Matcher }): Suite {
|
||||||
// Fast path.
|
// Fast path.
|
||||||
if (!options.cliFileFilters.length && !options.cliTitleMatcher && !options.testIdMatcher)
|
if (!options.cliFileFilters.length && !options.cliTitleMatcher && !options.testIdMatcher && !options.additionalFileMatcher)
|
||||||
return projectSuite;
|
return projectSuite;
|
||||||
|
|
||||||
const result = projectSuite._deepClone();
|
const result = projectSuite._deepClone();
|
||||||
|
|
@ -238,6 +238,8 @@ function filterProjectSuite(projectSuite: Suite, options: { cliFileFilters: Test
|
||||||
filterTestsRemoveEmptySuites(result, (test: TestCase) => {
|
filterTestsRemoveEmptySuites(result, (test: TestCase) => {
|
||||||
if (options.cliTitleMatcher && !options.cliTitleMatcher(test._grepTitle()))
|
if (options.cliTitleMatcher && !options.cliTitleMatcher(test._grepTitle()))
|
||||||
return false;
|
return false;
|
||||||
|
if (options.additionalFileMatcher && !options.additionalFileMatcher(test.location.file))
|
||||||
|
return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,18 @@ export async function detectChangedTestFiles(baseCommit: string, configDir: stri
|
||||||
).split('\n').filter(Boolean);
|
).split('\n').filter(Boolean);
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
const error = _error as childProcess.SpawnSyncReturns<string>;
|
const error = _error as childProcess.SpawnSyncReturns<string>;
|
||||||
|
|
||||||
|
const unknownRevision = error.output.some(line => line?.includes('unknown revision'));
|
||||||
|
if (unknownRevision) {
|
||||||
|
const isShallowClone = childProcess.execSync('git rev-parse --is-shallow-repository', { encoding: 'utf-8', stdio: 'pipe' }).trim() === 'true';
|
||||||
|
if (isShallowClone) {
|
||||||
|
throw new Error([
|
||||||
|
`The repository is a shallow clone and does not have '${baseCommit}' available locally.`,
|
||||||
|
`Note that GitHub Actions checkout is shallow by default: https://github.com/actions/checkout`
|
||||||
|
].join('\n'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error([
|
throw new Error([
|
||||||
`Cannot detect changed files for --only-changed mode:`,
|
`Cannot detect changed files for --only-changed mode:`,
|
||||||
`git ${command}`,
|
`git ${command}`,
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ export interface TestStepInternal {
|
||||||
complete(result: { error?: Error, attachments?: Attachment[] }): void;
|
complete(result: { error?: Error, attachments?: Attachment[] }): void;
|
||||||
stepId: string;
|
stepId: string;
|
||||||
title: string;
|
title: string;
|
||||||
category: 'hook' | 'fixture' | 'test.step' | 'expect' | string;
|
category: 'hook' | 'fixture' | 'test.step' | 'expect' | 'attach' | string;
|
||||||
location?: Location;
|
location?: Location;
|
||||||
boxedStack?: StackFrame[];
|
boxedStack?: StackFrame[];
|
||||||
steps: TestStepInternal[];
|
steps: TestStepInternal[];
|
||||||
|
|
@ -44,6 +44,9 @@ export interface TestStepInternal {
|
||||||
infectParentStepsWithError?: boolean;
|
infectParentStepsWithError?: boolean;
|
||||||
box?: boolean;
|
box?: boolean;
|
||||||
isStage?: boolean;
|
isStage?: boolean;
|
||||||
|
// TODO: this сould be decided based on the category, but pw:api
|
||||||
|
// is from a different abstraction layer.
|
||||||
|
canNestByTime?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TestStage = {
|
export type TestStage = {
|
||||||
|
|
@ -252,7 +255,7 @@ export class TestInfoImpl implements TestInfo {
|
||||||
parentStep = this._findLastStageStep();
|
parentStep = this._findLastStageStep();
|
||||||
} else {
|
} else {
|
||||||
parentStep = zones.zoneData<TestStepInternal>('stepZone');
|
parentStep = zones.zoneData<TestStepInternal>('stepZone');
|
||||||
if (!parentStep && data.category !== 'test.step') {
|
if (!parentStep && data.canNestByTime) {
|
||||||
// API steps (but not test.step calls) can be nested by time, instead of by stack.
|
// API steps (but not test.step calls) can be nested by time, instead of by stack.
|
||||||
// However, do not nest chains of route.continue by checking the title.
|
// However, do not nest chains of route.continue by checking the title.
|
||||||
parentStep = this._findLastNonFinishedStep(step => step.title !== data.title);
|
parentStep = this._findLastNonFinishedStep(step => step.title !== data.title);
|
||||||
|
|
|
||||||
4
packages/playwright/types/test.d.ts
vendored
4
packages/playwright/types/test.d.ts
vendored
|
|
@ -4815,9 +4815,9 @@ export type Fixtures<T extends KeyValue = {}, W extends KeyValue = {}, PT extend
|
||||||
} & {
|
} & {
|
||||||
[K in keyof PT]?: TestFixtureValue<PT[K], T & W & PT & PW> | [TestFixtureValue<PT[K], T & W & PT & PW>, { scope: 'test', timeout?: number | undefined, title?: string, box?: boolean }];
|
[K in keyof PT]?: TestFixtureValue<PT[K], T & W & PT & PW> | [TestFixtureValue<PT[K], T & W & PT & PW>, { scope: 'test', timeout?: number | undefined, title?: string, box?: boolean }];
|
||||||
} & {
|
} & {
|
||||||
[K in Exclude<keyof W, keyof PW>]?: [WorkerFixtureValue<W[K], W & PW>, { scope: 'worker', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }];
|
[K in keyof W]?: [WorkerFixtureValue<W[K], W & PW>, { scope: 'worker', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }];
|
||||||
} & {
|
} & {
|
||||||
[K in Exclude<keyof T, keyof PT>]?: TestFixtureValue<T[K], T & W & PT & PW> | [TestFixtureValue<T[K], T & W & PT & PW>, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }];
|
[K in keyof T]?: TestFixtureValue<T[K], T & W & PT & PW> | [TestFixtureValue<T[K], T & W & PT & PW>, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }];
|
||||||
};
|
};
|
||||||
|
|
||||||
type BrowserName = 'chromium' | 'firefox' | 'webkit';
|
type BrowserName = 'chromium' | 'firefox' | 'webkit';
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { escapeHTMLAttribute, escapeHTML } from '@isomorphic/stringUtils';
|
||||||
import type { FrameSnapshot, NodeNameAttributesChildNodesSnapshot, NodeSnapshot, RenderedFrameSnapshot, ResourceSnapshot, SubtreeReferenceSnapshot } from '@trace/snapshot';
|
import type { FrameSnapshot, NodeNameAttributesChildNodesSnapshot, NodeSnapshot, RenderedFrameSnapshot, ResourceSnapshot, SubtreeReferenceSnapshot } from '@trace/snapshot';
|
||||||
|
|
||||||
function isNodeNameAttributesChildNodesSnapshot(n: NodeSnapshot): n is NodeNameAttributesChildNodesSnapshot {
|
function isNodeNameAttributesChildNodesSnapshot(n: NodeSnapshot): n is NodeNameAttributesChildNodesSnapshot {
|
||||||
|
|
@ -57,7 +58,7 @@ export class SnapshotRenderer {
|
||||||
// Old snapshotter was sending lower-case.
|
// Old snapshotter was sending lower-case.
|
||||||
if (parentTag === 'STYLE' || parentTag === 'style')
|
if (parentTag === 'STYLE' || parentTag === 'style')
|
||||||
return rewriteURLsInStyleSheetForCustomProtocol(n);
|
return rewriteURLsInStyleSheetForCustomProtocol(n);
|
||||||
return escapeText(n);
|
return escapeHTML(n);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(n as any)._string) {
|
if (!(n as any)._string) {
|
||||||
|
|
@ -106,7 +107,7 @@ export class SnapshotRenderer {
|
||||||
attrValue = 'link://' + value;
|
attrValue = 'link://' + value;
|
||||||
else if (attr.toLowerCase() === 'href' || attr.toLowerCase() === 'src' || attr === kCurrentSrcAttribute)
|
else if (attr.toLowerCase() === 'href' || attr.toLowerCase() === 'src' || attr === kCurrentSrcAttribute)
|
||||||
attrValue = rewriteURLForCustomProtocol(value);
|
attrValue = rewriteURLForCustomProtocol(value);
|
||||||
builder.push(' ', attrName, '="', escapeAttribute(attrValue), '"');
|
builder.push(' ', attrName, '="', escapeHTMLAttribute(attrValue), '"');
|
||||||
}
|
}
|
||||||
builder.push('>');
|
builder.push('>');
|
||||||
for (const child of children)
|
for (const child of children)
|
||||||
|
|
@ -193,14 +194,6 @@ export class SnapshotRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
const autoClosing = new Set(['AREA', 'BASE', 'BR', 'COL', 'COMMAND', 'EMBED', 'HR', 'IMG', 'INPUT', 'KEYGEN', 'LINK', 'MENUITEM', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR']);
|
const autoClosing = new Set(['AREA', 'BASE', 'BR', 'COL', 'COMMAND', 'EMBED', 'HR', 'IMG', 'INPUT', 'KEYGEN', 'LINK', 'MENUITEM', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR']);
|
||||||
const escaped = { '&': '&', '<': '<', '>': '>', '"': '"', '\'': ''' };
|
|
||||||
|
|
||||||
function escapeAttribute(s: string): string {
|
|
||||||
return s.replace(/[&<>"']/ug, char => (escaped as any)[char]);
|
|
||||||
}
|
|
||||||
function escapeText(s: string): string {
|
|
||||||
return s.replace(/[&<]/ug, char => (escaped as any)[char]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function snapshotNodes(snapshot: FrameSnapshot): NodeSnapshot[] {
|
function snapshotNodes(snapshot: FrameSnapshot): NodeSnapshot[] {
|
||||||
if (!(snapshot as any)._nodes) {
|
if (!(snapshot as any)._nodes) {
|
||||||
|
|
|
||||||
|
|
@ -130,13 +130,12 @@ async function doFetch(event: FetchEvent): Promise<Response> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (relativePath.startsWith('/sha1/')) {
|
if (relativePath.startsWith('/sha1/')) {
|
||||||
const download = url.searchParams.has('download');
|
|
||||||
// Sha1 for sources is based on the file path, can't load it of a random model.
|
// Sha1 for sources is based on the file path, can't load it of a random model.
|
||||||
const sha1 = relativePath.slice('/sha1/'.length);
|
const sha1 = relativePath.slice('/sha1/'.length);
|
||||||
for (const trace of loadedTraces.values()) {
|
for (const trace of loadedTraces.values()) {
|
||||||
const blob = await trace.traceModel.resourceForSha1(sha1);
|
const blob = await trace.traceModel.resourceForSha1(sha1);
|
||||||
if (blob)
|
if (blob)
|
||||||
return new Response(blob, { status: 200, headers: download ? downloadHeadersForAttachment(trace.traceModel, sha1) : undefined });
|
return new Response(blob, { status: 200, headers: downloadHeaders(url.searchParams) });
|
||||||
}
|
}
|
||||||
return new Response(null, { status: 404 });
|
return new Response(null, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
@ -157,14 +156,15 @@ async function doFetch(event: FetchEvent): Promise<Response> {
|
||||||
return snapshotServer.serveResource(lookupUrls, request.method, snapshotUrl);
|
return snapshotServer.serveResource(lookupUrls, request.method, snapshotUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
function downloadHeadersForAttachment(traceModel: TraceModel, sha1: string): Headers | undefined {
|
function downloadHeaders(searchParams: URLSearchParams): Headers | undefined {
|
||||||
const attachment = traceModel.attachmentForSha1(sha1);
|
const name = searchParams.get('dn');
|
||||||
if (!attachment)
|
const contentType = searchParams.get('dct');
|
||||||
|
if (!name)
|
||||||
return;
|
return;
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
headers.set('Content-Disposition', `attachment; filename="attachment"; filename*=UTF-8''${encodeURIComponent(attachment.name)}`);
|
headers.set('Content-Disposition', `attachment; filename="attachment"; filename*=UTF-8''${encodeURIComponent(name)}`);
|
||||||
if (attachment.contentType)
|
if (contentType)
|
||||||
headers.set('Content-Type', attachment.contentType);
|
headers.set('Content-Type', contentType);
|
||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type * as trace from '@trace/trace';
|
|
||||||
import { parseClientSideCallMetadata } from '../../../packages/playwright-core/src/utils/isomorphic/traceUtils';
|
import { parseClientSideCallMetadata } from '../../../packages/playwright-core/src/utils/isomorphic/traceUtils';
|
||||||
import type { ContextEntry } from './entries';
|
import type { ContextEntry } from './entries';
|
||||||
import { createEmptyContext } from './entries';
|
import { createEmptyContext } from './entries';
|
||||||
|
|
@ -34,7 +33,6 @@ export class TraceModel {
|
||||||
contextEntries: ContextEntry[] = [];
|
contextEntries: ContextEntry[] = [];
|
||||||
private _snapshotStorage: SnapshotStorage | undefined;
|
private _snapshotStorage: SnapshotStorage | undefined;
|
||||||
private _backend!: TraceModelBackend;
|
private _backend!: TraceModelBackend;
|
||||||
private _attachments = new Map<string, trace.AfterActionTraceEventAttachment>();
|
|
||||||
private _resourceToContentType = new Map<string, string>();
|
private _resourceToContentType = new Map<string, string>();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -64,7 +62,7 @@ export class TraceModel {
|
||||||
const contextEntry = createEmptyContext();
|
const contextEntry = createEmptyContext();
|
||||||
contextEntry.traceUrl = backend.traceURL();
|
contextEntry.traceUrl = backend.traceURL();
|
||||||
contextEntry.hasSource = hasSource;
|
contextEntry.hasSource = hasSource;
|
||||||
const modernizer = new TraceModernizer(contextEntry, this._snapshotStorage, this._attachments);
|
const modernizer = new TraceModernizer(contextEntry, this._snapshotStorage);
|
||||||
|
|
||||||
const trace = await this._backend.readText(ordinal + '.trace') || '';
|
const trace = await this._backend.readText(ordinal + '.trace') || '';
|
||||||
modernizer.appendTrace(trace);
|
modernizer.appendTrace(trace);
|
||||||
|
|
@ -121,10 +119,6 @@ export class TraceModel {
|
||||||
return new Blob([blob], { type: this._resourceToContentType.get(sha1) || 'application/octet-stream' });
|
return new Blob([blob], { type: this._resourceToContentType.get(sha1) || 'application/octet-stream' });
|
||||||
}
|
}
|
||||||
|
|
||||||
attachmentForSha1(sha1: string): trace.AfterActionTraceEventAttachment | undefined {
|
|
||||||
return this._attachments.get(sha1);
|
|
||||||
}
|
|
||||||
|
|
||||||
storage(): SnapshotStorage {
|
storage(): SnapshotStorage {
|
||||||
return this._snapshotStorage!;
|
return this._snapshotStorage!;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,17 +34,15 @@ const latestVersion: trace.VERSION = 7;
|
||||||
export class TraceModernizer {
|
export class TraceModernizer {
|
||||||
private _contextEntry: ContextEntry;
|
private _contextEntry: ContextEntry;
|
||||||
private _snapshotStorage: SnapshotStorage;
|
private _snapshotStorage: SnapshotStorage;
|
||||||
private _attachments: Map<string, trace.AfterActionTraceEventAttachment>;
|
|
||||||
private _actionMap = new Map<string, ActionEntry>();
|
private _actionMap = new Map<string, ActionEntry>();
|
||||||
private _version: number | undefined;
|
private _version: number | undefined;
|
||||||
private _pageEntries = new Map<string, PageEntry>();
|
private _pageEntries = new Map<string, PageEntry>();
|
||||||
private _jsHandles = new Map<string, { preview: string }>();
|
private _jsHandles = new Map<string, { preview: string }>();
|
||||||
private _consoleObjects = new Map<string, { type: string, text: string, location: { url: string, lineNumber: number, columnNumber: number }, args?: { preview: string, value: string }[] }>();
|
private _consoleObjects = new Map<string, { type: string, text: string, location: { url: string, lineNumber: number, columnNumber: number }, args?: { preview: string, value: string }[] }>();
|
||||||
|
|
||||||
constructor(contextEntry: ContextEntry, snapshotStorage: SnapshotStorage, attachments: Map<string, trace.AfterActionTraceEventAttachment>) {
|
constructor(contextEntry: ContextEntry, snapshotStorage: SnapshotStorage) {
|
||||||
this._contextEntry = contextEntry;
|
this._contextEntry = contextEntry;
|
||||||
this._snapshotStorage = snapshotStorage;
|
this._snapshotStorage = snapshotStorage;
|
||||||
this._attachments = attachments;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
appendTrace(trace: string) {
|
appendTrace(trace: string) {
|
||||||
|
|
@ -129,8 +127,6 @@ export class TraceModernizer {
|
||||||
existing!.attachments = event.attachments;
|
existing!.attachments = event.attachments;
|
||||||
if (event.point)
|
if (event.point)
|
||||||
existing!.point = event.point;
|
existing!.point = event.point;
|
||||||
for (const attachment of event.attachments?.filter(a => a.sha1) || [])
|
|
||||||
this._attachments.set(attachment.sha1!, attachment);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'action': {
|
case 'action': {
|
||||||
|
|
|
||||||
28
packages/trace-viewer/src/ui/annotationsTab.css
Normal file
28
packages/trace-viewer/src/ui/annotationsTab.css
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) Microsoft Corporation.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.annotations-tab {
|
||||||
|
flex: auto;
|
||||||
|
line-height: 24px;
|
||||||
|
white-space: pre;
|
||||||
|
overflow: auto;
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.annotation-item {
|
||||||
|
margin: 4px 8px;
|
||||||
|
text-wrap: wrap;
|
||||||
|
}
|
||||||
39
packages/trace-viewer/src/ui/annotationsTab.tsx
Normal file
39
packages/trace-viewer/src/ui/annotationsTab.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import './annotationsTab.css';
|
||||||
|
import { PlaceholderPanel } from './placeholderPanel';
|
||||||
|
import { linkifyText } from '@web/renderUtils';
|
||||||
|
|
||||||
|
type Annotation = { type: string; description?: string; };
|
||||||
|
|
||||||
|
export const AnnotationsTab: React.FunctionComponent<{
|
||||||
|
annotations: Annotation[],
|
||||||
|
}> = ({ annotations }) => {
|
||||||
|
|
||||||
|
if (!annotations.length)
|
||||||
|
return <PlaceholderPanel text='No annotations' />;
|
||||||
|
|
||||||
|
return <div className='annotations-tab'>
|
||||||
|
{annotations.map((annotation, i) => {
|
||||||
|
return <div className='annotation-item' key={`annotation-${i}`}>
|
||||||
|
<span style={{ fontWeight: 'bold' }}>{annotation.type}</span>
|
||||||
|
{annotation.description && <span>: {linkifyText(annotation.description)}</span>}
|
||||||
|
</div>;
|
||||||
|
})}
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
@ -26,13 +26,14 @@
|
||||||
padding-left: 6px;
|
padding-left: 6px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
color: var(--vscode-sideBarTitle-foreground);
|
color: var(--vscode-sideBarTitle-foreground);
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachments-section:not(:first-child) {
|
.attachments-section:not(:first-child) {
|
||||||
border-top: 1px solid var(--vscode-panel-border);
|
border-top: 1px solid var(--vscode-panel-border);
|
||||||
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment-item {
|
.attachment-item {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import type { AfterActionTraceEventAttachment } from '@trace/trace';
|
||||||
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
|
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
|
||||||
import { isTextualMimeType } from '@isomorphic/mimeType';
|
import { isTextualMimeType } from '@isomorphic/mimeType';
|
||||||
import { Expandable } from '@web/components/expandable';
|
import { Expandable } from '@web/components/expandable';
|
||||||
|
import { linkifyText } from '@web/renderUtils';
|
||||||
|
|
||||||
type Attachment = AfterActionTraceEventAttachment & { traceUrl: string };
|
type Attachment = AfterActionTraceEventAttachment & { traceUrl: string };
|
||||||
|
|
||||||
|
|
@ -36,6 +37,7 @@ const ExpandableAttachment: React.FunctionComponent<ExpandableAttachmentProps> =
|
||||||
const [placeholder, setPlaceholder] = React.useState<string | null>(null);
|
const [placeholder, setPlaceholder] = React.useState<string | null>(null);
|
||||||
|
|
||||||
const isTextAttachment = isTextualMimeType(attachment.contentType);
|
const isTextAttachment = isTextualMimeType(attachment.contentType);
|
||||||
|
const hasContent = !!attachment.sha1 || !!attachment.path;
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (expanded && attachmentText === null && placeholder === null) {
|
if (expanded && attachmentText === null && placeholder === null) {
|
||||||
|
|
@ -49,11 +51,11 @@ const ExpandableAttachment: React.FunctionComponent<ExpandableAttachmentProps> =
|
||||||
}
|
}
|
||||||
}, [expanded, attachmentText, placeholder, attachment]);
|
}, [expanded, attachmentText, placeholder, attachment]);
|
||||||
|
|
||||||
const title = <>
|
const title = <span style={{ marginLeft: 5 }}>
|
||||||
{attachment.name} <a style={{ marginLeft: 5 }} href={attachmentURL(attachment) + '&download'}>download</a>
|
{linkifyText(attachment.name)} {hasContent && <a style={{ marginLeft: 5 }} href={downloadURL(attachment)}>download</a>}
|
||||||
</>;
|
</span>;
|
||||||
|
|
||||||
if (!isTextAttachment)
|
if (!isTextAttachment || !hasContent)
|
||||||
return <div style={{ marginLeft: 20 }}>{title}</div>;
|
return <div style={{ marginLeft: 20 }}>{title}</div>;
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
|
|
@ -63,6 +65,8 @@ const ExpandableAttachment: React.FunctionComponent<ExpandableAttachmentProps> =
|
||||||
{expanded && attachmentText !== null && <CodeMirrorWrapper
|
{expanded && attachmentText !== null && <CodeMirrorWrapper
|
||||||
text={attachmentText}
|
text={attachmentText}
|
||||||
readOnly
|
readOnly
|
||||||
|
mimeType={attachment.contentType}
|
||||||
|
linkify={true}
|
||||||
lineNumbers={true}
|
lineNumbers={true}
|
||||||
wrapLines={false}>
|
wrapLines={false}>
|
||||||
</CodeMirrorWrapper>}
|
</CodeMirrorWrapper>}
|
||||||
|
|
@ -93,8 +97,8 @@ export const AttachmentsTab: React.FunctionComponent<{
|
||||||
const entry = diffMap.get(name) || { expected: undefined, actual: undefined, diff: undefined };
|
const entry = diffMap.get(name) || { expected: undefined, actual: undefined, diff: undefined };
|
||||||
entry[type] = attachment;
|
entry[type] = attachment;
|
||||||
diffMap.set(name, entry);
|
diffMap.set(name, entry);
|
||||||
}
|
attachments.delete(attachment);
|
||||||
if (attachment.contentType.startsWith('image/')) {
|
} else if (attachment.contentType.startsWith('image/')) {
|
||||||
screenshots.add(attachment);
|
screenshots.add(attachment);
|
||||||
attachments.delete(attachment);
|
attachments.delete(attachment);
|
||||||
}
|
}
|
||||||
|
|
@ -109,11 +113,11 @@ export const AttachmentsTab: React.FunctionComponent<{
|
||||||
{[...diffMap.values()].map(({ expected, actual, diff }) => {
|
{[...diffMap.values()].map(({ expected, actual, diff }) => {
|
||||||
return <>
|
return <>
|
||||||
{expected && actual && <div className='attachments-section'>Image diff</div>}
|
{expected && actual && <div className='attachments-section'>Image diff</div>}
|
||||||
{expected && actual && <ImageDiffView diff={{
|
{expected && actual && <ImageDiffView noTargetBlank={true} diff={{
|
||||||
name: 'Image diff',
|
name: 'Image diff',
|
||||||
expected: { attachment: { ...expected, path: attachmentURL(expected) }, title: 'Expected' },
|
expected: { attachment: { ...expected, path: downloadURL(expected) }, title: 'Expected' },
|
||||||
actual: { attachment: { ...actual, path: attachmentURL(actual) } },
|
actual: { attachment: { ...actual, path: downloadURL(actual) } },
|
||||||
diff: diff ? { attachment: { ...diff, path: attachmentURL(diff) } } : undefined,
|
diff: diff ? { attachment: { ...diff, path: downloadURL(diff) } } : undefined,
|
||||||
}} />}
|
}} />}
|
||||||
</>;
|
</>;
|
||||||
})}
|
})}
|
||||||
|
|
@ -134,8 +138,19 @@ export const AttachmentsTab: React.FunctionComponent<{
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function attachmentURL(attachment: Attachment) {
|
function attachmentURL(attachment: Attachment, queryParams: Record<string, string> = {}) {
|
||||||
if (attachment.sha1)
|
const params = new URLSearchParams(queryParams);
|
||||||
return 'sha1/' + attachment.sha1 + '?trace=' + encodeURIComponent(attachment.traceUrl);
|
if (attachment.sha1) {
|
||||||
return 'file?path=' + encodeURIComponent(attachment.path!);
|
params.set('trace', attachment.traceUrl);
|
||||||
|
return 'sha1/' + attachment.sha1 + '?' + params.toString();
|
||||||
|
}
|
||||||
|
params.set('path', attachment.path!);
|
||||||
|
return 'file?' + params.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadURL(attachment: Attachment) {
|
||||||
|
const params = { dn: attachment.name } as Record<string, string>;
|
||||||
|
if (attachment.contentType)
|
||||||
|
params.dct = attachment.contentType;
|
||||||
|
return attachmentURL(attachment, params);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,16 +55,16 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
max-height: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.call-line .copy-icon {
|
.call-line:not(:hover) .toolbar-button.copy {
|
||||||
display: none;
|
display: none;
|
||||||
margin-left: 5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.call-line:hover .copy-icon {
|
.call-line .toolbar-button.copy {
|
||||||
display: block;
|
margin-left: 5px;
|
||||||
cursor: pointer;
|
transform: scale(0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
.call-value {
|
.call-value {
|
||||||
|
|
|
||||||
|
|
@ -15,23 +15,24 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import { ToolbarButton } from '@web/components/toolbarButton';
|
||||||
|
|
||||||
export const CopyToClipboard: React.FunctionComponent<{
|
export const CopyToClipboard: React.FunctionComponent<{
|
||||||
value: string,
|
value: string,
|
||||||
description?: string,
|
description?: string,
|
||||||
}> = ({ value, description }) => {
|
}> = ({ value, description }) => {
|
||||||
const [iconClassName, setIconClassName] = React.useState('codicon-clippy');
|
const [icon, setIcon] = React.useState('copy');
|
||||||
|
|
||||||
const handleCopy = React.useCallback(() => {
|
const handleCopy = React.useCallback(() => {
|
||||||
navigator.clipboard.writeText(value).then(() => {
|
navigator.clipboard.writeText(value).then(() => {
|
||||||
setIconClassName('codicon-check');
|
setIcon('check');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIconClassName('codicon-clippy');
|
setIcon('copy');
|
||||||
}, 3000);
|
}, 3000);
|
||||||
}, () => {
|
}, () => {
|
||||||
setIconClassName('codicon-close');
|
setIcon('close');
|
||||||
});
|
});
|
||||||
|
|
||||||
}, [value]);
|
}, [value]);
|
||||||
return <span title={description ? description : 'Copy'} className={`copy-icon codicon ${iconClassName}`} onClick={handleCopy}/>;
|
return <ToolbarButton title={description ? description : 'Copy'} icon={icon} onClick={handleCopy}/>;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,8 @@ export const MetadataView: React.FunctionComponent<{
|
||||||
}> = ({ model }) => {
|
}> = ({ model }) => {
|
||||||
if (!model)
|
if (!model)
|
||||||
return <></>;
|
return <></>;
|
||||||
return <div className='metadata-view vbox'>
|
|
||||||
|
return <div data-testid='metadata-view' className='vbox' style={{ flexShrink: 0 }}>
|
||||||
<div className='call-section' style={{ paddingTop: 2 }}>Time</div>
|
<div className='call-section' style={{ paddingTop: 2 }}>Time</div>
|
||||||
{!!model.wallTime && <div className='call-line'>start time:<span className='call-value datetime' title={new Date(model.wallTime).toLocaleString()}>{new Date(model.wallTime).toLocaleString()}</span></div>}
|
{!!model.wallTime && <div className='call-line'>start time:<span className='call-value datetime' title={new Date(model.wallTime).toLocaleString()}>{new Date(model.wallTime).toLocaleString()}</span></div>}
|
||||||
<div className='call-line'>duration:<span className='call-value number' title={msToString(model.endTime - model.startTime)}>{msToString(model.endTime - model.startTime)}</span></div>
|
<div className='call-line'>duration:<span className='call-value number' title={msToString(model.endTime - model.startTime)}>{msToString(model.endTime - model.startTime)}</span></div>
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,8 @@ const eventsSymbol = Symbol('events');
|
||||||
export type SourceLocation = {
|
export type SourceLocation = {
|
||||||
file: string;
|
file: string;
|
||||||
line: number;
|
line: number;
|
||||||
source: SourceModel;
|
column: number;
|
||||||
|
source?: SourceModel;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SourceModel = {
|
export type SourceModel = {
|
||||||
|
|
@ -408,3 +409,30 @@ function collectSources(actions: trace.ActionTraceEvent[], errorDescriptors: Err
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const kRouteMethods = new Set([
|
||||||
|
'page.route',
|
||||||
|
'page.routefromhar',
|
||||||
|
'page.unroute',
|
||||||
|
'page.unrouteall',
|
||||||
|
'browsercontext.route',
|
||||||
|
'browsercontext.routefromhar',
|
||||||
|
'browsercontext.unroute',
|
||||||
|
'browsercontext.unrouteall',
|
||||||
|
]);
|
||||||
|
{
|
||||||
|
// .NET adds async suffix.
|
||||||
|
for (const method of [...kRouteMethods])
|
||||||
|
kRouteMethods.add(method + 'async');
|
||||||
|
// Python methods which contain underscores.
|
||||||
|
for (const method of [
|
||||||
|
'page.route_from_har',
|
||||||
|
'page.unroute_all',
|
||||||
|
'context.route_from_har',
|
||||||
|
'context.unroute_all',
|
||||||
|
])
|
||||||
|
kRouteMethods.add(method);
|
||||||
|
}
|
||||||
|
export function isRouteAction(action: ActionTraceEventInContext) {
|
||||||
|
return action.class === 'Route' || kRouteMethods.has(action.apiName.toLowerCase());
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,3 +61,27 @@
|
||||||
.tab-network .tabbed-pane-tab.selected {
|
.tab-network .tabbed-pane-tab.selected {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.green-circle::before,
|
||||||
|
.red-circle::before,
|
||||||
|
.yellow-circle::before {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-right: 2px;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.green-circle::before {
|
||||||
|
background-color: var(--vscode-charts-green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.red-circle::before {
|
||||||
|
background-color: var(--vscode-charts-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.yellow-circle::before {
|
||||||
|
background-color: var(--vscode-charts-yellow);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ import * as React from 'react';
|
||||||
import './networkResourceDetails.css';
|
import './networkResourceDetails.css';
|
||||||
import { TabbedPane } from '@web/components/tabbedPane';
|
import { TabbedPane } from '@web/components/tabbedPane';
|
||||||
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
|
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
|
||||||
import type { Language } from '@web/components/codeMirrorWrapper';
|
|
||||||
import { ToolbarButton } from '@web/components/toolbarButton';
|
import { ToolbarButton } from '@web/components/toolbarButton';
|
||||||
|
|
||||||
export const NetworkResourceDetails: React.FunctionComponent<{
|
export const NetworkResourceDetails: React.FunctionComponent<{
|
||||||
|
|
@ -55,19 +54,18 @@ export const NetworkResourceDetails: React.FunctionComponent<{
|
||||||
const RequestTab: React.FunctionComponent<{
|
const RequestTab: React.FunctionComponent<{
|
||||||
resource: ResourceSnapshot;
|
resource: ResourceSnapshot;
|
||||||
}> = ({ resource }) => {
|
}> = ({ resource }) => {
|
||||||
const [requestBody, setRequestBody] = React.useState<{ text: string, language?: Language } | null>(null);
|
const [requestBody, setRequestBody] = React.useState<{ text: string, mimeType?: string } | null>(null);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const readResources = async () => {
|
const readResources = async () => {
|
||||||
if (resource.request.postData) {
|
if (resource.request.postData) {
|
||||||
const requestContentTypeHeader = resource.request.headers.find(q => q.name === 'Content-Type');
|
const requestContentTypeHeader = resource.request.headers.find(q => q.name === 'Content-Type');
|
||||||
const requestContentType = requestContentTypeHeader ? requestContentTypeHeader.value : '';
|
const requestContentType = requestContentTypeHeader ? requestContentTypeHeader.value : '';
|
||||||
const language = mimeTypeToHighlighter(requestContentType);
|
|
||||||
if (resource.request.postData._sha1) {
|
if (resource.request.postData._sha1) {
|
||||||
const response = await fetch(`sha1/${resource.request.postData._sha1}`);
|
const response = await fetch(`sha1/${resource.request.postData._sha1}`);
|
||||||
setRequestBody({ text: formatBody(await response.text(), requestContentType), language });
|
setRequestBody({ text: formatBody(await response.text(), requestContentType), mimeType: requestContentType });
|
||||||
} else {
|
} else {
|
||||||
setRequestBody({ text: formatBody(resource.request.postData.text, requestContentType), language });
|
setRequestBody({ text: formatBody(resource.request.postData.text, requestContentType), mimeType: requestContentType });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setRequestBody(null);
|
setRequestBody(null);
|
||||||
|
|
@ -80,15 +78,14 @@ const RequestTab: React.FunctionComponent<{
|
||||||
<div className='network-request-details-header'>General</div>
|
<div className='network-request-details-header'>General</div>
|
||||||
<div className='network-request-details-url'>{`URL: ${resource.request.url}`}</div>
|
<div className='network-request-details-url'>{`URL: ${resource.request.url}`}</div>
|
||||||
<div className='network-request-details-general'>{`Method: ${resource.request.method}`}</div>
|
<div className='network-request-details-general'>{`Method: ${resource.request.method}`}</div>
|
||||||
<div className='network-request-details-general'>{`Status Code: ${
|
<div className='network-request-details-general' style={{ display: 'flex' }}>
|
||||||
resource.response.status >= 200 && resource.response.status < 400
|
Status Code: <span className={statusClass(resource.response.status)} style={{ display: 'inline-flex' }}>
|
||||||
? `🟢 ${resource.response.status} ${resource.response.statusText}`
|
{`${resource.response.status} ${resource.response.statusText}`}
|
||||||
: `🔴 ${resource.response.status} ${resource.response.statusText}`
|
</span></div>
|
||||||
}`}</div>
|
|
||||||
<div className='network-request-details-header'>Request Headers</div>
|
<div className='network-request-details-header'>Request Headers</div>
|
||||||
<div className='network-request-details-headers'>{resource.request.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}</div>
|
<div className='network-request-details-headers'>{resource.request.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}</div>
|
||||||
{requestBody && <div className='network-request-details-header'>Request Body</div>}
|
{requestBody && <div className='network-request-details-header'>Request Body</div>}
|
||||||
{requestBody && <CodeMirrorWrapper text={requestBody.text} language={requestBody.language} readOnly lineNumbers={true}/>}
|
{requestBody && <CodeMirrorWrapper text={requestBody.text} mimeType={requestBody.mimeType} readOnly lineNumbers={true}/>}
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -104,7 +101,7 @@ const ResponseTab: React.FunctionComponent<{
|
||||||
const BodyTab: React.FunctionComponent<{
|
const BodyTab: React.FunctionComponent<{
|
||||||
resource: ResourceSnapshot;
|
resource: ResourceSnapshot;
|
||||||
}> = ({ resource }) => {
|
}> = ({ resource }) => {
|
||||||
const [responseBody, setResponseBody] = React.useState<{ dataUrl?: string, text?: string, language?: Language } | null>(null);
|
const [responseBody, setResponseBody] = React.useState<{ dataUrl?: string, text?: string, mimeType?: string } | null>(null);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const readResources = async () => {
|
const readResources = async () => {
|
||||||
|
|
@ -119,8 +116,7 @@ const BodyTab: React.FunctionComponent<{
|
||||||
setResponseBody({ dataUrl: (await eventPromise).target.result });
|
setResponseBody({ dataUrl: (await eventPromise).target.result });
|
||||||
} else {
|
} else {
|
||||||
const formattedBody = formatBody(await response.text(), resource.response.content.mimeType);
|
const formattedBody = formatBody(await response.text(), resource.response.content.mimeType);
|
||||||
const language = mimeTypeToHighlighter(resource.response.content.mimeType);
|
setResponseBody({ text: formattedBody, mimeType: resource.response.content.mimeType });
|
||||||
setResponseBody({ text: formattedBody, language });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -131,10 +127,18 @@ const BodyTab: React.FunctionComponent<{
|
||||||
return <div className='network-request-details-tab'>
|
return <div className='network-request-details-tab'>
|
||||||
{!resource.response.content._sha1 && <div>Response body is not available for this request.</div>}
|
{!resource.response.content._sha1 && <div>Response body is not available for this request.</div>}
|
||||||
{responseBody && responseBody.dataUrl && <img draggable='false' src={responseBody.dataUrl} />}
|
{responseBody && responseBody.dataUrl && <img draggable='false' src={responseBody.dataUrl} />}
|
||||||
{responseBody && responseBody.text && <CodeMirrorWrapper text={responseBody.text} language={responseBody.language} readOnly lineNumbers={true}/>}
|
{responseBody && responseBody.text && <CodeMirrorWrapper text={responseBody.text} mimeType={responseBody.mimeType} readOnly lineNumbers={true}/>}
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function statusClass(statusCode: number): string {
|
||||||
|
if (statusCode < 300 || statusCode === 304)
|
||||||
|
return 'green-circle';
|
||||||
|
if (statusCode < 400)
|
||||||
|
return 'yellow-circle';
|
||||||
|
return 'red-circle';
|
||||||
|
}
|
||||||
|
|
||||||
function formatBody(body: string | null, contentType: string): string {
|
function formatBody(body: string | null, contentType: string): string {
|
||||||
if (body === null)
|
if (body === null)
|
||||||
return 'Loading...';
|
return 'Loading...';
|
||||||
|
|
@ -156,12 +160,3 @@ function formatBody(body: string | null, contentType: string): string {
|
||||||
|
|
||||||
return bodyStr;
|
return bodyStr;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mimeTypeToHighlighter(mimeType: string): Language | undefined {
|
|
||||||
if (mimeType.includes('javascript') || mimeType.includes('json'))
|
|
||||||
return 'javascript';
|
|
||||||
if (mimeType.includes('html'))
|
|
||||||
return 'html';
|
|
||||||
if (mimeType.includes('css'))
|
|
||||||
return 'css';
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,10 @@
|
||||||
background-color: var(--vscode-sideBar-background);
|
background-color: var(--vscode-sideBar-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.snapshot-tab .toolbar .pick-locator {
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.snapshot-controls {
|
.snapshot-controls {
|
||||||
flex: none;
|
flex: none;
|
||||||
background-color: var(--vscode-sideBar-background);
|
background-color: var(--vscode-sideBar-background);
|
||||||
|
|
@ -102,29 +106,6 @@ iframe.snapshot-visible[name=snapshot] {
|
||||||
padding: 50px;
|
padding: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popout-icon {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
color: var(--vscode-sideBarTitle-foreground);
|
|
||||||
font-size: 14px;
|
|
||||||
z-index: 100;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popout-icon:not(.popout-disabled):hover {
|
|
||||||
color: var(--vscode-foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
.popout-icon.popout-disabled {
|
|
||||||
opacity: var(--vscode-disabledForeground);
|
|
||||||
}
|
|
||||||
|
|
||||||
.snapshot-tab .cm-wrapper {
|
.snapshot-tab .cm-wrapper {
|
||||||
line-height: 23px;
|
line-height: 23px;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
|
|
|
||||||
|
|
@ -181,6 +181,7 @@ export const SnapshotTab: React.FunctionComponent<{
|
||||||
iframe={iframeRef1.current}
|
iframe={iframeRef1.current}
|
||||||
iteration={loadingRef.current.iteration} />
|
iteration={loadingRef.current.iteration} />
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
|
<ToolbarButton className='pick-locator' title='Pick locator' icon='target' toggled={isInspecting} onClick={() => setIsInspecting(!isInspecting)} />
|
||||||
{['action', 'before', 'after'].map(tab => {
|
{['action', 'before', 'after'].map(tab => {
|
||||||
return <TabbedPaneTab
|
return <TabbedPaneTab
|
||||||
id={tab}
|
id={tab}
|
||||||
|
|
|
||||||
|
|
@ -23,21 +23,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.source-tab-file-name {
|
.source-tab-file-name {
|
||||||
height: 24px;
|
padding-left: 8px;
|
||||||
margin-left: 8px;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: var(--vscode-breadcrumb-background);
|
flex: 1 1 auto;
|
||||||
box-shadow: var(--vscode-scrollbar-shadow) 0 6px 6px -6px;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.source-tab-file-name .copy-icon.codicon {
|
|
||||||
display: block;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.source-copy-to-clipboard {
|
|
||||||
display: block;
|
|
||||||
padding-left: 4px;
|
|
||||||
}
|
|
||||||
|
|
@ -24,6 +24,8 @@ import type { SourceHighlight } from '@web/components/codeMirrorWrapper';
|
||||||
import type { SourceLocation, SourceModel } from './modelUtil';
|
import type { SourceLocation, SourceModel } from './modelUtil';
|
||||||
import type { StackFrame } from '@protocol/channels';
|
import type { StackFrame } from '@protocol/channels';
|
||||||
import { CopyToClipboard } from './copyToClipboard';
|
import { CopyToClipboard } from './copyToClipboard';
|
||||||
|
import { ToolbarButton } from '@web/components/toolbarButton';
|
||||||
|
import { Toolbar } from '@web/components/toolbar';
|
||||||
|
|
||||||
export const SourceTab: React.FunctionComponent<{
|
export const SourceTab: React.FunctionComponent<{
|
||||||
stack: StackFrame[] | undefined,
|
stack: StackFrame[] | undefined,
|
||||||
|
|
@ -31,7 +33,8 @@ export const SourceTab: React.FunctionComponent<{
|
||||||
sources: Map<string, SourceModel>,
|
sources: Map<string, SourceModel>,
|
||||||
rootDir?: string,
|
rootDir?: string,
|
||||||
fallbackLocation?: SourceLocation,
|
fallbackLocation?: SourceLocation,
|
||||||
}> = ({ stack, sources, rootDir, fallbackLocation, stackFrameLocation }) => {
|
onOpenExternally?: (location: SourceLocation) => void,
|
||||||
|
}> = ({ stack, sources, rootDir, fallbackLocation, stackFrameLocation, onOpenExternally }) => {
|
||||||
const [lastStack, setLastStack] = React.useState<StackFrame[] | undefined>();
|
const [lastStack, setLastStack] = React.useState<StackFrame[] | undefined>();
|
||||||
const [selectedFrame, setSelectedFrame] = React.useState<number>(0);
|
const [selectedFrame, setSelectedFrame] = React.useState<number>(0);
|
||||||
|
|
||||||
|
|
@ -42,7 +45,7 @@ export const SourceTab: React.FunctionComponent<{
|
||||||
}
|
}
|
||||||
}, [stack, lastStack, setLastStack, setSelectedFrame]);
|
}, [stack, lastStack, setLastStack, setSelectedFrame]);
|
||||||
|
|
||||||
const { source, highlight, targetLine, fileName } = useAsyncMemo<{ source: SourceModel, targetLine?: number, fileName?: string, highlight: SourceHighlight[] }>(async () => {
|
const { source, highlight, targetLine, fileName, location } = useAsyncMemo<{ source: SourceModel, targetLine?: number, fileName?: string, highlight: SourceHighlight[], location?: SourceLocation }>(async () => {
|
||||||
const actionLocation = stack?.[selectedFrame];
|
const actionLocation = stack?.[selectedFrame];
|
||||||
const shouldUseFallback = !actionLocation?.file;
|
const shouldUseFallback = !actionLocation?.file;
|
||||||
if (shouldUseFallback && !fallbackLocation)
|
if (shouldUseFallback && !fallbackLocation)
|
||||||
|
|
@ -56,6 +59,7 @@ export const SourceTab: React.FunctionComponent<{
|
||||||
sources.set(file, source);
|
sources.set(file, source);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const location = shouldUseFallback ? fallbackLocation! : actionLocation;
|
||||||
const targetLine = shouldUseFallback ? fallbackLocation?.line || source.errors[0]?.line || 0 : actionLocation.line;
|
const targetLine = shouldUseFallback ? fallbackLocation?.line || source.errors[0]?.line || 0 : actionLocation.line;
|
||||||
const fileName = rootDir && file.startsWith(rootDir) ? file.substring(rootDir.length + 1) : file;
|
const fileName = rootDir && file.startsWith(rootDir) ? file.substring(rootDir.length + 1) : file;
|
||||||
const highlight: SourceHighlight[] = source.errors.map(e => ({ type: 'error', line: e.line, message: e.message }));
|
const highlight: SourceHighlight[] = source.errors.map(e => ({ type: 'error', line: e.line, message: e.message }));
|
||||||
|
|
@ -76,21 +80,29 @@ export const SourceTab: React.FunctionComponent<{
|
||||||
source.content = `<Unable to read "${file}">`;
|
source.content = `<Unable to read "${file}">`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { source, highlight, targetLine, fileName };
|
return { source, highlight, targetLine, fileName, location };
|
||||||
}, [stack, selectedFrame, rootDir, fallbackLocation], { source: { errors: [], content: 'Loading\u2026' }, highlight: [] });
|
}, [stack, selectedFrame, rootDir, fallbackLocation], { source: { errors: [], content: 'Loading\u2026' }, highlight: [] });
|
||||||
|
|
||||||
|
const openExternally = React.useCallback(() => {
|
||||||
|
if (!location)
|
||||||
|
return;
|
||||||
|
if (onOpenExternally) {
|
||||||
|
onOpenExternally(location);
|
||||||
|
} else {
|
||||||
|
// This should open an external protocol handler instead of actually navigating away.
|
||||||
|
window.location.href = `vscode://file//${location.file}:${location.line}`;
|
||||||
|
}
|
||||||
|
}, [onOpenExternally, location]);
|
||||||
|
|
||||||
const showStackFrames = (stack?.length ?? 0) > 1;
|
const showStackFrames = (stack?.length ?? 0) > 1;
|
||||||
|
|
||||||
return <SplitView sidebarSize={200} orientation={stackFrameLocation === 'bottom' ? 'vertical' : 'horizontal'} sidebarHidden={!showStackFrames}>
|
return <SplitView sidebarSize={200} orientation={stackFrameLocation === 'bottom' ? 'vertical' : 'horizontal'} sidebarHidden={!showStackFrames}>
|
||||||
<div className='vbox' data-testid='source-code'>
|
<div className='vbox' data-testid='source-code'>
|
||||||
{fileName && (
|
{ fileName && <Toolbar>
|
||||||
<div className='source-tab-file-name'>
|
<span className='source-tab-file-name'>{fileName}</span>
|
||||||
{fileName}
|
<CopyToClipboard description='Copy filename' value={getFileName(fileName)}/>
|
||||||
<span className='source-copy-to-clipboard'>
|
{location && <ToolbarButton icon='link-external' title='Open in VS Code' onClick={openExternally}></ToolbarButton>}
|
||||||
<CopyToClipboard description='Copy filename' value={getFileName(fileName, targetLine)}/>
|
</Toolbar> }
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<CodeMirrorWrapper text={source.content || ''} language='javascript' highlight={highlight} revealLine={targetLine} readOnly={true} lineNumbers={true} />
|
<CodeMirrorWrapper text={source.content || ''} language='javascript' highlight={highlight} revealLine={targetLine} readOnly={true} lineNumbers={true} />
|
||||||
</div>
|
</div>
|
||||||
<StackTraceView stack={stack} selectedFrame={selectedFrame} setSelectedFrame={setSelectedFrame} />
|
<StackTraceView stack={stack} selectedFrame={selectedFrame} setSelectedFrame={setSelectedFrame} />
|
||||||
|
|
@ -109,10 +121,9 @@ export async function calculateSha1(text: string): Promise<string> {
|
||||||
return hexCodes.join('');
|
return hexCodes.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFileName(fullPath?: string, lineNum?: number): string {
|
function getFileName(fullPath?: string): string {
|
||||||
if (!fullPath)
|
if (!fullPath)
|
||||||
return '';
|
return '';
|
||||||
const pathSep = fullPath?.includes('/') ? '/' : '\\';
|
const pathSep = fullPath?.includes('/') ? '/' : '\\';
|
||||||
const fileName = fullPath?.split(pathSep).pop() ?? '';
|
return fullPath?.split(pathSep).pop() ?? '';
|
||||||
return lineNum ? `${fileName}:${lineNum}` : fileName;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,8 +47,9 @@ export const TestListView: React.FC<{
|
||||||
isLoading?: boolean,
|
isLoading?: boolean,
|
||||||
onItemSelected: (item: { treeItem?: TreeItem, testCase?: reporterTypes.TestCase, testFile?: SourceLocation }) => void,
|
onItemSelected: (item: { treeItem?: TreeItem, testCase?: reporterTypes.TestCase, testFile?: SourceLocation }) => void,
|
||||||
requestedCollapseAllCount: number,
|
requestedCollapseAllCount: number,
|
||||||
setFilterText: (text: string) => void;
|
setFilterText: (text: string) => void,
|
||||||
}> = ({ filterText, testModel, testServerConnection, testTree, runTests, runningState, watchAll, watchedTreeIds, setWatchedTreeIds, isLoading, onItemSelected, requestedCollapseAllCount, setFilterText }) => {
|
onRevealSource: () => void,
|
||||||
|
}> = ({ filterText, testModel, testServerConnection, testTree, runTests, runningState, watchAll, watchedTreeIds, setWatchedTreeIds, isLoading, onItemSelected, requestedCollapseAllCount, setFilterText, onRevealSource }) => {
|
||||||
const [treeState, setTreeState] = React.useState<TreeState>({ expandedItems: new Map() });
|
const [treeState, setTreeState] = React.useState<TreeState>({ expandedItems: new Map() });
|
||||||
const [selectedTreeItemId, setSelectedTreeItemId] = React.useState<string | undefined>();
|
const [selectedTreeItemId, setSelectedTreeItemId] = React.useState<string | undefined>();
|
||||||
const [collapseAllCount, setCollapseAllCount] = React.useState(requestedCollapseAllCount);
|
const [collapseAllCount, setCollapseAllCount] = React.useState(requestedCollapseAllCount);
|
||||||
|
|
@ -91,17 +92,7 @@ export const TestListView: React.FC<{
|
||||||
if (!testModel)
|
if (!testModel)
|
||||||
return { selectedTreeItem: undefined };
|
return { selectedTreeItem: undefined };
|
||||||
const selectedTreeItem = selectedTreeItemId ? testTree.treeItemById(selectedTreeItemId) : undefined;
|
const selectedTreeItem = selectedTreeItemId ? testTree.treeItemById(selectedTreeItemId) : undefined;
|
||||||
let testFile: SourceLocation | undefined;
|
const testFile = itemLocation(selectedTreeItem, testModel);
|
||||||
if (selectedTreeItem) {
|
|
||||||
testFile = {
|
|
||||||
file: selectedTreeItem.location.file,
|
|
||||||
line: selectedTreeItem.location.line,
|
|
||||||
source: {
|
|
||||||
errors: testModel.loadErrors.filter(e => e.location?.file === selectedTreeItem.location.file).map(e => ({ line: e.location!.line, message: e.message! })),
|
|
||||||
content: undefined,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
let selectedTest: reporterTypes.TestCase | undefined;
|
let selectedTest: reporterTypes.TestCase | undefined;
|
||||||
if (selectedTreeItem?.kind === 'test')
|
if (selectedTreeItem?.kind === 'test')
|
||||||
selectedTest = selectedTreeItem.test;
|
selectedTest = selectedTreeItem.test;
|
||||||
|
|
@ -164,7 +155,7 @@ export const TestListView: React.FC<{
|
||||||
{!!treeItem.duration && treeItem.status !== 'skipped' && <div className='ui-mode-list-item-time'>{msToString(treeItem.duration)}</div>}
|
{!!treeItem.duration && treeItem.status !== 'skipped' && <div className='ui-mode-list-item-time'>{msToString(treeItem.duration)}</div>}
|
||||||
<Toolbar noMinHeight={true} noShadow={true}>
|
<Toolbar noMinHeight={true} noShadow={true}>
|
||||||
<ToolbarButton icon='play' title='Run' onClick={() => runTreeItem(treeItem)} disabled={!!runningState}></ToolbarButton>
|
<ToolbarButton icon='play' title='Run' onClick={() => runTreeItem(treeItem)} disabled={!!runningState}></ToolbarButton>
|
||||||
<ToolbarButton icon='go-to-file' title='Open in VS Code' onClick={() => testServerConnection?.openNoReply({ location: treeItem.location })} style={(treeItem.kind === 'group' && treeItem.subKind === 'folder') ? { visibility: 'hidden' } : {}}></ToolbarButton>
|
<ToolbarButton icon='go-to-file' title='Show source' onClick={onRevealSource} style={(treeItem.kind === 'group' && treeItem.subKind === 'folder') ? { visibility: 'hidden' } : {}}></ToolbarButton>
|
||||||
{!watchAll && <ToolbarButton icon='eye' title='Watch' onClick={() => {
|
{!watchAll && <ToolbarButton icon='eye' title='Watch' onClick={() => {
|
||||||
if (watchedTreeIds.value.has(treeItem.id))
|
if (watchedTreeIds.value.has(treeItem.id))
|
||||||
watchedTreeIds.value.delete(treeItem.id);
|
watchedTreeIds.value.delete(treeItem.id);
|
||||||
|
|
@ -187,3 +178,17 @@ export const TestListView: React.FC<{
|
||||||
autoExpandDepth={filterText ? 5 : 1}
|
autoExpandDepth={filterText ? 5 : 1}
|
||||||
noItemsMessage={isLoading ? 'Loading\u2026' : 'No tests'} />;
|
noItemsMessage={isLoading ? 'Loading\u2026' : 'No tests'} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function itemLocation(item: TreeItem | undefined, model: TestModel | undefined): SourceLocation | undefined {
|
||||||
|
if (!item || !model)
|
||||||
|
return;
|
||||||
|
return {
|
||||||
|
file: item.location.file,
|
||||||
|
line: item.location.line,
|
||||||
|
column: item.location.column,
|
||||||
|
source: {
|
||||||
|
errors: model.loadErrors.filter(e => e.location?.file === item.location.file).map(e => ({ line: e.location!.line, message: e.message! })),
|
||||||
|
content: undefined,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,9 @@ export const TraceView: React.FC<{
|
||||||
showRouteActionsSetting: Setting<boolean>,
|
showRouteActionsSetting: Setting<boolean>,
|
||||||
item: { treeItem?: TreeItem, testFile?: SourceLocation, testCase?: reporterTypes.TestCase },
|
item: { treeItem?: TreeItem, testFile?: SourceLocation, testCase?: reporterTypes.TestCase },
|
||||||
rootDir?: string,
|
rootDir?: string,
|
||||||
}> = ({ showRouteActionsSetting, item, rootDir }) => {
|
onOpenExternally?: (location: SourceLocation) => void,
|
||||||
|
revealSource?: boolean,
|
||||||
|
}> = ({ showRouteActionsSetting, item, rootDir, onOpenExternally, revealSource }) => {
|
||||||
const [model, setModel] = React.useState<{ model: MultiTraceModel, isLive: boolean } | undefined>();
|
const [model, setModel] = React.useState<{ model: MultiTraceModel, isLive: boolean } | undefined>();
|
||||||
const [counter, setCounter] = React.useState(0);
|
const [counter, setCounter] = React.useState(0);
|
||||||
const pollTimer = React.useRef<NodeJS.Timeout | null>(null);
|
const pollTimer = React.useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
@ -97,7 +99,11 @@ export const TraceView: React.FC<{
|
||||||
onSelectionChanged={onSelectionChanged}
|
onSelectionChanged={onSelectionChanged}
|
||||||
fallbackLocation={item.testFile}
|
fallbackLocation={item.testFile}
|
||||||
isLive={model?.isLive}
|
isLive={model?.isLive}
|
||||||
status={item.treeItem?.status} />;
|
status={item.treeItem?.status}
|
||||||
|
annotations={item.testCase?.annotations || []}
|
||||||
|
onOpenExternally={onOpenExternally}
|
||||||
|
revealSource={revealSource}
|
||||||
|
/>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const outputDirForTestCase = (testCase: reporterTypes.TestCase): string | undefined => {
|
const outputDirForTestCase = (testCase: reporterTypes.TestCase): string | undefined => {
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui-mode-sidebar > .settings-view {
|
.ui-mode-sidebar > .settings-view {
|
||||||
margin: 0 0 3px 23px;
|
margin: 0 0 8px 23px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui-mode-sidebar input[type=search] {
|
.ui-mode-sidebar input[type=search] {
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,9 @@ export const UIModeView: React.FC<{}> = ({
|
||||||
const [testServerConnection, setTestServerConnection] = React.useState<TestServerConnection>();
|
const [testServerConnection, setTestServerConnection] = React.useState<TestServerConnection>();
|
||||||
const [settingsVisible, setSettingsVisible] = React.useState(false);
|
const [settingsVisible, setSettingsVisible] = React.useState(false);
|
||||||
const [testingOptionsVisible, setTestingOptionsVisible] = React.useState(false);
|
const [testingOptionsVisible, setTestingOptionsVisible] = React.useState(false);
|
||||||
|
const [revealSource, setRevealSource] = React.useState(false);
|
||||||
|
const onRevealSource = React.useCallback(() => setRevealSource(true), [setRevealSource]);
|
||||||
|
const showTestingOptions = false;
|
||||||
|
|
||||||
const [runWorkers, setRunWorkers] = React.useState(queryParams.workers);
|
const [runWorkers, setRunWorkers] = React.useState(queryParams.workers);
|
||||||
const singleWorkerSetting = React.useMemo(() => {
|
const singleWorkerSetting = React.useMemo(() => {
|
||||||
|
|
@ -435,7 +438,13 @@ export const UIModeView: React.FC<{}> = ({
|
||||||
<XtermWrapper source={xtermDataSource}></XtermWrapper>
|
<XtermWrapper source={xtermDataSource}></XtermWrapper>
|
||||||
</div>
|
</div>
|
||||||
<div className={'vbox' + (isShowingOutput ? ' hidden' : '')}>
|
<div className={'vbox' + (isShowingOutput ? ' hidden' : '')}>
|
||||||
<TraceView showRouteActionsSetting={showRouteActionsSetting} item={selectedItem} rootDir={testModel?.config?.rootDir} />
|
<TraceView
|
||||||
|
showRouteActionsSetting={showRouteActionsSetting}
|
||||||
|
item={selectedItem}
|
||||||
|
rootDir={testModel?.config?.rootDir}
|
||||||
|
revealSource={revealSource}
|
||||||
|
onOpenExternally={location => testServerConnection?.openNoReply({ location: { file: location.file, line: location.line, column: location.column } })}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='vbox ui-mode-sidebar'>
|
<div className='vbox ui-mode-sidebar'>
|
||||||
|
|
@ -487,20 +496,23 @@ export const UIModeView: React.FC<{}> = ({
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
requestedCollapseAllCount={collapseAllCount}
|
requestedCollapseAllCount={collapseAllCount}
|
||||||
setFilterText={setFilterText}
|
setFilterText={setFilterText}
|
||||||
|
onRevealSource={onRevealSource}
|
||||||
/>
|
/>
|
||||||
<Toolbar noShadow={true} noMinHeight={true} className='settings-toolbar' onClick={() => setTestingOptionsVisible(!testingOptionsVisible)}>
|
{showTestingOptions && <>
|
||||||
<span
|
<Toolbar noShadow={true} noMinHeight={true} className='settings-toolbar' onClick={() => setTestingOptionsVisible(!testingOptionsVisible)}>
|
||||||
className={`codicon codicon-${testingOptionsVisible ? 'chevron-down' : 'chevron-right'}`}
|
<span
|
||||||
style={{ marginLeft: 5 }}
|
className={`codicon codicon-${testingOptionsVisible ? 'chevron-down' : 'chevron-right'}`}
|
||||||
title={testingOptionsVisible ? 'Hide Testing Options' : 'Show Testing Options'}
|
style={{ marginLeft: 5 }}
|
||||||
/>
|
title={testingOptionsVisible ? 'Hide Testing Options' : 'Show Testing Options'}
|
||||||
<div className='section-title'>Testing Options</div>
|
/>
|
||||||
</Toolbar>
|
<div className='section-title'>Testing Options</div>
|
||||||
{testingOptionsVisible && <SettingsView settings={[
|
</Toolbar>
|
||||||
singleWorkerSetting,
|
{testingOptionsVisible && <SettingsView settings={[
|
||||||
showBrowserSetting,
|
singleWorkerSetting,
|
||||||
updateSnapshotsSetting,
|
showBrowserSetting,
|
||||||
]} />}
|
updateSnapshotsSetting,
|
||||||
|
]} />}
|
||||||
|
</>}
|
||||||
<Toolbar noShadow={true} noMinHeight={true} className='settings-toolbar' onClick={() => setSettingsVisible(!settingsVisible)}>
|
<Toolbar noShadow={true} noMinHeight={true} className='settings-toolbar' onClick={() => setSettingsVisible(!settingsVisible)}>
|
||||||
<span
|
<span
|
||||||
className={`codicon codicon-${settingsVisible ? 'chevron-down' : 'chevron-right'}`}
|
className={`codicon codicon-${settingsVisible ? 'chevron-down' : 'chevron-right'}`}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import { ErrorsTab, useErrorsTabModel } from './errorsTab';
|
||||||
import type { ConsoleEntry } from './consoleTab';
|
import type { ConsoleEntry } from './consoleTab';
|
||||||
import { ConsoleTab, useConsoleTabModel } from './consoleTab';
|
import { ConsoleTab, useConsoleTabModel } from './consoleTab';
|
||||||
import type * as modelUtil from './modelUtil';
|
import type * as modelUtil from './modelUtil';
|
||||||
import type { ActionTraceEventInContext, MultiTraceModel } from './modelUtil';
|
import { isRouteAction } from './modelUtil';
|
||||||
import type { StackFrame } from '@protocol/channels';
|
import type { StackFrame } from '@protocol/channels';
|
||||||
import { NetworkTab, useNetworkTabModel } from './networkTab';
|
import { NetworkTab, useNetworkTabModel } from './networkTab';
|
||||||
import { SnapshotTab } from './snapshotTab';
|
import { SnapshotTab } from './snapshotTab';
|
||||||
|
|
@ -33,6 +33,7 @@ import type { TabbedPaneTabModel } from '@web/components/tabbedPane';
|
||||||
import { Timeline } from './timeline';
|
import { Timeline } from './timeline';
|
||||||
import { MetadataView } from './metadataView';
|
import { MetadataView } from './metadataView';
|
||||||
import { AttachmentsTab } from './attachmentsTab';
|
import { AttachmentsTab } from './attachmentsTab';
|
||||||
|
import { AnnotationsTab } from './annotationsTab';
|
||||||
import type { Boundaries } from '../geometry';
|
import type { Boundaries } from '../geometry';
|
||||||
import { InspectorTab } from './inspectorTab';
|
import { InspectorTab } from './inspectorTab';
|
||||||
import { ToolbarButton } from '@web/components/toolbarButton';
|
import { ToolbarButton } from '@web/components/toolbarButton';
|
||||||
|
|
@ -44,26 +45,29 @@ import type { UITestStatus } from './testUtils';
|
||||||
import { SettingsView } from './settingsView';
|
import { SettingsView } from './settingsView';
|
||||||
|
|
||||||
export const Workbench: React.FunctionComponent<{
|
export const Workbench: React.FunctionComponent<{
|
||||||
model?: MultiTraceModel,
|
model?: modelUtil.MultiTraceModel,
|
||||||
showSourcesFirst?: boolean,
|
showSourcesFirst?: boolean,
|
||||||
rootDir?: string,
|
rootDir?: string,
|
||||||
fallbackLocation?: modelUtil.SourceLocation,
|
fallbackLocation?: modelUtil.SourceLocation,
|
||||||
initialSelection?: ActionTraceEventInContext,
|
initialSelection?: modelUtil.ActionTraceEventInContext,
|
||||||
onSelectionChanged?: (action: ActionTraceEventInContext) => void,
|
onSelectionChanged?: (action: modelUtil.ActionTraceEventInContext) => void,
|
||||||
isLive?: boolean,
|
isLive?: boolean,
|
||||||
status?: UITestStatus,
|
status?: UITestStatus,
|
||||||
|
annotations?: { type: string; description?: string; }[];
|
||||||
inert?: boolean,
|
inert?: boolean,
|
||||||
showRouteActionsSetting?: Setting<boolean>,
|
showRouteActionsSetting?: Setting<boolean>,
|
||||||
openPage?: (url: string, target?: string) => Window | any,
|
openPage?: (url: string, target?: string) => Window | any,
|
||||||
}> = ({ showRouteActionsSetting, model, showSourcesFirst, rootDir, fallbackLocation, initialSelection, onSelectionChanged, isLive, status, inert, openPage }) => {
|
onOpenExternally?: (location: modelUtil.SourceLocation) => void,
|
||||||
const [selectedAction, setSelectedActionImpl] = React.useState<ActionTraceEventInContext | undefined>(undefined);
|
revealSource?: boolean,
|
||||||
|
}> = ({ showRouteActionsSetting, model, showSourcesFirst, rootDir, fallbackLocation, initialSelection, onSelectionChanged, isLive, status, annotations, inert, openPage, onOpenExternally, revealSource }) => {
|
||||||
|
const [selectedAction, setSelectedActionImpl] = React.useState<modelUtil.ActionTraceEventInContext | undefined>(undefined);
|
||||||
const [revealedStack, setRevealedStack] = React.useState<StackFrame[] | undefined>(undefined);
|
const [revealedStack, setRevealedStack] = React.useState<StackFrame[] | undefined>(undefined);
|
||||||
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEventInContext | undefined>();
|
const [highlightedAction, setHighlightedAction] = React.useState<modelUtil.ActionTraceEventInContext | undefined>();
|
||||||
const [highlightedEntry, setHighlightedEntry] = React.useState<Entry | undefined>();
|
const [highlightedEntry, setHighlightedEntry] = React.useState<Entry | undefined>();
|
||||||
const [highlightedConsoleMessage, setHighlightedConsoleMessage] = React.useState<ConsoleEntry | undefined>();
|
const [highlightedConsoleMessage, setHighlightedConsoleMessage] = React.useState<ConsoleEntry | undefined>();
|
||||||
const [selectedNavigatorTab, setSelectedNavigatorTab] = React.useState<string>('actions');
|
const [selectedNavigatorTab, setSelectedNavigatorTab] = React.useState<string>('actions');
|
||||||
const [selectedPropertiesTab, setSelectedPropertiesTab] = useSetting<string>('propertiesTab', showSourcesFirst ? 'source' : 'call');
|
const [selectedPropertiesTab, setSelectedPropertiesTab] = useSetting<string>('propertiesTab', showSourcesFirst ? 'source' : 'call');
|
||||||
const [isInspecting, setIsInspecting] = React.useState(false);
|
const [isInspecting, setIsInspectingState] = React.useState(false);
|
||||||
const [highlightedLocator, setHighlightedLocator] = React.useState<string>('');
|
const [highlightedLocator, setHighlightedLocator] = React.useState<string>('');
|
||||||
const activeAction = model ? highlightedAction || selectedAction : undefined;
|
const activeAction = model ? highlightedAction || selectedAction : undefined;
|
||||||
const [selectedTime, setSelectedTime] = React.useState<Boundaries | undefined>();
|
const [selectedTime, setSelectedTime] = React.useState<Boundaries | undefined>();
|
||||||
|
|
@ -75,10 +79,10 @@ export const Workbench: React.FunctionComponent<{
|
||||||
const showRouteActions = showRouteActionsSetting[0];
|
const showRouteActions = showRouteActionsSetting[0];
|
||||||
|
|
||||||
const filteredActions = React.useMemo(() => {
|
const filteredActions = React.useMemo(() => {
|
||||||
return (model?.actions || []).filter(action => showRouteActions || action.class !== 'Route');
|
return (model?.actions || []).filter(action => showRouteActions || !isRouteAction(action));
|
||||||
}, [model, showRouteActions]);
|
}, [model, showRouteActions]);
|
||||||
|
|
||||||
const setSelectedAction = React.useCallback((action: ActionTraceEventInContext | undefined) => {
|
const setSelectedAction = React.useCallback((action: modelUtil.ActionTraceEventInContext | undefined) => {
|
||||||
setSelectedActionImpl(action);
|
setSelectedActionImpl(action);
|
||||||
setRevealedStack(action?.stack);
|
setRevealedStack(action?.stack);
|
||||||
}, [setSelectedActionImpl, setRevealedStack]);
|
}, [setSelectedActionImpl, setRevealedStack]);
|
||||||
|
|
@ -87,6 +91,7 @@ export const Workbench: React.FunctionComponent<{
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setSelectedTime(undefined);
|
setSelectedTime(undefined);
|
||||||
|
setRevealedStack(undefined);
|
||||||
}, [model]);
|
}, [model]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|
@ -110,7 +115,7 @@ export const Workbench: React.FunctionComponent<{
|
||||||
}
|
}
|
||||||
}, [model, selectedAction, setSelectedAction, initialSelection]);
|
}, [model, selectedAction, setSelectedAction, initialSelection]);
|
||||||
|
|
||||||
const onActionSelected = React.useCallback((action: ActionTraceEventInContext) => {
|
const onActionSelected = React.useCallback((action: modelUtil.ActionTraceEventInContext) => {
|
||||||
setSelectedAction(action);
|
setSelectedAction(action);
|
||||||
onSelectionChanged?.(action);
|
onSelectionChanged?.(action);
|
||||||
}, [setSelectedAction, onSelectionChanged]);
|
}, [setSelectedAction, onSelectionChanged]);
|
||||||
|
|
@ -118,14 +123,25 @@ export const Workbench: React.FunctionComponent<{
|
||||||
const selectPropertiesTab = React.useCallback((tab: string) => {
|
const selectPropertiesTab = React.useCallback((tab: string) => {
|
||||||
setSelectedPropertiesTab(tab);
|
setSelectedPropertiesTab(tab);
|
||||||
if (tab !== 'inspector')
|
if (tab !== 'inspector')
|
||||||
setIsInspecting(false);
|
setIsInspectingState(false);
|
||||||
}, [setSelectedPropertiesTab]);
|
}, [setSelectedPropertiesTab]);
|
||||||
|
|
||||||
|
const setIsInspecting = React.useCallback((value: boolean) => {
|
||||||
|
if (!isInspecting && value)
|
||||||
|
selectPropertiesTab('inspector');
|
||||||
|
setIsInspectingState(value);
|
||||||
|
}, [setIsInspectingState, selectPropertiesTab, isInspecting]);
|
||||||
|
|
||||||
const locatorPicked = React.useCallback((locator: string) => {
|
const locatorPicked = React.useCallback((locator: string) => {
|
||||||
setHighlightedLocator(locator);
|
setHighlightedLocator(locator);
|
||||||
selectPropertiesTab('inspector');
|
selectPropertiesTab('inspector');
|
||||||
}, [selectPropertiesTab]);
|
}, [selectPropertiesTab]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (revealSource)
|
||||||
|
selectPropertiesTab('source');
|
||||||
|
}, [revealSource, selectPropertiesTab]);
|
||||||
|
|
||||||
const consoleModel = useConsoleTabModel(model, selectedTime);
|
const consoleModel = useConsoleTabModel(model, selectedTime);
|
||||||
const networkModel = useNetworkTabModel(model, selectedTime);
|
const networkModel = useNetworkTabModel(model, selectedTime);
|
||||||
const errorsModel = useErrorsTabModel(model);
|
const errorsModel = useErrorsTabModel(model);
|
||||||
|
|
@ -174,7 +190,9 @@ export const Workbench: React.FunctionComponent<{
|
||||||
sources={sources}
|
sources={sources}
|
||||||
rootDir={rootDir}
|
rootDir={rootDir}
|
||||||
stackFrameLocation={sidebarLocation === 'bottom' ? 'right' : 'bottom'}
|
stackFrameLocation={sidebarLocation === 'bottom' ? 'right' : 'bottom'}
|
||||||
fallbackLocation={fallbackLocation} />
|
fallbackLocation={fallbackLocation}
|
||||||
|
onOpenExternally={onOpenExternally}
|
||||||
|
/>
|
||||||
};
|
};
|
||||||
const consoleTab: TabbedPaneTabModel = {
|
const consoleTab: TabbedPaneTabModel = {
|
||||||
id: 'console',
|
id: 'console',
|
||||||
|
|
@ -211,6 +229,17 @@ export const Workbench: React.FunctionComponent<{
|
||||||
sourceTab,
|
sourceTab,
|
||||||
attachmentsTab,
|
attachmentsTab,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (annotations !== undefined) {
|
||||||
|
const annotationsTab: TabbedPaneTabModel = {
|
||||||
|
id: 'annotations',
|
||||||
|
title: 'Annotations',
|
||||||
|
count: annotations.length,
|
||||||
|
render: () => <AnnotationsTab annotations={annotations} />
|
||||||
|
};
|
||||||
|
tabs.push(annotationsTab);
|
||||||
|
}
|
||||||
|
|
||||||
if (showSourcesFirst) {
|
if (showSourcesFirst) {
|
||||||
const sourceTabIndex = tabs.indexOf(sourceTab);
|
const sourceTabIndex = tabs.indexOf(sourceTab);
|
||||||
tabs.splice(sourceTabIndex, 1);
|
tabs.splice(sourceTabIndex, 1);
|
||||||
|
|
@ -302,13 +331,6 @@ export const Workbench: React.FunctionComponent<{
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
selectedTab={selectedPropertiesTab}
|
selectedTab={selectedPropertiesTab}
|
||||||
setSelectedTab={selectPropertiesTab}
|
setSelectedTab={selectPropertiesTab}
|
||||||
leftToolbar={[
|
|
||||||
<ToolbarButton title='Pick locator' icon='target' toggled={isInspecting} onClick={() => {
|
|
||||||
if (!isInspecting)
|
|
||||||
selectPropertiesTab('inspector');
|
|
||||||
setIsInspecting(!isInspecting);
|
|
||||||
}} />
|
|
||||||
]}
|
|
||||||
rightToolbar={[
|
rightToolbar={[
|
||||||
sidebarLocation === 'bottom' ?
|
sidebarLocation === 'bottom' ?
|
||||||
<ToolbarButton title='Dock to right' icon='layout-sidebar-right-off' onClick={() => {
|
<ToolbarButton title='Dock to right' icon='layout-sidebar-right-off' onClick={() => {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ import 'codemirror-shadow-1/mode/htmlmixed/htmlmixed';
|
||||||
import 'codemirror-shadow-1/mode/javascript/javascript';
|
import 'codemirror-shadow-1/mode/javascript/javascript';
|
||||||
import 'codemirror-shadow-1/mode/python/python';
|
import 'codemirror-shadow-1/mode/python/python';
|
||||||
import 'codemirror-shadow-1/mode/clike/clike';
|
import 'codemirror-shadow-1/mode/clike/clike';
|
||||||
|
import 'codemirror-shadow-1/mode/markdown/markdown';
|
||||||
|
import 'codemirror-shadow-1/addon/mode/simple';
|
||||||
|
|
||||||
export type CodeMirror = typeof codemirrorType;
|
export type CodeMirror = typeof codemirrorType;
|
||||||
export default codemirror;
|
export default codemirror;
|
||||||
|
|
|
||||||
|
|
@ -174,3 +174,9 @@ body.dark-mode .CodeMirror span.cm-type {
|
||||||
margin: 3px 10px;
|
margin: 3px 10px;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.CodeMirror span.cm-link, span.cm-linkified {
|
||||||
|
color: var(--vscode-textLink-foreground);
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import './codeMirrorWrapper.css';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import type { CodeMirror } from './codeMirrorModule';
|
import type { CodeMirror } from './codeMirrorModule';
|
||||||
import { ansi2html } from '../ansi2html';
|
import { ansi2html } from '../ansi2html';
|
||||||
import { useMeasure } from '../uiUtils';
|
import { useMeasure, kWebLinkRe } from '../uiUtils';
|
||||||
|
|
||||||
export type SourceHighlight = {
|
export type SourceHighlight = {
|
||||||
line: number;
|
line: number;
|
||||||
|
|
@ -26,11 +26,13 @@ export type SourceHighlight = {
|
||||||
message?: string;
|
message?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Language = 'javascript' | 'python' | 'java' | 'csharp' | 'jsonl' | 'html' | 'css';
|
export type Language = 'javascript' | 'python' | 'java' | 'csharp' | 'jsonl' | 'html' | 'css' | 'markdown';
|
||||||
|
|
||||||
export interface SourceProps {
|
export interface SourceProps {
|
||||||
text: string;
|
text: string;
|
||||||
language?: Language;
|
language?: Language;
|
||||||
|
mimeType?: string;
|
||||||
|
linkify?: boolean;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
// 1-based
|
// 1-based
|
||||||
highlight?: SourceHighlight[];
|
highlight?: SourceHighlight[];
|
||||||
|
|
@ -45,6 +47,8 @@ export interface SourceProps {
|
||||||
export const CodeMirrorWrapper: React.FC<SourceProps> = ({
|
export const CodeMirrorWrapper: React.FC<SourceProps> = ({
|
||||||
text,
|
text,
|
||||||
language,
|
language,
|
||||||
|
mimeType,
|
||||||
|
linkify,
|
||||||
readOnly,
|
readOnly,
|
||||||
highlight,
|
highlight,
|
||||||
revealLine,
|
revealLine,
|
||||||
|
|
@ -63,24 +67,13 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
|
||||||
(async () => {
|
(async () => {
|
||||||
// Always load the module first.
|
// Always load the module first.
|
||||||
const CodeMirror = await modulePromise;
|
const CodeMirror = await modulePromise;
|
||||||
|
defineCustomMode(CodeMirror);
|
||||||
|
|
||||||
const element = codemirrorElement.current;
|
const element = codemirrorElement.current;
|
||||||
if (!element)
|
if (!element)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
let mode = '';
|
const mode = languageToMode(language) || mimeTypeToMode(mimeType) || (linkify ? 'text/linkified' : '');
|
||||||
if (language === 'javascript')
|
|
||||||
mode = 'javascript';
|
|
||||||
if (language === 'python')
|
|
||||||
mode = 'python';
|
|
||||||
if (language === 'java')
|
|
||||||
mode = 'text/x-java';
|
|
||||||
if (language === 'csharp')
|
|
||||||
mode = 'text/x-csharp';
|
|
||||||
if (language === 'html')
|
|
||||||
mode = 'htmlmixed';
|
|
||||||
if (language === 'css')
|
|
||||||
mode = 'css';
|
|
||||||
|
|
||||||
if (codemirrorRef.current
|
if (codemirrorRef.current
|
||||||
&& mode === codemirrorRef.current.cm.getOption('mode')
|
&& mode === codemirrorRef.current.cm.getOption('mode')
|
||||||
|
|
@ -106,7 +99,7 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
|
||||||
setCodemirror(cm);
|
setCodemirror(cm);
|
||||||
return cm;
|
return cm;
|
||||||
})();
|
})();
|
||||||
}, [modulePromise, codemirror, codemirrorElement, language, lineNumbers, wrapLines, readOnly, isFocused]);
|
}, [modulePromise, codemirror, codemirrorElement, language, mimeType, linkify, lineNumbers, wrapLines, readOnly, isFocused]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (codemirrorRef.current)
|
if (codemirrorRef.current)
|
||||||
|
|
@ -175,5 +168,69 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
|
||||||
};
|
};
|
||||||
}, [codemirror, text, highlight, revealLine, focusOnChange, onChange]);
|
}, [codemirror, text, highlight, revealLine, focusOnChange, onChange]);
|
||||||
|
|
||||||
return <div className='cm-wrapper' ref={codemirrorElement}></div>;
|
return <div className='cm-wrapper' ref={codemirrorElement} onClick={onCodeMirrorClick}></div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function onCodeMirrorClick(event: React.MouseEvent) {
|
||||||
|
if (!(event.target instanceof HTMLElement))
|
||||||
|
return;
|
||||||
|
let url: string | undefined;
|
||||||
|
if (event.target.classList.contains('cm-linkified')) {
|
||||||
|
// 'text/linkified' custom mode
|
||||||
|
url = event.target.textContent!;
|
||||||
|
} else if (event.target.classList.contains('cm-link') && event.target.nextElementSibling?.classList.contains('cm-url')) {
|
||||||
|
// 'markdown' mode
|
||||||
|
url = event.target.nextElementSibling.textContent!.slice(1, -1);
|
||||||
|
}
|
||||||
|
if (url) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
window.open(url, '_blank');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let customModeDefined = false;
|
||||||
|
function defineCustomMode(cm: CodeMirror) {
|
||||||
|
if (customModeDefined)
|
||||||
|
return;
|
||||||
|
customModeDefined = true;
|
||||||
|
(cm as any).defineSimpleMode('text/linkified', {
|
||||||
|
start: [
|
||||||
|
{ regex: kWebLinkRe, token: 'linkified' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function mimeTypeToMode(mimeType: string | undefined): string | undefined {
|
||||||
|
if (!mimeType)
|
||||||
|
return;
|
||||||
|
if (mimeType.includes('javascript') || mimeType.includes('json'))
|
||||||
|
return 'javascript';
|
||||||
|
if (mimeType.includes('python'))
|
||||||
|
return 'python';
|
||||||
|
if (mimeType.includes('csharp'))
|
||||||
|
return 'text/x-csharp';
|
||||||
|
if (mimeType.includes('java'))
|
||||||
|
return 'text/x-java';
|
||||||
|
if (mimeType.includes('markdown'))
|
||||||
|
return 'markdown';
|
||||||
|
if (mimeType.includes('html') || mimeType.includes('svg'))
|
||||||
|
return 'htmlmixed';
|
||||||
|
if (mimeType.includes('css'))
|
||||||
|
return 'css';
|
||||||
|
}
|
||||||
|
|
||||||
|
function languageToMode(language: Language | undefined): string | undefined {
|
||||||
|
if (!language)
|
||||||
|
return;
|
||||||
|
return {
|
||||||
|
javascript: 'javascript',
|
||||||
|
jsonl: 'javascript',
|
||||||
|
python: 'python',
|
||||||
|
csharp: 'text/x-csharp',
|
||||||
|
java: 'text/x-java',
|
||||||
|
markdown: 'markdown',
|
||||||
|
html: 'htmlmixed',
|
||||||
|
css: 'css',
|
||||||
|
}[language];
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,14 +62,10 @@ export const TabbedPane: React.FunctionComponent<{
|
||||||
}}>
|
}}>
|
||||||
{tabs.map(tab => {
|
{tabs.map(tab => {
|
||||||
let suffix = '';
|
let suffix = '';
|
||||||
if (tab.count === 1)
|
if (tab.count)
|
||||||
suffix = ' 🔵';
|
suffix = ` (${tab.count})`;
|
||||||
else if (tab.count)
|
if (tab.errorCount)
|
||||||
suffix = ` 🔵✖️${tab.count}`;
|
suffix = ` (${tab.errorCount})`;
|
||||||
if (tab.errorCount === 1)
|
|
||||||
suffix = ` 🔴`;
|
|
||||||
else if (tab.errorCount)
|
|
||||||
suffix = ` 🔴✖️${tab.errorCount}`;
|
|
||||||
return <option value={tab.id} selected={tab.id === selectedTab}>{tab.title}{suffix}</option>;
|
return <option value={tab.id} selected={tab.id === selectedTab}>{tab.title}{suffix}</option>;
|
||||||
})}
|
})}
|
||||||
</select>
|
</select>
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ export interface ToolbarButtonProps {
|
||||||
onClick: (e: React.MouseEvent) => void,
|
onClick: (e: React.MouseEvent) => void,
|
||||||
style?: React.CSSProperties,
|
style?: React.CSSProperties,
|
||||||
testId?: string,
|
testId?: string,
|
||||||
|
className?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ToolbarButton: React.FC<React.PropsWithChildren<ToolbarButtonProps>> = ({
|
export const ToolbarButton: React.FC<React.PropsWithChildren<ToolbarButtonProps>> = ({
|
||||||
|
|
@ -37,8 +38,9 @@ export const ToolbarButton: React.FC<React.PropsWithChildren<ToolbarButtonProps>
|
||||||
onClick = () => {},
|
onClick = () => {},
|
||||||
style,
|
style,
|
||||||
testId,
|
testId,
|
||||||
|
className,
|
||||||
}) => {
|
}) => {
|
||||||
let className = `toolbar-button ${icon}`;
|
className = (className || '') + ` toolbar-button ${icon}`;
|
||||||
if (toggled)
|
if (toggled)
|
||||||
className += ' toggled';
|
className += ' toggled';
|
||||||
return <button
|
return <button
|
||||||
|
|
|
||||||
|
|
@ -14,15 +14,14 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function linkifyText(description: string) {
|
import { kWebLinkRe } from './uiUtils';
|
||||||
const CONTROL_CODES = '\\u0000-\\u0020\\u007f-\\u009f';
|
|
||||||
const WEB_LINK_REGEX = new RegExp('(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}:\\/\\/|www\\.)[^\\s' + CONTROL_CODES + '"]{2,}[^\\s' + CONTROL_CODES + '"\')}\\],:;.!?]', 'ug');
|
|
||||||
|
|
||||||
|
export function linkifyText(description: string) {
|
||||||
const result = [];
|
const result = [];
|
||||||
let currentIndex = 0;
|
let currentIndex = 0;
|
||||||
let match;
|
let match;
|
||||||
|
|
||||||
while ((match = WEB_LINK_REGEX.exec(description)) !== null) {
|
while ((match = kWebLinkRe.exec(description)) !== null) {
|
||||||
const stringBeforeMatch = description.substring(currentIndex, match.index);
|
const stringBeforeMatch = description.substring(currentIndex, match.index);
|
||||||
if (stringBeforeMatch)
|
if (stringBeforeMatch)
|
||||||
result.push(stringBeforeMatch);
|
result.push(stringBeforeMatch);
|
||||||
|
|
@ -59,8 +59,9 @@ const checkerboardStyle: React.CSSProperties = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ImageDiffView: React.FC<{
|
export const ImageDiffView: React.FC<{
|
||||||
diff: ImageDiff,
|
diff: ImageDiff,
|
||||||
}> = ({ diff }) => {
|
noTargetBlank?: boolean,
|
||||||
|
}> = ({ diff, noTargetBlank }) => {
|
||||||
const [mode, setMode] = React.useState<'diff' | 'actual' | 'expected' | 'slider' | 'sxs'>(diff.diff ? 'diff' : 'actual');
|
const [mode, setMode] = React.useState<'diff' | 'actual' | 'expected' | 'slider' | 'sxs'>(diff.diff ? 'diff' : 'actual');
|
||||||
const [showSxsDiff, setShowSxsDiff] = React.useState<boolean>(false);
|
const [showSxsDiff, setShowSxsDiff] = React.useState<boolean>(false);
|
||||||
|
|
||||||
|
|
@ -117,10 +118,10 @@ export const ImageDiffView: React.FC<{
|
||||||
<ImageWithSize image={actualImage} title='Actual' canvasWidth={sxsScale * imageWidth} canvasHeight={sxsScale * imageHeight} scale={sxsScale} />
|
<ImageWithSize image={actualImage} title='Actual' canvasWidth={sxsScale * imageWidth} canvasHeight={sxsScale * imageHeight} scale={sxsScale} />
|
||||||
</div>}
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ alignSelf: 'start', lineHeight: '18px' }}>
|
<div style={{ alignSelf: 'start', lineHeight: '18px', marginLeft: '15px' }}>
|
||||||
<div>{diff.diff && <a target='_blank' href={diff.diff.attachment.path}>{diff.diff.attachment.name}</a>}</div>
|
<div>{diff.diff && <a target='_blank' href={diff.diff.attachment.path}>{diff.diff.attachment.name}</a>}</div>
|
||||||
<div><a target='_blank' href={diff.actual!.attachment.path}>{diff.actual!.attachment.name}</a></div>
|
<div><a target={noTargetBlank ? '' : '_blank'} href={diff.actual!.attachment.path}>{diff.actual!.attachment.name}</a></div>
|
||||||
<div><a target='_blank' href={diff.expected!.attachment.path}>{diff.expected!.attachment.name}</a></div>
|
<div><a target={noTargetBlank ? '' : '_blank'} href={diff.expected!.attachment.path}>{diff.expected!.attachment.name}</a></div>
|
||||||
</div>
|
</div>
|
||||||
</>}
|
</>}
|
||||||
</div>;
|
</div>;
|
||||||
|
|
|
||||||
|
|
@ -183,3 +183,6 @@ export class Settings {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const settings = new Settings();
|
export const settings = new Settings();
|
||||||
|
|
||||||
|
const kControlCodesRe = '\\u0000-\\u0020\\u007f-\\u009f';
|
||||||
|
export const kWebLinkRe = new RegExp('(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}:\\/\\/|www\\.)[^\\s' + kControlCodesRe + '"]{2,}[^\\s' + kControlCodesRe + '"\')}\\],:;.!?]', 'ug');
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,8 @@ openssl x509 \
|
||||||
-out client/trusted/cert.pem \
|
-out client/trusted/cert.pem \
|
||||||
-set_serial 01 \
|
-set_serial 01 \
|
||||||
-days 365
|
-days 365
|
||||||
|
# create pfx
|
||||||
|
openssl pkcs12 -export -out client/trusted/cert.pfx -inkey client/trusted/key.pem -in client/trusted/cert.pem -passout pass:secure
|
||||||
```
|
```
|
||||||
|
|
||||||
## Self-signed certificate (invalid)
|
## Self-signed certificate (invalid)
|
||||||
|
|
|
||||||
BIN
tests/assets/client-certificates/client/trusted/cert-legacy.pfx
Normal file
BIN
tests/assets/client-certificates/client/trusted/cert-legacy.pfx
Normal file
Binary file not shown.
BIN
tests/assets/client-certificates/client/trusted/cert.pfx
Normal file
BIN
tests/assets/client-certificates/client/trusted/cert.pfx
Normal file
Binary file not shown.
|
|
@ -58,7 +58,7 @@ class TraceViewerPage {
|
||||||
this.stackFrames = page.getByTestId('stack-trace-list').locator('.list-view-entry');
|
this.stackFrames = page.getByTestId('stack-trace-list').locator('.list-view-entry');
|
||||||
this.networkRequests = page.getByTestId('network-list').locator('.list-view-entry');
|
this.networkRequests = page.getByTestId('network-list').locator('.list-view-entry');
|
||||||
this.snapshotContainer = page.locator('.snapshot-container iframe.snapshot-visible[name=snapshot]');
|
this.snapshotContainer = page.locator('.snapshot-container iframe.snapshot-visible[name=snapshot]');
|
||||||
this.metadataTab = page.locator('.metadata-view');
|
this.metadataTab = page.getByTestId('metadata-view');
|
||||||
}
|
}
|
||||||
|
|
||||||
async actionIconsText(action: string) {
|
async actionIconsText(action: string) {
|
||||||
|
|
|
||||||
|
|
@ -1288,7 +1288,7 @@ it('should not work after context dispose', async ({ context, server }) => {
|
||||||
expect(await context.request.get(server.EMPTY_PAGE).catch(e => e.message)).toContain('Test ended.');
|
expect(await context.request.get(server.EMPTY_PAGE).catch(e => e.message)).toContain('Test ended.');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should retrty ECONNRESET', {
|
it('should retry on ECONNRESET', {
|
||||||
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30978' }
|
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30978' }
|
||||||
}, async ({ context, server }) => {
|
}, async ({ context, server }) => {
|
||||||
let requestCount = 0;
|
let requestCount = 0;
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import tls from 'tls';
|
||||||
import type http2 from 'http2';
|
import type http2 from 'http2';
|
||||||
import type http from 'http';
|
import type http from 'http';
|
||||||
import { expect, playwrightTest as base } from '../config/browserTest';
|
import { expect, playwrightTest as base } from '../config/browserTest';
|
||||||
|
|
@ -24,6 +25,7 @@ const { createHttpsServer, createHttp2Server } = require('../../packages/playwri
|
||||||
|
|
||||||
type TestOptions = {
|
type TestOptions = {
|
||||||
startCCServer(options?: {
|
startCCServer(options?: {
|
||||||
|
host?: string;
|
||||||
http2?: boolean;
|
http2?: boolean;
|
||||||
enableHTTP1FallbackWhenUsingHttp2?: boolean;
|
enableHTTP1FallbackWhenUsingHttp2?: boolean;
|
||||||
useFakeLocalhost?: boolean;
|
useFakeLocalhost?: boolean;
|
||||||
|
|
@ -63,7 +65,7 @@ const test = base.extend<TestOptions>({
|
||||||
}
|
}
|
||||||
res.end(parts.map(({ key, value }) => `<div data-testid="${key}">${value}</div>`).join(''));
|
res.end(parts.map(({ key, value }) => `<div data-testid="${key}">${value}</div>`).join(''));
|
||||||
});
|
});
|
||||||
await new Promise<void>(f => server.listen(0, 'localhost', () => f()));
|
await new Promise<void>(f => server.listen(0, options?.host ?? 'localhost', () => f()));
|
||||||
const host = options?.useFakeLocalhost ? 'local.playwright' : 'localhost';
|
const host = options?.useFakeLocalhost ? 'local.playwright' : 'localhost';
|
||||||
return `https://${host}:${(server.address() as net.AddressInfo).port}/`;
|
return `https://${host}:${(server.address() as net.AddressInfo).port}/`;
|
||||||
});
|
});
|
||||||
|
|
@ -81,8 +83,6 @@ test.use({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test.skip(({ mode }) => mode !== 'default');
|
|
||||||
|
|
||||||
const kDummyFileName = __filename;
|
const kDummyFileName = __filename;
|
||||||
const kValidationSubTests: [BrowserContextOptions, string][] = [
|
const kValidationSubTests: [BrowserContextOptions, string][] = [
|
||||||
[{ clientCertificates: [{ origin: 'test' }] }, 'None of cert, key, passphrase or pfx is specified'],
|
[{ clientCertificates: [{ origin: 'test' }] }, 'None of cert, key, passphrase or pfx is specified'],
|
||||||
|
|
@ -113,7 +113,7 @@ test.describe('fetch', () => {
|
||||||
|
|
||||||
test('should fail with no client certificates provided', async ({ playwright, startCCServer }) => {
|
test('should fail with no client certificates provided', async ({ playwright, startCCServer }) => {
|
||||||
const serverURL = await startCCServer();
|
const serverURL = await startCCServer();
|
||||||
const request = await playwright.request.newContext();
|
const request = await playwright.request.newContext({ ignoreHTTPSErrors: true });
|
||||||
const response = await request.get(serverURL);
|
const response = await request.get(serverURL);
|
||||||
expect(response.status()).toBe(401);
|
expect(response.status()).toBe(401);
|
||||||
expect(await response.text()).toContain('Sorry, but you need to provide a client certificate to continue.');
|
expect(await response.text()).toContain('Sorry, but you need to provide a client certificate to continue.');
|
||||||
|
|
@ -122,6 +122,7 @@ test.describe('fetch', () => {
|
||||||
|
|
||||||
test('should keep supporting http', async ({ playwright, server, asset }) => {
|
test('should keep supporting http', async ({ playwright, server, asset }) => {
|
||||||
const request = await playwright.request.newContext({
|
const request = await playwright.request.newContext({
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
clientCertificates: [{
|
clientCertificates: [{
|
||||||
origin: new URL(server.PREFIX).origin,
|
origin: new URL(server.PREFIX).origin,
|
||||||
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||||
|
|
@ -138,6 +139,7 @@ test.describe('fetch', () => {
|
||||||
test('should throw with untrusted client certs', async ({ playwright, startCCServer, asset }) => {
|
test('should throw with untrusted client certs', async ({ playwright, startCCServer, asset }) => {
|
||||||
const serverURL = await startCCServer();
|
const serverURL = await startCCServer();
|
||||||
const request = await playwright.request.newContext({
|
const request = await playwright.request.newContext({
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
clientCertificates: [{
|
clientCertificates: [{
|
||||||
origin: new URL(serverURL).origin,
|
origin: new URL(serverURL).origin,
|
||||||
certPath: asset('client-certificates/client/self-signed/cert.pem'),
|
certPath: asset('client-certificates/client/self-signed/cert.pem'),
|
||||||
|
|
@ -154,6 +156,7 @@ test.describe('fetch', () => {
|
||||||
test('pass with trusted client certificates', async ({ playwright, startCCServer, asset }) => {
|
test('pass with trusted client certificates', async ({ playwright, startCCServer, asset }) => {
|
||||||
const serverURL = await startCCServer();
|
const serverURL = await startCCServer();
|
||||||
const request = await playwright.request.newContext({
|
const request = await playwright.request.newContext({
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
clientCertificates: [{
|
clientCertificates: [{
|
||||||
origin: new URL(serverURL).origin,
|
origin: new URL(serverURL).origin,
|
||||||
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||||
|
|
@ -167,9 +170,55 @@ test.describe('fetch', () => {
|
||||||
await request.dispose();
|
await request.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('pass with trusted client certificates in pfx format', async ({ playwright, startCCServer, asset }) => {
|
||||||
|
const serverURL = await startCCServer();
|
||||||
|
const request = await playwright.request.newContext({
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
|
clientCertificates: [{
|
||||||
|
origin: new URL(serverURL).origin,
|
||||||
|
pfxPath: asset('client-certificates/client/trusted/cert.pfx'),
|
||||||
|
passphrase: 'secure'
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
const response = await request.get(serverURL);
|
||||||
|
expect(response.url()).toBe(serverURL);
|
||||||
|
expect(response.status()).toBe(200);
|
||||||
|
expect(await response.text()).toContain('Hello Alice, your certificate was issued by localhost!');
|
||||||
|
await request.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw a http error if the pfx passphrase is incorect', async ({ playwright, startCCServer, asset, browserName }) => {
|
||||||
|
const serverURL = await startCCServer();
|
||||||
|
const request = await playwright.request.newContext({
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
|
clientCertificates: [{
|
||||||
|
origin: new URL(serverURL).origin,
|
||||||
|
pfxPath: asset('client-certificates/client/trusted/cert.pfx'),
|
||||||
|
passphrase: 'this-password-is-incorrect'
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
await expect(request.get(serverURL)).rejects.toThrow('mac verify failure');
|
||||||
|
await request.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should fail with matching certificates in legacy pfx format', async ({ playwright, startCCServer, asset, browserName }) => {
|
||||||
|
const serverURL = await startCCServer();
|
||||||
|
const request = await playwright.request.newContext({
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
|
clientCertificates: [{
|
||||||
|
origin: new URL(serverURL).origin,
|
||||||
|
pfxPath: asset('client-certificates/client/trusted/cert-legacy.pfx'),
|
||||||
|
passphrase: 'secure'
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
await expect(request.get(serverURL)).rejects.toThrow('Unsupported TLS certificate');
|
||||||
|
await request.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
test('should work in the browser with request interception', async ({ browser, playwright, startCCServer, asset }) => {
|
test('should work in the browser with request interception', async ({ browser, playwright, startCCServer, asset }) => {
|
||||||
const serverURL = await startCCServer();
|
const serverURL = await startCCServer();
|
||||||
const request = await playwright.request.newContext({
|
const request = await playwright.request.newContext({
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
clientCertificates: [{
|
clientCertificates: [{
|
||||||
origin: new URL(serverURL).origin,
|
origin: new URL(serverURL).origin,
|
||||||
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||||
|
|
@ -212,6 +261,7 @@ test.describe('browser', () => {
|
||||||
test('should fail with no client certificates', async ({ browser, startCCServer, asset, browserName }) => {
|
test('should fail with no client certificates', async ({ browser, startCCServer, asset, browserName }) => {
|
||||||
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
|
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
|
||||||
const page = await browser.newPage({
|
const page = await browser.newPage({
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
clientCertificates: [{
|
clientCertificates: [{
|
||||||
origin: 'https://not-matching.com',
|
origin: 'https://not-matching.com',
|
||||||
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||||
|
|
@ -226,6 +276,7 @@ test.describe('browser', () => {
|
||||||
test('should fail with self-signed client certificates', async ({ browser, startCCServer, asset, browserName }) => {
|
test('should fail with self-signed client certificates', async ({ browser, startCCServer, asset, browserName }) => {
|
||||||
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
|
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
|
||||||
const page = await browser.newPage({
|
const page = await browser.newPage({
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
clientCertificates: [{
|
clientCertificates: [{
|
||||||
origin: new URL(serverURL).origin,
|
origin: new URL(serverURL).origin,
|
||||||
certPath: asset('client-certificates/client/self-signed/cert.pem'),
|
certPath: asset('client-certificates/client/self-signed/cert.pem'),
|
||||||
|
|
@ -240,6 +291,7 @@ test.describe('browser', () => {
|
||||||
test('should pass with matching certificates', async ({ browser, startCCServer, asset, browserName }) => {
|
test('should pass with matching certificates', async ({ browser, startCCServer, asset, browserName }) => {
|
||||||
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
|
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
|
||||||
const page = await browser.newPage({
|
const page = await browser.newPage({
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
clientCertificates: [{
|
clientCertificates: [{
|
||||||
origin: new URL(serverURL).origin,
|
origin: new URL(serverURL).origin,
|
||||||
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||||
|
|
@ -251,9 +303,120 @@ test.describe('browser', () => {
|
||||||
await page.close();
|
await page.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should not hang on tls errors during TLS 1.2 handshake', async ({ browser, asset, platform, browserName }) => {
|
||||||
|
for (const tlsVersion of ['TLSv1.3', 'TLSv1.2'] as const) {
|
||||||
|
await test.step(`TLS version: ${tlsVersion}`, async () => {
|
||||||
|
const server = tls.createServer({
|
||||||
|
key: fs.readFileSync(asset('client-certificates/server/server_key.pem')),
|
||||||
|
cert: fs.readFileSync(asset('client-certificates/server/server_cert.pem')),
|
||||||
|
ca: [
|
||||||
|
fs.readFileSync(asset('client-certificates/server/server_cert.pem')),
|
||||||
|
],
|
||||||
|
requestCert: true,
|
||||||
|
rejectUnauthorized: true,
|
||||||
|
minVersion: tlsVersion,
|
||||||
|
maxVersion: tlsVersion,
|
||||||
|
SNICallback: (servername, cb) => {
|
||||||
|
// Always reject the connection by passing an error
|
||||||
|
cb(new Error('Connection rejected'), null);
|
||||||
|
}
|
||||||
|
}, () => {
|
||||||
|
// Do nothing
|
||||||
|
});
|
||||||
|
const serverURL = await new Promise<string>(resolve => {
|
||||||
|
server.listen(0, 'localhost', () => {
|
||||||
|
const host = browserName === 'webkit' && platform === 'darwin' ? 'local.playwright' : 'localhost';
|
||||||
|
resolve(`https://${host}:${(server.address() as net.AddressInfo).port}/`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const page = await browser.newPage({
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
|
clientCertificates: [{
|
||||||
|
origin: new URL(serverURL).origin,
|
||||||
|
certPath: asset('client-certificates/client/self-signed/cert.pem'),
|
||||||
|
keyPath: asset('client-certificates/client/self-signed/key.pem'),
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
await page.goto(serverURL);
|
||||||
|
await expect(page.getByText('Playwright client-certificate error: Client network socket disconnected before secure TLS connection was established')).toBeVisible();
|
||||||
|
await page.close();
|
||||||
|
await new Promise<void>(resolve => server.close(() => resolve()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should pass with matching certificates in pfx format', async ({ browser, startCCServer, asset, browserName }) => {
|
||||||
|
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
|
||||||
|
const page = await browser.newPage({
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
|
clientCertificates: [{
|
||||||
|
origin: new URL(serverURL).origin,
|
||||||
|
pfxPath: asset('client-certificates/client/trusted/cert.pfx'),
|
||||||
|
passphrase: 'secure'
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
await page.goto(serverURL);
|
||||||
|
await expect(page.getByTestId('message')).toHaveText('Hello Alice, your certificate was issued by localhost!');
|
||||||
|
await page.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should fail with matching certificates in legacy pfx format', async ({ browser, startCCServer, asset, browserName }) => {
|
||||||
|
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
|
||||||
|
const page = await browser.newPage({
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
|
clientCertificates: [{
|
||||||
|
origin: new URL(serverURL).origin,
|
||||||
|
pfxPath: asset('client-certificates/client/trusted/cert-legacy.pfx'),
|
||||||
|
passphrase: 'secure'
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
await page.goto(serverURL);
|
||||||
|
await expect(page.getByText('Unsupported TLS certificate.')).toBeVisible();
|
||||||
|
await page.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw a http error if the pfx passphrase is incorect', async ({ browser, startCCServer, asset, browserName }) => {
|
||||||
|
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
|
||||||
|
const page = await browser.newPage({
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
|
clientCertificates: [{
|
||||||
|
origin: new URL(serverURL).origin,
|
||||||
|
pfxPath: asset('client-certificates/client/trusted/cert.pfx'),
|
||||||
|
passphrase: 'this-password-is-incorrect'
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
await page.goto(serverURL);
|
||||||
|
await expect(page.getByText('Playwright client-certificate error: mac verify failure')).toBeVisible();
|
||||||
|
await page.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should pass with matching certificates on context APIRequestContext instance', async ({ browser, startCCServer, asset, browserName }) => {
|
||||||
|
const serverURL = await startCCServer({ host: '127.0.0.1' });
|
||||||
|
const baseOptions = {
|
||||||
|
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||||
|
keyPath: asset('client-certificates/client/trusted/key.pem'),
|
||||||
|
};
|
||||||
|
const page = await browser.newPage({
|
||||||
|
clientCertificates: [{
|
||||||
|
origin: new URL(serverURL).origin,
|
||||||
|
...baseOptions,
|
||||||
|
}, {
|
||||||
|
origin: new URL(serverURL).origin.replace('localhost', '127.0.0.1'),
|
||||||
|
...baseOptions,
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
for (const url of [serverURL, serverURL.replace('localhost', '127.0.0.1')]) {
|
||||||
|
const response = await page.request.get(url);
|
||||||
|
expect(response.status()).toBe(200);
|
||||||
|
expect(await response.text()).toContain('Hello Alice, your certificate was issued by localhost!');
|
||||||
|
}
|
||||||
|
await page.close();
|
||||||
|
});
|
||||||
|
|
||||||
test('should pass with matching certificates and trailing slash', async ({ browser, startCCServer, asset, browserName }) => {
|
test('should pass with matching certificates and trailing slash', async ({ browser, startCCServer, asset, browserName }) => {
|
||||||
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
|
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
|
||||||
const page = await browser.newPage({
|
const page = await browser.newPage({
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
clientCertificates: [{
|
clientCertificates: [{
|
||||||
origin: serverURL,
|
origin: serverURL,
|
||||||
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||||
|
|
@ -274,7 +437,7 @@ test.describe('browser', () => {
|
||||||
}],
|
}],
|
||||||
});
|
});
|
||||||
await page.goto(browserName === 'webkit' && platform === 'darwin' ? httpsServer.EMPTY_PAGE.replace('localhost', 'local.playwright') : httpsServer.EMPTY_PAGE);
|
await page.goto(browserName === 'webkit' && platform === 'darwin' ? httpsServer.EMPTY_PAGE.replace('localhost', 'local.playwright') : httpsServer.EMPTY_PAGE);
|
||||||
await expect(page.getByText('Playwright client-certificate error')).toBeVisible();
|
await expect(page.getByText('Playwright client-certificate error: self-signed certificate')).toBeVisible();
|
||||||
await page.close();
|
await page.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -283,6 +446,7 @@ test.describe('browser', () => {
|
||||||
const enableHTTP1FallbackWhenUsingHttp2 = browserName === 'webkit' && process.platform === 'linux';
|
const enableHTTP1FallbackWhenUsingHttp2 = browserName === 'webkit' && process.platform === 'linux';
|
||||||
const serverURL = await startCCServer({ http2: true, enableHTTP1FallbackWhenUsingHttp2 });
|
const serverURL = await startCCServer({ http2: true, enableHTTP1FallbackWhenUsingHttp2 });
|
||||||
const page = await browser.newPage({
|
const page = await browser.newPage({
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
clientCertificates: [{
|
clientCertificates: [{
|
||||||
origin: new URL(serverURL).origin,
|
origin: new URL(serverURL).origin,
|
||||||
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||||
|
|
@ -311,6 +475,7 @@ test.describe('browser', () => {
|
||||||
const serverURL = await startCCServer({ http2: true, enableHTTP1FallbackWhenUsingHttp2: true });
|
const serverURL = await startCCServer({ http2: true, enableHTTP1FallbackWhenUsingHttp2: true });
|
||||||
const browser = await browserType.launch({ args: ['--disable-http2'] });
|
const browser = await browserType.launch({ args: ['--disable-http2'] });
|
||||||
const page = await browser.newPage({
|
const page = await browser.newPage({
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
clientCertificates: [{
|
clientCertificates: [{
|
||||||
origin: new URL(serverURL).origin,
|
origin: new URL(serverURL).origin,
|
||||||
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||||
|
|
@ -335,7 +500,6 @@ test.describe('browser', () => {
|
||||||
test.fixme(browserName === 'webkit' && process.platform === 'linux', 'WebKit on Linux does not support http2 https://bugs.webkit.org/show_bug.cgi?id=276990');
|
test.fixme(browserName === 'webkit' && process.platform === 'linux', 'WebKit on Linux does not support http2 https://bugs.webkit.org/show_bug.cgi?id=276990');
|
||||||
test.skip(+process.versions.node.split('.')[0] < 20, 'http2.performServerHandshake is not supported in older Node.js versions');
|
test.skip(+process.versions.node.split('.')[0] < 20, 'http2.performServerHandshake is not supported in older Node.js versions');
|
||||||
|
|
||||||
process.env.PWTEST_UNSUPPORTED_CUSTOM_CA = asset('empty.html');
|
|
||||||
const serverURL = await startCCServer({ http2: true });
|
const serverURL = await startCCServer({ http2: true });
|
||||||
const page = await browser.newPage({
|
const page = await browser.newPage({
|
||||||
clientCertificates: [{
|
clientCertificates: [{
|
||||||
|
|
@ -359,6 +523,7 @@ test.describe('browser', () => {
|
||||||
test('should pass with matching certificates', async ({ launchPersistent, startCCServer, asset, browserName }) => {
|
test('should pass with matching certificates', async ({ launchPersistent, startCCServer, asset, browserName }) => {
|
||||||
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
|
const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
|
||||||
const { page } = await launchPersistent({
|
const { page } = await launchPersistent({
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
clientCertificates: [{
|
clientCertificates: [{
|
||||||
origin: new URL(serverURL).origin,
|
origin: new URL(serverURL).origin,
|
||||||
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
certPath: asset('client-certificates/client/trusted/cert.pem'),
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,15 @@
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import * as util from 'util';
|
import * as util from 'util';
|
||||||
import { getPlaywrightVersion } from '../../packages/playwright-core/lib/utils/userAgent';
|
import { getPlaywrightVersion } from '../../packages/playwright-core/lib/utils/userAgent';
|
||||||
import { expect, playwrightTest as it } from '../config/browserTest';
|
import { expect, playwrightTest as base } from '../config/browserTest';
|
||||||
import { kTargetClosedErrorMessage } from 'tests/config/errors';
|
import { kTargetClosedErrorMessage } from 'tests/config/errors';
|
||||||
|
|
||||||
|
const it = base.extend({
|
||||||
|
context: async ({}, use) => {
|
||||||
|
throw new Error('global fetch tests should not use context');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it.skip(({ mode }) => mode !== 'default');
|
it.skip(({ mode }) => mode !== 'default');
|
||||||
|
|
||||||
for (const method of ['fetch', 'delete', 'get', 'head', 'patch', 'post', 'put'] as const) {
|
for (const method of ['fetch', 'delete', 'get', 'head', 'patch', 'post', 'put'] as const) {
|
||||||
|
|
@ -33,9 +39,11 @@ for (const method of ['fetch', 'delete', 'get', 'head', 'patch', 'post', 'put']
|
||||||
expect(response.headers()['content-type']).toBe('application/json; charset=utf-8');
|
expect(response.headers()['content-type']).toBe('application/json; charset=utf-8');
|
||||||
expect(response.headersArray()).toContainEqual({ name: 'Content-Type', value: 'application/json; charset=utf-8' });
|
expect(response.headersArray()).toContainEqual({ name: 'Content-Type', value: 'application/json; charset=utf-8' });
|
||||||
expect(await response.text()).toBe('head' === method ? '' : '{"foo": "bar"}\n');
|
expect(await response.text()).toBe('head' === method ? '' : '{"foo": "bar"}\n');
|
||||||
|
await request.dispose();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
it(`should dispose global request`, async function({ playwright, server }) {
|
it(`should dispose global request`, async function({ playwright, server }) {
|
||||||
const request = await playwright.request.newContext();
|
const request = await playwright.request.newContext();
|
||||||
const response = await request.get(server.PREFIX + '/simple.json');
|
const response = await request.get(server.PREFIX + '/simple.json');
|
||||||
|
|
@ -43,6 +51,7 @@ it(`should dispose global request`, async function({ playwright, server }) {
|
||||||
await request.dispose();
|
await request.dispose();
|
||||||
const error = await response.body().catch(e => e);
|
const error = await response.body().catch(e => e);
|
||||||
expect(error.message).toContain('Response has been disposed');
|
expect(error.message).toContain('Response has been disposed');
|
||||||
|
await request.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support global userAgent option', async ({ playwright, server }) => {
|
it('should support global userAgent option', async ({ playwright, server }) => {
|
||||||
|
|
@ -54,6 +63,7 @@ it('should support global userAgent option', async ({ playwright, server }) => {
|
||||||
expect(response.ok()).toBeTruthy();
|
expect(response.ok()).toBeTruthy();
|
||||||
expect(response.url()).toBe(server.EMPTY_PAGE);
|
expect(response.url()).toBe(server.EMPTY_PAGE);
|
||||||
expect(serverRequest.headers['user-agent']).toBe('My Agent');
|
expect(serverRequest.headers['user-agent']).toBe('My Agent');
|
||||||
|
await request.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support global timeout option', async ({ playwright, server }) => {
|
it('should support global timeout option', async ({ playwright, server }) => {
|
||||||
|
|
@ -61,6 +71,7 @@ it('should support global timeout option', async ({ playwright, server }) => {
|
||||||
server.setRoute('/empty.html', (req, res) => {});
|
server.setRoute('/empty.html', (req, res) => {});
|
||||||
const error = await request.get(server.EMPTY_PAGE).catch(e => e);
|
const error = await request.get(server.EMPTY_PAGE).catch(e => e);
|
||||||
expect(error.message).toContain('Request timed out after 100ms');
|
expect(error.message).toContain('Request timed out after 100ms');
|
||||||
|
await request.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should propagate extra http headers with redirects', async ({ playwright, server }) => {
|
it('should propagate extra http headers with redirects', async ({ playwright, server }) => {
|
||||||
|
|
@ -76,6 +87,7 @@ it('should propagate extra http headers with redirects', async ({ playwright, se
|
||||||
expect(req1.headers['my-secret']).toBe('Value');
|
expect(req1.headers['my-secret']).toBe('Value');
|
||||||
expect(req2.headers['my-secret']).toBe('Value');
|
expect(req2.headers['my-secret']).toBe('Value');
|
||||||
expect(req3.headers['my-secret']).toBe('Value');
|
expect(req3.headers['my-secret']).toBe('Value');
|
||||||
|
await request.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support global httpCredentials option', async ({ playwright, server }) => {
|
it('should support global httpCredentials option', async ({ playwright, server }) => {
|
||||||
|
|
@ -96,6 +108,7 @@ it('should return error with wrong credentials', async ({ playwright, server })
|
||||||
const request = await playwright.request.newContext({ httpCredentials: { username: 'user', password: 'wrong' } });
|
const request = await playwright.request.newContext({ httpCredentials: { username: 'user', password: 'wrong' } });
|
||||||
const response = await request.get(server.EMPTY_PAGE);
|
const response = await request.get(server.EMPTY_PAGE);
|
||||||
expect(response.status()).toBe(401);
|
expect(response.status()).toBe(401);
|
||||||
|
await request.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work with correct credentials and matching origin', async ({ playwright, server }) => {
|
it('should work with correct credentials and matching origin', async ({ playwright, server }) => {
|
||||||
|
|
@ -103,6 +116,7 @@ it('should work with correct credentials and matching origin', async ({ playwrig
|
||||||
const request = await playwright.request.newContext({ httpCredentials: { username: 'user', password: 'pass', origin: server.PREFIX } });
|
const request = await playwright.request.newContext({ httpCredentials: { username: 'user', password: 'pass', origin: server.PREFIX } });
|
||||||
const response = await request.get(server.EMPTY_PAGE);
|
const response = await request.get(server.EMPTY_PAGE);
|
||||||
expect(response.status()).toBe(200);
|
expect(response.status()).toBe(200);
|
||||||
|
await request.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work with correct credentials and matching origin case insensitive', async ({ playwright, server }) => {
|
it('should work with correct credentials and matching origin case insensitive', async ({ playwright, server }) => {
|
||||||
|
|
@ -110,6 +124,7 @@ it('should work with correct credentials and matching origin case insensitive',
|
||||||
const request = await playwright.request.newContext({ httpCredentials: { username: 'user', password: 'pass', origin: server.PREFIX.toUpperCase() } });
|
const request = await playwright.request.newContext({ httpCredentials: { username: 'user', password: 'pass', origin: server.PREFIX.toUpperCase() } });
|
||||||
const response = await request.get(server.EMPTY_PAGE);
|
const response = await request.get(server.EMPTY_PAGE);
|
||||||
expect(response.status()).toBe(200);
|
expect(response.status()).toBe(200);
|
||||||
|
await request.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return error with correct credentials and mismatching scheme', async ({ playwright, server }) => {
|
it('should return error with correct credentials and mismatching scheme', async ({ playwright, server }) => {
|
||||||
|
|
@ -117,6 +132,7 @@ it('should return error with correct credentials and mismatching scheme', async
|
||||||
const request = await playwright.request.newContext({ httpCredentials: { username: 'user', password: 'pass', origin: server.PREFIX.replace('http://', 'https://') } });
|
const request = await playwright.request.newContext({ httpCredentials: { username: 'user', password: 'pass', origin: server.PREFIX.replace('http://', 'https://') } });
|
||||||
const response = await request.get(server.EMPTY_PAGE);
|
const response = await request.get(server.EMPTY_PAGE);
|
||||||
expect(response.status()).toBe(401);
|
expect(response.status()).toBe(401);
|
||||||
|
await request.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return error with correct credentials and mismatching hostname', async ({ playwright, server }) => {
|
it('should return error with correct credentials and mismatching hostname', async ({ playwright, server }) => {
|
||||||
|
|
@ -126,6 +142,7 @@ it('should return error with correct credentials and mismatching hostname', asyn
|
||||||
const request = await playwright.request.newContext({ httpCredentials: { username: 'user', password: 'pass', origin: origin } });
|
const request = await playwright.request.newContext({ httpCredentials: { username: 'user', password: 'pass', origin: origin } });
|
||||||
const response = await request.get(server.EMPTY_PAGE);
|
const response = await request.get(server.EMPTY_PAGE);
|
||||||
expect(response.status()).toBe(401);
|
expect(response.status()).toBe(401);
|
||||||
|
await request.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return error with correct credentials and mismatching port', async ({ playwright, server }) => {
|
it('should return error with correct credentials and mismatching port', async ({ playwright, server }) => {
|
||||||
|
|
@ -134,6 +151,7 @@ it('should return error with correct credentials and mismatching port', async ({
|
||||||
const request = await playwright.request.newContext({ httpCredentials: { username: 'user', password: 'pass', origin: origin } });
|
const request = await playwright.request.newContext({ httpCredentials: { username: 'user', password: 'pass', origin: origin } });
|
||||||
const response = await request.get(server.EMPTY_PAGE);
|
const response = await request.get(server.EMPTY_PAGE);
|
||||||
expect(response.status()).toBe(401);
|
expect(response.status()).toBe(401);
|
||||||
|
await request.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support WWW-Authenticate: Basic', async ({ playwright, server }) => {
|
it('should support WWW-Authenticate: Basic', async ({ playwright, server }) => {
|
||||||
|
|
@ -152,6 +170,7 @@ it('should support WWW-Authenticate: Basic', async ({ playwright, server }) => {
|
||||||
const response = await request.get(server.EMPTY_PAGE);
|
const response = await request.get(server.EMPTY_PAGE);
|
||||||
expect(response.status()).toBe(200);
|
expect(response.status()).toBe(200);
|
||||||
expect(credentials).toBe('user:pass');
|
expect(credentials).toBe('user:pass');
|
||||||
|
await request.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support HTTPCredentials.send', async ({ playwright, server }) => {
|
it('should support HTTPCredentials.send', async ({ playwright, server }) => {
|
||||||
|
|
@ -176,12 +195,14 @@ it('should support HTTPCredentials.send', async ({ playwright, server }) => {
|
||||||
expect(serverRequest.headers.authorization).toBe(undefined);
|
expect(serverRequest.headers.authorization).toBe(undefined);
|
||||||
expect(response.status()).toBe(200);
|
expect(response.status()).toBe(200);
|
||||||
}
|
}
|
||||||
|
await request.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support global ignoreHTTPSErrors option', async ({ playwright, httpsServer }) => {
|
it('should support global ignoreHTTPSErrors option', async ({ playwright, httpsServer }) => {
|
||||||
const request = await playwright.request.newContext({ ignoreHTTPSErrors: true });
|
const request = await playwright.request.newContext({ ignoreHTTPSErrors: true });
|
||||||
const response = await request.get(httpsServer.EMPTY_PAGE);
|
const response = await request.get(httpsServer.EMPTY_PAGE);
|
||||||
expect(response.status()).toBe(200);
|
expect(response.status()).toBe(200);
|
||||||
|
await request.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should propagate ignoreHTTPSErrors on redirects', async ({ playwright, httpsServer }) => {
|
it('should propagate ignoreHTTPSErrors on redirects', async ({ playwright, httpsServer }) => {
|
||||||
|
|
@ -189,12 +210,14 @@ it('should propagate ignoreHTTPSErrors on redirects', async ({ playwright, https
|
||||||
const request = await playwright.request.newContext();
|
const request = await playwright.request.newContext();
|
||||||
const response = await request.get(httpsServer.PREFIX + '/redir', { ignoreHTTPSErrors: true });
|
const response = await request.get(httpsServer.PREFIX + '/redir', { ignoreHTTPSErrors: true });
|
||||||
expect(response.status()).toBe(200);
|
expect(response.status()).toBe(200);
|
||||||
|
await request.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should resolve url relative to global baseURL option', async ({ playwright, server }) => {
|
it('should resolve url relative to global baseURL option', async ({ playwright, server }) => {
|
||||||
const request = await playwright.request.newContext({ baseURL: server.PREFIX });
|
const request = await playwright.request.newContext({ baseURL: server.PREFIX });
|
||||||
const response = await request.get('/empty.html');
|
const response = await request.get('/empty.html');
|
||||||
expect(response.url()).toBe(server.EMPTY_PAGE);
|
expect(response.url()).toBe(server.EMPTY_PAGE);
|
||||||
|
await request.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set playwright as user-agent', async ({ playwright, server, isWindows, isLinux, isMac }) => {
|
it('should set playwright as user-agent', async ({ playwright, server, isWindows, isLinux, isMac }) => {
|
||||||
|
|
@ -221,12 +244,14 @@ it('should set playwright as user-agent', async ({ playwright, server, isWindows
|
||||||
expect(userAgentMasked.replace(/<ARCH>; \w+ [^)]+/, '<ARCH>; distro version')).toBe('Playwright/X.X.X (<ARCH>; distro version) node/X.X' + suffix);
|
expect(userAgentMasked.replace(/<ARCH>; \w+ [^)]+/, '<ARCH>; distro version')).toBe('Playwright/X.X.X (<ARCH>; distro version) node/X.X' + suffix);
|
||||||
else if (isMac)
|
else if (isMac)
|
||||||
expect(userAgentMasked).toBe('Playwright/X.X.X (<ARCH>; macOS X.X) node/X.X' + suffix);
|
expect(userAgentMasked).toBe('Playwright/X.X.X (<ARCH>; macOS X.X) node/X.X' + suffix);
|
||||||
|
await request.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to construct with context options', async ({ playwright, browserType, server }) => {
|
it('should be able to construct with context options', async ({ playwright, browserType, server }) => {
|
||||||
const request = await playwright.request.newContext((browserType as any)._defaultContextOptions);
|
const request = await playwright.request.newContext((browserType as any)._defaultContextOptions);
|
||||||
const response = await request.get(server.EMPTY_PAGE);
|
const response = await request.get(server.EMPTY_PAGE);
|
||||||
expect(response.ok()).toBeTruthy();
|
expect(response.ok()).toBeTruthy();
|
||||||
|
await request.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return empty body', async ({ playwright, server }) => {
|
it('should return empty body', async ({ playwright, server }) => {
|
||||||
|
|
@ -254,6 +279,7 @@ it('should abort requests when context is disposed', async ({ playwright, server
|
||||||
expect(result.message).toContain(kTargetClosedErrorMessage);
|
expect(result.message).toContain(kTargetClosedErrorMessage);
|
||||||
}
|
}
|
||||||
await connectionClosed;
|
await connectionClosed;
|
||||||
|
await request.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should abort redirected requests when context is disposed', async ({ playwright, server }) => {
|
it('should abort redirected requests when context is disposed', async ({ playwright, server }) => {
|
||||||
|
|
@ -269,6 +295,7 @@ it('should abort redirected requests when context is disposed', async ({ playwri
|
||||||
expect(result instanceof Error).toBeTruthy();
|
expect(result instanceof Error).toBeTruthy();
|
||||||
expect(result.message).toContain(kTargetClosedErrorMessage);
|
expect(result.message).toContain(kTargetClosedErrorMessage);
|
||||||
await connectionClosed;
|
await connectionClosed;
|
||||||
|
await request.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove content-length from redirected post requests', async ({ playwright, server }) => {
|
it('should remove content-length from redirected post requests', async ({ playwright, server }) => {
|
||||||
|
|
@ -473,7 +500,6 @@ it('should serialize post data on the client', async ({ playwright, server }) =>
|
||||||
await postReq;
|
await postReq;
|
||||||
const body = await (await serverReq).postBody;
|
const body = await (await serverReq).postBody;
|
||||||
expect(body.toString()).toBe('{"foo":"bar"}');
|
expect(body.toString()).toBe('{"foo":"bar"}');
|
||||||
// expect(serverRequest.rawHeaders).toContain('vaLUE');
|
|
||||||
await request.dispose();
|
await request.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -486,7 +512,8 @@ it('should throw after dispose', async ({ playwright, server }) => {
|
||||||
|
|
||||||
it('should retry ECONNRESET', {
|
it('should retry ECONNRESET', {
|
||||||
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30978' }
|
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/30978' }
|
||||||
}, async ({ context, server }) => {
|
}, async ({ playwright, server }) => {
|
||||||
|
const request = await playwright.request.newContext();
|
||||||
let requestCount = 0;
|
let requestCount = 0;
|
||||||
server.setRoute('/test', (req, res) => {
|
server.setRoute('/test', (req, res) => {
|
||||||
if (requestCount++ < 3) {
|
if (requestCount++ < 3) {
|
||||||
|
|
@ -496,8 +523,9 @@ it('should retry ECONNRESET', {
|
||||||
res.writeHead(200, { 'content-type': 'text/plain' });
|
res.writeHead(200, { 'content-type': 'text/plain' });
|
||||||
res.end('Hello!');
|
res.end('Hello!');
|
||||||
});
|
});
|
||||||
const response = await context.request.fetch(server.PREFIX + '/test', { maxRetries: 3 });
|
const response = await request.fetch(server.PREFIX + '/test', { maxRetries: 3 });
|
||||||
expect(response.status()).toBe(200);
|
expect(response.status()).toBe(200);
|
||||||
expect(await response.text()).toBe('Hello!');
|
expect(await response.text()).toBe('Hello!');
|
||||||
expect(requestCount).toBe(4);
|
expect(requestCount).toBe(4);
|
||||||
|
await request.dispose();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -804,7 +804,7 @@ test('should follow redirects', async ({ page, runAndTrace, server, asset }) =>
|
||||||
test('should include metainfo', async ({ showTraceViewer }) => {
|
test('should include metainfo', async ({ showTraceViewer }) => {
|
||||||
const traceViewer = await showTraceViewer([traceFile]);
|
const traceViewer = await showTraceViewer([traceFile]);
|
||||||
await traceViewer.page.locator('text=Metadata').click();
|
await traceViewer.page.locator('text=Metadata').click();
|
||||||
const callLine = traceViewer.page.locator('.metadata-view .call-line');
|
const callLine = traceViewer.metadataTab.locator('.call-line');
|
||||||
await expect(callLine.getByText('start time')).toHaveText(/start time:[\d/,: ]+/);
|
await expect(callLine.getByText('start time')).toHaveText(/start time:[\d/,: ]+/);
|
||||||
await expect(callLine.getByText('duration')).toHaveText(/duration:[\dms]+/);
|
await expect(callLine.getByText('duration')).toHaveText(/duration:[\dms]+/);
|
||||||
await expect(callLine.getByText('engine')).toHaveText(/engine:[\w]+/);
|
await expect(callLine.getByText('engine')).toHaveText(/engine:[\w]+/);
|
||||||
|
|
@ -1357,7 +1357,6 @@ test('should allow hiding route actions', {
|
||||||
await traceViewer.page.getByRole('checkbox', { name: 'Show route actions' }).uncheck();
|
await traceViewer.page.getByRole('checkbox', { name: 'Show route actions' }).uncheck();
|
||||||
await traceViewer.page.getByText('Actions', { exact: true }).click();
|
await traceViewer.page.getByText('Actions', { exact: true }).click();
|
||||||
await expect(traceViewer.actionTitles).toHaveText([
|
await expect(traceViewer.actionTitles).toHaveText([
|
||||||
/page.route/,
|
|
||||||
/page.goto.*empty.html/,
|
/page.goto.*empty.html/,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -364,4 +364,53 @@ test('UI mode is not supported', async ({ runInlineTest }) => {
|
||||||
const result = await runInlineTest({}, { 'only-changed': true, 'ui': true });
|
const result = await runInlineTest({}, { 'only-changed': true, 'ui': true });
|
||||||
expect(result.exitCode).toBe(1);
|
expect(result.exitCode).toBe(1);
|
||||||
expect(result.output).toContain('--only-changed is not supported in UI mode');
|
expect(result.output).toContain('--only-changed is not supported in UI mode');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should run project dependencies of changed tests', {
|
||||||
|
annotation: {
|
||||||
|
type: 'issue',
|
||||||
|
description: 'https://github.com/microsoft/playwright/issues/32070',
|
||||||
|
},
|
||||||
|
}, async ({ runInlineTest, git, writeFiles }) => {
|
||||||
|
await writeFiles({
|
||||||
|
'playwright.config.ts': `
|
||||||
|
module.exports = {
|
||||||
|
projects: [
|
||||||
|
{ name: 'setup', testMatch: 'setup.spec.ts', },
|
||||||
|
{ name: 'main', dependencies: ['setup'] },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
'setup.spec.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test('setup test', async ({ page }) => {
|
||||||
|
console.log('setup test is executed')
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
'a.spec.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('fails', () => { expect(1).toBe(2); });
|
||||||
|
`,
|
||||||
|
'b.spec.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('fails', () => { expect(1).toBe(2); });
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
git(`add .`);
|
||||||
|
git(`commit -m init`);
|
||||||
|
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'c.spec.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('fails', () => { expect(1).toBe(2); });
|
||||||
|
`
|
||||||
|
}, { 'only-changed': true });
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.failed).toBe(1);
|
||||||
|
expect(result.passed).toBe(1);
|
||||||
|
|
||||||
|
expect(result.output).toContain('setup test is executed');
|
||||||
});
|
});
|
||||||
|
|
@ -1178,3 +1178,51 @@ test('should record trace for manually created context in a failed test', async
|
||||||
// Check console events to make sure that library trace is recorded.
|
// Check console events to make sure that library trace is recorded.
|
||||||
expect(trace.events).toContainEqual(expect.objectContaining({ type: 'console', text: 'from the page' }));
|
expect(trace.events).toContainEqual(expect.objectContaining({ type: 'console', text: 'from the page' }));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should not nest top level expect into unfinished api calls ', {
|
||||||
|
annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/31959' }
|
||||||
|
}, async ({ runInlineTest, server }) => {
|
||||||
|
server.setRoute('/index', (req, res) => {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||||
|
res.end(`<script>fetch('/api')</script><div>Hello!</div>`);
|
||||||
|
});
|
||||||
|
server.setRoute('/hang', () => {});
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'a.spec.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('pass', async ({ page }) => {
|
||||||
|
await page.route('**/api', async route => {
|
||||||
|
const response = await route.fetch({ url: '${server.PREFIX}/hang' });
|
||||||
|
await route.fulfill({ response });
|
||||||
|
});
|
||||||
|
await page.goto('${server.PREFIX}/index');
|
||||||
|
await expect(page.getByText('Hello!')).toBeVisible();
|
||||||
|
await page.unrouteAll({ behavior: 'ignoreErrors' });
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}, { trace: 'on' });
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.failed).toBe(0);
|
||||||
|
|
||||||
|
const tracePath = test.info().outputPath('test-results', 'a-pass', 'trace.zip');
|
||||||
|
const trace = await parseTrace(tracePath);
|
||||||
|
expect(trace.actionTree).toEqual([
|
||||||
|
'Before Hooks',
|
||||||
|
' fixture: browser',
|
||||||
|
' browserType.launch',
|
||||||
|
' fixture: context',
|
||||||
|
' browser.newContext',
|
||||||
|
' fixture: page',
|
||||||
|
' browserContext.newPage',
|
||||||
|
'page.route',
|
||||||
|
'page.goto',
|
||||||
|
' route.fetch',
|
||||||
|
' page.unrouteAll',
|
||||||
|
'expect.toBeVisible',
|
||||||
|
'After Hooks',
|
||||||
|
' fixture: page',
|
||||||
|
' fixture: context',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ for (const useIntermediateMergeReport of [false] as const) {
|
||||||
await expect(page.getByTestId('overall-duration'), 'should contain humanized total time with at most 1 decimal place').toContainText(/^Total time: \d+(\.\d)?(ms|s|m)$/);
|
await expect(page.getByTestId('overall-duration'), 'should contain humanized total time with at most 1 decimal place').toContainText(/^Total time: \d+(\.\d)?(ms|s|m)$/);
|
||||||
await expect(page.getByTestId('project-name'), 'should contain project name').toContainText('project-name');
|
await expect(page.getByTestId('project-name'), 'should contain project name').toContainText('project-name');
|
||||||
|
|
||||||
await expect(page.locator('.metadata-view')).not.toBeVisible();
|
await expect(page.getByTestId('metadata-view')).not.toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should allow navigating to testId=test.id', async ({ runInlineTest, page, showReport }) => {
|
test('should allow navigating to testId=test.id', async ({ runInlineTest, page, showReport }) => {
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import { test, expect } from './playwright-test-fixtures';
|
||||||
test('should check types of fixtures', async ({ runTSC }) => {
|
test('should check types of fixtures', async ({ runTSC }) => {
|
||||||
const result = await runTSC({
|
const result = await runTSC({
|
||||||
'helper.ts': `
|
'helper.ts': `
|
||||||
import { test as base, expect } from '@playwright/test';
|
import { test as base, expect, Page } from '@playwright/test';
|
||||||
export type MyOptions = { foo: string, bar: number };
|
export type MyOptions = { foo: string, bar: number };
|
||||||
export const test = base.extend<{ foo: string }, { bar: number }>({
|
export const test = base.extend<{ foo: string }, { bar: number }>({
|
||||||
foo: 'foo',
|
foo: 'foo',
|
||||||
|
|
@ -71,7 +71,7 @@ test('should check types of fixtures', async ({ runTSC }) => {
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
baz: true,
|
baz: true,
|
||||||
});
|
});
|
||||||
const fail9 = test.extend<{ foo: string }>({
|
const fail9 = test.extend({
|
||||||
foo: [ async ({}, use) => {
|
foo: [ async ({}, use) => {
|
||||||
await use('foo');
|
await use('foo');
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
|
|
@ -100,7 +100,21 @@ test('should check types of fixtures', async ({ runTSC }) => {
|
||||||
return y;
|
return y;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
|
const chain1 = base.extend({
|
||||||
|
page: async ({ page }, use) => {
|
||||||
|
await use(page);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const chain2 = chain1.extend<{ pageAsUser: Page }>({
|
||||||
|
pageAsUser: async ({ page }, use) => {
|
||||||
|
// @ts-expect-error
|
||||||
|
const x: number = page;
|
||||||
|
// @ts-expect-error
|
||||||
|
await use(x);
|
||||||
|
},
|
||||||
|
});
|
||||||
`,
|
`,
|
||||||
'playwright.config.ts': `
|
'playwright.config.ts': `
|
||||||
import { MyOptions } from './helper';
|
import { MyOptions } from './helper';
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,10 @@ test('should contain text attachment', async ({ runUITest }) => {
|
||||||
'a.test.ts': `
|
'a.test.ts': `
|
||||||
import { test } from '@playwright/test';
|
import { test } from '@playwright/test';
|
||||||
test('attach test', async () => {
|
test('attach test', async () => {
|
||||||
|
// Attach two files with the same content and different names,
|
||||||
|
// to make sure each is downloaded with an intended name.
|
||||||
await test.info().attach('file attachment', { path: __filename });
|
await test.info().attach('file attachment', { path: __filename });
|
||||||
|
await test.info().attach('file attachment 2', { path: __filename });
|
||||||
await test.info().attach('text attachment', { body: 'hi tester!', contentType: 'text/plain' });
|
await test.info().attach('text attachment', { body: 'hi tester!', contentType: 'text/plain' });
|
||||||
});
|
});
|
||||||
`,
|
`,
|
||||||
|
|
@ -35,14 +38,24 @@ test('should contain text attachment', async ({ runUITest }) => {
|
||||||
|
|
||||||
await page.locator('.tab-attachments').getByText('text attachment').click();
|
await page.locator('.tab-attachments').getByText('text attachment').click();
|
||||||
await expect(page.locator('.tab-attachments')).toContainText('hi tester!');
|
await expect(page.locator('.tab-attachments')).toContainText('hi tester!');
|
||||||
await page.locator('.tab-attachments').getByText('file attachment').click();
|
await page.locator('.tab-attachments').getByText('file attachment').first().click();
|
||||||
await expect(page.locator('.tab-attachments')).not.toContainText('attach test');
|
await expect(page.locator('.tab-attachments')).not.toContainText('attach test');
|
||||||
|
|
||||||
const downloadPromise = page.waitForEvent('download');
|
{
|
||||||
await page.getByRole('link', { name: 'download' }).first().click();
|
const downloadPromise = page.waitForEvent('download');
|
||||||
const download = await downloadPromise;
|
await page.getByRole('link', { name: 'download' }).first().click();
|
||||||
expect(download.suggestedFilename()).toBe('file attachment');
|
const download = await downloadPromise;
|
||||||
expect((await readAllFromStream(await download.createReadStream())).toString()).toContain('attach test');
|
expect(download.suggestedFilename()).toBe('file attachment');
|
||||||
|
expect((await readAllFromStream(await download.createReadStream())).toString()).toContain('attach test');
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const downloadPromise = page.waitForEvent('download');
|
||||||
|
await page.getByRole('link', { name: 'download' }).nth(1).click();
|
||||||
|
const download = await downloadPromise;
|
||||||
|
expect(download.suggestedFilename()).toBe('file attachment 2');
|
||||||
|
expect((await readAllFromStream(await download.createReadStream())).toString()).toContain('attach test');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should contain binary attachment', async ({ runUITest }) => {
|
test('should contain binary attachment', async ({ runUITest }) => {
|
||||||
|
|
@ -86,6 +99,55 @@ test('should contain string attachment', async ({ runUITest }) => {
|
||||||
expect((await readAllFromStream(await download.createReadStream())).toString()).toEqual('text42');
|
expect((await readAllFromStream(await download.createReadStream())).toString()).toEqual('text42');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should linkify string attachments', async ({ runUITest, server }) => {
|
||||||
|
server.setRoute('/one.html', (req, res) => res.end());
|
||||||
|
server.setRoute('/two.html', (req, res) => res.end());
|
||||||
|
server.setRoute('/three.html', (req, res) => res.end());
|
||||||
|
|
||||||
|
const { page } = await runUITest({
|
||||||
|
'a.test.ts': `
|
||||||
|
import { test } from '@playwright/test';
|
||||||
|
test('attach test', async () => {
|
||||||
|
await test.info().attach('Inline url: ${server.PREFIX + '/one.html'}');
|
||||||
|
await test.info().attach('Second', { body: 'Inline link ${server.PREFIX + '/two.html'} to be highlighted.' });
|
||||||
|
await test.info().attach('Third', { body: '[markdown link](${server.PREFIX + '/three.html'})', contentType: 'text/markdown' });
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
await page.getByText('attach test').click();
|
||||||
|
await page.getByTitle('Run all').click();
|
||||||
|
await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)');
|
||||||
|
await page.getByText('Attachments').click();
|
||||||
|
|
||||||
|
const attachmentsPane = page.locator('.attachments-tab');
|
||||||
|
|
||||||
|
{
|
||||||
|
const url = server.PREFIX + '/one.html';
|
||||||
|
const promise = page.waitForEvent('popup');
|
||||||
|
await attachmentsPane.getByText(url).click();
|
||||||
|
const popup = await promise;
|
||||||
|
await expect(popup).toHaveURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
await attachmentsPane.getByText('Second download').click();
|
||||||
|
const url = server.PREFIX + '/two.html';
|
||||||
|
const promise = page.waitForEvent('popup');
|
||||||
|
await attachmentsPane.getByText(url).click();
|
||||||
|
const popup = await promise;
|
||||||
|
await expect(popup).toHaveURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
await attachmentsPane.getByText('Third download').click();
|
||||||
|
const url = server.PREFIX + '/three.html';
|
||||||
|
const promise = page.waitForEvent('popup');
|
||||||
|
await attachmentsPane.getByText('[markdown link]').click();
|
||||||
|
const popup = await promise;
|
||||||
|
await expect(popup).toHaveURL(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function readAllFromStream(stream: NodeJS.ReadableStream): Promise<Buffer> {
|
function readAllFromStream(stream: NodeJS.ReadableStream): Promise<Buffer> {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const chunks: Buffer[] = [];
|
const chunks: Buffer[] = [];
|
||||||
|
|
|
||||||
|
|
@ -217,7 +217,8 @@ test('should update test locations', async ({ runUITest, writeFiles }) => {
|
||||||
|
|
||||||
const passesItemLocator = page.getByRole('listitem').filter({ hasText: 'passes' });
|
const passesItemLocator = page.getByRole('listitem').filter({ hasText: 'passes' });
|
||||||
await passesItemLocator.hover();
|
await passesItemLocator.hover();
|
||||||
await passesItemLocator.getByTitle('Open in VS Code').click();
|
await passesItemLocator.getByTitle('Show source').click();
|
||||||
|
await page.getByTitle('Open in VS Code').click();
|
||||||
|
|
||||||
expect(messages).toEqual([{
|
expect(messages).toEqual([{
|
||||||
method: 'open',
|
method: 'open',
|
||||||
|
|
@ -247,7 +248,8 @@ test('should update test locations', async ({ runUITest, writeFiles }) => {
|
||||||
|
|
||||||
messages.length = 0;
|
messages.length = 0;
|
||||||
await passesItemLocator.hover();
|
await passesItemLocator.hover();
|
||||||
await passesItemLocator.getByTitle('Open in VS Code').click();
|
await passesItemLocator.getByTitle('Show source').click();
|
||||||
|
await page.getByTitle('Open in VS Code').click();
|
||||||
|
|
||||||
expect(messages).toEqual([{
|
expect(messages).toEqual([{
|
||||||
method: 'open',
|
method: 'open',
|
||||||
|
|
|
||||||
|
|
@ -400,10 +400,18 @@ function generateNameDefault(member, name, t, parent) {
|
||||||
if (names[2] === names[1])
|
if (names[2] === names[1])
|
||||||
names.pop(); // get rid of duplicates, cheaply
|
names.pop(); // get rid of duplicates, cheaply
|
||||||
let attemptedName = names.pop();
|
let attemptedName = names.pop();
|
||||||
const typesDiffer = function(left, right) {
|
const typesDiffer = function(/** @type {Documentation.Type} */ left, /** @type {Documentation.Type} */ right) {
|
||||||
if (left.expression && right.expression)
|
if (left.expression && right.expression)
|
||||||
return left.expression !== right.expression;
|
return left.expression !== right.expression;
|
||||||
return JSON.stringify(right.properties) !== JSON.stringify(left.properties);
|
const toExpression = (/** @type {Documentation.Member} */ t) => t.name + t.type?.expression;
|
||||||
|
const leftOverRightProperties = new Set(left.properties?.map(toExpression) ?? []);
|
||||||
|
for (const prop of right.properties ?? []) {
|
||||||
|
const expression = toExpression(prop);
|
||||||
|
if (!leftOverRightProperties.has(expression))
|
||||||
|
return true;
|
||||||
|
leftOverRightProperties.delete(expression);
|
||||||
|
}
|
||||||
|
return leftOverRightProperties.size > 0;
|
||||||
};
|
};
|
||||||
while (true) {
|
while (true) {
|
||||||
// crude attempt at removing plurality
|
// crude attempt at removing plurality
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis" Version="4.6.0" />
|
<PackageReference Include="Microsoft.CodeAnalysis" Version="4.10.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
4
utils/generate_types/overrides-test.d.ts
vendored
4
utils/generate_types/overrides-test.d.ts
vendored
|
|
@ -144,9 +144,9 @@ export type Fixtures<T extends KeyValue = {}, W extends KeyValue = {}, PT extend
|
||||||
} & {
|
} & {
|
||||||
[K in keyof PT]?: TestFixtureValue<PT[K], T & W & PT & PW> | [TestFixtureValue<PT[K], T & W & PT & PW>, { scope: 'test', timeout?: number | undefined, title?: string, box?: boolean }];
|
[K in keyof PT]?: TestFixtureValue<PT[K], T & W & PT & PW> | [TestFixtureValue<PT[K], T & W & PT & PW>, { scope: 'test', timeout?: number | undefined, title?: string, box?: boolean }];
|
||||||
} & {
|
} & {
|
||||||
[K in Exclude<keyof W, keyof PW>]?: [WorkerFixtureValue<W[K], W & PW>, { scope: 'worker', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }];
|
[K in keyof W]?: [WorkerFixtureValue<W[K], W & PW>, { scope: 'worker', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }];
|
||||||
} & {
|
} & {
|
||||||
[K in Exclude<keyof T, keyof PT>]?: TestFixtureValue<T[K], T & W & PT & PW> | [TestFixtureValue<T[K], T & W & PT & PW>, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }];
|
[K in keyof T]?: TestFixtureValue<T[K], T & W & PT & PW> | [TestFixtureValue<T[K], T & W & PT & PW>, { scope?: 'test', auto?: boolean, option?: boolean, timeout?: number | undefined, title?: string, box?: boolean }];
|
||||||
};
|
};
|
||||||
|
|
||||||
type BrowserName = 'chromium' | 'firefox' | 'webkit';
|
type BrowserName = 'chromium' | 'firefox' | 'webkit';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue