@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 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=256m browsers
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
@@ -1,3 +1,3 @@
1
1
  {
2
- "timeout": 50000
2
+ "timeout": 80000
3
3
  }
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=256m -it browsers
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=256m -e TEST_ARGS="--grep chromium.*latest --timeout 100000" -it browsers
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.8.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": "^3.0.2",
34
- "got": "^11.8.2",
35
- "jimp": "^0.16.2",
36
- "selenium-webdriver": "^4.7.1"
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.17.0",
39
+ "eslint": "^8.33.0",
40
40
  "eslint-config-eyeo": "^3.2.0",
41
- "expect": "^25.5.0",
42
- "jsdoc": "^3.6.10",
43
- "mocha": "^10.0.0"
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
- let mainVersion = parseInt(version && version.split(".")[0], 10);
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 - The path to the browser binary.
76
- * @property {string} versionNumber - The version number of the browser
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 - Either full version number or channel/release.
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 - The path to the browser 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 - Run the browser in headless mode,
120
- * or not.
121
- * @property {Array.<string>} [extensionPaths=[]] - Loads extensions to the
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 - Runs the browser in incognito mode,
132
+ * @property {boolean} [incognito=false] Runs the browser in incognito mode,
124
133
  * or not.
125
- * @property {boolean} insecure=false - Forces the browser to accept insecure
134
+ * @property {boolean} [insecure=false] Forces the browser to accept insecure
126
135
  * certificates, or not.
127
- * @property {Array.<string>} [extraArgs=[]] - Additional arguments to start
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 - Either full version number or channel/release.
135
- * Please find examples on the subclasses.
136
- * @param {driverOptions?} options - Options to start the browser with.
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 - The driver controlling the browser.
149
- * @param {string} extensionTitle - Title of the extebsion to be enabled.
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
- "dawrin-arm64": "mac_arm64"
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 - Either "latest", "beta", "dev" or a full version
210
- * number (i.e. "77.0.3865.0"). Defaults to "latest".
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 = 80;
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
- "dawrin-arm64": ["Mac_Arm", "chrome-mac.zip"]
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 (e) {
263
- // Chromium advises decrementing the branch_base_position when no
264
- // matching build was found. See https://www.chromium.org/getting-involved/download-chromium
265
- base--;
266
- if (base <= startBase - MAX_VERSION_DECREMENTS)
267
- throw new Error(`${BROWSER_DOWNLOAD_ERROR}: Chromium base ${startBase}`);
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
- try {
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} = await Chromium.installBrowser(version);
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
- options.headless();
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 - Either "latest", "beta" or a full version
416
- * number (i.e. "68.0"). Defaults to "latest".
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
- await driver.execute(
502
- new command.Command("install addon")
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
- parseInt(v1.split(".")[0], 10) < parseInt(v2.split(".")[0], 10) ?
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 - Either "latest", "beta", "dev" or a full version
603
- * number (i.e. "95.0.1020.40"). Defaults to "latest".
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
- try {
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
- try {
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
- return builder.build();
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 - Browser and webdriver functionality for
809
+ * @property {Chromium} chromium Browser and webdriver functionality for
780
810
  * Chromium.
781
- * @property {Firefox} firefox - Browser and webdriver functionality for
782
- * Firefox.
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 - The url of the resource to be downloaded.
31
- * @param {string} destFile - The destination file path.
32
- * @throws {TypeError} Invalid URL.
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
- await promisify(pipeline)(got.stream(url), writable);
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
- try {
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 - The driver controlling the browser.
156
- * @property {boolean} hideScrollbars=true - Hides any scrollbars before
157
- * taking the screenshot, or not.
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
- try {
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
- let headless = browser == "firefox";
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
- let url = "https://gitlab.com/eyeo/developer-experience/get-browser-binary/-/raw/main/package.json";
32
- await download(url, destFile);
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("does not download invalid resources", async() => {
39
- expect(download("invalid", destFile)).rejects
40
- .toThrow("TypeError [ERR_INVALID_URL]: Invalid URL");
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
  });