@eyeo/get-browser-binary 0.2.0 → 0.4.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/.dockerignore +3 -0
- package/.gitlab-ci.yml +27 -20
- package/CODE_OF_CONDUCT.md +132 -0
- package/README.md +47 -9
- package/RELEASE_NOTES.md +14 -0
- package/index.js +1 -0
- package/package.json +4 -4
- package/src/browsers.js +430 -87
- package/src/utils.js +80 -9
- package/test/browsers.js +163 -0
- package/test/docker/Dockerfile +20 -0
- package/test/docker/entrypoint.sh +4 -0
- package/test/utils.js +42 -0
- package/test/runner.js +0 -178
package/src/browsers.js
CHANGED
|
@@ -16,9 +16,10 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import path from "path";
|
|
19
|
-
import {exec, execFile} from "child_process";
|
|
19
|
+
import {exec, execFile, spawn} from "child_process";
|
|
20
20
|
import {promisify} from "util";
|
|
21
21
|
import fs from "fs";
|
|
22
|
+
import fsExtra from "fs-extra";
|
|
22
23
|
|
|
23
24
|
import got from "got";
|
|
24
25
|
import webdriver from "selenium-webdriver";
|
|
@@ -27,8 +28,8 @@ import firefox from "selenium-webdriver/firefox.js";
|
|
|
27
28
|
import command from "selenium-webdriver/lib/command.js";
|
|
28
29
|
import extractZip from "extract-zip";
|
|
29
30
|
|
|
30
|
-
import {download, extractTar, extractDmg,
|
|
31
|
-
|
|
31
|
+
import {download, extractTar, extractDmg, getBrowserVersion, killDriverProcess,
|
|
32
|
+
wait} from "./utils.js";
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
35
|
* Root folder where browser and webdriver binaries get downloaded.
|
|
@@ -37,9 +38,9 @@ import {download, extractTar, extractDmg, runWinInstaller, getBrowserVersion}
|
|
|
37
38
|
export let snapshotsBaseDir = path.join(process.cwd(), "browser-snapshots");
|
|
38
39
|
|
|
39
40
|
let {until, By} = webdriver;
|
|
40
|
-
const ERROR_INCOGNITO_NOT_SUPPORTED = "Incognito mode is not supported";
|
|
41
41
|
const ERROR_DOWNLOAD_NOT_SUPPORTED =
|
|
42
42
|
"Downloading this browser is not supported";
|
|
43
|
+
let platform = `${process.platform}-${process.arch}`;
|
|
43
44
|
|
|
44
45
|
/**
|
|
45
46
|
* Base class for browser download functionality. Please see subclasses for
|
|
@@ -49,7 +50,9 @@ const ERROR_DOWNLOAD_NOT_SUPPORTED =
|
|
|
49
50
|
class Browser {
|
|
50
51
|
/**
|
|
51
52
|
* @typedef {Object} BrowserBinary
|
|
52
|
-
* @property {string} binary - The path to the
|
|
53
|
+
* @property {string} binary - The path to the browser binary.
|
|
54
|
+
* @property {string} versionNumber - The version number of the browser
|
|
55
|
+
* binary.
|
|
53
56
|
*/
|
|
54
57
|
|
|
55
58
|
/**
|
|
@@ -62,6 +65,25 @@ class Browser {
|
|
|
62
65
|
throw new Error(ERROR_DOWNLOAD_NOT_SUPPORTED);
|
|
63
66
|
}
|
|
64
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Gets the installed version returned by the browser binary.
|
|
70
|
+
* @param {string} binary - The path to the browser binary.
|
|
71
|
+
* @return {string} Installed browser version.
|
|
72
|
+
*/
|
|
73
|
+
static async getInstalledVersion(binary) {
|
|
74
|
+
let stdout;
|
|
75
|
+
if (process.platform == "win32") {
|
|
76
|
+
({stdout} = await promisify(exec)(
|
|
77
|
+
`(Get-ItemProperty ${binary}).VersionInfo.ProductVersion`,
|
|
78
|
+
{shell: "powershell.exe"})
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
({stdout} = await promisify(execFile)(binary, ["--version"]));
|
|
83
|
+
}
|
|
84
|
+
return stdout;
|
|
85
|
+
}
|
|
86
|
+
|
|
65
87
|
/**
|
|
66
88
|
* @typedef {Object} webdriver
|
|
67
89
|
* @see https://www.selenium.dev/selenium/docs/api/javascript/index.html
|
|
@@ -89,6 +111,7 @@ class Browser {
|
|
|
89
111
|
* no effect.
|
|
90
112
|
* @param {driverOptions?} options - Options to start the browser with.
|
|
91
113
|
* @return {webdriver}
|
|
114
|
+
* @throws {Error} Unsupported webdriver version.
|
|
92
115
|
*/
|
|
93
116
|
static async getDriver(version, options = {}) {
|
|
94
117
|
// to be implemented by the subclass
|
|
@@ -102,7 +125,10 @@ class Browser {
|
|
|
102
125
|
* @return {webdriver}
|
|
103
126
|
*/
|
|
104
127
|
static async enableExtensionInIncognito(driver, extensionTitle) {
|
|
105
|
-
|
|
128
|
+
// Allowing the extension in incognito mode can't happen programmatically:
|
|
129
|
+
// https://stackoverflow.com/questions/57419654
|
|
130
|
+
// https://bugzilla.mozilla.org/show_bug.cgi?id=1729315
|
|
131
|
+
// That is done through the UI, to be implemented by the subclass
|
|
106
132
|
}
|
|
107
133
|
}
|
|
108
134
|
|
|
@@ -159,17 +185,13 @@ class Chromium extends Browser {
|
|
|
159
185
|
|
|
160
186
|
let revision = parseInt(chromiumRevision, 10);
|
|
161
187
|
let startingRevision = revision;
|
|
162
|
-
let
|
|
163
|
-
|
|
164
|
-
let buildTypes = {
|
|
188
|
+
let [platformDir, fileName] = {
|
|
165
189
|
"win32-ia32": ["Win", "chrome-win.zip"],
|
|
166
190
|
"win32-x64": ["Win_x64", "chrome-win.zip"],
|
|
167
191
|
"linux-x64": ["Linux_x64", "chrome-linux.zip"],
|
|
168
192
|
"darwin-x64": ["Mac", "chrome-mac.zip"],
|
|
169
193
|
"dawrin-arm64": ["Mac_Arm", "chrome-mac.zip"]
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
let [platformDir, fileName] = buildTypes[platform];
|
|
194
|
+
}[platform];
|
|
173
195
|
let archive = null;
|
|
174
196
|
let browserDir = null;
|
|
175
197
|
let snapshotsDir = path.join(snapshotsBaseDir, "chromium");
|
|
@@ -225,19 +247,17 @@ class Chromium extends Browser {
|
|
|
225
247
|
({version, base} = await Chromium.#getLatestVersion(version));
|
|
226
248
|
|
|
227
249
|
let {binary, revision} = await Chromium.#downloadChromium(base);
|
|
228
|
-
return {binary,
|
|
250
|
+
return {binary, versionNumber: version, revision};
|
|
229
251
|
}
|
|
230
252
|
|
|
231
253
|
static async #installDriver(revision, version) {
|
|
232
|
-
let
|
|
233
|
-
let buildTypes = {
|
|
254
|
+
let [dir, zip, driver] = {
|
|
234
255
|
"win32-ia32": ["Win", "chromedriver_win32.zip", "chromedriver.exe"],
|
|
235
256
|
"win32-x64": ["Win_x64", "chromedriver_win32.zip", "chromedriver.exe"],
|
|
236
257
|
"linux-x64": ["Linux_x64", "chromedriver_linux64.zip", "chromedriver"],
|
|
237
258
|
"darwin-x64": ["Mac", "chromedriver_mac64.zip", "chromedriver"],
|
|
238
259
|
"darwin-arm64": ["Mac_Arm", "chromedriver_mac64.zip", "chromedriver"]
|
|
239
|
-
};
|
|
240
|
-
let [dir, zip, driver] = buildTypes[platform];
|
|
260
|
+
}[platform];
|
|
241
261
|
|
|
242
262
|
let cacheDir = path.join(snapshotsBaseDir, "chromium", "cache", version);
|
|
243
263
|
let destinationDir = path.join(process.cwd(), "node_modules",
|
|
@@ -247,28 +267,19 @@ class Chromium extends Browser {
|
|
|
247
267
|
await download(`https://commondatastorage.googleapis.com/chromium-browser-snapshots/${dir}/${revision}/${zip}`,
|
|
248
268
|
archive);
|
|
249
269
|
await extractZip(archive, {dir: cacheDir});
|
|
270
|
+
await killDriverProcess(Chromium.#DRIVER);
|
|
250
271
|
await fs.promises.mkdir(destinationDir, {recursive: true});
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
path.join(destinationDir, driver));
|
|
254
|
-
}
|
|
255
|
-
catch (err) {
|
|
256
|
-
// https://gitlab.com/eyeo/developer-experience/get-browser-binary/-/issues/3
|
|
257
|
-
if (!err.toString().includes("copyfile"))
|
|
258
|
-
throw err;
|
|
259
|
-
}
|
|
272
|
+
await fs.promises.copyFile(path.join(cacheDir, zip.split(".")[0], driver),
|
|
273
|
+
path.join(destinationDir, driver));
|
|
260
274
|
}
|
|
261
275
|
|
|
262
276
|
/** @see Browser.getDriver */
|
|
263
277
|
static async getDriver(version, {headless = true, extensionPaths = [],
|
|
264
278
|
incognito = false, insecure = false,
|
|
265
279
|
extraArgs = []} = {}) {
|
|
266
|
-
|
|
267
|
-
throw new Error(ERROR_INCOGNITO_NOT_SUPPORTED);
|
|
268
|
-
|
|
269
|
-
let {binary, revision, downloadedVersion} =
|
|
280
|
+
let {binary, revision, versionNumber} =
|
|
270
281
|
await Chromium.downloadBinary(version);
|
|
271
|
-
await Chromium.#installDriver(revision,
|
|
282
|
+
await Chromium.#installDriver(revision, versionNumber);
|
|
272
283
|
|
|
273
284
|
let options = new chrome.Options().addArguments("no-sandbox", ...extraArgs);
|
|
274
285
|
if (extensionPaths.length > 0)
|
|
@@ -277,6 +288,8 @@ class Chromium extends Browser {
|
|
|
277
288
|
options.headless();
|
|
278
289
|
if (insecure)
|
|
279
290
|
options.addArguments("ignore-certificate-errors");
|
|
291
|
+
if (incognito)
|
|
292
|
+
options.addArguments("incognito");
|
|
280
293
|
options.setChromeBinaryPath(binary);
|
|
281
294
|
|
|
282
295
|
let builder = new webdriver.Builder();
|
|
@@ -285,6 +298,42 @@ class Chromium extends Browser {
|
|
|
285
298
|
|
|
286
299
|
return builder.build();
|
|
287
300
|
}
|
|
301
|
+
|
|
302
|
+
/** @see Browser.enableExtensionInIncognito */
|
|
303
|
+
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
|
+
await driver.navigate().to("chrome://extensions");
|
|
311
|
+
await driver.executeScript(`
|
|
312
|
+
let enable = () => document.querySelector("extensions-manager").shadowRoot
|
|
313
|
+
.querySelector("extensions-detail-view").shadowRoot
|
|
314
|
+
.getElementById("allow-incognito").shadowRoot
|
|
315
|
+
.getElementById("crToggle").click();
|
|
316
|
+
|
|
317
|
+
let extensions = document.querySelector("extensions-manager").shadowRoot
|
|
318
|
+
.getElementById("items-list").shadowRoot
|
|
319
|
+
.querySelectorAll("extensions-item");
|
|
320
|
+
|
|
321
|
+
return new Promise((resolve, reject) => {
|
|
322
|
+
let extensionDetails;
|
|
323
|
+
for (let {shadowRoot} of extensions) {
|
|
324
|
+
if (shadowRoot.getElementById("name").innerHTML != arguments[0])
|
|
325
|
+
continue;
|
|
326
|
+
|
|
327
|
+
extensionDetails = shadowRoot.getElementById("detailsButton");
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (!extensionDetails)
|
|
331
|
+
reject("Extension was not found");
|
|
332
|
+
|
|
333
|
+
extensionDetails.click();
|
|
334
|
+
setTimeout(() => resolve(enable()), 100);
|
|
335
|
+
});`, extensionTitle);
|
|
336
|
+
}
|
|
288
337
|
}
|
|
289
338
|
|
|
290
339
|
/**
|
|
@@ -292,7 +341,7 @@ class Chromium extends Browser {
|
|
|
292
341
|
* @hideconstructor
|
|
293
342
|
* @extends Browser
|
|
294
343
|
*/
|
|
295
|
-
class Firefox {
|
|
344
|
+
class Firefox extends Browser {
|
|
296
345
|
static async #getLatestVersion(branch) {
|
|
297
346
|
let data = await got("https://product-details.mozilla.org/1.0/firefox_versions.json").json();
|
|
298
347
|
return branch == "beta" ?
|
|
@@ -315,7 +364,9 @@ class Firefox {
|
|
|
315
364
|
static #extractFirefoxArchive(archive, dir) {
|
|
316
365
|
switch (process.platform) {
|
|
317
366
|
case "win32":
|
|
318
|
-
|
|
367
|
+
// Procedure inspired from mozinstall:
|
|
368
|
+
// https://hg.mozilla.org/mozilla-central/file/tip/testing/mozbase/mozinstall/mozinstall/mozinstall.py
|
|
369
|
+
return promisify(exec)(`"${archive}" /extractdir=${dir}`);
|
|
319
370
|
case "linux":
|
|
320
371
|
return extractTar(archive, dir);
|
|
321
372
|
case "darwin":
|
|
@@ -326,18 +377,18 @@ class Firefox {
|
|
|
326
377
|
}
|
|
327
378
|
|
|
328
379
|
static async #downloadFirefox(version) {
|
|
329
|
-
let
|
|
330
|
-
if (platform == "win32")
|
|
331
|
-
platform += "-" + process.arch;
|
|
332
|
-
let buildTypes = {
|
|
380
|
+
let [buildPlatform, fileName] = {
|
|
333
381
|
"win32-ia32": ["win32", `Firefox Setup ${version}.exe`],
|
|
334
382
|
"win32-x64": ["win64", `Firefox Setup ${version}.exe`],
|
|
335
|
-
"linux": ["linux-x86_64", `firefox-${version}.tar.bz2`],
|
|
336
|
-
"darwin": ["mac", `Firefox ${version}.dmg`]
|
|
337
|
-
|
|
383
|
+
"linux-x64": ["linux-x86_64", `firefox-${version}.tar.bz2`],
|
|
384
|
+
"darwin-x64": ["mac", `Firefox ${version}.dmg`],
|
|
385
|
+
"darwin-arm64": ["mac", `Firefox ${version}.dmg`]
|
|
386
|
+
}[platform];
|
|
338
387
|
|
|
339
388
|
let snapshotsDir = path.join(snapshotsBaseDir, "firefox");
|
|
340
389
|
let browserDir = path.join(snapshotsDir, `firefox-${platform}-${version}`);
|
|
390
|
+
let archive = path.join(snapshotsDir, "cache", fileName);
|
|
391
|
+
|
|
341
392
|
try {
|
|
342
393
|
await fs.promises.access(browserDir);
|
|
343
394
|
return Firefox.#getFirefoxBinary(browserDir);
|
|
@@ -345,10 +396,6 @@ class Firefox {
|
|
|
345
396
|
catch (e) {}
|
|
346
397
|
|
|
347
398
|
await fs.promises.mkdir(path.dirname(browserDir), {recursive: true});
|
|
348
|
-
|
|
349
|
-
let [buildPlatform, fileName] = buildTypes[platform];
|
|
350
|
-
let archive = path.join(snapshotsDir, "cache", fileName);
|
|
351
|
-
|
|
352
399
|
try {
|
|
353
400
|
await fs.promises.access(archive);
|
|
354
401
|
}
|
|
@@ -372,7 +419,7 @@ class Firefox {
|
|
|
372
419
|
version = await Firefox.#getLatestVersion(version);
|
|
373
420
|
|
|
374
421
|
let binary = await Firefox.#downloadFirefox(version);
|
|
375
|
-
return {binary};
|
|
422
|
+
return {binary, versionNumber: version};
|
|
376
423
|
}
|
|
377
424
|
|
|
378
425
|
/** @see Browser.getDriver */
|
|
@@ -392,10 +439,23 @@ class Firefox {
|
|
|
392
439
|
options.addArguments(...extraArgs);
|
|
393
440
|
options.setBinary(binary);
|
|
394
441
|
|
|
395
|
-
let driver
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
442
|
+
let driver;
|
|
443
|
+
// The OS may be low on resources, that's why building the driver is retried
|
|
444
|
+
// https://github.com/mozilla/geckodriver/issues/1560
|
|
445
|
+
await wait(async() => {
|
|
446
|
+
try {
|
|
447
|
+
driver = await new webdriver.Builder()
|
|
448
|
+
.forBrowser("firefox")
|
|
449
|
+
.setFirefoxOptions(options)
|
|
450
|
+
.build();
|
|
451
|
+
return true;
|
|
452
|
+
}
|
|
453
|
+
catch (err) {
|
|
454
|
+
if (err.message != "Failed to decode response from marionette")
|
|
455
|
+
throw err;
|
|
456
|
+
await killDriverProcess("geckodriver");
|
|
457
|
+
}
|
|
458
|
+
}, 30000, "geckodriver didn't start, likely due to low OS resources", 1000);
|
|
399
459
|
|
|
400
460
|
for (let extensionPath of extensionPaths) {
|
|
401
461
|
await driver.execute(
|
|
@@ -409,14 +469,10 @@ class Firefox {
|
|
|
409
469
|
|
|
410
470
|
/** @see Browser.enableExtensionInIncognito */
|
|
411
471
|
static async enableExtensionInIncognito(driver, extensionTitle) {
|
|
412
|
-
// Allowing the extension in private browsing can't happen programmatically:
|
|
413
|
-
// https://bugzilla.mozilla.org/show_bug.cgi?id=1729315
|
|
414
|
-
// Therefore, that is done through the UI
|
|
415
472
|
let version = await getBrowserVersion(driver);
|
|
416
|
-
if (version < 87)
|
|
473
|
+
if (version < 87)
|
|
417
474
|
// The UI workaround assumes web elements only present on Firefox >= 87
|
|
418
475
|
throw new Error(`Only supported on Firefox >= 87. Current version: ${version}`);
|
|
419
|
-
}
|
|
420
476
|
|
|
421
477
|
await driver.navigate().to("about:addons");
|
|
422
478
|
await driver.wait(until.elementLocated(By.name("extension")), 1000).click();
|
|
@@ -438,41 +494,94 @@ class Firefox {
|
|
|
438
494
|
* @hideconstructor
|
|
439
495
|
* @extends Browser
|
|
440
496
|
*/
|
|
441
|
-
class Edge {
|
|
497
|
+
class Edge extends Browser {
|
|
442
498
|
static #DRIVER = "msedgedriver";
|
|
443
499
|
|
|
444
|
-
static async #
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
500
|
+
static async #getVersionForChannel(version) {
|
|
501
|
+
if (!["latest", "beta", "dev"].includes(version))
|
|
502
|
+
return {versionNumber: version, channel: "stable"};
|
|
503
|
+
|
|
504
|
+
let channel = version == "latest" ? "stable" : version;
|
|
505
|
+
let {body} = await got(`https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edge-${channel}`);
|
|
506
|
+
let regex = /href="microsoft-edge-(stable|beta|dev)_(.*?)-1_/gm;
|
|
507
|
+
let matches;
|
|
508
|
+
let versionNumbers = [];
|
|
509
|
+
while ((matches = regex.exec(body)) !== null)
|
|
510
|
+
versionNumbers.push(matches[2]);
|
|
511
|
+
|
|
512
|
+
let compareVersions = (v1, v2) =>
|
|
513
|
+
parseInt(v1.split(".")[0], 10) < parseInt(v2.split(".")[0], 10) ? 1 : -1;
|
|
514
|
+
let versionNumber = versionNumbers.sort(compareVersions)[0];
|
|
515
|
+
|
|
516
|
+
return {versionNumber, channel};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Downloads the browser binary file.
|
|
521
|
+
* @param {string} version - Either "latest", "beta", "dev" or a full version
|
|
522
|
+
* number (i.e. "95.0.1020.53"). Defaults to "latest". This is only
|
|
523
|
+
* available on Linux.
|
|
524
|
+
* @return {BrowserBinary}
|
|
525
|
+
*/
|
|
526
|
+
static async downloadBinary(version = "latest") {
|
|
527
|
+
if (process.platform != "linux")
|
|
528
|
+
throw new Error("Edge download is only supported on Linux");
|
|
529
|
+
|
|
530
|
+
let {versionNumber, channel} = await Edge.#getVersionForChannel(version);
|
|
531
|
+
|
|
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"
|
|
539
|
+
}[channel];
|
|
540
|
+
|
|
541
|
+
try {
|
|
542
|
+
if (await Edge.#getInstalledVersionNumber(binary) == versionNumber)
|
|
543
|
+
return {binary, versionNumber};
|
|
450
544
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
545
|
+
catch (e) {}
|
|
546
|
+
|
|
547
|
+
try {
|
|
548
|
+
await download(
|
|
549
|
+
`https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edge-${channel}/${filename}`,
|
|
550
|
+
archive
|
|
551
|
+
);
|
|
456
552
|
}
|
|
457
|
-
|
|
458
|
-
(
|
|
553
|
+
catch (err) {
|
|
554
|
+
throw new Error(`Edge download failed: ${err}`);
|
|
459
555
|
}
|
|
556
|
+
await promisify(exec)(`dpkg -i ${archive}`);
|
|
460
557
|
|
|
461
|
-
|
|
558
|
+
return {binary, versionNumber};
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
static async #getInstalledVersionNumber(binary) {
|
|
562
|
+
let installedVersion = await Edge.getInstalledVersion(binary);
|
|
563
|
+
return installedVersion.replace("beta", "").replace("dev", "")
|
|
564
|
+
.trim().replace(/.*\s/, "");
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
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);
|
|
462
575
|
if (!version)
|
|
463
576
|
throw new Error("Edge is not installed");
|
|
464
577
|
|
|
465
|
-
|
|
466
|
-
// mechanism when msedgedriver doesn't exist for the latest Edge version.
|
|
467
|
-
let platform = `${process.platform}-${process.arch}`;
|
|
468
|
-
let buildTypes = {
|
|
578
|
+
let [zip, driver] = {
|
|
469
579
|
"win32-ia32": ["edgedriver_win32.zip", "msedgedriver.exe"],
|
|
470
580
|
"win32-x64": ["edgedriver_win64.zip", "msedgedriver.exe"],
|
|
471
581
|
"linux-x64": ["edgedriver_linux64.zip", "msedgedriver"],
|
|
472
582
|
"darwin-x64": ["edgedriver_mac64.zip", "msedgedriver"],
|
|
473
583
|
"darwin-arm64": ["edgedriver_arm64.zip", "msedgedriver"]
|
|
474
|
-
};
|
|
475
|
-
let [zip, driver] = buildTypes[platform];
|
|
584
|
+
}[platform];
|
|
476
585
|
let cacheDir = path.join(snapshotsBaseDir, "edge", "cache");
|
|
477
586
|
let driverBinDir = path.join(process.cwd(), "node_modules", Edge.#DRIVER,
|
|
478
587
|
"bin");
|
|
@@ -498,17 +607,11 @@ class Edge {
|
|
|
498
607
|
throw new Error(`msedgedriver was not found for Edge ${version}`);
|
|
499
608
|
|
|
500
609
|
await extractZip(archive, {dir: cacheDir});
|
|
610
|
+
await killDriverProcess(Edge.#DRIVER);
|
|
501
611
|
for (let destinationDir of [driverBinDir, driverLibDir]) {
|
|
502
612
|
await fs.promises.mkdir(destinationDir, {recursive: true});
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
path.join(destinationDir, driver));
|
|
506
|
-
}
|
|
507
|
-
catch (err) {
|
|
508
|
-
// https://gitlab.com/eyeo/developer-experience/get-browser-binary/-/issues/3
|
|
509
|
-
if (!err.toString().includes("copyfile"))
|
|
510
|
-
throw err;
|
|
511
|
-
}
|
|
613
|
+
await fs.promises.copyFile(path.join(cacheDir, driver),
|
|
614
|
+
path.join(destinationDir, driver));
|
|
512
615
|
}
|
|
513
616
|
}
|
|
514
617
|
|
|
@@ -516,8 +619,8 @@ class Edge {
|
|
|
516
619
|
static async getDriver(version, {headless = true, extensionPaths = [],
|
|
517
620
|
incognito = false, insecure = false,
|
|
518
621
|
extraArgs = []} = {}) {
|
|
519
|
-
if (
|
|
520
|
-
|
|
622
|
+
if (process.platform == "linux")
|
|
623
|
+
await Edge.downloadBinary(version);
|
|
521
624
|
|
|
522
625
|
await Edge.#installDriver();
|
|
523
626
|
|
|
@@ -526,6 +629,8 @@ class Edge {
|
|
|
526
629
|
args.push("headless");
|
|
527
630
|
if (extensionPaths.length > 0)
|
|
528
631
|
args.push(`load-extension=${extensionPaths.join(",")}`);
|
|
632
|
+
if (incognito)
|
|
633
|
+
args.push("incognito");
|
|
529
634
|
|
|
530
635
|
let builder = new webdriver.Builder();
|
|
531
636
|
builder.forBrowser("MicrosoftEdge");
|
|
@@ -538,8 +643,245 @@ class Edge {
|
|
|
538
643
|
|
|
539
644
|
return builder.build();
|
|
540
645
|
}
|
|
646
|
+
|
|
647
|
+
/** @see Browser.enableExtensionInIncognito */
|
|
648
|
+
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
|
+
await driver.navigate().to("edge://extensions/");
|
|
655
|
+
for (let elem of await driver.findElements(By.css("[role=listitem]"))) {
|
|
656
|
+
let text = await elem.getAttribute("innerHTML");
|
|
657
|
+
if (!text.includes(extensionTitle))
|
|
658
|
+
continue;
|
|
659
|
+
|
|
660
|
+
for (let button of await elem.findElements(By.css("button"))) {
|
|
661
|
+
text = await elem.getAttribute("innerHTML");
|
|
662
|
+
if (!text.includes("Details"))
|
|
663
|
+
continue;
|
|
664
|
+
|
|
665
|
+
await button.click();
|
|
666
|
+
return await driver.findElement(By.id("itemAllowIncognito")).click();
|
|
667
|
+
}
|
|
668
|
+
throw new Error("Details button not found");
|
|
669
|
+
}
|
|
670
|
+
throw new Error(`Extension "${extensionTitle}" not found`);
|
|
671
|
+
}
|
|
541
672
|
}
|
|
542
673
|
|
|
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
|
+
}
|
|
543
885
|
|
|
544
886
|
/**
|
|
545
887
|
* @type {Object}
|
|
@@ -550,5 +892,6 @@ class Edge {
|
|
|
550
892
|
export const BROWSERS = {
|
|
551
893
|
chromium: Chromium,
|
|
552
894
|
firefox: Firefox,
|
|
553
|
-
edge: Edge
|
|
895
|
+
edge: Edge,
|
|
896
|
+
opera: Opera
|
|
554
897
|
};
|