@eyeo/get-browser-binary 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitlab-ci.yml +5 -9
- package/.mocharc.json +1 -1
- package/README.md +2 -2
- package/RELEASE_NOTES.md +24 -0
- package/package.json +9 -9
- package/src/browsers.js +106 -77
- package/src/utils.js +27 -15
- package/test/browsers.js +52 -7
- package/test/utils.js +24 -8
package/.gitlab-ci.yml
CHANGED
|
@@ -32,12 +32,10 @@ test:browsers:linux:
|
|
|
32
32
|
image: docker:20.10.16
|
|
33
33
|
services:
|
|
34
34
|
- docker:20.10.16-dind
|
|
35
|
-
variables:
|
|
36
|
-
DOCKER_DRIVER: overlay2
|
|
37
35
|
before_script:
|
|
38
36
|
- docker build -f test/docker/Dockerfile -t browsers .
|
|
39
37
|
script:
|
|
40
|
-
- docker run --shm-size=
|
|
38
|
+
- docker run --shm-size=512m -t browsers
|
|
41
39
|
|
|
42
40
|
test:browsers:windows:
|
|
43
41
|
stage: test
|
|
@@ -53,15 +51,13 @@ test:browsers:windows:
|
|
|
53
51
|
- choco upgrade -y nodejs --version 16.10.0
|
|
54
52
|
- npm install
|
|
55
53
|
script:
|
|
54
|
+
# Running Edge tests only on the preinstalled version
|
|
55
|
+
# https://gitlab.com/eyeo/developer-experience/get-browser-binary/-/issues/29
|
|
56
|
+
- npm test -- --grep "edge.*latest"
|
|
57
|
+
- npm test -- --grep "chromium"
|
|
56
58
|
# Running only a subset of Firefox tests to avoid low OS resources error
|
|
57
59
|
# https://gitlab.com/eyeo/developer-experience/get-browser-binary/-/issues/2
|
|
58
60
|
- npm test -- --grep "firefox.*installs"
|
|
59
|
-
# Running npm v8 on powershell has issues when the grep value contains the
|
|
60
|
-
# pipe (|) literal. Storing that string as a verbatim string (single quotes)
|
|
61
|
-
# and then sorrounding it with four double quotes does the trick.
|
|
62
|
-
# https://gitlab.com/eyeo/developer-experience/get-browser-binary/-/issues/29
|
|
63
|
-
- $full_tests = '(chromium|edge.*latest)'
|
|
64
|
-
- npm test -- --grep """"$full_tests""""
|
|
65
61
|
tags:
|
|
66
62
|
- shared-windows
|
|
67
63
|
- windows
|
package/.mocharc.json
CHANGED
package/README.md
CHANGED
|
@@ -88,14 +88,14 @@ Useful to reproduce the CI environment of the `test:browsers:linux` job:
|
|
|
88
88
|
|
|
89
89
|
```shell
|
|
90
90
|
docker build -f test/docker/Dockerfile -t browsers .
|
|
91
|
-
docker run --shm-size=
|
|
91
|
+
docker run --shm-size=512m -it browsers
|
|
92
92
|
```
|
|
93
93
|
|
|
94
94
|
The `grep` and `timeout` options can also be used on Docker via the `TEST_ARGS`
|
|
95
95
|
parameter:
|
|
96
96
|
|
|
97
97
|
```shell
|
|
98
|
-
docker run --shm-size=
|
|
98
|
+
docker run --shm-size=512m -e TEST_ARGS="--grep chromium.*latest --timeout 100000" -it browsers
|
|
99
99
|
```
|
|
100
100
|
|
|
101
101
|
By default, tests delete the `./browser-snapshots` before each `Browser` suite
|
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
# 0.10.0
|
|
2
|
+
|
|
3
|
+
- Added handling of the new headless mode in Chromium (#45)
|
|
4
|
+
- Fixed an issue that prevented Chromium to be installed on macOS ARM processors
|
|
5
|
+
(!59)
|
|
6
|
+
- Changed documentation nullable parameters to optional parameters (#41)
|
|
7
|
+
- Increased the npm geckodriver version to 3.1.0 (!57)
|
|
8
|
+
- Fixed an issue with Windows Edge and msedgedriver, which occasionally failed
|
|
9
|
+
when building the driver (!56)
|
|
10
|
+
|
|
11
|
+
# 0.9.0
|
|
12
|
+
|
|
13
|
+
- Fixed a Chromium install issue by increasing the value of
|
|
14
|
+
`MAX_VERSION_DECREMENTS` (!52)
|
|
15
|
+
- `installBrowser()`, `getDriver()` and `download()` have a new optional timeout
|
|
16
|
+
parameter on file downloads (!51)
|
|
17
|
+
|
|
18
|
+
### Testing
|
|
19
|
+
|
|
20
|
+
- Added a test checking that downloaded browser-snapshot files are actually
|
|
21
|
+
cached (#35)
|
|
22
|
+
- Tests running the minimum firefox version showed occasional failures. That was
|
|
23
|
+
fixed by increasing the shared memory size of the docker image (!50)
|
|
24
|
+
|
|
1
25
|
# 0.8.0
|
|
2
26
|
|
|
3
27
|
- Move `takeFullPageScreenshot` to the utils module (#38)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eyeo/get-browser-binary",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Install browser binaries and matching webdrivers",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -30,17 +30,17 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"dmg": "^0.1.0",
|
|
32
32
|
"extract-zip": "^2.0.1",
|
|
33
|
-
"geckodriver": "
|
|
34
|
-
"got": "^
|
|
35
|
-
"jimp": "^0.
|
|
36
|
-
"selenium-webdriver": "^4.
|
|
33
|
+
"geckodriver": "3.1.0",
|
|
34
|
+
"got": "^12.5.3",
|
|
35
|
+
"jimp": "^0.22.4",
|
|
36
|
+
"selenium-webdriver": "^4.8.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"eslint": "^8.
|
|
39
|
+
"eslint": "^8.33.0",
|
|
40
40
|
"eslint-config-eyeo": "^3.2.0",
|
|
41
|
-
"expect": "^
|
|
42
|
-
"jsdoc": "^
|
|
43
|
-
"mocha": "^10.
|
|
41
|
+
"expect": "^29.4.2",
|
|
42
|
+
"jsdoc": "^4.0.0",
|
|
43
|
+
"mocha": "^10.2.0"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
46
46
|
"docs": "jsdoc --readme README.md --destination docs src/*.js",
|
package/src/browsers.js
CHANGED
|
@@ -25,7 +25,6 @@ import webdriver from "selenium-webdriver";
|
|
|
25
25
|
import chrome from "selenium-webdriver/chrome.js";
|
|
26
26
|
import firefox from "selenium-webdriver/firefox.js";
|
|
27
27
|
import edge from "selenium-webdriver/edge.js";
|
|
28
|
-
import command from "selenium-webdriver/lib/command.js";
|
|
29
28
|
import extractZip from "extract-zip";
|
|
30
29
|
|
|
31
30
|
import {download, extractTar, extractDmg, killDriverProcess, wait}
|
|
@@ -50,12 +49,19 @@ const BROWSER_DOWNLOAD_ERROR = "Browser download failed";
|
|
|
50
49
|
const BROWSER_NOT_INSTALLED_ERROR = "Browser is not installed";
|
|
51
50
|
const ELEMENT_NOT_FOUND_ERROR = "HTML element not found";
|
|
52
51
|
|
|
52
|
+
function getMajorVersion(versionNumber) {
|
|
53
|
+
let majorVersion = parseInt(versionNumber && versionNumber.split(".")[0], 10);
|
|
54
|
+
if (isNaN(majorVersion))
|
|
55
|
+
throw new Error(`${UNSUPPORTED_VERSION_ERROR}: ${versionNumber}`);
|
|
56
|
+
|
|
57
|
+
return majorVersion;
|
|
58
|
+
}
|
|
59
|
+
|
|
53
60
|
function checkVersion(version, minVersion, channels = []) {
|
|
54
61
|
if (channels.includes(version))
|
|
55
62
|
return;
|
|
56
63
|
|
|
57
|
-
|
|
58
|
-
if (isNaN(mainVersion) || mainVersion < minVersion)
|
|
64
|
+
if (getMajorVersion(version) < minVersion)
|
|
59
65
|
throw new Error(`${UNSUPPORTED_VERSION_ERROR}: ${version}`);
|
|
60
66
|
}
|
|
61
67
|
|
|
@@ -72,27 +78,28 @@ function checkPlatform() {
|
|
|
72
78
|
class Browser {
|
|
73
79
|
/**
|
|
74
80
|
* @typedef {Object} BrowserBinary
|
|
75
|
-
* @property {string} binary
|
|
76
|
-
* @property {string} versionNumber
|
|
77
|
-
* binary.
|
|
81
|
+
* @property {string} binary The path to the browser binary.
|
|
82
|
+
* @property {string} versionNumber The version number of the browser binary.
|
|
78
83
|
*/
|
|
79
84
|
|
|
80
85
|
/**
|
|
81
86
|
* Installs the browser. The installation process is detailed on the
|
|
82
87
|
* subclasses.
|
|
83
|
-
* @param {string} version
|
|
84
|
-
* Please find examples on the subclasses.
|
|
88
|
+
* @param {string} [version=latest] Either full version number or
|
|
89
|
+
* channel/release. Please find examples on the subclasses.
|
|
90
|
+
* @param {number} [downloadTimeout=0] Allowed time in ms for the download of
|
|
91
|
+
* install files to complete. When set to 0 there is no time limit.
|
|
85
92
|
* @return {BrowserBinary}
|
|
86
93
|
* @throws {Error} Unsupported browser version, Unsupported platform, Browser
|
|
87
94
|
* download failed.
|
|
88
95
|
*/
|
|
89
|
-
static async installBrowser(version) {
|
|
96
|
+
static async installBrowser(version, downloadTimeout = 0) {
|
|
90
97
|
// to be implemented by the subclass
|
|
91
98
|
}
|
|
92
99
|
|
|
93
100
|
/**
|
|
94
101
|
* Gets the installed version returned by the browser binary.
|
|
95
|
-
* @param {string} binary
|
|
102
|
+
* @param {string} binary The path to the browser binary.
|
|
96
103
|
* @return {string} Installed browser version.
|
|
97
104
|
*/
|
|
98
105
|
static async getInstalledVersion(binary) {
|
|
@@ -116,37 +123,41 @@ class Browser {
|
|
|
116
123
|
|
|
117
124
|
/**
|
|
118
125
|
* @typedef {Object} driverOptions
|
|
119
|
-
* @property {boolean} headless=true
|
|
120
|
-
* or not.
|
|
121
|
-
*
|
|
126
|
+
* @property {boolean} [headless=true] Run the browser in headless mode,
|
|
127
|
+
* or not. In Chromium >= 111, the
|
|
128
|
+
* {@link https://developer.chrome.com/articles/new-headless/ new headless mode}
|
|
129
|
+
* is used.
|
|
130
|
+
* @property {Array.<string>} [extensionPaths=[]] Loads extensions to the
|
|
122
131
|
* browser.
|
|
123
|
-
* @property {boolean} incognito=false
|
|
132
|
+
* @property {boolean} [incognito=false] Runs the browser in incognito mode,
|
|
124
133
|
* or not.
|
|
125
|
-
* @property {boolean} insecure=false
|
|
134
|
+
* @property {boolean} [insecure=false] Forces the browser to accept insecure
|
|
126
135
|
* certificates, or not.
|
|
127
|
-
* @property {Array.<string>} [extraArgs=[]]
|
|
136
|
+
* @property {Array.<string>} [extraArgs=[]] Additional arguments to start
|
|
128
137
|
* the browser with.
|
|
129
138
|
*/
|
|
130
139
|
|
|
131
140
|
/**
|
|
132
141
|
* Installs the webdriver matching the browser version and runs the
|
|
133
142
|
* browser. If needed, the browser binary is also installed.
|
|
134
|
-
* @param {string} version
|
|
135
|
-
* Please find examples on the subclasses.
|
|
136
|
-
* @param {driverOptions
|
|
143
|
+
* @param {string} [version=latest] Either full version number or
|
|
144
|
+
* channel/release. Please find examples on the subclasses.
|
|
145
|
+
* @param {driverOptions} [options={}] Options to start the browser with.
|
|
146
|
+
* @param {number} [downloadTimeout=0] Allowed time in ms for the download of
|
|
147
|
+
* browser install files to complete. When set to 0 there is no time limit.
|
|
137
148
|
* @return {webdriver}
|
|
138
149
|
* @throws {Error} Unsupported browser version, Unsupported platform, Browser
|
|
139
150
|
* download failed, Driver download failed, Unable to start driver.
|
|
140
151
|
*/
|
|
141
|
-
static async getDriver(version, options = {}) {
|
|
152
|
+
static async getDriver(version, options = {}, downloadTimeout = 0) {
|
|
142
153
|
// to be implemented by the subclass
|
|
143
154
|
}
|
|
144
155
|
|
|
145
156
|
/**
|
|
146
157
|
* By default, extensions are disabled in incognito mode. This function
|
|
147
158
|
* enables the extension when loaded in incognito.
|
|
148
|
-
* @param {webdriver} driver
|
|
149
|
-
* @param {string} extensionTitle
|
|
159
|
+
* @param {webdriver} driver The driver controlling the browser.
|
|
160
|
+
* @param {string} extensionTitle Title of the extension to be enabled.
|
|
150
161
|
* @return {webdriver}
|
|
151
162
|
* @throws {Error} Unsupported browser version, Extension not found, HTML
|
|
152
163
|
* element not found.
|
|
@@ -179,7 +190,7 @@ class Chromium extends Browser {
|
|
|
179
190
|
"win32-x64": "win64",
|
|
180
191
|
"linux-x64": "linux",
|
|
181
192
|
"darwin-x64": "mac",
|
|
182
|
-
"
|
|
193
|
+
"darwin-arm64": "mac_arm64"
|
|
183
194
|
}[platformArch];
|
|
184
195
|
let data = await got(`https://omahaproxy.appspot.com/all.json?os=${os}`).json();
|
|
185
196
|
let release = data[0].versions.find(ver => ver.channel == channel);
|
|
@@ -206,15 +217,17 @@ class Chromium extends Browser {
|
|
|
206
217
|
/**
|
|
207
218
|
* Installs the browser. The Chromium executable gets extracted in the
|
|
208
219
|
* {@link snapshotsBaseDir} folder, ready to go.
|
|
209
|
-
* @param {string} version
|
|
210
|
-
* number (i.e. "77.0.3865.0").
|
|
220
|
+
* @param {string} [version=latest] Either "latest", "beta", "dev" or a full
|
|
221
|
+
* version number (i.e. "77.0.3865.0").
|
|
222
|
+
* @param {number} [downloadTimeout=0] Allowed time in ms for the download of
|
|
223
|
+
* install files to complete. When set to 0 there is no time limit.
|
|
211
224
|
* @return {BrowserBinary}
|
|
212
225
|
* @throws {Error} Unsupported browser version, Unsupported platform, Browser
|
|
213
226
|
* download failed.
|
|
214
227
|
*/
|
|
215
|
-
static async installBrowser(version = "latest") {
|
|
228
|
+
static async installBrowser(version = "latest", downloadTimeout = 0) {
|
|
216
229
|
const MIN_VERSION = 75;
|
|
217
|
-
const MAX_VERSION_DECREMENTS =
|
|
230
|
+
const MAX_VERSION_DECREMENTS = 200;
|
|
218
231
|
|
|
219
232
|
checkVersion(version, MIN_VERSION, Chromium.#CHANNELS);
|
|
220
233
|
let versionNumber = await Chromium.#getVersionForChannel(version);
|
|
@@ -228,7 +241,7 @@ class Chromium extends Browser {
|
|
|
228
241
|
"win32-x64": ["Win_x64", "chrome-win.zip"],
|
|
229
242
|
"linux-x64": ["Linux_x64", "chrome-linux.zip"],
|
|
230
243
|
"darwin-x64": ["Mac", "chrome-mac.zip"],
|
|
231
|
-
"
|
|
244
|
+
"darwin-arm64": ["Mac_Arm", "chrome-mac.zip"]
|
|
232
245
|
}[platformArch];
|
|
233
246
|
let archive;
|
|
234
247
|
let browserDir;
|
|
@@ -248,23 +261,27 @@ class Chromium extends Browser {
|
|
|
248
261
|
await fs.promises.mkdir(path.dirname(browserDir), {recursive: true});
|
|
249
262
|
|
|
250
263
|
archive = path.join(snapshotsDir, "cache", `${base}-${fileName}`);
|
|
264
|
+
let url = `https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/${platformDir}%2F${base}%2F${fileName}?alt=media`;
|
|
251
265
|
try {
|
|
252
266
|
try {
|
|
253
267
|
await fs.promises.access(archive);
|
|
254
268
|
}
|
|
255
269
|
catch (e) {
|
|
256
|
-
await download(
|
|
257
|
-
`https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/${platformDir}%2F${base}%2F${fileName}?alt=media`,
|
|
258
|
-
archive);
|
|
270
|
+
await download(url, archive, downloadTimeout);
|
|
259
271
|
}
|
|
260
272
|
break;
|
|
261
273
|
}
|
|
262
|
-
catch (
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
274
|
+
catch (err) {
|
|
275
|
+
if (err.name == "HTTPError") {
|
|
276
|
+
// Chromium advises decrementing the branch_base_position when no
|
|
277
|
+
// matching build was found. See https://www.chromium.org/getting-involved/download-chromium
|
|
278
|
+
base--;
|
|
279
|
+
if (base <= startBase - MAX_VERSION_DECREMENTS)
|
|
280
|
+
throw new Error(`${BROWSER_DOWNLOAD_ERROR}: Chromium base ${startBase}`);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
throw new Error(`${BROWSER_DOWNLOAD_ERROR}: ${url}\n${err}`);
|
|
284
|
+
}
|
|
268
285
|
}
|
|
269
286
|
}
|
|
270
287
|
await extractZip(archive, {dir: browserDir});
|
|
@@ -302,10 +319,7 @@ class Chromium extends Browser {
|
|
|
302
319
|
|
|
303
320
|
await killDriverProcess("chromedriver");
|
|
304
321
|
let driverPath = path.join(cacheDir, zip.split(".")[0], driverBinary);
|
|
305
|
-
|
|
306
|
-
await fs.promises.rm(driverPath, {recursive: true});
|
|
307
|
-
}
|
|
308
|
-
catch (e) {} // file does not exist
|
|
322
|
+
await fs.promises.rm(driverPath, {force: true});
|
|
309
323
|
await extractZip(archive, {dir: cacheDir});
|
|
310
324
|
|
|
311
325
|
return driverPath;
|
|
@@ -315,15 +329,22 @@ class Chromium extends Browser {
|
|
|
315
329
|
static async getDriver(version = "latest", {
|
|
316
330
|
headless = true, extensionPaths = [], incognito = false, insecure = false,
|
|
317
331
|
extraArgs = []
|
|
318
|
-
} = {}) {
|
|
319
|
-
let {binary, versionNumber, base} =
|
|
332
|
+
} = {}, downloadTimeout = 0) {
|
|
333
|
+
let {binary, versionNumber, base} =
|
|
334
|
+
await Chromium.installBrowser(version, downloadTimeout);
|
|
320
335
|
let driverPath = await Chromium.#installDriver(base, versionNumber);
|
|
321
336
|
let serviceBuilder = new chrome.ServiceBuilder(driverPath);
|
|
322
337
|
let options = new chrome.Options().addArguments("no-sandbox", ...extraArgs);
|
|
323
338
|
if (extensionPaths.length > 0)
|
|
324
339
|
options.addArguments(`load-extension=${extensionPaths.join(",")}`);
|
|
325
|
-
if (headless)
|
|
326
|
-
|
|
340
|
+
if (headless) {
|
|
341
|
+
// New headless mode introduced in Chrome 111
|
|
342
|
+
// https://developer.chrome.com/articles/new-headless/
|
|
343
|
+
if (getMajorVersion(versionNumber) >= 111)
|
|
344
|
+
options.addArguments("headless=new");
|
|
345
|
+
else
|
|
346
|
+
options.headless();
|
|
347
|
+
}
|
|
327
348
|
if (insecure)
|
|
328
349
|
options.addArguments("ignore-certificate-errors");
|
|
329
350
|
if (incognito)
|
|
@@ -412,13 +433,15 @@ class Firefox extends Browser {
|
|
|
412
433
|
/**
|
|
413
434
|
* Installs the browser. The Firefox executable gets extracted in the
|
|
414
435
|
* {@link snapshotsBaseDir} folder, ready to go.
|
|
415
|
-
* @param {string} version
|
|
416
|
-
* number (i.e. "68.0").
|
|
436
|
+
* @param {string} [version=latest] Either "latest", "beta" or a full version
|
|
437
|
+
* number (i.e. "68.0").
|
|
438
|
+
* @param {number} [downloadTimeout=0] Allowed time in ms for the download of
|
|
439
|
+
* install files to complete. When set to 0 there is no time limit.
|
|
417
440
|
* @return {BrowserBinary}
|
|
418
441
|
* @throws {Error} Unsupported browser version, Unsupported platform, Browser
|
|
419
442
|
* download failed.
|
|
420
443
|
*/
|
|
421
|
-
static async installBrowser(version = "latest") {
|
|
444
|
+
static async installBrowser(version = "latest", downloadTimeout = 0) {
|
|
422
445
|
const MIN_VERSION = 60;
|
|
423
446
|
|
|
424
447
|
checkVersion(version, MIN_VERSION, Firefox.#CHANNELS);
|
|
@@ -450,7 +473,7 @@ class Firefox extends Browser {
|
|
|
450
473
|
catch (e) {
|
|
451
474
|
let url = `https://archive.mozilla.org/pub/firefox/releases/${versionNumber}/${buildPlatform}/en-US/${fileName}`;
|
|
452
475
|
try {
|
|
453
|
-
await download(url, archive);
|
|
476
|
+
await download(url, archive, downloadTimeout);
|
|
454
477
|
}
|
|
455
478
|
catch (err) {
|
|
456
479
|
throw new Error(`${BROWSER_DOWNLOAD_ERROR}: ${url}\n${err}`);
|
|
@@ -465,8 +488,8 @@ class Firefox extends Browser {
|
|
|
465
488
|
static async getDriver(version = "latest", {
|
|
466
489
|
headless = true, extensionPaths = [], incognito = false, insecure = false,
|
|
467
490
|
extraArgs = []
|
|
468
|
-
} = {}) {
|
|
469
|
-
let {binary} = await Firefox.installBrowser(version);
|
|
491
|
+
} = {}, downloadTimeout = 0) {
|
|
492
|
+
let {binary} = await Firefox.installBrowser(version, downloadTimeout);
|
|
470
493
|
|
|
471
494
|
let options = new firefox.Options();
|
|
472
495
|
if (headless)
|
|
@@ -498,11 +521,8 @@ class Firefox extends Browser {
|
|
|
498
521
|
}, 30000, `${DRIVER_START_ERROR}: geckodriver`, 1000);
|
|
499
522
|
|
|
500
523
|
for (let extensionPath of extensionPaths) {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
.setParameter("path", extensionPath)
|
|
504
|
-
.setParameter("temporary", true)
|
|
505
|
-
);
|
|
524
|
+
let temporary = true; // Parameter not documented on the webdriver docs
|
|
525
|
+
await driver.installAddon(extensionPath, temporary);
|
|
506
526
|
}
|
|
507
527
|
return driver;
|
|
508
528
|
}
|
|
@@ -551,8 +571,7 @@ class Edge extends Browser {
|
|
|
551
571
|
versionNumbers.push(matches[2]);
|
|
552
572
|
|
|
553
573
|
let compareVersions = (v1, v2) =>
|
|
554
|
-
|
|
555
|
-
1 : -1;
|
|
574
|
+
getMajorVersion(v1) < getMajorVersion(v2) ? 1 : -1;
|
|
556
575
|
versionNumber = versionNumbers.sort(compareVersions)[0];
|
|
557
576
|
}
|
|
558
577
|
else {
|
|
@@ -599,13 +618,15 @@ class Edge extends Browser {
|
|
|
599
618
|
* Installs the browser. On Linux, Edge is installed as a system package,
|
|
600
619
|
* which requires root permissions. On MacOS, Edge is installed as a user
|
|
601
620
|
* app (not as a system app). Installing Edge on Windows is not supported.
|
|
602
|
-
* @param {string} version
|
|
603
|
-
* number (i.e. "95.0.1020.40").
|
|
621
|
+
* @param {string} [version=latest] Either "latest", "beta", "dev" or a full
|
|
622
|
+
* version number (i.e. "95.0.1020.40").
|
|
623
|
+
* @param {number} [downloadTimeout=0] Allowed time in ms for the download of
|
|
624
|
+
* install files to complete. When set to 0 there is no time limit.
|
|
604
625
|
* @return {BrowserBinary}
|
|
605
626
|
* @throws {Error} Unsupported browser version, Unsupported platform, Browser
|
|
606
627
|
* download failed.
|
|
607
628
|
*/
|
|
608
|
-
static async installBrowser(version = "latest") {
|
|
629
|
+
static async installBrowser(version = "latest", downloadTimeout = 0) {
|
|
609
630
|
if (platform == "win32")
|
|
610
631
|
// Edge is mandatory on Windows, can't be uninstalled or downgraded
|
|
611
632
|
// https://support.microsoft.com/en-us/microsoft-edge/why-can-t-i-uninstall-microsoft-edge-ee150b3b-7d7a-9984-6d83-eb36683d526d
|
|
@@ -642,7 +663,7 @@ class Edge extends Browser {
|
|
|
642
663
|
catch (e) {}
|
|
643
664
|
|
|
644
665
|
try {
|
|
645
|
-
await download(url, archive);
|
|
666
|
+
await download(url, archive, downloadTimeout);
|
|
646
667
|
}
|
|
647
668
|
catch (err) {
|
|
648
669
|
throw new Error(`${BROWSER_DOWNLOAD_ERROR}: ${url}\n${err}`);
|
|
@@ -653,10 +674,7 @@ class Edge extends Browser {
|
|
|
653
674
|
}
|
|
654
675
|
else if (platform == "darwin") {
|
|
655
676
|
let appName = Edge.#getDarwinAppName(channel);
|
|
656
|
-
|
|
657
|
-
await fs.promises.rm(`${process.env.HOME}/Applications/${appName}.app`, {recursive: true});
|
|
658
|
-
}
|
|
659
|
-
catch (e) {}
|
|
677
|
+
await fs.promises.rm(`${process.env.HOME}/Applications/${appName}.app`, {force: true, recursive: true});
|
|
660
678
|
await promisify(exec)(`installer -pkg ${archive} -target CurrentUserHomeDirectory`);
|
|
661
679
|
}
|
|
662
680
|
|
|
@@ -673,10 +691,7 @@ class Edge extends Browser {
|
|
|
673
691
|
static async #installDriver() {
|
|
674
692
|
async function extractEdgeZip(archive, cacheDir, driverPath) {
|
|
675
693
|
await killDriverProcess("msedgedriver");
|
|
676
|
-
|
|
677
|
-
await fs.promises.rm(driverPath, {recursive: true});
|
|
678
|
-
}
|
|
679
|
-
catch (e) {} // file does not exist
|
|
694
|
+
await fs.promises.rm(driverPath, {force: true});
|
|
680
695
|
await extractZip(archive, {dir: cacheDir});
|
|
681
696
|
}
|
|
682
697
|
|
|
@@ -727,9 +742,9 @@ class Edge extends Browser {
|
|
|
727
742
|
static async getDriver(version = "latest", {
|
|
728
743
|
headless = true, extensionPaths = [], incognito = false, insecure = false,
|
|
729
744
|
extraArgs = []
|
|
730
|
-
} = {}) {
|
|
745
|
+
} = {}, downloadTimeout = 0) {
|
|
731
746
|
if (platform == "linux" || platform == "darwin")
|
|
732
|
-
await Edge.installBrowser(version);
|
|
747
|
+
await Edge.installBrowser(version, downloadTimeout);
|
|
733
748
|
|
|
734
749
|
let driverPath = await Edge.#installDriver();
|
|
735
750
|
let serviceBuilder = new edge.ServiceBuilder(driverPath);
|
|
@@ -749,7 +764,22 @@ class Edge extends Browser {
|
|
|
749
764
|
builder.setEdgeOptions(options);
|
|
750
765
|
builder.setEdgeService(serviceBuilder);
|
|
751
766
|
|
|
752
|
-
|
|
767
|
+
let driver;
|
|
768
|
+
// On Windows CI, occasionally a SessionNotCreatedError is thrown, likely
|
|
769
|
+
// due to low OS resources, that's why building the driver is retried
|
|
770
|
+
await wait(async() => {
|
|
771
|
+
try {
|
|
772
|
+
driver = await builder.build();
|
|
773
|
+
return true;
|
|
774
|
+
}
|
|
775
|
+
catch (err) {
|
|
776
|
+
if (err.name != "SessionNotCreatedError")
|
|
777
|
+
throw err;
|
|
778
|
+
await killDriverProcess("msedgedriver");
|
|
779
|
+
}
|
|
780
|
+
}, 30000, `${DRIVER_START_ERROR}: msedgedriver`, 1000);
|
|
781
|
+
|
|
782
|
+
return driver;
|
|
753
783
|
}
|
|
754
784
|
|
|
755
785
|
/** @see Browser.enableExtensionInIncognito */
|
|
@@ -776,11 +806,10 @@ class Edge extends Browser {
|
|
|
776
806
|
|
|
777
807
|
/**
|
|
778
808
|
* @type {Object}
|
|
779
|
-
* @property {Chromium} chromium
|
|
809
|
+
* @property {Chromium} chromium Browser and webdriver functionality for
|
|
780
810
|
* Chromium.
|
|
781
|
-
* @property {Firefox} firefox
|
|
782
|
-
*
|
|
783
|
-
* @property {Edge} edge - Browser and webdriver functionality for Edge.
|
|
811
|
+
* @property {Firefox} firefox Browser and webdriver functionality for Firefox.
|
|
812
|
+
* @property {Edge} edge Browser and webdriver functionality for Edge.
|
|
784
813
|
*/
|
|
785
814
|
export const BROWSERS = {
|
|
786
815
|
chromium: Chromium,
|
package/src/utils.js
CHANGED
|
@@ -25,31 +25,43 @@ import got from "got";
|
|
|
25
25
|
import dmg from "dmg";
|
|
26
26
|
import Jimp from "jimp";
|
|
27
27
|
|
|
28
|
+
|
|
28
29
|
/**
|
|
29
30
|
* Downloads url resources.
|
|
30
|
-
* @param {string} url
|
|
31
|
-
* @param {string} destFile
|
|
32
|
-
* @
|
|
31
|
+
* @param {string} url The url of the resource to be downloaded.
|
|
32
|
+
* @param {string} destFile The destination file path.
|
|
33
|
+
* @param {number} [timeout=0] Allowed time in ms for the download to complete.
|
|
34
|
+
* When set to 0 there is no time limit.
|
|
35
|
+
* @throws {TypeError} Invalid URL, Download timeout.
|
|
33
36
|
*/
|
|
34
|
-
export async function download(url, destFile) {
|
|
37
|
+
export async function download(url, destFile, timeout = 0) {
|
|
35
38
|
let cacheDir = path.dirname(destFile);
|
|
36
|
-
|
|
37
39
|
await fs.promises.mkdir(cacheDir, {recursive: true});
|
|
38
40
|
|
|
39
41
|
let tempDest = `${destFile}-${process.pid}`;
|
|
40
42
|
let writable = fs.createWriteStream(tempDest);
|
|
41
|
-
|
|
43
|
+
let timeoutID;
|
|
42
44
|
try {
|
|
43
|
-
|
|
45
|
+
let downloading = promisify(pipeline)(got.stream(url), writable);
|
|
46
|
+
if (timeout == 0) {
|
|
47
|
+
await downloading;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
let timeoutPromise = new Promise((resolve, reject) => {
|
|
51
|
+
timeoutID = setTimeout(
|
|
52
|
+
() => reject(`Download timeout after ${timeout}ms`), timeout);
|
|
53
|
+
});
|
|
54
|
+
await Promise.race([downloading, timeoutPromise]);
|
|
55
|
+
}
|
|
44
56
|
}
|
|
45
57
|
catch (error) {
|
|
46
|
-
|
|
47
|
-
await fs.promises.rm(tempDest, {recursive: true});
|
|
48
|
-
}
|
|
49
|
-
catch (e) {}
|
|
50
|
-
|
|
58
|
+
await fs.promises.rm(tempDest, {force: true});
|
|
51
59
|
throw error;
|
|
52
60
|
}
|
|
61
|
+
finally {
|
|
62
|
+
if (timeoutID)
|
|
63
|
+
clearTimeout(timeoutID);
|
|
64
|
+
}
|
|
53
65
|
|
|
54
66
|
await fs.promises.rename(tempDest, destFile);
|
|
55
67
|
}
|
|
@@ -152,9 +164,9 @@ export function wait(condition, timeout = 0, message, pollTimeout = 100) {
|
|
|
152
164
|
|
|
153
165
|
/**
|
|
154
166
|
* Takes a screenshot of the full page by scrolling from top to bottom.
|
|
155
|
-
* @param {webdriver} driver
|
|
156
|
-
* @
|
|
157
|
-
*
|
|
167
|
+
* @param {webdriver} driver The driver controlling the browser.
|
|
168
|
+
* @param {boolean} [hideScrollbars=true] Hides any scrollbars before taking
|
|
169
|
+
* the screenshot, or not.
|
|
158
170
|
* @return {Jimp} A Jimp image object containing the screenshot.
|
|
159
171
|
* @example
|
|
160
172
|
* // Getting a base-64 encoded PNG from the returned Jimp image
|
package/test/browsers.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import fs from "fs";
|
|
19
|
-
import expect from "expect";
|
|
19
|
+
import {expect} from "expect";
|
|
20
20
|
import path from "path";
|
|
21
21
|
|
|
22
22
|
import {BROWSERS, snapshotsBaseDir, takeFullPageScreenshot} from "../index.js";
|
|
@@ -104,21 +104,47 @@ function getExtension(browser, version) {
|
|
|
104
104
|
return {extensionPaths, manifest};
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
async function getCachedTimes(browser) {
|
|
108
|
+
let cacheDir = path.join(snapshotsBaseDir, browser, "cache");
|
|
109
|
+
let cacheFiles = await fs.promises.readdir(cacheDir);
|
|
110
|
+
|
|
111
|
+
let browserCacheTime = null;
|
|
112
|
+
// Edge gets installed at OS level, that's why it's not tested here
|
|
113
|
+
if (browser != "edge") {
|
|
114
|
+
let installTypes = [".zip", ".dmg", ".bz2"];
|
|
115
|
+
let browserZip =
|
|
116
|
+
cacheFiles.find(elem => installTypes.some(type => elem.includes(type)));
|
|
117
|
+
let browserStat = await fs.promises.stat(path.join(cacheDir, browserZip));
|
|
118
|
+
browserCacheTime = browserStat.ctimeMs;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let driverCacheTime = null;
|
|
122
|
+
// Firefox install file includes the driver, that's why it's not tested here
|
|
123
|
+
if (browser != "firefox") {
|
|
124
|
+
let driverDir = cacheFiles.find(elem => !elem.endsWith(".zip"));
|
|
125
|
+
let driverFiles = await fs.promises.readdir(path.join(cacheDir, driverDir));
|
|
126
|
+
let driverZip = driverFiles.find(elem => elem.endsWith(".zip"));
|
|
127
|
+
let driverStat =
|
|
128
|
+
await fs.promises.stat(path.join(cacheDir, driverDir, driverZip));
|
|
129
|
+
driverCacheTime = driverStat.ctimeMs;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {browserCTime: browserCacheTime, driverCTime: driverCacheTime};
|
|
133
|
+
}
|
|
134
|
+
|
|
107
135
|
for (let browser of Object.keys(BROWSERS)) {
|
|
108
136
|
describe(`Browser: ${browser}`, () => {
|
|
109
137
|
before(async() => {
|
|
110
138
|
if (process.env.TEST_KEEP_SNAPSHOTS == "true")
|
|
111
139
|
return;
|
|
112
140
|
|
|
113
|
-
|
|
114
|
-
await fs.promises.rm(snapshotsBaseDir, {recursive: true});
|
|
115
|
-
}
|
|
116
|
-
catch (e) {}
|
|
141
|
+
await fs.promises.rm(snapshotsBaseDir, {force: true, recursive: true});
|
|
117
142
|
});
|
|
118
143
|
|
|
119
144
|
for (let version of VERSIONS[browser]) {
|
|
120
145
|
describe(`Version: ${version}`, () => {
|
|
121
146
|
let driver = null;
|
|
147
|
+
let emptyCacheTimes = null;
|
|
122
148
|
|
|
123
149
|
async function quitDriver() {
|
|
124
150
|
if (!driver)
|
|
@@ -140,7 +166,7 @@ for (let browser of Object.keys(BROWSERS)) {
|
|
|
140
166
|
this.skip();
|
|
141
167
|
|
|
142
168
|
let {binary, versionNumber} =
|
|
143
|
-
await BROWSERS[browser].installBrowser(version);
|
|
169
|
+
await BROWSERS[browser].installBrowser(version, 60000);
|
|
144
170
|
let browserName = browser == "edge" ? /(edge|Edge)/ : browser;
|
|
145
171
|
expect(binary).toEqual(expect.stringMatching(browserName));
|
|
146
172
|
|
|
@@ -165,6 +191,23 @@ for (let browser of Object.keys(BROWSERS)) {
|
|
|
165
191
|
|
|
166
192
|
expect((await driver.getCapabilities()).getBrowserName())
|
|
167
193
|
.toEqual(expect.stringMatching(names[browser]));
|
|
194
|
+
|
|
195
|
+
// When running all tests, this saves time on the cache test
|
|
196
|
+
emptyCacheTimes = await getCachedTimes(browser);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("uses cached install files", async() => {
|
|
200
|
+
if (emptyCacheTimes == null) { // Single test case run
|
|
201
|
+
driver = await BROWSERS[browser].getDriver(version);
|
|
202
|
+
emptyCacheTimes = await getCachedTimes(browser);
|
|
203
|
+
await quitDriver();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// assigning `driver` to allow the afterEach hook quit the driver
|
|
207
|
+
driver = await BROWSERS[browser].getDriver(version);
|
|
208
|
+
let existingCacheTimes = await getCachedTimes(browser);
|
|
209
|
+
|
|
210
|
+
expect(existingCacheTimes).toEqual(emptyCacheTimes);
|
|
168
211
|
});
|
|
169
212
|
|
|
170
213
|
it("supports extra args", async() => {
|
|
@@ -199,7 +242,9 @@ for (let browser of Object.keys(BROWSERS)) {
|
|
|
199
242
|
});
|
|
200
243
|
|
|
201
244
|
it("loads an extension", async() => {
|
|
202
|
-
|
|
245
|
+
// Chromium's old headless mode doesn't support loading extensions
|
|
246
|
+
let headless = browser == "firefox" || (browser == "chromium" &&
|
|
247
|
+
["latest", "beta", "dev"].includes(version));
|
|
203
248
|
let {extensionPaths} = getExtension(browser, version);
|
|
204
249
|
|
|
205
250
|
driver = await BROWSERS[browser].getDriver(
|
package/test/utils.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import fs from "fs";
|
|
19
|
-
import expect from "expect";
|
|
19
|
+
import {expect} from "expect";
|
|
20
20
|
import path from "path";
|
|
21
21
|
|
|
22
22
|
import {snapshotsBaseDir, download} from "../index.js";
|
|
@@ -25,18 +25,34 @@ describe("Utils", () => {
|
|
|
25
25
|
it("defines a browser snapshots folder", () => expect(snapshotsBaseDir)
|
|
26
26
|
.toBe(path.join(process.cwd(), "browser-snapshots")));
|
|
27
27
|
|
|
28
|
+
let resourceUrl = "https://gitlab.com/eyeo/developer-experience/get-browser-binary/-/raw/main/package.json";
|
|
28
29
|
let destFile = path.join(snapshotsBaseDir, "download-test.txt");
|
|
30
|
+
let expectedFileContents = "\"name\": \"@eyeo/get-browser-binary\"";
|
|
29
31
|
|
|
30
32
|
it("downloads resources", async() => {
|
|
31
|
-
|
|
32
|
-
await download(
|
|
33
|
+
await fs.promises.rm(destFile, {force: true});
|
|
34
|
+
await download(resourceUrl, destFile);
|
|
33
35
|
let contents = await fs.promises.readFile(destFile, {encoding: "utf8"});
|
|
34
|
-
expect(contents).toEqual(
|
|
35
|
-
expect.stringContaining("\"name\": \"@eyeo/get-browser-binary\""));
|
|
36
|
+
expect(contents).toEqual(expect.stringContaining(expectedFileContents));
|
|
36
37
|
});
|
|
37
38
|
|
|
38
|
-
it("
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
it("downloads resources on long timeout", async() => {
|
|
40
|
+
await fs.promises.rm(destFile, {force: true});
|
|
41
|
+
await download(resourceUrl, destFile, 10000);
|
|
42
|
+
let contents = await fs.promises.readFile(destFile, {encoding: "utf8"});
|
|
43
|
+
expect(contents).toEqual(expect.stringContaining(expectedFileContents));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("does not download on short timeout", async() => {
|
|
47
|
+
try {
|
|
48
|
+
await download(resourceUrl, destFile, 10);
|
|
49
|
+
throw new Error("Download timeout didn't throw");
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
expect(err).toBe("Download timeout after 10ms");
|
|
53
|
+
}
|
|
41
54
|
});
|
|
55
|
+
|
|
56
|
+
it("does not download invalid resources", () =>
|
|
57
|
+
expect(download("invalid", destFile)).rejects.toThrow("Invalid URL"));
|
|
42
58
|
});
|