UNPKG

@eyeo/get-browser-binary

Version:

Install browser binaries and matching webdrivers

272 lines (236 loc) 9.66 kB
/* * 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}`); } }