@eyeo/get-browser-binary
Version:
Install browser binaries and matching webdrivers
272 lines (236 loc) • 9.66 kB
JavaScript
/*
* Copyright (c) 2006-present eyeo GmbH
*
* This module is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import path from "path";
import {exec} from "child_process";
import {promisify} from "util";
import fs from "fs";
import got from "got";
import {until, By, Builder} from "selenium-webdriver";
import firefox from "selenium-webdriver/firefox.js";
import {Command} from "selenium-webdriver/lib/command.js";
import {Browser} from "./browser.js";
import {download, extractTar, extractDmg, killDriverProcess, wait,
getMajorVersion, checkVersion, checkPlatform, errMsg, snapshotsBaseDir,
platformArch, checkExtensionPaths} from "./utils.js";
/**
* Browser and webdriver functionality for Firefox.
* @hideconstructor
* @extends Browser
*/
export class Firefox extends Browser {
static #CHANNELS = ["latest", "beta", "dev"];
static async #getVersionForChannel(channel) {
if (!Firefox.#CHANNELS.includes(channel))
return channel;
// https://wiki.mozilla.org/Release_Management/Product_details#firefox_versions.json
let url = "https://product-details.mozilla.org/1.0/firefox_versions.json";
let data;
try {
data = await got(url).json();
}
catch (err) {
throw new Error(`${errMsg.browserVersionCheck}: ${url}\n${err}`);
}
if (channel === "beta") {
// The validated latest Firefox Beta built, exposed for downloading
return data.LATEST_FIREFOX_RELEASED_DEVEL_VERSION;
}
else if (channel === "dev") {
// Firefox Developer Edition
return data.FIREFOX_DEVEDITION;
}
// The Firefox Version shipped on the release channel
return data.LATEST_FIREFOX_VERSION;
}
static #getAppDir(dir) {
const appDirRegexes = {
win32: /^core$/,
linux: /^firefox$/,
darwin: /^Firefox.*\.app$/
};
const contents = fs.readdirSync(dir);
const appDir = contents.find(candidateDir =>
appDirRegexes[process.platform].test(candidateDir));
return path.join(dir, appDir);
}
static #getBinaryPath(dir) {
checkPlatform();
const defaultBinaries = {
win32: path.join(Firefox.#getAppDir(dir), "firefox.exe"),
linux: path.join(Firefox.#getAppDir(dir), "firefox"),
darwin: path.join(Firefox.#getAppDir(dir), "Contents", "MacOS", "firefox")
};
return defaultBinaries[process.platform];
}
static #extractFirefoxArchive(archive, dir) {
switch (process.platform) {
case "win32":
return promisify(exec)(`"${archive}" /extractdir=${dir}`);
case "linux":
return extractTar(archive, dir);
case "darwin":
return extractDmg(archive, dir);
default:
checkPlatform();
}
}
/** @see Browser.getInstalledVersion */
static async getInstalledVersion(binary) {
const installedVersion = await super.getInstalledVersion(binary);
// Linux example: "Mozilla Firefox 102.15.0esr"
return installedVersion.split(" ")[2] || installedVersion;
}
/**
* Installs the browser. The Firefox executable gets extracted in the
* {@link snapshotsBaseDir} folder, ready to go.
* @param {string} [version=latest] Either "latest", "beta" or a full version
* number (i.e. "68.0").
* @param {number} [downloadTimeout=0] Allowed time in ms for the download of
* install files to complete. When set to 0 there is no time limit.
* @return {BrowserBinary}
* @throws {Error} Unsupported browser version, Unsupported platform, Browser
* download failed.
*/
static async installBrowser(version = "latest", downloadTimeout = 0) {
const MIN_VERSION = 68;
checkVersion(version, MIN_VERSION, Firefox.#CHANNELS);
let versionNumber = await Firefox.#getVersionForChannel(version);
const majorVersion = getMajorVersion(versionNumber);
// Starting from Firefox 135 on Linux, the binary file extension
// was changed from .bz2 to .xz
const linuxExtension = majorVersion <= 134 ? "tar.bz2" : "tar.xz";
let [buildPlatform, fileName] = {
"win32-ia32": ["win32", `Firefox Setup ${versionNumber}.exe`],
"win32-x64": ["win64", `Firefox Setup ${versionNumber}.exe`],
"linux-x64": ["linux-x86_64", `firefox-${versionNumber}.${linuxExtension}`],
"darwin-x64": ["mac", `Firefox ${versionNumber}.dmg`],
"darwin-arm64": ["mac", `Firefox ${versionNumber}.dmg`]
}[platformArch];
let snapshotsDir = path.join(snapshotsBaseDir, "firefox");
let browserDir = path.join(snapshotsDir,
`firefox-${platformArch}-${versionNumber}`);
let binary;
try {
await fs.promises.access(browserDir);
binary = Firefox.#getBinaryPath(browserDir);
return {binary, versionNumber};
}
catch (e) {
// The binary was not already downloaded
}
let archive = path.join(snapshotsDir, "cache", fileName);
await fs.promises.mkdir(path.dirname(browserDir), {recursive: true});
try {
await fs.promises.access(archive);
}
catch (e) {
const baseUrl = "https://archive.mozilla.org/pub";
const channel = version === "dev" ? "devedition" : "firefox";
let url = `${baseUrl}/${channel}/releases/${versionNumber}/${buildPlatform}/en-US/${fileName}`;
try {
await download(url, archive, downloadTimeout);
}
catch (err) {
throw new Error(`${errMsg.browserDownload}: ${url}\n${err}`);
}
}
await Firefox.#extractFirefoxArchive(archive, browserDir);
binary = Firefox.#getBinaryPath(browserDir);
return {binary, versionNumber};
}
/** @see Browser.getDriver */
/** In Firefox you can define url to proxy autoconfig file.
* Proxy Autoconfig file determine whether web requests should go through
* a proxy server or connect directly. This allow to use localhost mapped
* to any url (f.ex testpages.adblockplus.org)
*/
static async getDriver(version = "latest", {
headless = true, extensionPaths = [], incognito = false, insecure = false,
extraArgs = [], customBrowserBinary, proxy
} = {}, downloadTimeout = 0) {
let binary;
let versionNumber;
if (!customBrowserBinary) {
({binary, versionNumber} =
await Firefox.installBrowser(version, downloadTimeout));
}
let options = new firefox.Options();
if (headless)
options.addArguments("--headless");
if (incognito)
options.addArguments("--private");
if (insecure)
options.set("acceptInsecureCerts", true);
if (extraArgs.length > 0)
options.addArguments(...extraArgs);
// Enabled by default on Firefox > 68
if (versionNumber && getMajorVersion(versionNumber) == 68)
options.setPreference("dom.promise_rejection_events.enabled", true);
if (proxy) {
options.setPreference("network.proxy.type", 2);
options.setPreference("network.proxy.autoconfig_url", proxy);
}
options.setBinary(customBrowserBinary || binary);
let driver;
// The OS may be low on resources, that's why building the driver is retried
// https://github.com/mozilla/geckodriver/issues/1560
await wait(async() => {
try {
driver = await new Builder()
.forBrowser("firefox")
.setFirefoxOptions(options)
.build();
return true;
}
catch (err) {
if (err.message != "Failed to decode response from marionette")
throw err;
await killDriverProcess("geckodriver");
}
}, 30000, `${errMsg.driverStart}: geckodriver`, 1000);
for (let extensionPath of extensionPaths) {
await checkExtensionPaths([extensionPath]);
// This is based on Selenium's Firefox `installAddon` function. Rather
// than setting the "addon" parameter, which is the actual extension
// base64 encoded, we need to set the "path" which is the filepath to the
// extension. This allows downstream to test upgrade paths by updating the
// extension source code and reloading it.
// See https://github.com/SeleniumHQ/selenium/blob/trunk/javascript/node/selenium-webdriver/firefox.js
await driver.execute(
new Command("install addon")
.setParameter("path", extensionPath)
.setParameter("temporary", true));
}
return driver;
}
/** @see Browser.enableExtensionInIncognito */
static async enableExtensionInIncognito(driver, extensionTitle) {
let version = (await driver.getCapabilities()).getBrowserVersion();
// The UI workaround assumes web elements only present on Firefox >= 87
checkVersion(version, 87);
await driver.navigate().to("about:addons");
await driver.wait(until.elementLocated(By.name("extension")), 1000).click();
for (let elem of await driver.findElements(By.className("card addon"))) {
let text = await elem.getAttribute("innerHTML");
if (!text.includes(extensionTitle))
continue;
await elem.click();
return await driver.findElement(By.name("private-browsing")).click();
}
throw new Error(`${errMsg.extensionNotFound}: ${extensionTitle}`);
}
}