@eyeo/get-browser-binary 0.1.1

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/.eslintignore ADDED
@@ -0,0 +1,3 @@
1
+ /node_modules/
2
+ /browser-snapshots/
3
+ /docs/
package/.eslintrc.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "extends": [
3
+ "eslint-config-eyeo"
4
+ ],
5
+ "root": true,
6
+ "env": {
7
+ "es2022": true,
8
+ "node": true
9
+ },
10
+ "rules": {
11
+ "brace-style": ["error", "stroustrup"],
12
+ "curly": [
13
+ "error",
14
+ "multi-or-nest",
15
+ "consistent"
16
+ ],
17
+ "max-len": [
18
+ "error",
19
+ {
20
+ "ignoreUrls": true,
21
+ "ignoreRegExpLiterals": true,
22
+ "ignoreTemplateLiterals": true
23
+ }
24
+ ],
25
+ "valid-jsdoc": 0
26
+ },
27
+ "parserOptions": {
28
+ "sourceType": "module",
29
+ "ecmaVersion": 2022
30
+ },
31
+ "overrides": [
32
+ {
33
+ "files": [
34
+ "background.js"
35
+ ],
36
+ "parserOptions": {
37
+ "sourceType": "script"
38
+ }
39
+ }
40
+ ]
41
+ }
package/.gitlab-ci.yml ADDED
@@ -0,0 +1,75 @@
1
+ default:
2
+ image: registry.gitlab.com/eyeo/docker/adblockplus-ci:node16-no-python
3
+ interruptible: true
4
+
5
+ stages:
6
+ - test
7
+ - docs
8
+
9
+ variables:
10
+ npm_config_audit: "false"
11
+ npm_config_fund: "false"
12
+ npm_config_prefer_offline: "true"
13
+
14
+ cache:
15
+ - key:
16
+ prefix: $CI_JOB_IMAGE
17
+ files:
18
+ - package-lock.json
19
+ paths:
20
+ - node_modules/
21
+
22
+ test:linux:
23
+ stage: test
24
+ before_script:
25
+ - apt-get update && apt-get install -y procps
26
+ - npm install
27
+ # https://www.how2shout.com/linux/install-microsoft-edge-on-linux/
28
+ - wget https://packages.microsoft.com/keys/microsoft.asc
29
+ - cat microsoft.asc | gpg --dearmor > microsoft.gpg
30
+ - install -o root -g root -m 644 microsoft.gpg /etc/apt/trusted.gpg.d/
31
+ - echo "deb [arch=amd64] https://packages.microsoft.com/repos/edge stable main" > /etc/apt/sources.list.d/microsoft-edge-dev.list
32
+ - rm microsoft.*
33
+ - apt-get update && apt-get install -y microsoft-edge-stable
34
+ script:
35
+ - npm run lint
36
+ - xvfb-run -a npm test -- -g "chromium (latest|beta)"
37
+ - xvfb-run -a npm test -- -g "chromium dev"
38
+ - xvfb-run -a npm test -- -g "chromium 77"
39
+ - xvfb-run -a npm test -- -g "(edge|firefox)"
40
+
41
+ test:windows:
42
+ stage: test
43
+ variables:
44
+ CI_PROJECT_ID_MAINSTREAM: 36688302
45
+ before_script:
46
+ - Invoke-WebRequest
47
+ -Uri "${Env:CI_API_V4_URL}/projects/${Env:CI_PROJECT_ID_MAINSTREAM}/packages/generic/microsoft-edge/79.0.309/MicrosoftEdgeEnterpriseX64.msi"
48
+ -Headers @{'JOB-TOKEN' = $Env:CI_JOB_TOKEN}
49
+ -OutFile 'MicrosoftEdgeEnterpriseX64.msi'
50
+ - Start-Process msiexec
51
+ -ArgumentList "/i MicrosoftEdgeEnterpriseX64.msi /norestart /qn" -Wait
52
+ - choco upgrade -y nodejs --version 16.10.0
53
+ - npm install
54
+ script:
55
+ - npm test -- -g "chromium latest"
56
+ - npm test -- -g "chromium beta"
57
+ - npm test -- -g "chromium dev"
58
+ - npm test -- -g "chromium 77"
59
+ - npm test -- -g "edge"
60
+ tags:
61
+ - shared-windows
62
+ - windows
63
+ - windows-1809
64
+ cache: {}
65
+
66
+ docs:
67
+ stage: docs
68
+ needs: []
69
+ before_script:
70
+ - npm install
71
+ script:
72
+ - npm run docs
73
+ artifacts:
74
+ paths:
75
+ - docs/
package/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # get-browser-binary
2
+
3
+ Download specific browser versions for Chromium, Firefox and Edge, and their
4
+ matching [selenium webdriver](https://www.selenium.dev/selenium/docs/api/javascript/index.html).
5
+
6
+ Note: Edge download is not implemented, and it is assumed to be already
7
+ installed on the running system. In that case, the matching `msedgedriver`
8
+ will be downloaded accordingly.
9
+
10
+ ## Getting started
11
+
12
+ The sample below shows how to download the latest Chromium and run it using
13
+ selenium webdriver:
14
+
15
+ ```javascript
16
+ import {BROWSERS} from "@eyeo/get-browser-binary";
17
+
18
+ (async function example() {
19
+ let {chromium} = BROWSERS;
20
+
21
+ let {binary} = await chromium.downloadBinary();
22
+ console.log(`Chromium binary downloaded to ${binary}`);
23
+
24
+ let driver = await chromium.getDriver();
25
+ await driver.navigate().to("https://example.com/");
26
+ await driver.quit();
27
+ })();
28
+
29
+ ```
30
+
31
+ [test/runner.js](https://gitlab.com/eyeo/developer-experience/get-browser-binary/-/blob/main/test/runner.js)
32
+ provides other usage examples of the library.
33
+
34
+ For more information, please refer to the [API documention](https://gitlab.com/eyeo/developer-experience/get-browser-binary/-/jobs/artifacts/main/file/docs/index.html?job=docs).
35
+ If you are already on the documentation page, you may find the API contents on
36
+ the right side.
37
+
38
+ ## Development
39
+
40
+ ### Prerequisites
41
+
42
+ - Node >= 16.10.0
43
+ - npm >= 7
44
+
45
+ ### Installing/Updating dependencies
46
+
47
+ ```shell
48
+ npm install
49
+ ```
50
+
51
+ ## Testing
52
+
53
+ The `grep` option filters the tests to run with a regular expression. Example:
54
+
55
+ ```shell
56
+ npm test -- -g "chromium latest"
57
+ ```
58
+
59
+ Note: Running all Chromium tests at once is currently not working.
60
+
61
+ ## Building the documentation
62
+
63
+ ```shell
64
+ npm run docs
65
+ ```
@@ -0,0 +1,7 @@
1
+ # 0.1.1
2
+
3
+ Change project name to `get-browser-binary`.
4
+
5
+ # 0.1.0
6
+
7
+ Initial release.
package/index.js ADDED
@@ -0,0 +1,18 @@
1
+ /*
2
+ * Copyright (c) 2006-present eyeo GmbH
3
+ *
4
+ * This module is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU General Public License as published by
6
+ * the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * This program is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU General Public License
15
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+ */
17
+
18
+ export * from "./src/browsers.js";
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@eyeo/get-browser-binary",
3
+ "version": "0.1.1",
4
+ "description": "Download browser binaries and matching webdrivers",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://gitlab.com/eyeo/developer-experience/get-browser-binary"
8
+ },
9
+ "author": "eyeo GmbH",
10
+ "license": "GPL-3.0",
11
+ "engines": {
12
+ "node": ">=16.10.0",
13
+ "npm": ">=7"
14
+ },
15
+ "type": "module",
16
+ "main": "index.js",
17
+ "dependencies": {
18
+ "chromedriver": "^90.0.1",
19
+ "dmg": "^0.1.0",
20
+ "extract-zip": "^2.0.1",
21
+ "fs-extra": "^10.0.0",
22
+ "geckodriver": "^2.0.4",
23
+ "got": "^11.8.2",
24
+ "msedgedriver": "^91.0.0",
25
+ "selenium-webdriver": "^4.2.0"
26
+ },
27
+ "devDependencies": {
28
+ "eslint": "^8.17.0",
29
+ "eslint-config-eyeo": "^3.2.0",
30
+ "expect": "^25.5.0",
31
+ "jsdoc": "^3.6.10",
32
+ "mocha": "^10.0.0"
33
+ },
34
+ "scripts": {
35
+ "docs": "jsdoc --readme README.md --destination docs src/browsers.js",
36
+ "lint": "eslint --ext js .",
37
+ "test": "mocha test/runner.js --"
38
+ }
39
+ }
@@ -0,0 +1,541 @@
1
+ /*
2
+ * Copyright (c) 2006-present eyeo GmbH
3
+ *
4
+ * This module is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU General Public License as published by
6
+ * the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * This program is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU General Public License
15
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+ */
17
+
18
+ import path from "path";
19
+ import {exec, execFile} from "child_process";
20
+ import {promisify} from "util";
21
+ import fs from "fs";
22
+
23
+ import got from "got";
24
+ import webdriver from "selenium-webdriver";
25
+ import chrome from "selenium-webdriver/chrome.js";
26
+ import firefox from "selenium-webdriver/firefox.js";
27
+ import command from "selenium-webdriver/lib/command.js";
28
+ import extractZip from "extract-zip";
29
+
30
+ import {download, extractTar, extractDmg, runWinInstaller, getBrowserVersion}
31
+ from "./utils.js";
32
+
33
+ /**
34
+ * Root folder where browser and webdriver binaries get downloaded.
35
+ * @type {string}
36
+ */
37
+ export let snapshotsBaseDir = path.join(process.cwd(), "browser-snapshots");
38
+
39
+ let {until, By} = webdriver;
40
+ const ERROR_INCOGNITO_NOT_SUPPORTED = "Incognito mode is not supported";
41
+ const ERROR_DOWNLOAD_NOT_SUPPORTED =
42
+ "Downloading this browser is not supported";
43
+
44
+ /**
45
+ * Base class for browser download functionality. Please see subclasses for
46
+ * browser specific details.
47
+ * @hideconstructor
48
+ */
49
+ class Browser {
50
+ /**
51
+ * @typedef {Object} BrowserBinary
52
+ * @property {string} binary - The path to the Chromium binary.
53
+ */
54
+
55
+ /**
56
+ * Downloads the browser binary file.
57
+ * @param {string} version - Either full version number or channel/release.
58
+ * Please find examples on the subclasses.
59
+ * @return {BrowserBinary}
60
+ */
61
+ static async downloadBinary(version) {
62
+ throw new Error(ERROR_DOWNLOAD_NOT_SUPPORTED);
63
+ }
64
+
65
+ /**
66
+ * @typedef {Object} webdriver
67
+ * @see https://www.selenium.dev/selenium/docs/api/javascript/index.html
68
+ */
69
+
70
+ /**
71
+ * Installs the webdriver matching the browser version and runs the
72
+ * browser. If needed, the browser binary is also installed.
73
+ * @param {string} version - Either full version number or channel/release.
74
+ * Please find examples on the subclasses. On Edge this parameter has
75
+ * no effect.
76
+ * @param {boolean?} headless - Run the browser in headless mode, or not.
77
+ * @param {Array.<string>?} extensionPaths - Loads extensions to the running
78
+ * browser.
79
+ * @param {boolean?} incognito - Runs the browser in incognito mode, or not.
80
+ * @param {boolean?} insecure - Forces the browser to accept insecure
81
+ certificates, or not.
82
+ * @return {webdriver}
83
+ */
84
+ static async getDriver(version, headless = true, extensionPaths = [],
85
+ incognito = false, insecure = false) {
86
+ // to be implemented by the subclass
87
+ }
88
+
89
+ /**
90
+ * By default, extensions are disabled in incognito mode. This function
91
+ * enables the extension when loaded in incognito.
92
+ * @param {webdriver} driver - The driver controlling the browser.
93
+ * @param {string} extensionTitle - Title of the extebsion to be enabled.
94
+ * @return {webdriver}
95
+ */
96
+ static async enableExtensionInIncognito(driver, extensionTitle) {
97
+ throw new Error(ERROR_INCOGNITO_NOT_SUPPORTED);
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Download functionality for Chromium. This class can be used statically.
103
+ * @hideconstructor
104
+ * @extends Browser
105
+ */
106
+ class Chromium extends Browser {
107
+ static #DRIVER = "chromedriver";
108
+
109
+ static async #getBranchBasePosition(version) {
110
+ let data = await got(`https://omahaproxy.appspot.com/deps.json?version=${version}`).json();
111
+ return data.chromium_base_position;
112
+ }
113
+
114
+ static async #getLatestVersion(channel = "stable") {
115
+ let os = process.platform;
116
+ if (os == "win32")
117
+ os = process.arch == "x64" ? "win64" : "win";
118
+ else if (os == "darwin")
119
+ os = process.arch == "arm64" ? "mac_arm64" : "mac";
120
+
121
+ let data = await got(`https://omahaproxy.appspot.com/all.json?os=${os}`).json();
122
+ let release = data[0].versions.find(ver => ver.channel == channel);
123
+ let {current_version: version, branch_base_position: base} = release;
124
+
125
+ if (release.true_branch && release.true_branch.includes("_")) {
126
+ // A wrong base may be caused by a mini-branch (patched) release
127
+ // In that case, the base is taken from the unpatched version
128
+ version = [...version.split(".").slice(0, 3), "0"].join(".");
129
+ base = await Chromium.#getBranchBasePosition(version);
130
+ }
131
+
132
+ return {version, base};
133
+ }
134
+
135
+ static #getChromiumBinary(dir) {
136
+ switch (process.platform) {
137
+ case "win32":
138
+ return path.join(dir, "chrome-win", "chrome.exe");
139
+ case "linux":
140
+ return path.join(dir, "chrome-linux", "chrome");
141
+ case "darwin":
142
+ return path.join(dir, "chrome-mac", "Chromium.app", "Contents", "MacOS",
143
+ "Chromium");
144
+ default:
145
+ throw new Error(`Unexpected platform: ${process.platform}`);
146
+ }
147
+ }
148
+
149
+ static async #downloadChromium(chromiumRevision) {
150
+ const MAX_VERSION_DECREMENTS = 50;
151
+
152
+ let revision = parseInt(chromiumRevision, 10);
153
+ let startingRevision = revision;
154
+ let platform = `${process.platform}-${process.arch}`;
155
+
156
+ let buildTypes = {
157
+ "win32-ia32": ["Win", "chrome-win.zip"],
158
+ "win32-x64": ["Win_x64", "chrome-win.zip"],
159
+ "linux-x64": ["Linux_x64", "chrome-linux.zip"],
160
+ "darwin-x64": ["Mac", "chrome-mac.zip"],
161
+ "dawrin-arm64": ["Mac_Arm", "chrome-mac.zip"]
162
+ };
163
+
164
+ let [platformDir, fileName] = buildTypes[platform];
165
+ let archive = null;
166
+ let browserDir = null;
167
+ let snapshotsDir = path.join(snapshotsBaseDir, "chromium");
168
+
169
+ while (true) {
170
+ browserDir = path.join(snapshotsDir, `chromium-${platform}-${revision}`);
171
+
172
+ try {
173
+ await fs.promises.access(browserDir);
174
+ return {binary: Chromium.#getChromiumBinary(browserDir), revision};
175
+ }
176
+ catch (e) {}
177
+
178
+ await fs.promises.mkdir(path.dirname(browserDir), {recursive: true});
179
+
180
+ archive = path.join(snapshotsDir, "cache", `${revision}-${fileName}`);
181
+
182
+ try {
183
+ try {
184
+ await fs.promises.access(archive);
185
+ }
186
+ catch (e) {
187
+ await download(
188
+ `https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/${platformDir}%2F${revision}%2F${fileName}?alt=media`,
189
+ archive);
190
+ }
191
+ break;
192
+ }
193
+ catch (e) {
194
+ // Chromium advises decrementing the branch_base_position when no
195
+ // matching build was found. See https://www.chromium.org/getting-involved/download-chromium
196
+ revision--;
197
+ if (revision <= startingRevision - MAX_VERSION_DECREMENTS)
198
+ throw new Error(`No Chromium package found for ${startingRevision}`);
199
+ }
200
+ }
201
+
202
+ await extractZip(archive, {dir: browserDir});
203
+ return {binary: Chromium.#getChromiumBinary(browserDir), revision};
204
+ }
205
+
206
+ /**
207
+ * Downloads the browser binary file.
208
+ * @param {string} version - Either "latest", "beta", "dev" or a full version
209
+ * number (i.e. "77.0.3865.0"). Defaults to "latest".
210
+ * @return {BrowserBinary}
211
+ */
212
+ static async downloadBinary(version) {
213
+ let base;
214
+ if (version && !(version == "beta" || version == "dev"))
215
+ base = await Chromium.#getBranchBasePosition(version);
216
+ else
217
+ ({version, base} = await Chromium.#getLatestVersion(version));
218
+
219
+ let {binary, revision} = await Chromium.#downloadChromium(base);
220
+ return {binary, downloadedVersion: version, revision};
221
+ }
222
+
223
+ static async #installDriver(revision, version) {
224
+ let platform = `${process.platform}-${process.arch}`;
225
+ let buildTypes = {
226
+ "win32-ia32": ["Win", "chromedriver_win32.zip", "chromedriver.exe"],
227
+ "win32-x64": ["Win_x64", "chromedriver_win32.zip", "chromedriver.exe"],
228
+ "linux-x64": ["Linux_x64", "chromedriver_linux64.zip", "chromedriver"],
229
+ "darwin-x64": ["Mac", "chromedriver_mac64.zip", "chromedriver"],
230
+ "darwin-arm64": ["Mac_Arm", "chromedriver_mac64.zip", "chromedriver"]
231
+ };
232
+ let [dir, zip, driver] = buildTypes[platform];
233
+
234
+ let cacheDir = path.join(snapshotsBaseDir, "chromium", "cache", version);
235
+ let destinationDir = path.join(process.cwd(), "node_modules",
236
+ Chromium.#DRIVER, "lib", Chromium.#DRIVER);
237
+ let archive = path.join(cacheDir, `${revision}-${zip}`);
238
+
239
+ await download(`https://commondatastorage.googleapis.com/chromium-browser-snapshots/${dir}/${revision}/${zip}`,
240
+ archive);
241
+ await extractZip(archive, {dir: cacheDir});
242
+ await fs.promises.mkdir(destinationDir, {recursive: true});
243
+ try {
244
+ await fs.promises.copyFile(path.join(cacheDir, zip.split(".")[0], driver),
245
+ path.join(destinationDir, driver));
246
+ }
247
+ catch (err) {
248
+ // https://gitlab.com/eyeo/developer-experience/get-browser-binary/-/issues/3
249
+ if (!err.toString().includes("copyfile"))
250
+ throw err;
251
+ }
252
+ }
253
+
254
+ /** @see Browser.getDriver */
255
+ static async getDriver(version, headless = true, extensionPaths = [],
256
+ incognito = false, insecure = false) {
257
+ if (incognito)
258
+ throw new Error(ERROR_INCOGNITO_NOT_SUPPORTED);
259
+
260
+ let {binary, revision, downloadedVersion} =
261
+ await Chromium.downloadBinary(version);
262
+ await Chromium.#installDriver(revision, downloadedVersion);
263
+
264
+ let options = new chrome.Options().addArguments("no-sandbox");
265
+ if (extensionPaths.length > 0)
266
+ options.addArguments(`load-extension=${extensionPaths.join(",")}`);
267
+ if (headless)
268
+ options.headless();
269
+ if (insecure)
270
+ options.addArguments("ignore-certificate-errors");
271
+ options.setChromeBinaryPath(binary);
272
+
273
+ let builder = new webdriver.Builder();
274
+ builder.forBrowser("chrome");
275
+ builder.setChromeOptions(options);
276
+
277
+ return builder.build();
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Download functionality for Firefox. This class can be used statically.
283
+ * @hideconstructor
284
+ * @extends Browser
285
+ */
286
+ class Firefox {
287
+ static async #getLatestVersion(branch) {
288
+ let data = await got("https://product-details.mozilla.org/1.0/firefox_versions.json").json();
289
+ return branch == "beta" ?
290
+ data.LATEST_FIREFOX_DEVEL_VERSION : data.LATEST_FIREFOX_VERSION;
291
+ }
292
+
293
+ static #getFirefoxBinary(dir) {
294
+ switch (process.platform) {
295
+ case "win32":
296
+ return path.join(dir, "core", "firefox.exe");
297
+ case "linux":
298
+ return path.join(dir, "firefox", "firefox");
299
+ case "darwin":
300
+ return path.join(dir, "Firefox.app", "Contents", "MacOS", "firefox");
301
+ default:
302
+ throw new Error(`Unexpected platform: ${process.platform}`);
303
+ }
304
+ }
305
+
306
+ static #extractFirefoxArchive(archive, dir) {
307
+ switch (process.platform) {
308
+ case "win32":
309
+ return runWinInstaller(archive, dir);
310
+ case "linux":
311
+ return extractTar(archive, dir);
312
+ case "darwin":
313
+ return extractDmg(archive, dir);
314
+ default:
315
+ throw new Error(`Unexpected platform: ${process.platform}`);
316
+ }
317
+ }
318
+
319
+ static async #downloadFirefox(version) {
320
+ let {platform} = process;
321
+ if (platform == "win32")
322
+ platform += "-" + process.arch;
323
+ let buildTypes = {
324
+ "win32-ia32": ["win32", `Firefox Setup ${version}.exe`],
325
+ "win32-x64": ["win64", `Firefox Setup ${version}.exe`],
326
+ "linux": ["linux-x86_64", `firefox-${version}.tar.bz2`],
327
+ "darwin": ["mac", `Firefox ${version}.dmg`]
328
+ };
329
+
330
+ let snapshotsDir = path.join(snapshotsBaseDir, "firefox");
331
+ let browserDir = path.join(snapshotsDir, `firefox-${platform}-${version}`);
332
+ try {
333
+ await fs.promises.access(browserDir);
334
+ return Firefox.#getFirefoxBinary(browserDir);
335
+ }
336
+ catch (e) {}
337
+
338
+ await fs.promises.mkdir(path.dirname(browserDir), {recursive: true});
339
+
340
+ let [buildPlatform, fileName] = buildTypes[platform];
341
+ let archive = path.join(snapshotsDir, "cache", fileName);
342
+
343
+ try {
344
+ await fs.promises.access(archive);
345
+ }
346
+ catch (e) {
347
+ let url = `https://archive.mozilla.org/pub/firefox/releases/${version}/${buildPlatform}/en-US/${fileName}`;
348
+ await download(url, archive);
349
+ }
350
+
351
+ await Firefox.#extractFirefoxArchive(archive, browserDir);
352
+ return Firefox.#getFirefoxBinary(browserDir);
353
+ }
354
+
355
+ /**
356
+ * Downloads the browser binary file
357
+ * @param {string} version - Either "latest", "beta" or a full version
358
+ * number (i.e. "68.0"). Defaults to "latest".
359
+ * @return {BrowserBinary}
360
+ */
361
+ static async downloadBinary(version) {
362
+ if (!version || version == "beta")
363
+ version = await Firefox.#getLatestVersion(version);
364
+
365
+ let binary = await Firefox.#downloadFirefox(version);
366
+ return {binary};
367
+ }
368
+
369
+ /** @see Browser.getDriver */
370
+ static async getDriver(version, headless = true, extensionPaths = [],
371
+ incognito = false, insecure = false) {
372
+ let {binary} = await Firefox.downloadBinary(version);
373
+
374
+ let options = new firefox.Options();
375
+ if (headless)
376
+ options.headless();
377
+ if (incognito)
378
+ options.addArguments("--private");
379
+ if (insecure)
380
+ options.set("acceptInsecureCerts", true);
381
+ options.setBinary(binary);
382
+
383
+ let driver = await new webdriver.Builder()
384
+ .forBrowser("firefox")
385
+ .setFirefoxOptions(options)
386
+ .build();
387
+
388
+ for (let extensionPath of extensionPaths) {
389
+ await driver.execute(
390
+ new command.Command("install addon")
391
+ .setParameter("path", extensionPath)
392
+ .setParameter("temporary", true)
393
+ );
394
+ }
395
+ return driver;
396
+ }
397
+
398
+ /** @see Browser.enableExtensionInIncognito */
399
+ static async enableExtensionInIncognito(driver, extensionTitle) {
400
+ // Allowing the extension in private browsing can't happen programmatically:
401
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1729315
402
+ // Therefore, that is done through the UI
403
+ let version = await getBrowserVersion(driver);
404
+ if (version < 87) {
405
+ // The UI workaround assumes web elements only present on Firefox >= 87
406
+ throw new Error(`Only supported on Firefox >= 87. Current version: ${version}`);
407
+ }
408
+
409
+ await driver.navigate().to("about:addons");
410
+ await driver.wait(until.elementLocated(By.name("extension")), 1000).click();
411
+
412
+ for (let elem of await driver.findElements(By.className("card addon"))) {
413
+ let text = await elem.getAttribute("innerHTML");
414
+ if (!text.includes(extensionTitle))
415
+ continue;
416
+
417
+ await elem.click();
418
+ return await driver.findElement(By.name("private-browsing")).click();
419
+ }
420
+ throw new Error(`Extension "${extensionTitle}" not found`);
421
+ }
422
+ }
423
+
424
+ /**
425
+ * Download functionality for Edge. This class can be used statically.
426
+ * @hideconstructor
427
+ * @extends Browser
428
+ */
429
+ class Edge {
430
+ static #DRIVER = "msedgedriver";
431
+
432
+ static async #installDriver() {
433
+ let stdout;
434
+ if (process.platform == "win32") {
435
+ let cmd = "(Get-ItemProperty ${Env:ProgramFiles(x86)}\\Microsoft\\" +
436
+ "Edge\\Application\\msedge.exe).VersionInfo.ProductVersion";
437
+ ({stdout} = await promisify(exec)(cmd, {shell: "powershell.exe"}));
438
+ }
439
+ else if (process.platform == "darwin") {
440
+ ({stdout} = await promisify(execFile)(
441
+ "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
442
+ ["--version"]
443
+ ));
444
+ }
445
+ else if (process.platform == "linux") {
446
+ ({stdout} = await promisify(execFile)("microsoft-edge", ["--version"]));
447
+ }
448
+
449
+ let version = stdout.trim().replace(/.*\s/, "");
450
+ if (!version)
451
+ throw new Error("Edge is not installed");
452
+
453
+ // Based on "node_modules/msedgedriver/install.js", adding a fallback
454
+ // mechanism when msedgedriver doesn't exist for the latest Edge version.
455
+ let platform = `${process.platform}-${process.arch}`;
456
+ let buildTypes = {
457
+ "win32-ia32": ["edgedriver_win32.zip", "msedgedriver.exe"],
458
+ "win32-x64": ["edgedriver_win64.zip", "msedgedriver.exe"],
459
+ "linux-x64": ["edgedriver_linux64.zip", "msedgedriver"],
460
+ "darwin-x64": ["edgedriver_mac64.zip", "msedgedriver"],
461
+ "darwin-arm64": ["edgedriver_arm64.zip", "msedgedriver"]
462
+ };
463
+ let [zip, driver] = buildTypes[platform];
464
+ let cacheDir = path.join(snapshotsBaseDir, "edge", "cache");
465
+ let driverBinDir = path.join(process.cwd(), "node_modules", Edge.#DRIVER,
466
+ "bin");
467
+ let driverLibDir = path.join(process.cwd(), "node_modules", Edge.#DRIVER,
468
+ "lib", Edge.#DRIVER);
469
+ let archive = path.join(cacheDir, `${version}-${zip}`);
470
+
471
+ let vSplit = version.split(".");
472
+ let lastNum = parseInt(vSplit[3], 10);
473
+ while (lastNum >= 0) {
474
+ try {
475
+ let attempt = `${vSplit[0]}.${vSplit[1]}.${vSplit[2]}.${lastNum}`;
476
+ await download(`https://msedgedriver.azureedge.net/${attempt}/${zip}`,
477
+ archive);
478
+ break;
479
+ }
480
+ catch (e) {
481
+ lastNum--;
482
+ }
483
+ }
484
+
485
+ if (lastNum < 0)
486
+ throw new Error(`msedgedriver was not found for Edge ${version}`);
487
+
488
+ await extractZip(archive, {dir: cacheDir});
489
+ for (let destinationDir of [driverBinDir, driverLibDir]) {
490
+ await fs.promises.mkdir(destinationDir, {recursive: true});
491
+ try {
492
+ await fs.promises.copyFile(path.join(cacheDir, driver),
493
+ path.join(destinationDir, driver));
494
+ }
495
+ catch (err) {
496
+ // https://gitlab.com/eyeo/developer-experience/get-browser-binary/-/issues/3
497
+ if (!err.toString().includes("copyfile"))
498
+ throw err;
499
+ }
500
+ }
501
+ }
502
+
503
+ /** @see Browser.getDriver */
504
+ static async getDriver(version, headless = true, extensionPaths = [],
505
+ incognito = false, insecure = false) {
506
+ if (incognito)
507
+ throw new Error(ERROR_INCOGNITO_NOT_SUPPORTED);
508
+
509
+ await Edge.#installDriver();
510
+
511
+ let args = ["no-sandbox"];
512
+ if (headless)
513
+ args.push("headless");
514
+ if (extensionPaths.length > 0)
515
+ args.push(`load-extension=${extensionPaths.join(",")}`);
516
+
517
+ let builder = new webdriver.Builder();
518
+ builder.forBrowser("MicrosoftEdge");
519
+ builder.withCapabilities({
520
+ "browserName": "MicrosoftEdge",
521
+ "ms:edgeChromium": true,
522
+ "ms:edgeOptions": {args},
523
+ "acceptInsecureCerts": insecure
524
+ });
525
+
526
+ return builder.build();
527
+ }
528
+ }
529
+
530
+
531
+ /**
532
+ * @type {Object}
533
+ * @property {Chromium} chromium - Download functionality for Chromium.
534
+ * @property {Firefox} firefox - Download functionality for Firefox.
535
+ * @property {Edge} edge - Download functionality for Edge.
536
+ */
537
+ export const BROWSERS = {
538
+ chromium: Chromium,
539
+ firefox: Firefox,
540
+ edge: Edge
541
+ };
package/src/utils.js ADDED
@@ -0,0 +1,80 @@
1
+ /*
2
+ * Copyright (c) 2006-present eyeo GmbH
3
+ *
4
+ * This module is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU General Public License as published by
6
+ * the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * This program is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU General Public License
15
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+ */
17
+
18
+ import fs from "fs";
19
+ import path from "path";
20
+ import {pipeline} from "stream";
21
+ import {promisify} from "util";
22
+ import {exec} from "child_process";
23
+
24
+ import got from "got";
25
+ import fsExtra from "fs-extra";
26
+ import dmg from "dmg";
27
+
28
+ export async function download(url, destFile) {
29
+ let cacheDir = path.dirname(destFile);
30
+
31
+ await fs.promises.mkdir(cacheDir, {recursive: true});
32
+
33
+ let tempDest = `${destFile}-${process.pid}`;
34
+ let writable = fs.createWriteStream(tempDest);
35
+
36
+ try {
37
+ await promisify(pipeline)(got.stream(url), writable);
38
+ }
39
+ catch (error) {
40
+ await fs.promises.rm(tempDest, {recursive: true});
41
+ throw error;
42
+ }
43
+
44
+ await fs.promises.rename(tempDest, destFile);
45
+ }
46
+
47
+ export async function extractTar(archive, dir) {
48
+ await fs.promises.mkdir(dir);
49
+ await promisify(exec)(["tar", "-jxf", archive, "-C", dir].join(" "));
50
+ }
51
+
52
+ export async function extractDmg(archive, dir) {
53
+ let mpath = await promisify(dmg.mount)(archive);
54
+ let files = await fs.promises.readdir(mpath);
55
+ let target = files.find(file => path.extname(file) == ".app");
56
+ let source = path.join(mpath, target);
57
+ await fs.promises.mkdir(dir);
58
+ try {
59
+ await fsExtra.copy(source, path.join(dir, target));
60
+ }
61
+ finally {
62
+ try {
63
+ await promisify(dmg.unmount)(mpath);
64
+ }
65
+ catch (err) {
66
+ console.error(`Error unmounting DMG: ${err}`);
67
+ }
68
+ }
69
+ }
70
+
71
+ export async function runWinInstaller(archive, dir) {
72
+ // Procedure inspired from mozinstall. Uninstaller will also need to be run.
73
+ // https://hg.mozilla.org/mozilla-central/file/tip/testing/mozbase/mozinstall/mozinstall/mozinstall.py
74
+ await promisify(exec)(`"${archive}" /extractdir=${dir}`);
75
+ }
76
+
77
+ export async function getBrowserVersion(driver) {
78
+ let version = (await driver.getCapabilities()).getBrowserVersion();
79
+ return parseInt(version.split(".")[0], 10);
80
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "env": {
3
+ "mocha": true
4
+ }
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "env": {
3
+ "webextensions": true
4
+ }
5
+ }
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+
3
+ chrome.tabs.create({url: "index.html"});
@@ -0,0 +1 @@
1
+ <h1>Browser download test extension</h1>
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "Browser download test extension",
3
+ "version": "0.1",
4
+ "description": "Browser download test extension",
5
+ "manifest_version": 2,
6
+ "background": {
7
+ "scripts": ["background.js"]
8
+ }
9
+ }
package/test/runner.js ADDED
@@ -0,0 +1,172 @@
1
+ /*
2
+ * Copyright (c) 2006-present eyeo GmbH
3
+ *
4
+ * This module is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU General Public License as published by
6
+ * the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * This program is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU General Public License
15
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+ */
17
+
18
+ import fs from "fs";
19
+ import expect from "expect";
20
+ import path from "path";
21
+ import {exec} from "child_process";
22
+ import {promisify} from "util";
23
+
24
+ import {BROWSERS, snapshotsBaseDir} from "../src/browsers.js";
25
+
26
+ // Required to start the driver on some platforms (e.g. Windows).
27
+ import "chromedriver";
28
+ import "geckodriver";
29
+ import "msedgedriver";
30
+
31
+ const VERSIONS = {
32
+ chromium: [void 0, "beta", "dev", "77.0.3865.0"],
33
+ firefox: [void 0, "beta", "68.0"],
34
+ edge: [void 0]
35
+ };
36
+ let extensionPaths = [path.resolve(process.cwd(), "test", "extension")];
37
+
38
+ async function switchToHandle(driver, testFn) {
39
+ for (let handle of await driver.getAllWindowHandles()) {
40
+ let url;
41
+ try {
42
+ await driver.switchTo().window(handle);
43
+ url = await driver.getCurrentUrl();
44
+ }
45
+ catch (e) {
46
+ continue;
47
+ }
48
+
49
+ if (testFn(url))
50
+ return handle;
51
+ }
52
+ }
53
+
54
+ async function getHandle(driver, page) {
55
+ let url;
56
+ let handle = await driver.wait(() => switchToHandle(driver, handleUrl => {
57
+ if (!handleUrl)
58
+ return false;
59
+
60
+ url = new URL(handleUrl);
61
+ return url.pathname == page;
62
+ }), 8000, `${page} did not open`);
63
+
64
+ return handle;
65
+ }
66
+
67
+ async function killDriverProcess() {
68
+ for (let name of ["chromedriver", "msedgedriver"]) {
69
+ let stdout;
70
+ if (process.platform == "win32") {
71
+ try {
72
+ ({stdout} = await promisify(exec)(
73
+ `(Get-Process -Name ${name}).Id | Stop-Process`,
74
+ {shell: "powershell.exe"}
75
+ ));
76
+ }
77
+ catch (err) {
78
+ if (err.toString().includes("Command failed"))
79
+ continue; // Command will fail when driver process is not found
80
+
81
+ throw err;
82
+ }
83
+ }
84
+ else {
85
+ try {
86
+ ({stdout} =
87
+ await promisify(exec)(`ps -a | grep ${name} | grep -v grep`));
88
+ }
89
+ catch (err) {
90
+ if (err.toString().includes("Command failed"))
91
+ continue; // Command will fail when driver process is not found
92
+
93
+ throw err;
94
+ }
95
+ let pid = /\s*(\d*)/.exec(stdout)[1]; // first number after any spaces
96
+ await promisify(exec)(`kill ${pid}`);
97
+ }
98
+ }
99
+ }
100
+
101
+ for (let browser of Object.keys(BROWSERS)) {
102
+ for (let version of VERSIONS[browser]) {
103
+ describe(`Browser: ${browser} ${version || "latest"}`, function() {
104
+ this.timeout(15000);
105
+
106
+ before(async() => {
107
+ try {
108
+ await fs.promises.rm(snapshotsBaseDir, {recursive: true});
109
+ }
110
+ catch (e) {}
111
+ });
112
+
113
+ let driver = null;
114
+ afterEach(async() => {
115
+ if (!driver)
116
+ return;
117
+
118
+ await driver.quit();
119
+ driver = null;
120
+ // Some platforms don't immediately kill the webdriver process
121
+ await killDriverProcess();
122
+ });
123
+
124
+ it("downloads", async function() {
125
+ if (browser == "edge")
126
+ // Edge download is not implemented. It is assumed to be installed.
127
+ this.skip();
128
+
129
+ this.timeout(40000);
130
+ let {binary} = await BROWSERS[browser].downloadBinary(version);
131
+
132
+ await fs.promises.access(binary);
133
+ expect(binary).toEqual(
134
+ expect.stringContaining(path.join(snapshotsBaseDir, browser)));
135
+ });
136
+
137
+ it("runs", async() => {
138
+ let names = {
139
+ chromium: "chrome",
140
+ firefox: "firefox",
141
+ edge: /(MicrosoftEdge|msedge)/
142
+ };
143
+
144
+ driver = await BROWSERS[browser].getDriver(version);
145
+ await driver.navigate().to("about:blank");
146
+
147
+ expect((await driver.getCapabilities()).getBrowserName())
148
+ .toEqual(expect.stringMatching(names[browser]));
149
+ });
150
+
151
+ it("loads an extension", async() => {
152
+ let headless = browser == "firefox";
153
+
154
+ driver = await BROWSERS[browser].getDriver(version, headless,
155
+ extensionPaths);
156
+ await getHandle(driver, "/index.html");
157
+ });
158
+
159
+ it("loads an extension in incognito mode", async function() {
160
+ if (browser != "firefox" || version == "68.0")
161
+ this.skip();
162
+
163
+ driver = await BROWSERS[browser].getDriver(version, false,
164
+ extensionPaths, true);
165
+ await BROWSERS[browser].enableExtensionInIncognito(
166
+ driver, "Browser download test extension"
167
+ );
168
+ await getHandle(driver, "/index.html");
169
+ });
170
+ });
171
+ }
172
+ }