diff --git a/package-lock.json b/package-lock.json index 3bca96c8a..f2501c15a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@babel/code-frame": "7.24.2", "@gemini-testing/commander": "2.15.4", "@jspm/core": "2.0.1", + "@puppeteer/browsers": "2.4.0", "@types/debug": "4.1.12", "@types/yallist": "4.0.4", "@vitest/spy": "2.1.4", @@ -23,12 +24,16 @@ "bluebird": "3.5.1", "chalk": "2.4.2", "clear-require": "1.0.1", + "cli-progress": "3.12.0", "debug": "2.6.9", "devtools": "8.39.0", + "edgedriver": "5.6.1", "error-stack-parser": "2.1.4", "expect-webdriverio": "3.6.0", + "extract-zip": "2.0.1", "fastq": "1.13.0", "fs-extra": "5.0.0", + "geckodriver": "4.5.0", "gemini-configparser": "1.4.1", "get-port": "5.1.1", "glob-extra": "5.0.2", @@ -54,6 +59,7 @@ "urijs": "1.19.11", "url-join": "4.0.1", "vite": "5.1.6", + "wait-port": "1.1.0", "webdriverio": "8.39.0", "worker-farm": "1.7.0", "yallist": "3.1.1" @@ -80,6 +86,7 @@ "@types/chai": "4.3.4", "@types/chai-as-promised": "7.1.5", "@types/clear-require": "3.2.1", + "@types/cli-progress": "3.11.6", "@types/escape-string-regexp": "2.0.1", "@types/fs-extra": "11.0.4", "@types/lodash": "4.14.191", @@ -2034,41 +2041,24 @@ } }, "node_modules/@puppeteer/browsers": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.3.0.tgz", - "integrity": "sha512-an3QdbNPkuU6qpxpbssxAbjRLJcF+eP4L8UqIY3+6n0sbaVxw5pz7PiCLy9g32XEZuoamUlV5ZQPnA6FxvkIHA==", - "dependencies": { - "debug": "4.3.4", - "extract-zip": "2.0.1", - "http-proxy-agent": "5.0.0", - "https-proxy-agent": "5.0.1", - "progress": "2.0.3", - "proxy-from-env": "1.1.0", - "tar-fs": "2.1.1", - "unbzip2-stream": "1.4.3", - "yargs": "17.7.1" + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.4.0.tgz", + "integrity": "sha512-x8J1csfIygOwf6D6qUAZ0ASk3z63zPb7wkNeHRerCMh82qWKUrOgkuP005AJC8lDL6/evtXETGEJVcwykKT4/g==", + "dependencies": { + "debug": "^4.3.6", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" }, "engines": { - "node": ">=16.0.0" - }, - "peerDependencies": { - "typescript": ">= 4.7.4" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@puppeteer/browsers/node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "engines": { - "node": ">= 10" + "node": ">=18" } }, "node_modules/@puppeteer/browsers/node_modules/cliui": { @@ -2085,11 +2075,11 @@ } }, "node_modules/@puppeteer/browsers/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -2100,28 +2090,64 @@ } } }, - "node_modules/@puppeteer/browsers/node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "node_modules/@puppeteer/browsers/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/@puppeteer/browsers/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/@puppeteer/browsers/node_modules/proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, - "node_modules/@puppeteer/browsers/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "node_modules/@puppeteer/browsers/node_modules/tar-fs": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0" + } + }, + "node_modules/@puppeteer/browsers/node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } }, "node_modules/@puppeteer/browsers/node_modules/yargs": { - "version": "17.7.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", - "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -2606,6 +2632,14 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "engines": { + "node": ">= 10" + } + }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", @@ -2740,6 +2774,15 @@ "clear-require": "*" } }, + "node_modules/@types/cli-progress": { + "version": "3.11.6", + "resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.6.tgz", + "integrity": "sha512-cE3+jb9WRlu+uOSAugewNpITJDt1VF8dHOopPO4IABFc3SXYL5WE/+PTz/FCdZRRfIujiWW3n3aMbv1eIGVRWA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/conventional-commits-parser": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", @@ -4095,14 +4138,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, - "node_modules/@wdio/utils/node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "engines": { - "node": ">= 10.x" - } - }, "node_modules/@wdio/utils/node_modules/strip-ansi": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", @@ -4243,22 +4278,22 @@ "dev": true }, "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "dependencies": { - "debug": "4" + "debug": "^4.3.4" }, "engines": { - "node": ">= 6.0.0" + "node": ">= 14" } }, "node_modules/agent-base/node_modules/debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -4270,9 +4305,9 @@ } }, "node_modules/agent-base/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/ajv": { "version": "8.12.0", @@ -4836,26 +4871,6 @@ "node": ">=10.0.0" } }, - "node_modules/big-integer": { - "version": "1.6.51", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", - "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/binary": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", - "dependencies": { - "buffers": "~0.1.1", - "chainsaw": "~0.1.0" - }, - "engines": { - "node": "*" - } - }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -5270,28 +5285,12 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, - "node_modules/buffer-indexof-polyfill": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", - "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", - "engines": { - "node": ">=0.10" - } - }, "node_modules/buffer-xor": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", "dev": true }, - "node_modules/buffers": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", - "engines": { - "node": ">=0.2.0" - } - }, "node_modules/builtin-status-codes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", @@ -5471,25 +5470,6 @@ "chai": ">= 2.1.2 < 5" } }, - "node_modules/chainsaw": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", - "dependencies": { - "traverse": ">=0.3.0 <0.4" - }, - "engines": { - "node": "*" - } - }, - "node_modules/chainsaw/node_modules/traverse": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", - "engines": { - "node": "*" - } - }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -5683,6 +5663,17 @@ "node": ">=0.10.0" } }, + "node_modules/cli-progress": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", + "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", + "dependencies": { + "string-width": "^4.2.3" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -6352,6 +6343,15 @@ "semver": "bin/semver" } }, + "node_modules/conventional-changelog-core/node_modules/split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "dev": true, + "dependencies": { + "readable-stream": "^3.0.0" + } + }, "node_modules/conventional-changelog-core/node_modules/through2": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", @@ -6568,15 +6568,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/conventional-commits-parser/node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "dev": true, - "engines": { - "node": ">= 10.x" - } - }, "node_modules/conventional-commits-parser/node_modules/text-extensions": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz", @@ -6679,6 +6670,15 @@ "node": ">= 6" } }, + "node_modules/conventional-recommended-bump/node_modules/split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "dev": true, + "dependencies": { + "readable-stream": "^3.0.0" + } + }, "node_modules/conventional-recommended-bump/node_modules/through2": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", @@ -7648,6 +7648,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, "dependencies": { "readable-stream": "^2.0.2" } @@ -9324,42 +9325,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "dependencies": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - }, - "engines": { - "node": ">=0.6" - } - }, - "node_modules/fstream/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/fstream/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -9367,18 +9332,18 @@ "dev": true }, "node_modules/geckodriver": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/geckodriver/-/geckodriver-4.3.3.tgz", - "integrity": "sha512-we2c2COgxFkLVuoknJNx+ioP+7VDq0sr6SCqWHTzlA4kzIbzR0EQ1Pps34s8WrsOnQqPC8a4sZV9dRPROOrkSg==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/geckodriver/-/geckodriver-4.5.0.tgz", + "integrity": "sha512-EnBCT9kJ5oEoP3DaJKjzxAhm7bbNNK6k2q7oCkCT58OIOOiE6Hsr+nVDHflsNaR68HMGtBKOLSZ+YvCDHecScw==", "hasInstallScript": true, "dependencies": { - "@wdio/logger": "^8.28.0", + "@wdio/logger": "^9.0.0", + "@zip.js/zip.js": "^2.7.48", "decamelize": "^6.0.0", "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.4", + "https-proxy-agent": "^7.0.5", "node-fetch": "^3.3.2", - "tar-fs": "^3.0.5", - "unzipper": "^0.10.14", + "tar-fs": "^3.0.6", "which": "^4.0.0" }, "bin": { @@ -9389,9 +9354,9 @@ } }, "node_modules/geckodriver/node_modules/@wdio/logger": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-8.28.0.tgz", - "integrity": "sha512-/s6zNCqwy1hoc+K4SJypis0Ud0dlJ+urOelJFO1x0G0rwDRWyFiUP6ijTaCcFxAm29jYEcEPWijl2xkVIHwOyA==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.1.0.tgz", + "integrity": "sha512-1Rfg9VCy87I9IrViA1ned1Rqa66JwhCzdEo8rA8T3Ro6lBfOEwDbK1XW8ETKLWcweddzGeFalfVnvUlNgPmFdA==", "dependencies": { "chalk": "^5.1.2", "loglevel": "^1.6.0", @@ -9399,24 +9364,13 @@ "strip-ansi": "^7.1.0" }, "engines": { - "node": "^16.13 || >=18" - } - }, - "node_modules/geckodriver/node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" + "node": ">=18.20.0" } }, "node_modules/geckodriver/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "engines": { "node": ">=12" }, @@ -9435,22 +9389,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/geckodriver/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/geckodriver/node_modules/decamelize": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.0.tgz", @@ -9462,18 +9400,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/geckodriver/node_modules/https-proxy-agent": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/geckodriver/node_modules/isexe": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", @@ -9482,11 +9408,6 @@ "node": ">=16" } }, - "node_modules/geckodriver/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, "node_modules/geckodriver/node_modules/strip-ansi": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", @@ -9502,9 +9423,9 @@ } }, "node_modules/geckodriver/node_modules/tar-fs": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.5.tgz", - "integrity": "sha512-JOgGAmZyMgbqpLwct7ZV8VzkEB6pxXFBVErLtb+XCOqzc6w1xiWKI9GVd6bwk68EX7eJ4DWmfXVmq8K2ziZTGg==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" @@ -9715,15 +9636,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/git-raw-commits/node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "dev": true, - "engines": { - "node": ">= 10.x" - } - }, "node_modules/git-remote-origin-url": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz", @@ -10139,17 +10051,6 @@ "node": ">= 14" } }, - "node_modules/http-proxy-agent/node_modules/agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/http-proxy-agent/node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -10201,23 +10102,23 @@ "dev": true }, "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", "dependencies": { - "agent-base": "6", + "agent-base": "^7.0.2", "debug": "4" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/https-proxy-agent/node_modules/debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -10229,9 +10130,9 @@ } }, "node_modules/https-proxy-agent/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/human-signals": { "version": "5.0.0", @@ -10874,54 +10775,6 @@ "jsdom": ">=10.0.0" } }, - "node_modules/jsdom/node_modules/agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", - "dev": true, - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/jsdom/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/jsdom/node_modules/https-proxy-agent": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", - "dev": true, - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/jsdom/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -11138,11 +10991,6 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, - "node_modules/listenercount": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", - "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==" - }, "node_modules/load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -11434,6 +11282,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -11444,7 +11293,8 @@ "node_modules/lru-cache/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "node_modules/magic-string": { "version": "0.30.11", @@ -12714,17 +12564,6 @@ "node": ">= 14" } }, - "node_modules/pac-proxy-agent/node_modules/agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/pac-proxy-agent/node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -12741,18 +12580,6 @@ } } }, - "node_modules/pac-proxy-agent/node_modules/https-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", - "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/pac-proxy-agent/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -13229,17 +13056,6 @@ "node": ">= 14" } }, - "node_modules/proxy-agent/node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/proxy-agent/node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -13256,18 +13072,6 @@ } } }, - "node_modules/proxy-agent/node_modules/https-proxy-agent": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/proxy-agent/node_modules/lru-cache": { "version": "7.18.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", @@ -13373,6 +13177,60 @@ } } }, + "node_modules/puppeteer-core/node_modules/@puppeteer/browsers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.3.0.tgz", + "integrity": "sha512-an3QdbNPkuU6qpxpbssxAbjRLJcF+eP4L8UqIY3+6n0sbaVxw5pz7PiCLy9g32XEZuoamUlV5ZQPnA6FxvkIHA==", + "dependencies": { + "debug": "4.3.4", + "extract-zip": "2.0.1", + "http-proxy-agent": "5.0.0", + "https-proxy-agent": "5.0.1", + "progress": "2.0.3", + "proxy-from-env": "1.1.0", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "yargs": "17.7.1" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "typescript": ">= 4.7.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/puppeteer-core/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/puppeteer-core/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/puppeteer-core/node_modules/cross-fetch": { "version": "3.1.6", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.6.tgz", @@ -13402,6 +13260,31 @@ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1120988.tgz", "integrity": "sha512-39fCpE3Z78IaIPChJsP6Lhmkbf4dWXOmzLk/KFTdRkNk/0JymRIfUynDVRndV9HoDz8PyalK1UH21ST/ivwW5Q==" }, + "node_modules/puppeteer-core/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/puppeteer-core/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/puppeteer-core/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -13465,6 +13348,31 @@ } } }, + "node_modules/puppeteer-core/node_modules/yargs": { + "version": "17.7.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", + "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/puppeteer-core/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, "node_modules/q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", @@ -14051,9 +13959,9 @@ } }, "node_modules/safaridriver": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/safaridriver/-/safaridriver-0.1.0.tgz", - "integrity": "sha512-azzzIP3gR1TB9bVPv7QO4Zjw0rR1BWEU/s2aFdUMN48gxDjxEB13grAEuXDmkKPgE74cObymDxmAmZnL3clj4w==" + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/safaridriver/-/safaridriver-0.1.2.tgz", + "integrity": "sha512-4R309+gWflJktzPXBQCobbWEHlzC4aK3a+Ov3tz2Ib2aBxiwd11phkdIBH1l0EO22x24CJMUQkpKFumRriCSRg==" }, "node_modules/safe-buffer": { "version": "5.1.2", @@ -14079,12 +13987,9 @@ } }, "node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "bin": { "semver": "bin/semver.js" }, @@ -14552,17 +14457,6 @@ "node": ">= 14" } }, - "node_modules/socks-proxy-agent/node_modules/agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/socks-proxy-agent/node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -14656,26 +14550,11 @@ } }, "node_modules/split2": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", - "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", - "dev": true, - "dependencies": { - "readable-stream": "^3.0.0" - } - }, - "node_modules/split2/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", "engines": { - "node": ">= 6" + "node": ">= 10.x" } }, "node_modules/stack-utils": { @@ -15702,28 +15581,6 @@ "node": ">=8" } }, - "node_modules/unzipper": { - "version": "0.10.14", - "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", - "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", - "dependencies": { - "big-integer": "^1.6.17", - "binary": "~0.3.0", - "bluebird": "~3.4.1", - "buffer-indexof-polyfill": "~1.0.0", - "duplexer2": "~0.1.4", - "fstream": "^1.0.12", - "graceful-fs": "^4.2.2", - "listenercount": "~1.0.1", - "readable-stream": "~2.3.6", - "setimmediate": "~1.0.4" - } - }, - "node_modules/unzipper/node_modules/bluebird": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==" - }, "node_modules/update-browserslist-db": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", @@ -16041,9 +15898,9 @@ } }, "node_modules/wait-port": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-1.0.4.tgz", - "integrity": "sha512-w8Ftna3h6XSFWWc2JC5gZEgp64nz8bnaTp5cvzbJSZ53j+omktWTDdwXxEF0jM8YveviLgFWvNGrSvRHnkyHyw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-1.1.0.tgz", + "integrity": "sha512-3e04qkoN3LxTMLakdqeWth8nih8usyg+sf1Bgdf9wwUkp05iuK1eSY/QpLvscT/+F/gA89+LpUmmgBtesbqI2Q==", "dependencies": { "chalk": "^4.1.2", "commander": "^9.3.0", @@ -16390,17 +16247,6 @@ "node": "^16.13 || >=18" } }, - "node_modules/webdriverio/node_modules/agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/webdriverio/node_modules/ansi-regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", @@ -16484,18 +16330,6 @@ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1302984.tgz", "integrity": "sha512-Rgh2Sk5fUSCtEx4QGH9iwTyECdFPySG2nlz5J8guGh2Wlha6uzSOCq/DCEC8faHlLaMPZJMuZ4ovgcX4LvOkKA==" }, - "node_modules/webdriverio/node_modules/https-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", - "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/webdriverio/node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -18347,26 +18181,20 @@ "optional": true }, "@puppeteer/browsers": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.3.0.tgz", - "integrity": "sha512-an3QdbNPkuU6qpxpbssxAbjRLJcF+eP4L8UqIY3+6n0sbaVxw5pz7PiCLy9g32XEZuoamUlV5ZQPnA6FxvkIHA==", - "requires": { - "debug": "4.3.4", - "extract-zip": "2.0.1", - "http-proxy-agent": "5.0.0", - "https-proxy-agent": "5.0.1", - "progress": "2.0.3", - "proxy-from-env": "1.1.0", - "tar-fs": "2.1.1", - "unbzip2-stream": "1.4.3", - "yargs": "17.7.1" + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.4.0.tgz", + "integrity": "sha512-x8J1csfIygOwf6D6qUAZ0ASk3z63zPb7wkNeHRerCMh82qWKUrOgkuP005AJC8lDL6/evtXETGEJVcwykKT4/g==", + "requires": { + "debug": "^4.3.6", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" }, "dependencies": { - "@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==" - }, "cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -18378,32 +18206,63 @@ } }, "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "requires": { - "ms": "2.1.2" + "ms": "^2.1.3" } }, - "http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==" + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "proxy-agent": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz", + "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==", "requires": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" + "agent-base": "^7.0.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.3", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.0.1", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.2" } }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "tar-fs": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", + "requires": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0", + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + } + }, + "tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "requires": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } }, "yargs": { - "version": "17.7.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", - "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "requires": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -18695,6 +18554,11 @@ } } }, + "@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==" + }, "@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", @@ -18828,6 +18692,15 @@ "clear-require": "*" } }, + "@types/cli-progress": { + "version": "3.11.6", + "resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.6.tgz", + "integrity": "sha512-cE3+jb9WRlu+uOSAugewNpITJDt1VF8dHOopPO4IABFc3SXYL5WE/+PTz/FCdZRRfIujiWW3n3aMbv1eIGVRWA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/conventional-commits-parser": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", @@ -19821,11 +19694,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, - "split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==" - }, "strip-ansi": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", @@ -19962,25 +19830,25 @@ "dev": true }, "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "requires": { - "debug": "4" + "debug": "^4.3.4" }, "dependencies": { "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "requires": { - "ms": "2.1.2" + "ms": "^2.1.3" } }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" } } }, @@ -20402,20 +20270,6 @@ "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.3.tgz", "integrity": "sha512-QHX8HLlncOLpy54mh+k/sWIFd0ThmRqwe9ZjELybGZK+tZ8rUb9VO0saKJUROTbE+KhzDUT7xziGpGrW8Kmd+g==" }, - "big-integer": { - "version": "1.6.51", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", - "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==" - }, - "binary": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", - "requires": { - "buffers": "~0.1.1", - "chainsaw": "~0.1.0" - } - }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -20765,22 +20619,12 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, - "buffer-indexof-polyfill": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", - "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==" - }, "buffer-xor": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", "dev": true }, - "buffers": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==" - }, "builtin-status-codes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", @@ -20899,24 +20743,9 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", - "dev": true, - "requires": { - "check-error": "^1.0.2" - } - }, - "chainsaw": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", - "requires": { - "traverse": ">=0.3.0 <0.4" - }, - "dependencies": { - "traverse": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==" - } + "dev": true, + "requires": { + "check-error": "^1.0.2" } }, "chalk": { @@ -21054,6 +20883,14 @@ "resolve-from": "^1.0.0" } }, + "cli-progress": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", + "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", + "requires": { + "string-width": "^4.2.3" + } + }, "cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -21588,6 +21425,15 @@ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true }, + "split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "dev": true, + "requires": { + "readable-stream": "^3.0.0" + } + }, "through2": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", @@ -21733,12 +21579,6 @@ "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", "dev": true }, - "split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "dev": true - }, "text-extensions": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz", @@ -21813,6 +21653,15 @@ "util-deprecate": "^1.0.1" } }, + "split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "dev": true, + "requires": { + "readable-stream": "^3.0.0" + } + }, "through2": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", @@ -22533,6 +22382,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, "requires": { "readable-stream": "^2.0.2" } @@ -23638,35 +23488,6 @@ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "optional": true }, - "fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - }, - "dependencies": { - "mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "requires": { - "minimist": "^1.2.6" - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "requires": { - "glob": "^7.1.3" - } - } - } - }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -23674,24 +23495,24 @@ "dev": true }, "geckodriver": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/geckodriver/-/geckodriver-4.3.3.tgz", - "integrity": "sha512-we2c2COgxFkLVuoknJNx+ioP+7VDq0sr6SCqWHTzlA4kzIbzR0EQ1Pps34s8WrsOnQqPC8a4sZV9dRPROOrkSg==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/geckodriver/-/geckodriver-4.5.0.tgz", + "integrity": "sha512-EnBCT9kJ5oEoP3DaJKjzxAhm7bbNNK6k2q7oCkCT58OIOOiE6Hsr+nVDHflsNaR68HMGtBKOLSZ+YvCDHecScw==", "requires": { - "@wdio/logger": "^8.28.0", + "@wdio/logger": "^9.0.0", + "@zip.js/zip.js": "^2.7.48", "decamelize": "^6.0.0", "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.4", + "https-proxy-agent": "^7.0.5", "node-fetch": "^3.3.2", - "tar-fs": "^3.0.5", - "unzipper": "^0.10.14", + "tar-fs": "^3.0.6", "which": "^4.0.0" }, "dependencies": { "@wdio/logger": { - "version": "8.28.0", - "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-8.28.0.tgz", - "integrity": "sha512-/s6zNCqwy1hoc+K4SJypis0Ud0dlJ+urOelJFO1x0G0rwDRWyFiUP6ijTaCcFxAm29jYEcEPWijl2xkVIHwOyA==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-9.1.0.tgz", + "integrity": "sha512-1Rfg9VCy87I9IrViA1ned1Rqa66JwhCzdEo8rA8T3Ro6lBfOEwDbK1XW8ETKLWcweddzGeFalfVnvUlNgPmFdA==", "requires": { "chalk": "^5.1.2", "loglevel": "^1.6.0", @@ -23699,56 +23520,26 @@ "strip-ansi": "^7.1.0" } }, - "agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "requires": { - "debug": "^4.3.4" - } - }, "ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==" + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==" }, "chalk": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==" }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "requires": { - "ms": "2.1.2" - } - }, "decamelize": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.0.tgz", "integrity": "sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==" }, - "https-proxy-agent": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", - "requires": { - "agent-base": "^7.0.2", - "debug": "4" - } - }, "isexe": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==" }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, "strip-ansi": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", @@ -23758,9 +23549,9 @@ } }, "tar-fs": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.5.tgz", - "integrity": "sha512-JOgGAmZyMgbqpLwct7ZV8VzkEB6pxXFBVErLtb+XCOqzc6w1xiWKI9GVd6bwk68EX7eJ4DWmfXVmq8K2ziZTGg==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz", + "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==", "requires": { "bare-fs": "^2.1.1", "bare-path": "^2.1.0", @@ -23904,12 +23695,6 @@ "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", "dev": true - }, - "split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "dev": true } } }, @@ -24220,14 +24005,6 @@ "debug": "^4.3.4" }, "dependencies": { - "agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", - "requires": { - "debug": "^4.3.4" - } - }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -24266,26 +24043,26 @@ "dev": true }, "https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", "requires": { - "agent-base": "6", + "agent-base": "^7.0.2", "debug": "4" }, "dependencies": { "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "requires": { - "ms": "2.1.2" + "ms": "^2.1.3" } }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" } } }, @@ -24750,42 +24527,6 @@ "whatwg-url": "^14.0.0", "ws": "^8.16.0", "xml-name-validator": "^5.0.0" - }, - "dependencies": { - "agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", - "dev": true, - "requires": { - "debug": "^4.3.4" - } - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "https-proxy-agent": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", - "dev": true, - "requires": { - "agent-base": "^7.0.2", - "debug": "4" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } } }, "jsdom-global": { @@ -24974,11 +24715,6 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, - "listenercount": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", - "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==" - }, "load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -25216,6 +24952,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, "requires": { "yallist": "^4.0.0" }, @@ -25223,7 +24960,8 @@ "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true } } }, @@ -26160,14 +25898,6 @@ "socks-proxy-agent": "^8.0.2" }, "dependencies": { - "agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", - "requires": { - "debug": "^4.3.4" - } - }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -26176,15 +25906,6 @@ "ms": "2.1.2" } }, - "https-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", - "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", - "requires": { - "agent-base": "^7.0.2", - "debug": "4" - } - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -26538,14 +26259,6 @@ "socks-proxy-agent": "^8.0.2" }, "dependencies": { - "agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "requires": { - "debug": "^4.3.4" - } - }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -26554,15 +26267,6 @@ "ms": "2.1.2" } }, - "https-proxy-agent": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", - "requires": { - "agent-base": "^7.0.2", - "debug": "4" - } - }, "lru-cache": { "version": "7.18.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", @@ -26660,6 +26364,40 @@ "ws": "8.13.0" }, "dependencies": { + "@puppeteer/browsers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-1.3.0.tgz", + "integrity": "sha512-an3QdbNPkuU6qpxpbssxAbjRLJcF+eP4L8UqIY3+6n0sbaVxw5pz7PiCLy9g32XEZuoamUlV5ZQPnA6FxvkIHA==", + "requires": { + "debug": "4.3.4", + "extract-zip": "2.0.1", + "http-proxy-agent": "5.0.0", + "https-proxy-agent": "5.0.1", + "progress": "2.0.3", + "proxy-from-env": "1.1.0", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "yargs": "17.7.1" + } + }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + } + }, + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, "cross-fetch": { "version": "3.1.6", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.6.tgz", @@ -26681,6 +26419,25 @@ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1120988.tgz", "integrity": "sha512-39fCpE3Z78IaIPChJsP6Lhmkbf4dWXOmzLk/KFTdRkNk/0JymRIfUynDVRndV9HoDz8PyalK1UH21ST/ivwW5Q==" }, + "http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "requires": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + } + }, + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "requires": { + "agent-base": "6", + "debug": "4" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -26718,6 +26475,25 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", "requires": {} + }, + "yargs": { + "version": "17.7.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", + "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" } } }, @@ -27161,9 +26937,9 @@ } }, "safaridriver": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/safaridriver/-/safaridriver-0.1.0.tgz", - "integrity": "sha512-azzzIP3gR1TB9bVPv7QO4Zjw0rR1BWEU/s2aFdUMN48gxDjxEB13grAEuXDmkKPgE74cObymDxmAmZnL3clj4w==" + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/safaridriver/-/safaridriver-0.1.2.tgz", + "integrity": "sha512-4R309+gWflJktzPXBQCobbWEHlzC4aK3a+Ov3tz2Ib2aBxiwd11phkdIBH1l0EO22x24CJMUQkpKFumRriCSRg==" }, "safe-buffer": { "version": "5.1.2", @@ -27186,12 +26962,9 @@ } }, "semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "requires": { - "lru-cache": "^6.0.0" - } + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==" }, "serialize-error": { "version": "11.0.3", @@ -27530,14 +27303,6 @@ "socks": "^2.7.1" }, "dependencies": { - "agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", - "requires": { - "debug": "^4.3.4" - } - }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -27611,26 +27376,9 @@ } }, "split2": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", - "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", - "dev": true, - "requires": { - "readable-stream": "^3.0.0" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==" }, "stack-utils": { "version": "2.0.6", @@ -28387,30 +28135,6 @@ "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", "dev": true }, - "unzipper": { - "version": "0.10.14", - "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", - "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", - "requires": { - "big-integer": "^1.6.17", - "binary": "~0.3.0", - "bluebird": "~3.4.1", - "buffer-indexof-polyfill": "~1.0.0", - "duplexer2": "~0.1.4", - "fstream": "^1.0.12", - "graceful-fs": "^4.2.2", - "listenercount": "~1.0.1", - "readable-stream": "~2.3.6", - "setimmediate": "~1.0.4" - }, - "dependencies": { - "bluebird": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==" - } - } - }, "update-browserslist-db": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", @@ -28620,9 +28344,9 @@ } }, "wait-port": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-1.0.4.tgz", - "integrity": "sha512-w8Ftna3h6XSFWWc2JC5gZEgp64nz8bnaTp5cvzbJSZ53j+omktWTDdwXxEF0jM8YveviLgFWvNGrSvRHnkyHyw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-1.1.0.tgz", + "integrity": "sha512-3e04qkoN3LxTMLakdqeWth8nih8usyg+sf1Bgdf9wwUkp05iuK1eSY/QpLvscT/+F/gA89+LpUmmgBtesbqI2Q==", "requires": { "chalk": "^4.1.2", "commander": "^9.3.0", @@ -28855,14 +28579,6 @@ "strip-ansi": "^7.1.0" } }, - "agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", - "requires": { - "debug": "^4.3.4" - } - }, "ansi-regex": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", @@ -28919,15 +28635,6 @@ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1302984.tgz", "integrity": "sha512-Rgh2Sk5fUSCtEx4QGH9iwTyECdFPySG2nlz5J8guGh2Wlha6uzSOCq/DCEC8faHlLaMPZJMuZ4ovgcX4LvOkKA==" }, - "https-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", - "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", - "requires": { - "agent-base": "^7.0.2", - "debug": "4" - } - }, "is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", diff --git a/package.json b/package.json index f207a98d7..f0a3a0ac4 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@babel/code-frame": "7.24.2", "@gemini-testing/commander": "2.15.4", "@jspm/core": "2.0.1", + "@puppeteer/browsers": "2.4.0", "@types/debug": "4.1.12", "@types/yallist": "4.0.4", "@vitest/spy": "2.1.4", @@ -65,12 +66,16 @@ "bluebird": "3.5.1", "chalk": "2.4.2", "clear-require": "1.0.1", + "cli-progress": "3.12.0", "debug": "2.6.9", "devtools": "8.39.0", + "edgedriver": "5.6.1", "error-stack-parser": "2.1.4", "expect-webdriverio": "3.6.0", + "extract-zip": "2.0.1", "fastq": "1.13.0", "fs-extra": "5.0.0", + "geckodriver": "4.5.0", "gemini-configparser": "1.4.1", "get-port": "5.1.1", "glob-extra": "5.0.2", @@ -96,6 +101,7 @@ "urijs": "1.19.11", "url-join": "4.0.1", "vite": "5.1.6", + "wait-port": "1.1.0", "webdriverio": "8.39.0", "worker-farm": "1.7.0", "yallist": "3.1.1" @@ -118,6 +124,7 @@ "@types/chai": "4.3.4", "@types/chai-as-promised": "7.1.5", "@types/clear-require": "3.2.1", + "@types/cli-progress": "3.11.6", "@types/escape-string-regexp": "2.0.1", "@types/fs-extra": "11.0.4", "@types/lodash": "4.14.191", diff --git a/src/browser-installer/chrome/browser.ts b/src/browser-installer/chrome/browser.ts new file mode 100644 index 000000000..91f85e01b --- /dev/null +++ b/src/browser-installer/chrome/browser.ts @@ -0,0 +1,61 @@ +import { resolveBuildId, canDownload, install as puppeteerInstall } from "@puppeteer/browsers"; +import { MIN_CHROME_FOR_TESTING_VERSION } from "../constants"; +import { + browserInstallerDebug, + getBrowserPlatform, + getBrowsersDir, + getMilestone, + Browser, + type DownloadProgressCallback, +} from "../utils"; +import { getBinaryPath, getMatchedBrowserVersion, installBinary } from "../registry"; +import { normalizeChromeVersion } from "../utils"; + +export const installChrome = async (version: string, { force = false } = {}): Promise => { + const milestone = getMilestone(version); + + if (Number(milestone) < MIN_CHROME_FOR_TESTING_VERSION) { + browserInstallerDebug(`couldn't install chrome@${version}, installing chromium instead`); + + const { installChromium } = await import("../chromium"); + + return installChromium(version, { force }); + } + + const platform = getBrowserPlatform(); + const existingLocallyBrowserVersion = getMatchedBrowserVersion(Browser.CHROME, platform, version); + + if (existingLocallyBrowserVersion && !force) { + browserInstallerDebug(`A locally installed chrome@${version} browser was found. Skipping the installation`); + + return getBinaryPath(Browser.CHROME, platform, existingLocallyBrowserVersion); + } + + const normalizedVersion = normalizeChromeVersion(version); + const buildId = await resolveBuildId(Browser.CHROME, platform, normalizedVersion); + + const cacheDir = getBrowsersDir(); + const canBeInstalled = await canDownload({ browser: Browser.CHROME, platform, buildId, cacheDir }); + + if (!canBeInstalled) { + throw new Error( + [ + `chrome@${version} can't be installed.`, + `Probably the version '${version}' is invalid, please try another version.`, + "Version examples: '120', '120.0'", + ].join("\n"), + ); + } + + const installFn = (downloadProgressCallback: DownloadProgressCallback): Promise => + puppeteerInstall({ + platform, + buildId, + cacheDir, + downloadProgressCallback, + browser: Browser.CHROME, + unpack: true, + }).then(result => result.executablePath); + + return installBinary(Browser.CHROME, platform, buildId, installFn); +}; diff --git a/src/browser-installer/chrome/driver.ts b/src/browser-installer/chrome/driver.ts new file mode 100644 index 000000000..85f642d53 --- /dev/null +++ b/src/browser-installer/chrome/driver.ts @@ -0,0 +1,63 @@ +import { resolveBuildId, install as puppeteerInstall, canDownload } from "@puppeteer/browsers"; +import { MIN_CHROMEDRIVER_FOR_TESTING_VERSION } from "../constants"; +import { + browserInstallerDebug, + getBrowserPlatform, + getChromeDriverDir, + getMilestone, + Driver, + type DownloadProgressCallback, +} from "../utils"; +import { getBinaryPath, getMatchedDriverVersion, installBinary } from "../registry"; + +export const installChromeDriver = async (chromeVersion: string, { force = false } = {}): Promise => { + const platform = getBrowserPlatform(); + const existingLocallyDriverVersion = getMatchedDriverVersion(Driver.CHROMEDRIVER, platform, chromeVersion); + + if (existingLocallyDriverVersion && !force) { + browserInstallerDebug( + `A locally installed chromedriver for chrome@${chromeVersion} was found. Skipping the installation`, + ); + + return getBinaryPath(Driver.CHROMEDRIVER, platform, existingLocallyDriverVersion); + } + + const milestone = getMilestone(chromeVersion); + + if (Number(milestone) < MIN_CHROMEDRIVER_FOR_TESTING_VERSION) { + browserInstallerDebug( + `installing chromedriver for chrome@${chromeVersion} from chromedriver.storage.googleapis.com manually`, + ); + + const { installChromeDriverManually } = await import("../chromium"); + + return installChromeDriverManually(milestone); + } + + const buildId = await resolveBuildId(Driver.CHROMEDRIVER, platform, milestone); + + const cacheDir = getChromeDriverDir(); + const canBeInstalled = await canDownload({ browser: Driver.CHROMEDRIVER, platform, buildId, cacheDir }); + + if (!canBeInstalled) { + throw new Error( + [ + `chromedriver@${buildId} can't be installed.`, + `Probably the major browser version '${milestone}' is invalid`, + "Correct chrome version examples: '123', '124'", + ].join("\n"), + ); + } + + const installFn = (downloadProgressCallback: DownloadProgressCallback): Promise => + puppeteerInstall({ + platform, + buildId, + cacheDir: getChromeDriverDir(), + browser: Driver.CHROMEDRIVER, + unpack: true, + downloadProgressCallback, + }).then(result => result.executablePath); + + return installBinary(Driver.CHROMEDRIVER, platform, buildId, installFn); +}; diff --git a/src/browser-installer/chrome/index.ts b/src/browser-installer/chrome/index.ts new file mode 100644 index 000000000..9ebb4061d --- /dev/null +++ b/src/browser-installer/chrome/index.ts @@ -0,0 +1,37 @@ +import { spawn, type ChildProcess } from "child_process"; +import getPort from "get-port"; +import waitPort from "wait-port"; +import { pipeLogsWithPrefix } from "../../dev-server/utils"; +import { DRIVER_WAIT_TIMEOUT } from "../constants"; +import { getMilestone } from "../utils"; +import { installChrome } from "./browser"; +import { installChromeDriver } from "./driver"; + +export { installChrome, installChromeDriver }; + +export const runChromeDriver = async ( + chromeVersion: string, + { debug = false } = {}, +): Promise<{ gridUrl: string; process: ChildProcess; port: number }> => { + const [chromeDriverPath] = await Promise.all([installChromeDriver(chromeVersion), installChrome(chromeVersion)]); + + const milestone = getMilestone(chromeVersion); + const randomPort = await getPort(); + + const chromeDriver = spawn(chromeDriverPath, [`--port=${randomPort}`, debug ? `--verbose` : "--silent"], { + windowsHide: true, + detached: false, + }); + + if (debug) { + pipeLogsWithPrefix(chromeDriver, `[chromedriver@${milestone}] `); + } + + const gridUrl = `http://127.0.0.1:${randomPort}`; + + process.once("exit", () => chromeDriver.kill()); + + await waitPort({ port: randomPort, output: "silent", timeout: DRIVER_WAIT_TIMEOUT }); + + return { gridUrl, process: chromeDriver, port: randomPort }; +}; diff --git a/src/browser-installer/chromium/browser.ts b/src/browser-installer/chromium/browser.ts new file mode 100644 index 000000000..55b92d3a1 --- /dev/null +++ b/src/browser-installer/chromium/browser.ts @@ -0,0 +1,56 @@ +import { install as puppeteerInstall, canDownload } from "@puppeteer/browsers"; +import { installBinary, getBinaryPath, getMatchedBrowserVersion } from "../registry"; +import { getMilestone, browserInstallerDebug, getBrowsersDir, Browser, type DownloadProgressCallback } from "../utils"; +import { getChromiumBuildId } from "./utils"; +import { getChromePlatform } from "../utils"; +import { MIN_CHROMIUM_VERSION } from "../constants"; + +export const installChromium = async (version: string, { force = false } = {}): Promise => { + const milestone = getMilestone(version); + + if (Number(milestone) < MIN_CHROMIUM_VERSION) { + throw new Error( + [ + `chrome@${version} can't be installed.`, + `Automatic browser downloader is not available for chrome versions < ${MIN_CHROMIUM_VERSION}`, + ].join("\n"), + ); + } + + const platform = getChromePlatform(version); + const existingLocallyBrowserVersion = getMatchedBrowserVersion(Browser.CHROMIUM, platform, version); + + if (existingLocallyBrowserVersion && !force) { + browserInstallerDebug(`A locally installed chromium@${version} browser was found. Skipping the installation`); + + return getBinaryPath(Browser.CHROMIUM, platform, existingLocallyBrowserVersion); + } + + const buildId = await getChromiumBuildId(platform, milestone); + const cacheDir = getBrowsersDir(); + const canBeInstalled = await canDownload({ browser: Browser.CHROMIUM, platform, buildId, cacheDir }); + + if (!canBeInstalled) { + throw new Error( + [ + `chrome@${version} can't be installed.`, + `Probably the version '${version}' is invalid, please try another version.`, + "Version examples: '93', '93.0'", + ].join("\n"), + ); + } + + browserInstallerDebug(`installing chromium@${buildId} (${milestone}) for ${platform}`); + + const installFn = (downloadProgressCallback: DownloadProgressCallback): Promise => + puppeteerInstall({ + platform, + buildId, + cacheDir, + downloadProgressCallback, + browser: Browser.CHROMIUM, + unpack: true, + }).then(result => result.executablePath); + + return installBinary(Browser.CHROMIUM, platform, milestone, installFn); +}; diff --git a/src/browser-installer/chromium/driver.ts b/src/browser-installer/chromium/driver.ts new file mode 100644 index 000000000..5804e0b77 --- /dev/null +++ b/src/browser-installer/chromium/driver.ts @@ -0,0 +1,54 @@ +import fs from "fs-extra"; +import path from "path"; +import { noop } from "lodash"; +import { CHROMEDRIVER_STORAGE_API, MIN_CHROMIUM_VERSION } from "../constants"; +import { installBinary } from "../registry"; +import { + downloadFile, + getChromiumDriverDir, + retryFetch, + unzipFile, + normalizeChromeVersion, + Driver, + getBrowserPlatform, +} from "../utils"; +import { getChromeDriverArchiveTmpPath, getChromeDriverArchiveUrl } from "./utils"; + +const getChromeDriverVersionByChromiumVersion = async (chromiumVersion: string | number): Promise => { + const suffix = typeof chromiumVersion === "number" ? chromiumVersion : normalizeChromeVersion(chromiumVersion); + + const result = await retryFetch(`${CHROMEDRIVER_STORAGE_API}/LATEST_RELEASE_${suffix}`).then(res => res.text()); + + return result; +}; + +export const installChromeDriverManually = async (milestone: string): Promise => { + const platform = getBrowserPlatform(); + + if (Number(milestone) < MIN_CHROMIUM_VERSION) { + throw new Error( + [ + `chromedriver@${milestone} can't be installed.`, + `Automatic driver downloader is not available for chrome versions < ${MIN_CHROMIUM_VERSION}`, + ].join("\n"), + ); + } + + const driverVersion = await getChromeDriverVersionByChromiumVersion(milestone); + + const installFn = async (): Promise => { + const archiveUrl = getChromeDriverArchiveUrl(driverVersion); + const archivePath = getChromeDriverArchiveTmpPath(driverVersion); + const chromeDriverDirPath = getChromiumDriverDir(driverVersion); + const chromeDriverPath = path.join(chromeDriverDirPath, "chromedriver"); + + await downloadFile(archiveUrl, archivePath); + await unzipFile(archivePath, chromeDriverDirPath); + + fs.remove(archivePath).then(noop, noop); + + return chromeDriverPath; + }; + + return installBinary(Driver.CHROMEDRIVER, platform, driverVersion, installFn); +}; diff --git a/src/browser-installer/chromium/index.ts b/src/browser-installer/chromium/index.ts new file mode 100644 index 000000000..f7c6d6186 --- /dev/null +++ b/src/browser-installer/chromium/index.ts @@ -0,0 +1,2 @@ +export { installChromium } from "./browser"; +export { installChromeDriverManually } from "./driver"; diff --git a/src/browser-installer/chromium/revisions/linux.ts b/src/browser-installer/chromium/revisions/linux.ts new file mode 100644 index 000000000..27817229c --- /dev/null +++ b/src/browser-installer/chromium/revisions/linux.ts @@ -0,0 +1,42 @@ +export default { + 73: 625983, + 74: 638903, + 75: 652459, + 76: 665032, + 77: 681154, + 78: 694594, + 79: 707231, + 80: 722374, + 81: 737198, + 82: 750023, + 83: 756143, + 84: 769125, + 85: 782822, + 86: 800433, + 87: 813060, + 88: 827143, + 89: 843934, + 90: 858016, + 91: 870827, + 92: 885357, + 93: 902296, + 94: 911605, + 95: 920070, + 96: 929514, + 97: 938637, + 98: 950416, + 99: 961779, + 100: 972803, + 101: 982577, + 102: 992824, + 103: 1002974, + 104: 1012822, + 105: 1027072, + 106: 1036920, + 107: 1047812, + 108: 1059082, + 109: 1070158, + 110: 1084167, + 111: 1097778, + 112: 1107206, +} as Record; diff --git a/src/browser-installer/chromium/revisions/mac.ts b/src/browser-installer/chromium/revisions/mac.ts new file mode 100644 index 000000000..400c85312 --- /dev/null +++ b/src/browser-installer/chromium/revisions/mac.ts @@ -0,0 +1,42 @@ +export default { + 73: 625980, + 74: 638898, + 75: 652451, + 76: 665035, + 77: 681144, + 78: 694594, + 79: 707225, + 80: 722372, + 81: 737194, + 82: 749986, + 83: 756141, + 84: 769122, + 85: 782819, + 86: 800433, + 87: 813052, + 88: 827138, + 89: 843934, + 90: 858007, + 91: 870831, + 92: 885361, + 93: 902294, + 94: 911610, + 95: 920094, + 96: 929514, + 97: 938636, + 98: 950414, + 99: 961780, + 100: 972801, + 101: 982575, + 102: 992180, + 103: 1002972, + 104: 1012821, + 105: 1027082, + 106: 1036918, + 107: 1047818, + 108: 1059080, + 109: 1070155, + 110: 1084167, + 111: 1097778, + 112: 1107206, +} as Record; diff --git a/src/browser-installer/chromium/revisions/mac_arm.ts b/src/browser-installer/chromium/revisions/mac_arm.ts new file mode 100644 index 000000000..69c5e1267 --- /dev/null +++ b/src/browser-installer/chromium/revisions/mac_arm.ts @@ -0,0 +1,22 @@ +export default { + 93: 902292, + 94: 911612, + 95: 920092, + 96: 929514, + 97: 938625, + 98: 950409, + 99: 961774, + 100: 972803, + 101: 982572, + 102: 992815, + 103: 1002973, + 104: 1012821, + 105: 1027072, + 106: 1036918, + 107: 1047805, + 108: 1059080, + 109: 1070155, + 110: 1084160, + 111: 1097774, + 112: 1107206, +} as Record; diff --git a/src/browser-installer/chromium/revisions/win32.ts b/src/browser-installer/chromium/revisions/win32.ts new file mode 100644 index 000000000..763cc6f2a --- /dev/null +++ b/src/browser-installer/chromium/revisions/win32.ts @@ -0,0 +1,42 @@ +export default { + 73: 625980, + 74: 638903, + 75: 652459, + 76: 665037, + 77: 681141, + 78: 694594, + 79: 707225, + 80: 722370, + 81: 737194, + 82: 750023, + 83: 756143, + 84: 769121, + 85: 782817, + 86: 800429, + 87: 813051, + 88: 827139, + 89: 843930, + 90: 858016, + 91: 870819, + 92: 885359, + 93: 902289, + 94: 911612, + 95: 920068, + 96: 929514, + 97: 938622, + 98: 950413, + 99: 961752, + 100: 972803, + 101: 982576, + 102: 992804, + 103: 1002976, + 104: 1012819, + 105: 1027031, + 106: 1036918, + 107: 1047790, + 108: 1059072, + 109: 1070143, + 110: 1084163, + 111: 1097773, + 112: 1107205, +} as Record; diff --git a/src/browser-installer/chromium/revisions/win64.ts b/src/browser-installer/chromium/revisions/win64.ts new file mode 100644 index 000000000..7bf9e27e7 --- /dev/null +++ b/src/browser-installer/chromium/revisions/win64.ts @@ -0,0 +1,42 @@ +export default { + 73: 625982, + 74: 638903, + 75: 652459, + 76: 665038, + 77: 681145, + 78: 694594, + 79: 707229, + 80: 722374, + 81: 737198, + 82: 750000, + 83: 756141, + 84: 769133, + 85: 782823, + 86: 800433, + 87: 813059, + 88: 827103, + 89: 843930, + 90: 858016, + 91: 870818, + 92: 885361, + 93: 902299, + 94: 911616, + 95: 920069, + 96: 929514, + 97: 938627, + 98: 950416, + 99: 961781, + 100: 972790, + 101: 982567, + 102: 992822, + 103: 1002974, + 104: 1012814, + 105: 1027071, + 106: 1036912, + 107: 1047802, + 108: 1059068, + 109: 1070151, + 110: 1084157, + 111: 1097757, + 112: 1107206, +} as Record; diff --git a/src/browser-installer/chromium/utils.ts b/src/browser-installer/chromium/utils.ts new file mode 100644 index 000000000..20a28375b --- /dev/null +++ b/src/browser-installer/chromium/utils.ts @@ -0,0 +1,36 @@ +import os from "os"; +import path from "path"; +import { BrowserPlatform } from "@puppeteer/browsers"; +import { getChromePlatform, getMilestone } from "../utils"; +import { CHROMEDRIVER_STORAGE_API, MIN_CHROMEDRIVER_MAC_ARM_NEW_ARCHIVE_NAME } from "../constants"; + +export const getChromiumBuildId = async (platform: BrowserPlatform, milestone: string | number): Promise => { + const { default: revisions } = await import(`./revisions/${platform}`); + + return String(revisions[milestone]); +}; + +export const getChromeDriverArchiveUrl = (version: string): string => { + const chromeDriverArchiveName: Record = { + linux: "linux64", + mac: "mac64", + mac_arm: "mac64_m1", // eslint-disable-line camelcase + win32: "win32", + win64: "win32", + }; + + const milestone = getMilestone(version); + const platform = getChromePlatform(version); + const isNewMacArm = + platform === BrowserPlatform.MAC_ARM && Number(milestone) >= MIN_CHROMEDRIVER_MAC_ARM_NEW_ARCHIVE_NAME; + const archiveName = isNewMacArm ? "mac_arm64" : chromeDriverArchiveName[platform]; + const archiveUrl = `${CHROMEDRIVER_STORAGE_API}/${version}/chromedriver_${archiveName}.zip`; + + return archiveUrl; +}; + +export const getChromeDriverArchiveTmpPath = (version: string): string => { + const randomString = Math.floor(Math.random() * Date.now()).toString(36); + + return path.join(os.tmpdir(), `chromedriver-${version}-${randomString}.zip`); +}; diff --git a/src/browser-installer/constants.ts b/src/browser-installer/constants.ts new file mode 100644 index 000000000..68cc14b3b --- /dev/null +++ b/src/browser-installer/constants.ts @@ -0,0 +1,13 @@ +export const CHROMEDRIVER_STORAGE_API = "https://chromedriver.storage.googleapis.com"; +export const GECKODRIVER_CARGO_TOML = "https://raw.githubusercontent.com/mozilla/geckodriver/release/Cargo.toml"; +export const MSEDGEDRIVER_API = "https://msedgedriver.azureedge.net"; +export const SAFARIDRIVER_PATH = "/usr/bin/safaridriver"; +export const MIN_CHROME_FOR_TESTING_VERSION = 113; +export const MIN_CHROMEDRIVER_FOR_TESTING_VERSION = 115; +export const MIN_CHROMEDRIVER_MAC_ARM_NEW_ARCHIVE_NAME = 106; +export const MIN_CHROMIUM_MAC_ARM_VERSION = 93; +export const MIN_CHROMIUM_VERSION = 73; +export const MIN_EDGEDRIVER_VERSION = 94; +export const DRIVER_WAIT_TIMEOUT = 10 * 1000; // 10s +export const BYTES_PER_KILOBYTE = 1 << 10; // eslint-disable-line no-bitwise +export const BYTES_PER_MEGABYTE = BYTES_PER_KILOBYTE << 10; // eslint-disable-line no-bitwise diff --git a/src/browser-installer/edge/driver.ts b/src/browser-installer/edge/driver.ts new file mode 100644 index 000000000..e18ea3b06 --- /dev/null +++ b/src/browser-installer/edge/driver.ts @@ -0,0 +1,53 @@ +import { download as downloadEdgeDriver } from "edgedriver"; +import { + Driver, + browserInstallerDebug, + getBrowserPlatform, + getEdgeDriverDir, + getMilestone, + retryFetch, +} from "../utils"; +import { getBinaryPath, getMatchedDriverVersion, installBinary } from "../registry"; +import { MIN_EDGEDRIVER_VERSION, MSEDGEDRIVER_API } from "../constants"; + +const getLatestMajorEdgeDriverVersion = async (milestone: string): Promise => { + const fullVersion = await retryFetch(`${MSEDGEDRIVER_API}/LATEST_RELEASE_${milestone}`).then(res => res.text()); + + if (!fullVersion) { + throw new Error(`Couldn't resolve latest edgedriver version for ${milestone}`); + } + + const versionNormalized = fullVersion + .split("") + .filter(char => /\.|\d/.test(char)) + .join(""); + + browserInstallerDebug(`resolved latest edgedriver@${milestone} version: ${versionNormalized}`); + + return versionNormalized; +}; + +export const installEdgeDriver = async (edgeVersion: string, { force = false } = {}): Promise => { + const platform = getBrowserPlatform(); + const existingLocallyDriverVersion = getMatchedDriverVersion(Driver.EDGEDRIVER, platform, edgeVersion); + + if (existingLocallyDriverVersion && !force) { + browserInstallerDebug( + `A locally installed edgedriver for edge@${edgeVersion} browser was found. Skipping the installation`, + ); + + return getBinaryPath(Driver.EDGEDRIVER, platform, existingLocallyDriverVersion); + } + + const milestone = getMilestone(edgeVersion); + + if (Number(milestone) < MIN_EDGEDRIVER_VERSION) { + throw new Error(`Automatic driver downloader is not available for Edge versions < ${MIN_EDGEDRIVER_VERSION}`); + } + + const driverVersion = await getLatestMajorEdgeDriverVersion(milestone); + + const installFn = (): Promise => downloadEdgeDriver(driverVersion, getEdgeDriverDir(driverVersion)); + + return installBinary(Driver.EDGEDRIVER, platform, driverVersion, installFn); +}; diff --git a/src/browser-installer/edge/index.ts b/src/browser-installer/edge/index.ts new file mode 100644 index 000000000..1aefbe84e --- /dev/null +++ b/src/browser-installer/edge/index.ts @@ -0,0 +1,33 @@ +import { installEdgeDriver } from "./driver"; +import { spawn, type ChildProcess } from "child_process"; +import getPort from "get-port"; +import waitPort from "wait-port"; +import { pipeLogsWithPrefix } from "../../dev-server/utils"; +import { DRIVER_WAIT_TIMEOUT } from "../constants"; + +export { installEdgeDriver }; + +export const runEdgeDriver = async ( + edgeVersion: string, + { debug = false }: { debug?: boolean } = {}, +): Promise<{ gridUrl: string; process: ChildProcess; port: number }> => { + const edgeDriverPath = await installEdgeDriver(edgeVersion); + const randomPort = await getPort(); + + const edgeDriver = spawn(edgeDriverPath, [`--port=${randomPort}`, debug ? `--verbose` : "--silent"], { + windowsHide: true, + detached: false, + }); + + if (debug) { + pipeLogsWithPrefix(edgeDriver, `[edgedriver@${edgeVersion}] `); + } + + const gridUrl = `http://127.0.0.1:${randomPort}`; + + process.once("exit", () => edgeDriver.kill()); + + await waitPort({ port: randomPort, output: "silent", timeout: DRIVER_WAIT_TIMEOUT }); + + return { gridUrl, process: edgeDriver, port: randomPort }; +}; diff --git a/src/browser-installer/firefox/browser.ts b/src/browser-installer/firefox/browser.ts new file mode 100644 index 000000000..b582b05dd --- /dev/null +++ b/src/browser-installer/firefox/browser.ts @@ -0,0 +1,51 @@ +import { canDownload, install as puppeteerInstall } from "@puppeteer/browsers"; +import { + Browser, + browserInstallerDebug, + getBrowserPlatform, + getBrowsersDir, + type DownloadProgressCallback, +} from "../utils"; +import { installBinary, getBinaryPath, getMatchedBrowserVersion } from "../registry"; +import { getFirefoxBuildId, normalizeFirefoxVersion } from "./utils"; + +export const installFirefox = async (version: string, { force = false } = {}): Promise => { + const platform = getBrowserPlatform(); + const existingLocallyBrowserVersion = getMatchedBrowserVersion(Browser.FIREFOX, platform, version); + + if (existingLocallyBrowserVersion && !force) { + browserInstallerDebug(`A locally installed firefox@${version} browser was found. Skipping the installation`); + + return getBinaryPath(Browser.FIREFOX, platform, existingLocallyBrowserVersion); + } + + const normalizedVersion = normalizeFirefoxVersion(version); + const buildId = getFirefoxBuildId(normalizedVersion); + + const cacheDir = getBrowsersDir(); + const canBeInstalled = await canDownload({ browser: Browser.FIREFOX, platform, buildId, cacheDir }); + + if (!canBeInstalled) { + throw new Error( + [ + `firefox@${version} can't be installed.`, + `Probably the version '${version}' is invalid, please try another version.`, + "Version examples: '120', '130.0', '131.0'", + ].join("\n"), + ); + } + + browserInstallerDebug(`installing firefox@${buildId} for ${platform}`); + + const installFn = (downloadProgressCallback: DownloadProgressCallback): Promise => + puppeteerInstall({ + platform, + buildId, + cacheDir, + downloadProgressCallback, + browser: Browser.FIREFOX, + unpack: true, + }).then(result => result.executablePath); + + return installBinary(Browser.FIREFOX, platform, buildId, installFn); +}; diff --git a/src/browser-installer/firefox/driver.ts b/src/browser-installer/firefox/driver.ts new file mode 100644 index 000000000..558cd2e78 --- /dev/null +++ b/src/browser-installer/firefox/driver.ts @@ -0,0 +1,38 @@ +import { download as downloadGeckoDriver } from "geckodriver"; +import { GECKODRIVER_CARGO_TOML } from "../constants"; +import { installBinary, getBinaryPath, getMatchedDriverVersion } from "../registry"; +import { Driver, browserInstallerDebug, getBrowserPlatform, getGeckoDriverDir, retryFetch } from "../utils"; + +const getLatestGeckoDriverVersion = async (): Promise => { + const cargoVersionsToml = await retryFetch(GECKODRIVER_CARGO_TOML).then(res => res.text()); + const version = cargoVersionsToml.split("\n").find(line => line.startsWith("version = ")); + + if (!version) { + throw new Error("Couldn't resolve latest geckodriver version while downloading geckodriver"); + } + + const latestGeckoVersion = version.split(" = ").pop()!.slice(1, -1); + + browserInstallerDebug(`resolved latest geckodriver version: ${latestGeckoVersion}`); + + return latestGeckoVersion; +}; + +export const installLatestGeckoDriver = async (firefoxVersion: string, { force = false } = {}): Promise => { + const platform = getBrowserPlatform(); + const existingLocallyDriverVersion = getMatchedDriverVersion(Driver.GECKODRIVER, platform, firefoxVersion); + + if (existingLocallyDriverVersion && !force) { + browserInstallerDebug( + `A locally installed geckodriver for firefox@${firefoxVersion} browser was found. Skipping the installation`, + ); + + return getBinaryPath(Driver.GECKODRIVER, platform, existingLocallyDriverVersion); + } + + const latestVersion = await getLatestGeckoDriverVersion(); + + const installFn = (): Promise => downloadGeckoDriver(latestVersion, getGeckoDriverDir(latestVersion)); + + return installBinary(Driver.GECKODRIVER, platform, latestVersion, installFn); +}; diff --git a/src/browser-installer/firefox/index.ts b/src/browser-installer/firefox/index.ts new file mode 100644 index 000000000..7d6d42bf9 --- /dev/null +++ b/src/browser-installer/firefox/index.ts @@ -0,0 +1,44 @@ +import type { ChildProcess } from "child_process"; +import { start as startGeckoDriver } from "geckodriver"; +import getPort from "get-port"; +import waitPort from "wait-port"; +import { installFirefox } from "./browser"; +import { installLatestGeckoDriver } from "./driver"; +import { pipeLogsWithPrefix } from "../../dev-server/utils"; +import { DRIVER_WAIT_TIMEOUT } from "../constants"; + +export { installFirefox, installLatestGeckoDriver }; + +export const runGeckoDriver = async ( + firefoxVersion: string, + { debug = false } = {}, +): Promise<{ gridUrl: string; process: ChildProcess; port: number }> => { + const [geckoDriverPath] = await Promise.all([ + installLatestGeckoDriver(firefoxVersion), + installFirefox(firefoxVersion), + ]); + + const randomPort = await getPort(); + + const geckoDriver = await startGeckoDriver({ + customGeckoDriverPath: geckoDriverPath, + port: randomPort, + log: debug ? "debug" : "fatal", + spawnOpts: { + windowsHide: true, + detached: false, + }, + }); + + if (debug) { + pipeLogsWithPrefix(geckoDriver, `[geckodriver@${firefoxVersion}] `); + } + + const gridUrl = `http://127.0.0.1:${randomPort}`; + + process.once("exit", () => geckoDriver.kill()); + + await waitPort({ port: randomPort, output: "silent", timeout: DRIVER_WAIT_TIMEOUT }); + + return { gridUrl, process: geckoDriver, port: randomPort }; +}; diff --git a/src/browser-installer/firefox/utils.ts b/src/browser-installer/firefox/utils.ts new file mode 100644 index 000000000..63275683c --- /dev/null +++ b/src/browser-installer/firefox/utils.ts @@ -0,0 +1,13 @@ +const firefoxChannels = ["stable", "nightly"]; + +export const normalizeFirefoxVersion = (version: string): string => { + return version.includes(".") ? version : `${version}.0`; +}; + +export const getFirefoxBuildId = (version: string): string => { + const normalizedVersion = normalizeFirefoxVersion(version); + + return firefoxChannels.some(channel => normalizedVersion.startsWith(`${channel}_`)) + ? version + : `stable_${normalizedVersion}`; +}; diff --git a/src/browser-installer/index.ts b/src/browser-installer/index.ts new file mode 100644 index 000000000..939b0976e --- /dev/null +++ b/src/browser-installer/index.ts @@ -0,0 +1,4 @@ +export { installBrowser, installBrowsersWithDrivers, BrowserInstallStatus } from "./install"; +export { runBrowserDriver } from "./run"; +export { getDriverNameForBrowserName } from "./utils"; +export type { SupportedBrowser, SupportedDriver } from "./utils"; diff --git a/src/browser-installer/install.ts b/src/browser-installer/install.ts new file mode 100644 index 000000000..b6f101d98 --- /dev/null +++ b/src/browser-installer/install.ts @@ -0,0 +1,112 @@ +import _ from "lodash"; + +/** + * @returns path to browser binary + */ +export const installBrowser = async ( + browserName?: string, + browserVersion?: string, + { force = false, installWebDriver = false } = {}, +): Promise => { + const unsupportedBrowserError = new Error( + [ + `Couldn't install browser '${browserName}', as it is not supported`, + `Currently supported for installation browsers: 'chrome', 'firefox`, + ].join("\n"), + ); + + if (!browserName) { + throw unsupportedBrowserError; + } + + if (!browserVersion) { + throw new Error( + `Couldn't install browser '${browserName}' because it has invalid version: '${browserVersion}'`, + ); + } + + if (/chrome/i.test(browserName)) { + const { installChrome, installChromeDriver } = await import("./chrome"); + + return installWebDriver + ? await Promise.all([ + installChrome(browserVersion, { force }), + installChromeDriver(browserVersion, { force }), + ]).then(binaries => binaries[0]) + : installChrome(browserVersion, { force }); + } else if (/firefox/i.test(browserName)) { + const { installFirefox, installLatestGeckoDriver } = await import("./firefox"); + + return installWebDriver + ? await Promise.all([ + installFirefox(browserVersion, { force }), + installLatestGeckoDriver(browserVersion, { force }), + ]).then(binaries => binaries[0]) + : installFirefox(browserVersion, { force }); + } else if (/edge/i.test(browserName)) { + const { installEdgeDriver } = await import("./edge"); + + if (installWebDriver) { + await installEdgeDriver(browserVersion, { force }); + } + + return null; + } else if (/safari/i.test(browserName)) { + return null; + } + + throw unsupportedBrowserError; +}; + +export const BrowserInstallStatus = { + Ok: "ok", + Skip: "skip", + Error: "error", +} as const; + +type InstallResultSuccess = { status: "ok" }; +type InstallResultSkip = { status: "skip"; reason: string }; +type InstallResultError = { status: "error"; reason: string }; + +export type InstallResult = (InstallResultSuccess | InstallResultSkip | InstallResultError) & { + status: Status; +}; + +type ForceInstallBinaryResult = Promise; + +const forceInstallBinaries = async ( + installFn: typeof installBrowser, + browserName?: string, + browserVersion?: string, +): ForceInstallBinaryResult => { + return installFn(browserName, browserVersion, { force: true, installWebDriver: true }) + .then(successResult => { + return successResult + ? { status: BrowserInstallStatus.Ok } + : { + status: BrowserInstallStatus.Skip, + reason: `Installing ${browserName} is unsupported. Assuming it is installed locally`, + }; + }) + .catch(errorResult => ({ status: BrowserInstallStatus.Error, reason: (errorResult as Error).message })); +}; + +export const installBrowsersWithDrivers = async ( + browsersToInstall: { browserName?: string; browserVersion?: string }[], +): Promise>> => { + const uniqBrowsers = _.uniqBy(browsersToInstall, b => `${b.browserName}@${b.browserVersion}`); + const installPromises = [] as Promise[]; + const browsersInstallResult: Record> = {}; + + for (const { browserName, browserVersion } of uniqBrowsers) { + installPromises.push( + forceInstallBinaries(installBrowser, browserName, browserVersion).then(result => { + browsersInstallResult[`${browserName}@${browserVersion}`] = result; + }), + ); + } + + await Promise.all(installPromises); + + return browsersInstallResult; +}; diff --git a/src/browser-installer/registry/cli-progress-bar.ts b/src/browser-installer/registry/cli-progress-bar.ts new file mode 100644 index 000000000..61a278d49 --- /dev/null +++ b/src/browser-installer/registry/cli-progress-bar.ts @@ -0,0 +1,35 @@ +import { MultiBar, type SingleBar } from "cli-progress"; +import type { DownloadProgressCallback } from "../utils"; +import { BYTES_PER_MEGABYTE } from "../constants"; + +export type RegisterProgressBarFn = (browserName: string, browserVersion: string) => DownloadProgressCallback; + +export const createBrowserDownloadProgressBar = (): { register: RegisterProgressBarFn } => { + const progressBar = new MultiBar({ + stopOnComplete: true, + forceRedraw: true, + autopadding: true, + hideCursor: true, + fps: 5, + format: " [{bar}] | {filename} | {value}/{total} MB", + }); + + const register: RegisterProgressBarFn = (browserName, browserVersion) => { + let bar: SingleBar; + + const downloadProgressCallback: DownloadProgressCallback = (downloadedBytes, totalBytes) => { + if (!bar) { + const totalMB = Math.round((totalBytes / BYTES_PER_MEGABYTE) * 100) / 100; + bar = progressBar.create(totalMB, 0, { filename: `${browserName}@${browserVersion}` }); + } + + const downloadedMB = Math.round((downloadedBytes / BYTES_PER_MEGABYTE) * 100) / 100; + + bar.update(downloadedMB); + }; + + return downloadProgressCallback; + }; + + return { register }; +}; diff --git a/src/browser-installer/registry/index.ts b/src/browser-installer/registry/index.ts new file mode 100644 index 000000000..e5b600e04 --- /dev/null +++ b/src/browser-installer/registry/index.ts @@ -0,0 +1,215 @@ +import type { BrowserPlatform } from "@puppeteer/browsers"; +import { readJsonSync, outputJSONSync, existsSync } from "fs-extra"; +import path from "path"; +import { + getRegistryPath, + browserInstallerDebug, + Driver, + Browser, + getMilestone, + normalizeChromeVersion, + semverVersionsComparator, + type SupportedBrowser, + type SupportedDriver, + type DownloadProgressCallback, +} from "../utils"; +import { getFirefoxBuildId } from "../firefox/utils"; +import logger from "../../utils/logger"; +import type { createBrowserDownloadProgressBar } from "./cli-progress-bar"; + +type VersionToPathMap = Record>; +type BinaryName = Exclude; +type RegistryKey = `${BinaryName}_${BrowserPlatform}`; +type Registry = Record; + +const registryPath = getRegistryPath(); +const registry: Registry = existsSync(registryPath) ? readJsonSync(registryPath) : {}; + +let cliProgressBar: ReturnType | null = null; +let warnedFirstTimeInstall = false; + +const getRegistryKey = (name: BinaryName, platform: BrowserPlatform): RegistryKey => `${name}_${platform}`; + +export const getBinaryPath = async (name: BinaryName, platform: BrowserPlatform, version: string): Promise => { + const registryKey = getRegistryKey(name, platform); + + if (!registry[registryKey]) { + throw new Error(`Binary '${name}' on '${platform}' is not installed`); + } + + if (!registry[registryKey][version]) { + throw new Error(`Version '${version}' of driver '${name}' on '${platform}' is not installed`); + } + + const binaryRelativePath = await registry[registryKey][version]; + + browserInstallerDebug(`resolved '${name}@${version}' on ${platform} to ${binaryRelativePath}`); + + return path.resolve(registryPath, binaryRelativePath); +}; + +const addBinaryToRegistry = ( + name: BinaryName, + platform: BrowserPlatform, + version: string, + absoluteBinaryPath: string, +): void => { + const registryKey = getRegistryKey(name, platform); + const relativePath = path.relative(registryPath, absoluteBinaryPath); + + registry[registryKey] ||= {}; + registry[registryKey][version] = relativePath; + + const replacer = (_: string, value: unknown): unknown | undefined => { + if ((value as Promise).then) { + return; + } + + return value; + }; + + browserInstallerDebug(`adding '${name}@${version}' on '${platform}' to registry at ${relativePath}`); + outputJSONSync(registryPath, registry, { replacer }); +}; + +const getBinaryVersions = (name: BinaryName, platform: BrowserPlatform): string[] => { + const registryKey = getRegistryKey(name, platform); + + if (!registry[registryKey]) { + return []; + } + + return Object.keys(registry[registryKey]); +}; + +const hasBinaryVersion = (name: BinaryName, platform: BrowserPlatform, version: string): boolean => + getBinaryVersions(name, platform).includes(version); + +export const getMatchedDriverVersion = ( + driverName: SupportedDriver, + platform: BrowserPlatform, + browserVersion: string, +): string | null => { + const registryKey = getRegistryKey(driverName, platform); + + if (!registry[registryKey]) { + return null; + } + + if (driverName === Driver.CHROMEDRIVER || driverName === Driver.EDGEDRIVER) { + const milestone = getMilestone(browserVersion); + const buildIds = getBinaryVersions(driverName, platform); + const suitableBuildIds = buildIds.filter(buildId => buildId.startsWith(milestone)); + + if (!suitableBuildIds.length) { + return null; + } + + return suitableBuildIds.sort(semverVersionsComparator).pop() as string; + } + + if (driverName === Driver.GECKODRIVER) { + const buildIds = Object.keys(registry[registryKey]); + const buildIdsSorted = buildIds.sort(semverVersionsComparator); + + return buildIdsSorted.length ? buildIdsSorted[buildIdsSorted.length - 1] : null; + } + + return null; +}; + +export const getMatchedBrowserVersion = ( + browserName: SupportedBrowser, + platform: BrowserPlatform, + browserVersion: string, +): string | null => { + const registryKey = getRegistryKey(browserName, platform); + + if (!registry[registryKey]) { + return null; + } + + let buildPrefix: string; + + switch (browserName) { + case Browser.CHROME: + buildPrefix = normalizeChromeVersion(browserVersion); + break; + + case Browser.CHROMIUM: + buildPrefix = getMilestone(browserVersion); + break; + + case Browser.FIREFOX: + buildPrefix = getFirefoxBuildId(browserVersion); + break; + + default: + return null; + } + + const buildIds = getBinaryVersions(browserName, platform); + const suitableBuildIds = buildIds.filter(buildId => buildId.startsWith(buildPrefix)); + + if (!suitableBuildIds.length) { + return null; + } + + const firefoxVersionComparator = (a: string, b: string): number => { + a = a.slice(a.indexOf("_") + 1); + b = b.slice(b.indexOf("_") + 1); + + // Firefox has versions like "stable_131.0a1" and "stable_129.0b9" + // Parsing raw numbers as hex values is needed in order to distinguish "129.0b9" and "129.0b7" for example + return parseInt(a.replace(".", ""), 16) - parseInt(b.replace(".", ""), 16); + }; + + const comparator = browserName === Browser.FIREFOX ? firefoxVersionComparator : semverVersionsComparator; + const suitableBuildIdsSorted = suitableBuildIds.sort(comparator); + + return suitableBuildIdsSorted[suitableBuildIdsSorted.length - 1]; +}; + +export const installBinary = async ( + name: BinaryName, + platform: BrowserPlatform, + version: string, + installFn: (downloadProgressCallback: DownloadProgressCallback) => Promise, +): Promise => { + const registryKey = getRegistryKey(name, platform); + + if (hasBinaryVersion(name, platform, version)) { + return getBinaryPath(name, platform, version); + } + + browserInstallerDebug(`installing '${name}@${version}' on '${platform}'`); + + if (!cliProgressBar) { + const { createBrowserDownloadProgressBar } = await import("./cli-progress-bar"); + + cliProgressBar = createBrowserDownloadProgressBar(); + } + + const originalDownloadProgressCallback = cliProgressBar.register(name, version); + const downloadProgressCallback: DownloadProgressCallback = (...args) => { + if (!warnedFirstTimeInstall) { + logger.warn("Downloading Testplane browsers"); + logger.warn("Note: this is one-time action. It may take a while..."); + + warnedFirstTimeInstall = true; + } + + return originalDownloadProgressCallback(...args); + }; + + const installPromise = installFn(downloadProgressCallback).then(executablePath => { + addBinaryToRegistry(name, platform, version, executablePath); + + return executablePath; + }); + + registry[registryKey] ||= {}; + registry[registryKey][version] = installPromise; + + return installPromise; +}; diff --git a/src/browser-installer/run.ts b/src/browser-installer/run.ts new file mode 100644 index 000000000..9e3adc45c --- /dev/null +++ b/src/browser-installer/run.ts @@ -0,0 +1,21 @@ +import type { ChildProcess } from "child_process"; +import { Driver, type SupportedDriver } from "./utils"; + +export const runBrowserDriver = async ( + driverName: SupportedDriver, + browserVersion: string, + { debug = false } = {}, +): Promise<{ gridUrl: string; process: ChildProcess; port: number }> => { + switch (driverName) { + case Driver.CHROMEDRIVER: + return import("./chrome").then(module => module.runChromeDriver(browserVersion, { debug })); + case Driver.EDGEDRIVER: + return import("./edge").then(module => module.runEdgeDriver(browserVersion, { debug })); + case Driver.GECKODRIVER: + return import("./firefox").then(module => module.runGeckoDriver(browserVersion, { debug })); + case Driver.SAFARIDRIVER: + return import("./safari").then(module => module.runSafariDriver({ debug })); + default: + throw new Error(`Invalid driver name: ${driverName}. Expected one of: ${Object.values(Driver).join(", ")}`); + } +}; diff --git a/src/browser-installer/safari/index.ts b/src/browser-installer/safari/index.ts new file mode 100644 index 000000000..d41338ea8 --- /dev/null +++ b/src/browser-installer/safari/index.ts @@ -0,0 +1,30 @@ +import { spawn, type ChildProcess } from "child_process"; +import getPort from "get-port"; +import waitPort from "wait-port"; +import { pipeLogsWithPrefix } from "../../dev-server/utils"; +import { DRIVER_WAIT_TIMEOUT, SAFARIDRIVER_PATH } from "../constants"; + +export const runSafariDriver = async ({ debug = false }: { debug?: boolean } = {}): Promise<{ + gridUrl: string; + process: ChildProcess; + port: number; +}> => { + const randomPort = await getPort(); + + const safariDriver = spawn(SAFARIDRIVER_PATH, [`--port=${randomPort}`], { + windowsHide: true, + detached: false, + }); + + if (debug) { + pipeLogsWithPrefix(safariDriver, `[safaridriver] `); + } + + const gridUrl = `http://127.0.0.1:${randomPort}`; + + process.once("exit", () => safariDriver.kill()); + + await waitPort({ port: randomPort, output: "silent", timeout: DRIVER_WAIT_TIMEOUT }); + + return { gridUrl, process: safariDriver, port: randomPort }; +}; diff --git a/src/browser-installer/utils.ts b/src/browser-installer/utils.ts new file mode 100644 index 000000000..c15947fa6 --- /dev/null +++ b/src/browser-installer/utils.ts @@ -0,0 +1,183 @@ +import { detectBrowserPlatform, BrowserPlatform, Browser as PuppeteerBrowser } from "@puppeteer/browsers"; +import extractZip from "extract-zip"; +import os from "os"; +import path from "path"; +import { createWriteStream } from "fs"; +import { Readable } from "stream"; +import debug from "debug"; +import { MIN_CHROMIUM_MAC_ARM_VERSION } from "./constants"; + +export type DownloadProgressCallback = (downloadedBytes: number, totalBytes: number) => void; + +export const browserInstallerDebug = debug("testplane:browser-installer"); + +export const Browser = { + CHROME: PuppeteerBrowser.CHROME, + CHROMIUM: PuppeteerBrowser.CHROMIUM, + FIREFOX: PuppeteerBrowser.FIREFOX, + SAFARI: "safari", + EDGE: "MicrosoftEdge", +} as const; + +export const Driver = { + CHROMEDRIVER: PuppeteerBrowser.CHROMEDRIVER, + GECKODRIVER: "geckodriver", + SAFARIDRIVER: "safaridriver", + EDGEDRIVER: "edgedriver", +} as const; + +export type SupportedBrowser = (typeof Browser)[keyof typeof Browser]; +export type SupportedDriver = (typeof Driver)[keyof typeof Driver]; + +export const getDriverNameForBrowserName = (browserName: SupportedBrowser): SupportedDriver | null => { + if (browserName === Browser.CHROME || browserName === Browser.CHROMIUM) { + return Driver.CHROMEDRIVER; + } + + if (browserName === Browser.FIREFOX) { + return Driver.GECKODRIVER; + } + + if (browserName === Browser.SAFARI) { + return Driver.SAFARIDRIVER; + } + + if (browserName === Browser.EDGE) { + return Driver.EDGEDRIVER; + } + + return null; +}; + +export const createBrowserLabel = (browserName: string, version = "latest"): string => browserName + "@" + version; + +export const getMilestone = (version: string | number): string => { + if (typeof version === "number") { + return String(version); + } + + return version.split(".")[0]; +}; + +export const semverVersionsComparator = (a: string, b: string): number => { + const splitVersion = (version: string): number[] => + version + .replaceAll(/[^\d.]/g, "") + .split(".") + .filter(Boolean) + .map(Number); + + const versionPartsA = splitVersion(a); + const versionPartsB = splitVersion(b); + + for (let i = 0; i < Math.min(versionPartsA.length, versionPartsB.length); i++) { + if (versionPartsA[i] !== versionPartsB[i]) { + return versionPartsA[i] - versionPartsB[i]; + } + } + + return 0; +}; + +export const normalizeChromeVersion = (version: string): string => { + const versionParts = version.split(".").filter(Boolean); + + if (versionParts.length === 2) { + return versionParts[0]; + } + + if (versionParts.length >= 3) { + return versionParts.slice(0, 3).join("."); + } + + return versionParts[0]; +}; + +export const getBrowserPlatform = (): BrowserPlatform => { + const platform = detectBrowserPlatform(); + + if (!platform) { + throw new Error(`Got an error while trying to download browsers: platform "${platform}" is not supported`); + } + + return platform; +}; + +export const getChromePlatform = (version: string): BrowserPlatform => { + const milestone = getMilestone(version); + const platform = getBrowserPlatform(); + + if (platform === BrowserPlatform.MAC_ARM && Number(milestone) < MIN_CHROMIUM_MAC_ARM_VERSION) { + return BrowserPlatform.MAC; + } + + return platform; +}; + +const resolveUserPath = (userPath: string): string => + userPath.startsWith("~") ? path.resolve(os.homedir(), userPath.slice(1)) : path.resolve(userPath); + +const getCacheDir = (envValueOverride = process.env.TESTPLANE_BROWSERS_PATH): string => + envValueOverride ? resolveUserPath(envValueOverride) : path.join(os.homedir(), ".testplane"); + +export const getRegistryPath = (envValueOverride?: string): string => + path.join(getCacheDir(envValueOverride), "registry.json"); + +export const getBrowsersDir = (): string => path.join(getCacheDir(), "browsers"); +const getDriversDir = (): string => path.join(getCacheDir(), "drivers"); + +const getDriverDir = (driverName: string, driverVersion: string): string => + path.join(getDriversDir(), driverName, driverVersion); + +export const getGeckoDriverDir = (driverVersion: string): string => + getDriverDir("geckodriver", getBrowserPlatform() + "-" + driverVersion); +export const getEdgeDriverDir = (driverVersion: string): string => + getDriverDir("edgedriver", getBrowserPlatform() + "-" + driverVersion); +export const getChromiumDriverDir = (driverVersion: string): string => + getDriverDir("chromedriver", getBrowserPlatform() + "-" + driverVersion); +export const getChromeDriverDir = (): string => getDriversDir(); // path is set by @puppeteer/browsers.install + +export const retryFetch = async ( + url: Parameters[0], + opts?: Parameters[1], + retry = 3, + retryDelay = 100, +): ReturnType => { + while (retry > 0) { + try { + return await fetch(url, opts); + } catch (e) { + retry = retry - 1; + + if (retry <= 0) { + throw e; + } + + await new Promise(resolve => setTimeout(resolve, retryDelay)); + } + } + + return null as never; +}; + +export const downloadFile = async (url: string, filePath: string): Promise => { + const writeStream = createWriteStream(filePath); + const response = await fetch(url); + + if (!response.ok || !response.body) { + throw new Error(`Unable to download file from ${url}: ${response.statusText}`); + } + + const stream = Readable.fromWeb(response.body as never).pipe(writeStream); + + return new Promise((resolve, reject) => { + stream.on("error", reject); + stream.on("close", resolve); + }); +}; + +export const unzipFile = async (zipPath: string, outputDir: string): Promise => { + await extractZip(zipPath, { dir: outputDir }); + + return outputDir; +}; diff --git a/src/browser-pool/basic-pool.ts b/src/browser-pool/basic-pool.ts index 82a3120a0..2988e9a7f 100644 --- a/src/browser-pool/basic-pool.ts +++ b/src/browser-pool/basic-pool.ts @@ -7,12 +7,14 @@ import { AsyncEmitter, MasterEvents } from "../events"; import { BrowserOpts, Pool } from "./types"; import { Config } from "../config"; import { Browser } from "../browser/browser"; +import { WebdriverPool } from "./webdriver-pool"; export class BasicPool implements Pool { private _config: Config; private _emitter: AsyncEmitter; private _activeSessions: Record; private _cancelled: boolean; + private _wdPool: WebdriverPool; log: debug.Debugger; static create(config: Config, emitter: AsyncEmitter): BasicPool { @@ -26,10 +28,11 @@ export class BasicPool implements Pool { this._activeSessions = {}; this._cancelled = false; + this._wdPool = new WebdriverPool(); } async getBrowser(id: string, opts: BrowserOpts = {}): Promise { - const browser = NewBrowser.create(this._config, { ...opts, id }); + const browser = NewBrowser.create(this._config, { ...opts, id, wdPool: this._wdPool }); try { await browser.init(); diff --git a/src/browser-pool/webdriver-pool.ts b/src/browser-pool/webdriver-pool.ts new file mode 100644 index 000000000..715acb6b3 --- /dev/null +++ b/src/browser-pool/webdriver-pool.ts @@ -0,0 +1,103 @@ +import type { ChildProcess } from "child_process"; +import { runBrowserDriver, getDriverNameForBrowserName } from "../browser-installer"; +import type { SupportedBrowser, SupportedDriver } from "../browser-installer"; + +type DriverVersion = string; +type Port = string; +type ChildProcessWithStatus = { process: ChildProcess; gridUrl: string; isBusy: boolean }; +export type WdProcess = { gridUrl: string; free: () => void; kill: () => void }; + +export class WebdriverPool { + private driverProcess: Map>>; + private portToDriverProcess: Map; + + constructor() { + this.driverProcess = new Map(); + this.portToDriverProcess = new Map(); + } + + async getWebdriver( + browserName: SupportedBrowser, + browserVersion: string, + { debug = false } = {}, + ): ReturnType { + const driverName = getDriverNameForBrowserName(browserName); + + if (!driverName) { + throw new Error( + [ + `Couldn't run browser driver for "${browserName}", as this browser is not supported`, + `Supported browsers: "chrome", "firefox", "safari", "MicrosoftEdge"`, + ].join("\n"), + ); + } + + if (!browserVersion) { + throw new Error(`Couldn't run browser driver for "${browserName}" because its version is undefined`); + } + + const wdProcesses = this.driverProcess.get(driverName)?.get(browserVersion) ?? {}; + + for (const port in wdProcesses) { + if (!wdProcesses[port].isBusy) { + wdProcesses[port].isBusy = true; + + return { + gridUrl: wdProcesses[port].gridUrl, + free: () => this.freeWebdriver(port), + kill: () => this.killWebdriver(driverName, browserVersion, port), + }; + } + } + + return this.createWebdriverProcess(driverName, browserVersion, { debug }); + } + + private freeWebdriver(port: Port): void { + const wdProcess = this.portToDriverProcess.get(port); + + if (wdProcess) { + wdProcess.isBusy = false; + } + } + + private killWebdriver(driverName: SupportedDriver, browserVersion: string, port: Port): void { + const wdProcess = this.portToDriverProcess.get(port); + const nodes = this.driverProcess.get(driverName)?.get(browserVersion); + + if (wdProcess && nodes) { + wdProcess.process.kill(); + this.portToDriverProcess.delete(port); + delete nodes[port]; + } + } + + private async createWebdriverProcess( + driverName: SupportedDriver, + browserVersion: string, + { debug = false } = {}, + ): Promise { + const driver = await runBrowserDriver(driverName, browserVersion, { debug }); + + if (!this.driverProcess.has(driverName)) { + this.driverProcess.set(driverName, new Map()); + } + + if (!this.driverProcess.get(driverName)?.has(browserVersion)) { + this.driverProcess.get(driverName)?.set(browserVersion, {}); + } + + const nodes = this.driverProcess.get(driverName)?.get(browserVersion) as Record; + const node = { process: driver.process, gridUrl: driver.gridUrl, isBusy: true }; + + nodes[driver.port] = node; + + this.portToDriverProcess.set(String(driver.port), node); + + return { + gridUrl: driver.gridUrl, + free: () => this.freeWebdriver(String(driver.port)), + kill: () => this.killWebdriver(driverName, browserVersion, String(driver.port)), + }; + } +} diff --git a/src/browser/browser.ts b/src/browser/browser.ts index c2f4d6efc..31f58796e 100644 --- a/src/browser/browser.ts +++ b/src/browser/browser.ts @@ -14,6 +14,7 @@ import { Config } from "../config"; import { AsyncEmitter } from "../events"; import { BrowserConfig } from "../config/browser-config"; import Callstack from "./history/callstack"; +import type { WdProcess, WebdriverPool } from "../browser-pool/webdriver-pool"; const CUSTOM_SESSION_OPTS = [ "outputDir", @@ -33,6 +34,7 @@ export type BrowserOpts = { version?: string; state?: Record; emitter?: AsyncEmitter; + wdPool?: WebdriverPool; }; export type BrowserState = { @@ -50,6 +52,8 @@ export class Browser { protected _callstackHistory: Callstack | null; protected _state: BrowserState; protected _customCommands: Set; + protected _wdPool?: WebdriverPool; + protected _wdProcess: WdProcess | null; id: string; version?: string; @@ -69,11 +73,13 @@ export class Browser { this._debug = config.system.debug; this._session = null; this._callstackHistory = null; + this._wdProcess = null; this._state = { ...opts.state, isBroken: false, }; this._customCommands = new Set(); + this._wdPool = opts.wdPool; } setHttpTimeout(timeout: number | null): void { diff --git a/src/browser/existing-browser.js b/src/browser/existing-browser.js index 77ef5d857..c010ae6df 100644 --- a/src/browser/existing-browser.js +++ b/src/browser/existing-browser.js @@ -90,7 +90,7 @@ module.exports = class ExistingBrowser extends Browser { } // https://github.com/webdriverio/webdriverio/issues/11396 - if (this._config.automationProtocol === "webdriver" && opts.disableAnimation) { + if (this._config.automationProtocol === WEBDRIVER_PROTOCOL && opts.disableAnimation) { await this._disableIframeAnimations(); } @@ -375,7 +375,7 @@ module.exports = class ExistingBrowser extends Browser { async _cleanupPageAnimations() { await this._cleanupFrameAnimations(); - if (this._config.automationProtocol === "webdriver") { + if (this._config.automationProtocol === WEBDRIVER_PROTOCOL) { await this._cleanupIframeAnimations(); } } diff --git a/src/browser/new-browser.ts b/src/browser/new-browser.ts index 7e231a6b3..58f27c8ff 100644 --- a/src/browser/new-browser.ts +++ b/src/browser/new-browser.ts @@ -9,9 +9,11 @@ import signalHandler from "../signal-handler"; import { runGroup } from "./history"; import { warn } from "../utils/logger"; import { getInstance } from "../config/runtime-config"; -import { DEVTOOLS_PROTOCOL } from "../constants/config"; +import { DEVTOOLS_PROTOCOL, WEBDRIVER_PROTOCOL, LOCAL_GRID_URL } from "../constants/config"; import { Config } from "../config"; import { BrowserConfig } from "../config/browser-config"; +import { gridUrl as DEFAULT_GRID_URL } from "../config/defaults"; +import { installBrowser, type SupportedBrowser } from "../browser-installer"; export type CapabilityName = "goog:chromeOptions" | "moz:firefoxOptions" | "ms:edgeOptions"; export type HeadlessBrowserOptions = Record< @@ -44,6 +46,10 @@ const headlessBrowserOptions: HeadlessBrowserOptions = { capabilityName: "ms:edgeOptions", getArgs: (): string[] => ["--headless"], }, + microsoftedge: { + capabilityName: "ms:edgeOptions", + getArgs: (): string[] => ["--headless"], + }, }; export class NewBrowser extends Browser { @@ -77,13 +83,17 @@ export class NewBrowser extends Browser { try { this.setHttpTimeout(this._config.sessionQuitTimeout); await this._session!.deleteSession(); + this._wdProcess?.free(); } catch (e) { warn(`WARNING: Can not close session: ${(e as Error).message}`); + this._wdProcess?.kill(); + } finally { + this._wdProcess = null; } } - protected _createSession(): Promise { - const sessionOpts = this._getSessionOpts(); + protected async _createSession(): Promise { + const sessionOpts = await this._getSessionOpts(); return remote(sessionOpts); } @@ -108,10 +118,26 @@ export class NewBrowser extends Browser { } } - protected _getSessionOpts(): RemoteOptions { + protected _isLocalGridUrl(): boolean { + return this._config.gridUrl === LOCAL_GRID_URL || getInstance().local; + } + + protected async _getSessionOpts(): Promise { const config = this._config; - const gridUri = new URI(config.gridUrl); - const capabilities = this._extendCapabilities(config); + + let gridUrl; + + if (this._isLocalGridUrl() && config.automationProtocol === WEBDRIVER_PROTOCOL) { + gridUrl = await this._getLocalWebdriverGridUrl(); + } else { + // if automationProtocol is not "webdriver", fallback to default grid url from "local" + // because in "devtools" protocol we dont need gridUrl, but it still has to be valid URL + gridUrl = config.gridUrl === LOCAL_GRID_URL ? DEFAULT_GRID_URL : config.gridUrl; + } + + const gridUri = new URI(gridUrl); + + const capabilities = await this._extendCapabilities(config); const { devtools } = getInstance(); const options = { @@ -133,7 +159,7 @@ export class NewBrowser extends Browser { return options as RemoteOptions; } - protected _extendCapabilities(config: BrowserConfig): WebdriverIO.Capabilities { + protected _extendCapabilities(config: BrowserConfig): Promise { const capabilitiesExtendedByVersion = this.version ? this._extendCapabilitiesByVersion() : config.desiredCapabilities; @@ -141,7 +167,10 @@ export class NewBrowser extends Browser { config.headless, capabilitiesExtendedByVersion!, ); - return capabilitiesWithAddedHeadless; + + return this._isLocalGridUrl() + ? this._addExecutablePath(config, capabilitiesWithAddedHeadless) + : Promise.resolve(capabilitiesWithAddedHeadless); } protected _addHeadlessCapability( @@ -151,8 +180,8 @@ export class NewBrowser extends Browser { if (!headless) { return capabilities; } - const capabilitySettings = - headlessBrowserOptions[capabilities.browserName as keyof typeof headlessBrowserOptions]; + const browserNameLowerCase = capabilities.browserName?.toLocaleLowerCase() as string; + const capabilitySettings = headlessBrowserOptions[browserNameLowerCase]; if (!capabilitySettings) { warn(`WARNING: Headless setting is not supported for ${capabilities.browserName} browserName`); return capabilities; @@ -175,6 +204,43 @@ export class NewBrowser extends Browser { return assign({}, desiredCapabilities, { [versionKeyName]: this.version }); } + protected async _getLocalWebdriverGridUrl(): Promise { + if (!this._wdPool) { + throw new Error("webdriver pool is not defined"); + } + + if (this._wdProcess) { + return this._wdProcess.gridUrl; + } + + this._wdProcess = await this._wdPool.getWebdriver( + this._config.desiredCapabilities?.browserName as SupportedBrowser, + this._config.desiredCapabilities?.browserVersion as string, + { debug: this._config.system.debug }, + ); + + return this._wdProcess.gridUrl; + } + + protected async _addExecutablePath( + config: BrowserConfig, + capabilities: WebdriverIO.Capabilities, + ): Promise { + const browserNameLowerCase = config.desiredCapabilities?.browserName?.toLowerCase() as string; + const executablePath = await installBrowser( + this._config.desiredCapabilities?.browserName as SupportedBrowser, + this._config.desiredCapabilities?.browserVersion as string, + ); + + if (executablePath) { + const { capabilityName } = headlessBrowserOptions[browserNameLowerCase]; + capabilities[capabilityName] ||= {}; + capabilities[capabilityName]!.binary ||= executablePath; + } + + return capabilities; + } + protected _getGridHost(url: URI): string { return new URI({ username: url.username(), diff --git a/src/cli/commands/install-deps/index.ts b/src/cli/commands/install-deps/index.ts new file mode 100644 index 000000000..7cc019509 --- /dev/null +++ b/src/cli/commands/install-deps/index.ts @@ -0,0 +1,106 @@ +import { Testplane } from "../../../testplane"; +import { CliCommands } from "../../constants"; +import logger from "../../../utils/logger"; +import { installBrowsersWithDrivers, BrowserInstallStatus } from "../../../browser-installer"; + +const { INSTALL_DEPS: commandName } = CliCommands; + +type BrowsersInstallPerStatus = { + ok: Array<{ tag: string }>; + skip: Array<{ tag: string; reason: string }>; + error: Array<{ tag: string; reason: string }>; +}; + +const logResult = ( + browsersInstallPerStatus: BrowsersInstallPerStatus, + logType: "log" | "warn" | "error", + status: T, + heading: string, + formatLine: (arg: BrowsersInstallPerStatus[T][number]) => string, +): void => { + if (!browsersInstallPerStatus[status].length) { + return; + } + + logger[logType]([heading, ...browsersInstallPerStatus[status].map(formatLine), ""].join("\n")); +}; + +export const registerCmd = (cliTool: typeof commander, testplane: Testplane): void => { + cliTool + .command(commandName) + .description("Install browsers to run locally with 'gridUrl': 'local' or '--local' cli argument") + .arguments("[browsers...]") + .action(async (browsers: string[]) => { + try { + if (!browsers.length) { + browsers = Object.keys(testplane.config.browsers); + } + + const browsersToInstall = browsers.map(browser => { + if (browser in testplane.config.browsers) { + const browserConfig = testplane.config.browsers[browser]; + const { browserName, browserVersion } = browserConfig.desiredCapabilities || {}; + + return { browserName, browserVersion }; + } else if (browser.includes("@")) { + const [browserName, browserVersion] = browser.split("@", 2); + + return { browserName, browserVersion }; + } else { + throw new Error( + [ + `Unknown browser: ${browser}.`, + `Expected config's or '@' (example: "chrome@130")`, + ].join("\n"), + ); + } + }); + + const browsersInstallResult = await installBrowsersWithDrivers(browsersToInstall); + const browserTags = Object.keys(browsersInstallResult); + const browsersInstallPerStatus: BrowsersInstallPerStatus = { + [BrowserInstallStatus.Ok]: [], + [BrowserInstallStatus.Skip]: [], + [BrowserInstallStatus.Error]: [], + }; + + for (const tag of browserTags) { + const result = browsersInstallResult[tag]; + const bucket = browsersInstallPerStatus[result.status]; + + if ("reason" in result) { + bucket.push({ tag, reason: result.reason }); + } else { + (bucket as { tag: string }[]).push({ tag }); + } + } + + logResult( + browsersInstallPerStatus, + "log", + BrowserInstallStatus.Ok, + "These browsers are downloaded successfully:", + ({ tag }) => `- ${tag}`, + ); + + logResult( + browsersInstallPerStatus, + "warn", + BrowserInstallStatus.Skip, + "Browser install for these browsers was skipped:", + ({ tag, reason }) => `- ${tag}: ${reason}`, + ); + + logResult( + browsersInstallPerStatus, + "error", + BrowserInstallStatus.Error, + "An error occured while trying to download these browsers:", + ({ tag, reason }) => `- ${tag}: ${reason}`, + ); + } catch (err) { + logger.error((err as Error).stack || err); + process.exit(1); + } + }); +}; diff --git a/src/cli/constants.ts b/src/cli/constants.ts index 22fa8ec7a..ff7019f03 100644 --- a/src/cli/constants.ts +++ b/src/cli/constants.ts @@ -1,3 +1,4 @@ export const CliCommands = { LIST_TESTS: "list-tests", + INSTALL_DEPS: "install-deps", } as const; diff --git a/src/cli/index.ts b/src/cli/index.ts index 53812a12d..929bc4a6b 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -67,6 +67,7 @@ export const run = (opts: TestplaneRunOpts = {}): void => { .option("--repl-before-test [type]", "open repl interface before test run", Boolean, false) .option("--repl-on-fail [type]", "open repl interface on test fail only", Boolean, false) .option("--devtools", "switches the browser to the devtools mode with using CDP protocol") + .option("--local", "use local browsers, managed by testplane (same as 'gridUrl': 'local')") .arguments("[paths...]") .action(async (paths: string[]) => { try { @@ -83,6 +84,7 @@ export const run = (opts: TestplaneRunOpts = {}): void => { replBeforeTest, replOnFail, devtools, + local, } = program; await handleRequires(requireModules); @@ -101,6 +103,7 @@ export const run = (opts: TestplaneRunOpts = {}): void => { onFail: replOnFail, }, devtools: devtools || false, + local: local || false, }); process.exit(isTestsSuccess ? 0 : 1); diff --git a/src/constants/config.js b/src/constants/config.js index eab3753ca..a37d86882 100644 --- a/src/constants/config.js +++ b/src/constants/config.js @@ -10,4 +10,5 @@ module.exports = { }, NODEJS_TEST_RUN_ENV: "nodejs", BROWSER_TEST_RUN_ENV: "browser", + LOCAL_GRID_URL: "local", }; diff --git a/src/dev-server/utils.ts b/src/dev-server/utils.ts index acec72b94..f4d70b8f2 100644 --- a/src/dev-server/utils.ts +++ b/src/dev-server/utils.ts @@ -2,7 +2,7 @@ import { pipeline, Transform, TransformCallback } from "stream"; import path from "path"; import fs from "fs"; import chalk from "chalk"; -import type { ChildProcessWithoutNullStreams } from "child_process"; +import type { ChildProcess, ChildProcessWithoutNullStreams } from "child_process"; import logger from "../utils/logger"; import type { Config } from "../config"; @@ -55,13 +55,22 @@ class WithPrefixTransformer extends Transform { } } -export const pipeLogsWithPrefix = (childProcess: ChildProcessWithoutNullStreams, prefix: string): void => { +export const pipeLogsWithPrefix = ( + childProcess: ChildProcess | ChildProcessWithoutNullStreams, + prefix: string, +): void => { const logOnErrorCb = (error: Error | null): void => { if (error) { logger.error("Got an error trying to pipeline dev server logs:", error.message); } }; + if (!childProcess.stdout || !childProcess.stderr) { + logger.error("Couldn't pipe child process logs as it seems to not be spawned successfully"); + + return; + } + pipeline(childProcess.stdout, new WithPrefixTransformer(prefix), process.stdout, logOnErrorCb); pipeline(childProcess.stderr, new WithPrefixTransformer(prefix), process.stderr, logOnErrorCb); }; diff --git a/src/testplane.ts b/src/testplane.ts index aad8a88ff..18560e048 100644 --- a/src/testplane.ts +++ b/src/testplane.ts @@ -36,6 +36,7 @@ interface RunOpts { onFail: boolean; }; devtools: boolean; + local: boolean; } export type FailedListItem = { @@ -99,12 +100,13 @@ export class Testplane extends BaseTestplane { inspectMode, replMode, devtools, + local, reporters = [], }: Partial = {}, ): Promise { validateUnknownBrowsers(browsers!, _.keys(this._config.browsers)); - RuntimeConfig.getInstance().extend({ updateRefs, requireModules, inspectMode, replMode, devtools }); + RuntimeConfig.getInstance().extend({ updateRefs, requireModules, inspectMode, replMode, devtools, local }); if (replMode?.enabled) { this._config.system.mochaOpts.timeout = 0; diff --git a/test/src/browser-installer/chrome/browser.ts b/test/src/browser-installer/chrome/browser.ts new file mode 100644 index 000000000..31a4c6f57 --- /dev/null +++ b/test/src/browser-installer/chrome/browser.ts @@ -0,0 +1,111 @@ +import proxyquire from "proxyquire"; +import sinon, { type SinonStub } from "sinon"; +import type { installChrome as InstallChromeType } from "../../../../src/browser-installer/chrome/browser"; +import { Browser } from "../../../../src/browser-installer/utils"; + +describe("browser-installer/chrome/browser", () => { + const sandbox = sinon.createSandbox(); + + let installChrome: typeof InstallChromeType; + + let installChromiumStub: SinonStub; + + let resolveBuildIdStub: SinonStub; + let puppeteerInstallStub: SinonStub; + let canDownloadStub: SinonStub; + + let getBinaryPathStub: SinonStub; + let getMatchedBrowserVersionStub: SinonStub; + let installBinaryStub: SinonStub; + + beforeEach(() => { + installChromiumStub = sandbox.stub().resolves("/chromium/browser/path"); + + puppeteerInstallStub = sandbox.stub().resolves({ executablePath: "/chrome/browser/path" }); + resolveBuildIdStub = sandbox.stub().resolves("115.0.5780.170"); + canDownloadStub = sandbox.stub().resolves(true); + + getBinaryPathStub = sandbox.stub().returns(null); + getMatchedBrowserVersionStub = sandbox.stub().returns(null); + installBinaryStub = sandbox.stub(); + + installChrome = proxyquire("../../../../src/browser-installer/chrome/browser", { + "../chromium": { installChromium: installChromiumStub }, + "@puppeteer/browsers": { + resolveBuildId: resolveBuildIdStub, + install: puppeteerInstallStub, + canDownload: canDownloadStub, + }, + "../registry": { + getBinaryPath: getBinaryPathStub, + getMatchedBrowserVersion: getMatchedBrowserVersionStub, + installBinary: installBinaryStub, + }, + }).installChrome; + }); + + afterEach(() => sandbox.restore()); + + it("should try to resolve browser path locally by default", async () => { + getMatchedBrowserVersionStub.withArgs(Browser.CHROME, sinon.match.string, "115").returns("115.0"); + getBinaryPathStub.withArgs(Browser.CHROME, sinon.match.string, "115.0").returns("/browser/path"); + + const binaryPath = await installChrome("115"); + + assert.equal(binaryPath, "/browser/path"); + assert.notCalled(resolveBuildIdStub); + assert.notCalled(installBinaryStub); + }); + + it("should not try to resolve browser path locally with 'force' flag", async () => { + getMatchedBrowserVersionStub.withArgs(Browser.CHROME, sinon.match.string, "115").returns("115.0"); + resolveBuildIdStub.withArgs(Browser.CHROME, sinon.match.string, "115").resolves("115.0.5678.170"); + + installBinaryStub + .withArgs(Browser.CHROME, sinon.match.string, "115.0.5678.170", sinon.match.func) + .resolves("/new/downloaded/browser/path"); + + const binaryPath = await installChrome("115", { force: true }); + + assert.notCalled(getBinaryPathStub); + assert.equal(binaryPath, "/new/downloaded/browser/path"); + }); + + it("should download browser if it is not downloaded", async () => { + getMatchedBrowserVersionStub.withArgs(Browser.CHROME, sinon.match.string, "115").returns(null); + resolveBuildIdStub.withArgs(Browser.CHROME, sinon.match.string, "115").resolves("115.0.5678.170"); + installBinaryStub + .withArgs(Browser.CHROME, sinon.match.string, "115.0.5678.170", sinon.match.func) + .resolves("/new/downloaded/browser/path"); + + const binaryPath = await installChrome("115"); + + assert.equal(binaryPath, "/new/downloaded/browser/path"); + }); + + it("should use chromium browser download if version is too low", async () => { + getMatchedBrowserVersionStub.returns(null); + installChromiumStub.withArgs("80").resolves("/browser/chromium/path"); + + const result = await installChrome("80"); + + assert.equal(result, "/browser/chromium/path"); + assert.notCalled(resolveBuildIdStub); + assert.notCalled(installBinaryStub); + }); + + it("should throw an error if can't download the browser", async () => { + getMatchedBrowserVersionStub.withArgs(Browser.CHROME, sinon.match.string, "115").returns(null); + resolveBuildIdStub.withArgs(Browser.CHROME, sinon.match.string, "115").resolves("115"); + canDownloadStub.resolves(false); + + await assert.isRejected( + installChrome("115"), + [ + `chrome@115 can't be installed.`, + `Probably the version '115' is invalid, please try another version.`, + "Version examples: '120', '120.0'", + ].join("\n"), + ); + }); +}); diff --git a/test/src/browser-installer/chrome/driver.ts b/test/src/browser-installer/chrome/driver.ts new file mode 100644 index 000000000..ff0464d8a --- /dev/null +++ b/test/src/browser-installer/chrome/driver.ts @@ -0,0 +1,110 @@ +import proxyquire from "proxyquire"; +import sinon, { type SinonStub } from "sinon"; +import type { installChromeDriver as InstallChromeDriverType } from "../../../../src/browser-installer/chrome/driver"; +import { Driver } from "../../../../src/browser-installer/utils"; + +describe("browser-installer/chrome/driver", () => { + const sandbox = sinon.createSandbox(); + + let installChromeDriver: typeof InstallChromeDriverType; + + let installChromeDriverManuallyStub: SinonStub; + + let resolveBuildIdStub: SinonStub; + let puppeteerInstallStub: SinonStub; + let canDownloadStub: SinonStub; + + let getBinaryPathStub: SinonStub; + let getMatchedDriverVersionStub: SinonStub; + let installBinaryStub: SinonStub; + + beforeEach(() => { + installChromeDriverManuallyStub = sandbox.stub().resolves({ executablePath: "/chromium/driver/path" }); + + puppeteerInstallStub = sandbox.stub().resolves({ executablePath: "/chrome/driver/path" }); + resolveBuildIdStub = sandbox.stub().resolves("115.0.5780.170"); + canDownloadStub = sandbox.stub().resolves(true); + + getBinaryPathStub = sandbox.stub().returns(null); + getMatchedDriverVersionStub = sandbox.stub().returns(null); + installBinaryStub = sandbox.stub(); + + installChromeDriver = proxyquire("../../../../src/browser-installer/chrome/driver", { + "../chromium": { installChromeDriverManually: installChromeDriverManuallyStub }, + "@puppeteer/browsers": { + resolveBuildId: resolveBuildIdStub, + install: puppeteerInstallStub, + canDownload: canDownloadStub, + }, + "../registry": { + getBinaryPath: getBinaryPathStub, + getMatchedDriverVersion: getMatchedDriverVersionStub, + installBinary: installBinaryStub, + }, + }).installChromeDriver; + }); + + afterEach(() => sandbox.restore()); + + it("should try to resolve driver path locally by default", async () => { + getMatchedDriverVersionStub.withArgs(Driver.CHROMEDRIVER, sinon.match.string, "115").returns("115.0"); + getBinaryPathStub.withArgs(Driver.CHROMEDRIVER, sinon.match.string, "115.0").returns("/driver/path"); + + const driverPath = await installChromeDriver("115"); + + assert.equal(driverPath, "/driver/path"); + assert.notCalled(resolveBuildIdStub); + assert.notCalled(installBinaryStub); + }); + + it("should not try to resolve driver path locally with 'force' flag", async () => { + getMatchedDriverVersionStub.withArgs(Driver.CHROMEDRIVER, sinon.match.string, "115").returns("115.0"); + resolveBuildIdStub.withArgs(Driver.CHROMEDRIVER, sinon.match.string, "115").resolves("115.0.5678.170"); + installBinaryStub + .withArgs(Driver.CHROMEDRIVER, sinon.match.string, "115.0.5678.170", sinon.match.func) + .resolves("/new/downloaded/driver/path"); + + const driverPath = await installChromeDriver("115", { force: true }); + + assert.notCalled(getBinaryPathStub); + assert.equal(driverPath, "/new/downloaded/driver/path"); + }); + + it("should download driver if it is not downloaded", async () => { + getMatchedDriverVersionStub.withArgs(Driver.CHROMEDRIVER, sinon.match.string, "115").returns(null); + resolveBuildIdStub.withArgs(Driver.CHROMEDRIVER, sinon.match.string, "115").resolves("115.0.5678.170"); + installBinaryStub + .withArgs(Driver.CHROMEDRIVER, sinon.match.string, "115.0.5678.170", sinon.match.func) + .resolves("/new/downloaded/driver/path"); + + const driverPath = await installChromeDriver("115"); + + assert.equal(driverPath, "/new/downloaded/driver/path"); + }); + + it("should use chromium driver manual download if version is too low", async () => { + getMatchedDriverVersionStub.returns(null); + installChromeDriverManuallyStub.withArgs("80").resolves("/driver/manual/path"); + + const result = await installChromeDriver("80"); + + assert.equal(result, "/driver/manual/path"); + assert.notCalled(resolveBuildIdStub); + assert.notCalled(installBinaryStub); + }); + + it("should throw an error if can't download the driver", async () => { + getMatchedDriverVersionStub.withArgs(Driver.CHROMEDRIVER, sinon.match.string, "115").returns(null); + resolveBuildIdStub.withArgs(Driver.CHROMEDRIVER, sinon.match.string, "115").resolves("115.0.5678.170"); + canDownloadStub.resolves(false); + + await assert.isRejected( + installChromeDriver("115"), + [ + "chromedriver@115.0.5678.170 can't be installed.", + "Probably the major browser version '115' is invalid", + "Correct chrome version examples: '123', '124'", + ].join("\n"), + ); + }); +}); diff --git a/test/src/browser-installer/chrome/index.ts b/test/src/browser-installer/chrome/index.ts new file mode 100644 index 000000000..4944f884c --- /dev/null +++ b/test/src/browser-installer/chrome/index.ts @@ -0,0 +1,87 @@ +import proxyquire from "proxyquire"; +import sinon, { type SinonStub } from "sinon"; +import type { ChildProcess } from "child_process"; +import type { runChromeDriver as RunChromeDriverType } from "../../../../src/browser-installer/chrome"; + +describe("browser-installer/chrome", () => { + const sandbox = sinon.createSandbox(); + + let runChromeDriver: typeof RunChromeDriverType; + + let pipeLogsWithPrefixStub: SinonStub; + let installChromeStub: SinonStub; + let installChromeDriverStub: SinonStub; + let spawnStub: SinonStub; + + let getPortStub: SinonStub; + let waitPortStub: SinonStub; + + beforeEach(() => { + pipeLogsWithPrefixStub = sandbox.stub(); + installChromeStub = sandbox.stub().resolves("/browser/path"); + installChromeDriverStub = sandbox.stub().resolves("/driver/path"); + spawnStub = sandbox.stub().returns({ kill: sandbox.stub() }); + getPortStub = sandbox.stub().resolves(12345); + waitPortStub = sandbox.stub().resolves(); + + runChromeDriver = proxyquire("../../../../src/browser-installer/chrome", { + "../../dev-server/utils": { pipeLogsWithPrefix: pipeLogsWithPrefixStub }, + "./driver": { installChromeDriver: installChromeDriverStub }, + "./browser": { installChrome: installChromeStub }, + child_process: { spawn: spawnStub }, // eslint-disable-line camelcase + "wait-port": waitPortStub, + "get-port": getPortStub, + }).runChromeDriver; + }); + + afterEach(() => sandbox.restore()); + + it("should launch child process on random port", async () => { + installChromeDriverStub.resolves("/driver/path"); + getPortStub.resolves(10050); + + await runChromeDriver("130"); + + assert.calledOnceWith(installChromeDriverStub, "130"); + assert.calledOnceWith(spawnStub, "/driver/path", ["--port=10050", "--silent"]); + }); + + it("should wait for port to be active", async () => { + getPortStub.resolves(10050); + + await runChromeDriver("130"); + + assert.calledOnceWith(waitPortStub, { port: 10050, output: "silent", timeout: 10000 }); + }); + + it("should be executed in right order", async () => { + await runChromeDriver("130"); + + assert.callOrder(installChromeDriverStub, getPortStub, spawnStub, waitPortStub); + }); + + it("should return gridUrl, process and port", async () => { + const processStub = { kill: sandbox.stub() } as unknown as ChildProcess; + spawnStub.returns(processStub); + getPortStub.resolves(10050); + + const result = await runChromeDriver("130"); + + assert.equal(result.gridUrl, "http://127.0.0.1:10050"); + assert.equal(result.port, 10050); + assert.equal(result.process, processStub); + }); + + it("should pipe logs if debug is enabled", async () => { + const result = await runChromeDriver("130", { debug: true }); + + assert.calledOnceWith(spawnStub, "/driver/path", ["--port=12345", "--verbose"]); + assert.calledOnceWith(pipeLogsWithPrefixStub, result.process, "[chromedriver@130] "); + }); + + it("should not pipe logs if debug is not enabled", async () => { + await runChromeDriver("130"); + + assert.notCalled(pipeLogsWithPrefixStub); + }); +}); diff --git a/test/src/browser-installer/chromium/browser.ts b/test/src/browser-installer/chromium/browser.ts new file mode 100644 index 000000000..0084e3d36 --- /dev/null +++ b/test/src/browser-installer/chromium/browser.ts @@ -0,0 +1,109 @@ +import proxyquire from "proxyquire"; +import sinon, { type SinonStub } from "sinon"; +import type { installChromium as InstallChromiumType } from "../../../../src/browser-installer/chromium/browser"; +import { Browser } from "../../../../src/browser-installer/utils"; + +describe("browser-installer/chromium/browser", () => { + const sandbox = sinon.createSandbox(); + + let installChromium: typeof InstallChromiumType; + + let getChromiumBuildIdStub: SinonStub; + let puppeteerInstallStub: SinonStub; + let canDownloadStub: SinonStub; + + let getBinaryPathStub: SinonStub; + let getMatchedBrowserVersionStub: SinonStub; + let installBinaryStub: SinonStub; + + beforeEach(() => { + puppeteerInstallStub = sandbox.stub().resolves({ executablePath: "/chromium/browser/path" }); + getChromiumBuildIdStub = sandbox.stub().resolves("100500"); + canDownloadStub = sandbox.stub().resolves(true); + + getBinaryPathStub = sandbox.stub().returns(null); + getMatchedBrowserVersionStub = sandbox.stub().returns(null); + installBinaryStub = sandbox.stub(); + + installChromium = proxyquire("../../../../src/browser-installer/chromium/browser", { + "@puppeteer/browsers": { + install: puppeteerInstallStub, + canDownload: canDownloadStub, + }, + "./utils": { getChromiumBuildId: getChromiumBuildIdStub }, + "../registry": { + getBinaryPath: getBinaryPathStub, + getMatchedBrowserVersion: getMatchedBrowserVersionStub, + installBinary: installBinaryStub, + }, + }).installChromium; + }); + + afterEach(() => sandbox.restore()); + + it("should try to resolve browser path locally by default", async () => { + getMatchedBrowserVersionStub.withArgs(Browser.CHROMIUM, sinon.match.string, "80").returns("80"); + getBinaryPathStub.withArgs(Browser.CHROMIUM, sinon.match.string, "80").returns("/browser/path"); + + const binaryPath = await installChromium("80"); + + assert.equal(binaryPath, "/browser/path"); + assert.notCalled(getChromiumBuildIdStub); + assert.notCalled(installBinaryStub); + }); + + it("should not try to resolve browser path locally with 'force' flag", async () => { + getMatchedBrowserVersionStub.withArgs(Browser.CHROMIUM, sinon.match.string, "80").returns("80"); + getChromiumBuildIdStub.withArgs(Browser.CHROMIUM, sinon.match.string, "80").resolves("100500"); + + installBinaryStub + .withArgs(Browser.CHROMIUM, sinon.match.string, "80", sinon.match.func) + .resolves("/new/downloaded/browser/path"); + + const binaryPath = await installChromium("80", { force: true }); + + assert.notCalled(getBinaryPathStub); + assert.equal(binaryPath, "/new/downloaded/browser/path"); + }); + + it("should download browser if it is not downloaded", async () => { + getMatchedBrowserVersionStub.withArgs(Browser.CHROMIUM, sinon.match.string, "80").returns(null); + getChromiumBuildIdStub.withArgs(Browser.CHROMIUM, sinon.match.string, "80").resolves("100500"); + installBinaryStub + .withArgs(Browser.CHROMIUM, sinon.match.string, "80", sinon.match.func) + .resolves("/new/downloaded/browser/path"); + + const binaryPath = await installChromium("80"); + + assert.equal(binaryPath, "/new/downloaded/browser/path"); + }); + + it("should throw an error if version is too low", async () => { + getMatchedBrowserVersionStub.returns(null); + + await assert.isRejected( + installChromium("60"), + [ + "chrome@60 can't be installed.", + "Automatic browser downloader is not available for chrome versions < 73", + ].join("\n"), + ); + assert.notCalled(getChromiumBuildIdStub); + assert.notCalled(installBinaryStub); + }); + + it("should throw an error if can't download the browser", async () => { + getMatchedBrowserVersionStub.withArgs(Browser.CHROMIUM, sinon.match.string, "115").returns(null); + getChromiumBuildIdStub.withArgs(Browser.CHROMIUM, sinon.match.string, "115").resolves("100500"); + canDownloadStub.resolves(false); + + await assert.isRejected( + installChromium("115"), + [ + `chrome@115 can't be installed.`, + `Probably the version '115' is invalid, please try another version.`, + "Version examples: '93', '93.0'", + ].join("\n"), + ); + }); +}); diff --git a/test/src/browser-installer/chromium/driver.ts b/test/src/browser-installer/chromium/driver.ts new file mode 100644 index 000000000..969a8c982 --- /dev/null +++ b/test/src/browser-installer/chromium/driver.ts @@ -0,0 +1,53 @@ +import proxyquire from "proxyquire"; +import sinon, { type SinonStub } from "sinon"; +import type { installChromeDriverManually as installChromeDriverManuallyType } from "../../../../src/browser-installer/chromium/driver"; +import { Driver } from "../../../../src/browser-installer/utils"; + +describe("browser-installer/chromium/driver", () => { + const sandbox = sinon.createSandbox(); + + let installChromeDriverManually: typeof installChromeDriverManuallyType; + + let retryFetchStub: SinonStub; + let installBinaryStub: SinonStub; + + beforeEach(() => { + retryFetchStub = sandbox.stub().resolves("result"); + installBinaryStub = sandbox.stub(); + + installChromeDriverManually = proxyquire("../../../../src/browser-installer/chromium/driver", { + "../utils": { + ...require("../../../../src/browser-installer/utils"), + retryFetch: retryFetchStub, + }, + "../registry": { installBinary: installBinaryStub }, + }).installChromeDriverManually; + }); + + afterEach(() => sandbox.restore()); + + it("should download driver if it is not downloaded", async () => { + retryFetchStub.withArgs("https://chromedriver.storage.googleapis.com/LATEST_RELEASE_115").resolves({ + text: () => Promise.resolve("115.0.5678.170"), + }); + installBinaryStub + .withArgs(Driver.CHROMEDRIVER, sinon.match.string, "115.0.5678.170", sinon.match.func) + .resolves("/driver/path"); + + const driverPath = await installChromeDriverManually("115"); + + assert.equal(driverPath, "/driver/path"); + }); + + it("should throw an error on unsupported old version", async () => { + await assert.isRejected( + installChromeDriverManually("35"), + [ + "chromedriver@35 can't be installed.", + "Automatic driver downloader is not available for chrome versions < 73", + ].join("\n"), + ); + assert.notCalled(retryFetchStub); + assert.notCalled(installBinaryStub); + }); +}); diff --git a/test/src/browser-installer/edge/driver.ts b/test/src/browser-installer/edge/driver.ts new file mode 100644 index 000000000..6eea60c69 --- /dev/null +++ b/test/src/browser-installer/edge/driver.ts @@ -0,0 +1,90 @@ +import proxyquire from "proxyquire"; +import sinon, { type SinonStub } from "sinon"; +import type { installEdgeDriver as InstallEdgeDriverType } from "../../../../src/browser-installer/edge/driver"; +import { Driver } from "../../../../src/browser-installer/utils"; + +describe("browser-installer/edge/driver", () => { + const sandbox = sinon.createSandbox(); + + let installEdgeDriver: typeof InstallEdgeDriverType; + + let downloadEdgeDriverStub: SinonStub; + let retryFetchStub: SinonStub; + let getBinaryPathStub: SinonStub; + let getMatchedDriverVersionStub: SinonStub; + let installBinaryStub: SinonStub; + + beforeEach(() => { + downloadEdgeDriverStub = sandbox.stub().resolves("/binary/path"); + retryFetchStub = sandbox.stub().resolves("result"); + getBinaryPathStub = sandbox.stub().resolves("/binary/path"); + getMatchedDriverVersionStub = sandbox.stub().returns(null); + installBinaryStub = sandbox.stub(); + + installEdgeDriver = proxyquire("../../../../src/browser-installer/edge/driver", { + edgedriver: { download: downloadEdgeDriverStub }, + "../utils": { + ...require("../../../../src/browser-installer/utils"), + retryFetch: retryFetchStub, + }, + "../registry": { + getBinaryPath: getBinaryPathStub, + getMatchedDriverVersion: getMatchedDriverVersionStub, + installBinary: installBinaryStub, + }, + }).installEdgeDriver; + }); + + afterEach(() => sandbox.restore()); + + it("should try to resolve driver path locally by default", async () => { + getMatchedDriverVersionStub.withArgs(Driver.EDGEDRIVER, sinon.match.string, "115").returns("115.0"); + getBinaryPathStub.withArgs(Driver.EDGEDRIVER, sinon.match.string, "115.0").returns("/driver/path"); + + const driverPath = await installEdgeDriver("115"); + + assert.equal(driverPath, "/driver/path"); + assert.notCalled(retryFetchStub); + assert.notCalled(installBinaryStub); + }); + + it("should not try to resolve driver path locally with 'force' flag", async () => { + getMatchedDriverVersionStub.withArgs(Driver.EDGEDRIVER, sinon.match.string, "115").returns("115.0"); + retryFetchStub.withArgs("https://msedgedriver.azureedge.net/LATEST_RELEASE_115").resolves({ + text: () => Promise.resolve("115.0.5678.170"), + }); + installBinaryStub + .withArgs(Driver.EDGEDRIVER, sinon.match.string, "115.0.5678.170", sinon.match.func) + .resolves("/new/downloaded/driver/path"); + + const driverPath = await installEdgeDriver("115", { force: true }); + + assert.notCalled(getBinaryPathStub); + assert.equal(driverPath, "/new/downloaded/driver/path"); + }); + + it("should download driver if it is not downloaded", async () => { + getMatchedDriverVersionStub.withArgs(Driver.EDGEDRIVER, sinon.match.string, "115").returns(null); + retryFetchStub.withArgs("https://msedgedriver.azureedge.net/LATEST_RELEASE_115").resolves({ + text: () => Promise.resolve("115.0.5678.170"), + }); + installBinaryStub + .withArgs(Driver.EDGEDRIVER, sinon.match.string, "115.0.5678.170", sinon.match.func) + .resolves("/new/downloaded/driver/path"); + + const driverPath = await installEdgeDriver("115"); + + assert.equal(driverPath, "/new/downloaded/driver/path"); + }); + + it("should throw an error on unsupported old version", async () => { + getMatchedDriverVersionStub.returns(null); + + await assert.isRejected( + installEdgeDriver("35"), + "Automatic driver downloader is not available for Edge versions < 94", + ); + assert.notCalled(retryFetchStub); + assert.notCalled(installBinaryStub); + }); +}); diff --git a/test/src/browser-installer/edge/index.ts b/test/src/browser-installer/edge/index.ts new file mode 100644 index 000000000..166266243 --- /dev/null +++ b/test/src/browser-installer/edge/index.ts @@ -0,0 +1,84 @@ +import proxyquire from "proxyquire"; +import sinon, { type SinonStub } from "sinon"; +import type { ChildProcess } from "child_process"; +import type { runEdgeDriver as RunEdgeDriverType } from "../../../../src/browser-installer/edge"; + +describe("browser-installer/edge", () => { + const sandbox = sinon.createSandbox(); + + let runEdgeDriver: typeof RunEdgeDriverType; + + let pipeLogsWithPrefixStub: SinonStub; + let installEdgeDriverStub: SinonStub; + let spawnStub: SinonStub; + + let getPortStub: SinonStub; + let waitPortStub: SinonStub; + + beforeEach(() => { + pipeLogsWithPrefixStub = sandbox.stub(); + installEdgeDriverStub = sandbox.stub().resolves("/driver/path"); + spawnStub = sandbox.stub().returns({ kill: sandbox.stub() }); + getPortStub = sandbox.stub().resolves(12345); + waitPortStub = sandbox.stub().resolves(); + + runEdgeDriver = proxyquire("../../../../src/browser-installer/edge", { + "../../dev-server/utils": { pipeLogsWithPrefix: pipeLogsWithPrefixStub }, + "./driver": { installEdgeDriver: installEdgeDriverStub }, + child_process: { spawn: spawnStub }, // eslint-disable-line camelcase + "wait-port": waitPortStub, + "get-port": getPortStub, + }).runEdgeDriver; + }); + + afterEach(() => sandbox.restore()); + + it("should launch child process on random port", async () => { + installEdgeDriverStub.resolves("/driver/path"); + getPortStub.resolves(10050); + + await runEdgeDriver("130"); + + assert.calledOnceWith(installEdgeDriverStub, "130"); + assert.calledOnceWith(spawnStub, "/driver/path", ["--port=10050", "--silent"]); + }); + + it("should wait for port to be active", async () => { + getPortStub.resolves(10050); + + await runEdgeDriver("130"); + + assert.calledOnceWith(waitPortStub, { port: 10050, output: "silent", timeout: 10000 }); + }); + + it("should be executed in right order", async () => { + await runEdgeDriver("130"); + + assert.callOrder(installEdgeDriverStub, getPortStub, spawnStub, waitPortStub); + }); + + it("should return gridUrl, process and port", async () => { + const processStub = { kill: sandbox.stub() } as unknown as ChildProcess; + spawnStub.returns(processStub); + getPortStub.resolves(10050); + + const result = await runEdgeDriver("130"); + + assert.equal(result.gridUrl, "http://127.0.0.1:10050"); + assert.equal(result.port, 10050); + assert.equal(result.process, processStub); + }); + + it("should pipe logs if debug is enabled", async () => { + const result = await runEdgeDriver("130", { debug: true }); + + assert.calledOnceWith(spawnStub, "/driver/path", ["--port=12345", "--verbose"]); + assert.calledOnceWith(pipeLogsWithPrefixStub, result.process, "[edgedriver@130] "); + }); + + it("should not pipe logs if debug is not enabled", async () => { + await runEdgeDriver("130"); + + assert.notCalled(pipeLogsWithPrefixStub); + }); +}); diff --git a/test/src/browser-installer/firefox/browser.ts b/test/src/browser-installer/firefox/browser.ts new file mode 100644 index 000000000..5bfe0eeaa --- /dev/null +++ b/test/src/browser-installer/firefox/browser.ts @@ -0,0 +1,87 @@ +import proxyquire from "proxyquire"; +import sinon, { type SinonStub } from "sinon"; +import type { installFirefox as InstallFirefoxType } from "../../../../src/browser-installer/firefox/browser"; +import { Browser } from "../../../../src/browser-installer/utils"; + +describe("browser-installer/firefox/browser", () => { + const sandbox = sinon.createSandbox(); + + let installFirefox: typeof InstallFirefoxType; + + let puppeteerInstallStub: SinonStub; + let canDownloadStub: SinonStub; + + let getBinaryPathStub: SinonStub; + let getMatchedBrowserVersionStub: SinonStub; + let installBinaryStub: SinonStub; + + beforeEach(() => { + puppeteerInstallStub = sandbox.stub().resolves({ executablePath: "/firefox/browser/path" }); + canDownloadStub = sandbox.stub().resolves(true); + + getBinaryPathStub = sandbox.stub().returns(null); + getMatchedBrowserVersionStub = sandbox.stub().returns(null); + installBinaryStub = sandbox.stub(); + + installFirefox = proxyquire("../../../../src/browser-installer/firefox/browser", { + "@puppeteer/browsers": { + install: puppeteerInstallStub, + canDownload: canDownloadStub, + }, + "../registry": { + getBinaryPath: getBinaryPathStub, + getMatchedBrowserVersion: getMatchedBrowserVersionStub, + installBinary: installBinaryStub, + }, + }).installFirefox; + }); + + afterEach(() => sandbox.restore()); + + it("should try to resolve browser path locally by default", async () => { + getMatchedBrowserVersionStub.withArgs(Browser.FIREFOX, sinon.match.string, "115").returns("115.0"); + getBinaryPathStub.withArgs(Browser.FIREFOX, sinon.match.string, "115.0").returns("/browser/path"); + + const binaryPath = await installFirefox("115"); + + assert.equal(binaryPath, "/browser/path"); + assert.notCalled(installBinaryStub); + }); + + it("should not try to resolve browser path locally with 'force' flag", async () => { + getMatchedBrowserVersionStub.withArgs(Browser.FIREFOX, sinon.match.string, "115").returns("stable_115.0"); + installBinaryStub + .withArgs(Browser.FIREFOX, sinon.match.string, "stable_115.0", sinon.match.func) + .resolves("/new/downloaded/browser/path"); + + const binaryPath = await installFirefox("115", { force: true }); + + assert.notCalled(getBinaryPathStub); + assert.equal(binaryPath, "/new/downloaded/browser/path"); + }); + + it("should download browser if it is not downloaded", async () => { + getMatchedBrowserVersionStub.withArgs(Browser.FIREFOX, sinon.match.string, "115").returns(null); + installBinaryStub + .withArgs(Browser.FIREFOX, sinon.match.string, "stable_115.0", sinon.match.func) + .resolves("/new/downloaded/browser/path"); + + const binaryPath = await installFirefox("115"); + + assert.equal(binaryPath, "/new/downloaded/browser/path"); + }); + + it("should throw an error if can't download the browser", async () => { + getMatchedBrowserVersionStub.withArgs(Browser.FIREFOX, sinon.match.string, "115").returns(null); + canDownloadStub.resolves(false); + + await assert.isRejected( + installFirefox("115"), + [ + `firefox@115 can't be installed.`, + `Probably the version '115' is invalid, please try another version.`, + "Version examples: '120', '130.0', '131.0'", + ].join("\n"), + ); + }); +}); diff --git a/test/src/browser-installer/firefox/driver.ts b/test/src/browser-installer/firefox/driver.ts new file mode 100644 index 000000000..221f2a90e --- /dev/null +++ b/test/src/browser-installer/firefox/driver.ts @@ -0,0 +1,79 @@ +import proxyquire from "proxyquire"; +import sinon, { type SinonStub } from "sinon"; +import type { installLatestGeckoDriver as InstallLatestGeckoDriverType } from "../../../../src/browser-installer/firefox/driver"; +import { Driver } from "../../../../src/browser-installer/utils"; + +describe("browser-installer/firefox/driver", () => { + const sandbox = sinon.createSandbox(); + + let installLatestGeckoDriver: typeof InstallLatestGeckoDriverType; + + let downloadGeckoDriverStub: SinonStub; + let retryFetchStub: SinonStub; + let getBinaryPathStub: SinonStub; + let getMatchedDriverVersionStub: SinonStub; + let installBinaryStub: SinonStub; + + beforeEach(() => { + downloadGeckoDriverStub = sandbox.stub().resolves("/binary/path"); + retryFetchStub = sandbox.stub().resolves("result"); + getBinaryPathStub = sandbox.stub().resolves("/binary/path"); + getMatchedDriverVersionStub = sandbox.stub().returns(null); + installBinaryStub = sandbox.stub(); + + installLatestGeckoDriver = proxyquire("../../../../src/browser-installer/firefox/driver", { + geckodriver: { download: downloadGeckoDriverStub }, + "../utils": { + ...require("../../../../src/browser-installer/utils"), + retryFetch: retryFetchStub, + }, + "../registry": { + getBinaryPath: getBinaryPathStub, + getMatchedDriverVersion: getMatchedDriverVersionStub, + installBinary: installBinaryStub, + }, + }).installLatestGeckoDriver; + }); + + afterEach(() => sandbox.restore()); + + it("should try to resolve driver path locally by default", async () => { + getMatchedDriverVersionStub.withArgs(Driver.GECKODRIVER, sinon.match.string, "115").returns("115.0"); + getBinaryPathStub.withArgs(Driver.GECKODRIVER, sinon.match.string, "115.0").returns("/driver/path"); + + const driverPath = await installLatestGeckoDriver("115"); + + assert.equal(driverPath, "/driver/path"); + assert.notCalled(retryFetchStub); + assert.notCalled(installBinaryStub); + }); + + it("should not try to resolve driver path locally with 'force' flag", async () => { + getMatchedDriverVersionStub.withArgs(Driver.GECKODRIVER, sinon.match.string, "115").returns("115.0"); + retryFetchStub.withArgs("https://raw.githubusercontent.com/mozilla/geckodriver/release/Cargo.toml").resolves({ + text: () => Promise.resolve("version = '0.35.0'"), + }); + installBinaryStub + .withArgs(Driver.GECKODRIVER, sinon.match.string, "0.35.0", sinon.match.func) + .resolves("/new/downloaded/driver/path"); + + const driverPath = await installLatestGeckoDriver("115", { force: true }); + + assert.notCalled(getBinaryPathStub); + assert.equal(driverPath, "/new/downloaded/driver/path"); + }); + + it("should download driver if it is not downloaded", async () => { + getMatchedDriverVersionStub.withArgs(Driver.GECKODRIVER, sinon.match.string, "115").returns(null); + retryFetchStub.withArgs("https://raw.githubusercontent.com/mozilla/geckodriver/release/Cargo.toml").resolves({ + text: () => Promise.resolve("version = '0.35.0'"), + }); + installBinaryStub + .withArgs(Driver.GECKODRIVER, sinon.match.string, "0.35.0", sinon.match.func) + .resolves("/new/downloaded/driver/path"); + + const driverPath = await installLatestGeckoDriver("115"); + + assert.equal(driverPath, "/new/downloaded/driver/path"); + }); +}); diff --git a/test/src/browser-installer/firefox/index.ts b/test/src/browser-installer/firefox/index.ts new file mode 100644 index 000000000..2c457bc97 --- /dev/null +++ b/test/src/browser-installer/firefox/index.ts @@ -0,0 +1,99 @@ +import proxyquire from "proxyquire"; +import sinon, { type SinonStub } from "sinon"; +import type { ChildProcess } from "child_process"; +import type { runGeckoDriver as RunGeckoDriverType } from "../../../../src/browser-installer/firefox"; + +describe("browser-installer/firefox", () => { + const sandbox = sinon.createSandbox(); + + let runGeckoDriver: typeof RunGeckoDriverType; + + let pipeLogsWithPrefixStub: SinonStub; + let installFirefoxStub: SinonStub; + let installLatestGeckoDriverStub: SinonStub; + let startGeckoDriverStub: SinonStub; + + let getPortStub: SinonStub; + let waitPortStub: SinonStub; + + beforeEach(() => { + pipeLogsWithPrefixStub = sandbox.stub(); + installFirefoxStub = sandbox.stub().resolves("/browser/path"); + installLatestGeckoDriverStub = sandbox.stub().resolves("/driver/path"); + startGeckoDriverStub = sandbox.stub().returns({ kill: sandbox.stub() }); + getPortStub = sandbox.stub().resolves(12345); + waitPortStub = sandbox.stub().resolves(); + + runGeckoDriver = proxyquire("../../../../src/browser-installer/firefox", { + "../../dev-server/utils": { pipeLogsWithPrefix: pipeLogsWithPrefixStub }, + "./browser": { installFirefox: installFirefoxStub }, + "./driver": { installLatestGeckoDriver: installLatestGeckoDriverStub }, + geckodriver: { start: startGeckoDriverStub }, + "wait-port": waitPortStub, + "get-port": getPortStub, + }).runGeckoDriver; + }); + + afterEach(() => sandbox.restore()); + + it("should launch child process on random port", async () => { + installLatestGeckoDriverStub.resolves("/driver/path"); + getPortStub.resolves(10050); + + await runGeckoDriver("130"); + + assert.calledOnceWith(startGeckoDriverStub, { + customGeckoDriverPath: "/driver/path", + port: 10050, + log: "fatal", + spawnOpts: { + windowsHide: true, + detached: false, + }, + }); + }); + + it("should wait for port to be active", async () => { + getPortStub.resolves(10050); + + await runGeckoDriver("130"); + + assert.calledOnceWith(waitPortStub, { port: 10050, output: "silent", timeout: 10000 }); + }); + + it("should be executed in right order", async () => { + await runGeckoDriver("130"); + + assert.callOrder(installLatestGeckoDriverStub, getPortStub, startGeckoDriverStub, waitPortStub); + }); + + it("should return gridUrl, process and port", async () => { + const processStub = { kill: sandbox.stub() } as unknown as ChildProcess; + startGeckoDriverStub.returns(processStub); + getPortStub.resolves(10050); + + const result = await runGeckoDriver("130"); + + assert.equal(result.gridUrl, "http://127.0.0.1:10050"); + assert.equal(result.port, 10050); + assert.equal(result.process, processStub); + }); + + it("should pipe logs if debug is enabled", async () => { + const result = await runGeckoDriver("130", { debug: true }); + + assert.calledOnceWith(startGeckoDriverStub, { + customGeckoDriverPath: "/driver/path", + port: 12345, + log: "debug", + spawnOpts: { windowsHide: true, detached: false }, + }); + assert.calledOnceWith(pipeLogsWithPrefixStub, result.process, "[geckodriver@130] "); + }); + + it("should not pipe logs if debug is not enabled", async () => { + await runGeckoDriver("130"); + + assert.notCalled(pipeLogsWithPrefixStub); + }); +}); diff --git a/test/src/browser-installer/install.ts b/test/src/browser-installer/install.ts new file mode 100644 index 000000000..cff6f5435 --- /dev/null +++ b/test/src/browser-installer/install.ts @@ -0,0 +1,153 @@ +import proxyquire from "proxyquire"; +import sinon, { type SinonStub } from "sinon"; +import type { + installBrowser as InstallBrowser, + installBrowsersWithDrivers as InstallBrowsersWithDrivers, +} from "../../../src/browser-installer/install"; + +describe("browser-installer/install", () => { + const sandbox = sinon.createSandbox(); + + let installBrowser: typeof InstallBrowser; + let installBrowsersWithDrivers: typeof InstallBrowsersWithDrivers; + + let installChromeStub: SinonStub; + let installChromeDriverStub: SinonStub; + let installFirefoxStub: SinonStub; + let installLatestGeckoDriverStub: SinonStub; + let installEdgeDriverStub: SinonStub; + + beforeEach(() => { + installChromeStub = sandbox.stub(); + installChromeDriverStub = sandbox.stub(); + installFirefoxStub = sandbox.stub(); + installLatestGeckoDriverStub = sandbox.stub(); + installEdgeDriverStub = sandbox.stub(); + + const installer = proxyquire("../../../src/browser-installer/install", { + "./chrome": { installChrome: installChromeStub, installChromeDriver: installChromeDriverStub }, + "./edge": { installEdgeDriver: installEdgeDriverStub }, + "./firefox": { installFirefox: installFirefoxStub, installLatestGeckoDriver: installLatestGeckoDriverStub }, + }); + + installBrowser = installer.installBrowser; + installBrowsersWithDrivers = installer.installBrowsersWithDrivers; + }); + + afterEach(() => sandbox.restore()); + + [true, false].forEach(force => { + describe(`installBrowser, force: ${force}`, () => { + describe("chrome", () => { + it("should install browser", async () => { + installChromeStub.withArgs("115").resolves("/browser/path"); + + const binaryPath = await installBrowser("chrome", "115", { force }); + + assert.equal(binaryPath, "/browser/path"); + assert.calledOnceWith(installChromeStub, "115", { force }); + assert.notCalled(installChromeDriverStub); + }); + + it("should install browser with webdriver", async () => { + installChromeStub.withArgs("115").resolves("/browser/path"); + + const binaryPath = await installBrowser("chrome", "115", { force, installWebDriver: true }); + + assert.equal(binaryPath, "/browser/path"); + assert.calledOnceWith(installChromeStub, "115", { force }); + assert.calledOnceWith(installChromeDriverStub, "115", { force }); + }); + }); + + describe("firefox", () => { + it("should install browser", async () => { + installFirefoxStub.withArgs("115").resolves("/browser/path"); + + const binaryPath = await installBrowser("firefox", "115", { force }); + + assert.equal(binaryPath, "/browser/path"); + assert.calledOnceWith(installFirefoxStub, "115", { force }); + assert.notCalled(installLatestGeckoDriverStub); + }); + + it("should install browser with webdriver", async () => { + installFirefoxStub.withArgs("115").resolves("/browser/path"); + + const binaryPath = await installBrowser("firefox", "115", { force, installWebDriver: true }); + + assert.equal(binaryPath, "/browser/path"); + assert.calledOnceWith(installFirefoxStub, "115", { force }); + assert.calledOnceWith(installLatestGeckoDriverStub, "115", { force }); + }); + }); + + describe("edge", () => { + it("should return null", async () => { + const binaryPath = await installBrowser("MicrosoftEdge", "115", { force }); + + assert.equal(binaryPath, null); + assert.notCalled(installEdgeDriverStub); + }); + + it("should install webdriver", async () => { + const binaryPath = await installBrowser("MicrosoftEdge", "115", { force, installWebDriver: true }); + + assert.equal(binaryPath, null); + assert.calledOnceWith(installEdgeDriverStub, "115", { force }); + }); + }); + + describe("safari", () => { + it("should return null", async () => { + const binaryPath = await installBrowser("safari", "115", { force, installWebDriver: true }); + + assert.equal(binaryPath, null); + }); + }); + + it("should throw exception on unsupported browser name", async () => { + await assert.isRejected( + installBrowser("foobar", "115", { force }), + /Couldn't install browser 'foobar', as it is not supported/, + ); + }); + + it("should throw exception on empty browser version", async () => { + await assert.isRejected( + installBrowser("chrome", "", { force }), + /Couldn't install browser 'chrome' because it has invalid version: ''/, + ); + }); + }); + }); + + describe("installBrowsersWithDrivers", () => { + it("should force install browser with driver", async () => { + await installBrowsersWithDrivers([{ browserName: "chrome", browserVersion: "115" }]); + + assert.calledOnceWith(installChromeStub, "115", { force: true }); + assert.calledOnceWith(installChromeDriverStub, "115", { force: true }); + }); + + it("should return result with browsers install status", async () => { + installChromeStub.rejects(new Error("test chrome install error")); + installFirefoxStub.resolves("/browser/path"); + + const result = await installBrowsersWithDrivers([ + { browserName: "chrome", browserVersion: "115" }, + { browserName: "firefox", browserVersion: "120" }, + { browserName: "edge", browserVersion: "125" }, + ]); + + assert.deepEqual(result, { + "chrome@115": { status: "error", reason: "test chrome install error" }, + "firefox@120": { status: "ok" }, + "edge@125": { + status: "skip", + reason: "Installing edge is unsupported. Assuming it is installed locally", + }, + }); + }); + }); +}); diff --git a/test/src/browser-installer/registry.ts b/test/src/browser-installer/registry.ts new file mode 100644 index 000000000..db900d5c3 --- /dev/null +++ b/test/src/browser-installer/registry.ts @@ -0,0 +1,263 @@ +import proxyquire from "proxyquire"; +import sinon, { type SinonStub } from "sinon"; +import type * as Registry from "../../../src/browser-installer/registry"; +import { Browser, Driver, type DownloadProgressCallback } from "../../../src/browser-installer/utils"; +import { BrowserPlatform } from "@puppeteer/browsers"; + +describe("browser-installer/registry", () => { + const sandbox = sinon.createSandbox(); + + let registry: typeof Registry; + + let readJsonSyncStub: SinonStub; + let outputJSONSyncStub: SinonStub; + let existsSyncStub: SinonStub; + let progressBarRegisterStub: SinonStub; + let loggerWarnStub: SinonStub; + + const createRegistry_ = (contents: Record> = {}): typeof Registry => { + return proxyquire("../../../src/browser-installer/registry", { + "../utils": { getRegistryPath: () => "/testplane/registry/registry.json" }, + "fs-extra": { readJsonSync: () => contents, existsSync: () => true }, + "../../utils/logger": { warn: loggerWarnStub }, + }); + }; + + beforeEach(() => { + readJsonSyncStub = sandbox.stub().returns({}); + outputJSONSyncStub = sandbox.stub(); + existsSyncStub = sandbox.stub().returns(false); + progressBarRegisterStub = sandbox.stub(); + loggerWarnStub = sandbox.stub(); + + registry = proxyquire("../../../src/browser-installer/registry", { + "./cli-progress-bar": { createBrowserDownloadProgressBar: () => ({ register: progressBarRegisterStub }) }, + "../utils": { getRegistryPath: () => "/testplane/registry/registry.json" }, + "../../utils/logger": { warn: loggerWarnStub }, + "fs-extra": { + readJsonSync: readJsonSyncStub, + outputJSONSync: outputJSONSyncStub, + existsSync: existsSyncStub, + }, + }); + }); + + afterEach(() => sandbox.restore()); + + describe("getBinaryPath", () => { + it("should return binary path", async () => { + registry = createRegistry_({ + // eslint-disable-next-line camelcase + chrome_mac_arm: { + "115.0.5790.170": "../browsers/chrome", + }, + }); + + const result = await registry.getBinaryPath(Browser.CHROME, BrowserPlatform.MAC_ARM, "115.0.5790.170"); + + assert.equal(result, "/testplane/registry/browsers/chrome"); + }); + + it("should throw an error if browser is not installed", async () => { + registry = createRegistry_({}); + + const fn = (): Promise => registry.getBinaryPath(Browser.CHROME, BrowserPlatform.MAC_ARM, "115"); + + await assert.isRejected(fn(), "Binary 'chrome' on 'mac_arm' is not installed"); + }); + + it("should throw an error if browser version is not installed", async () => { + // eslint-disable-next-line camelcase + registry = createRegistry_({ chrome_mac_arm: {} }); + + const fn = (): Promise => registry.getBinaryPath(Browser.CHROME, BrowserPlatform.MAC_ARM, "120"); + + await assert.isRejected(fn(), "Version '120' of driver 'chrome' on 'mac_arm' is not installed"); + }); + }); + + describe("getMatchedBrowserVersion", () => { + it("should return matching latest chrome browser version", () => { + registry = createRegistry_({ + // eslint-disable-next-line camelcase + chrome_mac_arm: { + "115.0.5790.170": "../browsers/chrome-115-0-5790-170", + "114.0.6980.170": "../browsers/chrome-114-0-6980-170", + "115.0.5320.180": "../browsers/chrome-115-0-5230-180", + }, + }); + + const version = registry.getMatchedBrowserVersion(Browser.CHROME, BrowserPlatform.MAC_ARM, "115"); + const versionFull = registry.getMatchedBrowserVersion(Browser.CHROME, BrowserPlatform.MAC_ARM, "115.0"); + + assert.equal(version, "115.0.5790.170"); + assert.equal(versionFull, "115.0.5790.170"); + }); + + it("should return matching latest firefox browser version", () => { + registry = createRegistry_({ + // eslint-disable-next-line camelcase + firefox_mac_arm: { + "stable_117.0b2": "../browsers/chrome-117-0b2", + "stable_118.0": "../browsers/firefox-118-0", + "stable_117.0b9": "../browsers/firefox-117-0b9", + }, + }); + + const version = registry.getMatchedBrowserVersion(Browser.FIREFOX, BrowserPlatform.MAC_ARM, "117"); + const versionFull = registry.getMatchedBrowserVersion(Browser.FIREFOX, BrowserPlatform.MAC_ARM, "117.0"); + + assert.equal(version, "stable_117.0b9"); + assert.equal(versionFull, "stable_117.0b9"); + }); + + it("should return null if no installed browser matching requirements", () => { + registry = createRegistry_({ + // eslint-disable-next-line camelcase + chrome_mac_arm: { + "115.0.5790.170": "../browsers/chrome-115-0-5790-170", + "114.0.6980.170": "../browsers/chrome-114-0-6980-170", + "115.0.5320.180": "../browsers/chrome-115-0-5230-180", + }, + }); + + const version = registry.getMatchedBrowserVersion(Browser.CHROME, BrowserPlatform.MAC_ARM, "116"); + const versionFull = registry.getMatchedBrowserVersion(Browser.CHROME, BrowserPlatform.MAC_ARM, "116.0"); + + assert.equal(version, null); + assert.equal(versionFull, null); + }); + }); + + describe("getMatchedDriverVersion", () => { + it("should return matching chromedriver version", () => { + registry = createRegistry_({ + // eslint-disable-next-line camelcase + chromedriver_mac_arm: { + "115.0.5790.170": "../drivers/chromedriver-115-0-5790-170", + "114.0.6980.170": "../drivers/chromedriver-114-0-6980-170", + "115.0.5320.180": "../drivers/chromedriver-115-0-5230-180", + }, + }); + + const version = registry.getMatchedDriverVersion(Driver.CHROMEDRIVER, BrowserPlatform.MAC_ARM, "115"); + const versionFull = registry.getMatchedDriverVersion(Driver.CHROMEDRIVER, BrowserPlatform.MAC_ARM, "115.0"); + + assert.equal(version, "115.0.5790.170"); + assert.equal(versionFull, "115.0.5790.170"); + }); + + it("should return matching chromedriver version", () => { + registry = createRegistry_({ + // eslint-disable-next-line camelcase + edgedriver_mac_arm: { + "115.0.5790.170": "../drivers/edgedriver-115-0-5790-170", + "114.0.6980.170": "../drivers/edgedriver-114-0-6980-170", + "115.0.5320.180": "../drivers/edgedriver-115-0-5230-180", + }, + }); + + const version = registry.getMatchedDriverVersion(Driver.EDGEDRIVER, BrowserPlatform.MAC_ARM, "115"); + const versionFull = registry.getMatchedDriverVersion(Driver.EDGEDRIVER, BrowserPlatform.MAC_ARM, "115.0"); + + assert.equal(version, "115.0.5790.170"); + assert.equal(versionFull, "115.0.5790.170"); + }); + + it("should return latest version for geckodriver", () => { + registry = createRegistry_({ + // eslint-disable-next-line camelcase + geckodriver_mac_arm: { + "0.33.0": "../drivers/geckodriver-33", + "0.35.0": "../drivers/geckodriver-35", + "0.34.0": "../drivers/geckodriver-34", + }, + }); + + const version = registry.getMatchedDriverVersion(Driver.GECKODRIVER, BrowserPlatform.MAC_ARM, "115"); + const versionFull = registry.getMatchedDriverVersion(Driver.GECKODRIVER, BrowserPlatform.MAC_ARM, "115.0"); + + assert.equal(version, "0.35.0"); + assert.equal(versionFull, "0.35.0"); + }); + + it("should return null if matching version is not found", () => { + registry = createRegistry_({ + // eslint-disable-next-line camelcase + chromedriver_mac_arm: {}, + }); + + const version = registry.getMatchedDriverVersion(Driver.GECKODRIVER, BrowserPlatform.MAC_ARM, "115"); + const versionFull = registry.getMatchedDriverVersion(Driver.GECKODRIVER, BrowserPlatform.MAC_ARM, "115.0"); + + assert.equal(version, null); + assert.equal(versionFull, null); + }); + }); + + describe("installBinary", () => { + it("should install binary and return its executable path", async () => { + const result = await registry.installBinary(Browser.CHROME, BrowserPlatform.LINUX, "100.0.0.0", () => + Promise.resolve("/browser/path"), + ); + + assert.equal(result, "/browser/path"); + }); + + it("should not install binary if it is already installed", async () => { + registry = createRegistry_({ + // eslint-disable-next-line camelcase + chrome_mac_arm: { + "115.0.5320.180": "../browser/path", + }, + }); + + const installFn = sinon.stub().resolves("/another/browser/path"); + const result = await registry.installBinary( + Browser.CHROME, + BrowserPlatform.MAC_ARM, + "115.0.5320.180", + installFn, + ); + + assert.notCalled(installFn); + assert.equal(result, "/testplane/registry/browser/path"); + }); + + it("should save binary to registry after install", async () => { + const installFn = sinon.stub().resolves("/testplane/registry/browser/path"); + await registry.installBinary(Browser.CHROME, BrowserPlatform.MAC_ARM, "115.0.5320.180", installFn); + + const savedPath = await registry.getBinaryPath(Browser.CHROME, BrowserPlatform.MAC_ARM, "115.0.5320.180"); + + assert.equal(savedPath, "/testplane/registry/browser/path"); + assert.calledOnceWith( + outputJSONSyncStub, + "/testplane/registry/registry.json", + { + // eslint-disable-next-line camelcase + chrome_mac_arm: { "115.0.5320.180": "../browser/path" }, + }, + { replacer: sinon.match.func }, + ); + }); + + it("should log warning once on install", async () => { + progressBarRegisterStub.returns(sandbox.stub()); + + const installFn = async (downloadProgressCallback: DownloadProgressCallback): Promise => { + downloadProgressCallback(0, 1024); + + return "/testplane/registry/browser/path"; + }; + + await registry.installBinary(Browser.CHROME, BrowserPlatform.MAC_ARM, "115.0.5320.180", installFn); + + await registry.installBinary(Browser.FIREFOX, BrowserPlatform.MAC_ARM, "120.0.5320.180", installFn); + + assert.calledWith(loggerWarnStub, "Downloading Testplane browsers"); + assert.calledWith(loggerWarnStub, "Note: this is one-time action. It may take a while..."); + assert.calledTwice(loggerWarnStub); + }); + }); +}); diff --git a/test/src/browser-installer/run.ts b/test/src/browser-installer/run.ts new file mode 100644 index 000000000..9f61eac71 --- /dev/null +++ b/test/src/browser-installer/run.ts @@ -0,0 +1,29 @@ +import proxyquire from "proxyquire"; +import sinon, { type SinonStub } from "sinon"; +import type { runBrowserDriver as RunBrowserDriver } from "../../../src/browser-installer/run"; +import { Driver } from "../../../src/browser-installer/utils"; + +describe("browser-installer/run", () => { + const sandbox = sinon.createSandbox(); + + let runBrowserDriver: typeof RunBrowserDriver; + let runChromeDriverStub: SinonStub; + + beforeEach(() => { + runChromeDriverStub = sandbox.stub(); + + runBrowserDriver = proxyquire.noCallThru()("../../../src/browser-installer/run", { + "./chrome": { runChromeDriver: runChromeDriverStub }, + }).runBrowserDriver; + }); + + afterEach(() => sandbox.restore()); + + [true, false, undefined].forEach(debug => { + it(`should run chrome driver with debug: ${debug}`, async () => { + await runBrowserDriver(Driver.CHROMEDRIVER, "some-version", { debug }); + + assert.calledOnceWith(runChromeDriverStub, "some-version", { debug: Boolean(debug) }); + }); + }); +}); diff --git a/test/src/browser-installer/safari/index.ts b/test/src/browser-installer/safari/index.ts new file mode 100644 index 000000000..e1ff1761c --- /dev/null +++ b/test/src/browser-installer/safari/index.ts @@ -0,0 +1,78 @@ +import proxyquire from "proxyquire"; +import sinon, { type SinonStub } from "sinon"; +import type { ChildProcess } from "child_process"; +import type { runSafariDriver as RunSafariDriverType } from "../../../../src/browser-installer/safari"; + +describe("browser-installer/edge", () => { + const sandbox = sinon.createSandbox(); + + let runSafariDriver: typeof RunSafariDriverType; + + let pipeLogsWithPrefixStub: SinonStub; + let spawnStub: SinonStub; + + let getPortStub: SinonStub; + let waitPortStub: SinonStub; + + beforeEach(() => { + pipeLogsWithPrefixStub = sandbox.stub(); + spawnStub = sandbox.stub().returns({ kill: sandbox.stub() }); + getPortStub = sandbox.stub().resolves(12345); + waitPortStub = sandbox.stub().resolves(); + + runSafariDriver = proxyquire("../../../../src/browser-installer/safari", { + "../../dev-server/utils": { pipeLogsWithPrefix: pipeLogsWithPrefixStub }, + child_process: { spawn: spawnStub }, // eslint-disable-line camelcase + "wait-port": waitPortStub, + "get-port": getPortStub, + }).runSafariDriver; + }); + + afterEach(() => sandbox.restore()); + + it("should launch child process on random port", async () => { + getPortStub.resolves(10050); + + await runSafariDriver(); + + assert.calledOnceWith(spawnStub, "/usr/bin/safaridriver", ["--port=10050"]); + }); + + it("should wait for port to be active", async () => { + getPortStub.resolves(10050); + + await runSafariDriver(); + + assert.calledOnceWith(waitPortStub, { port: 10050, output: "silent", timeout: 10000 }); + }); + + it("should be executed in right order", async () => { + await runSafariDriver(); + + assert.callOrder(getPortStub, spawnStub, waitPortStub); + }); + + it("should return gridUrl, process and port", async () => { + const processStub = { kill: sandbox.stub() } as unknown as ChildProcess; + spawnStub.returns(processStub); + getPortStub.resolves(10050); + + const result = await runSafariDriver(); + + assert.equal(result.gridUrl, "http://127.0.0.1:10050"); + assert.equal(result.port, 10050); + assert.equal(result.process, processStub); + }); + + it("should pipe logs if debug is enabled", async () => { + const result = await runSafariDriver({ debug: true }); + + assert.calledOnceWith(pipeLogsWithPrefixStub, result.process, "[safaridriver] "); + }); + + it("should not pipe logs if debug is not enabled", async () => { + await runSafariDriver(); + + assert.notCalled(pipeLogsWithPrefixStub); + }); +}); diff --git a/test/src/browser-installer/utils.ts b/test/src/browser-installer/utils.ts new file mode 100644 index 000000000..18b420a76 --- /dev/null +++ b/test/src/browser-installer/utils.ts @@ -0,0 +1,88 @@ +import { Browser, Driver } from "../../../src/browser-installer/utils"; +import * as utils from "../../../src/browser-installer/utils"; + +describe("browser-installer/utils", () => { + describe("getDriverNameForBrowserName", () => { + it("CHROMEDRIVER", () => { + assert.equal(utils.getDriverNameForBrowserName(Browser.CHROME), Driver.CHROMEDRIVER); + assert.equal(utils.getDriverNameForBrowserName(Browser.CHROMIUM), Driver.CHROMEDRIVER); + }); + + it("GECKODRIVER", () => { + assert.equal(utils.getDriverNameForBrowserName(Browser.FIREFOX), Driver.GECKODRIVER); + }); + + it("SAFARIDRIVER", () => { + assert.equal(utils.getDriverNameForBrowserName(Browser.SAFARI), Driver.SAFARIDRIVER); + }); + + it("EDGEDRIVER", () => { + assert.equal(utils.getDriverNameForBrowserName(Browser.EDGE), Driver.EDGEDRIVER); + }); + + it("null", () => { + const invalidValue = "unknown" as (typeof Browser)[keyof typeof Browser]; + + assert.equal(utils.getDriverNameForBrowserName(invalidValue), null); + }); + }); + + it("createBrowserLabel", () => { + assert.equal(utils.createBrowserLabel("browserName", "browserVersion"), "browserName@browserVersion"); + }); + + describe("getMilestone", () => { + it("if number", () => { + assert.equal(utils.getMilestone(89), "89"); + }); + + it("if semantic version", () => { + assert.equal(utils.getMilestone("100.0.5670.160"), "100"); + }); + }); + + describe("semverVersionsComparator", () => { + it("greater", () => { + assert.isAbove(utils.semverVersionsComparator("100.0.10.20", "99.0.20.10"), 0); + assert.isAbove(utils.semverVersionsComparator("101.0.20.10", "100.0.10.20"), 0); + assert.isAbove(utils.semverVersionsComparator("100.0.20.10", "100.0.10.20"), 0); + assert.isAbove(utils.semverVersionsComparator("100.0.10.30", "foo_100.0.10.20"), 0); + assert.isAbove(utils.semverVersionsComparator("bar_100.0.10.30", "100.0.10.20"), 0); + assert.isAbove(utils.semverVersionsComparator("100.0.20.30", "100.0.10"), 0); + }); + + it("less", () => { + assert.isBelow(utils.semverVersionsComparator("99.0.20.10", "100.0.10.20"), 0); + assert.isBelow(utils.semverVersionsComparator("100.0.10.20", "101.0.20.10"), 0); + assert.isBelow(utils.semverVersionsComparator("100.0.10.20", "100.0.20.10"), 0); + assert.isBelow(utils.semverVersionsComparator("foo_100.0.10.20", "100.0.10.30"), 0); + assert.isBelow(utils.semverVersionsComparator("100.0.10.20", "bar_100.0.10.30"), 0); + assert.isBelow(utils.semverVersionsComparator("100.0.10.20", "100.0.20"), 0); + }); + + it("equal", () => { + assert.equal(utils.semverVersionsComparator("100.0.10.20", "100.0.10.20"), 0); + assert.equal(utils.semverVersionsComparator("foo_100.0.10", "100.0.10"), 0); + assert.equal(utils.semverVersionsComparator("bar_100.0", "100.0"), 0); + assert.equal(utils.semverVersionsComparator("100", "100"), 0); + }); + }); + + describe("normalizeChromeVersion", () => { + it("1 part", () => { + assert.equal(utils.normalizeChromeVersion("112"), "112"); + }); + + it("2 parts", () => { + assert.equal(utils.normalizeChromeVersion("112.0"), "112"); + }); + + it("3 parts", () => { + assert.equal(utils.normalizeChromeVersion("112.0.5678"), "112.0.5678"); + }); + + it("4 parts", () => { + assert.equal(utils.normalizeChromeVersion("112.0.5678.170"), "112.0.5678"); + }); + }); +}); diff --git a/test/src/browser-pool/basic-pool.js b/test/src/browser-pool/basic-pool.js index ec2de54e3..5679cb47c 100644 --- a/test/src/browser-pool/basic-pool.js +++ b/test/src/browser-pool/basic-pool.js @@ -4,6 +4,7 @@ const { AsyncEmitter } = require("src/events/async-emitter"); const { BasicPool } = require("src/browser-pool/basic-pool"); const { NewBrowser } = require("src/browser/new-browser"); const { CancelledError } = require("src/browser-pool/cancelled-error"); +const { WebdriverPool } = require("src/browser-pool/webdriver-pool"); const { MasterEvents: Events } = require("src/events"); const { stubBrowser } = require("./util"); const _ = require("lodash"); @@ -33,13 +34,25 @@ describe("browser-pool/basic-pool", () => { await mkPool_({ config }).getBrowser("broId"); - assert.calledWith(NewBrowser.create, config, { id: "broId" }); + assert.calledWith(NewBrowser.create, config, sinon.match({ id: "broId" })); }); it("should create new browser with specified version when requested", async () => { await mkPool_().getBrowser("broId", { version: "1.0" }); - assert.calledWith(NewBrowser.create, sinon.match.any, { id: "broId", version: "1.0" }); + assert.calledWith(NewBrowser.create, sinon.match.any, sinon.match({ id: "broId", version: "1.0" })); + }); + + it("should pass webdriver pool when creating new browser", async () => { + await mkPool_().getBrowser("broId"); + + assert.calledWith( + NewBrowser.create, + sinon.match.any, + sinon.match({ + wdPool: sinon.match.instanceOf(WebdriverPool), + }), + ); }); it("should init browser", async () => { @@ -84,7 +97,7 @@ describe("browser-pool/basic-pool", () => { const browser = stubBrowser(); NewBrowser.create.returns(browser); - const onSessionStart = sinon.stub().named("onSessionStart"); + const onSessionStart = sandbox.stub().named("onSessionStart"); const emitter = new AsyncEmitter().on(Events.SESSION_START, onSessionStart); await mkPool_({ emitter }).getBrowser(); @@ -96,7 +109,7 @@ describe("browser-pool/basic-pool", () => { const browser = stubBrowser(); NewBrowser.create.returns(browser); - const afterSessionStart = sinon.stub().named("afterSessionStart"); + const afterSessionStart = sandbox.stub().named("afterSessionStart"); const emitter = new AsyncEmitter().on(Events.SESSION_START, () => Promise.delay(1).then(afterSessionStart)); await mkPool_({ emitter }).getBrowser(); @@ -137,7 +150,7 @@ describe("browser-pool/basic-pool", () => { describe("SESSION_END event", () => { it("should be emitted before browser quit", async () => { - const onSessionEnd = sinon.stub().named("onSessionEnd"); + const onSessionEnd = sandbox.stub().named("onSessionEnd"); const emitter = new AsyncEmitter().on(Events.SESSION_END, onSessionEnd); const pool = mkPool_({ emitter }); @@ -149,7 +162,7 @@ describe("browser-pool/basic-pool", () => { }); it("handler should be waited before actual quit", async () => { - const afterSessionEnd = sinon.stub().named("afterSessionEnd"); + const afterSessionEnd = sandbox.stub().named("afterSessionEnd"); const emitter = new AsyncEmitter().on(Events.SESSION_END, () => Promise.delay(1).then(afterSessionEnd)); const pool = mkPool_({ emitter }); diff --git a/test/src/browser-pool/webdriver-pool.ts b/test/src/browser-pool/webdriver-pool.ts new file mode 100644 index 000000000..7fa2aaa74 --- /dev/null +++ b/test/src/browser-pool/webdriver-pool.ts @@ -0,0 +1,79 @@ +import proxyquire from "proxyquire"; +import sinon, { type SinonStub } from "sinon"; +import type { WebdriverPool as WdPoolType } from "../../../src/browser-pool/webdriver-pool"; + +describe("browser-pool/webdriver-pool", () => { + const sandbox = sinon.createSandbox(); + + let wdPool: WdPoolType; + + let getDriverNameForBrowserNameStub: SinonStub; + let runBrowserDriverStub: SinonStub; + + beforeEach(() => { + getDriverNameForBrowserNameStub = sandbox.stub().returns("edgedriver"); + runBrowserDriverStub = sandbox.stub().resolves({ + gridUrl: "http://localhost:12345", + process: { kill: sandbox.stub() }, + port: 12345, + }); + + const { WebdriverPool } = proxyquire("../../../src/browser-pool/webdriver-pool", { + "../browser-installer": { + runBrowserDriver: runBrowserDriverStub, + getDriverNameForBrowserName: getDriverNameForBrowserNameStub, + }, + }); + + wdPool = new WebdriverPool(); + }); + + afterEach(() => sandbox.restore()); + + it("should run browser driver", async () => { + getDriverNameForBrowserNameStub.returns("edgedriver"); + runBrowserDriverStub.resolves({ + gridUrl: "http://localhost:100500", + process: sandbox.stub(), + port: 100500, + }); + + const driver = await wdPool.getWebdriver("MicrosoftEdge", "135.0"); + + assert.equal(driver.gridUrl, "http://localhost:100500"); + assert.calledOnceWith(runBrowserDriverStub, "edgedriver", "135.0", { debug: false }); + }); + + it("should run browser driver with debug mode", async () => { + await wdPool.getWebdriver("MicrosoftEdge", "135.0"); + + assert.calledOnceWith(runBrowserDriverStub, sinon.match.string, sinon.match.string, { debug: false }); + }); + + it("should run extra drivers if all of existing ones are busy", async () => { + await wdPool.getWebdriver("MicrosoftEdge", "135.0"); + await wdPool.getWebdriver("MicrosoftEdge", "135.0"); + + assert.calledTwice(runBrowserDriverStub); + }); + + it("should not run extra drivers if driver is freed", async () => { + const driver = await wdPool.getWebdriver("MicrosoftEdge", "135.0"); + + driver.free(); + + await wdPool.getWebdriver("MicrosoftEdge", "135.0"); + + assert.calledOnce(runBrowserDriverStub); + }); + + it("should run extra drivers if driver is dead", async () => { + const driver = await wdPool.getWebdriver("MicrosoftEdge", "135.0"); + + driver.kill(); + + await wdPool.getWebdriver("MicrosoftEdge", "135.0"); + + assert.calledTwice(runBrowserDriverStub); + }); +}); diff --git a/test/src/browser/commands/switchToRepl.ts b/test/src/browser/commands/switchToRepl.ts index 7320ef7ab..9d50a9bd0 100644 --- a/test/src/browser/commands/switchToRepl.ts +++ b/test/src/browser/commands/switchToRepl.ts @@ -1,19 +1,24 @@ import repl, { type REPLServer } from "node:repl"; import { EventEmitter } from "node:events"; +import proxyquire from "proxyquire"; import * as webdriverio from "webdriverio"; import chalk from "chalk"; import sinon, { type SinonStub, type SinonSpy } from "sinon"; import RuntimeConfig from "src/config/runtime-config"; import clientBridge from "src/browser/client-bridge"; -import logger from "src/utils/logger"; -import { mkExistingBrowser_ as mkBrowser_, mkSessionStub_ } from "../utils"; import type ExistingBrowser from "src/browser/existing-browser"; describe('"switchToRepl" command', () => { const sandbox = sinon.createSandbox(); + let mkBrowser_: SinonStub; + let mkSessionStub_: SinonStub; + + let logStub: SinonStub; + let warnStub: SinonStub; + const initBrowser_ = ({ browser = mkBrowser_(), session = mkSessionStub_() } = {}): Promise => { (webdriverio.attach as SinonStub).resolves(session); @@ -41,12 +46,25 @@ describe('"switchToRepl" command', () => { }; beforeEach(() => { + mkBrowser_ = sandbox.stub(); + mkSessionStub_ = sandbox.stub(); + + logStub = sandbox.stub(); + warnStub = sandbox.stub(); + sandbox.stub(webdriverio, "attach"); sandbox.stub(clientBridge, "build").resolves(); sandbox.stub(RuntimeConfig, "getInstance").returns({ replMode: { enabled: false }, extend: sinon.stub() }); - sandbox.stub(logger, "warn"); - sandbox.stub(logger, "log"); sandbox.stub(process, "chdir"); + + ({ mkExistingBrowser_: mkBrowser_, mkSessionStub_ } = proxyquire("../utils", { + "src/browser/existing-browser": proxyquire("src/browser/existing-browser", { + "../utils/logger": { warn: warnStub, log: logStub }, + "./commands/switchToRepl": proxyquire("src/browser/commands/switchToRepl", { + "../../utils/logger": { warn: warnStub, log: logStub }, + }), + }), + })); }); afterEach(() => sandbox.restore()); @@ -83,7 +101,7 @@ describe('"switchToRepl" command', () => { await switchToRepl_({ session }); assert.callOrder( - (logger.log as SinonStub).withArgs( + (logStub as SinonStub).withArgs( chalk.yellow("You have entered to REPL mode via terminal (test execution timeout is disabled)."), ), repl.start as SinonStub, @@ -177,7 +195,7 @@ describe('"switchToRepl" command', () => { await Promise.all([promise1, promise2]); assert.calledOnce(repl.start as SinonStub); - assert.calledOnceWith(logger.warn, chalk.yellow("Testplane is already in REPL mode")); + assert.calledOnceWith(warnStub, chalk.yellow("Testplane is already in REPL mode")); }); ["const", "let"].forEach(decl => { diff --git a/test/src/browser/new-browser.js b/test/src/browser/new-browser.js index 0d2d2fe5e..1445c9a8b 100644 --- a/test/src/browser/new-browser.js +++ b/test/src/browser/new-browser.js @@ -2,24 +2,37 @@ const crypto = require("crypto"); const webdriverio = require("webdriverio"); -const logger = require("src/utils/logger"); +const proxyquire = require("proxyquire"); const signalHandler = require("src/signal-handler"); const history = require("src/browser/history"); const { WEBDRIVER_PROTOCOL, DEVTOOLS_PROTOCOL, SAVE_HISTORY_MODE } = require("src/constants/config"); const { X_REQUEST_ID_DELIMITER } = require("src/constants/browser"); const RuntimeConfig = require("src/config/runtime-config"); -const { mkNewBrowser_: mkBrowser_, mkSessionStub_ } = require("./utils"); describe("NewBrowser", () => { const sandbox = sinon.createSandbox(); let session; + let mkBrowser_, mkSessionStub_, mkWdPool_, installBrowserStub, warnStub; beforeEach(() => { + installBrowserStub = sandbox.stub().resolves("/browser/path"); + warnStub = sandbox.stub(); + + ({ + mkNewBrowser_: mkBrowser_, + mkSessionStub_, + mkWdPool_, + } = proxyquire("./utils", { + "src/browser/new-browser": proxyquire("src/browser/new-browser", { + "../browser-installer": { installBrowser: installBrowserStub }, + "../utils/logger": { warn: warnStub }, + }), + })); + session = mkSessionStub_(); - sandbox.stub(logger); sandbox.stub(webdriverio, "remote").resolves(session); - sandbox.stub(RuntimeConfig, "getInstance").returns({ devtools: undefined }); + sandbox.stub(RuntimeConfig, "getInstance").returns({ devtools: undefined, local: undefined }); }); afterEach(() => sandbox.restore()); @@ -138,7 +151,7 @@ describe("NewBrowser", () => { desiredCapabilities: { browserName: "safari" }, }).init(); - assert.calledOnceWith(logger.warn, "WARNING: Headless setting is not supported for safari browserName"); + assert.calledOnceWith(warnStub, "WARNING: Headless setting is not supported for safari browserName"); }); }); @@ -224,7 +237,7 @@ describe("NewBrowser", () => { it("should call user handler from config", async () => { const request = { headers: {} }; - const transformRequestStub = sinon.stub().returns(request); + const transformRequestStub = sandbox.stub().returns(request); await mkBrowser_({ transformRequest: transformRequestStub }).init(); @@ -265,7 +278,7 @@ describe("NewBrowser", () => { describe("transformResponse option", () => { it("should call user handler from config", async () => { - const transformResponseStub = sinon.stub(); + const transformResponseStub = sandbox.stub(); const response = {}; await mkBrowser_({ transformResponse: transformResponseStub }).init(); @@ -339,7 +352,7 @@ describe("NewBrowser", () => { session.setTimeout.withArgs({ pageLoad: 100500 }).throws(new Error("o.O")); await assert.isRejected(browser.init(), "o.O"); - assert.notCalled(logger.warn); + assert.notCalled(warnStub); }); }); @@ -349,7 +362,75 @@ describe("NewBrowser", () => { session.setTimeout.withArgs({ pageLoad: 100500 }).throws(new Error("o.O")); await assert.isFulfilled(browser.init()); - assert.calledOnceWith(logger.warn, "WARNING: Can not set page load timeout: o.O"); + assert.calledOnceWith(warnStub, "WARNING: Can not set page load timeout: o.O"); + }); + }); + + describe("should use local grid url", () => { + it("if gridUrl is 'local'", async () => { + installBrowserStub.withArgs("chrome", "115.0").resolves("/browser/path/chrome/115.0"); + RuntimeConfig.getInstance.returns({ local: false }); + const wdPool = mkWdPool_({ gridUrl: "http://localhost:12345/" }); + const browser = mkBrowser_( + { + gridUrl: "local", + automationProtocol: "webdriver", + desiredCapabilities: { + browserName: "chrome", + browserVersion: "115.0", + }, + }, + { wdPool }, + ); + + await browser.init(); + + assert.calledWithMatch(webdriverio.remote, { + protocol: "http", + hostname: "localhost", + port: 12345, + path: "/", + capabilities: { + browserName: "chrome", + browserVersion: "115.0", + "goog:chromeOptions": { + binary: "/browser/path/chrome/115.0", + }, + }, + }); + }); + + it("if local cli arg is set", async () => { + installBrowserStub.withArgs("chrome", "115.0").resolves("/browser/path/chrome/115.0"); + RuntimeConfig.getInstance.returns({ local: true }); + const wdPool = mkWdPool_({ gridUrl: "http://localhost:12345/" }); + const browser = mkBrowser_( + { + gridUrl: "http://localhost:4444/wd/hub", + automationProtocol: "webdriver", + desiredCapabilities: { + browserName: "chrome", + browserVersion: "115.0", + }, + }, + { wdPool }, + ); + + await browser.init(); + + assert.calledWithMatch(webdriverio.remote, { + protocol: "http", + hostname: "localhost", + port: 12345, + path: "/", + capabilities: { + browserName: "chrome", + browserVersion: "115.0", + "goog:chromeOptions": { + binary: "/browser/path/chrome/115.0", + }, + }, + }); }); }); }); @@ -390,6 +471,55 @@ describe("NewBrowser", () => { assert.propertyVal(session.options, "connectionRetryTimeout", 100500); }); + + it("should free webdriver session", async () => { + RuntimeConfig.getInstance.returns({ local: false }); + const wdProcess = { gridUrl: "http://localhost:12345", free: sandbox.stub(), kill: sandbox.stub() }; + const wdPool = { getWebdriver: sandbox.stub().resolves(wdProcess) }; + const browser = mkBrowser_( + { + gridUrl: "local", + automationProtocol: "webdriver", + desiredCapabilities: { + browserName: "chrome", + browserVersion: "115.0", + }, + }, + { wdPool }, + ); + + await browser.init(); + + await browser.quit(); + + assert.notCalled(wdProcess.kill); + assert.calledOnce(wdProcess.free); + }); + + it("should kill webdriver session if cant quit normally", async () => { + RuntimeConfig.getInstance.returns({ local: false }); + session.deleteSession.rejects(new Error("failed end")); + const wdProcess = { gridUrl: "http://localhost:12345", free: sandbox.stub(), kill: sandbox.stub() }; + const wdPool = { getWebdriver: sandbox.stub().resolves(wdProcess) }; + const browser = mkBrowser_( + { + gridUrl: "local", + automationProtocol: "webdriver", + desiredCapabilities: { + browserName: "chrome", + browserVersion: "115.0", + }, + }, + { wdPool }, + ); + + await browser.init(); + + await browser.quit(); + + assert.notCalled(wdProcess.free); + assert.calledOnce(wdProcess.kill); + }); }); describe("sessionId", () => { @@ -409,7 +539,7 @@ describe("NewBrowser", () => { await browser.quit(); - assert.called(logger.warn); + assert.called(warnStub); }); }); }); diff --git a/test/src/browser/utils.js b/test/src/browser/utils.js index 30af7983d..b6719143d 100644 --- a/test/src/browser/utils.js +++ b/test/src/browser/utils.js @@ -49,7 +49,26 @@ function createBrowserConfig_(opts = {}) { }; } -exports.mkNewBrowser_ = (configOpts, opts = { id: "browser", version: "1.0", state: {} }) => { +exports.mkWdPool_ = ({ gridUrl = "http://localhost:12345/wd/local" } = {}) => ({ + getWebdriver: sinon + .stub() + .named("getWebdriver") + .resolves({ + gridUrl, + free: sinon.stub().named("free"), + kill: sinon.stub().named("kill"), + }), +}); + +exports.mkNewBrowser_ = ( + configOpts, + opts = { + id: "browser", + version: "1.0", + state: {}, + wdPool: exports.mkWdPool_(), + }, +) => { return NewBrowser.create(createBrowserConfig_(configOpts), opts); }; diff --git a/test/src/cli/commands/install-deps/index.ts b/test/src/cli/commands/install-deps/index.ts new file mode 100644 index 000000000..c5f1425de --- /dev/null +++ b/test/src/cli/commands/install-deps/index.ts @@ -0,0 +1,161 @@ +import path from "path"; +import { Command } from "@gemini-testing/commander"; +import proxyquire from "proxyquire"; +import sinon, { type SinonStub } from "sinon"; +import { Testplane } from "../../../../../src/testplane"; +import type { Writable } from "type-fest"; +import type { Config } from "../../../../../src/config"; + +describe("cli/commands/install-deps", () => { + const sandbox = sinon.createSandbox(); + + let cli: { run: () => void }; + let loggerStub: { log: SinonStub; warn: SinonStub; error: SinonStub }; + let testplaneStub: Writable; + let installBrowsersWithDriversStub: SinonStub; + + const installBrowsers_ = async (argv: string = ""): Promise => { + process.argv = ["foo/bar/node", "foo/bar/script", "install-deps", ...argv.split(" ")].filter(Boolean); + cli.run(); + + await (Command.prototype.action as SinonStub).lastCall.returnValue; + }; + + const mkBrowser_ = (browserName: string, browserVersion: string): Config["browsers"][string] => + ({ + desiredCapabilities: { browserName, browserVersion }, + } as Config["browsers"][string]); + + beforeEach(() => { + loggerStub = { log: sandbox.stub(), warn: sandbox.stub(), error: sandbox.stub() }; + testplaneStub = Object.create(Testplane.prototype); + + Object.defineProperty(testplaneStub, "config", { + value: { browsers: {} }, + writable: true, + configurable: true, + }); + + sandbox.stub(Testplane, "create").returns(testplaneStub as Testplane); + + sandbox.stub(process, "exit"); + + sandbox.spy(Command.prototype, "action"); + + installBrowsersWithDriversStub = sandbox.stub(); + + cli = proxyquire("../../../../../src/cli", { + [path.resolve(process.cwd(), "src/cli/commands/install-deps")]: proxyquire( + "../../../../../src/cli/commands/install-deps", + { + "../../../browser-installer": { + installBrowsersWithDrivers: installBrowsersWithDriversStub, + BrowserInstallStatus: { Ok: "ok", Skip: "skip", Error: "error" }, + }, + "../../../utils/logger": loggerStub, + }, + ), + }); + }); + + afterEach(() => sandbox.restore()); + + it("should install listed browsers with versions", async () => { + testplaneStub.config.browsers = {}; + + await installBrowsers_("chrome@113 firefox@120 chrome@80"); + + assert.calledWith(installBrowsersWithDriversStub, [ + { browserName: "chrome", browserVersion: "113" }, + { browserName: "firefox", browserVersion: "120" }, + { browserName: "chrome", browserVersion: "80" }, + ]); + }); + + it("should install browsers from config", async () => { + testplaneStub.config.browsers = { + "chrome@113": mkBrowser_("safari", "70"), + "firefox@123": mkBrowser_("edge", "100"), + "chrome@100": mkBrowser_("firefox", "120"), + "chrome@80": mkBrowser_("firefox", "115"), + }; + + await installBrowsers_("chrome@113 firefox@123 chrome@80"); + + assert.calledWith(installBrowsersWithDriversStub, [ + { browserName: "safari", browserVersion: "70" }, + { browserName: "edge", browserVersion: "100" }, + { browserName: "firefox", browserVersion: "115" }, + ]); + }); + + it("should install some browsers from config and others with browser name + versions", async () => { + testplaneStub.config.browsers = { + "my-chrome": mkBrowser_("chrome", "115"), + firefox: mkBrowser_("firefox", "120"), + }; + + await installBrowsers_("my-chrome chrome@80"); + + assert.calledWith(installBrowsersWithDriversStub, [ + { browserName: "chrome", browserVersion: "115" }, + { browserName: "chrome", browserVersion: "80" }, + ]); + }); + + it("should install all config browsers", async () => { + testplaneStub.config.browsers = { + safari: mkBrowser_("safari", "70"), + edge: mkBrowser_("edge", "100"), + "firefox-120": mkBrowser_("firefox", "120"), + "firefox-115": mkBrowser_("firefox", "115"), + }; + + await installBrowsers_(""); + + assert.calledWith(installBrowsersWithDriversStub, [ + { browserName: "safari", browserVersion: "70" }, + { browserName: "edge", browserVersion: "100" }, + { browserName: "firefox", browserVersion: "120" }, + { browserName: "firefox", browserVersion: "115" }, + ]); + }); + + describe("should log", () => { + it("successfully installed browsers", async () => { + installBrowsersWithDriversStub.resolves({ "chrome@110": { status: "ok" } }); + testplaneStub.config.browsers = {}; + + await installBrowsers_("chrome@110"); + + assert.calledOnceWithMatch(loggerStub.log, "These browsers are downloaded successfully:"); + assert.calledOnceWithMatch(loggerStub.log, "- chrome@110"); + assert.notCalled(loggerStub.warn); + assert.notCalled(loggerStub.error); + }); + + it("skipped browsers", async () => { + installBrowsersWithDriversStub.resolves({ "chrome@110": { status: "skip", reason: "some reason" } }); + testplaneStub.config.browsers = {}; + + await installBrowsers_("chrome@110"); + + assert.calledOnceWithMatch(loggerStub.warn, "Browser install for these browsers was skipped:"); + assert.calledOnceWithMatch(loggerStub.warn, "- chrome@110: some reason"); + assert.notCalled(loggerStub.log); + assert.notCalled(loggerStub.error); + }); + + it("failed to install browsers", async () => { + installBrowsersWithDriversStub.resolves({ "chrome@110": { status: "error", reason: "some reason" } }); + testplaneStub.config.browsers = {}; + + await installBrowsers_("chrome@110"); + + assert.calledOnceWithMatch(loggerStub.error, "An error occured while trying to download these browsers:"); + assert.calledOnceWithMatch(loggerStub.error, "- chrome@110: some reason"); + assert.notCalled(loggerStub.log); + assert.notCalled(loggerStub.warn); + }); + }); +}); diff --git a/test/src/cli/index.js b/test/src/cli/index.js index d5ec54184..2b4555981 100644 --- a/test/src/cli/index.js +++ b/test/src/cli/index.js @@ -305,4 +305,10 @@ describe("cli", () => { assert.calledWithMatch(Testplane.prototype.run, any, { devtools: true }); }); + + it("should turn on local mode from cli", async () => { + await run_("--local"); + + assert.calledWithMatch(Testplane.prototype.run, any, { local: true }); + }); }); diff --git a/test/src/testplane.js b/test/src/testplane.js index 6c7f2d5fd..3602f4c11 100644 --- a/test/src/testplane.js +++ b/test/src/testplane.js @@ -204,6 +204,7 @@ describe("testplane", () => { enabled: true, }, devtools: true, + local: false, }); assert.calledOnce(RuntimeConfig.getInstance); @@ -213,6 +214,7 @@ describe("testplane", () => { inspectMode: { inspect: true }, replMode: { enabled: true }, devtools: true, + local: false, }); assert.callOrder(RuntimeConfig.getInstance, NodejsEnvRunner.create); });