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