diff --git a/.github/workflows/tests_bidi.yml b/.github/workflows/tests_bidi.yml index 8224d24883..34af9e7096 100644 --- a/.github/workflows/tests_bidi.yml +++ b/.github/workflows/tests_bidi.yml @@ -26,7 +26,7 @@ jobs: strategy: fail-fast: false matrix: - channel: [bidi-chromium, bidi-firefox-beta] + channel: [bidi-chromium, bidi-firefox-nightly] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -38,8 +38,8 @@ jobs: - run: npm run build - run: npx playwright install --with-deps chromium if: matrix.channel == 'bidi-chromium' - - run: npx -y @puppeteer/browsers install firefox@beta - if: matrix.channel == 'bidi-firefox-beta' + - run: npx -y @puppeteer/browsers install firefox@nightly + if: matrix.channel == 'bidi-firefox-nightly' - name: Run tests run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run biditest -- --project=${{ matrix.channel }}* env: diff --git a/docs/src/api/class-frame.md b/docs/src/api/class-frame.md index f3f308622f..bf3666b9be 100644 --- a/docs/src/api/class-frame.md +++ b/docs/src/api/class-frame.md @@ -280,7 +280,7 @@ When all steps combined have not finished during the specified [`option: timeout ### option: Frame.click.timeout = %%-input-timeout-js-%% * since: v1.8 -### option: Frame.click.trial = %%-input-trial-%% +### option: Frame.click.trial = %%-input-trial-with-modifiers-%% * since: v1.11 ## async method: Frame.content @@ -341,7 +341,7 @@ When all steps combined have not finished during the specified [`option: timeout ### option: Frame.dblclick.timeout = %%-input-timeout-js-%% * since: v1.8 -### option: Frame.dblclick.trial = %%-input-trial-%% +### option: Frame.dblclick.trial = %%-input-trial-with-modifiers-%% * since: v1.11 ## async method: Frame.dispatchEvent @@ -1153,7 +1153,7 @@ When all steps combined have not finished during the specified [`option: timeout ### option: Frame.hover.timeout = %%-input-timeout-js-%% * since: v1.8 -### option: Frame.hover.trial = %%-input-trial-%% +### option: Frame.hover.trial = %%-input-trial-with-modifiers-%% * since: v1.11 ### option: Frame.hover.noWaitAfter = %%-input-no-wait-after-removed-%% @@ -1703,7 +1703,7 @@ When all steps combined have not finished during the specified [`option: timeout ### option: Frame.tap.timeout = %%-input-timeout-js-%% * since: v1.8 -### option: Frame.tap.trial = %%-input-trial-%% +### option: Frame.tap.trial = %%-input-trial-with-modifiers-%% * since: v1.11 ## async method: Frame.textContent diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md index 88658b5494..86de53841c 100644 --- a/docs/src/api/class-locator.md +++ b/docs/src/api/class-locator.md @@ -433,7 +433,7 @@ await page.Locator("canvas").ClickAsync(new() { ### option: Locator.click.timeout = %%-input-timeout-js-%% * since: v1.14 -### option: Locator.click.trial = %%-input-trial-%% +### option: Locator.click.trial = %%-input-trial-with-modifiers-%% * since: v1.14 ## async method: Locator.count @@ -516,7 +516,7 @@ When all steps combined have not finished during the specified [`option: timeout ### option: Locator.dblclick.timeout = %%-input-timeout-js-%% * since: v1.14 -### option: Locator.dblclick.trial = %%-input-trial-%% +### option: Locator.dblclick.trial = %%-input-trial-with-modifiers-%% * since: v1.14 ## async method: Locator.dispatchEvent @@ -1266,7 +1266,7 @@ When all steps combined have not finished during the specified [`option: timeout ### option: Locator.hover.timeout = %%-input-timeout-js-%% * since: v1.14 -### option: Locator.hover.trial = %%-input-trial-%% +### option: Locator.hover.trial = %%-input-trial-with-modifiers-%% * since: v1.14 ### option: Locator.hover.noWaitAfter = %%-input-no-wait-after-removed-%% @@ -2331,7 +2331,7 @@ When all steps combined have not finished during the specified [`option: timeout ### option: Locator.tap.timeout = %%-input-timeout-js-%% * since: v1.14 -### option: Locator.tap.trial = %%-input-trial-%% +### option: Locator.tap.trial = %%-input-trial-with-modifiers-%% * since: v1.14 ## async method: Locator.textContent diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index c0890d04ff..b437b9313e 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -812,7 +812,7 @@ When all steps combined have not finished during the specified [`option: timeout ### option: Page.click.timeout = %%-input-timeout-js-%% * since: v1.8 -### option: Page.click.trial = %%-input-trial-%% +### option: Page.click.trial = %%-input-trial-with-modifiers-%% * since: v1.11 ## async method: Page.close @@ -915,7 +915,7 @@ When all steps combined have not finished during the specified [`option: timeout ### option: Page.dblclick.timeout = %%-input-timeout-js-%% * since: v1.8 -### option: Page.dblclick.trial = %%-input-trial-%% +### option: Page.dblclick.trial = %%-input-trial-with-modifiers-%% * since: v1.11 ## async method: Page.dispatchEvent @@ -2437,7 +2437,7 @@ When all steps combined have not finished during the specified [`option: timeout ### option: Page.hover.timeout = %%-input-timeout-js-%% * since: v1.8 -### option: Page.hover.trial = %%-input-trial-%% +### option: Page.hover.trial = %%-input-trial-with-modifiers-%% * since: v1.11 ### option: Page.hover.noWaitAfter = %%-input-no-wait-after-removed-%% @@ -4099,7 +4099,7 @@ When all steps combined have not finished during the specified [`option: timeout ### option: Page.tap.timeout = %%-input-timeout-js-%% * since: v1.8 -### option: Page.tap.trial = %%-input-trial-%% +### option: Page.tap.trial = %%-input-trial-with-modifiers-%% * since: v1.11 ## async method: Page.textContent diff --git a/docs/src/api/params.md b/docs/src/api/params.md index de930ee97e..4a0e3af657 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -136,6 +136,11 @@ defaults to 1. See [UIEvent.detail]. When set, this method only performs the [actionability](../actionability.md) checks and skips the action. Defaults to `false`. Useful to wait until the element is ready for the action without performing it. +## input-trial-with-modifiers +- `trial` <[boolean]> + +When set, this method only performs the [actionability](../actionability.md) checks and skips the action. Defaults to `false`. Useful to wait until the element is ready for the action without performing it. Note that keyboard `modifiers` will be pressed regardless of `trial` to allow testing elements which are only visible when those keys are pressed. + ## input-source-position - `sourcePosition` <[Object]> - `x` <[float]> diff --git a/docs/src/ci.md b/docs/src/ci.md index 745ecc9d50..f4c11c9984 100644 --- a/docs/src/ci.md +++ b/docs/src/ci.md @@ -85,7 +85,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: lts/* - name: Install dependencies run: npm ci - name: Install Playwright Browsers @@ -214,7 +214,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: lts/* - name: Install dependencies run: npm ci - name: Run your tests @@ -319,7 +319,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: lts/* - name: Install dependencies run: npm ci - name: Install Playwright @@ -434,7 +434,7 @@ jobs: fetch-depth: 0 - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: lts/* - name: Install dependencies run: npm ci - name: Install Playwright Browsers diff --git a/docs/src/network.md b/docs/src/network.md index 152231556e..4d6f229678 100644 --- a/docs/src/network.md +++ b/docs/src/network.md @@ -700,6 +700,27 @@ await Page.RouteAsync("**/title.html", async route => }); ``` +## Glob URL patterns + +Playwright uses simplified glob patterns for URL matching in network interception methods like [`method: Page.route`] or [`method: Page.waitForResponse`]. These patterns support basic wildcards: + +1. Asterisks: + - A single `*` matches any characters except `/` + - A double `**` matches any characters including `/` +1. Question mark `?` matches any single character except `/` +1. Curly braces `{}` can be used to match a list of options separated by commas `,` + +Examples: +- `https://example.com/*.js` matches `https://example.com/file.js` but not `https://example.com/path/file.js` +- `**/*.js` matches both `https://example.com/file.js` and `https://example.com/path/file.js` +- `**/*.{png,jpg,jpeg}` matches all image requests + +Important notes: + +- The glob pattern must match the entire URL, not just a part of it. +- When using globs for URL matching, consider the full URL structure, including the protocol and path separators. +- For more complex matching requirements, consider using [RegExp] instead of glob patterns. + ## WebSockets Playwright supports [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) inspection out of the box. Every time a WebSocket is created, the [`event: Page.webSocket`] event is fired. This event contains the [WebSocket] instance for further web socket frames inspection: diff --git a/docs/src/test-sharding-js.md b/docs/src/test-sharding-js.md index d0312db811..263733e78d 100644 --- a/docs/src/test-sharding-js.md +++ b/docs/src/test-sharding-js.md @@ -85,7 +85,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: lts/* - name: Install dependencies run: npm ci - name: Install Playwright browsers @@ -118,7 +118,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: lts/* - name: Install dependencies run: npm ci diff --git a/package-lock.json b/package-lock.json index d113e1c42e..fded01102a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1517,9 +1517,9 @@ "link": true }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.3.tgz", - "integrity": "sha512-MmKSfaB9GX+zXl6E8z4koOr/xU63AMVleLEa64v7R0QF/ZloMs5vcD1sHgM64GXXS1csaJutG+ddtzcueI/BLg==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz", + "integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==", "cpu": [ "arm" ], @@ -1529,9 +1529,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.3.tgz", - "integrity": "sha512-zrt8ecH07PE3sB4jPOggweBjJMzI1JG5xI2DIsUbkA+7K+Gkjys6eV7i9pOenNSDJH3eOr/jLb/PzqtmdwDq5g==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz", + "integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==", "cpu": [ "arm64" ], @@ -1541,9 +1541,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.3.tgz", - "integrity": "sha512-P0UxIOrKNBFTQaXTxOH4RxuEBVCgEA5UTNV6Yz7z9QHnUJ7eLX9reOd/NYMO3+XZO2cco19mXTxDMXxit4R/eQ==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz", + "integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==", "cpu": [ "arm64" ], @@ -1553,9 +1553,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.3.tgz", - "integrity": "sha512-L1M0vKGO5ASKntqtsFEjTq/fD91vAqnzeaF6sfNAy55aD+Hi2pBI5DKwCO+UNDQHWsDViJLqshxOahXyLSh3EA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz", + "integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==", "cpu": [ "x64" ], @@ -1565,9 +1565,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.3.tgz", - "integrity": "sha512-btVgIsCjuYFKUjopPoWiDqmoUXQDiW2A4C3Mtmp5vACm7/GnyuprqIDPNczeyR5W8rTXEbkmrJux7cJmD99D2g==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz", + "integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==", "cpu": [ "arm" ], @@ -1577,9 +1577,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.3.tgz", - "integrity": "sha512-zmjbSphplZlau6ZTkxd3+NMtE4UKVy7U4aVFMmHcgO5CUbw17ZP6QCgyxhzGaU/wFFdTfiojjbLG3/0p9HhAqA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz", + "integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==", "cpu": [ "arm" ], @@ -1589,9 +1589,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.3.tgz", - "integrity": "sha512-nSZfcZtAnQPRZmUkUQwZq2OjQciR6tEoJaZVFvLHsj0MF6QhNMg0fQ6mUOsiCUpTqxTx0/O6gX0V/nYc7LrgPw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz", + "integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==", "cpu": [ "arm64" ], @@ -1601,9 +1601,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.3.tgz", - "integrity": "sha512-MnvSPGO8KJXIMGlQDYfvYS3IosFN2rKsvxRpPO2l2cum+Z3exiExLwVU+GExL96pn8IP+GdH8Tz70EpBhO0sIQ==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz", + "integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==", "cpu": [ "arm64" ], @@ -1613,9 +1613,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.3.tgz", - "integrity": "sha512-+W+p/9QNDr2vE2AXU0qIy0qQE75E8RTwTwgqS2G5CRQ11vzq0tbnfBd6brWhS9bCRjAjepJe2fvvkvS3dno+iw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz", + "integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==", "cpu": [ "ppc64" ], @@ -1625,9 +1625,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.3.tgz", - "integrity": "sha512-yXH6K6KfqGXaxHrtr+Uoy+JpNlUlI46BKVyonGiaD74ravdnF9BUNC+vV+SIuB96hUMGShhKV693rF9QDfO6nQ==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz", + "integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==", "cpu": [ "riscv64" ], @@ -1637,9 +1637,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.3.tgz", - "integrity": "sha512-R8cwY9wcnApN/KDYWTH4gV/ypvy9yZUHlbJvfaiXSB48JO3KpwSpjOGqO4jnGkLDSk1hgjYkTbTt6Q7uvPf8eg==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz", + "integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==", "cpu": [ "s390x" ], @@ -1649,9 +1649,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.3.tgz", - "integrity": "sha512-kZPbX/NOPh0vhS5sI+dR8L1bU2cSO9FgxwM8r7wHzGydzfSjLRCFAT87GR5U9scj2rhzN3JPYVC7NoBbl4FZ0g==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz", + "integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==", "cpu": [ "x64" ], @@ -1661,9 +1661,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.3.tgz", - "integrity": "sha512-S0Yq+xA1VEH66uiMNhijsWAafffydd2X5b77eLHfRmfLsRSpbiAWiRHV6DEpz6aOToPsgid7TI9rGd6zB1rhbg==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz", + "integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==", "cpu": [ "x64" ], @@ -1673,9 +1673,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.3.tgz", - "integrity": "sha512-9isNzeL34yquCPyerog+IMCNxKR8XYmGd0tHSV+OVx0TmE0aJOo9uw4fZfUuk2qxobP5sug6vNdZR6u7Mw7Q+Q==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz", + "integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==", "cpu": [ "arm64" ], @@ -1685,9 +1685,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.3.tgz", - "integrity": "sha512-nMIdKnfZfzn1Vsk+RuOvl43ONTZXoAPUUxgcU0tXooqg4YrAqzfKzVenqqk2g5efWh46/D28cKFrOzDSW28gTA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz", + "integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==", "cpu": [ "ia32" ], @@ -1697,9 +1697,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.3.tgz", - "integrity": "sha512-fOvu7PCQjAj4eWDEuD8Xz5gpzFqXzGlxHZozHP4b9Jxv9APtdxL6STqztDzMLuRXEc4UpXGGhx029Xgm91QBeA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz", + "integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==", "cpu": [ "x64" ], @@ -5417,9 +5417,9 @@ "dev": true }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "dependencies": { "braces": "^3.0.3", @@ -6313,9 +6313,9 @@ } }, "node_modules/rollup": { - "version": "4.21.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.3.tgz", - "integrity": "sha512-7sqRtBNnEbcBtMeRVc6VRsJMmpI+JU1z9VTvW8D4gXIYQFz0aLcsE6rRkyghZkLfEgUZgVvOG7A5CVz/VW5GIA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", + "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", "dependencies": { "@types/estree": "1.0.5" }, @@ -6327,22 +6327,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.21.3", - "@rollup/rollup-android-arm64": "4.21.3", - "@rollup/rollup-darwin-arm64": "4.21.3", - "@rollup/rollup-darwin-x64": "4.21.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.21.3", - "@rollup/rollup-linux-arm-musleabihf": "4.21.3", - "@rollup/rollup-linux-arm64-gnu": "4.21.3", - "@rollup/rollup-linux-arm64-musl": "4.21.3", - "@rollup/rollup-linux-powerpc64le-gnu": "4.21.3", - "@rollup/rollup-linux-riscv64-gnu": "4.21.3", - "@rollup/rollup-linux-s390x-gnu": "4.21.3", - "@rollup/rollup-linux-x64-gnu": "4.21.3", - "@rollup/rollup-linux-x64-musl": "4.21.3", - "@rollup/rollup-win32-arm64-msvc": "4.21.3", - "@rollup/rollup-win32-ia32-msvc": "4.21.3", - "@rollup/rollup-win32-x64-msvc": "4.21.3", + "@rollup/rollup-android-arm-eabi": "4.22.4", + "@rollup/rollup-android-arm64": "4.22.4", + "@rollup/rollup-darwin-arm64": "4.22.4", + "@rollup/rollup-darwin-x64": "4.22.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.22.4", + "@rollup/rollup-linux-arm-musleabihf": "4.22.4", + "@rollup/rollup-linux-arm64-gnu": "4.22.4", + "@rollup/rollup-linux-arm64-musl": "4.22.4", + "@rollup/rollup-linux-powerpc64le-gnu": "4.22.4", + "@rollup/rollup-linux-riscv64-gnu": "4.22.4", + "@rollup/rollup-linux-s390x-gnu": "4.22.4", + "@rollup/rollup-linux-x64-gnu": "4.22.4", + "@rollup/rollup-linux-x64-musl": "4.22.4", + "@rollup/rollup-win32-arm64-msvc": "4.22.4", + "@rollup/rollup-win32-ia32-msvc": "4.22.4", + "@rollup/rollup-win32-x64-msvc": "4.22.4", "fsevents": "~2.3.2" } }, diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 5fd2927700..d8605f9ed9 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -9,9 +9,9 @@ }, { "name": "chromium-tip-of-tree", - "revision": "1261", + "revision": "1263", "installByDefault": false, - "browserVersion": "131.0.6726.0" + "browserVersion": "131.0.6736.0" }, { "name": "firefox", @@ -27,7 +27,7 @@ }, { "name": "webkit", - "revision": "2080", + "revision": "2082", "installByDefault": true, "revisionOverrides": { "mac10.14": "1446", diff --git a/packages/playwright-core/src/cli/program.ts b/packages/playwright-core/src/cli/program.ts index fb27b14231..1895f2dfcf 100644 --- a/packages/playwright-core/src/cli/program.ts +++ b/packages/playwright-core/src/cli/program.ts @@ -397,7 +397,7 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro process.stdout.write('\n-------------8<-------------\n'); const autoExitCondition = process.env.PWTEST_CLI_AUTO_EXIT_WHEN; if (autoExitCondition && text.includes(autoExitCondition)) - Promise.all(context.pages().map(async p => p.close())); + closeBrowser(); }; // Make sure we exit abnormally when browser crashes. const logs: string[] = []; @@ -504,7 +504,7 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro if (hasPage) return; // Avoid the error when the last page is closed because the browser has been closed. - closeBrowser().catch(e => null); + closeBrowser().catch(() => {}); }); }); process.on('SIGINT', async () => { @@ -560,7 +560,7 @@ async function open(options: Options, url: string | undefined, language: string) async function codegen(options: Options & { target: string, output?: string, testIdAttribute?: string }, url: string | undefined) { const { target: language, output: outputFile, testIdAttribute: testIdAttributeName } = options; - const tracesDir = path.join(os.tmpdir(), `recorder-trace-${Date.now()}`); + const tracesDir = path.join(os.tmpdir(), `playwright-recorder-trace-${Date.now()}`); const { context, launchOptions, contextOptions } = await launchContext(options, { headless: !!process.env.PWTEST_CLI_HEADLESS, executablePath: process.env.PWTEST_CLI_EXECUTABLE_PATH, @@ -574,6 +574,7 @@ async function codegen(options: Options & { target: string, output?: string, tes device: options.device, saveStorage: options.saveStorage, mode: 'recording', + codegenMode: process.env.PW_RECORDER_IS_TRACE_VIEWER ? 'trace-events' : 'actions', testIdAttributeName, outputFile: outputFile ? path.resolve(outputFile) : undefined, }); diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index 6b3de08c8e..72ef29d6a6 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -220,7 +220,7 @@ export class BrowserContext extends ChannelOwner } // If the page is closed or unrouteAll() was called without waiting and interception disabled, // the method will throw an error - silence it. - await route._innerContinue(true).catch(() => {}); + await route._innerContinue(true /* isFallback */).catch(() => {}); } async _onWebSocketRoute(webSocketRoute: network.WebSocketRoute) { @@ -492,17 +492,8 @@ export class BrowserContext extends ChannelOwner await this._closedPromise; } - async _enableRecorder(params: { - language: string, - launchOptions?: LaunchOptions, - contextOptions?: BrowserContextOptions, - device?: string, - saveStorage?: string, - mode?: 'recording' | 'inspecting', - testIdAttributeName?: string, - outputFile?: string, - }) { - await this._channel.recorderSupplementEnable(params); + async _enableRecorder(params: channels.BrowserContextEnableRecorderParams) { + await this._channel.enableRecorder(params); } } diff --git a/packages/playwright-core/src/client/channelOwner.ts b/packages/playwright-core/src/client/channelOwner.ts index abe1cbf254..89f3edced3 100644 --- a/packages/playwright-core/src/client/channelOwner.ts +++ b/packages/playwright-core/src/client/channelOwner.ts @@ -40,6 +40,7 @@ export abstract class ChannelOwner = new Map(); + private _isInternalType = false; _wasCollected: boolean = false; constructor(parent: ChannelOwner | Connection, type: string, guid: string, initializer: channels.InitializerTraits) { @@ -61,6 +62,10 @@ export abstract class ChannelOwner) { this._eventToSubscriptionMapping = mapping; } @@ -173,7 +178,7 @@ export abstract class ChannelOwner { constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.LocalUtilsInitializer) { super(parent, type, guid, initializer); + this.markAsInternalType(); this.devices = {}; for (const { name, descriptor } of initializer.deviceDescriptors) this.devices[name] = descriptor; diff --git a/packages/playwright-core/src/client/network.ts b/packages/playwright-core/src/client/network.ts index 98d4fe7554..85bec1bf8d 100644 --- a/packages/playwright-core/src/client/network.ts +++ b/packages/playwright-core/src/client/network.ts @@ -299,6 +299,7 @@ export class Route extends ChannelOwner implements api.Ro constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.RouteInitializer) { super(parent, type, guid, initializer); + this.markAsInternalType(); } request(): Request { @@ -325,7 +326,7 @@ export class Route extends ChannelOwner implements api.Ro async abort(errorCode?: string) { await this._handleRoute(async () => { - await this._raceWithTargetClose(this._channel.abort({ requestUrl: this.request()._initializer.url, errorCode })); + await this._raceWithTargetClose(this._channel.abort({ errorCode })); }); } @@ -409,7 +410,6 @@ export class Route extends ChannelOwner implements api.Ro headers['content-length'] = String(length); await this._raceWithTargetClose(this._channel.fulfill({ - requestUrl: this.request()._initializer.url, status: statusOption || 200, headers: headersObjectToArray(headers), body, @@ -421,7 +421,7 @@ export class Route extends ChannelOwner implements api.Ro async continue(options: FallbackOverrides = {}) { await this._handleRoute(async () => { this.request()._applyFallbackOverrides(options); - await this._innerContinue(); + await this._innerContinue(false /* isFallback */); }); } @@ -436,18 +436,15 @@ export class Route extends ChannelOwner implements api.Ro chain.resolve(done); } - async _innerContinue(internal = false) { + async _innerContinue(isFallback: boolean) { const options = this.request()._fallbackOverridesForContinue(); - return await this._wrapApiCall(async () => { - await this._raceWithTargetClose(this._channel.continue({ - requestUrl: this.request()._initializer.url, - url: options.url, - method: options.method, - headers: options.headers ? headersObjectToArray(options.headers) : undefined, - postData: options.postDataBuffer, - isFallback: internal, - })); - }, !!internal); + return await this._raceWithTargetClose(this._channel.continue({ + url: options.url, + method: options.method, + headers: options.headers ? headersObjectToArray(options.headers) : undefined, + postData: options.postDataBuffer, + isFallback, + })); } } diff --git a/packages/playwright-core/src/client/tracing.ts b/packages/playwright-core/src/client/tracing.ts index 7330cd9f26..b5c411cc65 100644 --- a/packages/playwright-core/src/client/tracing.ts +++ b/packages/playwright-core/src/client/tracing.ts @@ -31,20 +31,18 @@ export class Tracing extends ChannelOwner implements ap constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.TracingInitializer) { super(parent, type, guid, initializer); + this.markAsInternalType(); } async start(options: { name?: string, title?: string, snapshots?: boolean, screenshots?: boolean, sources?: boolean, _live?: boolean } = {}) { this._includeSources = !!options.sources; - const traceName = await this._wrapApiCall(async () => { - await this._channel.tracingStart({ - name: options.name, - snapshots: options.snapshots, - screenshots: options.screenshots, - live: options._live, - }); - const response = await this._channel.tracingStartChunk({ name: options.name, title: options.title }); - return response.traceName; - }, true); + await this._channel.tracingStart({ + name: options.name, + snapshots: options.snapshots, + screenshots: options.screenshots, + live: options._live, + }); + const { traceName } = await this._channel.tracingStartChunk({ name: options.name, title: options.title }); await this._startCollectingStacks(traceName); } @@ -63,16 +61,12 @@ export class Tracing extends ChannelOwner implements ap } async stopChunk(options: { path?: string } = {}) { - await this._wrapApiCall(async () => { - await this._doStopChunk(options.path); - }, true); + await this._doStopChunk(options.path); } async stop(options: { path?: string } = {}) { - await this._wrapApiCall(async () => { - await this._doStopChunk(options.path); - await this._channel.tracingStop(); - }, true); + await this._doStopChunk(options.path); + await this._channel.tracingStop(); } private async _doStopChunk(filePath: string | undefined) { diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index dcf0433b1c..9b36ad8883 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -965,9 +965,10 @@ scheme.BrowserContextStorageStateResult = tObject({ }); scheme.BrowserContextPauseParams = tOptional(tObject({})); scheme.BrowserContextPauseResult = tOptional(tObject({})); -scheme.BrowserContextRecorderSupplementEnableParams = tObject({ +scheme.BrowserContextEnableRecorderParams = tObject({ language: tOptional(tString), mode: tOptional(tEnum(['inspecting', 'recording'])), + codegenMode: tOptional(tEnum(['actions', 'trace-events'])), pauseOnNextStatement: tOptional(tBoolean), testIdAttributeName: tOptional(tString), launchOptions: tOptional(tAny), @@ -977,7 +978,7 @@ scheme.BrowserContextRecorderSupplementEnableParams = tObject({ outputFile: tOptional(tString), omitCallTracking: tOptional(tBoolean), }); -scheme.BrowserContextRecorderSupplementEnableResult = tOptional(tObject({})); +scheme.BrowserContextEnableRecorderResult = tOptional(tObject({})); scheme.BrowserContextNewCDPSessionParams = tObject({ page: tOptional(tChannel(['Page'])), frame: tOptional(tChannel(['Frame'])), @@ -2115,7 +2116,6 @@ scheme.RouteRedirectNavigationRequestParams = tObject({ scheme.RouteRedirectNavigationRequestResult = tOptional(tObject({})); scheme.RouteAbortParams = tObject({ errorCode: tOptional(tString), - requestUrl: tString, }); scheme.RouteAbortResult = tOptional(tObject({})); scheme.RouteContinueParams = tObject({ @@ -2123,7 +2123,6 @@ scheme.RouteContinueParams = tObject({ method: tOptional(tString), headers: tOptional(tArray(tType('NameValue'))), postData: tOptional(tBinary), - requestUrl: tString, isFallback: tBoolean, }); scheme.RouteContinueResult = tOptional(tObject({})); @@ -2133,7 +2132,6 @@ scheme.RouteFulfillParams = tObject({ body: tOptional(tString), isBase64: tOptional(tBoolean), fetchResponseUid: tOptional(tString), - requestUrl: tString, }); scheme.RouteFulfillResult = tOptional(tObject({})); scheme.WebSocketRouteInitializer = tObject({ diff --git a/packages/playwright-core/src/server/bidi/bidiPage.ts b/packages/playwright-core/src/server/bidi/bidiPage.ts index 180e8a651e..a50217975a 100644 --- a/packages/playwright-core/src/server/bidi/bidiPage.ts +++ b/packages/playwright-core/src/server/bidi/bidiPage.ts @@ -22,6 +22,7 @@ import * as dom from '../dom'; import * as dialog from '../dialog'; import type * as frames from '../frames'; import { Page } from '../page'; +import type * as channels from '@protocol/channels'; import type { InitScript, PageDelegate } from '../page'; import type { Progress } from '../progress'; import type * as types from '../types'; @@ -32,6 +33,7 @@ import * as bidi from './third_party/bidiProtocol'; import { BidiExecutionContext } from './bidiExecutionContext'; import { BidiNetworkManager } from './bidiNetworkManager'; import { BrowserContext } from '../browserContext'; +import { BidiPDF } from './bidiPdf'; const UTILITY_WORLD_NAME = '__playwright_utility_world__'; const kPlaywrightBindingChannel = 'playwrightChannel'; @@ -48,6 +50,7 @@ export class BidiPage implements PageDelegate { private _sessionListeners: RegisteredListener[] = []; readonly _browserContext: BidiBrowserContext; readonly _networkManager: BidiNetworkManager; + private readonly _pdf: BidiPDF; _initializedPage: Page | null = null; private _initScriptIds: string[] = []; @@ -61,6 +64,7 @@ export class BidiPage implements PageDelegate { this._page = new Page(this, browserContext); this._browserContext = browserContext; this._networkManager = new BidiNetworkManager(this._session, this._page, this._onNavigationResponseStarted.bind(this)); + this._pdf = new BidiPDF(this._session); this._page.on(Page.Events.FrameDetached, (frame: frames.Frame) => this._removeContextsForFrame(frame, false)); this._sessionListeners = [ eventsHelper.addEventListener(bidiSession, 'script.realmCreated', this._onRealmCreated.bind(this)), @@ -279,6 +283,9 @@ export class BidiPage implements PageDelegate { } async bringToFront(): Promise { + await this._session.send('browsingContext.activate', { + context: this._session.sessionId, + }); } private async _updateViewport(): Promise { @@ -555,6 +562,10 @@ export class BidiPage implements PageDelegate { async resetForReuse(): Promise { } + async pdf(options: channels.PagePdfParams): Promise { + return this._pdf.generate(options); + } + async getFrameElement(frame: frames.Frame): Promise { const parent = frame.parentFrame(); if (!parent) diff --git a/packages/playwright-core/src/server/bidi/bidiPdf.ts b/packages/playwright-core/src/server/bidi/bidiPdf.ts new file mode 100644 index 0000000000..89fefb5260 --- /dev/null +++ b/packages/playwright-core/src/server/bidi/bidiPdf.ts @@ -0,0 +1,109 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * Modifications 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 { assert } from '../../utils'; +import type * as channels from '@protocol/channels'; +import type { BidiSession } from './bidiConnection'; + +const PagePaperFormats: { [key: string]: { width: number, height: number }} = { + letter: { width: 8.5, height: 11 }, + legal: { width: 8.5, height: 14 }, + tabloid: { width: 11, height: 17 }, + ledger: { width: 17, height: 11 }, + a0: { width: 33.1, height: 46.8 }, + a1: { width: 23.4, height: 33.1 }, + a2: { width: 16.54, height: 23.4 }, + a3: { width: 11.7, height: 16.54 }, + a4: { width: 8.27, height: 11.7 }, + a5: { width: 5.83, height: 8.27 }, + a6: { width: 4.13, height: 5.83 }, +}; + +const unitToPixels: { [key: string]: number } = { + 'px': 1, + 'in': 96, + 'cm': 37.8, + 'mm': 3.78 +}; + +function convertPrintParameterToInches(text: string | undefined): number | undefined { + if (text === undefined) + return undefined; + let unit = text.substring(text.length - 2).toLowerCase(); + let valueText = ''; + if (unitToPixels.hasOwnProperty(unit)) { + valueText = text.substring(0, text.length - 2); + } else { + // In case of unknown unit try to parse the whole parameter as number of pixels. + // This is consistent with phantom's paperSize behavior. + unit = 'px'; + valueText = text; + } + const value = Number(valueText); + assert(!isNaN(value), 'Failed to parse parameter value: ' + text); + const pixels = value * unitToPixels[unit]; + return pixels / 96; +} + +export class BidiPDF { + private _session: BidiSession; + + constructor(session: BidiSession) { + this._session = session; + } + + async generate(options: channels.PagePdfParams): Promise { + const { + scale = 1, + printBackground = false, + landscape = false, + pageRanges = '', + margin = {}, + } = options; + + let paperWidth = 8.5; + let paperHeight = 11; + if (options.format) { + const format = PagePaperFormats[options.format.toLowerCase()]; + assert(format, 'Unknown paper format: ' + options.format); + paperWidth = format.width; + paperHeight = format.height; + } else { + paperWidth = convertPrintParameterToInches(options.width) || paperWidth; + paperHeight = convertPrintParameterToInches(options.height) || paperHeight; + } + + const { data } = await this._session.send('browsingContext.print', { + context: this._session.sessionId, + background: printBackground, + margin: { + bottom: convertPrintParameterToInches(margin.bottom) || 0, + left: convertPrintParameterToInches(margin.left) || 0, + right: convertPrintParameterToInches(margin.right) || 0, + top: convertPrintParameterToInches(margin.top) || 0 + }, + orientation: landscape ? 'landscape' : 'portrait', + page: { + width: paperWidth, + height: paperHeight + }, + pageRanges: pageRanges ? pageRanges.split(',').map(r => r.trim()) : undefined, + scale, + }); + return Buffer.from(data, 'base64'); + } +} diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index 499356ca49..025bd0f388 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -131,15 +131,15 @@ export abstract class BrowserContext extends SdkObject { // When PWDEBUG=1, show inspector for each context. if (debugMode() === 'inspector') - await Recorder.show(this, RecorderApp.factory(this), { pauseOnNextStatement: true }); + await Recorder.show('actions', this, RecorderApp.factory(this), { pauseOnNextStatement: true }); // When paused, show inspector. if (this._debugger.isPaused()) - Recorder.showInspector(this, RecorderApp.factory(this)); + Recorder.showInspectorNoReply(this, RecorderApp.factory(this)); this._debugger.on(Debugger.Events.PausedStateChanged, () => { if (this._debugger.isPaused()) - Recorder.showInspector(this, RecorderApp.factory(this)); + Recorder.showInspectorNoReply(this, RecorderApp.factory(this)); }); if (debugMode() === 'console') @@ -525,7 +525,7 @@ export abstract class BrowserContext extends SdkObject { const internalMetadata = serverSideCallMetadata(); const page = await this.newPage(internalMetadata); await page._setServerRequestInterceptor(handler => { - handler.fulfill({ body: '', requestUrl: handler.request().url() }).catch(() => {}); + handler.fulfill({ body: '' }).catch(() => {}); return true; }); for (const origin of originsToSave) { @@ -559,7 +559,7 @@ export abstract class BrowserContext extends SdkObject { isServerSide: false, }); await page._setServerRequestInterceptor(handler => { - handler.fulfill({ body: '', requestUrl: handler.request().url() }).catch(() => {}); + handler.fulfill({ body: '' }).catch(() => {}); return true; }); @@ -594,7 +594,7 @@ export abstract class BrowserContext extends SdkObject { const internalMetadata = serverSideCallMetadata(); const page = await this.newPage(internalMetadata); await page._setServerRequestInterceptor(handler => { - handler.fulfill({ body: '', requestUrl: handler.request().url() }).catch(() => {}); + handler.fulfill({ body: '' }).catch(() => {}); return true; }); for (const originState of state.origins) { diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts index fbdc9db91a..bba14ff00e 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts @@ -694,16 +694,15 @@ class FrameSession { if (!frame || this._eventBelongsToStaleFrame(frame._id)) return; const delegate = new CRExecutionContext(this._client, contextPayload); - let worldName: types.World; + let worldName: types.World|null = null; if (contextPayload.auxData && !!contextPayload.auxData.isDefault) worldName = 'main'; else if (contextPayload.name === UTILITY_WORLD_NAME) worldName = 'utility'; - else - return; const context = new dom.FrameExecutionContext(delegate, frame, worldName); (context as any)[contextDelegateSymbol] = delegate; - frame._contextCreated(worldName, context); + if (worldName) + frame._contextCreated(worldName, context); this._contextIdToContext.set(contextPayload.id, context); } diff --git a/packages/playwright-core/src/server/debugController.ts b/packages/playwright-core/src/server/debugController.ts index 2a950d7c6a..53c6c3d99e 100644 --- a/packages/playwright-core/src/server/debugController.ts +++ b/packages/playwright-core/src/server/debugController.ts @@ -197,7 +197,7 @@ export class DebugController extends SdkObject { const contexts = new Set(); for (const page of this._playwright.allPages()) contexts.add(page.context()); - const result = await Promise.all([...contexts].map(c => Recorder.show(c, () => Promise.resolve(new InspectingRecorderApp(this)), { omitCallTracking: true }))); + const result = await Promise.all([...contexts].map(c => Recorder.showInspector(c, { omitCallTracking: true }, () => Promise.resolve(new InspectingRecorderApp(this))))); return result.filter(Boolean) as Recorder[]; } diff --git a/packages/playwright-core/src/server/dialog.ts b/packages/playwright-core/src/server/dialog.ts index 51dcfc2fc9..f0793d43fb 100644 --- a/packages/playwright-core/src/server/dialog.ts +++ b/packages/playwright-core/src/server/dialog.ts @@ -39,6 +39,7 @@ export class Dialog extends SdkObject { this._onHandle = onHandle; this._defaultValue = defaultValue || ''; this._page._frameManager.dialogDidOpen(this); + this.instrumentation.onDialog(this); } page() { diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts index ae0b722dfc..c6ffce49f7 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -41,7 +41,6 @@ import { serializeError } from '../errors'; import { ElementHandleDispatcher } from './elementHandlerDispatcher'; import { RecorderInTraceViewer } from '../recorder/recorderInTraceViewer'; import { RecorderApp } from '../recorder/recorderApp'; -import type { IRecorderAppFactory } from '../recorder/recorderFrontend'; import { WebSocketRouteDispatcher } from './webSocketRouteDispatcher'; export class BrowserContextDispatcher extends Dispatcher implements channels.BrowserContextChannel { @@ -301,21 +300,18 @@ export class BrowserContextDispatcher extends Dispatcher { - let factory: IRecorderAppFactory; - if (process.env.PW_RECORDER_IS_TRACE_VIEWER) { - factory = RecorderInTraceViewer.factory(this._context); + async enableRecorder(params: channels.BrowserContextEnableRecorderParams): Promise { + if (params.codegenMode === 'trace-events') { await this._context.tracing.start({ name: 'trace', snapshots: true, - screenshots: false, + screenshots: true, live: true, }); - await this._context.tracing.startChunk({ name: 'trace', title: 'trace' }); + await Recorder.show('trace-events', this._context, RecorderInTraceViewer.factory(this._context), params); } else { - factory = RecorderApp.factory(this._context); + await Recorder.show('actions', this._context, RecorderApp.factory(this._context), params); } - await Recorder.show(this._context, factory, params); } async pause(params: channels.BrowserContextPauseParams, metadata: CallMetadata) { diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts index 4458a10d93..c4f5c22857 100644 --- a/packages/playwright-core/src/server/dom.ts +++ b/packages/playwright-core/src/server/dom.ts @@ -50,9 +50,9 @@ export function isNonRecoverableDOMError(error: Error) { export class FrameExecutionContext extends js.ExecutionContext { readonly frame: frames.Frame; private _injectedScriptPromise?: Promise; - readonly world: types.World; + readonly world: types.World | null; - constructor(delegate: js.ExecutionContextDelegate, frame: frames.Frame, world: types.World) { + constructor(delegate: js.ExecutionContextDelegate, frame: frames.Frame, world: types.World|null) { super(frame, delegate, world || 'content-script'); this.frame = frame; this.world = world; diff --git a/packages/playwright-core/src/server/download.ts b/packages/playwright-core/src/server/download.ts index f7a92c8c7d..78a9c015dc 100644 --- a/packages/playwright-core/src/server/download.ts +++ b/packages/playwright-core/src/server/download.ts @@ -35,16 +35,25 @@ export class Download { this._suggestedFilename = suggestedFilename; page._browserContext._downloads.add(this); if (suggestedFilename !== undefined) - this._page.emit(Page.Events.Download, this); + this._fireDownloadEvent(); + } + + page(): Page { + return this._page; } _filenameSuggested(suggestedFilename: string) { assert(this._suggestedFilename === undefined); this._suggestedFilename = suggestedFilename; - this._page.emit(Page.Events.Download, this); + this._fireDownloadEvent(); } suggestedFilename(): string { return this._suggestedFilename!; } + + private _fireDownloadEvent() { + this._page.instrumentation.onDownload(this._page, this); + this._page.emit(Page.Events.Download, this); + } } diff --git a/packages/playwright-core/src/server/firefox/ffPage.ts b/packages/playwright-core/src/server/firefox/ffPage.ts index 03a27954dd..9dfffc1eb3 100644 --- a/packages/playwright-core/src/server/firefox/ffPage.ts +++ b/packages/playwright-core/src/server/firefox/ffPage.ts @@ -163,16 +163,15 @@ export class FFPage implements PageDelegate { if (!frame) return; const delegate = new FFExecutionContext(this._session, executionContextId); - let worldName: types.World; + let worldName: types.World|null = null; if (auxData.name === UTILITY_WORLD_NAME) worldName = 'utility'; else if (!auxData.name) worldName = 'main'; - else - return; const context = new dom.FrameExecutionContext(delegate, frame, worldName); (context as any)[contextDelegateSymbol] = delegate; - frame._contextCreated(worldName, context); + if (worldName) + frame._contextCreated(worldName, context); this._contextIdToContext.set(executionContextId, context); } diff --git a/packages/playwright-core/src/server/injected/recorder/pollingRecorder.ts b/packages/playwright-core/src/server/injected/recorder/pollingRecorder.ts new file mode 100644 index 0000000000..57627f3723 --- /dev/null +++ b/packages/playwright-core/src/server/injected/recorder/pollingRecorder.ts @@ -0,0 +1,91 @@ +/** + * 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 type { Mode, OverlayState, UIState } from '@recorder/recorderTypes'; +import type * as actions from '../../recorder/recorderActions'; +import type { InjectedScript } from '../injectedScript'; +import { Recorder } from './recorder'; +import type { RecorderDelegate } from './recorder'; + +interface Embedder { + __pw_recorderPerformAction(action: actions.PerformOnRecordAction): Promise; + __pw_recorderRecordAction(action: actions.Action): Promise; + __pw_recorderState(): Promise; + __pw_recorderSetSelector(selector: string): Promise; + __pw_recorderSetMode(mode: Mode): Promise; + __pw_recorderSetOverlayState(state: OverlayState): Promise; + __pw_refreshOverlay(): void; +} + +export class PollingRecorder implements RecorderDelegate { + private _recorder: Recorder; + private _embedder: Embedder; + private _pollRecorderModeTimer: number | undefined; + + constructor(injectedScript: InjectedScript) { + this._recorder = new Recorder(injectedScript); + this._embedder = injectedScript.window as any; + + injectedScript.onGlobalListenersRemoved.add(() => this._recorder.installListeners()); + + const refreshOverlay = () => { + this._pollRecorderMode().catch(e => console.log(e)); // eslint-disable-line no-console + }; + this._embedder.__pw_refreshOverlay = refreshOverlay; + refreshOverlay(); + } + + private async _pollRecorderMode() { + const pollPeriod = 1000; + if (this._pollRecorderModeTimer) + clearTimeout(this._pollRecorderModeTimer); + const state = await this._embedder.__pw_recorderState().catch(() => {}); + if (!state) { + this._pollRecorderModeTimer = this._recorder.injectedScript.builtinSetTimeout(() => this._pollRecorderMode(), pollPeriod); + return; + } + const win = this._recorder.document.defaultView!; + if (win.top !== win) { + // Only show action point in the main frame, since it is relative to the page's viewport. + // Otherwise we'll see multiple action points at different locations. + state.actionPoint = undefined; + } + this._recorder.setUIState(state, this); + this._pollRecorderModeTimer = this._recorder.injectedScript.builtinSetTimeout(() => this._pollRecorderMode(), pollPeriod); + } + + async performAction(action: actions.PerformOnRecordAction) { + await this._embedder.__pw_recorderPerformAction(action); + } + + async recordAction(action: actions.Action): Promise { + await this._embedder.__pw_recorderRecordAction(action); + } + + async setSelector(selector: string): Promise { + await this._embedder.__pw_recorderSetSelector(selector); + } + + async setMode(mode: Mode): Promise { + await this._embedder.__pw_recorderSetMode(mode); + } + + async setOverlayState(state: OverlayState): Promise { + await this._embedder.__pw_recorderSetOverlayState(state); + } +} + +export default PollingRecorder; diff --git a/packages/playwright-core/src/server/injected/recorder/recorder.ts b/packages/playwright-core/src/server/injected/recorder/recorder.ts index 76d5791b64..757b413704 100644 --- a/packages/playwright-core/src/server/injected/recorder/recorder.ts +++ b/packages/playwright-core/src/server/injected/recorder/recorder.ts @@ -21,9 +21,8 @@ import type { Mode, OverlayState, UIState } from '@recorder/recorderTypes'; import type { ElementText } from '../selectorUtils'; import type { Highlight, HighlightOptions } from '../highlight'; import clipPaths from './clipPaths'; -import type { SimpleDomNode } from '../simpleDom'; -interface RecorderDelegate { +export interface RecorderDelegate { performAction?(action: actions.PerformOnRecordAction): Promise; recordAction?(action: actions.Action): Promise; setSelector?(selector: string): Promise; @@ -206,7 +205,7 @@ class InspectTool implements RecorderTool { class RecordActionTool implements RecorderTool { private _recorder: Recorder; - private _performingAction = false; + private _performingAction: actions.PerformOnRecordAction | null = null; private _hoveredModel: HighlightModel | null = null; private _hoveredElement: HTMLElement | null = null; private _activeModel: HighlightModel | null = null; @@ -509,9 +508,15 @@ class RecordActionTool implements RecorderTool { private _actionInProgress(event: Event): boolean { // If Playwright is performing action for us, bail. - if (this._performingAction) + const isKeyEvent = event instanceof KeyboardEvent; + if (this._performingAction?.name === 'press' && isKeyEvent && event.key === this._performingAction.key) return true; - // Consume as the first thing. + + const isMouseOrPointerEvent = event instanceof MouseEvent || event instanceof PointerEvent; + if (isMouseOrPointerEvent && (this._performingAction?.name === 'click' || this._performingAction?.name === 'check' || this._performingAction?.name === 'uncheck')) + return true; + + // Consume event if action is not being executed. consumeEvent(event); return false; } @@ -535,9 +540,9 @@ class RecordActionTool implements RecorderTool { this._hoveredModel = null; this._activeModel = null; this._recorder.updateHighlight(null, false); - this._performingAction = true; + this._performingAction = action; void this._recorder.performAction(action).then(() => { - this._performingAction = false; + this._performingAction = null; // If that was a keyboard action, it similarly requires new selectors for active model. this._onFocus(false); @@ -1457,73 +1462,3 @@ function createSvgElement(doc: Document, { tagName, attrs, children }: SvgJson): return elem; } - -interface Embedder { - __pw_recorderPerformAction(action: actions.PerformOnRecordAction, simpleDomNode?: SimpleDomNode): Promise; - __pw_recorderRecordAction(action: actions.Action, simpleDomNode?: SimpleDomNode): Promise; - __pw_recorderState(): Promise; - __pw_recorderSetSelector(selector: string): Promise; - __pw_recorderSetMode(mode: Mode): Promise; - __pw_recorderSetOverlayState(state: OverlayState): Promise; - __pw_refreshOverlay(): void; -} - -export class PollingRecorder implements RecorderDelegate { - private _recorder: Recorder; - private _embedder: Embedder; - private _pollRecorderModeTimer: number | undefined; - - constructor(injectedScript: InjectedScript) { - this._recorder = new Recorder(injectedScript); - this._embedder = injectedScript.window as any; - - injectedScript.onGlobalListenersRemoved.add(() => this._recorder.installListeners()); - - const refreshOverlay = () => { - this._pollRecorderMode().catch(e => console.log(e)); // eslint-disable-line no-console - }; - this._embedder.__pw_refreshOverlay = refreshOverlay; - refreshOverlay(); - } - - private async _pollRecorderMode() { - const pollPeriod = 1000; - if (this._pollRecorderModeTimer) - clearTimeout(this._pollRecorderModeTimer); - const state = await this._embedder.__pw_recorderState().catch(() => {}); - if (!state) { - this._pollRecorderModeTimer = this._recorder.injectedScript.builtinSetTimeout(() => this._pollRecorderMode(), pollPeriod); - return; - } - const win = this._recorder.document.defaultView!; - if (win.top !== win) { - // Only show action point in the main frame, since it is relative to the page's viewport. - // Otherwise we'll see multiple action points at different locations. - state.actionPoint = undefined; - } - this._recorder.setUIState(state, this); - this._pollRecorderModeTimer = this._recorder.injectedScript.builtinSetTimeout(() => this._pollRecorderMode(), pollPeriod); - } - - async performAction(action: actions.PerformOnRecordAction, simpleDomNode?: SimpleDomNode) { - await this._embedder.__pw_recorderPerformAction(action, simpleDomNode); - } - - async recordAction(action: actions.Action, simpleDomNode?: SimpleDomNode): Promise { - await this._embedder.__pw_recorderRecordAction(action, simpleDomNode); - } - - async setSelector(selector: string): Promise { - await this._embedder.__pw_recorderSetSelector(selector); - } - - async setMode(mode: Mode): Promise { - await this._embedder.__pw_recorderSetMode(mode); - } - - async setOverlayState(state: OverlayState): Promise { - await this._embedder.__pw_recorderSetOverlayState(state); - } -} - -export default PollingRecorder; diff --git a/packages/playwright-core/src/server/instrumentation.ts b/packages/playwright-core/src/server/instrumentation.ts index b4628ac904..4d29be0284 100644 --- a/packages/playwright-core/src/server/instrumentation.ts +++ b/packages/playwright-core/src/server/instrumentation.ts @@ -35,6 +35,8 @@ export type Attribution = { }; import type { CallMetadata } from '@protocol/callMetadata'; +import type { Dialog } from './dialog'; +import type { Download } from './download'; export type { CallMetadata } from '@protocol/callMetadata'; export class SdkObject extends EventEmitter { @@ -62,6 +64,8 @@ export interface Instrumentation { onPageClose(page: Page): void; onBrowserOpen(browser: Browser): void; onBrowserClose(browser: Browser): void; + onDialog(dialog: Dialog): void; + onDownload(page: Page, download: Download): void; } export interface InstrumentationListener { @@ -73,6 +77,8 @@ export interface InstrumentationListener { onPageClose?(page: Page): void; onBrowserOpen?(browser: Browser): void; onBrowserClose?(browser: Browser): void; + onDialog?(dialog: Dialog): void; + onDownload?(page: Page, download: Download): void; } export function createInstrumentation(): Instrumentation { diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts index ddaa035811..19776c8a29 100644 --- a/packages/playwright-core/src/server/recorder.ts +++ b/packages/playwright-core/src/server/recorder.ts @@ -45,32 +45,35 @@ export class Recorder implements InstrumentationListener, IRecorder { private _omitCallTracking = false; private _currentLanguage: Language; - static showInspector(context: BrowserContext, recorderAppFactory: IRecorderAppFactory) { - const params: channels.BrowserContextRecorderSupplementEnableParams = {}; + static async showInspector(context: BrowserContext, params: channels.BrowserContextEnableRecorderParams, recorderAppFactory: IRecorderAppFactory) { if (isUnderTest()) params.language = process.env.TEST_INSPECTOR_LANGUAGE; - Recorder.show(context, recorderAppFactory, params).catch(() => {}); + return await Recorder.show('actions', context, recorderAppFactory, params); } - static show(context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextRecorderSupplementEnableParams = {}): Promise { + static showInspectorNoReply(context: BrowserContext, recorderAppFactory: IRecorderAppFactory) { + Recorder.showInspector(context, {}, recorderAppFactory).catch(() => {}); + } + + static show(codegenMode: 'actions' | 'trace-events', context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextEnableRecorderParams): Promise { let recorderPromise = (context as any)[recorderSymbol] as Promise; if (!recorderPromise) { - recorderPromise = Recorder._create(context, recorderAppFactory, params); + recorderPromise = Recorder._create(codegenMode, context, recorderAppFactory, params); (context as any)[recorderSymbol] = recorderPromise; } return recorderPromise; } - private static async _create(context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextRecorderSupplementEnableParams = {}): Promise { - const recorder = new Recorder(context, params); + private static async _create(codegenMode: 'actions' | 'trace-events', context: BrowserContext, recorderAppFactory: IRecorderAppFactory, params: channels.BrowserContextEnableRecorderParams = {}): Promise { + const recorder = new Recorder(codegenMode, context, params); const recorderApp = await recorderAppFactory(recorder); await recorder._install(recorderApp); return recorder; } - constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams) { + constructor(codegenMode: 'actions' | 'trace-events', context: BrowserContext, params: channels.BrowserContextEnableRecorderParams) { this._mode = params.mode || 'none'; - this._contextRecorder = new ContextRecorder(context, params, {}); + this._contextRecorder = new ContextRecorder(codegenMode, context, params, {}); this._context = context; this._omitCallTracking = !!params.omitCallTracking; this._debugger = context.debugger(); diff --git a/packages/playwright-core/src/server/recorder/DEPS.list b/packages/playwright-core/src/server/recorder/DEPS.list index f3bbfc23bf..85ae7c9152 100644 --- a/packages/playwright-core/src/server/recorder/DEPS.list +++ b/packages/playwright-core/src/server/recorder/DEPS.list @@ -5,7 +5,7 @@ ../isomorphic/** ../registry/** ../../common/ -../../generated/recorderSource.ts +../../generated/pollingRecorderSource.ts ../../protocol/ ../../utils/** ../../utilsBundle.ts diff --git a/packages/playwright-core/src/server/recorder/contextRecorder.ts b/packages/playwright-core/src/server/recorder/contextRecorder.ts index 856305f300..a02ea9f5b4 100644 --- a/packages/playwright-core/src/server/recorder/contextRecorder.ts +++ b/packages/playwright-core/src/server/recorder/contextRecorder.ts @@ -17,7 +17,7 @@ import type * as channels from '@protocol/channels'; import type { Source } from '@recorder/recorderTypes'; import { EventEmitter } from 'events'; -import * as recorderSource from '../../generated/recorderSource'; +import * as recorderSource from '../../generated/pollingRecorderSource'; import { eventsHelper, monotonicTime, quoteCSSAttributeValue, type RegisteredListener } from '../../utils'; import { raceAgainstDeadline } from '../../utils/timeoutRunner'; import { BrowserContext } from '../browserContext'; @@ -48,15 +48,17 @@ export class ContextRecorder extends EventEmitter { private _lastDialogOrdinal = -1; private _lastDownloadOrdinal = -1; private _context: BrowserContext; - private _params: channels.BrowserContextRecorderSupplementEnableParams; + private _params: channels.BrowserContextEnableRecorderParams; private _delegate: ContextRecorderDelegate; private _recorderSources: Source[]; private _throttledOutputFile: ThrottledFile | null = null; private _orderedLanguages: LanguageGenerator[] = []; private _listeners: RegisteredListener[] = []; + private _codegenMode: 'actions' | 'trace-events'; - constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams, delegate: ContextRecorderDelegate) { + constructor(codegenMode: 'actions' | 'trace-events', context: BrowserContext, params: channels.BrowserContextEnableRecorderParams, delegate: ContextRecorderDelegate) { super(); + this._codegenMode = codegenMode; this._context = context; this._params = params; this._delegate = delegate; @@ -73,8 +75,8 @@ export class ContextRecorder extends EventEmitter { saveStorage: params.saveStorage, }; - const collection = new RecorderCollection(context, this._pageAliases, params.mode === 'recording'); - collection.on('change', (actions: ActionInContext[]) => { + this._collection = new RecorderCollection(codegenMode, context, this._pageAliases); + this._collection.on('change', (actions: ActionInContext[]) => { this._recorderSources = []; for (const languageGenerator of this._orderedLanguages) { const { header, footer, actionTexts, text } = generateCode(actions, languageGenerator, languageGeneratorOptions); @@ -103,7 +105,7 @@ export class ContextRecorder extends EventEmitter { this._listeners.push(eventsHelper.addEventListener(process, 'exit', () => { this._throttledOutputFile?.flush(); })); - this._collection = collection; + this.setEnabled(true); } setOutput(codegenId: string, outputFile?: string) { @@ -145,6 +147,12 @@ export class ContextRecorder extends EventEmitter { setEnabled(enabled: boolean) { this._collection.setEnabled(enabled); + if (this._codegenMode === 'trace-events') { + if (enabled) + this._context.tracing.startChunk({ name: 'trace', title: 'trace' }).catch(() => {}); + else + this._context.tracing.stopChunk({ mode: 'discard' }).catch(() => {}); + } } dispose() { diff --git a/packages/playwright-core/src/server/recorder/recorderApp.ts b/packages/playwright-core/src/server/recorder/recorderApp.ts index c7120ef408..41c92a3198 100644 --- a/packages/playwright-core/src/server/recorder/recorderApp.ts +++ b/packages/playwright-core/src/server/recorder/recorderApp.ts @@ -81,7 +81,6 @@ export class RecorderApp extends EventEmitter implements IRecorderApp { const file = require.resolve('../../vite/recorder/' + uri); fs.promises.readFile(file).then(buffer => { route.fulfill({ - requestUrl: route.request().url(), status: 200, headers: [ { name: 'Content-Type', value: mime.getType(path.extname(file)) || 'application/octet-stream' } @@ -162,8 +161,10 @@ export class RecorderApp extends EventEmitter implements IRecorderApp { }).toString(), { isFunction: true }, sources).catch(() => {}); // Testing harness for runCLI mode. - if (process.env.PWTEST_CLI_IS_UNDER_TEST && sources.length) - (process as any)._didSetSourcesForTest(sources[0].text); + if (process.env.PWTEST_CLI_IS_UNDER_TEST && sources.length) { + if ((process as any)._didSetSourcesForTest(sources[0].text)) + this.close(); + } } async setSelector(selector: string, userGesture?: boolean): Promise { diff --git a/packages/playwright-core/src/server/recorder/recorderCollection.ts b/packages/playwright-core/src/server/recorder/recorderCollection.ts index 5b0d0b5b9e..1706de39ee 100644 --- a/packages/playwright-core/src/server/recorder/recorderCollection.ts +++ b/packages/playwright-core/src/server/recorder/recorderCollection.ts @@ -29,17 +29,16 @@ import type { BrowserContext } from '../browserContext'; export class RecorderCollection extends EventEmitter { private _actions: ActionInContext[] = []; - private _enabled: boolean; + private _enabled = false; private _pageAliases: Map; private _context: BrowserContext; - constructor(context: BrowserContext, pageAliases: Map, enabled: boolean) { + constructor(codegenMode: 'actions' | 'trace-events', context: BrowserContext, pageAliases: Map) { super(); this._context = context; - this._enabled = enabled; this._pageAliases = pageAliases; - if (process.env.PW_RECORDER_IS_TRACE_VIEWER) { + if (codegenMode === 'trace-events') { this._context.tracing.onMemoryEvents(events => { this._actions = traceEventsToAction(events); this._fireChange(); diff --git a/packages/playwright-core/src/server/recorder/recorderInTraceViewer.ts b/packages/playwright-core/src/server/recorder/recorderInTraceViewer.ts index 8f08b969e1..4c84547ade 100644 --- a/packages/playwright-core/src/server/recorder/recorderInTraceViewer.ts +++ b/packages/playwright-core/src/server/recorder/recorderInTraceViewer.ts @@ -21,77 +21,101 @@ import type { IRecorder, IRecorderApp, IRecorderAppFactory } from './recorderFro import { installRootRedirect, openTraceViewerApp, startTraceViewerServer } from '../trace/viewer/traceViewer'; import type { TraceViewerServerOptions } from '../trace/viewer/traceViewer'; import type { BrowserContext } from '../browserContext'; -import { gracefullyProcessExitDoNotHang } from '../../utils/processLauncher'; -import type { Transport } from '../../utils/httpServer'; +import type { HttpServer, Transport } from '../../utils/httpServer'; +import type { Page } from '../page'; +import { ManualPromise } from '../../utils/manualPromise'; export class RecorderInTraceViewer extends EventEmitter implements IRecorderApp { readonly wsEndpointForTest: string | undefined; - private _recorder: IRecorder; - private _transport: Transport; + private _transport: RecorderTransport; + private _tracePage: Page; + private _traceServer: HttpServer; static factory(context: BrowserContext): IRecorderAppFactory { return async (recorder: IRecorder) => { const transport = new RecorderTransport(); const trace = path.join(context._browser.options.tracesDir, 'trace'); - const wsEndpointForTest = await openApp(trace, { transport, headless: !context._browser.options.headful }); - return new RecorderInTraceViewer(context, recorder, transport, wsEndpointForTest); + const { wsEndpointForTest, tracePage, traceServer } = await openApp(trace, { transport, headless: !context._browser.options.headful }); + return new RecorderInTraceViewer(transport, tracePage, traceServer, wsEndpointForTest); }; } - constructor(context: BrowserContext, recorder: IRecorder, transport: Transport, wsEndpointForTest: string | undefined) { + constructor(transport: RecorderTransport, tracePage: Page, traceServer: HttpServer, wsEndpointForTest: string | undefined) { super(); - this._recorder = recorder; this._transport = transport; + this._transport.eventSink.resolve(this); + this._tracePage = tracePage; + this._traceServer = traceServer; this.wsEndpointForTest = wsEndpointForTest; + this._tracePage.once('close', () => { + this.close(); + }); } async close(): Promise { - this._transport.sendEvent?.('close', {}); + await this._tracePage.context().close({ reason: 'Recorder window closed' }); + await this._traceServer.stop(); } async setPaused(paused: boolean): Promise { - this._transport.sendEvent?.('setPaused', { paused }); + this._transport.deliverEvent('setPaused', { paused }); } async setMode(mode: Mode): Promise { - this._transport.sendEvent?.('setMode', { mode }); + this._transport.deliverEvent('setMode', { mode }); } async setFile(file: string): Promise { - this._transport.sendEvent?.('setFileIfNeeded', { file }); + this._transport.deliverEvent('setFileIfNeeded', { file }); } async setSelector(selector: string, userGesture?: boolean): Promise { - this._transport.sendEvent?.('setSelector', { selector, userGesture }); + this._transport.deliverEvent('setSelector', { selector, userGesture }); } async updateCallLogs(callLogs: CallLog[]): Promise { - this._transport.sendEvent?.('updateCallLogs', { callLogs }); + this._transport.deliverEvent('updateCallLogs', { callLogs }); } async setSources(sources: Source[]): Promise { - this._transport.sendEvent?.('setSources', { sources }); + this._transport.deliverEvent('setSources', { sources }); + if (process.env.PWTEST_CLI_IS_UNDER_TEST && sources.length) { + if ((process as any)._didSetSourcesForTest(sources[0].text)) + this.close(); + } } } -async function openApp(trace: string, options?: TraceViewerServerOptions & { headless?: boolean }): Promise { - const server = await startTraceViewerServer(options); - await installRootRedirect(server, [trace], { ...options, webApp: 'recorder.html' }); - const page = await openTraceViewerApp(server.urlPrefix('precise'), 'chromium', options); - page.on('close', () => gracefullyProcessExitDoNotHang(0)); - return page.context()._browser.options.wsEndpoint; +async function openApp(trace: string, options?: TraceViewerServerOptions & { headless?: boolean }): Promise<{ wsEndpointForTest: string | undefined, tracePage: Page, traceServer: HttpServer }> { + const traceServer = await startTraceViewerServer(options); + await installRootRedirect(traceServer, [trace], { ...options, webApp: 'recorder.html' }); + const page = await openTraceViewerApp(traceServer.urlPrefix('precise'), 'chromium', options); + return { wsEndpointForTest: page.context()._browser.options.wsEndpoint, tracePage: page, traceServer }; } class RecorderTransport implements Transport { + private _connected = new ManualPromise(); + readonly eventSink = new ManualPromise(); + constructor() { } - async dispatch(method: string, params: any) { + onconnect() { + this._connected.resolve(); + } + + async dispatch(method: string, params: any): Promise { + const eventSink = await this.eventSink; + eventSink.emit('event', { event: method, params }); } onclose() { } + deliverEvent(method: string, params: any) { + this._connected.then(() => this.sendEvent?.(method, params)); + } + sendEvent?: (method: string, params: any) => void; close?: () => void; } diff --git a/packages/playwright-core/src/server/recorder/recorderUtils.ts b/packages/playwright-core/src/server/recorder/recorderUtils.ts index 29186e9f96..3cde57052a 100644 --- a/packages/playwright-core/src/server/recorder/recorderUtils.ts +++ b/packages/playwright-core/src/server/recorder/recorderUtils.ts @@ -25,7 +25,7 @@ import type * as trace from '@trace/trace'; import { fromKeyboardModifiers, toKeyboardModifiers } from '../codegen/language'; import { serializeExpectedTextValues } from '../../utils/expectUtils'; import { createGuid, monotonicTime } from '../../utils'; -import { serializeValue } from '../../protocol/serializers'; +import { parseSerializedValue, serializeValue } from '../../protocol/serializers'; import type { SmartKeyboardModifier } from '../types'; export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus): CallLog { @@ -158,7 +158,7 @@ export function traceParamsForAction(actionInContext: ActionInContext): { method const params: channels.FrameExpectParams = { selector: action.selector, expression: 'to.be.checked', - isNot: action.checked, + isNot: !action.checked, }; return { method: 'expect', params }; } @@ -166,7 +166,7 @@ export function traceParamsForAction(actionInContext: ActionInContext): { method const params: channels.FrameExpectParams = { selector, expression: 'to.have.text', - expectedText: serializeExpectedTextValues([action.text], { matchSubstring: true, normalizeWhiteSpace: true }), + expectedText: serializeExpectedTextValues([action.text], { matchSubstring: action.substring, normalizeWhiteSpace: true }), isNot: false, }; return { method: 'expect', params }; @@ -193,12 +193,12 @@ export function traceParamsForAction(actionInContext: ActionInContext): { method export function callMetadataForAction(pageAliases: Map, actionInContext: ActionInContext): { callMetadata: CallMetadata, mainFrame: Frame } { const mainFrame = mainFrameForAction(pageAliases, actionInContext); - const { action } = actionInContext; const { method, params } = traceParamsForAction(actionInContext); + const callMetadata: CallMetadata = { id: `call@${createGuid()}`, stepId: `recorder@${createGuid()}`, - apiName: 'frame.' + action.name, + apiName: 'page.' + method, objectId: mainFrame.guid, pageId: mainFrame._page.guid, frameId: mainFrame.guid, @@ -215,38 +215,70 @@ export function callMetadataForAction(pageAliases: Map, actionInCo export function traceEventsToAction(events: trace.TraceEvent[]): ActionInContext[] { const result: ActionInContext[] = []; const pageAliases = new Map(); + let lastDownloadOrdinal = 0; + let lastDialogOrdinal = 0; + + const addSignal = (signal: actions.Signal) => { + const lastAction = result[result.length - 1]; + if (!lastAction) + return; + lastAction.action.signals.push(signal); + }; for (const event of events) { - if (event.type === 'event' && event.class === 'BrowserContext' && event.method === 'page') { - const pageAlias = 'page' + pageAliases.size; - pageAliases.set(event.params.pageId, pageAlias); - const lastAction = result[result.length - 1]; - lastAction.action.signals.push({ - name: 'popup', - popupAlias: pageAlias, - }); - result.push({ - frame: { pageAlias, framePath: [] }, - action: { - name: 'openPage', - url: '', - signals: [], - }, - timestamp: event.time, - }); - continue; - } + if (event.type === 'event' && event.class === 'BrowserContext') { + const { method, params } = event; + if (method === 'page') { + const pageAlias = 'page' + (pageAliases.size || ''); + pageAliases.set(params.pageId, pageAlias); + addSignal({ + name: 'popup', + popupAlias: pageAlias, + }); + result.push({ + frame: { pageAlias, framePath: [] }, + action: { + name: 'openPage', + url: '', + signals: [], + }, + timestamp: event.time, + }); + continue; + } - if (event.type === 'event' && event.class === 'BrowserContext' && event.method === 'pageClosed') { - const pageAlias = pageAliases.get(event.params.pageId) || 'page'; - result.push({ - frame: { pageAlias, framePath: [] }, - action: { - name: 'closePage', - signals: [], - }, - timestamp: event.time, - }); + if (method === 'pageClosed') { + const pageAlias = pageAliases.get(event.params.pageId) || 'page'; + result.push({ + frame: { pageAlias, framePath: [] }, + action: { + name: 'closePage', + signals: [], + }, + timestamp: event.time, + }); + continue; + } + + if (method === 'download') { + const downloadAlias = lastDownloadOrdinal ? String(lastDownloadOrdinal) : ''; + ++lastDownloadOrdinal; + addSignal({ + name: 'download', + downloadAlias, + }); + continue; + } + + if (method === 'dialog') { + const dialogAlias = lastDialogOrdinal ? String(lastDialogOrdinal) : ''; + ++lastDialogOrdinal; + addSignal({ + name: 'dialog', + dialogAlias, + }); + continue; + } continue; } @@ -389,6 +421,67 @@ export function traceEventsToAction(events: trace.TraceEvent[]): ActionInContext }); continue; } + if (method === 'expect') { + const params = untypedParams as channels.FrameExpectParams; + if (params.expression === 'to.have.text') { + const entry = params.expectedText?.[0]; + result.push({ + frame: { pageAlias, framePath: [] }, + action: { + name: 'assertText', + selector: params.selector, + signals: [], + text: entry?.string!, + substring: !!entry?.matchSubstring, + }, + timestamp: event.startTime + }); + continue; + } + + if (params.expression === 'to.have.value') { + result.push({ + frame: { pageAlias, framePath: [] }, + action: { + name: 'assertValue', + selector: params.selector, + signals: [], + value: parseSerializedValue(params.expectedValue!.value, params.expectedValue!.handles), + }, + timestamp: event.startTime + }); + continue; + } + + if (params.expression === 'to.be.checked') { + result.push({ + frame: { pageAlias, framePath: [] }, + action: { + name: 'assertChecked', + selector: params.selector, + signals: [], + checked: !params.isNot, + }, + timestamp: event.startTime + }); + continue; + } + + if (params.expression === 'to.be.visible') { + result.push({ + frame: { pageAlias, framePath: [] }, + action: { + name: 'assertVisible', + selector: params.selector, + signals: [], + }, + timestamp: event.startTime + }); + continue; + } + + continue; + } } return result; diff --git a/packages/playwright-core/src/server/trace/recorder/tracing.ts b/packages/playwright-core/src/server/trace/recorder/tracing.ts index 44e6bd7f1f..83b7fe6120 100644 --- a/packages/playwright-core/src/server/trace/recorder/tracing.ts +++ b/packages/playwright-core/src/server/trace/recorder/tracing.ts @@ -38,6 +38,8 @@ import { Snapshotter } from './snapshotter'; import type { ConsoleMessage } from '../../console'; import { Dispatcher } from '../../dispatchers/dispatcher'; import { serializeError } from '../../errors'; +import type { Dialog } from '../../dialog'; +import type { Download } from '../../download'; const version: trace.VERSION = 7; @@ -454,6 +456,28 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps this._appendTraceEvent(event); } + onDialog(dialog: Dialog) { + const event: trace.EventTraceEvent = { + type: 'event', + time: monotonicTime(), + class: 'BrowserContext', + method: 'dialog', + params: { pageId: dialog.page().guid, type: dialog.type(), message: dialog.message(), defaultValue: dialog.defaultValue() }, + }; + this._appendTraceEvent(event); + } + + onDownload(page: Page, download: Download) { + const event: trace.EventTraceEvent = { + type: 'event', + time: monotonicTime(), + class: 'BrowserContext', + method: 'download', + params: { pageId: page.guid, url: download.url, suggestedFilename: download.suggestedFilename() }, + }; + this._appendTraceEvent(event); + } + onPageOpen(page: Page) { const event: trace.EventTraceEvent = { type: 'event', diff --git a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts index 6ca0319aa3..6d45e7c6f5 100644 --- a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts +++ b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts @@ -46,7 +46,6 @@ export type TraceViewerRedirectOptions = { reporter?: string[]; webApp?: string; isServer?: boolean; - outputDir?: string; updateSnapshots?: 'all' | 'none' | 'missing'; }; @@ -133,8 +132,6 @@ export async function installRootRedirect(server: HttpServer, traceUrls: string[ params.append('timeout', String(options.timeout)); if (options.headed) params.append('headed', ''); - if (options.outputDir) - params.append('outputDir', options.outputDir); if (options.updateSnapshots) params.append('updateSnapshots', options.updateSnapshots); for (const reporter of options.reporter || []) @@ -223,6 +220,9 @@ class StdinServer implements Transport { process.stdin.on('close', () => gracefullyProcessExitDoNotHang(0)); } + onconnect() { + } + async dispatch(method: string, params: any) { if (method === 'initialize') { if (this._traceUrl) diff --git a/packages/playwright-core/src/server/webkit/wkPage.ts b/packages/playwright-core/src/server/webkit/wkPage.ts index 2f579b619b..320df04ce2 100644 --- a/packages/playwright-core/src/server/webkit/wkPage.ts +++ b/packages/playwright-core/src/server/webkit/wkPage.ts @@ -502,16 +502,15 @@ export class WKPage implements PageDelegate { if (!frame) return; const delegate = new WKExecutionContext(this._session, contextPayload.id); - let worldName: types.World; + let worldName: types.World|null = null; if (contextPayload.type === 'normal') worldName = 'main'; else if (contextPayload.type === 'user' && contextPayload.name === UTILITY_WORLD_NAME) worldName = 'utility'; - else - return; const context = new dom.FrameExecutionContext(delegate, frame, worldName); (context as any)[contextDelegateSymbol] = delegate; - frame._contextCreated(worldName, context); + if (worldName) + frame._contextCreated(worldName, context); this._contextIdToContext.set(contextPayload.id, context); } @@ -1116,7 +1115,7 @@ export class WKPage implements PageDelegate { const response = request.createResponse(event.response); this._page._frameManager.requestReceivedResponse(response); - if (response.status() === 204) { + if (response.status() === 204 && request.request.isNavigationRequest()) { this._onLoadingFailed(session, { requestId: event.requestId, errorText: 'Aborted: 204 No Content', diff --git a/packages/playwright-core/src/utils/httpServer.ts b/packages/playwright-core/src/utils/httpServer.ts index 24a84ea502..8da2a0e0d0 100644 --- a/packages/playwright-core/src/utils/httpServer.ts +++ b/packages/playwright-core/src/utils/httpServer.ts @@ -27,8 +27,9 @@ export type ServerRouteHandler = (request: http.IncomingMessage, response: http. export type Transport = { sendEvent?: (method: string, params: any) => void; - dispatch: (method: string, params: any) => Promise; close?: () => void; + onconnect: () => void; + dispatch: (method: string, params: any) => Promise; onclose: () => void; }; @@ -82,6 +83,7 @@ export class HttpServer { this._wsGuid = guid || createGuid(); const wss = new wsServer({ server: this._server, path: '/' + this._wsGuid }); wss.on('connection', ws => { + transport.onconnect(); transport.sendEvent = (method, params) => ws.send(JSON.stringify({ method, params })); transport.close = () => ws.close(); ws.on('message', async message => { diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index d6d4e7f39b..e0c640db6f 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -2120,7 +2120,9 @@ export interface Page { /** * When set, this method only performs the [actionability](https://playwright.dev/docs/actionability) checks and skips the action. Defaults - * to `false`. Useful to wait until the element is ready for the action without performing it. + * to `false`. Useful to wait until the element is ready for the action without performing it. Note that keyboard + * `modifiers` will be pressed regardless of `trial` to allow testing elements which are only visible when those keys + * are pressed. */ trial?: boolean; }): Promise; @@ -2233,7 +2235,9 @@ export interface Page { /** * When set, this method only performs the [actionability](https://playwright.dev/docs/actionability) checks and skips the action. Defaults - * to `false`. Useful to wait until the element is ready for the action without performing it. + * to `false`. Useful to wait until the element is ready for the action without performing it. Note that keyboard + * `modifiers` will be pressed regardless of `trial` to allow testing elements which are only visible when those keys + * are pressed. */ trial?: boolean; }): Promise; @@ -3125,7 +3129,9 @@ export interface Page { /** * When set, this method only performs the [actionability](https://playwright.dev/docs/actionability) checks and skips the action. Defaults - * to `false`. Useful to wait until the element is ready for the action without performing it. + * to `false`. Useful to wait until the element is ready for the action without performing it. Note that keyboard + * `modifiers` will be pressed regardless of `trial` to allow testing elements which are only visible when those keys + * are pressed. */ trial?: boolean; }): Promise; @@ -4266,7 +4272,9 @@ export interface Page { /** * When set, this method only performs the [actionability](https://playwright.dev/docs/actionability) checks and skips the action. Defaults - * to `false`. Useful to wait until the element is ready for the action without performing it. + * to `false`. Useful to wait until the element is ready for the action without performing it. Note that keyboard + * `modifiers` will be pressed regardless of `trial` to allow testing elements which are only visible when those keys + * are pressed. */ trial?: boolean; }): Promise; @@ -5845,7 +5853,9 @@ export interface Frame { /** * When set, this method only performs the [actionability](https://playwright.dev/docs/actionability) checks and skips the action. Defaults - * to `false`. Useful to wait until the element is ready for the action without performing it. + * to `false`. Useful to wait until the element is ready for the action without performing it. Note that keyboard + * `modifiers` will be pressed regardless of `trial` to allow testing elements which are only visible when those keys + * are pressed. */ trial?: boolean; }): Promise; @@ -5931,7 +5941,9 @@ export interface Frame { /** * When set, this method only performs the [actionability](https://playwright.dev/docs/actionability) checks and skips the action. Defaults - * to `false`. Useful to wait until the element is ready for the action without performing it. + * to `false`. Useful to wait until the element is ready for the action without performing it. Note that keyboard + * `modifiers` will be pressed regardless of `trial` to allow testing elements which are only visible when those keys + * are pressed. */ trial?: boolean; }): Promise; @@ -6621,7 +6633,9 @@ export interface Frame { /** * When set, this method only performs the [actionability](https://playwright.dev/docs/actionability) checks and skips the action. Defaults - * to `false`. Useful to wait until the element is ready for the action without performing it. + * to `false`. Useful to wait until the element is ready for the action without performing it. Note that keyboard + * `modifiers` will be pressed regardless of `trial` to allow testing elements which are only visible when those keys + * are pressed. */ trial?: boolean; }): Promise; @@ -7308,7 +7322,9 @@ export interface Frame { /** * When set, this method only performs the [actionability](https://playwright.dev/docs/actionability) checks and skips the action. Defaults - * to `false`. Useful to wait until the element is ready for the action without performing it. + * to `false`. Useful to wait until the element is ready for the action without performing it. Note that keyboard + * `modifiers` will be pressed regardless of `trial` to allow testing elements which are only visible when those keys + * are pressed. */ trial?: boolean; }): Promise; @@ -12045,7 +12061,9 @@ export interface Locator { /** * When set, this method only performs the [actionability](https://playwright.dev/docs/actionability) checks and skips the action. Defaults - * to `false`. Useful to wait until the element is ready for the action without performing it. + * to `false`. Useful to wait until the element is ready for the action without performing it. Note that keyboard + * `modifiers` will be pressed regardless of `trial` to allow testing elements which are only visible when those keys + * are pressed. */ trial?: boolean; }): Promise; @@ -12155,7 +12173,9 @@ export interface Locator { /** * When set, this method only performs the [actionability](https://playwright.dev/docs/actionability) checks and skips the action. Defaults - * to `false`. Useful to wait until the element is ready for the action without performing it. + * to `false`. Useful to wait until the element is ready for the action without performing it. Note that keyboard + * `modifiers` will be pressed regardless of `trial` to allow testing elements which are only visible when those keys + * are pressed. */ trial?: boolean; }): Promise; @@ -12825,7 +12845,9 @@ export interface Locator { /** * When set, this method only performs the [actionability](https://playwright.dev/docs/actionability) checks and skips the action. Defaults - * to `false`. Useful to wait until the element is ready for the action without performing it. + * to `false`. Useful to wait until the element is ready for the action without performing it. Note that keyboard + * `modifiers` will be pressed regardless of `trial` to allow testing elements which are only visible when those keys + * are pressed. */ trial?: boolean; }): Promise; @@ -13626,7 +13648,9 @@ export interface Locator { /** * When set, this method only performs the [actionability](https://playwright.dev/docs/actionability) checks and skips the action. Defaults - * to `false`. Useful to wait until the element is ready for the action without performing it. + * to `false`. Useful to wait until the element is ready for the action without performing it. Note that keyboard + * `modifiers` will be pressed regardless of `trial` to allow testing elements which are only visible when those keys + * are pressed. */ trial?: boolean; }): Promise; diff --git a/packages/playwright/src/isomorphic/testServerInterface.ts b/packages/playwright/src/isomorphic/testServerInterface.ts index 22cb9e35ef..ce1c1888d5 100644 --- a/packages/playwright/src/isomorphic/testServerInterface.ts +++ b/packages/playwright/src/isomorphic/testServerInterface.ts @@ -45,7 +45,7 @@ export interface TestServerInterface { installBrowsers(params: {}): Promise; - runGlobalSetup(params: { outputDir?: string }): Promise<{ + runGlobalSetup(params: {}): Promise<{ report: ReportEntry[], status: reporterTypes.FullResult['status'] }>; @@ -82,7 +82,6 @@ export interface TestServerInterface { locations?: string[]; grep?: string; grepInvert?: string; - outputDir?: string; }): Promise<{ report: ReportEntry[], status: reporterTypes.FullResult['status'] @@ -96,7 +95,6 @@ export interface TestServerInterface { headed?: boolean; workers?: number | string; timeout?: number, - outputDir?: string; updateSnapshots?: 'all' | 'none' | 'missing'; reporters?: string[], trace?: 'on' | 'off'; diff --git a/packages/playwright/src/matchers/expect.ts b/packages/playwright/src/matchers/expect.ts index 3a254ae7bf..16300607d9 100644 --- a/packages/playwright/src/matchers/expect.ts +++ b/packages/playwright/src/matchers/expect.ts @@ -140,8 +140,7 @@ function createExpect(info: ExpectMetaInfo, prefix: string[], customMatchers: Re const wrappedMatchers: any = {}; const extendedMatchers: any = { ...customMatchers }; for (const [name, matcher] of Object.entries(matchers)) { - const key = qualifiedMatcherName(qualifier, name); - wrappedMatchers[key] = function(...args: any[]) { + wrappedMatchers[name] = function(...args: any[]) { const { isNot, promise, utils } = this; const newThis: ExpectMatcherState = { isNot, @@ -152,6 +151,8 @@ function createExpect(info: ExpectMetaInfo, prefix: string[], customMatchers: Re (newThis as any).equals = throwUnsupportedExpectMatcherError; return (matcher as any).call(newThis, ...args); }; + const key = qualifiedMatcherName(qualifier, name); + wrappedMatchers[key] = wrappedMatchers[name]; Object.defineProperty(wrappedMatchers[key], 'name', { value: name }); extendedMatchers[name] = wrappedMatchers[key]; } diff --git a/packages/playwright/src/program.ts b/packages/playwright/src/program.ts index 775ef27065..136606b26c 100644 --- a/packages/playwright/src/program.ts +++ b/packages/playwright/src/program.ts @@ -161,7 +161,7 @@ async function runTests(args: string[], opts: { [key: string]: any }) { 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.`); - const status = await testServer.runUIMode(opts.config, { + const status = await testServer.runUIMode(opts.config, cliOverrides, { host: opts.uiHost, port: opts.uiPort ? +opts.uiPort : undefined, args, @@ -172,7 +172,6 @@ async function runTests(args: string[], opts: { [key: string]: any }) { reporter: Array.isArray(opts.reporter) ? opts.reporter : opts.reporter ? [opts.reporter] : undefined, workers: cliOverrides.workers, timeout: cliOverrides.timeout, - outputDir: cliOverrides.outputDir, updateSnapshots: cliOverrides.updateSnapshots, }); await stopProfiling('runner'); @@ -227,7 +226,7 @@ async function runTests(args: string[], opts: { [key: string]: any }) { async function runTestServer(opts: { [key: string]: any }) { const host = opts.host || 'localhost'; const port = opts.port ? +opts.port : 0; - const status = await testServer.runTestServer(opts.config, { host, port }); + const status = await testServer.runTestServer(opts.config, { }, { host, port }); if (status === 'restarted') return; const exitCode = status === 'interrupted' ? 130 : (status === 'passed' ? 0 : 1); diff --git a/packages/playwright/src/runner/testServer.ts b/packages/playwright/src/runner/testServer.ts index 5d67385dc5..ee3cefaac4 100644 --- a/packages/playwright/src/runner/testServer.ts +++ b/packages/playwright/src/runner/testServer.ts @@ -44,14 +44,16 @@ const originalStderrWrite = process.stderr.write; class TestServer { private _configLocation: ConfigLocation; + private _configCLIOverrides: ConfigCLIOverrides; private _dispatcher: TestServerDispatcher | undefined; - constructor(configLocation: ConfigLocation) { + constructor(configLocation: ConfigLocation, configCLIOverrides: ConfigCLIOverrides) { this._configLocation = configLocation; + this._configCLIOverrides = configCLIOverrides; } async start(options: { host?: string, port?: number }): Promise { - this._dispatcher = new TestServerDispatcher(this._configLocation); + this._dispatcher = new TestServerDispatcher(this._configLocation, this._configCLIOverrides); return await startTraceViewerServer({ ...options, transport: this._dispatcher.transport }); } @@ -63,6 +65,7 @@ class TestServer { export class TestServerDispatcher implements TestServerInterface { private _configLocation: ConfigLocation; + private _configCLIOverrides: ConfigCLIOverrides; private _watcher: Watcher; private _watchedProjectDirs = new Set(); @@ -81,9 +84,11 @@ export class TestServerDispatcher implements TestServerInterface { private _closeOnDisconnect = false; private _populateDependenciesOnList = false; - constructor(configLocation: ConfigLocation) { + constructor(configLocation: ConfigLocation, configCLIOverrides: ConfigCLIOverrides) { this._configLocation = configLocation; + this._configCLIOverrides = configCLIOverrides; this.transport = { + onconnect: () => {}, dispatch: (method, params) => (this as any)[method](params), onclose: () => { if (this._closeOnDisconnect) @@ -144,11 +149,8 @@ export class TestServerDispatcher implements TestServerInterface { async runGlobalSetup(params: Parameters[0]): ReturnType { await this.runGlobalTeardown(); - const overrides: ConfigCLIOverrides = { - outputDir: params.outputDir, - }; const { reporter, report } = await this._collectingInternalReporter(new ListReporter()); - const config = await this._loadConfigOrReportError(reporter, overrides); + const config = await this._loadConfigOrReportError(reporter, this._configCLIOverrides); if (!config) return { status: 'failed', report }; @@ -238,9 +240,9 @@ export class TestServerDispatcher implements TestServerInterface { config?: FullConfigInternal, }> { const overrides: ConfigCLIOverrides = { + ...this._configCLIOverrides, repeatEach: 1, retries: 0, - outputDir: params.outputDir, }; const { reporter, report } = await this._collectingInternalReporter(); const config = await this._loadConfigOrReportError(reporter, overrides); @@ -294,6 +296,7 @@ export class TestServerDispatcher implements TestServerInterface { private async _innerRunTests(params: Parameters[0]): ReturnType { await this.stopTests(); const overrides: ConfigCLIOverrides = { + ...this._configCLIOverrides, repeatEach: 1, retries: 0, preserveOutputDir: true, @@ -306,7 +309,6 @@ export class TestServerDispatcher implements TestServerInterface { _optionContextReuseMode: params.reuseContext ? 'when-possible' : undefined, _optionConnectOptions: params.connectWsEndpoint ? { wsEndpoint: params.connectWsEndpoint } : undefined, }, - outputDir: params.outputDir, updateSnapshots: params.updateSnapshots, workers: params.workers, }; @@ -423,9 +425,9 @@ export class TestServerDispatcher implements TestServerInterface { } } -export async function runUIMode(configFile: string | undefined, options: TraceViewerServerOptions & TraceViewerRedirectOptions): Promise { +export async function runUIMode(configFile: string | undefined, configCLIOverrides: ConfigCLIOverrides, options: TraceViewerServerOptions & TraceViewerRedirectOptions): Promise { const configLocation = resolveConfigLocation(configFile); - return await innerRunTestServer(configLocation, options, async (server: HttpServer, cancelPromise: ManualPromise) => { + return await innerRunTestServer(configLocation, configCLIOverrides, options, async (server: HttpServer, cancelPromise: ManualPromise) => { await installRootRedirect(server, [], { ...options, webApp: 'uiMode.html' }); if (options.host !== undefined || options.port !== undefined) { await openTraceInBrowser(server.urlPrefix('human-readable')); @@ -441,18 +443,18 @@ export async function runUIMode(configFile: string | undefined, options: TraceVi }); } -export async function runTestServer(configFile: string | undefined, options: { host?: string, port?: number }): Promise { +export async function runTestServer(configFile: string | undefined, configCLIOverrides: ConfigCLIOverrides, options: { host?: string, port?: number }): Promise { const configLocation = resolveConfigLocation(configFile); - return await innerRunTestServer(configLocation, options, async server => { + return await innerRunTestServer(configLocation, configCLIOverrides, options, async server => { // eslint-disable-next-line no-console console.log('Listening on ' + server.urlPrefix('precise').replace('http:', 'ws:') + '/' + server.wsGuid()); }); } -async function innerRunTestServer(configLocation: ConfigLocation, options: { host?: string, port?: number }, openUI: (server: HttpServer, cancelPromise: ManualPromise, configLocation: ConfigLocation) => Promise): Promise { +async function innerRunTestServer(configLocation: ConfigLocation, configCLIOverrides: ConfigCLIOverrides, options: { host?: string, port?: number }, openUI: (server: HttpServer, cancelPromise: ManualPromise, configLocation: ConfigLocation) => Promise): Promise { if (restartWithExperimentalTsEsm(undefined, true)) return 'restarted'; - const testServer = new TestServer(configLocation); + const testServer = new TestServer(configLocation, configCLIOverrides); const cancelPromise = new ManualPromise(); const sigintWatcher = new SigIntWatcher(); process.stdin.on('close', () => gracefullyProcessExitDoNotHang(0)); diff --git a/packages/playwright/src/runner/watchMode.ts b/packages/playwright/src/runner/watchMode.ts index a47ca0fe32..bdbe2d5c2d 100644 --- a/packages/playwright/src/runner/watchMode.ts +++ b/packages/playwright/src/runner/watchMode.ts @@ -75,7 +75,7 @@ export async function runWatchModeLoop(configLocation: ConfigLocation, initialOp const options: WatchModeOptions = { ...initialOptions }; let bufferMode = false; - const testServerDispatcher = new TestServerDispatcher(configLocation); + const testServerDispatcher = new TestServerDispatcher(configLocation, {}); const transport = new InMemoryTransport( async data => { const { id, method, params } = JSON.parse(data); @@ -144,11 +144,13 @@ export async function runWatchModeLoop(configLocation: ConfigLocation, initialOp else printPrompt(); + const waitForCommand = readCommand(); const command = await Promise.race([ onDirtyTests, - readCommand(), + waitForCommand.result, ]); - + if (command === 'changed') + waitForCommand.cancel(); if (bufferMode && command === 'changed') continue; @@ -282,7 +284,7 @@ async function runTests(watchOptions: WatchModeOptions, testServerConnection: Te }); } -function readCommand(): ManualPromise { +function readCommand(): { result: Promise, cancel: () => void } { const result = new ManualPromise(); const rl = readline.createInterface({ input: process.stdin, escapeCodeTimeout: 50 }); readline.emitKeypressEvents(process.stdin, rl); @@ -334,13 +336,14 @@ Change settings }; process.stdin.on('keypress', handler); - void result.finally(() => { + const cancel = () => { process.stdin.off('keypress', handler); rl.close(); if (process.stdin.isTTY) process.stdin.setRawMode(false); - }); - return result; + }; + void result.finally(cancel); + return { result, cancel }; } let showBrowserServer: PlaywrightServer | undefined; diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index d7df4e0157..66cf417c87 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -1526,7 +1526,7 @@ export interface BrowserContextChannel extends BrowserContextEventTarget, EventT setOffline(params: BrowserContextSetOfflineParams, metadata?: CallMetadata): Promise; storageState(params?: BrowserContextStorageStateParams, metadata?: CallMetadata): Promise; pause(params?: BrowserContextPauseParams, metadata?: CallMetadata): Promise; - recorderSupplementEnable(params: BrowserContextRecorderSupplementEnableParams, metadata?: CallMetadata): Promise; + enableRecorder(params: BrowserContextEnableRecorderParams, metadata?: CallMetadata): Promise; newCDPSession(params: BrowserContextNewCDPSessionParams, metadata?: CallMetadata): Promise; harStart(params: BrowserContextHarStartParams, metadata?: CallMetadata): Promise; harExport(params: BrowserContextHarExportParams, metadata?: CallMetadata): Promise; @@ -1766,9 +1766,10 @@ export type BrowserContextStorageStateResult = { export type BrowserContextPauseParams = {}; export type BrowserContextPauseOptions = {}; export type BrowserContextPauseResult = void; -export type BrowserContextRecorderSupplementEnableParams = { +export type BrowserContextEnableRecorderParams = { language?: string, mode?: 'inspecting' | 'recording', + codegenMode?: 'actions' | 'trace-events', pauseOnNextStatement?: boolean, testIdAttributeName?: string, launchOptions?: any, @@ -1778,9 +1779,10 @@ export type BrowserContextRecorderSupplementEnableParams = { outputFile?: string, omitCallTracking?: boolean, }; -export type BrowserContextRecorderSupplementEnableOptions = { +export type BrowserContextEnableRecorderOptions = { language?: string, mode?: 'inspecting' | 'recording', + codegenMode?: 'actions' | 'trace-events', pauseOnNextStatement?: boolean, testIdAttributeName?: string, launchOptions?: any, @@ -1790,7 +1792,7 @@ export type BrowserContextRecorderSupplementEnableOptions = { outputFile?: string, omitCallTracking?: boolean, }; -export type BrowserContextRecorderSupplementEnableResult = void; +export type BrowserContextEnableRecorderResult = void; export type BrowserContextNewCDPSessionParams = { page?: PageChannel, frame?: FrameChannel, @@ -3769,7 +3771,6 @@ export type RouteRedirectNavigationRequestOptions = { export type RouteRedirectNavigationRequestResult = void; export type RouteAbortParams = { errorCode?: string, - requestUrl: string, }; export type RouteAbortOptions = { errorCode?: string, @@ -3780,7 +3781,6 @@ export type RouteContinueParams = { method?: string, headers?: NameValue[], postData?: Binary, - requestUrl: string, isFallback: boolean, }; export type RouteContinueOptions = { @@ -3796,7 +3796,6 @@ export type RouteFulfillParams = { body?: string, isBase64?: boolean, fetchResponseUid?: string, - requestUrl: string, }; export type RouteFulfillOptions = { status?: number, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 9532e9f07f..5133c0672e 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1187,7 +1187,7 @@ BrowserContext: pause: experimental: True - recorderSupplementEnable: + enableRecorder: experimental: True parameters: language: string? @@ -1196,6 +1196,11 @@ BrowserContext: literals: - inspecting - recording + codegenMode: + type: enum? + literals: + - actions + - trace-events pauseOnNextStatement: boolean? testIdAttributeName: string? launchOptions: json? @@ -2941,7 +2946,6 @@ Route: abort: parameters: errorCode: string? - requestUrl: string continue: parameters: @@ -2951,7 +2955,6 @@ Route: type: array? items: NameValue postData: binary? - requestUrl: string isFallback: boolean fulfill: @@ -2964,7 +2967,6 @@ Route: body: string? isBase64: boolean? fetchResponseUid: string? - requestUrl: string WebSocketRoute: diff --git a/packages/recorder/src/recorder.tsx b/packages/recorder/src/recorder.tsx index 31bad2b70b..9d5c0feeba 100644 --- a/packages/recorder/src/recorder.tsx +++ b/packages/recorder/src/recorder.tsx @@ -19,6 +19,7 @@ import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper'; import { SplitView } from '@web/components/splitView'; import { TabbedPane } from '@web/components/tabbedPane'; import { Toolbar } from '@web/components/toolbar'; +import { emptySource, SourceChooser } from '@web/components/sourceChooser'; import { ToolbarButton, ToolbarSeparator } from '@web/components/toolbarButton'; import * as React from 'react'; import { CallLogView } from './callLog'; @@ -54,15 +55,7 @@ export const Recorder: React.FC = ({ if (source) return source; } - const source: Source = { - id: 'default', - isRecorded: false, - text: '', - language: 'javascript', - label: '', - highlight: [] - }; - return source; + return emptySource(); }, [sources, fileId]); const [locator, setLocator] = React.useState(''); @@ -152,10 +145,10 @@ export const Recorder: React.FC = ({ }}>
Target:
- + { + setFileId(fileId); + window.dispatch({ event: 'fileChanged', params: { file: fileId } }); + }} /> { window.dispatch({ event: 'clear' }); }}> @@ -184,22 +177,3 @@ export const Recorder: React.FC = ({ /> ; }; - -function renderSourceOptions(sources: Source[]): React.ReactNode { - const transformTitle = (title: string): string => title.replace(/.*[/\\]([^/\\]+)/, '$1'); - const renderOption = (source: Source): React.ReactNode => ( - - ); - - const hasGroup = sources.some(s => s.group); - if (hasGroup) { - const groups = new Set(sources.map(s => s.group)); - return [...groups].filter(Boolean).map(group => ( - - {sources.filter(s => s.group === group).map(source => renderOption(source))} - - )); - } - - return sources.map(source => renderOption(source)); -} diff --git a/packages/trace-viewer/src/sw.ts b/packages/trace-viewer/src/sw.ts index 7888aa6a30..43029ed5bb 100644 --- a/packages/trace-viewer/src/sw.ts +++ b/packages/trace-viewer/src/sw.ts @@ -47,12 +47,14 @@ async function loadTrace(traceUrl: string, traceFileName: string | null, clientI } set.add(traceUrl); + const isRecorderMode = traceUrl.includes('/playwright-recorder-trace-'); + const traceModel = new TraceModel(); try { // Allow 10% to hop from sw to page. const [fetchProgress, unzipProgress] = splitProgress(progress, [0.5, 0.4, 0.1]); const backend = traceUrl.endsWith('json') ? new FetchTraceModelBackend(traceUrl) : new ZipTraceModelBackend(traceUrl, fetchProgress); - await traceModel.load(backend, unzipProgress); + await traceModel.load(backend, isRecorderMode, unzipProgress); } catch (error: any) { // eslint-disable-next-line no-console console.error(error); diff --git a/packages/trace-viewer/src/traceModel.ts b/packages/trace-viewer/src/traceModel.ts index 87a3d42491..893e928691 100644 --- a/packages/trace-viewer/src/traceModel.ts +++ b/packages/trace-viewer/src/traceModel.ts @@ -15,7 +15,7 @@ */ import { parseClientSideCallMetadata } from '../../../packages/playwright-core/src/utils/isomorphic/traceUtils'; -import type { ContextEntry } from './entries'; +import type { ActionEntry, ContextEntry } from './entries'; import { createEmptyContext } from './entries'; import { SnapshotStorage } from './snapshotStorage'; import { TraceModernizer } from './traceModernizer'; @@ -38,7 +38,7 @@ export class TraceModel { constructor() { } - async load(backend: TraceModelBackend, unzipProgress: (done: number, total: number) => void) { + async load(backend: TraceModelBackend, isRecorderMode: boolean, unzipProgress: (done: number, total: number) => void) { this._backend = backend; const ordinals: string[] = []; @@ -72,7 +72,8 @@ export class TraceModel { modernizer.appendTrace(network); unzipProgress(++done, total); - contextEntry.actions = modernizer.actions().sort((a1, a2) => a1.startTime - a2.startTime); + const actions = modernizer.actions().sort((a1, a2) => a1.startTime - a2.startTime); + contextEntry.actions = isRecorderMode ? collapseActionsForRecorder(actions) : actions; if (!backend.isLive()) { // Terminate actions w/o after event gracefully. @@ -133,3 +134,19 @@ function stripEncodingFromContentType(contentType: string) { return charset[1]; return contentType; } + +function collapseActionsForRecorder(actions: ActionEntry[]): ActionEntry[] { + const result: ActionEntry[] = []; + for (const action of actions) { + const lastAction = result[result.length - 1]; + const isSameAction = lastAction && lastAction.method === action.method && lastAction.pageId === action.pageId; + const isSameSelector = lastAction && 'selector' in lastAction.params && 'selector' in action.params && action.params.selector === lastAction.params.selector; + const shouldMerge = isSameAction && (action.method === 'goto' || (action.method === 'fill' && isSameSelector)); + if (!shouldMerge) { + result.push(action); + continue; + } + result[result.length - 1] = action; + } + return result; +} diff --git a/packages/trace-viewer/src/ui/actionList.tsx b/packages/trace-viewer/src/ui/actionList.tsx index 3935447e7d..55f4f4028a 100644 --- a/packages/trace-viewer/src/ui/actionList.tsx +++ b/packages/trace-viewer/src/ui/actionList.tsx @@ -32,9 +32,9 @@ export interface ActionListProps { selectedTime: Boundaries | undefined, setSelectedTime: (time: Boundaries | undefined) => void, sdkLanguage: Language | undefined; - onSelected: (action: ActionTraceEventInContext) => void, - onHighlighted: (action: ActionTraceEventInContext | undefined) => void, - revealConsole: () => void, + onSelected?: (action: ActionTraceEventInContext) => void, + onHighlighted?: (action: ActionTraceEventInContext | undefined) => void, + revealConsole?: () => void, isLive?: boolean, } @@ -67,8 +67,8 @@ export const ActionList: React.FC = ({ treeState={treeState} setTreeState={setTreeState} selectedItem={selectedItem} - onSelected={item => onSelected(item.action!)} - onHighlighted={item => onHighlighted(item?.action)} + onSelected={item => onSelected?.(item.action!)} + onHighlighted={item => onHighlighted?.(item?.action)} onAccepted={item => setSelectedTime({ minimum: item.action!.startTime, maximum: item.action!.endTime })} isError={item => !!item.action?.error?.message} isVisible={item => !selectedTime || (item.action!.startTime <= selectedTime.maximum && item.action!.endTime >= selectedTime.minimum)} diff --git a/packages/trace-viewer/src/ui/callTab.css b/packages/trace-viewer/src/ui/callTab.css index 56928088b5..f57f3f1529 100644 --- a/packages/trace-viewer/src/ui/callTab.css +++ b/packages/trace-viewer/src/ui/callTab.css @@ -36,6 +36,8 @@ .call-section { padding-left: 6px; + padding-top: 2px; + margin-top: 2px; font-weight: bold; text-transform: uppercase; font-size: 10px; @@ -53,9 +55,8 @@ align-items: center; text-overflow: ellipsis; overflow: hidden; - line-height: 18px; + line-height: 20px; white-space: nowrap; - max-height: 18px; } .call-line:not(:hover) .toolbar-button.copy { @@ -64,7 +65,8 @@ .call-line .toolbar-button.copy { margin-left: 5px; - transform: scale(0.8); + margin-top: -2px; + margin-bottom: -2px; } .call-value { diff --git a/packages/trace-viewer/src/ui/consoleTab.tsx b/packages/trace-viewer/src/ui/consoleTab.tsx index b2947f5011..3ae847ef50 100644 --- a/packages/trace-viewer/src/ui/consoleTab.tsx +++ b/packages/trace-viewer/src/ui/consoleTab.tsx @@ -107,7 +107,7 @@ export const ConsoleTab: React.FunctionComponent<{ boundaries: Boundaries, consoleModel: ConsoleTabModel, selectedTime: Boundaries | undefined, - onEntryHovered: (entry: ConsoleEntry | undefined) => void, + onEntryHovered?: (entry: ConsoleEntry | undefined) => void, onAccepted: (entry: ConsoleEntry) => void, }> = ({ consoleModel, boundaries, onEntryHovered, onAccepted }) => { if (!consoleModel.entries.length) diff --git a/packages/trace-viewer/src/ui/networkTab.tsx b/packages/trace-viewer/src/ui/networkTab.tsx index 207dd33547..62b139be0d 100644 --- a/packages/trace-viewer/src/ui/networkTab.tsx +++ b/packages/trace-viewer/src/ui/networkTab.tsx @@ -65,7 +65,7 @@ export function useNetworkTabModel(model: MultiTraceModel | undefined, selectedT export const NetworkTab: React.FunctionComponent<{ boundaries: Boundaries, networkModel: NetworkTabModel, - onEntryHovered: (entry: Entry | undefined) => void, + onEntryHovered?: (entry: Entry | undefined) => void, }> = ({ boundaries, networkModel, onEntryHovered }) => { const [sorting, setSorting] = React.useState(undefined); const [selectedEntry, setSelectedEntry] = React.useState(undefined); @@ -95,7 +95,7 @@ export const NetworkTab: React.FunctionComponent<{ items={renderedEntries} selectedItem={selectedEntry} onSelected={item => setSelectedEntry(item)} - onHighlighted={item => onEntryHovered(item?.resource)} + onHighlighted={item => onEntryHovered?.(item?.resource)} columns={visibleColumns(!!selectedEntry, renderedEntries)} columnTitle={columnTitle} columnWidths={columnWidths} diff --git a/packages/trace-viewer/src/ui/recorderView.tsx b/packages/trace-viewer/src/ui/recorderView.tsx index 4d8ec8d297..159536d925 100644 --- a/packages/trace-viewer/src/ui/recorderView.tsx +++ b/packages/trace-viewer/src/ui/recorderView.tsx @@ -14,54 +14,52 @@ limitations under the License. */ -import * as React from 'react'; -import './recorderView.css'; -import { MultiTraceModel } from './modelUtil'; -import type { SourceLocation } from './modelUtil'; -import { Workbench } from './workbench'; +import type { Language } from '@isomorphic/locatorGenerators'; import type { Mode, Source } from '@recorder/recorderTypes'; +import { SplitView } from '@web/components/splitView'; +import type { TabbedPaneTabModel } from '@web/components/tabbedPane'; +import { TabbedPane } from '@web/components/tabbedPane'; +import { sha1, useSetting } from '@web/uiUtils'; +import * as React from 'react'; import type { ContextEntry } from '../entries'; +import type { Boundaries } from '../geometry'; +import { ActionList } from './actionList'; +import { ConsoleTab, useConsoleTabModel } from './consoleTab'; +import { InspectorTab } from './inspectorTab'; +import type * as modelUtil from './modelUtil'; +import type { SourceLocation } from './modelUtil'; +import { MultiTraceModel } from './modelUtil'; +import { NetworkTab, useNetworkTabModel } from './networkTab'; +import './recorderView.css'; +import { collectSnapshots, extendSnapshot, SnapshotView } from './snapshotTab'; +import { SourceTab } from './sourceTab'; +import { Toolbar } from '@web/components/toolbar'; +import { ToolbarButton, ToolbarSeparator } from '@web/components/toolbarButton'; +import { toggleTheme } from '@web/theme'; +import { SourceChooser } from '@web/components/sourceChooser'; const searchParams = new URLSearchParams(window.location.search); const guid = searchParams.get('ws'); -const trace = searchParams.get('trace') + '.json'; +const traceLocation = searchParams.get('trace') + '.json'; export const RecorderView: React.FunctionComponent = () => { const [connection, setConnection] = React.useState(null); const [sources, setSources] = React.useState([]); + const [model, setModel] = React.useState<{ model: MultiTraceModel, isLive: boolean, sha1: string } | undefined>(); + const [mode, setMode] = React.useState('none'); + const [counter, setCounter] = React.useState(0); + const pollTimer = React.useRef(null); + React.useEffect(() => { const wsURL = new URL(`../${guid}`, window.location.toString()); wsURL.protocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:'); const webSocket = new WebSocket(wsURL.toString()); - setConnection(new Connection(webSocket, { setSources })); + setConnection(new Connection(webSocket, { setMode, setSources })); return () => { webSocket.close(); }; }, []); - React.useEffect(() => { - if (!connection) - return; - connection.setMode('recording'); - }, [connection]); - - window.playwrightSourcesEchoForTest = sources; - - return
- -
; -}; - -export const TraceView: React.FC<{ - traceLocation: string, - sources: Source[], -}> = ({ traceLocation, sources }) => { - const [model, setModel] = React.useState<{ model: MultiTraceModel, isLive: boolean } | undefined>(); - const [counter, setCounter] = React.useState(0); - const pollTimer = React.useRef(null); - React.useEffect(() => { if (pollTimer.current) clearTimeout(pollTimer.current); @@ -69,8 +67,9 @@ export const TraceView: React.FC<{ // Start polling running test. pollTimer.current = setTimeout(async () => { try { - const model = await loadSingleTraceFile(traceLocation); - setModel({ model, isLive: true }); + const result = await loadSingleTraceFile(traceLocation); + if (result.sha1 !== model?.sha1) + setModel({ ...result, isLive: true }); } catch { setModel(undefined); } finally { @@ -81,10 +80,94 @@ export const TraceView: React.FC<{ if (pollTimer.current) clearTimeout(pollTimer.current); }; - }, [counter, traceLocation]); + }, [counter, model]); + + return
+ connection?.setMode(mode)} + model={model?.model} + sources={sources} + /> +
; +}; + +async function loadSingleTraceFile(url: string): Promise<{ model: MultiTraceModel, sha1: string }> { + const params = new URLSearchParams(); + params.set('trace', url); + const response = await fetch(`contexts?${params.toString()}`); + const contextEntries = await response.json() as ContextEntry[]; + + const tokens: string[] = []; + for (const entry of contextEntries) { + entry.actions.forEach(a => tokens.push(a.type + '@' + a.startTime + '-' + a.endTime)); + entry.events.forEach(e => tokens.push(e.type + '@' + e.time)); + } + return { model: new MultiTraceModel(contextEntries), sha1: await sha1(tokens.join('|')) }; +} + +export const Workbench: React.FunctionComponent<{ + mode: Mode, + setMode: (mode: Mode) => void, + model?: modelUtil.MultiTraceModel, + sources: Source[], +}> = ({ mode, setMode, model, sources }) => { + const [fileId, setFileId] = React.useState(); + const [selectedCallId, setSelectedCallId] = React.useState(undefined); + const [selectedPropertiesTab, setSelectedPropertiesTab] = useSetting('recorderPropertiesTab', 'source'); + const [isInspecting, setIsInspectingState] = React.useState(false); + const [highlightedLocator, setHighlightedLocator] = React.useState(''); + const [selectedTime, setSelectedTime] = React.useState(); + const sourceModel = React.useRef(new Map()); + + const setSelectedAction = React.useCallback((action: modelUtil.ActionTraceEventInContext | undefined) => { + setSelectedCallId(action?.callId); + }, []); + + const selectedAction = React.useMemo(() => { + return model?.actions.find(a => a.callId === selectedCallId); + }, [model, selectedCallId]); + + const onActionSelected = React.useCallback((action: modelUtil.ActionTraceEventInContext) => { + setSelectedAction(action); + }, [setSelectedAction]); + + const selectPropertiesTab = React.useCallback((tab: string) => { + setSelectedPropertiesTab(tab); + if (tab !== 'inspector') + setIsInspectingState(false); + }, [setSelectedPropertiesTab]); + + const setIsInspecting = React.useCallback((value: boolean) => { + if (!isInspecting && value) + selectPropertiesTab('inspector'); + setIsInspectingState(value); + }, [setIsInspectingState, selectPropertiesTab, isInspecting]); + + const locatorPicked = React.useCallback((locator: string) => { + setHighlightedLocator(locator); + selectPropertiesTab('inspector'); + }, [selectPropertiesTab]); + + const consoleModel = useConsoleTabModel(model, selectedTime); + const networkModel = useNetworkTabModel(model, selectedTime); + const sdkLanguage = model?.sdkLanguage || 'javascript'; + + const inspectorTab: TabbedPaneTabModel = { + id: 'inspector', + title: 'Locator', + render: () => , + }; + + const source = React.useMemo(() => sources.find(s => s.id === fileId) || sources[0], [sources, fileId]); const fallbackLocation = React.useMemo(() => { - if (!sources.length) + if (!source) return undefined; const fallbackLocation: SourceLocation = { file: '', @@ -92,36 +175,178 @@ export const TraceView: React.FC<{ column: 0, source: { errors: [], - content: sources[0].text + content: source.text } }; return fallbackLocation; - }, [sources]); + }, [source]); - return + }; + const consoleTab: TabbedPaneTabModel = { + id: 'console', + title: 'Console', + count: consoleModel.entries.length, + render: () => setSelectedTime({ minimum: m.timestamp, maximum: m.timestamp })} + /> + }; + const networkTab: TabbedPaneTabModel = { + id: 'network', + title: 'Network', + count: networkModel.resources.length, + render: () => + }; + + const tabs: TabbedPaneTabModel[] = [ + sourceTab, + inspectorTab, + consoleTab, + networkTab, + ]; + + const { boundaries } = React.useMemo(() => { + const boundaries = { minimum: model?.startTime || 0, maximum: model?.endTime || 30000 }; + if (boundaries.minimum > boundaries.maximum) { + boundaries.minimum = 0; + boundaries.maximum = 30000; + } + // Leave some nice free space on the right hand side. + boundaries.maximum += (boundaries.maximum - boundaries.minimum) / 20; + return { boundaries }; + }, [model]); + + const actionList = selectPropertiesTab('console')} isLive={true} />; + + const actionsTab: TabbedPaneTabModel = { + id: 'actions', + title: 'Actions', + component: actionList, + }; + + const toolbar = +
+ { + setMode(mode === 'recording' ? 'standby' : 'recording'); + }}>Record + + { + setIsInspecting(!isInspecting); + }} /> + { + }} /> + { + }} /> + { + }} /> + + { + }} /> +
+
Target:
+ { + setFileId(fileId); + }} /> + { + }}> + toggleTheme()}> +
; + + const sidebarTabbedPane = ; + + const propertiesTabbedPane = ; + + const snapshotView = ; + + return
+ + {toolbar} + {snapshotView} +
} + sidebar={propertiesTabbedPane} + />} + sidebar={sidebarTabbedPane} + /> + ; }; -async function loadSingleTraceFile(url: string): Promise { - const params = new URLSearchParams(); - params.set('trace', url); - const response = await fetch(`contexts?${params.toString()}`); - const contextEntries = await response.json() as ContextEntry[]; - return new MultiTraceModel(contextEntries); -} +const SnapshotContainer: React.FunctionComponent<{ + sdkLanguage: Language, + action: modelUtil.ActionTraceEventInContext | undefined, + testIdAttributeName?: string, + isInspecting: boolean, + highlightedLocator: string, + setIsInspecting: (value: boolean) => void, + locatorPicked: (locator: string) => void, +}> = ({ sdkLanguage, action, testIdAttributeName, isInspecting, setIsInspecting, highlightedLocator, locatorPicked }) => { + const snapshot = React.useMemo(() => { + const snapshot = collectSnapshots(action); + return snapshot.action || snapshot.after || snapshot.before; + }, [action]); + const snapshotUrls = React.useMemo(() => { + return snapshot ? extendSnapshot(snapshot) : undefined; + }, [snapshot]); + return ; +}; + +type ConnectionOptions = { + setSources: (sources: Source[]) => void; + setMode: (mode: Mode) => void; +}; class Connection { private _lastId = 0; private _webSocket: WebSocket; private _callbacks = new Map void, reject: (arg: Error) => void }>(); - private _options: { setSources: (sources: Source[]) => void; }; + private _options: ConnectionOptions; - constructor(webSocket: WebSocket, options: { setSources: (sources: Source[]) => void }) { + constructor(webSocket: WebSocket, options: ConnectionOptions) { this._webSocket = webSocket; this._callbacks = new Map(); this._options = options; @@ -165,6 +390,11 @@ class Connection { if (method === 'setSources') { const { sources } = params as { sources: Source[] }; this._options.setSources(sources); + window.playwrightSourcesEchoForTest = sources; + } + if (method === 'setMode') { + const { mode } = params as { mode: Mode }; + this._options.setMode(mode); } } } diff --git a/packages/trace-viewer/src/ui/snapshotTab.css b/packages/trace-viewer/src/ui/snapshotTab.css index 2677cfe53a..926685dc81 100644 --- a/packages/trace-viewer/src/ui/snapshotTab.css +++ b/packages/trace-viewer/src/ui/snapshotTab.css @@ -15,12 +15,8 @@ */ .snapshot-tab { - display: flex; - flex: auto; - flex-direction: column; align-items: stretch; outline: none; - --browser-frame-header-height: 40px; overflow: hidden; } @@ -73,6 +69,7 @@ margin: 1px; padding: 10px; position: relative; + --browser-frame-header-height: 40px; } .snapshot-container { diff --git a/packages/trace-viewer/src/ui/snapshotTab.tsx b/packages/trace-viewer/src/ui/snapshotTab.tsx index 9dafa10b96..b845c60cd5 100644 --- a/packages/trace-viewer/src/ui/snapshotTab.tsx +++ b/packages/trace-viewer/src/ui/snapshotTab.tsx @@ -40,7 +40,7 @@ function findClosest(items: T[], metric: (v: T) => number, target: number) { }); } -export const SnapshotTab: React.FunctionComponent<{ +export const SnapshotTabsView: React.FunctionComponent<{ action: ActionTraceEvent | undefined, model?: MultiTraceModel, sdkLanguage: Language, @@ -50,63 +50,69 @@ export const SnapshotTab: React.FunctionComponent<{ highlightedLocator: string, setHighlightedLocator: (locator: string) => void, openPage?: (url: string, target?: string) => Window | any, -}> = ({ action, model, sdkLanguage, testIdAttributeName, isInspecting, setIsInspecting, highlightedLocator, setHighlightedLocator, openPage }) => { - const [measure, ref] = useMeasure(); +}> = ({ action, sdkLanguage, testIdAttributeName, isInspecting, setIsInspecting, highlightedLocator, setHighlightedLocator, openPage }) => { const [snapshotTab, setSnapshotTab] = React.useState<'action'|'before'|'after'>('action'); const [showScreenshotInsteadOfSnapshot] = useSetting('screenshot-instead-of-snapshot', false); - type Snapshot = { action: ActionTraceEvent, snapshotName: string, point?: { x: number, y: number }, hasInputTarget?: boolean }; - const { snapshots } = React.useMemo(() => { - if (!action) - return { snapshots: {} }; - - // if the action has no beforeSnapshot, use the last available afterSnapshot. - let beforeSnapshot: Snapshot | undefined = action.beforeSnapshot ? { action, snapshotName: action.beforeSnapshot } : undefined; - let a = action; - while (!beforeSnapshot && a) { - a = prevInList(a); - beforeSnapshot = a?.afterSnapshot ? { action: a, snapshotName: a?.afterSnapshot } : undefined; - } - const afterSnapshot: Snapshot | undefined = action.afterSnapshot ? { action, snapshotName: action.afterSnapshot } : beforeSnapshot; - const actionSnapshot: Snapshot | undefined = action.inputSnapshot ? { action, snapshotName: action.inputSnapshot, hasInputTarget: true } : afterSnapshot; - if (actionSnapshot) - actionSnapshot.point = action.point; - return { snapshots: { action: actionSnapshot, before: beforeSnapshot, after: afterSnapshot } }; + const snapshots = React.useMemo(() => { + return collectSnapshots(action); }, [action]); - - const { snapshotInfoUrl, snapshotUrl, popoutUrl, point } = React.useMemo(() => { + const snapshotUrls = React.useMemo(() => { const snapshot = snapshots[snapshotTab]; - if (!snapshot) - return { snapshotUrl: kBlankSnapshotUrl }; - - const params = new URLSearchParams(); - params.set('trace', context(snapshot.action).traceUrl); - params.set('name', snapshot.snapshotName); - if (snapshot.point) { - params.set('pointX', String(snapshot.point.x)); - params.set('pointY', String(snapshot.point.y)); - if (snapshot.hasInputTarget) - params.set('hasInputTarget', '1'); - } - const snapshotUrl = new URL(`snapshot/${snapshot.action.pageId}?${params.toString()}`, window.location.href).toString(); - const snapshotInfoUrl = new URL(`snapshotInfo/${snapshot.action.pageId}?${params.toString()}`, window.location.href).toString(); - - const popoutParams = new URLSearchParams(); - popoutParams.set('r', snapshotUrl); - popoutParams.set('trace', context(snapshot.action).traceUrl); - if (snapshot.point) { - popoutParams.set('pointX', String(snapshot.point.x)); - popoutParams.set('pointY', String(snapshot.point.y)); - if (snapshot.hasInputTarget) - params.set('hasInputTarget', '1'); - } - const popoutUrl = new URL(`snapshot.html?${popoutParams.toString()}`, window.location.href).toString(); - return { snapshots, snapshotInfoUrl, snapshotUrl, popoutUrl, point: snapshot.point }; + return snapshot ? extendSnapshot(snapshot) : undefined; }, [snapshots, snapshotTab]); + return
+ + setIsInspecting(!isInspecting)} disabled={showScreenshotInsteadOfSnapshot} /> + {['action', 'before', 'after'].map(tab => { + return setSnapshotTab(tab as 'action' | 'before' | 'after')} + >; + })} +
+ { + if (!openPage) + openPage = window.open; + const win = openPage(snapshotUrls?.popoutUrl || '', '_blank'); + win?.addEventListener('DOMContentLoaded', () => { + const injectedScript = new InjectedScript(win as any, false, sdkLanguage, testIdAttributeName, 1, 'chromium', []); + new ConsoleAPI(injectedScript); + }); + }} /> +
+ {!showScreenshotInsteadOfSnapshot && } + {showScreenshotInsteadOfSnapshot && } +
; +}; + +export const SnapshotView: React.FunctionComponent<{ + snapshotUrls: SnapshotUrls | undefined, + sdkLanguage: Language, + testIdAttributeName: string, + isInspecting: boolean, + setIsInspecting: (isInspecting: boolean) => void, + highlightedLocator: string, + setHighlightedLocator: (locator: string) => void, +}> = ({ snapshotUrls, sdkLanguage, testIdAttributeName, isInspecting, setIsInspecting, highlightedLocator, setHighlightedLocator }) => { const iframeRef0 = React.useRef(null); const iframeRef1 = React.useRef(null); - const [snapshotInfo, setSnapshotInfo] = React.useState<{ viewport: typeof kDefaultViewport, url: string, timestamp?: number, wallTime?: undefined }>({ viewport: kDefaultViewport, url: '' }); + const [snapshotInfo, setSnapshotInfo] = React.useState({ viewport: kDefaultViewport, url: '' }); const loadingRef = React.useRef({ iteration: 0, visibleIframe: 0 }); React.useEffect(() => { @@ -115,17 +121,7 @@ export const SnapshotTab: React.FunctionComponent<{ const newVisibleIframe = 1 - loadingRef.current.visibleIframe; loadingRef.current.iteration = thisIteration; - const newSnapshotInfo = { url: '', viewport: kDefaultViewport, timestamp: undefined, wallTime: undefined }; - if (snapshotInfoUrl) { - const response = await fetch(snapshotInfoUrl); - const info = await response.json(); - if (!info.error) { - newSnapshotInfo.url = info.url; - newSnapshotInfo.viewport = info.viewport; - newSnapshotInfo.timestamp = info.timestamp; - newSnapshotInfo.wallTime = info.wallTime; - } - } + const newSnapshotInfo = await fetchSnapshotInfo(snapshotUrls?.snapshotInfoUrl); // Interrupted by another load - bail out. if (loadingRef.current.iteration !== thisIteration) @@ -140,6 +136,7 @@ export const SnapshotTab: React.FunctionComponent<{ iframe.addEventListener('error', loadedCallback); // Try preventing history entry from being created. + const snapshotUrl = snapshotUrls?.snapshotUrl || kBlankSnapshotUrl; if (iframe.contentWindow) iframe.contentWindow.location.replace(snapshotUrl); else @@ -159,33 +156,10 @@ export const SnapshotTab: React.FunctionComponent<{ loadingRef.current.visibleIframe = newVisibleIframe; setSnapshotInfo(newSnapshotInfo); })(); - }, [snapshotUrl, snapshotInfoUrl]); - - const windowHeaderHeight = 40; - const snapshotContainerSize = { - width: snapshotInfo.viewport.width, - height: snapshotInfo.viewport.height + windowHeaderHeight, - }; - const scale = Math.min(measure.width / snapshotContainerSize.width, measure.height / snapshotContainerSize.height, 1); - const translate = { - x: (measure.width - snapshotContainerSize.width) / 2, - y: (measure.height - snapshotContainerSize.height) / 2, - }; - - const page = action ? pageForAction(action) : undefined; - const screencastFrame = React.useMemo( - () => { - if (snapshotInfo.wallTime && page?.screencastFrames[0]?.frameSwapWallTime) - return findClosest(page.screencastFrames, frame => frame.frameSwapWallTime!, snapshotInfo.wallTime); - - if (snapshotInfo.timestamp && page?.screencastFrames) - return findClosest(page.screencastFrames, frame => frame.timestamp, snapshotInfo.timestamp); - }, - [page?.screencastFrames, snapshotInfo.timestamp, snapshotInfo.wallTime] - ); + }, [snapshotUrls]); return
{ if (event.key === 'Escape') { @@ -210,46 +184,72 @@ export const SnapshotTab: React.FunctionComponent<{ setHighlightedLocator={setHighlightedLocator} iframe={iframeRef1.current} iteration={loadingRef.current.iteration} /> - - setIsInspecting(!isInspecting)} disabled={showScreenshotInsteadOfSnapshot} /> - {['action', 'before', 'after'].map(tab => { - return setSnapshotTab(tab as 'action' | 'before' | 'after')} - >; - })} -
- { - if (!openPage) - openPage = window.open; - const win = openPage(popoutUrl || '', '_blank'); - win?.addEventListener('DOMContentLoaded', () => { - const injectedScript = new InjectedScript(win as any, false, sdkLanguage, testIdAttributeName, 1, 'chromium', []); - new ConsoleAPI(injectedScript); - }); - }}> -
-
-
- - {(showScreenshotInsteadOfSnapshot && screencastFrame) && ( - <> - {point && } - {`Screenshot ${renderTitle(snapshotTab)}`} src={`sha1/${screencastFrame.sha1}`} width={screencastFrame.width} height={screencastFrame.height} /> - - )} -
- - -
+ +
+ +
+
+
; +}; + +export const ScreenshotView: React.FunctionComponent<{ + action: ActionTraceEvent | undefined, + snapshotUrls: SnapshotUrls | undefined, + snapshot: Snapshot | undefined, +}> = ({ action, snapshotUrls, snapshot }) => { + const [snapshotInfo, setSnapshotInfo] = React.useState({ viewport: kDefaultViewport, url: '' }); + React.useEffect(() => { + fetchSnapshotInfo(snapshotUrls?.snapshotInfoUrl).then(setSnapshotInfo); + }, [snapshotUrls?.snapshotInfoUrl]); + + const page = action ? pageForAction(action) : undefined; + const screencastFrame = React.useMemo(() => { + if (snapshotInfo.wallTime && page?.screencastFrames[0]?.frameSwapWallTime) + return findClosest(page.screencastFrames, frame => frame.frameSwapWallTime!, snapshotInfo.wallTime); + + if (snapshotInfo.timestamp && page?.screencastFrames) + return findClosest(page.screencastFrames, frame => frame.timestamp, snapshotInfo.timestamp); + }, + [page?.screencastFrames, snapshotInfo.timestamp, snapshotInfo.wallTime]); + + const point = snapshot?.point; + + return + {screencastFrame && ( + <> + {point && } + {`Screenshot + + )} + ; +}; + +const SnapshotWrapper: React.FunctionComponent> = ({ snapshotInfo, children }) => { + const [measure, ref] = useMeasure(); + + const windowHeaderHeight = 40; + const snapshotContainerSize = { + width: snapshotInfo.viewport.width, + height: snapshotInfo.viewport.height + windowHeaderHeight, + }; + + const scale = Math.min(measure.width / snapshotContainerSize.width, measure.height / snapshotContainerSize.height, 1); + const translate = { + x: (measure.width - snapshotContainerSize.width) / 2, + y: (measure.height - snapshotContainerSize.height) / 2, + }; + + return
+
+ + {children}
; }; @@ -325,5 +325,90 @@ function createRecorders(recorders: { recorder: Recorder, frameSelector: string } } -const kDefaultViewport = { width: 1280, height: 720 }; +export type Snapshot = { + action: ActionTraceEvent; + snapshotName: string; + point?: { x: number, y: number }; + hasInputTarget?: boolean; +}; + +export type SnapshotInfo = { + url: string; + viewport: { width: number, height: number }; + timestamp?: number; + wallTime?: undefined; +}; + +export type Snapshots = { + action?: Snapshot; + before?: Snapshot; + after?: Snapshot; +}; + +export type SnapshotUrls = { + snapshotInfoUrl: string; + snapshotUrl: string; + popoutUrl: string; +}; + +export function collectSnapshots(action: ActionTraceEvent | undefined): Snapshots { + if (!action) + return {}; + + // if the action has no beforeSnapshot, use the last available afterSnapshot. + let beforeSnapshot: Snapshot | undefined = action.beforeSnapshot ? { action, snapshotName: action.beforeSnapshot } : undefined; + let a = action; + while (!beforeSnapshot && a) { + a = prevInList(a); + beforeSnapshot = a?.afterSnapshot ? { action: a, snapshotName: a?.afterSnapshot } : undefined; + } + const afterSnapshot: Snapshot | undefined = action.afterSnapshot ? { action, snapshotName: action.afterSnapshot } : beforeSnapshot; + const actionSnapshot: Snapshot | undefined = action.inputSnapshot ? { action, snapshotName: action.inputSnapshot, hasInputTarget: true } : afterSnapshot; + if (actionSnapshot) + actionSnapshot.point = action.point; + return { action: actionSnapshot, before: beforeSnapshot, after: afterSnapshot }; +} + +export function extendSnapshot(snapshot: Snapshot): SnapshotUrls { + const params = new URLSearchParams(); + params.set('trace', context(snapshot.action).traceUrl); + params.set('name', snapshot.snapshotName); + if (snapshot.point) { + params.set('pointX', String(snapshot.point.x)); + params.set('pointY', String(snapshot.point.y)); + if (snapshot.hasInputTarget) + params.set('hasInputTarget', '1'); + } + const snapshotUrl = new URL(`snapshot/${snapshot.action.pageId}?${params.toString()}`, window.location.href).toString(); + const snapshotInfoUrl = new URL(`snapshotInfo/${snapshot.action.pageId}?${params.toString()}`, window.location.href).toString(); + + const popoutParams = new URLSearchParams(); + popoutParams.set('r', snapshotUrl); + popoutParams.set('trace', context(snapshot.action).traceUrl); + if (snapshot.point) { + popoutParams.set('pointX', String(snapshot.point.x)); + popoutParams.set('pointY', String(snapshot.point.y)); + if (snapshot.hasInputTarget) + params.set('hasInputTarget', '1'); + } + const popoutUrl = new URL(`snapshot.html?${popoutParams.toString()}`, window.location.href).toString(); + return { snapshotInfoUrl, snapshotUrl, popoutUrl }; +} + +export async function fetchSnapshotInfo(snapshotInfoUrl: string | undefined) { + const result = { url: '', viewport: kDefaultViewport, timestamp: undefined, wallTime: undefined }; + if (snapshotInfoUrl) { + const response = await fetch(snapshotInfoUrl); + const info = await response.json(); + if (!info.error) { + result.url = info.url; + result.viewport = info.viewport; + result.timestamp = info.timestamp; + result.wallTime = info.wallTime; + } + } + return result; +} + +export const kDefaultViewport = { width: 1280, height: 720 }; const kBlankSnapshotUrl = 'data:text/html,'; diff --git a/packages/trace-viewer/src/ui/sourceTab.tsx b/packages/trace-viewer/src/ui/sourceTab.tsx index ce54b34d53..d130499207 100644 --- a/packages/trace-viewer/src/ui/sourceTab.tsx +++ b/packages/trace-viewer/src/ui/sourceTab.tsx @@ -28,7 +28,7 @@ import { ToolbarButton } from '@web/components/toolbarButton'; import { Toolbar } from '@web/components/toolbar'; export const SourceTab: React.FunctionComponent<{ - stack: StackFrame[] | undefined, + stack?: StackFrame[], stackFrameLocation: 'bottom' | 'right', sources: Map, rootDir?: string, diff --git a/packages/trace-viewer/src/ui/uiModeView.tsx b/packages/trace-viewer/src/ui/uiModeView.tsx index a97716bdc4..5c6ab7969b 100644 --- a/packages/trace-viewer/src/ui/uiModeView.tsx +++ b/packages/trace-viewer/src/ui/uiModeView.tsx @@ -58,7 +58,6 @@ const queryParams = { workers: searchParams.get('workers') || undefined, timeout: searchParams.has('timeout') ? +searchParams.get('timeout')! : undefined, headed: searchParams.has('headed'), - outputDir: searchParams.get('outputDir') || undefined, updateSnapshots: (searchParams.get('updateSnapshots') as 'all' | 'none' | 'missing' | undefined) || undefined, reporters: searchParams.has('reporter') ? searchParams.getAll('reporter') : undefined, pathSeparator: searchParams.get('pathSeparator') || '/', @@ -105,7 +104,6 @@ export const UIModeView: React.FC<{}> = ({ const [singleWorker, setSingleWorker] = React.useState(queryParams.workers === '1'); const [showBrowser, setShowBrowser] = React.useState(queryParams.headed); const [updateSnapshots, setUpdateSnapshots] = React.useState(queryParams.updateSnapshots === 'all'); - const [showRouteActions, setShowRouteActions] = useSetting('show-route-actions', true); const [darkMode, setDarkMode] = useDarkModeSetting(); const [showScreenshot, setShowScreenshot] = useSetting('screenshot-instead-of-snapshot', false); @@ -188,14 +186,12 @@ export const UIModeView: React.FC<{}> = ({ interceptStdio: true, watchTestDirs: true }); - const { status, report } = await testServerConnection.runGlobalSetup({ - outputDir: queryParams.outputDir, - }); + const { status, report } = await testServerConnection.runGlobalSetup({}); teleSuiteUpdater.processGlobalReport(report); if (status !== 'passed') return; - const result = await testServerConnection.listTests({ projects: queryParams.projects, locations: queryParams.args, grep: queryParams.grep, grepInvert: queryParams.grepInvert, outputDir: queryParams.outputDir }); + const result = await testServerConnection.listTests({ projects: queryParams.projects, locations: queryParams.args, grep: queryParams.grep, grepInvert: queryParams.grepInvert }); teleSuiteUpdater.processListReport(result.report); testServerConnection.onReport(params => { @@ -297,7 +293,6 @@ export const UIModeView: React.FC<{}> = ({ workers: singleWorker ? '1' : (queryParams.workers === '1' ? undefined : queryParams.workers), timeout: queryParams.timeout, headed: showBrowser, - outputDir: queryParams.outputDir, updateSnapshots: updateSnapshots ? 'all' : queryParams.updateSnapshots, reporters: queryParams.reporters, trace: 'on', @@ -320,7 +315,7 @@ export const UIModeView: React.FC<{}> = ({ commandQueue.current = commandQueue.current.then(async () => { setIsLoading(true); try { - const result = await testServerConnection.listTests({ projects: queryParams.projects, locations: queryParams.args, grep: queryParams.grep, grepInvert: queryParams.grepInvert, outputDir: queryParams.outputDir }); + const result = await testServerConnection.listTests({ projects: queryParams.projects, locations: queryParams.args, grep: queryParams.grep, grepInvert: queryParams.grepInvert }); teleSuiteUpdater.processListReport(result.report); } catch (e) { // eslint-disable-next-line no-console @@ -526,7 +521,6 @@ export const UIModeView: React.FC<{}> = ({ {settingsVisible && }
diff --git a/packages/trace-viewer/src/ui/workbench.tsx b/packages/trace-viewer/src/ui/workbench.tsx index e1ce2298ae..2905bb052f 100644 --- a/packages/trace-viewer/src/ui/workbench.tsx +++ b/packages/trace-viewer/src/ui/workbench.tsx @@ -24,9 +24,8 @@ import type { ErrorDescription } from './errorsTab'; import type { ConsoleEntry } from './consoleTab'; import { ConsoleTab, useConsoleTabModel } from './consoleTab'; import type * as modelUtil from './modelUtil'; -import { isRouteAction } from './modelUtil'; import { NetworkTab, useNetworkTabModel } from './networkTab'; -import { SnapshotTab } from './snapshotTab'; +import { SnapshotTabsView } from './snapshotTab'; import { SourceTab } from './sourceTab'; import { TabbedPane } from '@web/components/tabbedPane'; import type { TabbedPaneTabModel } from '@web/components/tabbedPane'; @@ -50,6 +49,7 @@ export const Workbench: React.FunctionComponent<{ rootDir?: string, fallbackLocation?: modelUtil.SourceLocation, isLive?: boolean, + hideTimeline?: boolean, status?: UITestStatus, annotations?: { type: string; description?: string; }[]; inert?: boolean, @@ -57,11 +57,10 @@ export const Workbench: React.FunctionComponent<{ onOpenExternally?: (location: modelUtil.SourceLocation) => void, revealSource?: boolean, showSettings?: boolean, -}> = ({ model, showSourcesFirst, rootDir, fallbackLocation, isLive, status, annotations, inert, openPage, onOpenExternally, revealSource, showSettings }) => { +}> = ({ model, showSourcesFirst, rootDir, fallbackLocation, isLive, hideTimeline, status, annotations, inert, openPage, onOpenExternally, revealSource, showSettings }) => { const [selectedCallId, setSelectedCallId] = React.useState(undefined); const [revealedError, setRevealedError] = React.useState(undefined); - - const [highlightedAction, setHighlightedAction] = React.useState(); + const [highlightedCallId, setHighlightedCallId] = React.useState(); const [highlightedEntry, setHighlightedEntry] = React.useState(); const [highlightedConsoleMessage, setHighlightedConsoleMessage] = React.useState(); const [selectedNavigatorTab, setSelectedNavigatorTab] = React.useState('actions'); @@ -70,18 +69,21 @@ export const Workbench: React.FunctionComponent<{ const [highlightedLocator, setHighlightedLocator] = React.useState(''); const [selectedTime, setSelectedTime] = React.useState(); const [sidebarLocation, setSidebarLocation] = useSetting<'bottom' | 'right'>('propertiesSidebarLocation', 'bottom'); - const [showRouteActions, setShowRouteActions] = useSetting('show-route-actions', true); const [showScreenshot, setShowScreenshot] = useSetting('screenshot-instead-of-snapshot', false); - const filteredActions = React.useMemo(() => { - return (model?.actions || []).filter(action => showRouteActions || !isRouteAction(action)); - }, [model, showRouteActions]); - const setSelectedAction = React.useCallback((action: modelUtil.ActionTraceEventInContext | undefined) => { setSelectedCallId(action?.callId); setRevealedError(undefined); }, []); + const highlightedAction = React.useMemo(() => { + return model?.actions.find(a => a.callId === highlightedCallId); + }, [model, highlightedCallId]); + + const setHighlightedAction = React.useCallback((highlightedAction: modelUtil.ActionTraceEventInContext | undefined) => { + setHighlightedCallId(highlightedAction?.callId); + }, []); + const sources = React.useMemo(() => model?.sources || new Map(), [model]); React.useEffect(() => { @@ -291,7 +293,7 @@ export const Workbench: React.FunctionComponent<{
} , }; return
- + />} void, +}> = ({ sources, fileId, setFileId }) => { + return ; +}; + +function renderSourceOptions(sources: Source[]): React.ReactNode { + const transformTitle = (title: string): string => title.replace(/.*[/\\]([^/\\]+)/, '$1'); + const renderOption = (source: Source): React.ReactNode => ( + + ); + + const hasGroup = sources.some(s => s.group); + if (hasGroup) { + const groups = new Set(sources.map(s => s.group)); + return [...groups].filter(Boolean).map(group => ( + + {sources.filter(s => s.group === group).map(source => renderOption(source))} + + )); + } + + return sources.map(source => renderOption(source)); +} + +export function emptySource(): Source { + return { + id: 'default', + isRecorded: false, + text: '', + language: 'javascript', + label: '', + highlight: [] + }; +} \ No newline at end of file diff --git a/packages/web/src/components/tabbedPane.tsx b/packages/web/src/components/tabbedPane.tsx index b9872294df..5df94ec4c3 100644 --- a/packages/web/src/components/tabbedPane.tsx +++ b/packages/web/src/components/tabbedPane.tsx @@ -32,11 +32,13 @@ export const TabbedPane: React.FunctionComponent<{ tabs: TabbedPaneTabModel[], leftToolbar?: React.ReactElement[], rightToolbar?: React.ReactElement[], - selectedTab: string, - setSelectedTab: (tab: string) => void, + selectedTab?: string, + setSelectedTab?: (tab: string) => void, dataTestId?: string, mode?: 'default' | 'select', }> = ({ tabs, selectedTab, setSelectedTab, leftToolbar, rightToolbar, dataTestId, mode }) => { + if (!selectedTab) + selectedTab = tabs[0].id; if (!mode) mode = 'default'; return
@@ -60,7 +62,7 @@ export const TabbedPane: React.FunctionComponent<{
} {mode === 'select' &&
[fail] +library/selector-generator.spec.ts › selector generator › should generate title selector [fail] +library/selector-generator.spec.ts › selector generator › should handle first non-unique data-testid [fail] +library/selector-generator.spec.ts › selector generator › should handle second non-unique data-testid [fail] +library/selector-generator.spec.ts › selector generator › should ignore empty aria-label for candidate consideration [fail] +library/selector-generator.spec.ts › selector generator › should ignore empty data-test-id for candidate consideration [fail] +library/selector-generator.spec.ts › selector generator › should ignore empty role for candidate consideration [fail] +library/selector-generator.spec.ts › selector generator › should match in deep shadow dom [fail] +library/selector-generator.spec.ts › selector generator › should match in shadow dom [fail] +library/selector-generator.spec.ts › selector generator › should not accept invalid role for candidate consideration [fail] +library/selector-generator.spec.ts › selector generator › should not escape spaces inside named attr selectors [fail] +library/selector-generator.spec.ts › selector generator › should not escape text with >> [fail] +library/selector-generator.spec.ts › selector generator › should not improve guid text [fail] +library/selector-generator.spec.ts › selector generator › should not prefer zero-sized button over inner span [fail] +library/selector-generator.spec.ts › selector generator › should not use generated id [fail] +library/selector-generator.spec.ts › selector generator › should not use input[value] [fail] +library/selector-generator.spec.ts › selector generator › should not use text for select [fail] +library/selector-generator.spec.ts › selector generator › should prefer button over inner span [fail] +library/selector-generator.spec.ts › selector generator › should prefer data-testid [fail] +library/selector-generator.spec.ts › selector generator › should prefer role other input[type] [fail] +library/selector-generator.spec.ts › selector generator › should prefer role=button over inner span [fail] +library/selector-generator.spec.ts › selector generator › should prioritise attributes correctly › name [fail] +library/selector-generator.spec.ts › selector generator › should prioritise attributes correctly › placeholder [fail] +library/selector-generator.spec.ts › selector generator › should prioritise attributes correctly › role [fail] +library/selector-generator.spec.ts › selector generator › should prioritise attributes correctly › type [fail] +library/selector-generator.spec.ts › selector generator › should properly join child selectors under nested ordinals [fail] +library/selector-generator.spec.ts › selector generator › should separate selectors by >> [fail] +library/selector-generator.spec.ts › selector generator › should trim long text [fail] +library/selector-generator.spec.ts › selector generator › should trim text [fail] +library/selector-generator.spec.ts › selector generator › should try to improve label text by shortening [fail] +library/selector-generator.spec.ts › selector generator › should try to improve role name [fail] +library/selector-generator.spec.ts › selector generator › should try to improve text [fail] +library/selector-generator.spec.ts › selector generator › should try to improve text by shortening [fail] +library/selector-generator.spec.ts › selector generator › should use data-testid in strict errors [fail] +library/selector-generator.spec.ts › selector generator › should use internal:has-text [fail] +library/selector-generator.spec.ts › selector generator › should use internal:has-text with regexp [fail] +library/selector-generator.spec.ts › selector generator › should use internal:has-text with regexp with a quote [fail] +library/selector-generator.spec.ts › selector generator › should use nested ordinals [fail] +library/selector-generator.spec.ts › selector generator › should use ordinal for identical nodes [fail] +library/selector-generator.spec.ts › selector generator › should use parent text [fail] +library/selector-generator.spec.ts › selector generator › should use readable id [fail] +library/selector-generator.spec.ts › selector generator › should use the name attributes for elements that can have it [fail] +library/selector-generator.spec.ts › selector generator › should work in dynamic iframes without navigation [fail] +library/selector-generator.spec.ts › selector generator › should work with tricky attributes [fail] +library/selector-generator.spec.ts › selector generator › should work without CSS.escape [fail] +library/selectors-register.spec.ts › should handle errors [pass] +library/selectors-register.spec.ts › should not rely on engines working from the root [fail] +library/selectors-register.spec.ts › should throw a nice error if the selector returns a bad value [pass] +library/selectors-register.spec.ts › should work [fail] +library/selectors-register.spec.ts › should work in main and isolated world [fail] +library/selectors-register.spec.ts › should work when registered on global [fail] +library/selectors-register.spec.ts › should work with path [fail] +library/shared-worker.spec.ts › should survive shared worker restart [pass] +library/signals.spec.ts › should close the browser when the node process closes [timeout] +library/signals.spec.ts › should remove temp dir on process.exit [timeout] +library/signals.spec.ts › signals › should close the browser on SIGHUP [timeout] +library/signals.spec.ts › signals › should close the browser on SIGINT [timeout] +library/signals.spec.ts › signals › should close the browser on SIGTERM [timeout] +library/signals.spec.ts › signals › should kill the browser on SIGINT + SIGTERM [timeout] +library/signals.spec.ts › signals › should kill the browser on SIGTERM + SIGINT [timeout] +library/signals.spec.ts › signals › should kill the browser on double SIGINT and remove temp dir [timeout] +library/signals.spec.ts › signals › should not prevent default SIGTERM handling after browser close [timeout] +library/signals.spec.ts › signals › should report browser close signal 2 [timeout] +library/slowmo.spec.ts › slowMo › ElementHandle SlowMo check [fail] +library/slowmo.spec.ts › slowMo › ElementHandle SlowMo click [fail] +library/slowmo.spec.ts › slowMo › ElementHandle SlowMo dblclick [fail] +library/slowmo.spec.ts › slowMo › ElementHandle SlowMo dispatchEvent [fail] +library/slowmo.spec.ts › slowMo › ElementHandle SlowMo fill [fail] +library/slowmo.spec.ts › slowMo › ElementHandle SlowMo focus [fail] +library/slowmo.spec.ts › slowMo › ElementHandle SlowMo hover [fail] +library/slowmo.spec.ts › slowMo › ElementHandle SlowMo press [fail] +library/slowmo.spec.ts › slowMo › ElementHandle SlowMo selectOption [fail] +library/slowmo.spec.ts › slowMo › ElementHandle SlowMo setInputFiles [fail] +library/slowmo.spec.ts › slowMo › ElementHandle SlowMo type [fail] +library/slowmo.spec.ts › slowMo › ElementHandle SlowMo uncheck [fail] +library/slowmo.spec.ts › slowMo › Frame SlowMo check [fail] +library/slowmo.spec.ts › slowMo › Frame SlowMo click [fail] +library/slowmo.spec.ts › slowMo › Frame SlowMo dblclick [fail] +library/slowmo.spec.ts › slowMo › Frame SlowMo dispatchEvent [fail] +library/slowmo.spec.ts › slowMo › Frame SlowMo fill [fail] +library/slowmo.spec.ts › slowMo › Frame SlowMo focus [fail] +library/slowmo.spec.ts › slowMo › Frame SlowMo goto [fail] +library/slowmo.spec.ts › slowMo › Frame SlowMo hover [fail] +library/slowmo.spec.ts › slowMo › Frame SlowMo press [fail] +library/slowmo.spec.ts › slowMo › Frame SlowMo selectOption [fail] +library/slowmo.spec.ts › slowMo › Frame SlowMo setInputFiles [fail] +library/slowmo.spec.ts › slowMo › Frame SlowMo type [fail] +library/slowmo.spec.ts › slowMo › Frame SlowMo uncheck [fail] +library/slowmo.spec.ts › slowMo › Page SlowMo check [fail] +library/slowmo.spec.ts › slowMo › Page SlowMo click [fail] +library/slowmo.spec.ts › slowMo › Page SlowMo dblclick [fail] +library/slowmo.spec.ts › slowMo › Page SlowMo dispatchEvent [fail] +library/slowmo.spec.ts › slowMo › Page SlowMo fill [fail] +library/slowmo.spec.ts › slowMo › Page SlowMo focus [fail] +library/slowmo.spec.ts › slowMo › Page SlowMo goto [fail] +library/slowmo.spec.ts › slowMo › Page SlowMo hover [fail] +library/slowmo.spec.ts › slowMo › Page SlowMo press [fail] +library/slowmo.spec.ts › slowMo › Page SlowMo reload [fail] +library/slowmo.spec.ts › slowMo › Page SlowMo selectOption [fail] +library/slowmo.spec.ts › slowMo › Page SlowMo setInputFiles [fail] +library/slowmo.spec.ts › slowMo › Page SlowMo type [fail] +library/slowmo.spec.ts › slowMo › Page SlowMo uncheck [fail] +library/snapshotter.spec.ts › snapshots › empty adopted style sheets should not prevent node refs [fail] +library/snapshotter.spec.ts › snapshots › should capture frame [pass] +library/snapshotter.spec.ts › snapshots › should capture iframe [pass] +library/snapshotter.spec.ts › snapshots › should capture iframe with srcdoc [pass] +library/snapshotter.spec.ts › snapshots › should capture resources [fail] +library/snapshotter.spec.ts › snapshots › should capture snapshot target [fail] +library/snapshotter.spec.ts › snapshots › should collect multiple [fail] +library/snapshotter.spec.ts › snapshots › should collect on attribute change [fail] +library/snapshotter.spec.ts › snapshots › should collect snapshot [fail] +library/snapshotter.spec.ts › snapshots › should have a custom doctype [fail] +library/snapshotter.spec.ts › snapshots › should not navigate on anchor clicks [fail] +library/snapshotter.spec.ts › snapshots › should preserve BASE and other content on reset [pass] +library/snapshotter.spec.ts › snapshots › should replace meta charset attr that specifies charset [fail] +library/snapshotter.spec.ts › snapshots › should replace meta content attr that specifies charset [fail] +library/snapshotter.spec.ts › snapshots › should respect CSSOM change through CSSGroupingRule [fail] +library/snapshotter.spec.ts › snapshots › should respect attr removal [fail] +library/snapshotter.spec.ts › snapshots › should respect inline CSSOM change [fail] +library/snapshotter.spec.ts › snapshots › should respect node removal [fail] +library/snapshotter.spec.ts › snapshots › should respect subresource CSSOM change [fail] +library/tap.spec.ts › locators › should send all of the correct events [fail] +library/tap.spec.ts › should not send mouse events touchstart is canceled [fail] +library/tap.spec.ts › should not send mouse events when touchend is canceled [fail] +library/tap.spec.ts › should not wait for a navigation caused by a tap [fail] +library/tap.spec.ts › should send all of the correct events @smoke [fail] +library/tap.spec.ts › should send well formed touch points [fail] +library/tap.spec.ts › should wait until an element is visible to tap it [fail] +library/tap.spec.ts › should work with modifiers [fail] +library/tap.spec.ts › trial run should not tap [fail] +library/trace-viewer.spec.ts › should allow hiding route actions [unknown] +library/trace-viewer.spec.ts › should allow showing screenshots instead of snapshots [unknown] +library/trace-viewer.spec.ts › should capture data-url svg iframe [unknown] +library/trace-viewer.spec.ts › should capture iframe with sandbox attribute [unknown] +library/trace-viewer.spec.ts › should complain about newer version of trace in old viewer [unknown] +library/trace-viewer.spec.ts › should contain action info [fail] +library/trace-viewer.spec.ts › should contain adopted style sheets [fail] +library/trace-viewer.spec.ts › should display language-specific locators [unknown] +library/trace-viewer.spec.ts › should display waitForLoadState even if did not wait for it [unknown] +library/trace-viewer.spec.ts › should filter network requests by resource type [fail] +library/trace-viewer.spec.ts › should filter network requests by url [unknown] +library/trace-viewer.spec.ts › should follow redirects [unknown] +library/trace-viewer.spec.ts › should handle case where neither snapshots nor screenshots exist [fail] +library/trace-viewer.spec.ts › should handle file URIs [fail] +library/trace-viewer.spec.ts › should handle multiple headers [unknown] +library/trace-viewer.spec.ts › should handle src=blob [unknown] +library/trace-viewer.spec.ts › should have correct snapshot size [fail] +library/trace-viewer.spec.ts › should have correct stack trace [unknown] +library/trace-viewer.spec.ts › should have network request overrides [fail] +library/trace-viewer.spec.ts › should have network request overrides 2 [unknown] +library/trace-viewer.spec.ts › should have network requests [unknown] +library/trace-viewer.spec.ts › should highlight expect failure [unknown] +library/trace-viewer.spec.ts › should highlight locator in iframe while typing [unknown] +library/trace-viewer.spec.ts › should highlight target element in shadow dom [unknown] +library/trace-viewer.spec.ts › should highlight target elements [fail] +library/trace-viewer.spec.ts › should ignore 304 responses [unknown] +library/trace-viewer.spec.ts › should include metainfo [unknown] +library/trace-viewer.spec.ts › should include requestUrl in route.abort [unknown] +library/trace-viewer.spec.ts › should include requestUrl in route.continue [unknown] +library/trace-viewer.spec.ts › should include requestUrl in route.fulfill [unknown] +library/trace-viewer.spec.ts › should not crash with broken locator [fail] +library/trace-viewer.spec.ts › should open console errors on click [fail] +library/trace-viewer.spec.ts › should open simple trace viewer [fail] +library/trace-viewer.spec.ts › should open snapshot in new browser context [unknown] +library/trace-viewer.spec.ts › should open trace viewer on specific host [unknown] +library/trace-viewer.spec.ts › should open trace-1.31 [unknown] +library/trace-viewer.spec.ts › should open trace-1.37 [fail] +library/trace-viewer.spec.ts › should open two trace files [fail] +library/trace-viewer.spec.ts › should open two trace files of the same test [unknown] +library/trace-viewer.spec.ts › should open two trace viewers [unknown] +library/trace-viewer.spec.ts › should pick locator [fail] +library/trace-viewer.spec.ts › should pick locator in iframe [fail] +library/trace-viewer.spec.ts › should popup snapshot [fail] +library/trace-viewer.spec.ts › should prefer later resource request with the same method [unknown] +library/trace-viewer.spec.ts › should preserve currentSrc [unknown] +library/trace-viewer.spec.ts › should preserve noscript when javascript is disabled [unknown] +library/trace-viewer.spec.ts › should properly synchronize local and remote time [unknown] +library/trace-viewer.spec.ts › should register custom elements [unknown] +library/trace-viewer.spec.ts › should remove noscript by default [fail] +library/trace-viewer.spec.ts › should remove noscript when javaScriptEnabled is set to true [unknown] +library/trace-viewer.spec.ts › should render console [unknown] +library/trace-viewer.spec.ts › should render network bars [unknown] +library/trace-viewer.spec.ts › should restore control values [unknown] +library/trace-viewer.spec.ts › should restore scroll positions [unknown] +library/trace-viewer.spec.ts › should serve css without content-type [unknown] +library/trace-viewer.spec.ts › should serve overridden request [fail] +library/trace-viewer.spec.ts › should show action source [fail] +library/trace-viewer.spec.ts › should show baseURL in metadata pane [fail] +library/trace-viewer.spec.ts › should show correct request start time [unknown] +library/trace-viewer.spec.ts › should show empty trace viewer [fail] +library/trace-viewer.spec.ts › should show font preview [unknown] +library/trace-viewer.spec.ts › should show null as a param [unknown] +library/trace-viewer.spec.ts › should show only one pointer with multilevel iframes [unknown] +library/trace-viewer.spec.ts › should show params and return value [unknown] +library/trace-viewer.spec.ts › should show similar actions from library-only trace [fail] +library/trace-viewer.spec.ts › should show snapshot URL [unknown] +library/trace-viewer.spec.ts › should update highlight when typing [unknown] +library/trace-viewer.spec.ts › should work with adopted style sheets and all: unset [unknown] +library/trace-viewer.spec.ts › should work with adopted style sheets and replace/replaceSync [unknown] +library/trace-viewer.spec.ts › should work with meta CSP [fail] +library/trace-viewer.spec.ts › should work with nesting CSS selectors [fail] +library/tracing.spec.ts › should collect sources [fail] +library/tracing.spec.ts › should collect trace with resources, but no js [fail] +library/tracing.spec.ts › should collect two traces [fail] +library/tracing.spec.ts › should exclude internal pages [pass] +library/tracing.spec.ts › should export trace concurrently to second navigation [pass] +library/tracing.spec.ts › should flush console events on tracing stop [pass] +library/tracing.spec.ts › should hide internal stack frames [fail] +library/tracing.spec.ts › should hide internal stack frames in expect [fail] +library/tracing.spec.ts › should ignore iframes in head [pass] +library/tracing.spec.ts › should include context API requests [pass] +library/tracing.spec.ts › should include interrupted actions [fail] +library/tracing.spec.ts › should not collect snapshots by default [fail] +library/tracing.spec.ts › should not crash when browser closes mid-trace [pass] +library/tracing.spec.ts › should not emit after w/o before [pass] +library/tracing.spec.ts › should not flush console events [pass] +library/tracing.spec.ts › should not hang for clicks that open dialogs [fail] +library/tracing.spec.ts › should not include buffers in the trace [pass] +library/tracing.spec.ts › should not include trace resources from the previous chunks [fail] +library/tracing.spec.ts › should not stall on dialogs [fail] +library/tracing.spec.ts › should not throw when stopping without start but not exporting [pass] +library/tracing.spec.ts › should overwrite existing file [fail] +library/tracing.spec.ts › should produce screencast frames crop [fail] +library/tracing.spec.ts › should produce screencast frames fit [fail] +library/tracing.spec.ts › should produce screencast frames scale [fail] +library/tracing.spec.ts › should record global request trace [pass] +library/tracing.spec.ts › should record network failures [pass] +library/tracing.spec.ts › should respect tracesDir and name [fail] +library/tracing.spec.ts › should store global request traces separately [pass] +library/tracing.spec.ts › should store postData for global request [pass] +library/tracing.spec.ts › should survive browser.close with auto-created traces dir [pass] +library/tracing.spec.ts › should throw when starting with different options [pass] +library/tracing.spec.ts › should throw when stopping without start [pass] +library/tracing.spec.ts › should use the correct apiName for event driven callbacks [pass] +library/tracing.spec.ts › should work with multiple chunks [fail] +library/unroute-behavior.spec.ts › context.close should not wait for active route handlers on the owned pages [pass] +library/unroute-behavior.spec.ts › context.unroute should not wait for pending handlers to complete [timeout] +library/unroute-behavior.spec.ts › context.unrouteAll removes all handlers [pass] +library/unroute-behavior.spec.ts › context.unrouteAll should not wait for pending handlers to complete if behavior is ignoreErrors [timeout] +library/unroute-behavior.spec.ts › context.unrouteAll should wait for pending handlers to complete [timeout] +library/unroute-behavior.spec.ts › page.close does not wait for active route handlers [pass] +library/unroute-behavior.spec.ts › page.close should not wait for active route handlers on the owning context [pass] +library/unroute-behavior.spec.ts › page.unroute should not wait for pending handlers to complete [pass] +library/unroute-behavior.spec.ts › page.unrouteAll removes all routes [pass] +library/unroute-behavior.spec.ts › page.unrouteAll should not wait for pending handlers to complete if behavior is ignoreErrors [pass] +library/unroute-behavior.spec.ts › page.unrouteAll should wait for pending handlers to complete [pass] +library/unroute-behavior.spec.ts › route.continue should not throw if page has been closed [pass] +library/unroute-behavior.spec.ts › route.fallback should not throw if page has been closed [pass] +library/unroute-behavior.spec.ts › route.fulfill should not throw if page has been closed [pass] +library/video.spec.ts › screencast › saveAs should throw when no video frames [timeout] +library/video.spec.ts › screencast › should be 800x450 by default [timeout] +library/video.spec.ts › screencast › should be 800x600 with null viewport [timeout] +library/video.spec.ts › screencast › should capture css transformation [timeout] +library/video.spec.ts › screencast › should capture full viewport [fail] +library/video.spec.ts › screencast › should capture full viewport on hidpi [fail] +library/video.spec.ts › screencast › should capture navigation [timeout] +library/video.spec.ts › screencast › should capture static page [timeout] +library/video.spec.ts › screencast › should capture static page in persistent context @smoke [fail] +library/video.spec.ts › screencast › should continue recording main page after popup closes [fail] +library/video.spec.ts › screencast › should delete video [timeout] +library/video.spec.ts › screencast › should emulate an iphone [timeout] +library/video.spec.ts › screencast › should expose video path [fail] +library/video.spec.ts › screencast › should expose video path blank page [timeout] +library/video.spec.ts › screencast › should expose video path blank popup [fail] +library/video.spec.ts › screencast › should not create video for internal pages [unknown] +library/video.spec.ts › screencast › should scale frames down to the requested size [timeout] +library/video.spec.ts › screencast › should throw if browser dies [fail] +library/video.spec.ts › screencast › should throw on browser close [fail] +library/video.spec.ts › screencast › should throw without recordVideo.dir [pass] +library/video.spec.ts › screencast › should use viewport scaled down to fit into 800x800 as default size [timeout] +library/video.spec.ts › screencast › should wait for video to finish if page was closed [fail] +library/video.spec.ts › screencast › should work for popups [fail] +library/video.spec.ts › screencast › should work with old options [timeout] +library/video.spec.ts › screencast › should work with relative path for recordVideo.dir [timeout] +library/video.spec.ts › screencast › should work with video+trace [timeout] +library/video.spec.ts › screencast › should work with weird screen resolution [timeout] +library/video.spec.ts › screencast › videoSize should require videosPath [pass] +library/video.spec.ts › should saveAs video [timeout] +library/web-socket.spec.ts › should emit binary frame events [timeout] +library/web-socket.spec.ts › should emit close events [timeout] +library/web-socket.spec.ts › should emit error [timeout] +library/web-socket.spec.ts › should emit frame events [timeout] +library/web-socket.spec.ts › should filter out the close events when the server closes with a message [timeout] +library/web-socket.spec.ts › should not have stray error events [timeout] +library/web-socket.spec.ts › should pass self as argument to close event [timeout] +library/web-socket.spec.ts › should reject waitForEvent on page close [timeout] +library/web-socket.spec.ts › should reject waitForEvent on socket close [timeout] +library/web-socket.spec.ts › should turn off when offline [unknown] +library/web-socket.spec.ts › should work @smoke [pass] \ No newline at end of file diff --git a/tests/bidi/expectations/bidi-firefox-nightly-page.txt b/tests/bidi/expectations/bidi-firefox-nightly-page.txt new file mode 100644 index 0000000000..366d8fdf36 --- /dev/null +++ b/tests/bidi/expectations/bidi-firefox-nightly-page.txt @@ -0,0 +1,1968 @@ +page/elementhandle-bounding-box.spec.ts › should force a layout [fail] +page/elementhandle-bounding-box.spec.ts › should get frame box [fail] +page/elementhandle-bounding-box.spec.ts › should handle nested frames [fail] +page/elementhandle-bounding-box.spec.ts › should handle scroll offset and click [fail] +page/elementhandle-bounding-box.spec.ts › should return null for invisible elements [fail] +page/elementhandle-bounding-box.spec.ts › should work [fail] +page/elementhandle-bounding-box.spec.ts › should work when inline box child is outside of viewport [fail] +page/elementhandle-bounding-box.spec.ts › should work with SVG nodes [fail] +page/elementhandle-click.spec.ts › should double click the button [fail] +page/elementhandle-click.spec.ts › should throw for
elements with force [fail] +page/elementhandle-click.spec.ts › should throw for detached nodes [pass] +page/elementhandle-click.spec.ts › should throw for hidden nodes with force [pass] +page/elementhandle-click.spec.ts › should throw for recursively hidden nodes with force [pass] +page/elementhandle-click.spec.ts › should work @smoke [pass] +page/elementhandle-click.spec.ts › should work for Shadow DOM v1 [pass] +page/elementhandle-click.spec.ts › should work for TextNodes [fail] +page/elementhandle-click.spec.ts › should work with Node removed [pass] +page/elementhandle-content-frame.spec.ts › should return null for document.documentElement [pass] +page/elementhandle-content-frame.spec.ts › should return null for non-iframes [pass] +page/elementhandle-content-frame.spec.ts › should work [pass] +page/elementhandle-content-frame.spec.ts › should work for cross-frame evaluations [fail] +page/elementhandle-content-frame.spec.ts › should work for cross-process iframes [pass] +page/elementhandle-convenience.spec.ts › getAttribute should work [pass] +page/elementhandle-convenience.spec.ts › innerHTML should work [pass] +page/elementhandle-convenience.spec.ts › innerText should throw [fail] +page/elementhandle-convenience.spec.ts › innerText should work [pass] +page/elementhandle-convenience.spec.ts › inputValue should work [pass] +page/elementhandle-convenience.spec.ts › isChecked should work [fail] +page/elementhandle-convenience.spec.ts › isEditable should work [fail] +page/elementhandle-convenience.spec.ts › isEnabled and isDisabled should work [fail] +page/elementhandle-convenience.spec.ts › isEnabled and isDisabled should work with [fail] +page/page-fill.spec.ts › should throw on incorrect color value [fail] +page/page-fill.spec.ts › should throw on incorrect date [fail] +page/page-fill.spec.ts › should throw on incorrect datetime-local [unknown] +page/page-fill.spec.ts › should throw on incorrect month [unknown] +page/page-fill.spec.ts › should throw on incorrect range value [fail] +page/page-fill.spec.ts › should throw on incorrect time [fail] +page/page-fill.spec.ts › should throw on incorrect week [unknown] +page/page-fill.spec.ts › should throw on unsupported inputs [pass] +page/page-focus.spec.ts › clicking checkbox should activate it [unknown] +page/page-focus.spec.ts › keeps focus on element when attempting to focus a non-focusable element [fail] +page/page-focus.spec.ts › should emit blur event [fail] +page/page-focus.spec.ts › should emit focus event [fail] +page/page-focus.spec.ts › should traverse focus [fail] +page/page-focus.spec.ts › should traverse focus in all directions [fail] +page/page-focus.spec.ts › should traverse only form elements [unknown] +page/page-focus.spec.ts › should work @smoke [fail] +page/page-force-gc.spec.ts › should work [fail] +page/page-goto.spec.ts › js redirect overrides url bar navigation [pass] +page/page-goto.spec.ts › should be able to navigate to a page controlled by service worker [pass] +page/page-goto.spec.ts › should capture cross-process iframe navigation request [pass] +page/page-goto.spec.ts › should capture iframe navigation request [pass] +page/page-goto.spec.ts › should disable timeout when its set to 0 [pass] +page/page-goto.spec.ts › should fail when canceled by another navigation [pass] +page/page-goto.spec.ts › should fail when exceeding browser context navigation timeout [pass] +page/page-goto.spec.ts › should fail when exceeding browser context timeout [pass] +page/page-goto.spec.ts › should fail when exceeding default maximum navigation timeout [pass] +page/page-goto.spec.ts › should fail when exceeding default maximum timeout [pass] +page/page-goto.spec.ts › should fail when exceeding maximum navigation timeout [pass] +page/page-goto.spec.ts › should fail when main resources failed to load [pass] +page/page-goto.spec.ts › should fail when navigating and show the url at the error message [pass] +page/page-goto.spec.ts › should fail when navigating to bad SSL [fail] +page/page-goto.spec.ts › should fail when navigating to bad SSL after redirects [fail] +page/page-goto.spec.ts › should fail when navigating to bad url [fail] +page/page-goto.spec.ts › should fail when replaced by another navigation [pass] +page/page-goto.spec.ts › should fail when server returns 204 [timeout] +page/page-goto.spec.ts › should navigate to URL with hash and fire requests without hash [pass] +page/page-goto.spec.ts › should navigate to about:blank [pass] +page/page-goto.spec.ts › should navigate to dataURL and not fire dataURL requests [pass] +page/page-goto.spec.ts › should navigate to empty page with domcontentloaded [pass] +page/page-goto.spec.ts › should not crash when RTCPeerConnection is used [pass] +page/page-goto.spec.ts › should not crash when navigating to bad SSL after a cross origin navigation [pass] +page/page-goto.spec.ts › should not leak listeners during 20 waitForNavigation [pass] +page/page-goto.spec.ts › should not leak listeners during bad navigation [pass] +page/page-goto.spec.ts › should not leak listeners during navigation [pass] +page/page-goto.spec.ts › should not resolve goto upon window.stop() [pass] +page/page-goto.spec.ts › should not throw if networkidle0 is passed as an option [pass] +page/page-goto.spec.ts › should not throw unhandled rejections on invalid url [pass] +page/page-goto.spec.ts › should override referrer-policy [fail] +page/page-goto.spec.ts › should prioritize default navigation timeout over default timeout [pass] +page/page-goto.spec.ts › should properly wait for load [fail] +page/page-goto.spec.ts › should reject referer option when setExtraHTTPHeaders provides referer [pass] +page/page-goto.spec.ts › should report raw buffer for main resource [fail] +page/page-goto.spec.ts › should return from goto if new navigation is started [pass] +page/page-goto.spec.ts › should return last response in redirect chain [pass] +page/page-goto.spec.ts › should return response when page changes its URL after load [pass] +page/page-goto.spec.ts › should return url with basic auth info [pass] +page/page-goto.spec.ts › should return when navigation is committed if commit is specified [pass] +page/page-goto.spec.ts › should send referer [fail] +page/page-goto.spec.ts › should send referer of cross-origin URL [fail] +page/page-goto.spec.ts › should succeed on url bar navigation when there is pending navigation [pass] +page/page-goto.spec.ts › should throw if networkidle2 is passed as an option [fail] +page/page-goto.spec.ts › should use http for no protocol [pass] +page/page-goto.spec.ts › should wait for load when iframe attaches and detaches [pass] +page/page-goto.spec.ts › should work @smoke [pass] +page/page-goto.spec.ts › should work cross-process [pass] +page/page-goto.spec.ts › should work when navigating to 404 [pass] +page/page-goto.spec.ts › should work when navigating to data url [pass] +page/page-goto.spec.ts › should work when navigating to valid url [pass] +page/page-goto.spec.ts › should work when page calls history API in beforeunload [fail] +page/page-goto.spec.ts › should work with Cross-Origin-Opener-Policy [pass] +page/page-goto.spec.ts › should work with Cross-Origin-Opener-Policy after redirect [pass] +page/page-goto.spec.ts › should work with Cross-Origin-Opener-Policy and interception [pass] +page/page-goto.spec.ts › should work with anchor navigation [timeout] +page/page-goto.spec.ts › should work with cross-process that fails before committing [pass] +page/page-goto.spec.ts › should work with file URL [pass] +page/page-goto.spec.ts › should work with file URL with subframes [pass] +page/page-goto.spec.ts › should work with lazy loading iframes [fail] +page/page-goto.spec.ts › should work with redirects [pass] +page/page-goto.spec.ts › should work with self requesting page [pass] +page/page-goto.spec.ts › should work with subframes return 204 [pass] +page/page-goto.spec.ts › should work with subframes return 204 with domcontentloaded [pass] +page/page-history.spec.ts › goBack/goForward should work with bfcache-able pages [fail] +page/page-history.spec.ts › page.goBack during renderer-initiated navigation [fail] +page/page-history.spec.ts › page.goBack should work @smoke [pass] +page/page-history.spec.ts › page.goBack should work for file urls [fail] +page/page-history.spec.ts › page.goBack should work with HistoryAPI [fail] +page/page-history.spec.ts › page.goForward during renderer-initiated navigation [fail] +page/page-history.spec.ts › page.reload during renderer-initiated navigation [fail] +page/page-history.spec.ts › page.reload should not resolve with same-document navigation [fail] +page/page-history.spec.ts › page.reload should work [pass] +page/page-history.spec.ts › page.reload should work on a page with a hash [pass] +page/page-history.spec.ts › page.reload should work on a page with a hash at the end [pass] +page/page-history.spec.ts › page.reload should work with cross-origin redirect [pass] +page/page-history.spec.ts › page.reload should work with data url [pass] +page/page-history.spec.ts › page.reload should work with same origin redirect [pass] +page/page-history.spec.ts › regression test for issue 20791 [pass] +page/page-history.spec.ts › should reload proper page [timeout] +page/page-keyboard.spec.ts › insertText should only emit input event [fail] +page/page-keyboard.spec.ts › pressing Meta should not result in any text insertion on any platform [fail] +page/page-keyboard.spec.ts › should be able to prevent selectAll [pass] +page/page-keyboard.spec.ts › should dispatch a click event on a button when Enter gets pressed [fail] +page/page-keyboard.spec.ts › should dispatch a click event on a button when Space gets pressed [fail] +page/page-keyboard.spec.ts › should dispatch insertText after context menu was opened [pass] +page/page-keyboard.spec.ts › should expose keyIdentifier in webkit [unknown] +page/page-keyboard.spec.ts › should handle selectAll [pass] +page/page-keyboard.spec.ts › should have correct Keydown/Keyup order when pressing Escape key [pass] +page/page-keyboard.spec.ts › should move around the selection in a contenteditable [fail] +page/page-keyboard.spec.ts › should move to the start of the document [unknown] +page/page-keyboard.spec.ts › should move with the arrow keys [pass] +page/page-keyboard.spec.ts › should not type canceled events [pass] +page/page-keyboard.spec.ts › should press Enter [fail] +page/page-keyboard.spec.ts › should press plus [fail] +page/page-keyboard.spec.ts › should press shift plus [fail] +page/page-keyboard.spec.ts › should press the meta key [pass] +page/page-keyboard.spec.ts › should report multiple modifiers [fail] +page/page-keyboard.spec.ts › should report shiftKey [pass] +page/page-keyboard.spec.ts › should scroll with PageDown [pass] +page/page-keyboard.spec.ts › should send a character with ElementHandle.press [pass] +page/page-keyboard.spec.ts › should send a character with insertText [fail] +page/page-keyboard.spec.ts › should send proper codes while typing [pass] +page/page-keyboard.spec.ts › should send proper codes while typing with shift [pass] +page/page-keyboard.spec.ts › should shift raw codes [pass] +page/page-keyboard.spec.ts › should specify location [fail] +page/page-keyboard.spec.ts › should specify repeat property [pass] +page/page-keyboard.spec.ts › should support MacOS shortcuts [unknown] +page/page-keyboard.spec.ts › should support multiple plus-separated modifiers [pass] +page/page-keyboard.spec.ts › should support plus-separated modifiers [pass] +page/page-keyboard.spec.ts › should support simple copy-pasting [fail] +page/page-keyboard.spec.ts › should support simple cut-pasting [fail] +page/page-keyboard.spec.ts › should support undo-redo [fail] +page/page-keyboard.spec.ts › should throw on unknown keys [pass] +page/page-keyboard.spec.ts › should type after context menu was opened [pass] +page/page-keyboard.spec.ts › should type all kinds of characters [pass] +page/page-keyboard.spec.ts › should type emoji [pass] +page/page-keyboard.spec.ts › should type emoji into an iframe [pass] +page/page-keyboard.spec.ts › should type into a textarea @smoke [pass] +page/page-keyboard.spec.ts › should type repeatedly in contenteditable in shadow dom [fail] +page/page-keyboard.spec.ts › should type repeatedly in contenteditable in shadow dom with nested elements [fail] +page/page-keyboard.spec.ts › should type repeatedly in input in shadow dom [fail] +page/page-keyboard.spec.ts › should work after a cross origin navigation [pass] +page/page-keyboard.spec.ts › should work with keyboard events with empty.html [pass] +page/page-keyboard.spec.ts › type to non-focusable element should maintain old focus [fail] +page/page-leaks.spec.ts › click should not leak [fail] +page/page-leaks.spec.ts › expect should not leak [fail] +page/page-leaks.spec.ts › fill should not leak [fail] +page/page-leaks.spec.ts › waitFor should not leak [fail] +page/page-listeners.spec.ts › should not throw with ignoreErrors [pass] +page/page-listeners.spec.ts › should wait [pass] +page/page-listeners.spec.ts › wait should throw [pass] +page/page-mouse.spec.ts › down and up should generate click [pass] +page/page-mouse.spec.ts › should always round down [fail] +page/page-mouse.spec.ts › should click the document @smoke [pass] +page/page-mouse.spec.ts › should dblclick the div [fail] +page/page-mouse.spec.ts › should dispatch mouse move after context menu was opened [pass] +page/page-mouse.spec.ts › should not crash on mouse drag with any button [pass] +page/page-mouse.spec.ts › should pointerdown the div with a custom button [fail] +page/page-mouse.spec.ts › should report correct buttons property [pass] +page/page-mouse.spec.ts › should select the text with mouse [pass] +page/page-mouse.spec.ts › should set modifier keys on click [pass] +page/page-mouse.spec.ts › should trigger hover state [pass] +page/page-mouse.spec.ts › should trigger hover state on disabled button [pass] +page/page-mouse.spec.ts › should trigger hover state with removed window.Node [pass] +page/page-mouse.spec.ts › should tween mouse movement [pass] +page/page-navigation.spec.ts › should work with _blank target [pass] +page/page-navigation.spec.ts › should work with _blank target in form [fail] +page/page-navigation.spec.ts › should work with cross-process _blank target [pass] +page/page-network-idle.spec.ts › should navigate to empty page with networkidle [pass] +page/page-network-idle.spec.ts › should wait for networkidle from the child frame [pass] +page/page-network-idle.spec.ts › should wait for networkidle from the popup [fail] +page/page-network-idle.spec.ts › should wait for networkidle in setContent [fail] +page/page-network-idle.spec.ts › should wait for networkidle in setContent from the child frame [fail] +page/page-network-idle.spec.ts › should wait for networkidle in setContent with request from previous navigation [fail] +page/page-network-idle.spec.ts › should wait for networkidle in waitForNavigation [pass] +page/page-network-idle.spec.ts › should wait for networkidle to succeed navigation [pass] +page/page-network-idle.spec.ts › should wait for networkidle to succeed navigation with request from previous navigation [fail] +page/page-network-idle.spec.ts › should wait for networkidle when iframe attaches and detaches [fail] +page/page-network-idle.spec.ts › should wait for networkidle when navigating iframe [pass] +page/page-network-idle.spec.ts › should work after repeated navigations in the same page [pass] +page/page-network-request.spec.ts › page.reload return 304 status code [pass] +page/page-network-request.spec.ts › should get the same headers as the server [fail] +page/page-network-request.spec.ts › should get the same headers as the server CORS [fail] +page/page-network-request.spec.ts › should get |undefined| with postData() when there is no post data [pass] +page/page-network-request.spec.ts › should get |undefined| with postDataJSON() when there is no post data [pass] +page/page-network-request.spec.ts › should handle mixed-content blocked requests [unknown] +page/page-network-request.spec.ts › should not allow to access frame on popup main request [fail] +page/page-network-request.spec.ts › should not get preflight CORS requests when intercepting [fail] +page/page-network-request.spec.ts › should not return allHeaders() until they are available [fail] +page/page-network-request.spec.ts › should not work for a redirect and interception [pass] +page/page-network-request.spec.ts › should override post data content type [pass] +page/page-network-request.spec.ts › should parse the data if content-type is application/x-www-form-urlencoded [fail] +page/page-network-request.spec.ts › should parse the data if content-type is application/x-www-form-urlencoded; charset=UTF-8 [fail] +page/page-network-request.spec.ts › should parse the json post data [fail] +page/page-network-request.spec.ts › should report all cookies in one header [pass] +page/page-network-request.spec.ts › should report raw headers [fail] +page/page-network-request.spec.ts › should report raw response headers in redirects [pass] +page/page-network-request.spec.ts › should return event source [fail] +page/page-network-request.spec.ts › should return headers [pass] +page/page-network-request.spec.ts › should return multipart/form-data [fail] +page/page-network-request.spec.ts › should return navigation bit [pass] +page/page-network-request.spec.ts › should return navigation bit when navigating to image [pass] +page/page-network-request.spec.ts › should return postData [fail] +page/page-network-request.spec.ts › should work for a redirect [pass] +page/page-network-request.spec.ts › should work for fetch requests @smoke [pass] +page/page-network-request.spec.ts › should work for main frame navigation request [pass] +page/page-network-request.spec.ts › should work for subframe navigation request [pass] +page/page-network-request.spec.ts › should work with binary post data [fail] +page/page-network-request.spec.ts › should work with binary post data and interception [fail] +page/page-network-response.spec.ts › should behave the same way for headers and allHeaders [pass] +page/page-network-response.spec.ts › should bypass disk cache when context interception is enabled [fail] +page/page-network-response.spec.ts › should bypass disk cache when page interception is enabled [pass] +page/page-network-response.spec.ts › should provide a Response with a file URL [fail] +page/page-network-response.spec.ts › should reject response.finished if context closes [timeout] +page/page-network-response.spec.ts › should reject response.finished if page closes [pass] +page/page-network-response.spec.ts › should report all headers [fail] +page/page-network-response.spec.ts › should report if request was fromServiceWorker [fail] +page/page-network-response.spec.ts › should report multiple set-cookie headers [fail] +page/page-network-response.spec.ts › should return body [fail] +page/page-network-response.spec.ts › should return body for prefetch script [fail] +page/page-network-response.spec.ts › should return body with compression [fail] +page/page-network-response.spec.ts › should return headers after route.fulfill [pass] +page/page-network-response.spec.ts › should return json [fail] +page/page-network-response.spec.ts › should return multiple header value [fail] +page/page-network-response.spec.ts › should return set-cookie header after route.fulfill [pass] +page/page-network-response.spec.ts › should return status text [fail] +page/page-network-response.spec.ts › should return text [fail] +page/page-network-response.spec.ts › should return uncompressed text [fail] +page/page-network-response.spec.ts › should throw when requesting body of redirected response [pass] +page/page-network-response.spec.ts › should wait until response completes [fail] +page/page-network-response.spec.ts › should work @smoke [pass] +page/page-network-sizes.spec.ts › should handle redirects [pass] +page/page-network-sizes.spec.ts › should have correct responseBodySize for 404 with content [pass] +page/page-network-sizes.spec.ts › should have the correct responseBodySize [pass] +page/page-network-sizes.spec.ts › should have the correct responseBodySize for chunked request [fail] +page/page-network-sizes.spec.ts › should have the correct responseBodySize with gzip compression [pass] +page/page-network-sizes.spec.ts › should return sizes without hanging [fail] +page/page-network-sizes.spec.ts › should set bodySize and headersSize [pass] +page/page-network-sizes.spec.ts › should set bodySize to 0 if there was no body [pass] +page/page-network-sizes.spec.ts › should set bodySize to 0 when there was no response body [pass] +page/page-network-sizes.spec.ts › should set bodySize, headersSize, and transferSize [pass] +page/page-network-sizes.spec.ts › should throw for failed requests [pass] +page/page-network-sizes.spec.ts › should work with 200 status code [pass] +page/page-network-sizes.spec.ts › should work with 401 status code [pass] +page/page-network-sizes.spec.ts › should work with 404 status code [pass] +page/page-network-sizes.spec.ts › should work with 500 status code [pass] +page/page-object-count.spec.ts › should count objects [unknown] +page/page-request-continue.spec.ts › continue should delete headers on redirects [fail] +page/page-request-continue.spec.ts › continue should not change multipart/form-data body [pass] +page/page-request-continue.spec.ts › continue should propagate headers to redirects [fail] +page/page-request-continue.spec.ts › post data › should amend binary post data [fail] +page/page-request-continue.spec.ts › post data › should amend longer post data [pass] +page/page-request-continue.spec.ts › post data › should amend method and post data [pass] +page/page-request-continue.spec.ts › post data › should amend post data [pass] +page/page-request-continue.spec.ts › post data › should amend utf8 post data [fail] +page/page-request-continue.spec.ts › post data › should compute content-length from post data [pass] +page/page-request-continue.spec.ts › post data › should use content-type from original request [pass] +page/page-request-continue.spec.ts › redirected requests should report overridden headers [fail] +page/page-request-continue.spec.ts › should amend HTTP headers [pass] +page/page-request-continue.spec.ts › should amend method [pass] +page/page-request-continue.spec.ts › should amend method on main request [fail] +page/page-request-continue.spec.ts › should continue preload link requests [pass] +page/page-request-continue.spec.ts › should delete header with undefined value [pass] +page/page-request-continue.spec.ts › should delete the origin header [pass] +page/page-request-continue.spec.ts › should intercept css variable with background url [fail] +page/page-request-continue.spec.ts › should not allow changing protocol when overriding url [pass] +page/page-request-continue.spec.ts › should not throw if request was cancelled by the page [timeout] +page/page-request-continue.spec.ts › should not throw when continuing after page is closed [fail] +page/page-request-continue.spec.ts › should not throw when continuing while page is closing [fail] +page/page-request-continue.spec.ts › should override method along with url [timeout] +page/page-request-continue.spec.ts › should override request url [timeout] +page/page-request-continue.spec.ts › should work [fail] +page/page-request-continue.spec.ts › should work with Cross-Origin-Opener-Policy [pass] +page/page-request-fallback.spec.ts › post data › should amend binary post data [pass] +page/page-request-fallback.spec.ts › post data › should amend json post data [pass] +page/page-request-fallback.spec.ts › post data › should amend post data [pass] +page/page-request-fallback.spec.ts › should amend HTTP headers [pass] +page/page-request-fallback.spec.ts › should amend method [fail] +page/page-request-fallback.spec.ts › should chain once [fail] +page/page-request-fallback.spec.ts › should delete header with undefined value [pass] +page/page-request-fallback.spec.ts › should fall back [fail] +page/page-request-fallback.spec.ts › should fall back after exception [pass] +page/page-request-fallback.spec.ts › should fall back async [pass] +page/page-request-fallback.spec.ts › should not chain abort [pass] +page/page-request-fallback.spec.ts › should not chain fulfill [fail] +page/page-request-fallback.spec.ts › should override request url [fail] +page/page-request-fallback.spec.ts › should work [pass] +page/page-request-fulfill.spec.ts › headerValue should return set-cookie from intercepted response [pass] +page/page-request-fulfill.spec.ts › should allow mocking binary responses [fail] +page/page-request-fulfill.spec.ts › should allow mocking svg with charset [fail] +page/page-request-fulfill.spec.ts › should fetch original request and fulfill [fail] +page/page-request-fulfill.spec.ts › should fulfill json [pass] +page/page-request-fulfill.spec.ts › should fulfill preload link requests [fail] +page/page-request-fulfill.spec.ts › should fulfill with fetch response that has multiple set-cookie [fail] +page/page-request-fulfill.spec.ts › should fulfill with fetch result [fail] +page/page-request-fulfill.spec.ts › should fulfill with fetch result and overrides [fail] +page/page-request-fulfill.spec.ts › should fulfill with global fetch result [fail] +page/page-request-fulfill.spec.ts › should fulfill with gzip and readback [fail] +page/page-request-fulfill.spec.ts › should fulfill with har response [pass] +page/page-request-fulfill.spec.ts › should fulfill with multiple set-cookie [pass] +page/page-request-fulfill.spec.ts › should fulfill with unuassigned status codes [pass] +page/page-request-fulfill.spec.ts › should include the origin header [pass] +page/page-request-fulfill.spec.ts › should not go to the network for fulfilled requests body [fail] +page/page-request-fulfill.spec.ts › should not modify the headers sent to the server [fail] +page/page-request-fulfill.spec.ts › should not throw if request was cancelled by the page [fail] +page/page-request-fulfill.spec.ts › should stringify intercepted request response headers [pass] +page/page-request-fulfill.spec.ts › should work [pass] +page/page-request-fulfill.spec.ts › should work with buffer as body [fail] +page/page-request-fulfill.spec.ts › should work with file path [pass] +page/page-request-fulfill.spec.ts › should work with status code 422 [pass] +page/page-request-intercept.spec.ts › request.postData is not null when fetching FormData with a Blob [fail] +page/page-request-intercept.spec.ts › should fulfill intercepted response [pass] +page/page-request-intercept.spec.ts › should fulfill intercepted response using alias [pass] +page/page-request-intercept.spec.ts › should fulfill popup main request using alias [fail] +page/page-request-intercept.spec.ts › should fulfill response with empty body [fail] +page/page-request-intercept.spec.ts › should fulfill with any response [fail] +page/page-request-intercept.spec.ts › should give access to the intercepted response [pass] +page/page-request-intercept.spec.ts › should give access to the intercepted response body [fail] +page/page-request-intercept.spec.ts › should intercept multipart/form-data request body [unknown] +page/page-request-intercept.spec.ts › should intercept with post data override [pass] +page/page-request-intercept.spec.ts › should intercept with url override [fail] +page/page-request-intercept.spec.ts › should not follow redirects when maxRedirects is set to 0 in route.fetch [fail] +page/page-request-intercept.spec.ts › should override with defaults when intercepted response not provided [fail] +page/page-request-intercept.spec.ts › should support fulfill after intercept [fail] +page/page-request-intercept.spec.ts › should support timeout option in route.fetch [pass] +page/page-route.spec.ts › route.abort should throw if called twice [pass] +page/page-route.spec.ts › route.continue should throw if called twice [pass] +page/page-route.spec.ts › route.fallback should throw if called twice [pass] +page/page-route.spec.ts › route.fulfill should throw if called twice [fail] +page/page-route.spec.ts › should add Access-Control-Allow-Origin by default when fulfill [fail] +page/page-route.spec.ts › should allow null origin for about:blank [fail] +page/page-route.spec.ts › should be able to fetch dataURL and not fire dataURL requests [fail] +page/page-route.spec.ts › should be able to remove headers [pass] +page/page-route.spec.ts › should be abortable [pass] +page/page-route.spec.ts › should be abortable with custom error codes [fail] +page/page-route.spec.ts › should chain fallback w/ dynamic URL [fail] +page/page-route.spec.ts › should contain raw request header [pass] +page/page-route.spec.ts › should contain raw response header [pass] +page/page-route.spec.ts › should contain raw response header after fulfill [pass] +page/page-route.spec.ts › should contain referer header [fail] +page/page-route.spec.ts › should fail navigation when aborting main resource [fail] +page/page-route.spec.ts › should fulfill with redirect status [fail] +page/page-route.spec.ts › should intercept @smoke [fail] +page/page-route.spec.ts › should intercept main resource during cross-process navigation [pass] +page/page-route.spec.ts › should intercept when postData is more than 1MB [fail] +page/page-route.spec.ts › should navigate to URL with hash and and fire requests without hash [pass] +page/page-route.spec.ts › should navigate to dataURL and not fire dataURL requests [pass] +page/page-route.spec.ts › should not auto-intercept non-preflight OPTIONS [fail] +page/page-route.spec.ts › should not fulfill with redirect status [unknown] +page/page-route.spec.ts › should not throw "Invalid Interception Id" if the request was cancelled [fail] +page/page-route.spec.ts › should not throw if request was cancelled by the page [timeout] +page/page-route.spec.ts › should not work with redirects [fail] +page/page-route.spec.ts › should override cookie header [fail] +page/page-route.spec.ts › should pause intercepted XHR until continue [fail] +page/page-route.spec.ts › should pause intercepted fetch request until continue [pass] +page/page-route.spec.ts › should properly return navigation response when URL has cookies [pass] +page/page-route.spec.ts › should reject cors with disallowed credentials [fail] +page/page-route.spec.ts › should respect cors overrides [fail] +page/page-route.spec.ts › should send referer [fail] +page/page-route.spec.ts › should show custom HTTP headers [fail] +page/page-route.spec.ts › should support ? in glob pattern [fail] +page/page-route.spec.ts › should support async handler w/ times [pass] +page/page-route.spec.ts › should support cors for different methods [fail] +page/page-route.spec.ts › should support cors with GET [pass] +page/page-route.spec.ts › should support cors with POST [fail] +page/page-route.spec.ts › should support cors with credentials [fail] +page/page-route.spec.ts › should support the times parameter with route matching [fail] +page/page-route.spec.ts › should unroute [pass] +page/page-route.spec.ts › should work if handler with times parameter was removed from another handler [pass] +page/page-route.spec.ts › should work when POST is redirected with 302 [fail] +page/page-route.spec.ts › should work when header manipulation headers with redirect [fail] +page/page-route.spec.ts › should work with badly encoded server [pass] +page/page-route.spec.ts › should work with custom referer headers [fail] +page/page-route.spec.ts › should work with encoded server [fail] +page/page-route.spec.ts › should work with encoded server - 2 [fail] +page/page-route.spec.ts › should work with equal requests [fail] +page/page-route.spec.ts › should work with redirect inside sync XHR [pass] +page/page-route.spec.ts › should work with redirects for subresources [fail] +page/page-screenshot.spec.ts › page screenshot animations › should capture screenshots after layoutchanges in transitionend event [pass] +page/page-screenshot.spec.ts › page screenshot animations › should fire transitionend for finite transitions [pass] +page/page-screenshot.spec.ts › page screenshot animations › should not capture css animations in shadow DOM [fail] +page/page-screenshot.spec.ts › page screenshot animations › should not capture infinite css animation [fail] +page/page-screenshot.spec.ts › page screenshot animations › should not capture infinite web animations [fail] +page/page-screenshot.spec.ts › page screenshot animations › should not capture pseudo element css animation [fail] +page/page-screenshot.spec.ts › page screenshot animations › should not change animation with playbackRate equal to 0 [pass] +page/page-screenshot.spec.ts › page screenshot animations › should resume infinite animations [pass] +page/page-screenshot.spec.ts › page screenshot animations › should stop animations that happen right before screenshot [pass] +page/page-screenshot.spec.ts › page screenshot animations › should trigger particular events for INfinite css animation [pass] +page/page-screenshot.spec.ts › page screenshot animations › should trigger particular events for css transitions [pass] +page/page-screenshot.spec.ts › page screenshot animations › should trigger particular events for finite css animation [pass] +page/page-screenshot.spec.ts › page screenshot animations › should wait for fonts to load [fail] +page/page-screenshot.spec.ts › page screenshot should capture css transform [fail] +page/page-screenshot.spec.ts › page screenshot › mask option › should hide elements based on attr [fail] +page/page-screenshot.spec.ts › page screenshot › mask option › should mask in parallel [fail] +page/page-screenshot.spec.ts › page screenshot › mask option › should mask inside iframe [fail] +page/page-screenshot.spec.ts › page screenshot › mask option › should mask multiple elements [fail] +page/page-screenshot.spec.ts › page screenshot › mask option › should remove elements based on attr [fail] +page/page-screenshot.spec.ts › page screenshot › mask option › should remove mask after screenshot [pass] +page/page-screenshot.spec.ts › page screenshot › mask option › should work [fail] +page/page-screenshot.spec.ts › page screenshot › mask option › should work when mask color is not pink #F0F [fail] +page/page-screenshot.spec.ts › page screenshot › mask option › should work when subframe has stalled navigation [fail] +page/page-screenshot.spec.ts › page screenshot › mask option › should work when subframe used document.open after a weird url [fail] +page/page-screenshot.spec.ts › page screenshot › mask option › should work with elementhandle [fail] +page/page-screenshot.spec.ts › page screenshot › mask option › should work with locator [fail] +page/page-screenshot.spec.ts › page screenshot › path option should create subdirectories [pass] +page/page-screenshot.spec.ts › page screenshot › path option should detect jpeg [fail] +page/page-screenshot.spec.ts › page screenshot › path option should throw for unsupported mime type [pass] +page/page-screenshot.spec.ts › page screenshot › path option should work [fail] +page/page-screenshot.spec.ts › page screenshot › quality option should throw for png [pass] +page/page-screenshot.spec.ts › page screenshot › should allow transparency [fail] +page/page-screenshot.spec.ts › page screenshot › should capture blinking caret if explicitly asked for [fail] +page/page-screenshot.spec.ts › page screenshot › should capture blinking caret in shadow dom [pass] +page/page-screenshot.spec.ts › page screenshot › should capture canvas changes [fail] +page/page-screenshot.spec.ts › page screenshot › should clip elements to the viewport [fail] +page/page-screenshot.spec.ts › page screenshot › should clip rect [fail] +page/page-screenshot.spec.ts › page screenshot › should clip rect with fullPage [fail] +page/page-screenshot.spec.ts › page screenshot › should not capture blinking caret by default [fail] +page/page-screenshot.spec.ts › page screenshot › should not issue resize event [pass] +page/page-screenshot.spec.ts › page screenshot › should prefer type over extension [fail] +page/page-screenshot.spec.ts › page screenshot › should render white background on jpeg file [fail] +page/page-screenshot.spec.ts › page screenshot › should restore viewport after fullPage screenshot [fail] +page/page-screenshot.spec.ts › page screenshot › should run in parallel [fail] +page/page-screenshot.spec.ts › page screenshot › should take fullPage screenshots [fail] +page/page-screenshot.spec.ts › page screenshot › should take fullPage screenshots and mask elements outside of it [fail] +page/page-screenshot.spec.ts › page screenshot › should take fullPage screenshots during navigation [pass] +page/page-screenshot.spec.ts › page screenshot › should throw on clip outside the viewport [pass] +page/page-screenshot.spec.ts › page screenshot › should work @smoke [fail] +page/page-screenshot.spec.ts › page screenshot › should work for canvas [fail] +page/page-screenshot.spec.ts › page screenshot › should work for translateZ [fail] +page/page-screenshot.spec.ts › page screenshot › should work for webgl [fail] +page/page-screenshot.spec.ts › page screenshot › should work while navigating [fail] +page/page-screenshot.spec.ts › page screenshot › should work with Array deleted [fail] +page/page-screenshot.spec.ts › page screenshot › should work with iframe in shadow [fail] +page/page-screenshot.spec.ts › page screenshot › should work with odd clip size on Retina displays [fail] +page/page-screenshot.spec.ts › page screenshot › zero quality option should throw for png [fail] +page/page-screenshot.spec.ts › should capture css box-shadow [fail] +page/page-screenshot.spec.ts › should throw if screenshot size is too large [fail] +page/page-select-option.spec.ts › input event.composed should be true and cross shadow dom boundary [fail] +page/page-select-option.spec.ts › should deselect all options when passed no values for a multiple select [pass] +page/page-select-option.spec.ts › should deselect all options when passed no values for a select without multiple [pass] +page/page-select-option.spec.ts › should fall back to selecting by label [pass] +page/page-select-option.spec.ts › should not allow null items [pass] +page/page-select-option.spec.ts › should not select single option when some attributes do not match [pass] +page/page-select-option.spec.ts › should not throw when select causes navigation [pass] +page/page-select-option.spec.ts › should respect event bubbling [pass] +page/page-select-option.spec.ts › should return [] on no matched values [pass] +page/page-select-option.spec.ts › should return [] on no values [pass] +page/page-select-option.spec.ts › should return an array of matched values [fail] +page/page-select-option.spec.ts › should return an array of one element when multiple is not set [pass] +page/page-select-option.spec.ts › should select multiple options [pass] +page/page-select-option.spec.ts › should select multiple options with attributes [pass] +page/page-select-option.spec.ts › should select only first option [pass] +page/page-select-option.spec.ts › should select single option @smoke [pass] +page/page-select-option.spec.ts › should select single option by handle [pass] +page/page-select-option.spec.ts › should select single option by index [pass] +page/page-select-option.spec.ts › should select single option by label [pass] +page/page-select-option.spec.ts › should select single option by multiple attributes [pass] +page/page-select-option.spec.ts › should select single option by value [pass] +page/page-select-option.spec.ts › should throw if passed wrong types [fail] +page/page-select-option.spec.ts › should throw when element is not a + `); + expect(await page.evaluate(() => document.activeElement.tagName)).toBe('BODY'); + await page.keyboard.press('Tab'); + expect(await page.evaluate(() => document.activeElement.id)).toBe('input1'); + expect(await page.evaluate(() => (window as any).focusEvents)).toEqual(['focus']); + await page.keyboard.press('Tab'); + expect(await page.evaluate(() => document.activeElement.tagName)).toBe('BODY'); + expect(await page.evaluate(() => (window as any).focusEvents)).toEqual(['focus', 'blur']); + await page.keyboard.press('Tab'); + expect(await page.evaluate(() => document.activeElement.id)).toBe('input1'); + expect(await page.evaluate(() => (window as any).focusEvents)).toEqual(['focus', 'blur', 'focus']); +}); + +it('tab should cycle between document elements and browser', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32339' } +}, async ({ page, browserName, headless }) => { + it.fixme(browserName === 'chromium' && (!headless || !!process.env.PLAYWRIGHT_CHROMIUM_USE_HEADLESS_NEW), + 'Chromium in headful mode keeps last input focused.'); + it.fixme(browserName !== 'chromium'); + await page.setContent(` + + + `); + expect(await page.evaluate(() => document.activeElement.tagName)).toBe('BODY'); + await page.keyboard.press('Tab'); + expect(await page.evaluate(() => document.activeElement.id)).toBe('input1'); + expect(await page.evaluate(() => (window as any).focusEvents)).toEqual(['focus1']); + await page.keyboard.press('Tab'); + expect(await page.evaluate(() => document.activeElement.id)).toBe('input2'); + expect(await page.evaluate(() => (window as any).focusEvents)).toEqual(['focus1', 'blur1', 'focus2']); + await page.keyboard.press('Tab'); + expect(await page.evaluate(() => document.activeElement.tagName)).toBe('BODY'); + expect(await page.evaluate(() => (window as any).focusEvents)).toEqual(['focus1', 'blur1', 'focus2', 'blur2']); + await page.keyboard.press('Tab'); + expect(await page.evaluate(() => document.activeElement.id)).toBe('input1'); + expect(await page.evaluate(() => (window as any).focusEvents)).toEqual(['focus1', 'blur1', 'focus2', 'blur2', 'focus1']); +}); + it('keeps focus on element when attempting to focus a non-focusable element', async ({ page }) => { it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/14254' }); diff --git a/tests/page/page-leaks.spec.ts b/tests/page/page-leaks.spec.ts index b33ce8e1d4..ce356f1e12 100644 --- a/tests/page/page-leaks.spec.ts +++ b/tests/page/page-leaks.spec.ts @@ -83,9 +83,11 @@ test('click should not leak', async ({ page, browserName, toImpl }) => { expect(leakedJSHandles()).toBeFalsy(); if (browserName === 'chromium') { - const counts = await objectCounts(toImpl(page), 'HTMLButtonElement'); - expect(counts.main + counts.utility).toBeGreaterThanOrEqual(2); - expect(counts.main + counts.utility).toBeLessThan(25); + await expect(async () => { + const counts = await objectCounts(toImpl(page), 'HTMLButtonElement'); + expect(counts.main + counts.utility).toBeGreaterThanOrEqual(2); + expect(counts.main + counts.utility).toBeLessThan(25); + }).toPass(); } }); @@ -114,9 +116,11 @@ test('fill should not leak', async ({ page, mode, browserName, toImpl }) => { expect(leakedJSHandles()).toBeFalsy(); if (browserName === 'chromium') { - const counts = await objectCounts(toImpl(page), 'HTMLInputElement'); - expect(counts.main + counts.utility).toBeGreaterThanOrEqual(2); - expect(counts.main + counts.utility).toBeLessThan(25); + await expect(async () => { + const counts = await objectCounts(toImpl(page), 'HTMLInputElement'); + expect(counts.main + counts.utility).toBeGreaterThanOrEqual(2); + expect(counts.main + counts.utility).toBeLessThan(25); + }).toPass(); } }); @@ -144,9 +148,11 @@ test('expect should not leak', async ({ page, mode, browserName, toImpl }) => { expect(leakedJSHandles()).toBeFalsy(); if (browserName === 'chromium') { - const counts = await objectCounts(toImpl(page), 'HTMLButtonElement'); - expect(counts.main + counts.utility).toBeGreaterThanOrEqual(2); - expect(counts.main + counts.utility).toBeLessThan(25); + await expect(async () => { + const counts = await objectCounts(toImpl(page), 'HTMLButtonElement'); + expect(counts.main + counts.utility).toBeGreaterThanOrEqual(2); + expect(counts.main + counts.utility).toBeLessThan(25); + }).toPass(); } }); @@ -174,8 +180,10 @@ test('waitFor should not leak', async ({ page, mode, browserName, toImpl }) => { expect(leakedJSHandles()).toBeFalsy(); if (browserName === 'chromium') { - const counts = await objectCounts(toImpl(page), 'HTMLButtonElement'); - expect(counts.main + counts.utility).toBeGreaterThanOrEqual(2); - expect(counts.main + counts.utility).toBeLessThan(25); + await expect(async () => { + const counts = await objectCounts(toImpl(page), 'HTMLButtonElement'); + expect(counts.main + counts.utility).toBeGreaterThanOrEqual(2); + expect(counts.main + counts.utility).toBeLessThan(25); + }).toPass(); } }); diff --git a/tests/page/page-screenshot.spec.ts b/tests/page/page-screenshot.spec.ts index 8d0e68e12d..27cc28c141 100644 --- a/tests/page/page-screenshot.spec.ts +++ b/tests/page/page-screenshot.spec.ts @@ -280,12 +280,15 @@ it.describe('page screenshot', () => { expect(screenshot).toMatchSnapshot('screenshot-clip-odd-size.png'); }); - it('should work for canvas', async ({ page, server, isElectron, isMac }) => { + it('should work for canvas', async ({ page, server, isElectron, isMac, browserName, headless }) => { it.fixme(isElectron && isMac, 'Fails on the bots'); await page.setViewportSize({ width: 500, height: 500 }); await page.goto(server.PREFIX + '/screenshots/canvas.html'); const screenshot = await page.screenshot(); - expect(screenshot).toMatchSnapshot('screenshot-canvas.png'); + if (!headless && browserName === 'chromium' && isMac && os.arch() === 'arm64' && /* macOS 14+ */ parseInt(os.release(), 10) >= 23) + expect(screenshot).toMatchSnapshot('screenshot-canvas-with-accurate-corners.png'); + else + expect(screenshot).toMatchSnapshot('screenshot-canvas.png'); }); it('should capture canvas changes', async ({ page, isElectron, browserName, isMac, isWebView2 }) => { @@ -323,7 +326,7 @@ it.describe('page screenshot', () => { it('should work for webgl', async ({ page, server, browserName, platform }) => { it.fixme(browserName === 'firefox'); it.fixme(browserName === 'chromium' && platform === 'darwin' && os.arch() === 'arm64', 'SwiftShader is not available on macOS-arm64 - https://github.com/microsoft/playwright/issues/28216'); - it.skip(browserName === 'webkit' && platform === 'darwin' && parseInt(os.release(), 10) === 22, 'WebGL is not available in macOS-13 - https://bugs.webkit.org/show_bug.cgi?id=278277'); + it.skip(browserName === 'webkit' && platform === 'darwin' && os.arch() === 'x64', 'Modernizr uses WebGL which is not available on Intel macOS - https://bugs.webkit.org/show_bug.cgi?id=278277'); await page.setViewportSize({ width: 640, height: 480 }); await page.goto(server.PREFIX + '/screenshots/webgl.html'); diff --git a/tests/page/page-screenshot.spec.ts-snapshots/screenshot-canvas-with-accurate-corners-chromium.png b/tests/page/page-screenshot.spec.ts-snapshots/screenshot-canvas-with-accurate-corners-chromium.png new file mode 100644 index 0000000000..830872e8d2 Binary files /dev/null and b/tests/page/page-screenshot.spec.ts-snapshots/screenshot-canvas-with-accurate-corners-chromium.png differ diff --git a/tests/playwright-test/expect.spec.ts b/tests/playwright-test/expect.spec.ts index a541ad9c96..8f82c256fb 100644 --- a/tests/playwright-test/expect.spec.ts +++ b/tests/playwright-test/expect.spec.ts @@ -1068,4 +1068,38 @@ test('expect.extend should be immutable', async ({ runInlineTest }) => { 'foo', 'bar', ]); -}); \ No newline at end of file +}); + +test('expect.extend should fall back to legacy behavior', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'expect-test.spec.ts': ` + import { test, expect } from '@playwright/test'; + expect.extend({ + toFoo() { + console.log('%%foo'); + return { pass: true }; + } + }); + expect.extend({ + toFoo() { + console.log('%%foo2'); + return { pass: true }; + } + }); + expect.extend({ + toBar() { + console.log('%%bar'); + return { pass: true }; + } + }); + test('logs', () => { + expect().toFoo(); + expect().toBar(); + }); + ` + }); + expect(result.outputLines).toEqual([ + 'foo2', + 'bar', + ]); +}); diff --git a/tests/playwright-test/only-changed.spec.ts b/tests/playwright-test/only-changed.spec.ts index 674234a240..b632ba262a 100644 --- a/tests/playwright-test/only-changed.spec.ts +++ b/tests/playwright-test/only-changed.spec.ts @@ -26,6 +26,7 @@ const test = baseTest.extend<{ git(command: string): void }>({ git(`init --initial-branch=main`); git(`config --local user.name "Robert Botman"`); git(`config --local user.email "botty@mcbotface.com"`); + git(`config --local core.autocrlf false`); await use((command: string) => git(command)); }, diff --git a/tests/playwright-test/playwright.trace.spec.ts b/tests/playwright-test/playwright.trace.spec.ts index 5ca08925b4..ba6020fade 100644 --- a/tests/playwright-test/playwright.trace.spec.ts +++ b/tests/playwright-test/playwright.trace.spec.ts @@ -1216,7 +1216,6 @@ test('should not nest top level expect into unfinished api calls ', { ' browserContext.newPage', 'page.route', 'page.goto', - 'route.fetch', 'expect.toBeVisible', 'page.unrouteAll', 'After Hooks', diff --git a/tests/playwright-test/test-step.spec.ts b/tests/playwright-test/test-step.spec.ts index f0bd883777..cbd4ec38c4 100644 --- a/tests/playwright-test/test-step.spec.ts +++ b/tests/playwright-test/test-step.spec.ts @@ -546,37 +546,6 @@ fixture | fixture: browser `); }); -test('should not nest page.continue inside page.goto steps', async ({ runInlineTest }) => { - const result = await runInlineTest({ - 'reporter.ts': stepIndentReporter, - 'playwright.config.ts': `module.exports = { reporter: './reporter', };`, - 'a.test.ts': ` - import { test, expect } from '@playwright/test'; - test('pass', async ({ page }) => { - await page.route('**/*', route => route.fulfill('')); - await page.goto('http://localhost:1234'); - }); - ` - }, { reporter: '' }); - - expect(result.exitCode).toBe(0); - expect(result.output).toBe(` -hook |Before Hooks -fixture | fixture: browser -pw:api | browserType.launch -fixture | fixture: context -pw:api | browser.newContext -fixture | fixture: page -pw:api | browserContext.newPage -pw:api |page.route @ a.test.ts:4 -pw:api |page.goto(http://localhost:1234) @ a.test.ts:5 -pw:api |route.fulfill @ a.test.ts:4 -hook |After Hooks -fixture | fixture: page -fixture | fixture: context -`); -}); - test('should not propagate errors from within toPass', async ({ runInlineTest }) => { const result = await runInlineTest({ 'reporter.ts': stepIndentReporter, diff --git a/tests/playwright-test/ui-mode-test-run.spec.ts b/tests/playwright-test/ui-mode-test-run.spec.ts index 9c1120ea04..81316f42ee 100644 --- a/tests/playwright-test/ui-mode-test-run.spec.ts +++ b/tests/playwright-test/ui-mode-test-run.spec.ts @@ -444,3 +444,62 @@ test('should show proper total when using deps', async ({ runUITest }) => { `); await expect(page.getByTestId('status-line')).toHaveText('2/2 passed (100%)'); }); + +test('should respect --tsconfig option', { + annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32797' } +}, async ({ runUITest }) => { + const { page } = await runUITest({ + 'playwright.config.ts': ` + import { foo } from '~/foo'; + export default { + testDir: './tests' + foo, + }; + `, + 'tsconfig.json': `{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "~/*": ["./does-not-exist/*"], + }, + }, + }`, + 'tsconfig.special.json': `{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "~/*": ["./mapped-from-root/*"], + }, + }, + }`, + 'mapped-from-root/foo.ts': ` + export const foo = 42; + `, + 'tests42/tsconfig.json': `{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "~/*": ["../should-be-ignored/*"], + }, + }, + }`, + 'tests42/a.test.ts': ` + import { foo } from '~/foo'; + import { test, expect } from '@playwright/test'; + test('test', ({}) => { + expect(foo).toBe(42); + }); + `, + 'should-be-ignored/foo.ts': ` + export const foo = 43; + `, + }, undefined, { additionalArgs: ['--tsconfig=tsconfig.special.json'] }); + + await page.getByTitle('Run all').click(); + + await expect.poll(dumpTestTree(page)).toBe(` + ▼ ✅ a.test.ts + ✅ test + `); + + await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)'); +}); \ No newline at end of file diff --git a/utils/doclint/api_parser.js b/utils/doclint/api_parser.js index a593872f99..f2f53cbd1f 100644 --- a/utils/doclint/api_parser.js +++ b/utils/doclint/api_parser.js @@ -192,8 +192,7 @@ class ApiParser { method.argsArray.push(options); } p.required = false; - // @ts-ignore - options.type.properties.push(p); + options.type?.properties?.push(p); } } diff --git a/utils/doclint/documentation.js b/utils/doclint/documentation.js index 77b20ca769..e3e92c3ec7 100644 --- a/utils/doclint/documentation.js +++ b/utils/doclint/documentation.js @@ -363,11 +363,6 @@ class Member { this.alias = match[1]; this.overloadIndex = (+match[2]) - 1; } - /** - * Param is true and option false - * @type {Boolean | null} - */ - this.paramOrOption = null; } index() { @@ -384,10 +379,8 @@ class Member { for (const arg of this.argsArray) { this.args.set(arg.name, arg); arg.enclosingMethod = this; - if (arg.name === 'options') { - // @ts-ignore - arg.type.properties.sort((p1, p2) => p1.name.localeCompare(p2.name)); - } + if (arg.name === 'options') + arg.type?.properties?.sort((p1, p2) => p1.name.localeCompare(p2.name)); indexArg(arg); } } @@ -410,11 +403,9 @@ class Member { continue; const overriddenArg = (arg.langs.overrides && arg.langs.overrides[lang]) || arg; overriddenArg.filterForLanguage(lang, options); - // @ts-ignore - if (overriddenArg.name === 'options' && !overriddenArg.type.properties.length) + if (overriddenArg.name === 'options' && !overriddenArg.type?.properties?.length) continue; - // @ts-ignore - overriddenArg.type.filterForLanguage(lang, options); + overriddenArg.type?.filterForLanguage(lang, options); argsArray.push(overriddenArg); } this.argsArray = argsArray; @@ -433,7 +424,6 @@ class Member { const result = new Member(this.kind, { langs: this.langs, since: this.since, deprecated: this.deprecated, discouraged: this.discouraged }, this.name, this.type?.clone(), this.argsArray.map(arg => arg.clone()), this.spec, this.required); result.alias = this.alias; result.async = this.async; - result.paramOrOption = this.paramOrOption; return result; } @@ -526,8 +516,7 @@ class Type { if (!inUnion && (parsedType.union || parsedType.unionName)) { const type = new Type(parsedType.unionName || ''); type.union = []; - // @ts-ignore - for (let t = parsedType; t; t = t.union) { + for (let /** @type {ParsedType | null} */ t = parsedType; t; t = t.union) { const nestedUnion = !!t.unionName && t !== parsedType; type.union.push(Type.fromParsedType(t, !nestedUnion)); if (nestedUnion) @@ -539,7 +528,6 @@ class Type { if (parsedType.args || parsedType.retType) { const type = new Type('function'); type.args = []; - // @ts-ignore for (let t = parsedType.args; t; t = t.next) type.args.push(Type.fromParsedType(t)); type.returnType = parsedType.retType ? Type.fromParsedType(parsedType.retType) : undefined; @@ -549,8 +537,7 @@ class Type { if (parsedType.template) { const type = new Type(parsedType.name); type.templates = []; - // @ts-ignore - for (let t = parsedType.template; t; t = t.next) + for (let /** @type {ParsedType | null} */ t = parsedType.template; t; t = t.next) type.templates.push(Type.fromParsedType(t)); return type; } @@ -613,17 +600,6 @@ class Type { return []; } - /** - * @returns {Member[] | undefined} - */ - sortedProperties() { - if (!this.properties) - return this.properties; - const sortedProperties = [...this.properties]; - sortedProperties.sort((p1, p2) => p1.name.localeCompare(p2.name)); - return sortedProperties; - } - /** * @param {string} lang * @param {LanguageOptions=} options @@ -768,11 +744,10 @@ function patchLinksInText(classOrMember, text, classesMap, membersMap, linkRende let alias = p2; if (classOrMember) { // param/option reference can only be in method or same method parameter comments. - // @ts-ignore - const method = classOrMember.enclosingMethod; - const param = method.argsArray.find(a => a.name === p2); + const method = /** @type {Member} */(classOrMember).enclosingMethod; + const param = method?.argsArray.find(a => a.name === p2); if (!param) - throw new Error(`Referenced parameter ${match} not found in the parent method ${method.name} `); + throw new Error(`Referenced parameter ${match} not found in the parent method ${method?.name} `); alias = param.alias; } return linkRenderer({ param: alias, href }) || match; diff --git a/utils/doclint/generateApiJson.js b/utils/doclint/generateApiJson.js index bb639abbaf..028e983d75 100644 --- a/utils/doclint/generateApiJson.js +++ b/utils/doclint/generateApiJson.js @@ -17,7 +17,6 @@ // @ts-check const path = require('path'); -const Documentation = require('./documentation'); const { parseApi } = require('./api_parser'); const PROJECT_DIR = path.join(__dirname, '..', '..'); @@ -38,14 +37,14 @@ const PROJECT_DIR = path.join(__dirname, '..', '..'); } /** - * @param {Documentation} documentation + * @param {import('./documentation').Documentation} documentation */ function serialize(documentation) { return documentation.classesArray.map(serializeClass); } /** - * @param {Documentation.Class} clazz + * @param {import('./documentation').Class} clazz */ function serializeClass(clazz) { const result = { name: clazz.name, spec: clazz.spec }; @@ -65,7 +64,7 @@ function serializeClass(clazz) { } /** - * @param {Documentation.Member} member + * @param {import('./documentation').Member} member */ function serializeMember(member) { const result = /** @type {any} */ ({ ...member }); @@ -76,14 +75,20 @@ function serializeMember(member) { return result; } +/** + * @param {import('./documentation').Member} arg + */ function serializeProperty(arg) { const result = { ...arg, parent: undefined }; sanitize(result); if (arg.type) - result.type = serializeType(arg.type, arg.name === 'options'); + result.type = serializeType(arg.type); return result; } +/** + * @param {object} result + */ function sanitize(result) { delete result.args; delete result.argsArray; @@ -92,14 +97,13 @@ function sanitize(result) { } /** - * @param {Documentation.Type} type - * @param {boolean} sortProperties + * @param {import('./documentation').Type} type */ -function serializeType(type, sortProperties = false) { +function serializeType(type) { /** @type {any} */ const result = { ...type }; if (type.properties) - result.properties = (sortProperties ? type.sortedProperties() : type.properties).map(serializeProperty); + result.properties = type.properties.map(serializeProperty); if (type.union) result.union = type.union.map(type => serializeType(type)); if (type.templates) diff --git a/utils/generate_injected.js b/utils/generate_injected.js index cb56ce2db5..bffef1cfaa 100644 --- a/utils/generate_injected.js +++ b/utils/generate_injected.js @@ -45,7 +45,7 @@ const injectedScripts = [ true, ], [ - path.join(ROOT, 'packages', 'playwright-core', 'src', 'server', 'injected', 'recorder', 'recorder.ts'), + path.join(ROOT, 'packages', 'playwright-core', 'src', 'server', 'injected', 'recorder', 'pollingRecorder.ts'), path.join(ROOT, 'packages', 'playwright-core', 'lib', 'server', 'injected', 'packed'), path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'), true, diff --git a/utils/generate_types/index.js b/utils/generate_types/index.js index eb1527c80b..ae988ac32c 100644 --- a/utils/generate_types/index.js +++ b/utils/generate_types/index.js @@ -91,7 +91,7 @@ class TypesGenerator { if (!docClass) return ''; handledClasses.add(className); - return this.writeComment(docClass.comment) + '\n'; + return this.writeComment(docClass.comment, '') + '\n'; }, (className, methodName, overloadIndex) => { if (className === 'SuiteFunction' && methodName === '__call') { const cls = this.documentation.classes.get('Test'); @@ -218,7 +218,7 @@ class TypesGenerator { classToString(classDesc) { const parts = []; if (classDesc.comment) { - parts.push(this.writeComment(classDesc.comment)) + parts.push(this.writeComment(classDesc.comment, '')) } const shouldExport = !this.doNotExportClassNames.has(classDesc.name); parts.push(`${shouldExport ? 'export ' : ''}interface ${classDesc.name} ${classDesc.extends ? `extends ${classDesc.extends} ` : ''}{`); @@ -258,7 +258,7 @@ class TypesGenerator { const descriptions = []; for (let [eventName, value] of classDesc.events) { eventName = eventName.toLowerCase(); - const type = this.stringifyComplexType(value && value.type, 'out', ' ', classDesc.name, eventName, 'payload'); + const type = this.stringifyComplexType(value && value.type, 'out', ' ', [classDesc.name, eventName, 'payload']); const argName = this.argNameForType(type); const params = argName ? `${argName}: ${type}` : ''; descriptions.push({ @@ -311,8 +311,8 @@ class TypesGenerator { return parts.join('\n'); } const jsdoc = this.memberJSDOC(member, indent); - const args = this.argsFromMember(member, indent, classDesc.name); - let type = this.stringifyComplexType(member.type, 'out', indent, classDesc.name, member.alias); + const args = this.argsFromMember(member, indent, [classDesc.name]); + let type = this.stringifyComplexType(member.type, 'out', indent, [classDesc.name, member.alias]); if (member.async) type = `Promise<${type}>`; // do this late, because we still want object definitions for overridden types @@ -351,7 +351,12 @@ class TypesGenerator { return this.documentation.classes.get(classDesc.extends); } - writeComment(comment, indent = '') { + /** + * @param {string} comment + * @param {string} indent + * @returns {string} + */ + writeComment(comment, indent) { const parts = []; const out = []; const pushLine = (line) => { @@ -387,26 +392,30 @@ class TypesGenerator { /** * @param {docs.Type|null} type + * @param {'in' | 'out'} direction + * @param {string} indent + * @param {string[]} namespace + * @returns {string} */ - stringifyComplexType(type, direction, indent, ...namespace) { + stringifyComplexType(type, direction, indent, namespace) { if (!type) return 'void'; - return this.stringifySimpleType(type, direction, indent, ...namespace); + return this.stringifySimpleType(type, direction, indent, namespace); } /** * @param {docs.Member[]} properties * @param {string} name - * @param {string=} indent + * @param {string} indent * @returns {string} */ - stringifyObjectType(properties, name, indent = '') { + stringifyObjectType(properties, name, indent) { const parts = []; parts.push(`{`); parts.push(properties.map(member => { const comment = this.memberJSDOC(member, indent + ' '); - const args = this.argsFromMember(member, indent + ' ', name); - const type = this.stringifyComplexType(member.type, 'out', indent + ' ', name, member.name); + const args = this.argsFromMember(member, indent + ' ', [name]); + const type = this.stringifyComplexType(member.type, 'out', indent + ' ', [name, member.name]); return `${comment}${this.nameForProperty(member)}${args}: ${type};`; }).join('\n\n')); parts.push(indent + '}'); @@ -416,14 +425,16 @@ class TypesGenerator { /** * @param {docs.Type | null | undefined} type * @param {'in' | 'out'} direction - * @returns{string} + * @param {string} indent + * @param {string[]} namespace + * @returns {string} */ - stringifySimpleType(type, direction, indent = '', ...namespace) { + stringifySimpleType(type, direction, indent, namespace) { if (!type) return 'void'; if (type.name === 'Object' && type.templates) { - const keyType = this.stringifySimpleType(type.templates[0], direction, indent, ...namespace); - const valueType = this.stringifySimpleType(type.templates[1], direction, indent, ...namespace); + const keyType = this.stringifySimpleType(type.templates[0], direction, indent, namespace); + const valueType = this.stringifySimpleType(type.templates[1], direction, indent, namespace); return `{ [key: ${keyType}]: ${valueType}; }`; } let out = type.name; @@ -434,7 +445,7 @@ class TypesGenerator { if (type.name === 'Object' && type.properties && type.properties.length) { const name = namespace.map(n => n[0].toUpperCase() + n.substring(1)).join(''); const shouldExport = exported[name]; - const properties = namespace[namespace.length - 1] === 'options' ? type.sortedProperties() : type.properties; + const properties = type.properties; if (!properties) throw new Error(`Object type must have properties`); if (!this.objectDefinitions.some(o => o.name === name)) @@ -448,10 +459,10 @@ class TypesGenerator { if (type.args) { const stringArgs = type.args.map(a => ({ - type: this.stringifySimpleType(a, direction, indent, ...namespace), + type: this.stringifySimpleType(a, direction, indent, namespace), name: a.name.toLowerCase() })); - out = `((${stringArgs.map(({ name, type }) => `${name}: ${type}`).join(', ')}) => ${this.stringifySimpleType(type.returnType, 'out', indent, ...namespace)})`; + out = `((${stringArgs.map(({ name, type }) => `${name}: ${type}`).join(', ')}) => ${this.stringifySimpleType(type.returnType, 'out', indent, namespace)})`; } else if (type.name === 'function') { out = 'Function'; } @@ -460,19 +471,22 @@ class TypesGenerator { if (out === 'Any') return 'any'; if (type.templates) - out += '<' + type.templates.map(t => this.stringifySimpleType(t, direction, indent, ...namespace)).join(', ') + '>'; + out += '<' + type.templates.map(t => this.stringifySimpleType(t, direction, indent, namespace)).join(', ') + '>'; if (type.union) - out = type.union.map(t => this.stringifySimpleType(t, direction, indent, ...namespace)).join('|'); + out = type.union.map(t => this.stringifySimpleType(t, direction, indent, namespace)).join('|'); return out.trim(); } /** * @param {docs.Member} member + * @param {string} indent + * @param {string[]} namespace + * @returns {string} */ - argsFromMember(member, indent, ...namespace) { + argsFromMember(member, indent, namespace) { if (member.kind === 'property') return ''; - return '(' + member.argsArray.map(arg => `${this.nameForProperty(arg)}: ${this.stringifyComplexType(arg.type, 'in', indent, ...namespace, member.alias, arg.alias)}`).join(', ') + ')'; + return '(' + member.argsArray.map(arg => `${this.nameForProperty(arg)}: ${this.stringifyComplexType(arg.type, 'in', indent, [...namespace, member.alias, arg.alias])}`).join(', ') + ')'; } /** diff --git a/utils/upload_flakiness_dashboard.sh b/utils/upload_flakiness_dashboard.sh index d4e02894c5..cb610f9cdf 100755 --- a/utils/upload_flakiness_dashboard.sh +++ b/utils/upload_flakiness_dashboard.sh @@ -84,6 +84,8 @@ gzip "${REPORT_NAME}" AZ_STORAGE_ACCOUNT="folioflakinessdashboard" +echo "Uploading ${REPORT_NAME}.gz" + az storage blob upload --auth-mode login --account-name "${AZ_STORAGE_ACCOUNT}" -c uploads -f "${REPORT_NAME}.gz" -n "${REPORT_NAME}.gz" UTC_DATE=$(cat <