@eyeo/get-browser-binary 0.5.0 → 0.7.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/.gitlab-ci.yml +6 -7
- package/.mocharc.json +3 -0
- package/README.md +24 -14
- package/RELEASE_NOTES.md +26 -0
- package/package.json +4 -5
- package/src/browsers.js +329 -453
- package/src/utils.js +1 -7
- package/test/browsers.js +49 -24
- package/test/extension/{background.js → mv2/background.js} +0 -0
- package/test/extension/mv2/index.html +1 -0
- package/test/extension/{manifest.json → mv2/manifest.json} +1 -2
- package/test/extension/mv3/background.js +3 -0
- package/test/extension/mv3/index.html +1 -0
- package/test/extension/mv3/manifest.json +8 -0
- package/test/extension/index.html +0 -1
package/src/browsers.js
CHANGED
|
@@ -16,10 +16,9 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import path from "path";
|
|
19
|
-
import {exec, execFile
|
|
19
|
+
import {exec, execFile} from "child_process";
|
|
20
20
|
import {promisify} from "util";
|
|
21
21
|
import fs from "fs";
|
|
22
|
-
import fsExtra from "fs-extra";
|
|
23
22
|
|
|
24
23
|
import got from "got";
|
|
25
24
|
import webdriver from "selenium-webdriver";
|
|
@@ -28,31 +27,47 @@ import firefox from "selenium-webdriver/firefox.js";
|
|
|
28
27
|
import edge from "selenium-webdriver/edge.js";
|
|
29
28
|
import command from "selenium-webdriver/lib/command.js";
|
|
30
29
|
import extractZip from "extract-zip";
|
|
30
|
+
import Jimp from "jimp";
|
|
31
31
|
|
|
32
|
-
import {download, extractTar, extractDmg,
|
|
33
|
-
|
|
32
|
+
import {download, extractTar, extractDmg, killDriverProcess, wait}
|
|
33
|
+
from "./utils.js";
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
* Root folder where browser and webdriver
|
|
36
|
+
* Root folder where browser and webdriver files get downloaded and extracted.
|
|
37
37
|
* @type {string}
|
|
38
38
|
*/
|
|
39
39
|
export let snapshotsBaseDir = path.join(process.cwd(), "browser-snapshots");
|
|
40
40
|
|
|
41
41
|
let {until, By} = webdriver;
|
|
42
|
-
let platform =
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
let {platform, arch} = process;
|
|
43
|
+
let platformArch = `${platform}-${arch}`;
|
|
44
|
+
|
|
45
|
+
const UNSUPPORTED_VERSION_ERROR = "Unsupported browser version";
|
|
46
|
+
const UNSUPPORTED_PLATFORM_ERROR = "Unsupported platform";
|
|
47
|
+
const DRIVER_DOWNLOAD_ERROR = "Driver download failed";
|
|
48
|
+
const DRIVER_START_ERROR = "Unable to start driver";
|
|
49
|
+
const EXTENSION_NOT_FOUND_ERROR = "Extension not found";
|
|
50
|
+
const BROWSER_DOWNLOAD_ERROR = "Browser download failed";
|
|
51
|
+
const BROWSER_NOT_INSTALLED_ERROR = "Browser is not installed";
|
|
52
|
+
const ELEMENT_NOT_FOUND_ERROR = "HTML element not found";
|
|
53
|
+
|
|
54
|
+
function checkVersion(version, minVersion, channels = []) {
|
|
45
55
|
if (channels.includes(version))
|
|
46
56
|
return;
|
|
47
57
|
|
|
48
58
|
let mainVersion = parseInt(version && version.split(".")[0], 10);
|
|
49
|
-
if (mainVersion < minVersion)
|
|
50
|
-
throw new Error(
|
|
59
|
+
if (isNaN(mainVersion) || mainVersion < minVersion)
|
|
60
|
+
throw new Error(`${UNSUPPORTED_VERSION_ERROR}: ${version}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function checkPlatform() {
|
|
64
|
+
if (!["win32", "linux", "darwin"].includes(platform))
|
|
65
|
+
throw new Error(`${UNSUPPORTED_PLATFORM_ERROR}: ${platform}`);
|
|
51
66
|
}
|
|
52
67
|
|
|
53
68
|
/**
|
|
54
|
-
* Base class for browser
|
|
55
|
-
*
|
|
69
|
+
* Base class for browser and webdriver functionality. Please see subclasses for
|
|
70
|
+
* browser specific details. All classes can be used statically.
|
|
56
71
|
* @hideconstructor
|
|
57
72
|
*/
|
|
58
73
|
class Browser {
|
|
@@ -64,13 +79,15 @@ class Browser {
|
|
|
64
79
|
*/
|
|
65
80
|
|
|
66
81
|
/**
|
|
67
|
-
*
|
|
82
|
+
* Installs the browser. The installation process is detailed on the
|
|
83
|
+
* subclasses.
|
|
68
84
|
* @param {string} version - Either full version number or channel/release.
|
|
69
85
|
* Please find examples on the subclasses.
|
|
70
86
|
* @return {BrowserBinary}
|
|
71
|
-
* @throws {Error} Unsupported browser version
|
|
87
|
+
* @throws {Error} Unsupported browser version, Unsupported platform, Browser
|
|
88
|
+
* download failed.
|
|
72
89
|
*/
|
|
73
|
-
static async
|
|
90
|
+
static async installBrowser(version) {
|
|
74
91
|
// to be implemented by the subclass
|
|
75
92
|
}
|
|
76
93
|
|
|
@@ -81,7 +98,7 @@ class Browser {
|
|
|
81
98
|
*/
|
|
82
99
|
static async getInstalledVersion(binary) {
|
|
83
100
|
let stdout;
|
|
84
|
-
if (
|
|
101
|
+
if (platform == "win32") {
|
|
85
102
|
({stdout} = await promisify(exec)(
|
|
86
103
|
`(Get-ItemProperty ${binary}).VersionInfo.ProductVersion`,
|
|
87
104
|
{shell: "powershell.exe"})
|
|
@@ -119,7 +136,8 @@ class Browser {
|
|
|
119
136
|
* Please find examples on the subclasses.
|
|
120
137
|
* @param {driverOptions?} options - Options to start the browser with.
|
|
121
138
|
* @return {webdriver}
|
|
122
|
-
* @throws {Error} Unsupported browser version
|
|
139
|
+
* @throws {Error} Unsupported browser version, Unsupported platform, Browser
|
|
140
|
+
* download failed, Driver download failed, Unable to start driver.
|
|
123
141
|
*/
|
|
124
142
|
static async getDriver(version, options = {}) {
|
|
125
143
|
// to be implemented by the subclass
|
|
@@ -131,7 +149,8 @@ class Browser {
|
|
|
131
149
|
* @param {webdriver} driver - The driver controlling the browser.
|
|
132
150
|
* @param {string} extensionTitle - Title of the extebsion to be enabled.
|
|
133
151
|
* @return {webdriver}
|
|
134
|
-
* @throws {Error} Unsupported browser version
|
|
152
|
+
* @throws {Error} Unsupported browser version, Extension not found, HTML
|
|
153
|
+
* element not found.
|
|
135
154
|
*/
|
|
136
155
|
static async enableExtensionInIncognito(driver, extensionTitle) {
|
|
137
156
|
// Allowing the extension in incognito mode can't happen programmatically:
|
|
@@ -139,22 +158,80 @@ class Browser {
|
|
|
139
158
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=1729315
|
|
140
159
|
// That is done through the UI, to be implemented by the subclass
|
|
141
160
|
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @typedef {Object} Jimp
|
|
164
|
+
* @see https://github.com/oliver-moran/jimp/tree/master/packages/jimp
|
|
165
|
+
*/
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Takes a screenshot of the full page by scrolling from top to bottom.
|
|
169
|
+
* @param {webdriver} driver - The driver controlling the browser.
|
|
170
|
+
* @property {boolean} hideScrollbars=true - Hides any scrollbars before
|
|
171
|
+
* taking the screenshot, or not.
|
|
172
|
+
* @return {Jimp} A Jimp image object containing the screenshot.
|
|
173
|
+
* @example
|
|
174
|
+
* // Getting a base-64 encoded PNG from the returned screenshot
|
|
175
|
+
* let image = await Browser.takeFullPageScreenshot(driver);
|
|
176
|
+
* let encodedPNG = await image.getBase64Async("image/png");
|
|
177
|
+
*/
|
|
178
|
+
static async takeFullPageScreenshot(driver, hideScrollbars = true) {
|
|
179
|
+
// On macOS scrollbars appear and disappear overlapping the content as
|
|
180
|
+
// scrolling occurs. Hiding the scrollbars helps getting reproducible
|
|
181
|
+
// screenshots.
|
|
182
|
+
if (hideScrollbars) {
|
|
183
|
+
await driver.executeScript(() => {
|
|
184
|
+
if (!document.head)
|
|
185
|
+
return;
|
|
186
|
+
|
|
187
|
+
let style = document.createElement("style");
|
|
188
|
+
style.textContent = "html { overflow-y: scroll; }";
|
|
189
|
+
document.head.appendChild(style);
|
|
190
|
+
if (document.documentElement.clientWidth == window.innerWidth)
|
|
191
|
+
style.textContent = "html::-webkit-scrollbar { display: none; }";
|
|
192
|
+
else
|
|
193
|
+
document.head.removeChild(style);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let fullScreenshot = new Jimp(0, 0);
|
|
198
|
+
while (true) {
|
|
199
|
+
let [width, height, offset] = await driver.executeScript((...args) => {
|
|
200
|
+
window.scrollTo(0, args[0]);
|
|
201
|
+
// Math.ceil rounds up potential decimal values on window.scrollY,
|
|
202
|
+
// ensuring the loop will not hang due to never reaching enough
|
|
203
|
+
// fullScreenshot's height.
|
|
204
|
+
return [document.documentElement.clientWidth,
|
|
205
|
+
document.documentElement.scrollHeight,
|
|
206
|
+
Math.ceil(window.scrollY)];
|
|
207
|
+
}, fullScreenshot.bitmap.height);
|
|
208
|
+
let data = await driver.takeScreenshot();
|
|
209
|
+
let partialScreenshot = await Jimp.read(Buffer.from(data, "base64"));
|
|
210
|
+
let combinedScreenshot =
|
|
211
|
+
new Jimp(width, offset + partialScreenshot.bitmap.height);
|
|
212
|
+
combinedScreenshot.composite(fullScreenshot, 0, 0);
|
|
213
|
+
combinedScreenshot.composite(partialScreenshot, 0, offset);
|
|
214
|
+
fullScreenshot = combinedScreenshot;
|
|
215
|
+
|
|
216
|
+
if (fullScreenshot.bitmap.height >= height)
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
return fullScreenshot;
|
|
220
|
+
}
|
|
142
221
|
}
|
|
143
222
|
|
|
144
223
|
/**
|
|
145
|
-
*
|
|
224
|
+
* Browser and webdriver functionality for Chromium.
|
|
146
225
|
* @hideconstructor
|
|
147
226
|
* @extends Browser
|
|
148
227
|
*/
|
|
149
228
|
class Chromium extends Browser {
|
|
150
|
-
static #
|
|
151
|
-
|
|
152
|
-
static async #getBranchBasePosition(version) {
|
|
153
|
-
let data = await got(`https://omahaproxy.appspot.com/deps.json?version=${version}`).json();
|
|
154
|
-
return data.chromium_base_position;
|
|
155
|
-
}
|
|
229
|
+
static #CHANNELS = ["latest", "beta", "dev"];
|
|
156
230
|
|
|
157
231
|
static async #getVersionForChannel(channel) {
|
|
232
|
+
if (!Chromium.#CHANNELS.includes(channel))
|
|
233
|
+
return channel;
|
|
234
|
+
|
|
158
235
|
if (channel == "latest")
|
|
159
236
|
channel = "stable";
|
|
160
237
|
|
|
@@ -164,7 +241,7 @@ class Chromium extends Browser {
|
|
|
164
241
|
"linux-x64": "linux",
|
|
165
242
|
"darwin-x64": "mac",
|
|
166
243
|
"dawrin-arm64": "mac_arm64"
|
|
167
|
-
}[
|
|
244
|
+
}[platformArch];
|
|
168
245
|
let data = await got(`https://omahaproxy.appspot.com/all.json?os=${os}`).json();
|
|
169
246
|
let release = data[0].versions.find(ver => ver.channel == channel);
|
|
170
247
|
let {current_version: version} = release;
|
|
@@ -178,55 +255,67 @@ class Chromium extends Browser {
|
|
|
178
255
|
}
|
|
179
256
|
|
|
180
257
|
static #getBinaryPath(dir) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
"Chromium");
|
|
189
|
-
default:
|
|
190
|
-
throw new Error(`Unexpected platform: ${process.platform}`);
|
|
191
|
-
}
|
|
258
|
+
checkPlatform();
|
|
259
|
+
return {
|
|
260
|
+
win32: path.join(dir, "chrome-win", "chrome.exe"),
|
|
261
|
+
linux: path.join(dir, "chrome-linux", "chrome"),
|
|
262
|
+
darwin: path.join(dir, "chrome-mac", "Chromium.app", "Contents", "MacOS",
|
|
263
|
+
"Chromium")
|
|
264
|
+
}[platform];
|
|
192
265
|
}
|
|
193
266
|
|
|
194
|
-
|
|
195
|
-
|
|
267
|
+
/**
|
|
268
|
+
* Installs the browser. The Chromium executable gets extracted in the
|
|
269
|
+
* {@link snapshotsBaseDir} folder, ready to go.
|
|
270
|
+
* @param {string} version - Either "latest", "beta", "dev" or a full version
|
|
271
|
+
* number (i.e. "77.0.3865.0"). Defaults to "latest".
|
|
272
|
+
* @return {BrowserBinary}
|
|
273
|
+
* @throws {Error} Unsupported browser version, Unsupported platform, Browser
|
|
274
|
+
* download failed.
|
|
275
|
+
*/
|
|
276
|
+
static async installBrowser(version = "latest") {
|
|
277
|
+
const MIN_VERSION = 75;
|
|
278
|
+
const MAX_VERSION_DECREMENTS = 80;
|
|
196
279
|
|
|
197
|
-
|
|
198
|
-
let
|
|
280
|
+
checkVersion(version, MIN_VERSION, Chromium.#CHANNELS);
|
|
281
|
+
let versionNumber = await Chromium.#getVersionForChannel(version);
|
|
282
|
+
|
|
283
|
+
let {chromium_base_position: chromiumBase} =
|
|
284
|
+
await got(`https://omahaproxy.appspot.com/deps.json?version=${versionNumber}`).json();
|
|
285
|
+
let base = parseInt(chromiumBase, 10);
|
|
286
|
+
let startBase = base;
|
|
199
287
|
let [platformDir, fileName] = {
|
|
200
288
|
"win32-ia32": ["Win", "chrome-win.zip"],
|
|
201
289
|
"win32-x64": ["Win_x64", "chrome-win.zip"],
|
|
202
290
|
"linux-x64": ["Linux_x64", "chrome-linux.zip"],
|
|
203
291
|
"darwin-x64": ["Mac", "chrome-mac.zip"],
|
|
204
292
|
"dawrin-arm64": ["Mac_Arm", "chrome-mac.zip"]
|
|
205
|
-
}[
|
|
206
|
-
let archive
|
|
207
|
-
let browserDir
|
|
293
|
+
}[platformArch];
|
|
294
|
+
let archive;
|
|
295
|
+
let browserDir;
|
|
208
296
|
let snapshotsDir = path.join(snapshotsBaseDir, "chromium");
|
|
297
|
+
let binary;
|
|
209
298
|
|
|
210
299
|
while (true) {
|
|
211
|
-
browserDir = path.join(snapshotsDir, `chromium-${
|
|
300
|
+
browserDir = path.join(snapshotsDir, `chromium-${platformArch}-${base}`);
|
|
301
|
+
binary = Chromium.#getBinaryPath(browserDir);
|
|
212
302
|
|
|
213
303
|
try {
|
|
214
304
|
await fs.promises.access(browserDir);
|
|
215
|
-
return {binary
|
|
305
|
+
return {binary, versionNumber, base};
|
|
216
306
|
}
|
|
217
307
|
catch (e) {}
|
|
218
308
|
|
|
219
309
|
await fs.promises.mkdir(path.dirname(browserDir), {recursive: true});
|
|
220
310
|
|
|
221
|
-
archive = path.join(snapshotsDir, "cache", `${
|
|
222
|
-
|
|
311
|
+
archive = path.join(snapshotsDir, "cache", `${base}-${fileName}`);
|
|
223
312
|
try {
|
|
224
313
|
try {
|
|
225
314
|
await fs.promises.access(archive);
|
|
226
315
|
}
|
|
227
316
|
catch (e) {
|
|
228
317
|
await download(
|
|
229
|
-
`https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/${platformDir}%2F${
|
|
318
|
+
`https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/${platformDir}%2F${base}%2F${fileName}?alt=media`,
|
|
230
319
|
archive);
|
|
231
320
|
}
|
|
232
321
|
break;
|
|
@@ -234,58 +323,53 @@ class Chromium extends Browser {
|
|
|
234
323
|
catch (e) {
|
|
235
324
|
// Chromium advises decrementing the branch_base_position when no
|
|
236
325
|
// matching build was found. See https://www.chromium.org/getting-involved/download-chromium
|
|
237
|
-
|
|
238
|
-
if (
|
|
239
|
-
throw new Error(
|
|
326
|
+
base--;
|
|
327
|
+
if (base <= startBase - MAX_VERSION_DECREMENTS)
|
|
328
|
+
throw new Error(`${BROWSER_DOWNLOAD_ERROR}: Chromium base ${startBase}`);
|
|
240
329
|
}
|
|
241
330
|
}
|
|
242
|
-
|
|
243
331
|
await extractZip(archive, {dir: browserDir});
|
|
244
|
-
return {binary: Chromium.#getBinaryPath(browserDir), revision};
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Downloads the browser binary file.
|
|
249
|
-
* @param {string} version - Either "latest", "beta", "dev" or a full version
|
|
250
|
-
* number (i.e. "77.0.3865.0"). Defaults to "latest".
|
|
251
|
-
* @return {BrowserBinary}
|
|
252
|
-
*/
|
|
253
|
-
static async downloadBinary(version = "latest") {
|
|
254
|
-
const MIN_VERSION = 75;
|
|
255
|
-
const CHANNELS = ["latest", "beta", "dev"];
|
|
256
332
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
let versionNumber = CHANNELS.includes(version) ?
|
|
260
|
-
await Chromium.#getVersionForChannel(version) : version;
|
|
261
|
-
|
|
262
|
-
let base = await Chromium.#getBranchBasePosition(versionNumber);
|
|
263
|
-
|
|
264
|
-
let {binary, revision} = await Chromium.#downloadChromium(base);
|
|
265
|
-
return {binary, versionNumber, revision};
|
|
333
|
+
return {binary, versionNumber, base};
|
|
266
334
|
}
|
|
267
335
|
|
|
268
|
-
static async #installDriver(
|
|
269
|
-
let [dir, zip,
|
|
336
|
+
static async #installDriver(base, versionNumber) {
|
|
337
|
+
let [dir, zip, driverBinary] = {
|
|
270
338
|
"win32-ia32": ["Win", "chromedriver_win32.zip", "chromedriver.exe"],
|
|
271
339
|
"win32-x64": ["Win_x64", "chromedriver_win32.zip", "chromedriver.exe"],
|
|
272
340
|
"linux-x64": ["Linux_x64", "chromedriver_linux64.zip", "chromedriver"],
|
|
273
341
|
"darwin-x64": ["Mac", "chromedriver_mac64.zip", "chromedriver"],
|
|
274
342
|
"darwin-arm64": ["Mac_Arm", "chromedriver_mac64.zip", "chromedriver"]
|
|
275
|
-
}[
|
|
343
|
+
}[platformArch];
|
|
276
344
|
|
|
277
|
-
let cacheDir = path.join(snapshotsBaseDir, "chromium", "cache",
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
let archive = path.join(cacheDir, `${revision}-${zip}`);
|
|
345
|
+
let cacheDir = path.join(snapshotsBaseDir, "chromium", "cache",
|
|
346
|
+
versionNumber);
|
|
347
|
+
let archive = path.join(cacheDir, `${base}-${zip}`);
|
|
281
348
|
|
|
282
|
-
|
|
283
|
-
|
|
349
|
+
try {
|
|
350
|
+
await fs.promises.access(archive);
|
|
351
|
+
await extractZip(archive, {dir: cacheDir});
|
|
352
|
+
}
|
|
353
|
+
catch (e) { // zip file is either not cached or corrupted
|
|
354
|
+
let url = `https://commondatastorage.googleapis.com/chromium-browser-snapshots/${dir}/${base}/${zip}`;
|
|
355
|
+
try {
|
|
356
|
+
await download(url, archive);
|
|
357
|
+
}
|
|
358
|
+
catch (err) {
|
|
359
|
+
throw new Error(`${DRIVER_DOWNLOAD_ERROR}: ${url}\n${err}`);
|
|
360
|
+
}
|
|
361
|
+
await extractZip(archive, {dir: cacheDir});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
await killDriverProcess("chromedriver");
|
|
365
|
+
let driverPath = path.join(cacheDir, zip.split(".")[0], driverBinary);
|
|
366
|
+
try {
|
|
367
|
+
await fs.promises.rm(driverPath, {recursive: true});
|
|
368
|
+
}
|
|
369
|
+
catch (e) {} // file does not exist
|
|
284
370
|
await extractZip(archive, {dir: cacheDir});
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
await fs.promises.copyFile(path.join(cacheDir, zip.split(".")[0], driver),
|
|
288
|
-
path.join(destinationDir, driver));
|
|
371
|
+
|
|
372
|
+
return driverPath;
|
|
289
373
|
}
|
|
290
374
|
|
|
291
375
|
/** @see Browser.getDriver */
|
|
@@ -293,10 +377,9 @@ class Chromium extends Browser {
|
|
|
293
377
|
headless = true, extensionPaths = [], incognito = false, insecure = false,
|
|
294
378
|
extraArgs = []
|
|
295
379
|
} = {}) {
|
|
296
|
-
let {binary,
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
380
|
+
let {binary, versionNumber, base} = await Chromium.installBrowser(version);
|
|
381
|
+
let driverPath = await Chromium.#installDriver(base, versionNumber);
|
|
382
|
+
let serviceBuilder = new chrome.ServiceBuilder(driverPath);
|
|
300
383
|
let options = new chrome.Options().addArguments("no-sandbox", ...extraArgs);
|
|
301
384
|
if (extensionPaths.length > 0)
|
|
302
385
|
options.addArguments(`load-extension=${extensionPaths.join(",")}`);
|
|
@@ -311,6 +394,7 @@ class Chromium extends Browser {
|
|
|
311
394
|
let builder = new webdriver.Builder();
|
|
312
395
|
builder.forBrowser("chrome");
|
|
313
396
|
builder.setChromeOptions(options);
|
|
397
|
+
builder.setChromeService(serviceBuilder);
|
|
314
398
|
|
|
315
399
|
return builder.build();
|
|
316
400
|
}
|
|
@@ -335,106 +419,106 @@ class Chromium extends Browser {
|
|
|
335
419
|
continue;
|
|
336
420
|
|
|
337
421
|
extensionDetails = shadowRoot.getElementById("detailsButton");
|
|
422
|
+
break;
|
|
338
423
|
}
|
|
339
|
-
|
|
340
424
|
if (!extensionDetails)
|
|
341
|
-
reject(
|
|
425
|
+
reject(`${args[1]}: ${args[0]}`);
|
|
342
426
|
|
|
343
427
|
extensionDetails.click();
|
|
344
428
|
setTimeout(() => resolve(enable()), 100);
|
|
345
429
|
});
|
|
346
|
-
}, extensionTitle);
|
|
430
|
+
}, extensionTitle, EXTENSION_NOT_FOUND_ERROR);
|
|
347
431
|
}
|
|
348
432
|
}
|
|
349
433
|
|
|
350
434
|
/**
|
|
351
|
-
*
|
|
435
|
+
* Browser and webdriver functionality for Firefox.
|
|
352
436
|
* @hideconstructor
|
|
353
437
|
* @extends Browser
|
|
354
438
|
*/
|
|
355
439
|
class Firefox extends Browser {
|
|
356
|
-
static
|
|
440
|
+
static #CHANNELS = ["latest", "beta"];
|
|
441
|
+
|
|
442
|
+
static async #getVersionForChannel(channel) {
|
|
443
|
+
if (!Firefox.#CHANNELS.includes(channel))
|
|
444
|
+
return channel;
|
|
445
|
+
|
|
357
446
|
let data = await got("https://product-details.mozilla.org/1.0/firefox_versions.json").json();
|
|
358
|
-
return
|
|
447
|
+
return channel == "beta" ?
|
|
359
448
|
data.LATEST_FIREFOX_DEVEL_VERSION : data.LATEST_FIREFOX_VERSION;
|
|
360
449
|
}
|
|
361
450
|
|
|
362
451
|
static #getBinaryPath(dir) {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
return path.join(dir, "Firefox.app", "Contents", "MacOS", "firefox");
|
|
370
|
-
default:
|
|
371
|
-
throw new Error(`Unexpected platform: ${process.platform}`);
|
|
372
|
-
}
|
|
452
|
+
checkPlatform();
|
|
453
|
+
return {
|
|
454
|
+
win32: path.join(dir, "core", "firefox.exe"),
|
|
455
|
+
linux: path.join(dir, "firefox", "firefox"),
|
|
456
|
+
darwin: path.join(dir, "Firefox.app", "Contents", "MacOS", "firefox")
|
|
457
|
+
}[platform];
|
|
373
458
|
}
|
|
374
459
|
|
|
375
460
|
static #extractFirefoxArchive(archive, dir) {
|
|
376
|
-
switch (
|
|
461
|
+
switch (platform) {
|
|
377
462
|
case "win32":
|
|
378
|
-
// Procedure inspired from mozinstall:
|
|
379
|
-
// https://hg.mozilla.org/mozilla-central/file/tip/testing/mozbase/mozinstall/mozinstall/mozinstall.py
|
|
380
463
|
return promisify(exec)(`"${archive}" /extractdir=${dir}`);
|
|
381
464
|
case "linux":
|
|
382
465
|
return extractTar(archive, dir);
|
|
383
466
|
case "darwin":
|
|
384
467
|
return extractDmg(archive, dir);
|
|
385
468
|
default:
|
|
386
|
-
|
|
469
|
+
checkPlatform();
|
|
387
470
|
}
|
|
388
471
|
}
|
|
389
472
|
|
|
390
|
-
|
|
473
|
+
/**
|
|
474
|
+
* Installs the browser. The Firefox executable gets extracted in the
|
|
475
|
+
* {@link snapshotsBaseDir} folder, ready to go.
|
|
476
|
+
* @param {string} version - Either "latest", "beta" or a full version
|
|
477
|
+
* number (i.e. "68.0"). Defaults to "latest".
|
|
478
|
+
* @return {BrowserBinary}
|
|
479
|
+
* @throws {Error} Unsupported browser version, Unsupported platform, Browser
|
|
480
|
+
* download failed.
|
|
481
|
+
*/
|
|
482
|
+
static async installBrowser(version = "latest") {
|
|
483
|
+
const MIN_VERSION = 60;
|
|
484
|
+
|
|
485
|
+
checkVersion(version, MIN_VERSION, Firefox.#CHANNELS);
|
|
486
|
+
let versionNumber = await Firefox.#getVersionForChannel(version);
|
|
487
|
+
|
|
391
488
|
let [buildPlatform, fileName] = {
|
|
392
|
-
"win32-ia32": ["win32", `Firefox Setup ${
|
|
393
|
-
"win32-x64": ["win64", `Firefox Setup ${
|
|
394
|
-
"linux-x64": ["linux-x86_64", `firefox-${
|
|
395
|
-
"darwin-x64": ["mac", `Firefox ${
|
|
396
|
-
"darwin-arm64": ["mac", `Firefox ${
|
|
397
|
-
}[
|
|
489
|
+
"win32-ia32": ["win32", `Firefox Setup ${versionNumber}.exe`],
|
|
490
|
+
"win32-x64": ["win64", `Firefox Setup ${versionNumber}.exe`],
|
|
491
|
+
"linux-x64": ["linux-x86_64", `firefox-${versionNumber}.tar.bz2`],
|
|
492
|
+
"darwin-x64": ["mac", `Firefox ${versionNumber}.dmg`],
|
|
493
|
+
"darwin-arm64": ["mac", `Firefox ${versionNumber}.dmg`]
|
|
494
|
+
}[platformArch];
|
|
398
495
|
|
|
399
496
|
let snapshotsDir = path.join(snapshotsBaseDir, "firefox");
|
|
400
|
-
let browserDir = path.join(snapshotsDir,
|
|
401
|
-
|
|
402
|
-
|
|
497
|
+
let browserDir = path.join(snapshotsDir,
|
|
498
|
+
`firefox-${platformArch}-${versionNumber}`);
|
|
499
|
+
let binary = Firefox.#getBinaryPath(browserDir);
|
|
403
500
|
try {
|
|
404
501
|
await fs.promises.access(browserDir);
|
|
405
|
-
return
|
|
502
|
+
return {binary, versionNumber};
|
|
406
503
|
}
|
|
407
504
|
catch (e) {}
|
|
408
505
|
|
|
506
|
+
let archive = path.join(snapshotsDir, "cache", fileName);
|
|
409
507
|
await fs.promises.mkdir(path.dirname(browserDir), {recursive: true});
|
|
410
508
|
try {
|
|
411
509
|
await fs.promises.access(archive);
|
|
412
510
|
}
|
|
413
511
|
catch (e) {
|
|
414
|
-
let url = `https://archive.mozilla.org/pub/firefox/releases/${
|
|
415
|
-
|
|
512
|
+
let url = `https://archive.mozilla.org/pub/firefox/releases/${versionNumber}/${buildPlatform}/en-US/${fileName}`;
|
|
513
|
+
try {
|
|
514
|
+
await download(url, archive);
|
|
515
|
+
}
|
|
516
|
+
catch (err) {
|
|
517
|
+
throw new Error(`${BROWSER_DOWNLOAD_ERROR}: ${url}\n${err}`);
|
|
518
|
+
}
|
|
416
519
|
}
|
|
417
|
-
|
|
418
520
|
await Firefox.#extractFirefoxArchive(archive, browserDir);
|
|
419
|
-
return Firefox.#getBinaryPath(browserDir);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
/**
|
|
423
|
-
* Downloads the browser binary file
|
|
424
|
-
* @param {string} version - Either "latest", "beta" or a full version
|
|
425
|
-
* number (i.e. "68.0"). Defaults to "latest".
|
|
426
|
-
* @return {BrowserBinary}
|
|
427
|
-
*/
|
|
428
|
-
static async downloadBinary(version = "latest") {
|
|
429
|
-
const MIN_VERSION = 60;
|
|
430
|
-
const CHANNELS = ["latest", "beta"];
|
|
431
|
-
|
|
432
|
-
checkVersion(version, MIN_VERSION, CHANNELS);
|
|
433
521
|
|
|
434
|
-
let versionNumber = CHANNELS.includes(version) ?
|
|
435
|
-
await Firefox.#getVersionForChannel(version) : version;
|
|
436
|
-
|
|
437
|
-
let binary = await Firefox.#downloadFirefox(versionNumber);
|
|
438
522
|
return {binary, versionNumber};
|
|
439
523
|
}
|
|
440
524
|
|
|
@@ -443,7 +527,7 @@ class Firefox extends Browser {
|
|
|
443
527
|
headless = true, extensionPaths = [], incognito = false, insecure = false,
|
|
444
528
|
extraArgs = []
|
|
445
529
|
} = {}) {
|
|
446
|
-
let {binary} = await Firefox.
|
|
530
|
+
let {binary} = await Firefox.installBrowser(version);
|
|
447
531
|
|
|
448
532
|
let options = new firefox.Options();
|
|
449
533
|
if (headless)
|
|
@@ -472,7 +556,7 @@ class Firefox extends Browser {
|
|
|
472
556
|
throw err;
|
|
473
557
|
await killDriverProcess("geckodriver");
|
|
474
558
|
}
|
|
475
|
-
}, 30000,
|
|
559
|
+
}, 30000, `${DRIVER_START_ERROR}: geckodriver`, 1000);
|
|
476
560
|
|
|
477
561
|
for (let extensionPath of extensionPaths) {
|
|
478
562
|
await driver.execute(
|
|
@@ -486,10 +570,9 @@ class Firefox extends Browser {
|
|
|
486
570
|
|
|
487
571
|
/** @see Browser.enableExtensionInIncognito */
|
|
488
572
|
static async enableExtensionInIncognito(driver, extensionTitle) {
|
|
489
|
-
let version = await getBrowserVersion(
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
throw new Error(`Only supported on Firefox >= 87. Current version: ${version}`);
|
|
573
|
+
let version = (await driver.getCapabilities()).getBrowserVersion();
|
|
574
|
+
// The UI workaround assumes web elements only present on Firefox >= 87
|
|
575
|
+
checkVersion(version, 87);
|
|
493
576
|
|
|
494
577
|
await driver.navigate().to("about:addons");
|
|
495
578
|
await driver.wait(until.elementLocated(By.name("extension")), 1000).click();
|
|
@@ -502,33 +585,50 @@ class Firefox extends Browser {
|
|
|
502
585
|
await elem.click();
|
|
503
586
|
return await driver.findElement(By.name("private-browsing")).click();
|
|
504
587
|
}
|
|
505
|
-
throw new Error(
|
|
588
|
+
throw new Error(`${EXTENSION_NOT_FOUND_ERROR}: ${extensionTitle}`);
|
|
506
589
|
}
|
|
507
590
|
}
|
|
508
591
|
|
|
509
592
|
/**
|
|
510
|
-
*
|
|
593
|
+
* Browser and webdriver functionality for Edge.
|
|
511
594
|
* @hideconstructor
|
|
512
595
|
* @extends Browser
|
|
513
596
|
*/
|
|
514
597
|
class Edge extends Browser {
|
|
515
|
-
static #
|
|
598
|
+
static #CHANNELS = ["latest", "beta", "dev"];
|
|
516
599
|
|
|
517
600
|
static async #getVersionForChannel(version) {
|
|
518
|
-
|
|
519
|
-
|
|
601
|
+
let channel = "stable";
|
|
602
|
+
if (Edge.#CHANNELS.includes(version) && version != "latest")
|
|
603
|
+
channel = version;
|
|
520
604
|
|
|
521
|
-
let channel = version == "latest" ? "stable" : version;
|
|
522
605
|
let {body} = await got(`https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edge-${channel}/`);
|
|
523
|
-
let
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
versionNumbers
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
606
|
+
let versionNumber;
|
|
607
|
+
if (Edge.#CHANNELS.includes(version)) {
|
|
608
|
+
let regex = /href="microsoft-edge-(stable|beta|dev)_(.*?)-1_/gm;
|
|
609
|
+
let matches;
|
|
610
|
+
let versionNumbers = [];
|
|
611
|
+
while ((matches = regex.exec(body)) !== null)
|
|
612
|
+
versionNumbers.push(matches[2]);
|
|
613
|
+
|
|
614
|
+
let compareVersions = (v1, v2) =>
|
|
615
|
+
parseInt(v1.split(".")[0], 10) < parseInt(v2.split(".")[0], 10) ?
|
|
616
|
+
1 : -1;
|
|
617
|
+
versionNumber = versionNumbers.sort(compareVersions)[0];
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
let split = version.split(".");
|
|
621
|
+
let minorVersion = split.length == 4 ? parseInt(split.pop(), 10) : -1;
|
|
622
|
+
let majorVersion = split.join(".");
|
|
623
|
+
let found;
|
|
624
|
+
while (!found && minorVersion >= 0) {
|
|
625
|
+
versionNumber = `${majorVersion}.${minorVersion}`;
|
|
626
|
+
found = body.includes(versionNumber);
|
|
627
|
+
minorVersion--;
|
|
628
|
+
}
|
|
629
|
+
if (!found)
|
|
630
|
+
throw new Error(`${UNSUPPORTED_VERSION_ERROR}: ${version}`);
|
|
631
|
+
}
|
|
532
632
|
|
|
533
633
|
return {versionNumber, channel};
|
|
534
634
|
}
|
|
@@ -540,10 +640,11 @@ class Edge extends Browser {
|
|
|
540
640
|
}
|
|
541
641
|
|
|
542
642
|
static #getBinaryPath(channel = "stable") {
|
|
543
|
-
switch (
|
|
643
|
+
switch (platform) {
|
|
544
644
|
case "win32":
|
|
545
|
-
|
|
546
|
-
"
|
|
645
|
+
let programFiles = process.env["ProgramFiles(x86)"] ?
|
|
646
|
+
"${Env:ProgramFiles(x86)}" : "${Env:ProgramFiles}";
|
|
647
|
+
return `${programFiles}\\Microsoft\\Edge\\Application\\msedge.exe`;
|
|
547
648
|
case "linux":
|
|
548
649
|
return channel == "stable" ?
|
|
549
650
|
"microsoft-edge" : `microsoft-edge-${channel}`;
|
|
@@ -551,26 +652,28 @@ class Edge extends Browser {
|
|
|
551
652
|
let appName = Edge.#getDarwinAppName(channel);
|
|
552
653
|
return `${process.env.HOME}/Applications/${appName}.app/Contents/MacOS/${appName}`;
|
|
553
654
|
default:
|
|
554
|
-
|
|
655
|
+
checkPlatform();
|
|
555
656
|
}
|
|
556
657
|
}
|
|
557
658
|
|
|
558
659
|
/**
|
|
559
|
-
*
|
|
660
|
+
* Installs the browser. On Linux, Edge is installed as a system package,
|
|
661
|
+
* which requires root permissions. On MacOS, Edge is installed as a user
|
|
662
|
+
* app (not as a system app). Installing Edge on Windows is not supported.
|
|
560
663
|
* @param {string} version - Either "latest", "beta", "dev" or a full version
|
|
561
|
-
* number (i.e. "95.0.1020.
|
|
562
|
-
* available on Linux.
|
|
664
|
+
* number (i.e. "95.0.1020.40"). Defaults to "latest".
|
|
563
665
|
* @return {BrowserBinary}
|
|
666
|
+
* @throws {Error} Unsupported browser version, Unsupported platform, Browser
|
|
667
|
+
* download failed.
|
|
564
668
|
*/
|
|
565
|
-
static async
|
|
566
|
-
if (
|
|
669
|
+
static async installBrowser(version = "latest") {
|
|
670
|
+
if (platform == "win32")
|
|
567
671
|
// Edge is mandatory on Windows, can't be uninstalled or downgraded
|
|
568
672
|
// https://support.microsoft.com/en-us/microsoft-edge/why-can-t-i-uninstall-microsoft-edge-ee150b3b-7d7a-9984-6d83-eb36683d526d
|
|
569
|
-
throw new Error(
|
|
673
|
+
throw new Error(`${UNSUPPORTED_PLATFORM_ERROR}: ${platform}`);
|
|
570
674
|
|
|
571
675
|
const MIN_VERSION = 95;
|
|
572
|
-
|
|
573
|
-
checkVersion(version, MIN_VERSION, CHANNELS);
|
|
676
|
+
checkVersion(version, MIN_VERSION, Edge.#CHANNELS);
|
|
574
677
|
let {versionNumber, channel} = await Edge.#getVersionForChannel(version);
|
|
575
678
|
|
|
576
679
|
let darwinName = {
|
|
@@ -581,14 +684,14 @@ class Edge extends Browser {
|
|
|
581
684
|
let filename = {
|
|
582
685
|
linux: `microsoft-edge-${channel}_${versionNumber}-1_amd64.deb`,
|
|
583
686
|
darwin: `${darwinName}-${versionNumber}.pkg`
|
|
584
|
-
}[
|
|
687
|
+
}[platform];
|
|
585
688
|
let darwinArch = process.arch == "arm64" ?
|
|
586
689
|
"03adf619-38c6-4249-95ff-4a01c0ffc962" :
|
|
587
690
|
"C1297A47-86C4-4C1F-97FA-950631F94777";
|
|
588
|
-
let
|
|
691
|
+
let url = {
|
|
589
692
|
linux: `https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edge-${channel}/${filename}`,
|
|
590
693
|
darwin: `https://officecdnmac.microsoft.com/pr/${darwinArch}/MacAutoupdate/${filename}`
|
|
591
|
-
}[
|
|
694
|
+
}[platform];
|
|
592
695
|
|
|
593
696
|
let snapshotsDir = path.join(snapshotsBaseDir, "edge");
|
|
594
697
|
let archive = path.join(snapshotsDir, "cache", filename);
|
|
@@ -600,16 +703,16 @@ class Edge extends Browser {
|
|
|
600
703
|
catch (e) {}
|
|
601
704
|
|
|
602
705
|
try {
|
|
603
|
-
await download(
|
|
706
|
+
await download(url, archive);
|
|
604
707
|
}
|
|
605
708
|
catch (err) {
|
|
606
|
-
throw new Error(
|
|
709
|
+
throw new Error(`${BROWSER_DOWNLOAD_ERROR}: ${url}\n${err}`);
|
|
607
710
|
}
|
|
608
711
|
|
|
609
|
-
if (
|
|
712
|
+
if (platform == "linux") {
|
|
610
713
|
await promisify(exec)(`dpkg -i ${archive}`);
|
|
611
714
|
}
|
|
612
|
-
else if (
|
|
715
|
+
else if (platform == "darwin") {
|
|
613
716
|
let appName = Edge.#getDarwinAppName(channel);
|
|
614
717
|
try {
|
|
615
718
|
await fs.promises.rm(`${process.env.HOME}/Applications/${appName}.app`, {recursive: true});
|
|
@@ -629,47 +732,55 @@ class Edge extends Browser {
|
|
|
629
732
|
}
|
|
630
733
|
|
|
631
734
|
static async #installDriver() {
|
|
735
|
+
async function extractEdgeZip(archive, cacheDir, driverPath) {
|
|
736
|
+
await killDriverProcess("msedgedriver");
|
|
737
|
+
try {
|
|
738
|
+
await fs.promises.rm(driverPath, {recursive: true});
|
|
739
|
+
}
|
|
740
|
+
catch (e) {} // file does not exist
|
|
741
|
+
await extractZip(archive, {dir: cacheDir});
|
|
742
|
+
}
|
|
743
|
+
|
|
632
744
|
let binary = Edge.#getBinaryPath();
|
|
633
745
|
let versionNumber = await Edge.#getInstalledVersionNumber(binary);
|
|
634
746
|
if (!versionNumber)
|
|
635
|
-
throw new Error(
|
|
747
|
+
throw new Error(`${BROWSER_NOT_INSTALLED_ERROR}: Edge`);
|
|
636
748
|
|
|
637
|
-
let [zip,
|
|
749
|
+
let [zip, driverBinary] = {
|
|
638
750
|
"win32-ia32": ["edgedriver_win32.zip", "msedgedriver.exe"],
|
|
639
751
|
"win32-x64": ["edgedriver_win64.zip", "msedgedriver.exe"],
|
|
640
752
|
"linux-x64": ["edgedriver_linux64.zip", "msedgedriver"],
|
|
641
753
|
"darwin-x64": ["edgedriver_mac64.zip", "msedgedriver"],
|
|
642
754
|
"darwin-arm64": ["edgedriver_arm64.zip", "msedgedriver"]
|
|
643
|
-
}[
|
|
755
|
+
}[platformArch];
|
|
644
756
|
let cacheDir = path.join(snapshotsBaseDir, "edge", "cache",
|
|
645
757
|
`edgedriver-${versionNumber}`);
|
|
646
758
|
let archive = path.join(cacheDir, `${versionNumber}-${zip}`);
|
|
759
|
+
let driverPath = path.join(cacheDir, driverBinary);
|
|
647
760
|
|
|
648
|
-
let vSplit = versionNumber.split(".");
|
|
649
|
-
let lastNum = parseInt(vSplit[3], 10);
|
|
650
|
-
while (lastNum >= 0) {
|
|
651
|
-
try {
|
|
652
|
-
let attempt = `${vSplit[0]}.${vSplit[1]}.${vSplit[2]}.${lastNum}`;
|
|
653
|
-
await download(`https://msedgedriver.azureedge.net/${attempt}/${zip}`,
|
|
654
|
-
archive);
|
|
655
|
-
break;
|
|
656
|
-
}
|
|
657
|
-
catch (e) {
|
|
658
|
-
lastNum--;
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
if (lastNum < 0)
|
|
663
|
-
throw new Error(`msedgedriver was not found for Edge ${versionNumber}`);
|
|
664
|
-
|
|
665
|
-
await killDriverProcess(Edge.#DRIVER);
|
|
666
|
-
let driverPath = path.join(cacheDir, driver);
|
|
667
761
|
try {
|
|
668
|
-
await fs.promises.
|
|
762
|
+
await fs.promises.access(archive);
|
|
763
|
+
await extractEdgeZip(archive, cacheDir, driverPath);
|
|
669
764
|
}
|
|
670
|
-
catch (e) {
|
|
671
|
-
|
|
765
|
+
catch (e) { // zip file is either not cached or corrupted
|
|
766
|
+
let vSplit = versionNumber.split(".");
|
|
767
|
+
let lastNum = parseInt(vSplit[3], 10);
|
|
768
|
+
while (lastNum >= 0) {
|
|
769
|
+
try {
|
|
770
|
+
let attempt = `${vSplit[0]}.${vSplit[1]}.${vSplit[2]}.${lastNum}`;
|
|
771
|
+
await download(`https://msedgedriver.azureedge.net/${attempt}/${zip}`,
|
|
772
|
+
archive);
|
|
773
|
+
break;
|
|
774
|
+
}
|
|
775
|
+
catch (e2) {
|
|
776
|
+
lastNum--;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
if (lastNum < 0)
|
|
780
|
+
throw new Error(`${DRIVER_DOWNLOAD_ERROR}: Edge ${versionNumber}`);
|
|
672
781
|
|
|
782
|
+
await extractEdgeZip(archive, cacheDir, driverPath);
|
|
783
|
+
}
|
|
673
784
|
return driverPath;
|
|
674
785
|
}
|
|
675
786
|
|
|
@@ -678,8 +789,8 @@ class Edge extends Browser {
|
|
|
678
789
|
headless = true, extensionPaths = [], incognito = false, insecure = false,
|
|
679
790
|
extraArgs = []
|
|
680
791
|
} = {}) {
|
|
681
|
-
if (
|
|
682
|
-
await Edge.
|
|
792
|
+
if (platform == "linux" || platform == "darwin")
|
|
793
|
+
await Edge.installBrowser(version);
|
|
683
794
|
|
|
684
795
|
let driverPath = await Edge.#installDriver();
|
|
685
796
|
let serviceBuilder = new edge.ServiceBuilder(driverPath);
|
|
@@ -718,257 +829,22 @@ class Edge extends Browser {
|
|
|
718
829
|
await button.click();
|
|
719
830
|
return await driver.findElement(By.id("itemAllowIncognito")).click();
|
|
720
831
|
}
|
|
721
|
-
throw new Error(
|
|
722
|
-
}
|
|
723
|
-
throw new Error(`Extension "${extensionTitle}" not found`);
|
|
724
|
-
}
|
|
725
|
-
}
|
|
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);
|
|
832
|
+
throw new Error(`${ELEMENT_NOT_FOUND_ERROR}: Details button`);
|
|
905
833
|
}
|
|
906
|
-
|
|
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);
|
|
834
|
+
throw new Error(`${EXTENSION_NOT_FOUND_ERROR}: ${extensionTitle}`);
|
|
960
835
|
}
|
|
961
836
|
}
|
|
962
837
|
|
|
963
838
|
/**
|
|
964
839
|
* @type {Object}
|
|
965
|
-
* @property {Chromium} chromium -
|
|
966
|
-
*
|
|
967
|
-
* @property {
|
|
840
|
+
* @property {Chromium} chromium - Browser and webdriver functionality for
|
|
841
|
+
* Chromium.
|
|
842
|
+
* @property {Firefox} firefox - Browser and webdriver functionality for
|
|
843
|
+
* Firefox.
|
|
844
|
+
* @property {Edge} edge - Browser and webdriver functionality for Edge.
|
|
968
845
|
*/
|
|
969
846
|
export const BROWSERS = {
|
|
970
847
|
chromium: Chromium,
|
|
971
848
|
firefox: Firefox,
|
|
972
|
-
edge: Edge
|
|
973
|
-
opera: Opera
|
|
849
|
+
edge: Edge
|
|
974
850
|
};
|