Merge branch 'main' into relative/user-data-dir
Signed-off-by: Max Schmitt <max@schmitt.mx>
This commit is contained in:
commit
f44451e2b8
315
package-lock.json
generated
315
package-lock.json
generated
|
|
@ -50,7 +50,7 @@
|
|||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^16.4.5",
|
||||
"electron": "^30.1.2",
|
||||
"esbuild": "^0.18.11",
|
||||
"esbuild": "^0.25.0",
|
||||
"eslint": "^9.19.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-notice": "^1.0.0",
|
||||
|
|
@ -885,355 +885,411 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
|
||||
"integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz",
|
||||
"integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
|
||||
"integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
|
||||
"integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
|
||||
"integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
|
||||
"integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz",
|
||||
"integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
|
||||
"integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
|
||||
"integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz",
|
||||
"integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
|
||||
"integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz",
|
||||
"integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
|
||||
"integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz",
|
||||
"integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
|
||||
"integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz",
|
||||
"integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
|
||||
"integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz",
|
||||
"integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
|
||||
"integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz",
|
||||
"integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
|
||||
"integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz",
|
||||
"integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
|
||||
"integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz",
|
||||
"integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
|
||||
"integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz",
|
||||
"integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
|
|
@ -3857,40 +3913,61 @@
|
|||
"optional": true
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
|
||||
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz",
|
||||
"integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/android-arm": "0.18.20",
|
||||
"@esbuild/android-arm64": "0.18.20",
|
||||
"@esbuild/android-x64": "0.18.20",
|
||||
"@esbuild/darwin-arm64": "0.18.20",
|
||||
"@esbuild/darwin-x64": "0.18.20",
|
||||
"@esbuild/freebsd-arm64": "0.18.20",
|
||||
"@esbuild/freebsd-x64": "0.18.20",
|
||||
"@esbuild/linux-arm": "0.18.20",
|
||||
"@esbuild/linux-arm64": "0.18.20",
|
||||
"@esbuild/linux-ia32": "0.18.20",
|
||||
"@esbuild/linux-loong64": "0.18.20",
|
||||
"@esbuild/linux-mips64el": "0.18.20",
|
||||
"@esbuild/linux-ppc64": "0.18.20",
|
||||
"@esbuild/linux-riscv64": "0.18.20",
|
||||
"@esbuild/linux-s390x": "0.18.20",
|
||||
"@esbuild/linux-x64": "0.18.20",
|
||||
"@esbuild/netbsd-x64": "0.18.20",
|
||||
"@esbuild/openbsd-x64": "0.18.20",
|
||||
"@esbuild/sunos-x64": "0.18.20",
|
||||
"@esbuild/win32-arm64": "0.18.20",
|
||||
"@esbuild/win32-ia32": "0.18.20",
|
||||
"@esbuild/win32-x64": "0.18.20"
|
||||
"@esbuild/aix-ppc64": "0.25.0",
|
||||
"@esbuild/android-arm": "0.25.0",
|
||||
"@esbuild/android-arm64": "0.25.0",
|
||||
"@esbuild/android-x64": "0.25.0",
|
||||
"@esbuild/darwin-arm64": "0.25.0",
|
||||
"@esbuild/darwin-x64": "0.25.0",
|
||||
"@esbuild/freebsd-arm64": "0.25.0",
|
||||
"@esbuild/freebsd-x64": "0.25.0",
|
||||
"@esbuild/linux-arm": "0.25.0",
|
||||
"@esbuild/linux-arm64": "0.25.0",
|
||||
"@esbuild/linux-ia32": "0.25.0",
|
||||
"@esbuild/linux-loong64": "0.25.0",
|
||||
"@esbuild/linux-mips64el": "0.25.0",
|
||||
"@esbuild/linux-ppc64": "0.25.0",
|
||||
"@esbuild/linux-riscv64": "0.25.0",
|
||||
"@esbuild/linux-s390x": "0.25.0",
|
||||
"@esbuild/linux-x64": "0.25.0",
|
||||
"@esbuild/netbsd-arm64": "0.25.0",
|
||||
"@esbuild/netbsd-x64": "0.25.0",
|
||||
"@esbuild/openbsd-arm64": "0.25.0",
|
||||
"@esbuild/openbsd-x64": "0.25.0",
|
||||
"@esbuild/sunos-x64": "0.25.0",
|
||||
"@esbuild/win32-arm64": "0.25.0",
|
||||
"@esbuild/win32-ia32": "0.25.0",
|
||||
"@esbuild/win32-x64": "0.25.0"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild/node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz",
|
||||
"integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@
|
|||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^16.4.5",
|
||||
"electron": "^30.1.2",
|
||||
"esbuild": "^0.18.11",
|
||||
"esbuild": "^0.25.0",
|
||||
"eslint": "^9.19.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-notice": "^1.0.0",
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export const CopyToClipboard: React.FunctionComponent<CopyToClipboardProps> = ({
|
|||
});
|
||||
}, [value]);
|
||||
const iconElement = icon === 'check' ? icons.check() : icon === 'cross' ? icons.cross() : icons.copy();
|
||||
return <button className='copy-icon' aria-label='Copy to clipboard' onClick={handleCopy}>{iconElement}</button>;
|
||||
return <button className='copy-icon' title='Copy to clipboard' aria-label='Copy to clipboard' onClick={handleCopy}>{iconElement}</button>;
|
||||
};
|
||||
|
||||
type CopyToClipboardContainerProps = CopyToClipboardProps & {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
cursor: pointer;
|
||||
user-select: none;
|
||||
margin-left: 5px;
|
||||
color: var(--color-fg-default);
|
||||
}
|
||||
|
||||
.metadata-view {
|
||||
|
|
@ -26,16 +27,46 @@
|
|||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.metadata-view .metadata-section {
|
||||
margin: 8px 10px 8px 32px;
|
||||
}
|
||||
|
||||
.metadata-view span:not(.copy-button-container),
|
||||
.metadata-view a {
|
||||
display: inline-block;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.metadata-section {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.metadata-properties {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: normal;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.metadata-properties > div {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.metadata-separator {
|
||||
height: 1px;
|
||||
border-bottom: 1px solid var(--color-border-default);
|
||||
}
|
||||
|
||||
.metadata-view .copy-value-container {
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
.git-commit-info a {
|
||||
color: var(--color-fg-default);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.copyable-property {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.copyable-property > span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,9 +26,24 @@ import { linkifyText } from '@web/renderUtils';
|
|||
|
||||
type MetadataEntries = [string, unknown][];
|
||||
|
||||
export function filterMetadata(metadata: Metadata): MetadataEntries {
|
||||
// TODO: do not plumb actualWorkers through metadata.
|
||||
return Object.entries(metadata).filter(([key]) => key !== 'actualWorkers');
|
||||
export const MetadataContext = React.createContext<MetadataEntries>([]);
|
||||
|
||||
export function MetadataProvider({ metadata, children }: React.PropsWithChildren<{ metadata: Metadata }>) {
|
||||
const entries = React.useMemo(() => {
|
||||
// TODO: do not plumb actualWorkers through metadata.
|
||||
return Object.entries(metadata).filter(([key]) => key !== 'actualWorkers');
|
||||
}, [metadata]);
|
||||
|
||||
return <MetadataContext.Provider value={entries}>{children}</MetadataContext.Provider>;
|
||||
}
|
||||
|
||||
export function useMetadata() {
|
||||
return React.useContext(MetadataContext);
|
||||
}
|
||||
|
||||
export function useGitCommitInfo() {
|
||||
const metadataEntries = useMetadata();
|
||||
return metadataEntries.find(([key]) => key === 'git.commit.info')?.[1] as GitCommitInfo | undefined;
|
||||
}
|
||||
|
||||
class ErrorBoundary extends React.Component<React.PropsWithChildren<{}>, { error: Error | null, errorInfo: React.ErrorInfo | null }> {
|
||||
|
|
@ -57,12 +72,13 @@ class ErrorBoundary extends React.Component<React.PropsWithChildren<{}>, { error
|
|||
}
|
||||
}
|
||||
|
||||
export const MetadataView: React.FC<{ metadataEntries: MetadataEntries }> = ({ metadataEntries }) => {
|
||||
return <ErrorBoundary><InnerMetadataView metadataEntries={metadataEntries}/></ErrorBoundary>;
|
||||
export const MetadataView = () => {
|
||||
return <ErrorBoundary><InnerMetadataView/></ErrorBoundary>;
|
||||
};
|
||||
|
||||
const InnerMetadataView: React.FC<{ metadataEntries: MetadataEntries }> = ({ metadataEntries }) => {
|
||||
const gitCommitInfo = metadataEntries.find(([key]) => key === 'git.commit.info')?.[1] as GitCommitInfo | undefined;
|
||||
const InnerMetadataView = () => {
|
||||
const metadataEntries = useMetadata();
|
||||
const gitCommitInfo = useGitCommitInfo();
|
||||
const entries = metadataEntries.filter(([key]) => key !== 'git.commit.info');
|
||||
if (!gitCommitInfo && !entries.length)
|
||||
return null;
|
||||
|
|
@ -71,30 +87,43 @@ const InnerMetadataView: React.FC<{ metadataEntries: MetadataEntries }> = ({ met
|
|||
<GitCommitInfoView info={gitCommitInfo}/>
|
||||
{entries.length > 0 && <div className='metadata-separator' />}
|
||||
</>}
|
||||
{entries.map(([key, value]) => {
|
||||
const valueString = typeof value !== 'object' || value === null || value === undefined ? String(value) : JSON.stringify(value);
|
||||
const trimmedValue = valueString.length > 1000 ? valueString.slice(0, 1000) + '\u2026' : valueString;
|
||||
return <div className='m-1 ml-5' key={key}>
|
||||
<span style={{ fontWeight: 'bold' }} title={key}>{key}</span>
|
||||
{valueString && <CopyToClipboardContainer value={valueString}>: <span title={trimmedValue}>{linkifyText(trimmedValue)}</span></CopyToClipboardContainer>}
|
||||
</div>;
|
||||
})}
|
||||
<div className='metadata-section metadata-properties'>
|
||||
{entries.map(([propertyName, value]) => {
|
||||
const valueString = typeof value !== 'object' || value === null || value === undefined ? String(value) : JSON.stringify(value);
|
||||
const trimmedValue = valueString.length > 1000 ? valueString.slice(0, 1000) + '\u2026' : valueString;
|
||||
return (
|
||||
<div key={propertyName} className='copyable-property'>
|
||||
<CopyToClipboardContainer value={valueString}>
|
||||
<span style={{ fontWeight: 'bold' }} title={propertyName}>{propertyName}</span>
|
||||
: <span title={trimmedValue}>{linkifyText(trimmedValue)}</span>
|
||||
</CopyToClipboardContainer>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
||||
const GitCommitInfoView: React.FC<{ info: GitCommitInfo }> = ({ info }) => {
|
||||
const email = info['revision.email'] ? ` <${info['revision.email']}>` : '';
|
||||
const author = `${info['revision.author'] || ''}${email}`;
|
||||
const subject = info['revision.subject'] || '';
|
||||
const shortTimestamp = Intl.DateTimeFormat(undefined, { dateStyle: 'medium' }).format(info['revision.timestamp']);
|
||||
const longTimestamp = Intl.DateTimeFormat(undefined, { dateStyle: 'full', timeStyle: 'long' }).format(info['revision.timestamp']);
|
||||
return <div className='hbox pl-4 pr-2 git-commit-info' style={{ alignItems: 'center' }}>
|
||||
<div className='vbox'>
|
||||
<a className='m-2' href={info['revision.link']} target='_blank' rel='noopener noreferrer'>
|
||||
<span title={info['revision.subject'] || ''}>{info['revision.subject'] || ''}</span>
|
||||
</a>
|
||||
<div className='hbox m-2 mt-1'>
|
||||
<div className='mr-1'>{author}</div>
|
||||
<div title={longTimestamp}> on {shortTimestamp}</div>
|
||||
return <div className='hbox git-commit-info metadata-section'>
|
||||
<div className='vbox metadata-properties'>
|
||||
<div>
|
||||
{info['revision.link'] ? (
|
||||
<a href={info['revision.link']} target='_blank' rel='noopener noreferrer' title={subject}>
|
||||
{subject}
|
||||
</a>
|
||||
) : <span title={subject}>
|
||||
{subject}
|
||||
</span>}
|
||||
</div>
|
||||
<div className='hbox'>
|
||||
<span className='mr-1'>{author}</span>
|
||||
<span title={longTimestamp}> on {shortTimestamp}</span>
|
||||
{info['ci.link'] && (
|
||||
<>
|
||||
<span className='mx-2'>·</span>
|
||||
|
|
@ -109,9 +138,10 @@ const GitCommitInfoView: React.FC<{ info: GitCommitInfo }> = ({ info }) => {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!!info['revision.link'] && <a href={info['revision.link']} target='_blank' rel='noopener noreferrer'>
|
||||
<span title='View commit details'>{info['revision.id']?.slice(0, 7) || 'unknown'}</span>
|
||||
</a>}
|
||||
{!info['revision.link'] && !!info['revision.id'] && <span>{info['revision.id'].slice(0, 7)}</span>}
|
||||
{!!info['revision.link'] ? (
|
||||
<a href={info['revision.link']} target='_blank' rel='noopener noreferrer' title='View commit details'>
|
||||
{info['revision.id']?.slice(0, 7) || 'unknown'}
|
||||
</a>
|
||||
) : !!info['revision.id'] && <span>{info['revision.id'].slice(0, 7)}</span>}
|
||||
</div>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import './reportView.css';
|
|||
import { TestCaseView } from './testCaseView';
|
||||
import { TestFilesHeader, TestFilesView } from './testFilesView';
|
||||
import './theme.css';
|
||||
import { MetadataProvider } from './metadataView';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
|
@ -72,7 +73,7 @@ export const ReportView: React.FC<{
|
|||
return result;
|
||||
}, [report, filter]);
|
||||
|
||||
return <div className='htmlreport vbox px-4 pb-4'>
|
||||
return <MetadataProvider metadata={report?.json().metadata ?? {}}><div className='htmlreport vbox px-4 pb-4'>
|
||||
<main>
|
||||
{report?.json() && <HeaderView stats={report.json().stats} filterText={filterText} setFilterText={setFilterText}></HeaderView>}
|
||||
<Route predicate={testFilesRoutePredicate}>
|
||||
|
|
@ -88,7 +89,7 @@ export const ReportView: React.FC<{
|
|||
{!!report && <TestCaseViewLoader report={report} tests={filteredTests.tests} testIdToFileIdMap={testIdToFileIdMap} />}
|
||||
</Route>
|
||||
</main>
|
||||
</div>;
|
||||
</div></MetadataProvider>;
|
||||
};
|
||||
|
||||
const TestCaseViewLoader: React.FC<{
|
||||
|
|
|
|||
|
|
@ -16,18 +16,47 @@
|
|||
|
||||
@import '@web/third_party/vscode/colors.css';
|
||||
|
||||
.test-error-view {
|
||||
.test-error-container {
|
||||
white-space: pre;
|
||||
overflow: auto;
|
||||
flex: none;
|
||||
padding: 0;
|
||||
background-color: var(--color-canvas-subtle);
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
line-height: initial;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.test-error-view {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.test-error-text {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.prompt-button {
|
||||
flex: none;
|
||||
height: 24px;
|
||||
width: 80px;
|
||||
border: 1px solid var(--color-btn-border);
|
||||
outline: none;
|
||||
color: var(--color-btn-text);
|
||||
background: var(--color-btn-bg);
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.prompt-button svg {
|
||||
color: var(--color-fg-subtle);
|
||||
}
|
||||
|
||||
.prompt-button:not(:disabled):hover {
|
||||
border-color: var(--color-btn-hover-border);
|
||||
background-color: var(--color-btn-hover-bg);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,15 +17,57 @@
|
|||
import { ansi2html } from '@web/ansi2html';
|
||||
import * as React from 'react';
|
||||
import './testErrorView.css';
|
||||
import * as icons from './icons';
|
||||
import type { ImageDiff } from '@web/shared/imageDiffView';
|
||||
import { ImageDiffView } from '@web/shared/imageDiffView';
|
||||
import type { TestResult } from './types';
|
||||
import { fixTestPrompt } from '@web/components/prompts';
|
||||
import { useGitCommitInfo } from './metadataView';
|
||||
|
||||
export const TestErrorView: React.FC<{
|
||||
export const TestErrorView: React.FC<{ error: string; testId?: string; result?: TestResult }> = ({ error, testId, result }) => {
|
||||
return (
|
||||
<CodeSnippet code={error} testId={testId}>
|
||||
<div style={{ float: 'right', padding: '5px' }}>
|
||||
<PromptButton error={error} result={result} />
|
||||
</div>
|
||||
</CodeSnippet>
|
||||
);
|
||||
};
|
||||
|
||||
export const CodeSnippet = ({ code, children, testId }: React.PropsWithChildren<{ code: string; testId?: string; }>) => {
|
||||
const html = React.useMemo(() => ansiErrorToHtml(code), [code]);
|
||||
return (
|
||||
<div className='test-error-container test-error-text' data-testid={testId}>
|
||||
{children}
|
||||
<div className='test-error-view' dangerouslySetInnerHTML={{ __html: html || '' }}></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PromptButton: React.FC<{
|
||||
error: string;
|
||||
testId?: string;
|
||||
}> = ({ error, testId }) => {
|
||||
const html = React.useMemo(() => ansiErrorToHtml(error), [error]);
|
||||
return <div className='test-error-view test-error-text' data-testid={testId} dangerouslySetInnerHTML={{ __html: html || '' }}></div>;
|
||||
result?: TestResult;
|
||||
}> = ({ error, result }) => {
|
||||
const gitCommitInfo = useGitCommitInfo();
|
||||
const prompt = React.useMemo(() => fixTestPrompt(
|
||||
error,
|
||||
gitCommitInfo?.['pull.diff'] ?? gitCommitInfo?.['revision.diff'],
|
||||
result?.attachments.find(a => a.name === 'pageSnapshot')?.body
|
||||
), [gitCommitInfo, result, error]);
|
||||
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
|
||||
return <button
|
||||
className='prompt-button'
|
||||
onClick={async () => {
|
||||
await navigator.clipboard.writeText(prompt);
|
||||
setCopied(true);
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 3000);
|
||||
}}>
|
||||
{copied ? <span className='prompt-button-copied'>Copied <icons.copy/></span> : 'Fix with AI'}
|
||||
</button>;
|
||||
};
|
||||
|
||||
export const TestScreenshotErrorView: React.FC<{
|
||||
|
|
|
|||
|
|
@ -69,4 +69,11 @@
|
|||
|
||||
.test-file-test-status-icon {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.test-file-header-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--color-fg-subtle);
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@ import { msToString } from './utils';
|
|||
import { AutoChip } from './chip';
|
||||
import { TestErrorView } from './testErrorView';
|
||||
import * as icons from './icons';
|
||||
import { filterMetadata, MetadataView } from './metadataView';
|
||||
import { MetadataView, useMetadata } from './metadataView';
|
||||
|
||||
export const TestFilesView: React.FC<{
|
||||
tests: TestFileSummary[],
|
||||
|
|
@ -67,21 +67,23 @@ export const TestFilesHeader: React.FC<{
|
|||
metadataVisible: boolean,
|
||||
toggleMetadataVisible: () => void,
|
||||
}> = ({ report, filteredStats, metadataVisible, toggleMetadataVisible }) => {
|
||||
const metadataEntries = useMetadata();
|
||||
if (!report)
|
||||
return;
|
||||
const metadataEntries = filterMetadata(report.metadata || {});
|
||||
return null;
|
||||
return <>
|
||||
<div className='mx-1' style={{ display: 'flex', marginTop: 10 }}>
|
||||
{metadataEntries.length > 0 && <div className='metadata-toggle' role='button' onClick={toggleMetadataVisible} title={metadataVisible ? 'Hide metadata' : 'Show metadata'}>
|
||||
{metadataVisible ? icons.downArrow() : icons.rightArrow()}Metadata
|
||||
</div>}
|
||||
{report.projectNames.length === 1 && !!report.projectNames[0] && <div data-testid='project-name' style={{ color: 'var(--color-fg-subtle)' }}>Project: {report.projectNames[0]}</div>}
|
||||
{filteredStats && <div data-testid='filtered-tests-count' style={{ color: 'var(--color-fg-subtle)', padding: '0 10px' }}>Filtered: {filteredStats.total} {!!filteredStats.total && ('(' + msToString(filteredStats.duration) + ')')}</div>}
|
||||
<div className='test-file-header-info'>
|
||||
{metadataEntries.length > 0 && <div className='metadata-toggle' role='button' onClick={toggleMetadataVisible} title={metadataVisible ? 'Hide metadata' : 'Show metadata'}>
|
||||
{metadataVisible ? icons.downArrow() : icons.rightArrow()}Metadata
|
||||
</div>}
|
||||
{report.projectNames.length === 1 && !!report.projectNames[0] && <div data-testid='project-name'>Project: {report.projectNames[0]}</div>}
|
||||
{filteredStats && <div data-testid='filtered-tests-count'>Filtered: {filteredStats.total} {!!filteredStats.total && ('(' + msToString(filteredStats.duration) + ')')}</div>}
|
||||
</div>
|
||||
<div style={{ flex: 'auto' }}></div>
|
||||
<div data-testid='overall-time' style={{ color: 'var(--color-fg-subtle)', marginRight: '10px' }}>{report ? new Date(report.startTime).toLocaleString() : ''}</div>
|
||||
<div data-testid='overall-duration' style={{ color: 'var(--color-fg-subtle)' }}>Total time: {msToString(report.duration ?? 0)}</div>
|
||||
</div>
|
||||
{metadataVisible && <MetadataView metadataEntries={metadataEntries}/>}
|
||||
{metadataVisible && <MetadataView/>}
|
||||
{!!report.errors.length && <AutoChip header='Errors' dataTestId='report-errors'>
|
||||
{report.errors.map((error, index) => <TestErrorView key={'test-report-error-message-' + index} error={error}></TestErrorView>)}
|
||||
</AutoChip>}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import { Anchor, AttachmentLink, generateTraceUrl, testResultHref } from './link
|
|||
import { statusIcon } from './statusIcon';
|
||||
import type { ImageDiff } from '@web/shared/imageDiffView';
|
||||
import { ImageDiffView } from '@web/shared/imageDiffView';
|
||||
import { TestErrorView, TestScreenshotErrorView } from './testErrorView';
|
||||
import { CodeSnippet, TestErrorView, TestScreenshotErrorView } from './testErrorView';
|
||||
import * as icons from './icons';
|
||||
import './testResultView.css';
|
||||
|
||||
|
|
@ -90,7 +90,7 @@ export const TestResultView: React.FC<{
|
|||
{errors.map((error, index) => {
|
||||
if (error.type === 'screenshot')
|
||||
return <TestScreenshotErrorView key={'test-result-error-message-' + index} errorPrefix={error.errorPrefix} diff={error.diff!} errorSuffix={error.errorSuffix}></TestScreenshotErrorView>;
|
||||
return <TestErrorView key={'test-result-error-message-' + index} error={error.error!}></TestErrorView>;
|
||||
return <TestErrorView key={'test-result-error-message-' + index} error={error.error!} result={result}></TestErrorView>;
|
||||
})}
|
||||
</AutoChip>}
|
||||
{!!result.steps.length && <AutoChip header='Test Steps'>
|
||||
|
|
@ -182,7 +182,7 @@ const StepTreeItem: React.FC<{
|
|||
{step.count > 1 && <> ✕ <span className='test-result-counter'>{step.count}</span></>}
|
||||
{step.location && <span className='test-result-path'>— {step.location.file}:{step.location.line}</span>}
|
||||
</span>} loadChildren={step.steps.length || step.snippet ? () => {
|
||||
const snippet = step.snippet ? [<TestErrorView testId='test-snippet' key='line' error={step.snippet}/>] : [];
|
||||
const snippet = step.snippet ? [<CodeSnippet testId='test-snippet' key='line' code={step.snippet} />] : [];
|
||||
const steps = step.steps.map((s, i) => <StepTreeItem key={i} step={s} depth={depth + 1} result={result} test={test} />);
|
||||
return snippet.concat(steps);
|
||||
} : undefined} depth={depth}/>;
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@
|
|||
},
|
||||
{
|
||||
"name": "webkit",
|
||||
"revision": "2130",
|
||||
"revision": "2132",
|
||||
"installByDefault": true,
|
||||
"revisionOverrides": {
|
||||
"debian11-x64": "2105",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,11 @@
|
|||
[inProcessFactory.ts]
|
||||
**
|
||||
|
||||
[inprocess.ts]
|
||||
utils/
|
||||
|
||||
[outofprocess.ts]
|
||||
client/
|
||||
protocol/
|
||||
utils/
|
||||
common/
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
../common
|
||||
../debug/injected
|
||||
../generated/
|
||||
../server/
|
||||
../server/injected/
|
||||
../server/trace
|
||||
../utils
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import * as playwright from '../..';
|
|||
import { PipeTransport } from '../protocol/transport';
|
||||
import { PlaywrightServer } from '../remote/playwrightServer';
|
||||
import { DispatcherConnection, PlaywrightDispatcher, RootDispatcher, createPlaywright } from '../server';
|
||||
import { gracefullyProcessExitDoNotHang } from '../utils/processLauncher';
|
||||
import { gracefullyProcessExitDoNotHang } from '../server/processLauncher';
|
||||
|
||||
import type { BrowserType } from '../client/browserType';
|
||||
import type { LaunchServerOptions } from '../client/types';
|
||||
|
|
|
|||
|
|
@ -21,11 +21,11 @@ import * as os from 'os';
|
|||
import * as path from 'path';
|
||||
|
||||
import * as playwright from '../..';
|
||||
import { registry, writeDockerVersion } from '../server';
|
||||
import { launchBrowserServer, printApiJson, runDriver, runServer } from './driver';
|
||||
import { isTargetClosedError } from '../client/errors';
|
||||
import { gracefullyProcessExitDoNotHang, registry, writeDockerVersion } from '../server';
|
||||
import { runTraceInBrowser, runTraceViewerApp } from '../server/trace/viewer/traceViewer';
|
||||
import { assert, getPackageManagerExecCommand, gracefullyProcessExitDoNotHang, isLikelyNpxGlobal, wrapInASCIIBox } from '../utils';
|
||||
import { assert, getPackageManagerExecCommand, isLikelyNpxGlobal, wrapInASCIIBox } from '../utils';
|
||||
import { dotenv, program } from '../utilsBundle';
|
||||
|
||||
import type { Browser } from '../client/browser';
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@
|
|||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import { getPackageManager, gracefullyProcessExitDoNotHang } from '../utils';
|
||||
import { gracefullyProcessExitDoNotHang } from '../server';
|
||||
import { getPackageManager } from '../utils';
|
||||
import { program } from './program';
|
||||
export { program } from './program';
|
||||
|
||||
|
|
|
|||
|
|
@ -15,22 +15,22 @@
|
|||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import * as fs from 'fs';
|
||||
|
||||
import { isRegExp, isString, monotonicTime } from '../utils';
|
||||
import { BrowserContext, prepareBrowserContextParams } from './browserContext';
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
import { Connection } from './connection';
|
||||
import { TargetClosedError, isTargetClosedError } from './errors';
|
||||
import { Events } from './events';
|
||||
import { Waiter } from './waiter';
|
||||
import { TimeoutSettings } from '../common/timeoutSettings';
|
||||
import { isRegExp, isString } from '../utils/rtti';
|
||||
import { monotonicTime } from '../utils/time';
|
||||
import { raceAgainstDeadline } from '../utils/timeoutRunner';
|
||||
|
||||
import type { Page } from './page';
|
||||
import type * as types from './types';
|
||||
import type * as api from '../../types/types';
|
||||
import type { AndroidServerLauncherImpl } from '../androidServerImpl';
|
||||
import type { Platform } from '../utils/platform';
|
||||
import type * as channels from '@protocol/channels';
|
||||
|
||||
type Direction = 'down' | 'up' | 'left' | 'right';
|
||||
|
|
@ -71,45 +71,28 @@ export class Android extends ChannelOwner<channels.AndroidChannel> implements ap
|
|||
const headers = { 'x-playwright-browser': 'android', ...options.headers };
|
||||
const localUtils = this._connection.localUtils();
|
||||
const connectParams: channels.LocalUtilsConnectParams = { wsEndpoint, headers, slowMo: options.slowMo, timeout: options.timeout };
|
||||
const { pipe } = await localUtils._channel.connect(connectParams);
|
||||
const closePipe = () => pipe.close().catch(() => {});
|
||||
const connection = new Connection(localUtils, this._instrumentation);
|
||||
connection.markAsRemote();
|
||||
connection.on('close', closePipe);
|
||||
const connection = await localUtils.connect(connectParams);
|
||||
|
||||
let device: AndroidDevice;
|
||||
let closeError: string | undefined;
|
||||
const onPipeClosed = () => {
|
||||
connection.on('close', () => {
|
||||
device?._didClose();
|
||||
connection.close(closeError);
|
||||
};
|
||||
pipe.on('closed', onPipeClosed);
|
||||
connection.onmessage = message => pipe.send({ message }).catch(onPipeClosed);
|
||||
|
||||
pipe.on('message', ({ message }) => {
|
||||
try {
|
||||
connection!.dispatch(message);
|
||||
} catch (e) {
|
||||
closeError = String(e);
|
||||
closePipe();
|
||||
}
|
||||
});
|
||||
|
||||
const result = await raceAgainstDeadline(async () => {
|
||||
const playwright = await connection!.initializePlaywright();
|
||||
if (!playwright._initializer.preConnectedAndroidDevice) {
|
||||
closePipe();
|
||||
connection.close();
|
||||
throw new Error('Malformed endpoint. Did you use Android.launchServer method?');
|
||||
}
|
||||
device = AndroidDevice.from(playwright._initializer.preConnectedAndroidDevice!);
|
||||
device._shouldCloseConnectionOnClose = true;
|
||||
device.on(Events.AndroidDevice.Close, closePipe);
|
||||
device.on(Events.AndroidDevice.Close, () => connection.close());
|
||||
return device;
|
||||
}, deadline);
|
||||
if (!result.timedOut) {
|
||||
return result.result;
|
||||
} else {
|
||||
closePipe();
|
||||
connection.close();
|
||||
throw new Error(`Timeout ${options.timeout}ms exceeded`);
|
||||
}
|
||||
});
|
||||
|
|
@ -232,7 +215,7 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel> i
|
|||
async screenshot(options: { path?: string } = {}): Promise<Buffer> {
|
||||
const { binary } = await this._channel.screenshot();
|
||||
if (options.path)
|
||||
await fs.promises.writeFile(options.path, binary);
|
||||
await this._platform.fs().promises.writeFile(options.path, binary);
|
||||
return binary;
|
||||
}
|
||||
|
||||
|
|
@ -267,15 +250,15 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel> i
|
|||
}
|
||||
|
||||
async installApk(file: string | Buffer, options?: { args: string[] }): Promise<void> {
|
||||
await this._channel.installApk({ file: await loadFile(file), args: options && options.args });
|
||||
await this._channel.installApk({ file: await loadFile(this._platform, file), args: options && options.args });
|
||||
}
|
||||
|
||||
async push(file: string | Buffer, path: string, options?: { mode: number }): Promise<void> {
|
||||
await this._channel.push({ file: await loadFile(file), path, mode: options ? options.mode : undefined });
|
||||
await this._channel.push({ file: await loadFile(this._platform, file), path, mode: options ? options.mode : undefined });
|
||||
}
|
||||
|
||||
async launchBrowser(options: types.BrowserContextOptions & { pkg?: string } = {}): Promise<BrowserContext> {
|
||||
const contextOptions = await prepareBrowserContextParams(options);
|
||||
const contextOptions = await prepareBrowserContextParams(this._platform, options);
|
||||
const result = await this._channel.launchBrowser(contextOptions);
|
||||
const context = BrowserContext.from(result.context) as BrowserContext;
|
||||
context._setOptions(contextOptions, {});
|
||||
|
|
@ -321,9 +304,9 @@ export class AndroidSocket extends ChannelOwner<channels.AndroidSocketChannel> i
|
|||
}
|
||||
}
|
||||
|
||||
async function loadFile(file: string | Buffer): Promise<Buffer> {
|
||||
async function loadFile(platform: Platform, file: string | Buffer): Promise<Buffer> {
|
||||
if (isString(file))
|
||||
return await fs.promises.readFile(file);
|
||||
return await platform.fs().promises.readFile(file);
|
||||
return file;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
import { Stream } from './stream';
|
||||
import { mkdirIfNeeded } from '../utils/fileUtils';
|
||||
|
|
@ -42,9 +40,9 @@ export class Artifact extends ChannelOwner<channels.ArtifactChannel> {
|
|||
|
||||
const result = await this._channel.saveAsStream();
|
||||
const stream = Stream.from(result.stream);
|
||||
await mkdirIfNeeded(path);
|
||||
await mkdirIfNeeded(this._platform, path);
|
||||
await new Promise((resolve, reject) => {
|
||||
stream.stream().pipe(fs.createWriteStream(path))
|
||||
stream.stream().pipe(this._platform.fs().createWriteStream(path))
|
||||
.on('finish' as any, resolve)
|
||||
.on('error' as any, reject);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,19 +14,17 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
|
||||
import { Artifact } from './artifact';
|
||||
import { BrowserContext, prepareBrowserContextParams } from './browserContext';
|
||||
import { CDPSession } from './cdpSession';
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
import { isTargetClosedError } from './errors';
|
||||
import { Events } from './events';
|
||||
import { mkdirIfNeeded } from '../utils';
|
||||
import { mkdirIfNeeded } from '../utils/fileUtils';
|
||||
|
||||
import type { BrowserType } from './browserType';
|
||||
import type { Page } from './page';
|
||||
import type { BrowserContextOptions, HeadersArray, LaunchOptions } from './types';
|
||||
import type { BrowserContextOptions, LaunchOptions } from './types';
|
||||
import type * as api from '../../types/types';
|
||||
import type * as channels from '@protocol/channels';
|
||||
|
||||
|
|
@ -39,9 +37,6 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
|
|||
_options: LaunchOptions = {};
|
||||
readonly _name: string;
|
||||
private _path: string | undefined;
|
||||
|
||||
// Used from @playwright/test fixtures.
|
||||
_connectHeaders?: HeadersArray;
|
||||
_closeReason: string | undefined;
|
||||
|
||||
static from(browser: channels.BrowserChannel): Browser {
|
||||
|
|
@ -83,7 +78,7 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
|
|||
|
||||
async _innerNewContext(options: BrowserContextOptions = {}, forReuse: boolean): Promise<BrowserContext> {
|
||||
options = { ...this._browserType._playwright._defaultContextOptions, ...options };
|
||||
const contextOptions = await prepareBrowserContextParams(options);
|
||||
const contextOptions = await prepareBrowserContextParams(this._platform, options);
|
||||
const response = forReuse ? await this._channel.newContextForReuse(contextOptions) : await this._channel.newContext(contextOptions);
|
||||
const context = BrowserContext.from(response.context);
|
||||
await this._browserType._didCreateContext(context, contextOptions, this._options, options.logger || this._logger);
|
||||
|
|
@ -126,8 +121,8 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
|
|||
const buffer = await artifact.readIntoBuffer();
|
||||
await artifact.delete();
|
||||
if (this._path) {
|
||||
await mkdirIfNeeded(this._path);
|
||||
await fs.promises.writeFile(this._path, buffer);
|
||||
await mkdirIfNeeded(this._platform, this._path);
|
||||
await this._platform.fs().promises.writeFile(this._path, buffer);
|
||||
this._path = undefined;
|
||||
}
|
||||
return buffer;
|
||||
|
|
|
|||
|
|
@ -15,9 +15,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { Artifact } from './artifact';
|
||||
import { Browser } from './browser';
|
||||
import { CDPSession } from './cdpSession';
|
||||
|
|
@ -38,14 +35,18 @@ import { Waiter } from './waiter';
|
|||
import { WebError } from './webError';
|
||||
import { Worker } from './worker';
|
||||
import { TimeoutSettings } from '../common/timeoutSettings';
|
||||
import { headersObjectToArray, isRegExp, isString, mkdirIfNeeded, urlMatchesEqual } from '../utils';
|
||||
import { mkdirIfNeeded } from '../utils/fileUtils';
|
||||
import { headersObjectToArray } from '../utils/headers';
|
||||
import { urlMatchesEqual } from '../utils/isomorphic/urlMatch';
|
||||
import { isRegExp, isString } from '../utils/rtti';
|
||||
import { rewriteErrorMessage } from '../utils/stackTrace';
|
||||
|
||||
import type { BrowserType } from './browserType';
|
||||
import type { BrowserContextOptions, Headers, LaunchOptions, StorageState, WaitForEventOptions } from './types';
|
||||
import type * as structs from '../../types/structs';
|
||||
import type * as api from '../../types/types';
|
||||
import type { URLMatch } from '../utils';
|
||||
import type { URLMatch } from '../utils/isomorphic/urlMatch';
|
||||
import type { Platform } from '../utils/platform';
|
||||
import type * as channels from '@protocol/channels';
|
||||
|
||||
export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel> implements api.BrowserContext {
|
||||
|
|
@ -107,7 +108,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
this.emit(Events.BrowserContext.ServiceWorker, serviceWorker);
|
||||
});
|
||||
this._channel.on('console', event => {
|
||||
const consoleMessage = new ConsoleMessage(event);
|
||||
const consoleMessage = new ConsoleMessage(this._platform, event);
|
||||
this.emit(Events.BrowserContext.Console, consoleMessage);
|
||||
const page = consoleMessage.page();
|
||||
if (page)
|
||||
|
|
@ -321,7 +322,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
}
|
||||
|
||||
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any): Promise<void> {
|
||||
const source = await evaluationScript(script, arg);
|
||||
const source = await evaluationScript(this._platform, script, arg);
|
||||
await this._channel.addInitScript({ source });
|
||||
}
|
||||
|
||||
|
|
@ -431,8 +432,8 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
async storageState(options: { path?: string, indexedDB?: boolean } = {}): Promise<StorageState> {
|
||||
const state = await this._channel.storageState({ indexedDB: options.indexedDB });
|
||||
if (options.path) {
|
||||
await mkdirIfNeeded(options.path);
|
||||
await fs.promises.writeFile(options.path, JSON.stringify(state, undefined, 2), 'utf8');
|
||||
await mkdirIfNeeded(this._platform, options.path);
|
||||
await this._platform.fs().promises.writeFile(options.path, JSON.stringify(state, undefined, 2), 'utf8');
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
|
@ -484,7 +485,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
const needCompressed = harParams.path.endsWith('.zip');
|
||||
if (isCompressed && !needCompressed) {
|
||||
await artifact.saveAs(harParams.path + '.tmp');
|
||||
await this._connection.localUtils()._channel.harUnzip({ zipFile: harParams.path + '.tmp', harFile: harParams.path });
|
||||
await this._connection.localUtils().harUnzip({ zipFile: harParams.path + '.tmp', harFile: harParams.path });
|
||||
} else {
|
||||
await artifact.saveAs(harParams.path);
|
||||
}
|
||||
|
|
@ -500,11 +501,11 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
}
|
||||
}
|
||||
|
||||
async function prepareStorageState(options: BrowserContextOptions): Promise<channels.BrowserNewContextParams['storageState']> {
|
||||
async function prepareStorageState(platform: Platform, options: BrowserContextOptions): Promise<channels.BrowserNewContextParams['storageState']> {
|
||||
if (typeof options.storageState !== 'string')
|
||||
return options.storageState;
|
||||
try {
|
||||
return JSON.parse(await fs.promises.readFile(options.storageState, 'utf8'));
|
||||
return JSON.parse(await platform.fs().promises.readFile(options.storageState, 'utf8'));
|
||||
} catch (e) {
|
||||
rewriteErrorMessage(e, `Error reading storage state from ${options.storageState}:\n` + e.message);
|
||||
throw e;
|
||||
|
|
@ -524,7 +525,7 @@ function prepareRecordHarOptions(options: BrowserContextOptions['recordHar']): c
|
|||
};
|
||||
}
|
||||
|
||||
export async function prepareBrowserContextParams(options: BrowserContextOptions): Promise<channels.BrowserNewContextParams> {
|
||||
export async function prepareBrowserContextParams(platform: Platform, options: BrowserContextOptions): Promise<channels.BrowserNewContextParams> {
|
||||
if (options.videoSize && !options.videosPath)
|
||||
throw new Error(`"videoSize" option requires "videosPath" to be specified`);
|
||||
if (options.extraHTTPHeaders)
|
||||
|
|
@ -534,7 +535,7 @@ export async function prepareBrowserContextParams(options: BrowserContextOptions
|
|||
viewport: options.viewport === null ? undefined : options.viewport,
|
||||
noDefaultViewport: options.viewport === null,
|
||||
extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined,
|
||||
storageState: await prepareStorageState(options),
|
||||
storageState: await prepareStorageState(platform, options),
|
||||
serviceWorkers: options.serviceWorkers,
|
||||
recordHar: prepareRecordHarOptions(options.recordHar),
|
||||
colorScheme: options.colorScheme === null ? 'no-override' : options.colorScheme,
|
||||
|
|
@ -542,7 +543,7 @@ export async function prepareBrowserContextParams(options: BrowserContextOptions
|
|||
forcedColors: options.forcedColors === null ? 'no-override' : options.forcedColors,
|
||||
contrast: options.contrast === null ? 'no-override' : options.contrast,
|
||||
acceptDownloads: toAcceptDownloadsProtocol(options.acceptDownloads),
|
||||
clientCertificates: await toClientCertificatesProtocol(options.clientCertificates),
|
||||
clientCertificates: await toClientCertificatesProtocol(platform, options.clientCertificates),
|
||||
};
|
||||
if (!contextParams.recordVideo && options.videosPath) {
|
||||
contextParams.recordVideo = {
|
||||
|
|
@ -551,7 +552,7 @@ export async function prepareBrowserContextParams(options: BrowserContextOptions
|
|||
};
|
||||
}
|
||||
if (contextParams.recordVideo && contextParams.recordVideo.dir)
|
||||
contextParams.recordVideo.dir = path.resolve(process.cwd(), contextParams.recordVideo.dir);
|
||||
contextParams.recordVideo.dir = platform.path().resolve(process.cwd(), contextParams.recordVideo.dir);
|
||||
return contextParams;
|
||||
}
|
||||
|
||||
|
|
@ -563,7 +564,7 @@ function toAcceptDownloadsProtocol(acceptDownloads?: boolean) {
|
|||
return 'deny';
|
||||
}
|
||||
|
||||
export async function toClientCertificatesProtocol(certs?: BrowserContextOptions['clientCertificates']): Promise<channels.PlaywrightNewRequestParams['clientCertificates']> {
|
||||
export async function toClientCertificatesProtocol(platform: Platform, certs?: BrowserContextOptions['clientCertificates']): Promise<channels.PlaywrightNewRequestParams['clientCertificates']> {
|
||||
if (!certs)
|
||||
return undefined;
|
||||
|
||||
|
|
@ -571,7 +572,7 @@ export async function toClientCertificatesProtocol(certs?: BrowserContextOptions
|
|||
if (value)
|
||||
return value;
|
||||
if (path)
|
||||
return await fs.promises.readFile(path);
|
||||
return await platform.fs().promises.readFile(path);
|
||||
};
|
||||
|
||||
return await Promise.all(certs.map(async cert => ({
|
||||
|
|
|
|||
|
|
@ -20,9 +20,10 @@ import { Browser } from './browser';
|
|||
import { BrowserContext, prepareBrowserContextParams } from './browserContext';
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
import { envObjectToArray } from './clientHelper';
|
||||
import { Connection } from './connection';
|
||||
import { Events } from './events';
|
||||
import { assert, headersObjectToArray, monotonicTime } from '../utils';
|
||||
import { assert } from '../utils/debug';
|
||||
import { headersObjectToArray } from '../utils/headers';
|
||||
import { monotonicTime } from '../utils/time';
|
||||
import { raceAgainstDeadline } from '../utils/timeoutRunner';
|
||||
|
||||
import type { Playwright } from './playwright';
|
||||
|
|
@ -92,7 +93,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
|||
const logger = options.logger || this._playwright._defaultLaunchOptions?.logger;
|
||||
assert(!(options as any).port, 'Cannot specify a port without launching as a server.');
|
||||
options = { ...this._playwright._defaultLaunchOptions, ...this._playwright._defaultContextOptions, ...options };
|
||||
const contextParams = await prepareBrowserContextParams(options);
|
||||
const contextParams = await prepareBrowserContextParams(this._platform, options);
|
||||
const persistentParams: channels.BrowserTypeLaunchPersistentContextParams = {
|
||||
...contextParams,
|
||||
ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : undefined,
|
||||
|
|
@ -133,40 +134,16 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
|||
};
|
||||
if ((params as any).__testHookRedirectPortForwarding)
|
||||
connectParams.socksProxyRedirectPortForTest = (params as any).__testHookRedirectPortForwarding;
|
||||
const { pipe, headers: connectHeaders } = await localUtils._channel.connect(connectParams);
|
||||
const closePipe = () => pipe.close().catch(() => {});
|
||||
const connection = new Connection(localUtils, this._instrumentation);
|
||||
connection.markAsRemote();
|
||||
connection.on('close', closePipe);
|
||||
|
||||
const connection = await localUtils.connect(connectParams);
|
||||
let browser: Browser;
|
||||
let closeError: string | undefined;
|
||||
const onPipeClosed = (reason?: string) => {
|
||||
connection.on('close', () => {
|
||||
// Emulate all pages, contexts and the browser closing upon disconnect.
|
||||
for (const context of browser?.contexts() || []) {
|
||||
for (const page of context.pages())
|
||||
page._onClose();
|
||||
context._onClose();
|
||||
}
|
||||
connection.close(reason || closeError);
|
||||
// Give a chance to any API call promises to reject upon page/context closure.
|
||||
// This happens naturally when we receive page.onClose and browser.onClose from the server
|
||||
// in separate tasks. However, upon pipe closure we used to dispatch them all synchronously
|
||||
// here and promises did not have a chance to reject.
|
||||
// The order of rejects vs closure is a part of the API contract and our test runner
|
||||
// relies on it to attribute rejections to the right test.
|
||||
setTimeout(() => browser?._didClose(), 0);
|
||||
};
|
||||
pipe.on('closed', params => onPipeClosed(params.reason));
|
||||
connection.onmessage = message => this._wrapApiCall(() => pipe.send({ message }).catch(() => onPipeClosed()), /* isInternal */ true);
|
||||
|
||||
pipe.on('message', ({ message }) => {
|
||||
try {
|
||||
connection!.dispatch(message);
|
||||
} catch (e) {
|
||||
closeError = String(e);
|
||||
closePipe();
|
||||
}
|
||||
});
|
||||
|
||||
const result = await raceAgainstDeadline(async () => {
|
||||
|
|
@ -176,21 +153,20 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
|||
|
||||
const playwright = await connection!.initializePlaywright();
|
||||
if (!playwright._initializer.preLaunchedBrowser) {
|
||||
closePipe();
|
||||
connection.close();
|
||||
throw new Error('Malformed endpoint. Did you use BrowserType.launchServer method?');
|
||||
}
|
||||
playwright._setSelectors(this._playwright.selectors);
|
||||
browser = Browser.from(playwright._initializer.preLaunchedBrowser!);
|
||||
this._didLaunchBrowser(browser, {}, logger);
|
||||
browser._shouldCloseConnectionOnClose = true;
|
||||
browser._connectHeaders = connectHeaders;
|
||||
browser.on(Events.Browser.Disconnected, () => this._wrapApiCall(() => closePipe(), /* isInternal */ true));
|
||||
browser.on(Events.Browser.Disconnected, () => connection.close());
|
||||
return browser;
|
||||
}, deadline);
|
||||
if (!result.timedOut) {
|
||||
return result.result;
|
||||
} else {
|
||||
closePipe();
|
||||
connection.close();
|
||||
throw new Error(`Timeout ${params.timeout}ms exceeded`);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
import { EventEmitter } from './eventEmitter';
|
||||
import { ValidationError, maybeFindValidator } from '../protocol/validator';
|
||||
import { isUnderTest } from '../utils';
|
||||
import { isUnderTest } from '../utils/debug';
|
||||
import { debugLogger } from '../utils/debugLogger';
|
||||
import { captureLibraryStackTrace, stringifyStackFrames } from '../utils/stackTrace';
|
||||
import { zones } from '../utils/zones';
|
||||
|
|
@ -25,6 +25,7 @@ import type { ClientInstrumentation } from './clientInstrumentation';
|
|||
import type { Connection } from './connection';
|
||||
import type { Logger } from './types';
|
||||
import type { ValidatorContext } from '../protocol/validator';
|
||||
import type { Platform } from '../utils/platform';
|
||||
import type * as channels from '@protocol/channels';
|
||||
|
||||
type Listener = (...args: any[]) => void;
|
||||
|
|
@ -39,6 +40,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
|||
readonly _channel: T;
|
||||
readonly _initializer: channels.InitializerTraits<T>;
|
||||
_logger: Logger | undefined;
|
||||
readonly _platform: Platform;
|
||||
readonly _instrumentation: ClientInstrumentation;
|
||||
private _eventToSubscriptionMapping: Map<string, string> = new Map();
|
||||
private _isInternalType = false;
|
||||
|
|
@ -52,6 +54,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
|||
this._guid = guid;
|
||||
this._parent = parent instanceof ChannelOwner ? parent : undefined;
|
||||
this._instrumentation = this._connection._instrumentation;
|
||||
this._platform = this._connection.platform;
|
||||
|
||||
this._connection._objects.set(guid, this);
|
||||
if (this._parent) {
|
||||
|
|
|
|||
|
|
@ -15,11 +15,10 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
|
||||
import { isString } from '../utils';
|
||||
import { isString } from '../utils/rtti';
|
||||
|
||||
import type * as types from './types';
|
||||
import type { Platform } from '../utils/platform';
|
||||
|
||||
export function envObjectToArray(env: types.Env): { name: string, value: string }[] {
|
||||
const result: { name: string, value: string }[] = [];
|
||||
|
|
@ -30,7 +29,7 @@ export function envObjectToArray(env: types.Env): { name: string, value: string
|
|||
return result;
|
||||
}
|
||||
|
||||
export async function evaluationScript(fun: Function | string | { path?: string, content?: string }, arg?: any, addSourceUrl: boolean = true): Promise<string> {
|
||||
export async function evaluationScript(platform: Platform, fun: Function | string | { path?: string, content?: string }, arg?: any, addSourceUrl: boolean = true): Promise<string> {
|
||||
if (typeof fun === 'function') {
|
||||
const source = fun.toString();
|
||||
const argString = Object.is(arg, undefined) ? 'undefined' : JSON.stringify(arg);
|
||||
|
|
@ -43,7 +42,7 @@ export async function evaluationScript(fun: Function | string | { path?: string,
|
|||
if (fun.content !== undefined)
|
||||
return fun.content;
|
||||
if (fun.path !== undefined) {
|
||||
let source = await fs.promises.readFile(fun.path, 'utf8');
|
||||
let source = await platform.fs().promises.readFile(fun.path, 'utf8');
|
||||
if (addSourceUrl)
|
||||
source = addSourceUrlToScript(source, fun.path);
|
||||
return source;
|
||||
|
|
|
|||
|
|
@ -42,11 +42,14 @@ import { Tracing } from './tracing';
|
|||
import { Worker } from './worker';
|
||||
import { WritableStream } from './writableStream';
|
||||
import { ValidationError, findValidator } from '../protocol/validator';
|
||||
import { formatCallLog, rewriteErrorMessage, zones } from '../utils';
|
||||
import { debugLogger } from '../utils/debugLogger';
|
||||
import { formatCallLog, rewriteErrorMessage } from '../utils/stackTrace';
|
||||
import { zones } from '../utils/zones';
|
||||
|
||||
import type { ClientInstrumentation } from './clientInstrumentation';
|
||||
import type { HeadersArray } from './types';
|
||||
import type { ValidatorContext } from '../protocol/validator';
|
||||
import type { Platform } from '../utils/platform';
|
||||
import type * as channels from '@protocol/channels';
|
||||
|
||||
class Root extends ChannelOwner<channels.RootChannel> {
|
||||
|
|
@ -78,12 +81,17 @@ export class Connection extends EventEmitter {
|
|||
toImpl: ((client: ChannelOwner) => any) | undefined;
|
||||
private _tracingCount = 0;
|
||||
readonly _instrumentation: ClientInstrumentation;
|
||||
readonly platform: Platform;
|
||||
// Used from @playwright/test fixtures -> TODO remove?
|
||||
readonly headers: HeadersArray;
|
||||
|
||||
constructor(localUtils: LocalUtils | undefined, instrumentation: ClientInstrumentation | undefined) {
|
||||
constructor(localUtils: LocalUtils | undefined, platform: Platform, instrumentation: ClientInstrumentation | undefined, headers: HeadersArray) {
|
||||
super();
|
||||
this._instrumentation = instrumentation || createInstrumentation();
|
||||
this._localUtils = localUtils;
|
||||
this.platform = platform;
|
||||
this._rootObject = new Root(this);
|
||||
this.headers = headers;
|
||||
}
|
||||
|
||||
markAsRemote() {
|
||||
|
|
@ -138,7 +146,7 @@ export class Connection extends EventEmitter {
|
|||
const location = frames[0] ? { file: frames[0].file, line: frames[0].line, column: frames[0].column } : undefined;
|
||||
const metadata: channels.Metadata = { apiName, location, internal: !apiName, stepId };
|
||||
if (this._tracingCount && frames && type !== 'LocalUtils')
|
||||
this._localUtils?._channel.addStackToTracingNoReply({ callData: { stack: frames, id } }).catch(() => {});
|
||||
this._localUtils?.addStackToTracingNoReply({ callData: { stack: frames, id } }).catch(() => {});
|
||||
// We need to exit zones before calling into the server, otherwise
|
||||
// when we receive events from the server, we would be in an API zone.
|
||||
zones.empty().run(() => this.onmessage({ ...message, metadata }));
|
||||
|
|
|
|||
|
|
@ -14,12 +14,11 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as util from 'util';
|
||||
|
||||
import { JSHandle } from './jsHandle';
|
||||
import { Page } from './page';
|
||||
|
||||
import type * as api from '../../types/types';
|
||||
import type { Platform } from '../utils/platform';
|
||||
import type * as channels from '@protocol/channels';
|
||||
|
||||
type ConsoleMessageLocation = channels.BrowserContextConsoleEvent['location'];
|
||||
|
|
@ -29,9 +28,11 @@ export class ConsoleMessage implements api.ConsoleMessage {
|
|||
private _page: Page | null;
|
||||
private _event: channels.BrowserContextConsoleEvent | channels.ElectronApplicationConsoleEvent;
|
||||
|
||||
constructor(event: channels.BrowserContextConsoleEvent | channels.ElectronApplicationConsoleEvent) {
|
||||
constructor(platform: Platform, event: channels.BrowserContextConsoleEvent | channels.ElectronApplicationConsoleEvent) {
|
||||
this._page = ('page' in event && event.page) ? Page.from(event.page) : null;
|
||||
this._event = event;
|
||||
if (platform.inspectCustom)
|
||||
(this as any)[platform.inspectCustom] = () => this._inspect();
|
||||
}
|
||||
|
||||
page() {
|
||||
|
|
@ -54,7 +55,7 @@ export class ConsoleMessage implements api.ConsoleMessage {
|
|||
return this._event.location;
|
||||
}
|
||||
|
||||
[util.inspect.custom]() {
|
||||
private _inspect() {
|
||||
return this.text();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export class Electron extends ChannelOwner<channels.ElectronChannel> implements
|
|||
|
||||
async launch(options: ElectronOptions = {}): Promise<ElectronApplication> {
|
||||
const params: channels.ElectronLaunchParams = {
|
||||
...await prepareBrowserContextParams(options),
|
||||
...await prepareBrowserContextParams(this._platform, options),
|
||||
env: envObjectToArray(options.env ? options.env : process.env),
|
||||
tracesDir: options.tracesDir,
|
||||
};
|
||||
|
|
@ -81,7 +81,7 @@ export class ElectronApplication extends ChannelOwner<channels.ElectronApplicati
|
|||
this._channel.on('close', () => {
|
||||
this.emit(Events.ElectronApplication.Close);
|
||||
});
|
||||
this._channel.on('console', event => this.emit(Events.ElectronApplication.Console, new ConsoleMessage(event)));
|
||||
this._channel.on('console', event => this.emit(Events.ElectronApplication.Console, new ConsoleMessage(this._platform, event)));
|
||||
this._setEventToSubscriptionMapping(new Map<string, channels.ElectronApplicationUpdateSubscriptionParams['event']>([
|
||||
[Events.ElectronApplication.Console, 'console'],
|
||||
]));
|
||||
|
|
|
|||
|
|
@ -14,15 +14,14 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { pipeline } from 'stream';
|
||||
import { promisify } from 'util';
|
||||
|
||||
import { Frame } from './frame';
|
||||
import { JSHandle, parseResult, serializeArgument } from './jsHandle';
|
||||
import { assert, isString } from '../utils';
|
||||
import { assert } from '../utils/debug';
|
||||
import { fileUploadSizeLimit, mkdirIfNeeded } from '../utils/fileUtils';
|
||||
import { isString } from '../utils/rtti';
|
||||
import { mime } from '../utilsBundle';
|
||||
import { WritableStream } from './writableStream';
|
||||
|
||||
|
|
@ -32,6 +31,7 @@ import type { Locator } from './locator';
|
|||
import type { FilePayload, Rect, SelectOption, SelectOptionOptions } from './types';
|
||||
import type * as structs from '../../types/structs';
|
||||
import type * as api from '../../types/types';
|
||||
import type { Platform } from '../utils/platform';
|
||||
import type * as channels from '@protocol/channels';
|
||||
|
||||
const pipelineAsync = promisify(pipeline);
|
||||
|
|
@ -156,7 +156,7 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> implements
|
|||
const frame = await this.ownerFrame();
|
||||
if (!frame)
|
||||
throw new Error('Cannot set input files to detached element');
|
||||
const converted = await convertInputFiles(files, frame.page().context());
|
||||
const converted = await convertInputFiles(this._platform, files, frame.page().context());
|
||||
await this._elementChannel.setInputFiles({ ...converted, ...options });
|
||||
}
|
||||
|
||||
|
|
@ -192,20 +192,21 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> implements
|
|||
return value === undefined ? null : value;
|
||||
}
|
||||
|
||||
async screenshot(options: Omit<channels.ElementHandleScreenshotOptions, 'mask'> & { path?: string, mask?: Locator[] } = {}): Promise<Buffer> {
|
||||
async screenshot(options: Omit<channels.ElementHandleScreenshotOptions, 'mask'> & { path?: string, mask?: api.Locator[] } = {}): Promise<Buffer> {
|
||||
const mask = options.mask as Locator[] | undefined;
|
||||
const copy: channels.ElementHandleScreenshotOptions = { ...options, mask: undefined };
|
||||
if (!copy.type)
|
||||
copy.type = determineScreenshotType(options);
|
||||
if (options.mask) {
|
||||
copy.mask = options.mask.map(locator => ({
|
||||
if (mask) {
|
||||
copy.mask = mask.map(locator => ({
|
||||
frame: locator._frame._channel,
|
||||
selector: locator._selector,
|
||||
}));
|
||||
}
|
||||
const result = await this._elementChannel.screenshot(copy);
|
||||
if (options.path) {
|
||||
await mkdirIfNeeded(options.path);
|
||||
await fs.promises.writeFile(options.path, result.binary);
|
||||
await mkdirIfNeeded(this._platform, options.path);
|
||||
await this._platform.fs().promises.writeFile(options.path, result.binary);
|
||||
}
|
||||
return result.binary;
|
||||
}
|
||||
|
|
@ -263,18 +264,18 @@ function filePayloadExceedsSizeLimit(payloads: FilePayload[]) {
|
|||
return payloads.reduce((size, item) => size + (item.buffer ? item.buffer.byteLength : 0), 0) >= fileUploadSizeLimit;
|
||||
}
|
||||
|
||||
async function resolvePathsAndDirectoryForInputFiles(items: string[]): Promise<[string[] | undefined, string | undefined]> {
|
||||
async function resolvePathsAndDirectoryForInputFiles(platform: Platform, items: string[]): Promise<[string[] | undefined, string | undefined]> {
|
||||
let localPaths: string[] | undefined;
|
||||
let localDirectory: string | undefined;
|
||||
for (const item of items) {
|
||||
const stat = await fs.promises.stat(item as string);
|
||||
const stat = await platform.fs().promises.stat(item as string);
|
||||
if (stat.isDirectory()) {
|
||||
if (localDirectory)
|
||||
throw new Error('Multiple directories are not supported');
|
||||
localDirectory = path.resolve(item as string);
|
||||
localDirectory = platform.path().resolve(item as string);
|
||||
} else {
|
||||
localPaths ??= [];
|
||||
localPaths.push(path.resolve(item as string));
|
||||
localPaths.push(platform.path().resolve(item as string));
|
||||
}
|
||||
}
|
||||
if (localPaths?.length && localDirectory)
|
||||
|
|
@ -282,30 +283,30 @@ async function resolvePathsAndDirectoryForInputFiles(items: string[]): Promise<[
|
|||
return [localPaths, localDirectory];
|
||||
}
|
||||
|
||||
export async function convertInputFiles(files: string | FilePayload | string[] | FilePayload[], context: BrowserContext): Promise<SetInputFilesFiles> {
|
||||
export async function convertInputFiles(platform: Platform, files: string | FilePayload | string[] | FilePayload[], context: BrowserContext): Promise<SetInputFilesFiles> {
|
||||
const items: (string | FilePayload)[] = Array.isArray(files) ? files.slice() : [files];
|
||||
|
||||
if (items.some(item => typeof item === 'string')) {
|
||||
if (!items.every(item => typeof item === 'string'))
|
||||
throw new Error('File paths cannot be mixed with buffers');
|
||||
|
||||
const [localPaths, localDirectory] = await resolvePathsAndDirectoryForInputFiles(items);
|
||||
const [localPaths, localDirectory] = await resolvePathsAndDirectoryForInputFiles(platform, items);
|
||||
|
||||
if (context._connection.isRemote()) {
|
||||
const files = localDirectory ? (await fs.promises.readdir(localDirectory, { withFileTypes: true, recursive: true })).filter(f => f.isFile()).map(f => path.join(f.path, f.name)) : localPaths!;
|
||||
const files = localDirectory ? (await platform.fs().promises.readdir(localDirectory, { withFileTypes: true, recursive: true })).filter(f => f.isFile()).map(f => platform.path().join(f.path, f.name)) : localPaths!;
|
||||
const { writableStreams, rootDir } = await context._wrapApiCall(async () => context._channel.createTempFiles({
|
||||
rootDirName: localDirectory ? path.basename(localDirectory) : undefined,
|
||||
rootDirName: localDirectory ? platform.path().basename(localDirectory) : undefined,
|
||||
items: await Promise.all(files.map(async file => {
|
||||
const lastModifiedMs = (await fs.promises.stat(file)).mtimeMs;
|
||||
const lastModifiedMs = (await platform.fs().promises.stat(file)).mtimeMs;
|
||||
return {
|
||||
name: localDirectory ? path.relative(localDirectory, file) : path.basename(file),
|
||||
name: localDirectory ? platform.path().relative(localDirectory, file) : platform.path().basename(file),
|
||||
lastModifiedMs
|
||||
};
|
||||
})),
|
||||
}), true);
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const writable = WritableStream.from(writableStreams[i]);
|
||||
await pipelineAsync(fs.createReadStream(files[i]), writable.stream());
|
||||
await pipelineAsync(platform.fs().createReadStream(files[i]), writable.stream());
|
||||
}
|
||||
return {
|
||||
directoryStream: rootDir,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
*/
|
||||
|
||||
import { parseSerializedValue, serializeValue } from '../protocol/serializers';
|
||||
import { isError } from '../utils';
|
||||
import { isError } from '../utils/rtti';
|
||||
|
||||
import type { SerializedError } from '@protocol/channels';
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
|
||||
import { EventEmitter as OriginalEventEmitter } from 'events';
|
||||
|
||||
import { isUnderTest } from '../utils';
|
||||
import { isUnderTest } from '../utils/debug';
|
||||
|
||||
import type { EventEmitter as EventEmitterType } from 'events';
|
||||
|
||||
|
|
|
|||
|
|
@ -14,24 +14,24 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as util from 'util';
|
||||
|
||||
import { assert, headersObjectToArray, isString } from '../utils';
|
||||
import { toClientCertificatesProtocol } from './browserContext';
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
import { TargetClosedError, isTargetClosedError } from './errors';
|
||||
import { RawHeaders } from './network';
|
||||
import { Tracing } from './tracing';
|
||||
import { assert } from '../utils/debug';
|
||||
import { mkdirIfNeeded } from '../utils/fileUtils';
|
||||
import { headersObjectToArray } from '../utils/headers';
|
||||
import { isString } from '../utils/rtti';
|
||||
|
||||
import type { Playwright } from './playwright';
|
||||
import type { ClientCertificate, FilePayload, Headers, SetStorageState, StorageState } from './types';
|
||||
import type { Serializable } from '../../types/structs';
|
||||
import type * as api from '../../types/types';
|
||||
import type { HeadersArray, NameValue } from '../common/types';
|
||||
import type { Platform } from '../utils/platform';
|
||||
import type * as channels from '@protocol/channels';
|
||||
import type * as fs from 'fs';
|
||||
|
||||
export type FetchOptions = {
|
||||
params?: { [key: string]: string | number | boolean; } | URLSearchParams | string,
|
||||
|
|
@ -70,14 +70,14 @@ export class APIRequest implements api.APIRequest {
|
|||
...options,
|
||||
};
|
||||
const storageState = typeof options.storageState === 'string' ?
|
||||
JSON.parse(await fs.promises.readFile(options.storageState, 'utf8')) :
|
||||
JSON.parse(await this._playwright._platform.fs().promises.readFile(options.storageState, 'utf8')) :
|
||||
options.storageState;
|
||||
const context = APIRequestContext.from((await this._playwright._channel.newRequest({
|
||||
...options,
|
||||
extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined,
|
||||
storageState,
|
||||
tracesDir: this._playwright._defaultLaunchOptions?.tracesDir, // We do not expose tracesDir in the API, so do not allow options to accidentally override it.
|
||||
clientCertificates: await toClientCertificatesProtocol(options.clientCertificates),
|
||||
clientCertificates: await toClientCertificatesProtocol(this._playwright._platform, options.clientCertificates),
|
||||
})).request);
|
||||
this._contexts.add(context);
|
||||
context._request = this;
|
||||
|
|
@ -232,7 +232,7 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
|
|||
} else {
|
||||
// Convert file-like values to ServerFilePayload structs.
|
||||
for (const [name, value] of Object.entries(options.multipart))
|
||||
multipartData.push(await toFormField(name, value));
|
||||
multipartData.push(await toFormField(this._platform, name, value));
|
||||
}
|
||||
}
|
||||
if (postDataBuffer === undefined && jsonData === undefined && formData === undefined && multipartData === undefined)
|
||||
|
|
@ -264,23 +264,24 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
|
|||
async storageState(options: { path?: string, indexedDB?: boolean } = {}): Promise<StorageState> {
|
||||
const state = await this._channel.storageState({ indexedDB: options.indexedDB });
|
||||
if (options.path) {
|
||||
await mkdirIfNeeded(options.path);
|
||||
await fs.promises.writeFile(options.path, JSON.stringify(state, undefined, 2), 'utf8');
|
||||
await mkdirIfNeeded(this._platform, options.path);
|
||||
await this._platform.fs().promises.writeFile(options.path, JSON.stringify(state, undefined, 2), 'utf8');
|
||||
}
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
async function toFormField(name: string, value: string|number|boolean|fs.ReadStream|FilePayload): Promise<channels.FormField> {
|
||||
async function toFormField(platform: Platform, name: string, value: string | number | boolean | fs.ReadStream | FilePayload): Promise<channels.FormField> {
|
||||
const typeOfValue = typeof value;
|
||||
if (isFilePayload(value)) {
|
||||
const payload = value as FilePayload;
|
||||
if (!Buffer.isBuffer(payload.buffer))
|
||||
throw new Error(`Unexpected buffer type of 'data.${name}'`);
|
||||
return { name, file: filePayloadToJson(payload) };
|
||||
} else if (value instanceof fs.ReadStream) {
|
||||
return { name, file: await readStreamToJson(value as fs.ReadStream) };
|
||||
} else {
|
||||
} else if (typeOfValue === 'string' || typeOfValue === 'number' || typeOfValue === 'boolean') {
|
||||
return { name, value: String(value) };
|
||||
} else {
|
||||
return { name, file: await readStreamToJson(platform, value as fs.ReadStream) };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -307,6 +308,9 @@ export class APIResponse implements api.APIResponse {
|
|||
this._request = context;
|
||||
this._initializer = initializer;
|
||||
this._headers = new RawHeaders(this._initializer.headers);
|
||||
|
||||
if (context._platform.inspectCustom)
|
||||
(this as any)[context._platform.inspectCustom] = () => this._inspect();
|
||||
}
|
||||
|
||||
ok(): boolean {
|
||||
|
|
@ -364,7 +368,7 @@ export class APIResponse implements api.APIResponse {
|
|||
await this._request._channel.disposeAPIResponse({ fetchUid: this._fetchUid() });
|
||||
}
|
||||
|
||||
[util.inspect.custom]() {
|
||||
private _inspect() {
|
||||
const headers = this.headersArray().map(({ name, value }) => ` ${name}: ${value}`);
|
||||
return `APIResponse: ${this.status()} ${this.statusText()}\n${headers.join('\n')}`;
|
||||
}
|
||||
|
|
@ -389,7 +393,7 @@ function filePayloadToJson(payload: FilePayload): ServerFilePayload {
|
|||
};
|
||||
}
|
||||
|
||||
async function readStreamToJson(stream: fs.ReadStream): Promise<ServerFilePayload> {
|
||||
async function readStreamToJson(platform: Platform, stream: fs.ReadStream): Promise<ServerFilePayload> {
|
||||
const buffer = await new Promise<Buffer>((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
stream.on('data', chunk => chunks.push(chunk as Buffer));
|
||||
|
|
@ -398,7 +402,7 @@ async function readStreamToJson(stream: fs.ReadStream): Promise<ServerFilePayloa
|
|||
});
|
||||
const streamPath: string = Buffer.isBuffer(stream.path) ? stream.path.toString('utf8') : stream.path;
|
||||
return {
|
||||
name: path.basename(streamPath),
|
||||
name: platform.path().basename(streamPath),
|
||||
buffer,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,28 +16,27 @@
|
|||
*/
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import * as fs from 'fs';
|
||||
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
import { FrameLocator, Locator, testIdAttributeName } from './locator';
|
||||
import { assert } from '../utils';
|
||||
import { urlMatches } from '../utils';
|
||||
import { addSourceUrlToScript } from './clientHelper';
|
||||
import { ElementHandle, convertInputFiles, convertSelectOptionValues } from './elementHandle';
|
||||
import { Events } from './events';
|
||||
import { JSHandle, assertMaxArguments, parseResult, serializeArgument } from './jsHandle';
|
||||
import { FrameLocator, Locator, testIdAttributeName } from './locator';
|
||||
import * as network from './network';
|
||||
import { kLifecycleEvents } from './types';
|
||||
import { Waiter } from './waiter';
|
||||
import { assert } from '../utils/debug';
|
||||
import { getByAltTextSelector, getByLabelSelector, getByPlaceholderSelector, getByRoleSelector, getByTestIdSelector, getByTextSelector, getByTitleSelector } from '../utils/isomorphic/locatorUtils';
|
||||
import { urlMatches } from '../utils/isomorphic/urlMatch';
|
||||
|
||||
import type { LocatorOptions } from './locator';
|
||||
import type { Page } from './page';
|
||||
import type { FilePayload, LifecycleEvent, SelectOption, SelectOptionOptions, StrictOptions, WaitForFunctionOptions } from './types';
|
||||
import type * as structs from '../../types/structs';
|
||||
import type * as api from '../../types/types';
|
||||
import type { URLMatch } from '../utils';
|
||||
import type { ByRoleOptions } from '../utils/isomorphic/locatorUtils';
|
||||
import type { URLMatch } from '../utils/isomorphic/urlMatch';
|
||||
import type * as channels from '@protocol/channels';
|
||||
|
||||
export type WaitForNavigationOptions = {
|
||||
|
|
@ -269,7 +268,7 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
|
|||
async addScriptTag(options: { url?: string, path?: string, content?: string, type?: string } = {}): Promise<ElementHandle> {
|
||||
const copy = { ...options };
|
||||
if (copy.path) {
|
||||
copy.content = (await fs.promises.readFile(copy.path)).toString();
|
||||
copy.content = (await this._platform.fs().promises.readFile(copy.path)).toString();
|
||||
copy.content = addSourceUrlToScript(copy.content, copy.path);
|
||||
}
|
||||
return ElementHandle.from((await this._channel.addScriptTag({ ...copy })).element);
|
||||
|
|
@ -278,7 +277,7 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
|
|||
async addStyleTag(options: { url?: string; path?: string; content?: string; } = {}): Promise<ElementHandle> {
|
||||
const copy = { ...options };
|
||||
if (copy.path) {
|
||||
copy.content = (await fs.promises.readFile(copy.path)).toString();
|
||||
copy.content = (await this._platform.fs().promises.readFile(copy.path)).toString();
|
||||
copy.content += '/*# sourceURL=' + copy.path.replace(/\n/g, '') + '*/';
|
||||
}
|
||||
return ElementHandle.from((await this._channel.addStyleTag({ ...copy })).element);
|
||||
|
|
@ -403,7 +402,7 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
|
|||
}
|
||||
|
||||
async setInputFiles(selector: string, files: string | FilePayload | string[] | FilePayload[], options: channels.FrameSetInputFilesOptions = {}): Promise<void> {
|
||||
const converted = await convertInputFiles(files, this.page().context());
|
||||
const converted = await convertInputFiles(this._platform, files, this.page().context());
|
||||
await this._channel.setInputFiles({ selector, ...converted, ...options });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ import { debugLogger } from '../utils/debugLogger';
|
|||
import type { BrowserContext } from './browserContext';
|
||||
import type { LocalUtils } from './localUtils';
|
||||
import type { Route } from './network';
|
||||
import type { URLMatch } from '../utils';
|
||||
import type { Page } from './page';
|
||||
import type { URLMatch } from '../utils/isomorphic/urlMatch';
|
||||
|
||||
type HarNotFoundAction = 'abort' | 'fallback';
|
||||
|
||||
|
|
@ -31,7 +31,7 @@ export class HarRouter {
|
|||
private _options: { urlMatch?: URLMatch; baseURL?: string; };
|
||||
|
||||
static async create(localUtils: LocalUtils, file: string, notFoundAction: HarNotFoundAction, options: { urlMatch?: URLMatch }): Promise<HarRouter> {
|
||||
const { harId, error } = await localUtils._channel.harOpen({ file });
|
||||
const { harId, error } = await localUtils.harOpen({ file });
|
||||
if (error)
|
||||
throw new Error(error);
|
||||
return new HarRouter(localUtils, harId!, notFoundAction, options);
|
||||
|
|
@ -47,7 +47,7 @@ export class HarRouter {
|
|||
private async _handle(route: Route) {
|
||||
const request = route.request();
|
||||
|
||||
const response = await this._localUtils._channel.harLookup({
|
||||
const response = await this._localUtils.harLookup({
|
||||
harId: this._harId,
|
||||
url: request.url(),
|
||||
method: request.method(),
|
||||
|
|
@ -103,6 +103,6 @@ export class HarRouter {
|
|||
}
|
||||
|
||||
dispose() {
|
||||
this._localUtils._channel.harClose({ harId: this._harId }).catch(() => {});
|
||||
this._localUtils.harClose({ harId: this._harId }).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,8 +15,11 @@
|
|||
*/
|
||||
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
import { Connection } from './connection';
|
||||
import * as localUtils from '../utils/localUtils';
|
||||
|
||||
import type { Size } from './types';
|
||||
import type { HarBackend } from '../utils/harBackend';
|
||||
import type * as channels from '@protocol/channels';
|
||||
|
||||
type DeviceDescriptor = {
|
||||
|
|
@ -31,6 +34,8 @@ type Devices = { [name: string]: DeviceDescriptor };
|
|||
|
||||
export class LocalUtils extends ChannelOwner<channels.LocalUtilsChannel> {
|
||||
readonly devices: Devices;
|
||||
private _harBackends = new Map<string, HarBackend>();
|
||||
private _stackSessions = new Map<string, localUtils.StackSession>();
|
||||
|
||||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.LocalUtilsInitializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
|
|
@ -39,4 +44,61 @@ export class LocalUtils extends ChannelOwner<channels.LocalUtilsChannel> {
|
|||
for (const { name, descriptor } of initializer.deviceDescriptors)
|
||||
this.devices[name] = descriptor;
|
||||
}
|
||||
|
||||
async zip(params: channels.LocalUtilsZipParams): Promise<void> {
|
||||
return await localUtils.zip(this._platform, this._stackSessions, params);
|
||||
}
|
||||
|
||||
async harOpen(params: channels.LocalUtilsHarOpenParams): Promise<channels.LocalUtilsHarOpenResult> {
|
||||
return await localUtils.harOpen(this._harBackends, params);
|
||||
}
|
||||
|
||||
async harLookup(params: channels.LocalUtilsHarLookupParams): Promise<channels.LocalUtilsHarLookupResult> {
|
||||
return await localUtils.harLookup(this._harBackends, params);
|
||||
}
|
||||
|
||||
async harClose(params: channels.LocalUtilsHarCloseParams): Promise<void> {
|
||||
return await localUtils.harClose(this._harBackends, params);
|
||||
}
|
||||
|
||||
async harUnzip(params: channels.LocalUtilsHarUnzipParams): Promise<void> {
|
||||
return await localUtils.harUnzip(params);
|
||||
}
|
||||
|
||||
async tracingStarted(params: channels.LocalUtilsTracingStartedParams): Promise<channels.LocalUtilsTracingStartedResult> {
|
||||
return await localUtils.tracingStarted(this._stackSessions, params);
|
||||
}
|
||||
|
||||
async traceDiscarded(params: channels.LocalUtilsTraceDiscardedParams): Promise<void> {
|
||||
return await localUtils.traceDiscarded(this._platform, this._stackSessions, params);
|
||||
}
|
||||
|
||||
async addStackToTracingNoReply(params: channels.LocalUtilsAddStackToTracingNoReplyParams): Promise<void> {
|
||||
return await localUtils.addStackToTracingNoReply(this._stackSessions, params);
|
||||
}
|
||||
|
||||
async connect(params: channels.LocalUtilsConnectParams): Promise<Connection> {
|
||||
const { pipe, headers: connectHeaders } = await this._channel.connect(params);
|
||||
const closePipe = () => this._wrapApiCall(() => pipe.close().catch(() => {}), /* isInternal */ true);
|
||||
const connection = new Connection(this, this._platform, this._instrumentation, connectHeaders);
|
||||
connection.markAsRemote();
|
||||
connection.on('close', closePipe);
|
||||
|
||||
let closeError: string | undefined;
|
||||
const onPipeClosed = (reason?: string) => {
|
||||
connection.close(reason || closeError);
|
||||
};
|
||||
pipe.on('closed', params => onPipeClosed(params.reason));
|
||||
connection.onmessage = message => this._wrapApiCall(() => pipe.send({ message }).catch(() => onPipeClosed()), /* isInternal */ true);
|
||||
|
||||
pipe.on('message', ({ message }) => {
|
||||
try {
|
||||
connection!.dispatch(message);
|
||||
} catch (e) {
|
||||
closeError = String(e);
|
||||
closePipe();
|
||||
}
|
||||
});
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,13 +14,13 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as util from 'util';
|
||||
|
||||
import { asLocator, isString, monotonicTime } from '../utils';
|
||||
import { ElementHandle } from './elementHandle';
|
||||
import { parseResult, serializeArgument } from './jsHandle';
|
||||
import { asLocator } from '../utils/isomorphic/locatorGenerators';
|
||||
import { getByAltTextSelector, getByLabelSelector, getByPlaceholderSelector, getByRoleSelector, getByTestIdSelector, getByTextSelector, getByTitleSelector } from '../utils/isomorphic/locatorUtils';
|
||||
import { escapeForTextSelector } from '../utils/isomorphic/stringUtils';
|
||||
import { isString } from '../utils/rtti';
|
||||
import { monotonicTime } from '../utils/time';
|
||||
|
||||
import type { Frame } from './frame';
|
||||
import type { FilePayload, FrameExpectParams, Rect, SelectOption, SelectOptionOptions, TimeoutOptions } from './types';
|
||||
|
|
@ -64,6 +64,9 @@ export class Locator implements api.Locator {
|
|||
throw new Error(`Inner "hasNot" locator must belong to the same frame.`);
|
||||
this._selector += ` >> internal:has-not=` + JSON.stringify(locator._selector);
|
||||
}
|
||||
|
||||
if (this._frame._platform.inspectCustom)
|
||||
(this as any)[this._frame._platform.inspectCustom] = () => this._inspect();
|
||||
}
|
||||
|
||||
private async _withElement<R>(task: (handle: ElementHandle<SVGElement | HTMLElement>, timeout?: number) => Promise<R>, timeout?: number): Promise<R> {
|
||||
|
|
@ -291,8 +294,9 @@ export class Locator implements api.Locator {
|
|||
return await this._frame.press(this._selector, key, { strict: true, ...options });
|
||||
}
|
||||
|
||||
async screenshot(options: Omit<channels.ElementHandleScreenshotOptions, 'mask'> & { path?: string, mask?: Locator[] } = {}): Promise<Buffer> {
|
||||
return await this._withElement((h, timeout) => h.screenshot({ ...options, timeout }), options.timeout);
|
||||
async screenshot(options: Omit<channels.ElementHandleScreenshotOptions, 'mask'> & { path?: string, mask?: api.Locator[] } = {}): Promise<Buffer> {
|
||||
const mask = options.mask as Locator[] | undefined;
|
||||
return await this._withElement((h, timeout) => h.screenshot({ ...options, mask, timeout }), options.timeout);
|
||||
}
|
||||
|
||||
async ariaSnapshot(options?: { _id?: boolean, _mode?: 'raw' | 'regex' } & TimeoutOptions): Promise<string> {
|
||||
|
|
@ -370,7 +374,7 @@ export class Locator implements api.Locator {
|
|||
return result;
|
||||
}
|
||||
|
||||
[util.inspect.custom]() {
|
||||
private _inspect() {
|
||||
return this.toString();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import { URLSearchParams } from 'url';
|
||||
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
|
|
@ -22,19 +21,26 @@ import { isTargetClosedError } from './errors';
|
|||
import { Events } from './events';
|
||||
import { APIResponse } from './fetch';
|
||||
import { Frame } from './frame';
|
||||
import { Worker } from './worker';
|
||||
import { MultiMap, assert, headersObjectToArray, isRegExp, isString, rewriteErrorMessage, urlMatches, zones } from '../utils';
|
||||
import { Waiter } from './waiter';
|
||||
import { Worker } from './worker';
|
||||
import { assert } from '../utils/debug';
|
||||
import { headersObjectToArray } from '../utils/headers';
|
||||
import { urlMatches } from '../utils/isomorphic/urlMatch';
|
||||
import { LongStandingScope, ManualPromise } from '../utils/manualPromise';
|
||||
import { MultiMap } from '../utils/multimap';
|
||||
import { isRegExp, isString } from '../utils/rtti';
|
||||
import { rewriteErrorMessage } from '../utils/stackTrace';
|
||||
import { zones } from '../utils/zones';
|
||||
import { mime } from '../utilsBundle';
|
||||
|
||||
import type { Headers, RemoteAddr, SecurityDetails, WaitForEventOptions } from './types';
|
||||
import type { URLMatch, Zone } from '../utils';
|
||||
import type { BrowserContext } from './browserContext';
|
||||
import type { Page } from './page';
|
||||
import type { Headers, RemoteAddr, SecurityDetails, WaitForEventOptions } from './types';
|
||||
import type { Serializable } from '../../types/structs';
|
||||
import type * as api from '../../types/types';
|
||||
import type { HeadersArray } from '../common/types';
|
||||
import type { URLMatch } from '../utils/isomorphic/urlMatch';
|
||||
import type { Zone } from '../utils/zones';
|
||||
import type * as channels from '@protocol/channels';
|
||||
|
||||
export type NetworkCookie = {
|
||||
|
|
@ -387,7 +393,7 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
|||
let isBase64 = false;
|
||||
let length = 0;
|
||||
if (options.path) {
|
||||
const buffer = await fs.promises.readFile(options.path);
|
||||
const buffer = await this._platform.fs().promises.readFile(options.path);
|
||||
body = buffer.toString('base64');
|
||||
isBase64 = true;
|
||||
length = buffer.length;
|
||||
|
|
|
|||
|
|
@ -15,12 +15,6 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { TargetClosedError, isTargetClosedError, serializeError } from './errors';
|
||||
import { TimeoutSettings } from '../common/timeoutSettings';
|
||||
import { LongStandingScope, assert, headersObjectToArray, isObject, isRegExp, isString, mkdirIfNeeded, trimStringWithEllipsis, urlMatches, urlMatchesEqual } from '../utils';
|
||||
import { Accessibility } from './accessibility';
|
||||
import { Artifact } from './artifact';
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
|
|
@ -28,16 +22,25 @@ import { evaluationScript } from './clientHelper';
|
|||
import { Coverage } from './coverage';
|
||||
import { Download } from './download';
|
||||
import { ElementHandle, determineScreenshotType } from './elementHandle';
|
||||
import { TargetClosedError, isTargetClosedError, serializeError } from './errors';
|
||||
import { Events } from './events';
|
||||
import { FileChooser } from './fileChooser';
|
||||
import { Frame, verifyLoadState } from './frame';
|
||||
import { HarRouter } from './harRouter';
|
||||
import { Keyboard, Mouse, Touchscreen } from './input';
|
||||
import { JSHandle, assertMaxArguments, parseResult, serializeArgument } from './jsHandle';
|
||||
import { Response, Route, RouteHandler, WebSocket, WebSocketRoute, WebSocketRouteHandler, validateHeaders } from './network';
|
||||
import { Response, Route, RouteHandler, WebSocket, WebSocketRoute, WebSocketRouteHandler, validateHeaders } from './network';
|
||||
import { Video } from './video';
|
||||
import { Waiter } from './waiter';
|
||||
import { Worker } from './worker';
|
||||
import { TimeoutSettings } from '../common/timeoutSettings';
|
||||
import { assert } from '../utils/debug';
|
||||
import { mkdirIfNeeded } from '../utils/fileUtils';
|
||||
import { headersObjectToArray } from '../utils/headers';
|
||||
import { trimStringWithEllipsis } from '../utils/isomorphic/stringUtils';
|
||||
import { urlMatches, urlMatchesEqual } from '../utils/isomorphic/urlMatch';
|
||||
import { LongStandingScope } from '../utils/manualPromise';
|
||||
import { isObject, isRegExp, isString } from '../utils/rtti';
|
||||
|
||||
import type { BrowserContext } from './browserContext';
|
||||
import type { Clock } from './clock';
|
||||
|
|
@ -48,8 +51,8 @@ import type { Request, RouteHandlerCallback, WebSocketRouteHandlerCallback } fro
|
|||
import type { FilePayload, Headers, LifecycleEvent, SelectOption, SelectOptionOptions, Size, WaitForEventOptions, WaitForFunctionOptions } from './types';
|
||||
import type * as structs from '../../types/structs';
|
||||
import type * as api from '../../types/types';
|
||||
import type { URLMatch } from '../utils';
|
||||
import type { ByRoleOptions } from '../utils/isomorphic/locatorUtils';
|
||||
import type { URLMatch } from '../utils/isomorphic/urlMatch';
|
||||
import type * as channels from '@protocol/channels';
|
||||
|
||||
type PDFOptions = Omit<channels.PagePdfParams, 'width' | 'height' | 'margin'> & {
|
||||
|
|
@ -512,7 +515,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
}
|
||||
|
||||
async addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any) {
|
||||
const source = await evaluationScript(script, arg);
|
||||
const source = await evaluationScript(this._platform, script, arg);
|
||||
await this._channel.addInitScript({ source });
|
||||
}
|
||||
|
||||
|
|
@ -590,8 +593,8 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
}
|
||||
const result = await this._channel.screenshot(copy);
|
||||
if (options.path) {
|
||||
await mkdirIfNeeded(options.path);
|
||||
await fs.promises.writeFile(options.path, result.binary);
|
||||
await mkdirIfNeeded(this._platform, options.path);
|
||||
await this._platform.fs().promises.writeFile(options.path, result.binary);
|
||||
}
|
||||
return result.binary;
|
||||
}
|
||||
|
|
@ -820,8 +823,9 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
}
|
||||
const result = await this._channel.pdf(transportOptions);
|
||||
if (options.path) {
|
||||
await fs.promises.mkdir(path.dirname(options.path), { recursive: true });
|
||||
await fs.promises.writeFile(options.path, result.pdf);
|
||||
const platform = this._platform;
|
||||
await platform.fs().promises.mkdir(platform.path().dirname(options.path), { recursive: true });
|
||||
await platform.fs().promises.writeFile(options.path, result.pdf);
|
||||
}
|
||||
return result.pdf;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
import { ChannelOwner } from './channelOwner';
|
||||
import { evaluationScript } from './clientHelper';
|
||||
import { setTestIdAttribute, testIdAttributeName } from './locator';
|
||||
import { nodePlatform } from '../utils/platform';
|
||||
|
||||
import type { SelectorEngine } from './types';
|
||||
import type * as api from '../../types/types';
|
||||
|
|
@ -28,7 +29,7 @@ export class Selectors implements api.Selectors {
|
|||
private _registrations: channels.SelectorsRegisterParams[] = [];
|
||||
|
||||
async register(name: string, script: string | (() => SelectorEngine) | { path?: string, content?: string }, options: { contentScript?: boolean } = {}): Promise<void> {
|
||||
const source = await evaluationScript(script, undefined, false);
|
||||
const source = await evaluationScript(nodePlatform, script, undefined, false);
|
||||
const params = { ...options, name, source };
|
||||
for (const channel of this._channels)
|
||||
await channel._channel.register(params);
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
|
|||
this._isTracing = true;
|
||||
this._connection.setIsTracing(true);
|
||||
}
|
||||
const result = await this._connection.localUtils()._channel.tracingStarted({ tracesDir: this._tracesDir, traceName });
|
||||
const result = await this._connection.localUtils().tracingStarted({ tracesDir: this._tracesDir, traceName });
|
||||
this._stacksId = result.stacksId;
|
||||
}
|
||||
|
||||
|
|
@ -89,7 +89,7 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
|
|||
// Not interested in artifacts.
|
||||
await this._channel.tracingStopChunk({ mode: 'discard' });
|
||||
if (this._stacksId)
|
||||
await this._connection.localUtils()._channel.traceDiscarded({ stacksId: this._stacksId });
|
||||
await this._connection.localUtils().traceDiscarded({ stacksId: this._stacksId });
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -97,7 +97,7 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
|
|||
|
||||
if (isLocal) {
|
||||
const result = await this._channel.tracingStopChunk({ mode: 'entries' });
|
||||
await this._connection.localUtils()._channel.zip({ zipFile: filePath, entries: result.entries!, mode: 'write', stacksId: this._stacksId, includeSources: this._includeSources });
|
||||
await this._connection.localUtils().zip({ zipFile: filePath, entries: result.entries!, mode: 'write', stacksId: this._stacksId, includeSources: this._includeSources });
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -106,7 +106,7 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
|
|||
// The artifact may be missing if the browser closed while stopping tracing.
|
||||
if (!result.artifact) {
|
||||
if (this._stacksId)
|
||||
await this._connection.localUtils()._channel.traceDiscarded({ stacksId: this._stacksId });
|
||||
await this._connection.localUtils().traceDiscarded({ stacksId: this._stacksId });
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -115,7 +115,7 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
|
|||
await artifact.saveAs(filePath);
|
||||
await artifact.delete();
|
||||
|
||||
await this._connection.localUtils()._channel.zip({ zipFile: filePath, entries: [], mode: 'append', stacksId: this._stacksId, includeSources: this._includeSources });
|
||||
await this._connection.localUtils().zip({ zipFile: filePath, entries: [], mode: 'append', stacksId: this._stacksId, includeSources: this._includeSources });
|
||||
}
|
||||
|
||||
_resetStackCounter() {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ManualPromise } from '../utils';
|
||||
import { ManualPromise } from '../utils/manualPromise';
|
||||
|
||||
import type { Artifact } from './artifact';
|
||||
import type { Connection } from './connection';
|
||||
|
|
|
|||
|
|
@ -15,11 +15,12 @@
|
|||
*/
|
||||
|
||||
import { TimeoutError } from './errors';
|
||||
import { createGuid, zones } from '../utils';
|
||||
import { createGuid } from '../utils/crypto';
|
||||
import { rewriteErrorMessage } from '../utils/stackTrace';
|
||||
import { zones } from '../utils/zones';
|
||||
|
||||
import type { Zone } from '../utils';
|
||||
import type { ChannelOwner } from './channelOwner';
|
||||
import type { Zone } from '../utils/zones';
|
||||
import type * as channels from '@protocol/channels';
|
||||
import type { EventEmitter } from 'events';
|
||||
|
||||
|
|
|
|||
|
|
@ -15,10 +15,10 @@
|
|||
*/
|
||||
|
||||
import { ChannelOwner } from './channelOwner';
|
||||
import { TargetClosedError } from './errors';
|
||||
import { Events } from './events';
|
||||
import { JSHandle, assertMaxArguments, parseResult, serializeArgument } from './jsHandle';
|
||||
import { LongStandingScope } from '../utils';
|
||||
import { TargetClosedError } from './errors';
|
||||
import { LongStandingScope } from '../utils/manualPromise';
|
||||
|
||||
import type { BrowserContext } from './browserContext';
|
||||
import type { Page } from './page';
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
[*]
|
||||
../utils/
|
||||
../utilsBundle.ts
|
||||
../utilsBundle.ts
|
||||
../zipBundle.ts
|
||||
|
|
|
|||
23
packages/playwright-core/src/common/progress.ts
Normal file
23
packages/playwright-core/src/common/progress.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export interface Progress {
|
||||
log(message: string): void;
|
||||
timeUntilDeadline(): number;
|
||||
isRunning(): boolean;
|
||||
cleanupWhenAborted(cleanup: () => any): void;
|
||||
throwIfAborted(): void;
|
||||
}
|
||||
|
|
@ -21,11 +21,12 @@ import { DispatcherConnection, PlaywrightDispatcher, RootDispatcher, createPlayw
|
|||
|
||||
import type { Playwright as PlaywrightAPI } from './client/playwright';
|
||||
import type { Language } from './utils';
|
||||
import type { Platform } from './utils/platform';
|
||||
|
||||
export function createInProcessPlaywright(): PlaywrightAPI {
|
||||
export function createInProcessPlaywright(platform: Platform): PlaywrightAPI {
|
||||
const playwright = createPlaywright({ sdkLanguage: (process.env.PW_LANG_NAME as Language | undefined) || 'javascript' });
|
||||
|
||||
const clientConnection = new Connection(undefined, undefined);
|
||||
const clientConnection = new Connection(undefined, platform, undefined, []);
|
||||
clientConnection.useRawBuffers();
|
||||
const dispatcherConnection = new DispatcherConnection(true /* local */);
|
||||
|
||||
|
|
|
|||
|
|
@ -15,5 +15,6 @@
|
|||
*/
|
||||
|
||||
import { createInProcessPlaywright } from './inProcessFactory';
|
||||
import { nodePlatform } from './utils/platform';
|
||||
|
||||
module.exports = createInProcessPlaywright();
|
||||
module.exports = createInProcessPlaywright(nodePlatform);
|
||||
|
|
|
|||
|
|
@ -20,10 +20,10 @@ import * as path from 'path';
|
|||
import { Connection } from './client/connection';
|
||||
import { PipeTransport } from './protocol/transport';
|
||||
import { ManualPromise } from './utils/manualPromise';
|
||||
import { nodePlatform } from './utils/platform';
|
||||
|
||||
import type { Playwright } from './client/playwright';
|
||||
|
||||
|
||||
export async function start(env: any = {}): Promise<{ playwright: Playwright, stop: () => Promise<void> }> {
|
||||
const client = new PlaywrightClient(env);
|
||||
const playwright = await client._playwright;
|
||||
|
|
@ -48,7 +48,7 @@ class PlaywrightClient {
|
|||
this._driverProcess.unref();
|
||||
this._driverProcess.stderr!.on('data', data => process.stderr.write(data));
|
||||
|
||||
const connection = new Connection(undefined, undefined);
|
||||
const connection = new Connection(undefined, nodePlatform, undefined, []);
|
||||
const transport = new PipeTransport(this._driverProcess.stdin!, this._driverProcess.stdout!);
|
||||
connection.onmessage = message => transport.send(JSON.stringify(message));
|
||||
transport.onmessage = message => connection.dispatch(JSON.parse(message));
|
||||
|
|
|
|||
|
|
@ -23,15 +23,15 @@ import { TimeoutSettings } from '../../common/timeoutSettings';
|
|||
import { PipeTransport } from '../../protocol/transport';
|
||||
import { createGuid, getPackageManagerExecCommand, isUnderTest, makeWaitForNextTask } from '../../utils';
|
||||
import { RecentLogsCollector } from '../../utils/debugLogger';
|
||||
import { removeFolders } from '../../utils/fileUtils';
|
||||
import { gracefullyCloseSet } from '../../utils/processLauncher';
|
||||
import { debug } from '../../utilsBundle';
|
||||
import { wsReceiver, wsSender } from '../../utilsBundle';
|
||||
import { validateBrowserContextOptions } from '../browserContext';
|
||||
import { chromiumSwitches } from '../chromium/chromiumSwitches';
|
||||
import { CRBrowser } from '../chromium/crBrowser';
|
||||
import { removeFolders } from '../fileUtils';
|
||||
import { helper } from '../helper';
|
||||
import { SdkObject, serverSideCallMetadata } from '../instrumentation';
|
||||
import { gracefullyCloseSet } from '../processLauncher';
|
||||
import { ProgressController } from '../progress';
|
||||
import { registry } from '../registry';
|
||||
|
||||
|
|
|
|||
|
|
@ -22,9 +22,9 @@ import { BidiBrowser } from './bidiBrowser';
|
|||
import { kBrowserCloseMessageId } from './bidiConnection';
|
||||
import { chromiumSwitches } from '../chromium/chromiumSwitches';
|
||||
|
||||
import type { Env } from '../../utils/processLauncher';
|
||||
import type { BrowserOptions } from '../browser';
|
||||
import type { SdkObject } from '../instrumentation';
|
||||
import type { Env } from '../processLauncher';
|
||||
import type { ProtocolError } from '../protocolError';
|
||||
import type { ConnectionTransport } from '../transport';
|
||||
import type * as types from '../types';
|
||||
|
|
|
|||
|
|
@ -23,9 +23,9 @@ import { BidiBrowser } from './bidiBrowser';
|
|||
import { kBrowserCloseMessageId } from './bidiConnection';
|
||||
import { createProfile } from './third_party/firefoxPrefs';
|
||||
|
||||
import type { Env } from '../../utils/processLauncher';
|
||||
import type { BrowserOptions } from '../browser';
|
||||
import type { SdkObject } from '../instrumentation';
|
||||
import type { Env } from '../processLauncher';
|
||||
import type { ProtocolError } from '../protocolError';
|
||||
import type { ConnectionTransport } from '../transport';
|
||||
import type * as types from '../types';
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import { createGuid, debugMode } from '../utils';
|
|||
import { Clock } from './clock';
|
||||
import { Debugger } from './debugger';
|
||||
import { BrowserContextAPIRequestContext } from './fetch';
|
||||
import { mkdirIfNeeded } from './fileUtils';
|
||||
import { HarRecorder } from './har/harRecorder';
|
||||
import { helper } from './helper';
|
||||
import { SdkObject, serverSideCallMetadata } from './instrumentation';
|
||||
|
|
@ -31,9 +32,8 @@ import * as network from './network';
|
|||
import { InitScript } from './page';
|
||||
import { Page, PageBinding } from './page';
|
||||
import { Recorder } from './recorder';
|
||||
import * as storageScript from './storageScript';
|
||||
import { mkdirIfNeeded } from '../utils/fileUtils';
|
||||
import { RecorderApp } from './recorder/recorderApp';
|
||||
import * as storageScript from './storageScript';
|
||||
import * as consoleApiSource from '../generated/consoleApiSource';
|
||||
import { Tracing } from './trace/recorder/tracing';
|
||||
|
||||
|
|
|
|||
|
|
@ -21,27 +21,27 @@ import * as path from 'path';
|
|||
import { normalizeProxySettings, validateBrowserContextOptions } from './browserContext';
|
||||
import { DEFAULT_TIMEOUT, TimeoutSettings } from '../common/timeoutSettings';
|
||||
import { ManualPromise, assert, debugMode } from '../utils';
|
||||
import { existsAsync } from './fileUtils';
|
||||
import { helper } from './helper';
|
||||
import { SdkObject } from './instrumentation';
|
||||
import { PipeTransport } from './pipeTransport';
|
||||
import { envArrayToObject, launchProcess } from './processLauncher';
|
||||
import { ProgressController } from './progress';
|
||||
import { isProtocolError } from './protocolError';
|
||||
import { registry } from './registry';
|
||||
import { ClientCertificatesProxy } from './socksClientCertificatesInterceptor';
|
||||
import { WebSocketTransport } from './transport';
|
||||
import { RecentLogsCollector } from '../utils/debugLogger';
|
||||
import { existsAsync } from '../utils/fileUtils';
|
||||
import { envArrayToObject, launchProcess } from '../utils/processLauncher';
|
||||
|
||||
import type { Browser, BrowserOptions, BrowserProcess } from './browser';
|
||||
import type { BrowserContext } from './browserContext';
|
||||
import type { CallMetadata } from './instrumentation';
|
||||
import type { Env } from './processLauncher';
|
||||
import type { Progress } from './progress';
|
||||
import type { ProtocolError } from './protocolError';
|
||||
import type { BrowserName } from './registry';
|
||||
import type { ConnectionTransport } from './transport';
|
||||
import type * as types from './types';
|
||||
import type { Env } from '../utils/processLauncher';
|
||||
import type * as channels from '@protocol/channels';
|
||||
|
||||
export const kNoXServerRunningError = 'Looks like you launched a headed browser without having a XServer running.\n' +
|
||||
|
|
|
|||
|
|
@ -26,10 +26,8 @@ import { TimeoutSettings } from '../../common/timeoutSettings';
|
|||
import { debugMode, headersArrayToObject, headersObjectToArray, } from '../../utils';
|
||||
import { wrapInASCIIBox } from '../../utils/ascii';
|
||||
import { RecentLogsCollector } from '../../utils/debugLogger';
|
||||
import { removeFolders } from '../../utils/fileUtils';
|
||||
import { ManualPromise } from '../../utils/manualPromise';
|
||||
import { fetchData } from '../../utils/network';
|
||||
import { gracefullyCloseSet } from '../../utils/processLauncher';
|
||||
import { getUserAgent } from '../../utils/userAgent';
|
||||
import { validateBrowserContextOptions } from '../browserContext';
|
||||
import { BrowserType, kNoXServerRunningError } from '../browserType';
|
||||
|
|
@ -39,12 +37,14 @@ import { registry } from '../registry';
|
|||
import { WebSocketTransport } from '../transport';
|
||||
import { CRDevTools } from './crDevTools';
|
||||
import { Browser } from '../browser';
|
||||
import { removeFolders } from '../fileUtils';
|
||||
import { gracefullyCloseSet } from '../processLauncher';
|
||||
import { ProgressController } from '../progress';
|
||||
|
||||
import type { HTTPRequestParams } from '../../utils/network';
|
||||
import type { Env } from '../../utils/processLauncher';
|
||||
import type { BrowserOptions, BrowserProcess } from '../browser';
|
||||
import type { CallMetadata, SdkObject } from '../instrumentation';
|
||||
import type { Env } from '../processLauncher';
|
||||
import type { Progress } from '../progress';
|
||||
import type { ProtocolError } from '../protocolError';
|
||||
import type { ConnectionTransport, ProtocolRequest } from '../transport';
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@
|
|||
|
||||
import * as fs from 'fs';
|
||||
|
||||
import { mkdirIfNeeded } from '../../utils/fileUtils';
|
||||
import { splitErrorMessage } from '../../utils/stackTrace';
|
||||
import { mkdirIfNeeded } from '../fileUtils';
|
||||
|
||||
import type { CRSession } from './crConnection';
|
||||
import type { Protocol } from './protocol';
|
||||
|
|
|
|||
|
|
@ -15,9 +15,9 @@
|
|||
*/
|
||||
|
||||
import { assert, monotonicTime } from '../../utils';
|
||||
import { launchProcess } from '../../utils/processLauncher';
|
||||
import { serverSideCallMetadata } from '../instrumentation';
|
||||
import { Page } from '../page';
|
||||
import { launchProcess } from '../processLauncher';
|
||||
import { ProgressController } from '../progress';
|
||||
|
||||
import type { Progress } from '../progress';
|
||||
|
|
|
|||
|
|
@ -15,13 +15,13 @@
|
|||
*/
|
||||
|
||||
import { SdkObject, createInstrumentation, serverSideCallMetadata } from './instrumentation';
|
||||
import { gracefullyProcessExitDoNotHang } from './processLauncher';
|
||||
import { Recorder } from './recorder';
|
||||
import { asLocator } from '../utils';
|
||||
import { parseAriaSnapshotUnsafe } from '../utils/isomorphic/ariaSnapshot';
|
||||
import { yaml } from '../utilsBundle';
|
||||
import { EmptyRecorderApp } from './recorder/recorderApp';
|
||||
import { unsafeLocatorOrSelectorAsSelector } from '../utils/isomorphic/locatorParser';
|
||||
import { gracefullyProcessExitDoNotHang } from '../utils/processLauncher';
|
||||
|
||||
import type { Language } from '../utils';
|
||||
import type { Browser } from './browser';
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import * as fs from 'fs';
|
|||
|
||||
import { Dispatcher, existingDispatcher } from './dispatcher';
|
||||
import { StreamDispatcher } from './streamDispatcher';
|
||||
import { mkdirIfNeeded } from '../../utils/fileUtils';
|
||||
import { mkdirIfNeeded } from '../fileUtils';
|
||||
|
||||
import type { DispatcherScope } from './dispatcher';
|
||||
import type { Artifact } from '../artifact';
|
||||
|
|
|
|||
|
|
@ -14,45 +14,27 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
import { Dispatcher } from './dispatcher';
|
||||
import { SdkObject } from '../../server/instrumentation';
|
||||
import { assert, calculateSha1, createGuid, removeFolders } from '../../utils';
|
||||
import { serializeClientSideCallMetadata } from '../../utils';
|
||||
import { ManualPromise } from '../../utils/manualPromise';
|
||||
import { fetchData } from '../../utils/network';
|
||||
import * as localUtils from '../../utils/localUtils';
|
||||
import { nodePlatform } from '../../utils/platform';
|
||||
import { getUserAgent } from '../../utils/userAgent';
|
||||
import { ZipFile } from '../../utils/zipFile';
|
||||
import { yauzl, yazl } from '../../zipBundle';
|
||||
import { deviceDescriptors as descriptors } from '../deviceDescriptors';
|
||||
import { JsonPipeDispatcher } from '../dispatchers/jsonPipeDispatcher';
|
||||
import { ProgressController } from '../progress';
|
||||
import { SocksInterceptor } from '../socksInterceptor';
|
||||
import { WebSocketTransport } from '../transport';
|
||||
|
||||
import type { HTTPRequestParams } from '../../utils/network';
|
||||
import type { HarBackend } from '../../utils/harBackend';
|
||||
import type { CallMetadata } from '../instrumentation';
|
||||
import type { Playwright } from '../playwright';
|
||||
import type { Progress } from '../progress';
|
||||
import type { HeadersArray } from '../types';
|
||||
import type { RootDispatcher } from './dispatcher';
|
||||
import type * as channels from '@protocol/channels';
|
||||
import type * as har from '@trace/har';
|
||||
import type EventEmitter from 'events';
|
||||
import type http from 'http';
|
||||
|
||||
export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.LocalUtilsChannel, RootDispatcher> implements channels.LocalUtilsChannel {
|
||||
_type_LocalUtils: boolean;
|
||||
private _harBackends = new Map<string, HarBackend>();
|
||||
private _stackSessions = new Map<string, {
|
||||
file: string,
|
||||
writer: Promise<void>,
|
||||
tmpDir: string | undefined,
|
||||
callStacks: channels.ClientSideCallMetadata[]
|
||||
}>();
|
||||
private _stackSessions = new Map<string, localUtils.StackSession>();
|
||||
|
||||
constructor(scope: RootDispatcher, playwright: Playwright) {
|
||||
const localUtils = new SdkObject(playwright, 'localUtils', 'localUtils');
|
||||
|
|
@ -65,139 +47,35 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
|
|||
}
|
||||
|
||||
async zip(params: channels.LocalUtilsZipParams): Promise<void> {
|
||||
const promise = new ManualPromise<void>();
|
||||
const zipFile = new yazl.ZipFile();
|
||||
(zipFile as any as EventEmitter).on('error', error => promise.reject(error));
|
||||
|
||||
const addFile = (file: string, name: string) => {
|
||||
try {
|
||||
if (fs.statSync(file).isFile())
|
||||
zipFile.addFile(file, name);
|
||||
} catch (e) {
|
||||
}
|
||||
};
|
||||
|
||||
for (const entry of params.entries)
|
||||
addFile(entry.value, entry.name);
|
||||
|
||||
// Add stacks and the sources.
|
||||
const stackSession = params.stacksId ? this._stackSessions.get(params.stacksId) : undefined;
|
||||
if (stackSession?.callStacks.length) {
|
||||
await stackSession.writer;
|
||||
if (process.env.PW_LIVE_TRACE_STACKS) {
|
||||
zipFile.addFile(stackSession.file, 'trace.stacks');
|
||||
} else {
|
||||
const buffer = Buffer.from(JSON.stringify(serializeClientSideCallMetadata(stackSession.callStacks)));
|
||||
zipFile.addBuffer(buffer, 'trace.stacks');
|
||||
}
|
||||
}
|
||||
|
||||
// Collect sources from stacks.
|
||||
if (params.includeSources) {
|
||||
const sourceFiles = new Set<string>();
|
||||
for (const { stack } of stackSession?.callStacks || []) {
|
||||
if (!stack)
|
||||
continue;
|
||||
for (const { file } of stack)
|
||||
sourceFiles.add(file);
|
||||
}
|
||||
for (const sourceFile of sourceFiles)
|
||||
addFile(sourceFile, 'resources/src@' + calculateSha1(sourceFile) + '.txt');
|
||||
}
|
||||
|
||||
if (params.mode === 'write') {
|
||||
// New file, just compress the entries.
|
||||
await fs.promises.mkdir(path.dirname(params.zipFile), { recursive: true });
|
||||
zipFile.end(undefined, () => {
|
||||
zipFile.outputStream.pipe(fs.createWriteStream(params.zipFile))
|
||||
.on('close', () => promise.resolve())
|
||||
.on('error', error => promise.reject(error));
|
||||
});
|
||||
await promise;
|
||||
await this._deleteStackSession(params.stacksId);
|
||||
return;
|
||||
}
|
||||
|
||||
// File already exists. Repack and add new entries.
|
||||
const tempFile = params.zipFile + '.tmp';
|
||||
await fs.promises.rename(params.zipFile, tempFile);
|
||||
|
||||
yauzl.open(tempFile, (err, inZipFile) => {
|
||||
if (err) {
|
||||
promise.reject(err);
|
||||
return;
|
||||
}
|
||||
assert(inZipFile);
|
||||
let pendingEntries = inZipFile.entryCount;
|
||||
inZipFile.on('entry', entry => {
|
||||
inZipFile.openReadStream(entry, (err, readStream) => {
|
||||
if (err) {
|
||||
promise.reject(err);
|
||||
return;
|
||||
}
|
||||
zipFile.addReadStream(readStream!, entry.fileName);
|
||||
if (--pendingEntries === 0) {
|
||||
zipFile.end(undefined, () => {
|
||||
zipFile.outputStream.pipe(fs.createWriteStream(params.zipFile)).on('close', () => {
|
||||
fs.promises.unlink(tempFile).then(() => {
|
||||
promise.resolve();
|
||||
}).catch(error => promise.reject(error));
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
await promise;
|
||||
await this._deleteStackSession(params.stacksId);
|
||||
return await localUtils.zip(nodePlatform, this._stackSessions, params);
|
||||
}
|
||||
|
||||
async harOpen(params: channels.LocalUtilsHarOpenParams, metadata: CallMetadata): Promise<channels.LocalUtilsHarOpenResult> {
|
||||
let harBackend: HarBackend;
|
||||
if (params.file.endsWith('.zip')) {
|
||||
const zipFile = new ZipFile(params.file);
|
||||
const entryNames = await zipFile.entries();
|
||||
const harEntryName = entryNames.find(e => e.endsWith('.har'));
|
||||
if (!harEntryName)
|
||||
return { error: 'Specified archive does not have a .har file' };
|
||||
const har = await zipFile.read(harEntryName);
|
||||
const harFile = JSON.parse(har.toString()) as har.HARFile;
|
||||
harBackend = new HarBackend(harFile, null, zipFile);
|
||||
} else {
|
||||
const harFile = JSON.parse(await fs.promises.readFile(params.file, 'utf-8')) as har.HARFile;
|
||||
harBackend = new HarBackend(harFile, path.dirname(params.file), null);
|
||||
}
|
||||
this._harBackends.set(harBackend.id, harBackend);
|
||||
return { harId: harBackend.id };
|
||||
return await localUtils.harOpen(this._harBackends, params);
|
||||
}
|
||||
|
||||
async harLookup(params: channels.LocalUtilsHarLookupParams, metadata: CallMetadata): Promise<channels.LocalUtilsHarLookupResult> {
|
||||
const harBackend = this._harBackends.get(params.harId);
|
||||
if (!harBackend)
|
||||
return { action: 'error', message: `Internal error: har was not opened` };
|
||||
return await harBackend.lookup(params.url, params.method, params.headers, params.postData, params.isNavigationRequest);
|
||||
return await localUtils.harLookup(this._harBackends, params);
|
||||
}
|
||||
|
||||
async harClose(params: channels.LocalUtilsHarCloseParams, metadata: CallMetadata): Promise<void> {
|
||||
const harBackend = this._harBackends.get(params.harId);
|
||||
if (harBackend) {
|
||||
this._harBackends.delete(harBackend.id);
|
||||
harBackend.dispose();
|
||||
}
|
||||
return await localUtils.harClose(this._harBackends, params);
|
||||
}
|
||||
|
||||
async harUnzip(params: channels.LocalUtilsHarUnzipParams, metadata: CallMetadata): Promise<void> {
|
||||
const dir = path.dirname(params.zipFile);
|
||||
const zipFile = new ZipFile(params.zipFile);
|
||||
for (const entry of await zipFile.entries()) {
|
||||
const buffer = await zipFile.read(entry);
|
||||
if (entry === 'har.har')
|
||||
await fs.promises.writeFile(params.harFile, buffer);
|
||||
else
|
||||
await fs.promises.writeFile(path.join(dir, entry), buffer);
|
||||
}
|
||||
zipFile.close();
|
||||
await fs.promises.unlink(params.zipFile);
|
||||
return await localUtils.harUnzip(params);
|
||||
}
|
||||
|
||||
async tracingStarted(params: channels.LocalUtilsTracingStartedParams, metadata?: CallMetadata | undefined): Promise<channels.LocalUtilsTracingStartedResult> {
|
||||
return await localUtils.tracingStarted(this._stackSessions, params);
|
||||
}
|
||||
|
||||
async traceDiscarded(params: channels.LocalUtilsTraceDiscardedParams, metadata?: CallMetadata | undefined): Promise<void> {
|
||||
return await localUtils.traceDiscarded(nodePlatform, this._stackSessions, params);
|
||||
}
|
||||
|
||||
async addStackToTracingNoReply(params: channels.LocalUtilsAddStackToTracingNoReplyParams, metadata?: CallMetadata | undefined): Promise<void> {
|
||||
return await localUtils.addStackToTracingNoReply(this._stackSessions, params);
|
||||
}
|
||||
|
||||
async connect(params: channels.LocalUtilsConnectParams, metadata: CallMetadata): Promise<channels.LocalUtilsConnectResult> {
|
||||
|
|
@ -209,7 +87,7 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
|
|||
'x-playwright-proxy': params.exposeNetwork ?? '',
|
||||
...params.headers,
|
||||
};
|
||||
const wsEndpoint = await urlToWSEndpoint(progress, params.wsEndpoint);
|
||||
const wsEndpoint = await localUtils.urlToWSEndpoint(progress, params.wsEndpoint);
|
||||
|
||||
const transport = await WebSocketTransport.connect(progress, wsEndpoint, wsHeaders, true, 'x-playwright-debug-log');
|
||||
const socksInterceptor = new SocksInterceptor(transport, params.exposeNetwork, params.socksProxyRedirectPortForTest);
|
||||
|
|
@ -240,221 +118,4 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
|
|||
return { pipe, headers: transport.headers };
|
||||
}, params.timeout || 0);
|
||||
}
|
||||
|
||||
async tracingStarted(params: channels.LocalUtilsTracingStartedParams, metadata?: CallMetadata | undefined): Promise<channels.LocalUtilsTracingStartedResult> {
|
||||
let tmpDir = undefined;
|
||||
if (!params.tracesDir)
|
||||
tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'playwright-tracing-'));
|
||||
const traceStacksFile = path.join(params.tracesDir || tmpDir!, params.traceName + '.stacks');
|
||||
this._stackSessions.set(traceStacksFile, { callStacks: [], file: traceStacksFile, writer: Promise.resolve(), tmpDir });
|
||||
return { stacksId: traceStacksFile };
|
||||
}
|
||||
|
||||
async traceDiscarded(params: channels.LocalUtilsTraceDiscardedParams, metadata?: CallMetadata | undefined): Promise<void> {
|
||||
await this._deleteStackSession(params.stacksId);
|
||||
}
|
||||
|
||||
async addStackToTracingNoReply(params: channels.LocalUtilsAddStackToTracingNoReplyParams, metadata?: CallMetadata | undefined): Promise<void> {
|
||||
for (const session of this._stackSessions.values()) {
|
||||
session.callStacks.push(params.callData);
|
||||
if (process.env.PW_LIVE_TRACE_STACKS) {
|
||||
session.writer = session.writer.then(() => {
|
||||
const buffer = Buffer.from(JSON.stringify(serializeClientSideCallMetadata(session.callStacks)));
|
||||
return fs.promises.writeFile(session.file, buffer);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _deleteStackSession(stacksId?: string) {
|
||||
const session = stacksId ? this._stackSessions.get(stacksId) : undefined;
|
||||
if (!session)
|
||||
return;
|
||||
await session.writer;
|
||||
if (session.tmpDir)
|
||||
await removeFolders([session.tmpDir]);
|
||||
this._stackSessions.delete(stacksId!);
|
||||
}
|
||||
}
|
||||
|
||||
const redirectStatus = [301, 302, 303, 307, 308];
|
||||
|
||||
class HarBackend {
|
||||
readonly id = createGuid();
|
||||
private _harFile: har.HARFile;
|
||||
private _zipFile: ZipFile | null;
|
||||
private _baseDir: string | null;
|
||||
|
||||
constructor(harFile: har.HARFile, baseDir: string | null, zipFile: ZipFile | null) {
|
||||
this._harFile = harFile;
|
||||
this._baseDir = baseDir;
|
||||
this._zipFile = zipFile;
|
||||
}
|
||||
|
||||
async lookup(url: string, method: string, headers: HeadersArray, postData: Buffer | undefined, isNavigationRequest: boolean): Promise<{
|
||||
action: 'error' | 'redirect' | 'fulfill' | 'noentry',
|
||||
message?: string,
|
||||
redirectURL?: string,
|
||||
status?: number,
|
||||
headers?: HeadersArray,
|
||||
body?: Buffer }> {
|
||||
let entry;
|
||||
try {
|
||||
entry = await this._harFindResponse(url, method, headers, postData);
|
||||
} catch (e) {
|
||||
return { action: 'error', message: 'HAR error: ' + e.message };
|
||||
}
|
||||
|
||||
if (!entry)
|
||||
return { action: 'noentry' };
|
||||
|
||||
// If navigation is being redirected, restart it with the final url to ensure the document's url changes.
|
||||
if (entry.request.url !== url && isNavigationRequest)
|
||||
return { action: 'redirect', redirectURL: entry.request.url };
|
||||
|
||||
const response = entry.response;
|
||||
try {
|
||||
const buffer = await this._loadContent(response.content);
|
||||
return {
|
||||
action: 'fulfill',
|
||||
status: response.status,
|
||||
headers: response.headers,
|
||||
body: buffer,
|
||||
};
|
||||
} catch (e) {
|
||||
return { action: 'error', message: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadContent(content: { text?: string, encoding?: string, _file?: string }): Promise<Buffer> {
|
||||
const file = content._file;
|
||||
let buffer: Buffer;
|
||||
if (file) {
|
||||
if (this._zipFile)
|
||||
buffer = await this._zipFile.read(file);
|
||||
else
|
||||
buffer = await fs.promises.readFile(path.resolve(this._baseDir!, file));
|
||||
} else {
|
||||
buffer = Buffer.from(content.text || '', content.encoding === 'base64' ? 'base64' : 'utf-8');
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private async _harFindResponse(url: string, method: string, headers: HeadersArray, postData: Buffer | undefined): Promise<har.Entry | undefined> {
|
||||
const harLog = this._harFile.log;
|
||||
const visited = new Set<har.Entry>();
|
||||
while (true) {
|
||||
const entries: har.Entry[] = [];
|
||||
for (const candidate of harLog.entries) {
|
||||
if (candidate.request.url !== url || candidate.request.method !== method)
|
||||
continue;
|
||||
if (method === 'POST' && postData && candidate.request.postData) {
|
||||
const buffer = await this._loadContent(candidate.request.postData);
|
||||
if (!buffer.equals(postData)) {
|
||||
const boundary = multipartBoundary(headers);
|
||||
if (!boundary)
|
||||
continue;
|
||||
const candidataBoundary = multipartBoundary(candidate.request.headers);
|
||||
if (!candidataBoundary)
|
||||
continue;
|
||||
// Try to match multipart/form-data ignroing boundary as it changes between requests.
|
||||
if (postData.toString().replaceAll(boundary, '') !== buffer.toString().replaceAll(candidataBoundary, ''))
|
||||
continue;
|
||||
}
|
||||
}
|
||||
entries.push(candidate);
|
||||
}
|
||||
|
||||
if (!entries.length)
|
||||
return;
|
||||
|
||||
let entry = entries[0];
|
||||
|
||||
// Disambiguate using headers - then one with most matching headers wins.
|
||||
if (entries.length > 1) {
|
||||
const list: { candidate: har.Entry, matchingHeaders: number }[] = [];
|
||||
for (const candidate of entries) {
|
||||
const matchingHeaders = countMatchingHeaders(candidate.request.headers, headers);
|
||||
list.push({ candidate, matchingHeaders });
|
||||
}
|
||||
list.sort((a, b) => b.matchingHeaders - a.matchingHeaders);
|
||||
entry = list[0].candidate;
|
||||
}
|
||||
|
||||
if (visited.has(entry))
|
||||
throw new Error(`Found redirect cycle for ${url}`);
|
||||
|
||||
visited.add(entry);
|
||||
|
||||
// Follow redirects.
|
||||
const locationHeader = entry.response.headers.find(h => h.name.toLowerCase() === 'location');
|
||||
if (redirectStatus.includes(entry.response.status) && locationHeader) {
|
||||
const locationURL = new URL(locationHeader.value, url);
|
||||
url = locationURL.toString();
|
||||
if ((entry.response.status === 301 || entry.response.status === 302) && method === 'POST' ||
|
||||
entry.response.status === 303 && !['GET', 'HEAD'].includes(method)) {
|
||||
// HTTP-redirect fetch step 13 (https://fetch.spec.whatwg.org/#http-redirect-fetch)
|
||||
method = 'GET';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._zipFile?.close();
|
||||
}
|
||||
}
|
||||
|
||||
function countMatchingHeaders(harHeaders: har.Header[], headers: HeadersArray): number {
|
||||
const set = new Set(headers.map(h => h.name.toLowerCase() + ':' + h.value));
|
||||
let matches = 0;
|
||||
for (const h of harHeaders) {
|
||||
if (set.has(h.name.toLowerCase() + ':' + h.value))
|
||||
++matches;
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
export async function urlToWSEndpoint(progress: Progress|undefined, endpointURL: string): Promise<string> {
|
||||
if (endpointURL.startsWith('ws'))
|
||||
return endpointURL;
|
||||
|
||||
progress?.log(`<ws preparing> retrieving websocket url from ${endpointURL}`);
|
||||
const fetchUrl = new URL(endpointURL);
|
||||
if (!fetchUrl.pathname.endsWith('/'))
|
||||
fetchUrl.pathname += '/';
|
||||
fetchUrl.pathname += 'json';
|
||||
const json = await fetchData({
|
||||
url: fetchUrl.toString(),
|
||||
method: 'GET',
|
||||
timeout: progress?.timeUntilDeadline() ?? 30_000,
|
||||
headers: { 'User-Agent': getUserAgent() },
|
||||
}, async (params: HTTPRequestParams, response: http.IncomingMessage) => {
|
||||
return new Error(`Unexpected status ${response.statusCode} when connecting to ${fetchUrl.toString()}.\n` +
|
||||
`This does not look like a Playwright server, try connecting via ws://.`);
|
||||
});
|
||||
progress?.throwIfAborted();
|
||||
|
||||
const wsUrl = new URL(endpointURL);
|
||||
let wsEndpointPath = JSON.parse(json).wsEndpointPath;
|
||||
if (wsEndpointPath.startsWith('/'))
|
||||
wsEndpointPath = wsEndpointPath.substring(1);
|
||||
if (!wsUrl.pathname.endsWith('/'))
|
||||
wsUrl.pathname += '/';
|
||||
wsUrl.pathname += wsEndpointPath;
|
||||
wsUrl.protocol = wsUrl.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
return wsUrl.toString();
|
||||
}
|
||||
|
||||
function multipartBoundary(headers: HeadersArray) {
|
||||
const contentType = headers.find(h => h.name.toLowerCase() === 'content-type');
|
||||
if (!contentType?.value.includes('multipart/form-data'))
|
||||
return undefined;
|
||||
const boundary = contentType.value.match(/boundary=(\S+)/);
|
||||
if (boundary)
|
||||
return boundary[1];
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import { TimeoutSettings } from '../../common/timeoutSettings';
|
|||
import { ManualPromise, wrapInASCIIBox } from '../../utils';
|
||||
import { RecentLogsCollector } from '../../utils/debugLogger';
|
||||
import { eventsHelper } from '../../utils/eventsHelper';
|
||||
import { envArrayToObject, launchProcess } from '../../utils/processLauncher';
|
||||
import { validateBrowserContextOptions } from '../browserContext';
|
||||
import { CRBrowser } from '../chromium/crBrowser';
|
||||
import { CRConnection } from '../chromium/crConnection';
|
||||
|
|
@ -33,6 +32,7 @@ import { ConsoleMessage } from '../console';
|
|||
import { helper } from '../helper';
|
||||
import { SdkObject, serverSideCallMetadata } from '../instrumentation';
|
||||
import * as js from '../javascript';
|
||||
import { envArrayToObject, launchProcess } from '../processLauncher';
|
||||
import { ProgressController } from '../progress';
|
||||
import { WebSocketTransport } from '../transport';
|
||||
|
||||
|
|
|
|||
205
packages/playwright-core/src/server/fileUtils.ts
Normal file
205
packages/playwright-core/src/server/fileUtils.ts
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { ManualPromise } from '../utils/manualPromise';
|
||||
import { yazl } from '../zipBundle';
|
||||
|
||||
import type { EventEmitter } from 'events';
|
||||
|
||||
export const existsAsync = (path: string): Promise<boolean> => new Promise(resolve => fs.stat(path, err => resolve(!err)));
|
||||
|
||||
export async function mkdirIfNeeded(filePath: string) {
|
||||
// This will harmlessly throw on windows if the dirname is the root directory.
|
||||
await fs.promises.mkdir(path.dirname(filePath), { recursive: true }).catch(() => {});
|
||||
}
|
||||
|
||||
export async function removeFolders(dirs: string[]): Promise<Error[]> {
|
||||
return await Promise.all(dirs.map((dir: string) =>
|
||||
fs.promises.rm(dir, { recursive: true, force: true, maxRetries: 10 }).catch(e => e)
|
||||
));
|
||||
}
|
||||
|
||||
export function canAccessFile(file: string) {
|
||||
if (!file)
|
||||
return false;
|
||||
|
||||
try {
|
||||
fs.accessSync(file);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function copyFileAndMakeWritable(from: string, to: string) {
|
||||
await fs.promises.copyFile(from, to);
|
||||
await fs.promises.chmod(to, 0o664);
|
||||
}
|
||||
|
||||
export function sanitizeForFilePath(s: string) {
|
||||
return s.replace(/[\x00-\x2C\x2E-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, '-');
|
||||
}
|
||||
|
||||
export function toPosixPath(aPath: string): string {
|
||||
return aPath.split(path.sep).join(path.posix.sep);
|
||||
}
|
||||
|
||||
type NameValue = { name: string, value: string };
|
||||
type SerializedFSOperation = {
|
||||
op: 'mkdir', dir: string,
|
||||
} | {
|
||||
op: 'writeFile', file: string, content: string | Buffer, skipIfExists?: boolean,
|
||||
} | {
|
||||
op: 'appendFile', file: string, content: string,
|
||||
} | {
|
||||
op: 'copyFile', from: string, to: string,
|
||||
} | {
|
||||
op: 'zip', entries: NameValue[], zipFileName: string,
|
||||
};
|
||||
|
||||
export class SerializedFS {
|
||||
private _buffers = new Map<string, string[]>(); // Should never be accessed from within appendOperation.
|
||||
private _error: Error | undefined;
|
||||
private _operations: SerializedFSOperation[] = [];
|
||||
private _operationsDone: ManualPromise<void>;
|
||||
|
||||
constructor() {
|
||||
this._operationsDone = new ManualPromise();
|
||||
this._operationsDone.resolve(); // No operations scheduled yet.
|
||||
}
|
||||
|
||||
mkdir(dir: string) {
|
||||
this._appendOperation({ op: 'mkdir', dir });
|
||||
}
|
||||
|
||||
writeFile(file: string, content: string | Buffer, skipIfExists?: boolean) {
|
||||
this._buffers.delete(file); // No need to flush the buffer since we'll overwrite anyway.
|
||||
this._appendOperation({ op: 'writeFile', file, content, skipIfExists });
|
||||
}
|
||||
|
||||
appendFile(file: string, text: string, flush?: boolean) {
|
||||
if (!this._buffers.has(file))
|
||||
this._buffers.set(file, []);
|
||||
this._buffers.get(file)!.push(text);
|
||||
if (flush)
|
||||
this._flushFile(file);
|
||||
}
|
||||
|
||||
private _flushFile(file: string) {
|
||||
const buffer = this._buffers.get(file);
|
||||
if (buffer === undefined)
|
||||
return;
|
||||
const content = buffer.join('');
|
||||
this._buffers.delete(file);
|
||||
this._appendOperation({ op: 'appendFile', file, content });
|
||||
}
|
||||
|
||||
copyFile(from: string, to: string) {
|
||||
this._flushFile(from);
|
||||
this._buffers.delete(to); // No need to flush the buffer since we'll overwrite anyway.
|
||||
this._appendOperation({ op: 'copyFile', from, to });
|
||||
}
|
||||
|
||||
async syncAndGetError() {
|
||||
for (const file of this._buffers.keys())
|
||||
this._flushFile(file);
|
||||
await this._operationsDone;
|
||||
return this._error;
|
||||
}
|
||||
|
||||
zip(entries: NameValue[], zipFileName: string) {
|
||||
for (const file of this._buffers.keys())
|
||||
this._flushFile(file);
|
||||
|
||||
// Chain the export operation against write operations,
|
||||
// so that files do not change during the export.
|
||||
this._appendOperation({ op: 'zip', entries, zipFileName });
|
||||
}
|
||||
|
||||
// This method serializes all writes to the trace.
|
||||
private _appendOperation(op: SerializedFSOperation): void {
|
||||
const last = this._operations[this._operations.length - 1];
|
||||
if (last?.op === 'appendFile' && op.op === 'appendFile' && last.file === op.file) {
|
||||
// Merge pending appendFile operations for performance.
|
||||
last.content += op.content;
|
||||
return;
|
||||
}
|
||||
|
||||
this._operations.push(op);
|
||||
if (this._operationsDone.isDone())
|
||||
this._performOperations();
|
||||
}
|
||||
|
||||
private async _performOperations() {
|
||||
this._operationsDone = new ManualPromise();
|
||||
while (this._operations.length) {
|
||||
const op = this._operations.shift()!;
|
||||
// Ignore all operations after the first error.
|
||||
if (this._error)
|
||||
continue;
|
||||
try {
|
||||
await this._performOperation(op);
|
||||
} catch (e) {
|
||||
this._error = e;
|
||||
}
|
||||
}
|
||||
this._operationsDone.resolve();
|
||||
}
|
||||
|
||||
private async _performOperation(op: SerializedFSOperation) {
|
||||
switch (op.op) {
|
||||
case 'mkdir': {
|
||||
await fs.promises.mkdir(op.dir, { recursive: true });
|
||||
return;
|
||||
}
|
||||
case 'writeFile': {
|
||||
// Note: 'wx' flag only writes when the file does not exist.
|
||||
// See https://nodejs.org/api/fs.html#file-system-flags.
|
||||
// This way tracing never have to write the same resource twice.
|
||||
if (op.skipIfExists)
|
||||
await fs.promises.writeFile(op.file, op.content, { flag: 'wx' }).catch(() => {});
|
||||
else
|
||||
await fs.promises.writeFile(op.file, op.content);
|
||||
return;
|
||||
}
|
||||
case 'copyFile': {
|
||||
await fs.promises.copyFile(op.from, op.to);
|
||||
return;
|
||||
}
|
||||
case 'appendFile': {
|
||||
await fs.promises.appendFile(op.file, op.content);
|
||||
return;
|
||||
}
|
||||
case 'zip': {
|
||||
const zipFile = new yazl.ZipFile();
|
||||
const result = new ManualPromise<void>();
|
||||
(zipFile as any as EventEmitter).on('error', error => result.reject(error));
|
||||
for (const entry of op.entries)
|
||||
zipFile.addFile(entry.value, entry.name);
|
||||
zipFile.end();
|
||||
zipFile.outputStream
|
||||
.pipe(fs.createWriteStream(op.zipFileName))
|
||||
.on('close', () => result.resolve())
|
||||
.on('error', error => result.reject(error));
|
||||
await result;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -24,9 +24,9 @@ import { wrapInASCIIBox } from '../../utils';
|
|||
import { BrowserType, kNoXServerRunningError } from '../browserType';
|
||||
import { BrowserReadyState } from '../browserType';
|
||||
|
||||
import type { Env } from '../../utils/processLauncher';
|
||||
import type { BrowserOptions } from '../browser';
|
||||
import type { SdkObject } from '../instrumentation';
|
||||
import type { Env } from '../processLauncher';
|
||||
import type { ProtocolError } from '../protocolError';
|
||||
import type { ConnectionTransport } from '../transport';
|
||||
import type * as types from '../types';
|
||||
|
|
|
|||
|
|
@ -31,3 +31,5 @@ export type { Playwright } from './playwright';
|
|||
export { installRootRedirect, openTraceInBrowser, openTraceViewerApp, runTraceViewerApp, startTraceViewerServer } from './trace/viewer/traceViewer';
|
||||
export { serverSideCallMetadata } from './instrumentation';
|
||||
export { SocksProxy } from '../common/socksProxy';
|
||||
export * from './fileUtils';
|
||||
export * from './processLauncher';
|
||||
|
|
|
|||
|
|
@ -20,8 +20,7 @@ import * as fs from 'fs';
|
|||
import * as readline from 'readline';
|
||||
|
||||
import { removeFolders } from './fileUtils';
|
||||
|
||||
import { isUnderTest } from './';
|
||||
import { isUnderTest } from '../utils';
|
||||
|
||||
export type Env = {[key: string]: string | number | boolean | undefined};
|
||||
|
||||
|
|
@ -19,14 +19,10 @@ import { assert, monotonicTime } from '../utils';
|
|||
import { ManualPromise } from '../utils/manualPromise';
|
||||
|
||||
import type { CallMetadata, Instrumentation, SdkObject } from './instrumentation';
|
||||
import type { Progress as CommonProgress } from '../common/progress';
|
||||
import type { LogName } from '../utils/debugLogger';
|
||||
|
||||
export interface Progress {
|
||||
log(message: string): void;
|
||||
timeUntilDeadline(): number;
|
||||
isRunning(): boolean;
|
||||
cleanupWhenAborted(cleanup: () => any): void;
|
||||
throwIfAborted(): void;
|
||||
export interface Progress extends CommonProgress {
|
||||
metadata: CallMetadata;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,10 +21,10 @@ import * as os from 'os';
|
|||
import * as path from 'path';
|
||||
|
||||
import { debugLogger } from '../../utils/debugLogger';
|
||||
import { existsAsync } from '../../utils/fileUtils';
|
||||
import { ManualPromise } from '../../utils/manualPromise';
|
||||
import { getUserAgent } from '../../utils/userAgent';
|
||||
import { colors, progress as ProgressBar } from '../../utilsBundle';
|
||||
import { existsAsync } from '../fileUtils';
|
||||
|
||||
import { browserDirectoryToMarkerFilePath } from '.';
|
||||
|
||||
|
|
|
|||
|
|
@ -25,12 +25,12 @@ import { dockerVersion, readDockerVersionSync, transformCommandsForRoot } from '
|
|||
import { installDependenciesLinux, installDependenciesWindows, validateDependenciesLinux, validateDependenciesWindows } from './dependencies';
|
||||
import { calculateSha1, getAsBooleanFromENV, getFromENV, getPackageManagerExecCommand, wrapInASCIIBox } from '../../utils';
|
||||
import { debugLogger } from '../../utils/debugLogger';
|
||||
import { canAccessFile, existsAsync, removeFolders } from '../../utils/fileUtils';
|
||||
import { hostPlatform, isOfficiallySupportedPlatform } from '../../utils/hostPlatform';
|
||||
import { fetchData } from '../../utils/network';
|
||||
import { spawnAsync } from '../../utils/spawnAsync';
|
||||
import { getEmbedderName } from '../../utils/userAgent';
|
||||
import { lockfile } from '../../utilsBundle';
|
||||
import { canAccessFile, existsAsync, removeFolders } from '../fileUtils';
|
||||
|
||||
import type { DependencyGroup } from './dependencies';
|
||||
import type { HostPlatform } from '../../utils/hostPlatform';
|
||||
|
|
|
|||
|
|
@ -20,11 +20,12 @@ import * as path from 'path';
|
|||
|
||||
import { Snapshotter } from './snapshotter';
|
||||
import { commandsWithTracingSnapshots } from '../../../protocol/debug';
|
||||
import { SerializedFS, assert, createGuid, eventsHelper, monotonicTime, removeFolders } from '../../../utils';
|
||||
import { assert, createGuid, eventsHelper, monotonicTime } from '../../../utils';
|
||||
import { Artifact } from '../../artifact';
|
||||
import { BrowserContext } from '../../browserContext';
|
||||
import { Dispatcher } from '../../dispatchers/dispatcher';
|
||||
import { serializeError } from '../../errors';
|
||||
import { SerializedFS, removeFolders } from '../../fileUtils';
|
||||
import { HarTracer } from '../../har/harTracer';
|
||||
import { SdkObject } from '../../instrumentation';
|
||||
import { Page } from '../../page';
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@
|
|||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { gracefullyProcessExitDoNotHang, isUnderTest } from '../../../utils';
|
||||
import { gracefullyProcessExitDoNotHang } from '../../../server';
|
||||
import { isUnderTest } from '../../../utils';
|
||||
import { HttpServer } from '../../../utils/httpServer';
|
||||
import { open } from '../../../utilsBundle';
|
||||
import { serverSideCallMetadata } from '../../instrumentation';
|
||||
|
|
|
|||
|
|
@ -7781,6 +7781,18 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the
|
|||
}
|
||||
export type setIgnoreCertificateErrorsReturnValue = {
|
||||
}
|
||||
/**
|
||||
* Changes page zoom factor.
|
||||
*/
|
||||
export type setPageZoomFactorParameters = {
|
||||
/**
|
||||
* Unique identifier of the page proxy.
|
||||
*/
|
||||
pageProxyId: PageProxyID;
|
||||
zoomFactor: number;
|
||||
}
|
||||
export type setPageZoomFactorReturnValue = {
|
||||
}
|
||||
/**
|
||||
* Returns all cookies in the given browser context.
|
||||
*/
|
||||
|
|
@ -9658,6 +9670,7 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the
|
|||
"Playwright.grantFileReadAccess": Playwright.grantFileReadAccessParameters;
|
||||
"Playwright.takePageScreenshot": Playwright.takePageScreenshotParameters;
|
||||
"Playwright.setIgnoreCertificateErrors": Playwright.setIgnoreCertificateErrorsParameters;
|
||||
"Playwright.setPageZoomFactor": Playwright.setPageZoomFactorParameters;
|
||||
"Playwright.getAllCookies": Playwright.getAllCookiesParameters;
|
||||
"Playwright.setCookies": Playwright.setCookiesParameters;
|
||||
"Playwright.deleteAllCookies": Playwright.deleteAllCookiesParameters;
|
||||
|
|
@ -9970,6 +9983,7 @@ the top of the viewport and Y increases as it proceeds towards the bottom of the
|
|||
"Playwright.grantFileReadAccess": Playwright.grantFileReadAccessReturnValue;
|
||||
"Playwright.takePageScreenshot": Playwright.takePageScreenshotReturnValue;
|
||||
"Playwright.setIgnoreCertificateErrors": Playwright.setIgnoreCertificateErrorsReturnValue;
|
||||
"Playwright.setPageZoomFactor": Playwright.setPageZoomFactorReturnValue;
|
||||
"Playwright.getAllCookies": Playwright.getAllCookiesReturnValue;
|
||||
"Playwright.setCookies": Playwright.setCookiesReturnValue;
|
||||
"Playwright.deleteAllCookies": Playwright.deleteAllCookiesReturnValue;
|
||||
|
|
|
|||
|
|
@ -22,9 +22,9 @@ import { wrapInASCIIBox } from '../../utils';
|
|||
import { BrowserType, kNoXServerRunningError } from '../browserType';
|
||||
import { WKBrowser } from '../webkit/wkBrowser';
|
||||
|
||||
import type { Env } from '../../utils/processLauncher';
|
||||
import type { BrowserOptions } from '../browser';
|
||||
import type { SdkObject } from '../instrumentation';
|
||||
import type { Env } from '../processLauncher';
|
||||
import type { ProtocolError } from '../protocolError';
|
||||
import type { ConnectionTransport } from '../transport';
|
||||
import type * as types from '../types';
|
||||
|
|
|
|||
|
|
@ -14,194 +14,17 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { ManualPromise } from './manualPromise';
|
||||
import { yazl } from '../zipBundle';
|
||||
|
||||
import type { EventEmitter } from 'events';
|
||||
import type { Platform } from './platform';
|
||||
|
||||
export const fileUploadSizeLimit = 50 * 1024 * 1024;
|
||||
|
||||
export const existsAsync = (path: string): Promise<boolean> => new Promise(resolve => fs.stat(path, err => resolve(!err)));
|
||||
|
||||
export async function mkdirIfNeeded(filePath: string) {
|
||||
export async function mkdirIfNeeded(platform: Platform, filePath: string) {
|
||||
// This will harmlessly throw on windows if the dirname is the root directory.
|
||||
await fs.promises.mkdir(path.dirname(filePath), { recursive: true }).catch(() => {});
|
||||
await platform.fs().promises.mkdir(platform.path().dirname(filePath), { recursive: true }).catch(() => {});
|
||||
}
|
||||
|
||||
export async function removeFolders(dirs: string[]): Promise<Error[]> {
|
||||
export async function removeFolders(platform: Platform, dirs: string[]): Promise<Error[]> {
|
||||
return await Promise.all(dirs.map((dir: string) =>
|
||||
fs.promises.rm(dir, { recursive: true, force: true, maxRetries: 10 }).catch(e => e)
|
||||
platform.fs().promises.rm(dir, { recursive: true, force: true, maxRetries: 10 }).catch(e => e)
|
||||
));
|
||||
}
|
||||
|
||||
export function canAccessFile(file: string) {
|
||||
if (!file)
|
||||
return false;
|
||||
|
||||
try {
|
||||
fs.accessSync(file);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function copyFileAndMakeWritable(from: string, to: string) {
|
||||
await fs.promises.copyFile(from, to);
|
||||
await fs.promises.chmod(to, 0o664);
|
||||
}
|
||||
|
||||
export function sanitizeForFilePath(s: string) {
|
||||
return s.replace(/[\x00-\x2C\x2E-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, '-');
|
||||
}
|
||||
|
||||
export function toPosixPath(aPath: string): string {
|
||||
return aPath.split(path.sep).join(path.posix.sep);
|
||||
}
|
||||
|
||||
type NameValue = { name: string, value: string };
|
||||
type SerializedFSOperation = {
|
||||
op: 'mkdir', dir: string,
|
||||
} | {
|
||||
op: 'writeFile', file: string, content: string | Buffer, skipIfExists?: boolean,
|
||||
} | {
|
||||
op: 'appendFile', file: string, content: string,
|
||||
} | {
|
||||
op: 'copyFile', from: string, to: string,
|
||||
} | {
|
||||
op: 'zip', entries: NameValue[], zipFileName: string,
|
||||
};
|
||||
|
||||
export class SerializedFS {
|
||||
private _buffers = new Map<string, string[]>(); // Should never be accessed from within appendOperation.
|
||||
private _error: Error | undefined;
|
||||
private _operations: SerializedFSOperation[] = [];
|
||||
private _operationsDone: ManualPromise<void>;
|
||||
|
||||
constructor() {
|
||||
this._operationsDone = new ManualPromise();
|
||||
this._operationsDone.resolve(); // No operations scheduled yet.
|
||||
}
|
||||
|
||||
mkdir(dir: string) {
|
||||
this._appendOperation({ op: 'mkdir', dir });
|
||||
}
|
||||
|
||||
writeFile(file: string, content: string | Buffer, skipIfExists?: boolean) {
|
||||
this._buffers.delete(file); // No need to flush the buffer since we'll overwrite anyway.
|
||||
this._appendOperation({ op: 'writeFile', file, content, skipIfExists });
|
||||
}
|
||||
|
||||
appendFile(file: string, text: string, flush?: boolean) {
|
||||
if (!this._buffers.has(file))
|
||||
this._buffers.set(file, []);
|
||||
this._buffers.get(file)!.push(text);
|
||||
if (flush)
|
||||
this._flushFile(file);
|
||||
}
|
||||
|
||||
private _flushFile(file: string) {
|
||||
const buffer = this._buffers.get(file);
|
||||
if (buffer === undefined)
|
||||
return;
|
||||
const content = buffer.join('');
|
||||
this._buffers.delete(file);
|
||||
this._appendOperation({ op: 'appendFile', file, content });
|
||||
}
|
||||
|
||||
copyFile(from: string, to: string) {
|
||||
this._flushFile(from);
|
||||
this._buffers.delete(to); // No need to flush the buffer since we'll overwrite anyway.
|
||||
this._appendOperation({ op: 'copyFile', from, to });
|
||||
}
|
||||
|
||||
async syncAndGetError() {
|
||||
for (const file of this._buffers.keys())
|
||||
this._flushFile(file);
|
||||
await this._operationsDone;
|
||||
return this._error;
|
||||
}
|
||||
|
||||
zip(entries: NameValue[], zipFileName: string) {
|
||||
for (const file of this._buffers.keys())
|
||||
this._flushFile(file);
|
||||
|
||||
// Chain the export operation against write operations,
|
||||
// so that files do not change during the export.
|
||||
this._appendOperation({ op: 'zip', entries, zipFileName });
|
||||
}
|
||||
|
||||
// This method serializes all writes to the trace.
|
||||
private _appendOperation(op: SerializedFSOperation): void {
|
||||
const last = this._operations[this._operations.length - 1];
|
||||
if (last?.op === 'appendFile' && op.op === 'appendFile' && last.file === op.file) {
|
||||
// Merge pending appendFile operations for performance.
|
||||
last.content += op.content;
|
||||
return;
|
||||
}
|
||||
|
||||
this._operations.push(op);
|
||||
if (this._operationsDone.isDone())
|
||||
this._performOperations();
|
||||
}
|
||||
|
||||
private async _performOperations() {
|
||||
this._operationsDone = new ManualPromise();
|
||||
while (this._operations.length) {
|
||||
const op = this._operations.shift()!;
|
||||
// Ignore all operations after the first error.
|
||||
if (this._error)
|
||||
continue;
|
||||
try {
|
||||
await this._performOperation(op);
|
||||
} catch (e) {
|
||||
this._error = e;
|
||||
}
|
||||
}
|
||||
this._operationsDone.resolve();
|
||||
}
|
||||
|
||||
private async _performOperation(op: SerializedFSOperation) {
|
||||
switch (op.op) {
|
||||
case 'mkdir': {
|
||||
await fs.promises.mkdir(op.dir, { recursive: true });
|
||||
return;
|
||||
}
|
||||
case 'writeFile': {
|
||||
// Note: 'wx' flag only writes when the file does not exist.
|
||||
// See https://nodejs.org/api/fs.html#file-system-flags.
|
||||
// This way tracing never have to write the same resource twice.
|
||||
if (op.skipIfExists)
|
||||
await fs.promises.writeFile(op.file, op.content, { flag: 'wx' }).catch(() => {});
|
||||
else
|
||||
await fs.promises.writeFile(op.file, op.content);
|
||||
return;
|
||||
}
|
||||
case 'copyFile': {
|
||||
await fs.promises.copyFile(op.from, op.to);
|
||||
return;
|
||||
}
|
||||
case 'appendFile': {
|
||||
await fs.promises.appendFile(op.file, op.content);
|
||||
return;
|
||||
}
|
||||
case 'zip': {
|
||||
const zipFile = new yazl.ZipFile();
|
||||
const result = new ManualPromise<void>();
|
||||
(zipFile as any as EventEmitter).on('error', error => result.reject(error));
|
||||
for (const entry of op.entries)
|
||||
zipFile.addFile(entry.value, entry.name);
|
||||
zipFile.end();
|
||||
zipFile.outputStream
|
||||
.pipe(fs.createWriteStream(op.zipFileName))
|
||||
.on('close', () => result.resolve())
|
||||
.on('error', error => result.reject(error));
|
||||
await result;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
175
packages/playwright-core/src/utils/harBackend.ts
Normal file
175
packages/playwright-core/src/utils/harBackend.ts
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the 'License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { createGuid } from './crypto';
|
||||
import { ZipFile } from './zipFile';
|
||||
|
||||
import type { HeadersArray } from '../common/types';
|
||||
import type * as har from '@trace/har';
|
||||
|
||||
const redirectStatus = [301, 302, 303, 307, 308];
|
||||
|
||||
export class HarBackend {
|
||||
readonly id = createGuid();
|
||||
private _harFile: har.HARFile;
|
||||
private _zipFile: ZipFile | null;
|
||||
private _baseDir: string | null;
|
||||
|
||||
constructor(harFile: har.HARFile, baseDir: string | null, zipFile: ZipFile | null) {
|
||||
this._harFile = harFile;
|
||||
this._baseDir = baseDir;
|
||||
this._zipFile = zipFile;
|
||||
}
|
||||
|
||||
async lookup(url: string, method: string, headers: HeadersArray, postData: Buffer | undefined, isNavigationRequest: boolean): Promise<{
|
||||
action: 'error' | 'redirect' | 'fulfill' | 'noentry',
|
||||
message?: string,
|
||||
redirectURL?: string,
|
||||
status?: number,
|
||||
headers?: HeadersArray,
|
||||
body?: Buffer }> {
|
||||
let entry;
|
||||
try {
|
||||
entry = await this._harFindResponse(url, method, headers, postData);
|
||||
} catch (e) {
|
||||
return { action: 'error', message: 'HAR error: ' + e.message };
|
||||
}
|
||||
|
||||
if (!entry)
|
||||
return { action: 'noentry' };
|
||||
|
||||
// If navigation is being redirected, restart it with the final url to ensure the document's url changes.
|
||||
if (entry.request.url !== url && isNavigationRequest)
|
||||
return { action: 'redirect', redirectURL: entry.request.url };
|
||||
|
||||
const response = entry.response;
|
||||
try {
|
||||
const buffer = await this._loadContent(response.content);
|
||||
return {
|
||||
action: 'fulfill',
|
||||
status: response.status,
|
||||
headers: response.headers,
|
||||
body: buffer,
|
||||
};
|
||||
} catch (e) {
|
||||
return { action: 'error', message: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadContent(content: { text?: string, encoding?: string, _file?: string }): Promise<Buffer> {
|
||||
const file = content._file;
|
||||
let buffer: Buffer;
|
||||
if (file) {
|
||||
if (this._zipFile)
|
||||
buffer = await this._zipFile.read(file);
|
||||
else
|
||||
buffer = await fs.promises.readFile(path.resolve(this._baseDir!, file));
|
||||
} else {
|
||||
buffer = Buffer.from(content.text || '', content.encoding === 'base64' ? 'base64' : 'utf-8');
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private async _harFindResponse(url: string, method: string, headers: HeadersArray, postData: Buffer | undefined): Promise<har.Entry | undefined> {
|
||||
const harLog = this._harFile.log;
|
||||
const visited = new Set<har.Entry>();
|
||||
while (true) {
|
||||
const entries: har.Entry[] = [];
|
||||
for (const candidate of harLog.entries) {
|
||||
if (candidate.request.url !== url || candidate.request.method !== method)
|
||||
continue;
|
||||
if (method === 'POST' && postData && candidate.request.postData) {
|
||||
const buffer = await this._loadContent(candidate.request.postData);
|
||||
if (!buffer.equals(postData)) {
|
||||
const boundary = multipartBoundary(headers);
|
||||
if (!boundary)
|
||||
continue;
|
||||
const candidataBoundary = multipartBoundary(candidate.request.headers);
|
||||
if (!candidataBoundary)
|
||||
continue;
|
||||
// Try to match multipart/form-data ignroing boundary as it changes between requests.
|
||||
if (postData.toString().replaceAll(boundary, '') !== buffer.toString().replaceAll(candidataBoundary, ''))
|
||||
continue;
|
||||
}
|
||||
}
|
||||
entries.push(candidate);
|
||||
}
|
||||
|
||||
if (!entries.length)
|
||||
return;
|
||||
|
||||
let entry = entries[0];
|
||||
|
||||
// Disambiguate using headers - then one with most matching headers wins.
|
||||
if (entries.length > 1) {
|
||||
const list: { candidate: har.Entry, matchingHeaders: number }[] = [];
|
||||
for (const candidate of entries) {
|
||||
const matchingHeaders = countMatchingHeaders(candidate.request.headers, headers);
|
||||
list.push({ candidate, matchingHeaders });
|
||||
}
|
||||
list.sort((a, b) => b.matchingHeaders - a.matchingHeaders);
|
||||
entry = list[0].candidate;
|
||||
}
|
||||
|
||||
if (visited.has(entry))
|
||||
throw new Error(`Found redirect cycle for ${url}`);
|
||||
|
||||
visited.add(entry);
|
||||
|
||||
// Follow redirects.
|
||||
const locationHeader = entry.response.headers.find(h => h.name.toLowerCase() === 'location');
|
||||
if (redirectStatus.includes(entry.response.status) && locationHeader) {
|
||||
const locationURL = new URL(locationHeader.value, url);
|
||||
url = locationURL.toString();
|
||||
if ((entry.response.status === 301 || entry.response.status === 302) && method === 'POST' ||
|
||||
entry.response.status === 303 && !['GET', 'HEAD'].includes(method)) {
|
||||
// HTTP-redirect fetch step 13 (https://fetch.spec.whatwg.org/#http-redirect-fetch)
|
||||
method = 'GET';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._zipFile?.close();
|
||||
}
|
||||
}
|
||||
|
||||
function countMatchingHeaders(harHeaders: har.Header[], headers: HeadersArray): number {
|
||||
const set = new Set(headers.map(h => h.name.toLowerCase() + ':' + h.value));
|
||||
let matches = 0;
|
||||
for (const h of harHeaders) {
|
||||
if (set.has(h.name.toLowerCase() + ':' + h.value))
|
||||
++matches;
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
function multipartBoundary(headers: HeadersArray) {
|
||||
const contentType = headers.find(h => h.name.toLowerCase() === 'content-type');
|
||||
if (!contentType?.value.includes('multipart/form-data'))
|
||||
return undefined;
|
||||
const boundary = contentType.value.match(/boundary=(\S+)/);
|
||||
if (boundary)
|
||||
return boundary[1];
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -33,7 +33,6 @@ export * from './isomorphic/stringUtils';
|
|||
export * from './isomorphic/urlMatch';
|
||||
export * from './multimap';
|
||||
export * from './network';
|
||||
export * from './processLauncher';
|
||||
export * from './profiler';
|
||||
export * from './rtti';
|
||||
export * from './semaphore';
|
||||
|
|
|
|||
248
packages/playwright-core/src/utils/localUtils.ts
Normal file
248
packages/playwright-core/src/utils/localUtils.ts
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the 'License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
import { removeFolders } from './fileUtils';
|
||||
import { HarBackend } from './harBackend';
|
||||
import { ManualPromise } from './manualPromise';
|
||||
import { fetchData } from './network';
|
||||
import { getUserAgent } from './userAgent';
|
||||
import { ZipFile } from './zipFile';
|
||||
import { yauzl, yazl } from '../zipBundle';
|
||||
|
||||
import { serializeClientSideCallMetadata } from '.';
|
||||
import { assert, calculateSha1 } from '.';
|
||||
|
||||
import type { HTTPRequestParams } from './network';
|
||||
import type { Platform } from './platform';
|
||||
import type { Progress } from '../common/progress';
|
||||
import type * as channels from '@protocol/channels';
|
||||
import type * as har from '@trace/har';
|
||||
import type EventEmitter from 'events';
|
||||
import type http from 'http';
|
||||
|
||||
|
||||
export type StackSession = {
|
||||
file: string;
|
||||
writer: Promise<void>;
|
||||
tmpDir: string | undefined;
|
||||
callStacks: channels.ClientSideCallMetadata[];
|
||||
};
|
||||
|
||||
export async function zip(platform: Platform, stackSessions: Map<string, StackSession>, params: channels.LocalUtilsZipParams): Promise<void> {
|
||||
const promise = new ManualPromise<void>();
|
||||
const zipFile = new yazl.ZipFile();
|
||||
(zipFile as any as EventEmitter).on('error', error => promise.reject(error));
|
||||
|
||||
const addFile = (file: string, name: string) => {
|
||||
try {
|
||||
if (fs.statSync(file).isFile())
|
||||
zipFile.addFile(file, name);
|
||||
} catch (e) {
|
||||
}
|
||||
};
|
||||
|
||||
for (const entry of params.entries)
|
||||
addFile(entry.value, entry.name);
|
||||
|
||||
// Add stacks and the sources.
|
||||
const stackSession = params.stacksId ? stackSessions.get(params.stacksId) : undefined;
|
||||
if (stackSession?.callStacks.length) {
|
||||
await stackSession.writer;
|
||||
if (process.env.PW_LIVE_TRACE_STACKS) {
|
||||
zipFile.addFile(stackSession.file, 'trace.stacks');
|
||||
} else {
|
||||
const buffer = Buffer.from(JSON.stringify(serializeClientSideCallMetadata(stackSession.callStacks)));
|
||||
zipFile.addBuffer(buffer, 'trace.stacks');
|
||||
}
|
||||
}
|
||||
|
||||
// Collect sources from stacks.
|
||||
if (params.includeSources) {
|
||||
const sourceFiles = new Set<string>();
|
||||
for (const { stack } of stackSession?.callStacks || []) {
|
||||
if (!stack)
|
||||
continue;
|
||||
for (const { file } of stack)
|
||||
sourceFiles.add(file);
|
||||
}
|
||||
for (const sourceFile of sourceFiles)
|
||||
addFile(sourceFile, 'resources/src@' + calculateSha1(sourceFile) + '.txt');
|
||||
}
|
||||
|
||||
if (params.mode === 'write') {
|
||||
// New file, just compress the entries.
|
||||
await fs.promises.mkdir(path.dirname(params.zipFile), { recursive: true });
|
||||
zipFile.end(undefined, () => {
|
||||
zipFile.outputStream.pipe(fs.createWriteStream(params.zipFile))
|
||||
.on('close', () => promise.resolve())
|
||||
.on('error', error => promise.reject(error));
|
||||
});
|
||||
await promise;
|
||||
await deleteStackSession(platform, stackSessions, params.stacksId);
|
||||
return;
|
||||
}
|
||||
|
||||
// File already exists. Repack and add new entries.
|
||||
const tempFile = params.zipFile + '.tmp';
|
||||
await fs.promises.rename(params.zipFile, tempFile);
|
||||
|
||||
yauzl.open(tempFile, (err, inZipFile) => {
|
||||
if (err) {
|
||||
promise.reject(err);
|
||||
return;
|
||||
}
|
||||
assert(inZipFile);
|
||||
let pendingEntries = inZipFile.entryCount;
|
||||
inZipFile.on('entry', entry => {
|
||||
inZipFile.openReadStream(entry, (err, readStream) => {
|
||||
if (err) {
|
||||
promise.reject(err);
|
||||
return;
|
||||
}
|
||||
zipFile.addReadStream(readStream!, entry.fileName);
|
||||
if (--pendingEntries === 0) {
|
||||
zipFile.end(undefined, () => {
|
||||
zipFile.outputStream.pipe(fs.createWriteStream(params.zipFile)).on('close', () => {
|
||||
fs.promises.unlink(tempFile).then(() => {
|
||||
promise.resolve();
|
||||
}).catch(error => promise.reject(error));
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
await promise;
|
||||
await deleteStackSession(platform, stackSessions, params.stacksId);
|
||||
}
|
||||
|
||||
async function deleteStackSession(platform: Platform, stackSessions: Map<string, StackSession>, stacksId?: string) {
|
||||
const session = stacksId ? stackSessions.get(stacksId) : undefined;
|
||||
if (!session)
|
||||
return;
|
||||
await session.writer;
|
||||
if (session.tmpDir)
|
||||
await removeFolders(platform, [session.tmpDir]);
|
||||
stackSessions.delete(stacksId!);
|
||||
}
|
||||
|
||||
export async function harOpen(harBackends: Map<string, HarBackend>, params: channels.LocalUtilsHarOpenParams): Promise<channels.LocalUtilsHarOpenResult> {
|
||||
let harBackend: HarBackend;
|
||||
if (params.file.endsWith('.zip')) {
|
||||
const zipFile = new ZipFile(params.file);
|
||||
const entryNames = await zipFile.entries();
|
||||
const harEntryName = entryNames.find(e => e.endsWith('.har'));
|
||||
if (!harEntryName)
|
||||
return { error: 'Specified archive does not have a .har file' };
|
||||
const har = await zipFile.read(harEntryName);
|
||||
const harFile = JSON.parse(har.toString()) as har.HARFile;
|
||||
harBackend = new HarBackend(harFile, null, zipFile);
|
||||
} else {
|
||||
const harFile = JSON.parse(await fs.promises.readFile(params.file, 'utf-8')) as har.HARFile;
|
||||
harBackend = new HarBackend(harFile, path.dirname(params.file), null);
|
||||
}
|
||||
harBackends.set(harBackend.id, harBackend);
|
||||
return { harId: harBackend.id };
|
||||
}
|
||||
|
||||
export async function harLookup(harBackends: Map<string, HarBackend>, params: channels.LocalUtilsHarLookupParams): Promise<channels.LocalUtilsHarLookupResult> {
|
||||
const harBackend = harBackends.get(params.harId);
|
||||
if (!harBackend)
|
||||
return { action: 'error', message: `Internal error: har was not opened` };
|
||||
return await harBackend.lookup(params.url, params.method, params.headers, params.postData, params.isNavigationRequest);
|
||||
}
|
||||
|
||||
export async function harClose(harBackends: Map<string, HarBackend>, params: channels.LocalUtilsHarCloseParams): Promise<void> {
|
||||
const harBackend = harBackends.get(params.harId);
|
||||
if (harBackend) {
|
||||
harBackends.delete(harBackend.id);
|
||||
harBackend.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export async function harUnzip(params: channels.LocalUtilsHarUnzipParams): Promise<void> {
|
||||
const dir = path.dirname(params.zipFile);
|
||||
const zipFile = new ZipFile(params.zipFile);
|
||||
for (const entry of await zipFile.entries()) {
|
||||
const buffer = await zipFile.read(entry);
|
||||
if (entry === 'har.har')
|
||||
await fs.promises.writeFile(params.harFile, buffer);
|
||||
else
|
||||
await fs.promises.writeFile(path.join(dir, entry), buffer);
|
||||
}
|
||||
zipFile.close();
|
||||
await fs.promises.unlink(params.zipFile);
|
||||
}
|
||||
|
||||
export async function tracingStarted(stackSessions: Map<string, StackSession>, params: channels.LocalUtilsTracingStartedParams): Promise<channels.LocalUtilsTracingStartedResult> {
|
||||
let tmpDir = undefined;
|
||||
if (!params.tracesDir)
|
||||
tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'playwright-tracing-'));
|
||||
const traceStacksFile = path.join(params.tracesDir || tmpDir!, params.traceName + '.stacks');
|
||||
stackSessions.set(traceStacksFile, { callStacks: [], file: traceStacksFile, writer: Promise.resolve(), tmpDir });
|
||||
return { stacksId: traceStacksFile };
|
||||
}
|
||||
|
||||
export async function traceDiscarded(platform: Platform, stackSessions: Map<string, StackSession>, params: channels.LocalUtilsTraceDiscardedParams): Promise<void> {
|
||||
await deleteStackSession(platform, stackSessions, params.stacksId);
|
||||
}
|
||||
|
||||
export async function addStackToTracingNoReply(stackSessions: Map<string, StackSession>, params: channels.LocalUtilsAddStackToTracingNoReplyParams): Promise<void> {
|
||||
for (const session of stackSessions.values()) {
|
||||
session.callStacks.push(params.callData);
|
||||
if (process.env.PW_LIVE_TRACE_STACKS) {
|
||||
session.writer = session.writer.then(() => {
|
||||
const buffer = Buffer.from(JSON.stringify(serializeClientSideCallMetadata(session.callStacks)));
|
||||
return fs.promises.writeFile(session.file, buffer);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function urlToWSEndpoint(progress: Progress | undefined, endpointURL: string): Promise<string> {
|
||||
if (endpointURL.startsWith('ws'))
|
||||
return endpointURL;
|
||||
|
||||
progress?.log(`<ws preparing> retrieving websocket url from ${endpointURL}`);
|
||||
const fetchUrl = new URL(endpointURL);
|
||||
if (!fetchUrl.pathname.endsWith('/'))
|
||||
fetchUrl.pathname += '/';
|
||||
fetchUrl.pathname += 'json';
|
||||
const json = await fetchData({
|
||||
url: fetchUrl.toString(),
|
||||
method: 'GET',
|
||||
timeout: progress?.timeUntilDeadline() ?? 30_000,
|
||||
headers: { 'User-Agent': getUserAgent() },
|
||||
}, async (params: HTTPRequestParams, response: http.IncomingMessage) => {
|
||||
return new Error(`Unexpected status ${response.statusCode} when connecting to ${fetchUrl.toString()}.\n` +
|
||||
`This does not look like a Playwright server, try connecting via ws://.`);
|
||||
});
|
||||
progress?.throwIfAborted();
|
||||
|
||||
const wsUrl = new URL(endpointURL);
|
||||
let wsEndpointPath = JSON.parse(json).wsEndpointPath;
|
||||
if (wsEndpointPath.startsWith('/'))
|
||||
wsEndpointPath = wsEndpointPath.substring(1);
|
||||
if (!wsUrl.pathname.endsWith('/'))
|
||||
wsUrl.pathname += '/';
|
||||
wsUrl.pathname += wsEndpointPath;
|
||||
wsUrl.protocol = wsUrl.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
return wsUrl.toString();
|
||||
}
|
||||
43
packages/playwright-core/src/utils/platform.ts
Normal file
43
packages/playwright-core/src/utils/platform.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as util from 'util';
|
||||
|
||||
export type Platform = {
|
||||
fs: () => typeof fs;
|
||||
path: () => typeof path;
|
||||
inspectCustom: symbol | undefined;
|
||||
};
|
||||
|
||||
export const emptyPlatform: Platform = {
|
||||
fs: () => {
|
||||
throw new Error('File system is not available');
|
||||
},
|
||||
|
||||
path: () => {
|
||||
throw new Error('Path module is not available');
|
||||
},
|
||||
|
||||
inspectCustom: undefined,
|
||||
};
|
||||
|
||||
export const nodePlatform: Platform = {
|
||||
fs: () => fs,
|
||||
path: () => path,
|
||||
inspectCustom: util.inspect.custom,
|
||||
};
|
||||
|
|
@ -17,7 +17,8 @@
|
|||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { gracefullyProcessExitDoNotHang, isRegExp } from 'playwright-core/lib/utils';
|
||||
import { gracefullyProcessExitDoNotHang } from 'playwright-core/lib/server';
|
||||
import { isRegExp } from 'playwright-core/lib/utils';
|
||||
|
||||
import { requireOrImport, setSingleTSConfig, setTransformConfig } from '../transform/transform';
|
||||
import { errorWithFile, fileIsModule } from '../util';
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@
|
|||
|
||||
import * as path from 'path';
|
||||
|
||||
import { calculateSha1, toPosixPath } from 'playwright-core/lib/utils';
|
||||
import { toPosixPath } from 'playwright-core/lib/server';
|
||||
import { calculateSha1 } from 'playwright-core/lib/utils';
|
||||
|
||||
import { createFileMatcher } from '../util';
|
||||
|
||||
|
|
|
|||
|
|
@ -471,7 +471,7 @@ function normalizeScreenshotMode(screenshot: ScreenshotOption): ScreenshotMode {
|
|||
}
|
||||
|
||||
function attachConnectedHeaderIfNeeded(testInfo: TestInfo, browser: Browser | null) {
|
||||
const connectHeaders: { name: string, value: string }[] | undefined = (browser as any)?._connectHeaders;
|
||||
const connectHeaders: { name: string, value: string }[] | undefined = (browser as any)?._connection.headers;
|
||||
if (!connectHeaders)
|
||||
return;
|
||||
for (const header of connectHeaders) {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@
|
|||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { escapeTemplateString, isString, sanitizeForFilePath } from 'playwright-core/lib/utils';
|
||||
import { sanitizeForFilePath } from 'playwright-core/lib/server';
|
||||
import { escapeTemplateString, isString } from 'playwright-core/lib/utils';
|
||||
|
||||
import { kNoElementsFoundError, matcherHint } from './matcherHint';
|
||||
import { EXPECTED_COLOR } from '../common/expectBundle';
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@
|
|||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { compareBuffersOrStrings, getComparator, isString, sanitizeForFilePath } from 'playwright-core/lib/utils';
|
||||
import { sanitizeForFilePath } from 'playwright-core/lib/server';
|
||||
import { compareBuffersOrStrings, getComparator, isString } from 'playwright-core/lib/utils';
|
||||
import { colors } from 'playwright-core/lib/utilsBundle';
|
||||
import { mime } from 'playwright-core/lib/utilsBundle';
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@
|
|||
import * as net from 'net';
|
||||
import * as path from 'path';
|
||||
|
||||
import { isURLAvailable, launchProcess, monotonicTime, raceAgainstDeadline } from 'playwright-core/lib/utils';
|
||||
import { launchProcess } from 'playwright-core/lib/server';
|
||||
import { isURLAvailable, monotonicTime, raceAgainstDeadline } from 'playwright-core/lib/utils';
|
||||
import { colors, debug } from 'playwright-core/lib/utilsBundle';
|
||||
|
||||
import type { TestRunnerPlugin } from '.';
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ import * as fs from 'fs';
|
|||
import * as path from 'path';
|
||||
|
||||
import { program } from 'playwright-core/lib/cli/program';
|
||||
import { gracefullyProcessExitDoNotHang, startProfiling, stopProfiling } from 'playwright-core/lib/utils';
|
||||
import { gracefullyProcessExitDoNotHang } from 'playwright-core/lib/server';
|
||||
import { startProfiling, stopProfiling } from 'playwright-core/lib/utils';
|
||||
|
||||
import { builtInReporters, defaultReporter, defaultTimeout } from './common/config';
|
||||
import { loadConfigFromFileRestartIfNeeded, loadEmptyConfigForMergeReports, resolveConfigLocation } from './common/configLoader';
|
||||
|
|
@ -28,6 +29,7 @@ export { program } from 'playwright-core/lib/cli/program';
|
|||
import { prepareErrorStack } from './reporters/base';
|
||||
import { showHTMLReport } from './reporters/html';
|
||||
import { createMergedReport } from './reporters/merge';
|
||||
import { filterProjects } from './runner/projectUtils';
|
||||
import { Runner } from './runner/runner';
|
||||
import * as testServer from './runner/testServer';
|
||||
import { runWatchModeLoop } from './runner/watchMode';
|
||||
|
|
@ -160,6 +162,23 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
|
|||
await startProfiling();
|
||||
const cliOverrides = overridesFromOptions(opts);
|
||||
|
||||
const config = await loadConfigFromFileRestartIfNeeded(opts.config, cliOverrides, opts.deps === false);
|
||||
if (!config)
|
||||
return;
|
||||
|
||||
config.cliArgs = args;
|
||||
config.cliGrep = opts.grep as string | undefined;
|
||||
config.cliOnlyChanged = opts.onlyChanged === true ? 'HEAD' : opts.onlyChanged;
|
||||
config.cliGrepInvert = opts.grepInvert as string | undefined;
|
||||
config.cliListOnly = !!opts.list;
|
||||
config.cliProjectFilter = opts.project || undefined;
|
||||
config.cliPassWithNoTests = !!opts.passWithNoTests;
|
||||
config.cliFailOnFlakyTests = !!opts.failOnFlakyTests;
|
||||
config.cliLastFailed = !!opts.lastFailed;
|
||||
|
||||
// Evaluate project filters against config before starting execution. This enables a consistent error message across run modes
|
||||
filterProjects(config.projects, config.cliProjectFilter);
|
||||
|
||||
if (opts.ui || opts.uiHost || opts.uiPort) {
|
||||
if (opts.onlyChanged)
|
||||
throw new Error(`--only-changed is not supported in UI mode. If you'd like that to change, see https://github.com/microsoft/playwright/issues/15075 for more details.`);
|
||||
|
|
@ -201,20 +220,6 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
|
|||
return;
|
||||
}
|
||||
|
||||
const config = await loadConfigFromFileRestartIfNeeded(opts.config, cliOverrides, opts.deps === false);
|
||||
if (!config)
|
||||
return;
|
||||
|
||||
config.cliArgs = args;
|
||||
config.cliGrep = opts.grep as string | undefined;
|
||||
config.cliOnlyChanged = opts.onlyChanged === true ? 'HEAD' : opts.onlyChanged;
|
||||
config.cliGrepInvert = opts.grepInvert as string | undefined;
|
||||
config.cliListOnly = !!opts.list;
|
||||
config.cliProjectFilter = opts.project || undefined;
|
||||
config.cliPassWithNoTests = !!opts.passWithNoTests;
|
||||
config.cliFailOnFlakyTests = !!opts.failOnFlakyTests;
|
||||
config.cliLastFailed = !!opts.lastFailed;
|
||||
|
||||
const runner = new Runner(config);
|
||||
const status = await runner.runAllTests();
|
||||
await stopProfiling('runner');
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ import * as fs from 'fs';
|
|||
import * as path from 'path';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
import { ManualPromise, calculateSha1, createGuid, getUserAgent, removeFolders, sanitizeForFilePath } from 'playwright-core/lib/utils';
|
||||
import { removeFolders, sanitizeForFilePath } from 'playwright-core/lib/server';
|
||||
import { ManualPromise, calculateSha1, createGuid, getUserAgent } from 'playwright-core/lib/utils';
|
||||
import { mime } from 'playwright-core/lib/utilsBundle';
|
||||
import { yazl } from 'playwright-core/lib/zipBundle';
|
||||
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ import * as fs from 'fs';
|
|||
import * as path from 'path';
|
||||
import { Transform } from 'stream';
|
||||
|
||||
import { MultiMap, getPackageManagerExecCommand } from 'playwright-core/lib/utils';
|
||||
import { HttpServer, assert, calculateSha1, copyFileAndMakeWritable, gracefullyProcessExitDoNotHang, removeFolders, sanitizeForFilePath, toPosixPath } from 'playwright-core/lib/utils';
|
||||
import { copyFileAndMakeWritable, gracefullyProcessExitDoNotHang, removeFolders, sanitizeForFilePath, toPosixPath } from 'playwright-core/lib/server';
|
||||
import { HttpServer, MultiMap, assert, calculateSha1, getPackageManagerExecCommand } from 'playwright-core/lib/utils';
|
||||
import { colors, open } from 'playwright-core/lib/utilsBundle';
|
||||
import { mime } from 'playwright-core/lib/utilsBundle';
|
||||
import { yazl } from 'playwright-core/lib/zipBundle';
|
||||
|
|
@ -449,6 +449,17 @@ class HtmlBuilder {
|
|||
return a;
|
||||
}
|
||||
|
||||
if (a.name === 'pageSnapshot') {
|
||||
try {
|
||||
const body = fs.readFileSync(a.path!, { encoding: 'utf-8' });
|
||||
return {
|
||||
name: 'pageSnapshot',
|
||||
contentType: a.contentType,
|
||||
body,
|
||||
};
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (a.path) {
|
||||
let fileName = a.path;
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@
|
|||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { MultiMap, toPosixPath } from 'playwright-core/lib/utils';
|
||||
import { toPosixPath } from 'playwright-core/lib/server';
|
||||
import { MultiMap } from 'playwright-core/lib/utils';
|
||||
|
||||
import { formatError, nonTerminalScreen, prepareErrorStack, resolveOutputFile } from './base';
|
||||
import { getProjectId } from '../common/config';
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ import * as fs from 'fs';
|
|||
import * as path from 'path';
|
||||
import { promisify } from 'util';
|
||||
|
||||
import { monotonicTime, removeFolders } from 'playwright-core/lib/utils';
|
||||
import { removeFolders } from 'playwright-core/lib/server';
|
||||
import { monotonicTime } from 'playwright-core/lib/utils';
|
||||
import { debug } from 'playwright-core/lib/utilsBundle';
|
||||
|
||||
import { Dispatcher } from './dispatcher';
|
||||
|
|
@ -26,12 +27,12 @@ import { FailureTracker } from './failureTracker';
|
|||
import { collectProjectsAndTestFiles, createRootSuite, loadFileSuites, loadGlobalHook } from './loadUtils';
|
||||
import { buildDependentProjects, buildTeardownToSetupsMap, filterProjects } from './projectUtils';
|
||||
import { applySuggestedRebaselines, clearSuggestedRebaselines } from './rebase';
|
||||
import { Suite } from '../common/test';
|
||||
import { createTestGroups } from '../runner/testGroups';
|
||||
import { removeDirAndLogToConsole } from '../util';
|
||||
import { TaskRunner } from './taskRunner';
|
||||
import { detectChangedTestFiles } from './vcs';
|
||||
import { Suite } from '../common/test';
|
||||
import { createTestGroups } from '../runner/testGroups';
|
||||
import { cacheDir } from '../transform/compilationCache';
|
||||
import { removeDirAndLogToConsole } from '../util';
|
||||
|
||||
import type { TestGroup } from '../runner/testGroups';
|
||||
import type { Matcher } from '../util';
|
||||
|
|
|
|||
|
|
@ -17,8 +17,8 @@
|
|||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { installRootRedirect, openTraceInBrowser, openTraceViewerApp, registry, startTraceViewerServer } from 'playwright-core/lib/server';
|
||||
import { ManualPromise, gracefullyProcessExitDoNotHang, isUnderTest } from 'playwright-core/lib/utils';
|
||||
import { gracefullyProcessExitDoNotHang, installRootRedirect, openTraceInBrowser, openTraceViewerApp, registry, startTraceViewerServer } from 'playwright-core/lib/server';
|
||||
import { ManualPromise, isUnderTest } from 'playwright-core/lib/utils';
|
||||
import { open } from 'playwright-core/lib/utilsBundle';
|
||||
|
||||
import { createErrorCollectingReporter, createReporterForTestServer, createReporters } from './reporters';
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { removeFolders } from 'playwright-core/lib/utils';
|
||||
import { removeFolders } from 'playwright-core/lib/server';
|
||||
|
||||
import { ProcessHost } from './processHost';
|
||||
import { stdioChunkToParams } from '../common/ipc';
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ import * as path from 'path';
|
|||
import * as url from 'url';
|
||||
import util from 'util';
|
||||
|
||||
import { formatCallLog } from 'playwright-core/lib/utils';
|
||||
import { calculateSha1, isRegExp, isString, sanitizeForFilePath, stringifyStackFrames } from 'playwright-core/lib/utils';
|
||||
import { sanitizeForFilePath } from 'playwright-core/lib/server';
|
||||
import { calculateSha1, formatCallLog, isRegExp, isString, stringifyStackFrames } from 'playwright-core/lib/utils';
|
||||
import { debug, mime, minimatch, parseStackTraceLine } from 'playwright-core/lib/utilsBundle';
|
||||
|
||||
import type { Location } from './../types/testReporter';
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@
|
|||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { captureRawStack, monotonicTime, sanitizeForFilePath, stringifyStackFrames, zones } from 'playwright-core/lib/utils';
|
||||
import { sanitizeForFilePath } from 'playwright-core/lib/server';
|
||||
import { captureRawStack, monotonicTime, stringifyStackFrames, zones } from 'playwright-core/lib/utils';
|
||||
|
||||
import { TimeoutManager, TimeoutManagerError, kMaxDeadline } from './timeoutManager';
|
||||
import { debugTest, filteredStackTrace, formatLocation, getContainedPath, normalizeAndSaveAttachment, trimLongString, windowsFilesystemFriendlyLength } from '../util';
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@
|
|||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { ManualPromise, SerializedFS, calculateSha1, createGuid, monotonicTime } from 'playwright-core/lib/utils';
|
||||
import { SerializedFS } from 'playwright-core/lib/server';
|
||||
import { ManualPromise, calculateSha1, createGuid, monotonicTime } from 'playwright-core/lib/utils';
|
||||
import { yauzl, yazl } from 'playwright-core/lib/zipBundle';
|
||||
|
||||
import { filteredStackTrace } from '../util';
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ManualPromise, gracefullyCloseAll, removeFolders } from 'playwright-core/lib/utils';
|
||||
import { removeFolders } from 'playwright-core/lib/server';
|
||||
import { gracefullyCloseAll } from 'playwright-core/lib/server';
|
||||
import { ManualPromise } from 'playwright-core/lib/utils';
|
||||
import { colors } from 'playwright-core/lib/utilsBundle';
|
||||
|
||||
import { deserializeConfig } from '../common/configLoader';
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ function isEqualAttachment(a: Attachment, b: AfterActionTraceEventAttachment): b
|
|||
return a.name === b.name && a.path === b.path && a.sha1 === b.sha1;
|
||||
}
|
||||
|
||||
function attachmentURL(attachment: Attachment, queryParams: Record<string, string> = {}) {
|
||||
export function attachmentURL(attachment: Attachment, queryParams: Record<string, string> = {}) {
|
||||
const params = new URLSearchParams(queryParams);
|
||||
if (attachment.sha1) {
|
||||
params.set('trace', attachment.traceUrl);
|
||||
|
|
|
|||
|
|
@ -46,11 +46,16 @@ export const CopyToClipboard: React.FunctionComponent<{
|
|||
export const CopyToClipboardTextButton: React.FunctionComponent<{
|
||||
value: string | (() => Promise<string>),
|
||||
description: string,
|
||||
}> = ({ value, description }) => {
|
||||
copiedDescription?: React.ReactNode,
|
||||
style?: React.CSSProperties,
|
||||
}> = ({ value, description, copiedDescription = description, style }) => {
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
const handleCopy = React.useCallback(async () => {
|
||||
const valueToCopy = typeof value === 'function' ? await value() : value;
|
||||
await navigator.clipboard.writeText(valueToCopy);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 3000);
|
||||
}, [value]);
|
||||
|
||||
return <ToolbarButton title={description} onClick={handleCopy} className='copy-to-clipboard-text-button'>{description}</ToolbarButton>;
|
||||
return <ToolbarButton style={style} title={description} onClick={handleCopy} className='copy-to-clipboard-text-button'>{copied ? copiedDescription : description}</ToolbarButton>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -21,6 +21,59 @@ import { PlaceholderPanel } from './placeholderPanel';
|
|||
import { renderAction } from './actionList';
|
||||
import type { Language } from '@isomorphic/locatorGenerators';
|
||||
import type { StackFrame } from '@protocol/channels';
|
||||
import { CopyToClipboardTextButton } from './copyToClipboard';
|
||||
import { attachmentURL } from './attachmentsTab';
|
||||
import { fixTestPrompt } from '@web/components/prompts';
|
||||
import type { GitCommitInfo } from '@testIsomorphic/types';
|
||||
|
||||
const GitCommitInfoContext = React.createContext<GitCommitInfo | undefined>(undefined);
|
||||
|
||||
export function GitCommitInfoProvider({ children, gitCommitInfo }: React.PropsWithChildren<{ gitCommitInfo: GitCommitInfo }>) {
|
||||
return <GitCommitInfoContext.Provider value={gitCommitInfo}>{children}</GitCommitInfoContext.Provider>;
|
||||
}
|
||||
|
||||
export function useGitCommitInfo() {
|
||||
return React.useContext(GitCommitInfoContext);
|
||||
}
|
||||
|
||||
const PromptButton: React.FC<{
|
||||
error: string;
|
||||
actions: modelUtil.ActionTraceEventInContext[];
|
||||
}> = ({ error, actions }) => {
|
||||
const [pageSnapshot, setPageSnapshot] = React.useState<string>();
|
||||
|
||||
React.useEffect(() => {
|
||||
for (const action of actions) {
|
||||
for (const attachment of action.attachments ?? []) {
|
||||
if (attachment.name === 'pageSnapshot') {
|
||||
fetch(attachmentURL({ ...attachment, traceUrl: action.context.traceUrl })).then(async response => {
|
||||
setPageSnapshot(await response.text());
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [actions]);
|
||||
|
||||
const gitCommitInfo = useGitCommitInfo();
|
||||
const prompt = React.useMemo(
|
||||
() => fixTestPrompt(
|
||||
error,
|
||||
gitCommitInfo?.['pull.diff'] ?? gitCommitInfo?.['revision.diff'],
|
||||
pageSnapshot
|
||||
),
|
||||
[error, gitCommitInfo, pageSnapshot]
|
||||
);
|
||||
|
||||
return (
|
||||
<CopyToClipboardTextButton
|
||||
value={prompt}
|
||||
description='Fix with AI'
|
||||
copiedDescription={<>Copied <span className='codicon codicon-copy' style={{ marginLeft: '5px' }}/></>}
|
||||
style={{ width: '90px', justifyContent: 'center' }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export type ErrorDescription = {
|
||||
action?: modelUtil.ActionTraceEventInContext;
|
||||
|
|
@ -44,9 +97,10 @@ export function useErrorsTabModel(model: modelUtil.MultiTraceModel | undefined):
|
|||
|
||||
export const ErrorsTab: React.FunctionComponent<{
|
||||
errorsModel: ErrorsTabModel,
|
||||
actions: modelUtil.ActionTraceEventInContext[],
|
||||
sdkLanguage: Language,
|
||||
revealInSource: (error: ErrorDescription) => void,
|
||||
}> = ({ errorsModel, sdkLanguage, revealInSource }) => {
|
||||
}> = ({ errorsModel, sdkLanguage, revealInSource, actions }) => {
|
||||
if (!errorsModel.errors.size)
|
||||
return <PlaceholderPanel text='No errors' />;
|
||||
|
||||
|
|
@ -72,6 +126,9 @@ export const ErrorsTab: React.FunctionComponent<{
|
|||
{location && <div className='action-location'>
|
||||
@ <span title={longLocation} onClick={() => revealInSource(error)}>{location}</span>
|
||||
</div>}
|
||||
<span style={{ position: 'absolute', right: '5px' }}>
|
||||
<PromptButton error={message} actions={actions} />
|
||||
</span>
|
||||
</div>
|
||||
<ErrorMessage error={message} />
|
||||
</div>;
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import { TestListView } from './uiModeTestListView';
|
|||
import { TraceView } from './uiModeTraceView';
|
||||
import { SettingsView } from './settingsView';
|
||||
import { DefaultSettingsView } from './defaultSettingsView';
|
||||
import { GitCommitInfoProvider } from './errorsTab';
|
||||
|
||||
let xtermSize = { cols: 80, rows: 24 };
|
||||
const xtermDataSource: XtermDataSource = {
|
||||
|
|
@ -430,13 +431,15 @@ export const UIModeView: React.FC<{}> = ({
|
|||
<XtermWrapper source={xtermDataSource}></XtermWrapper>
|
||||
</div>
|
||||
<div className={clsx('vbox', isShowingOutput && 'hidden')}>
|
||||
<TraceView
|
||||
pathSeparator={queryParams.pathSeparator}
|
||||
item={selectedItem}
|
||||
rootDir={testModel?.config?.rootDir}
|
||||
revealSource={revealSource}
|
||||
onOpenExternally={location => testServerConnection?.openNoReply({ location: { file: location.file, line: location.line, column: location.column } })}
|
||||
/>
|
||||
<GitCommitInfoProvider gitCommitInfo={testModel?.config.metadata['git.commit.info']}>
|
||||
<TraceView
|
||||
pathSeparator={queryParams.pathSeparator}
|
||||
item={selectedItem}
|
||||
rootDir={testModel?.config?.rootDir}
|
||||
revealSource={revealSource}
|
||||
onOpenExternally={location => testServerConnection?.openNoReply({ location: { file: location.file, line: location.line, column: location.column } })}
|
||||
/>
|
||||
</GitCommitInfoProvider>
|
||||
</div>
|
||||
</div>}
|
||||
sidebar={<div className='vbox ui-mode-sidebar'>
|
||||
|
|
|
|||
|
|
@ -199,7 +199,7 @@ export const Workbench: React.FunctionComponent<{
|
|||
else
|
||||
setRevealedError(error);
|
||||
selectPropertiesTab('source');
|
||||
}} />
|
||||
}} actions={model?.actions ?? []} />
|
||||
};
|
||||
|
||||
// Fallback location w/o action stands for file / test.
|
||||
|
|
|
|||
56
packages/web/src/components/prompts.ts
Normal file
56
packages/web/src/components/prompts.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const ansiRegex = new RegExp('([\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~])))', 'g');
|
||||
function stripAnsiEscapes(str: string): string {
|
||||
return str.replace(ansiRegex, '');
|
||||
}
|
||||
|
||||
export function fixTestPrompt(error: string, diff?: string, pageSnapshot?: string) {
|
||||
const promptParts = [
|
||||
`My Playwright test failed. What's going wrong?`,
|
||||
`Please give me a suggestion how to fix it, and then explain what went wrong. Be very concise and apply Playwright best practices.`,
|
||||
`Don't include many headings in your output. Make sure what you're saying is correct, and take into account whether there might be a bug in the app.`,
|
||||
'Here is the error:',
|
||||
'\n',
|
||||
'```js',
|
||||
stripAnsiEscapes(error),
|
||||
'```',
|
||||
'\n',
|
||||
];
|
||||
|
||||
if (pageSnapshot) {
|
||||
promptParts.push(
|
||||
'This is how the page looked at the end of the test:\n',
|
||||
'```yaml',
|
||||
pageSnapshot,
|
||||
'```',
|
||||
'\n'
|
||||
);
|
||||
}
|
||||
|
||||
if (diff) {
|
||||
promptParts.push(
|
||||
'And this is the code diff:\n',
|
||||
'```diff',
|
||||
diff,
|
||||
'```',
|
||||
'\n'
|
||||
);
|
||||
}
|
||||
|
||||
return promptParts.join('\n');
|
||||
}
|
||||
|
|
@ -16,15 +16,17 @@
|
|||
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import type { PageTestFixtures, PageWorkerFixtures } from '../page/pageTestApi';
|
||||
import * as path from 'path';
|
||||
import type { BrowserContext, BrowserContextOptions, BrowserType, Page } from 'playwright-core';
|
||||
import { removeFolders } from '../../packages/playwright-core/lib/utils/fileUtils';
|
||||
import { baseTest } from './baseTest';
|
||||
import { type RemoteServerOptions, type PlaywrightServer, RunServer, RemoteServer } from './remoteServer';
|
||||
import type { Log } from '../../packages/trace/src/har';
|
||||
import { RunServer, RemoteServer } from './remoteServer';
|
||||
import { removeFolders } from '../../packages/playwright-core/lib/server/fileUtils';
|
||||
import { parseHar } from '../config/utils';
|
||||
import { createSkipTestPredicate } from '../bidi/expectationUtil';
|
||||
|
||||
import type { PageTestFixtures, PageWorkerFixtures } from '../page/pageTestApi';
|
||||
import type { RemoteServerOptions, PlaywrightServer } from './remoteServer';
|
||||
import type { BrowserContext, BrowserContextOptions, BrowserType, Page } from 'playwright-core';
|
||||
import type { Log } from '../../packages/trace/src/har';
|
||||
import type { TestInfo } from '@playwright/test';
|
||||
|
||||
export type BrowserTestWorkerFixtures = PageWorkerFixtures & {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { spawnAsync } from '../../packages/playwright-core/lib/utils/spawnAsync';
|
||||
import { removeFolders } from '../../packages/playwright-core/lib/utils/fileUtils';
|
||||
import { removeFolders } from '../../packages/playwright-core/lib/server/fileUtils';
|
||||
import { TMP_WORKSPACES } from './npmTest';
|
||||
|
||||
const PACKAGE_BUILDER_SCRIPT = path.join(__dirname, '..', '..', 'utils', 'pack_package.js');
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue