diff --git a/package-lock.json b/package-lock.json index f3af7adbac..aa99afd72f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -82,6 +82,7 @@ "ansi-to-html": "^0.7.1", "babel-loader": "^8.2.2", "chokidar": "^3.5.0", + "chromedriver": "^93.0.1", "commonmark": "^0.29.1", "concurrently": "^6.2.1", "cross-env": "^7.0.2", @@ -1358,6 +1359,12 @@ "node": ">=6" } }, + "node_modules/@testim/chrome-version": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@testim/chrome-version/-/chrome-version-1.0.7.tgz", + "integrity": "sha512-8UT/J+xqCYfn3fKtOznAibsHpiuDshCb0fwgWxRazTT19Igp9ovoXMPhXyLD6m3CKQGTMHgqoxaFfMWaL40Rnw==", + "dev": true + }, "node_modules/@types/anymatch": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz", @@ -2123,6 +2130,19 @@ "node": ">= 6.0.0" } }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2379,6 +2399,15 @@ "node": ">= 4.5.0" } }, + "node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, "node_modules/babel-loader": { "version": "8.2.2", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.2.tgz", @@ -3064,6 +3093,28 @@ "node": ">=6.0" } }, + "node_modules/chromedriver": { + "version": "93.0.1", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-93.0.1.tgz", + "integrity": "sha512-KDzbW34CvQLF5aTkm3b5VdlTrvdIt4wEpCzT2p4XJIQWQZEPco5pNce7Lu9UqZQGkhQ4mpZt4Ky6NKVyIS2N8A==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@testim/chrome-version": "^1.0.7", + "axios": "^0.21.2", + "del": "^6.0.0", + "extract-zip": "^2.0.1", + "https-proxy-agent": "^5.0.0", + "proxy-from-env": "^1.1.0", + "tcp-port-used": "^1.0.1" + }, + "bin": { + "chromedriver": "bin/chromedriver" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/cipher-base": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", @@ -3113,6 +3164,15 @@ "node": ">= 4.0" } }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/cli": { "version": "0.4.5", "resolved": "https://registry.npmjs.org/cli/-/cli-0.4.5.tgz", @@ -3899,6 +3959,28 @@ "node": ">=0.10.0" } }, + "node_modules/del": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/del/-/del-6.0.0.tgz", + "integrity": "sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==", + "dev": true, + "dependencies": { + "globby": "^11.0.1", + "graceful-fs": "^4.2.4", + "is-glob": "^4.0.1", + "is-path-cwd": "^2.2.0", + "is-path-inside": "^3.0.2", + "p-map": "^4.0.0", + "rimraf": "^3.0.2", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/des.js": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", @@ -5222,6 +5304,26 @@ "readable-stream": "^2.3.6" } }, + "node_modules/follow-redirects": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz", + "integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -5987,6 +6089,15 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/infer-owner": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", @@ -6022,6 +6133,15 @@ "node": ">= 0.10" } }, + "node_modules/ip-regex": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", + "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-accessor-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", @@ -6244,6 +6364,24 @@ "node": ">= 0.4" } }, + "node_modules/is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -6290,6 +6428,12 @@ "node": ">= 0.4" } }, + "node_modules/is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "dev": true + }, "node_modules/is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", @@ -6308,6 +6452,20 @@ "node": ">=4" } }, + "node_modules/is2": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/is2/-/is2-2.0.7.tgz", + "integrity": "sha512-4vBQoURAXC6hnLFxD4VW7uc04XiwTTl/8ydYJxKvPwkWQrSjInkuM5VZVg6BGr1/natq69zDuvO9lGpLClJqvA==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "ip-regex": "^4.1.0", + "is-url": "^1.2.4" + }, + "engines": { + "node": ">=v0.10.0" + } + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -7454,6 +7612,21 @@ "node": ">=6" } }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -9258,6 +9431,16 @@ "node": ">=6" } }, + "node_modules/tcp-port-used": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.2.tgz", + "integrity": "sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA==", + "dev": true, + "dependencies": { + "debug": "4.3.1", + "is2": "^2.0.6" + } + }, "node_modules/terser": { "version": "4.8.0", "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", @@ -11626,6 +11809,12 @@ "defer-to-connect": "^1.0.1" } }, + "@testim/chrome-version": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@testim/chrome-version/-/chrome-version-1.0.7.tgz", + "integrity": "sha512-8UT/J+xqCYfn3fKtOznAibsHpiuDshCb0fwgWxRazTT19Igp9ovoXMPhXyLD6m3CKQGTMHgqoxaFfMWaL40Rnw==", + "dev": true + }, "@types/anymatch": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz", @@ -12296,6 +12485,16 @@ "debug": "4" } }, + "aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -12509,6 +12708,15 @@ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", "dev": true }, + "axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "dev": true, + "requires": { + "follow-redirects": "^1.14.0" + } + }, "babel-loader": { "version": "8.2.2", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.2.tgz", @@ -13086,6 +13294,21 @@ "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", "dev": true }, + "chromedriver": { + "version": "93.0.1", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-93.0.1.tgz", + "integrity": "sha512-KDzbW34CvQLF5aTkm3b5VdlTrvdIt4wEpCzT2p4XJIQWQZEPco5pNce7Lu9UqZQGkhQ4mpZt4Ky6NKVyIS2N8A==", + "dev": true, + "requires": { + "@testim/chrome-version": "^1.0.7", + "axios": "^0.21.2", + "del": "^6.0.0", + "extract-zip": "^2.0.1", + "https-proxy-agent": "^5.0.0", + "proxy-from-env": "^1.1.0", + "tcp-port-used": "^1.0.1" + } + }, "cipher-base": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", @@ -13128,6 +13351,12 @@ "source-map": "~0.6.0" } }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true + }, "cli": { "version": "0.4.5", "resolved": "https://registry.npmjs.org/cli/-/cli-0.4.5.tgz", @@ -13756,6 +13985,22 @@ } } }, + "del": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/del/-/del-6.0.0.tgz", + "integrity": "sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==", + "dev": true, + "requires": { + "globby": "^11.0.1", + "graceful-fs": "^4.2.4", + "is-glob": "^4.0.1", + "is-path-cwd": "^2.2.0", + "is-path-inside": "^3.0.2", + "p-map": "^4.0.0", + "rimraf": "^3.0.2", + "slash": "^3.0.0" + } + }, "des.js": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", @@ -14860,6 +15105,12 @@ "readable-stream": "^2.3.6" } }, + "follow-redirects": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz", + "integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==", + "dev": true + }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -15483,6 +15734,12 @@ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "dev": true }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + }, "infer-owner": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", @@ -15515,6 +15772,12 @@ "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", "dev": true }, + "ip-regex": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", + "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", + "dev": true + }, "is-accessor-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", @@ -15680,6 +15943,18 @@ "integrity": "sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==", "dev": true }, + "is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true + }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -15714,6 +15989,12 @@ "has-symbols": "^1.0.2" } }, + "is-url": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", + "dev": true + }, "is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", @@ -15726,6 +16007,17 @@ "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", "dev": true }, + "is2": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/is2/-/is2-2.0.7.tgz", + "integrity": "sha512-4vBQoURAXC6hnLFxD4VW7uc04XiwTTl/8ydYJxKvPwkWQrSjInkuM5VZVg6BGr1/natq69zDuvO9lGpLClJqvA==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "ip-regex": "^4.1.0", + "is-url": "^1.2.4" + } + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -16670,6 +16962,15 @@ "p-limit": "^2.0.0" } }, + "p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -18144,6 +18445,16 @@ "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", "dev": true }, + "tcp-port-used": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.2.tgz", + "integrity": "sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA==", + "dev": true, + "requires": { + "debug": "4.3.1", + "is2": "^2.0.6" + } + }, "terser": { "version": "4.8.0", "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", diff --git a/package.json b/package.json index 7e39e5f6ae..5eb0d2e74d 100644 --- a/package.json +++ b/package.json @@ -111,6 +111,7 @@ "ansi-to-html": "^0.7.1", "babel-loader": "^8.2.2", "chokidar": "^3.5.0", + "chromedriver": "^93.0.1", "commonmark": "^0.29.1", "concurrently": "^6.2.1", "cross-env": "^7.0.2", diff --git a/src/server/browserType.ts b/src/server/browserType.ts index 0a2c3b5237..7b21f51929 100644 --- a/src/server/browserType.ts +++ b/src/server/browserType.ts @@ -57,6 +57,9 @@ export abstract class BrowserType extends SdkObject { const controller = new ProgressController(metadata, this); controller.setLogName('browser'); const browser = await controller.run(progress => { + const seleniumHubUrl = (options as any).__testHookSeleniumRemoteURL || process.env.SELENIUM_REMOTE_URL; + if (seleniumHubUrl) + return this._launchWithSeleniumHub(progress, seleniumHubUrl, options); return this._innerLaunchWithRetries(progress, options, undefined, helper.debugProtocolLogger(protocolLogger)).catch(e => { throw this._rewriteStartupError(e); }); }, TimeoutSettings.timeout(options)); return browser; @@ -243,6 +246,10 @@ export abstract class BrowserType extends SdkObject { throw new Error('CDP connections are only supported by Chromium'); } + async _launchWithSeleniumHub(progress: Progress, hubUrl: string, options: types.LaunchOptions): Promise { + throw new Error('Connecting to SELENIUM_REMOTE_URL is only supported by Chromium'); + } + private _validateLaunchOptions(options: Options): Options { const { devtools = false } = options; let { headless = !devtools, downloadsPath, proxy } = options; diff --git a/src/server/chromium/chromium.ts b/src/server/chromium/chromium.ts index 73d2dd2e73..4b0972311a 100644 --- a/src/server/chromium/chromium.ts +++ b/src/server/chromium/chromium.ts @@ -27,9 +27,9 @@ import { ConnectionTransport, ProtocolRequest, WebSocketTransport } from '../tra import { CRDevTools } from './crDevTools'; import { BrowserOptions, BrowserProcess, PlaywrightOptions } from '../browser'; import * as types from '../types'; -import { debugMode, headersArrayToObject, removeFolders } from '../../utils/utils'; +import { debugMode, fetchData, headersArrayToObject, removeFolders, streamToString } from '../../utils/utils'; import { RecentLogsCollector } from '../../utils/debugLogger'; -import { ProgressController } from '../progress'; +import { Progress, ProgressController } from '../progress'; import { TimeoutSettings } from '../../utils/timeoutSettings'; import { helper } from '../helper'; import { CallMetadata } from '../instrumentation'; @@ -52,42 +52,45 @@ export class Chromium extends BrowserType { override async connectOverCDP(metadata: CallMetadata, endpointURL: string, options: { slowMo?: number, headers?: types.HeadersArray }, timeout?: number) { const controller = new ProgressController(metadata, this); controller.setLogName('browser'); - const browserLogsCollector = new RecentLogsCollector(); return controller.run(async progress => { - let headersMap: { [key: string]: string; } | undefined; - if (options.headers) - headersMap = headersArrayToObject(options.headers, false); - - const artifactsDir = await fs.promises.mkdtemp(ARTIFACTS_FOLDER); - - const chromeTransport = await WebSocketTransport.connect(progress, await urlToWSEndpoint(endpointURL), headersMap); - const browserProcess: BrowserProcess = { - close: async () => { - await removeFolders([ artifactsDir ]); - await chromeTransport.closeAndWait(); - }, - kill: async () => { - await removeFolders([ artifactsDir ]); - await chromeTransport.closeAndWait(); - } - }; - const browserOptions: BrowserOptions = { - ...this._playwrightOptions, - slowMo: options.slowMo, - name: 'chromium', - isChromium: true, - persistent: { noDefaultViewport: true }, - browserProcess, - protocolLogger: helper.debugProtocolLogger(), - browserLogsCollector, - artifactsDir, - downloadsPath: artifactsDir, - tracesDir: artifactsDir - }; - return await CRBrowser.connect(chromeTransport, browserOptions); + return await this._connectOverCDPInternal(progress, endpointURL, options); }, TimeoutSettings.timeout({timeout})); } + async _connectOverCDPInternal(progress: Progress, endpointURL: string, options: { slowMo?: number, headers?: types.HeadersArray }, onClose?: () => Promise) { + let headersMap: { [key: string]: string; } | undefined; + if (options.headers) + headersMap = headersArrayToObject(options.headers, false); + + const artifactsDir = await fs.promises.mkdtemp(ARTIFACTS_FOLDER); + + const wsEndpoint = await urlToWSEndpoint(endpointURL); + progress.throwIfAborted(); + + const chromeTransport = await WebSocketTransport.connect(progress, wsEndpoint, headersMap); + const doClose = async () => { + await removeFolders([ artifactsDir ]); + await chromeTransport.closeAndWait(); + await onClose?.(); + }; + const browserProcess: BrowserProcess = { close: doClose, kill: doClose }; + const browserOptions: BrowserOptions = { + ...this._playwrightOptions, + slowMo: options.slowMo, + name: 'chromium', + isChromium: true, + persistent: { noDefaultViewport: true }, + browserProcess, + protocolLogger: helper.debugProtocolLogger(), + browserLogsCollector: new RecentLogsCollector(), + artifactsDir, + downloadsPath: artifactsDir, + tracesDir: artifactsDir + }; + progress.throwIfAborted(); + return await CRBrowser.connect(chromeTransport, browserOptions); + } + private _createDevTools() { // TODO: this is totally wrong when using channels. const directory = registry.findExecutable('chromium').directory; @@ -128,7 +131,77 @@ export class Chromium extends BrowserType { transport.send(message); } + override async _launchWithSeleniumHub(progress: Progress, hubUrl: string, options: types.LaunchOptions): Promise { + if (!hubUrl.endsWith('/')) + hubUrl = hubUrl + '/'; + + const args = this._innerDefaultArgs(options); + args.push('--remote-debugging-port=0'); + const desiredCapabilities = { 'browserName': 'chrome', 'goog:chromeOptions': { args } }; + + progress.log(` ${hubUrl}`); + const response = await fetchData({ + url: hubUrl + 'session', + method: 'POST', + data: JSON.stringify({ + desiredCapabilities, + capabilities: { alwaysMatch: desiredCapabilities } + }), + timeout: progress.timeUntilDeadline(), + }, async response => { + const body = await streamToString(response); + let message = ''; + try { + const json = JSON.parse(body); + message = json.value.localizedMessage || json.value.message; + } catch (e) { + } + return new Error(`Error connecting to Selenium at ${hubUrl}: ${message}`); + }); + const value = JSON.parse(response).value; + const sessionId = value.sessionId; + progress.log(` sessionId=${sessionId}`); + + const disconnectFromSelenium = async () => { + progress.log(` sessionId=${sessionId}`); + await fetchData({ + url: hubUrl + 'session/' + sessionId, + method: 'DELETE', + }).catch(error => progress.log(`: ${error}`)); + progress.log(` sessionId=${sessionId}`); + }; + + try { + const capabilities = value.capabilities; + const maybeChromeOptions = capabilities['goog:chromeOptions']; + const chromeOptions = maybeChromeOptions && typeof maybeChromeOptions === 'object' ? maybeChromeOptions : undefined; + const debuggerAddress = chromeOptions && typeof chromeOptions.debuggerAddress === 'string' ? chromeOptions.debuggerAddress : undefined; + const chromeOptionsURL = typeof maybeChromeOptions === 'string' ? maybeChromeOptions : undefined; + let endpointURL = capabilities['se:cdp'] || debuggerAddress || chromeOptionsURL; + if (!['ws://', 'wss://', 'http://', 'https://'].some(protocol => endpointURL.startsWith(protocol))) + endpointURL = 'http://' + endpointURL; + return this._connectOverCDPInternal(progress, endpointURL, { slowMo: options.slowMo }, disconnectFromSelenium); + } catch (e) { + await disconnectFromSelenium(); + throw e; + } + } + _defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[] { + const chromeArguments = this._innerDefaultArgs(options); + chromeArguments.push(`--user-data-dir=${userDataDir}`); + if (options.useWebSocket) + chromeArguments.push('--remote-debugging-port=0'); + else + chromeArguments.push('--remote-debugging-pipe'); + if (isPersistent) + chromeArguments.push('about:blank'); + else + chromeArguments.push('--no-startup-window'); + return chromeArguments; + } + + private _innerDefaultArgs(options: types.LaunchOptions): string[] { const { args = [], proxy } = options; const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir')); if (userDataDirArg) @@ -138,16 +211,11 @@ export class Chromium extends BrowserType { if (args.find(arg => !arg.startsWith('-'))) throw new Error('Arguments can not specify page to be opened'); const chromeArguments = [...DEFAULT_ARGS]; - chromeArguments.push(`--user-data-dir=${userDataDir}`); // See https://github.com/microsoft/playwright/issues/7362 if (os.platform() === 'darwin') chromeArguments.push('--enable-use-zoom-for-dsf=false'); - if (options.useWebSocket) - chromeArguments.push('--remote-debugging-port=0'); - else - chromeArguments.push('--remote-debugging-pipe'); if (options.devtools) chromeArguments.push('--auto-open-devtools-for-tabs'); if (options.headless) { @@ -179,10 +247,6 @@ export class Chromium extends BrowserType { chromeArguments.push(`--proxy-bypass-list=${proxyBypassRules.join(';')}`); } chromeArguments.push(...args); - if (isPersistent) - chromeArguments.push('about:blank'); - else - chromeArguments.push('--no-startup-window'); return chromeArguments; } } diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 6e38bc1974..2e5ecfc1c4 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -44,6 +44,7 @@ type HTTPRequestParams = { method?: string, headers?: http.OutgoingHttpHeaders, data?: string | Buffer, + timeout?: number, }; function httpRequest(params: HTTPRequestParams, onResponse: (r: http.IncomingMessage) => void, onError: (error: Error) => void) { @@ -81,14 +82,26 @@ function httpRequest(params: HTTPRequestParams, onResponse: (r: http.IncomingMes https.request(options, requestCallback) : http.request(options, requestCallback); request.on('error', onError); + if (params.timeout !== undefined) { + const rejectOnTimeout = () => { + onError(new Error(`Request to ${params.url} timed out after ${params.timeout}ms`)); + request.abort(); + }; + if (params.timeout <= 0) { + rejectOnTimeout(); + return; + } + request.setTimeout(params.timeout, rejectOnTimeout); + } request.end(params.data); } -export function fetchData(params: HTTPRequestParams): Promise { +export function fetchData(params: HTTPRequestParams, onError?: (response: http.IncomingMessage) => Promise): Promise { return new Promise((resolve, reject) => { - httpRequest(params, response => { + httpRequest(params, async response => { if (response.statusCode !== 200) { - reject(new Error(`fetch failed: server returned code ${response.statusCode}. URL: ${params.url}`)); + const error = onError ? await onError(response) : new Error(`fetch failed: server returned code ${response.statusCode}. URL: ${params.url}`); + reject(error); return; } let body = ''; @@ -426,3 +439,12 @@ export function wrapInASCIIBox(text: string, padding = 0): string { export function isFilePayload(value: any): boolean { return typeof value === 'object' && value['name'] && value['mimeType'] && value['buffer']; } + +export function streamToString(stream: stream.Readable): Promise { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + stream.on('data', chunk => chunks.push(Buffer.from(chunk))); + stream.on('error', reject); + stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))); + }); +} diff --git a/tests/assets/selenium-grid/README.md b/tests/assets/selenium-grid/README.md new file mode 100644 index 0000000000..9473f8ea96 --- /dev/null +++ b/tests/assets/selenium-grid/README.md @@ -0,0 +1,4 @@ +Download locations: +- https://github.com/SeleniumHQ/selenium/releases/download/selenium-3.141.59/selenium-server-standalone-3.141.59.jar +- https://github.com/SeleniumHQ/selenium/releases/download/selenium-4.0.0-rc-1/selenium-server-4.0.0-rc-1.jar + diff --git a/tests/assets/selenium-grid/broken-selenium-driver.js b/tests/assets/selenium-grid/broken-selenium-driver.js new file mode 100755 index 0000000000..92f1453c55 --- /dev/null +++ b/tests/assets/selenium-grid/broken-selenium-driver.js @@ -0,0 +1,3 @@ +#!/usr/bin/env node + +process.exit(1); diff --git a/tests/assets/selenium-grid/selenium-config-standalone.json b/tests/assets/selenium-grid/selenium-config-standalone.json new file mode 100644 index 0000000000..e1d4b8c1cd --- /dev/null +++ b/tests/assets/selenium-grid/selenium-config-standalone.json @@ -0,0 +1,19 @@ +{ + "capabilities": + [ + { + "browserName": "chrome", + "maxInstances": 5, + "seleniumProtocol": "WebDriver" + } + ], + "role": "standalone", + "port": 4444, + "debug": false, + "browserTimeout": 0, + "timeout": 1800, + "enablePassThrough": true, + "server": { + "port": 4444 + } +} diff --git a/tests/assets/selenium-grid/selenium-server-4.0.0-rc-1.jar b/tests/assets/selenium-grid/selenium-server-4.0.0-rc-1.jar new file mode 100644 index 0000000000..6ba53035a0 Binary files /dev/null and b/tests/assets/selenium-grid/selenium-server-4.0.0-rc-1.jar differ diff --git a/tests/assets/selenium-grid/selenium-server-standalone-3.141.59.jar b/tests/assets/selenium-grid/selenium-server-standalone-3.141.59.jar new file mode 100644 index 0000000000..8410e9565a Binary files /dev/null and b/tests/assets/selenium-grid/selenium-server-standalone-3.141.59.jar differ diff --git a/tests/browsertype-launch-selenium.spec.ts b/tests/browsertype-launch-selenium.spec.ts new file mode 100644 index 0000000000..ce67becd52 --- /dev/null +++ b/tests/browsertype-launch-selenium.spec.ts @@ -0,0 +1,107 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { playwrightTest as test, expect } from './config/browserTest'; +import type { TestInfo } from '../types/test'; +import path from 'path'; +import fs from 'fs'; + +const chromeDriver = require('chromedriver').path; +const brokenDriver = path.join(__dirname, 'assets', 'selenium-grid', 'broken-selenium-driver.js'); +const seleniumConfigStandalone = path.join(__dirname, 'assets', 'selenium-grid', 'selenium-config-standalone.json'); +const standalone_3_141_59 = path.join(__dirname, 'assets', 'selenium-grid', 'selenium-server-standalone-3.141.59.jar'); +const selenium_4_0_0_rc1 = path.join(__dirname, 'assets', 'selenium-grid', 'selenium-server-4.0.0-rc-1.jar'); + +function writeSeleniumConfig(testInfo: TestInfo, port: number) { + const content = fs.readFileSync(seleniumConfigStandalone, 'utf8').replace(/4444/g, String(port)); + const file = testInfo.outputPath('selenium-config.json'); + fs.writeFileSync(file, content, 'utf8'); + return file; +} + +test.skip(({ mode }) => mode !== 'default', 'Using test hooks'); +test.skip(() => !!process.env.INSIDE_DOCKER, 'Docker image does not have Java'); + +test('selenium grid 3.141.59 standalone chromium', async ({ browserOptions, browserName, childProcess, waitForPort, browserType }, testInfo) => { + test.skip(browserName !== 'chromium'); + + const port = testInfo.workerIndex + 15123; + const grid = childProcess({ + command: ['java', `-Dwebdriver.chrome.driver=${chromeDriver}`, '-jar', standalone_3_141_59, '-config', writeSeleniumConfig(testInfo, port)], + cwd: __dirname, + }); + await waitForPort(port); + + const __testHookSeleniumRemoteURL = `http://localhost:${port}/wd/hub`; + const browser = await browserType.launch({ ...browserOptions, __testHookSeleniumRemoteURL } as any); + const page = await browser.newPage(); + await page.setContent('Hello world
Get Started
'); + await page.click('text=Get Started'); + await expect(page).toHaveTitle('Hello world'); + await browser.close(); + + expect(grid.output).toContain('Starting ChromeDriver'); + expect(grid.output).toContain('Started new session'); + await grid.waitForOutput('Removing session'); +}); + +test('selenium grid 4.0.0-rc-1 standalone chromium', async ({ browserOptions, browserName, childProcess, waitForPort, browserType }, testInfo) => { + test.skip(browserName !== 'chromium'); + + const port = testInfo.workerIndex + 15123; + const grid = childProcess({ + command: ['java', `-Dwebdriver.chrome.driver=${chromeDriver}`, '-jar', selenium_4_0_0_rc1, 'standalone', '--config', writeSeleniumConfig(testInfo, port)], + cwd: __dirname, + }); + await waitForPort(port); + + const __testHookSeleniumRemoteURL = `http://localhost:${port}/wd/hub`; + const browser = await browserType.launch({ ...browserOptions, __testHookSeleniumRemoteURL } as any); + const page = await browser.newPage(); + await page.setContent('Hello world
Get Started
'); + await page.click('text=Get Started'); + await expect(page).toHaveTitle('Hello world'); + await browser.close(); + + expect(grid.output).toContain('Starting ChromeDriver'); + expect(grid.output).toContain('Session created'); + await grid.waitForOutput('Deleted session'); +}); + +test('selenium grid 4.0.0-rc-1 standalone chromium broken driver', async ({ browserOptions, browserName, childProcess, waitForPort, browserType }, testInfo) => { + test.skip(browserName !== 'chromium'); + + const port = testInfo.workerIndex + 15123; + const grid = childProcess({ + command: ['java', `-Dwebdriver.chrome.driver=${brokenDriver}`, '-jar', selenium_4_0_0_rc1, 'standalone', '--config', writeSeleniumConfig(testInfo, port)], + cwd: __dirname, + }); + await waitForPort(port); + + const __testHookSeleniumRemoteURL = `http://localhost:${port}/wd/hub`; + const error = await browserType.launch({ ...browserOptions, __testHookSeleniumRemoteURL } as any).catch(e => e); + expect(error.message).toContain(`Error connecting to Selenium at http://localhost:${port}/wd/hub/: Could not start a new session`); + + expect(grid.output).not.toContain('Starting ChromeDriver'); +}); + +test('selenium grid 3.141.59 standalone non-chromium', async ({ browserName, browserType }, testInfo) => { + test.skip(browserName === 'chromium'); + + const __testHookSeleniumRemoteURL = `http://localhost:4444/wd/hub`; + const error = await browserType.launch({ __testHookSeleniumRemoteURL } as any).catch(e => e); + expect(error.message).toContain('Connecting to SELENIUM_REMOTE_URL is only supported by Chromium'); +}); diff --git a/tests/config/commonFixtures.ts b/tests/config/commonFixtures.ts index 591847e62c..48566fcc97 100644 --- a/tests/config/commonFixtures.ts +++ b/tests/config/commonFixtures.ts @@ -34,6 +34,8 @@ export class TestChildProcess { exited: Promise<{ exitCode: number | null, signal: string | null }>; exitCode: Promise; + private _outputCallbacks = new Set<() => void>(); + constructor(params: TestChildParams) { this.params = params; this.process = spawn(params.command[0], params.command.slice(1), { @@ -53,6 +55,9 @@ export class TestChildProcess { if (process.env.PWTEST_DEBUG) process.stdout.write(String(chunk)); this.onOutput?.(); + for (const cb of this._outputCallbacks) + cb(); + this._outputCallbacks.clear(); }; this.process.stderr.on('data', appendChunk); @@ -91,6 +96,11 @@ export class TestChildProcess { if (r.signal) throw new Error(`Process recieved signal: ${r.signal}`); } + + async waitForOutput(substring: string) { + while (!this.output.includes(substring)) + await new Promise(f => this._outputCallbacks.add(f)); + } } export type CommonFixtures = { diff --git a/utils/roll_browser.js b/utils/roll_browser.js index 47d01d6d3e..a62cb9fb1b 100755 --- a/utils/roll_browser.js +++ b/utils/roll_browser.js @@ -87,6 +87,15 @@ Example: process.stdout.write(execSync('npm run --silent doc')); } catch (e) { } + + if (browserName === 'chromium') { + // 5. Update chromedriver. + console.log('\nUpdating chromedriver...'); + try { + process.stdout.write(execSync('npm install --save-dev chromedriver@latest')); + } catch (e) { + } + } } console.log(`\nRolled ${browserName} to ${revision}`); })().catch(err => {