diff --git a/docs/src/test-api/class-test.md b/docs/src/test-api/class-test.md index 9b56fbf2e6..b6ae7d1522 100644 --- a/docs/src/test-api/class-test.md +++ b/docs/src/test-api/class-test.md @@ -1710,6 +1710,11 @@ Step body. Whether to box the step in the report. Defaults to `false`. When the step is boxed, errors thrown from the step internals point to the step call site. See below for more details. +### option: Test.step.location +* since: v1.48 +- `location` <[Location]> +Specifies a custom location for the step to be shown in test reports. By default, location of the [`method: Test.step`] call is shown. + ## method: Test.use * since: v1.10 diff --git a/package-lock.json b/package-lock.json index b7cdad125e..efe6fafd8b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,7 +61,7 @@ "react-dom": "^18.1.0", "ssim.js": "^3.5.0", "typescript": "^5.5.3", - "vite": "^5.0.13", + "vite": "^5.4.6", "ws": "^8.17.1", "xml2js": "^0.5.0", "yaml": "^2.2.2" @@ -852,9 +852,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", "cpu": [ "ppc64" ], @@ -1517,9 +1517,9 @@ "link": true }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.0.tgz", - "integrity": "sha512-jwXtxYbRt1V+CdQSy6Z+uZti7JF5irRKF8hlKfEnF/xJpcNGuuiZMBvuoYM+x9sr9iWGnzrlM0+9hvQ1kgkf1w==", + "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==", "cpu": [ "arm" ], @@ -1529,9 +1529,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.0.tgz", - "integrity": "sha512-fI9nduZhCccjzlsA/OuAwtFGWocxA4gqXGTLvOyiF8d+8o0fZUeSztixkYjcGq1fGZY3Tkq4yRvHPFxU+jdZ9Q==", + "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==", "cpu": [ "arm64" ], @@ -1541,9 +1541,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.0.tgz", - "integrity": "sha512-BcnSPRM76/cD2gQC+rQNGBN6GStBs2pl/FpweW8JYuz5J/IEa0Fr4AtrPv766DB/6b2MZ/AfSIOSGw3nEIP8SA==", + "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==", "cpu": [ "arm64" ], @@ -1553,9 +1553,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.0.tgz", - "integrity": "sha512-LDyFB9GRolGN7XI6955aFeI3wCdCUszFWumWU0deHA8VpR3nWRrjG6GtGjBrQxQKFevnUTHKCfPR4IvrW3kCgQ==", + "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==", "cpu": [ "x64" ], @@ -1565,9 +1565,21 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.0.tgz", - "integrity": "sha512-ygrGVhQP47mRh0AAD0zl6QqCbNsf0eTo+vgwkY6LunBcg0f2Jv365GXlDUECIyoXp1kKwL5WW6rsO429DBY/bA==", + "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==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "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==", "cpu": [ "arm" ], @@ -1577,9 +1589,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.0.tgz", - "integrity": "sha512-x+uJ6MAYRlHGe9wi4HQjxpaKHPM3d3JjqqCkeC5gpnnI6OWovLdXTpfa8trjxPLnWKyBsSi5kne+146GAxFt4A==", + "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==", "cpu": [ "arm64" ], @@ -1589,9 +1601,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.0.tgz", - "integrity": "sha512-nrRw8ZTQKg6+Lttwqo6a2VxR9tOroa2m91XbdQ2sUUzHoedXlsyvY1fN4xWdqz8PKmf4orDwejxXHjh7YBGUCA==", + "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==", "cpu": [ "arm64" ], @@ -1601,11 +1613,11 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.0.tgz", - "integrity": "sha512-xV0d5jDb4aFu84XKr+lcUJ9y3qpIWhttO3Qev97z8DKLXR62LC3cXT/bMZXrjLF9X+P5oSmJTzAhqwUbY96PnA==", + "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==", "cpu": [ - "ppc64le" + "ppc64" ], "optional": true, "os": [ @@ -1613,9 +1625,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.0.tgz", - "integrity": "sha512-SDDhBQwZX6LPRoPYjAZWyL27LbcBo7WdBFWJi5PI9RPCzU8ijzkQn7tt8NXiXRiFMJCVpkuMkBf4OxSxVMizAw==", + "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==", "cpu": [ "riscv64" ], @@ -1625,9 +1637,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.0.tgz", - "integrity": "sha512-RxB/qez8zIDshNJDufYlTT0ZTVut5eCpAZ3bdXDU9yTxBzui3KhbGjROK2OYTTor7alM7XBhssgoO3CZ0XD3qA==", + "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==", "cpu": [ "s390x" ], @@ -1637,9 +1649,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.0.tgz", - "integrity": "sha512-C6y6z2eCNCfhZxT9u+jAM2Fup89ZjiG5pIzZIDycs1IwESviLxwkQcFRGLjnDrP+PT+v5i4YFvlcfAs+LnreXg==", + "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==", "cpu": [ "x64" ], @@ -1649,9 +1661,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.0.tgz", - "integrity": "sha512-i0QwbHYfnOMYsBEyjxcwGu5SMIi9sImDVjDg087hpzXqhBSosxkE7gyIYFHgfFl4mr7RrXksIBZ4DoLoP4FhJg==", + "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==", "cpu": [ "x64" ], @@ -1661,9 +1673,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.0.tgz", - "integrity": "sha512-Fq52EYb0riNHLBTAcL0cun+rRwyZ10S9vKzhGKKgeD+XbwunszSY0rVMco5KbOsTlwovP2rTOkiII/fQ4ih/zQ==", + "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==", "cpu": [ "arm64" ], @@ -1673,9 +1685,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.0.tgz", - "integrity": "sha512-e/PBHxPdJ00O9p5Ui43+vixSgVf4NlLsmV6QneGERJ3lnjIua/kim6PRFe3iDueT1rQcgSkYP8ZBBXa/h4iPvw==", + "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==", "cpu": [ "ia32" ], @@ -1685,9 +1697,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.0.tgz", - "integrity": "sha512-aGg7iToJjdklmxlUlJh/PaPNa4PmqHfyRMLunbL3eaMO0gp656+q1zOKkpJ/CVe9CryJv6tAN1HDoR8cNGzkag==", + "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==", "cpu": [ "x64" ], @@ -5862,9 +5874,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -5917,9 +5929,9 @@ } }, "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "funding": [ { "type": "opencollective", @@ -5936,8 +5948,8 @@ ], "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -6301,9 +6313,9 @@ } }, "node_modules/rollup": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.0.tgz", - "integrity": "sha512-Qe7w62TyawbDzB4yt32R0+AbIo6m1/sqO7UPzFS8Z/ksL5mrfhA0v4CavfdmFav3D+ub4QeAgsGEe84DoWe/nQ==", + "version": "4.21.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.3.tgz", + "integrity": "sha512-7sqRtBNnEbcBtMeRVc6VRsJMmpI+JU1z9VTvW8D4gXIYQFz0aLcsE6rRkyghZkLfEgUZgVvOG7A5CVz/VW5GIA==", "dependencies": { "@types/estree": "1.0.5" }, @@ -6315,21 +6327,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.14.0", - "@rollup/rollup-android-arm64": "4.14.0", - "@rollup/rollup-darwin-arm64": "4.14.0", - "@rollup/rollup-darwin-x64": "4.14.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.14.0", - "@rollup/rollup-linux-arm64-gnu": "4.14.0", - "@rollup/rollup-linux-arm64-musl": "4.14.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.14.0", - "@rollup/rollup-linux-riscv64-gnu": "4.14.0", - "@rollup/rollup-linux-s390x-gnu": "4.14.0", - "@rollup/rollup-linux-x64-gnu": "4.14.0", - "@rollup/rollup-linux-x64-musl": "4.14.0", - "@rollup/rollup-win32-arm64-msvc": "4.14.0", - "@rollup/rollup-win32-ia32-msvc": "4.14.0", - "@rollup/rollup-win32-x64-msvc": "4.14.0", + "@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", "fsevents": "~2.3.2" } }, @@ -6589,9 +6602,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "engines": { "node": ">=0.10.0" } @@ -7171,13 +7184,13 @@ } }, "node_modules/vite": { - "version": "5.2.8", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.8.tgz", - "integrity": "sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==", + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz", + "integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==", "dependencies": { - "esbuild": "^0.20.1", - "postcss": "^8.4.38", - "rollup": "^4.13.0" + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" @@ -7196,6 +7209,7 @@ "less": "*", "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" @@ -7213,6 +7227,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, @@ -7243,9 +7260,9 @@ } }, "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "cpu": [ "arm" ], @@ -7258,9 +7275,9 @@ } }, "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "cpu": [ "arm64" ], @@ -7273,9 +7290,9 @@ } }, "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "cpu": [ "x64" ], @@ -7288,9 +7305,9 @@ } }, "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "cpu": [ "arm64" ], @@ -7303,9 +7320,9 @@ } }, "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "cpu": [ "x64" ], @@ -7318,9 +7335,9 @@ } }, "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "cpu": [ "arm64" ], @@ -7333,9 +7350,9 @@ } }, "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "cpu": [ "x64" ], @@ -7348,9 +7365,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "cpu": [ "arm" ], @@ -7363,9 +7380,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "cpu": [ "arm64" ], @@ -7378,9 +7395,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "cpu": [ "ia32" ], @@ -7393,9 +7410,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "cpu": [ "loong64" ], @@ -7408,9 +7425,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "cpu": [ "mips64el" ], @@ -7423,9 +7440,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "cpu": [ "ppc64" ], @@ -7438,9 +7455,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "cpu": [ "riscv64" ], @@ -7453,9 +7470,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "cpu": [ "s390x" ], @@ -7468,9 +7485,9 @@ } }, "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "cpu": [ "x64" ], @@ -7483,9 +7500,9 @@ } }, "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", "cpu": [ "x64" ], @@ -7498,9 +7515,9 @@ } }, "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "cpu": [ "x64" ], @@ -7513,9 +7530,9 @@ } }, "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", "cpu": [ "x64" ], @@ -7528,9 +7545,9 @@ } }, "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", "cpu": [ "arm64" ], @@ -7543,9 +7560,9 @@ } }, "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", "cpu": [ "ia32" ], @@ -7558,9 +7575,9 @@ } }, "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "cpu": [ "x64" ], @@ -7573,9 +7590,9 @@ } }, "node_modules/vite/node_modules/esbuild": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", - "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" @@ -7584,29 +7601,29 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.2", - "@esbuild/android-arm": "0.20.2", - "@esbuild/android-arm64": "0.20.2", - "@esbuild/android-x64": "0.20.2", - "@esbuild/darwin-arm64": "0.20.2", - "@esbuild/darwin-x64": "0.20.2", - "@esbuild/freebsd-arm64": "0.20.2", - "@esbuild/freebsd-x64": "0.20.2", - "@esbuild/linux-arm": "0.20.2", - "@esbuild/linux-arm64": "0.20.2", - "@esbuild/linux-ia32": "0.20.2", - "@esbuild/linux-loong64": "0.20.2", - "@esbuild/linux-mips64el": "0.20.2", - "@esbuild/linux-ppc64": "0.20.2", - "@esbuild/linux-riscv64": "0.20.2", - "@esbuild/linux-s390x": "0.20.2", - "@esbuild/linux-x64": "0.20.2", - "@esbuild/netbsd-x64": "0.20.2", - "@esbuild/openbsd-x64": "0.20.2", - "@esbuild/sunos-x64": "0.20.2", - "@esbuild/win32-arm64": "0.20.2", - "@esbuild/win32-ia32": "0.20.2", - "@esbuild/win32-x64": "0.20.2" + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" } }, "node_modules/vitefu": { diff --git a/package.json b/package.json index c095546c7d..511b17aa9e 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ "react-dom": "^18.1.0", "ssim.js": "^3.5.0", "typescript": "^5.5.3", - "vite": "^5.0.13", + "vite": "^5.4.6", "ws": "^8.17.1", "xml2js": "^0.5.0", "yaml": "^2.2.2" diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index dc91235659..aa62401421 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -9,9 +9,9 @@ }, { "name": "chromium-tip-of-tree", - "revision": "1259", + "revision": "1260", "installByDefault": false, - "browserVersion": "130.0.6713.0" + "browserVersion": "130.0.6723.0" }, { "name": "firefox", diff --git a/packages/playwright-core/src/cli/program.ts b/packages/playwright-core/src/cli/program.ts index d8fa8230c6..fb27b14231 100644 --- a/packages/playwright-core/src/cli/program.ts +++ b/packages/playwright-core/src/cli/program.ts @@ -567,10 +567,6 @@ async function codegen(options: Options & { target: string, output?: string, tes tracesDir, }); dotenv.config({ path: 'playwright.env' }); - if (process.env.PW_RECORDER_IS_TRACE_VIEWER) { - await fs.promises.mkdir(tracesDir, { recursive: true }); - await context.tracing.start({ name: 'trace', _live: true }); - } await context._enableRecorder({ language, launchOptions, diff --git a/packages/playwright-core/src/server/bidi/bidiConnection.ts b/packages/playwright-core/src/server/bidi/bidiConnection.ts index 7138f2e06a..f348815940 100644 --- a/packages/playwright-core/src/server/bidi/bidiConnection.ts +++ b/packages/playwright-core/src/server/bidi/bidiConnection.ts @@ -72,7 +72,7 @@ export class BidiConnection { let context; if ('context' in object.params) context = object.params.context; - else if (object.method === 'log.entryAdded') + else if (object.method === 'log.entryAdded' || object.method === 'script.message') context = object.params.source?.context; if (context) { const session = this._browsingContextToSession.get(context); diff --git a/packages/playwright-core/src/server/bidi/bidiExecutionContext.ts b/packages/playwright-core/src/server/bidi/bidiExecutionContext.ts index eaacb629e6..c037ba44b4 100644 --- a/packages/playwright-core/src/server/bidi/bidiExecutionContext.ts +++ b/packages/playwright-core/src/server/bidi/bidiExecutionContext.ts @@ -23,7 +23,7 @@ import { BidiSerializer } from './third_party/bidiSerializer'; export class BidiExecutionContext implements js.ExecutionContextDelegate { private readonly _session: BidiSession; - private readonly _target: bidi.Script.Target; + readonly _target: bidi.Script.Target; constructor(session: BidiSession, realmInfo: bidi.Script.RealmInfo) { this._session = session; diff --git a/packages/playwright-core/src/server/bidi/bidiPage.ts b/packages/playwright-core/src/server/bidi/bidiPage.ts index c2d499bd67..180e8a651e 100644 --- a/packages/playwright-core/src/server/bidi/bidiPage.ts +++ b/packages/playwright-core/src/server/bidi/bidiPage.ts @@ -21,7 +21,8 @@ import type * as accessibility from '../accessibility'; import * as dom from '../dom'; import * as dialog from '../dialog'; import type * as frames from '../frames'; -import { type InitScript, Page, type PageDelegate } from '../page'; +import { Page } from '../page'; +import type { InitScript, PageDelegate } from '../page'; import type { Progress } from '../progress'; import type * as types from '../types'; import type { BidiBrowserContext } from './bidiBrowser'; @@ -33,6 +34,7 @@ import { BidiNetworkManager } from './bidiNetworkManager'; import { BrowserContext } from '../browserContext'; const UTILITY_WORLD_NAME = '__playwright_utility_world__'; +const kPlaywrightBindingChannel = 'playwrightChannel'; export class BidiPage implements PageDelegate { readonly rawMouse: RawMouseImpl; @@ -62,6 +64,7 @@ export class BidiPage implements PageDelegate { this._page.on(Page.Events.FrameDetached, (frame: frames.Frame) => this._removeContextsForFrame(frame, false)); this._sessionListeners = [ eventsHelper.addEventListener(bidiSession, 'script.realmCreated', this._onRealmCreated.bind(this)), + eventsHelper.addEventListener(bidiSession, 'script.message', this._onScriptMessage.bind(this)), eventsHelper.addEventListener(bidiSession, 'browsingContext.contextDestroyed', this._onBrowsingContextDestroyed.bind(this)), eventsHelper.addEventListener(bidiSession, 'browsingContext.navigationStarted', this._onNavigationStarted.bind(this)), eventsHelper.addEventListener(bidiSession, 'browsingContext.navigationAborted', this._onNavigationAborted.bind(this)), @@ -93,6 +96,7 @@ export class BidiPage implements PageDelegate { this.updateHttpCredentials(), this.updateRequestInterception(), this._updateViewport(), + this._installMainBinding(), this._addAllInitScripts(), ]); } @@ -315,18 +319,63 @@ export class BidiPage implements PageDelegate { }); } - goBack(): Promise { - throw new Error('Method not implemented.'); + async goBack(): Promise { + return await this._session.send('browsingContext.traverseHistory', { + context: this._session.sessionId, + delta: -1, + }).then(() => true).catch(() => false); } - goForward(): Promise { - throw new Error('Method not implemented.'); + async goForward(): Promise { + return await this._session.send('browsingContext.traverseHistory', { + context: this._session.sessionId, + delta: +1, + }).then(() => true).catch(() => false); } async forceGarbageCollection(): Promise { throw new Error('Method not implemented.'); } + // TODO: consider calling this only when bindings are added. + private async _installMainBinding() { + const functionDeclaration = addMainBinding.toString(); + const args: bidi.Script.ChannelValue[] = [{ + type: 'channel', + value: { + channel: kPlaywrightBindingChannel, + ownership: bidi.Script.ResultOwnership.Root, + } + }]; + const promises = []; + promises.push(this._session.send('script.addPreloadScript', { + functionDeclaration, + arguments: args, + })); + promises.push(this._session.send('script.callFunction', { + functionDeclaration, + arguments: args, + target: toBidiExecutionContext(await this._page.mainFrame()._mainContext())._target, + awaitPromise: false, + userActivation: false, + })); + await Promise.all(promises); + } + + private async _onScriptMessage(event: bidi.Script.MessageParameters) { + if (event.channel !== kPlaywrightBindingChannel) + return; + const pageOrError = await this.pageOrError(); + if (pageOrError instanceof Error) + return; + const context = this._realmToContext.get(event.source.realm); + if (!context) + return; + if (event.data.type !== 'string') + return; + await this._page._onBindingCalled(event.data.value, context); + } + async addInitScript(initScript: InitScript): Promise { const { script } = await this._session.send('script.addPreloadScript', { // TODO: remove function call from the source. @@ -355,7 +404,20 @@ export class BidiPage implements PageDelegate { } async takeScreenshot(progress: Progress, format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined, fitsViewport: boolean, scale: 'css' | 'device'): Promise { - throw new Error('Method not implemented.'); + const rect = (documentRect || viewportRect)!; + const { data } = await this._session.send('browsingContext.captureScreenshot', { + context: this._session.sessionId, + format: { + type: `image/${format === 'png' ? 'png' : 'jpeg'}`, + quality: quality || 80, + }, + origin: documentRect ? 'document' : 'viewport', + clip: { + type: 'box', + ...rect, + } + }); + return Buffer.from(data, 'base64'); } async getContentFrame(handle: dom.ElementHandle): Promise { @@ -522,6 +584,10 @@ export class BidiPage implements PageDelegate { } } +function addMainBinding(callback: (arg: any) => void) { + (globalThis as any)['__playwright__binding__'] = callback; +} + function toBidiExecutionContext(executionContext: dom.FrameExecutionContext): BidiExecutionContext { return (executionContext as any)[contextDelegateSymbol] as BidiExecutionContext; } diff --git a/packages/playwright-core/src/server/codegen/language.ts b/packages/playwright-core/src/server/codegen/language.ts index 4b1ba99b6f..7ee775b18b 100644 --- a/packages/playwright-core/src/server/codegen/language.ts +++ b/packages/playwright-core/src/server/codegen/language.ts @@ -20,7 +20,6 @@ import type * as types from '../types'; import type { ActionInContext, LanguageGenerator, LanguageGeneratorOptions } from './types'; export function generateCode(actions: ActionInContext[], languageGenerator: LanguageGenerator, options: LanguageGeneratorOptions) { - actions = collapseActions(actions); const header = languageGenerator.generateHeader(options); const footer = languageGenerator.generateFooter(options.saveStorage); const actionTexts = actions.map(a => languageGenerator.generateAction(a)).filter(Boolean); @@ -70,6 +69,23 @@ export function toKeyboardModifiers(modifiers: number): types.SmartKeyboardModif return result; } +export function fromKeyboardModifiers(modifiers?: types.SmartKeyboardModifier[]): number { + let result = 0; + if (!modifiers) + return result; + if (modifiers.includes('Alt')) + result |= 1; + if (modifiers.includes('Control')) + result |= 2; + if (modifiers.includes('ControlOrMeta')) + result |= 2; + if (modifiers.includes('Meta')) + result |= 4; + if (modifiers.includes('Shift')) + result |= 8; + return result; +} + export function toClickOptionsForSourceCode(action: actions.ClickAction): types.MouseClickOptions { const modifiers = toKeyboardModifiers(action.modifiers); const options: types.MouseClickOptions = {}; @@ -84,19 +100,3 @@ export function toClickOptionsForSourceCode(action: actions.ClickAction): types. options.position = action.position; return options; } - -function collapseActions(actions: ActionInContext[]): ActionInContext[] { - const result: ActionInContext[] = []; - for (const action of actions) { - const lastAction = result[result.length - 1]; - const isSameAction = lastAction && lastAction.action.name === action.action.name && lastAction.frame.pageAlias === action.frame.pageAlias && lastAction.frame.framePath.join('|') === action.frame.framePath.join('|'); - const isSameSelector = lastAction && 'selector' in lastAction.action && 'selector' in action.action && action.action.selector === lastAction.action.selector; - const shouldMerge = isSameAction && (action.action.name === 'navigate' || (action.action.name === 'fill' && isSameSelector)); - if (!shouldMerge) { - result.push(action); - continue; - } - result[result.length - 1] = action; - } - return result; -} diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts index 5c8fa550a7..c2d5d8e1f6 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -41,6 +41,7 @@ import { serializeError } from '../errors'; import { ElementHandleDispatcher } from './elementHandlerDispatcher'; import { RecorderInTraceViewer } from '../recorder/recorderInTraceViewer'; import { RecorderApp } from '../recorder/recorderApp'; +import type { IRecorderAppFactory } from '../recorder/recorderFrontend'; export class BrowserContextDispatcher extends Dispatcher implements channels.BrowserContextChannel { _type_EventTarget = true; @@ -293,7 +294,20 @@ export class BrowserContextDispatcher extends Dispatcher { - const factory = process.env.PW_RECORDER_IS_TRACE_VIEWER ? RecorderInTraceViewer.factory(this._context) : RecorderApp.factory(this._context); + let factory: IRecorderAppFactory; + if (process.env.PW_RECORDER_IS_TRACE_VIEWER) { + factory = RecorderInTraceViewer.factory(this._context); + await this._context.tracing.start({ + name: 'trace', + snapshots: true, + screenshots: false, + live: true, + inMemory: true, + }); + await this._context.tracing.startChunk({ name: 'trace', title: 'trace' }); + } else { + factory = RecorderApp.factory(this._context); + } await Recorder.show(this._context, factory, params); } diff --git a/packages/playwright-core/src/server/fetch.ts b/packages/playwright-core/src/server/fetch.ts index 39df932a50..c1e394e249 100644 --- a/packages/playwright-core/src/server/fetch.ts +++ b/packages/playwright-core/src/server/fetch.ts @@ -41,6 +41,7 @@ import type * as types from './types'; import type { HeadersArray, ProxySettings } from './types'; import { getMatchingTLSOptionsForOrigin, rewriteOpenSSLErrorIfNeeded } from './socksClientCertificatesInterceptor'; import type * as har from '@trace/har'; +import { TLSSocket } from 'tls'; type FetchRequestOptions = { userAgent: string; @@ -73,6 +74,9 @@ export type APIRequestFinishedEvent = { statusMessage: string; body?: Buffer; timings: har.Timings; + serverIPAddress?: string; + serverPort?: number; + securityDetails?: har.SecurityDetails; }; type SendRequestOptions = https.RequestOptions & { @@ -302,6 +306,10 @@ export abstract class APIRequestContext extends SdkObject { let tcpConnectionAt: number | undefined; let tlsHandshakeAt: number | undefined; let requestFinishAt: number | undefined; + let serverIPAddress: string | undefined; + let serverPort: number | undefined; + + let securityDetails: har.SecurityDetails | undefined; const request = requestConstructor(url, requestOptions as any, async response => { const responseAt = monotonicTime(); @@ -328,6 +336,9 @@ export abstract class APIRequestContext extends SdkObject { cookies, body, timings, + serverIPAddress, + serverPort, + securityDetails, }; this.emit(APIRequestContext.Events.RequestFinished, requestFinishedEvent); }; @@ -503,10 +514,26 @@ export abstract class APIRequestContext extends SdkObject { // non-happy-eyeballs sockets socket.on('lookup', () => { dnsLookupAt = monotonicTime(); }); socket.on('connect', () => { tcpConnectionAt = monotonicTime(); }); - socket.on('secureConnect', () => { tlsHandshakeAt = monotonicTime(); }); + socket.on('secureConnect', () => { + tlsHandshakeAt = monotonicTime(); + + if (socket instanceof TLSSocket) { + const peerCertificate = socket.getPeerCertificate(); + securityDetails = { + protocol: socket.getProtocol() ?? undefined, + subjectName: peerCertificate.subject.CN, + validFrom: new Date(peerCertificate.valid_from).getTime() / 1000, + validTo: new Date(peerCertificate.valid_to).getTime() / 1000, + issuer: peerCertificate.issuer.CN + }; + } + }); // socks / http proxy socket.on('proxyConnect', () => { tcpConnectionAt = monotonicTime(); }); + + serverIPAddress = socket.remoteAddress; + serverPort = socket.remotePort; }); request.on('finish', () => { requestFinishAt = monotonicTime(); }); diff --git a/packages/playwright-core/src/server/har/harTracer.ts b/packages/playwright-core/src/server/har/harTracer.ts index 76da6682d4..b4b90d7976 100644 --- a/packages/playwright-core/src/server/har/harTracer.ts +++ b/packages/playwright-core/src/server/har/harTracer.ts @@ -213,11 +213,19 @@ export class HarTracer { harEntry.response.httpVersion = event.httpVersion; harEntry.response.redirectURL = event.headers.location || ''; + if (!this._options.omitServerIP) { + harEntry.serverIPAddress = event.serverIPAddress; + harEntry._serverPort = event.serverPort; + } + if (!this._options.omitTiming) { harEntry.timings = event.timings; this._computeHarEntryTotalTime(harEntry); } + if (!this._options.omitSecurityDetails) + harEntry._securityDetails = event.securityDetails; + for (let i = 0; i < event.rawHeaders.length; i += 2) { harEntry.response.headers.push({ name: event.rawHeaders[i], @@ -236,6 +244,8 @@ export class HarTracer { if (contentType) content.mimeType = contentType; this._storeResponseContent(event.body, content, 'other'); + if (!this._options.omitSizes) + harEntry.response.bodySize = event.body?.length ?? 0; if (this._started) this._delegate.onEntryFinished(harEntry); diff --git a/packages/playwright-core/src/server/injected/recorder/recorder.ts b/packages/playwright-core/src/server/injected/recorder/recorder.ts index dfbd4608f0..76d5791b64 100644 --- a/packages/playwright-core/src/server/injected/recorder/recorder.ts +++ b/packages/playwright-core/src/server/injected/recorder/recorder.ts @@ -1039,9 +1039,12 @@ export class Recorder { this.highlight.install(); // some frameworks erase the DOM on hydration, this ensures it's reattached - const recreationInterval = setInterval(() => { + let recreationInterval: number | undefined; + const recreate = () => { this.highlight.install(); - }, 500); + recreationInterval = this.injectedScript.builtinSetTimeout(recreate, 500); + }; + recreationInterval = this.injectedScript.builtinSetTimeout(recreate, 500); this._listeners.push(() => clearInterval(recreationInterval)); this.overlay?.install(); diff --git a/packages/playwright-core/src/server/recorder/contextRecorder.ts b/packages/playwright-core/src/server/recorder/contextRecorder.ts index 71a1d3ec75..13f3ae1a62 100644 --- a/packages/playwright-core/src/server/recorder/contextRecorder.ts +++ b/packages/playwright-core/src/server/recorder/contextRecorder.ts @@ -73,7 +73,7 @@ export class ContextRecorder extends EventEmitter { saveStorage: params.saveStorage, }; - const collection = new RecorderCollection(this._pageAliases, params.mode === 'recording'); + const collection = new RecorderCollection(context, this._pageAliases, params.mode === 'recording'); collection.on('change', () => { this._recorderSources = []; for (const languageGenerator of this._orderedLanguages) { diff --git a/packages/playwright-core/src/server/recorder/recorderApp.ts b/packages/playwright-core/src/server/recorder/recorderApp.ts index 67d8b6e8dc..5e170b2e3e 100644 --- a/packages/playwright-core/src/server/recorder/recorderApp.ts +++ b/packages/playwright-core/src/server/recorder/recorderApp.ts @@ -43,6 +43,7 @@ declare global { } export class EmptyRecorderApp extends EventEmitter implements IRecorderApp { + wsEndpointForTest: undefined; async close(): Promise {} async setPaused(paused: boolean): Promise {} async setMode(mode: Mode): Promise {} @@ -54,7 +55,7 @@ export class EmptyRecorderApp extends EventEmitter implements IRecorderApp { export class RecorderApp extends EventEmitter implements IRecorderApp { private _page: Page; - readonly wsEndpoint: string | undefined; + readonly wsEndpointForTest: string | undefined; private _recorder: IRecorder; constructor(recorder: IRecorder, page: Page, wsEndpoint: string | undefined) { @@ -62,7 +63,7 @@ export class RecorderApp extends EventEmitter implements IRecorderApp { this.setMaxListeners(0); this._recorder = recorder; this._page = page; - this.wsEndpoint = wsEndpoint; + this.wsEndpointForTest = wsEndpoint; } async close() { diff --git a/packages/playwright-core/src/server/recorder/recorderCollection.ts b/packages/playwright-core/src/server/recorder/recorderCollection.ts index e9c2b31427..e67865a2dc 100644 --- a/packages/playwright-core/src/server/recorder/recorderCollection.ts +++ b/packages/playwright-core/src/server/recorder/recorderCollection.ts @@ -20,31 +20,35 @@ import type { Page } from '../page'; import type { Signal } from './recorderActions'; import type { ActionInContext } from '../codegen/types'; import { monotonicTime } from '../../utils/time'; -import { callMetadataForAction } from './recorderUtils'; +import { callMetadataForAction, collapseActions, traceEventsToAction } from './recorderUtils'; import { serializeError } from '../errors'; import { performAction } from './recorderRunner'; import type { CallMetadata } from '@protocol/callMetadata'; import { isUnderTest } from '../../utils/debug'; +import type { BrowserContext } from '../browserContext'; export class RecorderCollection extends EventEmitter { private _actions: ActionInContext[] = []; private _enabled: boolean; private _pageAliases: Map; + private _context: BrowserContext; - constructor(pageAliases: Map, enabled: boolean) { + constructor(context: BrowserContext, pageAliases: Map, enabled: boolean) { super(); + this._context = context; this._enabled = enabled; this._pageAliases = pageAliases; - this.restart(); } restart() { this._actions = []; - this.emit('change'); + this._fireChange(); } actions() { - return this._actions; + if (!process.env.PW_RECORDER_IS_TRACE_VIEWER) + return collapseActions(this._actions); + return collapseActions(traceEventsToAction(this._context.tracing.inMemoryEvents())); } setEnabled(enabled: boolean) { @@ -60,7 +64,7 @@ export class RecorderCollection extends EventEmitter { addRecordedAction(actionInContext: ActionInContext) { if (['openPage', 'closePage'].includes(actionInContext.action.name)) { this._actions.push(actionInContext); - this.emit('change'); + this._fireChange(); return; } this._addAction(actionInContext).catch(() => {}); @@ -69,11 +73,16 @@ export class RecorderCollection extends EventEmitter { private async _addAction(actionInContext: ActionInContext, callback?: (callMetadata: CallMetadata) => Promise) { if (!this._enabled) return; + if (actionInContext.action.name === 'openPage' || actionInContext.action.name === 'closePage') { + this._actions.push(actionInContext); + this._fireChange(); + return; + } const { callMetadata, mainFrame } = callMetadataForAction(this._pageAliases, actionInContext); await mainFrame.instrumentation.onBeforeCall(mainFrame, callMetadata); this._actions.push(actionInContext); - this.emit('change'); + this._fireChange(); const error = await callback?.(callMetadata).catch((e: Error) => e); callMetadata.endTime = monotonicTime(); callMetadata.error = error ? serializeError(error) : undefined; @@ -120,4 +129,8 @@ export class RecorderCollection extends EventEmitter { return; } } + + private _fireChange() { + this.emit('change'); + } } diff --git a/packages/playwright-core/src/server/recorder/recorderFrontend.ts b/packages/playwright-core/src/server/recorder/recorderFrontend.ts index 162c9f9964..d2cdffdca4 100644 --- a/packages/playwright-core/src/server/recorder/recorderFrontend.ts +++ b/packages/playwright-core/src/server/recorder/recorderFrontend.ts @@ -23,6 +23,7 @@ export interface IRecorder { } export interface IRecorderApp extends EventEmitter { + readonly wsEndpointForTest: string | undefined; close(): Promise; setPaused(paused: boolean): Promise; setMode(mode: Mode): Promise; diff --git a/packages/playwright-core/src/server/recorder/recorderInTraceViewer.ts b/packages/playwright-core/src/server/recorder/recorderInTraceViewer.ts index a9fd766141..8f08b969e1 100644 --- a/packages/playwright-core/src/server/recorder/recorderInTraceViewer.ts +++ b/packages/playwright-core/src/server/recorder/recorderInTraceViewer.ts @@ -25,6 +25,7 @@ import { gracefullyProcessExitDoNotHang } from '../../utils/processLauncher'; import type { Transport } from '../../utils/httpServer'; export class RecorderInTraceViewer extends EventEmitter implements IRecorderApp { + readonly wsEndpointForTest: string | undefined; private _recorder: IRecorder; private _transport: Transport; @@ -32,15 +33,16 @@ export class RecorderInTraceViewer extends EventEmitter implements IRecorderApp return async (recorder: IRecorder) => { const transport = new RecorderTransport(); const trace = path.join(context._browser.options.tracesDir, 'trace'); - await openApp(trace, { transport }); - return new RecorderInTraceViewer(context, recorder, transport); + const wsEndpointForTest = await openApp(trace, { transport, headless: !context._browser.options.headful }); + return new RecorderInTraceViewer(context, recorder, transport, wsEndpointForTest); }; } - constructor(context: BrowserContext, recorder: IRecorder, transport: Transport) { + constructor(context: BrowserContext, recorder: IRecorder, transport: Transport, wsEndpointForTest: string | undefined) { super(); this._recorder = recorder; this._transport = transport; + this.wsEndpointForTest = wsEndpointForTest; } async close(): Promise { @@ -72,11 +74,12 @@ export class RecorderInTraceViewer extends EventEmitter implements IRecorderApp } } -async function openApp(trace: string, options?: TraceViewerServerOptions & { headless?: boolean }) { +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; } class RecorderTransport implements Transport { diff --git a/packages/playwright-core/src/server/recorder/recorderUtils.ts b/packages/playwright-core/src/server/recorder/recorderUtils.ts index ac6c970489..27c212c6e5 100644 --- a/packages/playwright-core/src/server/recorder/recorderUtils.ts +++ b/packages/playwright-core/src/server/recorder/recorderUtils.ts @@ -20,9 +20,12 @@ import type { Page } from '../page'; import type { ActionInContext } from '../codegen/types'; import type { Frame } from '../frames'; import type * as actions from './recorderActions'; -import { toKeyboardModifiers } from '../codegen/language'; +import type * as channels from '@protocol/channels'; +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'; export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus): CallLog { let title = metadata.apiName || metadata.method; @@ -76,57 +79,113 @@ export async function frameForAction(pageAliases: Map, actionInCon return result.frame; } -export function traceParamsForAction(actionInContext: ActionInContext) { +export function traceParamsForAction(actionInContext: ActionInContext): { method: string, params: any } { const { action } = actionInContext; switch (action.name) { - case 'navigate': return { url: action.url }; - case 'openPage': return {}; - case 'closePage': return {}; + case 'navigate': { + const params: channels.FrameGotoParams = { + url: action.url, + }; + return { method: 'goto', params }; + } + case 'openPage': + case 'closePage': + throw new Error('Not reached'); } const selector = buildFullSelector(actionInContext.frame.framePath, action.selector); switch (action.name) { - case 'click': return { selector, clickCount: action.clickCount }; - case 'press': { - const modifiers = toKeyboardModifiers(action.modifiers); - const shortcut = [...modifiers, action.key].join('+'); - return { selector, key: shortcut }; - } - case 'fill': return { selector, text: action.text }; - case 'setInputFiles': return { selector, files: action.files }; - case 'check': return { selector }; - case 'uncheck': return { selector }; - case 'select': return { selector, values: action.options.map(value => ({ value })) }; - case 'assertChecked': { - return { + case 'click': { + const params: channels.FrameClickParams = { selector, - expression: 'to.be.checked', - isNot: !action.checked, + strict: true, + modifiers: toKeyboardModifiers(action.modifiers), + button: action.button, + clickCount: action.clickCount, + position: action.position, }; + return { method: 'click', params }; + } + case 'press': { + const params: channels.FramePressParams = { + selector, + strict: true, + key: [...toKeyboardModifiers(action.modifiers), action.key].join('+'), + }; + return { method: 'press', params }; + } + case 'fill': { + const params: channels.FrameFillParams = { + selector, + strict: true, + value: action.text, + }; + return { method: 'fill', params }; + } + case 'setInputFiles': { + const params: channels.FrameSetInputFilesParams = { + selector, + strict: true, + localPaths: action.files, + }; + return { method: 'setInputFiles', params }; + } + case 'check': { + const params: channels.FrameCheckParams = { + selector, + strict: true, + }; + return { method: 'check', params }; + } + case 'uncheck': { + const params: channels.FrameUncheckParams = { + selector, + strict: true, + }; + return { method: 'uncheck', params }; + } + case 'select': { + const params: channels.FrameSelectOptionParams = { + selector, + strict: true, + options: action.options.map(option => ({ value: option })), + }; + return { method: 'selectOption', params }; + } + case 'assertChecked': { + const params: channels.FrameExpectParams = { + selector: action.selector, + expression: 'to.be.checked', + isNot: action.checked, + }; + return { method: 'expect', params }; } case 'assertText': { - return { + const params: channels.FrameExpectParams = { selector, expression: 'to.have.text', expectedText: serializeExpectedTextValues([action.text], { matchSubstring: true, normalizeWhiteSpace: true }), isNot: false, }; + return { method: 'expect', params }; } case 'assertValue': { - return { + const params: channels.FrameExpectParams = { selector, expression: 'to.have.value', - expectedValue: action.value, + expectedValue: { value: serializeValue(action.value, value => ({ fallThrough: value })), handles: [] }, isNot: false, }; + return { method: 'expect', params }; } case 'assertVisible': { - return { + const params: channels.FrameExpectParams = { selector, expression: 'to.be.visible', isNot: false, }; + return { method: 'expect', params }; } } } @@ -134,8 +193,10 @@ export function traceParamsForAction(actionInContext: ActionInContext) { 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, objectId: mainFrame.guid, pageId: mainFrame._page.guid, @@ -143,9 +204,132 @@ export function callMetadataForAction(pageAliases: Map, actionInCo startTime: monotonicTime(), endTime: 0, type: 'Frame', - method: action.name, - params: traceParamsForAction(actionInContext), + method, + params, log: [], }; return { callMetadata, mainFrame }; } + +export function traceEventsToAction(events: trace.TraceEvent[]): ActionInContext[] { + const result: ActionInContext[] = []; + for (const event of events) { + if (event.type !== 'before') + continue; + if (!event.stepId?.startsWith('recorder@')) + continue; + + if (event.method === 'goto') { + result.push({ + frame: { pageAlias: 'page', framePath: [] }, + action: { + name: 'navigate', + url: event.params.url, + signals: [], + }, + timestamp: event.startTime, + }); + continue; + } + if (event.method === 'click') { + result.push({ + frame: { pageAlias: 'page', framePath: [] }, + action: { + name: 'click', + selector: event.params.selector, + signals: [], + button: event.params.button, + modifiers: fromKeyboardModifiers(event.params.modifiers), + clickCount: event.params.clickCount, + position: event.params.position, + }, + timestamp: event.startTime + }); + continue; + } + if (event.method === 'fill') { + result.push({ + frame: { pageAlias: 'page', framePath: [] }, + action: { + name: 'fill', + selector: event.params.selector, + signals: [], + text: event.params.value, + }, + timestamp: event.startTime + }); + continue; + } + if (event.method === 'press') { + const tokens = event.params.key.split('+'); + const modifiers = tokens.slice(0, tokens.length - 1); + const key = tokens[tokens.length - 1]; + result.push({ + frame: { pageAlias: 'page', framePath: [] }, + action: { + name: 'press', + selector: event.params.selector, + signals: [], + key, + modifiers: fromKeyboardModifiers(modifiers), + }, + timestamp: event.startTime + }); + continue; + } + if (event.method === 'check') { + result.push({ + frame: { pageAlias: 'page', framePath: [] }, + action: { + name: 'check', + selector: event.params.selector, + signals: [], + }, + timestamp: event.startTime + }); + continue; + } + if (event.method === 'uncheck') { + result.push({ + frame: { pageAlias: 'page', framePath: [] }, + action: { + name: 'uncheck', + selector: event.params.selector, + signals: [], + }, + timestamp: event.startTime + }); + continue; + } + if (event.method === 'selectOption') { + result.push({ + frame: { pageAlias: 'page', framePath: [] }, + action: { + name: 'select', + selector: event.params.selector, + signals: [], + options: event.params.options.map((option: any) => option.value), + }, + timestamp: event.startTime + }); + continue; + } + } + return result; +} + +export function collapseActions(actions: ActionInContext[]): ActionInContext[] { + const result: ActionInContext[] = []; + for (const action of actions) { + const lastAction = result[result.length - 1]; + const isSameAction = lastAction && lastAction.action.name === action.action.name && lastAction.frame.pageAlias === action.frame.pageAlias && lastAction.frame.framePath.join('|') === action.frame.framePath.join('|'); + const isSameSelector = lastAction && 'selector' in lastAction.action && 'selector' in action.action && action.action.selector === lastAction.action.selector; + const shouldMerge = isSameAction && (action.action.name === 'navigate' || (action.action.name === 'fill' && isSameSelector)); + if (!shouldMerge) { + result.push(action); + continue; + } + result[result.length - 1] = action; + } + 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 b09bbe3134..25437e53a2 100644 --- a/packages/playwright-core/src/server/trace/recorder/tracing.ts +++ b/packages/playwright-core/src/server/trace/recorder/tracing.ts @@ -46,6 +46,7 @@ export type TracerOptions = { snapshots?: boolean; screenshots?: boolean; live?: boolean; + inMemory?: boolean; }; type RecordingState = { @@ -79,6 +80,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps private _allResources = new Set(); private _contextCreatedEvent: trace.ContextCreatedTraceEvent; private _pendingHarEntries = new Set(); + private _inMemoryEvents: trace.TraceEvent[] | undefined; constructor(context: BrowserContext | APIRequestContext, tracesDir: string | undefined) { super(context, 'tracing'); @@ -153,6 +155,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps // Tracing is 10x bigger if we include scripts in every trace. if (options.snapshots) this._harTracer.start({ omitScripts: !options.live }); + this._inMemoryEvents = options.inMemory ? [] : undefined; } async startChunk(options: { name?: string, title?: string } = {}): Promise<{ traceName: string }> { @@ -179,7 +182,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps wallTime: Date.now(), monotonicTime: monotonicTime() }; - this._fs.appendFile(this._state.traceFile, JSON.stringify(event) + '\n'); + this._appendTraceEvent(event); this._context.instrumentation.addListener(this, this._context); this._eventListeners.push( @@ -193,6 +196,10 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps return { traceName: this._state.traceName }; } + inMemoryEvents(): trace.TraceEvent[] { + return this._inMemoryEvents || []; + } + private _startScreencast() { if (!(this._context instanceof BrowserContext)) return; @@ -487,6 +494,8 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps // Do not flush (console) events, they are too noisy, unless we are in ui mode (live). const flush = this._state!.options.live || (event.type !== 'event' && event.type !== 'console' && event.type !== 'log'); this._fs.appendFile(this._state!.traceFile, JSON.stringify(visited) + '\n', flush); + if (this._inMemoryEvents) + this._inMemoryEvents.push(event); } private _appendResource(sha1: string, buffer: Buffer) { diff --git a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts index 6ca0319aa3..78848142a8 100644 --- a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts +++ b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts @@ -178,6 +178,7 @@ export async function openTraceViewerApp(url: string, browserName: string, optio ...options?.persistentContextOptions, useWebSocket: isUnderTest(), headless: !!options?.headless, + args: process.env.PWTEST_RECORDER_PORT ? [`--remote-debugging-port=${process.env.PWTEST_RECORDER_PORT}`] : [], }, }); diff --git a/packages/playwright/bundles/babel/package-lock.json b/packages/playwright/bundles/babel/package-lock.json index 0902f0601e..d3531ca0d7 100644 --- a/packages/playwright/bundles/babel/package-lock.json +++ b/packages/playwright/bundles/babel/package-lock.json @@ -11,7 +11,6 @@ "@babel/code-frame": "^7.24.2", "@babel/core": "^7.24.4", "@babel/helper-plugin-utils": "^7.24.0", - "@babel/parser": "^7.24.4", "@babel/plugin-proposal-decorators": "^7.24.1", "@babel/plugin-proposal-explicit-resource-management": "^7.24.1", "@babel/plugin-syntax-async-generators": "^7.8.4", diff --git a/packages/playwright/bundles/babel/package.json b/packages/playwright/bundles/babel/package.json index 27853cf80e..90a0aa85de 100644 --- a/packages/playwright/bundles/babel/package.json +++ b/packages/playwright/bundles/babel/package.json @@ -12,7 +12,6 @@ "@babel/code-frame": "^7.24.2", "@babel/core": "^7.24.4", "@babel/helper-plugin-utils": "^7.24.0", - "@babel/parser": "^7.24.4", "@babel/plugin-proposal-decorators": "^7.24.1", "@babel/plugin-proposal-explicit-resource-management": "^7.24.1", "@babel/plugin-syntax-async-generators": "^7.8.4", diff --git a/packages/playwright/bundles/babel/src/babelBundleImpl.ts b/packages/playwright/bundles/babel/src/babelBundleImpl.ts index f4e44fcea3..82610247f1 100644 --- a/packages/playwright/bundles/babel/src/babelBundleImpl.ts +++ b/packages/playwright/bundles/babel/src/babelBundleImpl.ts @@ -23,7 +23,6 @@ import * as babel from '@babel/core'; export { codeFrameColumns } from '@babel/code-frame'; export { declare } from '@babel/helper-plugin-utils'; export { types } from '@babel/core'; -export { parse } from '@babel/parser'; import traverseFunction from '@babel/traverse'; export const traverse = traverseFunction; diff --git a/packages/playwright/src/common/testType.ts b/packages/playwright/src/common/testType.ts index 5c7850a3df..f0882735dc 100644 --- a/packages/playwright/src/common/testType.ts +++ b/packages/playwright/src/common/testType.ts @@ -259,11 +259,11 @@ export class TestTypeImpl { suite._use.push({ fixtures, location }); } - async _step(title: string, body: () => Promise, options: { box?: boolean } = {}): Promise { + async _step(title: string, body: () => Promise, options: {box?: boolean, location?: Location } = {}): Promise { const testInfo = currentTestInfo(); if (!testInfo) throw new Error(`test.step() can only be called from a test`); - const step = testInfo._addStep({ category: 'test.step', title, box: options.box }); + const step = testInfo._addStep({ category: 'test.step', title, location: options.location, box: options.box }); return await zones.run('stepZone', step, async () => { try { const result = await body(); diff --git a/packages/playwright/src/reporters/html.ts b/packages/playwright/src/reporters/html.ts index fd1fb0cbdf..5aada7e495 100644 --- a/packages/playwright/src/reporters/html.ts +++ b/packages/playwright/src/reporters/html.ts @@ -143,7 +143,7 @@ class HtmlReporter implements ReporterV2 { const shouldOpen = !this._options._isTestServer && (this._open === 'always' || (!ok && this._open === 'on-failure')); if (shouldOpen) { await showHTMLReport(this._outputFolder, this._host, this._port, singleTestId); - } else if (this._options._mode === 'test') { + } else if (this._options._mode === 'test' && !this._options._isTestServer) { const packageManagerCommand = getPackageManagerExecCommand(); const relativeReportPath = this._outputFolder === standaloneDefaultFolder() ? '' : ' ' + path.relative(process.cwd(), this._outputFolder); const hostArg = this._host ? ` --host ${this._host}` : ''; diff --git a/packages/playwright/src/transform/babelBundle.ts b/packages/playwright/src/transform/babelBundle.ts index 2806a05aec..faf06b7158 100644 --- a/packages/playwright/src/transform/babelBundle.ts +++ b/packages/playwright/src/transform/babelBundle.ts @@ -18,7 +18,6 @@ import type { BabelFileResult } from '../../bundles/babel/node_modules/@types/ba export const codeFrameColumns: typeof import('../../bundles/babel/node_modules/@types/babel__code-frame').codeFrameColumns = require('./babelBundleImpl').codeFrameColumns; export const declare: typeof import('../../bundles/babel/node_modules/@types/babel__helper-plugin-utils').declare = require('./babelBundleImpl').declare; export const types: typeof import('../../bundles/babel/node_modules/@types/babel__core').types = require('./babelBundleImpl').types; -export const parse: typeof import('../../bundles/babel/node_modules/@babel/parser/typings/babel-parser').parse = require('./babelBundleImpl').parse; export const traverse: typeof import('../../bundles/babel/node_modules/@types/babel__traverse').default = require('./babelBundleImpl').traverse; export type BabelPlugin = [string, any?]; export type BabelTransformFunction = (code: string, filename: string, isTypeScript: boolean, isModule: boolean, pluginsPrefix: BabelPlugin[], pluginsSuffix: BabelPlugin[]) => BabelFileResult; diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index e17a43843c..51a6720a2e 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -4703,7 +4703,7 @@ export interface TestType(title: string, body: () => T | Promise, options?: { box?: boolean }): Promise; + step(title: string, body: () => T | Promise, options?: { box?: boolean, location?: Location }): Promise; /** * `expect` function can be used to create test assertions. Read more about [test assertions](https://playwright.dev/docs/test-assertions). * diff --git a/packages/trace-viewer/src/third_party/devtools.ts b/packages/trace-viewer/src/third_party/devtools.ts new file mode 100644 index 0000000000..27c520cbce --- /dev/null +++ b/packages/trace-viewer/src/third_party/devtools.ts @@ -0,0 +1,285 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Modifications copyright (c) Microsoft Corporation. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/* + * Copyright (C) 2007, 2008 Apple Inc. All rights reserved. + * Copyright (C) 2008, 2009 Anthony Ricaud + * Copyright (C) 2011 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of + * its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import type { Entry } from '@trace/har'; + +// The following function is derived from Chromium's source code +// https://github.com/ChromeDevTools/devtools-frontend/blob/83cbe41b4107e188a1f66fdf6ea3a9cca42587c6/front_end/panels/network/NetworkLogView.ts#L2363 +export async function generateCurlCommand(resource: Entry): Promise { + const platform = navigator.platform.includes('Win') ? 'win' : 'unix'; + let command: string[] = []; + // Most of these headers are derived from the URL and are automatically added by cURL. + // The |Accept-Encoding| header is ignored to prevent decompression errors. crbug.com/1015321 + const ignoredHeaders = + new Set(['accept-encoding', 'host', 'method', 'path', 'scheme', 'version', 'authority', 'protocol']); + + function escapeStringWin(str: string): string { + /* Always escape the " characters so that we can use caret escaping. + + Because cmd.exe parser and MS Crt arguments parsers use some of the + same escape characters, they can interact with each other in + horrible ways, the order of operations is critical. + + Replace \ with \\ first because it is an escape character for certain + conditions in both parsers. + + Replace all " with \" to ensure the first parser does not remove it. + + Then escape all characters we are not sure about with ^ to ensure it + gets to MS Crt parser safely. + + The % character is special because MS Crt parser will try and look for + ENV variables and fill them in its place. We cannot escape them with % + and cannot escape them with ^ (because it's cmd.exe's escape not MS Crt + parser); So we can get cmd.exe parser to escape the character after it, + if it is followed by a valid beginning character of an ENV variable. + This ensures we do not try and double escape another ^ if it was placed + by the previous replace. + + Lastly we replace new lines with ^ and TWO new lines because the first + new line is there to enact the escape command the second is the character + to escape (in this case new line). + */ + const encapsChars = '^"'; + return encapsChars + + str.replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/[^a-zA-Z0-9\s_\-:=+~'\/.',?;()*`]/g, '^$&') + .replace(/%(?=[a-zA-Z0-9_])/g, '%^') + .replace(/\r?\n/g, '^\n\n') + + encapsChars; + } + + function escapeStringPosix(str: string): string { + function escapeCharacter(x: string): string { + const code = x.charCodeAt(0); + let hexString = code.toString(16); + // Zero pad to four digits to comply with ANSI-C Quoting: + // http://www.gnu.org/software/bash/manual/html_node/ANSI_002dC-Quoting.html + while (hexString.length < 4) + hexString = '0' + hexString; + + + return '\\u' + hexString; + } + + if (/[\0-\x1F\x7F-\x9F!]|\'/.test(str)) { + // Use ANSI-C quoting syntax. + return '$\'' + + str.replace(/\\/g, '\\\\') + .replace(/\'/g, '\\\'') + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(/[\0-\x1F\x7F-\x9F!]/g, escapeCharacter) + + '\''; + } + // Use single quote syntax. + return '\'' + str + '\''; + } + + // cURL command expected to run on the same platform that DevTools run + // (it may be different from the inspected page platform). + const escapeString = platform === 'win' ? escapeStringWin : escapeStringPosix; + + command.push(escapeString(resource.request.url).replace(/[[{}\]]/g, '\\$&')); + + let inferredMethod = 'GET'; + const data = []; + const formData = await fetchRequestPostData(resource); + if (formData) { + // Note that formData is not necessarily urlencoded because it might for example + // come from a fetch request made with an explicitly unencoded body. + data.push('--data-raw ' + escapeString(formData)); + ignoredHeaders.add('content-length'); + inferredMethod = 'POST'; + } + + if (resource.request.method !== inferredMethod) + command.push('-X ' + escapeString(resource.request.method)); + + + const requestHeaders = resource.request.headers; + for (let i = 0; i < requestHeaders.length; i++) { + const header = requestHeaders[i]; + const name = header.name.replace(/^:/, ''); // Translate SPDY v3 headers to HTTP headers. + if (ignoredHeaders.has(name.toLowerCase())) + continue; + + if (header.value.trim()) { + command.push('-H ' + escapeString(name + ': ' + header.value)); + } else { + // A header passed with -H with no value or only whitespace as its + // value tells curl to not set the header at all. To post an empty + // header, you have to terminate it with a semicolon. + command.push('-H ' + escapeString(name + ';')); + } + } + command = command.concat(data); + + return 'curl ' + command.join(command.length >= 3 ? (platform === 'win' ? ' ^\n ' : ' \\\n ') : ' '); +} + +const enum FetchStyle { + BROWSER = 0, + NODE_JS = 1, +} + +export async function generateFetchCall(resource: Entry, style: FetchStyle = FetchStyle.BROWSER): Promise { + const ignoredHeaders = new Set([ + // Internal headers + 'method', + 'path', + 'scheme', + 'version', + + // Unsafe headers + // Keep this list synchronized with src/net/http/http_util.cc + 'accept-charset', + 'accept-encoding', + 'access-control-request-headers', + 'access-control-request-method', + 'connection', + 'content-length', + 'cookie', + 'cookie2', + 'date', + 'dnt', + 'expect', + 'host', + 'keep-alive', + 'origin', + 'referer', + 'te', + 'trailer', + 'transfer-encoding', + 'upgrade', + 'via', + // TODO(phistuck) - remove this once crbug.com/571722 is fixed. + 'user-agent', + ]); + + const credentialHeaders = new Set(['cookie', 'authorization']); + + const url = JSON.stringify(resource.request.url); + + const requestHeaders = resource.request.headers; + const headerData: Headers = requestHeaders.reduce((result, header) => { + const name = header.name; + + if (!ignoredHeaders.has(name.toLowerCase()) && !name.includes(':')) + result.append(name, header.value); + + + return result; + }, new Headers()); + + const headers: HeadersInit = {}; + for (const headerArray of headerData) + headers[headerArray[0]] = headerArray[1]; + + + const credentials = resource.request.cookies.length || + requestHeaders.some(({ name }) => credentialHeaders.has(name.toLowerCase())) ? + 'include' : + 'omit'; + + const referrerHeader = requestHeaders.find(({ name }) => name.toLowerCase() === 'referer'); + + const referrer = referrerHeader ? referrerHeader.value : void 0; + + const requestBody = await fetchRequestPostData(resource); + + const fetchOptions: RequestInit = { + headers: Object.keys(headers).length ? headers : void 0, + referrer, + body: requestBody, + method: resource.request.method, + mode: 'cors', + }; + + if (style === FetchStyle.NODE_JS) { + const cookieHeader = requestHeaders.find(header => header.name.toLowerCase() === 'cookie'); + const extraHeaders: HeadersInit = {}; + // According to https://www.npmjs.com/package/node-fetch#class-request the + // following properties are not implemented in Node.js. + delete fetchOptions.mode; + if (cookieHeader) + extraHeaders['cookie'] = cookieHeader.value; + + if (referrer) { + delete fetchOptions.referrer; + extraHeaders['Referer'] = referrer; + } + if (Object.keys(extraHeaders).length) { + fetchOptions.headers = { + ...headers, + ...extraHeaders, + }; + } + } else { + fetchOptions.credentials = credentials; + } + + const options = JSON.stringify(fetchOptions, null, 2); + return `fetch(${url}, ${options});`; +} + +async function fetchRequestPostData(resource: Entry) { + return resource.request.postData?._sha1 ? await fetch(`sha1/${resource.request.postData._sha1}`).then(r => r.text()) : resource.request.postData?.text; +} \ No newline at end of file diff --git a/packages/trace-viewer/src/traceModel.ts b/packages/trace-viewer/src/traceModel.ts index 1248dde967..87a3d42491 100644 --- a/packages/trace-viewer/src/traceModel.ts +++ b/packages/trace-viewer/src/traceModel.ts @@ -73,6 +73,7 @@ export class TraceModel { unzipProgress(++done, total); contextEntry.actions = modernizer.actions().sort((a1, a2) => a1.startTime - a2.startTime); + if (!backend.isLive()) { // Terminate actions w/o after event gracefully. // This would close after hooks event that has not been closed because diff --git a/packages/trace-viewer/src/ui/DEPS.list b/packages/trace-viewer/src/ui/DEPS.list index 3fab0da95b..0056375c05 100644 --- a/packages/trace-viewer/src/ui/DEPS.list +++ b/packages/trace-viewer/src/ui/DEPS.list @@ -6,3 +6,4 @@ ../entries.ts ../geometry.ts ../../../playwright/src/isomorphic/** +../third_party/devtools.ts diff --git a/packages/trace-viewer/src/ui/copyToClipboard.tsx b/packages/trace-viewer/src/ui/copyToClipboard.tsx index 3e570ede56..301be3dd03 100644 --- a/packages/trace-viewer/src/ui/copyToClipboard.tsx +++ b/packages/trace-viewer/src/ui/copyToClipboard.tsx @@ -18,19 +18,22 @@ import * as React from 'react'; import { ToolbarButton } from '@web/components/toolbarButton'; export const CopyToClipboard: React.FunctionComponent<{ - value: string, + value: string | (() => Promise), description?: string, }> = ({ value, description }) => { const [icon, setIcon] = React.useState('copy'); const handleCopy = React.useCallback(() => { - navigator.clipboard.writeText(value).then(() => { - setIcon('check'); - setTimeout(() => { - setIcon('copy'); - }, 3000); - }, () => { - setIcon('close'); + const valuePromise = typeof value === 'function' ? value() : Promise.resolve(value); + valuePromise.then(value => { + navigator.clipboard.writeText(value).then(() => { + setIcon('check'); + setTimeout(() => { + setIcon('copy'); + }, 3000); + }, () => { + setIcon('close'); + }); }); }, [value]); diff --git a/packages/trace-viewer/src/ui/modelUtil.ts b/packages/trace-viewer/src/ui/modelUtil.ts index a544d4dc3f..098b387c8f 100644 --- a/packages/trace-viewer/src/ui/modelUtil.ts +++ b/packages/trace-viewer/src/ui/modelUtil.ts @@ -312,7 +312,7 @@ function monotonicTimeDeltaBetweenLibraryAndRunner(nonPrimaryContexts: ContextEn for (const action of context.actions) { if (!action.startTime) continue; - const key = matchByStepId ? action.stepId! : `${action.apiName}@${(action as any).wallTime}`; + const key = matchByStepId ? action.callId! : `${action.apiName}@${(action as any).wallTime}`; const libraryAction = libraryActions.get(key); if (libraryAction) return action.startTime - libraryAction.startTime; diff --git a/packages/trace-viewer/src/ui/networkResourceDetails.css b/packages/trace-viewer/src/ui/networkResourceDetails.css index 59989b89dd..ac1245f70d 100644 --- a/packages/trace-viewer/src/ui/networkResourceDetails.css +++ b/packages/trace-viewer/src/ui/networkResourceDetails.css @@ -49,6 +49,15 @@ overflow: hidden; } +.network-request-details-copy { + display: flex; + margin-left: 10px; +} + +.network-request-details-copy button { + border-radius: 4px +} + .network-font-preview { font-family: font-preview; font-size: 30px; diff --git a/packages/trace-viewer/src/ui/networkResourceDetails.tsx b/packages/trace-viewer/src/ui/networkResourceDetails.tsx index 8df091b262..1f9fcc4581 100644 --- a/packages/trace-viewer/src/ui/networkResourceDetails.tsx +++ b/packages/trace-viewer/src/ui/networkResourceDetails.tsx @@ -20,6 +20,8 @@ import './networkResourceDetails.css'; import { TabbedPane } from '@web/components/tabbedPane'; import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper'; import { ToolbarButton } from '@web/components/toolbarButton'; +import { generateCurlCommand, generateFetchCall } from '../third_party/devtools'; +import { CopyToClipboard } from './copyToClipboard'; export const NetworkResourceDetails: React.FunctionComponent<{ resource: ResourceSnapshot; @@ -90,6 +92,13 @@ const RequestTab: React.FunctionComponent<{ : null}
Request Headers
{resource.request.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}
+
Copy Request
+
+ As cURL: generateCurlCommand(resource)}/> +
+
+ As Fetch: generateFetchCall(resource)}/> +
{requestBody &&
Request Body
} {requestBody && } ; diff --git a/packages/trace-viewer/src/ui/recorderView.tsx b/packages/trace-viewer/src/ui/recorderView.tsx index 940fd146a9..4d8ec8d297 100644 --- a/packages/trace-viewer/src/ui/recorderView.tsx +++ b/packages/trace-viewer/src/ui/recorderView.tsx @@ -45,6 +45,8 @@ export const RecorderView: React.FunctionComponent = () => { connection.setMode('recording'); }, [connection]); + window.playwrightSourcesEchoForTest = sources; + return
testId, className, }) => { - className = (className || '') + ` toolbar-button ${icon}`; - if (toggled) - className += ' toggled'; return