@eyeo/get-browser-binary
Version:
Install browser binaries and matching webdrivers
308 lines (270 loc) • 11.1 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 edge from "selenium-webdriver/edge.js";
import {Browser} from "./browser.js";
import {download, killDriverProcess, wait, getMajorVersion, checkVersion,
checkPlatform, errMsg, snapshotsBaseDir, checkExtensionPaths}
from "./utils.js";
let {platform} = process;
async function enableDeveloperMode(driver) {
const currentHandle = await driver.getWindowHandle();
await driver.switchTo().newWindow("window");
await driver.navigate().to("edge://extensions/");
await driver.executeScript(() => {
const devModeToggle = document.getElementById("developer-mode");
if (!devModeToggle.checked)
devModeToggle.click();
});
await driver.wait(async() => {
const checked = await driver.executeScript(() => {
const devModeToggle = document.getElementById("developer-mode");
return devModeToggle.checked;
});
return checked;
}, 500);
await driver.close();
await driver.switchTo().window(currentHandle);
}
/**
* Browser and webdriver functionality for Edge.
* @hideconstructor
* @extends Browser
*/
export class Edge extends Browser {
static #CHANNELS = ["latest", "beta", "dev"];
static #MIN_VERSION = 114;
static async #getVersionForChannel(version) {
let channel = "stable";
if (Edge.#CHANNELS.includes(version) && version != "latest")
channel = version;
let url = `https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edge-${channel}/`;
let body;
try {
({body} = await got(url));
}
catch (err) {
throw new Error(`${errMsg.browserVersionCheck}: ${url}\n${err}`);
}
let versionNumber;
if (Edge.#CHANNELS.includes(version)) {
let regex = /href="microsoft-edge-(stable|beta|dev)_(.*?)-1_/gm;
let matches;
let versionNumbers = [];
while ((matches = regex.exec(body)) !== null)
versionNumbers.push(matches[2]);
let compareVersions = (v1, v2) =>
getMajorVersion(v1) < getMajorVersion(v2) ? 1 : -1;
versionNumber = versionNumbers.sort(compareVersions)[0];
}
else {
let split = version.split(".");
let minorVersion = split.length == 4 ? parseInt(split.pop(), 10) : -1;
let majorVersion = split.join(".");
let found;
while (!found && minorVersion >= 0) {
versionNumber = `${majorVersion}.${minorVersion}`;
found = body.includes(versionNumber);
minorVersion--;
}
if (!found)
throw new Error(`${errMsg.unsupportedVersion}: ${version}`);
}
return {versionNumber, channel};
}
static #getDarwinApp(channel = "stable") {
const firstUppercase =
String(channel).charAt(0).toUpperCase() + String(channel).slice(1);
return channel === "stable" ? "Microsoft Edge" : `Microsoft Edge ${firstUppercase}`;
}
static #getBinaryPath(channel = "stable") {
switch (platform) {
case "win32":
const programFiles = process.env["ProgramFiles(x86)"] ?
"${Env:ProgramFiles(x86)}" : "${Env:ProgramFiles}";
return `${programFiles}\\Microsoft\\Edge\\Application\\msedge.exe`;
case "linux":
return channel == "stable" ?
"/usr/bin/microsoft-edge" : `/usr/bin/microsoft-edge-${channel}`;
case "darwin":
const darwinApp = Edge.#getDarwinApp(channel);
return `${process.env.HOME}/Applications/${darwinApp}.app/Contents/MacOS/${darwinApp}`;
default:
checkPlatform();
}
}
/**
* Installs the browser. On Linux, Edge is installed as a system package,
* which requires root permissions. On MacOS, Edge is installed as a user
* app (not as a system app). Installing Edge on Windows is not supported.
* @param {string} [version=latest] Either "latest", "beta", "dev" or a full
* version number (i.e. "114.0.1823.82").
* @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) {
if (platform == "win32")
// Edge is mandatory on Windows, can't be uninstalled or downgraded
// https://support.microsoft.com/en-us/microsoft-edge/why-can-t-i-uninstall-microsoft-edge-ee150b3b-7d7a-9984-6d83-eb36683d526d
throw new Error(`${errMsg.unsupportedPlatform}: ${platform}`);
if (platform == "darwin" && !/latest|beta|dev/.test(version))
throw new Error(`${errMsg.unsupportedVersion}: ${version}. Only "latest", "beta" or "dev" are supported`);
checkVersion(version, Edge.#MIN_VERSION, Edge.#CHANNELS);
let {versionNumber, channel} = await Edge.#getVersionForChannel(version);
let filename = {
linux: `microsoft-edge-${channel}_${versionNumber}-1_amd64.deb`,
darwin: `MicrosoftEdge-${versionNumber}.pkg`
}[platform];
let snapshotsDir = path.join(snapshotsBaseDir, "edge");
let archive = path.join(snapshotsDir, "cache", filename);
let binary = Edge.#getBinaryPath(channel);
try {
if (await Edge.getInstalledVersion(binary) == versionNumber)
return {binary, versionNumber};
}
catch (e) {}
let url = `https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edge-${channel}/${filename}`;
try {
if (platform == "darwin") {
const caskFile = channel === "stable" ? "microsoft-edge.json" : `microsoft-edge@${channel}.json`;
const caskUrl = `https://formulae.brew.sh/api/cask/${caskFile}`;
try {
({url} = await got(caskUrl).json());
}
catch (err) {
throw new Error(`${errMsg.browserVersionCheck}: ${caskUrl}\n${err}`);
}
}
await download(url, archive, downloadTimeout);
}
catch (err) {
throw new Error(`${errMsg.browserDownload}: ${url}\n${err}`);
}
if (platform == "linux") {
await promisify(exec)(`dpkg -i ${archive}`);
}
else if (platform == "darwin") {
await fs.promises.rm(`${process.env.HOME}/Applications/${Edge.#getDarwinApp(channel)}.app`, {force: true, recursive: true});
await promisify(exec)(`installer -pkg ${archive} -target CurrentUserHomeDirectory`);
}
return {binary, versionNumber};
}
/** @see Browser.getInstalledVersion */
static async getInstalledVersion(binary) {
let installedVersion = await super.getInstalledVersion(binary);
for (let word of ["beta", "dev", "Beta", "Dev"])
installedVersion = installedVersion.replace(word, "");
return installedVersion.trim().replace(/.*\s/, "");
}
/** @see Browser.getDriver */
static async getDriver(version = "latest", {
headless = true, extensionPaths = [], incognito = false, insecure = false,
extraArgs = [], customBrowserBinary
} = {}, downloadTimeout = 0) {
checkVersion(version, Edge.#MIN_VERSION, Edge.#CHANNELS);
let binary;
let versionNumber;
if (!customBrowserBinary && (platform == "linux" || platform == "darwin")) {
({binary, versionNumber} =
await Edge.installBrowser(version, downloadTimeout));
}
else {
binary = customBrowserBinary || Edge.#getBinaryPath();
versionNumber =
await Edge.getInstalledVersion(binary);
}
let options = new edge.Options().addArguments("no-sandbox", ...extraArgs);
let majorVersion = getMajorVersion(versionNumber);
if (headless) {
if (versionNumber && getMajorVersion(versionNumber) >= 114)
options.addArguments("headless=new");
else
options.addArguments("headless");
}
if (extensionPaths.length > 0) {
await checkExtensionPaths(extensionPaths);
options.addArguments(`load-extension=${extensionPaths.join(",")}`);
}
if (incognito)
options.addArguments("inprivate");
if (insecure)
options.addArguments("ignore-certificate-errors");
if (platform === "linux" || platform === "darwin")
options.setEdgeChromiumBinaryPath(binary);
let builder = new Builder();
builder.forBrowser("MicrosoftEdge");
builder.setEdgeOptions(options);
let driver;
// On Windows CI, occasionally a SessionNotCreatedError is thrown, likely
// due to low OS resources, that's why building the driver is retried
await wait(async() => {
try {
driver = await builder.build();
return true;
}
catch (err) {
if (err.name != "SessionNotCreatedError")
throw err;
await killDriverProcess("msedgedriver");
}
}, 30000, `${errMsg.driverStart}: msedgedriver`, 1000);
// From Edge 134 on, developer mode needs to be enabled
// for custom extensions to work properly
if (majorVersion >= 134)
await enableDeveloperMode(driver);
return driver;
}
/** @see Browser.enableExtensionInIncognito */
static async enableExtensionInIncognito(driver, extensionTitle) {
await driver.navigate().to("edge://extensions/");
for (let elem of await driver.findElements(By.css("[role=listitem]"))) {
let text = await elem.getAttribute("innerHTML");
if (!text.includes(extensionTitle))
continue;
for (let button of await elem.findElements(By.css("button"))) {
text = await button.getAttribute("outerHTML");
if (!text.includes("Details"))
continue;
await button.click();
await driver.findElement(By.id("itemAllowIncognito")).click();
// On Edge 134, extension gets turned off when incognito mode is enabled
// We need to turn it back on by clicking the toggle
try {
await driver.wait(until.elementLocated(
By.css('[aria-label="Extension on"]:not(:checked)')), 3000).click();
}
catch (err) {
// ignoring timeout errors, as the element is not expected
// to be present for all Edge versions
if (err.name !== "TimeoutError")
throw err;
}
return;
}
throw new Error(`${errMsg.elemNotFound}: Details button`);
}
throw new Error(`${errMsg.extensionNotFound}: ${extensionTitle}`);
}
}