@eyeo/get-browser-binary 0.4.0 → 0.6.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/.eslintrc.json CHANGED
@@ -5,7 +5,8 @@
5
5
  "root": true,
6
6
  "env": {
7
7
  "es2022": true,
8
- "node": true
8
+ "node": true,
9
+ "browser": true
9
10
  },
10
11
  "rules": {
11
12
  "brace-style": ["error", "stroustrup"],
package/.gitlab-ci.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  default:
2
- image: node:16-bullseye-slim
2
+ image: registry.gitlab.com/eyeo/docker/get-browser-binary:node16
3
3
  interruptible: true
4
4
 
5
5
  stages:
@@ -37,7 +37,7 @@ test:browsers:linux:
37
37
  before_script:
38
38
  - docker build -f test/docker/Dockerfile -t browsers .
39
39
  script:
40
- - docker run browsers
40
+ - docker run --shm-size=256m browsers
41
41
 
42
42
  test:browsers:windows:
43
43
  stage: test
@@ -51,17 +51,14 @@ test:browsers:windows:
51
51
  - Start-Process msiexec
52
52
  -ArgumentList "/i MicrosoftEdgeEnterpriseX64.msi /norestart /qn" -Wait
53
53
  - choco upgrade -y nodejs --version 16.10.0
54
- - npm install -g npm
55
54
  - npm install
56
55
  script:
57
- # Running only a subset of Firefox and Opera tests to avoid low OS resources error
56
+ # Running only a subset of Firefox tests to avoid low OS resources error
58
57
  # https://gitlab.com/eyeo/developer-experience/get-browser-binary/-/issues/2
59
- # Regarding grep, running npm v8 on powershell has issues when the grep
60
- # value contains the pipe (|) literal. Storing that string as a verbatim
61
- # string (single quotes) and then sorrounding it with four double quotes
62
- # does the trick.
63
- - $subset_tests = '(firefox|opera).*latest.*downloads'
64
- - npm test -- --grep """"$subset_tests""""
58
+ - npm test -- --grep "firefox.*latest.*downloads"
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.
65
62
  - $full_tests = '(chromium|edge.*latest)'
66
63
  - npm test -- --grep """"$full_tests""""
67
64
  tags:
package/README.md CHANGED
@@ -1,12 +1,7 @@
1
1
  # get-browser-binary
2
2
 
3
- Download specific browser versions for Chromium, Firefox, Edge and Opera, and
4
- their matching [selenium webdriver](https://www.selenium.dev/selenium/docs/api/javascript/index.html).
5
-
6
- Note: Edge download is only supported on Linux. On other platforms it is assumed
7
- to be already installed.
8
-
9
- Note: Using the `operadriver` on Windows is not supported.
3
+ Download specific browser versions for Chromium, Firefox and Edge, and their
4
+ matching [selenium webdriver](https://www.selenium.dev/selenium/docs/api/javascript/index.html).
10
5
 
11
6
  ## Getting started
12
7
 
@@ -17,12 +12,10 @@ selenium webdriver:
17
12
  import {BROWSERS} from "@eyeo/get-browser-binary";
18
13
 
19
14
  (async function example() {
20
- let {chromium} = BROWSERS;
21
-
22
- let {binary} = await chromium.downloadBinary();
15
+ let {binary} = await BROWSERS.chromium.downloadBinary("latest");
23
16
  console.log(`Chromium binary downloaded to ${binary}`);
24
17
 
25
- let driver = await chromium.getDriver();
18
+ let driver = await BROWSERS.chromium.getDriver("latest");
26
19
  await driver.navigate().to("https://example.com/");
27
20
  await driver.quit();
28
21
  })();
@@ -35,6 +28,15 @@ For more information, please refer to the [API documention](https://gitlab.com/e
35
28
  If you are already on the documentation page, you may find the API contents on
36
29
  the right side.
37
30
 
31
+ ### Supported browser versions
32
+
33
+ - Chromium >= 75
34
+ - Firefox >= 60
35
+ - Edge >= 95
36
+
37
+ Note: Edge download is not supported on Windows. It is assumed to be installed
38
+ because it is the default browser on that platform.
39
+
38
40
  ## Development
39
41
 
40
42
  ### Prerequisites
@@ -79,18 +81,15 @@ Useful to reproduce the CI environment of the `test:browsers:linux` job:
79
81
 
80
82
  ```shell
81
83
  docker build -f test/docker/Dockerfile -t browsers .
82
- docker run -it browsers
84
+ docker run --shm-size=256m -it browsers
83
85
  ```
84
86
 
85
87
  The `grep` option can also be used on Docker via the `TEST_ARGS` parameter:
86
88
 
87
89
  ```shell
88
- docker run -e TEST_ARGS="--grep chromium.*latest" -it browsers
90
+ docker run --shm-size=256m -e TEST_ARGS="--grep chromium.*latest" -it browsers
89
91
  ```
90
92
 
91
- Note: For a full automated run, `opera` tests should not use the interactive
92
- tty `-it` flag.
93
-
94
93
  ## Building the documentation
95
94
 
96
95
  ```shell
package/RELEASE_NOTES.md CHANGED
@@ -1,3 +1,18 @@
1
+ # 0.6.0
2
+
3
+ - Removes Opera support (#26)
4
+ - Improves error handling when the downloaded browser version doesn't match
5
+ known channels (#25)
6
+ - Fixes an Edge running issue on Windows (32-bit) (#23)
7
+
8
+ # 0.5.0
9
+
10
+ - Enables Edge binary downloads on MacOS (!25)
11
+ - Sets unsupported browser versions (#16)
12
+ - Fixes the Edge repo url (!27)
13
+ - Fixes an issue when downloading Opera (#21)
14
+ - Fixes redundant Opera installations on Linux (!24)
15
+
1
16
  # 0.4.0
2
17
 
3
18
  - Adds Opera as a supported browser (#7)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eyeo/get-browser-binary",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Download browser binaries and matching webdrivers",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,11 +18,9 @@
18
18
  "chromedriver": "^90.0.1",
19
19
  "dmg": "^0.1.0",
20
20
  "extract-zip": "^2.0.1",
21
- "fs-extra": "^10.0.0",
22
21
  "geckodriver": "^3.0.2",
23
22
  "got": "^11.8.2",
24
- "msedgedriver": "^91.0.0",
25
- "selenium-webdriver": "^4.2.0"
23
+ "selenium-webdriver": "^4.6.1"
26
24
  },
27
25
  "devDependencies": {
28
26
  "eslint": "^8.17.0",
package/src/browsers.js CHANGED
@@ -16,15 +16,15 @@
16
16
  */
17
17
 
18
18
  import path from "path";
19
- import {exec, execFile, spawn} from "child_process";
19
+ import {exec, execFile} from "child_process";
20
20
  import {promisify} from "util";
21
21
  import fs from "fs";
22
- import fsExtra from "fs-extra";
23
22
 
24
23
  import got from "got";
25
24
  import webdriver from "selenium-webdriver";
26
25
  import chrome from "selenium-webdriver/chrome.js";
27
26
  import firefox from "selenium-webdriver/firefox.js";
27
+ import edge from "selenium-webdriver/edge.js";
28
28
  import command from "selenium-webdriver/lib/command.js";
29
29
  import extractZip from "extract-zip";
30
30
 
@@ -38,10 +38,17 @@ import {download, extractTar, extractDmg, getBrowserVersion, killDriverProcess,
38
38
  export let snapshotsBaseDir = path.join(process.cwd(), "browser-snapshots");
39
39
 
40
40
  let {until, By} = webdriver;
41
- const ERROR_DOWNLOAD_NOT_SUPPORTED =
42
- "Downloading this browser is not supported";
43
41
  let platform = `${process.platform}-${process.arch}`;
44
42
 
43
+ function checkVersion(version, minVersion, channels) {
44
+ if (channels.includes(version))
45
+ return;
46
+
47
+ let mainVersion = parseInt(version && version.split(".")[0], 10);
48
+ if (isNaN(mainVersion) || mainVersion < minVersion)
49
+ throw new Error(`Unsupported browser version: ${version}`);
50
+ }
51
+
45
52
  /**
46
53
  * Base class for browser download functionality. Please see subclasses for
47
54
  * browser specific details.
@@ -60,9 +67,10 @@ class Browser {
60
67
  * @param {string} version - Either full version number or channel/release.
61
68
  * Please find examples on the subclasses.
62
69
  * @return {BrowserBinary}
70
+ * @throws {Error} Unsupported browser version.
63
71
  */
64
72
  static async downloadBinary(version) {
65
- throw new Error(ERROR_DOWNLOAD_NOT_SUPPORTED);
73
+ // to be implemented by the subclass
66
74
  }
67
75
 
68
76
  /**
@@ -81,7 +89,7 @@ class Browser {
81
89
  else {
82
90
  ({stdout} = await promisify(execFile)(binary, ["--version"]));
83
91
  }
84
- return stdout;
92
+ return stdout.trim();
85
93
  }
86
94
 
87
95
  /**
@@ -107,11 +115,10 @@ class Browser {
107
115
  * Installs the webdriver matching the browser version and runs the
108
116
  * browser. If needed, the browser binary is also installed.
109
117
  * @param {string} version - Either full version number or channel/release.
110
- * Please find examples on the subclasses. On Edge this parameter has
111
- * no effect.
118
+ * Please find examples on the subclasses.
112
119
  * @param {driverOptions?} options - Options to start the browser with.
113
120
  * @return {webdriver}
114
- * @throws {Error} Unsupported webdriver version.
121
+ * @throws {Error} Unsupported browser version.
115
122
  */
116
123
  static async getDriver(version, options = {}) {
117
124
  // to be implemented by the subclass
@@ -123,6 +130,7 @@ class Browser {
123
130
  * @param {webdriver} driver - The driver controlling the browser.
124
131
  * @param {string} extensionTitle - Title of the extebsion to be enabled.
125
132
  * @return {webdriver}
133
+ * @throws {Error} Unsupported browser version.
126
134
  */
127
135
  static async enableExtensionInIncognito(driver, extensionTitle) {
128
136
  // Allowing the extension in incognito mode can't happen programmatically:
@@ -145,28 +153,30 @@ class Chromium extends Browser {
145
153
  return data.chromium_base_position;
146
154
  }
147
155
 
148
- static async #getLatestVersion(channel = "stable") {
149
- let os = process.platform;
150
- if (os == "win32")
151
- os = process.arch == "x64" ? "win64" : "win";
152
- else if (os == "darwin")
153
- os = process.arch == "arm64" ? "mac_arm64" : "mac";
156
+ static async #getVersionForChannel(channel) {
157
+ if (channel == "latest")
158
+ channel = "stable";
154
159
 
160
+ let os = {
161
+ "win32-ia32": "win",
162
+ "win32-x64": "win64",
163
+ "linux-x64": "linux",
164
+ "darwin-x64": "mac",
165
+ "dawrin-arm64": "mac_arm64"
166
+ }[platform];
155
167
  let data = await got(`https://omahaproxy.appspot.com/all.json?os=${os}`).json();
156
168
  let release = data[0].versions.find(ver => ver.channel == channel);
157
- let {current_version: version, branch_base_position: base} = release;
169
+ let {current_version: version} = release;
158
170
 
159
- if (release.true_branch && release.true_branch.includes("_")) {
171
+ if (release.true_branch && release.true_branch.includes("_"))
160
172
  // A wrong base may be caused by a mini-branch (patched) release
161
173
  // In that case, the base is taken from the unpatched version
162
174
  version = [...version.split(".").slice(0, 3), "0"].join(".");
163
- base = await Chromium.#getBranchBasePosition(version);
164
- }
165
175
 
166
- return {version, base};
176
+ return version;
167
177
  }
168
178
 
169
- static #getChromiumBinary(dir) {
179
+ static #getBinaryPath(dir) {
170
180
  switch (process.platform) {
171
181
  case "win32":
172
182
  return path.join(dir, "chrome-win", "chrome.exe");
@@ -201,7 +211,7 @@ class Chromium extends Browser {
201
211
 
202
212
  try {
203
213
  await fs.promises.access(browserDir);
204
- return {binary: Chromium.#getChromiumBinary(browserDir), revision};
214
+ return {binary: Chromium.#getBinaryPath(browserDir), revision};
205
215
  }
206
216
  catch (e) {}
207
217
 
@@ -230,7 +240,7 @@ class Chromium extends Browser {
230
240
  }
231
241
 
232
242
  await extractZip(archive, {dir: browserDir});
233
- return {binary: Chromium.#getChromiumBinary(browserDir), revision};
243
+ return {binary: Chromium.#getBinaryPath(browserDir), revision};
234
244
  }
235
245
 
236
246
  /**
@@ -239,15 +249,19 @@ class Chromium extends Browser {
239
249
  * number (i.e. "77.0.3865.0"). Defaults to "latest".
240
250
  * @return {BrowserBinary}
241
251
  */
242
- static async downloadBinary(version) {
243
- let base;
244
- if (version && !(version == "beta" || version == "dev"))
245
- base = await Chromium.#getBranchBasePosition(version);
246
- else
247
- ({version, base} = await Chromium.#getLatestVersion(version));
252
+ static async downloadBinary(version = "latest") {
253
+ const MIN_VERSION = 75;
254
+ const CHANNELS = ["latest", "beta", "dev"];
255
+
256
+ checkVersion(version, MIN_VERSION, CHANNELS);
257
+
258
+ let versionNumber = CHANNELS.includes(version) ?
259
+ await Chromium.#getVersionForChannel(version) : version;
260
+
261
+ let base = await Chromium.#getBranchBasePosition(versionNumber);
248
262
 
249
263
  let {binary, revision} = await Chromium.#downloadChromium(base);
250
- return {binary, versionNumber: version, revision};
264
+ return {binary, versionNumber, revision};
251
265
  }
252
266
 
253
267
  static async #installDriver(revision, version) {
@@ -274,9 +288,10 @@ class Chromium extends Browser {
274
288
  }
275
289
 
276
290
  /** @see Browser.getDriver */
277
- static async getDriver(version, {headless = true, extensionPaths = [],
278
- incognito = false, insecure = false,
279
- extraArgs = []} = {}) {
291
+ static async getDriver(version = "latest", {
292
+ headless = true, extensionPaths = [], incognito = false, insecure = false,
293
+ extraArgs = []
294
+ } = {}) {
280
295
  let {binary, revision, versionNumber} =
281
296
  await Chromium.downloadBinary(version);
282
297
  await Chromium.#installDriver(revision, versionNumber);
@@ -301,14 +316,8 @@ class Chromium extends Browser {
301
316
 
302
317
  /** @see Browser.enableExtensionInIncognito */
303
318
  static async enableExtensionInIncognito(driver, extensionTitle) {
304
- let version = await getBrowserVersion(driver);
305
- // Webdriver capabilities don't include a browser version for Opera
306
- if (version && version < 75)
307
- // The UI workaround needs a chromedriver >= 75
308
- throw new Error(`Only supported on Chromium >= 75. Current version: ${version}`);
309
-
310
319
  await driver.navigate().to("chrome://extensions");
311
- await driver.executeScript(`
320
+ await driver.executeScript((...args) => {
312
321
  let enable = () => document.querySelector("extensions-manager").shadowRoot
313
322
  .querySelector("extensions-detail-view").shadowRoot
314
323
  .getElementById("allow-incognito").shadowRoot
@@ -321,7 +330,7 @@ class Chromium extends Browser {
321
330
  return new Promise((resolve, reject) => {
322
331
  let extensionDetails;
323
332
  for (let {shadowRoot} of extensions) {
324
- if (shadowRoot.getElementById("name").innerHTML != arguments[0])
333
+ if (shadowRoot.getElementById("name").innerHTML != args[0])
325
334
  continue;
326
335
 
327
336
  extensionDetails = shadowRoot.getElementById("detailsButton");
@@ -332,7 +341,8 @@ class Chromium extends Browser {
332
341
 
333
342
  extensionDetails.click();
334
343
  setTimeout(() => resolve(enable()), 100);
335
- });`, extensionTitle);
344
+ });
345
+ }, extensionTitle);
336
346
  }
337
347
  }
338
348
 
@@ -342,13 +352,13 @@ class Chromium extends Browser {
342
352
  * @extends Browser
343
353
  */
344
354
  class Firefox extends Browser {
345
- static async #getLatestVersion(branch) {
355
+ static async #getVersionForChannel(branch) {
346
356
  let data = await got("https://product-details.mozilla.org/1.0/firefox_versions.json").json();
347
357
  return branch == "beta" ?
348
358
  data.LATEST_FIREFOX_DEVEL_VERSION : data.LATEST_FIREFOX_VERSION;
349
359
  }
350
360
 
351
- static #getFirefoxBinary(dir) {
361
+ static #getBinaryPath(dir) {
352
362
  switch (process.platform) {
353
363
  case "win32":
354
364
  return path.join(dir, "core", "firefox.exe");
@@ -391,7 +401,7 @@ class Firefox extends Browser {
391
401
 
392
402
  try {
393
403
  await fs.promises.access(browserDir);
394
- return Firefox.#getFirefoxBinary(browserDir);
404
+ return Firefox.#getBinaryPath(browserDir);
395
405
  }
396
406
  catch (e) {}
397
407
 
@@ -405,7 +415,7 @@ class Firefox extends Browser {
405
415
  }
406
416
 
407
417
  await Firefox.#extractFirefoxArchive(archive, browserDir);
408
- return Firefox.#getFirefoxBinary(browserDir);
418
+ return Firefox.#getBinaryPath(browserDir);
409
419
  }
410
420
 
411
421
  /**
@@ -414,18 +424,24 @@ class Firefox extends Browser {
414
424
  * number (i.e. "68.0"). Defaults to "latest".
415
425
  * @return {BrowserBinary}
416
426
  */
417
- static async downloadBinary(version) {
418
- if (!version || version == "beta")
419
- version = await Firefox.#getLatestVersion(version);
427
+ static async downloadBinary(version = "latest") {
428
+ const MIN_VERSION = 60;
429
+ const CHANNELS = ["latest", "beta"];
430
+
431
+ checkVersion(version, MIN_VERSION, CHANNELS);
420
432
 
421
- let binary = await Firefox.#downloadFirefox(version);
422
- return {binary, versionNumber: version};
433
+ let versionNumber = CHANNELS.includes(version) ?
434
+ await Firefox.#getVersionForChannel(version) : version;
435
+
436
+ let binary = await Firefox.#downloadFirefox(versionNumber);
437
+ return {binary, versionNumber};
423
438
  }
424
439
 
425
440
  /** @see Browser.getDriver */
426
- static async getDriver(version, {headless = true, extensionPaths = [],
427
- incognito = false, insecure = false,
428
- extraArgs = []} = {}) {
441
+ static async getDriver(version = "latest", {
442
+ headless = true, extensionPaths = [], incognito = false, insecure = false,
443
+ extraArgs = []
444
+ } = {}) {
429
445
  let {binary} = await Firefox.downloadBinary(version);
430
446
 
431
447
  let options = new firefox.Options();
@@ -502,7 +518,7 @@ class Edge extends Browser {
502
518
  return {versionNumber: version, channel: "stable"};
503
519
 
504
520
  let channel = version == "latest" ? "stable" : version;
505
- let {body} = await got(`https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edge-${channel}`);
521
+ let {body} = await got(`https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edge-${channel}/`);
506
522
  let regex = /href="microsoft-edge-(stable|beta|dev)_(.*?)-1_/gm;
507
523
  let matches;
508
524
  let versionNumbers = [];
@@ -516,6 +532,29 @@ class Edge extends Browser {
516
532
  return {versionNumber, channel};
517
533
  }
518
534
 
535
+ static #getDarwinAppName(channel) {
536
+ let extra = channel == "stable" ?
537
+ "" : " " + channel.charAt(0).toUpperCase() + channel.slice(1);
538
+ return `Microsoft Edge${extra}`;
539
+ }
540
+
541
+ static #getBinaryPath(channel = "stable") {
542
+ switch (process.platform) {
543
+ case "win32":
544
+ let programFiles = process.env["ProgramFiles(x86)"] ?
545
+ "${Env:ProgramFiles(x86)}" : "${Env:ProgramFiles}";
546
+ return `${programFiles}\\Microsoft\\Edge\\Application\\msedge.exe`;
547
+ case "linux":
548
+ return channel == "stable" ?
549
+ "microsoft-edge" : `microsoft-edge-${channel}`;
550
+ case "darwin":
551
+ let appName = Edge.#getDarwinAppName(channel);
552
+ return `${process.env.HOME}/Applications/${appName}.app/Contents/MacOS/${appName}`;
553
+ default:
554
+ throw new Error(`Unexpected platform: ${process.platform}`);
555
+ }
556
+ }
557
+
519
558
  /**
520
559
  * Downloads the browser binary file.
521
560
  * @param {string} version - Either "latest", "beta", "dev" or a full version
@@ -524,20 +563,36 @@ class Edge extends Browser {
524
563
  * @return {BrowserBinary}
525
564
  */
526
565
  static async downloadBinary(version = "latest") {
527
- if (process.platform != "linux")
528
- throw new Error("Edge download is only supported on Linux");
529
-
566
+ if (process.platform == "win32")
567
+ // Edge is mandatory on Windows, can't be uninstalled or downgraded
568
+ // https://support.microsoft.com/en-us/microsoft-edge/why-can-t-i-uninstall-microsoft-edge-ee150b3b-7d7a-9984-6d83-eb36683d526d
569
+ throw new Error("Edge download is not supported in Windows");
570
+
571
+ const MIN_VERSION = 95;
572
+ const CHANNELS = ["latest", "beta", "dev"];
573
+ checkVersion(version, MIN_VERSION, CHANNELS);
530
574
  let {versionNumber, channel} = await Edge.#getVersionForChannel(version);
531
575
 
532
- let snapshotsDir = path.join(snapshotsBaseDir, "edge");
533
- let filename = `microsoft-edge-${channel}_${versionNumber}-1_amd64.deb`;
534
- let archive = path.join(snapshotsDir, "cache", filename);
535
- let binary = {
536
- stable: "microsoft-edge",
537
- beta: "microsoft-edge-beta",
538
- dev: "microsoft-edge-dev"
576
+ let darwinName = {
577
+ stable: "MicrosoftEdge",
578
+ beta: "MicrosoftEdgeBeta",
579
+ dev: "MicrosoftEdgeDev"
539
580
  }[channel];
581
+ let filename = {
582
+ linux: `microsoft-edge-${channel}_${versionNumber}-1_amd64.deb`,
583
+ darwin: `${darwinName}-${versionNumber}.pkg`
584
+ }[process.platform];
585
+ let darwinArch = process.arch == "arm64" ?
586
+ "03adf619-38c6-4249-95ff-4a01c0ffc962" :
587
+ "C1297A47-86C4-4C1F-97FA-950631F94777";
588
+ let downloadUrl = {
589
+ linux: `https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edge-${channel}/${filename}`,
590
+ darwin: `https://officecdnmac.microsoft.com/pr/${darwinArch}/MacAutoupdate/${filename}`
591
+ }[process.platform];
540
592
 
593
+ let snapshotsDir = path.join(snapshotsBaseDir, "edge");
594
+ let archive = path.join(snapshotsDir, "cache", filename);
595
+ let binary = Edge.#getBinaryPath(channel);
541
596
  try {
542
597
  if (await Edge.#getInstalledVersionNumber(binary) == versionNumber)
543
598
  return {binary, versionNumber};
@@ -545,34 +600,38 @@ class Edge extends Browser {
545
600
  catch (e) {}
546
601
 
547
602
  try {
548
- await download(
549
- `https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edge-${channel}/${filename}`,
550
- archive
551
- );
603
+ await download(downloadUrl, archive);
552
604
  }
553
605
  catch (err) {
554
- throw new Error(`Edge download failed: ${err}`);
606
+ throw new Error(`Edge download failed at ${downloadUrl}\n${err}`);
607
+ }
608
+
609
+ if (process.platform == "linux") {
610
+ await promisify(exec)(`dpkg -i ${archive}`);
611
+ }
612
+ else if (process.platform == "darwin") {
613
+ let appName = Edge.#getDarwinAppName(channel);
614
+ try {
615
+ await fs.promises.rm(`${process.env.HOME}/Applications/${appName}.app`, {recursive: true});
616
+ }
617
+ catch (e) {}
618
+ await promisify(exec)(`installer -pkg ${archive} -target CurrentUserHomeDirectory`);
555
619
  }
556
- await promisify(exec)(`dpkg -i ${archive}`);
557
620
 
558
621
  return {binary, versionNumber};
559
622
  }
560
623
 
561
624
  static async #getInstalledVersionNumber(binary) {
562
625
  let installedVersion = await Edge.getInstalledVersion(binary);
563
- return installedVersion.replace("beta", "").replace("dev", "")
564
- .trim().replace(/.*\s/, "");
626
+ for (let word of ["beta", "dev", "Beta", "Dev"])
627
+ installedVersion = installedVersion.replace(word, "");
628
+ return installedVersion.trim().replace(/.*\s/, "");
565
629
  }
566
630
 
567
631
  static async #installDriver() {
568
- let binary = {
569
- win32:
570
- "${Env:ProgramFiles(x86)}\\Microsoft\\Edge\\Application\\msedge.exe",
571
- darwin: "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
572
- linux: "microsoft-edge"
573
- }[process.platform];
574
- let version = await Edge.#getInstalledVersionNumber(binary);
575
- if (!version)
632
+ let binary = Edge.#getBinaryPath();
633
+ let versionNumber = await Edge.#getInstalledVersionNumber(binary);
634
+ if (!versionNumber)
576
635
  throw new Error("Edge is not installed");
577
636
 
578
637
  let [zip, driver] = {
@@ -582,14 +641,11 @@ class Edge extends Browser {
582
641
  "darwin-x64": ["edgedriver_mac64.zip", "msedgedriver"],
583
642
  "darwin-arm64": ["edgedriver_arm64.zip", "msedgedriver"]
584
643
  }[platform];
585
- let cacheDir = path.join(snapshotsBaseDir, "edge", "cache");
586
- let driverBinDir = path.join(process.cwd(), "node_modules", Edge.#DRIVER,
587
- "bin");
588
- let driverLibDir = path.join(process.cwd(), "node_modules", Edge.#DRIVER,
589
- "lib", Edge.#DRIVER);
590
- let archive = path.join(cacheDir, `${version}-${zip}`);
591
-
592
- let vSplit = version.split(".");
644
+ let cacheDir = path.join(snapshotsBaseDir, "edge", "cache",
645
+ `edgedriver-${versionNumber}`);
646
+ let archive = path.join(cacheDir, `${versionNumber}-${zip}`);
647
+
648
+ let vSplit = versionNumber.split(".");
593
649
  let lastNum = parseInt(vSplit[3], 10);
594
650
  while (lastNum >= 0) {
595
651
  try {
@@ -604,53 +660,50 @@ class Edge extends Browser {
604
660
  }
605
661
 
606
662
  if (lastNum < 0)
607
- throw new Error(`msedgedriver was not found for Edge ${version}`);
663
+ throw new Error(`msedgedriver was not found for Edge ${versionNumber}`);
608
664
 
609
- await extractZip(archive, {dir: cacheDir});
610
665
  await killDriverProcess(Edge.#DRIVER);
611
- for (let destinationDir of [driverBinDir, driverLibDir]) {
612
- await fs.promises.mkdir(destinationDir, {recursive: true});
613
- await fs.promises.copyFile(path.join(cacheDir, driver),
614
- path.join(destinationDir, driver));
666
+ let driverPath = path.join(cacheDir, driver);
667
+ try {
668
+ await fs.promises.rm(driverPath, {recursive: true});
615
669
  }
670
+ catch (e) {} // file does not exist
671
+ await extractZip(archive, {dir: cacheDir});
672
+
673
+ return driverPath;
616
674
  }
617
675
 
618
676
  /** @see Browser.getDriver */
619
- static async getDriver(version, {headless = true, extensionPaths = [],
620
- incognito = false, insecure = false,
621
- extraArgs = []} = {}) {
622
- if (process.platform == "linux")
677
+ static async getDriver(version = "latest", {
678
+ headless = true, extensionPaths = [], incognito = false, insecure = false,
679
+ extraArgs = []
680
+ } = {}) {
681
+ if (process.platform == "linux" || process.platform == "darwin")
623
682
  await Edge.downloadBinary(version);
624
683
 
625
- await Edge.#installDriver();
684
+ let driverPath = await Edge.#installDriver();
685
+ let serviceBuilder = new edge.ServiceBuilder(driverPath);
626
686
 
627
- let args = ["no-sandbox", ...extraArgs];
687
+ let options = new edge.Options().addArguments("no-sandbox", ...extraArgs);
628
688
  if (headless)
629
- args.push("headless");
689
+ options.headless();
630
690
  if (extensionPaths.length > 0)
631
- args.push(`load-extension=${extensionPaths.join(",")}`);
691
+ options.addArguments(`load-extension=${extensionPaths.join(",")}`);
632
692
  if (incognito)
633
- args.push("incognito");
693
+ options.addArguments("incognito");
694
+ if (insecure)
695
+ options.addArguments("ignore-certificate-errors");
634
696
 
635
697
  let builder = new webdriver.Builder();
636
698
  builder.forBrowser("MicrosoftEdge");
637
- builder.withCapabilities({
638
- "browserName": "MicrosoftEdge",
639
- "ms:edgeChromium": true,
640
- "ms:edgeOptions": {args},
641
- "acceptInsecureCerts": insecure
642
- });
699
+ builder.setEdgeOptions(options);
700
+ builder.setEdgeService(serviceBuilder);
643
701
 
644
702
  return builder.build();
645
703
  }
646
704
 
647
705
  /** @see Browser.enableExtensionInIncognito */
648
706
  static async enableExtensionInIncognito(driver, extensionTitle) {
649
- let version = await getBrowserVersion(driver);
650
- if (version < 79)
651
- // The UI workaround needs a chromium based msedgedriver
652
- throw new Error(`Only supported on Edge >= 79. Current version: ${version}`);
653
-
654
707
  await driver.navigate().to("edge://extensions/");
655
708
  for (let elem of await driver.findElements(By.css("[role=listitem]"))) {
656
709
  let text = await elem.getAttribute("innerHTML");
@@ -671,218 +724,6 @@ class Edge extends Browser {
671
724
  }
672
725
  }
673
726
 
674
- /**
675
- * Download functionality for Opera. This class can be used statically.
676
- * @hideconstructor
677
- * @extends Browser
678
- */
679
- class Opera extends Browser {
680
- static async #getVersionForChannel(version) {
681
- let channelPath = "opera/desktop";
682
- let filePrefix = "Opera";
683
- if (version != "latest")
684
- return {versionNumber: version, channelPath, filePrefix};
685
-
686
- let {body} = await got(`https://ftp.opera.com/pub/${channelPath}`);
687
- let regex = /href="(\d.*)\/"/gm;
688
- let matches = body.match(regex);
689
- let versionNumber = regex.exec(matches[matches.length - 1])[1];
690
-
691
- return {versionNumber, channelPath, filePrefix};
692
- }
693
-
694
- static #getOperaBinary(dir) {
695
- switch (process.platform) {
696
- case "win32":
697
- return path.join(dir, "launcher.exe");
698
- case "linux":
699
- return path.join("/", "usr", "bin", "opera");
700
- case "darwin":
701
- return path.join(dir, "Opera.app", "Contents", "MacOS", "Opera");
702
- default:
703
- throw new Error(`Unexpected platform: ${process.platform}`);
704
- }
705
- }
706
-
707
- static async #extractDeb(archive) {
708
- let child = spawn("dpkg", ["-i", archive]);
709
-
710
- child.stdout.on("data", data => {
711
- if (data.toString().startsWith("Do you want to update Opera")) {
712
- process.stdin.pipe(child.stdin);
713
- child.stdin.write("no\r\n");
714
- }
715
- });
716
-
717
- child.stderr.on("data", data => {
718
- let expectedWarnings = [
719
- "debconf: unable to initialize frontend",
720
- "dpkg: warning: downgrading opera-stable",
721
- "update-alternatives",
722
- "using /usr/bin/opera",
723
- "skip creation of",
724
- "\r\n"
725
- ];
726
- if (!expectedWarnings.find(err => data.toString().includes(err.trim())))
727
- console.error(`stderr: ${data.toString()}`);
728
- });
729
-
730
- await new Promise((resolve, reject) => child.on("close", code => {
731
- if (code != 0)
732
- reject(`dpkg process exited with code ${code}`);
733
-
734
- resolve();
735
- }));
736
- }
737
-
738
- static async #installOnWindows(archive, dir, filename) {
739
- let archiveCopy = path.join(dir, filename);
740
- await fsExtra.copy(archive, archiveCopy);
741
- await promisify(exec)(`"${archiveCopy}"`);
742
- }
743
-
744
- static #extractOperaArchive(archive, dir, filename) {
745
- switch (process.platform) {
746
- case "win32":
747
- return Opera.#installOnWindows(archive, dir, filename);
748
- case "linux":
749
- return Opera.#extractDeb(archive, dir);
750
- case "darwin":
751
- return extractTar(archive, dir);
752
- default:
753
- throw new Error(`Unexpected platform: ${process.platform}`);
754
- }
755
- }
756
-
757
- /**
758
- * Downloads the browser binary file.
759
- * @param {string} version - Either "latest" or a full version number
760
- * (i.e. "64.0.3417.92"). Defaults to "latest".
761
- * @return {BrowserBinary}
762
- */
763
- static async downloadBinary(version = "latest") {
764
- let {versionNumber, channelPath, filePrefix} =
765
- await Opera.#getVersionForChannel(version);
766
-
767
- let [platformDir, fileSuffix] = {
768
- "win32-ia32": ["win", "Autoupdate.exe"],
769
- "win32-x64": ["win", "Autoupdate_x64.exe"],
770
- "linux-x64": ["linux", "amd64.deb"],
771
- "darwin-x64": ["mac", "Autoupdate.tar.xz"],
772
- "dawrin-arm64": ["mac", "Autoupdate_arm64.tar.xz"]
773
- }[platform];
774
-
775
- let snapshotsDir = path.join(snapshotsBaseDir, "opera");
776
- let browserDir = path.join(snapshotsDir, `opera-${platform}-${versionNumber}`);
777
- let filename = `${filePrefix}_${versionNumber}_${fileSuffix}`;
778
- let archive = path.join(snapshotsDir, "cache", filename);
779
-
780
- let binary = Opera.#getOperaBinary(browserDir);
781
- try {
782
- await fs.promises.access(browserDir);
783
- return {binary, versionNumber};
784
- }
785
- catch (e) {}
786
-
787
- await fs.promises.mkdir(path.dirname(browserDir), {recursive: true});
788
- try {
789
- await fs.promises.access(archive);
790
- }
791
- catch (e) {
792
- await download(
793
- `https://ftp.opera.com/pub/${channelPath}/${versionNumber}/${platformDir}/${filename}`,
794
- archive
795
- );
796
- }
797
-
798
- await Opera.#extractOperaArchive(archive, browserDir, filename);
799
- return {binary, versionNumber};
800
- }
801
-
802
- static async #installDriver(version, originalVersion) {
803
- let [zip, driver] = {
804
- "win32-ia32": ["operadriver_win32.zip", "operadriver.exe"],
805
- "win32-x64": ["operadriver_win64.zip", "operadriver.exe"],
806
- "linux-x64": ["operadriver_linux64.zip", "operadriver"],
807
- "darwin-x64": ["operadriver_mac64.zip", "operadriver"],
808
- "darwin-arm64": ["operadriver_mac64.zip", "operadriver"]
809
- }[platform];
810
-
811
- let {versionNumber} = await Opera.#getVersionForChannel(version);
812
- versionNumber = versionNumber.split(".")[0];
813
-
814
- let cacheDir = path.join(snapshotsBaseDir, "opera", "cache",
815
- `driver-for-opera-${versionNumber}`);
816
- let archive = path.join(cacheDir, zip);
817
-
818
- let {body} = await got(`https://github.com/operasoftware/operachromiumdriver/releases?q=Opera+${versionNumber}&expanded=true`);
819
- let regex = /release-card[\s\S]*Link--primary.*>(.*)<\/a/gm;
820
- let matches = body.match(regex);
821
- if (!matches || matches.length == 0)
822
- throw new Error(`Driver for Opera ${version} was not found`);
823
- let driverVersion = regex.exec(matches[matches.length - 1])[1];
824
-
825
- try {
826
- await download(`https://github.com/operasoftware/operachromiumdriver/releases/download/v.${driverVersion}/${zip}`,
827
- archive);
828
- }
829
- catch (err) {
830
- throw new Error(`Downloading operadriver failed: ${err}`);
831
- }
832
-
833
- await killDriverProcess("operadriver");
834
- let driverPath = path.join(cacheDir, zip.split(".")[0], driver);
835
- try {
836
- await fs.promises.rm(driverPath, {recursive: true});
837
- }
838
- catch (e) {} // file does not exist
839
- await extractZip(archive, {dir: cacheDir});
840
- await fs.promises.chmod(driverPath, 577);
841
-
842
- return driverPath;
843
- }
844
-
845
- /** @see Browser.getDriver */
846
- static async getDriver(version = "latest", {
847
- headless = true, extensionPaths = [], incognito = false, insecure = false,
848
- extraArgs = []
849
- } = {}) {
850
- let {binary, versionNumber} = await Opera.downloadBinary(version);
851
- let driverPath = await Opera.#installDriver(versionNumber, version);
852
- // operadriver uses chrome as a service builder:
853
- // https://github.com/operasoftware/operachromiumdriver/blob/master/docs/desktop.md
854
- let serviceBuilder = new chrome.ServiceBuilder(driverPath);
855
- let service = serviceBuilder.build();
856
- await service.start();
857
-
858
- let options = new chrome.Options().addArguments("no-sandbox", ...extraArgs);
859
- if (extensionPaths.length > 0)
860
- options.addArguments(`load-extension=${extensionPaths.join(",")}`);
861
- if (headless)
862
- options.headless();
863
- if (insecure)
864
- options.addArguments("ignore-certificate-errors");
865
- if (incognito)
866
- options.addArguments("incognito");
867
- options.setChromeBinaryPath(binary);
868
- // https://github.com/operasoftware/operachromiumdriver/issues/61#issuecomment-579331657
869
- options.addArguments("remote-debugging-port=9222");
870
-
871
- let builder = new webdriver.Builder();
872
- builder.forBrowser("chrome");
873
- builder.setChromeOptions(options);
874
- builder.setChromeService(serviceBuilder);
875
-
876
- return builder.build();
877
- }
878
-
879
- /** @see Browser.enableExtensionInIncognito */
880
- static async enableExtensionInIncognito(driver, extensionTitle) {
881
- // Extensions page in Opera has the same web elements as Chromium
882
- await Chromium.enableExtensionInIncognito(driver, extensionTitle);
883
- }
884
- }
885
-
886
727
  /**
887
728
  * @type {Object}
888
729
  * @property {Chromium} chromium - Download functionality for Chromium.
@@ -892,6 +733,5 @@ class Opera extends Browser {
892
733
  export const BROWSERS = {
893
734
  chromium: Chromium,
894
735
  firefox: Firefox,
895
- edge: Edge,
896
- opera: Opera
736
+ edge: Edge
897
737
  };
package/src/utils.js CHANGED
@@ -22,7 +22,6 @@ import {promisify} from "util";
22
22
  import {exec} from "child_process";
23
23
 
24
24
  import got from "got";
25
- import fsExtra from "fs-extra";
26
25
  import dmg from "dmg";
27
26
 
28
27
  /**
@@ -66,7 +65,7 @@ export async function extractDmg(archive, dir) {
66
65
  let source = path.join(mpath, target);
67
66
  await fs.promises.mkdir(dir);
68
67
  try {
69
- await fsExtra.copy(source, path.join(dir, target));
68
+ await fs.promises.cp(source, path.join(dir, target), {recursive: true});
70
69
  }
71
70
  finally {
72
71
  try {
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "env": {
3
- "mocha": true
3
+ "mocha": true,
4
+ "browser": true
4
5
  }
5
6
  }
package/test/browsers.js CHANGED
@@ -25,13 +25,11 @@ import {killDriverProcess} from "../src/utils.js";
25
25
  // Required to set the driver path on Windows
26
26
  import "chromedriver";
27
27
  import "geckodriver";
28
- import "msedgedriver";
29
28
 
30
29
  const VERSIONS = {
31
- chromium: [void 0, "77.0.3865.0", "beta", "dev"],
32
- firefox: [void 0, "68.0", "beta"],
33
- edge: [void 0, "95.0.1020.53", "beta", "dev"],
34
- opera: [void 0, "64.0.3417.92"]
30
+ chromium: ["latest", "75.0.3770.0", "beta", "dev"],
31
+ firefox: ["latest", "60.0", "beta"],
32
+ edge: ["latest", "95.0.1020.40", "beta", "dev"]
35
33
  };
36
34
  const TEST_URL = "https://gitlab.com/eyeo/developer-experience/get-browser-binary";
37
35
  let extensionPaths = [path.resolve(process.cwd(), "test", "extension")];
@@ -74,9 +72,27 @@ function normalize(version) {
74
72
  return normalized.split("b")[0];
75
73
  }
76
74
 
75
+ function getWindowSize(driver) {
76
+ return driver.executeScript(() => {
77
+ return {height: window.innerHeight, width: window.innerWidth};
78
+ });
79
+ }
80
+
81
+ expect.extend({
82
+ toMeasureLessThan(small, big) {
83
+ let pass = small.width < big.width || small.height < big.height;
84
+ let message = () =>
85
+ `expected small sizes (w: ${small.width}, h: ${small.height}) ` +
86
+ `to be smaller than big sizes (w: ${big.width}, h: ${big.height})`;
87
+ if (pass)
88
+ return {message, pass: true};
89
+ return {message, pass: false};
90
+ }
91
+ });
92
+
77
93
  for (let browser of Object.keys(BROWSERS)) {
78
94
  describe(`Browser: ${browser}`, function() {
79
- this.timeout(40000);
95
+ this.timeout(150000);
80
96
 
81
97
  before(async() => {
82
98
  try {
@@ -86,9 +102,10 @@ for (let browser of Object.keys(BROWSERS)) {
86
102
  });
87
103
 
88
104
  for (let version of VERSIONS[browser]) {
89
- describe(`Version: ${version || "latest"}`, () => {
105
+ describe(`Version: ${version}`, () => {
90
106
  let driver = null;
91
- afterEach(async() => {
107
+
108
+ async function quitDriver() {
92
109
  if (!driver)
93
110
  return;
94
111
 
@@ -99,16 +116,18 @@ for (let browser of Object.keys(BROWSERS)) {
99
116
  await killDriverProcess("chromedriver");
100
117
  else if (browser == "firefox")
101
118
  await killDriverProcess("geckodriver");
102
- });
119
+ }
120
+
121
+ afterEach(quitDriver);
103
122
 
104
123
  it("downloads", async function() {
105
- if (browser == "edge" && process.platform != "linux")
124
+ if (browser == "edge" && process.platform == "win32")
106
125
  this.skip();
107
126
 
108
127
  let {binary, versionNumber} =
109
128
  await BROWSERS[browser].downloadBinary(version);
110
- let browserDir = browser == "opera" ? /(opera|Opera)/ : browser;
111
- expect(binary).toEqual(expect.stringMatching(browserDir));
129
+ let browserName = browser == "edge" ? /(edge|Edge)/ : browser;
130
+ expect(binary).toEqual(expect.stringMatching(browserName));
112
131
 
113
132
  let installedVersion =
114
133
  await BROWSERS[browser].getInstalledVersion(binary);
@@ -120,8 +139,7 @@ for (let browser of Object.keys(BROWSERS)) {
120
139
  let names = {
121
140
  chromium: "chrome",
122
141
  firefox: "firefox",
123
- edge: /(MicrosoftEdge|msedge)/,
124
- opera: /(opera|chrome)/
142
+ edge: /(MicrosoftEdge|msedge)/
125
143
  };
126
144
 
127
145
  driver = await BROWSERS[browser].getDriver(version);
@@ -132,10 +150,21 @@ for (let browser of Object.keys(BROWSERS)) {
132
150
  });
133
151
 
134
152
  it("supports extra args", async() => {
153
+ let headless = false;
135
154
  let extraArgs = browser == "firefox" ?
136
155
  ["--devtools"] : ["auto-open-devtools-for-tabs"];
137
- driver = await BROWSERS[browser].getDriver(version, {extraArgs});
156
+
157
+ driver = await BROWSERS[browser].getDriver(
158
+ version, {headless, extraArgs});
159
+ await driver.navigate().to(TEST_URL);
160
+ let sizeDevToolsOpen = await getWindowSize(driver);
161
+ await quitDriver();
162
+
163
+ driver = await BROWSERS[browser].getDriver(version, {headless});
138
164
  await driver.navigate().to(TEST_URL);
165
+ let sizeDevToolsClosed = await getWindowSize(driver);
166
+
167
+ expect(sizeDevToolsOpen).toMeasureLessThan(sizeDevToolsClosed);
139
168
  });
140
169
 
141
170
  it("loads an extension", async() => {
@@ -147,7 +176,7 @@ for (let browser of Object.keys(BROWSERS)) {
147
176
  });
148
177
 
149
178
  it("loads an extension in incognito mode", async function() {
150
- if (browser == "firefox" && version == "68.0")
179
+ if (browser == "firefox" && version == "60.0")
151
180
  this.skip();
152
181
 
153
182
  driver = await BROWSERS[browser].getDriver(
@@ -159,5 +188,15 @@ for (let browser of Object.keys(BROWSERS)) {
159
188
  });
160
189
  });
161
190
  }
191
+
192
+ it("does not download unsupported versions", async function() {
193
+ if (browser == "edge" && process.platform == "win32")
194
+ this.skip();
195
+
196
+ for (let unsupported of ["0.0", "invalid"]) {
197
+ await expect(BROWSERS[browser].downloadBinary(unsupported))
198
+ .rejects.toThrow(`Unsupported browser version: ${unsupported}`);
199
+ }
200
+ });
162
201
  });
163
202
  }
@@ -1,15 +1,4 @@
1
- FROM node:16-bullseye-slim
2
-
3
- # General packages
4
- RUN apt-get update && apt-get install -y git procps wget unzip bzip2 gnupg
5
- # xvfb (headful browser run)
6
- RUN apt-get install -y libgtk-3-0 libxt6 xvfb libnss3 libxss1
7
- # General browser dependencies
8
- RUN apt-get install -y libgconf-2-4 libasound2 libgbm1
9
- # Edge dependencies
10
- RUN apt-get install -y fonts-liberation libatomic1 xdg-utils
11
- # Opera dependencies
12
- RUN apt-get install -y libcurl4 libgdk-pixbuf2.0-0
1
+ FROM registry.gitlab.com/eyeo/docker/get-browser-binary:node16
13
2
 
14
3
  COPY package*.json get-browser-binary/
15
4
  RUN cd get-browser-binary && npm install