@eyeo/get-browser-binary 0.12.0 → 0.14.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/src/browsers.js DELETED
@@ -1,919 +0,0 @@
1
- /*
2
- * Copyright (c) 2006-present eyeo GmbH
3
- *
4
- * This module is free software: you can redistribute it and/or modify
5
- * it under the terms of the GNU General Public License as published by
6
- * the Free Software Foundation, either version 3 of the License, or
7
- * (at your option) any later version.
8
- *
9
- * This program is distributed in the hope that it will be useful,
10
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
- * GNU General Public License for more details.
13
- *
14
- * You should have received a copy of the GNU General Public License
15
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
16
- */
17
-
18
- import path from "path";
19
- import {exec, execFile} from "child_process";
20
- import {promisify} from "util";
21
- import fs from "fs";
22
-
23
- import got from "got";
24
- import webdriver from "selenium-webdriver";
25
- import chrome from "selenium-webdriver/chrome.js";
26
- import firefox from "selenium-webdriver/firefox.js";
27
- import edge from "selenium-webdriver/edge.js";
28
- import extractZip from "extract-zip";
29
-
30
- import {download, extractTar, extractDmg, killDriverProcess, wait}
31
- from "./utils.js";
32
-
33
- /**
34
- * Root folder where browser and webdriver files get downloaded and extracted.
35
- * @type {string}
36
- */
37
- export let snapshotsBaseDir = path.join(process.cwd(), "browser-snapshots");
38
-
39
- let {until, By} = webdriver;
40
- let {platform, arch} = process;
41
- let platformArch = `${platform}-${arch}`;
42
-
43
- const UNSUPPORTED_VERSION_ERROR = "Unsupported browser version";
44
- const UNSUPPORTED_PLATFORM_ERROR = "Unsupported platform";
45
- const DRIVER_DOWNLOAD_ERROR = "Driver download failed";
46
- const DRIVER_START_ERROR = "Unable to start driver";
47
- const EXTENSION_NOT_FOUND_ERROR = "Extension not found";
48
- const BROWSER_DOWNLOAD_ERROR = "Browser download failed";
49
- const BROWSER_NOT_INSTALLED_ERROR = "Browser is not installed";
50
- const BROWSER_VERSION_CHECK_ERROR = "Checking the browser version failed";
51
- const ELEMENT_NOT_FOUND_ERROR = "HTML element not found";
52
-
53
- function getMajorVersion(versionNumber) {
54
- let majorVersion = parseInt(versionNumber && versionNumber.split(".")[0], 10);
55
- if (isNaN(majorVersion))
56
- throw new Error(`${UNSUPPORTED_VERSION_ERROR}: ${versionNumber}`);
57
-
58
- return majorVersion;
59
- }
60
-
61
- function checkVersion(version, minVersion, channels = []) {
62
- if (channels.includes(version))
63
- return;
64
-
65
- if (getMajorVersion(version) < minVersion)
66
- throw new Error(`${UNSUPPORTED_VERSION_ERROR}: ${version}`);
67
- }
68
-
69
- function checkPlatform() {
70
- if (!["win32", "linux", "darwin"].includes(platform))
71
- throw new Error(`${UNSUPPORTED_PLATFORM_ERROR}: ${platform}`);
72
- }
73
-
74
- /**
75
- * Base class for browser and webdriver functionality. Please see subclasses for
76
- * browser specific details. All classes can be used statically.
77
- * @hideconstructor
78
- */
79
- class Browser {
80
- /**
81
- * @typedef {Object} BrowserBinary
82
- * @property {string} binary The path to the browser binary.
83
- * @property {string} versionNumber The version number of the browser binary.
84
- */
85
-
86
- /**
87
- * Installs the browser. The installation process is detailed on the
88
- * subclasses.
89
- * @param {string} [version=latest] Either full version number or
90
- * channel/release. Please find examples on the subclasses.
91
- * @param {number} [downloadTimeout=0] Allowed time in ms for the download of
92
- * install files to complete. When set to 0 there is no time limit.
93
- * @return {BrowserBinary}
94
- * @throws {Error} Unsupported browser version, Unsupported platform, Browser
95
- * download failed.
96
- */
97
- static async installBrowser(version, downloadTimeout = 0) {
98
- // to be implemented by the subclass
99
- }
100
-
101
- /**
102
- * Gets the installed version returned by the browser binary.
103
- * @param {string} binary The path to the browser binary.
104
- * @return {string} Installed browser version.
105
- */
106
- static async getInstalledVersion(binary) {
107
- let stdout;
108
- try {
109
- if (platform == "win32") {
110
- ({stdout} = await promisify(exec)(
111
- `(Get-ItemProperty ${binary}).VersionInfo.ProductVersion`,
112
- {shell: "powershell.exe"})
113
- );
114
- }
115
- else {
116
- ({stdout} = await promisify(execFile)(binary, ["--version"]));
117
- }
118
- return stdout.trim();
119
- }
120
- catch (e) {}
121
- return "";
122
- }
123
-
124
- /**
125
- * @typedef {Object} webdriver
126
- * @see https://www.selenium.dev/selenium/docs/api/javascript/index.html
127
- */
128
-
129
- /**
130
- * @typedef {Object} driverOptions
131
- * @property {boolean} [headless=true] Run the browser in headless mode,
132
- * or not. In Chromium >= 111, the
133
- * {@link https://developer.chrome.com/articles/new-headless/ new headless mode}
134
- * is used.
135
- * @property {Array.<string>} [extensionPaths=[]] Loads extensions to the
136
- * browser.
137
- * @property {boolean} [incognito=false] Runs the browser in incognito mode,
138
- * or not.
139
- * @property {boolean} [insecure=false] Forces the browser to accept insecure
140
- * certificates, or not.
141
- * @property {Array.<string>} [extraArgs=[]] Additional arguments to start
142
- * the browser with.
143
- * @property {string} [customBrowserBinary] Path to the browser binary to be
144
- * used, instead of the browser installed by installBrowser(). This option
145
- * overrides the version parameter in getDriver().
146
- */
147
-
148
- /**
149
- * Installs the webdriver matching the browser version and runs the
150
- * browser. If needed, the browser binary is also installed.
151
- * @param {string} [version=latest] Either full version number or
152
- * channel/release. Please find examples on the subclasses.
153
- * @param {driverOptions} [options={}] Options to start the browser with.
154
- * @param {number} [downloadTimeout=0] Allowed time in ms for the download of
155
- * browser install files to complete. When set to 0 there is no time limit.
156
- * @return {webdriver}
157
- * @throws {Error} Unsupported browser version, Unsupported platform, Browser
158
- * download failed, Driver download failed, Unable to start driver.
159
- */
160
- static async getDriver(version, options = {}, downloadTimeout = 0) {
161
- // to be implemented by the subclass
162
- }
163
-
164
- /**
165
- * By default, extensions are disabled in incognito mode. This function
166
- * enables the extension when loaded in incognito.
167
- * @param {webdriver} driver The driver controlling the browser.
168
- * @param {string} extensionTitle Title of the extension to be enabled.
169
- * @return {webdriver}
170
- * @throws {Error} Unsupported browser version, Extension not found, HTML
171
- * element not found.
172
- */
173
- static async enableExtensionInIncognito(driver, extensionTitle) {
174
- // Allowing the extension in incognito mode can't happen programmatically:
175
- // https://stackoverflow.com/questions/57419654
176
- // https://bugzilla.mozilla.org/show_bug.cgi?id=1729315
177
- // That is done through the UI, to be implemented by the subclass
178
- }
179
- }
180
-
181
- /**
182
- * Browser and webdriver functionality for Chromium.
183
- * @hideconstructor
184
- * @extends Browser
185
- */
186
- class Chromium extends Browser {
187
- static #CHANNELS = ["latest", "beta", "dev"];
188
- static #MAX_VERSION_DECREMENTS = 200;
189
-
190
- static async #getVersionForChannel(channel) {
191
- if (!Chromium.#CHANNELS.includes(channel))
192
- return channel;
193
-
194
- if (channel == "latest")
195
- channel = "stable";
196
-
197
- let os = {
198
- "win32-ia32": "win",
199
- "win32-x64": "win64",
200
- "linux-x64": "linux",
201
- "darwin-x64": "mac",
202
- "darwin-arm64": "mac_arm64"
203
- }[platformArch];
204
- let url = `https://omahaproxy.appspot.com/all.json?os=${os}`;
205
- let data;
206
- try {
207
- data = await got(url).json();
208
- }
209
- catch (err) {
210
- throw new Error(`${BROWSER_VERSION_CHECK_ERROR}: ${url}\n${err}`);
211
- }
212
- let release = data[0].versions.find(ver => ver.channel == channel);
213
- let {current_version: version} = release;
214
-
215
- if (release.true_branch && release.true_branch.includes("_"))
216
- // A wrong base may be caused by a mini-branch (patched) release
217
- // In that case, the base is taken from the unpatched version
218
- version = [...version.split(".").slice(0, 3), "0"].join(".");
219
-
220
- return version;
221
- }
222
-
223
- static #getBinaryPath(dir) {
224
- checkPlatform();
225
- return {
226
- win32: path.join(dir, "chrome-win", "chrome.exe"),
227
- linux: path.join(dir, "chrome-linux", "chrome"),
228
- darwin: path.join(dir, "chrome-mac", "Chromium.app", "Contents", "MacOS",
229
- "Chromium")
230
- }[platform];
231
- }
232
-
233
- static async #getBase(chromiumVersion) {
234
- let url = `https://omahaproxy.appspot.com/deps.json?version=${chromiumVersion}`;
235
- let chromiumBase;
236
- try {
237
- ({chromium_base_position: chromiumBase} = await got(url).json());
238
- }
239
- catch (err) {
240
- throw new Error(`${BROWSER_VERSION_CHECK_ERROR}: ${url}\n${err}`);
241
- }
242
- return parseInt(chromiumBase, 10);
243
- }
244
-
245
- static async #getInstalledBrowserInfo(binary) {
246
- let installedVersion = await Chromium.getInstalledVersion(binary);
247
- // Linux example: "Chromium 112.0.5615.49 built on Debian 11.6"
248
- // Windows example: "114.0.5735.0"
249
- let versionNumber = installedVersion.split(" ")[1] || installedVersion;
250
- let base = await Chromium.#getBase(versionNumber);
251
- return {binary, versionNumber, base};
252
- }
253
-
254
- /**
255
- * Installs the browser. The Chromium executable gets extracted in the
256
- * {@link snapshotsBaseDir} folder, ready to go.
257
- * @param {string} [version=latest] Either "latest", "beta", "dev" or a full
258
- * version number (i.e. "77.0.3865.0").
259
- * @param {number} [downloadTimeout=0] Allowed time in ms for the download of
260
- * install files to complete. When set to 0 there is no time limit.
261
- * @return {BrowserBinary}
262
- * @throws {Error} Unsupported browser version, Unsupported platform, Browser
263
- * download failed.
264
- */
265
- static async installBrowser(version = "latest", downloadTimeout = 0) {
266
- const MIN_VERSION = 75;
267
-
268
- let binary;
269
- let versionNumber;
270
- let base;
271
-
272
- // https://gitlab.com/eyeo/developer-experience/get-browser-binary/-/issues/46
273
- if (platformArch == "linux-arm64")
274
- return await Chromium.#getInstalledBrowserInfo("/usr/bin/chromium");
275
-
276
- checkVersion(version, MIN_VERSION, Chromium.#CHANNELS);
277
- versionNumber = await Chromium.#getVersionForChannel(version);
278
-
279
- base = await Chromium.#getBase(versionNumber);
280
- let startBase = base;
281
- let [platformDir, fileName] = {
282
- "win32-ia32": ["Win", "chrome-win.zip"],
283
- "win32-x64": ["Win_x64", "chrome-win.zip"],
284
- "linux-x64": ["Linux_x64", "chrome-linux.zip"],
285
- "darwin-x64": ["Mac", "chrome-mac.zip"],
286
- "darwin-arm64": ["Mac_Arm", "chrome-mac.zip"]
287
- }[platformArch];
288
- let archive;
289
- let browserDir;
290
- let snapshotsDir = path.join(snapshotsBaseDir, "chromium");
291
-
292
- while (true) {
293
- browserDir = path.join(snapshotsDir, `chromium-${platformArch}-${base}`);
294
- binary = Chromium.#getBinaryPath(browserDir);
295
-
296
- try {
297
- await fs.promises.access(browserDir);
298
- return {binary, versionNumber, base};
299
- }
300
- catch (e) {}
301
-
302
- await fs.promises.mkdir(path.dirname(browserDir), {recursive: true});
303
-
304
- archive = path.join(snapshotsDir, "cache", `${base}-${fileName}`);
305
- let url = `https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/${platformDir}%2F${base}%2F${fileName}?alt=media`;
306
- try {
307
- try {
308
- await fs.promises.access(archive);
309
- }
310
- catch (e) {
311
- await download(url, archive, downloadTimeout);
312
- }
313
- break;
314
- }
315
- catch (err) {
316
- if (err.name == "HTTPError") {
317
- // Chromium advises decrementing the branch_base_position when no
318
- // matching build was found. See https://www.chromium.org/getting-involved/download-chromium
319
- base--;
320
- if (base <= startBase - Chromium.#MAX_VERSION_DECREMENTS)
321
- throw new Error(`${BROWSER_DOWNLOAD_ERROR}: Chromium base ${startBase}`);
322
- }
323
- else {
324
- throw new Error(`${BROWSER_DOWNLOAD_ERROR}: ${url}\n${err}`);
325
- }
326
- }
327
- }
328
- await extractZip(archive, {dir: browserDir});
329
-
330
- return {binary, versionNumber, base};
331
- }
332
-
333
- static async #installDriver(startBase) {
334
- let [dir, zip, driverBinary] = {
335
- "win32-ia32": ["Win", "chromedriver_win32.zip", "chromedriver.exe"],
336
- "win32-x64": ["Win_x64", "chromedriver_win32.zip", "chromedriver.exe"],
337
- "linux-x64": ["Linux_x64", "chromedriver_linux64.zip", "chromedriver"],
338
- "linux-arm64": ["", "", "chromedriver"],
339
- "darwin-x64": ["Mac", "chromedriver_mac64.zip", "chromedriver"],
340
- "darwin-arm64": ["Mac_Arm", "chromedriver_mac64.zip", "chromedriver"]
341
- }[platformArch];
342
-
343
- let cacheDir = path.join(snapshotsBaseDir, "chromium", "cache",
344
- "chromedriver");
345
- let archive = path.join(cacheDir, `${startBase}-${zip}`);
346
- try {
347
- await fs.promises.access(archive);
348
- await extractZip(archive, {dir: cacheDir});
349
- }
350
- catch (e) { // zip file is either not cached or corrupted
351
- if (platformArch == "linux-arm64") {
352
- // https://gitlab.com/eyeo/developer-experience/get-browser-binary/-/issues/46
353
- // It's unclear how electron releases match Chromium versions. Once that
354
- // is figured out, the link below will depend on the Chromium version.
355
- // https://stackoverflow.com/questions/38732822/compile-chromedriver-on-arm
356
- let url = "https://github.com/electron/electron/releases/download/v24.1.2/chromedriver-v24.1.2-linux-arm64.zip";
357
- try {
358
- await download(url, archive);
359
- }
360
- catch (err) {
361
- throw new Error(`${DRIVER_DOWNLOAD_ERROR}: ${url}\n${err}`);
362
- }
363
- }
364
- else {
365
- let base = startBase;
366
- while (true) {
367
- let url = `https://commondatastorage.googleapis.com/chromium-browser-snapshots/${dir}/${base}/${zip}`;
368
- try {
369
- await download(url, archive);
370
- break;
371
- }
372
- catch (err) {
373
- if (err.name == "HTTPError") {
374
- base--;
375
- archive = path.join(cacheDir, `${base}-${zip}`);
376
- if (base <= startBase - Chromium.#MAX_VERSION_DECREMENTS)
377
- throw new Error(`${DRIVER_DOWNLOAD_ERROR}: Chromium base ${startBase}`);
378
- }
379
- else {
380
- throw new Error(`${DRIVER_DOWNLOAD_ERROR}: ${url}\n${err}`);
381
- }
382
- }
383
- }
384
- }
385
- await extractZip(archive, {dir: cacheDir});
386
- }
387
-
388
- await killDriverProcess("chromedriver");
389
- let driverPath = path.join(cacheDir, zip.split(".")[0], driverBinary);
390
- if (platformArch == "linux-arm64")
391
- driverPath = path.join(cacheDir, driverBinary);
392
- await fs.promises.rm(driverPath, {force: true});
393
- await extractZip(archive, {dir: cacheDir});
394
-
395
- return driverPath;
396
- }
397
-
398
- /** @see Browser.getDriver */
399
- static async getDriver(version = "latest", {
400
- headless = true, extensionPaths = [], incognito = false, insecure = false,
401
- extraArgs = [], customBrowserBinary
402
- } = {}, downloadTimeout = 0) {
403
- let {binary, versionNumber, base} = customBrowserBinary ?
404
- await Chromium.#getInstalledBrowserInfo(customBrowserBinary) :
405
- await Chromium.installBrowser(version, downloadTimeout);
406
- let driverPath = await Chromium.#installDriver(base);
407
- let serviceBuilder = new chrome.ServiceBuilder(driverPath);
408
- let options = new chrome.Options().addArguments("no-sandbox", ...extraArgs);
409
- if (extensionPaths.length > 0)
410
- options.addArguments(`load-extension=${extensionPaths.join(",")}`);
411
- if (headless) {
412
- // New headless mode introduced in Chrome 111
413
- // https://developer.chrome.com/articles/new-headless/
414
- if (getMajorVersion(versionNumber) >= 111)
415
- options.addArguments("headless=new");
416
- else
417
- options.headless();
418
- }
419
- if (insecure)
420
- options.addArguments("ignore-certificate-errors");
421
- if (incognito)
422
- options.addArguments("incognito");
423
- options.setChromeBinaryPath(binary);
424
-
425
- let builder = new webdriver.Builder();
426
- builder.forBrowser("chrome");
427
- builder.setChromeOptions(options);
428
- builder.setChromeService(serviceBuilder);
429
-
430
- return builder.build();
431
- }
432
-
433
- /** @see Browser.enableExtensionInIncognito */
434
- static async enableExtensionInIncognito(driver, extensionTitle) {
435
- let handle = await driver.getWindowHandle();
436
-
437
- let version = getMajorVersion(
438
- (await driver.getCapabilities()).getBrowserVersion());
439
- if (version >= 115)
440
- // On Chromium 115 opening chrome://extensions on the default tab causes
441
- // WebDriverError: disconnected. Switching to a new window as a workaround
442
- await driver.switchTo().newWindow("window");
443
-
444
- await driver.navigate().to("chrome://extensions");
445
- await driver.executeScript((...args) => {
446
- let enable = () => document.querySelector("extensions-manager").shadowRoot
447
- .querySelector("extensions-detail-view").shadowRoot
448
- .getElementById("allow-incognito").shadowRoot
449
- .getElementById("crToggle").click();
450
-
451
- let extensions = document.querySelector("extensions-manager").shadowRoot
452
- .getElementById("items-list").shadowRoot
453
- .querySelectorAll("extensions-item");
454
-
455
- return new Promise((resolve, reject) => {
456
- let extensionDetails;
457
- for (let {shadowRoot} of extensions) {
458
- if (shadowRoot.getElementById("name").innerHTML != args[0])
459
- continue;
460
-
461
- extensionDetails = shadowRoot.getElementById("detailsButton");
462
- break;
463
- }
464
- if (!extensionDetails)
465
- reject(`${args[1]}: ${args[0]}`);
466
-
467
- extensionDetails.click();
468
- setTimeout(() => resolve(enable()), 100);
469
- });
470
- }, extensionTitle, EXTENSION_NOT_FOUND_ERROR);
471
- if (version >= 115)
472
- // Closing the previously opened new window
473
- await driver.close();
474
-
475
- await driver.switchTo().window(handle);
476
- }
477
- }
478
-
479
- /**
480
- * Browser and webdriver functionality for Firefox.
481
- * @hideconstructor
482
- * @extends Browser
483
- */
484
- class Firefox extends Browser {
485
- static #CHANNELS = ["latest", "beta"];
486
-
487
- static async #getVersionForChannel(channel) {
488
- if (!Firefox.#CHANNELS.includes(channel))
489
- return channel;
490
-
491
- let url = "https://product-details.mozilla.org/1.0/firefox_versions.json";
492
- let data;
493
- try {
494
- data = await got(url).json();
495
- }
496
- catch (err) {
497
- throw new Error(`${BROWSER_VERSION_CHECK_ERROR}: ${url}\n${err}`);
498
- }
499
- return channel == "beta" ?
500
- data.LATEST_FIREFOX_DEVEL_VERSION : data.LATEST_FIREFOX_VERSION;
501
- }
502
-
503
- static #getBinaryPath(dir) {
504
- checkPlatform();
505
- return {
506
- win32: path.join(dir, "core", "firefox.exe"),
507
- linux: path.join(dir, "firefox", "firefox"),
508
- darwin: path.join(dir, "Firefox.app", "Contents", "MacOS", "firefox")
509
- }[platform];
510
- }
511
-
512
- static #extractFirefoxArchive(archive, dir) {
513
- switch (platform) {
514
- case "win32":
515
- return promisify(exec)(`"${archive}" /extractdir=${dir}`);
516
- case "linux":
517
- return extractTar(archive, dir);
518
- case "darwin":
519
- return extractDmg(archive, dir);
520
- default:
521
- checkPlatform();
522
- }
523
- }
524
-
525
- /**
526
- * Installs the browser. The Firefox executable gets extracted in the
527
- * {@link snapshotsBaseDir} folder, ready to go.
528
- * @param {string} [version=latest] Either "latest", "beta" or a full version
529
- * number (i.e. "68.0").
530
- * @param {number} [downloadTimeout=0] Allowed time in ms for the download of
531
- * install files to complete. When set to 0 there is no time limit.
532
- * @return {BrowserBinary}
533
- * @throws {Error} Unsupported browser version, Unsupported platform, Browser
534
- * download failed.
535
- */
536
- static async installBrowser(version = "latest", downloadTimeout = 0) {
537
- const MIN_VERSION = 60;
538
-
539
- checkVersion(version, MIN_VERSION, Firefox.#CHANNELS);
540
- let versionNumber = await Firefox.#getVersionForChannel(version);
541
-
542
- let [buildPlatform, fileName] = {
543
- "win32-ia32": ["win32", `Firefox Setup ${versionNumber}.exe`],
544
- "win32-x64": ["win64", `Firefox Setup ${versionNumber}.exe`],
545
- "linux-x64": ["linux-x86_64", `firefox-${versionNumber}.tar.bz2`],
546
- "darwin-x64": ["mac", `Firefox ${versionNumber}.dmg`],
547
- "darwin-arm64": ["mac", `Firefox ${versionNumber}.dmg`]
548
- }[platformArch];
549
-
550
- let snapshotsDir = path.join(snapshotsBaseDir, "firefox");
551
- let browserDir = path.join(snapshotsDir,
552
- `firefox-${platformArch}-${versionNumber}`);
553
- let binary = Firefox.#getBinaryPath(browserDir);
554
- try {
555
- await fs.promises.access(browserDir);
556
- return {binary, versionNumber};
557
- }
558
- catch (e) {}
559
-
560
- let archive = path.join(snapshotsDir, "cache", fileName);
561
- await fs.promises.mkdir(path.dirname(browserDir), {recursive: true});
562
- try {
563
- await fs.promises.access(archive);
564
- }
565
- catch (e) {
566
- let url = `https://archive.mozilla.org/pub/firefox/releases/${versionNumber}/${buildPlatform}/en-US/${fileName}`;
567
- try {
568
- await download(url, archive, downloadTimeout);
569
- }
570
- catch (err) {
571
- throw new Error(`${BROWSER_DOWNLOAD_ERROR}: ${url}\n${err}`);
572
- }
573
- }
574
- await Firefox.#extractFirefoxArchive(archive, browserDir);
575
-
576
- return {binary, versionNumber};
577
- }
578
-
579
- /** @see Browser.getDriver */
580
- static async getDriver(version = "latest", {
581
- headless = true, extensionPaths = [], incognito = false, insecure = false,
582
- extraArgs = [], customBrowserBinary
583
- } = {}, downloadTimeout = 0) {
584
- let binary;
585
- if (!customBrowserBinary)
586
- ({binary} = await Firefox.installBrowser(version, downloadTimeout));
587
-
588
- let options = new firefox.Options();
589
- if (headless)
590
- options.headless();
591
- if (incognito)
592
- options.addArguments("--private");
593
- if (insecure)
594
- options.set("acceptInsecureCerts", true);
595
- if (extraArgs.length > 0)
596
- options.addArguments(...extraArgs);
597
- options.setBinary(customBrowserBinary || binary);
598
-
599
- let driver;
600
- // The OS may be low on resources, that's why building the driver is retried
601
- // https://github.com/mozilla/geckodriver/issues/1560
602
- await wait(async() => {
603
- try {
604
- driver = await new webdriver.Builder()
605
- .forBrowser("firefox")
606
- .setFirefoxOptions(options)
607
- .build();
608
- return true;
609
- }
610
- catch (err) {
611
- if (err.message != "Failed to decode response from marionette")
612
- throw err;
613
- await killDriverProcess("geckodriver");
614
- }
615
- }, 30000, `${DRIVER_START_ERROR}: geckodriver`, 1000);
616
-
617
- for (let extensionPath of extensionPaths) {
618
- let temporary = true; // Parameter not documented on the webdriver docs
619
- await driver.installAddon(extensionPath, temporary);
620
- }
621
- return driver;
622
- }
623
-
624
- /** @see Browser.enableExtensionInIncognito */
625
- static async enableExtensionInIncognito(driver, extensionTitle) {
626
- let version = (await driver.getCapabilities()).getBrowserVersion();
627
- // The UI workaround assumes web elements only present on Firefox >= 87
628
- checkVersion(version, 87);
629
-
630
- await driver.navigate().to("about:addons");
631
- await driver.wait(until.elementLocated(By.name("extension")), 1000).click();
632
-
633
- for (let elem of await driver.findElements(By.className("card addon"))) {
634
- let text = await elem.getAttribute("innerHTML");
635
- if (!text.includes(extensionTitle))
636
- continue;
637
-
638
- await elem.click();
639
- return await driver.findElement(By.name("private-browsing")).click();
640
- }
641
- throw new Error(`${EXTENSION_NOT_FOUND_ERROR}: ${extensionTitle}`);
642
- }
643
- }
644
-
645
- /**
646
- * Browser and webdriver functionality for Edge.
647
- * @hideconstructor
648
- * @extends Browser
649
- */
650
- class Edge extends Browser {
651
- static #CHANNELS = ["latest", "beta", "dev"];
652
-
653
- static async #getVersionForChannel(version) {
654
- let channel = "stable";
655
- if (Edge.#CHANNELS.includes(version) && version != "latest")
656
- channel = version;
657
-
658
- let url = `https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edge-${channel}/`;
659
- let body;
660
- try {
661
- ({body} = await got(url));
662
- }
663
- catch (err) {
664
- throw new Error(`${BROWSER_VERSION_CHECK_ERROR}: ${url}\n${err}`);
665
- }
666
- let versionNumber;
667
- if (Edge.#CHANNELS.includes(version)) {
668
- let regex = /href="microsoft-edge-(stable|beta|dev)_(.*?)-1_/gm;
669
- let matches;
670
- let versionNumbers = [];
671
- while ((matches = regex.exec(body)) !== null)
672
- versionNumbers.push(matches[2]);
673
-
674
- let compareVersions = (v1, v2) =>
675
- getMajorVersion(v1) < getMajorVersion(v2) ? 1 : -1;
676
- versionNumber = versionNumbers.sort(compareVersions)[0];
677
- }
678
- else {
679
- let split = version.split(".");
680
- let minorVersion = split.length == 4 ? parseInt(split.pop(), 10) : -1;
681
- let majorVersion = split.join(".");
682
- let found;
683
- while (!found && minorVersion >= 0) {
684
- versionNumber = `${majorVersion}.${minorVersion}`;
685
- found = body.includes(versionNumber);
686
- minorVersion--;
687
- }
688
- if (!found)
689
- throw new Error(`${UNSUPPORTED_VERSION_ERROR}: ${version}`);
690
- }
691
-
692
- return {versionNumber, channel};
693
- }
694
-
695
- static #darwinApp = "Microsoft Edge";
696
-
697
- static #getBinaryPath(channel = "stable") {
698
- switch (platform) {
699
- case "win32":
700
- let programFiles = process.env["ProgramFiles(x86)"] ?
701
- "${Env:ProgramFiles(x86)}" : "${Env:ProgramFiles}";
702
- return `${programFiles}\\Microsoft\\Edge\\Application\\msedge.exe`;
703
- case "linux":
704
- return channel == "stable" ?
705
- "microsoft-edge" : `microsoft-edge-${channel}`;
706
- case "darwin":
707
- return `${process.env.HOME}/Applications/${Edge.#darwinApp}.app/Contents/MacOS/${Edge.#darwinApp}`;
708
- default:
709
- checkPlatform();
710
- }
711
- }
712
-
713
- /**
714
- * Installs the browser. On Linux, Edge is installed as a system package,
715
- * which requires root permissions. On MacOS, Edge is installed as a user
716
- * app (not as a system app). Installing Edge on Windows is not supported.
717
- * @param {string} [version=latest] Either "latest", "beta", "dev" or a full
718
- * version number (i.e. "95.0.1020.40").
719
- * @param {number} [downloadTimeout=0] Allowed time in ms for the download of
720
- * install files to complete. When set to 0 there is no time limit.
721
- * @return {BrowserBinary}
722
- * @throws {Error} Unsupported browser version, Unsupported platform, Browser
723
- * download failed.
724
- */
725
- static async installBrowser(version = "latest", downloadTimeout = 0) {
726
- if (platform == "win32")
727
- // Edge is mandatory on Windows, can't be uninstalled or downgraded
728
- // https://support.microsoft.com/en-us/microsoft-edge/why-can-t-i-uninstall-microsoft-edge-ee150b3b-7d7a-9984-6d83-eb36683d526d
729
- throw new Error(`${UNSUPPORTED_PLATFORM_ERROR}: ${platform}`);
730
-
731
- if (platform == "darwin" && version != "latest")
732
- // Only latest Edge is supported on macOS
733
- throw new Error(`${UNSUPPORTED_VERSION_ERROR}: ${version}. Only "latest" is supported`);
734
-
735
- const MIN_VERSION = 95;
736
- checkVersion(version, MIN_VERSION, Edge.#CHANNELS);
737
- let {versionNumber, channel} = await Edge.#getVersionForChannel(version);
738
-
739
- let filename = {
740
- linux: `microsoft-edge-${channel}_${versionNumber}-1_amd64.deb`,
741
- darwin: `MicrosoftEdge-${versionNumber}.pkg`
742
- }[platform];
743
-
744
- let snapshotsDir = path.join(snapshotsBaseDir, "edge");
745
- let archive = path.join(snapshotsDir, "cache", filename);
746
- let binary = Edge.#getBinaryPath(channel);
747
- try {
748
- if (await Edge.#getInstalledVersionNumber(binary) == versionNumber)
749
- return {binary, versionNumber};
750
- }
751
- catch (e) {}
752
-
753
- let url = `https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edge-${channel}/${filename}`;
754
- try {
755
- if (platform == "darwin") {
756
- let caskUrl = "https://formulae.brew.sh/api/cask/microsoft-edge.json";
757
- let caskJson;
758
- try {
759
- caskJson = await got(caskUrl).json();
760
- }
761
- catch (err) {
762
- throw new Error(`${BROWSER_VERSION_CHECK_ERROR}: ${caskUrl}\n${err}`);
763
- }
764
- ({url} = process.arch == "arm64" ?
765
- caskJson.variations.arm64_ventura : caskJson);
766
- }
767
- await download(url, archive, downloadTimeout);
768
- }
769
- catch (err) {
770
- throw new Error(`${BROWSER_DOWNLOAD_ERROR}: ${url}\n${err}`);
771
- }
772
-
773
- if (platform == "linux") {
774
- await promisify(exec)(`dpkg -i ${archive}`);
775
- }
776
- else if (platform == "darwin") {
777
- await fs.promises.rm(`${process.env.HOME}/Applications/${Edge.#darwinApp}.app`, {force: true, recursive: true});
778
- await promisify(exec)(`installer -pkg ${archive} -target CurrentUserHomeDirectory`);
779
- }
780
-
781
- return {binary, versionNumber};
782
- }
783
-
784
- static async #getInstalledVersionNumber(binary) {
785
- let installedVersion = await Edge.getInstalledVersion(binary);
786
- for (let word of ["beta", "dev", "Beta", "Dev"])
787
- installedVersion = installedVersion.replace(word, "");
788
- return installedVersion.trim().replace(/.*\s/, "");
789
- }
790
-
791
- static async #installDriver(binary) {
792
- async function extractEdgeZip(archive, cacheDir, driverPath) {
793
- await killDriverProcess("msedgedriver");
794
- await fs.promises.rm(driverPath, {force: true});
795
- await extractZip(archive, {dir: cacheDir});
796
- }
797
-
798
- let binaryPath = binary || Edge.#getBinaryPath();
799
- let versionNumber = await Edge.#getInstalledVersionNumber(binaryPath);
800
- if (!versionNumber)
801
- throw new Error(`${BROWSER_NOT_INSTALLED_ERROR}. Binary path: ${binaryPath}`);
802
-
803
- let [zip, driverBinary] = {
804
- "win32-ia32": ["edgedriver_win32.zip", "msedgedriver.exe"],
805
- "win32-x64": ["edgedriver_win64.zip", "msedgedriver.exe"],
806
- "linux-x64": ["edgedriver_linux64.zip", "msedgedriver"],
807
- "darwin-x64": ["edgedriver_mac64.zip", "msedgedriver"],
808
- "darwin-arm64": ["edgedriver_mac64_m1.zip", "msedgedriver"]
809
- }[platformArch];
810
- let cacheDir = path.join(snapshotsBaseDir, "edge", "cache",
811
- `edgedriver-${versionNumber}`);
812
- let archive = path.join(cacheDir, `${versionNumber}-${zip}`);
813
- let driverPath = path.join(cacheDir, driverBinary);
814
-
815
- try {
816
- await fs.promises.access(archive);
817
- await extractEdgeZip(archive, cacheDir, driverPath);
818
- }
819
- catch (e) { // zip file is either not cached or corrupted
820
- let vSplit = versionNumber.split(".");
821
- let lastNum = parseInt(vSplit[3], 10);
822
- while (lastNum >= 0) {
823
- try {
824
- let attempt = `${vSplit[0]}.${vSplit[1]}.${vSplit[2]}.${lastNum}`;
825
- await download(`https://msedgedriver.azureedge.net/${attempt}/${zip}`,
826
- archive);
827
- break;
828
- }
829
- catch (e2) {
830
- lastNum--;
831
- }
832
- }
833
- if (lastNum < 0)
834
- throw new Error(`${DRIVER_DOWNLOAD_ERROR}: Edge ${versionNumber}`);
835
-
836
- await extractEdgeZip(archive, cacheDir, driverPath);
837
- }
838
- return driverPath;
839
- }
840
-
841
- /** @see Browser.getDriver */
842
- static async getDriver(version = "latest", {
843
- headless = true, extensionPaths = [], incognito = false, insecure = false,
844
- extraArgs = [], customBrowserBinary
845
- } = {}, downloadTimeout = 0) {
846
- let binary;
847
- if (!customBrowserBinary && (platform == "linux" || platform == "darwin"))
848
- ({binary} = await Edge.installBrowser(version, downloadTimeout));
849
-
850
- let driverPath = await Edge.#installDriver(customBrowserBinary || binary);
851
- let serviceBuilder = new edge.ServiceBuilder(driverPath);
852
-
853
- let options = new edge.Options().addArguments("no-sandbox", ...extraArgs);
854
- if (headless)
855
- options.headless();
856
- if (extensionPaths.length > 0)
857
- options.addArguments(`load-extension=${extensionPaths.join(",")}`);
858
- if (incognito)
859
- options.addArguments("inprivate");
860
- if (insecure)
861
- options.addArguments("ignore-certificate-errors");
862
-
863
- let builder = new webdriver.Builder();
864
- builder.forBrowser("MicrosoftEdge");
865
- builder.setEdgeOptions(options);
866
- builder.setEdgeService(serviceBuilder);
867
-
868
- let driver;
869
- // On Windows CI, occasionally a SessionNotCreatedError is thrown, likely
870
- // due to low OS resources, that's why building the driver is retried
871
- await wait(async() => {
872
- try {
873
- driver = await builder.build();
874
- return true;
875
- }
876
- catch (err) {
877
- if (err.name != "SessionNotCreatedError")
878
- throw err;
879
- await killDriverProcess("msedgedriver");
880
- }
881
- }, 30000, `${DRIVER_START_ERROR}: msedgedriver`, 1000);
882
-
883
- return driver;
884
- }
885
-
886
- /** @see Browser.enableExtensionInIncognito */
887
- static async enableExtensionInIncognito(driver, extensionTitle) {
888
- await driver.navigate().to("edge://extensions/");
889
- for (let elem of await driver.findElements(By.css("[role=listitem]"))) {
890
- let text = await elem.getAttribute("innerHTML");
891
- if (!text.includes(extensionTitle))
892
- continue;
893
-
894
- for (let button of await elem.findElements(By.css("button"))) {
895
- text = await elem.getAttribute("innerHTML");
896
- if (!text.includes("Details"))
897
- continue;
898
-
899
- await button.click();
900
- return await driver.findElement(By.id("itemAllowIncognito")).click();
901
- }
902
- throw new Error(`${ELEMENT_NOT_FOUND_ERROR}: Details button`);
903
- }
904
- throw new Error(`${EXTENSION_NOT_FOUND_ERROR}: ${extensionTitle}`);
905
- }
906
- }
907
-
908
- /**
909
- * @type {Object}
910
- * @property {Chromium} chromium Browser and webdriver functionality for
911
- * Chromium.
912
- * @property {Firefox} firefox Browser and webdriver functionality for Firefox.
913
- * @property {Edge} edge Browser and webdriver functionality for Edge.
914
- */
915
- export const BROWSERS = {
916
- chromium: Chromium,
917
- firefox: Firefox,
918
- edge: Edge
919
- };