Merge branch 'main' into watch-icon-focused
This commit is contained in:
commit
7ad93b85b6
|
|
@ -1,6 +1,6 @@
|
|||
# 🎭 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://aka.ms/playwright/discord)
|
||||
[](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://aka.ms/playwright/discord)
|
||||
|
||||
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
|
||||
|
||||
|
|
@ -8,9 +8,9 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
|
|||
|
||||
| | Linux | macOS | Windows |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| Chromium <!-- GEN:chromium-version -->133.0.6943.35<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| Chromium <!-- GEN:chromium-version -->134.0.6998.3<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| WebKit <!-- GEN:webkit-version -->18.2<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| Firefox <!-- GEN:firefox-version -->134.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| Firefox <!-- GEN:firefox-version -->135.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
|
||||
Headless execution is supported for all browsers on all platforms. Check out [system requirements](https://playwright.dev/docs/intro#system-requirements) for details.
|
||||
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ Methods like [`method: APIRequestContext.get`] take the base URL into considerat
|
|||
- `records` <[Array]<[Object]>>
|
||||
- `key` ?<[Object]>
|
||||
- `keyEncoded` ?<[Object]> if `key` is not JSON-serializable, this contains an encoded version that preserves types.
|
||||
- `value` <[Object]>
|
||||
- `value` ?<[Object]>
|
||||
- `valueEncoded` ?<[Object]> if `value` is not JSON-serializable, this contains an encoded version that preserves types.
|
||||
|
||||
Populates context with given storage state. This option can be used to initialize context with logged-in information
|
||||
|
|
|
|||
|
|
@ -897,7 +897,7 @@ context cookies from the response. The method will automatically follow redirect
|
|||
- `records` <[Array]<[Object]>>
|
||||
- `key` ?<[Object]>
|
||||
- `keyEncoded` ?<[Object]> if `key` is not JSON-serializable, this contains an encoded version that preserves types.
|
||||
- `value` <[Object]>
|
||||
- `value` ?<[Object]>
|
||||
- `valueEncoded` ?<[Object]> if `value` is not JSON-serializable, this contains an encoded version that preserves types.
|
||||
|
||||
Returns storage state for this request context, contains current cookies and local storage snapshot if it was passed to the constructor.
|
||||
|
|
|
|||
|
|
@ -1528,7 +1528,7 @@ Whether to emulate network being offline for the browser context.
|
|||
- `records` <[Array]<[Object]>>
|
||||
- `key` ?<[Object]>
|
||||
- `keyEncoded` ?<[Object]> if `key` is not JSON-serializable, this contains an encoded version that preserves types.
|
||||
- `value` <[Object]>
|
||||
- `value` ?<[Object]>
|
||||
- `valueEncoded` ?<[Object]> if `value` is not JSON-serializable, this contains an encoded version that preserves types.
|
||||
|
||||
Returns storage state for this browser context, contains current cookies, local storage snapshot and IndexedDB snapshot.
|
||||
|
|
|
|||
|
|
@ -281,7 +281,7 @@ Specify environment variables that will be visible to the browser. Defaults to `
|
|||
- `records` <[Array]<[Object]>>
|
||||
- `key` ?<[Object]>
|
||||
- `keyEncoded` ?<[Object]> if `key` is not JSON-serializable, this contains an encoded version that preserves types.
|
||||
- `value` <[Object]>
|
||||
- `value` ?<[Object]>
|
||||
- `valueEncoded` ?<[Object]> if `value` is not JSON-serializable, this contains an encoded version that preserves types.
|
||||
|
||||
Learn more about [storage state and auth](../auth.md).
|
||||
|
|
|
|||
|
|
@ -18,12 +18,13 @@ UI Mode lets you explore, run, and debug tests with a time travel experience com
|
|||
|
||||
To open UI mode, run the following command in your terminal:
|
||||
|
||||
```bash
|
||||
npx playwright test --ui
|
||||
```
|
||||
```bash
|
||||
npx playwright test --ui
|
||||
```
|
||||
|
||||
## Running your tests
|
||||
|
||||
Once you launch UI Mode you will see a list of all your test files. You can run all your tests by clicking the triangle icon in the sidebar. You can also run a single test file, a block of tests or a single test by hovering over the name and clicking on the triangle next to it.
|
||||
Once you launch UI Mode you will see a list of all your test files. You can run all your tests by clicking the triangle icon in the sidebar. You can also run a single test file, a block of tests or a single test by hovering over the name and clicking on the triangle next to it.
|
||||
|
||||

|
||||
|
||||
|
|
@ -33,17 +34,15 @@ Filter tests by text or `@tag` or by passed, failed or skipped tests. You can al
|
|||
|
||||

|
||||
|
||||
|
||||
## Timeline view
|
||||
|
||||
At the top of the trace you can see a timeline view of your test with different colors to highlight navigation and actions. Hover back and forth to see an image snapshot for each action. Double click on an action to see the time range for that action. You can use the slider in the timeline to increase the actions selected and these will be shown in the Actions tab and all console logs and network logs will be filtered to only show the logs for the actions selected.
|
||||
|
||||

|
||||
|
||||
|
||||
## Actions
|
||||
|
||||
In the Actions tab you can see what locator was used for every action and how long each one took to run. Hover over each action of your test and visually see the change in the DOM snapshot. Go back and forward in time and click an action to inspect and debug. Use the Before and After tabs to visually see what happened before and after the action.
|
||||
In the Actions tab you can see what locator was used for every action and how long each one took to run. Hover over each action of your test and visually see the change in the DOM snapshot. Go back and forward in time and click an action to inspect and debug. Use the Before and After tabs to visually see what happened before and after the action.
|
||||

|
||||
|
||||
## Pop out and inspect the DOM
|
||||
|
|
@ -60,7 +59,7 @@ Click on the pick locator button and hover over the DOM snapshot to see the loca
|
|||
|
||||
## Source
|
||||
|
||||
As you hover over each action of your test the line of code for that action is highlighted in the source panel.
|
||||
As you hover over each action of your test the line of code for that action is highlighted in the source panel. The button "Open in VSCode" is at the top-right of this section. Upon clicking the button, it will open your test in VS Code right at the line of code that you clicked on.
|
||||
|
||||

|
||||
|
||||
|
|
@ -108,7 +107,7 @@ Next to the Actions tab you will find the Metadata tab which will show you more
|
|||
|
||||
## Watch mode
|
||||
|
||||
Next to the name of each test in the sidebar you will find an eye icon. Clicking on the icon will activate watch mode which will re-run the test when you make changes to it. You can watch a number of tests at the same time be clicking the eye icon next to each one or all tests by clicking the eye icon at the top of the sidebar. If you are using VS Code then you can easily open your test by clicking on the file icon next to the eye icon. This will open your test in VS Code right at the line of code that you clicked on.
|
||||
Next to the name of each test in the sidebar you will find an eye icon. Clicking on the icon will activate watch mode which will re-run the test when you make changes to it. You can watch a number of tests at the same time be clicking the eye icon next to each one or all tests by clicking the eye icon at the top of the sidebar.
|
||||
|
||||

|
||||
|
||||
|
|
|
|||
315
package-lock.json
generated
315
package-lock.json
generated
|
|
@ -50,7 +50,7 @@
|
|||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^16.4.5",
|
||||
"electron": "^30.1.2",
|
||||
"esbuild": "^0.18.11",
|
||||
"esbuild": "^0.25.0",
|
||||
"eslint": "^9.19.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-notice": "^1.0.0",
|
||||
|
|
@ -885,355 +885,411 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
|
||||
"integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz",
|
||||
"integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
|
||||
"integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
|
||||
"integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
|
||||
"integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
|
||||
"integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz",
|
||||
"integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
|
||||
"integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
|
||||
"integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz",
|
||||
"integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
|
||||
"integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz",
|
||||
"integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
|
||||
"integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz",
|
||||
"integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
|
||||
"integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz",
|
||||
"integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
|
||||
"integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz",
|
||||
"integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
|
||||
"integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz",
|
||||
"integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
|
||||
"integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
|
||||
"integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz",
|
||||
"integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
|
|
@ -3857,40 +3913,61 @@
|
|||
"optional": true
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
|
||||
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz",
|
||||
"integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/android-arm": "0.18.20",
|
||||
"@esbuild/android-arm64": "0.18.20",
|
||||
"@esbuild/android-x64": "0.18.20",
|
||||
"@esbuild/darwin-arm64": "0.18.20",
|
||||
"@esbuild/darwin-x64": "0.18.20",
|
||||
"@esbuild/freebsd-arm64": "0.18.20",
|
||||
"@esbuild/freebsd-x64": "0.18.20",
|
||||
"@esbuild/linux-arm": "0.18.20",
|
||||
"@esbuild/linux-arm64": "0.18.20",
|
||||
"@esbuild/linux-ia32": "0.18.20",
|
||||
"@esbuild/linux-loong64": "0.18.20",
|
||||
"@esbuild/linux-mips64el": "0.18.20",
|
||||
"@esbuild/linux-ppc64": "0.18.20",
|
||||
"@esbuild/linux-riscv64": "0.18.20",
|
||||
"@esbuild/linux-s390x": "0.18.20",
|
||||
"@esbuild/linux-x64": "0.18.20",
|
||||
"@esbuild/netbsd-x64": "0.18.20",
|
||||
"@esbuild/openbsd-x64": "0.18.20",
|
||||
"@esbuild/sunos-x64": "0.18.20",
|
||||
"@esbuild/win32-arm64": "0.18.20",
|
||||
"@esbuild/win32-ia32": "0.18.20",
|
||||
"@esbuild/win32-x64": "0.18.20"
|
||||
"@esbuild/aix-ppc64": "0.25.0",
|
||||
"@esbuild/android-arm": "0.25.0",
|
||||
"@esbuild/android-arm64": "0.25.0",
|
||||
"@esbuild/android-x64": "0.25.0",
|
||||
"@esbuild/darwin-arm64": "0.25.0",
|
||||
"@esbuild/darwin-x64": "0.25.0",
|
||||
"@esbuild/freebsd-arm64": "0.25.0",
|
||||
"@esbuild/freebsd-x64": "0.25.0",
|
||||
"@esbuild/linux-arm": "0.25.0",
|
||||
"@esbuild/linux-arm64": "0.25.0",
|
||||
"@esbuild/linux-ia32": "0.25.0",
|
||||
"@esbuild/linux-loong64": "0.25.0",
|
||||
"@esbuild/linux-mips64el": "0.25.0",
|
||||
"@esbuild/linux-ppc64": "0.25.0",
|
||||
"@esbuild/linux-riscv64": "0.25.0",
|
||||
"@esbuild/linux-s390x": "0.25.0",
|
||||
"@esbuild/linux-x64": "0.25.0",
|
||||
"@esbuild/netbsd-arm64": "0.25.0",
|
||||
"@esbuild/netbsd-x64": "0.25.0",
|
||||
"@esbuild/openbsd-arm64": "0.25.0",
|
||||
"@esbuild/openbsd-x64": "0.25.0",
|
||||
"@esbuild/sunos-x64": "0.25.0",
|
||||
"@esbuild/win32-arm64": "0.25.0",
|
||||
"@esbuild/win32-ia32": "0.25.0",
|
||||
"@esbuild/win32-x64": "0.25.0"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild/node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz",
|
||||
"integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@
|
|||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^16.4.5",
|
||||
"electron": "^30.1.2",
|
||||
"esbuild": "^0.18.11",
|
||||
"esbuild": "^0.25.0",
|
||||
"eslint": "^9.19.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-notice": "^1.0.0",
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export const CopyToClipboard: React.FunctionComponent<CopyToClipboardProps> = ({
|
|||
});
|
||||
}, [value]);
|
||||
const iconElement = icon === 'check' ? icons.check() : icon === 'cross' ? icons.cross() : icons.copy();
|
||||
return <button className='copy-icon' aria-label='Copy to clipboard' onClick={handleCopy}>{iconElement}</button>;
|
||||
return <button className='copy-icon' title='Copy to clipboard' aria-label='Copy to clipboard' onClick={handleCopy}>{iconElement}</button>;
|
||||
};
|
||||
|
||||
type CopyToClipboardContainerProps = CopyToClipboardProps & {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
cursor: pointer;
|
||||
user-select: none;
|
||||
margin-left: 5px;
|
||||
color: var(--color-fg-default);
|
||||
}
|
||||
|
||||
.metadata-view {
|
||||
|
|
@ -26,16 +27,46 @@
|
|||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.metadata-view .metadata-section {
|
||||
margin: 8px 10px 8px 32px;
|
||||
}
|
||||
|
||||
.metadata-view span:not(.copy-button-container),
|
||||
.metadata-view a {
|
||||
display: inline-block;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.metadata-section {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.metadata-properties {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: normal;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.metadata-properties > div {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.metadata-separator {
|
||||
height: 1px;
|
||||
border-bottom: 1px solid var(--color-border-default);
|
||||
}
|
||||
|
||||
.metadata-view .copy-value-container {
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
.git-commit-info a {
|
||||
color: var(--color-fg-default);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.copyable-property {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.copyable-property > span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,9 +26,24 @@ import { linkifyText } from '@web/renderUtils';
|
|||
|
||||
type MetadataEntries = [string, unknown][];
|
||||
|
||||
export function filterMetadata(metadata: Metadata): MetadataEntries {
|
||||
// TODO: do not plumb actualWorkers through metadata.
|
||||
return Object.entries(metadata).filter(([key]) => key !== 'actualWorkers');
|
||||
export const MetadataContext = React.createContext<MetadataEntries>([]);
|
||||
|
||||
export function MetadataProvider({ metadata, children }: React.PropsWithChildren<{ metadata: Metadata }>) {
|
||||
const entries = React.useMemo(() => {
|
||||
// TODO: do not plumb actualWorkers through metadata.
|
||||
return Object.entries(metadata).filter(([key]) => key !== 'actualWorkers');
|
||||
}, [metadata]);
|
||||
|
||||
return <MetadataContext.Provider value={entries}>{children}</MetadataContext.Provider>;
|
||||
}
|
||||
|
||||
export function useMetadata() {
|
||||
return React.useContext(MetadataContext);
|
||||
}
|
||||
|
||||
export function useGitCommitInfo() {
|
||||
const metadataEntries = useMetadata();
|
||||
return metadataEntries.find(([key]) => key === 'git.commit.info')?.[1] as GitCommitInfo | undefined;
|
||||
}
|
||||
|
||||
class ErrorBoundary extends React.Component<React.PropsWithChildren<{}>, { error: Error | null, errorInfo: React.ErrorInfo | null }> {
|
||||
|
|
@ -57,12 +72,13 @@ class ErrorBoundary extends React.Component<React.PropsWithChildren<{}>, { error
|
|||
}
|
||||
}
|
||||
|
||||
export const MetadataView: React.FC<{ metadataEntries: MetadataEntries }> = ({ metadataEntries }) => {
|
||||
return <ErrorBoundary><InnerMetadataView metadataEntries={metadataEntries}/></ErrorBoundary>;
|
||||
export const MetadataView = () => {
|
||||
return <ErrorBoundary><InnerMetadataView/></ErrorBoundary>;
|
||||
};
|
||||
|
||||
const InnerMetadataView: React.FC<{ metadataEntries: MetadataEntries }> = ({ metadataEntries }) => {
|
||||
const gitCommitInfo = metadataEntries.find(([key]) => key === 'git.commit.info')?.[1] as GitCommitInfo | undefined;
|
||||
const InnerMetadataView = () => {
|
||||
const metadataEntries = useMetadata();
|
||||
const gitCommitInfo = useGitCommitInfo();
|
||||
const entries = metadataEntries.filter(([key]) => key !== 'git.commit.info');
|
||||
if (!gitCommitInfo && !entries.length)
|
||||
return null;
|
||||
|
|
@ -71,30 +87,43 @@ const InnerMetadataView: React.FC<{ metadataEntries: MetadataEntries }> = ({ met
|
|||
<GitCommitInfoView info={gitCommitInfo}/>
|
||||
{entries.length > 0 && <div className='metadata-separator' />}
|
||||
</>}
|
||||
{entries.map(([key, value]) => {
|
||||
const valueString = typeof value !== 'object' || value === null || value === undefined ? String(value) : JSON.stringify(value);
|
||||
const trimmedValue = valueString.length > 1000 ? valueString.slice(0, 1000) + '\u2026' : valueString;
|
||||
return <div className='m-1 ml-5' key={key}>
|
||||
<span style={{ fontWeight: 'bold' }} title={key}>{key}</span>
|
||||
{valueString && <CopyToClipboardContainer value={valueString}>: <span title={trimmedValue}>{linkifyText(trimmedValue)}</span></CopyToClipboardContainer>}
|
||||
</div>;
|
||||
})}
|
||||
<div className='metadata-section metadata-properties'>
|
||||
{entries.map(([propertyName, value]) => {
|
||||
const valueString = typeof value !== 'object' || value === null || value === undefined ? String(value) : JSON.stringify(value);
|
||||
const trimmedValue = valueString.length > 1000 ? valueString.slice(0, 1000) + '\u2026' : valueString;
|
||||
return (
|
||||
<div key={propertyName} className='copyable-property'>
|
||||
<CopyToClipboardContainer value={valueString}>
|
||||
<span style={{ fontWeight: 'bold' }} title={propertyName}>{propertyName}</span>
|
||||
: <span title={trimmedValue}>{linkifyText(trimmedValue)}</span>
|
||||
</CopyToClipboardContainer>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
const GitCommitInfoView: React.FC<{ info: GitCommitInfo }> = ({ info }) => {
|
||||
const email = info['revision.email'] ? ` <${info['revision.email']}>` : '';
|
||||
const author = `${info['revision.author'] || ''}${email}`;
|
||||
const subject = info['revision.subject'] || '';
|
||||
const shortTimestamp = Intl.DateTimeFormat(undefined, { dateStyle: 'medium' }).format(info['revision.timestamp']);
|
||||
const longTimestamp = Intl.DateTimeFormat(undefined, { dateStyle: 'full', timeStyle: 'long' }).format(info['revision.timestamp']);
|
||||
return <div className='hbox pl-4 pr-2 git-commit-info' style={{ alignItems: 'center' }}>
|
||||
<div className='vbox'>
|
||||
<a className='m-2' href={info['revision.link']} target='_blank' rel='noopener noreferrer'>
|
||||
<span title={info['revision.subject'] || ''}>{info['revision.subject'] || ''}</span>
|
||||
</a>
|
||||
<div className='hbox m-2 mt-1'>
|
||||
<div className='mr-1'>{author}</div>
|
||||
<div title={longTimestamp}> on {shortTimestamp}</div>
|
||||
return <div className='hbox git-commit-info metadata-section'>
|
||||
<div className='vbox metadata-properties'>
|
||||
<div>
|
||||
{info['revision.link'] ? (
|
||||
<a href={info['revision.link']} target='_blank' rel='noopener noreferrer' title={subject}>
|
||||
{subject}
|
||||
</a>
|
||||
) : <span title={subject}>
|
||||
{subject}
|
||||
</span>}
|
||||
</div>
|
||||
<div className='hbox'>
|
||||
<span className='mr-1'>{author}</span>
|
||||
<span title={longTimestamp}> on {shortTimestamp}</span>
|
||||
{info['ci.link'] && (
|
||||
<>
|
||||
<span className='mx-2'>·</span>
|
||||
|
|
@ -109,9 +138,10 @@ const GitCommitInfoView: React.FC<{ info: GitCommitInfo }> = ({ info }) => {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!!info['revision.link'] && <a href={info['revision.link']} target='_blank' rel='noopener noreferrer'>
|
||||
<span title='View commit details'>{info['revision.id']?.slice(0, 7) || 'unknown'}</span>
|
||||
</a>}
|
||||
{!info['revision.link'] && !!info['revision.id'] && <span>{info['revision.id'].slice(0, 7)}</span>}
|
||||
{!!info['revision.link'] ? (
|
||||
<a href={info['revision.link']} target='_blank' rel='noopener noreferrer' title='View commit details'>
|
||||
{info['revision.id']?.slice(0, 7) || 'unknown'}
|
||||
</a>
|
||||
) : !!info['revision.id'] && <span>{info['revision.id'].slice(0, 7)}</span>}
|
||||
</div>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import './reportView.css';
|
|||
import { TestCaseView } from './testCaseView';
|
||||
import { TestFilesHeader, TestFilesView } from './testFilesView';
|
||||
import './theme.css';
|
||||
import { MetadataProvider } from './metadataView';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
|
@ -72,7 +73,7 @@ export const ReportView: React.FC<{
|
|||
return result;
|
||||
}, [report, filter]);
|
||||
|
||||
return <div className='htmlreport vbox px-4 pb-4'>
|
||||
return <MetadataProvider metadata={report?.json().metadata ?? {}}><div className='htmlreport vbox px-4 pb-4'>
|
||||
<main>
|
||||
{report?.json() && <HeaderView stats={report.json().stats} filterText={filterText} setFilterText={setFilterText}></HeaderView>}
|
||||
<Route predicate={testFilesRoutePredicate}>
|
||||
|
|
@ -88,7 +89,7 @@ export const ReportView: React.FC<{
|
|||
{!!report && <TestCaseViewLoader report={report} tests={filteredTests.tests} testIdToFileIdMap={testIdToFileIdMap} />}
|
||||
</Route>
|
||||
</main>
|
||||
</div>;
|
||||
</div></MetadataProvider>;
|
||||
};
|
||||
|
||||
const TestCaseViewLoader: React.FC<{
|
||||
|
|
|
|||
|
|
@ -16,18 +16,47 @@
|
|||
|
||||
@import '@web/third_party/vscode/colors.css';
|
||||
|
||||
.test-error-view {
|
||||
.test-error-container {
|
||||
white-space: pre;
|
||||
overflow: auto;
|
||||
flex: none;
|
||||
padding: 0;
|
||||
background-color: var(--color-canvas-subtle);
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
line-height: initial;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.test-error-view {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.test-error-text {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.prompt-button {
|
||||
flex: none;
|
||||
height: 24px;
|
||||
width: 80px;
|
||||
border: 1px solid var(--color-btn-border);
|
||||
outline: none;
|
||||
color: var(--color-btn-text);
|
||||
background: var(--color-btn-bg);
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.prompt-button svg {
|
||||
color: var(--color-fg-subtle);
|
||||
}
|
||||
|
||||
.prompt-button:not(:disabled):hover {
|
||||
border-color: var(--color-btn-hover-border);
|
||||
background-color: var(--color-btn-hover-bg);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,15 +17,57 @@
|
|||
import { ansi2html } from '@web/ansi2html';
|
||||
import * as React from 'react';
|
||||
import './testErrorView.css';
|
||||
import * as icons from './icons';
|
||||
import type { ImageDiff } from '@web/shared/imageDiffView';
|
||||
import { ImageDiffView } from '@web/shared/imageDiffView';
|
||||
import type { TestResult } from './types';
|
||||
import { fixTestPrompt } from '@web/components/prompts';
|
||||
import { useGitCommitInfo } from './metadataView';
|
||||
|
||||
export const TestErrorView: React.FC<{
|
||||
export const TestErrorView: React.FC<{ error: string; testId?: string; result?: TestResult }> = ({ error, testId, result }) => {
|
||||
return (
|
||||
<CodeSnippet code={error} testId={testId}>
|
||||
<div style={{ float: 'right', padding: '5px' }}>
|
||||
<PromptButton error={error} result={result} />
|
||||
</div>
|
||||
</CodeSnippet>
|
||||
);
|
||||
};
|
||||
|
||||
export const CodeSnippet = ({ code, children, testId }: React.PropsWithChildren<{ code: string; testId?: string; }>) => {
|
||||
const html = React.useMemo(() => ansiErrorToHtml(code), [code]);
|
||||
return (
|
||||
<div className='test-error-container test-error-text' data-testid={testId}>
|
||||
{children}
|
||||
<div className='test-error-view' dangerouslySetInnerHTML={{ __html: html || '' }}></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PromptButton: React.FC<{
|
||||
error: string;
|
||||
testId?: string;
|
||||
}> = ({ error, testId }) => {
|
||||
const html = React.useMemo(() => ansiErrorToHtml(error), [error]);
|
||||
return <div className='test-error-view test-error-text' data-testid={testId} dangerouslySetInnerHTML={{ __html: html || '' }}></div>;
|
||||
result?: TestResult;
|
||||
}> = ({ error, result }) => {
|
||||
const gitCommitInfo = useGitCommitInfo();
|
||||
const prompt = React.useMemo(() => fixTestPrompt(
|
||||
error,
|
||||
gitCommitInfo?.['pull.diff'] ?? gitCommitInfo?.['revision.diff'],
|
||||
result?.attachments.find(a => a.name === 'pageSnapshot')?.body
|
||||
), [gitCommitInfo, result, error]);
|
||||
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
|
||||
return <button
|
||||
className='prompt-button'
|
||||
onClick={async () => {
|
||||
await navigator.clipboard.writeText(prompt);
|
||||
setCopied(true);
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 3000);
|
||||
}}>
|
||||
{copied ? <span className='prompt-button-copied'>Copied <icons.copy/></span> : 'Fix with AI'}
|
||||
</button>;
|
||||
};
|
||||
|
||||
export const TestScreenshotErrorView: React.FC<{
|
||||
|
|
|
|||
|
|
@ -69,4 +69,11 @@
|
|||
|
||||
.test-file-test-status-icon {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.test-file-header-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--color-fg-subtle);
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@ import { msToString } from './utils';
|
|||
import { AutoChip } from './chip';
|
||||
import { TestErrorView } from './testErrorView';
|
||||
import * as icons from './icons';
|
||||
import { filterMetadata, MetadataView } from './metadataView';
|
||||
import { MetadataView, useMetadata } from './metadataView';
|
||||
|
||||
export const TestFilesView: React.FC<{
|
||||
tests: TestFileSummary[],
|
||||
|
|
@ -67,21 +67,23 @@ export const TestFilesHeader: React.FC<{
|
|||
metadataVisible: boolean,
|
||||
toggleMetadataVisible: () => void,
|
||||
}> = ({ report, filteredStats, metadataVisible, toggleMetadataVisible }) => {
|
||||
const metadataEntries = useMetadata();
|
||||
if (!report)
|
||||
return;
|
||||
const metadataEntries = filterMetadata(report.metadata || {});
|
||||
return null;
|
||||
return <>
|
||||
<div className='mx-1' style={{ display: 'flex', marginTop: 10 }}>
|
||||
{metadataEntries.length > 0 && <div className='metadata-toggle' role='button' onClick={toggleMetadataVisible} title={metadataVisible ? 'Hide metadata' : 'Show metadata'}>
|
||||
{metadataVisible ? icons.downArrow() : icons.rightArrow()}Metadata
|
||||
</div>}
|
||||
{report.projectNames.length === 1 && !!report.projectNames[0] && <div data-testid='project-name' style={{ color: 'var(--color-fg-subtle)' }}>Project: {report.projectNames[0]}</div>}
|
||||
{filteredStats && <div data-testid='filtered-tests-count' style={{ color: 'var(--color-fg-subtle)', padding: '0 10px' }}>Filtered: {filteredStats.total} {!!filteredStats.total && ('(' + msToString(filteredStats.duration) + ')')}</div>}
|
||||
<div className='test-file-header-info'>
|
||||
{metadataEntries.length > 0 && <div className='metadata-toggle' role='button' onClick={toggleMetadataVisible} title={metadataVisible ? 'Hide metadata' : 'Show metadata'}>
|
||||
{metadataVisible ? icons.downArrow() : icons.rightArrow()}Metadata
|
||||
</div>}
|
||||
{report.projectNames.length === 1 && !!report.projectNames[0] && <div data-testid='project-name'>Project: {report.projectNames[0]}</div>}
|
||||
{filteredStats && <div data-testid='filtered-tests-count'>Filtered: {filteredStats.total} {!!filteredStats.total && ('(' + msToString(filteredStats.duration) + ')')}</div>}
|
||||
</div>
|
||||
<div style={{ flex: 'auto' }}></div>
|
||||
<div data-testid='overall-time' style={{ color: 'var(--color-fg-subtle)', marginRight: '10px' }}>{report ? new Date(report.startTime).toLocaleString() : ''}</div>
|
||||
<div data-testid='overall-duration' style={{ color: 'var(--color-fg-subtle)' }}>Total time: {msToString(report.duration ?? 0)}</div>
|
||||
</div>
|
||||
{metadataVisible && <MetadataView metadataEntries={metadataEntries}/>}
|
||||
{metadataVisible && <MetadataView/>}
|
||||
{!!report.errors.length && <AutoChip header='Errors' dataTestId='report-errors'>
|
||||
{report.errors.map((error, index) => <TestErrorView key={'test-report-error-message-' + index} error={error}></TestErrorView>)}
|
||||
</AutoChip>}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import { Anchor, AttachmentLink, generateTraceUrl, testResultHref } from './link
|
|||
import { statusIcon } from './statusIcon';
|
||||
import type { ImageDiff } from '@web/shared/imageDiffView';
|
||||
import { ImageDiffView } from '@web/shared/imageDiffView';
|
||||
import { TestErrorView, TestScreenshotErrorView } from './testErrorView';
|
||||
import { CodeSnippet, TestErrorView, TestScreenshotErrorView } from './testErrorView';
|
||||
import * as icons from './icons';
|
||||
import './testResultView.css';
|
||||
|
||||
|
|
@ -90,7 +90,7 @@ export const TestResultView: React.FC<{
|
|||
{errors.map((error, index) => {
|
||||
if (error.type === 'screenshot')
|
||||
return <TestScreenshotErrorView key={'test-result-error-message-' + index} errorPrefix={error.errorPrefix} diff={error.diff!} errorSuffix={error.errorSuffix}></TestScreenshotErrorView>;
|
||||
return <TestErrorView key={'test-result-error-message-' + index} error={error.error!}></TestErrorView>;
|
||||
return <TestErrorView key={'test-result-error-message-' + index} error={error.error!} result={result}></TestErrorView>;
|
||||
})}
|
||||
</AutoChip>}
|
||||
{!!result.steps.length && <AutoChip header='Test Steps'>
|
||||
|
|
@ -182,7 +182,7 @@ const StepTreeItem: React.FC<{
|
|||
{step.count > 1 && <> ✕ <span className='test-result-counter'>{step.count}</span></>}
|
||||
{step.location && <span className='test-result-path'>— {step.location.file}:{step.location.line}</span>}
|
||||
</span>} loadChildren={step.steps.length || step.snippet ? () => {
|
||||
const snippet = step.snippet ? [<TestErrorView testId='test-snippet' key='line' error={step.snippet}/>] : [];
|
||||
const snippet = step.snippet ? [<CodeSnippet testId='test-snippet' key='line' code={step.snippet} />] : [];
|
||||
const steps = step.steps.map((s, i) => <StepTreeItem key={i} step={s} depth={depth + 1} result={result} test={test} />);
|
||||
return snippet.concat(steps);
|
||||
} : undefined} depth={depth}/>;
|
||||
|
|
|
|||
|
|
@ -3,33 +3,33 @@
|
|||
"browsers": [
|
||||
{
|
||||
"name": "chromium",
|
||||
"revision": "1157",
|
||||
"revision": "1158",
|
||||
"installByDefault": true,
|
||||
"browserVersion": "133.0.6943.35"
|
||||
"browserVersion": "134.0.6998.3"
|
||||
},
|
||||
{
|
||||
"name": "chromium-headless-shell",
|
||||
"revision": "1157",
|
||||
"revision": "1158",
|
||||
"installByDefault": true,
|
||||
"browserVersion": "133.0.6943.35"
|
||||
"browserVersion": "134.0.6998.3"
|
||||
},
|
||||
{
|
||||
"name": "chromium-tip-of-tree",
|
||||
"revision": "1300",
|
||||
"revision": "1302",
|
||||
"installByDefault": false,
|
||||
"browserVersion": "134.0.6998.0"
|
||||
"browserVersion": "135.0.7011.0"
|
||||
},
|
||||
{
|
||||
"name": "chromium-tip-of-tree-headless-shell",
|
||||
"revision": "1300",
|
||||
"revision": "1302",
|
||||
"installByDefault": false,
|
||||
"browserVersion": "134.0.6998.0"
|
||||
"browserVersion": "135.0.7011.0"
|
||||
},
|
||||
{
|
||||
"name": "firefox",
|
||||
"revision": "1474",
|
||||
"revision": "1475",
|
||||
"installByDefault": true,
|
||||
"browserVersion": "134.0"
|
||||
"browserVersion": "135.0"
|
||||
},
|
||||
{
|
||||
"name": "firefox-beta",
|
||||
|
|
@ -39,7 +39,7 @@
|
|||
},
|
||||
{
|
||||
"name": "webkit",
|
||||
"revision": "2130",
|
||||
"revision": "2132",
|
||||
"installByDefault": true,
|
||||
"revisionOverrides": {
|
||||
"debian11-x64": "2105",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,11 @@
|
|||
[inProcessFactory.ts]
|
||||
**
|
||||
|
||||
[inprocess.ts]
|
||||
utils/
|
||||
|
||||
[outofprocess.ts]
|
||||
client/
|
||||
protocol/
|
||||
utils/
|
||||
common/
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
../common
|
||||
../debug/injected
|
||||
../generated/
|
||||
../server/
|
||||
../server/injected/
|
||||
../server/trace
|
||||
../utils
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import * as playwright from '../..';
|
|||
import { PipeTransport } from '../protocol/transport';
|
||||
import { PlaywrightServer } from '../remote/playwrightServer';
|
||||
import { DispatcherConnection, PlaywrightDispatcher, RootDispatcher, createPlaywright } from '../server';
|
||||
import { gracefullyProcessExitDoNotHang } from '../utils/processLauncher';
|
||||
import { gracefullyProcessExitDoNotHang } from '../server/processLauncher';
|
||||
|
||||
import type { BrowserType } from '../client/browserType';
|
||||
import type { LaunchServerOptions } from '../client/types';
|
||||
|
|
|
|||
|
|
@ -21,11 +21,11 @@ import * as os from 'os';
|
|||
import * as path from 'path';
|
||||
|
||||
import * as playwright from '../..';
|
||||
import { registry, writeDockerVersion } from '../server';
|
||||
import { launchBrowserServer, printApiJson, runDriver, runServer } from './driver';
|
||||
import { isTargetClosedError } from '../client/errors';
|
||||
import { gracefullyProcessExitDoNotHang, registry, writeDockerVersion } from '../server';
|
||||
import { runTraceInBrowser, runTraceViewerApp } from '../server/trace/viewer/traceViewer';
|
||||
import { assert, getPackageManagerExecCommand, gracefullyProcessExitDoNotHang, isLikelyNpxGlobal, wrapInASCIIBox } from '../utils';
|
||||
import { assert, getPackageManagerExecCommand, isLikelyNpxGlobal, wrapInASCIIBox } from '../utils';
|
||||
import { dotenv, program } from '../utilsBundle';
|
||||
|
||||
import type { Browser } from '../client/browser';
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@
|
|||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import { getPackageManager, gracefullyProcessExitDoNotHang } from '../utils';
|
||||
import { gracefullyProcessExitDoNotHang } from '../server';
|
||||
import { getPackageManager } from '../utils';
|
||||
import { program } from './program';
|
||||
export { program } from './program';
|
||||
|
||||
|
|
|
|||
|
|
@ -15,22 +15,22 @@
|
|||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import * as fs from 'fs';
|
||||
|
||||
import { isRegExp, isString, monotonicTime } from '../utils';
|
||||
import { BrowserContext, prepareBrowserContextParams } from './browserContext';
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
import { Connection } from './connection';
|
||||
import { TargetClosedError, isTargetClosedError } from './errors';
|
||||
import { Events } from './events';
|
||||
import { Waiter } from './waiter';
|
||||
import { TimeoutSettings } from '../common/timeoutSettings';
|
||||
import { isRegExp, isString } from '../utils/rtti';
|
||||
import { monotonicTime } from '../utils/time';
|
||||
import { raceAgainstDeadline } from '../utils/timeoutRunner';
|
||||
|
||||
import type { Page } from './page';
|
||||
import type * as types from './types';
|
||||
import type * as api from '../../types/types';
|
||||
import type { AndroidServerLauncherImpl } from '../androidServerImpl';
|
||||
import type { Platform } from '../utils/platform';
|
||||
import type * as channels from '@protocol/channels';
|
||||
|
||||
type Direction = 'down' | 'up' | 'left' | 'right';
|
||||
|
|
@ -71,45 +71,28 @@ export class Android extends ChannelOwner<channels.AndroidChannel> implements ap
|
|||
const headers = { 'x-playwright-browser': 'android', ...options.headers };
|
||||
const localUtils = this._connection.localUtils();
|
||||
const connectParams: channels.LocalUtilsConnectParams = { wsEndpoint, headers, slowMo: options.slowMo, timeout: options.timeout };
|
||||
const { pipe } = await localUtils._channel.connect(connectParams);
|
||||
const closePipe = () => pipe.close().catch(() => {});
|
||||
const connection = new Connection(localUtils, this._instrumentation);
|
||||
connection.markAsRemote();
|
||||
connection.on('close', closePipe);
|
||||
const connection = await localUtils.connect(connectParams);
|
||||
|
||||
let device: AndroidDevice;
|
||||
let closeError: string | undefined;
|
||||
const onPipeClosed = () => {
|
||||
connection.on('close', () => {
|
||||
device?._didClose();
|
||||
connection.close(closeError);
|
||||
};
|
||||
pipe.on('closed', onPipeClosed);
|
||||
connection.onmessage = message => pipe.send({ message }).catch(onPipeClosed);
|
||||
|
||||
pipe.on('message', ({ message }) => {
|
||||
try {
|
||||
connection!.dispatch(message);
|
||||
} catch (e) {
|
||||
closeError = String(e);
|
||||
closePipe();
|
||||
}
|
||||
});
|
||||
|
||||
const result = await raceAgainstDeadline(async () => {
|
||||
const playwright = await connection!.initializePlaywright();
|
||||
if (!playwright._initializer.preConnectedAndroidDevice) {
|
||||
closePipe();
|
||||
connection.close();
|
||||
throw new Error('Malformed endpoint. Did you use Android.launchServer method?');
|
||||
}
|
||||
device = AndroidDevice.from(playwright._initializer.preConnectedAndroidDevice!);
|
||||
device._shouldCloseConnectionOnClose = true;
|
||||
device.on(Events.AndroidDevice.Close, closePipe);
|
||||
device.on(Events.AndroidDevice.Close, () => connection.close());
|
||||
return device;
|
||||
}, deadline);
|
||||
if (!result.timedOut) {
|
||||
return result.result;
|
||||
} else {
|
||||
closePipe();
|
||||
connection.close();
|
||||
throw new Error(`Timeout ${options.timeout}ms exceeded`);
|
||||
}
|
||||
});
|
||||
|
|
@ -232,7 +215,7 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel> i
|
|||
async screenshot(options: { path?: string } = {}): Promise<Buffer> {
|
||||
const { binary } = await this._channel.screenshot();
|
||||
if (options.path)
|
||||
await fs.promises.writeFile(options.path, binary);
|
||||
await this._platform.fs().promises.writeFile(options.path, binary);
|
||||
return binary;
|
||||
}
|
||||
|
||||
|
|
@ -267,15 +250,15 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel> i
|
|||
}
|
||||
|
||||
async installApk(file: string | Buffer, options?: { args: string[] }): Promise<void> {
|
||||
await this._channel.installApk({ file: await loadFile(file), args: options && options.args });
|
||||
await this._channel.installApk({ file: await loadFile(this._platform, file), args: options && options.args });
|
||||
}
|
||||
|
||||
async push(file: string | Buffer, path: string, options?: { mode: number }): Promise<void> {
|
||||
await this._channel.push({ file: await loadFile(file), path, mode: options ? options.mode : undefined });
|
||||
await this._channel.push({ file: await loadFile(this._platform, file), path, mode: options ? options.mode : undefined });
|
||||
}
|
||||
|
||||
async launchBrowser(options: types.BrowserContextOptions & { pkg?: string } = {}): Promise<BrowserContext> {
|
||||
const contextOptions = await prepareBrowserContextParams(options);
|
||||
const contextOptions = await prepareBrowserContextParams(this._platform, options);
|
||||
const result = await this._channel.launchBrowser(contextOptions);
|
||||
const context = BrowserContext.from(result.context) as BrowserContext;
|
||||
context._setOptions(contextOptions, {});
|
||||
|
|
@ -321,9 +304,9 @@ export class AndroidSocket extends ChannelOwner<channels.AndroidSocketChannel> i
|
|||
}
|
||||
}
|
||||
|
||||
async function loadFile(file: string | Buffer): Promise<Buffer> {
|
||||
async function loadFile(platform: Platform, file: string | Buffer): Promise<Buffer> {
|
||||
if (isString(file))
|
||||
return await fs.promises.readFile(file);
|
||||
return await platform.fs().promises.readFile(file);
|
||||
return file;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
import { Stream } from './stream';
|
||||
import { mkdirIfNeeded } from '../utils/fileUtils';
|
||||
|
|
@ -42,9 +40,9 @@ export class Artifact extends ChannelOwner<channels.ArtifactChannel> {
|
|||
|
||||
const result = await this._channel.saveAsStream();
|
||||
const stream = Stream.from(result.stream);
|
||||
await mkdirIfNeeded(path);
|
||||
await mkdirIfNeeded(this._platform, path);
|
||||
await new Promise((resolve, reject) => {
|
||||
stream.stream().pipe(fs.createWriteStream(path))
|
||||
stream.stream().pipe(this._platform.fs().createWriteStream(path))
|
||||
.on('finish' as any, resolve)
|
||||
.on('error' as any, reject);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,19 +14,17 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
|
||||
import { Artifact } from './artifact';
|
||||
import { BrowserContext, prepareBrowserContextParams } from './browserContext';
|
||||
import { CDPSession } from './cdpSession';
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
import { isTargetClosedError } from './errors';
|
||||
import { Events } from './events';
|
||||
import { mkdirIfNeeded } from '../utils';
|
||||
import { mkdirIfNeeded } from '../utils/fileUtils';
|
||||
|
||||
import type { BrowserType } from './browserType';
|
||||
import type { Page } from './page';
|
||||
import type { BrowserContextOptions, HeadersArray, LaunchOptions } from './types';
|
||||
import type { BrowserContextOptions, LaunchOptions } from './types';
|
||||
import type * as api from '../../types/types';
|
||||
import type * as channels from '@protocol/channels';
|
||||
|
||||
|
|
@ -39,9 +37,6 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
|
|||
_options: LaunchOptions = {};
|
||||
readonly _name: string;
|
||||
private _path: string | undefined;
|
||||
|
||||
// Used from @playwright/test fixtures.
|
||||
_connectHeaders?: HeadersArray;
|
||||
_closeReason: string | undefined;
|
||||
|
||||
static from(browser: channels.BrowserChannel): Browser {
|
||||
|
|
@ -83,7 +78,7 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
|
|||
|
||||
async _innerNewContext(options: BrowserContextOptions = {}, forReuse: boolean): Promise<BrowserContext> {
|
||||
options = { ...this._browserType._playwright._defaultContextOptions, ...options };
|
||||
const contextOptions = await prepareBrowserContextParams(options);
|
||||
const contextOptions = await prepareBrowserContextParams(this._platform, options);
|
||||
const response = forReuse ? await this._channel.newContextForReuse(contextOptions) : await this._channel.newContext(contextOptions);
|
||||
const context = BrowserContext.from(response.context);
|
||||
await this._browserType._didCreateContext(context, contextOptions, this._options, options.logger || this._logger);
|
||||
|
|
@ -126,8 +121,8 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
|
|||
const buffer = await artifact.readIntoBuffer();
|
||||
await artifact.delete();
|
||||
if (this._path) {
|
||||
await mkdirIfNeeded(this._path);
|
||||
await fs.promises.writeFile(this._path, buffer);
|
||||
await mkdirIfNeeded(this._platform, this._path);
|
||||
await this._platform.fs().promises.writeFile(this._path, buffer);
|
||||
this._path = undefined;
|
||||
}
|
||||
return buffer;
|
||||
|
|
|
|||
|
|
@ -15,9 +15,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { Artifact } from './artifact';
|
||||
import { Browser } from './browser';
|
||||
import { CDPSession } from './cdpSession';
|
||||
|
|
@ -38,14 +35,18 @@ import { Waiter } from './waiter';
|
|||
import { WebError } from './webError';
|
||||
import { Worker } from './worker';
|
||||
import { TimeoutSettings } from '../common/timeoutSettings';
|
||||
import { headersObjectToArray, isRegExp, isString, mkdirIfNeeded, urlMatchesEqual } from '../utils';
|
||||
import { mkdirIfNeeded } from '../utils/fileUtils';
|
||||
import { headersObjectToArray } from '../utils/headers';
|
||||
import { urlMatchesEqual } from '../utils/isomorphic/urlMatch';
|
||||
import { isRegExp, isString } from '../utils/rtti';
|
||||
import { rewriteErrorMessage } from '../utils/stackTrace';
|
||||
|
||||
import type { BrowserType } from './browserType';
|
||||
import type { BrowserContextOptions, Headers, LaunchOptions, StorageState, WaitForEventOptions } from './types';
|
||||
import type * as structs from '../../types/structs';
|
||||
import type * as api from '../../types/types';
|
||||
import type { URLMatch } from '../utils';
|
||||
import type { URLMatch } from '../utils/isomorphic/urlMatch';
|
||||
import type { Platform } from '../utils/platform';
|
||||
import type * as channels from '@protocol/channels';
|
||||
|
||||
export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel> implements api.BrowserContext {
|
||||
|
|
@ -107,7 +108,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
this.emit(Events.BrowserContext.ServiceWorker, serviceWorker);
|
||||
});
|
||||
this._channel.on('console', event => {
|
||||
const consoleMessage = new ConsoleMessage(event);
|
||||
const consoleMessage = new ConsoleMessage(this._platform, event);
|
||||
this.emit(Events.BrowserContext.Console, consoleMessage);
|
||||
const page = consoleMessage.page();
|
||||
if (page)
|
||||
|
|
@ -321,7 +322,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
}
|
||||
|
||||
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any): Promise<void> {
|
||||
const source = await evaluationScript(script, arg);
|
||||
const source = await evaluationScript(this._platform, script, arg);
|
||||
await this._channel.addInitScript({ source });
|
||||
}
|
||||
|
||||
|
|
@ -431,8 +432,8 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
async storageState(options: { path?: string, indexedDB?: boolean } = {}): Promise<StorageState> {
|
||||
const state = await this._channel.storageState({ indexedDB: options.indexedDB });
|
||||
if (options.path) {
|
||||
await mkdirIfNeeded(options.path);
|
||||
await fs.promises.writeFile(options.path, JSON.stringify(state, undefined, 2), 'utf8');
|
||||
await mkdirIfNeeded(this._platform, options.path);
|
||||
await this._platform.fs().promises.writeFile(options.path, JSON.stringify(state, undefined, 2), 'utf8');
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
|
@ -484,7 +485,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
const needCompressed = harParams.path.endsWith('.zip');
|
||||
if (isCompressed && !needCompressed) {
|
||||
await artifact.saveAs(harParams.path + '.tmp');
|
||||
await this._connection.localUtils()._channel.harUnzip({ zipFile: harParams.path + '.tmp', harFile: harParams.path });
|
||||
await this._connection.localUtils().harUnzip({ zipFile: harParams.path + '.tmp', harFile: harParams.path });
|
||||
} else {
|
||||
await artifact.saveAs(harParams.path);
|
||||
}
|
||||
|
|
@ -500,11 +501,11 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
}
|
||||
}
|
||||
|
||||
async function prepareStorageState(options: BrowserContextOptions): Promise<channels.BrowserNewContextParams['storageState']> {
|
||||
async function prepareStorageState(platform: Platform, options: BrowserContextOptions): Promise<channels.BrowserNewContextParams['storageState']> {
|
||||
if (typeof options.storageState !== 'string')
|
||||
return options.storageState;
|
||||
try {
|
||||
return JSON.parse(await fs.promises.readFile(options.storageState, 'utf8'));
|
||||
return JSON.parse(await platform.fs().promises.readFile(options.storageState, 'utf8'));
|
||||
} catch (e) {
|
||||
rewriteErrorMessage(e, `Error reading storage state from ${options.storageState}:\n` + e.message);
|
||||
throw e;
|
||||
|
|
@ -524,7 +525,7 @@ function prepareRecordHarOptions(options: BrowserContextOptions['recordHar']): c
|
|||
};
|
||||
}
|
||||
|
||||
export async function prepareBrowserContextParams(options: BrowserContextOptions): Promise<channels.BrowserNewContextParams> {
|
||||
export async function prepareBrowserContextParams(platform: Platform, options: BrowserContextOptions): Promise<channels.BrowserNewContextParams> {
|
||||
if (options.videoSize && !options.videosPath)
|
||||
throw new Error(`"videoSize" option requires "videosPath" to be specified`);
|
||||
if (options.extraHTTPHeaders)
|
||||
|
|
@ -534,7 +535,7 @@ export async function prepareBrowserContextParams(options: BrowserContextOptions
|
|||
viewport: options.viewport === null ? undefined : options.viewport,
|
||||
noDefaultViewport: options.viewport === null,
|
||||
extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined,
|
||||
storageState: await prepareStorageState(options),
|
||||
storageState: await prepareStorageState(platform, options),
|
||||
serviceWorkers: options.serviceWorkers,
|
||||
recordHar: prepareRecordHarOptions(options.recordHar),
|
||||
colorScheme: options.colorScheme === null ? 'no-override' : options.colorScheme,
|
||||
|
|
@ -542,7 +543,7 @@ export async function prepareBrowserContextParams(options: BrowserContextOptions
|
|||
forcedColors: options.forcedColors === null ? 'no-override' : options.forcedColors,
|
||||
contrast: options.contrast === null ? 'no-override' : options.contrast,
|
||||
acceptDownloads: toAcceptDownloadsProtocol(options.acceptDownloads),
|
||||
clientCertificates: await toClientCertificatesProtocol(options.clientCertificates),
|
||||
clientCertificates: await toClientCertificatesProtocol(platform, options.clientCertificates),
|
||||
};
|
||||
if (!contextParams.recordVideo && options.videosPath) {
|
||||
contextParams.recordVideo = {
|
||||
|
|
@ -551,7 +552,7 @@ export async function prepareBrowserContextParams(options: BrowserContextOptions
|
|||
};
|
||||
}
|
||||
if (contextParams.recordVideo && contextParams.recordVideo.dir)
|
||||
contextParams.recordVideo.dir = path.resolve(process.cwd(), contextParams.recordVideo.dir);
|
||||
contextParams.recordVideo.dir = platform.path().resolve(process.cwd(), contextParams.recordVideo.dir);
|
||||
return contextParams;
|
||||
}
|
||||
|
||||
|
|
@ -563,7 +564,7 @@ function toAcceptDownloadsProtocol(acceptDownloads?: boolean) {
|
|||
return 'deny';
|
||||
}
|
||||
|
||||
export async function toClientCertificatesProtocol(certs?: BrowserContextOptions['clientCertificates']): Promise<channels.PlaywrightNewRequestParams['clientCertificates']> {
|
||||
export async function toClientCertificatesProtocol(platform: Platform, certs?: BrowserContextOptions['clientCertificates']): Promise<channels.PlaywrightNewRequestParams['clientCertificates']> {
|
||||
if (!certs)
|
||||
return undefined;
|
||||
|
||||
|
|
@ -571,7 +572,7 @@ export async function toClientCertificatesProtocol(certs?: BrowserContextOptions
|
|||
if (value)
|
||||
return value;
|
||||
if (path)
|
||||
return await fs.promises.readFile(path);
|
||||
return await platform.fs().promises.readFile(path);
|
||||
};
|
||||
|
||||
return await Promise.all(certs.map(async cert => ({
|
||||
|
|
|
|||
|
|
@ -14,13 +14,16 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
|
||||
import { Browser } from './browser';
|
||||
import { BrowserContext, prepareBrowserContextParams } from './browserContext';
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
import { envObjectToArray } from './clientHelper';
|
||||
import { Connection } from './connection';
|
||||
import { Events } from './events';
|
||||
import { assert, headersObjectToArray, monotonicTime } from '../utils';
|
||||
import { assert } from '../utils/debug';
|
||||
import { headersObjectToArray } from '../utils/headers';
|
||||
import { monotonicTime } from '../utils/time';
|
||||
import { raceAgainstDeadline } from '../utils/timeoutRunner';
|
||||
|
||||
import type { Playwright } from './playwright';
|
||||
|
|
@ -90,14 +93,14 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
|||
const logger = options.logger || this._playwright._defaultLaunchOptions?.logger;
|
||||
assert(!(options as any).port, 'Cannot specify a port without launching as a server.');
|
||||
options = { ...this._playwright._defaultLaunchOptions, ...this._playwright._defaultContextOptions, ...options };
|
||||
const contextParams = await prepareBrowserContextParams(options);
|
||||
const contextParams = await prepareBrowserContextParams(this._platform, options);
|
||||
const persistentParams: channels.BrowserTypeLaunchPersistentContextParams = {
|
||||
...contextParams,
|
||||
ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : undefined,
|
||||
ignoreAllDefaultArgs: !!options.ignoreDefaultArgs && !Array.isArray(options.ignoreDefaultArgs),
|
||||
env: options.env ? envObjectToArray(options.env) : undefined,
|
||||
channel: options.channel,
|
||||
userDataDir,
|
||||
userDataDir: path.isAbsolute(userDataDir) ? userDataDir : path.resolve(userDataDir),
|
||||
};
|
||||
return await this._wrapApiCall(async () => {
|
||||
const result = await this._channel.launchPersistentContext(persistentParams);
|
||||
|
|
@ -131,40 +134,16 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
|||
};
|
||||
if ((params as any).__testHookRedirectPortForwarding)
|
||||
connectParams.socksProxyRedirectPortForTest = (params as any).__testHookRedirectPortForwarding;
|
||||
const { pipe, headers: connectHeaders } = await localUtils._channel.connect(connectParams);
|
||||
const closePipe = () => pipe.close().catch(() => {});
|
||||
const connection = new Connection(localUtils, this._instrumentation);
|
||||
connection.markAsRemote();
|
||||
connection.on('close', closePipe);
|
||||
|
||||
const connection = await localUtils.connect(connectParams);
|
||||
let browser: Browser;
|
||||
let closeError: string | undefined;
|
||||
const onPipeClosed = (reason?: string) => {
|
||||
connection.on('close', () => {
|
||||
// Emulate all pages, contexts and the browser closing upon disconnect.
|
||||
for (const context of browser?.contexts() || []) {
|
||||
for (const page of context.pages())
|
||||
page._onClose();
|
||||
context._onClose();
|
||||
}
|
||||
connection.close(reason || closeError);
|
||||
// Give a chance to any API call promises to reject upon page/context closure.
|
||||
// This happens naturally when we receive page.onClose and browser.onClose from the server
|
||||
// in separate tasks. However, upon pipe closure we used to dispatch them all synchronously
|
||||
// here and promises did not have a chance to reject.
|
||||
// The order of rejects vs closure is a part of the API contract and our test runner
|
||||
// relies on it to attribute rejections to the right test.
|
||||
setTimeout(() => browser?._didClose(), 0);
|
||||
};
|
||||
pipe.on('closed', params => onPipeClosed(params.reason));
|
||||
connection.onmessage = message => this._wrapApiCall(() => pipe.send({ message }).catch(() => onPipeClosed()), /* isInternal */ true);
|
||||
|
||||
pipe.on('message', ({ message }) => {
|
||||
try {
|
||||
connection!.dispatch(message);
|
||||
} catch (e) {
|
||||
closeError = String(e);
|
||||
closePipe();
|
||||
}
|
||||
});
|
||||
|
||||
const result = await raceAgainstDeadline(async () => {
|
||||
|
|
@ -174,21 +153,20 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
|||
|
||||
const playwright = await connection!.initializePlaywright();
|
||||
if (!playwright._initializer.preLaunchedBrowser) {
|
||||
closePipe();
|
||||
connection.close();
|
||||
throw new Error('Malformed endpoint. Did you use BrowserType.launchServer method?');
|
||||
}
|
||||
playwright._setSelectors(this._playwright.selectors);
|
||||
browser = Browser.from(playwright._initializer.preLaunchedBrowser!);
|
||||
this._didLaunchBrowser(browser, {}, logger);
|
||||
browser._shouldCloseConnectionOnClose = true;
|
||||
browser._connectHeaders = connectHeaders;
|
||||
browser.on(Events.Browser.Disconnected, () => this._wrapApiCall(() => closePipe(), /* isInternal */ true));
|
||||
browser.on(Events.Browser.Disconnected, () => connection.close());
|
||||
return browser;
|
||||
}, deadline);
|
||||
if (!result.timedOut) {
|
||||
return result.result;
|
||||
} else {
|
||||
closePipe();
|
||||
connection.close();
|
||||
throw new Error(`Timeout ${params.timeout}ms exceeded`);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
import { EventEmitter } from './eventEmitter';
|
||||
import { ValidationError, maybeFindValidator } from '../protocol/validator';
|
||||
import { isUnderTest } from '../utils';
|
||||
import { isUnderTest } from '../utils/debug';
|
||||
import { debugLogger } from '../utils/debugLogger';
|
||||
import { captureLibraryStackTrace, stringifyStackFrames } from '../utils/stackTrace';
|
||||
import { zones } from '../utils/zones';
|
||||
|
|
@ -25,6 +25,7 @@ import type { ClientInstrumentation } from './clientInstrumentation';
|
|||
import type { Connection } from './connection';
|
||||
import type { Logger } from './types';
|
||||
import type { ValidatorContext } from '../protocol/validator';
|
||||
import type { Platform } from '../utils/platform';
|
||||
import type * as channels from '@protocol/channels';
|
||||
|
||||
type Listener = (...args: any[]) => void;
|
||||
|
|
@ -39,6 +40,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
|||
readonly _channel: T;
|
||||
readonly _initializer: channels.InitializerTraits<T>;
|
||||
_logger: Logger | undefined;
|
||||
readonly _platform: Platform;
|
||||
readonly _instrumentation: ClientInstrumentation;
|
||||
private _eventToSubscriptionMapping: Map<string, string> = new Map();
|
||||
private _isInternalType = false;
|
||||
|
|
@ -52,6 +54,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
|||
this._guid = guid;
|
||||
this._parent = parent instanceof ChannelOwner ? parent : undefined;
|
||||
this._instrumentation = this._connection._instrumentation;
|
||||
this._platform = this._connection.platform;
|
||||
|
||||
this._connection._objects.set(guid, this);
|
||||
if (this._parent) {
|
||||
|
|
|
|||
|
|
@ -15,11 +15,10 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
|
||||
import { isString } from '../utils';
|
||||
import { isString } from '../utils/rtti';
|
||||
|
||||
import type * as types from './types';
|
||||
import type { Platform } from '../utils/platform';
|
||||
|
||||
export function envObjectToArray(env: types.Env): { name: string, value: string }[] {
|
||||
const result: { name: string, value: string }[] = [];
|
||||
|
|
@ -30,7 +29,7 @@ export function envObjectToArray(env: types.Env): { name: string, value: string
|
|||
return result;
|
||||
}
|
||||
|
||||
export async function evaluationScript(fun: Function | string | { path?: string, content?: string }, arg?: any, addSourceUrl: boolean = true): Promise<string> {
|
||||
export async function evaluationScript(platform: Platform, fun: Function | string | { path?: string, content?: string }, arg?: any, addSourceUrl: boolean = true): Promise<string> {
|
||||
if (typeof fun === 'function') {
|
||||
const source = fun.toString();
|
||||
const argString = Object.is(arg, undefined) ? 'undefined' : JSON.stringify(arg);
|
||||
|
|
@ -43,7 +42,7 @@ export async function evaluationScript(fun: Function | string | { path?: string,
|
|||
if (fun.content !== undefined)
|
||||
return fun.content;
|
||||
if (fun.path !== undefined) {
|
||||
let source = await fs.promises.readFile(fun.path, 'utf8');
|
||||
let source = await platform.fs().promises.readFile(fun.path, 'utf8');
|
||||
if (addSourceUrl)
|
||||
source = addSourceUrlToScript(source, fun.path);
|
||||
return source;
|
||||
|
|
|
|||
|
|
@ -42,11 +42,14 @@ import { Tracing } from './tracing';
|
|||
import { Worker } from './worker';
|
||||
import { WritableStream } from './writableStream';
|
||||
import { ValidationError, findValidator } from '../protocol/validator';
|
||||
import { formatCallLog, rewriteErrorMessage, zones } from '../utils';
|
||||
import { debugLogger } from '../utils/debugLogger';
|
||||
import { formatCallLog, rewriteErrorMessage } from '../utils/stackTrace';
|
||||
import { zones } from '../utils/zones';
|
||||
|
||||
import type { ClientInstrumentation } from './clientInstrumentation';
|
||||
import type { HeadersArray } from './types';
|
||||
import type { ValidatorContext } from '../protocol/validator';
|
||||
import type { Platform } from '../utils/platform';
|
||||
import type * as channels from '@protocol/channels';
|
||||
|
||||
class Root extends ChannelOwner<channels.RootChannel> {
|
||||
|
|
@ -78,12 +81,17 @@ export class Connection extends EventEmitter {
|
|||
toImpl: ((client: ChannelOwner) => any) | undefined;
|
||||
private _tracingCount = 0;
|
||||
readonly _instrumentation: ClientInstrumentation;
|
||||
readonly platform: Platform;
|
||||
// Used from @playwright/test fixtures -> TODO remove?
|
||||
readonly headers: HeadersArray;
|
||||
|
||||
constructor(localUtils: LocalUtils | undefined, instrumentation: ClientInstrumentation | undefined) {
|
||||
constructor(localUtils: LocalUtils | undefined, platform: Platform, instrumentation: ClientInstrumentation | undefined, headers: HeadersArray) {
|
||||
super();
|
||||
this._instrumentation = instrumentation || createInstrumentation();
|
||||
this._localUtils = localUtils;
|
||||
this.platform = platform;
|
||||
this._rootObject = new Root(this);
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
markAsRemote() {
|
||||
|
|
@ -138,7 +146,7 @@ export class Connection extends EventEmitter {
|
|||
const location = frames[0] ? { file: frames[0].file, line: frames[0].line, column: frames[0].column } : undefined;
|
||||
const metadata: channels.Metadata = { apiName, location, internal: !apiName, stepId };
|
||||
if (this._tracingCount && frames && type !== 'LocalUtils')
|
||||
this._localUtils?._channel.addStackToTracingNoReply({ callData: { stack: frames, id } }).catch(() => {});
|
||||
this._localUtils?.addStackToTracingNoReply({ callData: { stack: frames, id } }).catch(() => {});
|
||||
// We need to exit zones before calling into the server, otherwise
|
||||
// when we receive events from the server, we would be in an API zone.
|
||||
zones.empty().run(() => this.onmessage({ ...message, metadata }));
|
||||
|
|
|
|||
|
|
@ -14,12 +14,11 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as util from 'util';
|
||||
|
||||
import { JSHandle } from './jsHandle';
|
||||
import { Page } from './page';
|
||||
|
||||
import type * as api from '../../types/types';
|
||||
import type { Platform } from '../utils/platform';
|
||||
import type * as channels from '@protocol/channels';
|
||||
|
||||
type ConsoleMessageLocation = channels.BrowserContextConsoleEvent['location'];
|
||||
|
|
@ -29,9 +28,11 @@ export class ConsoleMessage implements api.ConsoleMessage {
|
|||
private _page: Page | null;
|
||||
private _event: channels.BrowserContextConsoleEvent | channels.ElectronApplicationConsoleEvent;
|
||||
|
||||
constructor(event: channels.BrowserContextConsoleEvent | channels.ElectronApplicationConsoleEvent) {
|
||||
constructor(platform: Platform, event: channels.BrowserContextConsoleEvent | channels.ElectronApplicationConsoleEvent) {
|
||||
this._page = ('page' in event && event.page) ? Page.from(event.page) : null;
|
||||
this._event = event;
|
||||
if (platform.inspectCustom)
|
||||
(this as any)[platform.inspectCustom] = () => this._inspect();
|
||||
}
|
||||
|
||||
page() {
|
||||
|
|
@ -54,7 +55,7 @@ export class ConsoleMessage implements api.ConsoleMessage {
|
|||
return this._event.location;
|
||||
}
|
||||
|
||||
[util.inspect.custom]() {
|
||||
private _inspect() {
|
||||
return this.text();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export class Electron extends ChannelOwner<channels.ElectronChannel> implements
|
|||
|
||||
async launch(options: ElectronOptions = {}): Promise<ElectronApplication> {
|
||||
const params: channels.ElectronLaunchParams = {
|
||||
...await prepareBrowserContextParams(options),
|
||||
...await prepareBrowserContextParams(this._platform, options),
|
||||
env: envObjectToArray(options.env ? options.env : process.env),
|
||||
tracesDir: options.tracesDir,
|
||||
};
|
||||
|
|
@ -81,7 +81,7 @@ export class ElectronApplication extends ChannelOwner<channels.ElectronApplicati
|
|||
this._channel.on('close', () => {
|
||||
this.emit(Events.ElectronApplication.Close);
|
||||
});
|
||||
this._channel.on('console', event => this.emit(Events.ElectronApplication.Console, new ConsoleMessage(event)));
|
||||
this._channel.on('console', event => this.emit(Events.ElectronApplication.Console, new ConsoleMessage(this._platform, event)));
|
||||
this._setEventToSubscriptionMapping(new Map<string, channels.ElectronApplicationUpdateSubscriptionParams['event']>([
|
||||
[Events.ElectronApplication.Console, 'console'],
|
||||
]));
|
||||
|
|
|
|||
|
|
@ -14,15 +14,14 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { pipeline } from 'stream';
|
||||
import { promisify } from 'util';
|
||||
|
||||
import { Frame } from './frame';
|
||||
import { JSHandle, parseResult, serializeArgument } from './jsHandle';
|
||||
import { assert, isString } from '../utils';
|
||||
import { assert } from '../utils/debug';
|
||||
import { fileUploadSizeLimit, mkdirIfNeeded } from '../utils/fileUtils';
|
||||
import { isString } from '../utils/rtti';
|
||||
import { mime } from '../utilsBundle';
|
||||
import { WritableStream } from './writableStream';
|
||||
|
||||
|
|
@ -32,6 +31,7 @@ import type { Locator } from './locator';
|
|||
import type { FilePayload, Rect, SelectOption, SelectOptionOptions } from './types';
|
||||
import type * as structs from '../../types/structs';
|
||||
import type * as api from '../../types/types';
|
||||
import type { Platform } from '../utils/platform';
|
||||
import type * as channels from '@protocol/channels';
|
||||
|
||||
const pipelineAsync = promisify(pipeline);
|
||||
|
|
@ -156,7 +156,7 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> implements
|
|||
const frame = await this.ownerFrame();
|
||||
if (!frame)
|
||||
throw new Error('Cannot set input files to detached element');
|
||||
const converted = await convertInputFiles(files, frame.page().context());
|
||||
const converted = await convertInputFiles(this._platform, files, frame.page().context());
|
||||
await this._elementChannel.setInputFiles({ ...converted, ...options });
|
||||
}
|
||||
|
||||
|
|
@ -192,20 +192,21 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> implements
|
|||
return value === undefined ? null : value;
|
||||
}
|
||||
|
||||
async screenshot(options: Omit<channels.ElementHandleScreenshotOptions, 'mask'> & { path?: string, mask?: Locator[] } = {}): Promise<Buffer> {
|
||||
async screenshot(options: Omit<channels.ElementHandleScreenshotOptions, 'mask'> & { path?: string, mask?: api.Locator[] } = {}): Promise<Buffer> {
|
||||
const mask = options.mask as Locator[] | undefined;
|
||||
const copy: channels.ElementHandleScreenshotOptions = { ...options, mask: undefined };
|
||||
if (!copy.type)
|
||||
copy.type = determineScreenshotType(options);
|
||||
if (options.mask) {
|
||||
copy.mask = options.mask.map(locator => ({
|
||||
if (mask) {
|
||||
copy.mask = mask.map(locator => ({
|
||||
frame: locator._frame._channel,
|
||||
selector: locator._selector,
|
||||
}));
|
||||
}
|
||||
const result = await this._elementChannel.screenshot(copy);
|
||||
if (options.path) {
|
||||
await mkdirIfNeeded(options.path);
|
||||
await fs.promises.writeFile(options.path, result.binary);
|
||||
await mkdirIfNeeded(this._platform, options.path);
|
||||
await this._platform.fs().promises.writeFile(options.path, result.binary);
|
||||
}
|
||||
return result.binary;
|
||||
}
|
||||
|
|
@ -263,18 +264,18 @@ function filePayloadExceedsSizeLimit(payloads: FilePayload[]) {
|
|||
return payloads.reduce((size, item) => size + (item.buffer ? item.buffer.byteLength : 0), 0) >= fileUploadSizeLimit;
|
||||
}
|
||||
|
||||
async function resolvePathsAndDirectoryForInputFiles(items: string[]): Promise<[string[] | undefined, string | undefined]> {
|
||||
async function resolvePathsAndDirectoryForInputFiles(platform: Platform, items: string[]): Promise<[string[] | undefined, string | undefined]> {
|
||||
let localPaths: string[] | undefined;
|
||||
let localDirectory: string | undefined;
|
||||
for (const item of items) {
|
||||
const stat = await fs.promises.stat(item as string);
|
||||
const stat = await platform.fs().promises.stat(item as string);
|
||||
if (stat.isDirectory()) {
|
||||
if (localDirectory)
|
||||
throw new Error('Multiple directories are not supported');
|
||||
localDirectory = path.resolve(item as string);
|
||||
localDirectory = platform.path().resolve(item as string);
|
||||
} else {
|
||||
localPaths ??= [];
|
||||
localPaths.push(path.resolve(item as string));
|
||||
localPaths.push(platform.path().resolve(item as string));
|
||||
}
|
||||
}
|
||||
if (localPaths?.length && localDirectory)
|
||||
|
|
@ -282,30 +283,30 @@ async function resolvePathsAndDirectoryForInputFiles(items: string[]): Promise<[
|
|||
return [localPaths, localDirectory];
|
||||
}
|
||||
|
||||
export async function convertInputFiles(files: string | FilePayload | string[] | FilePayload[], context: BrowserContext): Promise<SetInputFilesFiles> {
|
||||
export async function convertInputFiles(platform: Platform, files: string | FilePayload | string[] | FilePayload[], context: BrowserContext): Promise<SetInputFilesFiles> {
|
||||
const items: (string | FilePayload)[] = Array.isArray(files) ? files.slice() : [files];
|
||||
|
||||
if (items.some(item => typeof item === 'string')) {
|
||||
if (!items.every(item => typeof item === 'string'))
|
||||
throw new Error('File paths cannot be mixed with buffers');
|
||||
|
||||
const [localPaths, localDirectory] = await resolvePathsAndDirectoryForInputFiles(items);
|
||||
const [localPaths, localDirectory] = await resolvePathsAndDirectoryForInputFiles(platform, items);
|
||||
|
||||
if (context._connection.isRemote()) {
|
||||
const files = localDirectory ? (await fs.promises.readdir(localDirectory, { withFileTypes: true, recursive: true })).filter(f => f.isFile()).map(f => path.join(f.path, f.name)) : localPaths!;
|
||||
const files = localDirectory ? (await platform.fs().promises.readdir(localDirectory, { withFileTypes: true, recursive: true })).filter(f => f.isFile()).map(f => platform.path().join(f.path, f.name)) : localPaths!;
|
||||
const { writableStreams, rootDir } = await context._wrapApiCall(async () => context._channel.createTempFiles({
|
||||
rootDirName: localDirectory ? path.basename(localDirectory) : undefined,
|
||||
rootDirName: localDirectory ? platform.path().basename(localDirectory) : undefined,
|
||||
items: await Promise.all(files.map(async file => {
|
||||
const lastModifiedMs = (await fs.promises.stat(file)).mtimeMs;
|
||||
const lastModifiedMs = (await platform.fs().promises.stat(file)).mtimeMs;
|
||||
return {
|
||||
name: localDirectory ? path.relative(localDirectory, file) : path.basename(file),
|
||||
name: localDirectory ? platform.path().relative(localDirectory, file) : platform.path().basename(file),
|
||||
lastModifiedMs
|
||||
};
|
||||
})),
|
||||
}), true);
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const writable = WritableStream.from(writableStreams[i]);
|
||||
await pipelineAsync(fs.createReadStream(files[i]), writable.stream());
|
||||
await pipelineAsync(platform.fs().createReadStream(files[i]), writable.stream());
|
||||
}
|
||||
return {
|
||||
directoryStream: rootDir,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
*/
|
||||
|
||||
import { parseSerializedValue, serializeValue } from '../protocol/serializers';
|
||||
import { isError } from '../utils';
|
||||
import { isError } from '../utils/rtti';
|
||||
|
||||
import type { SerializedError } from '@protocol/channels';
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
|
||||
import { EventEmitter as OriginalEventEmitter } from 'events';
|
||||
|
||||
import { isUnderTest } from '../utils';
|
||||
import { isUnderTest } from '../utils/debug';
|
||||
|
||||
import type { EventEmitter as EventEmitterType } from 'events';
|
||||
|
||||
|
|
|
|||
|
|
@ -14,24 +14,24 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as util from 'util';
|
||||
|
||||
import { assert, headersObjectToArray, isString } from '../utils';
|
||||
import { toClientCertificatesProtocol } from './browserContext';
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
import { TargetClosedError, isTargetClosedError } from './errors';
|
||||
import { RawHeaders } from './network';
|
||||
import { Tracing } from './tracing';
|
||||
import { assert } from '../utils/debug';
|
||||
import { mkdirIfNeeded } from '../utils/fileUtils';
|
||||
import { headersObjectToArray } from '../utils/headers';
|
||||
import { isString } from '../utils/rtti';
|
||||
|
||||
import type { Playwright } from './playwright';
|
||||
import type { ClientCertificate, FilePayload, Headers, SetStorageState, StorageState } from './types';
|
||||
import type { Serializable } from '../../types/structs';
|
||||
import type * as api from '../../types/types';
|
||||
import type { HeadersArray, NameValue } from '../common/types';
|
||||
import type { Platform } from '../utils/platform';
|
||||
import type * as channels from '@protocol/channels';
|
||||
import type * as fs from 'fs';
|
||||
|
||||
export type FetchOptions = {
|
||||
params?: { [key: string]: string | number | boolean; } | URLSearchParams | string,
|
||||
|
|
@ -70,14 +70,14 @@ export class APIRequest implements api.APIRequest {
|
|||
...options,
|
||||
};
|
||||
const storageState = typeof options.storageState === 'string' ?
|
||||
JSON.parse(await fs.promises.readFile(options.storageState, 'utf8')) :
|
||||
JSON.parse(await this._playwright._platform.fs().promises.readFile(options.storageState, 'utf8')) :
|
||||
options.storageState;
|
||||
const context = APIRequestContext.from((await this._playwright._channel.newRequest({
|
||||
...options,
|
||||
extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined,
|
||||
storageState,
|
||||
tracesDir: this._playwright._defaultLaunchOptions?.tracesDir, // We do not expose tracesDir in the API, so do not allow options to accidentally override it.
|
||||
clientCertificates: await toClientCertificatesProtocol(options.clientCertificates),
|
||||
clientCertificates: await toClientCertificatesProtocol(this._playwright._platform, options.clientCertificates),
|
||||
})).request);
|
||||
this._contexts.add(context);
|
||||
context._request = this;
|
||||
|
|
@ -232,7 +232,7 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
|
|||
} else {
|
||||
// Convert file-like values to ServerFilePayload structs.
|
||||
for (const [name, value] of Object.entries(options.multipart))
|
||||
multipartData.push(await toFormField(name, value));
|
||||
multipartData.push(await toFormField(this._platform, name, value));
|
||||
}
|
||||
}
|
||||
if (postDataBuffer === undefined && jsonData === undefined && formData === undefined && multipartData === undefined)
|
||||
|
|
@ -264,23 +264,24 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
|
|||
async storageState(options: { path?: string, indexedDB?: boolean } = {}): Promise<StorageState> {
|
||||
const state = await this._channel.storageState({ indexedDB: options.indexedDB });
|
||||
if (options.path) {
|
||||
await mkdirIfNeeded(options.path);
|
||||
await fs.promises.writeFile(options.path, JSON.stringify(state, undefined, 2), 'utf8');
|
||||
await mkdirIfNeeded(this._platform, options.path);
|
||||
await this._platform.fs().promises.writeFile(options.path, JSON.stringify(state, undefined, 2), 'utf8');
|
||||
}
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
async function toFormField(name: string, value: string|number|boolean|fs.ReadStream|FilePayload): Promise<channels.FormField> {
|
||||
async function toFormField(platform: Platform, name: string, value: string | number | boolean | fs.ReadStream | FilePayload): Promise<channels.FormField> {
|
||||
const typeOfValue = typeof value;
|
||||
if (isFilePayload(value)) {
|
||||
const payload = value as FilePayload;
|
||||
if (!Buffer.isBuffer(payload.buffer))
|
||||
throw new Error(`Unexpected buffer type of 'data.${name}'`);
|
||||
return { name, file: filePayloadToJson(payload) };
|
||||
} else if (value instanceof fs.ReadStream) {
|
||||
return { name, file: await readStreamToJson(value as fs.ReadStream) };
|
||||
} else {
|
||||
} else if (typeOfValue === 'string' || typeOfValue === 'number' || typeOfValue === 'boolean') {
|
||||
return { name, value: String(value) };
|
||||
} else {
|
||||
return { name, file: await readStreamToJson(platform, value as fs.ReadStream) };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -307,6 +308,9 @@ export class APIResponse implements api.APIResponse {
|
|||
this._request = context;
|
||||
this._initializer = initializer;
|
||||
this._headers = new RawHeaders(this._initializer.headers);
|
||||
|
||||
if (context._platform.inspectCustom)
|
||||
(this as any)[context._platform.inspectCustom] = () => this._inspect();
|
||||
}
|
||||
|
||||
ok(): boolean {
|
||||
|
|
@ -364,7 +368,7 @@ export class APIResponse implements api.APIResponse {
|
|||
await this._request._channel.disposeAPIResponse({ fetchUid: this._fetchUid() });
|
||||
}
|
||||
|
||||
[util.inspect.custom]() {
|
||||
private _inspect() {
|
||||
const headers = this.headersArray().map(({ name, value }) => ` ${name}: ${value}`);
|
||||
return `APIResponse: ${this.status()} ${this.statusText()}\n${headers.join('\n')}`;
|
||||
}
|
||||
|
|
@ -389,7 +393,7 @@ function filePayloadToJson(payload: FilePayload): ServerFilePayload {
|
|||
};
|
||||
}
|
||||
|
||||
async function readStreamToJson(stream: fs.ReadStream): Promise<ServerFilePayload> {
|
||||
async function readStreamToJson(platform: Platform, stream: fs.ReadStream): Promise<ServerFilePayload> {
|
||||
const buffer = await new Promise<Buffer>((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
stream.on('data', chunk => chunks.push(chunk as Buffer));
|
||||
|
|
@ -398,7 +402,7 @@ async function readStreamToJson(stream: fs.ReadStream): Promise<ServerFilePayloa
|
|||
});
|
||||
const streamPath: string = Buffer.isBuffer(stream.path) ? stream.path.toString('utf8') : stream.path;
|
||||
return {
|
||||
name: path.basename(streamPath),
|
||||
name: platform.path().basename(streamPath),
|
||||
buffer,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,28 +16,27 @@
|
|||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import * as fs from 'fs';
|
||||
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
import { FrameLocator, Locator, testIdAttributeName } from './locator';
|
||||
import { assert } from '../utils';
|
||||
import { urlMatches } from '../utils';
|
||||
import { addSourceUrlToScript } from './clientHelper';
|
||||
import { ElementHandle, convertInputFiles, convertSelectOptionValues } from './elementHandle';
|
||||
import { Events } from './events';
|
||||
import { JSHandle, assertMaxArguments, parseResult, serializeArgument } from './jsHandle';
|
||||
import { FrameLocator, Locator, testIdAttributeName } from './locator';
|
||||
import * as network from './network';
|
||||
import { kLifecycleEvents } from './types';
|
||||
import { Waiter } from './waiter';
|
||||
import { assert } from '../utils/debug';
|
||||
import { getByAltTextSelector, getByLabelSelector, getByPlaceholderSelector, getByRoleSelector, getByTestIdSelector, getByTextSelector, getByTitleSelector } from '../utils/isomorphic/locatorUtils';
|
||||
import { urlMatches } from '../utils/isomorphic/urlMatch';
|
||||
|
||||
import type { LocatorOptions } from './locator';
|
||||
import type { Page } from './page';
|
||||
import type { FilePayload, LifecycleEvent, SelectOption, SelectOptionOptions, StrictOptions, WaitForFunctionOptions } from './types';
|
||||
import type * as structs from '../../types/structs';
|
||||
import type * as api from '../../types/types';
|
||||
import type { URLMatch } from '../utils';
|
||||
import type { ByRoleOptions } from '../utils/isomorphic/locatorUtils';
|
||||
import type { URLMatch } from '../utils/isomorphic/urlMatch';
|
||||
import type * as channels from '@protocol/channels';
|
||||
|
||||
export type WaitForNavigationOptions = {
|
||||
|
|
@ -269,7 +268,7 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
|
|||
async addScriptTag(options: { url?: string, path?: string, content?: string, type?: string } = {}): Promise<ElementHandle> {
|
||||
const copy = { ...options };
|
||||
if (copy.path) {
|
||||
copy.content = (await fs.promises.readFile(copy.path)).toString();
|
||||
copy.content = (await this._platform.fs().promises.readFile(copy.path)).toString();
|
||||
copy.content = addSourceUrlToScript(copy.content, copy.path);
|
||||
}
|
||||
return ElementHandle.from((await this._channel.addScriptTag({ ...copy })).element);
|
||||
|
|
@ -278,7 +277,7 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
|
|||
async addStyleTag(options: { url?: string; path?: string; content?: string; } = {}): Promise<ElementHandle> {
|
||||
const copy = { ...options };
|
||||
if (copy.path) {
|
||||
copy.content = (await fs.promises.readFile(copy.path)).toString();
|
||||
copy.content = (await this._platform.fs().promises.readFile(copy.path)).toString();
|
||||
copy.content += '/*# sourceURL=' + copy.path.replace(/\n/g, '') + '*/';
|
||||
}
|
||||
return ElementHandle.from((await this._channel.addStyleTag({ ...copy })).element);
|
||||
|
|
@ -403,7 +402,7 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
|
|||
}
|
||||
|
||||
async setInputFiles(selector: string, files: string | FilePayload | string[] | FilePayload[], options: channels.FrameSetInputFilesOptions = {}): Promise<void> {
|
||||
const converted = await convertInputFiles(files, this.page().context());
|
||||
const converted = await convertInputFiles(this._platform, files, this.page().context());
|
||||
await this._channel.setInputFiles({ selector, ...converted, ...options });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ import { debugLogger } from '../utils/debugLogger';
|
|||
import type { BrowserContext } from './browserContext';
|
||||
import type { LocalUtils } from './localUtils';
|
||||
import type { Route } from './network';
|
||||
import type { URLMatch } from '../utils';
|
||||
import type { Page } from './page';
|
||||
import type { URLMatch } from '../utils/isomorphic/urlMatch';
|
||||
|
||||
type HarNotFoundAction = 'abort' | 'fallback';
|
||||
|
||||
|
|
@ -31,7 +31,7 @@ export class HarRouter {
|
|||
private _options: { urlMatch?: URLMatch; baseURL?: string; };
|
||||
|
||||
static async create(localUtils: LocalUtils, file: string, notFoundAction: HarNotFoundAction, options: { urlMatch?: URLMatch }): Promise<HarRouter> {
|
||||
const { harId, error } = await localUtils._channel.harOpen({ file });
|
||||
const { harId, error } = await localUtils.harOpen({ file });
|
||||
if (error)
|
||||
throw new Error(error);
|
||||
return new HarRouter(localUtils, harId!, notFoundAction, options);
|
||||
|
|
@ -47,7 +47,7 @@ export class HarRouter {
|
|||
private async _handle(route: Route) {
|
||||
const request = route.request();
|
||||
|
||||
const response = await this._localUtils._channel.harLookup({
|
||||
const response = await this._localUtils.harLookup({
|
||||
harId: this._harId,
|
||||
url: request.url(),
|
||||
method: request.method(),
|
||||
|
|
@ -103,6 +103,6 @@ export class HarRouter {
|
|||
}
|
||||
|
||||
dispose() {
|
||||
this._localUtils._channel.harClose({ harId: this._harId }).catch(() => {});
|
||||
this._localUtils.harClose({ harId: this._harId }).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,8 +15,12 @@
|
|||
*/
|
||||
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
import { Connection } from './connection';
|
||||
import * as localUtils from '../utils/localUtils';
|
||||
|
||||
import type { Size } from './types';
|
||||
import type { HeadersArray, Size } from './types';
|
||||
import type { HarBackend } from '../utils/harBackend';
|
||||
import type { Platform } from '../utils/platform';
|
||||
import type * as channels from '@protocol/channels';
|
||||
|
||||
type DeviceDescriptor = {
|
||||
|
|
@ -31,6 +35,8 @@ type Devices = { [name: string]: DeviceDescriptor };
|
|||
|
||||
export class LocalUtils extends ChannelOwner<channels.LocalUtilsChannel> {
|
||||
readonly devices: Devices;
|
||||
private _harBackends = new Map<string, HarBackend>();
|
||||
private _stackSessions = new Map<string, localUtils.StackSession>();
|
||||
|
||||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.LocalUtilsInitializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
|
|
@ -39,4 +45,134 @@ export class LocalUtils extends ChannelOwner<channels.LocalUtilsChannel> {
|
|||
for (const { name, descriptor } of initializer.deviceDescriptors)
|
||||
this.devices[name] = descriptor;
|
||||
}
|
||||
|
||||
async zip(params: channels.LocalUtilsZipParams): Promise<void> {
|
||||
return await localUtils.zip(this._platform, this._stackSessions, params);
|
||||
}
|
||||
|
||||
async harOpen(params: channels.LocalUtilsHarOpenParams): Promise<channels.LocalUtilsHarOpenResult> {
|
||||
return await localUtils.harOpen(this._harBackends, params);
|
||||
}
|
||||
|
||||
async harLookup(params: channels.LocalUtilsHarLookupParams): Promise<channels.LocalUtilsHarLookupResult> {
|
||||
return await localUtils.harLookup(this._harBackends, params);
|
||||
}
|
||||
|
||||
async harClose(params: channels.LocalUtilsHarCloseParams): Promise<void> {
|
||||
return await localUtils.harClose(this._harBackends, params);
|
||||
}
|
||||
|
||||
async harUnzip(params: channels.LocalUtilsHarUnzipParams): Promise<void> {
|
||||
return await localUtils.harUnzip(params);
|
||||
}
|
||||
|
||||
async tracingStarted(params: channels.LocalUtilsTracingStartedParams): Promise<channels.LocalUtilsTracingStartedResult> {
|
||||
return await localUtils.tracingStarted(this._stackSessions, params);
|
||||
}
|
||||
|
||||
async traceDiscarded(params: channels.LocalUtilsTraceDiscardedParams): Promise<void> {
|
||||
return await localUtils.traceDiscarded(this._platform, this._stackSessions, params);
|
||||
}
|
||||
|
||||
async addStackToTracingNoReply(params: channels.LocalUtilsAddStackToTracingNoReplyParams): Promise<void> {
|
||||
return await localUtils.addStackToTracingNoReply(this._stackSessions, params);
|
||||
}
|
||||
|
||||
async connect(params: channels.LocalUtilsConnectParams): Promise<Connection> {
|
||||
const transport = this._platform.ws ? new WebSocketTransport(this._platform) : new JsonPipeTransport(this);
|
||||
const connectHeaders = await transport.connect(params);
|
||||
const connection = new Connection(this, this._platform, this._instrumentation, connectHeaders);
|
||||
connection.markAsRemote();
|
||||
connection.on('close', () => transport.close());
|
||||
|
||||
let closeError: string | undefined;
|
||||
const onTransportClosed = (reason?: string) => {
|
||||
connection.close(reason || closeError);
|
||||
};
|
||||
transport.onClose(reason => onTransportClosed(reason));
|
||||
connection.onmessage = message => transport.send(message).catch(() => onTransportClosed());
|
||||
transport.onMessage(message => {
|
||||
try {
|
||||
connection!.dispatch(message);
|
||||
} catch (e) {
|
||||
closeError = String(e);
|
||||
transport.close();
|
||||
}
|
||||
});
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
interface Transport {
|
||||
connect(params: channels.LocalUtilsConnectParams): Promise<HeadersArray>;
|
||||
send(message: any): Promise<void>;
|
||||
onMessage(callback: (message: object) => void): void;
|
||||
onClose(callback: (reason?: string) => void): void;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
class JsonPipeTransport implements Transport {
|
||||
private _pipe: channels.JsonPipeChannel | undefined;
|
||||
private _owner: ChannelOwner<channels.LocalUtilsChannel>;
|
||||
|
||||
constructor(owner: ChannelOwner<channels.LocalUtilsChannel>) {
|
||||
this._owner = owner;
|
||||
}
|
||||
|
||||
async connect(params: channels.LocalUtilsConnectParams) {
|
||||
const { pipe, headers: connectHeaders } = await this._owner._wrapApiCall(async () => {
|
||||
return await this._owner._channel.connect(params);
|
||||
}, /* isInternal */ true);
|
||||
this._pipe = pipe;
|
||||
return connectHeaders;
|
||||
}
|
||||
|
||||
async send(message: object) {
|
||||
this._owner._wrapApiCall(async () => {
|
||||
await this._pipe!.send({ message });
|
||||
}, /* isInternal */ true);
|
||||
}
|
||||
|
||||
onMessage(callback: (message: object) => void) {
|
||||
this._pipe!.on('message', ({ message }) => callback(message));
|
||||
}
|
||||
|
||||
onClose(callback: (reason?: string) => void) {
|
||||
this._pipe!.on('closed', ({ reason }) => callback(reason));
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this._owner._wrapApiCall(async () => {
|
||||
await this._pipe!.close().catch(() => {});
|
||||
}, /* isInternal */ true);
|
||||
}
|
||||
}
|
||||
|
||||
class WebSocketTransport implements Transport {
|
||||
private _platform: Platform;
|
||||
private _ws: WebSocket | undefined;
|
||||
|
||||
constructor(platform: Platform) {
|
||||
this._platform = platform;
|
||||
}
|
||||
|
||||
async connect(params: channels.LocalUtilsConnectParams) {
|
||||
this._ws = this._platform.ws!(params.wsEndpoint);
|
||||
return [];
|
||||
}
|
||||
|
||||
async send(message: object) {
|
||||
this._ws!.send(JSON.stringify(message));
|
||||
}
|
||||
|
||||
onMessage(callback: (message: object) => void) {
|
||||
this._ws!.addEventListener('message', event => callback(JSON.parse(event.data)));
|
||||
}
|
||||
|
||||
onClose(callback: (reason?: string) => void) {
|
||||
this._ws!.addEventListener('close', () => callback());
|
||||
}
|
||||
|
||||
async close() {
|
||||
this._ws!.close();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,13 +14,13 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as util from 'util';
|
||||
|
||||
import { asLocator, isString, monotonicTime } from '../utils';
|
||||
import { ElementHandle } from './elementHandle';
|
||||
import { parseResult, serializeArgument } from './jsHandle';
|
||||
import { asLocator } from '../utils/isomorphic/locatorGenerators';
|
||||
import { getByAltTextSelector, getByLabelSelector, getByPlaceholderSelector, getByRoleSelector, getByTestIdSelector, getByTextSelector, getByTitleSelector } from '../utils/isomorphic/locatorUtils';
|
||||
import { escapeForTextSelector } from '../utils/isomorphic/stringUtils';
|
||||
import { isString } from '../utils/rtti';
|
||||
import { monotonicTime } from '../utils/time';
|
||||
|
||||
import type { Frame } from './frame';
|
||||
import type { FilePayload, FrameExpectParams, Rect, SelectOption, SelectOptionOptions, TimeoutOptions } from './types';
|
||||
|
|
@ -64,6 +64,9 @@ export class Locator implements api.Locator {
|
|||
throw new Error(`Inner "hasNot" locator must belong to the same frame.`);
|
||||
this._selector += ` >> internal:has-not=` + JSON.stringify(locator._selector);
|
||||
}
|
||||
|
||||
if (this._frame._platform.inspectCustom)
|
||||
(this as any)[this._frame._platform.inspectCustom] = () => this._inspect();
|
||||
}
|
||||
|
||||
private async _withElement<R>(task: (handle: ElementHandle<SVGElement | HTMLElement>, timeout?: number) => Promise<R>, timeout?: number): Promise<R> {
|
||||
|
|
@ -291,8 +294,9 @@ export class Locator implements api.Locator {
|
|||
return await this._frame.press(this._selector, key, { strict: true, ...options });
|
||||
}
|
||||
|
||||
async screenshot(options: Omit<channels.ElementHandleScreenshotOptions, 'mask'> & { path?: string, mask?: Locator[] } = {}): Promise<Buffer> {
|
||||
return await this._withElement((h, timeout) => h.screenshot({ ...options, timeout }), options.timeout);
|
||||
async screenshot(options: Omit<channels.ElementHandleScreenshotOptions, 'mask'> & { path?: string, mask?: api.Locator[] } = {}): Promise<Buffer> {
|
||||
const mask = options.mask as Locator[] | undefined;
|
||||
return await this._withElement((h, timeout) => h.screenshot({ ...options, mask, timeout }), options.timeout);
|
||||
}
|
||||
|
||||
async ariaSnapshot(options?: { _id?: boolean, _mode?: 'raw' | 'regex' } & TimeoutOptions): Promise<string> {
|
||||
|
|
@ -370,7 +374,7 @@ export class Locator implements api.Locator {
|
|||
return result;
|
||||
}
|
||||
|
||||
[util.inspect.custom]() {
|
||||
private _inspect() {
|
||||
return this.toString();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import { URLSearchParams } from 'url';
|
||||
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
|
|
@ -22,19 +21,26 @@ import { isTargetClosedError } from './errors';
|
|||
import { Events } from './events';
|
||||
import { APIResponse } from './fetch';
|
||||
import { Frame } from './frame';
|
||||
import { Worker } from './worker';
|
||||
import { MultiMap, assert, headersObjectToArray, isRegExp, isString, rewriteErrorMessage, urlMatches, zones } from '../utils';
|
||||
import { Waiter } from './waiter';
|
||||
import { Worker } from './worker';
|
||||
import { assert } from '../utils/debug';
|
||||
import { headersObjectToArray } from '../utils/headers';
|
||||
import { urlMatches } from '../utils/isomorphic/urlMatch';
|
||||
import { LongStandingScope, ManualPromise } from '../utils/manualPromise';
|
||||
import { MultiMap } from '../utils/multimap';
|
||||
import { isRegExp, isString } from '../utils/rtti';
|
||||
import { rewriteErrorMessage } from '../utils/stackTrace';
|
||||
import { zones } from '../utils/zones';
|
||||
import { mime } from '../utilsBundle';
|
||||
|
||||
import type { Headers, RemoteAddr, SecurityDetails, WaitForEventOptions } from './types';
|
||||
import type { URLMatch, Zone } from '../utils';
|
||||
import type { BrowserContext } from './browserContext';
|
||||
import type { Page } from './page';
|
||||
import type { Headers, RemoteAddr, SecurityDetails, WaitForEventOptions } from './types';
|
||||
import type { Serializable } from '../../types/structs';
|
||||
import type * as api from '../../types/types';
|
||||
import type { HeadersArray } from '../common/types';
|
||||
import type { URLMatch } from '../utils/isomorphic/urlMatch';
|
||||
import type { Zone } from '../utils/zones';
|
||||
import type * as channels from '@protocol/channels';
|
||||
|
||||
export type NetworkCookie = {
|
||||
|
|
@ -387,7 +393,7 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
|||
let isBase64 = false;
|
||||
let length = 0;
|
||||
if (options.path) {
|
||||
const buffer = await fs.promises.readFile(options.path);
|
||||
const buffer = await this._platform.fs().promises.readFile(options.path);
|
||||
body = buffer.toString('base64');
|
||||
isBase64 = true;
|
||||
length = buffer.length;
|
||||
|
|
|
|||
|
|
@ -15,12 +15,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { TargetClosedError, isTargetClosedError, serializeError } from './errors';
|
||||
import { TimeoutSettings } from '../common/timeoutSettings';
|
||||
import { LongStandingScope, assert, headersObjectToArray, isObject, isRegExp, isString, mkdirIfNeeded, trimStringWithEllipsis, urlMatches, urlMatchesEqual } from '../utils';
|
||||
import { Accessibility } from './accessibility';
|
||||
import { Artifact } from './artifact';
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
|
|
@ -28,16 +22,25 @@ import { evaluationScript } from './clientHelper';
|
|||
import { Coverage } from './coverage';
|
||||
import { Download } from './download';
|
||||
import { ElementHandle, determineScreenshotType } from './elementHandle';
|
||||
import { TargetClosedError, isTargetClosedError, serializeError } from './errors';
|
||||
import { Events } from './events';
|
||||
import { FileChooser } from './fileChooser';
|
||||
import { Frame, verifyLoadState } from './frame';
|
||||
import { HarRouter } from './harRouter';
|
||||
import { Keyboard, Mouse, Touchscreen } from './input';
|
||||
import { JSHandle, assertMaxArguments, parseResult, serializeArgument } from './jsHandle';
|
||||
import { Response, Route, RouteHandler, WebSocket, WebSocketRoute, WebSocketRouteHandler, validateHeaders } from './network';
|
||||
import { Response, Route, RouteHandler, WebSocket, WebSocketRoute, WebSocketRouteHandler, validateHeaders } from './network';
|
||||
import { Video } from './video';
|
||||
import { Waiter } from './waiter';
|
||||
import { Worker } from './worker';
|
||||
import { TimeoutSettings } from '../common/timeoutSettings';
|
||||
import { assert } from '../utils/debug';
|
||||
import { mkdirIfNeeded } from '../utils/fileUtils';
|
||||
import { headersObjectToArray } from '../utils/headers';
|
||||
import { trimStringWithEllipsis } from '../utils/isomorphic/stringUtils';
|
||||
import { urlMatches, urlMatchesEqual } from '../utils/isomorphic/urlMatch';
|
||||
import { LongStandingScope } from '../utils/manualPromise';
|
||||
import { isObject, isRegExp, isString } from '../utils/rtti';
|
||||
|
||||
import type { BrowserContext } from './browserContext';
|
||||
import type { Clock } from './clock';
|
||||
|
|
@ -48,8 +51,8 @@ import type { Request, RouteHandlerCallback, WebSocketRouteHandlerCallback } fro
|
|||
import type { FilePayload, Headers, LifecycleEvent, SelectOption, SelectOptionOptions, Size, WaitForEventOptions, WaitForFunctionOptions } from './types';
|
||||
import type * as structs from '../../types/structs';
|
||||
import type * as api from '../../types/types';
|
||||
import type { URLMatch } from '../utils';
|
||||
import type { ByRoleOptions } from '../utils/isomorphic/locatorUtils';
|
||||
import type { URLMatch } from '../utils/isomorphic/urlMatch';
|
||||
import type * as channels from '@protocol/channels';
|
||||
|
||||
type PDFOptions = Omit<channels.PagePdfParams, 'width' | 'height' | 'margin'> & {
|
||||
|
|
@ -512,7 +515,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
}
|
||||
|
||||
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) {
|
||||
const source = await evaluationScript(script, arg);
|
||||
const source = await evaluationScript(this._platform, script, arg);
|
||||
await this._channel.addInitScript({ source });
|
||||
}
|
||||
|
||||
|
|
@ -590,8 +593,8 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
}
|
||||
const result = await this._channel.screenshot(copy);
|
||||
if (options.path) {
|
||||
await mkdirIfNeeded(options.path);
|
||||
await fs.promises.writeFile(options.path, result.binary);
|
||||
await mkdirIfNeeded(this._platform, options.path);
|
||||
await this._platform.fs().promises.writeFile(options.path, result.binary);
|
||||
}
|
||||
return result.binary;
|
||||
}
|
||||
|
|
@ -820,8 +823,9 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
}
|
||||
const result = await this._channel.pdf(transportOptions);
|
||||
if (options.path) {
|
||||
await fs.promises.mkdir(path.dirname(options.path), { recursive: true });
|
||||
await fs.promises.writeFile(options.path, result.pdf);
|
||||
const platform = this._platform;
|
||||
await platform.fs().promises.mkdir(platform.path().dirname(options.path), { recursive: true });
|
||||
await platform.fs().promises.writeFile(options.path, result.pdf);
|
||||
}
|
||||
return result.pdf;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
import { ChannelOwner } from './channelOwner';
|
||||
import { evaluationScript } from './clientHelper';
|
||||
import { setTestIdAttribute, testIdAttributeName } from './locator';
|
||||
import { nodePlatform } from '../utils/platform';
|
||||
|
||||
import type { SelectorEngine } from './types';
|
||||
import type * as api from '../../types/types';
|
||||
|
|
@ -28,7 +29,7 @@ export class Selectors implements api.Selectors {
|
|||
private _registrations: channels.SelectorsRegisterParams[] = [];
|
||||
|
||||
async register(name: string, script: string | (() => SelectorEngine) | { path?: string, content?: string }, options: { contentScript?: boolean } = {}): Promise<void> {
|
||||
const source = await evaluationScript(script, undefined, false);
|
||||
const source = await evaluationScript(nodePlatform, script, undefined, false);
|
||||
const params = { ...options, name, source };
|
||||
for (const channel of this._channels)
|
||||
await channel._channel.register(params);
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
|
|||
this._isTracing = true;
|
||||
this._connection.setIsTracing(true);
|
||||
}
|
||||
const result = await this._connection.localUtils()._channel.tracingStarted({ tracesDir: this._tracesDir, traceName });
|
||||
const result = await this._connection.localUtils().tracingStarted({ tracesDir: this._tracesDir, traceName });
|
||||
this._stacksId = result.stacksId;
|
||||
}
|
||||
|
||||
|
|
@ -89,7 +89,7 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
|
|||
// Not interested in artifacts.
|
||||
await this._channel.tracingStopChunk({ mode: 'discard' });
|
||||
if (this._stacksId)
|
||||
await this._connection.localUtils()._channel.traceDiscarded({ stacksId: this._stacksId });
|
||||
await this._connection.localUtils().traceDiscarded({ stacksId: this._stacksId });
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -97,7 +97,7 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
|
|||
|
||||
if (isLocal) {
|
||||
const result = await this._channel.tracingStopChunk({ mode: 'entries' });
|
||||
await this._connection.localUtils()._channel.zip({ zipFile: filePath, entries: result.entries!, mode: 'write', stacksId: this._stacksId, includeSources: this._includeSources });
|
||||
await this._connection.localUtils().zip({ zipFile: filePath, entries: result.entries!, mode: 'write', stacksId: this._stacksId, includeSources: this._includeSources });
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -106,7 +106,7 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
|
|||
// The artifact may be missing if the browser closed while stopping tracing.
|
||||
if (!result.artifact) {
|
||||
if (this._stacksId)
|
||||
await this._connection.localUtils()._channel.traceDiscarded({ stacksId: this._stacksId });
|
||||
await this._connection.localUtils().traceDiscarded({ stacksId: this._stacksId });
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -115,7 +115,7 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
|
|||
await artifact.saveAs(filePath);
|
||||
await artifact.delete();
|
||||
|
||||
await this._connection.localUtils()._channel.zip({ zipFile: filePath, entries: [], mode: 'append', stacksId: this._stacksId, includeSources: this._includeSources });
|
||||
await this._connection.localUtils().zip({ zipFile: filePath, entries: [], mode: 'append', stacksId: this._stacksId, includeSources: this._includeSources });
|
||||
}
|
||||
|
||||
_resetStackCounter() {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export type SelectOptionOptions = { force?: boolean, timeout?: number };
|
|||
export type FilePayload = { name: string, mimeType: string, buffer: Buffer };
|
||||
export type StorageState = {
|
||||
cookies: channels.NetworkCookie[],
|
||||
origins: channels.OriginStorage[]
|
||||
origins: channels.OriginStorage[],
|
||||
};
|
||||
export type SetStorageState = {
|
||||
cookies?: channels.SetNetworkCookie[],
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ManualPromise } from '../utils';
|
||||
import { ManualPromise } from '../utils/manualPromise';
|
||||
|
||||
import type { Artifact } from './artifact';
|
||||
import type { Connection } from './connection';
|
||||
|
|
|
|||
|
|
@ -15,11 +15,12 @@
|
|||
*/
|
||||
|
||||
import { TimeoutError } from './errors';
|
||||
import { createGuid, zones } from '../utils';
|
||||
import { createGuid } from '../utils/crypto';
|
||||
import { rewriteErrorMessage } from '../utils/stackTrace';
|
||||
import { zones } from '../utils/zones';
|
||||
|
||||
import type { Zone } from '../utils';
|
||||
import type { ChannelOwner } from './channelOwner';
|
||||
import type { Zone } from '../utils/zones';
|
||||
import type * as channels from '@protocol/channels';
|
||||
import type { EventEmitter } from 'events';
|
||||
|
||||
|
|
|
|||
|
|
@ -15,10 +15,10 @@
|
|||
*/
|
||||
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
import { TargetClosedError } from './errors';
|
||||
import { Events } from './events';
|
||||
import { JSHandle, assertMaxArguments, parseResult, serializeArgument } from './jsHandle';
|
||||
import { LongStandingScope } from '../utils';
|
||||
import { TargetClosedError } from './errors';
|
||||
import { LongStandingScope } from '../utils/manualPromise';
|
||||
|
||||
import type { BrowserContext } from './browserContext';
|
||||
import type { Page } from './page';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
[*]
|
||||
../utils/
|
||||
../utilsBundle.ts
|
||||
../utilsBundle.ts
|
||||
../zipBundle.ts
|
||||
|
|
|
|||
23
packages/playwright-core/src/common/progress.ts
Normal file
23
packages/playwright-core/src/common/progress.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export interface Progress {
|
||||
log(message: string): void;
|
||||
timeUntilDeadline(): number;
|
||||
isRunning(): boolean;
|
||||
cleanupWhenAborted(cleanup: () => any): void;
|
||||
throwIfAborted(): void;
|
||||
}
|
||||
|
|
@ -21,11 +21,12 @@ import { DispatcherConnection, PlaywrightDispatcher, RootDispatcher, createPlayw
|
|||
|
||||
import type { Playwright as PlaywrightAPI } from './client/playwright';
|
||||
import type { Language } from './utils';
|
||||
import type { Platform } from './utils/platform';
|
||||
|
||||
export function createInProcessPlaywright(): PlaywrightAPI {
|
||||
export function createInProcessPlaywright(platform: Platform): PlaywrightAPI {
|
||||
const playwright = createPlaywright({ sdkLanguage: (process.env.PW_LANG_NAME as Language | undefined) || 'javascript' });
|
||||
|
||||
const clientConnection = new Connection(undefined, undefined);
|
||||
const clientConnection = new Connection(undefined, platform, undefined, []);
|
||||
clientConnection.useRawBuffers();
|
||||
const dispatcherConnection = new DispatcherConnection(true /* local */);
|
||||
|
||||
|
|
|
|||
|
|
@ -15,5 +15,6 @@
|
|||
*/
|
||||
|
||||
import { createInProcessPlaywright } from './inProcessFactory';
|
||||
import { nodePlatform } from './utils/platform';
|
||||
|
||||
module.exports = createInProcessPlaywright();
|
||||
module.exports = createInProcessPlaywright(nodePlatform);
|
||||
|
|
|
|||
|
|
@ -20,10 +20,10 @@ import * as path from 'path';
|
|||
import { Connection } from './client/connection';
|
||||
import { PipeTransport } from './protocol/transport';
|
||||
import { ManualPromise } from './utils/manualPromise';
|
||||
import { nodePlatform } from './utils/platform';
|
||||
|
||||
import type { Playwright } from './client/playwright';
|
||||
|
||||
|
||||
export async function start(env: any = {}): Promise<{ playwright: Playwright, stop: () => Promise<void> }> {
|
||||
const client = new PlaywrightClient(env);
|
||||
const playwright = await client._playwright;
|
||||
|
|
@ -48,7 +48,7 @@ class PlaywrightClient {
|
|||
this._driverProcess.unref();
|
||||
this._driverProcess.stderr!.on('data', data => process.stderr.write(data));
|
||||
|
||||
const connection = new Connection(undefined, undefined);
|
||||
const connection = new Connection(undefined, nodePlatform, undefined, []);
|
||||
const transport = new PipeTransport(this._driverProcess.stdin!, this._driverProcess.stdout!);
|
||||
connection.onmessage = message => transport.send(JSON.stringify(message));
|
||||
transport.onmessage = message => connection.dispatch(JSON.parse(message));
|
||||
|
|
|
|||
|
|
@ -23,15 +23,15 @@ import { TimeoutSettings } from '../../common/timeoutSettings';
|
|||
import { PipeTransport } from '../../protocol/transport';
|
||||
import { createGuid, getPackageManagerExecCommand, isUnderTest, makeWaitForNextTask } from '../../utils';
|
||||
import { RecentLogsCollector } from '../../utils/debugLogger';
|
||||
import { removeFolders } from '../../utils/fileUtils';
|
||||
import { gracefullyCloseSet } from '../../utils/processLauncher';
|
||||
import { debug } from '../../utilsBundle';
|
||||
import { wsReceiver, wsSender } from '../../utilsBundle';
|
||||
import { validateBrowserContextOptions } from '../browserContext';
|
||||
import { chromiumSwitches } from '../chromium/chromiumSwitches';
|
||||
import { CRBrowser } from '../chromium/crBrowser';
|
||||
import { removeFolders } from '../fileUtils';
|
||||
import { helper } from '../helper';
|
||||
import { SdkObject, serverSideCallMetadata } from '../instrumentation';
|
||||
import { gracefullyCloseSet } from '../processLauncher';
|
||||
import { ProgressController } from '../progress';
|
||||
import { registry } from '../registry';
|
||||
|
||||
|
|
|
|||
|
|
@ -22,9 +22,9 @@ import { BidiBrowser } from './bidiBrowser';
|
|||
import { kBrowserCloseMessageId } from './bidiConnection';
|
||||
import { chromiumSwitches } from '../chromium/chromiumSwitches';
|
||||
|
||||
import type { Env } from '../../utils/processLauncher';
|
||||
import type { BrowserOptions } from '../browser';
|
||||
import type { SdkObject } from '../instrumentation';
|
||||
import type { Env } from '../processLauncher';
|
||||
import type { ProtocolError } from '../protocolError';
|
||||
import type { ConnectionTransport } from '../transport';
|
||||
import type * as types from '../types';
|
||||
|
|
|
|||
|
|
@ -23,9 +23,9 @@ import { BidiBrowser } from './bidiBrowser';
|
|||
import { kBrowserCloseMessageId } from './bidiConnection';
|
||||
import { createProfile } from './third_party/firefoxPrefs';
|
||||
|
||||
import type { Env } from '../../utils/processLauncher';
|
||||
import type { BrowserOptions } from '../browser';
|
||||
import type { SdkObject } from '../instrumentation';
|
||||
import type { Env } from '../processLauncher';
|
||||
import type { ProtocolError } from '../protocolError';
|
||||
import type { ConnectionTransport } from '../transport';
|
||||
import type * as types from '../types';
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import { createGuid, debugMode } from '../utils';
|
|||
import { Clock } from './clock';
|
||||
import { Debugger } from './debugger';
|
||||
import { BrowserContextAPIRequestContext } from './fetch';
|
||||
import { mkdirIfNeeded } from './fileUtils';
|
||||
import { HarRecorder } from './har/harRecorder';
|
||||
import { helper } from './helper';
|
||||
import { SdkObject, serverSideCallMetadata } from './instrumentation';
|
||||
|
|
@ -31,9 +32,8 @@ import * as network from './network';
|
|||
import { InitScript } from './page';
|
||||
import { Page, PageBinding } from './page';
|
||||
import { Recorder } from './recorder';
|
||||
import * as storageScript from './storageScript';
|
||||
import { mkdirIfNeeded } from '../utils/fileUtils';
|
||||
import { RecorderApp } from './recorder/recorderApp';
|
||||
import * as storageScript from './storageScript';
|
||||
import * as consoleApiSource from '../generated/consoleApiSource';
|
||||
import { Tracing } from './trace/recorder/tracing';
|
||||
|
||||
|
|
|
|||
|
|
@ -20,28 +20,28 @@ import * as path from 'path';
|
|||
|
||||
import { normalizeProxySettings, validateBrowserContextOptions } from './browserContext';
|
||||
import { DEFAULT_TIMEOUT, TimeoutSettings } from '../common/timeoutSettings';
|
||||
import { ManualPromise, debugMode } from '../utils';
|
||||
import { ManualPromise, assert, debugMode } from '../utils';
|
||||
import { existsAsync } from './fileUtils';
|
||||
import { helper } from './helper';
|
||||
import { SdkObject } from './instrumentation';
|
||||
import { PipeTransport } from './pipeTransport';
|
||||
import { envArrayToObject, launchProcess } from './processLauncher';
|
||||
import { ProgressController } from './progress';
|
||||
import { isProtocolError } from './protocolError';
|
||||
import { registry } from './registry';
|
||||
import { ClientCertificatesProxy } from './socksClientCertificatesInterceptor';
|
||||
import { WebSocketTransport } from './transport';
|
||||
import { RecentLogsCollector } from '../utils/debugLogger';
|
||||
import { existsAsync } from '../utils/fileUtils';
|
||||
import { envArrayToObject, launchProcess } from '../utils/processLauncher';
|
||||
|
||||
import type { Browser, BrowserOptions, BrowserProcess } from './browser';
|
||||
import type { BrowserContext } from './browserContext';
|
||||
import type { CallMetadata } from './instrumentation';
|
||||
import type { Env } from './processLauncher';
|
||||
import type { Progress } from './progress';
|
||||
import type { ProtocolError } from './protocolError';
|
||||
import type { BrowserName } from './registry';
|
||||
import type { ConnectionTransport } from './transport';
|
||||
import type * as types from './types';
|
||||
import type { Env } from '../utils/processLauncher';
|
||||
import type * as channels from '@protocol/channels';
|
||||
|
||||
export const kNoXServerRunningError = 'Looks like you launched a headed browser without having a XServer running.\n' +
|
||||
|
|
@ -188,6 +188,7 @@ export abstract class BrowserType extends SdkObject {
|
|||
tempDirectories.push(artifactsDir);
|
||||
|
||||
if (userDataDir) {
|
||||
assert(path.isAbsolute(userDataDir), 'userDataDir must be an absolute path');
|
||||
// Firefox bails if the profile directory does not exist, Chrome creates it. We ensure consistent behavior here.
|
||||
if (!await existsAsync(userDataDir))
|
||||
await fs.promises.mkdir(userDataDir, { recursive: true, mode: 0o700 });
|
||||
|
|
|
|||
|
|
@ -26,10 +26,8 @@ import { TimeoutSettings } from '../../common/timeoutSettings';
|
|||
import { debugMode, headersArrayToObject, headersObjectToArray, } from '../../utils';
|
||||
import { wrapInASCIIBox } from '../../utils/ascii';
|
||||
import { RecentLogsCollector } from '../../utils/debugLogger';
|
||||
import { removeFolders } from '../../utils/fileUtils';
|
||||
import { ManualPromise } from '../../utils/manualPromise';
|
||||
import { fetchData } from '../../utils/network';
|
||||
import { gracefullyCloseSet } from '../../utils/processLauncher';
|
||||
import { getUserAgent } from '../../utils/userAgent';
|
||||
import { validateBrowserContextOptions } from '../browserContext';
|
||||
import { BrowserType, kNoXServerRunningError } from '../browserType';
|
||||
|
|
@ -39,12 +37,14 @@ import { registry } from '../registry';
|
|||
import { WebSocketTransport } from '../transport';
|
||||
import { CRDevTools } from './crDevTools';
|
||||
import { Browser } from '../browser';
|
||||
import { removeFolders } from '../fileUtils';
|
||||
import { gracefullyCloseSet } from '../processLauncher';
|
||||
import { ProgressController } from '../progress';
|
||||
|
||||
import type { HTTPRequestParams } from '../../utils/network';
|
||||
import type { Env } from '../../utils/processLauncher';
|
||||
import type { BrowserOptions, BrowserProcess } from '../browser';
|
||||
import type { CallMetadata, SdkObject } from '../instrumentation';
|
||||
import type { Env } from '../processLauncher';
|
||||
import type { Progress } from '../progress';
|
||||
import type { ProtocolError } from '../protocolError';
|
||||
import type { ConnectionTransport, ProtocolRequest } from '../transport';
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@
|
|||
|
||||
import * as fs from 'fs';
|
||||
|
||||
import { mkdirIfNeeded } from '../../utils/fileUtils';
|
||||
import { splitErrorMessage } from '../../utils/stackTrace';
|
||||
import { mkdirIfNeeded } from '../fileUtils';
|
||||
|
||||
import type { CRSession } from './crConnection';
|
||||
import type { Protocol } from './protocol';
|
||||
|
|
|
|||
|
|
@ -952,7 +952,7 @@ Should be updated alongside RequestIdTokenStatus in
|
|||
third_party/blink/public/mojom/devtools/inspector_issue.mojom to include
|
||||
all cases except for success.
|
||||
*/
|
||||
export type FederatedAuthRequestIssueReason = "ShouldEmbargo"|"TooManyRequests"|"WellKnownHttpNotFound"|"WellKnownNoResponse"|"WellKnownInvalidResponse"|"WellKnownListEmpty"|"WellKnownInvalidContentType"|"ConfigNotInWellKnown"|"WellKnownTooBig"|"ConfigHttpNotFound"|"ConfigNoResponse"|"ConfigInvalidResponse"|"ConfigInvalidContentType"|"ClientMetadataHttpNotFound"|"ClientMetadataNoResponse"|"ClientMetadataInvalidResponse"|"ClientMetadataInvalidContentType"|"IdpNotPotentiallyTrustworthy"|"DisabledInSettings"|"DisabledInFlags"|"ErrorFetchingSignin"|"InvalidSigninResponse"|"AccountsHttpNotFound"|"AccountsNoResponse"|"AccountsInvalidResponse"|"AccountsListEmpty"|"AccountsInvalidContentType"|"IdTokenHttpNotFound"|"IdTokenNoResponse"|"IdTokenInvalidResponse"|"IdTokenIdpErrorResponse"|"IdTokenCrossSiteIdpErrorResponse"|"IdTokenInvalidRequest"|"IdTokenInvalidContentType"|"ErrorIdToken"|"Canceled"|"RpPageNotVisible"|"SilentMediationFailure"|"ThirdPartyCookiesBlocked"|"NotSignedInWithIdp"|"MissingTransientUserActivation"|"ReplacedByActiveMode"|"InvalidFieldsSpecified"|"RelyingPartyOriginIsOpaque"|"TypeNotMatching";
|
||||
export type FederatedAuthRequestIssueReason = "ShouldEmbargo"|"TooManyRequests"|"WellKnownHttpNotFound"|"WellKnownNoResponse"|"WellKnownInvalidResponse"|"WellKnownListEmpty"|"WellKnownInvalidContentType"|"ConfigNotInWellKnown"|"WellKnownTooBig"|"ConfigHttpNotFound"|"ConfigNoResponse"|"ConfigInvalidResponse"|"ConfigInvalidContentType"|"ClientMetadataHttpNotFound"|"ClientMetadataNoResponse"|"ClientMetadataInvalidResponse"|"ClientMetadataInvalidContentType"|"IdpNotPotentiallyTrustworthy"|"DisabledInSettings"|"DisabledInFlags"|"ErrorFetchingSignin"|"InvalidSigninResponse"|"AccountsHttpNotFound"|"AccountsNoResponse"|"AccountsInvalidResponse"|"AccountsListEmpty"|"AccountsInvalidContentType"|"IdTokenHttpNotFound"|"IdTokenNoResponse"|"IdTokenInvalidResponse"|"IdTokenIdpErrorResponse"|"IdTokenCrossSiteIdpErrorResponse"|"IdTokenInvalidRequest"|"IdTokenInvalidContentType"|"ErrorIdToken"|"Canceled"|"RpPageNotVisible"|"SilentMediationFailure"|"ThirdPartyCookiesBlocked"|"NotSignedInWithIdp"|"MissingTransientUserActivation"|"ReplacedByActiveMode"|"InvalidFieldsSpecified"|"RelyingPartyOriginIsOpaque"|"TypeNotMatching"|"UiDismissedNoEmbargo";
|
||||
export interface FederatedAuthUserInfoRequestIssueDetails {
|
||||
federatedAuthUserInfoRequestIssueReason: FederatedAuthUserInfoRequestIssueReason;
|
||||
}
|
||||
|
|
@ -983,7 +983,7 @@ features, encourage the use of new ones, and provide general guidance.
|
|||
}
|
||||
export type SelectElementAccessibilityIssueReason = "DisallowedSelectChild"|"DisallowedOptGroupChild"|"NonPhrasingContentOptionChild"|"InteractiveContentOptionChild"|"InteractiveContentLegendChild";
|
||||
/**
|
||||
* This isue warns about errors in the select element content model.
|
||||
* This issue warns about errors in the select element content model.
|
||||
*/
|
||||
export interface SelectElementAccessibilityIssueDetails {
|
||||
nodeId: DOM.BackendNodeId;
|
||||
|
|
@ -1187,6 +1187,19 @@ flag is set.
|
|||
*/
|
||||
id: string;
|
||||
}
|
||||
/**
|
||||
* Uninstalls an unpacked extension (others not supported) from the profile.
|
||||
Available if the client is connected using the --remote-debugging-pipe flag
|
||||
and the --enable-unsafe-extension-debugging.
|
||||
*/
|
||||
export type uninstallParameters = {
|
||||
/**
|
||||
* Extension id.
|
||||
*/
|
||||
id: string;
|
||||
}
|
||||
export type uninstallReturnValue = {
|
||||
}
|
||||
/**
|
||||
* Gets data from extension storage in the given `storageArea`. If `keys` is
|
||||
specified, these are used to filter the result.
|
||||
|
|
@ -2913,6 +2926,13 @@ incorrect results if the declaration contains a var() for example.
|
|||
* Identifier of the frame where "via-inspector" stylesheet should be created.
|
||||
*/
|
||||
frameId: Page.FrameId;
|
||||
/**
|
||||
* If true, creates a new stylesheet for every call. If false,
|
||||
returns a stylesheet previously created by a call with force=false
|
||||
for the frame's document if it exists or creates a new stylesheet
|
||||
(default: false).
|
||||
*/
|
||||
force?: boolean;
|
||||
}
|
||||
export type createStyleSheetReturnValue = {
|
||||
/**
|
||||
|
|
@ -5974,81 +5994,6 @@ The final text color opacity is computed based on the opacity of all overlapping
|
|||
}
|
||||
}
|
||||
|
||||
export module Database {
|
||||
/**
|
||||
* Unique identifier of Database object.
|
||||
*/
|
||||
export type DatabaseId = string;
|
||||
/**
|
||||
* Database object.
|
||||
*/
|
||||
export interface Database {
|
||||
/**
|
||||
* Database ID.
|
||||
*/
|
||||
id: DatabaseId;
|
||||
/**
|
||||
* Database domain.
|
||||
*/
|
||||
domain: string;
|
||||
/**
|
||||
* Database name.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Database version.
|
||||
*/
|
||||
version: string;
|
||||
}
|
||||
/**
|
||||
* Database error.
|
||||
*/
|
||||
export interface Error {
|
||||
/**
|
||||
* Error message.
|
||||
*/
|
||||
message: string;
|
||||
/**
|
||||
* Error code.
|
||||
*/
|
||||
code: number;
|
||||
}
|
||||
|
||||
export type addDatabasePayload = {
|
||||
database: Database;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables database tracking, prevents database events from being sent to the client.
|
||||
*/
|
||||
export type disableParameters = {
|
||||
}
|
||||
export type disableReturnValue = {
|
||||
}
|
||||
/**
|
||||
* Enables database tracking, database events will now be delivered to the client.
|
||||
*/
|
||||
export type enableParameters = {
|
||||
}
|
||||
export type enableReturnValue = {
|
||||
}
|
||||
export type executeSQLParameters = {
|
||||
databaseId: DatabaseId;
|
||||
query: string;
|
||||
}
|
||||
export type executeSQLReturnValue = {
|
||||
columnNames?: string[];
|
||||
values?: any[];
|
||||
sqlError?: Error;
|
||||
}
|
||||
export type getDatabaseTableNamesParameters = {
|
||||
databaseId: DatabaseId;
|
||||
}
|
||||
export type getDatabaseTableNamesReturnValue = {
|
||||
tableNames: string[];
|
||||
}
|
||||
}
|
||||
|
||||
export module DeviceOrientation {
|
||||
|
||||
|
||||
|
|
@ -9126,7 +9071,7 @@ This is a temporary ability and it will be removed in the future.
|
|||
/**
|
||||
* Types of reasons why a cookie should have been blocked by 3PCD but is exempted for the request.
|
||||
*/
|
||||
export type CookieExemptionReason = "None"|"UserSetting"|"TPCDMetadata"|"TPCDDeprecationTrial"|"TopLevelTPCDDeprecationTrial"|"TPCDHeuristics"|"EnterprisePolicy"|"StorageAccess"|"TopLevelStorageAccess"|"Scheme";
|
||||
export type CookieExemptionReason = "None"|"UserSetting"|"TPCDMetadata"|"TPCDDeprecationTrial"|"TopLevelTPCDDeprecationTrial"|"TPCDHeuristics"|"EnterprisePolicy"|"StorageAccess"|"TopLevelStorageAccess"|"Scheme"|"SameSiteNoneCookiesInSandbox";
|
||||
/**
|
||||
* A cookie which was not stored from a response with the corresponding reason.
|
||||
*/
|
||||
|
|
@ -11702,7 +11647,7 @@ as an ad.
|
|||
* All Permissions Policy features. This enum should match the one defined
|
||||
in third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5.
|
||||
*/
|
||||
export type PermissionsPolicyFeature = "accelerometer"|"all-screens-capture"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"controlled-frame"|"cross-origin-isolated"|"deferred-fetch"|"deferred-fetch-minimal"|"digital-credentials-get"|"direct-sockets"|"direct-sockets-private"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"fenced-unpartitioned-storage-read"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"media-playback-while-not-visible"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"popins"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-app-installation"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking";
|
||||
export type PermissionsPolicyFeature = "accelerometer"|"all-screens-capture"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-high-entropy-values"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"controlled-frame"|"cross-origin-isolated"|"deferred-fetch"|"deferred-fetch-minimal"|"digital-credentials-get"|"direct-sockets"|"direct-sockets-private"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"fenced-unpartitioned-storage-read"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"media-playback-while-not-visible"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"popins"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-app-installation"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking";
|
||||
/**
|
||||
* Reason for a permissions policy feature to be disabled.
|
||||
*/
|
||||
|
|
@ -12431,6 +12376,33 @@ subtree is actually detached.
|
|||
frame: Frame;
|
||||
}
|
||||
export type frameResizedPayload = void;
|
||||
/**
|
||||
* Fired when a navigation starts. This event is fired for both
|
||||
renderer-initiated and browser-initiated navigations. For renderer-initiated
|
||||
navigations, the event is fired after `frameRequestedNavigation`.
|
||||
Navigation may still be cancelled after the event is issued. Multiple events
|
||||
can be fired for a single navigation, for example, when a same-document
|
||||
navigation becomes a cross-document navigation (such as in the case of a
|
||||
frameset).
|
||||
*/
|
||||
export type frameStartedNavigatingPayload = {
|
||||
/**
|
||||
* ID of the frame that is being navigated.
|
||||
*/
|
||||
frameId: FrameId;
|
||||
/**
|
||||
* The URL the navigation started with. The final URL can be different.
|
||||
*/
|
||||
url: string;
|
||||
/**
|
||||
* Loader identifier. Even though it is present in case of same-document
|
||||
navigation, the previously committed loaderId would not change unless
|
||||
the navigation changes from a same-document to a cross-document
|
||||
navigation.
|
||||
*/
|
||||
loaderId: Network.LoaderId;
|
||||
navigationType: "reload"|"reloadBypassingCache"|"restore"|"restoreWithPost"|"historySameDocument"|"historyDifferentDocument"|"sameDocument"|"differentDocument";
|
||||
}
|
||||
/**
|
||||
* Fired when a renderer-initiated navigation is requested.
|
||||
Navigation may still be cancelled after the event is issued.
|
||||
|
|
@ -14281,7 +14253,7 @@ For cached script it is the last time the cache entry was validated.
|
|||
/**
|
||||
* Enum of possible storage types.
|
||||
*/
|
||||
export type StorageType = "appcache"|"cookies"|"file_systems"|"indexeddb"|"local_storage"|"shader_cache"|"websql"|"service_workers"|"cache_storage"|"interest_groups"|"shared_storage"|"storage_buckets"|"all"|"other";
|
||||
export type StorageType = "cookies"|"file_systems"|"indexeddb"|"local_storage"|"shader_cache"|"websql"|"service_workers"|"cache_storage"|"interest_groups"|"shared_storage"|"storage_buckets"|"all"|"other";
|
||||
/**
|
||||
* Usage for a storage type.
|
||||
*/
|
||||
|
|
@ -15193,6 +15165,28 @@ session. The effective Related Website Sets will not change during a browser ses
|
|||
export type getRelatedWebsiteSetsReturnValue = {
|
||||
sets: RelatedWebsiteSet[];
|
||||
}
|
||||
/**
|
||||
* Returns the list of URLs from a page and its embedded resources that match
|
||||
existing grace period URL pattern rules.
|
||||
https://developers.google.com/privacy-sandbox/cookies/temporary-exceptions/grace-period
|
||||
*/
|
||||
export type getAffectedUrlsForThirdPartyCookieMetadataParameters = {
|
||||
/**
|
||||
* The URL of the page currently being visited.
|
||||
*/
|
||||
firstPartyUrl: string;
|
||||
/**
|
||||
* The list of embedded resource URLs from the page.
|
||||
*/
|
||||
thirdPartyUrls: string[];
|
||||
}
|
||||
export type getAffectedUrlsForThirdPartyCookieMetadataReturnValue = {
|
||||
/**
|
||||
* Array of matching URLs. If there is a primary pattern match for the first-
|
||||
party URL, only the first-party URL is returned in the array.
|
||||
*/
|
||||
matchedUrls: string[];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -15485,6 +15479,10 @@ If filter is not specified, the one assumed is
|
|||
host: string;
|
||||
port: number;
|
||||
}
|
||||
/**
|
||||
* The state of the target window.
|
||||
*/
|
||||
export type WindowState = "normal"|"minimized"|"maximized"|"fullscreen";
|
||||
|
||||
/**
|
||||
* Issued when attached to target because of auto-attach or `attachToTarget` command.
|
||||
|
|
@ -15677,37 +15675,42 @@ Parts of the URL other than those constituting origin are ignored.
|
|||
*/
|
||||
url: string;
|
||||
/**
|
||||
* Frame left origin in DIP (headless chrome only).
|
||||
* Frame left origin in DIP (requires newWindow to be true or headless shell).
|
||||
*/
|
||||
left?: number;
|
||||
/**
|
||||
* Frame top origin in DIP (headless chrome only).
|
||||
* Frame top origin in DIP (requires newWindow to be true or headless shell).
|
||||
*/
|
||||
top?: number;
|
||||
/**
|
||||
* Frame width in DIP (headless chrome only).
|
||||
* Frame width in DIP (requires newWindow to be true or headless shell).
|
||||
*/
|
||||
width?: number;
|
||||
/**
|
||||
* Frame height in DIP (headless chrome only).
|
||||
* Frame height in DIP (requires newWindow to be true or headless shell).
|
||||
*/
|
||||
height?: number;
|
||||
/**
|
||||
* Frame window state (requires newWindow to be true or headless shell).
|
||||
Default is normal.
|
||||
*/
|
||||
windowState?: WindowState;
|
||||
/**
|
||||
* The browser context to create the page in.
|
||||
*/
|
||||
browserContextId?: Browser.BrowserContextID;
|
||||
/**
|
||||
* Whether BeginFrames for this target will be controlled via DevTools (headless chrome only,
|
||||
* Whether BeginFrames for this target will be controlled via DevTools (headless shell only,
|
||||
not supported on MacOS yet, false by default).
|
||||
*/
|
||||
enableBeginFrameControl?: boolean;
|
||||
/**
|
||||
* Whether to create a new Window or Tab (chrome-only, false by default).
|
||||
* Whether to create a new Window or Tab (false by default, not supported by headless shell).
|
||||
*/
|
||||
newWindow?: boolean;
|
||||
/**
|
||||
* Whether to create the target in background or foreground (chrome-only,
|
||||
false by default).
|
||||
* Whether to create the target in background or foreground (false by default, not supported
|
||||
by headless shell).
|
||||
*/
|
||||
background?: boolean;
|
||||
/**
|
||||
|
|
@ -18012,9 +18015,20 @@ variables as its properties.
|
|||
*/
|
||||
externalURL?: string;
|
||||
}
|
||||
export interface ResolvedBreakpoint {
|
||||
/**
|
||||
* Breakpoint unique identifier.
|
||||
*/
|
||||
breakpointId: BreakpointId;
|
||||
/**
|
||||
* Actual breakpoint location.
|
||||
*/
|
||||
location: Location;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fired when breakpoint is resolved to an actual script and location.
|
||||
Deprecated in favor of `resolvedBreakpoints` in the `scriptParsed` event.
|
||||
*/
|
||||
export type breakpointResolvedPayload = {
|
||||
/**
|
||||
|
|
@ -18225,6 +18239,12 @@ scripts upon enabling debugger.
|
|||
* The name the embedder supplied for this script.
|
||||
*/
|
||||
embedderName?: string;
|
||||
/**
|
||||
* The list of set breakpoints in this script if calls to `setBreakpointByUrl`
|
||||
matches this script's URL or hash. Clients that use this list can ignore the
|
||||
`breakpointResolved` event. They are equivalent.
|
||||
*/
|
||||
resolvedBreakpoints?: ResolvedBreakpoint[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -20150,13 +20170,21 @@ It is the total usage of the corresponding isolate not scoped to a particular Ru
|
|||
}
|
||||
export type getHeapUsageReturnValue = {
|
||||
/**
|
||||
* Used heap size in bytes.
|
||||
* Used JavaScript heap size in bytes.
|
||||
*/
|
||||
usedSize: number;
|
||||
/**
|
||||
* Allocated heap size in bytes.
|
||||
* Allocated JavaScript heap size in bytes.
|
||||
*/
|
||||
totalSize: number;
|
||||
/**
|
||||
* Used size in bytes in the embedder's garbage-collected heap.
|
||||
*/
|
||||
embedderHeapUsedSize: number;
|
||||
/**
|
||||
* Size in bytes of backing storage for array buffers and external strings.
|
||||
*/
|
||||
backingStorageSize: number;
|
||||
}
|
||||
/**
|
||||
* Returns properties of a given object. Object group of the result is inherited from the target
|
||||
|
|
@ -20472,7 +20500,6 @@ Error was thrown.
|
|||
"DOMStorage.domStorageItemRemoved": DOMStorage.domStorageItemRemovedPayload;
|
||||
"DOMStorage.domStorageItemUpdated": DOMStorage.domStorageItemUpdatedPayload;
|
||||
"DOMStorage.domStorageItemsCleared": DOMStorage.domStorageItemsClearedPayload;
|
||||
"Database.addDatabase": Database.addDatabasePayload;
|
||||
"Emulation.virtualTimeBudgetExpired": Emulation.virtualTimeBudgetExpiredPayload;
|
||||
"Input.dragIntercepted": Input.dragInterceptedPayload;
|
||||
"Inspector.detached": Inspector.detachedPayload;
|
||||
|
|
@ -20526,6 +20553,7 @@ Error was thrown.
|
|||
"Page.frameNavigated": Page.frameNavigatedPayload;
|
||||
"Page.documentOpened": Page.documentOpenedPayload;
|
||||
"Page.frameResized": Page.frameResizedPayload;
|
||||
"Page.frameStartedNavigating": Page.frameStartedNavigatingPayload;
|
||||
"Page.frameRequestedNavigation": Page.frameRequestedNavigationPayload;
|
||||
"Page.frameScheduledNavigation": Page.frameScheduledNavigationPayload;
|
||||
"Page.frameStartedLoading": Page.frameStartedLoadingPayload;
|
||||
|
|
@ -20656,6 +20684,7 @@ Error was thrown.
|
|||
"Audits.checkContrast": Audits.checkContrastParameters;
|
||||
"Audits.checkFormsIssues": Audits.checkFormsIssuesParameters;
|
||||
"Extensions.loadUnpacked": Extensions.loadUnpackedParameters;
|
||||
"Extensions.uninstall": Extensions.uninstallParameters;
|
||||
"Extensions.getStorageItems": Extensions.getStorageItemsParameters;
|
||||
"Extensions.removeStorageItems": Extensions.removeStorageItemsParameters;
|
||||
"Extensions.clearStorageItems": Extensions.clearStorageItemsParameters;
|
||||
|
|
@ -20808,10 +20837,6 @@ Error was thrown.
|
|||
"DOMStorage.getDOMStorageItems": DOMStorage.getDOMStorageItemsParameters;
|
||||
"DOMStorage.removeDOMStorageItem": DOMStorage.removeDOMStorageItemParameters;
|
||||
"DOMStorage.setDOMStorageItem": DOMStorage.setDOMStorageItemParameters;
|
||||
"Database.disable": Database.disableParameters;
|
||||
"Database.enable": Database.enableParameters;
|
||||
"Database.executeSQL": Database.executeSQLParameters;
|
||||
"Database.getDatabaseTableNames": Database.getDatabaseTableNamesParameters;
|
||||
"DeviceOrientation.clearDeviceOrientationOverride": DeviceOrientation.clearDeviceOrientationOverrideParameters;
|
||||
"DeviceOrientation.setDeviceOrientationOverride": DeviceOrientation.setDeviceOrientationOverrideParameters;
|
||||
"Emulation.canEmulate": Emulation.canEmulateParameters;
|
||||
|
|
@ -21088,6 +21113,7 @@ Error was thrown.
|
|||
"Storage.setAttributionReportingTracking": Storage.setAttributionReportingTrackingParameters;
|
||||
"Storage.sendPendingAttributionReports": Storage.sendPendingAttributionReportsParameters;
|
||||
"Storage.getRelatedWebsiteSets": Storage.getRelatedWebsiteSetsParameters;
|
||||
"Storage.getAffectedUrlsForThirdPartyCookieMetadata": Storage.getAffectedUrlsForThirdPartyCookieMetadataParameters;
|
||||
"SystemInfo.getInfo": SystemInfo.getInfoParameters;
|
||||
"SystemInfo.getFeatureState": SystemInfo.getFeatureStateParameters;
|
||||
"SystemInfo.getProcessInfo": SystemInfo.getProcessInfoParameters;
|
||||
|
|
@ -21273,6 +21299,7 @@ Error was thrown.
|
|||
"Audits.checkContrast": Audits.checkContrastReturnValue;
|
||||
"Audits.checkFormsIssues": Audits.checkFormsIssuesReturnValue;
|
||||
"Extensions.loadUnpacked": Extensions.loadUnpackedReturnValue;
|
||||
"Extensions.uninstall": Extensions.uninstallReturnValue;
|
||||
"Extensions.getStorageItems": Extensions.getStorageItemsReturnValue;
|
||||
"Extensions.removeStorageItems": Extensions.removeStorageItemsReturnValue;
|
||||
"Extensions.clearStorageItems": Extensions.clearStorageItemsReturnValue;
|
||||
|
|
@ -21425,10 +21452,6 @@ Error was thrown.
|
|||
"DOMStorage.getDOMStorageItems": DOMStorage.getDOMStorageItemsReturnValue;
|
||||
"DOMStorage.removeDOMStorageItem": DOMStorage.removeDOMStorageItemReturnValue;
|
||||
"DOMStorage.setDOMStorageItem": DOMStorage.setDOMStorageItemReturnValue;
|
||||
"Database.disable": Database.disableReturnValue;
|
||||
"Database.enable": Database.enableReturnValue;
|
||||
"Database.executeSQL": Database.executeSQLReturnValue;
|
||||
"Database.getDatabaseTableNames": Database.getDatabaseTableNamesReturnValue;
|
||||
"DeviceOrientation.clearDeviceOrientationOverride": DeviceOrientation.clearDeviceOrientationOverrideReturnValue;
|
||||
"DeviceOrientation.setDeviceOrientationOverride": DeviceOrientation.setDeviceOrientationOverrideReturnValue;
|
||||
"Emulation.canEmulate": Emulation.canEmulateReturnValue;
|
||||
|
|
@ -21705,6 +21728,7 @@ Error was thrown.
|
|||
"Storage.setAttributionReportingTracking": Storage.setAttributionReportingTrackingReturnValue;
|
||||
"Storage.sendPendingAttributionReports": Storage.sendPendingAttributionReportsReturnValue;
|
||||
"Storage.getRelatedWebsiteSets": Storage.getRelatedWebsiteSetsReturnValue;
|
||||
"Storage.getAffectedUrlsForThirdPartyCookieMetadata": Storage.getAffectedUrlsForThirdPartyCookieMetadataReturnValue;
|
||||
"SystemInfo.getInfo": SystemInfo.getInfoReturnValue;
|
||||
"SystemInfo.getFeatureState": SystemInfo.getFeatureStateReturnValue;
|
||||
"SystemInfo.getProcessInfo": SystemInfo.getProcessInfoReturnValue;
|
||||
|
|
|
|||
|
|
@ -15,9 +15,9 @@
|
|||
*/
|
||||
|
||||
import { assert, monotonicTime } from '../../utils';
|
||||
import { launchProcess } from '../../utils/processLauncher';
|
||||
import { serverSideCallMetadata } from '../instrumentation';
|
||||
import { Page } from '../page';
|
||||
import { launchProcess } from '../processLauncher';
|
||||
import { ProgressController } from '../progress';
|
||||
|
||||
import type { Progress } from '../progress';
|
||||
|
|
|
|||
|
|
@ -15,13 +15,13 @@
|
|||
*/
|
||||
|
||||
import { SdkObject, createInstrumentation, serverSideCallMetadata } from './instrumentation';
|
||||
import { gracefullyProcessExitDoNotHang } from './processLauncher';
|
||||
import { Recorder } from './recorder';
|
||||
import { asLocator } from '../utils';
|
||||
import { parseAriaSnapshotUnsafe } from '../utils/isomorphic/ariaSnapshot';
|
||||
import { yaml } from '../utilsBundle';
|
||||
import { EmptyRecorderApp } from './recorder/recorderApp';
|
||||
import { unsafeLocatorOrSelectorAsSelector } from '../utils/isomorphic/locatorParser';
|
||||
import { gracefullyProcessExitDoNotHang } from '../utils/processLauncher';
|
||||
|
||||
import type { Language } from '../utils';
|
||||
import type { Browser } from './browser';
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"Galaxy S5": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -121,7 +121,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Galaxy S5 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -132,7 +132,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Galaxy S8": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 360,
|
||||
"height": 740
|
||||
|
|
@ -143,7 +143,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Galaxy S8 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 740,
|
||||
"height": 360
|
||||
|
|
@ -154,7 +154,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Galaxy S9+": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 320,
|
||||
"height": 658
|
||||
|
|
@ -165,7 +165,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Galaxy S9+ landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 658,
|
||||
"height": 320
|
||||
|
|
@ -176,7 +176,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Galaxy Tab S4": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 712,
|
||||
"height": 1138
|
||||
|
|
@ -187,7 +187,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Galaxy Tab S4 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 1138,
|
||||
"height": 712
|
||||
|
|
@ -1098,7 +1098,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"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/133.0.6943.35 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/134.0.6998.3 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 384,
|
||||
"height": 640
|
||||
|
|
@ -1109,7 +1109,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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/133.0.6943.35 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/134.0.6998.3 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 640,
|
||||
"height": 384
|
||||
|
|
@ -1120,7 +1120,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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/133.0.6943.35 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/134.0.6998.3 Mobile Safari/537.36 Edge/14.14263",
|
||||
"viewport": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -1131,7 +1131,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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/133.0.6943.35 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/134.0.6998.3 Mobile Safari/537.36 Edge/14.14263",
|
||||
"viewport": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -1142,7 +1142,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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/133.0.6943.35 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/134.0.6998.3 Mobile Safari/537.36 Edge/14.14263",
|
||||
"viewport": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -1153,7 +1153,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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/133.0.6943.35 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/134.0.6998.3 Mobile Safari/537.36 Edge/14.14263",
|
||||
"viewport": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -1164,7 +1164,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 10": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 800,
|
||||
"height": 1280
|
||||
|
|
@ -1175,7 +1175,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 10 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 1280,
|
||||
"height": 800
|
||||
|
|
@ -1186,7 +1186,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 4": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 384,
|
||||
"height": 640
|
||||
|
|
@ -1197,7 +1197,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 4 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 640,
|
||||
"height": 384
|
||||
|
|
@ -1208,7 +1208,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 5": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -1219,7 +1219,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 5 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -1230,7 +1230,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 5X": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 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/134.0.6998.3 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 412,
|
||||
"height": 732
|
||||
|
|
@ -1241,7 +1241,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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/133.0.6943.35 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/134.0.6998.3 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 732,
|
||||
"height": 412
|
||||
|
|
@ -1252,7 +1252,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 6": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 412,
|
||||
"height": 732
|
||||
|
|
@ -1263,7 +1263,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 6 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 732,
|
||||
"height": 412
|
||||
|
|
@ -1274,7 +1274,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 6P": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 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/134.0.6998.3 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 412,
|
||||
"height": 732
|
||||
|
|
@ -1285,7 +1285,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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/133.0.6943.35 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/134.0.6998.3 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 732,
|
||||
"height": 412
|
||||
|
|
@ -1296,7 +1296,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 7": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 600,
|
||||
"height": 960
|
||||
|
|
@ -1307,7 +1307,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Nexus 7 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 960,
|
||||
"height": 600
|
||||
|
|
@ -1362,7 +1362,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"Pixel 2": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 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/134.0.6998.3 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 411,
|
||||
"height": 731
|
||||
|
|
@ -1373,7 +1373,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 2 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 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/134.0.6998.3 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 731,
|
||||
"height": 411
|
||||
|
|
@ -1384,7 +1384,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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/133.0.6943.35 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/134.0.6998.3 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 411,
|
||||
"height": 823
|
||||
|
|
@ -1395,7 +1395,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"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/133.0.6943.35 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/134.0.6998.3 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 823,
|
||||
"height": 411
|
||||
|
|
@ -1406,7 +1406,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 3": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 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/134.0.6998.3 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 393,
|
||||
"height": 786
|
||||
|
|
@ -1417,7 +1417,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 3 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 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/134.0.6998.3 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 786,
|
||||
"height": 393
|
||||
|
|
@ -1428,7 +1428,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 4": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 353,
|
||||
"height": 745
|
||||
|
|
@ -1439,7 +1439,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 4 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 745,
|
||||
"height": 353
|
||||
|
|
@ -1450,7 +1450,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 4a (5G)": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
|
||||
"screen": {
|
||||
"width": 412,
|
||||
"height": 892
|
||||
|
|
@ -1465,7 +1465,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 4a (5G) landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
|
||||
"screen": {
|
||||
"height": 892,
|
||||
"width": 412
|
||||
|
|
@ -1480,7 +1480,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 5": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
|
||||
"screen": {
|
||||
"width": 393,
|
||||
"height": 851
|
||||
|
|
@ -1495,7 +1495,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 5 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
|
||||
"screen": {
|
||||
"width": 851,
|
||||
"height": 393
|
||||
|
|
@ -1510,7 +1510,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 7": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
|
||||
"screen": {
|
||||
"width": 412,
|
||||
"height": 915
|
||||
|
|
@ -1525,7 +1525,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Pixel 7 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
|
||||
"screen": {
|
||||
"width": 915,
|
||||
"height": 412
|
||||
|
|
@ -1540,7 +1540,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Moto G4": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 360,
|
||||
"height": 640
|
||||
|
|
@ -1551,7 +1551,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Moto G4 landscape": {
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Mobile Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Mobile Safari/537.36",
|
||||
"viewport": {
|
||||
"width": 640,
|
||||
"height": 360
|
||||
|
|
@ -1562,7 +1562,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Desktop Chrome HiDPI": {
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Safari/537.36",
|
||||
"screen": {
|
||||
"width": 1792,
|
||||
"height": 1120
|
||||
|
|
@ -1577,7 +1577,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Desktop Edge HiDPI": {
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Safari/537.36 Edg/133.0.6943.35",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Safari/537.36 Edg/134.0.6998.3",
|
||||
"screen": {
|
||||
"width": 1792,
|
||||
"height": 1120
|
||||
|
|
@ -1592,7 +1592,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Desktop Firefox HiDPI": {
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:134.0) Gecko/20100101 Firefox/134.0",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0",
|
||||
"screen": {
|
||||
"width": 1792,
|
||||
"height": 1120
|
||||
|
|
@ -1622,7 +1622,7 @@
|
|||
"defaultBrowserType": "webkit"
|
||||
},
|
||||
"Desktop Chrome": {
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Safari/537.36",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Safari/537.36",
|
||||
"screen": {
|
||||
"width": 1920,
|
||||
"height": 1080
|
||||
|
|
@ -1637,7 +1637,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Desktop Edge": {
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.6943.35 Safari/537.36 Edg/133.0.6943.35",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.3 Safari/537.36 Edg/134.0.6998.3",
|
||||
"screen": {
|
||||
"width": 1920,
|
||||
"height": 1080
|
||||
|
|
@ -1652,7 +1652,7 @@
|
|||
"defaultBrowserType": "chromium"
|
||||
},
|
||||
"Desktop Firefox": {
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:134.0) Gecko/20100101 Firefox/134.0",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0",
|
||||
"screen": {
|
||||
"width": 1920,
|
||||
"height": 1080
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import * as fs from 'fs';
|
|||
|
||||
import { Dispatcher, existingDispatcher } from './dispatcher';
|
||||
import { StreamDispatcher } from './streamDispatcher';
|
||||
import { mkdirIfNeeded } from '../../utils/fileUtils';
|
||||
import { mkdirIfNeeded } from '../fileUtils';
|
||||
|
||||
import type { DispatcherScope } from './dispatcher';
|
||||
import type { Artifact } from '../artifact';
|
||||
|
|
|
|||
|
|
@ -14,45 +14,27 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
import { Dispatcher } from './dispatcher';
|
||||
import { SdkObject } from '../../server/instrumentation';
|
||||
import { assert, calculateSha1, createGuid, removeFolders } from '../../utils';
|
||||
import { serializeClientSideCallMetadata } from '../../utils';
|
||||
import { ManualPromise } from '../../utils/manualPromise';
|
||||
import { fetchData } from '../../utils/network';
|
||||
import * as localUtils from '../../utils/localUtils';
|
||||
import { nodePlatform } from '../../utils/platform';
|
||||
import { getUserAgent } from '../../utils/userAgent';
|
||||
import { ZipFile } from '../../utils/zipFile';
|
||||
import { yauzl, yazl } from '../../zipBundle';
|
||||
import { deviceDescriptors as descriptors } from '../deviceDescriptors';
|
||||
import { JsonPipeDispatcher } from '../dispatchers/jsonPipeDispatcher';
|
||||
import { ProgressController } from '../progress';
|
||||
import { SocksInterceptor } from '../socksInterceptor';
|
||||
import { WebSocketTransport } from '../transport';
|
||||
|
||||
import type { HTTPRequestParams } from '../../utils/network';
|
||||
import type { HarBackend } from '../../utils/harBackend';
|
||||
import type { CallMetadata } from '../instrumentation';
|
||||
import type { Playwright } from '../playwright';
|
||||
import type { Progress } from '../progress';
|
||||
import type { HeadersArray } from '../types';
|
||||
import type { RootDispatcher } from './dispatcher';
|
||||
import type * as channels from '@protocol/channels';
|
||||
import type * as har from '@trace/har';
|
||||
import type EventEmitter from 'events';
|
||||
import type http from 'http';
|
||||
|
||||
export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.LocalUtilsChannel, RootDispatcher> implements channels.LocalUtilsChannel {
|
||||
_type_LocalUtils: boolean;
|
||||
private _harBackends = new Map<string, HarBackend>();
|
||||
private _stackSessions = new Map<string, {
|
||||
file: string,
|
||||
writer: Promise<void>,
|
||||
tmpDir: string | undefined,
|
||||
callStacks: channels.ClientSideCallMetadata[]
|
||||
}>();
|
||||
private _stackSessions = new Map<string, localUtils.StackSession>();
|
||||
|
||||
constructor(scope: RootDispatcher, playwright: Playwright) {
|
||||
const localUtils = new SdkObject(playwright, 'localUtils', 'localUtils');
|
||||
|
|
@ -65,139 +47,35 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
|
|||
}
|
||||
|
||||
async zip(params: channels.LocalUtilsZipParams): Promise<void> {
|
||||
const promise = new ManualPromise<void>();
|
||||
const zipFile = new yazl.ZipFile();
|
||||
(zipFile as any as EventEmitter).on('error', error => promise.reject(error));
|
||||
|
||||
const addFile = (file: string, name: string) => {
|
||||
try {
|
||||
if (fs.statSync(file).isFile())
|
||||
zipFile.addFile(file, name);
|
||||
} catch (e) {
|
||||
}
|
||||
};
|
||||
|
||||
for (const entry of params.entries)
|
||||
addFile(entry.value, entry.name);
|
||||
|
||||
// Add stacks and the sources.
|
||||
const stackSession = params.stacksId ? this._stackSessions.get(params.stacksId) : undefined;
|
||||
if (stackSession?.callStacks.length) {
|
||||
await stackSession.writer;
|
||||
if (process.env.PW_LIVE_TRACE_STACKS) {
|
||||
zipFile.addFile(stackSession.file, 'trace.stacks');
|
||||
} else {
|
||||
const buffer = Buffer.from(JSON.stringify(serializeClientSideCallMetadata(stackSession.callStacks)));
|
||||
zipFile.addBuffer(buffer, 'trace.stacks');
|
||||
}
|
||||
}
|
||||
|
||||
// Collect sources from stacks.
|
||||
if (params.includeSources) {
|
||||
const sourceFiles = new Set<string>();
|
||||
for (const { stack } of stackSession?.callStacks || []) {
|
||||
if (!stack)
|
||||
continue;
|
||||
for (const { file } of stack)
|
||||
sourceFiles.add(file);
|
||||
}
|
||||
for (const sourceFile of sourceFiles)
|
||||
addFile(sourceFile, 'resources/src@' + calculateSha1(sourceFile) + '.txt');
|
||||
}
|
||||
|
||||
if (params.mode === 'write') {
|
||||
// New file, just compress the entries.
|
||||
await fs.promises.mkdir(path.dirname(params.zipFile), { recursive: true });
|
||||
zipFile.end(undefined, () => {
|
||||
zipFile.outputStream.pipe(fs.createWriteStream(params.zipFile))
|
||||
.on('close', () => promise.resolve())
|
||||
.on('error', error => promise.reject(error));
|
||||
});
|
||||
await promise;
|
||||
await this._deleteStackSession(params.stacksId);
|
||||
return;
|
||||
}
|
||||
|
||||
// File already exists. Repack and add new entries.
|
||||
const tempFile = params.zipFile + '.tmp';
|
||||
await fs.promises.rename(params.zipFile, tempFile);
|
||||
|
||||
yauzl.open(tempFile, (err, inZipFile) => {
|
||||
if (err) {
|
||||
promise.reject(err);
|
||||
return;
|
||||
}
|
||||
assert(inZipFile);
|
||||
let pendingEntries = inZipFile.entryCount;
|
||||
inZipFile.on('entry', entry => {
|
||||
inZipFile.openReadStream(entry, (err, readStream) => {
|
||||
if (err) {
|
||||
promise.reject(err);
|
||||
return;
|
||||
}
|
||||
zipFile.addReadStream(readStream!, entry.fileName);
|
||||
if (--pendingEntries === 0) {
|
||||
zipFile.end(undefined, () => {
|
||||
zipFile.outputStream.pipe(fs.createWriteStream(params.zipFile)).on('close', () => {
|
||||
fs.promises.unlink(tempFile).then(() => {
|
||||
promise.resolve();
|
||||
}).catch(error => promise.reject(error));
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
await promise;
|
||||
await this._deleteStackSession(params.stacksId);
|
||||
return await localUtils.zip(nodePlatform, this._stackSessions, params);
|
||||
}
|
||||
|
||||
async harOpen(params: channels.LocalUtilsHarOpenParams, metadata: CallMetadata): Promise<channels.LocalUtilsHarOpenResult> {
|
||||
let harBackend: HarBackend;
|
||||
if (params.file.endsWith('.zip')) {
|
||||
const zipFile = new ZipFile(params.file);
|
||||
const entryNames = await zipFile.entries();
|
||||
const harEntryName = entryNames.find(e => e.endsWith('.har'));
|
||||
if (!harEntryName)
|
||||
return { error: 'Specified archive does not have a .har file' };
|
||||
const har = await zipFile.read(harEntryName);
|
||||
const harFile = JSON.parse(har.toString()) as har.HARFile;
|
||||
harBackend = new HarBackend(harFile, null, zipFile);
|
||||
} else {
|
||||
const harFile = JSON.parse(await fs.promises.readFile(params.file, 'utf-8')) as har.HARFile;
|
||||
harBackend = new HarBackend(harFile, path.dirname(params.file), null);
|
||||
}
|
||||
this._harBackends.set(harBackend.id, harBackend);
|
||||
return { harId: harBackend.id };
|
||||
return await localUtils.harOpen(this._harBackends, params);
|
||||
}
|
||||
|
||||
async harLookup(params: channels.LocalUtilsHarLookupParams, metadata: CallMetadata): Promise<channels.LocalUtilsHarLookupResult> {
|
||||
const harBackend = this._harBackends.get(params.harId);
|
||||
if (!harBackend)
|
||||
return { action: 'error', message: `Internal error: har was not opened` };
|
||||
return await harBackend.lookup(params.url, params.method, params.headers, params.postData, params.isNavigationRequest);
|
||||
return await localUtils.harLookup(this._harBackends, params);
|
||||
}
|
||||
|
||||
async harClose(params: channels.LocalUtilsHarCloseParams, metadata: CallMetadata): Promise<void> {
|
||||
const harBackend = this._harBackends.get(params.harId);
|
||||
if (harBackend) {
|
||||
this._harBackends.delete(harBackend.id);
|
||||
harBackend.dispose();
|
||||
}
|
||||
return await localUtils.harClose(this._harBackends, params);
|
||||
}
|
||||
|
||||
async harUnzip(params: channels.LocalUtilsHarUnzipParams, metadata: CallMetadata): Promise<void> {
|
||||
const dir = path.dirname(params.zipFile);
|
||||
const zipFile = new ZipFile(params.zipFile);
|
||||
for (const entry of await zipFile.entries()) {
|
||||
const buffer = await zipFile.read(entry);
|
||||
if (entry === 'har.har')
|
||||
await fs.promises.writeFile(params.harFile, buffer);
|
||||
else
|
||||
await fs.promises.writeFile(path.join(dir, entry), buffer);
|
||||
}
|
||||
zipFile.close();
|
||||
await fs.promises.unlink(params.zipFile);
|
||||
return await localUtils.harUnzip(params);
|
||||
}
|
||||
|
||||
async tracingStarted(params: channels.LocalUtilsTracingStartedParams, metadata?: CallMetadata | undefined): Promise<channels.LocalUtilsTracingStartedResult> {
|
||||
return await localUtils.tracingStarted(this._stackSessions, params);
|
||||
}
|
||||
|
||||
async traceDiscarded(params: channels.LocalUtilsTraceDiscardedParams, metadata?: CallMetadata | undefined): Promise<void> {
|
||||
return await localUtils.traceDiscarded(nodePlatform, this._stackSessions, params);
|
||||
}
|
||||
|
||||
async addStackToTracingNoReply(params: channels.LocalUtilsAddStackToTracingNoReplyParams, metadata?: CallMetadata | undefined): Promise<void> {
|
||||
return await localUtils.addStackToTracingNoReply(this._stackSessions, params);
|
||||
}
|
||||
|
||||
async connect(params: channels.LocalUtilsConnectParams, metadata: CallMetadata): Promise<channels.LocalUtilsConnectResult> {
|
||||
|
|
@ -209,7 +87,7 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
|
|||
'x-playwright-proxy': params.exposeNetwork ?? '',
|
||||
...params.headers,
|
||||
};
|
||||
const wsEndpoint = await urlToWSEndpoint(progress, params.wsEndpoint);
|
||||
const wsEndpoint = await localUtils.urlToWSEndpoint(progress, params.wsEndpoint);
|
||||
|
||||
const transport = await WebSocketTransport.connect(progress, wsEndpoint, wsHeaders, true, 'x-playwright-debug-log');
|
||||
const socksInterceptor = new SocksInterceptor(transport, params.exposeNetwork, params.socksProxyRedirectPortForTest);
|
||||
|
|
@ -240,221 +118,4 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
|
|||
return { pipe, headers: transport.headers };
|
||||
}, params.timeout || 0);
|
||||
}
|
||||
|
||||
async tracingStarted(params: channels.LocalUtilsTracingStartedParams, metadata?: CallMetadata | undefined): Promise<channels.LocalUtilsTracingStartedResult> {
|
||||
let tmpDir = undefined;
|
||||
if (!params.tracesDir)
|
||||
tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'playwright-tracing-'));
|
||||
const traceStacksFile = path.join(params.tracesDir || tmpDir!, params.traceName + '.stacks');
|
||||
this._stackSessions.set(traceStacksFile, { callStacks: [], file: traceStacksFile, writer: Promise.resolve(), tmpDir });
|
||||
return { stacksId: traceStacksFile };
|
||||
}
|
||||
|
||||
async traceDiscarded(params: channels.LocalUtilsTraceDiscardedParams, metadata?: CallMetadata | undefined): Promise<void> {
|
||||
await this._deleteStackSession(params.stacksId);
|
||||
}
|
||||
|
||||
async addStackToTracingNoReply(params: channels.LocalUtilsAddStackToTracingNoReplyParams, metadata?: CallMetadata | undefined): Promise<void> {
|
||||
for (const session of this._stackSessions.values()) {
|
||||
session.callStacks.push(params.callData);
|
||||
if (process.env.PW_LIVE_TRACE_STACKS) {
|
||||
session.writer = session.writer.then(() => {
|
||||
const buffer = Buffer.from(JSON.stringify(serializeClientSideCallMetadata(session.callStacks)));
|
||||
return fs.promises.writeFile(session.file, buffer);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _deleteStackSession(stacksId?: string) {
|
||||
const session = stacksId ? this._stackSessions.get(stacksId) : undefined;
|
||||
if (!session)
|
||||
return;
|
||||
await session.writer;
|
||||
if (session.tmpDir)
|
||||
await removeFolders([session.tmpDir]);
|
||||
this._stackSessions.delete(stacksId!);
|
||||
}
|
||||
}
|
||||
|
||||
const redirectStatus = [301, 302, 303, 307, 308];
|
||||
|
||||
class HarBackend {
|
||||
readonly id = createGuid();
|
||||
private _harFile: har.HARFile;
|
||||
private _zipFile: ZipFile | null;
|
||||
private _baseDir: string | null;
|
||||
|
||||
constructor(harFile: har.HARFile, baseDir: string | null, zipFile: ZipFile | null) {
|
||||
this._harFile = harFile;
|
||||
this._baseDir = baseDir;
|
||||
this._zipFile = zipFile;
|
||||
}
|
||||
|
||||
async lookup(url: string, method: string, headers: HeadersArray, postData: Buffer | undefined, isNavigationRequest: boolean): Promise<{
|
||||
action: 'error' | 'redirect' | 'fulfill' | 'noentry',
|
||||
message?: string,
|
||||
redirectURL?: string,
|
||||
status?: number,
|
||||
headers?: HeadersArray,
|
||||
body?: Buffer }> {
|
||||
let entry;
|
||||
try {
|
||||
entry = await this._harFindResponse(url, method, headers, postData);
|
||||
} catch (e) {
|
||||
return { action: 'error', message: 'HAR error: ' + e.message };
|
||||
}
|
||||
|
||||
if (!entry)
|
||||
return { action: 'noentry' };
|
||||
|
||||
// If navigation is being redirected, restart it with the final url to ensure the document's url changes.
|
||||
if (entry.request.url !== url && isNavigationRequest)
|
||||
return { action: 'redirect', redirectURL: entry.request.url };
|
||||
|
||||
const response = entry.response;
|
||||
try {
|
||||
const buffer = await this._loadContent(response.content);
|
||||
return {
|
||||
action: 'fulfill',
|
||||
status: response.status,
|
||||
headers: response.headers,
|
||||
body: buffer,
|
||||
};
|
||||
} catch (e) {
|
||||
return { action: 'error', message: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadContent(content: { text?: string, encoding?: string, _file?: string }): Promise<Buffer> {
|
||||
const file = content._file;
|
||||
let buffer: Buffer;
|
||||
if (file) {
|
||||
if (this._zipFile)
|
||||
buffer = await this._zipFile.read(file);
|
||||
else
|
||||
buffer = await fs.promises.readFile(path.resolve(this._baseDir!, file));
|
||||
} else {
|
||||
buffer = Buffer.from(content.text || '', content.encoding === 'base64' ? 'base64' : 'utf-8');
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private async _harFindResponse(url: string, method: string, headers: HeadersArray, postData: Buffer | undefined): Promise<har.Entry | undefined> {
|
||||
const harLog = this._harFile.log;
|
||||
const visited = new Set<har.Entry>();
|
||||
while (true) {
|
||||
const entries: har.Entry[] = [];
|
||||
for (const candidate of harLog.entries) {
|
||||
if (candidate.request.url !== url || candidate.request.method !== method)
|
||||
continue;
|
||||
if (method === 'POST' && postData && candidate.request.postData) {
|
||||
const buffer = await this._loadContent(candidate.request.postData);
|
||||
if (!buffer.equals(postData)) {
|
||||
const boundary = multipartBoundary(headers);
|
||||
if (!boundary)
|
||||
continue;
|
||||
const candidataBoundary = multipartBoundary(candidate.request.headers);
|
||||
if (!candidataBoundary)
|
||||
continue;
|
||||
// Try to match multipart/form-data ignroing boundary as it changes between requests.
|
||||
if (postData.toString().replaceAll(boundary, '') !== buffer.toString().replaceAll(candidataBoundary, ''))
|
||||
continue;
|
||||
}
|
||||
}
|
||||
entries.push(candidate);
|
||||
}
|
||||
|
||||
if (!entries.length)
|
||||
return;
|
||||
|
||||
let entry = entries[0];
|
||||
|
||||
// Disambiguate using headers - then one with most matching headers wins.
|
||||
if (entries.length > 1) {
|
||||
const list: { candidate: har.Entry, matchingHeaders: number }[] = [];
|
||||
for (const candidate of entries) {
|
||||
const matchingHeaders = countMatchingHeaders(candidate.request.headers, headers);
|
||||
list.push({ candidate, matchingHeaders });
|
||||
}
|
||||
list.sort((a, b) => b.matchingHeaders - a.matchingHeaders);
|
||||
entry = list[0].candidate;
|
||||
}
|
||||
|
||||
if (visited.has(entry))
|
||||
throw new Error(`Found redirect cycle for ${url}`);
|
||||
|
||||
visited.add(entry);
|
||||
|
||||
// Follow redirects.
|
||||
const locationHeader = entry.response.headers.find(h => h.name.toLowerCase() === 'location');
|
||||
if (redirectStatus.includes(entry.response.status) && locationHeader) {
|
||||
const locationURL = new URL(locationHeader.value, url);
|
||||
url = locationURL.toString();
|
||||
if ((entry.response.status === 301 || entry.response.status === 302) && method === 'POST' ||
|
||||
entry.response.status === 303 && !['GET', 'HEAD'].includes(method)) {
|
||||
// HTTP-redirect fetch step 13 (https://fetch.spec.whatwg.org/#http-redirect-fetch)
|
||||
method = 'GET';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._zipFile?.close();
|
||||
}
|
||||
}
|
||||
|
||||
function countMatchingHeaders(harHeaders: har.Header[], headers: HeadersArray): number {
|
||||
const set = new Set(headers.map(h => h.name.toLowerCase() + ':' + h.value));
|
||||
let matches = 0;
|
||||
for (const h of harHeaders) {
|
||||
if (set.has(h.name.toLowerCase() + ':' + h.value))
|
||||
++matches;
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
export async function urlToWSEndpoint(progress: Progress|undefined, endpointURL: string): Promise<string> {
|
||||
if (endpointURL.startsWith('ws'))
|
||||
return endpointURL;
|
||||
|
||||
progress?.log(`<ws preparing> retrieving websocket url from ${endpointURL}`);
|
||||
const fetchUrl = new URL(endpointURL);
|
||||
if (!fetchUrl.pathname.endsWith('/'))
|
||||
fetchUrl.pathname += '/';
|
||||
fetchUrl.pathname += 'json';
|
||||
const json = await fetchData({
|
||||
url: fetchUrl.toString(),
|
||||
method: 'GET',
|
||||
timeout: progress?.timeUntilDeadline() ?? 30_000,
|
||||
headers: { 'User-Agent': getUserAgent() },
|
||||
}, async (params: HTTPRequestParams, response: http.IncomingMessage) => {
|
||||
return new Error(`Unexpected status ${response.statusCode} when connecting to ${fetchUrl.toString()}.\n` +
|
||||
`This does not look like a Playwright server, try connecting via ws://.`);
|
||||
});
|
||||
progress?.throwIfAborted();
|
||||
|
||||
const wsUrl = new URL(endpointURL);
|
||||
let wsEndpointPath = JSON.parse(json).wsEndpointPath;
|
||||
if (wsEndpointPath.startsWith('/'))
|
||||
wsEndpointPath = wsEndpointPath.substring(1);
|
||||
if (!wsUrl.pathname.endsWith('/'))
|
||||
wsUrl.pathname += '/';
|
||||
wsUrl.pathname += wsEndpointPath;
|
||||
wsUrl.protocol = wsUrl.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
return wsUrl.toString();
|
||||
}
|
||||
|
||||
function multipartBoundary(headers: HeadersArray) {
|
||||
const contentType = headers.find(h => h.name.toLowerCase() === 'content-type');
|
||||
if (!contentType?.value.includes('multipart/form-data'))
|
||||
return undefined;
|
||||
const boundary = contentType.value.match(/boundary=(\S+)/);
|
||||
if (boundary)
|
||||
return boundary[1];
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import { TimeoutSettings } from '../../common/timeoutSettings';
|
|||
import { ManualPromise, wrapInASCIIBox } from '../../utils';
|
||||
import { RecentLogsCollector } from '../../utils/debugLogger';
|
||||
import { eventsHelper } from '../../utils/eventsHelper';
|
||||
import { envArrayToObject, launchProcess } from '../../utils/processLauncher';
|
||||
import { validateBrowserContextOptions } from '../browserContext';
|
||||
import { CRBrowser } from '../chromium/crBrowser';
|
||||
import { CRConnection } from '../chromium/crConnection';
|
||||
|
|
@ -33,6 +32,7 @@ import { ConsoleMessage } from '../console';
|
|||
import { helper } from '../helper';
|
||||
import { SdkObject, serverSideCallMetadata } from '../instrumentation';
|
||||
import * as js from '../javascript';
|
||||
import { envArrayToObject, launchProcess } from '../processLauncher';
|
||||
import { ProgressController } from '../progress';
|
||||
import { WebSocketTransport } from '../transport';
|
||||
|
||||
|
|
|
|||
205
packages/playwright-core/src/server/fileUtils.ts
Normal file
205
packages/playwright-core/src/server/fileUtils.ts
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
/**
|
||||
* 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 fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { ManualPromise } from '../utils/manualPromise';
|
||||
import { yazl } from '../zipBundle';
|
||||
|
||||
import type { EventEmitter } from 'events';
|
||||
|
||||
export const existsAsync = (path: string): Promise<boolean> => new Promise(resolve => fs.stat(path, err => resolve(!err)));
|
||||
|
||||
export async function mkdirIfNeeded(filePath: string) {
|
||||
// This will harmlessly throw on windows if the dirname is the root directory.
|
||||
await fs.promises.mkdir(path.dirname(filePath), { recursive: true }).catch(() => {});
|
||||
}
|
||||
|
||||
export async function removeFolders(dirs: string[]): Promise<Error[]> {
|
||||
return await Promise.all(dirs.map((dir: string) =>
|
||||
fs.promises.rm(dir, { recursive: true, force: true, maxRetries: 10 }).catch(e => e)
|
||||
));
|
||||
}
|
||||
|
||||
export function canAccessFile(file: string) {
|
||||
if (!file)
|
||||
return false;
|
||||
|
||||
try {
|
||||
fs.accessSync(file);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function copyFileAndMakeWritable(from: string, to: string) {
|
||||
await fs.promises.copyFile(from, to);
|
||||
await fs.promises.chmod(to, 0o664);
|
||||
}
|
||||
|
||||
export function sanitizeForFilePath(s: string) {
|
||||
return s.replace(/[\x00-\x2C\x2E-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, '-');
|
||||
}
|
||||
|
||||
export function toPosixPath(aPath: string): string {
|
||||
return aPath.split(path.sep).join(path.posix.sep);
|
||||
}
|
||||
|
||||
type NameValue = { name: string, value: string };
|
||||
type SerializedFSOperation = {
|
||||
op: 'mkdir', dir: string,
|
||||
} | {
|
||||
op: 'writeFile', file: string, content: string | Buffer, skipIfExists?: boolean,
|
||||
} | {
|
||||
op: 'appendFile', file: string, content: string,
|
||||
} | {
|
||||
op: 'copyFile', from: string, to: string,
|
||||
} | {
|
||||
op: 'zip', entries: NameValue[], zipFileName: string,
|
||||
};
|
||||
|
||||
export class SerializedFS {
|
||||
private _buffers = new Map<string, string[]>(); // Should never be accessed from within appendOperation.
|
||||
private _error: Error | undefined;
|
||||
private _operations: SerializedFSOperation[] = [];
|
||||
private _operationsDone: ManualPromise<void>;
|
||||
|
||||
constructor() {
|
||||
this._operationsDone = new ManualPromise();
|
||||
this._operationsDone.resolve(); // No operations scheduled yet.
|
||||
}
|
||||
|
||||
mkdir(dir: string) {
|
||||
this._appendOperation({ op: 'mkdir', dir });
|
||||
}
|
||||
|
||||
writeFile(file: string, content: string | Buffer, skipIfExists?: boolean) {
|
||||
this._buffers.delete(file); // No need to flush the buffer since we'll overwrite anyway.
|
||||
this._appendOperation({ op: 'writeFile', file, content, skipIfExists });
|
||||
}
|
||||
|
||||
appendFile(file: string, text: string, flush?: boolean) {
|
||||
if (!this._buffers.has(file))
|
||||
this._buffers.set(file, []);
|
||||
this._buffers.get(file)!.push(text);
|
||||
if (flush)
|
||||
this._flushFile(file);
|
||||
}
|
||||
|
||||
private _flushFile(file: string) {
|
||||
const buffer = this._buffers.get(file);
|
||||
if (buffer === undefined)
|
||||
return;
|
||||
const content = buffer.join('');
|
||||
this._buffers.delete(file);
|
||||
this._appendOperation({ op: 'appendFile', file, content });
|
||||
}
|
||||
|
||||
copyFile(from: string, to: string) {
|
||||
this._flushFile(from);
|
||||
this._buffers.delete(to); // No need to flush the buffer since we'll overwrite anyway.
|
||||
this._appendOperation({ op: 'copyFile', from, to });
|
||||
}
|
||||
|
||||
async syncAndGetError() {
|
||||
for (const file of this._buffers.keys())
|
||||
this._flushFile(file);
|
||||
await this._operationsDone;
|
||||
return this._error;
|
||||
}
|
||||
|
||||
zip(entries: NameValue[], zipFileName: string) {
|
||||
for (const file of this._buffers.keys())
|
||||
this._flushFile(file);
|
||||
|
||||
// Chain the export operation against write operations,
|
||||
// so that files do not change during the export.
|
||||
this._appendOperation({ op: 'zip', entries, zipFileName });
|
||||
}
|
||||
|
||||
// This method serializes all writes to the trace.
|
||||
private _appendOperation(op: SerializedFSOperation): void {
|
||||
const last = this._operations[this._operations.length - 1];
|
||||
if (last?.op === 'appendFile' && op.op === 'appendFile' && last.file === op.file) {
|
||||
// Merge pending appendFile operations for performance.
|
||||
last.content += op.content;
|
||||
return;
|
||||
}
|
||||
|
||||
this._operations.push(op);
|
||||
if (this._operationsDone.isDone())
|
||||
this._performOperations();
|
||||
}
|
||||
|
||||
private async _performOperations() {
|
||||
this._operationsDone = new ManualPromise();
|
||||
while (this._operations.length) {
|
||||
const op = this._operations.shift()!;
|
||||
// Ignore all operations after the first error.
|
||||
if (this._error)
|
||||
continue;
|
||||
try {
|
||||
await this._performOperation(op);
|
||||
} catch (e) {
|
||||
this._error = e;
|
||||
}
|
||||
}
|
||||
this._operationsDone.resolve();
|
||||
}
|
||||
|
||||
private async _performOperation(op: SerializedFSOperation) {
|
||||
switch (op.op) {
|
||||
case 'mkdir': {
|
||||
await fs.promises.mkdir(op.dir, { recursive: true });
|
||||
return;
|
||||
}
|
||||
case 'writeFile': {
|
||||
// Note: 'wx' flag only writes when the file does not exist.
|
||||
// See https://nodejs.org/api/fs.html#file-system-flags.
|
||||
// This way tracing never have to write the same resource twice.
|
||||
if (op.skipIfExists)
|
||||
await fs.promises.writeFile(op.file, op.content, { flag: 'wx' }).catch(() => {});
|
||||
else
|
||||
await fs.promises.writeFile(op.file, op.content);
|
||||
return;
|
||||
}
|
||||
case 'copyFile': {
|
||||
await fs.promises.copyFile(op.from, op.to);
|
||||
return;
|
||||
}
|
||||
case 'appendFile': {
|
||||
await fs.promises.appendFile(op.file, op.content);
|
||||
return;
|
||||
}
|
||||
case 'zip': {
|
||||
const zipFile = new yazl.ZipFile();
|
||||
const result = new ManualPromise<void>();
|
||||
(zipFile as any as EventEmitter).on('error', error => result.reject(error));
|
||||
for (const entry of op.entries)
|
||||
zipFile.addFile(entry.value, entry.name);
|
||||
zipFile.end();
|
||||
zipFile.outputStream
|
||||
.pipe(fs.createWriteStream(op.zipFileName))
|
||||
.on('close', () => result.resolve())
|
||||
.on('error', error => result.reject(error));
|
||||
await result;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -24,9 +24,9 @@ import { wrapInASCIIBox } from '../../utils';
|
|||
import { BrowserType, kNoXServerRunningError } from '../browserType';
|
||||
import { BrowserReadyState } from '../browserType';
|
||||
|
||||
import type { Env } from '../../utils/processLauncher';
|
||||
import type { BrowserOptions } from '../browser';
|
||||
import type { SdkObject } from '../instrumentation';
|
||||
import type { Env } from '../processLauncher';
|
||||
import type { ProtocolError } from '../protocolError';
|
||||
import type { ConnectionTransport } from '../transport';
|
||||
import type * as types from '../types';
|
||||
|
|
|
|||
|
|
@ -31,3 +31,5 @@ export type { Playwright } from './playwright';
|
|||
export { installRootRedirect, openTraceInBrowser, openTraceViewerApp, runTraceViewerApp, startTraceViewerServer } from './trace/viewer/traceViewer';
|
||||
export { serverSideCallMetadata } from './instrumentation';
|
||||
export { SocksProxy } from '../common/socksProxy';
|
||||
export * from './fileUtils';
|
||||
export * from './processLauncher';
|
||||
|
|
|
|||
|
|
@ -20,8 +20,7 @@ import * as fs from 'fs';
|
|||
import * as readline from 'readline';
|
||||
|
||||
import { removeFolders } from './fileUtils';
|
||||
|
||||
import { isUnderTest } from './';
|
||||
import { isUnderTest } from '../utils';
|
||||
|
||||
export type Env = {[key: string]: string | number | boolean | undefined};
|
||||
|
||||
|
|
@ -19,14 +19,10 @@ import { assert, monotonicTime } from '../utils';
|
|||
import { ManualPromise } from '../utils/manualPromise';
|
||||
|
||||
import type { CallMetadata, Instrumentation, SdkObject } from './instrumentation';
|
||||
import type { Progress as CommonProgress } from '../common/progress';
|
||||
import type { LogName } from '../utils/debugLogger';
|
||||
|
||||
export interface Progress {
|
||||
log(message: string): void;
|
||||
timeUntilDeadline(): number;
|
||||
isRunning(): boolean;
|
||||
cleanupWhenAborted(cleanup: () => any): void;
|
||||
throwIfAborted(): void;
|
||||
export interface Progress extends CommonProgress {
|
||||
metadata: CallMetadata;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,10 +21,10 @@ import * as os from 'os';
|
|||
import * as path from 'path';
|
||||
|
||||
import { debugLogger } from '../../utils/debugLogger';
|
||||
import { existsAsync } from '../../utils/fileUtils';
|
||||
import { ManualPromise } from '../../utils/manualPromise';
|
||||
import { getUserAgent } from '../../utils/userAgent';
|
||||
import { colors, progress as ProgressBar } from '../../utilsBundle';
|
||||
import { existsAsync } from '../fileUtils';
|
||||
|
||||
import { browserDirectoryToMarkerFilePath } from '.';
|
||||
|
||||
|
|
|
|||
|
|
@ -25,12 +25,12 @@ import { dockerVersion, readDockerVersionSync, transformCommandsForRoot } from '
|
|||
import { installDependenciesLinux, installDependenciesWindows, validateDependenciesLinux, validateDependenciesWindows } from './dependencies';
|
||||
import { calculateSha1, getAsBooleanFromENV, getFromENV, getPackageManagerExecCommand, wrapInASCIIBox } from '../../utils';
|
||||
import { debugLogger } from '../../utils/debugLogger';
|
||||
import { canAccessFile, existsAsync, removeFolders } from '../../utils/fileUtils';
|
||||
import { hostPlatform, isOfficiallySupportedPlatform } from '../../utils/hostPlatform';
|
||||
import { fetchData } from '../../utils/network';
|
||||
import { spawnAsync } from '../../utils/spawnAsync';
|
||||
import { getEmbedderName } from '../../utils/userAgent';
|
||||
import { lockfile } from '../../utilsBundle';
|
||||
import { canAccessFile, existsAsync, removeFolders } from '../fileUtils';
|
||||
|
||||
import type { DependencyGroup } from './dependencies';
|
||||
import type { HostPlatform } from '../../utils/hostPlatform';
|
||||
|
|
|
|||
|
|
@ -20,11 +20,12 @@ import * as path from 'path';
|
|||
|
||||
import { Snapshotter } from './snapshotter';
|
||||
import { commandsWithTracingSnapshots } from '../../../protocol/debug';
|
||||
import { SerializedFS, assert, createGuid, eventsHelper, monotonicTime, removeFolders } from '../../../utils';
|
||||
import { assert, createGuid, eventsHelper, monotonicTime } from '../../../utils';
|
||||
import { Artifact } from '../../artifact';
|
||||
import { BrowserContext } from '../../browserContext';
|
||||
import { Dispatcher } from '../../dispatchers/dispatcher';
|
||||
import { serializeError } from '../../errors';
|
||||
import { SerializedFS, removeFolders } from '../../fileUtils';
|
||||
import { HarTracer } from '../../har/harTracer';
|
||||
import { SdkObject } from '../../instrumentation';
|
||||
import { Page } from '../../page';
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@
|
|||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { gracefullyProcessExitDoNotHang, isUnderTest } from '../../../utils';
|
||||
import { gracefullyProcessExitDoNotHang } from '../../../server';
|
||||
import { isUnderTest } from '../../../utils';
|
||||
import { HttpServer } from '../../../utils/httpServer';
|
||||
import { open } from '../../../utilsBundle';
|
||||
import { serverSideCallMetadata } from '../../instrumentation';
|
||||
|
|
|
|||
|
|
@ -7781,6 +7781,18 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the
|
|||
}
|
||||
export type setIgnoreCertificateErrorsReturnValue = {
|
||||
}
|
||||
/**
|
||||
* Changes page zoom factor.
|
||||
*/
|
||||
export type setPageZoomFactorParameters = {
|
||||
/**
|
||||
* Unique identifier of the page proxy.
|
||||
*/
|
||||
pageProxyId: PageProxyID;
|
||||
zoomFactor: number;
|
||||
}
|
||||
export type setPageZoomFactorReturnValue = {
|
||||
}
|
||||
/**
|
||||
* Returns all cookies in the given browser context.
|
||||
*/
|
||||
|
|
@ -9658,6 +9670,7 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the
|
|||
"Playwright.grantFileReadAccess": Playwright.grantFileReadAccessParameters;
|
||||
"Playwright.takePageScreenshot": Playwright.takePageScreenshotParameters;
|
||||
"Playwright.setIgnoreCertificateErrors": Playwright.setIgnoreCertificateErrorsParameters;
|
||||
"Playwright.setPageZoomFactor": Playwright.setPageZoomFactorParameters;
|
||||
"Playwright.getAllCookies": Playwright.getAllCookiesParameters;
|
||||
"Playwright.setCookies": Playwright.setCookiesParameters;
|
||||
"Playwright.deleteAllCookies": Playwright.deleteAllCookiesParameters;
|
||||
|
|
@ -9970,6 +9983,7 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the
|
|||
"Playwright.grantFileReadAccess": Playwright.grantFileReadAccessReturnValue;
|
||||
"Playwright.takePageScreenshot": Playwright.takePageScreenshotReturnValue;
|
||||
"Playwright.setIgnoreCertificateErrors": Playwright.setIgnoreCertificateErrorsReturnValue;
|
||||
"Playwright.setPageZoomFactor": Playwright.setPageZoomFactorReturnValue;
|
||||
"Playwright.getAllCookies": Playwright.getAllCookiesReturnValue;
|
||||
"Playwright.setCookies": Playwright.setCookiesReturnValue;
|
||||
"Playwright.deleteAllCookies": Playwright.deleteAllCookiesReturnValue;
|
||||
|
|
|
|||
|
|
@ -22,9 +22,9 @@ import { wrapInASCIIBox } from '../../utils';
|
|||
import { BrowserType, kNoXServerRunningError } from '../browserType';
|
||||
import { WKBrowser } from '../webkit/wkBrowser';
|
||||
|
||||
import type { Env } from '../../utils/processLauncher';
|
||||
import type { BrowserOptions } from '../browser';
|
||||
import type { SdkObject } from '../instrumentation';
|
||||
import type { Env } from '../processLauncher';
|
||||
import type { ProtocolError } from '../protocolError';
|
||||
import type { ConnectionTransport } from '../transport';
|
||||
import type * as types from '../types';
|
||||
|
|
|
|||
|
|
@ -14,194 +14,17 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { ManualPromise } from './manualPromise';
|
||||
import { yazl } from '../zipBundle';
|
||||
|
||||
import type { EventEmitter } from 'events';
|
||||
import type { Platform } from './platform';
|
||||
|
||||
export const fileUploadSizeLimit = 50 * 1024 * 1024;
|
||||
|
||||
export const existsAsync = (path: string): Promise<boolean> => new Promise(resolve => fs.stat(path, err => resolve(!err)));
|
||||
|
||||
export async function mkdirIfNeeded(filePath: string) {
|
||||
export async function mkdirIfNeeded(platform: Platform, filePath: string) {
|
||||
// This will harmlessly throw on windows if the dirname is the root directory.
|
||||
await fs.promises.mkdir(path.dirname(filePath), { recursive: true }).catch(() => {});
|
||||
await platform.fs().promises.mkdir(platform.path().dirname(filePath), { recursive: true }).catch(() => {});
|
||||
}
|
||||
|
||||
export async function removeFolders(dirs: string[]): Promise<Error[]> {
|
||||
export async function removeFolders(platform: Platform, dirs: string[]): Promise<Error[]> {
|
||||
return await Promise.all(dirs.map((dir: string) =>
|
||||
fs.promises.rm(dir, { recursive: true, force: true, maxRetries: 10 }).catch(e => e)
|
||||
platform.fs().promises.rm(dir, { recursive: true, force: true, maxRetries: 10 }).catch(e => e)
|
||||
));
|
||||
}
|
||||
|
||||
export function canAccessFile(file: string) {
|
||||
if (!file)
|
||||
return false;
|
||||
|
||||
try {
|
||||
fs.accessSync(file);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function copyFileAndMakeWritable(from: string, to: string) {
|
||||
await fs.promises.copyFile(from, to);
|
||||
await fs.promises.chmod(to, 0o664);
|
||||
}
|
||||
|
||||
export function sanitizeForFilePath(s: string) {
|
||||
return s.replace(/[\x00-\x2C\x2E-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, '-');
|
||||
}
|
||||
|
||||
export function toPosixPath(aPath: string): string {
|
||||
return aPath.split(path.sep).join(path.posix.sep);
|
||||
}
|
||||
|
||||
type NameValue = { name: string, value: string };
|
||||
type SerializedFSOperation = {
|
||||
op: 'mkdir', dir: string,
|
||||
} | {
|
||||
op: 'writeFile', file: string, content: string | Buffer, skipIfExists?: boolean,
|
||||
} | {
|
||||
op: 'appendFile', file: string, content: string,
|
||||
} | {
|
||||
op: 'copyFile', from: string, to: string,
|
||||
} | {
|
||||
op: 'zip', entries: NameValue[], zipFileName: string,
|
||||
};
|
||||
|
||||
export class SerializedFS {
|
||||
private _buffers = new Map<string, string[]>(); // Should never be accessed from within appendOperation.
|
||||
private _error: Error | undefined;
|
||||
private _operations: SerializedFSOperation[] = [];
|
||||
private _operationsDone: ManualPromise<void>;
|
||||
|
||||
constructor() {
|
||||
this._operationsDone = new ManualPromise();
|
||||
this._operationsDone.resolve(); // No operations scheduled yet.
|
||||
}
|
||||
|
||||
mkdir(dir: string) {
|
||||
this._appendOperation({ op: 'mkdir', dir });
|
||||
}
|
||||
|
||||
writeFile(file: string, content: string | Buffer, skipIfExists?: boolean) {
|
||||
this._buffers.delete(file); // No need to flush the buffer since we'll overwrite anyway.
|
||||
this._appendOperation({ op: 'writeFile', file, content, skipIfExists });
|
||||
}
|
||||
|
||||
appendFile(file: string, text: string, flush?: boolean) {
|
||||
if (!this._buffers.has(file))
|
||||
this._buffers.set(file, []);
|
||||
this._buffers.get(file)!.push(text);
|
||||
if (flush)
|
||||
this._flushFile(file);
|
||||
}
|
||||
|
||||
private _flushFile(file: string) {
|
||||
const buffer = this._buffers.get(file);
|
||||
if (buffer === undefined)
|
||||
return;
|
||||
const content = buffer.join('');
|
||||
this._buffers.delete(file);
|
||||
this._appendOperation({ op: 'appendFile', file, content });
|
||||
}
|
||||
|
||||
copyFile(from: string, to: string) {
|
||||
this._flushFile(from);
|
||||
this._buffers.delete(to); // No need to flush the buffer since we'll overwrite anyway.
|
||||
this._appendOperation({ op: 'copyFile', from, to });
|
||||
}
|
||||
|
||||
async syncAndGetError() {
|
||||
for (const file of this._buffers.keys())
|
||||
this._flushFile(file);
|
||||
await this._operationsDone;
|
||||
return this._error;
|
||||
}
|
||||
|
||||
zip(entries: NameValue[], zipFileName: string) {
|
||||
for (const file of this._buffers.keys())
|
||||
this._flushFile(file);
|
||||
|
||||
// Chain the export operation against write operations,
|
||||
// so that files do not change during the export.
|
||||
this._appendOperation({ op: 'zip', entries, zipFileName });
|
||||
}
|
||||
|
||||
// This method serializes all writes to the trace.
|
||||
private _appendOperation(op: SerializedFSOperation): void {
|
||||
const last = this._operations[this._operations.length - 1];
|
||||
if (last?.op === 'appendFile' && op.op === 'appendFile' && last.file === op.file) {
|
||||
// Merge pending appendFile operations for performance.
|
||||
last.content += op.content;
|
||||
return;
|
||||
}
|
||||
|
||||
this._operations.push(op);
|
||||
if (this._operationsDone.isDone())
|
||||
this._performOperations();
|
||||
}
|
||||
|
||||
private async _performOperations() {
|
||||
this._operationsDone = new ManualPromise();
|
||||
while (this._operations.length) {
|
||||
const op = this._operations.shift()!;
|
||||
// Ignore all operations after the first error.
|
||||
if (this._error)
|
||||
continue;
|
||||
try {
|
||||
await this._performOperation(op);
|
||||
} catch (e) {
|
||||
this._error = e;
|
||||
}
|
||||
}
|
||||
this._operationsDone.resolve();
|
||||
}
|
||||
|
||||
private async _performOperation(op: SerializedFSOperation) {
|
||||
switch (op.op) {
|
||||
case 'mkdir': {
|
||||
await fs.promises.mkdir(op.dir, { recursive: true });
|
||||
return;
|
||||
}
|
||||
case 'writeFile': {
|
||||
// Note: 'wx' flag only writes when the file does not exist.
|
||||
// See https://nodejs.org/api/fs.html#file-system-flags.
|
||||
// This way tracing never have to write the same resource twice.
|
||||
if (op.skipIfExists)
|
||||
await fs.promises.writeFile(op.file, op.content, { flag: 'wx' }).catch(() => {});
|
||||
else
|
||||
await fs.promises.writeFile(op.file, op.content);
|
||||
return;
|
||||
}
|
||||
case 'copyFile': {
|
||||
await fs.promises.copyFile(op.from, op.to);
|
||||
return;
|
||||
}
|
||||
case 'appendFile': {
|
||||
await fs.promises.appendFile(op.file, op.content);
|
||||
return;
|
||||
}
|
||||
case 'zip': {
|
||||
const zipFile = new yazl.ZipFile();
|
||||
const result = new ManualPromise<void>();
|
||||
(zipFile as any as EventEmitter).on('error', error => result.reject(error));
|
||||
for (const entry of op.entries)
|
||||
zipFile.addFile(entry.value, entry.name);
|
||||
zipFile.end();
|
||||
zipFile.outputStream
|
||||
.pipe(fs.createWriteStream(op.zipFileName))
|
||||
.on('close', () => result.resolve())
|
||||
.on('error', error => result.reject(error));
|
||||
await result;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
175
packages/playwright-core/src/utils/harBackend.ts
Normal file
175
packages/playwright-core/src/utils/harBackend.ts
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
/**
|
||||
* 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 fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { createGuid } from './crypto';
|
||||
import { ZipFile } from './zipFile';
|
||||
|
||||
import type { HeadersArray } from '../common/types';
|
||||
import type * as har from '@trace/har';
|
||||
|
||||
const redirectStatus = [301, 302, 303, 307, 308];
|
||||
|
||||
export class HarBackend {
|
||||
readonly id = createGuid();
|
||||
private _harFile: har.HARFile;
|
||||
private _zipFile: ZipFile | null;
|
||||
private _baseDir: string | null;
|
||||
|
||||
constructor(harFile: har.HARFile, baseDir: string | null, zipFile: ZipFile | null) {
|
||||
this._harFile = harFile;
|
||||
this._baseDir = baseDir;
|
||||
this._zipFile = zipFile;
|
||||
}
|
||||
|
||||
async lookup(url: string, method: string, headers: HeadersArray, postData: Buffer | undefined, isNavigationRequest: boolean): Promise<{
|
||||
action: 'error' | 'redirect' | 'fulfill' | 'noentry',
|
||||
message?: string,
|
||||
redirectURL?: string,
|
||||
status?: number,
|
||||
headers?: HeadersArray,
|
||||
body?: Buffer }> {
|
||||
let entry;
|
||||
try {
|
||||
entry = await this._harFindResponse(url, method, headers, postData);
|
||||
} catch (e) {
|
||||
return { action: 'error', message: 'HAR error: ' + e.message };
|
||||
}
|
||||
|
||||
if (!entry)
|
||||
return { action: 'noentry' };
|
||||
|
||||
// If navigation is being redirected, restart it with the final url to ensure the document's url changes.
|
||||
if (entry.request.url !== url && isNavigationRequest)
|
||||
return { action: 'redirect', redirectURL: entry.request.url };
|
||||
|
||||
const response = entry.response;
|
||||
try {
|
||||
const buffer = await this._loadContent(response.content);
|
||||
return {
|
||||
action: 'fulfill',
|
||||
status: response.status,
|
||||
headers: response.headers,
|
||||
body: buffer,
|
||||
};
|
||||
} catch (e) {
|
||||
return { action: 'error', message: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadContent(content: { text?: string, encoding?: string, _file?: string }): Promise<Buffer> {
|
||||
const file = content._file;
|
||||
let buffer: Buffer;
|
||||
if (file) {
|
||||
if (this._zipFile)
|
||||
buffer = await this._zipFile.read(file);
|
||||
else
|
||||
buffer = await fs.promises.readFile(path.resolve(this._baseDir!, file));
|
||||
} else {
|
||||
buffer = Buffer.from(content.text || '', content.encoding === 'base64' ? 'base64' : 'utf-8');
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private async _harFindResponse(url: string, method: string, headers: HeadersArray, postData: Buffer | undefined): Promise<har.Entry | undefined> {
|
||||
const harLog = this._harFile.log;
|
||||
const visited = new Set<har.Entry>();
|
||||
while (true) {
|
||||
const entries: har.Entry[] = [];
|
||||
for (const candidate of harLog.entries) {
|
||||
if (candidate.request.url !== url || candidate.request.method !== method)
|
||||
continue;
|
||||
if (method === 'POST' && postData && candidate.request.postData) {
|
||||
const buffer = await this._loadContent(candidate.request.postData);
|
||||
if (!buffer.equals(postData)) {
|
||||
const boundary = multipartBoundary(headers);
|
||||
if (!boundary)
|
||||
continue;
|
||||
const candidataBoundary = multipartBoundary(candidate.request.headers);
|
||||
if (!candidataBoundary)
|
||||
continue;
|
||||
// Try to match multipart/form-data ignroing boundary as it changes between requests.
|
||||
if (postData.toString().replaceAll(boundary, '') !== buffer.toString().replaceAll(candidataBoundary, ''))
|
||||
continue;
|
||||
}
|
||||
}
|
||||
entries.push(candidate);
|
||||
}
|
||||
|
||||
if (!entries.length)
|
||||
return;
|
||||
|
||||
let entry = entries[0];
|
||||
|
||||
// Disambiguate using headers - then one with most matching headers wins.
|
||||
if (entries.length > 1) {
|
||||
const list: { candidate: har.Entry, matchingHeaders: number }[] = [];
|
||||
for (const candidate of entries) {
|
||||
const matchingHeaders = countMatchingHeaders(candidate.request.headers, headers);
|
||||
list.push({ candidate, matchingHeaders });
|
||||
}
|
||||
list.sort((a, b) => b.matchingHeaders - a.matchingHeaders);
|
||||
entry = list[0].candidate;
|
||||
}
|
||||
|
||||
if (visited.has(entry))
|
||||
throw new Error(`Found redirect cycle for ${url}`);
|
||||
|
||||
visited.add(entry);
|
||||
|
||||
// Follow redirects.
|
||||
const locationHeader = entry.response.headers.find(h => h.name.toLowerCase() === 'location');
|
||||
if (redirectStatus.includes(entry.response.status) && locationHeader) {
|
||||
const locationURL = new URL(locationHeader.value, url);
|
||||
url = locationURL.toString();
|
||||
if ((entry.response.status === 301 || entry.response.status === 302) && method === 'POST' ||
|
||||
entry.response.status === 303 && !['GET', 'HEAD'].includes(method)) {
|
||||
// HTTP-redirect fetch step 13 (https://fetch.spec.whatwg.org/#http-redirect-fetch)
|
||||
method = 'GET';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._zipFile?.close();
|
||||
}
|
||||
}
|
||||
|
||||
function countMatchingHeaders(harHeaders: har.Header[], headers: HeadersArray): number {
|
||||
const set = new Set(headers.map(h => h.name.toLowerCase() + ':' + h.value));
|
||||
let matches = 0;
|
||||
for (const h of harHeaders) {
|
||||
if (set.has(h.name.toLowerCase() + ':' + h.value))
|
||||
++matches;
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
function multipartBoundary(headers: HeadersArray) {
|
||||
const contentType = headers.find(h => h.name.toLowerCase() === 'content-type');
|
||||
if (!contentType?.value.includes('multipart/form-data'))
|
||||
return undefined;
|
||||
const boundary = contentType.value.match(/boundary=(\S+)/);
|
||||
if (boundary)
|
||||
return boundary[1];
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -33,7 +33,6 @@ export * from './isomorphic/stringUtils';
|
|||
export * from './isomorphic/urlMatch';
|
||||
export * from './multimap';
|
||||
export * from './network';
|
||||
export * from './processLauncher';
|
||||
export * from './profiler';
|
||||
export * from './rtti';
|
||||
export * from './semaphore';
|
||||
|
|
|
|||
248
packages/playwright-core/src/utils/localUtils.ts
Normal file
248
packages/playwright-core/src/utils/localUtils.ts
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
/**
|
||||
* 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 fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
import { removeFolders } from './fileUtils';
|
||||
import { HarBackend } from './harBackend';
|
||||
import { ManualPromise } from './manualPromise';
|
||||
import { fetchData } from './network';
|
||||
import { getUserAgent } from './userAgent';
|
||||
import { ZipFile } from './zipFile';
|
||||
import { yauzl, yazl } from '../zipBundle';
|
||||
|
||||
import { serializeClientSideCallMetadata } from '.';
|
||||
import { assert, calculateSha1 } from '.';
|
||||
|
||||
import type { HTTPRequestParams } from './network';
|
||||
import type { Platform } from './platform';
|
||||
import type { Progress } from '../common/progress';
|
||||
import type * as channels from '@protocol/channels';
|
||||
import type * as har from '@trace/har';
|
||||
import type EventEmitter from 'events';
|
||||
import type http from 'http';
|
||||
|
||||
|
||||
export type StackSession = {
|
||||
file: string;
|
||||
writer: Promise<void>;
|
||||
tmpDir: string | undefined;
|
||||
callStacks: channels.ClientSideCallMetadata[];
|
||||
};
|
||||
|
||||
export async function zip(platform: Platform, stackSessions: Map<string, StackSession>, params: channels.LocalUtilsZipParams): Promise<void> {
|
||||
const promise = new ManualPromise<void>();
|
||||
const zipFile = new yazl.ZipFile();
|
||||
(zipFile as any as EventEmitter).on('error', error => promise.reject(error));
|
||||
|
||||
const addFile = (file: string, name: string) => {
|
||||
try {
|
||||
if (fs.statSync(file).isFile())
|
||||
zipFile.addFile(file, name);
|
||||
} catch (e) {
|
||||
}
|
||||
};
|
||||
|
||||
for (const entry of params.entries)
|
||||
addFile(entry.value, entry.name);
|
||||
|
||||
// Add stacks and the sources.
|
||||
const stackSession = params.stacksId ? stackSessions.get(params.stacksId) : undefined;
|
||||
if (stackSession?.callStacks.length) {
|
||||
await stackSession.writer;
|
||||
if (process.env.PW_LIVE_TRACE_STACKS) {
|
||||
zipFile.addFile(stackSession.file, 'trace.stacks');
|
||||
} else {
|
||||
const buffer = Buffer.from(JSON.stringify(serializeClientSideCallMetadata(stackSession.callStacks)));
|
||||
zipFile.addBuffer(buffer, 'trace.stacks');
|
||||
}
|
||||
}
|
||||
|
||||
// Collect sources from stacks.
|
||||
if (params.includeSources) {
|
||||
const sourceFiles = new Set<string>();
|
||||
for (const { stack } of stackSession?.callStacks || []) {
|
||||
if (!stack)
|
||||
continue;
|
||||
for (const { file } of stack)
|
||||
sourceFiles.add(file);
|
||||
}
|
||||
for (const sourceFile of sourceFiles)
|
||||
addFile(sourceFile, 'resources/src@' + calculateSha1(sourceFile) + '.txt');
|
||||
}
|
||||
|
||||
if (params.mode === 'write') {
|
||||
// New file, just compress the entries.
|
||||
await fs.promises.mkdir(path.dirname(params.zipFile), { recursive: true });
|
||||
zipFile.end(undefined, () => {
|
||||
zipFile.outputStream.pipe(fs.createWriteStream(params.zipFile))
|
||||
.on('close', () => promise.resolve())
|
||||
.on('error', error => promise.reject(error));
|
||||
});
|
||||
await promise;
|
||||
await deleteStackSession(platform, stackSessions, params.stacksId);
|
||||
return;
|
||||
}
|
||||
|
||||
// File already exists. Repack and add new entries.
|
||||
const tempFile = params.zipFile + '.tmp';
|
||||
await fs.promises.rename(params.zipFile, tempFile);
|
||||
|
||||
yauzl.open(tempFile, (err, inZipFile) => {
|
||||
if (err) {
|
||||
promise.reject(err);
|
||||
return;
|
||||
}
|
||||
assert(inZipFile);
|
||||
let pendingEntries = inZipFile.entryCount;
|
||||
inZipFile.on('entry', entry => {
|
||||
inZipFile.openReadStream(entry, (err, readStream) => {
|
||||
if (err) {
|
||||
promise.reject(err);
|
||||
return;
|
||||
}
|
||||
zipFile.addReadStream(readStream!, entry.fileName);
|
||||
if (--pendingEntries === 0) {
|
||||
zipFile.end(undefined, () => {
|
||||
zipFile.outputStream.pipe(fs.createWriteStream(params.zipFile)).on('close', () => {
|
||||
fs.promises.unlink(tempFile).then(() => {
|
||||
promise.resolve();
|
||||
}).catch(error => promise.reject(error));
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
await promise;
|
||||
await deleteStackSession(platform, stackSessions, params.stacksId);
|
||||
}
|
||||
|
||||
async function deleteStackSession(platform: Platform, stackSessions: Map<string, StackSession>, stacksId?: string) {
|
||||
const session = stacksId ? stackSessions.get(stacksId) : undefined;
|
||||
if (!session)
|
||||
return;
|
||||
await session.writer;
|
||||
if (session.tmpDir)
|
||||
await removeFolders(platform, [session.tmpDir]);
|
||||
stackSessions.delete(stacksId!);
|
||||
}
|
||||
|
||||
export async function harOpen(harBackends: Map<string, HarBackend>, params: channels.LocalUtilsHarOpenParams): Promise<channels.LocalUtilsHarOpenResult> {
|
||||
let harBackend: HarBackend;
|
||||
if (params.file.endsWith('.zip')) {
|
||||
const zipFile = new ZipFile(params.file);
|
||||
const entryNames = await zipFile.entries();
|
||||
const harEntryName = entryNames.find(e => e.endsWith('.har'));
|
||||
if (!harEntryName)
|
||||
return { error: 'Specified archive does not have a .har file' };
|
||||
const har = await zipFile.read(harEntryName);
|
||||
const harFile = JSON.parse(har.toString()) as har.HARFile;
|
||||
harBackend = new HarBackend(harFile, null, zipFile);
|
||||
} else {
|
||||
const harFile = JSON.parse(await fs.promises.readFile(params.file, 'utf-8')) as har.HARFile;
|
||||
harBackend = new HarBackend(harFile, path.dirname(params.file), null);
|
||||
}
|
||||
harBackends.set(harBackend.id, harBackend);
|
||||
return { harId: harBackend.id };
|
||||
}
|
||||
|
||||
export async function harLookup(harBackends: Map<string, HarBackend>, params: channels.LocalUtilsHarLookupParams): Promise<channels.LocalUtilsHarLookupResult> {
|
||||
const harBackend = harBackends.get(params.harId);
|
||||
if (!harBackend)
|
||||
return { action: 'error', message: `Internal error: har was not opened` };
|
||||
return await harBackend.lookup(params.url, params.method, params.headers, params.postData, params.isNavigationRequest);
|
||||
}
|
||||
|
||||
export async function harClose(harBackends: Map<string, HarBackend>, params: channels.LocalUtilsHarCloseParams): Promise<void> {
|
||||
const harBackend = harBackends.get(params.harId);
|
||||
if (harBackend) {
|
||||
harBackends.delete(harBackend.id);
|
||||
harBackend.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export async function harUnzip(params: channels.LocalUtilsHarUnzipParams): Promise<void> {
|
||||
const dir = path.dirname(params.zipFile);
|
||||
const zipFile = new ZipFile(params.zipFile);
|
||||
for (const entry of await zipFile.entries()) {
|
||||
const buffer = await zipFile.read(entry);
|
||||
if (entry === 'har.har')
|
||||
await fs.promises.writeFile(params.harFile, buffer);
|
||||
else
|
||||
await fs.promises.writeFile(path.join(dir, entry), buffer);
|
||||
}
|
||||
zipFile.close();
|
||||
await fs.promises.unlink(params.zipFile);
|
||||
}
|
||||
|
||||
export async function tracingStarted(stackSessions: Map<string, StackSession>, params: channels.LocalUtilsTracingStartedParams): Promise<channels.LocalUtilsTracingStartedResult> {
|
||||
let tmpDir = undefined;
|
||||
if (!params.tracesDir)
|
||||
tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'playwright-tracing-'));
|
||||
const traceStacksFile = path.join(params.tracesDir || tmpDir!, params.traceName + '.stacks');
|
||||
stackSessions.set(traceStacksFile, { callStacks: [], file: traceStacksFile, writer: Promise.resolve(), tmpDir });
|
||||
return { stacksId: traceStacksFile };
|
||||
}
|
||||
|
||||
export async function traceDiscarded(platform: Platform, stackSessions: Map<string, StackSession>, params: channels.LocalUtilsTraceDiscardedParams): Promise<void> {
|
||||
await deleteStackSession(platform, stackSessions, params.stacksId);
|
||||
}
|
||||
|
||||
export async function addStackToTracingNoReply(stackSessions: Map<string, StackSession>, params: channels.LocalUtilsAddStackToTracingNoReplyParams): Promise<void> {
|
||||
for (const session of stackSessions.values()) {
|
||||
session.callStacks.push(params.callData);
|
||||
if (process.env.PW_LIVE_TRACE_STACKS) {
|
||||
session.writer = session.writer.then(() => {
|
||||
const buffer = Buffer.from(JSON.stringify(serializeClientSideCallMetadata(session.callStacks)));
|
||||
return fs.promises.writeFile(session.file, buffer);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function urlToWSEndpoint(progress: Progress | undefined, endpointURL: string): Promise<string> {
|
||||
if (endpointURL.startsWith('ws'))
|
||||
return endpointURL;
|
||||
|
||||
progress?.log(`<ws preparing> retrieving websocket url from ${endpointURL}`);
|
||||
const fetchUrl = new URL(endpointURL);
|
||||
if (!fetchUrl.pathname.endsWith('/'))
|
||||
fetchUrl.pathname += '/';
|
||||
fetchUrl.pathname += 'json';
|
||||
const json = await fetchData({
|
||||
url: fetchUrl.toString(),
|
||||
method: 'GET',
|
||||
timeout: progress?.timeUntilDeadline() ?? 30_000,
|
||||
headers: { 'User-Agent': getUserAgent() },
|
||||
}, async (params: HTTPRequestParams, response: http.IncomingMessage) => {
|
||||
return new Error(`Unexpected status ${response.statusCode} when connecting to ${fetchUrl.toString()}.\n` +
|
||||
`This does not look like a Playwright server, try connecting via ws://.`);
|
||||
});
|
||||
progress?.throwIfAborted();
|
||||
|
||||
const wsUrl = new URL(endpointURL);
|
||||
let wsEndpointPath = JSON.parse(json).wsEndpointPath;
|
||||
if (wsEndpointPath.startsWith('/'))
|
||||
wsEndpointPath = wsEndpointPath.substring(1);
|
||||
if (!wsUrl.pathname.endsWith('/'))
|
||||
wsUrl.pathname += '/';
|
||||
wsUrl.pathname += wsEndpointPath;
|
||||
wsUrl.protocol = wsUrl.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
return wsUrl.toString();
|
||||
}
|
||||
49
packages/playwright-core/src/utils/platform.ts
Normal file
49
packages/playwright-core/src/utils/platform.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* 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 fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as util from 'util';
|
||||
|
||||
export type Platform = {
|
||||
fs: () => typeof fs;
|
||||
path: () => typeof path;
|
||||
inspectCustom: symbol | undefined;
|
||||
ws?: (url: string) => WebSocket;
|
||||
};
|
||||
|
||||
export const emptyPlatform: Platform = {
|
||||
fs: () => {
|
||||
throw new Error('File system is not available');
|
||||
},
|
||||
|
||||
path: () => {
|
||||
throw new Error('Path module is not available');
|
||||
},
|
||||
|
||||
inspectCustom: undefined,
|
||||
};
|
||||
|
||||
export const nodePlatform: Platform = {
|
||||
fs: () => fs,
|
||||
path: () => path,
|
||||
inspectCustom: util.inspect.custom,
|
||||
};
|
||||
|
||||
export const webPlatform: Platform = {
|
||||
...emptyPlatform,
|
||||
ws: (url: string) => new WebSocket(url),
|
||||
};
|
||||
222
packages/playwright-core/types/protocol.d.ts
vendored
222
packages/playwright-core/types/protocol.d.ts
vendored
|
|
@ -952,7 +952,7 @@ Should be updated alongside RequestIdTokenStatus in
|
|||
third_party/blink/public/mojom/devtools/inspector_issue.mojom to include
|
||||
all cases except for success.
|
||||
*/
|
||||
export type FederatedAuthRequestIssueReason = "ShouldEmbargo"|"TooManyRequests"|"WellKnownHttpNotFound"|"WellKnownNoResponse"|"WellKnownInvalidResponse"|"WellKnownListEmpty"|"WellKnownInvalidContentType"|"ConfigNotInWellKnown"|"WellKnownTooBig"|"ConfigHttpNotFound"|"ConfigNoResponse"|"ConfigInvalidResponse"|"ConfigInvalidContentType"|"ClientMetadataHttpNotFound"|"ClientMetadataNoResponse"|"ClientMetadataInvalidResponse"|"ClientMetadataInvalidContentType"|"IdpNotPotentiallyTrustworthy"|"DisabledInSettings"|"DisabledInFlags"|"ErrorFetchingSignin"|"InvalidSigninResponse"|"AccountsHttpNotFound"|"AccountsNoResponse"|"AccountsInvalidResponse"|"AccountsListEmpty"|"AccountsInvalidContentType"|"IdTokenHttpNotFound"|"IdTokenNoResponse"|"IdTokenInvalidResponse"|"IdTokenIdpErrorResponse"|"IdTokenCrossSiteIdpErrorResponse"|"IdTokenInvalidRequest"|"IdTokenInvalidContentType"|"ErrorIdToken"|"Canceled"|"RpPageNotVisible"|"SilentMediationFailure"|"ThirdPartyCookiesBlocked"|"NotSignedInWithIdp"|"MissingTransientUserActivation"|"ReplacedByActiveMode"|"InvalidFieldsSpecified"|"RelyingPartyOriginIsOpaque"|"TypeNotMatching";
|
||||
export type FederatedAuthRequestIssueReason = "ShouldEmbargo"|"TooManyRequests"|"WellKnownHttpNotFound"|"WellKnownNoResponse"|"WellKnownInvalidResponse"|"WellKnownListEmpty"|"WellKnownInvalidContentType"|"ConfigNotInWellKnown"|"WellKnownTooBig"|"ConfigHttpNotFound"|"ConfigNoResponse"|"ConfigInvalidResponse"|"ConfigInvalidContentType"|"ClientMetadataHttpNotFound"|"ClientMetadataNoResponse"|"ClientMetadataInvalidResponse"|"ClientMetadataInvalidContentType"|"IdpNotPotentiallyTrustworthy"|"DisabledInSettings"|"DisabledInFlags"|"ErrorFetchingSignin"|"InvalidSigninResponse"|"AccountsHttpNotFound"|"AccountsNoResponse"|"AccountsInvalidResponse"|"AccountsListEmpty"|"AccountsInvalidContentType"|"IdTokenHttpNotFound"|"IdTokenNoResponse"|"IdTokenInvalidResponse"|"IdTokenIdpErrorResponse"|"IdTokenCrossSiteIdpErrorResponse"|"IdTokenInvalidRequest"|"IdTokenInvalidContentType"|"ErrorIdToken"|"Canceled"|"RpPageNotVisible"|"SilentMediationFailure"|"ThirdPartyCookiesBlocked"|"NotSignedInWithIdp"|"MissingTransientUserActivation"|"ReplacedByActiveMode"|"InvalidFieldsSpecified"|"RelyingPartyOriginIsOpaque"|"TypeNotMatching"|"UiDismissedNoEmbargo";
|
||||
export interface FederatedAuthUserInfoRequestIssueDetails {
|
||||
federatedAuthUserInfoRequestIssueReason: FederatedAuthUserInfoRequestIssueReason;
|
||||
}
|
||||
|
|
@ -983,7 +983,7 @@ features, encourage the use of new ones, and provide general guidance.
|
|||
}
|
||||
export type SelectElementAccessibilityIssueReason = "DisallowedSelectChild"|"DisallowedOptGroupChild"|"NonPhrasingContentOptionChild"|"InteractiveContentOptionChild"|"InteractiveContentLegendChild";
|
||||
/**
|
||||
* This isue warns about errors in the select element content model.
|
||||
* This issue warns about errors in the select element content model.
|
||||
*/
|
||||
export interface SelectElementAccessibilityIssueDetails {
|
||||
nodeId: DOM.BackendNodeId;
|
||||
|
|
@ -1187,6 +1187,19 @@ flag is set.
|
|||
*/
|
||||
id: string;
|
||||
}
|
||||
/**
|
||||
* Uninstalls an unpacked extension (others not supported) from the profile.
|
||||
Available if the client is connected using the --remote-debugging-pipe flag
|
||||
and the --enable-unsafe-extension-debugging.
|
||||
*/
|
||||
export type uninstallParameters = {
|
||||
/**
|
||||
* Extension id.
|
||||
*/
|
||||
id: string;
|
||||
}
|
||||
export type uninstallReturnValue = {
|
||||
}
|
||||
/**
|
||||
* Gets data from extension storage in the given `storageArea`. If `keys` is
|
||||
specified, these are used to filter the result.
|
||||
|
|
@ -2913,6 +2926,13 @@ incorrect results if the declaration contains a var() for example.
|
|||
* Identifier of the frame where "via-inspector" stylesheet should be created.
|
||||
*/
|
||||
frameId: Page.FrameId;
|
||||
/**
|
||||
* If true, creates a new stylesheet for every call. If false,
|
||||
returns a stylesheet previously created by a call with force=false
|
||||
for the frame's document if it exists or creates a new stylesheet
|
||||
(default: false).
|
||||
*/
|
||||
force?: boolean;
|
||||
}
|
||||
export type createStyleSheetReturnValue = {
|
||||
/**
|
||||
|
|
@ -5974,81 +5994,6 @@ The final text color opacity is computed based on the opacity of all overlapping
|
|||
}
|
||||
}
|
||||
|
||||
export module Database {
|
||||
/**
|
||||
* Unique identifier of Database object.
|
||||
*/
|
||||
export type DatabaseId = string;
|
||||
/**
|
||||
* Database object.
|
||||
*/
|
||||
export interface Database {
|
||||
/**
|
||||
* Database ID.
|
||||
*/
|
||||
id: DatabaseId;
|
||||
/**
|
||||
* Database domain.
|
||||
*/
|
||||
domain: string;
|
||||
/**
|
||||
* Database name.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Database version.
|
||||
*/
|
||||
version: string;
|
||||
}
|
||||
/**
|
||||
* Database error.
|
||||
*/
|
||||
export interface Error {
|
||||
/**
|
||||
* Error message.
|
||||
*/
|
||||
message: string;
|
||||
/**
|
||||
* Error code.
|
||||
*/
|
||||
code: number;
|
||||
}
|
||||
|
||||
export type addDatabasePayload = {
|
||||
database: Database;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables database tracking, prevents database events from being sent to the client.
|
||||
*/
|
||||
export type disableParameters = {
|
||||
}
|
||||
export type disableReturnValue = {
|
||||
}
|
||||
/**
|
||||
* Enables database tracking, database events will now be delivered to the client.
|
||||
*/
|
||||
export type enableParameters = {
|
||||
}
|
||||
export type enableReturnValue = {
|
||||
}
|
||||
export type executeSQLParameters = {
|
||||
databaseId: DatabaseId;
|
||||
query: string;
|
||||
}
|
||||
export type executeSQLReturnValue = {
|
||||
columnNames?: string[];
|
||||
values?: any[];
|
||||
sqlError?: Error;
|
||||
}
|
||||
export type getDatabaseTableNamesParameters = {
|
||||
databaseId: DatabaseId;
|
||||
}
|
||||
export type getDatabaseTableNamesReturnValue = {
|
||||
tableNames: string[];
|
||||
}
|
||||
}
|
||||
|
||||
export module DeviceOrientation {
|
||||
|
||||
|
||||
|
|
@ -9126,7 +9071,7 @@ This is a temporary ability and it will be removed in the future.
|
|||
/**
|
||||
* Types of reasons why a cookie should have been blocked by 3PCD but is exempted for the request.
|
||||
*/
|
||||
export type CookieExemptionReason = "None"|"UserSetting"|"TPCDMetadata"|"TPCDDeprecationTrial"|"TopLevelTPCDDeprecationTrial"|"TPCDHeuristics"|"EnterprisePolicy"|"StorageAccess"|"TopLevelStorageAccess"|"Scheme";
|
||||
export type CookieExemptionReason = "None"|"UserSetting"|"TPCDMetadata"|"TPCDDeprecationTrial"|"TopLevelTPCDDeprecationTrial"|"TPCDHeuristics"|"EnterprisePolicy"|"StorageAccess"|"TopLevelStorageAccess"|"Scheme"|"SameSiteNoneCookiesInSandbox";
|
||||
/**
|
||||
* A cookie which was not stored from a response with the corresponding reason.
|
||||
*/
|
||||
|
|
@ -11702,7 +11647,7 @@ as an ad.
|
|||
* All Permissions Policy features. This enum should match the one defined
|
||||
in third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5.
|
||||
*/
|
||||
export type PermissionsPolicyFeature = "accelerometer"|"all-screens-capture"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"controlled-frame"|"cross-origin-isolated"|"deferred-fetch"|"deferred-fetch-minimal"|"digital-credentials-get"|"direct-sockets"|"direct-sockets-private"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"fenced-unpartitioned-storage-read"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"media-playback-while-not-visible"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"popins"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-app-installation"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking";
|
||||
export type PermissionsPolicyFeature = "accelerometer"|"all-screens-capture"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-high-entropy-values"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"controlled-frame"|"cross-origin-isolated"|"deferred-fetch"|"deferred-fetch-minimal"|"digital-credentials-get"|"direct-sockets"|"direct-sockets-private"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"fenced-unpartitioned-storage-read"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"media-playback-while-not-visible"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"popins"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-app-installation"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking";
|
||||
/**
|
||||
* Reason for a permissions policy feature to be disabled.
|
||||
*/
|
||||
|
|
@ -12431,6 +12376,33 @@ subtree is actually detached.
|
|||
frame: Frame;
|
||||
}
|
||||
export type frameResizedPayload = void;
|
||||
/**
|
||||
* Fired when a navigation starts. This event is fired for both
|
||||
renderer-initiated and browser-initiated navigations. For renderer-initiated
|
||||
navigations, the event is fired after `frameRequestedNavigation`.
|
||||
Navigation may still be cancelled after the event is issued. Multiple events
|
||||
can be fired for a single navigation, for example, when a same-document
|
||||
navigation becomes a cross-document navigation (such as in the case of a
|
||||
frameset).
|
||||
*/
|
||||
export type frameStartedNavigatingPayload = {
|
||||
/**
|
||||
* ID of the frame that is being navigated.
|
||||
*/
|
||||
frameId: FrameId;
|
||||
/**
|
||||
* The URL the navigation started with. The final URL can be different.
|
||||
*/
|
||||
url: string;
|
||||
/**
|
||||
* Loader identifier. Even though it is present in case of same-document
|
||||
navigation, the previously committed loaderId would not change unless
|
||||
the navigation changes from a same-document to a cross-document
|
||||
navigation.
|
||||
*/
|
||||
loaderId: Network.LoaderId;
|
||||
navigationType: "reload"|"reloadBypassingCache"|"restore"|"restoreWithPost"|"historySameDocument"|"historyDifferentDocument"|"sameDocument"|"differentDocument";
|
||||
}
|
||||
/**
|
||||
* Fired when a renderer-initiated navigation is requested.
|
||||
Navigation may still be cancelled after the event is issued.
|
||||
|
|
@ -14281,7 +14253,7 @@ For cached script it is the last time the cache entry was validated.
|
|||
/**
|
||||
* Enum of possible storage types.
|
||||
*/
|
||||
export type StorageType = "appcache"|"cookies"|"file_systems"|"indexeddb"|"local_storage"|"shader_cache"|"websql"|"service_workers"|"cache_storage"|"interest_groups"|"shared_storage"|"storage_buckets"|"all"|"other";
|
||||
export type StorageType = "cookies"|"file_systems"|"indexeddb"|"local_storage"|"shader_cache"|"websql"|"service_workers"|"cache_storage"|"interest_groups"|"shared_storage"|"storage_buckets"|"all"|"other";
|
||||
/**
|
||||
* Usage for a storage type.
|
||||
*/
|
||||
|
|
@ -15193,6 +15165,28 @@ session. The effective Related Website Sets will not change during a browser ses
|
|||
export type getRelatedWebsiteSetsReturnValue = {
|
||||
sets: RelatedWebsiteSet[];
|
||||
}
|
||||
/**
|
||||
* Returns the list of URLs from a page and its embedded resources that match
|
||||
existing grace period URL pattern rules.
|
||||
https://developers.google.com/privacy-sandbox/cookies/temporary-exceptions/grace-period
|
||||
*/
|
||||
export type getAffectedUrlsForThirdPartyCookieMetadataParameters = {
|
||||
/**
|
||||
* The URL of the page currently being visited.
|
||||
*/
|
||||
firstPartyUrl: string;
|
||||
/**
|
||||
* The list of embedded resource URLs from the page.
|
||||
*/
|
||||
thirdPartyUrls: string[];
|
||||
}
|
||||
export type getAffectedUrlsForThirdPartyCookieMetadataReturnValue = {
|
||||
/**
|
||||
* Array of matching URLs. If there is a primary pattern match for the first-
|
||||
party URL, only the first-party URL is returned in the array.
|
||||
*/
|
||||
matchedUrls: string[];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -15485,6 +15479,10 @@ If filter is not specified, the one assumed is
|
|||
host: string;
|
||||
port: number;
|
||||
}
|
||||
/**
|
||||
* The state of the target window.
|
||||
*/
|
||||
export type WindowState = "normal"|"minimized"|"maximized"|"fullscreen";
|
||||
|
||||
/**
|
||||
* Issued when attached to target because of auto-attach or `attachToTarget` command.
|
||||
|
|
@ -15677,37 +15675,42 @@ Parts of the URL other than those constituting origin are ignored.
|
|||
*/
|
||||
url: string;
|
||||
/**
|
||||
* Frame left origin in DIP (headless chrome only).
|
||||
* Frame left origin in DIP (requires newWindow to be true or headless shell).
|
||||
*/
|
||||
left?: number;
|
||||
/**
|
||||
* Frame top origin in DIP (headless chrome only).
|
||||
* Frame top origin in DIP (requires newWindow to be true or headless shell).
|
||||
*/
|
||||
top?: number;
|
||||
/**
|
||||
* Frame width in DIP (headless chrome only).
|
||||
* Frame width in DIP (requires newWindow to be true or headless shell).
|
||||
*/
|
||||
width?: number;
|
||||
/**
|
||||
* Frame height in DIP (headless chrome only).
|
||||
* Frame height in DIP (requires newWindow to be true or headless shell).
|
||||
*/
|
||||
height?: number;
|
||||
/**
|
||||
* Frame window state (requires newWindow to be true or headless shell).
|
||||
Default is normal.
|
||||
*/
|
||||
windowState?: WindowState;
|
||||
/**
|
||||
* The browser context to create the page in.
|
||||
*/
|
||||
browserContextId?: Browser.BrowserContextID;
|
||||
/**
|
||||
* Whether BeginFrames for this target will be controlled via DevTools (headless chrome only,
|
||||
* Whether BeginFrames for this target will be controlled via DevTools (headless shell only,
|
||||
not supported on MacOS yet, false by default).
|
||||
*/
|
||||
enableBeginFrameControl?: boolean;
|
||||
/**
|
||||
* Whether to create a new Window or Tab (chrome-only, false by default).
|
||||
* Whether to create a new Window or Tab (false by default, not supported by headless shell).
|
||||
*/
|
||||
newWindow?: boolean;
|
||||
/**
|
||||
* Whether to create the target in background or foreground (chrome-only,
|
||||
false by default).
|
||||
* Whether to create the target in background or foreground (false by default, not supported
|
||||
by headless shell).
|
||||
*/
|
||||
background?: boolean;
|
||||
/**
|
||||
|
|
@ -18012,9 +18015,20 @@ variables as its properties.
|
|||
*/
|
||||
externalURL?: string;
|
||||
}
|
||||
export interface ResolvedBreakpoint {
|
||||
/**
|
||||
* Breakpoint unique identifier.
|
||||
*/
|
||||
breakpointId: BreakpointId;
|
||||
/**
|
||||
* Actual breakpoint location.
|
||||
*/
|
||||
location: Location;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fired when breakpoint is resolved to an actual script and location.
|
||||
Deprecated in favor of `resolvedBreakpoints` in the `scriptParsed` event.
|
||||
*/
|
||||
export type breakpointResolvedPayload = {
|
||||
/**
|
||||
|
|
@ -18225,6 +18239,12 @@ scripts upon enabling debugger.
|
|||
* The name the embedder supplied for this script.
|
||||
*/
|
||||
embedderName?: string;
|
||||
/**
|
||||
* The list of set breakpoints in this script if calls to `setBreakpointByUrl`
|
||||
matches this script's URL or hash. Clients that use this list can ignore the
|
||||
`breakpointResolved` event. They are equivalent.
|
||||
*/
|
||||
resolvedBreakpoints?: ResolvedBreakpoint[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -20150,13 +20170,21 @@ It is the total usage of the corresponding isolate not scoped to a particular Ru
|
|||
}
|
||||
export type getHeapUsageReturnValue = {
|
||||
/**
|
||||
* Used heap size in bytes.
|
||||
* Used JavaScript heap size in bytes.
|
||||
*/
|
||||
usedSize: number;
|
||||
/**
|
||||
* Allocated heap size in bytes.
|
||||
* Allocated JavaScript heap size in bytes.
|
||||
*/
|
||||
totalSize: number;
|
||||
/**
|
||||
* Used size in bytes in the embedder's garbage-collected heap.
|
||||
*/
|
||||
embedderHeapUsedSize: number;
|
||||
/**
|
||||
* Size in bytes of backing storage for array buffers and external strings.
|
||||
*/
|
||||
backingStorageSize: number;
|
||||
}
|
||||
/**
|
||||
* Returns properties of a given object. Object group of the result is inherited from the target
|
||||
|
|
@ -20472,7 +20500,6 @@ Error was thrown.
|
|||
"DOMStorage.domStorageItemRemoved": DOMStorage.domStorageItemRemovedPayload;
|
||||
"DOMStorage.domStorageItemUpdated": DOMStorage.domStorageItemUpdatedPayload;
|
||||
"DOMStorage.domStorageItemsCleared": DOMStorage.domStorageItemsClearedPayload;
|
||||
"Database.addDatabase": Database.addDatabasePayload;
|
||||
"Emulation.virtualTimeBudgetExpired": Emulation.virtualTimeBudgetExpiredPayload;
|
||||
"Input.dragIntercepted": Input.dragInterceptedPayload;
|
||||
"Inspector.detached": Inspector.detachedPayload;
|
||||
|
|
@ -20526,6 +20553,7 @@ Error was thrown.
|
|||
"Page.frameNavigated": Page.frameNavigatedPayload;
|
||||
"Page.documentOpened": Page.documentOpenedPayload;
|
||||
"Page.frameResized": Page.frameResizedPayload;
|
||||
"Page.frameStartedNavigating": Page.frameStartedNavigatingPayload;
|
||||
"Page.frameRequestedNavigation": Page.frameRequestedNavigationPayload;
|
||||
"Page.frameScheduledNavigation": Page.frameScheduledNavigationPayload;
|
||||
"Page.frameStartedLoading": Page.frameStartedLoadingPayload;
|
||||
|
|
@ -20656,6 +20684,7 @@ Error was thrown.
|
|||
"Audits.checkContrast": Audits.checkContrastParameters;
|
||||
"Audits.checkFormsIssues": Audits.checkFormsIssuesParameters;
|
||||
"Extensions.loadUnpacked": Extensions.loadUnpackedParameters;
|
||||
"Extensions.uninstall": Extensions.uninstallParameters;
|
||||
"Extensions.getStorageItems": Extensions.getStorageItemsParameters;
|
||||
"Extensions.removeStorageItems": Extensions.removeStorageItemsParameters;
|
||||
"Extensions.clearStorageItems": Extensions.clearStorageItemsParameters;
|
||||
|
|
@ -20808,10 +20837,6 @@ Error was thrown.
|
|||
"DOMStorage.getDOMStorageItems": DOMStorage.getDOMStorageItemsParameters;
|
||||
"DOMStorage.removeDOMStorageItem": DOMStorage.removeDOMStorageItemParameters;
|
||||
"DOMStorage.setDOMStorageItem": DOMStorage.setDOMStorageItemParameters;
|
||||
"Database.disable": Database.disableParameters;
|
||||
"Database.enable": Database.enableParameters;
|
||||
"Database.executeSQL": Database.executeSQLParameters;
|
||||
"Database.getDatabaseTableNames": Database.getDatabaseTableNamesParameters;
|
||||
"DeviceOrientation.clearDeviceOrientationOverride": DeviceOrientation.clearDeviceOrientationOverrideParameters;
|
||||
"DeviceOrientation.setDeviceOrientationOverride": DeviceOrientation.setDeviceOrientationOverrideParameters;
|
||||
"Emulation.canEmulate": Emulation.canEmulateParameters;
|
||||
|
|
@ -21088,6 +21113,7 @@ Error was thrown.
|
|||
"Storage.setAttributionReportingTracking": Storage.setAttributionReportingTrackingParameters;
|
||||
"Storage.sendPendingAttributionReports": Storage.sendPendingAttributionReportsParameters;
|
||||
"Storage.getRelatedWebsiteSets": Storage.getRelatedWebsiteSetsParameters;
|
||||
"Storage.getAffectedUrlsForThirdPartyCookieMetadata": Storage.getAffectedUrlsForThirdPartyCookieMetadataParameters;
|
||||
"SystemInfo.getInfo": SystemInfo.getInfoParameters;
|
||||
"SystemInfo.getFeatureState": SystemInfo.getFeatureStateParameters;
|
||||
"SystemInfo.getProcessInfo": SystemInfo.getProcessInfoParameters;
|
||||
|
|
@ -21273,6 +21299,7 @@ Error was thrown.
|
|||
"Audits.checkContrast": Audits.checkContrastReturnValue;
|
||||
"Audits.checkFormsIssues": Audits.checkFormsIssuesReturnValue;
|
||||
"Extensions.loadUnpacked": Extensions.loadUnpackedReturnValue;
|
||||
"Extensions.uninstall": Extensions.uninstallReturnValue;
|
||||
"Extensions.getStorageItems": Extensions.getStorageItemsReturnValue;
|
||||
"Extensions.removeStorageItems": Extensions.removeStorageItemsReturnValue;
|
||||
"Extensions.clearStorageItems": Extensions.clearStorageItemsReturnValue;
|
||||
|
|
@ -21425,10 +21452,6 @@ Error was thrown.
|
|||
"DOMStorage.getDOMStorageItems": DOMStorage.getDOMStorageItemsReturnValue;
|
||||
"DOMStorage.removeDOMStorageItem": DOMStorage.removeDOMStorageItemReturnValue;
|
||||
"DOMStorage.setDOMStorageItem": DOMStorage.setDOMStorageItemReturnValue;
|
||||
"Database.disable": Database.disableReturnValue;
|
||||
"Database.enable": Database.enableReturnValue;
|
||||
"Database.executeSQL": Database.executeSQLReturnValue;
|
||||
"Database.getDatabaseTableNames": Database.getDatabaseTableNamesReturnValue;
|
||||
"DeviceOrientation.clearDeviceOrientationOverride": DeviceOrientation.clearDeviceOrientationOverrideReturnValue;
|
||||
"DeviceOrientation.setDeviceOrientationOverride": DeviceOrientation.setDeviceOrientationOverrideReturnValue;
|
||||
"Emulation.canEmulate": Emulation.canEmulateReturnValue;
|
||||
|
|
@ -21705,6 +21728,7 @@ Error was thrown.
|
|||
"Storage.setAttributionReportingTracking": Storage.setAttributionReportingTrackingReturnValue;
|
||||
"Storage.sendPendingAttributionReports": Storage.sendPendingAttributionReportsReturnValue;
|
||||
"Storage.getRelatedWebsiteSets": Storage.getRelatedWebsiteSetsReturnValue;
|
||||
"Storage.getAffectedUrlsForThirdPartyCookieMetadata": Storage.getAffectedUrlsForThirdPartyCookieMetadataReturnValue;
|
||||
"SystemInfo.getInfo": SystemInfo.getInfoReturnValue;
|
||||
"SystemInfo.getFeatureState": SystemInfo.getFeatureStateReturnValue;
|
||||
"SystemInfo.getProcessInfo": SystemInfo.getProcessInfoReturnValue;
|
||||
|
|
|
|||
10
packages/playwright-core/types/types.d.ts
vendored
10
packages/playwright-core/types/types.d.ts
vendored
|
|
@ -9351,7 +9351,7 @@ export interface BrowserContext {
|
|||
*/
|
||||
keyEncoded?: Object;
|
||||
|
||||
value: Object;
|
||||
value?: Object;
|
||||
|
||||
/**
|
||||
* if `value` is not JSON-serializable, this contains an encoded version that preserves types.
|
||||
|
|
@ -10170,7 +10170,7 @@ export interface Browser {
|
|||
*/
|
||||
keyEncoded?: Object;
|
||||
|
||||
value: Object;
|
||||
value?: Object;
|
||||
|
||||
/**
|
||||
* if `value` is not JSON-serializable, this contains an encoded version that preserves types.
|
||||
|
|
@ -17758,7 +17758,7 @@ export interface APIRequest {
|
|||
*/
|
||||
keyEncoded?: Object;
|
||||
|
||||
value: Object;
|
||||
value?: Object;
|
||||
|
||||
/**
|
||||
* if `value` is not JSON-serializable, this contains an encoded version that preserves types.
|
||||
|
|
@ -18616,7 +18616,7 @@ export interface APIRequestContext {
|
|||
*/
|
||||
keyEncoded?: Object;
|
||||
|
||||
value: Object;
|
||||
value?: Object;
|
||||
|
||||
/**
|
||||
* if `value` is not JSON-serializable, this contains an encoded version that preserves types.
|
||||
|
|
@ -22512,7 +22512,7 @@ export interface BrowserContextOptions {
|
|||
*/
|
||||
keyEncoded?: Object;
|
||||
|
||||
value: Object;
|
||||
value?: Object;
|
||||
|
||||
/**
|
||||
* if `value` is not JSON-serializable, this contains an encoded version that preserves types.
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@
|
|||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { gracefullyProcessExitDoNotHang, isRegExp } from 'playwright-core/lib/utils';
|
||||
import { gracefullyProcessExitDoNotHang } from 'playwright-core/lib/server';
|
||||
import { isRegExp } from 'playwright-core/lib/utils';
|
||||
|
||||
import { requireOrImport, setSingleTSConfig, setTransformConfig } from '../transform/transform';
|
||||
import { errorWithFile, fileIsModule } from '../util';
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@
|
|||
|
||||
import * as path from 'path';
|
||||
|
||||
import { calculateSha1, toPosixPath } from 'playwright-core/lib/utils';
|
||||
import { toPosixPath } from 'playwright-core/lib/server';
|
||||
import { calculateSha1 } from 'playwright-core/lib/utils';
|
||||
|
||||
import { createFileMatcher } from '../util';
|
||||
|
||||
|
|
|
|||
|
|
@ -471,7 +471,7 @@ function normalizeScreenshotMode(screenshot: ScreenshotOption): ScreenshotMode {
|
|||
}
|
||||
|
||||
function attachConnectedHeaderIfNeeded(testInfo: TestInfo, browser: Browser | null) {
|
||||
const connectHeaders: { name: string, value: string }[] | undefined = (browser as any)?._connectHeaders;
|
||||
const connectHeaders: { name: string, value: string }[] | undefined = (browser as any)?._connection.headers;
|
||||
if (!connectHeaders)
|
||||
return;
|
||||
for (const header of connectHeaders) {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@
|
|||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { escapeTemplateString, isString, sanitizeForFilePath } from 'playwright-core/lib/utils';
|
||||
import { sanitizeForFilePath } from 'playwright-core/lib/server';
|
||||
import { escapeTemplateString, isString } from 'playwright-core/lib/utils';
|
||||
|
||||
import { kNoElementsFoundError, matcherHint } from './matcherHint';
|
||||
import { EXPECTED_COLOR } from '../common/expectBundle';
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@
|
|||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { compareBuffersOrStrings, getComparator, isString, sanitizeForFilePath } from 'playwright-core/lib/utils';
|
||||
import { sanitizeForFilePath } from 'playwright-core/lib/server';
|
||||
import { compareBuffersOrStrings, getComparator, isString } from 'playwright-core/lib/utils';
|
||||
import { colors } from 'playwright-core/lib/utilsBundle';
|
||||
import { mime } from 'playwright-core/lib/utilsBundle';
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@
|
|||
import * as net from 'net';
|
||||
import * as path from 'path';
|
||||
|
||||
import { isURLAvailable, launchProcess, monotonicTime, raceAgainstDeadline } from 'playwright-core/lib/utils';
|
||||
import { launchProcess } from 'playwright-core/lib/server';
|
||||
import { isURLAvailable, monotonicTime, raceAgainstDeadline } from 'playwright-core/lib/utils';
|
||||
import { colors, debug } from 'playwright-core/lib/utilsBundle';
|
||||
|
||||
import type { TestRunnerPlugin } from '.';
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ import * as fs from 'fs';
|
|||
import * as path from 'path';
|
||||
|
||||
import { program } from 'playwright-core/lib/cli/program';
|
||||
import { gracefullyProcessExitDoNotHang, startProfiling, stopProfiling } from 'playwright-core/lib/utils';
|
||||
import { gracefullyProcessExitDoNotHang } from 'playwright-core/lib/server';
|
||||
import { startProfiling, stopProfiling } from 'playwright-core/lib/utils';
|
||||
|
||||
import { builtInReporters, defaultReporter, defaultTimeout } from './common/config';
|
||||
import { loadConfigFromFileRestartIfNeeded, loadEmptyConfigForMergeReports, resolveConfigLocation } from './common/configLoader';
|
||||
|
|
@ -28,6 +29,7 @@ export { program } from 'playwright-core/lib/cli/program';
|
|||
import { prepareErrorStack } from './reporters/base';
|
||||
import { showHTMLReport } from './reporters/html';
|
||||
import { createMergedReport } from './reporters/merge';
|
||||
import { filterProjects } from './runner/projectUtils';
|
||||
import { Runner } from './runner/runner';
|
||||
import * as testServer from './runner/testServer';
|
||||
import { runWatchModeLoop } from './runner/watchMode';
|
||||
|
|
@ -160,6 +162,23 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
|
|||
await startProfiling();
|
||||
const cliOverrides = overridesFromOptions(opts);
|
||||
|
||||
const config = await loadConfigFromFileRestartIfNeeded(opts.config, cliOverrides, opts.deps === false);
|
||||
if (!config)
|
||||
return;
|
||||
|
||||
config.cliArgs = args;
|
||||
config.cliGrep = opts.grep as string | undefined;
|
||||
config.cliOnlyChanged = opts.onlyChanged === true ? 'HEAD' : opts.onlyChanged;
|
||||
config.cliGrepInvert = opts.grepInvert as string | undefined;
|
||||
config.cliListOnly = !!opts.list;
|
||||
config.cliProjectFilter = opts.project || undefined;
|
||||
config.cliPassWithNoTests = !!opts.passWithNoTests;
|
||||
config.cliFailOnFlakyTests = !!opts.failOnFlakyTests;
|
||||
config.cliLastFailed = !!opts.lastFailed;
|
||||
|
||||
// Evaluate project filters against config before starting execution. This enables a consistent error message across run modes
|
||||
filterProjects(config.projects, config.cliProjectFilter);
|
||||
|
||||
if (opts.ui || opts.uiHost || opts.uiPort) {
|
||||
if (opts.onlyChanged)
|
||||
throw new Error(`--only-changed is not supported in UI mode. If you'd like that to change, see https://github.com/microsoft/playwright/issues/15075 for more details.`);
|
||||
|
|
@ -201,20 +220,6 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
|
|||
return;
|
||||
}
|
||||
|
||||
const config = await loadConfigFromFileRestartIfNeeded(opts.config, cliOverrides, opts.deps === false);
|
||||
if (!config)
|
||||
return;
|
||||
|
||||
config.cliArgs = args;
|
||||
config.cliGrep = opts.grep as string | undefined;
|
||||
config.cliOnlyChanged = opts.onlyChanged === true ? 'HEAD' : opts.onlyChanged;
|
||||
config.cliGrepInvert = opts.grepInvert as string | undefined;
|
||||
config.cliListOnly = !!opts.list;
|
||||
config.cliProjectFilter = opts.project || undefined;
|
||||
config.cliPassWithNoTests = !!opts.passWithNoTests;
|
||||
config.cliFailOnFlakyTests = !!opts.failOnFlakyTests;
|
||||
config.cliLastFailed = !!opts.lastFailed;
|
||||
|
||||
const runner = new Runner(config);
|
||||
const status = await runner.runAllTests();
|
||||
await stopProfiling('runner');
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ import * as fs from 'fs';
|
|||
import * as path from 'path';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
import { ManualPromise, calculateSha1, createGuid, getUserAgent, removeFolders, sanitizeForFilePath } from 'playwright-core/lib/utils';
|
||||
import { removeFolders, sanitizeForFilePath } from 'playwright-core/lib/server';
|
||||
import { ManualPromise, calculateSha1, createGuid, getUserAgent } from 'playwright-core/lib/utils';
|
||||
import { mime } from 'playwright-core/lib/utilsBundle';
|
||||
import { yazl } from 'playwright-core/lib/zipBundle';
|
||||
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ import * as fs from 'fs';
|
|||
import * as path from 'path';
|
||||
import { Transform } from 'stream';
|
||||
|
||||
import { MultiMap, getPackageManagerExecCommand } from 'playwright-core/lib/utils';
|
||||
import { HttpServer, assert, calculateSha1, copyFileAndMakeWritable, gracefullyProcessExitDoNotHang, removeFolders, sanitizeForFilePath, toPosixPath } from 'playwright-core/lib/utils';
|
||||
import { copyFileAndMakeWritable, gracefullyProcessExitDoNotHang, removeFolders, sanitizeForFilePath, toPosixPath } from 'playwright-core/lib/server';
|
||||
import { HttpServer, MultiMap, assert, calculateSha1, getPackageManagerExecCommand } from 'playwright-core/lib/utils';
|
||||
import { colors, open } from 'playwright-core/lib/utilsBundle';
|
||||
import { mime } from 'playwright-core/lib/utilsBundle';
|
||||
import { yazl } from 'playwright-core/lib/zipBundle';
|
||||
|
|
@ -449,6 +449,17 @@ class HtmlBuilder {
|
|||
return a;
|
||||
}
|
||||
|
||||
if (a.name === 'pageSnapshot') {
|
||||
try {
|
||||
const body = fs.readFileSync(a.path!, { encoding: 'utf-8' });
|
||||
return {
|
||||
name: 'pageSnapshot',
|
||||
contentType: a.contentType,
|
||||
body,
|
||||
};
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (a.path) {
|
||||
let fileName = a.path;
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@
|
|||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { MultiMap, toPosixPath } from 'playwright-core/lib/utils';
|
||||
import { toPosixPath } from 'playwright-core/lib/server';
|
||||
import { MultiMap } from 'playwright-core/lib/utils';
|
||||
|
||||
import { formatError, nonTerminalScreen, prepareErrorStack, resolveOutputFile } from './base';
|
||||
import { getProjectId } from '../common/config';
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ import * as fs from 'fs';
|
|||
import * as path from 'path';
|
||||
import { promisify } from 'util';
|
||||
|
||||
import { monotonicTime, removeFolders } from 'playwright-core/lib/utils';
|
||||
import { removeFolders } from 'playwright-core/lib/server';
|
||||
import { monotonicTime } from 'playwright-core/lib/utils';
|
||||
import { debug } from 'playwright-core/lib/utilsBundle';
|
||||
|
||||
import { Dispatcher } from './dispatcher';
|
||||
|
|
@ -26,12 +27,12 @@ import { FailureTracker } from './failureTracker';
|
|||
import { collectProjectsAndTestFiles, createRootSuite, loadFileSuites, loadGlobalHook } from './loadUtils';
|
||||
import { buildDependentProjects, buildTeardownToSetupsMap, filterProjects } from './projectUtils';
|
||||
import { applySuggestedRebaselines, clearSuggestedRebaselines } from './rebase';
|
||||
import { Suite } from '../common/test';
|
||||
import { createTestGroups } from '../runner/testGroups';
|
||||
import { removeDirAndLogToConsole } from '../util';
|
||||
import { TaskRunner } from './taskRunner';
|
||||
import { detectChangedTestFiles } from './vcs';
|
||||
import { Suite } from '../common/test';
|
||||
import { createTestGroups } from '../runner/testGroups';
|
||||
import { cacheDir } from '../transform/compilationCache';
|
||||
import { removeDirAndLogToConsole } from '../util';
|
||||
|
||||
import type { TestGroup } from '../runner/testGroups';
|
||||
import type { Matcher } from '../util';
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@
|
|||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { installRootRedirect, openTraceInBrowser, openTraceViewerApp, registry, startTraceViewerServer } from 'playwright-core/lib/server';
|
||||
import { ManualPromise, gracefullyProcessExitDoNotHang, isUnderTest } from 'playwright-core/lib/utils';
|
||||
import { gracefullyProcessExitDoNotHang, installRootRedirect, openTraceInBrowser, openTraceViewerApp, registry, startTraceViewerServer } from 'playwright-core/lib/server';
|
||||
import { ManualPromise, isUnderTest } from 'playwright-core/lib/utils';
|
||||
import { open } from 'playwright-core/lib/utilsBundle';
|
||||
|
||||
import { createErrorCollectingReporter, createReporterForTestServer, createReporters } from './reporters';
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { removeFolders } from 'playwright-core/lib/utils';
|
||||
import { removeFolders } from 'playwright-core/lib/server';
|
||||
|
||||
import { ProcessHost } from './processHost';
|
||||
import { stdioChunkToParams } from '../common/ipc';
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ import * as path from 'path';
|
|||
import * as url from 'url';
|
||||
import util from 'util';
|
||||
|
||||
import { formatCallLog } from 'playwright-core/lib/utils';
|
||||
import { calculateSha1, isRegExp, isString, sanitizeForFilePath, stringifyStackFrames } from 'playwright-core/lib/utils';
|
||||
import { sanitizeForFilePath } from 'playwright-core/lib/server';
|
||||
import { calculateSha1, formatCallLog, isRegExp, isString, stringifyStackFrames } from 'playwright-core/lib/utils';
|
||||
import { debug, mime, minimatch, parseStackTraceLine } from 'playwright-core/lib/utilsBundle';
|
||||
|
||||
import type { Location } from './../types/testReporter';
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue