@eyeo/get-browser-binary 0.3.0 → 0.5.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/.eslintrc.json +2 -1
- package/.gitlab-ci.yml +18 -13
- package/CODE_OF_CONDUCT.md +132 -0
- package/README.md +40 -11
- package/RELEASE_NOTES.md +14 -0
- package/package.json +1 -2
- package/src/browsers.js +499 -116
- package/src/utils.js +51 -8
- package/test/.eslintrc.json +2 -1
- package/test/browsers.js +136 -75
- package/test/docker/Dockerfile +9 -0
- package/test/docker/entrypoint.sh +4 -0
package/src/browsers.js
CHANGED
|
@@ -16,19 +16,21 @@
|
|
|
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";
|
|
25
26
|
import chrome from "selenium-webdriver/chrome.js";
|
|
26
27
|
import firefox from "selenium-webdriver/firefox.js";
|
|
28
|
+
import edge from "selenium-webdriver/edge.js";
|
|
27
29
|
import command from "selenium-webdriver/lib/command.js";
|
|
28
30
|
import extractZip from "extract-zip";
|
|
29
31
|
|
|
30
|
-
import {download, extractTar, extractDmg,
|
|
31
|
-
|
|
32
|
+
import {download, extractTar, extractDmg, getBrowserVersion, killDriverProcess,
|
|
33
|
+
wait} from "./utils.js";
|
|
32
34
|
|
|
33
35
|
/**
|
|
34
36
|
* Root folder where browser and webdriver binaries get downloaded.
|
|
@@ -37,10 +39,17 @@ import {download, extractTar, extractDmg, runWinInstaller, getBrowserVersion,
|
|
|
37
39
|
export let snapshotsBaseDir = path.join(process.cwd(), "browser-snapshots");
|
|
38
40
|
|
|
39
41
|
let {until, By} = webdriver;
|
|
40
|
-
const ERROR_DOWNLOAD_NOT_SUPPORTED =
|
|
41
|
-
"Downloading this browser is not supported";
|
|
42
42
|
let platform = `${process.platform}-${process.arch}`;
|
|
43
43
|
|
|
44
|
+
function checkVersion(version, minVersion, channels) {
|
|
45
|
+
if (channels.includes(version))
|
|
46
|
+
return;
|
|
47
|
+
|
|
48
|
+
let mainVersion = parseInt(version && version.split(".")[0], 10);
|
|
49
|
+
if (mainVersion < minVersion)
|
|
50
|
+
throw new Error(`Unsupported browser version: ${version}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
44
53
|
/**
|
|
45
54
|
* Base class for browser download functionality. Please see subclasses for
|
|
46
55
|
* browser specific details.
|
|
@@ -49,7 +58,9 @@ let platform = `${process.platform}-${process.arch}`;
|
|
|
49
58
|
class Browser {
|
|
50
59
|
/**
|
|
51
60
|
* @typedef {Object} BrowserBinary
|
|
52
|
-
* @property {string} binary - The path to the
|
|
61
|
+
* @property {string} binary - The path to the browser binary.
|
|
62
|
+
* @property {string} versionNumber - The version number of the browser
|
|
63
|
+
* binary.
|
|
53
64
|
*/
|
|
54
65
|
|
|
55
66
|
/**
|
|
@@ -57,9 +68,29 @@ class Browser {
|
|
|
57
68
|
* @param {string} version - Either full version number or channel/release.
|
|
58
69
|
* Please find examples on the subclasses.
|
|
59
70
|
* @return {BrowserBinary}
|
|
71
|
+
* @throws {Error} Unsupported browser version.
|
|
60
72
|
*/
|
|
61
73
|
static async downloadBinary(version) {
|
|
62
|
-
|
|
74
|
+
// to be implemented by the subclass
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Gets the installed version returned by the browser binary.
|
|
79
|
+
* @param {string} binary - The path to the browser binary.
|
|
80
|
+
* @return {string} Installed browser version.
|
|
81
|
+
*/
|
|
82
|
+
static async getInstalledVersion(binary) {
|
|
83
|
+
let stdout;
|
|
84
|
+
if (process.platform == "win32") {
|
|
85
|
+
({stdout} = await promisify(exec)(
|
|
86
|
+
`(Get-ItemProperty ${binary}).VersionInfo.ProductVersion`,
|
|
87
|
+
{shell: "powershell.exe"})
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
({stdout} = await promisify(execFile)(binary, ["--version"]));
|
|
92
|
+
}
|
|
93
|
+
return stdout.trim();
|
|
63
94
|
}
|
|
64
95
|
|
|
65
96
|
/**
|
|
@@ -85,11 +116,10 @@ class Browser {
|
|
|
85
116
|
* Installs the webdriver matching the browser version and runs the
|
|
86
117
|
* browser. If needed, the browser binary is also installed.
|
|
87
118
|
* @param {string} version - Either full version number or channel/release.
|
|
88
|
-
* Please find examples on the subclasses.
|
|
89
|
-
* no effect.
|
|
119
|
+
* Please find examples on the subclasses.
|
|
90
120
|
* @param {driverOptions?} options - Options to start the browser with.
|
|
91
121
|
* @return {webdriver}
|
|
92
|
-
* @throws {Error} Unsupported
|
|
122
|
+
* @throws {Error} Unsupported browser version.
|
|
93
123
|
*/
|
|
94
124
|
static async getDriver(version, options = {}) {
|
|
95
125
|
// to be implemented by the subclass
|
|
@@ -101,6 +131,7 @@ class Browser {
|
|
|
101
131
|
* @param {webdriver} driver - The driver controlling the browser.
|
|
102
132
|
* @param {string} extensionTitle - Title of the extebsion to be enabled.
|
|
103
133
|
* @return {webdriver}
|
|
134
|
+
* @throws {Error} Unsupported browser version.
|
|
104
135
|
*/
|
|
105
136
|
static async enableExtensionInIncognito(driver, extensionTitle) {
|
|
106
137
|
// Allowing the extension in incognito mode can't happen programmatically:
|
|
@@ -123,28 +154,30 @@ class Chromium extends Browser {
|
|
|
123
154
|
return data.chromium_base_position;
|
|
124
155
|
}
|
|
125
156
|
|
|
126
|
-
static async #
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
os = process.arch == "x64" ? "win64" : "win";
|
|
130
|
-
else if (os == "darwin")
|
|
131
|
-
os = process.arch == "arm64" ? "mac_arm64" : "mac";
|
|
157
|
+
static async #getVersionForChannel(channel) {
|
|
158
|
+
if (channel == "latest")
|
|
159
|
+
channel = "stable";
|
|
132
160
|
|
|
161
|
+
let os = {
|
|
162
|
+
"win32-ia32": "win",
|
|
163
|
+
"win32-x64": "win64",
|
|
164
|
+
"linux-x64": "linux",
|
|
165
|
+
"darwin-x64": "mac",
|
|
166
|
+
"dawrin-arm64": "mac_arm64"
|
|
167
|
+
}[platform];
|
|
133
168
|
let data = await got(`https://omahaproxy.appspot.com/all.json?os=${os}`).json();
|
|
134
169
|
let release = data[0].versions.find(ver => ver.channel == channel);
|
|
135
|
-
let {current_version: version
|
|
170
|
+
let {current_version: version} = release;
|
|
136
171
|
|
|
137
|
-
if (release.true_branch && release.true_branch.includes("_"))
|
|
172
|
+
if (release.true_branch && release.true_branch.includes("_"))
|
|
138
173
|
// A wrong base may be caused by a mini-branch (patched) release
|
|
139
174
|
// In that case, the base is taken from the unpatched version
|
|
140
175
|
version = [...version.split(".").slice(0, 3), "0"].join(".");
|
|
141
|
-
base = await Chromium.#getBranchBasePosition(version);
|
|
142
|
-
}
|
|
143
176
|
|
|
144
|
-
return
|
|
177
|
+
return version;
|
|
145
178
|
}
|
|
146
179
|
|
|
147
|
-
static #
|
|
180
|
+
static #getBinaryPath(dir) {
|
|
148
181
|
switch (process.platform) {
|
|
149
182
|
case "win32":
|
|
150
183
|
return path.join(dir, "chrome-win", "chrome.exe");
|
|
@@ -179,7 +212,7 @@ class Chromium extends Browser {
|
|
|
179
212
|
|
|
180
213
|
try {
|
|
181
214
|
await fs.promises.access(browserDir);
|
|
182
|
-
return {binary: Chromium.#
|
|
215
|
+
return {binary: Chromium.#getBinaryPath(browserDir), revision};
|
|
183
216
|
}
|
|
184
217
|
catch (e) {}
|
|
185
218
|
|
|
@@ -208,7 +241,7 @@ class Chromium extends Browser {
|
|
|
208
241
|
}
|
|
209
242
|
|
|
210
243
|
await extractZip(archive, {dir: browserDir});
|
|
211
|
-
return {binary: Chromium.#
|
|
244
|
+
return {binary: Chromium.#getBinaryPath(browserDir), revision};
|
|
212
245
|
}
|
|
213
246
|
|
|
214
247
|
/**
|
|
@@ -217,15 +250,19 @@ class Chromium extends Browser {
|
|
|
217
250
|
* number (i.e. "77.0.3865.0"). Defaults to "latest".
|
|
218
251
|
* @return {BrowserBinary}
|
|
219
252
|
*/
|
|
220
|
-
static async downloadBinary(version) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
253
|
+
static async downloadBinary(version = "latest") {
|
|
254
|
+
const MIN_VERSION = 75;
|
|
255
|
+
const CHANNELS = ["latest", "beta", "dev"];
|
|
256
|
+
|
|
257
|
+
checkVersion(version, MIN_VERSION, CHANNELS);
|
|
258
|
+
|
|
259
|
+
let versionNumber = CHANNELS.includes(version) ?
|
|
260
|
+
await Chromium.#getVersionForChannel(version) : version;
|
|
261
|
+
|
|
262
|
+
let base = await Chromium.#getBranchBasePosition(versionNumber);
|
|
226
263
|
|
|
227
264
|
let {binary, revision} = await Chromium.#downloadChromium(base);
|
|
228
|
-
return {binary,
|
|
265
|
+
return {binary, versionNumber, revision};
|
|
229
266
|
}
|
|
230
267
|
|
|
231
268
|
static async #installDriver(revision, version) {
|
|
@@ -245,7 +282,6 @@ class Chromium extends Browser {
|
|
|
245
282
|
await download(`https://commondatastorage.googleapis.com/chromium-browser-snapshots/${dir}/${revision}/${zip}`,
|
|
246
283
|
archive);
|
|
247
284
|
await extractZip(archive, {dir: cacheDir});
|
|
248
|
-
// avoid driver copy failing if the file would be locked
|
|
249
285
|
await killDriverProcess(Chromium.#DRIVER);
|
|
250
286
|
await fs.promises.mkdir(destinationDir, {recursive: true});
|
|
251
287
|
await fs.promises.copyFile(path.join(cacheDir, zip.split(".")[0], driver),
|
|
@@ -253,12 +289,13 @@ class Chromium extends Browser {
|
|
|
253
289
|
}
|
|
254
290
|
|
|
255
291
|
/** @see Browser.getDriver */
|
|
256
|
-
static async getDriver(version
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
292
|
+
static async getDriver(version = "latest", {
|
|
293
|
+
headless = true, extensionPaths = [], incognito = false, insecure = false,
|
|
294
|
+
extraArgs = []
|
|
295
|
+
} = {}) {
|
|
296
|
+
let {binary, revision, versionNumber} =
|
|
260
297
|
await Chromium.downloadBinary(version);
|
|
261
|
-
await Chromium.#installDriver(revision,
|
|
298
|
+
await Chromium.#installDriver(revision, versionNumber);
|
|
262
299
|
|
|
263
300
|
let options = new chrome.Options().addArguments("no-sandbox", ...extraArgs);
|
|
264
301
|
if (extensionPaths.length > 0)
|
|
@@ -280,13 +317,8 @@ class Chromium extends Browser {
|
|
|
280
317
|
|
|
281
318
|
/** @see Browser.enableExtensionInIncognito */
|
|
282
319
|
static async enableExtensionInIncognito(driver, extensionTitle) {
|
|
283
|
-
let version = await getBrowserVersion(driver);
|
|
284
|
-
if (version < 75)
|
|
285
|
-
// The UI workaround needs a chromedriver >= 75
|
|
286
|
-
throw new Error(`Only supported on Chromium >= 75. Current version: ${version}`);
|
|
287
|
-
|
|
288
320
|
await driver.navigate().to("chrome://extensions");
|
|
289
|
-
await driver.executeScript(
|
|
321
|
+
await driver.executeScript((...args) => {
|
|
290
322
|
let enable = () => document.querySelector("extensions-manager").shadowRoot
|
|
291
323
|
.querySelector("extensions-detail-view").shadowRoot
|
|
292
324
|
.getElementById("allow-incognito").shadowRoot
|
|
@@ -299,7 +331,7 @@ class Chromium extends Browser {
|
|
|
299
331
|
return new Promise((resolve, reject) => {
|
|
300
332
|
let extensionDetails;
|
|
301
333
|
for (let {shadowRoot} of extensions) {
|
|
302
|
-
if (shadowRoot.getElementById("name").innerHTML !=
|
|
334
|
+
if (shadowRoot.getElementById("name").innerHTML != args[0])
|
|
303
335
|
continue;
|
|
304
336
|
|
|
305
337
|
extensionDetails = shadowRoot.getElementById("detailsButton");
|
|
@@ -310,7 +342,8 @@ class Chromium extends Browser {
|
|
|
310
342
|
|
|
311
343
|
extensionDetails.click();
|
|
312
344
|
setTimeout(() => resolve(enable()), 100);
|
|
313
|
-
})
|
|
345
|
+
});
|
|
346
|
+
}, extensionTitle);
|
|
314
347
|
}
|
|
315
348
|
}
|
|
316
349
|
|
|
@@ -319,14 +352,14 @@ class Chromium extends Browser {
|
|
|
319
352
|
* @hideconstructor
|
|
320
353
|
* @extends Browser
|
|
321
354
|
*/
|
|
322
|
-
class Firefox {
|
|
323
|
-
static async #
|
|
355
|
+
class Firefox extends Browser {
|
|
356
|
+
static async #getVersionForChannel(branch) {
|
|
324
357
|
let data = await got("https://product-details.mozilla.org/1.0/firefox_versions.json").json();
|
|
325
358
|
return branch == "beta" ?
|
|
326
359
|
data.LATEST_FIREFOX_DEVEL_VERSION : data.LATEST_FIREFOX_VERSION;
|
|
327
360
|
}
|
|
328
361
|
|
|
329
|
-
static #
|
|
362
|
+
static #getBinaryPath(dir) {
|
|
330
363
|
switch (process.platform) {
|
|
331
364
|
case "win32":
|
|
332
365
|
return path.join(dir, "core", "firefox.exe");
|
|
@@ -342,7 +375,9 @@ class Firefox {
|
|
|
342
375
|
static #extractFirefoxArchive(archive, dir) {
|
|
343
376
|
switch (process.platform) {
|
|
344
377
|
case "win32":
|
|
345
|
-
|
|
378
|
+
// Procedure inspired from mozinstall:
|
|
379
|
+
// https://hg.mozilla.org/mozilla-central/file/tip/testing/mozbase/mozinstall/mozinstall/mozinstall.py
|
|
380
|
+
return promisify(exec)(`"${archive}" /extractdir=${dir}`);
|
|
346
381
|
case "linux":
|
|
347
382
|
return extractTar(archive, dir);
|
|
348
383
|
case "darwin":
|
|
@@ -367,7 +402,7 @@ class Firefox {
|
|
|
367
402
|
|
|
368
403
|
try {
|
|
369
404
|
await fs.promises.access(browserDir);
|
|
370
|
-
return Firefox.#
|
|
405
|
+
return Firefox.#getBinaryPath(browserDir);
|
|
371
406
|
}
|
|
372
407
|
catch (e) {}
|
|
373
408
|
|
|
@@ -381,7 +416,7 @@ class Firefox {
|
|
|
381
416
|
}
|
|
382
417
|
|
|
383
418
|
await Firefox.#extractFirefoxArchive(archive, browserDir);
|
|
384
|
-
return Firefox.#
|
|
419
|
+
return Firefox.#getBinaryPath(browserDir);
|
|
385
420
|
}
|
|
386
421
|
|
|
387
422
|
/**
|
|
@@ -390,18 +425,24 @@ class Firefox {
|
|
|
390
425
|
* number (i.e. "68.0"). Defaults to "latest".
|
|
391
426
|
* @return {BrowserBinary}
|
|
392
427
|
*/
|
|
393
|
-
static async downloadBinary(version) {
|
|
394
|
-
|
|
395
|
-
|
|
428
|
+
static async downloadBinary(version = "latest") {
|
|
429
|
+
const MIN_VERSION = 60;
|
|
430
|
+
const CHANNELS = ["latest", "beta"];
|
|
431
|
+
|
|
432
|
+
checkVersion(version, MIN_VERSION, CHANNELS);
|
|
433
|
+
|
|
434
|
+
let versionNumber = CHANNELS.includes(version) ?
|
|
435
|
+
await Firefox.#getVersionForChannel(version) : version;
|
|
396
436
|
|
|
397
|
-
let binary = await Firefox.#downloadFirefox(
|
|
398
|
-
return {binary};
|
|
437
|
+
let binary = await Firefox.#downloadFirefox(versionNumber);
|
|
438
|
+
return {binary, versionNumber};
|
|
399
439
|
}
|
|
400
440
|
|
|
401
441
|
/** @see Browser.getDriver */
|
|
402
|
-
static async getDriver(version
|
|
403
|
-
|
|
404
|
-
|
|
442
|
+
static async getDriver(version = "latest", {
|
|
443
|
+
headless = true, extensionPaths = [], incognito = false, insecure = false,
|
|
444
|
+
extraArgs = []
|
|
445
|
+
} = {}) {
|
|
405
446
|
let {binary} = await Firefox.downloadBinary(version);
|
|
406
447
|
|
|
407
448
|
let options = new firefox.Options();
|
|
@@ -415,10 +456,23 @@ class Firefox {
|
|
|
415
456
|
options.addArguments(...extraArgs);
|
|
416
457
|
options.setBinary(binary);
|
|
417
458
|
|
|
418
|
-
let driver
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
459
|
+
let driver;
|
|
460
|
+
// The OS may be low on resources, that's why building the driver is retried
|
|
461
|
+
// https://github.com/mozilla/geckodriver/issues/1560
|
|
462
|
+
await wait(async() => {
|
|
463
|
+
try {
|
|
464
|
+
driver = await new webdriver.Builder()
|
|
465
|
+
.forBrowser("firefox")
|
|
466
|
+
.setFirefoxOptions(options)
|
|
467
|
+
.build();
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
catch (err) {
|
|
471
|
+
if (err.message != "Failed to decode response from marionette")
|
|
472
|
+
throw err;
|
|
473
|
+
await killDriverProcess("geckodriver");
|
|
474
|
+
}
|
|
475
|
+
}, 30000, "geckodriver didn't start, likely due to low OS resources", 1000);
|
|
422
476
|
|
|
423
477
|
for (let extensionPath of extensionPaths) {
|
|
424
478
|
await driver.execute(
|
|
@@ -457,32 +511,129 @@ class Firefox {
|
|
|
457
511
|
* @hideconstructor
|
|
458
512
|
* @extends Browser
|
|
459
513
|
*/
|
|
460
|
-
class Edge {
|
|
514
|
+
class Edge extends Browser {
|
|
461
515
|
static #DRIVER = "msedgedriver";
|
|
462
516
|
|
|
463
|
-
static async #
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
517
|
+
static async #getVersionForChannel(version) {
|
|
518
|
+
if (!["latest", "beta", "dev"].includes(version))
|
|
519
|
+
return {versionNumber: version, channel: "stable"};
|
|
520
|
+
|
|
521
|
+
let channel = version == "latest" ? "stable" : version;
|
|
522
|
+
let {body} = await got(`https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edge-${channel}/`);
|
|
523
|
+
let regex = /href="microsoft-edge-(stable|beta|dev)_(.*?)-1_/gm;
|
|
524
|
+
let matches;
|
|
525
|
+
let versionNumbers = [];
|
|
526
|
+
while ((matches = regex.exec(body)) !== null)
|
|
527
|
+
versionNumbers.push(matches[2]);
|
|
528
|
+
|
|
529
|
+
let compareVersions = (v1, v2) =>
|
|
530
|
+
parseInt(v1.split(".")[0], 10) < parseInt(v2.split(".")[0], 10) ? 1 : -1;
|
|
531
|
+
let versionNumber = versionNumbers.sort(compareVersions)[0];
|
|
532
|
+
|
|
533
|
+
return {versionNumber, channel};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
static #getDarwinAppName(channel) {
|
|
537
|
+
let extra = channel == "stable" ?
|
|
538
|
+
"" : " " + channel.charAt(0).toUpperCase() + channel.slice(1);
|
|
539
|
+
return `Microsoft Edge${extra}`;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
static #getBinaryPath(channel = "stable") {
|
|
543
|
+
switch (process.platform) {
|
|
544
|
+
case "win32":
|
|
545
|
+
return "${Env:ProgramFiles(x86)}\\Microsoft\\Edge\\Application\\" +
|
|
546
|
+
"msedge.exe";
|
|
547
|
+
case "linux":
|
|
548
|
+
return channel == "stable" ?
|
|
549
|
+
"microsoft-edge" : `microsoft-edge-${channel}`;
|
|
550
|
+
case "darwin":
|
|
551
|
+
let appName = Edge.#getDarwinAppName(channel);
|
|
552
|
+
return `${process.env.HOME}/Applications/${appName}.app/Contents/MacOS/${appName}`;
|
|
553
|
+
default:
|
|
554
|
+
throw new Error(`Unexpected platform: ${process.platform}`);
|
|
469
555
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Downloads the browser binary file.
|
|
560
|
+
* @param {string} version - Either "latest", "beta", "dev" or a full version
|
|
561
|
+
* number (i.e. "95.0.1020.53"). Defaults to "latest". This is only
|
|
562
|
+
* available on Linux.
|
|
563
|
+
* @return {BrowserBinary}
|
|
564
|
+
*/
|
|
565
|
+
static async downloadBinary(version = "latest") {
|
|
566
|
+
if (process.platform == "win32")
|
|
567
|
+
// Edge is mandatory on Windows, can't be uninstalled or downgraded
|
|
568
|
+
// https://support.microsoft.com/en-us/microsoft-edge/why-can-t-i-uninstall-microsoft-edge-ee150b3b-7d7a-9984-6d83-eb36683d526d
|
|
569
|
+
throw new Error("Edge download is not supported in Windows");
|
|
570
|
+
|
|
571
|
+
const MIN_VERSION = 95;
|
|
572
|
+
const CHANNELS = ["latest", "beta", "dev"];
|
|
573
|
+
checkVersion(version, MIN_VERSION, CHANNELS);
|
|
574
|
+
let {versionNumber, channel} = await Edge.#getVersionForChannel(version);
|
|
575
|
+
|
|
576
|
+
let darwinName = {
|
|
577
|
+
stable: "MicrosoftEdge",
|
|
578
|
+
beta: "MicrosoftEdgeBeta",
|
|
579
|
+
dev: "MicrosoftEdgeDev"
|
|
580
|
+
}[channel];
|
|
581
|
+
let filename = {
|
|
582
|
+
linux: `microsoft-edge-${channel}_${versionNumber}-1_amd64.deb`,
|
|
583
|
+
darwin: `${darwinName}-${versionNumber}.pkg`
|
|
584
|
+
}[process.platform];
|
|
585
|
+
let darwinArch = process.arch == "arm64" ?
|
|
586
|
+
"03adf619-38c6-4249-95ff-4a01c0ffc962" :
|
|
587
|
+
"C1297A47-86C4-4C1F-97FA-950631F94777";
|
|
588
|
+
let downloadUrl = {
|
|
589
|
+
linux: `https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edge-${channel}/${filename}`,
|
|
590
|
+
darwin: `https://officecdnmac.microsoft.com/pr/${darwinArch}/MacAutoupdate/${filename}`
|
|
591
|
+
}[process.platform];
|
|
592
|
+
|
|
593
|
+
let snapshotsDir = path.join(snapshotsBaseDir, "edge");
|
|
594
|
+
let archive = path.join(snapshotsDir, "cache", filename);
|
|
595
|
+
let binary = Edge.#getBinaryPath(channel);
|
|
596
|
+
try {
|
|
597
|
+
if (await Edge.#getInstalledVersionNumber(binary) == versionNumber)
|
|
598
|
+
return {binary, versionNumber};
|
|
599
|
+
}
|
|
600
|
+
catch (e) {}
|
|
601
|
+
|
|
602
|
+
try {
|
|
603
|
+
await download(downloadUrl, archive);
|
|
604
|
+
}
|
|
605
|
+
catch (err) {
|
|
606
|
+
throw new Error(`Edge download failed: ${err}`);
|
|
475
607
|
}
|
|
476
|
-
|
|
477
|
-
|
|
608
|
+
|
|
609
|
+
if (process.platform == "linux") {
|
|
610
|
+
await promisify(exec)(`dpkg -i ${archive}`);
|
|
611
|
+
}
|
|
612
|
+
else if (process.platform == "darwin") {
|
|
613
|
+
let appName = Edge.#getDarwinAppName(channel);
|
|
614
|
+
try {
|
|
615
|
+
await fs.promises.rm(`${process.env.HOME}/Applications/${appName}.app`, {recursive: true});
|
|
616
|
+
}
|
|
617
|
+
catch (e) {}
|
|
618
|
+
await promisify(exec)(`installer -pkg ${archive} -target CurrentUserHomeDirectory`);
|
|
478
619
|
}
|
|
479
620
|
|
|
480
|
-
|
|
481
|
-
|
|
621
|
+
return {binary, versionNumber};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
static async #getInstalledVersionNumber(binary) {
|
|
625
|
+
let installedVersion = await Edge.getInstalledVersion(binary);
|
|
626
|
+
for (let word of ["beta", "dev", "Beta", "Dev"])
|
|
627
|
+
installedVersion = installedVersion.replace(word, "");
|
|
628
|
+
return installedVersion.trim().replace(/.*\s/, "");
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
static async #installDriver() {
|
|
632
|
+
let binary = Edge.#getBinaryPath();
|
|
633
|
+
let versionNumber = await Edge.#getInstalledVersionNumber(binary);
|
|
634
|
+
if (!versionNumber)
|
|
482
635
|
throw new Error("Edge is not installed");
|
|
483
636
|
|
|
484
|
-
// Based on "node_modules/msedgedriver/install.js", adding a fallback
|
|
485
|
-
// mechanism when msedgedriver doesn't exist for the latest Edge version.
|
|
486
637
|
let [zip, driver] = {
|
|
487
638
|
"win32-ia32": ["edgedriver_win32.zip", "msedgedriver.exe"],
|
|
488
639
|
"win32-x64": ["edgedriver_win64.zip", "msedgedriver.exe"],
|
|
@@ -490,14 +641,11 @@ class Edge {
|
|
|
490
641
|
"darwin-x64": ["edgedriver_mac64.zip", "msedgedriver"],
|
|
491
642
|
"darwin-arm64": ["edgedriver_arm64.zip", "msedgedriver"]
|
|
492
643
|
}[platform];
|
|
493
|
-
let cacheDir = path.join(snapshotsBaseDir, "edge", "cache"
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
let archive = path.join(cacheDir, `${version}-${zip}`);
|
|
499
|
-
|
|
500
|
-
let vSplit = version.split(".");
|
|
644
|
+
let cacheDir = path.join(snapshotsBaseDir, "edge", "cache",
|
|
645
|
+
`edgedriver-${versionNumber}`);
|
|
646
|
+
let archive = path.join(cacheDir, `${versionNumber}-${zip}`);
|
|
647
|
+
|
|
648
|
+
let vSplit = versionNumber.split(".");
|
|
501
649
|
let lastNum = parseInt(vSplit[3], 10);
|
|
502
650
|
while (lastNum >= 0) {
|
|
503
651
|
try {
|
|
@@ -512,51 +660,50 @@ class Edge {
|
|
|
512
660
|
}
|
|
513
661
|
|
|
514
662
|
if (lastNum < 0)
|
|
515
|
-
throw new Error(`msedgedriver was not found for Edge ${
|
|
663
|
+
throw new Error(`msedgedriver was not found for Edge ${versionNumber}`);
|
|
516
664
|
|
|
517
|
-
await extractZip(archive, {dir: cacheDir});
|
|
518
|
-
// avoid driver copy failing if the file would be locked
|
|
519
665
|
await killDriverProcess(Edge.#DRIVER);
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
await fs.promises.
|
|
523
|
-
path.join(destinationDir, driver));
|
|
666
|
+
let driverPath = path.join(cacheDir, driver);
|
|
667
|
+
try {
|
|
668
|
+
await fs.promises.rm(driverPath, {recursive: true});
|
|
524
669
|
}
|
|
670
|
+
catch (e) {} // file does not exist
|
|
671
|
+
await extractZip(archive, {dir: cacheDir});
|
|
672
|
+
|
|
673
|
+
return driverPath;
|
|
525
674
|
}
|
|
526
675
|
|
|
527
676
|
/** @see Browser.getDriver */
|
|
528
|
-
static async getDriver(version
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
677
|
+
static async getDriver(version = "latest", {
|
|
678
|
+
headless = true, extensionPaths = [], incognito = false, insecure = false,
|
|
679
|
+
extraArgs = []
|
|
680
|
+
} = {}) {
|
|
681
|
+
if (process.platform == "linux" || process.platform == "darwin")
|
|
682
|
+
await Edge.downloadBinary(version);
|
|
683
|
+
|
|
684
|
+
let driverPath = await Edge.#installDriver();
|
|
685
|
+
let serviceBuilder = new edge.ServiceBuilder(driverPath);
|
|
532
686
|
|
|
533
|
-
let
|
|
687
|
+
let options = new edge.Options().addArguments("no-sandbox", ...extraArgs);
|
|
534
688
|
if (headless)
|
|
535
|
-
|
|
689
|
+
options.headless();
|
|
536
690
|
if (extensionPaths.length > 0)
|
|
537
|
-
|
|
691
|
+
options.addArguments(`load-extension=${extensionPaths.join(",")}`);
|
|
538
692
|
if (incognito)
|
|
539
|
-
|
|
693
|
+
options.addArguments("incognito");
|
|
694
|
+
if (insecure)
|
|
695
|
+
options.addArguments("ignore-certificate-errors");
|
|
540
696
|
|
|
541
697
|
let builder = new webdriver.Builder();
|
|
542
698
|
builder.forBrowser("MicrosoftEdge");
|
|
543
|
-
builder.
|
|
544
|
-
|
|
545
|
-
"ms:edgeChromium": true,
|
|
546
|
-
"ms:edgeOptions": {args},
|
|
547
|
-
"acceptInsecureCerts": insecure
|
|
548
|
-
});
|
|
699
|
+
builder.setEdgeOptions(options);
|
|
700
|
+
builder.setEdgeService(serviceBuilder);
|
|
549
701
|
|
|
550
702
|
return builder.build();
|
|
551
703
|
}
|
|
552
704
|
|
|
553
705
|
/** @see Browser.enableExtensionInIncognito */
|
|
554
706
|
static async enableExtensionInIncognito(driver, extensionTitle) {
|
|
555
|
-
let version = await getBrowserVersion(driver);
|
|
556
|
-
if (version < 79)
|
|
557
|
-
// The UI workaround needs a chromium based msedgedriver
|
|
558
|
-
throw new Error(`Only supported on Edge >= 79. Current version: ${version}`);
|
|
559
|
-
|
|
560
707
|
await driver.navigate().to("edge://extensions/");
|
|
561
708
|
for (let elem of await driver.findElements(By.css("[role=listitem]"))) {
|
|
562
709
|
let text = await elem.getAttribute("innerHTML");
|
|
@@ -577,6 +724,241 @@ class Edge {
|
|
|
577
724
|
}
|
|
578
725
|
}
|
|
579
726
|
|
|
727
|
+
/**
|
|
728
|
+
* Download functionality for Opera. This class can be used statically.
|
|
729
|
+
* @hideconstructor
|
|
730
|
+
* @extends Browser
|
|
731
|
+
*/
|
|
732
|
+
class Opera extends Browser {
|
|
733
|
+
static async #getVersionForChannel(version, platformDir) {
|
|
734
|
+
let channelPath = "opera/desktop";
|
|
735
|
+
let filePrefix = "Opera";
|
|
736
|
+
if (version != "latest")
|
|
737
|
+
return {versionNumber: version, channelPath, filePrefix};
|
|
738
|
+
|
|
739
|
+
let {body} = await got(`https://ftp.opera.com/pub/${channelPath}`);
|
|
740
|
+
let regex = /href="(\d.*)\/"/gm;
|
|
741
|
+
let matches = body.match(regex);
|
|
742
|
+
let versionNumber;
|
|
743
|
+
while (matches.length > 0) {
|
|
744
|
+
let result = regex.exec(matches.pop());
|
|
745
|
+
if (!result)
|
|
746
|
+
continue;
|
|
747
|
+
|
|
748
|
+
versionNumber = result[1];
|
|
749
|
+
try {
|
|
750
|
+
await got(`https://ftp.opera.com/pub/${channelPath}/${versionNumber}/${platformDir}`);
|
|
751
|
+
break;
|
|
752
|
+
}
|
|
753
|
+
catch (e) {}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
return {versionNumber, channelPath, filePrefix};
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
static #getBinaryPath(dir) {
|
|
760
|
+
switch (process.platform) {
|
|
761
|
+
case "win32":
|
|
762
|
+
return path.join(dir, "launcher.exe");
|
|
763
|
+
case "linux":
|
|
764
|
+
return path.join("/", "usr", "bin", "opera");
|
|
765
|
+
case "darwin":
|
|
766
|
+
return path.join(dir, "Opera.app", "Contents", "MacOS", "Opera");
|
|
767
|
+
default:
|
|
768
|
+
throw new Error(`Unexpected platform: ${process.platform}`);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
static async #extractDeb(archive) {
|
|
773
|
+
let child = spawn("dpkg", ["-i", archive]);
|
|
774
|
+
|
|
775
|
+
child.stdout.on("data", data => {
|
|
776
|
+
if (data.toString().includes("Do you want to update Opera")) {
|
|
777
|
+
process.stdin.pipe(child.stdin);
|
|
778
|
+
child.stdin.write("no\r\n");
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
child.stderr.on("data", data => {
|
|
783
|
+
let expectedWarnings = [
|
|
784
|
+
"debconf: unable to initialize frontend",
|
|
785
|
+
"dpkg: warning: downgrading opera-stable",
|
|
786
|
+
"update-alternatives",
|
|
787
|
+
"using /usr/bin/opera",
|
|
788
|
+
"skip creation of",
|
|
789
|
+
"\r\n"
|
|
790
|
+
];
|
|
791
|
+
if (!expectedWarnings.find(err => data.toString().includes(err.trim())))
|
|
792
|
+
console.error(`stderr: ${data.toString()}`);
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
await new Promise((resolve, reject) => child.on("close", code => {
|
|
796
|
+
if (code != 0)
|
|
797
|
+
reject(`dpkg process exited with code ${code}`);
|
|
798
|
+
|
|
799
|
+
resolve();
|
|
800
|
+
}));
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
static async #installOnWindows(archive, dir, filename) {
|
|
804
|
+
let archiveCopy = path.join(dir, filename);
|
|
805
|
+
await fsExtra.copy(archive, archiveCopy);
|
|
806
|
+
await promisify(exec)(`"${archiveCopy}"`);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
static #extractOperaArchive(archive, dir, filename) {
|
|
810
|
+
switch (process.platform) {
|
|
811
|
+
case "win32":
|
|
812
|
+
return Opera.#installOnWindows(archive, dir, filename);
|
|
813
|
+
case "linux":
|
|
814
|
+
return Opera.#extractDeb(archive, dir);
|
|
815
|
+
case "darwin":
|
|
816
|
+
return extractTar(archive, dir);
|
|
817
|
+
default:
|
|
818
|
+
throw new Error(`Unexpected platform: ${process.platform}`);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Downloads the browser binary file.
|
|
824
|
+
* @param {string} version - Either "latest" or a full version number
|
|
825
|
+
* (i.e. "64.0.3417.92"). Defaults to "latest".
|
|
826
|
+
* @return {BrowserBinary}
|
|
827
|
+
*/
|
|
828
|
+
static async downloadBinary(version = "latest") {
|
|
829
|
+
const MIN_VERSION = 62;
|
|
830
|
+
const CHANNELS = ["latest"];
|
|
831
|
+
|
|
832
|
+
checkVersion(version, MIN_VERSION, CHANNELS);
|
|
833
|
+
|
|
834
|
+
let [platformDir, fileSuffix] = {
|
|
835
|
+
"win32-ia32": ["win", "Autoupdate.exe"],
|
|
836
|
+
"win32-x64": ["win", "Autoupdate_x64.exe"],
|
|
837
|
+
"linux-x64": ["linux", "amd64.deb"],
|
|
838
|
+
"darwin-x64": ["mac", "Autoupdate.tar.xz"],
|
|
839
|
+
"dawrin-arm64": ["mac", "Autoupdate_arm64.tar.xz"]
|
|
840
|
+
}[platform];
|
|
841
|
+
|
|
842
|
+
let {versionNumber, channelPath, filePrefix} =
|
|
843
|
+
await Opera.#getVersionForChannel(version, platformDir);
|
|
844
|
+
|
|
845
|
+
let snapshotsDir = path.join(snapshotsBaseDir, "opera");
|
|
846
|
+
let browserDir = path.join(snapshotsDir, `opera-${platform}-${versionNumber}`);
|
|
847
|
+
let filename = `${filePrefix}_${versionNumber}_${fileSuffix}`;
|
|
848
|
+
let archive = path.join(snapshotsDir, "cache", filename);
|
|
849
|
+
|
|
850
|
+
let binary = Opera.#getBinaryPath(browserDir);
|
|
851
|
+
try {
|
|
852
|
+
if (process.platform == "linux" &&
|
|
853
|
+
await Opera.getInstalledVersion("opera") == versionNumber)
|
|
854
|
+
return {binary, versionNumber};
|
|
855
|
+
|
|
856
|
+
await fs.promises.access(browserDir);
|
|
857
|
+
return {binary, versionNumber};
|
|
858
|
+
}
|
|
859
|
+
catch (e) {}
|
|
860
|
+
|
|
861
|
+
await fs.promises.mkdir(path.dirname(browserDir), {recursive: true});
|
|
862
|
+
try {
|
|
863
|
+
await fs.promises.access(archive);
|
|
864
|
+
}
|
|
865
|
+
catch (e) {
|
|
866
|
+
let url = `https://ftp.opera.com/pub/${channelPath}/${versionNumber}/${platformDir}/${filename}`;
|
|
867
|
+
try {
|
|
868
|
+
await download(url, archive);
|
|
869
|
+
}
|
|
870
|
+
catch (err) {
|
|
871
|
+
throw new Error(`Browser download unavailable at ${url}\n${err}`);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
await Opera.#extractOperaArchive(archive, browserDir, filename);
|
|
876
|
+
return {binary, versionNumber};
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
static async #installDriver(version, originalVersion) {
|
|
880
|
+
let [zip, driver] = {
|
|
881
|
+
"win32-ia32": ["operadriver_win32.zip", "operadriver.exe"],
|
|
882
|
+
"win32-x64": ["operadriver_win64.zip", "operadriver.exe"],
|
|
883
|
+
"linux-x64": ["operadriver_linux64.zip", "operadriver"],
|
|
884
|
+
"darwin-x64": ["operadriver_mac64.zip", "operadriver"],
|
|
885
|
+
"darwin-arm64": ["operadriver_mac64.zip", "operadriver"]
|
|
886
|
+
}[platform];
|
|
887
|
+
|
|
888
|
+
let {versionNumber} = await Opera.#getVersionForChannel(version);
|
|
889
|
+
versionNumber = versionNumber.split(".")[0];
|
|
890
|
+
|
|
891
|
+
let cacheDir = path.join(snapshotsBaseDir, "opera", "cache",
|
|
892
|
+
`operadriver-${versionNumber}`);
|
|
893
|
+
let archive = path.join(cacheDir, zip);
|
|
894
|
+
|
|
895
|
+
let {body} = await got(`https://github.com/operasoftware/operachromiumdriver/releases?q=Opera+${versionNumber}&expanded=true`);
|
|
896
|
+
let regex = /release-card[\s\S]*Link--primary.*>(.*)<\/a/gm;
|
|
897
|
+
let matches = body.match(regex);
|
|
898
|
+
if (!matches || matches.length == 0)
|
|
899
|
+
throw new Error(`Driver for Opera ${version} was not found`);
|
|
900
|
+
let driverVersion = regex.exec(matches[matches.length - 1])[1];
|
|
901
|
+
|
|
902
|
+
try {
|
|
903
|
+
await download(`https://github.com/operasoftware/operachromiumdriver/releases/download/v.${driverVersion}/${zip}`,
|
|
904
|
+
archive);
|
|
905
|
+
}
|
|
906
|
+
catch (err) {
|
|
907
|
+
throw new Error(`Downloading operadriver failed: ${err}`);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
await killDriverProcess("operadriver");
|
|
911
|
+
let driverPath = path.join(cacheDir, zip.split(".")[0], driver);
|
|
912
|
+
try {
|
|
913
|
+
await fs.promises.rm(driverPath, {recursive: true});
|
|
914
|
+
}
|
|
915
|
+
catch (e) {} // file does not exist
|
|
916
|
+
await extractZip(archive, {dir: cacheDir});
|
|
917
|
+
await fs.promises.chmod(driverPath, 577);
|
|
918
|
+
|
|
919
|
+
return driverPath;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/** @see Browser.getDriver */
|
|
923
|
+
static async getDriver(version = "latest", {
|
|
924
|
+
headless = true, extensionPaths = [], incognito = false, insecure = false,
|
|
925
|
+
extraArgs = []
|
|
926
|
+
} = {}) {
|
|
927
|
+
let {binary, versionNumber} = await Opera.downloadBinary(version);
|
|
928
|
+
let driverPath = await Opera.#installDriver(versionNumber, version);
|
|
929
|
+
// operadriver uses chrome as a service builder:
|
|
930
|
+
// https://github.com/operasoftware/operachromiumdriver/blob/master/docs/desktop.md
|
|
931
|
+
let serviceBuilder = new chrome.ServiceBuilder(driverPath);
|
|
932
|
+
let service = serviceBuilder.build();
|
|
933
|
+
await service.start();
|
|
934
|
+
|
|
935
|
+
let options = new chrome.Options().addArguments("no-sandbox", ...extraArgs);
|
|
936
|
+
if (extensionPaths.length > 0)
|
|
937
|
+
options.addArguments(`load-extension=${extensionPaths.join(",")}`);
|
|
938
|
+
if (headless)
|
|
939
|
+
options.headless();
|
|
940
|
+
if (insecure)
|
|
941
|
+
options.addArguments("ignore-certificate-errors");
|
|
942
|
+
if (incognito)
|
|
943
|
+
options.addArguments("incognito");
|
|
944
|
+
options.setChromeBinaryPath(binary);
|
|
945
|
+
// https://github.com/operasoftware/operachromiumdriver/issues/61#issuecomment-579331657
|
|
946
|
+
options.addArguments("remote-debugging-port=9222");
|
|
947
|
+
|
|
948
|
+
let builder = new webdriver.Builder();
|
|
949
|
+
builder.forBrowser("chrome");
|
|
950
|
+
builder.setChromeOptions(options);
|
|
951
|
+
builder.setChromeService(serviceBuilder);
|
|
952
|
+
|
|
953
|
+
return builder.build();
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/** @see Browser.enableExtensionInIncognito */
|
|
957
|
+
static async enableExtensionInIncognito(driver, extensionTitle) {
|
|
958
|
+
// Extensions page in Opera has the same web elements as Chromium
|
|
959
|
+
await Chromium.enableExtensionInIncognito(driver, extensionTitle);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
580
962
|
|
|
581
963
|
/**
|
|
582
964
|
* @type {Object}
|
|
@@ -587,5 +969,6 @@ class Edge {
|
|
|
587
969
|
export const BROWSERS = {
|
|
588
970
|
chromium: Chromium,
|
|
589
971
|
firefox: Firefox,
|
|
590
|
-
edge: Edge
|
|
972
|
+
edge: Edge,
|
|
973
|
+
opera: Opera
|
|
591
974
|
};
|