Compare commits
26 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,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 +71,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
|
||||||
|
|
|
||||||
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.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "playwright-internal",
|
"name": "playwright-internal",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.0",
|
||||||
"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.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.46.0-next"
|
"playwright-core": "1.46.0"
|
||||||
},
|
},
|
||||||
"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.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.46.0-next"
|
"playwright-core": "1.46.0"
|
||||||
},
|
},
|
||||||
"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.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.46.0-next"
|
"playwright-core": "1.46.0"
|
||||||
},
|
},
|
||||||
"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.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.46.0-next"
|
"playwright-core": "1.46.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/playwright-chromium": {
|
"packages/playwright-chromium": {
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.46.0-next"
|
"playwright-core": "1.46.0"
|
||||||
},
|
},
|
||||||
"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.0",
|
||||||
"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.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright": "1.46.0-next",
|
"playwright": "1.46.0",
|
||||||
"playwright-core": "1.46.0-next",
|
"playwright-core": "1.46.0",
|
||||||
"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.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.46.0-next",
|
"@playwright/experimental-ct-core": "1.46.0",
|
||||||
"@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.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.46.0-next",
|
"@playwright/experimental-ct-core": "1.46.0",
|
||||||
"@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.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.46.0-next",
|
"@playwright/experimental-ct-core": "1.46.0",
|
||||||
"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.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.46.0-next",
|
"@playwright/experimental-ct-core": "1.46.0",
|
||||||
"@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.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.46.0-next",
|
"@playwright/experimental-ct-core": "1.46.0",
|
||||||
"@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.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@playwright/experimental-ct-core": "1.46.0-next",
|
"@playwright/experimental-ct-core": "1.46.0",
|
||||||
"@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.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.46.0-next"
|
"playwright-core": "1.46.0"
|
||||||
},
|
},
|
||||||
"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.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright": "1.46.0-next"
|
"playwright": "1.46.0"
|
||||||
},
|
},
|
||||||
"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.0",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.46.0-next"
|
"playwright-core": "1.46.0"
|
||||||
},
|
},
|
||||||
"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.0",
|
||||||
"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.0",
|
||||||
"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.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/browser-firefox",
|
"name": "@playwright/browser-firefox",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.0",
|
||||||
"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.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/browser-webkit",
|
"name": "@playwright/browser-webkit",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.0",
|
||||||
"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.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "playwright-chromium",
|
"name": "playwright-chromium",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.0",
|
||||||
"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.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.0",
|
||||||
"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 }));
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ 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, 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';
|
||||||
|
|
@ -142,38 +142,18 @@ class SocksProxyConnection {
|
||||||
dummyServer.emit('connection', this.internal);
|
dummyServer.emit('connection', this.internal);
|
||||||
dummyServer.on('secureConnection', internalTLS => {
|
dummyServer.on('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) {
|
||||||
|
|
@ -201,12 +181,43 @@ 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.on('secureConnect', () => {
|
||||||
|
internalTLS.pipe(targetTLS);
|
||||||
|
targetTLS.pipe(internalTLS);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
internalTLS.on('end', () => closeBothSockets());
|
||||||
|
targetTLS.on('end', () => closeBothSockets());
|
||||||
|
|
||||||
|
internalTLS.on('error', () => closeBothSockets());
|
||||||
|
targetTLS.on('error', handleError);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -288,3 +299,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'));
|
||||||
|
}
|
||||||
|
|
@ -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.0",
|
||||||
"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.0",
|
||||||
"vite": "^5.2.8",
|
"vite": "^5.2.8",
|
||||||
"playwright": "1.46.0-next"
|
"playwright": "1.46.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/experimental-ct-react",
|
"name": "@playwright/experimental-ct-react",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.0",
|
||||||
"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.0",
|
||||||
"@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.0",
|
||||||
"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.0",
|
||||||
"@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.0",
|
||||||
"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.0",
|
||||||
"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.0",
|
||||||
"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.0",
|
||||||
"@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.0",
|
||||||
"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.0",
|
||||||
"@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.0",
|
||||||
"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.0",
|
||||||
"@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.0",
|
||||||
"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.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@playwright/test",
|
"name": "@playwright/test",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.0",
|
||||||
"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.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "playwright-webkit",
|
"name": "playwright-webkit",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.0",
|
||||||
"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.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "playwright",
|
"name": "playwright",
|
||||||
"version": "1.46.0-next",
|
"version": "1.46.0",
|
||||||
"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.0"
|
||||||
},
|
},
|
||||||
"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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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,7 +496,9 @@ export const UIModeView: React.FC<{}> = ({
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
requestedCollapseAllCount={collapseAllCount}
|
requestedCollapseAllCount={collapseAllCount}
|
||||||
setFilterText={setFilterText}
|
setFilterText={setFilterText}
|
||||||
|
onRevealSource={onRevealSource}
|
||||||
/>
|
/>
|
||||||
|
{showTestingOptions && <>
|
||||||
<Toolbar noShadow={true} noMinHeight={true} className='settings-toolbar' onClick={() => setTestingOptionsVisible(!testingOptionsVisible)}>
|
<Toolbar noShadow={true} noMinHeight={true} className='settings-toolbar' onClick={() => setTestingOptionsVisible(!testingOptionsVisible)}>
|
||||||
<span
|
<span
|
||||||
className={`codicon codicon-${testingOptionsVisible ? 'chevron-down' : 'chevron-right'}`}
|
className={`codicon codicon-${testingOptionsVisible ? 'chevron-down' : 'chevron-right'}`}
|
||||||
|
|
@ -501,6 +512,7 @@ export const UIModeView: React.FC<{}> = ({
|
||||||
showBrowserSetting,
|
showBrowserSetting,
|
||||||
updateSnapshotsSetting,
|
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);
|
||||||
|
|
@ -60,7 +60,8 @@ 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;
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,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 +64,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 +82,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 +112,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 +121,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 +138,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 +155,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 +169,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({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
|
||||||
|
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({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' });
|
||||||
|
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 +260,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 +275,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 +290,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 +302,78 @@ test.describe('browser', () => {
|
||||||
await page.close();
|
await page.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 +394,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 +403,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 +432,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 +457,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 +480,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 () => {
|
||||||
|
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/,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 }) => {
|
||||||
|
|
|
||||||
|
|
@ -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');
|
const downloadPromise = page.waitForEvent('download');
|
||||||
await page.getByRole('link', { name: 'download' }).first().click();
|
await page.getByRole('link', { name: 'download' }).first().click();
|
||||||
const download = await downloadPromise;
|
const download = await downloadPromise;
|
||||||
expect(download.suggestedFilename()).toBe('file attachment');
|
expect(download.suggestedFilename()).toBe('file attachment');
|
||||||
expect((await readAllFromStream(await download.createReadStream())).toString()).toContain('attach test');
|
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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue