Merge branch 'main' into har-trace-response-bodysize

This commit is contained in:
Simon Knott 2024-09-18 08:20:26 +02:00
commit 0338eae452
No known key found for this signature in database
GPG key ID: 8CEDC00028084AEC
69 changed files with 659 additions and 257 deletions

View file

@ -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. 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 ## method: Test.use
* since: v1.10 * since: v1.10

371
package-lock.json generated
View file

@ -61,7 +61,7 @@
"react-dom": "^18.1.0", "react-dom": "^18.1.0",
"ssim.js": "^3.5.0", "ssim.js": "^3.5.0",
"typescript": "^5.5.3", "typescript": "^5.5.3",
"vite": "^5.0.13", "vite": "^5.4.6",
"ws": "^8.17.1", "ws": "^8.17.1",
"xml2js": "^0.5.0", "xml2js": "^0.5.0",
"yaml": "^2.2.2" "yaml": "^2.2.2"
@ -852,9 +852,9 @@
} }
}, },
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
"integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -1517,9 +1517,9 @@
"link": true "link": true
}, },
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.14.0", "version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.3.tgz",
"integrity": "sha512-jwXtxYbRt1V+CdQSy6Z+uZti7JF5irRKF8hlKfEnF/xJpcNGuuiZMBvuoYM+x9sr9iWGnzrlM0+9hvQ1kgkf1w==", "integrity": "sha512-MmKSfaB9GX+zXl6E8z4koOr/xU63AMVleLEa64v7R0QF/ZloMs5vcD1sHgM64GXXS1csaJutG+ddtzcueI/BLg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -1529,9 +1529,9 @@
] ]
}, },
"node_modules/@rollup/rollup-android-arm64": { "node_modules/@rollup/rollup-android-arm64": {
"version": "4.14.0", "version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.3.tgz",
"integrity": "sha512-fI9nduZhCccjzlsA/OuAwtFGWocxA4gqXGTLvOyiF8d+8o0fZUeSztixkYjcGq1fGZY3Tkq4yRvHPFxU+jdZ9Q==", "integrity": "sha512-zrt8ecH07PE3sB4jPOggweBjJMzI1JG5xI2DIsUbkA+7K+Gkjys6eV7i9pOenNSDJH3eOr/jLb/PzqtmdwDq5g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1541,9 +1541,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-arm64": { "node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.14.0", "version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.3.tgz",
"integrity": "sha512-BcnSPRM76/cD2gQC+rQNGBN6GStBs2pl/FpweW8JYuz5J/IEa0Fr4AtrPv766DB/6b2MZ/AfSIOSGw3nEIP8SA==", "integrity": "sha512-P0UxIOrKNBFTQaXTxOH4RxuEBVCgEA5UTNV6Yz7z9QHnUJ7eLX9reOd/NYMO3+XZO2cco19mXTxDMXxit4R/eQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1553,9 +1553,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-x64": { "node_modules/@rollup/rollup-darwin-x64": {
"version": "4.14.0", "version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.3.tgz",
"integrity": "sha512-LDyFB9GRolGN7XI6955aFeI3wCdCUszFWumWU0deHA8VpR3nWRrjG6GtGjBrQxQKFevnUTHKCfPR4IvrW3kCgQ==", "integrity": "sha512-L1M0vKGO5ASKntqtsFEjTq/fD91vAqnzeaF6sfNAy55aD+Hi2pBI5DKwCO+UNDQHWsDViJLqshxOahXyLSh3EA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1565,9 +1565,21 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-gnueabihf": { "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.14.0", "version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.3.tgz",
"integrity": "sha512-ygrGVhQP47mRh0AAD0zl6QqCbNsf0eTo+vgwkY6LunBcg0f2Jv365GXlDUECIyoXp1kKwL5WW6rsO429DBY/bA==", "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": [ "cpu": [
"arm" "arm"
], ],
@ -1577,9 +1589,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-gnu": { "node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.14.0", "version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.3.tgz",
"integrity": "sha512-x+uJ6MAYRlHGe9wi4HQjxpaKHPM3d3JjqqCkeC5gpnnI6OWovLdXTpfa8trjxPLnWKyBsSi5kne+146GAxFt4A==", "integrity": "sha512-nSZfcZtAnQPRZmUkUQwZq2OjQciR6tEoJaZVFvLHsj0MF6QhNMg0fQ6mUOsiCUpTqxTx0/O6gX0V/nYc7LrgPw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1589,9 +1601,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-musl": { "node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.14.0", "version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.3.tgz",
"integrity": "sha512-nrRw8ZTQKg6+Lttwqo6a2VxR9tOroa2m91XbdQ2sUUzHoedXlsyvY1fN4xWdqz8PKmf4orDwejxXHjh7YBGUCA==", "integrity": "sha512-MnvSPGO8KJXIMGlQDYfvYS3IosFN2rKsvxRpPO2l2cum+Z3exiExLwVU+GExL96pn8IP+GdH8Tz70EpBhO0sIQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1601,11 +1613,11 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": { "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.14.0", "version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.3.tgz",
"integrity": "sha512-xV0d5jDb4aFu84XKr+lcUJ9y3qpIWhttO3Qev97z8DKLXR62LC3cXT/bMZXrjLF9X+P5oSmJTzAhqwUbY96PnA==", "integrity": "sha512-+W+p/9QNDr2vE2AXU0qIy0qQE75E8RTwTwgqS2G5CRQ11vzq0tbnfBd6brWhS9bCRjAjepJe2fvvkvS3dno+iw==",
"cpu": [ "cpu": [
"ppc64le" "ppc64"
], ],
"optional": true, "optional": true,
"os": [ "os": [
@ -1613,9 +1625,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-gnu": { "node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.14.0", "version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.3.tgz",
"integrity": "sha512-SDDhBQwZX6LPRoPYjAZWyL27LbcBo7WdBFWJi5PI9RPCzU8ijzkQn7tt8NXiXRiFMJCVpkuMkBf4OxSxVMizAw==", "integrity": "sha512-yXH6K6KfqGXaxHrtr+Uoy+JpNlUlI46BKVyonGiaD74ravdnF9BUNC+vV+SIuB96hUMGShhKV693rF9QDfO6nQ==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@ -1625,9 +1637,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-s390x-gnu": { "node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.14.0", "version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.3.tgz",
"integrity": "sha512-RxB/qez8zIDshNJDufYlTT0ZTVut5eCpAZ3bdXDU9yTxBzui3KhbGjROK2OYTTor7alM7XBhssgoO3CZ0XD3qA==", "integrity": "sha512-R8cwY9wcnApN/KDYWTH4gV/ypvy9yZUHlbJvfaiXSB48JO3KpwSpjOGqO4jnGkLDSk1hgjYkTbTt6Q7uvPf8eg==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@ -1637,9 +1649,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-gnu": { "node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.14.0", "version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.3.tgz",
"integrity": "sha512-C6y6z2eCNCfhZxT9u+jAM2Fup89ZjiG5pIzZIDycs1IwESviLxwkQcFRGLjnDrP+PT+v5i4YFvlcfAs+LnreXg==", "integrity": "sha512-kZPbX/NOPh0vhS5sI+dR8L1bU2cSO9FgxwM8r7wHzGydzfSjLRCFAT87GR5U9scj2rhzN3JPYVC7NoBbl4FZ0g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1649,9 +1661,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-musl": { "node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.14.0", "version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.3.tgz",
"integrity": "sha512-i0QwbHYfnOMYsBEyjxcwGu5SMIi9sImDVjDg087hpzXqhBSosxkE7gyIYFHgfFl4mr7RrXksIBZ4DoLoP4FhJg==", "integrity": "sha512-S0Yq+xA1VEH66uiMNhijsWAafffydd2X5b77eLHfRmfLsRSpbiAWiRHV6DEpz6aOToPsgid7TI9rGd6zB1rhbg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -1661,9 +1673,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-arm64-msvc": { "node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.14.0", "version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.3.tgz",
"integrity": "sha512-Fq52EYb0riNHLBTAcL0cun+rRwyZ10S9vKzhGKKgeD+XbwunszSY0rVMco5KbOsTlwovP2rTOkiII/fQ4ih/zQ==", "integrity": "sha512-9isNzeL34yquCPyerog+IMCNxKR8XYmGd0tHSV+OVx0TmE0aJOo9uw4fZfUuk2qxobP5sug6vNdZR6u7Mw7Q+Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -1673,9 +1685,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-ia32-msvc": { "node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.14.0", "version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.3.tgz",
"integrity": "sha512-e/PBHxPdJ00O9p5Ui43+vixSgVf4NlLsmV6QneGERJ3lnjIua/kim6PRFe3iDueT1rQcgSkYP8ZBBXa/h4iPvw==", "integrity": "sha512-nMIdKnfZfzn1Vsk+RuOvl43ONTZXoAPUUxgcU0tXooqg4YrAqzfKzVenqqk2g5efWh46/D28cKFrOzDSW28gTA==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -1685,9 +1697,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-msvc": { "node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.14.0", "version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.3.tgz",
"integrity": "sha512-aGg7iToJjdklmxlUlJh/PaPNa4PmqHfyRMLunbL3eaMO0gp656+q1zOKkpJ/CVe9CryJv6tAN1HDoR8cNGzkag==", "integrity": "sha512-fOvu7PCQjAj4eWDEuD8Xz5gpzFqXzGlxHZozHP4b9Jxv9APtdxL6STqztDzMLuRXEc4UpXGGhx029Xgm91QBeA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -5862,9 +5874,9 @@
} }
}, },
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.0.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "2.3.1", "version": "2.3.1",
@ -5917,9 +5929,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.38", "version": "8.4.47",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
"integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@ -5936,8 +5948,8 @@
], ],
"dependencies": { "dependencies": {
"nanoid": "^3.3.7", "nanoid": "^3.3.7",
"picocolors": "^1.0.0", "picocolors": "^1.1.0",
"source-map-js": "^1.2.0" "source-map-js": "^1.2.1"
}, },
"engines": { "engines": {
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
@ -6301,9 +6313,9 @@
} }
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.14.0", "version": "4.21.3",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.0.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.3.tgz",
"integrity": "sha512-Qe7w62TyawbDzB4yt32R0+AbIo6m1/sqO7UPzFS8Z/ksL5mrfhA0v4CavfdmFav3D+ub4QeAgsGEe84DoWe/nQ==", "integrity": "sha512-7sqRtBNnEbcBtMeRVc6VRsJMmpI+JU1z9VTvW8D4gXIYQFz0aLcsE6rRkyghZkLfEgUZgVvOG7A5CVz/VW5GIA==",
"dependencies": { "dependencies": {
"@types/estree": "1.0.5" "@types/estree": "1.0.5"
}, },
@ -6315,21 +6327,22 @@
"npm": ">=8.0.0" "npm": ">=8.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.14.0", "@rollup/rollup-android-arm-eabi": "4.21.3",
"@rollup/rollup-android-arm64": "4.14.0", "@rollup/rollup-android-arm64": "4.21.3",
"@rollup/rollup-darwin-arm64": "4.14.0", "@rollup/rollup-darwin-arm64": "4.21.3",
"@rollup/rollup-darwin-x64": "4.14.0", "@rollup/rollup-darwin-x64": "4.21.3",
"@rollup/rollup-linux-arm-gnueabihf": "4.14.0", "@rollup/rollup-linux-arm-gnueabihf": "4.21.3",
"@rollup/rollup-linux-arm64-gnu": "4.14.0", "@rollup/rollup-linux-arm-musleabihf": "4.21.3",
"@rollup/rollup-linux-arm64-musl": "4.14.0", "@rollup/rollup-linux-arm64-gnu": "4.21.3",
"@rollup/rollup-linux-powerpc64le-gnu": "4.14.0", "@rollup/rollup-linux-arm64-musl": "4.21.3",
"@rollup/rollup-linux-riscv64-gnu": "4.14.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.21.3",
"@rollup/rollup-linux-s390x-gnu": "4.14.0", "@rollup/rollup-linux-riscv64-gnu": "4.21.3",
"@rollup/rollup-linux-x64-gnu": "4.14.0", "@rollup/rollup-linux-s390x-gnu": "4.21.3",
"@rollup/rollup-linux-x64-musl": "4.14.0", "@rollup/rollup-linux-x64-gnu": "4.21.3",
"@rollup/rollup-win32-arm64-msvc": "4.14.0", "@rollup/rollup-linux-x64-musl": "4.21.3",
"@rollup/rollup-win32-ia32-msvc": "4.14.0", "@rollup/rollup-win32-arm64-msvc": "4.21.3",
"@rollup/rollup-win32-x64-msvc": "4.14.0", "@rollup/rollup-win32-ia32-msvc": "4.21.3",
"@rollup/rollup-win32-x64-msvc": "4.21.3",
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
@ -6589,9 +6602,9 @@
} }
}, },
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.2.0", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -7171,13 +7184,13 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "5.2.8", "version": "5.4.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.8.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz",
"integrity": "sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==", "integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==",
"dependencies": { "dependencies": {
"esbuild": "^0.20.1", "esbuild": "^0.21.3",
"postcss": "^8.4.38", "postcss": "^8.4.43",
"rollup": "^4.13.0" "rollup": "^4.20.0"
}, },
"bin": { "bin": {
"vite": "bin/vite.js" "vite": "bin/vite.js"
@ -7196,6 +7209,7 @@
"less": "*", "less": "*",
"lightningcss": "^1.21.0", "lightningcss": "^1.21.0",
"sass": "*", "sass": "*",
"sass-embedded": "*",
"stylus": "*", "stylus": "*",
"sugarss": "*", "sugarss": "*",
"terser": "^5.4.0" "terser": "^5.4.0"
@ -7213,6 +7227,9 @@
"sass": { "sass": {
"optional": true "optional": true
}, },
"sass-embedded": {
"optional": true
},
"stylus": { "stylus": {
"optional": true "optional": true
}, },
@ -7243,9 +7260,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/android-arm": { "node_modules/vite/node_modules/@esbuild/android-arm": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
"integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -7258,9 +7275,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/android-arm64": { "node_modules/vite/node_modules/@esbuild/android-arm64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
"integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -7273,9 +7290,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/android-x64": { "node_modules/vite/node_modules/@esbuild/android-x64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
"integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -7288,9 +7305,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/darwin-arm64": { "node_modules/vite/node_modules/@esbuild/darwin-arm64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
"integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -7303,9 +7320,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/darwin-x64": { "node_modules/vite/node_modules/@esbuild/darwin-x64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
"integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -7318,9 +7335,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/freebsd-arm64": { "node_modules/vite/node_modules/@esbuild/freebsd-arm64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
"integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -7333,9 +7350,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/freebsd-x64": { "node_modules/vite/node_modules/@esbuild/freebsd-x64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
"integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -7348,9 +7365,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/linux-arm": { "node_modules/vite/node_modules/@esbuild/linux-arm": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
"integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -7363,9 +7380,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/linux-arm64": { "node_modules/vite/node_modules/@esbuild/linux-arm64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
"integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -7378,9 +7395,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/linux-ia32": { "node_modules/vite/node_modules/@esbuild/linux-ia32": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
"integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -7393,9 +7410,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/linux-loong64": { "node_modules/vite/node_modules/@esbuild/linux-loong64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
"integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@ -7408,9 +7425,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/linux-mips64el": { "node_modules/vite/node_modules/@esbuild/linux-mips64el": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
"integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
@ -7423,9 +7440,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/linux-ppc64": { "node_modules/vite/node_modules/@esbuild/linux-ppc64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
"integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -7438,9 +7455,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/linux-riscv64": { "node_modules/vite/node_modules/@esbuild/linux-riscv64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
"integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@ -7453,9 +7470,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/linux-s390x": { "node_modules/vite/node_modules/@esbuild/linux-s390x": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
"integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@ -7468,9 +7485,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/linux-x64": { "node_modules/vite/node_modules/@esbuild/linux-x64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
"integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -7483,9 +7500,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/netbsd-x64": { "node_modules/vite/node_modules/@esbuild/netbsd-x64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
"integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -7498,9 +7515,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/openbsd-x64": { "node_modules/vite/node_modules/@esbuild/openbsd-x64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
"integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -7513,9 +7530,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/sunos-x64": { "node_modules/vite/node_modules/@esbuild/sunos-x64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
"integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -7528,9 +7545,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/win32-arm64": { "node_modules/vite/node_modules/@esbuild/win32-arm64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
"integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -7543,9 +7560,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/win32-ia32": { "node_modules/vite/node_modules/@esbuild/win32-ia32": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
"integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -7558,9 +7575,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/win32-x64": { "node_modules/vite/node_modules/@esbuild/win32-x64": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
"integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -7573,9 +7590,9 @@
} }
}, },
"node_modules/vite/node_modules/esbuild": { "node_modules/vite/node_modules/esbuild": {
"version": "0.20.2", "version": "0.21.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
"hasInstallScript": true, "hasInstallScript": true,
"bin": { "bin": {
"esbuild": "bin/esbuild" "esbuild": "bin/esbuild"
@ -7584,29 +7601,29 @@
"node": ">=12" "node": ">=12"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/aix-ppc64": "0.20.2", "@esbuild/aix-ppc64": "0.21.5",
"@esbuild/android-arm": "0.20.2", "@esbuild/android-arm": "0.21.5",
"@esbuild/android-arm64": "0.20.2", "@esbuild/android-arm64": "0.21.5",
"@esbuild/android-x64": "0.20.2", "@esbuild/android-x64": "0.21.5",
"@esbuild/darwin-arm64": "0.20.2", "@esbuild/darwin-arm64": "0.21.5",
"@esbuild/darwin-x64": "0.20.2", "@esbuild/darwin-x64": "0.21.5",
"@esbuild/freebsd-arm64": "0.20.2", "@esbuild/freebsd-arm64": "0.21.5",
"@esbuild/freebsd-x64": "0.20.2", "@esbuild/freebsd-x64": "0.21.5",
"@esbuild/linux-arm": "0.20.2", "@esbuild/linux-arm": "0.21.5",
"@esbuild/linux-arm64": "0.20.2", "@esbuild/linux-arm64": "0.21.5",
"@esbuild/linux-ia32": "0.20.2", "@esbuild/linux-ia32": "0.21.5",
"@esbuild/linux-loong64": "0.20.2", "@esbuild/linux-loong64": "0.21.5",
"@esbuild/linux-mips64el": "0.20.2", "@esbuild/linux-mips64el": "0.21.5",
"@esbuild/linux-ppc64": "0.20.2", "@esbuild/linux-ppc64": "0.21.5",
"@esbuild/linux-riscv64": "0.20.2", "@esbuild/linux-riscv64": "0.21.5",
"@esbuild/linux-s390x": "0.20.2", "@esbuild/linux-s390x": "0.21.5",
"@esbuild/linux-x64": "0.20.2", "@esbuild/linux-x64": "0.21.5",
"@esbuild/netbsd-x64": "0.20.2", "@esbuild/netbsd-x64": "0.21.5",
"@esbuild/openbsd-x64": "0.20.2", "@esbuild/openbsd-x64": "0.21.5",
"@esbuild/sunos-x64": "0.20.2", "@esbuild/sunos-x64": "0.21.5",
"@esbuild/win32-arm64": "0.20.2", "@esbuild/win32-arm64": "0.21.5",
"@esbuild/win32-ia32": "0.20.2", "@esbuild/win32-ia32": "0.21.5",
"@esbuild/win32-x64": "0.20.2" "@esbuild/win32-x64": "0.21.5"
} }
}, },
"node_modules/vitefu": { "node_modules/vitefu": {

View file

@ -100,7 +100,7 @@
"react-dom": "^18.1.0", "react-dom": "^18.1.0",
"ssim.js": "^3.5.0", "ssim.js": "^3.5.0",
"typescript": "^5.5.3", "typescript": "^5.5.3",
"vite": "^5.0.13", "vite": "^5.4.6",
"ws": "^8.17.1", "ws": "^8.17.1",
"xml2js": "^0.5.0", "xml2js": "^0.5.0",
"yaml": "^2.2.2" "yaml": "^2.2.2"

View file

@ -567,10 +567,6 @@ async function codegen(options: Options & { target: string, output?: string, tes
tracesDir, tracesDir,
}); });
dotenv.config({ path: 'playwright.env' }); 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({ await context._enableRecorder({
language, language,
launchOptions, launchOptions,

View file

@ -72,7 +72,7 @@ export class BidiConnection {
let context; let context;
if ('context' in object.params) if ('context' in object.params)
context = object.params.context; 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; context = object.params.source?.context;
if (context) { if (context) {
const session = this._browsingContextToSession.get(context); const session = this._browsingContextToSession.get(context);

View file

@ -23,7 +23,7 @@ import { BidiSerializer } from './third_party/bidiSerializer';
export class BidiExecutionContext implements js.ExecutionContextDelegate { export class BidiExecutionContext implements js.ExecutionContextDelegate {
private readonly _session: BidiSession; private readonly _session: BidiSession;
private readonly _target: bidi.Script.Target; readonly _target: bidi.Script.Target;
constructor(session: BidiSession, realmInfo: bidi.Script.RealmInfo) { constructor(session: BidiSession, realmInfo: bidi.Script.RealmInfo) {
this._session = session; this._session = session;

View file

@ -21,7 +21,8 @@ import type * as accessibility from '../accessibility';
import * as dom from '../dom'; import * as dom from '../dom';
import * as dialog from '../dialog'; import * as dialog from '../dialog';
import type * as frames from '../frames'; 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 { Progress } from '../progress';
import type * as types from '../types'; import type * as types from '../types';
import type { BidiBrowserContext } from './bidiBrowser'; import type { BidiBrowserContext } from './bidiBrowser';
@ -33,6 +34,7 @@ import { BidiNetworkManager } from './bidiNetworkManager';
import { BrowserContext } from '../browserContext'; import { BrowserContext } from '../browserContext';
const UTILITY_WORLD_NAME = '__playwright_utility_world__'; const UTILITY_WORLD_NAME = '__playwright_utility_world__';
const kPlaywrightBindingChannel = 'playwrightChannel';
export class BidiPage implements PageDelegate { export class BidiPage implements PageDelegate {
readonly rawMouse: RawMouseImpl; 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._page.on(Page.Events.FrameDetached, (frame: frames.Frame) => this._removeContextsForFrame(frame, false));
this._sessionListeners = [ this._sessionListeners = [
eventsHelper.addEventListener(bidiSession, 'script.realmCreated', this._onRealmCreated.bind(this)), 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.contextDestroyed', this._onBrowsingContextDestroyed.bind(this)),
eventsHelper.addEventListener(bidiSession, 'browsingContext.navigationStarted', this._onNavigationStarted.bind(this)), eventsHelper.addEventListener(bidiSession, 'browsingContext.navigationStarted', this._onNavigationStarted.bind(this)),
eventsHelper.addEventListener(bidiSession, 'browsingContext.navigationAborted', this._onNavigationAborted.bind(this)), eventsHelper.addEventListener(bidiSession, 'browsingContext.navigationAborted', this._onNavigationAborted.bind(this)),
@ -93,6 +96,7 @@ export class BidiPage implements PageDelegate {
this.updateHttpCredentials(), this.updateHttpCredentials(),
this.updateRequestInterception(), this.updateRequestInterception(),
this._updateViewport(), this._updateViewport(),
this._installMainBinding(),
this._addAllInitScripts(), this._addAllInitScripts(),
]); ]);
} }
@ -315,18 +319,63 @@ export class BidiPage implements PageDelegate {
}); });
} }
goBack(): Promise<boolean> { async goBack(): Promise<boolean> {
throw new Error('Method not implemented.'); return await this._session.send('browsingContext.traverseHistory', {
context: this._session.sessionId,
delta: -1,
}).then(() => true).catch(() => false);
} }
goForward(): Promise<boolean> { async goForward(): Promise<boolean> {
throw new Error('Method not implemented.'); return await this._session.send('browsingContext.traverseHistory', {
context: this._session.sessionId,
delta: +1,
}).then(() => true).catch(() => false);
} }
async forceGarbageCollection(): Promise<void> { async forceGarbageCollection(): Promise<void> {
throw new Error('Method not implemented.'); 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<void> { async addInitScript(initScript: InitScript): Promise<void> {
const { script } = await this._session.send('script.addPreloadScript', { const { script } = await this._session.send('script.addPreloadScript', {
// TODO: remove function call from the source. // 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<Buffer> { async takeScreenshot(progress: Progress, format: string, documentRect: types.Rect | undefined, viewportRect: types.Rect | undefined, quality: number | undefined, fitsViewport: boolean, scale: 'css' | 'device'): Promise<Buffer> {
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<frames.Frame | null> { async getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {
@ -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 { function toBidiExecutionContext(executionContext: dom.FrameExecutionContext): BidiExecutionContext {
return (executionContext as any)[contextDelegateSymbol] as BidiExecutionContext; return (executionContext as any)[contextDelegateSymbol] as BidiExecutionContext;
} }

View file

@ -20,7 +20,6 @@ import type * as types from '../types';
import type { ActionInContext, LanguageGenerator, LanguageGeneratorOptions } from './types'; import type { ActionInContext, LanguageGenerator, LanguageGeneratorOptions } from './types';
export function generateCode(actions: ActionInContext[], languageGenerator: LanguageGenerator, options: LanguageGeneratorOptions) { export function generateCode(actions: ActionInContext[], languageGenerator: LanguageGenerator, options: LanguageGeneratorOptions) {
actions = collapseActions(actions);
const header = languageGenerator.generateHeader(options); const header = languageGenerator.generateHeader(options);
const footer = languageGenerator.generateFooter(options.saveStorage); const footer = languageGenerator.generateFooter(options.saveStorage);
const actionTexts = actions.map(a => languageGenerator.generateAction(a)).filter(Boolean); const actionTexts = actions.map(a => languageGenerator.generateAction(a)).filter(Boolean);
@ -70,6 +69,23 @@ export function toKeyboardModifiers(modifiers: number): types.SmartKeyboardModif
return result; 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 { export function toClickOptionsForSourceCode(action: actions.ClickAction): types.MouseClickOptions {
const modifiers = toKeyboardModifiers(action.modifiers); const modifiers = toKeyboardModifiers(action.modifiers);
const options: types.MouseClickOptions = {}; const options: types.MouseClickOptions = {};
@ -84,19 +100,3 @@ export function toClickOptionsForSourceCode(action: actions.ClickAction): types.
options.position = action.position; options.position = action.position;
return options; 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;
}

View file

@ -41,6 +41,7 @@ import { serializeError } from '../errors';
import { ElementHandleDispatcher } from './elementHandlerDispatcher'; import { ElementHandleDispatcher } from './elementHandlerDispatcher';
import { RecorderInTraceViewer } from '../recorder/recorderInTraceViewer'; import { RecorderInTraceViewer } from '../recorder/recorderInTraceViewer';
import { RecorderApp } from '../recorder/recorderApp'; import { RecorderApp } from '../recorder/recorderApp';
import type { IRecorderAppFactory } from '../recorder/recorderFrontend';
export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channels.BrowserContextChannel, DispatcherScope> implements channels.BrowserContextChannel { export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channels.BrowserContextChannel, DispatcherScope> implements channels.BrowserContextChannel {
_type_EventTarget = true; _type_EventTarget = true;
@ -293,7 +294,20 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
} }
async recorderSupplementEnable(params: channels.BrowserContextRecorderSupplementEnableParams): Promise<void> { async recorderSupplementEnable(params: channels.BrowserContextRecorderSupplementEnableParams): Promise<void> {
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); await Recorder.show(this._context, factory, params);
} }

View file

@ -41,6 +41,7 @@ import type * as types from './types';
import type { HeadersArray, ProxySettings } from './types'; import type { HeadersArray, ProxySettings } from './types';
import { getMatchingTLSOptionsForOrigin, rewriteOpenSSLErrorIfNeeded } from './socksClientCertificatesInterceptor'; import { getMatchingTLSOptionsForOrigin, rewriteOpenSSLErrorIfNeeded } from './socksClientCertificatesInterceptor';
import type * as har from '@trace/har'; import type * as har from '@trace/har';
import { TLSSocket } from 'tls';
type FetchRequestOptions = { type FetchRequestOptions = {
userAgent: string; userAgent: string;
@ -73,6 +74,7 @@ export type APIRequestFinishedEvent = {
statusMessage: string; statusMessage: string;
body?: Buffer; body?: Buffer;
timings: har.Timings; timings: har.Timings;
securityDetails?: har.SecurityDetails;
}; };
type SendRequestOptions = https.RequestOptions & { type SendRequestOptions = https.RequestOptions & {
@ -303,6 +305,8 @@ export abstract class APIRequestContext extends SdkObject {
let tlsHandshakeAt: number | undefined; let tlsHandshakeAt: number | undefined;
let requestFinishAt: number | undefined; let requestFinishAt: number | undefined;
let securityDetails: har.SecurityDetails | undefined;
const request = requestConstructor(url, requestOptions as any, async response => { const request = requestConstructor(url, requestOptions as any, async response => {
const responseAt = monotonicTime(); const responseAt = monotonicTime();
const notifyRequestFinished = (body?: Buffer) => { const notifyRequestFinished = (body?: Buffer) => {
@ -328,6 +332,7 @@ export abstract class APIRequestContext extends SdkObject {
cookies, cookies,
body, body,
timings, timings,
securityDetails,
}; };
this.emit(APIRequestContext.Events.RequestFinished, requestFinishedEvent); this.emit(APIRequestContext.Events.RequestFinished, requestFinishedEvent);
}; };
@ -482,7 +487,20 @@ export abstract class APIRequestContext extends SdkObject {
// non-happy-eyeballs sockets // non-happy-eyeballs sockets
socket.on('lookup', () => { dnsLookupAt = monotonicTime(); }); socket.on('lookup', () => { dnsLookupAt = monotonicTime(); });
socket.on('connect', () => { tcpConnectionAt = 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
};
}
});
}); });
request.on('finish', () => { requestFinishAt = monotonicTime(); }); request.on('finish', () => { requestFinishAt = monotonicTime(); });

View file

@ -218,6 +218,9 @@ export class HarTracer {
this._computeHarEntryTotalTime(harEntry); this._computeHarEntryTotalTime(harEntry);
} }
if (!this._options.omitSecurityDetails)
harEntry._securityDetails = event.securityDetails;
for (let i = 0; i < event.rawHeaders.length; i += 2) { for (let i = 0; i < event.rawHeaders.length; i += 2) {
harEntry.response.headers.push({ harEntry.response.headers.push({
name: event.rawHeaders[i], name: event.rawHeaders[i],

View file

@ -73,7 +73,7 @@ export class ContextRecorder extends EventEmitter {
saveStorage: params.saveStorage, 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', () => { collection.on('change', () => {
this._recorderSources = []; this._recorderSources = [];
for (const languageGenerator of this._orderedLanguages) { for (const languageGenerator of this._orderedLanguages) {

View file

@ -43,6 +43,7 @@ declare global {
} }
export class EmptyRecorderApp extends EventEmitter implements IRecorderApp { export class EmptyRecorderApp extends EventEmitter implements IRecorderApp {
wsEndpointForTest: undefined;
async close(): Promise<void> {} async close(): Promise<void> {}
async setPaused(paused: boolean): Promise<void> {} async setPaused(paused: boolean): Promise<void> {}
async setMode(mode: Mode): Promise<void> {} async setMode(mode: Mode): Promise<void> {}
@ -54,7 +55,7 @@ export class EmptyRecorderApp extends EventEmitter implements IRecorderApp {
export class RecorderApp extends EventEmitter implements IRecorderApp { export class RecorderApp extends EventEmitter implements IRecorderApp {
private _page: Page; private _page: Page;
readonly wsEndpoint: string | undefined; readonly wsEndpointForTest: string | undefined;
private _recorder: IRecorder; private _recorder: IRecorder;
constructor(recorder: IRecorder, page: Page, wsEndpoint: string | undefined) { constructor(recorder: IRecorder, page: Page, wsEndpoint: string | undefined) {
@ -62,7 +63,7 @@ export class RecorderApp extends EventEmitter implements IRecorderApp {
this.setMaxListeners(0); this.setMaxListeners(0);
this._recorder = recorder; this._recorder = recorder;
this._page = page; this._page = page;
this.wsEndpoint = wsEndpoint; this.wsEndpointForTest = wsEndpoint;
} }
async close() { async close() {

View file

@ -20,31 +20,35 @@ import type { Page } from '../page';
import type { Signal } from './recorderActions'; import type { Signal } from './recorderActions';
import type { ActionInContext } from '../codegen/types'; import type { ActionInContext } from '../codegen/types';
import { monotonicTime } from '../../utils/time'; import { monotonicTime } from '../../utils/time';
import { callMetadataForAction } from './recorderUtils'; import { callMetadataForAction, collapseActions, traceEventsToAction } from './recorderUtils';
import { serializeError } from '../errors'; import { serializeError } from '../errors';
import { performAction } from './recorderRunner'; import { performAction } from './recorderRunner';
import type { CallMetadata } from '@protocol/callMetadata'; import type { CallMetadata } from '@protocol/callMetadata';
import { isUnderTest } from '../../utils/debug'; import { isUnderTest } from '../../utils/debug';
import type { BrowserContext } from '../browserContext';
export class RecorderCollection extends EventEmitter { export class RecorderCollection extends EventEmitter {
private _actions: ActionInContext[] = []; private _actions: ActionInContext[] = [];
private _enabled: boolean; private _enabled: boolean;
private _pageAliases: Map<Page, string>; private _pageAliases: Map<Page, string>;
private _context: BrowserContext;
constructor(pageAliases: Map<Page, string>, enabled: boolean) { constructor(context: BrowserContext, pageAliases: Map<Page, string>, enabled: boolean) {
super(); super();
this._context = context;
this._enabled = enabled; this._enabled = enabled;
this._pageAliases = pageAliases; this._pageAliases = pageAliases;
this.restart();
} }
restart() { restart() {
this._actions = []; this._actions = [];
this.emit('change'); this._fireChange();
} }
actions() { 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) { setEnabled(enabled: boolean) {
@ -60,7 +64,7 @@ export class RecorderCollection extends EventEmitter {
addRecordedAction(actionInContext: ActionInContext) { addRecordedAction(actionInContext: ActionInContext) {
if (['openPage', 'closePage'].includes(actionInContext.action.name)) { if (['openPage', 'closePage'].includes(actionInContext.action.name)) {
this._actions.push(actionInContext); this._actions.push(actionInContext);
this.emit('change'); this._fireChange();
return; return;
} }
this._addAction(actionInContext).catch(() => {}); this._addAction(actionInContext).catch(() => {});
@ -69,11 +73,16 @@ export class RecorderCollection extends EventEmitter {
private async _addAction(actionInContext: ActionInContext, callback?: (callMetadata: CallMetadata) => Promise<void>) { private async _addAction(actionInContext: ActionInContext, callback?: (callMetadata: CallMetadata) => Promise<void>) {
if (!this._enabled) if (!this._enabled)
return; return;
if (actionInContext.action.name === 'openPage' || actionInContext.action.name === 'closePage') {
this._actions.push(actionInContext);
this._fireChange();
return;
}
const { callMetadata, mainFrame } = callMetadataForAction(this._pageAliases, actionInContext); const { callMetadata, mainFrame } = callMetadataForAction(this._pageAliases, actionInContext);
await mainFrame.instrumentation.onBeforeCall(mainFrame, callMetadata); await mainFrame.instrumentation.onBeforeCall(mainFrame, callMetadata);
this._actions.push(actionInContext); this._actions.push(actionInContext);
this.emit('change'); this._fireChange();
const error = await callback?.(callMetadata).catch((e: Error) => e); const error = await callback?.(callMetadata).catch((e: Error) => e);
callMetadata.endTime = monotonicTime(); callMetadata.endTime = monotonicTime();
callMetadata.error = error ? serializeError(error) : undefined; callMetadata.error = error ? serializeError(error) : undefined;
@ -120,4 +129,8 @@ export class RecorderCollection extends EventEmitter {
return; return;
} }
} }
private _fireChange() {
this.emit('change');
}
} }

View file

@ -23,6 +23,7 @@ export interface IRecorder {
} }
export interface IRecorderApp extends EventEmitter { export interface IRecorderApp extends EventEmitter {
readonly wsEndpointForTest: string | undefined;
close(): Promise<void>; close(): Promise<void>;
setPaused(paused: boolean): Promise<void>; setPaused(paused: boolean): Promise<void>;
setMode(mode: Mode): Promise<void>; setMode(mode: Mode): Promise<void>;

View file

@ -25,6 +25,7 @@ import { gracefullyProcessExitDoNotHang } from '../../utils/processLauncher';
import type { Transport } from '../../utils/httpServer'; import type { Transport } from '../../utils/httpServer';
export class RecorderInTraceViewer extends EventEmitter implements IRecorderApp { export class RecorderInTraceViewer extends EventEmitter implements IRecorderApp {
readonly wsEndpointForTest: string | undefined;
private _recorder: IRecorder; private _recorder: IRecorder;
private _transport: Transport; private _transport: Transport;
@ -32,15 +33,16 @@ export class RecorderInTraceViewer extends EventEmitter implements IRecorderApp
return async (recorder: IRecorder) => { return async (recorder: IRecorder) => {
const transport = new RecorderTransport(); const transport = new RecorderTransport();
const trace = path.join(context._browser.options.tracesDir, 'trace'); const trace = path.join(context._browser.options.tracesDir, 'trace');
await openApp(trace, { transport }); const wsEndpointForTest = await openApp(trace, { transport, headless: !context._browser.options.headful });
return new RecorderInTraceViewer(context, recorder, transport); 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(); super();
this._recorder = recorder; this._recorder = recorder;
this._transport = transport; this._transport = transport;
this.wsEndpointForTest = wsEndpointForTest;
} }
async close(): Promise<void> { async close(): Promise<void> {
@ -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<string | undefined> {
const server = await startTraceViewerServer(options); const server = await startTraceViewerServer(options);
await installRootRedirect(server, [trace], { ...options, webApp: 'recorder.html' }); await installRootRedirect(server, [trace], { ...options, webApp: 'recorder.html' });
const page = await openTraceViewerApp(server.urlPrefix('precise'), 'chromium', options); const page = await openTraceViewerApp(server.urlPrefix('precise'), 'chromium', options);
page.on('close', () => gracefullyProcessExitDoNotHang(0)); page.on('close', () => gracefullyProcessExitDoNotHang(0));
return page.context()._browser.options.wsEndpoint;
} }
class RecorderTransport implements Transport { class RecorderTransport implements Transport {

View file

@ -20,9 +20,12 @@ import type { Page } from '../page';
import type { ActionInContext } from '../codegen/types'; import type { ActionInContext } from '../codegen/types';
import type { Frame } from '../frames'; import type { Frame } from '../frames';
import type * as actions from './recorderActions'; 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 { serializeExpectedTextValues } from '../../utils/expectUtils';
import { createGuid, monotonicTime } from '../../utils'; import { createGuid, monotonicTime } from '../../utils';
import { serializeValue } from '../../protocol/serializers';
export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus): CallLog { export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus): CallLog {
let title = metadata.apiName || metadata.method; let title = metadata.apiName || metadata.method;
@ -76,57 +79,113 @@ export async function frameForAction(pageAliases: Map<Page, string>, actionInCon
return result.frame; return result.frame;
} }
export function traceParamsForAction(actionInContext: ActionInContext) { export function traceParamsForAction(actionInContext: ActionInContext): { method: string, params: any } {
const { action } = actionInContext; const { action } = actionInContext;
switch (action.name) { switch (action.name) {
case 'navigate': return { url: action.url }; case 'navigate': {
case 'openPage': return {}; const params: channels.FrameGotoParams = {
case 'closePage': return {}; url: action.url,
};
return { method: 'goto', params };
}
case 'openPage':
case 'closePage':
throw new Error('Not reached');
} }
const selector = buildFullSelector(actionInContext.frame.framePath, action.selector); const selector = buildFullSelector(actionInContext.frame.framePath, action.selector);
switch (action.name) { switch (action.name) {
case 'click': return { selector, clickCount: action.clickCount }; case 'click': {
case 'press': { const params: channels.FrameClickParams = {
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 {
selector, selector,
expression: 'to.be.checked', strict: true,
isNot: !action.checked, 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': { case 'assertText': {
return { const params: channels.FrameExpectParams = {
selector, selector,
expression: 'to.have.text', expression: 'to.have.text',
expectedText: serializeExpectedTextValues([action.text], { matchSubstring: true, normalizeWhiteSpace: true }), expectedText: serializeExpectedTextValues([action.text], { matchSubstring: true, normalizeWhiteSpace: true }),
isNot: false, isNot: false,
}; };
return { method: 'expect', params };
} }
case 'assertValue': { case 'assertValue': {
return { const params: channels.FrameExpectParams = {
selector, selector,
expression: 'to.have.value', expression: 'to.have.value',
expectedValue: action.value, expectedValue: { value: serializeValue(action.value, value => ({ fallThrough: value })), handles: [] },
isNot: false, isNot: false,
}; };
return { method: 'expect', params };
} }
case 'assertVisible': { case 'assertVisible': {
return { const params: channels.FrameExpectParams = {
selector, selector,
expression: 'to.be.visible', expression: 'to.be.visible',
isNot: false, isNot: false,
}; };
return { method: 'expect', params };
} }
} }
} }
@ -134,8 +193,10 @@ export function traceParamsForAction(actionInContext: ActionInContext) {
export function callMetadataForAction(pageAliases: Map<Page, string>, actionInContext: ActionInContext): { callMetadata: CallMetadata, mainFrame: Frame } { export function callMetadataForAction(pageAliases: Map<Page, string>, actionInContext: ActionInContext): { callMetadata: CallMetadata, mainFrame: Frame } {
const mainFrame = mainFrameForAction(pageAliases, actionInContext); const mainFrame = mainFrameForAction(pageAliases, actionInContext);
const { action } = actionInContext; const { action } = actionInContext;
const { method, params } = traceParamsForAction(actionInContext);
const callMetadata: CallMetadata = { const callMetadata: CallMetadata = {
id: `call@${createGuid()}`, id: `call@${createGuid()}`,
stepId: `recorder@${createGuid()}`,
apiName: 'frame.' + action.name, apiName: 'frame.' + action.name,
objectId: mainFrame.guid, objectId: mainFrame.guid,
pageId: mainFrame._page.guid, pageId: mainFrame._page.guid,
@ -143,9 +204,132 @@ export function callMetadataForAction(pageAliases: Map<Page, string>, actionInCo
startTime: monotonicTime(), startTime: monotonicTime(),
endTime: 0, endTime: 0,
type: 'Frame', type: 'Frame',
method: action.name, method,
params: traceParamsForAction(actionInContext), params,
log: [], log: [],
}; };
return { callMetadata, mainFrame }; 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;
}

View file

@ -46,6 +46,7 @@ export type TracerOptions = {
snapshots?: boolean; snapshots?: boolean;
screenshots?: boolean; screenshots?: boolean;
live?: boolean; live?: boolean;
inMemory?: boolean;
}; };
type RecordingState = { type RecordingState = {
@ -79,6 +80,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
private _allResources = new Set<string>(); private _allResources = new Set<string>();
private _contextCreatedEvent: trace.ContextCreatedTraceEvent; private _contextCreatedEvent: trace.ContextCreatedTraceEvent;
private _pendingHarEntries = new Set<har.Entry>(); private _pendingHarEntries = new Set<har.Entry>();
private _inMemoryEvents: trace.TraceEvent[] | undefined;
constructor(context: BrowserContext | APIRequestContext, tracesDir: string | undefined) { constructor(context: BrowserContext | APIRequestContext, tracesDir: string | undefined) {
super(context, 'tracing'); 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. // Tracing is 10x bigger if we include scripts in every trace.
if (options.snapshots) if (options.snapshots)
this._harTracer.start({ omitScripts: !options.live }); this._harTracer.start({ omitScripts: !options.live });
this._inMemoryEvents = options.inMemory ? [] : undefined;
} }
async startChunk(options: { name?: string, title?: string } = {}): Promise<{ traceName: string }> { 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(), wallTime: Date.now(),
monotonicTime: monotonicTime() monotonicTime: monotonicTime()
}; };
this._fs.appendFile(this._state.traceFile, JSON.stringify(event) + '\n'); this._appendTraceEvent(event);
this._context.instrumentation.addListener(this, this._context); this._context.instrumentation.addListener(this, this._context);
this._eventListeners.push( this._eventListeners.push(
@ -193,6 +196,10 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
return { traceName: this._state.traceName }; return { traceName: this._state.traceName };
} }
inMemoryEvents(): trace.TraceEvent[] {
return this._inMemoryEvents || [];
}
private _startScreencast() { private _startScreencast() {
if (!(this._context instanceof BrowserContext)) if (!(this._context instanceof BrowserContext))
return; 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). // 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'); 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); this._fs.appendFile(this._state!.traceFile, JSON.stringify(visited) + '\n', flush);
if (this._inMemoryEvents)
this._inMemoryEvents.push(event);
} }
private _appendResource(sha1: string, buffer: Buffer) { private _appendResource(sha1: string, buffer: Buffer) {

View file

@ -178,6 +178,7 @@ export async function openTraceViewerApp(url: string, browserName: string, optio
...options?.persistentContextOptions, ...options?.persistentContextOptions,
useWebSocket: isUnderTest(), useWebSocket: isUnderTest(),
headless: !!options?.headless, headless: !!options?.headless,
args: process.env.PWTEST_RECORDER_PORT ? [`--remote-debugging-port=${process.env.PWTEST_RECORDER_PORT}`] : [],
}, },
}); });

View file

@ -259,11 +259,11 @@ export class TestTypeImpl {
suite._use.push({ fixtures, location }); suite._use.push({ fixtures, location });
} }
async _step<T>(title: string, body: () => Promise<T>, options: { box?: boolean } = {}): Promise<T> { async _step<T>(title: string, body: () => Promise<T>, options: {box?: boolean, location?: Location } = {}): Promise<T> {
const testInfo = currentTestInfo(); const testInfo = currentTestInfo();
if (!testInfo) if (!testInfo)
throw new Error(`test.step() can only be called from a test`); 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 () => { return await zones.run('stepZone', step, async () => {
try { try {
const result = await body(); const result = await body();

View file

@ -143,7 +143,7 @@ class HtmlReporter implements ReporterV2 {
const shouldOpen = !this._options._isTestServer && (this._open === 'always' || (!ok && this._open === 'on-failure')); const shouldOpen = !this._options._isTestServer && (this._open === 'always' || (!ok && this._open === 'on-failure'));
if (shouldOpen) { if (shouldOpen) {
await showHTMLReport(this._outputFolder, this._host, this._port, singleTestId); 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 packageManagerCommand = getPackageManagerExecCommand();
const relativeReportPath = this._outputFolder === standaloneDefaultFolder() ? '' : ' ' + path.relative(process.cwd(), this._outputFolder); const relativeReportPath = this._outputFolder === standaloneDefaultFolder() ? '' : ' ' + path.relative(process.cwd(), this._outputFolder);
const hostArg = this._host ? ` --host ${this._host}` : ''; const hostArg = this._host ? ` --host ${this._host}` : '';

View file

@ -4703,7 +4703,7 @@ export interface TestType<TestArgs extends KeyValue, WorkerArgs extends KeyValue
* @param body Step body. * @param body Step body.
* @param options * @param options
*/ */
step<T>(title: string, body: () => T | Promise<T>, options?: { box?: boolean }): Promise<T>; step<T>(title: string, body: () => T | Promise<T>, options?: { box?: boolean, location?: Location }): Promise<T>;
/** /**
* `expect` function can be used to create test assertions. Read more about [test assertions](https://playwright.dev/docs/test-assertions). * `expect` function can be used to create test assertions. Read more about [test assertions](https://playwright.dev/docs/test-assertions).
* *

View file

@ -73,6 +73,7 @@ export class TraceModel {
unzipProgress(++done, total); unzipProgress(++done, total);
contextEntry.actions = modernizer.actions().sort((a1, a2) => a1.startTime - a2.startTime); contextEntry.actions = modernizer.actions().sort((a1, a2) => a1.startTime - a2.startTime);
if (!backend.isLive()) { if (!backend.isLive()) {
// Terminate actions w/o after event gracefully. // Terminate actions w/o after event gracefully.
// This would close after hooks event that has not been closed because // This would close after hooks event that has not been closed because

View file

@ -312,7 +312,7 @@ function monotonicTimeDeltaBetweenLibraryAndRunner(nonPrimaryContexts: ContextEn
for (const action of context.actions) { for (const action of context.actions) {
if (!action.startTime) if (!action.startTime)
continue; 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); const libraryAction = libraryActions.get(key);
if (libraryAction) if (libraryAction)
return action.startTime - libraryAction.startTime; return action.startTime - libraryAction.startTime;

View file

@ -45,6 +45,8 @@ export const RecorderView: React.FunctionComponent = () => {
connection.setMode('recording'); connection.setMode('recording');
}, [connection]); }, [connection]);
window.playwrightSourcesEchoForTest = sources;
return <div className='vbox workbench-loader'> return <div className='vbox workbench-loader'>
<TraceView <TraceView
traceLocation={trace} traceLocation={trace}

Binary file not shown.

View file

@ -610,6 +610,7 @@ it('should have security details', async ({ contextFactory, httpsServer, browser
const { page, getLog } = await pageWithHar(contextFactory, testInfo); const { page, getLog } = await pageWithHar(contextFactory, testInfo);
await page.goto(httpsServer.EMPTY_PAGE); await page.goto(httpsServer.EMPTY_PAGE);
await page.request.get(httpsServer.EMPTY_PAGE);
const log = await getLog(); const log = await getLog();
const { serverIPAddress, _serverPort: port, _securityDetails: securityDetails } = log.entries[0]; const { serverIPAddress, _serverPort: port, _securityDetails: securityDetails } = log.entries[0];
if (!mode.startsWith('service')) { if (!mode.startsWith('service')) {
@ -620,6 +621,8 @@ it('should have security details', async ({ contextFactory, httpsServer, browser
expect(securityDetails).toEqual({ protocol: 'TLS 1.3', subjectName: 'playwright-test', validFrom: 1691708270, validTo: 2007068270 }); expect(securityDetails).toEqual({ protocol: 'TLS 1.3', subjectName: 'playwright-test', validFrom: 1691708270, validTo: 2007068270 });
else else
expect(securityDetails).toEqual({ issuer: 'playwright-test', protocol: 'TLS 1.3', subjectName: 'playwright-test', validFrom: 1691708270, validTo: 2007068270 }); expect(securityDetails).toEqual({ issuer: 'playwright-test', protocol: 'TLS 1.3', subjectName: 'playwright-test', validFrom: 1691708270, validTo: 2007068270 });
expect(log.entries[1]._securityDetails).toEqual({ issuer: 'playwright-test', protocol: 'TLSv1.3', subjectName: 'playwright-test', validFrom: 1691708270, validTo: 2007068270 });
}); });
it('should have connection details for redirects', async ({ contextFactory, server, browserName, mode }, testInfo) => { it('should have connection details for redirects', async ({ contextFactory, server, browserName, mode }, testInfo) => {
@ -845,6 +848,8 @@ it('should respect minimal mode for API Requests', async ({ contextFactory, serv
expect(entries).toHaveLength(1); expect(entries).toHaveLength(1);
const [entry] = entries; const [entry] = entries;
expect(entry.timings).toEqual({ receive: -1, send: -1, wait: -1 }); expect(entry.timings).toEqual({ receive: -1, send: -1, wait: -1 });
expect(entry.request.cookies).toEqual([]);
expect(entry.request.bodySize).toBe(-1);
expect(entry.response.bodySize).toBe(-1); expect(entry.response.bodySize).toBe(-1);
}); });

View file

@ -55,7 +55,7 @@ export const test = contextTest.extend<CLITestArgs>({
await run(async () => { await run(async () => {
while (!toImpl(context).recorderAppForTest) while (!toImpl(context).recorderAppForTest)
await new Promise(f => setTimeout(f, 100)); await new Promise(f => setTimeout(f, 100));
const wsEndpoint = toImpl(context).recorderAppForTest.wsEndpoint; const wsEndpoint = toImpl(context).recorderAppForTest.wsEndpointForTest;
const browser = await playwrightToAutomateInspector.chromium.connectOverCDP({ wsEndpoint }); const browser = await playwrightToAutomateInspector.chromium.connectOverCDP({ wsEndpoint });
const c = browser.contexts()[0]; const c = browser.contexts()[0];
return c.pages()[0] || await c.waitForEvent('page'); return c.pages()[0] || await c.waitForEvent('page');

View file

@ -127,6 +127,12 @@ test('should complain about newer version of trace in old viewer', async ({ show
await expect(traceViewer.page.getByText('The trace was created by a newer version of Playwright and is not supported by this version of the viewer.')).toBeVisible(); await expect(traceViewer.page.getByText('The trace was created by a newer version of Playwright and is not supported by this version of the viewer.')).toBeVisible();
}); });
test('should properly synchronize local and remote time', async ({ showTraceViewer, asset }, testInfo) => {
const traceViewer = await showTraceViewer([asset('trace-remote-time-diff.zip')]);
// The total duration should be sub 10s, rather than 16h.
await expect(traceViewer.page.locator('.timeline-time').last()).toHaveText('8.5s');
});
test('should contain action info', async ({ showTraceViewer }) => { test('should contain action info', async ({ showTraceViewer }) => {
const traceViewer = await showTraceViewer([traceFile]); const traceViewer = await showTraceViewer([traceFile]);
await traceViewer.selectAction('locator.click'); await traceViewer.selectAction('locator.click');

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 874 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -1246,3 +1246,42 @@ fixture | fixture: page
fixture | fixture: context fixture | fixture: context
`); `);
}); });
test('test location to test.step', async ({ runInlineTest }) => {
const result = await runInlineTest({
'reporter.ts': stepIndentReporter,
'helper.ts': `
import { test } from '@playwright/test';
export async function dummyStep(test, title, action, location) {
return await test.step(title, action, { location });
}
export function getCustomLocation() {
return { file: 'dummy-file.ts', line: 123, column: 45 };
}
`,
'playwright.config.ts': `
module.exports = {
reporter: './reporter',
};
`,
'a.test.ts': `
import { test } from '@playwright/test';
import { dummyStep, getCustomLocation } from './helper';
test('custom location test', async () => {
const location = getCustomLocation();
await dummyStep(test, 'Perform a dummy step', async () => {
}, location);
});
`
}, { reporter: '', workers: 1 });
expect(result.exitCode).toBe(0);
expect(stripAnsi(result.output)).toBe(`
hook |Before Hooks
test.step |Perform a dummy step @ dummy-file.ts:123
hook |After Hooks
`);
});

View file

@ -215,6 +215,24 @@ test('should run tests on Enter', async ({ runWatchTest }) => {
await testProcess.waitForOutput('Waiting for file changes.'); await testProcess.waitForOutput('Waiting for file changes.');
}); });
test('should not print show-report command of HTML reporter', async ({ runWatchTest }) => {
const testProcess = await runWatchTest({
'a.test.ts': `
import { test, expect } from '@playwright/test';
test('passes', () => {});
`,
'playwright.config.ts': `
import { defineConfig } from '@playwright/test';
export default defineConfig({ reporter: 'html' });
`,
});
await testProcess.waitForOutput('Waiting for file changes.');
testProcess.clearOutput();
testProcess.write('\r\n');
await testProcess.waitForOutput('Waiting for file changes.');
expect(testProcess.output).not.toContain('To open last HTML report run:');
});
test('should run tests on R', async ({ runWatchTest }) => { test('should run tests on R', async ({ runWatchTest }) => {
const testProcess = await runWatchTest({ const testProcess = await runWatchTest({
'a.test.ts': ` 'a.test.ts': `

View file

@ -128,7 +128,7 @@ export interface TestType<TestArgs extends KeyValue, WorkerArgs extends KeyValue
afterAll(inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise<any> | any): void; afterAll(inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise<any> | any): void;
afterAll(title: string, inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise<any> | any): void; afterAll(title: string, inner: (args: TestArgs & WorkerArgs, testInfo: TestInfo) => Promise<any> | any): void;
use(fixtures: Fixtures<{}, {}, TestArgs, WorkerArgs>): void; use(fixtures: Fixtures<{}, {}, TestArgs, WorkerArgs>): void;
step<T>(title: string, body: () => T | Promise<T>, options?: { box?: boolean }): Promise<T>; step<T>(title: string, body: () => T | Promise<T>, options?: { box?: boolean, location?: Location }): Promise<T>;
expect: Expect<{}>; expect: Expect<{}>;
extend<T extends KeyValue, W extends KeyValue = {}>(fixtures: Fixtures<T, W, TestArgs, WorkerArgs>): TestType<TestArgs & T, WorkerArgs & W>; extend<T extends KeyValue, W extends KeyValue = {}>(fixtures: Fixtures<T, W, TestArgs, WorkerArgs>): TestType<TestArgs & T, WorkerArgs & W>;
info(): TestInfo; info(): TestInfo;