diff --git a/README.md b/README.md index b0e9902352..f236eafd1a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ๐ŸŽญ Playwright -[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-133.0.6943.35-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-134.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-18.2-blue.svg?logo=safari)](https://webkit.org/) [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord) +[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-134.0.6998.3-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-135.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-18.2-blue.svg?logo=safari)](https://webkit.org/) [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](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 133.0.6943.35 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Chromium 134.0.6998.3 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | WebKit 18.2 | :white_check_mark: | :white_check_mark: | :white_check_mark: | -| Firefox 134.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Firefox 135.0 | :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. diff --git a/docs/src/api/class-apirequest.md b/docs/src/api/class-apirequest.md index feea5b8378..f1465cb8c6 100644 --- a/docs/src/api/class-apirequest.md +++ b/docs/src/api/class-apirequest.md @@ -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 diff --git a/docs/src/api/class-apirequestcontext.md b/docs/src/api/class-apirequestcontext.md index 838625bcbf..165b9f3bd6 100644 --- a/docs/src/api/class-apirequestcontext.md +++ b/docs/src/api/class-apirequestcontext.md @@ -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. diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index 156a577504..f831bd4cae 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -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. diff --git a/docs/src/api/params.md b/docs/src/api/params.md index f2d8734336..920ba10943 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -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). diff --git a/docs/src/test-ui-mode-js.md b/docs/src/test-ui-mode-js.md index 41a00264d9..4aaeae92d6 100644 --- a/docs/src/test-ui-mode-js.md +++ b/docs/src/test-ui-mode-js.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. ![running tests in ui mode](https://github.com/microsoft/playwright/assets/13063165/6b87712f-64a5-4d73-a91d-6562b864712c) @@ -33,17 +34,15 @@ Filter tests by text or `@tag` or by passed, failed or skipped tests. You can al ![filtering tests in ui mode](https://github.com/microsoft/playwright/assets/13063165/6f05e589-036d-45d5-9078-38134e1261e4) - ## 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. ![timeline view in ui mode](https://github.com/microsoft/playwright/assets/13063165/811a9985-32aa-4a3e-9869-de32053cf468) - ## 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. ![use before and after actions in ui mode](https://github.com/microsoft/playwright/assets/13063165/7b22fab5-7346-4b98-8fdd-a78ed280647f) ## 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. ![showing source code of tests in ui mode](https://github.com/microsoft/playwright/assets/13063165/49b9fa2a-8a57-4044-acaa-0a2ea4784c5c) @@ -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. ![watch mode in ui mode](https://github.com/microsoft/playwright/assets/13063165/20d7d44c-b52d-43ff-8871-8b828671f3da) diff --git a/package-lock.json b/package-lock.json index 6b59e3ab0b..eb69ce2f36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/package.json b/package.json index 30578abf1b..8b54d54e6c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/html-reporter/src/copyToClipboard.tsx b/packages/html-reporter/src/copyToClipboard.tsx index 17b1dfbf95..8171cdd6c3 100644 --- a/packages/html-reporter/src/copyToClipboard.tsx +++ b/packages/html-reporter/src/copyToClipboard.tsx @@ -39,7 +39,7 @@ export const CopyToClipboard: React.FunctionComponent = ({ }); }, [value]); const iconElement = icon === 'check' ? icons.check() : icon === 'cross' ? icons.cross() : icons.copy(); - return ; + return ; }; type CopyToClipboardContainerProps = CopyToClipboardProps & { diff --git a/packages/html-reporter/src/metadataView.css b/packages/html-reporter/src/metadataView.css index 70d6ba78ab..0cbced5250 100644 --- a/packages/html-reporter/src/metadataView.css +++ b/packages/html-reporter/src/metadataView.css @@ -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; +} diff --git a/packages/html-reporter/src/metadataView.tsx b/packages/html-reporter/src/metadataView.tsx index 8cc66571ab..03a5ed06f4 100644 --- a/packages/html-reporter/src/metadataView.tsx +++ b/packages/html-reporter/src/metadataView.tsx @@ -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([]); + +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 {children}; +} + +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, { error: Error | null, errorInfo: React.ErrorInfo | null }> { @@ -57,12 +72,13 @@ class ErrorBoundary extends React.Component, { error } } -export const MetadataView: React.FC<{ metadataEntries: MetadataEntries }> = ({ metadataEntries }) => { - return ; +export const MetadataView = () => { + return ; }; -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 {entries.length > 0 &&
} } - {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
- {key} - {valueString && : {linkifyText(trimmedValue)}} -
; - })} +
+ {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 ( +
+ + {propertyName} + : {linkifyText(trimmedValue)} + +
+ ); + })} +
; }; 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
-
- - {info['revision.subject'] || ''} - -
-
{author}
-
on {shortTimestamp}
+ return
+
+
+ {info['revision.link'] ? ( + + {subject} + + ) : + {subject} + } +
+
+ {author} + on {shortTimestamp} {info['ci.link'] && ( <> ยท @@ -109,9 +138,10 @@ const GitCommitInfoView: React.FC<{ info: GitCommitInfo }> = ({ info }) => { )}
- {!!info['revision.link'] && - {info['revision.id']?.slice(0, 7) || 'unknown'} - } - {!info['revision.link'] && !!info['revision.id'] && {info['revision.id'].slice(0, 7)}} + {!!info['revision.link'] ? ( + + {info['revision.id']?.slice(0, 7) || 'unknown'} + + ) : !!info['revision.id'] && {info['revision.id'].slice(0, 7)}}
; }; diff --git a/packages/html-reporter/src/reportView.tsx b/packages/html-reporter/src/reportView.tsx index e48064201c..baf85f32a8 100644 --- a/packages/html-reporter/src/reportView.tsx +++ b/packages/html-reporter/src/reportView.tsx @@ -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
+ return
{report?.json() && } @@ -88,7 +89,7 @@ export const ReportView: React.FC<{ {!!report && }
-
; +
; }; const TestCaseViewLoader: React.FC<{ diff --git a/packages/html-reporter/src/testErrorView.css b/packages/html-reporter/src/testErrorView.css index e29ea2a18b..7cc9e88a79 100644 --- a/packages/html-reporter/src/testErrorView.css +++ b/packages/html-reporter/src/testErrorView.css @@ -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); +} + diff --git a/packages/html-reporter/src/testErrorView.tsx b/packages/html-reporter/src/testErrorView.tsx index ea402106a8..11f8e4b474 100644 --- a/packages/html-reporter/src/testErrorView.tsx +++ b/packages/html-reporter/src/testErrorView.tsx @@ -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 ( + +
+ +
+
+ ); +}; + +export const CodeSnippet = ({ code, children, testId }: React.PropsWithChildren<{ code: string; testId?: string; }>) => { + const html = React.useMemo(() => ansiErrorToHtml(code), [code]); + return ( +
+ {children} +
+
+ ); +}; + +const PromptButton: React.FC<{ error: string; - testId?: string; -}> = ({ error, testId }) => { - const html = React.useMemo(() => ansiErrorToHtml(error), [error]); - return
; + 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 ; }; export const TestScreenshotErrorView: React.FC<{ diff --git a/packages/html-reporter/src/testFileView.css b/packages/html-reporter/src/testFileView.css index 72858846bb..37bf428490 100644 --- a/packages/html-reporter/src/testFileView.css +++ b/packages/html-reporter/src/testFileView.css @@ -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); } \ No newline at end of file diff --git a/packages/html-reporter/src/testFilesView.tsx b/packages/html-reporter/src/testFilesView.tsx index bcb0696946..49e2233669 100644 --- a/packages/html-reporter/src/testFilesView.tsx +++ b/packages/html-reporter/src/testFilesView.tsx @@ -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 <>
- {metadataEntries.length > 0 &&
- {metadataVisible ? icons.downArrow() : icons.rightArrow()}Metadata -
} - {report.projectNames.length === 1 && !!report.projectNames[0] &&
Project: {report.projectNames[0]}
} - {filteredStats &&
Filtered: {filteredStats.total} {!!filteredStats.total && ('(' + msToString(filteredStats.duration) + ')')}
} +
+ {metadataEntries.length > 0 &&
+ {metadataVisible ? icons.downArrow() : icons.rightArrow()}Metadata +
} + {report.projectNames.length === 1 && !!report.projectNames[0] &&
Project: {report.projectNames[0]}
} + {filteredStats &&
Filtered: {filteredStats.total} {!!filteredStats.total && ('(' + msToString(filteredStats.duration) + ')')}
} +
{report ? new Date(report.startTime).toLocaleString() : ''}
Total time: {msToString(report.duration ?? 0)}
- {metadataVisible && } + {metadataVisible && } {!!report.errors.length && {report.errors.map((error, index) => )} } diff --git a/packages/html-reporter/src/testResultView.tsx b/packages/html-reporter/src/testResultView.tsx index 681f4b507a..3243b2bcb9 100644 --- a/packages/html-reporter/src/testResultView.tsx +++ b/packages/html-reporter/src/testResultView.tsx @@ -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 ; - return ; + return ; })} } {!!result.steps.length && @@ -182,7 +182,7 @@ const StepTreeItem: React.FC<{ {step.count > 1 && <> โœ• {step.count}} {step.location && โ€” {step.location.file}:{step.location.line}} } loadChildren={step.steps.length || step.snippet ? () => { - const snippet = step.snippet ? [] : []; + const snippet = step.snippet ? [] : []; const steps = step.steps.map((s, i) => ); return snippet.concat(steps); } : undefined} depth={depth}/>; diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 2a160902f1..0f0d5f643b 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -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", diff --git a/packages/playwright-core/src/DEPS.list b/packages/playwright-core/src/DEPS.list index 2f7995ab62..026473f98a 100644 --- a/packages/playwright-core/src/DEPS.list +++ b/packages/playwright-core/src/DEPS.list @@ -7,7 +7,11 @@ [inProcessFactory.ts] ** +[inprocess.ts] +utils/ + [outofprocess.ts] client/ protocol/ utils/ +common/ \ No newline at end of file diff --git a/packages/playwright-core/src/cli/DEPS.list b/packages/playwright-core/src/cli/DEPS.list index ab30158290..54eb6a5e49 100644 --- a/packages/playwright-core/src/cli/DEPS.list +++ b/packages/playwright-core/src/cli/DEPS.list @@ -4,6 +4,7 @@ ../common ../debug/injected ../generated/ +../server/ ../server/injected/ ../server/trace ../utils diff --git a/packages/playwright-core/src/cli/driver.ts b/packages/playwright-core/src/cli/driver.ts index 0e4cbb5816..cb7ade9d7a 100644 --- a/packages/playwright-core/src/cli/driver.ts +++ b/packages/playwright-core/src/cli/driver.ts @@ -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'; diff --git a/packages/playwright-core/src/cli/program.ts b/packages/playwright-core/src/cli/program.ts index 2c0a955207..38b3072f3a 100644 --- a/packages/playwright-core/src/cli/program.ts +++ b/packages/playwright-core/src/cli/program.ts @@ -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'; diff --git a/packages/playwright-core/src/cli/programWithTestStub.ts b/packages/playwright-core/src/cli/programWithTestStub.ts index 1c11c14ec2..08a306f2f0 100644 --- a/packages/playwright-core/src/cli/programWithTestStub.ts +++ b/packages/playwright-core/src/cli/programWithTestStub.ts @@ -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'; diff --git a/packages/playwright-core/src/client/android.ts b/packages/playwright-core/src/client/android.ts index 1f4b89ed8a..7f062143fc 100644 --- a/packages/playwright-core/src/client/android.ts +++ b/packages/playwright-core/src/client/android.ts @@ -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 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 i async screenshot(options: { path?: string } = {}): Promise { 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 i } async installApk(file: string | Buffer, options?: { args: string[] }): Promise { - 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 { - 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 { - 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 i } } -async function loadFile(file: string | Buffer): Promise { +async function loadFile(platform: Platform, file: string | Buffer): Promise { if (isString(file)) - return await fs.promises.readFile(file); + return await platform.fs().promises.readFile(file); return file; } diff --git a/packages/playwright-core/src/client/artifact.ts b/packages/playwright-core/src/client/artifact.ts index 1874ba0c1d..fc1e9bca12 100644 --- a/packages/playwright-core/src/client/artifact.ts +++ b/packages/playwright-core/src/client/artifact.ts @@ -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 { 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); }); diff --git a/packages/playwright-core/src/client/browser.ts b/packages/playwright-core/src/client/browser.ts index 444074ba54..9d2ca1fab0 100644 --- a/packages/playwright-core/src/client/browser.ts +++ b/packages/playwright-core/src/client/browser.ts @@ -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 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 implements ap async _innerNewContext(options: BrowserContextOptions = {}, forReuse: boolean): Promise { 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 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; diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index a8417d896b..d625135f73 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -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 implements api.BrowserContext { @@ -107,7 +108,7 @@ export class BrowserContext extends ChannelOwner 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 } async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any): Promise { - 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 async storageState(options: { path?: string, indexedDB?: boolean } = {}): Promise { 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 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 } } -async function prepareStorageState(options: BrowserContextOptions): Promise { +async function prepareStorageState(platform: Platform, options: BrowserContextOptions): Promise { 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 { +export async function prepareBrowserContextParams(platform: Platform, options: BrowserContextOptions): Promise { 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 { +export async function toClientCertificatesProtocol(platform: Platform, certs?: BrowserContextOptions['clientCertificates']): Promise { 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 => ({ diff --git a/packages/playwright-core/src/client/browserType.ts b/packages/playwright-core/src/client/browserType.ts index 36b9fe4d7e..59ce8c56d7 100644 --- a/packages/playwright-core/src/client/browserType.ts +++ b/packages/playwright-core/src/client/browserType.ts @@ -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 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 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 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`); } }); diff --git a/packages/playwright-core/src/client/channelOwner.ts b/packages/playwright-core/src/client/channelOwner.ts index 2624be7282..70a5c51777 100644 --- a/packages/playwright-core/src/client/channelOwner.ts +++ b/packages/playwright-core/src/client/channelOwner.ts @@ -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; _logger: Logger | undefined; + readonly _platform: Platform; readonly _instrumentation: ClientInstrumentation; private _eventToSubscriptionMapping: Map = new Map(); private _isInternalType = false; @@ -52,6 +54,7 @@ export abstract class ChannelOwner { +export async function evaluationScript(platform: Platform, fun: Function | string | { path?: string, content?: string }, arg?: any, addSourceUrl: boolean = true): Promise { 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; diff --git a/packages/playwright-core/src/client/connection.ts b/packages/playwright-core/src/client/connection.ts index 30eba37cf0..0c9ad7f66b 100644 --- a/packages/playwright-core/src/client/connection.ts +++ b/packages/playwright-core/src/client/connection.ts @@ -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 { @@ -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 })); diff --git a/packages/playwright-core/src/client/consoleMessage.ts b/packages/playwright-core/src/client/consoleMessage.ts index db2ed1a246..5ad37b305e 100644 --- a/packages/playwright-core/src/client/consoleMessage.ts +++ b/packages/playwright-core/src/client/consoleMessage.ts @@ -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(); } } diff --git a/packages/playwright-core/src/client/electron.ts b/packages/playwright-core/src/client/electron.ts index 15551ebce9..01d194e029 100644 --- a/packages/playwright-core/src/client/electron.ts +++ b/packages/playwright-core/src/client/electron.ts @@ -53,7 +53,7 @@ export class Electron extends ChannelOwner implements async launch(options: ElectronOptions = {}): Promise { 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 { 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([ [Events.ElectronApplication.Console, 'console'], ])); diff --git a/packages/playwright-core/src/client/elementHandle.ts b/packages/playwright-core/src/client/elementHandle.ts index 2883a69dfc..e39e4413e7 100644 --- a/packages/playwright-core/src/client/elementHandle.ts +++ b/packages/playwright-core/src/client/elementHandle.ts @@ -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 extends JSHandle 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 extends JSHandle implements return value === undefined ? null : value; } - async screenshot(options: Omit & { path?: string, mask?: Locator[] } = {}): Promise { + async screenshot(options: Omit & { path?: string, mask?: api.Locator[] } = {}): Promise { + 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 { +export async function convertInputFiles(platform: Platform, files: string | FilePayload | string[] | FilePayload[], context: BrowserContext): Promise { 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, diff --git a/packages/playwright-core/src/client/errors.ts b/packages/playwright-core/src/client/errors.ts index 4757a6d0f7..81f32080b7 100644 --- a/packages/playwright-core/src/client/errors.ts +++ b/packages/playwright-core/src/client/errors.ts @@ -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'; diff --git a/packages/playwright-core/src/client/eventEmitter.ts b/packages/playwright-core/src/client/eventEmitter.ts index b295f87eba..5fbe3372cb 100644 --- a/packages/playwright-core/src/client/eventEmitter.ts +++ b/packages/playwright-core/src/client/eventEmitter.ts @@ -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'; diff --git a/packages/playwright-core/src/client/fetch.ts b/packages/playwright-core/src/client/fetch.ts index fea6c1a17e..eba881a5ca 100644 --- a/packages/playwright-core/src/client/fetch.ts +++ b/packages/playwright-core/src/client/fetch.ts @@ -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 { 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 { +async function toFormField(platform: Platform, name: string, value: string | number | boolean | fs.ReadStream | FilePayload): Promise { + 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 { +async function readStreamToJson(platform: Platform, stream: fs.ReadStream): Promise { const buffer = await new Promise((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 implements api.Fr async addScriptTag(options: { url?: string, path?: string, content?: string, type?: string } = {}): Promise { 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 implements api.Fr async addStyleTag(options: { url?: string; path?: string; content?: string; } = {}): Promise { 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 implements api.Fr } async setInputFiles(selector: string, files: string | FilePayload | string[] | FilePayload[], options: channels.FrameSetInputFilesOptions = {}): Promise { - 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 }); } diff --git a/packages/playwright-core/src/client/harRouter.ts b/packages/playwright-core/src/client/harRouter.ts index 3fb2ce0e7b..33767e11a0 100644 --- a/packages/playwright-core/src/client/harRouter.ts +++ b/packages/playwright-core/src/client/harRouter.ts @@ -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 { - 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(() => {}); } } diff --git a/packages/playwright-core/src/client/localUtils.ts b/packages/playwright-core/src/client/localUtils.ts index eb8990abe9..8ab7421980 100644 --- a/packages/playwright-core/src/client/localUtils.ts +++ b/packages/playwright-core/src/client/localUtils.ts @@ -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 { readonly devices: Devices; + private _harBackends = new Map(); + private _stackSessions = new Map(); constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.LocalUtilsInitializer) { super(parent, type, guid, initializer); @@ -39,4 +45,134 @@ export class LocalUtils extends ChannelOwner { for (const { name, descriptor } of initializer.deviceDescriptors) this.devices[name] = descriptor; } + + async zip(params: channels.LocalUtilsZipParams): Promise { + return await localUtils.zip(this._platform, this._stackSessions, params); + } + + async harOpen(params: channels.LocalUtilsHarOpenParams): Promise { + return await localUtils.harOpen(this._harBackends, params); + } + + async harLookup(params: channels.LocalUtilsHarLookupParams): Promise { + return await localUtils.harLookup(this._harBackends, params); + } + + async harClose(params: channels.LocalUtilsHarCloseParams): Promise { + return await localUtils.harClose(this._harBackends, params); + } + + async harUnzip(params: channels.LocalUtilsHarUnzipParams): Promise { + return await localUtils.harUnzip(params); + } + + async tracingStarted(params: channels.LocalUtilsTracingStartedParams): Promise { + return await localUtils.tracingStarted(this._stackSessions, params); + } + + async traceDiscarded(params: channels.LocalUtilsTraceDiscardedParams): Promise { + return await localUtils.traceDiscarded(this._platform, this._stackSessions, params); + } + + async addStackToTracingNoReply(params: channels.LocalUtilsAddStackToTracingNoReplyParams): Promise { + return await localUtils.addStackToTracingNoReply(this._stackSessions, params); + } + + async connect(params: channels.LocalUtilsConnectParams): Promise { + 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; + send(message: any): Promise; + onMessage(callback: (message: object) => void): void; + onClose(callback: (reason?: string) => void): void; + close(): Promise; +} + +class JsonPipeTransport implements Transport { + private _pipe: channels.JsonPipeChannel | undefined; + private _owner: ChannelOwner; + + constructor(owner: ChannelOwner) { + 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(); + } } diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts index af84294d2e..04cfa5fadf 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -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(task: (handle: ElementHandle, timeout?: number) => Promise, timeout?: number): Promise { @@ -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 & { path?: string, mask?: Locator[] } = {}): Promise { - return await this._withElement((h, timeout) => h.screenshot({ ...options, timeout }), options.timeout); + async screenshot(options: Omit & { path?: string, mask?: api.Locator[] } = {}): Promise { + 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 { @@ -370,7 +374,7 @@ export class Locator implements api.Locator { return result; } - [util.inspect.custom]() { + private _inspect() { return this.toString(); } diff --git a/packages/playwright-core/src/client/network.ts b/packages/playwright-core/src/client/network.ts index 8667447e8b..ae7c7bb251 100644 --- a/packages/playwright-core/src/client/network.ts +++ b/packages/playwright-core/src/client/network.ts @@ -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 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; diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index 12983f7b62..d4bb32ffbd 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -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 & { @@ -512,7 +515,7 @@ export class Page extends ChannelOwner 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 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 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; } diff --git a/packages/playwright-core/src/client/selectors.ts b/packages/playwright-core/src/client/selectors.ts index 4f8ee784a8..2a1097f7ec 100644 --- a/packages/playwright-core/src/client/selectors.ts +++ b/packages/playwright-core/src/client/selectors.ts @@ -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 { - 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); diff --git a/packages/playwright-core/src/client/tracing.ts b/packages/playwright-core/src/client/tracing.ts index a75f026625..61124107fa 100644 --- a/packages/playwright-core/src/client/tracing.ts +++ b/packages/playwright-core/src/client/tracing.ts @@ -69,7 +69,7 @@ export class Tracing extends ChannelOwner 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 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 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 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 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() { diff --git a/packages/playwright-core/src/client/types.ts b/packages/playwright-core/src/client/types.ts index 5d232e9607..2cad775712 100644 --- a/packages/playwright-core/src/client/types.ts +++ b/packages/playwright-core/src/client/types.ts @@ -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[], diff --git a/packages/playwright-core/src/client/video.ts b/packages/playwright-core/src/client/video.ts index f15d75bade..993647dc72 100644 --- a/packages/playwright-core/src/client/video.ts +++ b/packages/playwright-core/src/client/video.ts @@ -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'; diff --git a/packages/playwright-core/src/client/waiter.ts b/packages/playwright-core/src/client/waiter.ts index 6453fb875d..90e48108a2 100644 --- a/packages/playwright-core/src/client/waiter.ts +++ b/packages/playwright-core/src/client/waiter.ts @@ -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'; diff --git a/packages/playwright-core/src/client/worker.ts b/packages/playwright-core/src/client/worker.ts index 36d5bddff3..ee9c2dcd6e 100644 --- a/packages/playwright-core/src/client/worker.ts +++ b/packages/playwright-core/src/client/worker.ts @@ -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'; diff --git a/packages/playwright-core/src/common/DEPS.list b/packages/playwright-core/src/common/DEPS.list index 60df36081b..43bff9dba4 100644 --- a/packages/playwright-core/src/common/DEPS.list +++ b/packages/playwright-core/src/common/DEPS.list @@ -1,3 +1,4 @@ [*] ../utils/ -../utilsBundle.ts \ No newline at end of file +../utilsBundle.ts +../zipBundle.ts diff --git a/packages/playwright-core/src/common/progress.ts b/packages/playwright-core/src/common/progress.ts new file mode 100644 index 0000000000..f09670e823 --- /dev/null +++ b/packages/playwright-core/src/common/progress.ts @@ -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; +} diff --git a/packages/playwright-core/src/inProcessFactory.ts b/packages/playwright-core/src/inProcessFactory.ts index d6cd8110c2..296c742127 100644 --- a/packages/playwright-core/src/inProcessFactory.ts +++ b/packages/playwright-core/src/inProcessFactory.ts @@ -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 */); diff --git a/packages/playwright-core/src/inprocess.ts b/packages/playwright-core/src/inprocess.ts index 90b1bf499d..c057f7b5c0 100644 --- a/packages/playwright-core/src/inprocess.ts +++ b/packages/playwright-core/src/inprocess.ts @@ -15,5 +15,6 @@ */ import { createInProcessPlaywright } from './inProcessFactory'; +import { nodePlatform } from './utils/platform'; -module.exports = createInProcessPlaywright(); +module.exports = createInProcessPlaywright(nodePlatform); diff --git a/packages/playwright-core/src/outofprocess.ts b/packages/playwright-core/src/outofprocess.ts index 3d8c43788e..3af4065e1d 100644 --- a/packages/playwright-core/src/outofprocess.ts +++ b/packages/playwright-core/src/outofprocess.ts @@ -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 }> { 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)); diff --git a/packages/playwright-core/src/server/android/android.ts b/packages/playwright-core/src/server/android/android.ts index d411581eab..47c46ca2cf 100644 --- a/packages/playwright-core/src/server/android/android.ts +++ b/packages/playwright-core/src/server/android/android.ts @@ -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'; diff --git a/packages/playwright-core/src/server/bidi/bidiChromium.ts b/packages/playwright-core/src/server/bidi/bidiChromium.ts index a46f1a017a..8df3460b72 100644 --- a/packages/playwright-core/src/server/bidi/bidiChromium.ts +++ b/packages/playwright-core/src/server/bidi/bidiChromium.ts @@ -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'; diff --git a/packages/playwright-core/src/server/bidi/bidiFirefox.ts b/packages/playwright-core/src/server/bidi/bidiFirefox.ts index 94d943898f..566d2aae95 100644 --- a/packages/playwright-core/src/server/bidi/bidiFirefox.ts +++ b/packages/playwright-core/src/server/bidi/bidiFirefox.ts @@ -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'; diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index 69f879ded6..f6380e8d66 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -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'; diff --git a/packages/playwright-core/src/server/browserType.ts b/packages/playwright-core/src/server/browserType.ts index 62ec63011e..19c40e7813 100644 --- a/packages/playwright-core/src/server/browserType.ts +++ b/packages/playwright-core/src/server/browserType.ts @@ -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 }); diff --git a/packages/playwright-core/src/server/chromium/chromium.ts b/packages/playwright-core/src/server/chromium/chromium.ts index badbe700bb..ad28e5b6a3 100644 --- a/packages/playwright-core/src/server/chromium/chromium.ts +++ b/packages/playwright-core/src/server/chromium/chromium.ts @@ -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'; diff --git a/packages/playwright-core/src/server/chromium/crProtocolHelper.ts b/packages/playwright-core/src/server/chromium/crProtocolHelper.ts index 4852626b93..71cf3b5816 100644 --- a/packages/playwright-core/src/server/chromium/crProtocolHelper.ts +++ b/packages/playwright-core/src/server/chromium/crProtocolHelper.ts @@ -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'; diff --git a/packages/playwright-core/src/server/chromium/protocol.d.ts b/packages/playwright-core/src/server/chromium/protocol.d.ts index fa1d6121f9..ebfd237d45 100644 --- a/packages/playwright-core/src/server/chromium/protocol.d.ts +++ b/packages/playwright-core/src/server/chromium/protocol.d.ts @@ -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; diff --git a/packages/playwright-core/src/server/chromium/videoRecorder.ts b/packages/playwright-core/src/server/chromium/videoRecorder.ts index e8f96e4129..5dfd32569a 100644 --- a/packages/playwright-core/src/server/chromium/videoRecorder.ts +++ b/packages/playwright-core/src/server/chromium/videoRecorder.ts @@ -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'; diff --git a/packages/playwright-core/src/server/debugController.ts b/packages/playwright-core/src/server/debugController.ts index 601a8338b9..c29cbb5a20 100644 --- a/packages/playwright-core/src/server/debugController.ts +++ b/packages/playwright-core/src/server/debugController.ts @@ -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'; diff --git a/packages/playwright-core/src/server/deviceDescriptorsSource.json b/packages/playwright-core/src/server/deviceDescriptorsSource.json index e5d6c93b57..1e6d0be7b8 100644 --- a/packages/playwright-core/src/server/deviceDescriptorsSource.json +++ b/packages/playwright-core/src/server/deviceDescriptorsSource.json @@ -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 diff --git a/packages/playwright-core/src/server/dispatchers/artifactDispatcher.ts b/packages/playwright-core/src/server/dispatchers/artifactDispatcher.ts index d43a846607..d0bc6967d2 100644 --- a/packages/playwright-core/src/server/dispatchers/artifactDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/artifactDispatcher.ts @@ -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'; diff --git a/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts b/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts index d65e3a5775..1d2859af59 100644 --- a/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts @@ -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(); - private _stackSessions = new Map, - tmpDir: string | undefined, - callStacks: channels.ClientSideCallMetadata[] - }>(); + private _stackSessions = new Map(); 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 { - const promise = new ManualPromise(); - 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(); - 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 { - 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 { - 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 { - 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 { - 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 { + return await localUtils.tracingStarted(this._stackSessions, params); + } + + async traceDiscarded(params: channels.LocalUtilsTraceDiscardedParams, metadata?: CallMetadata | undefined): Promise { + return await localUtils.traceDiscarded(nodePlatform, this._stackSessions, params); + } + + async addStackToTracingNoReply(params: channels.LocalUtilsAddStackToTracingNoReplyParams, metadata?: CallMetadata | undefined): Promise { + return await localUtils.addStackToTracingNoReply(this._stackSessions, params); } async connect(params: channels.LocalUtilsConnectParams, metadata: CallMetadata): Promise { @@ -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 { - 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 { - await this._deleteStackSession(params.stacksId); - } - - async addStackToTracingNoReply(params: channels.LocalUtilsAddStackToTracingNoReplyParams, metadata?: CallMetadata | undefined): Promise { - 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 { - 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 { - const harLog = this._harFile.log; - const visited = new Set(); - 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 { - if (endpointURL.startsWith('ws')) - return endpointURL; - - progress?.log(` 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; } diff --git a/packages/playwright-core/src/server/electron/electron.ts b/packages/playwright-core/src/server/electron/electron.ts index 565fce7fb1..f20813fc5b 100644 --- a/packages/playwright-core/src/server/electron/electron.ts +++ b/packages/playwright-core/src/server/electron/electron.ts @@ -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'; diff --git a/packages/playwright-core/src/server/fileUtils.ts b/packages/playwright-core/src/server/fileUtils.ts new file mode 100644 index 0000000000..b2ac823844 --- /dev/null +++ b/packages/playwright-core/src/server/fileUtils.ts @@ -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 => 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 { + 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(); // Should never be accessed from within appendOperation. + private _error: Error | undefined; + private _operations: SerializedFSOperation[] = []; + private _operationsDone: ManualPromise; + + 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(); + (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; + } + } + } +} diff --git a/packages/playwright-core/src/server/firefox/firefox.ts b/packages/playwright-core/src/server/firefox/firefox.ts index fe55417c8c..c3d21446dd 100644 --- a/packages/playwright-core/src/server/firefox/firefox.ts +++ b/packages/playwright-core/src/server/firefox/firefox.ts @@ -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'; diff --git a/packages/playwright-core/src/server/index.ts b/packages/playwright-core/src/server/index.ts index e93399007a..1627192370 100644 --- a/packages/playwright-core/src/server/index.ts +++ b/packages/playwright-core/src/server/index.ts @@ -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'; diff --git a/packages/playwright-core/src/utils/processLauncher.ts b/packages/playwright-core/src/server/processLauncher.ts similarity index 99% rename from packages/playwright-core/src/utils/processLauncher.ts rename to packages/playwright-core/src/server/processLauncher.ts index 68176acc8b..6f67ebc699 100644 --- a/packages/playwright-core/src/utils/processLauncher.ts +++ b/packages/playwright-core/src/server/processLauncher.ts @@ -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}; diff --git a/packages/playwright-core/src/server/progress.ts b/packages/playwright-core/src/server/progress.ts index f1c54edfe8..1b18089233 100644 --- a/packages/playwright-core/src/server/progress.ts +++ b/packages/playwright-core/src/server/progress.ts @@ -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; } diff --git a/packages/playwright-core/src/server/registry/browserFetcher.ts b/packages/playwright-core/src/server/registry/browserFetcher.ts index f206382b17..91e9c21f3d 100644 --- a/packages/playwright-core/src/server/registry/browserFetcher.ts +++ b/packages/playwright-core/src/server/registry/browserFetcher.ts @@ -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 '.'; diff --git a/packages/playwright-core/src/server/registry/index.ts b/packages/playwright-core/src/server/registry/index.ts index a1aa6a68d2..74313fb113 100644 --- a/packages/playwright-core/src/server/registry/index.ts +++ b/packages/playwright-core/src/server/registry/index.ts @@ -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'; diff --git a/packages/playwright-core/src/server/trace/recorder/tracing.ts b/packages/playwright-core/src/server/trace/recorder/tracing.ts index a245de16f6..12fd42d2bc 100644 --- a/packages/playwright-core/src/server/trace/recorder/tracing.ts +++ b/packages/playwright-core/src/server/trace/recorder/tracing.ts @@ -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'; diff --git a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts index ad9de86715..e380625957 100644 --- a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts +++ b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts @@ -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'; diff --git a/packages/playwright-core/src/server/webkit/protocol.d.ts b/packages/playwright-core/src/server/webkit/protocol.d.ts index 9abd47bcfd..ea34222382 100644 --- a/packages/playwright-core/src/server/webkit/protocol.d.ts +++ b/packages/playwright-core/src/server/webkit/protocol.d.ts @@ -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; diff --git a/packages/playwright-core/src/server/webkit/webkit.ts b/packages/playwright-core/src/server/webkit/webkit.ts index 69273b8e95..f92f7b58ee 100644 --- a/packages/playwright-core/src/server/webkit/webkit.ts +++ b/packages/playwright-core/src/server/webkit/webkit.ts @@ -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'; diff --git a/packages/playwright-core/src/utils/fileUtils.ts b/packages/playwright-core/src/utils/fileUtils.ts index 8cfd3d1fe0..261b72d84a 100644 --- a/packages/playwright-core/src/utils/fileUtils.ts +++ b/packages/playwright-core/src/utils/fileUtils.ts @@ -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 => 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 { +export async function removeFolders(platform: Platform, dirs: string[]): Promise { 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(); // Should never be accessed from within appendOperation. - private _error: Error | undefined; - private _operations: SerializedFSOperation[] = []; - private _operationsDone: ManualPromise; - - 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(); - (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; - } - } - } -} diff --git a/packages/playwright-core/src/utils/harBackend.ts b/packages/playwright-core/src/utils/harBackend.ts new file mode 100644 index 0000000000..5c68e87c7d --- /dev/null +++ b/packages/playwright-core/src/utils/harBackend.ts @@ -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 { + 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 { + const harLog = this._harFile.log; + const visited = new Set(); + 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; +} diff --git a/packages/playwright-core/src/utils/index.ts b/packages/playwright-core/src/utils/index.ts index 0bc7a75b08..03fc46d237 100644 --- a/packages/playwright-core/src/utils/index.ts +++ b/packages/playwright-core/src/utils/index.ts @@ -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'; diff --git a/packages/playwright-core/src/utils/localUtils.ts b/packages/playwright-core/src/utils/localUtils.ts new file mode 100644 index 0000000000..0a895dd3e7 --- /dev/null +++ b/packages/playwright-core/src/utils/localUtils.ts @@ -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; + tmpDir: string | undefined; + callStacks: channels.ClientSideCallMetadata[]; +}; + +export async function zip(platform: Platform, stackSessions: Map, params: channels.LocalUtilsZipParams): Promise { + const promise = new ManualPromise(); + 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(); + 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, 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, params: channels.LocalUtilsHarOpenParams): Promise { + 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, params: channels.LocalUtilsHarLookupParams): Promise { + 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, params: channels.LocalUtilsHarCloseParams): Promise { + const harBackend = harBackends.get(params.harId); + if (harBackend) { + harBackends.delete(harBackend.id); + harBackend.dispose(); + } +} + +export async function harUnzip(params: channels.LocalUtilsHarUnzipParams): Promise { + 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, params: channels.LocalUtilsTracingStartedParams): Promise { + 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, params: channels.LocalUtilsTraceDiscardedParams): Promise { + await deleteStackSession(platform, stackSessions, params.stacksId); +} + +export async function addStackToTracingNoReply(stackSessions: Map, params: channels.LocalUtilsAddStackToTracingNoReplyParams): Promise { + 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 { + if (endpointURL.startsWith('ws')) + return endpointURL; + + progress?.log(` 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(); +} diff --git a/packages/playwright-core/src/utils/platform.ts b/packages/playwright-core/src/utils/platform.ts new file mode 100644 index 0000000000..e682ad46b0 --- /dev/null +++ b/packages/playwright-core/src/utils/platform.ts @@ -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), +}; diff --git a/packages/playwright-core/types/protocol.d.ts b/packages/playwright-core/types/protocol.d.ts index fa1d6121f9..ebfd237d45 100644 --- a/packages/playwright-core/types/protocol.d.ts +++ b/packages/playwright-core/types/protocol.d.ts @@ -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; diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 5a74ee6d76..cdd4dcb930 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -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. diff --git a/packages/playwright/src/common/configLoader.ts b/packages/playwright/src/common/configLoader.ts index da02b83c75..31f5ffc269 100644 --- a/packages/playwright/src/common/configLoader.ts +++ b/packages/playwright/src/common/configLoader.ts @@ -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'; diff --git a/packages/playwright/src/common/suiteUtils.ts b/packages/playwright/src/common/suiteUtils.ts index d229e62732..1422494b91 100644 --- a/packages/playwright/src/common/suiteUtils.ts +++ b/packages/playwright/src/common/suiteUtils.ts @@ -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'; diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index b2ba203386..2b19dc53ee 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -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) { diff --git a/packages/playwright/src/matchers/toMatchAriaSnapshot.ts b/packages/playwright/src/matchers/toMatchAriaSnapshot.ts index 1ad5777871..fdc791d073 100644 --- a/packages/playwright/src/matchers/toMatchAriaSnapshot.ts +++ b/packages/playwright/src/matchers/toMatchAriaSnapshot.ts @@ -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'; diff --git a/packages/playwright/src/matchers/toMatchSnapshot.ts b/packages/playwright/src/matchers/toMatchSnapshot.ts index 220c948887..4fcd49a0cd 100644 --- a/packages/playwright/src/matchers/toMatchSnapshot.ts +++ b/packages/playwright/src/matchers/toMatchSnapshot.ts @@ -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'; diff --git a/packages/playwright/src/plugins/webServerPlugin.ts b/packages/playwright/src/plugins/webServerPlugin.ts index 5f30a58e95..e3103faaa8 100644 --- a/packages/playwright/src/plugins/webServerPlugin.ts +++ b/packages/playwright/src/plugins/webServerPlugin.ts @@ -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 '.'; diff --git a/packages/playwright/src/program.ts b/packages/playwright/src/program.ts index 5100e06f37..7aef21faee 100644 --- a/packages/playwright/src/program.ts +++ b/packages/playwright/src/program.ts @@ -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'); diff --git a/packages/playwright/src/reporters/blob.ts b/packages/playwright/src/reporters/blob.ts index 4f913ba452..434ab8f798 100644 --- a/packages/playwright/src/reporters/blob.ts +++ b/packages/playwright/src/reporters/blob.ts @@ -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'; diff --git a/packages/playwright/src/reporters/html.ts b/packages/playwright/src/reporters/html.ts index 557ad580b7..5583bb7316 100644 --- a/packages/playwright/src/reporters/html.ts +++ b/packages/playwright/src/reporters/html.ts @@ -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 { diff --git a/packages/playwright/src/reporters/json.ts b/packages/playwright/src/reporters/json.ts index 377f4f3b88..cd3ec41b31 100644 --- a/packages/playwright/src/reporters/json.ts +++ b/packages/playwright/src/reporters/json.ts @@ -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'; diff --git a/packages/playwright/src/runner/tasks.ts b/packages/playwright/src/runner/tasks.ts index 4a1056f6a8..77b1f34a52 100644 --- a/packages/playwright/src/runner/tasks.ts +++ b/packages/playwright/src/runner/tasks.ts @@ -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'; diff --git a/packages/playwright/src/runner/testServer.ts b/packages/playwright/src/runner/testServer.ts index 14fa5796d4..d1a2c26166 100644 --- a/packages/playwright/src/runner/testServer.ts +++ b/packages/playwright/src/runner/testServer.ts @@ -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'; diff --git a/packages/playwright/src/runner/workerHost.ts b/packages/playwright/src/runner/workerHost.ts index d92093e72b..e6c97e45a6 100644 --- a/packages/playwright/src/runner/workerHost.ts +++ b/packages/playwright/src/runner/workerHost.ts @@ -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'; diff --git a/packages/playwright/src/util.ts b/packages/playwright/src/util.ts index 74911a85cb..d0a63e8e6e 100644 --- a/packages/playwright/src/util.ts +++ b/packages/playwright/src/util.ts @@ -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'; diff --git a/packages/playwright/src/worker/testInfo.ts b/packages/playwright/src/worker/testInfo.ts index 5e964af5e0..002c126487 100644 --- a/packages/playwright/src/worker/testInfo.ts +++ b/packages/playwright/src/worker/testInfo.ts @@ -17,7 +17,8 @@ import * as fs from 'fs'; import * as path from 'path'; -import { captureRawStack, monotonicTime, sanitizeForFilePath, stringifyStackFrames, zones } from 'playwright-core/lib/utils'; +import { sanitizeForFilePath } from 'playwright-core/lib/server'; +import { captureRawStack, monotonicTime, stringifyStackFrames, zones } from 'playwright-core/lib/utils'; import { TimeoutManager, TimeoutManagerError, kMaxDeadline } from './timeoutManager'; import { debugTest, filteredStackTrace, formatLocation, getContainedPath, normalizeAndSaveAttachment, trimLongString, windowsFilesystemFriendlyLength } from '../util'; diff --git a/packages/playwright/src/worker/testTracing.ts b/packages/playwright/src/worker/testTracing.ts index a90c006616..73ae364591 100644 --- a/packages/playwright/src/worker/testTracing.ts +++ b/packages/playwright/src/worker/testTracing.ts @@ -17,7 +17,8 @@ import * as fs from 'fs'; import * as path from 'path'; -import { ManualPromise, SerializedFS, calculateSha1, createGuid, monotonicTime } from 'playwright-core/lib/utils'; +import { SerializedFS } from 'playwright-core/lib/server'; +import { ManualPromise, calculateSha1, createGuid, monotonicTime } from 'playwright-core/lib/utils'; import { yauzl, yazl } from 'playwright-core/lib/zipBundle'; import { filteredStackTrace } from '../util'; diff --git a/packages/playwright/src/worker/workerMain.ts b/packages/playwright/src/worker/workerMain.ts index 5188614271..a813b0308c 100644 --- a/packages/playwright/src/worker/workerMain.ts +++ b/packages/playwright/src/worker/workerMain.ts @@ -14,7 +14,9 @@ * limitations under the License. */ -import { ManualPromise, gracefullyCloseAll, removeFolders } from 'playwright-core/lib/utils'; +import { removeFolders } from 'playwright-core/lib/server'; +import { gracefullyCloseAll } from 'playwright-core/lib/server'; +import { ManualPromise } from 'playwright-core/lib/utils'; import { colors } from 'playwright-core/lib/utilsBundle'; import { deserializeConfig } from '../common/configLoader'; diff --git a/packages/recorder/tsconfig.json b/packages/recorder/tsconfig.json index 164b02dba5..05e3481057 100644 --- a/packages/recorder/tsconfig.json +++ b/packages/recorder/tsconfig.json @@ -9,7 +9,7 @@ "allowSyntheticDefaultImports": true, "strict": true, "module": "ESNext", - "moduleResolution": "Node", + "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, diff --git a/packages/trace-viewer/src/ui/attachmentsTab.tsx b/packages/trace-viewer/src/ui/attachmentsTab.tsx index 7a636a83b0..4536fd4325 100644 --- a/packages/trace-viewer/src/ui/attachmentsTab.tsx +++ b/packages/trace-viewer/src/ui/attachmentsTab.tsx @@ -165,7 +165,7 @@ function isEqualAttachment(a: Attachment, b: AfterActionTraceEventAttachment): b return a.name === b.name && a.path === b.path && a.sha1 === b.sha1; } -function attachmentURL(attachment: Attachment, queryParams: Record = {}) { +export function attachmentURL(attachment: Attachment, queryParams: Record = {}) { const params = new URLSearchParams(queryParams); if (attachment.sha1) { params.set('trace', attachment.traceUrl); diff --git a/packages/trace-viewer/src/ui/copyToClipboard.tsx b/packages/trace-viewer/src/ui/copyToClipboard.tsx index 1eb989d08e..8f3f8cb448 100644 --- a/packages/trace-viewer/src/ui/copyToClipboard.tsx +++ b/packages/trace-viewer/src/ui/copyToClipboard.tsx @@ -46,11 +46,16 @@ export const CopyToClipboard: React.FunctionComponent<{ export const CopyToClipboardTextButton: React.FunctionComponent<{ value: string | (() => Promise), description: string, -}> = ({ value, description }) => { + copiedDescription?: React.ReactNode, + style?: React.CSSProperties, +}> = ({ value, description, copiedDescription = description, style }) => { + const [copied, setCopied] = React.useState(false); const handleCopy = React.useCallback(async () => { const valueToCopy = typeof value === 'function' ? await value() : value; await navigator.clipboard.writeText(valueToCopy); + setCopied(true); + setTimeout(() => setCopied(false), 3000); }, [value]); - return {description}; + return {copied ? copiedDescription : description}; }; diff --git a/packages/trace-viewer/src/ui/errorsTab.tsx b/packages/trace-viewer/src/ui/errorsTab.tsx index acf5bf838e..3d8651f74d 100644 --- a/packages/trace-viewer/src/ui/errorsTab.tsx +++ b/packages/trace-viewer/src/ui/errorsTab.tsx @@ -21,6 +21,59 @@ import { PlaceholderPanel } from './placeholderPanel'; import { renderAction } from './actionList'; import type { Language } from '@isomorphic/locatorGenerators'; import type { StackFrame } from '@protocol/channels'; +import { CopyToClipboardTextButton } from './copyToClipboard'; +import { attachmentURL } from './attachmentsTab'; +import { fixTestPrompt } from '@web/components/prompts'; +import type { GitCommitInfo } from '@testIsomorphic/types'; + +const GitCommitInfoContext = React.createContext(undefined); + +export function GitCommitInfoProvider({ children, gitCommitInfo }: React.PropsWithChildren<{ gitCommitInfo: GitCommitInfo }>) { + return {children}; +} + +export function useGitCommitInfo() { + return React.useContext(GitCommitInfoContext); +} + +const PromptButton: React.FC<{ + error: string; + actions: modelUtil.ActionTraceEventInContext[]; +}> = ({ error, actions }) => { + const [pageSnapshot, setPageSnapshot] = React.useState(); + + React.useEffect(() => { + for (const action of actions) { + for (const attachment of action.attachments ?? []) { + if (attachment.name === 'pageSnapshot') { + fetch(attachmentURL({ ...attachment, traceUrl: action.context.traceUrl })).then(async response => { + setPageSnapshot(await response.text()); + }); + return; + } + } + } + }, [actions]); + + const gitCommitInfo = useGitCommitInfo(); + const prompt = React.useMemo( + () => fixTestPrompt( + error, + gitCommitInfo?.['pull.diff'] ?? gitCommitInfo?.['revision.diff'], + pageSnapshot + ), + [error, gitCommitInfo, pageSnapshot] + ); + + return ( + Copied } + style={{ width: '90px', justifyContent: 'center' }} + /> + ); +}; export type ErrorDescription = { action?: modelUtil.ActionTraceEventInContext; @@ -44,9 +97,10 @@ export function useErrorsTabModel(model: modelUtil.MultiTraceModel | undefined): export const ErrorsTab: React.FunctionComponent<{ errorsModel: ErrorsTabModel, + actions: modelUtil.ActionTraceEventInContext[], sdkLanguage: Language, revealInSource: (error: ErrorDescription) => void, -}> = ({ errorsModel, sdkLanguage, revealInSource }) => { +}> = ({ errorsModel, sdkLanguage, revealInSource, actions }) => { if (!errorsModel.errors.size) return ; @@ -72,6 +126,9 @@ export const ErrorsTab: React.FunctionComponent<{ {location &&
@ revealInSource(error)}>{location}
} + + +
; diff --git a/packages/trace-viewer/src/ui/uiModeView.tsx b/packages/trace-viewer/src/ui/uiModeView.tsx index 4375018765..8e27ed0137 100644 --- a/packages/trace-viewer/src/ui/uiModeView.tsx +++ b/packages/trace-viewer/src/ui/uiModeView.tsx @@ -37,6 +37,7 @@ import { TestListView } from './uiModeTestListView'; import { TraceView } from './uiModeTraceView'; import { SettingsView } from './settingsView'; import { DefaultSettingsView } from './defaultSettingsView'; +import { GitCommitInfoProvider } from './errorsTab'; let xtermSize = { cols: 80, rows: 24 }; const xtermDataSource: XtermDataSource = { @@ -430,13 +431,15 @@ export const UIModeView: React.FC<{}> = ({
- testServerConnection?.openNoReply({ location: { file: location.file, line: location.line, column: location.column } })} - /> + + testServerConnection?.openNoReply({ location: { file: location.file, line: location.line, column: location.column } })} + /> +
} sidebar={
diff --git a/packages/trace-viewer/src/ui/workbench.tsx b/packages/trace-viewer/src/ui/workbench.tsx index de59892772..25d01098ed 100644 --- a/packages/trace-viewer/src/ui/workbench.tsx +++ b/packages/trace-viewer/src/ui/workbench.tsx @@ -199,7 +199,7 @@ export const Workbench: React.FunctionComponent<{ else setRevealedError(error); selectPropertiesTab('source'); - }} /> + }} actions={model?.actions ?? []} /> }; // Fallback location w/o action stands for file / test. diff --git a/packages/web/src/components/prompts.ts b/packages/web/src/components/prompts.ts new file mode 100644 index 0000000000..8ecc78e9c6 --- /dev/null +++ b/packages/web/src/components/prompts.ts @@ -0,0 +1,56 @@ +/** + * 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. + */ + +const ansiRegex = new RegExp('([\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~])))', 'g'); +function stripAnsiEscapes(str: string): string { + return str.replace(ansiRegex, ''); +} + +export function fixTestPrompt(error: string, diff?: string, pageSnapshot?: string) { + const promptParts = [ + `My Playwright test failed. What's going wrong?`, + `Please give me a suggestion how to fix it, and then explain what went wrong. Be very concise and apply Playwright best practices.`, + `Don't include many headings in your output. Make sure what you're saying is correct, and take into account whether there might be a bug in the app.`, + 'Here is the error:', + '\n', + '```js', + stripAnsiEscapes(error), + '```', + '\n', + ]; + + if (pageSnapshot) { + promptParts.push( + 'This is how the page looked at the end of the test:\n', + '```yaml', + pageSnapshot, + '```', + '\n' + ); + } + + if (diff) { + promptParts.push( + 'And this is the code diff:\n', + '```diff', + diff, + '```', + '\n' + ); + } + + return promptParts.join('\n'); +} diff --git a/tests/config/browserTest.ts b/tests/config/browserTest.ts index 9eba4f30d1..ac996eebcb 100644 --- a/tests/config/browserTest.ts +++ b/tests/config/browserTest.ts @@ -16,15 +16,17 @@ import * as fs from 'fs'; import * as os from 'os'; -import type { PageTestFixtures, PageWorkerFixtures } from '../page/pageTestApi'; import * as path from 'path'; -import type { BrowserContext, BrowserContextOptions, BrowserType, Page } from 'playwright-core'; -import { removeFolders } from '../../packages/playwright-core/lib/utils/fileUtils'; import { baseTest } from './baseTest'; -import { type RemoteServerOptions, type PlaywrightServer, RunServer, RemoteServer } from './remoteServer'; -import type { Log } from '../../packages/trace/src/har'; +import { RunServer, RemoteServer } from './remoteServer'; +import { removeFolders } from '../../packages/playwright-core/lib/server/fileUtils'; import { parseHar } from '../config/utils'; import { createSkipTestPredicate } from '../bidi/expectationUtil'; + +import type { PageTestFixtures, PageWorkerFixtures } from '../page/pageTestApi'; +import type { RemoteServerOptions, PlaywrightServer } from './remoteServer'; +import type { BrowserContext, BrowserContextOptions, BrowserType, Page } from 'playwright-core'; +import type { Log } from '../../packages/trace/src/har'; import type { TestInfo } from '@playwright/test'; export type BrowserTestWorkerFixtures = PageWorkerFixtures & { diff --git a/tests/installation/globalSetup.ts b/tests/installation/globalSetup.ts index 7714f35226..bf2b7df3e4 100644 --- a/tests/installation/globalSetup.ts +++ b/tests/installation/globalSetup.ts @@ -17,7 +17,7 @@ import path from 'path'; import fs from 'fs'; import { spawnAsync } from '../../packages/playwright-core/lib/utils/spawnAsync'; -import { removeFolders } from '../../packages/playwright-core/lib/utils/fileUtils'; +import { removeFolders } from '../../packages/playwright-core/lib/server/fileUtils'; import { TMP_WORKSPACES } from './npmTest'; const PACKAGE_BUILDER_SCRIPT = path.join(__dirname, '..', '..', 'utils', 'pack_package.js'); diff --git a/tests/installation/npmTest.ts b/tests/installation/npmTest.ts index 4e39fb8c98..128e7a7ab6 100644 --- a/tests/installation/npmTest.ts +++ b/tests/installation/npmTest.ts @@ -22,7 +22,7 @@ import debugLogger from 'debug'; import { Registry } from './registry'; import type { CommonFixtures, CommonWorkerFixtures } from '../config/commonFixtures'; import { commonFixtures } from '../config/commonFixtures'; -import { removeFolders } from '../../packages/playwright-core/lib/utils/fileUtils'; +import { removeFolders } from '../../packages/playwright-core/lib/server/fileUtils'; import { spawnAsync } from '../../packages/playwright-core/lib/utils/spawnAsync'; import type { SpawnOptions } from 'child_process'; diff --git a/tests/library/defaultbrowsercontext-2.spec.ts b/tests/library/defaultbrowsercontext-2.spec.ts index e16dc6b0ba..f3f273724a 100644 --- a/tests/library/defaultbrowsercontext-2.spec.ts +++ b/tests/library/defaultbrowsercontext-2.spec.ts @@ -101,6 +101,13 @@ it('should accept userDataDir', async ({ createUserDataDir, browserType }) => { expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); }); +it('should accept relative userDataDir', async ({ createUserDataDir, browserType }) => { + const userDataDir = await createUserDataDir(); + const context = await browserType.launchPersistentContext(path.relative(process.cwd(), path.join(userDataDir, 'foobar'))); + expect(fs.readdirSync(path.join(userDataDir, 'foobar')).length).toBeGreaterThan(0); + await context.close(); +}); + it('should restore state from userDataDir', async ({ browserType, server, createUserDataDir, isMac, browserName }) => { it.slow(); diff --git a/tests/library/inspector/inspectorTest.ts b/tests/library/inspector/inspectorTest.ts index f5e3631b3a..fa13420fc3 100644 --- a/tests/library/inspector/inspectorTest.ts +++ b/tests/library/inspector/inspectorTest.ts @@ -22,6 +22,7 @@ import type { Source } from '../../../packages/recorder/src/recorderTypes'; import type { CommonFixtures, TestChildProcess } from '../../config/commonFixtures'; import { stripAnsi } from '../../config/utils'; import { expect } from '@playwright/test'; +import { nodePlatform } from '../../../packages/playwright-core/lib/utils/platform'; export { expect } from '@playwright/test'; type CLITestArgs = { @@ -46,7 +47,7 @@ const codegenLang2Id: Map = new Map([ ]); const codegenLangId2lang = new Map([...codegenLang2Id.entries()].map(([lang, langId]) => [langId, lang])); -const playwrightToAutomateInspector = require('../../../packages/playwright-core/lib/inProcessFactory').createInProcessPlaywright(); +const playwrightToAutomateInspector = require('../../../packages/playwright-core/lib/inProcessFactory').createInProcessPlaywright(nodePlatform); export const test = contextTest.extend({ recorderPageGetter: async ({ context, toImpl, mode }, run, testInfo) => { diff --git a/tests/library/inspector/title.spec.ts b/tests/library/inspector/title.spec.ts index edd73be21a..e185db2303 100644 --- a/tests/library/inspector/title.spec.ts +++ b/tests/library/inspector/title.spec.ts @@ -77,6 +77,8 @@ test('should update primary page URL when original primary closes', async ({ ); await page3.close(); + // URL will not update without performing some action + await page4.locator('div').first().click(); await expect(recorder.recorderPage).toHaveTitle( `Playwright Inspector - ${server.PREFIX}/grid.html`, ); diff --git a/tests/playwright-test/config.spec.ts b/tests/playwright-test/config.spec.ts index 7870d74706..518bf33839 100644 --- a/tests/playwright-test/config.spec.ts +++ b/tests/playwright-test/config.spec.ts @@ -327,6 +327,25 @@ test('should print nice error when project is unknown', async ({ runInlineTest } expect(output).toContain('Project(s) "suite3" not found. Available projects: "suite1", "suite2"'); }); +test('should print nice error when project is unknown and launching UI mode', async ({ runInlineTest }) => { + // Prevent UI mode from opening and the test never finishing + test.setTimeout(5000); + const { output, exitCode } = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { projects: [ + { name: 'suite1' }, + { name: 'suite2' }, + ] }; + `, + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('pass', async ({}, testInfo) => {}); + ` + }, { project: 'suite3', ui: true }); + expect(exitCode).toBe(1); + expect(output).toContain('Project(s) "suite3" not found. Available projects: "suite1", "suite2"'); +}); + test('should filter by project list, case-insensitive', async ({ runInlineTest }) => { const { passed, failed, outputLines, skipped } = await runInlineTest({ 'playwright.config.ts': ` diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index d5cc7ed50f..2c7915a357 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -1238,7 +1238,7 @@ for (const useIntermediateMergeReport of [true, false] as const) { - link "Logs" - link "Pull Request" - link /^[a-f0-9]{7}$/ - - text: 'foo: value1 bar: {"prop":"value2"} baz: ["value3",123]' + - text: 'foo : value1 bar : {"prop":"value2"} baz : ["value3",123]' `); }); @@ -2694,6 +2694,59 @@ for (const useIntermediateMergeReport of [true, false] as const) { await page.getByText('my test').click(); await expect(page.locator('.tree-item', { hasText: 'stdout' })).toHaveCount(1); }); + + test('should show AI prompt', async ({ runInlineTest, writeFiles, showReport, page }) => { + const files = { + 'uncommitted.txt': `uncommitted file`, + 'playwright.config.ts': ` + export default { + populateGitInfo: true, + metadata: { foo: 'value1', bar: { prop: 'value2' }, baz: ['value3', 123] } + }; + `, + 'example.spec.ts': ` + import { test, expect } from '@playwright/test'; + test('sample', async ({}) => { expect(2).toBe(3); }); + `, + }; + const baseDir = await writeFiles(files); + + const execGit = async (args: string[]) => { + const { code, stdout, stderr } = await spawnAsync('git', args, { stdio: 'pipe', cwd: baseDir }); + if (!!code) + throw new Error(`Non-zero exit of:\n$ git ${args.join(' ')}\nConsole:\nstdout:\n${stdout}\n\nstderr:\n${stderr}\n\n`); + return; + }; + + await execGit(['init']); + await execGit(['config', '--local', 'user.email', 'shakespeare@example.local']); + await execGit(['config', '--local', 'user.name', 'William']); + await execGit(['add', 'playwright.config.ts']); + await execGit(['commit', '-m', 'init']); + await execGit(['add', '*.ts']); + await execGit(['commit', '-m', 'chore(html): make this test look nice']); + + const result = await runInlineTest(files, { reporter: 'dot,html' }, { + PLAYWRIGHT_HTML_OPEN: 'never', + GITHUB_REPOSITORY: 'microsoft/playwright-example-for-test', + GITHUB_RUN_ID: 'example-run-id', + GITHUB_SERVER_URL: 'https://playwright.dev', + GITHUB_SHA: 'example-sha', + GITHUB_REF_NAME: '42/merge', + GITHUB_BASE_REF: 'HEAD~1', + }); + + expect(result.exitCode).toBe(1); + await showReport(); + + await page.context().grantPermissions(['clipboard-read', 'clipboard-write']); + + await page.getByRole('link', { name: 'sample' }).click(); + await page.getByRole('button', { name: 'Fix with AI' }).click(); + const prompt = await page.evaluate(() => navigator.clipboard.readText()); + expect(prompt, 'contains error').toContain('expect(received).toBe(expected)'); + expect(prompt, 'contains diff').toContain(`+ test('sample', async ({}) => { expect(2).toBe(3); });`); + }); }); } diff --git a/tests/playwright-test/ui-mode-test-setup.spec.ts b/tests/playwright-test/ui-mode-test-setup.spec.ts index d3fefb27fc..c912038571 100644 --- a/tests/playwright-test/ui-mode-test-setup.spec.ts +++ b/tests/playwright-test/ui-mode-test-setup.spec.ts @@ -95,17 +95,6 @@ test('should teardown on sigint', async ({ runUITest, nodeVersion }) => { ]); }); -test('should show errors in config', async ({ runUITest }) => { - const { page } = await runUITest({ - 'playwright.config.ts': ` - import { defineConfig, devices } from '@playwright/test'; - throw new Error("URL is empty") - `, - }); - await page.getByText('playwright.config.ts').click(); - await expect(page.getByText('Error: URL is empty')).toBeInViewport(); -}); - const testsWithSetup = { 'playwright.config.ts': ` import { defineConfig } from '@playwright/test'; diff --git a/tests/playwright-test/ui-mode-trace.spec.ts b/tests/playwright-test/ui-mode-trace.spec.ts index 8001623721..e7479cab1a 100644 --- a/tests/playwright-test/ui-mode-trace.spec.ts +++ b/tests/playwright-test/ui-mode-trace.spec.ts @@ -499,3 +499,22 @@ test('skipped steps should have an indicator', async ({ runUITest }) => { await expect(skippedMarker).toBeVisible(); await expect(skippedMarker).toHaveAccessibleName('skipped'); }); + +test('should show copy prompt button in errors tab', async ({ runUITest }) => { + const { page } = await runUITest({ + 'a.spec.ts': ` + import { test, expect } from '@playwright/test'; + test('fails', async () => { + expect(1).toBe(2); + }); + `, + }); + + await page.getByText('fails').dblclick(); + + await page.context().grantPermissions(['clipboard-read', 'clipboard-write']); + await page.getByText('Errors', { exact: true }).click(); + await page.locator('.tab-errors').getByRole('button', { name: 'Fix with AI' }).click(); + const prompt = await page.evaluate(() => navigator.clipboard.readText()); + expect(prompt, 'contains error').toContain('expect(received).toBe(expected)'); +});