@eyeo/get-browser-binary
Version:
Install browser binaries and matching webdrivers
366 lines (318 loc) • 12.6 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, spawn} 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, getMajorVersion, checkVersion, checkPlatform, errMsg,
snapshotsBaseDir, checkExtensionPaths, platformArch} 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 #appName(channel) {
const channels = {stable: "", beta: " Beta", dev: " Dev"};
if (platform == "linux") {
return channel == "stable" ?
"microsoft-edge" : `microsoft-edge-${channel}`;
}
else if (platform == "darwin") {
return `Microsoft Edge${channels[channel]}`;
}
else if (platform == "win32") {
return `"Edge${channels[channel]}"`;
}
}
static #getBinaryPath(channel = "stable") {
switch (platform) {
case "win32":
return path.join("C:", "\"Program Files (x86)\"", "Microsoft",
Edge.#appName(channel), "Application", "msedge.exe");
case "linux":
return path.join("/usr", "bin", Edge.#appName(channel));
case "darwin":
// Root path may no longer be needed after switching to custom runners
// https://eyeo.atlassian.net/browse/DOPE-8
const homePath = process.env.CI === "true" ? "/" : process.env.HOME;
return path.join(homePath, "Applications", `${Edge.#appName(channel)}.app`,
"Contents", "MacOS", Edge.#appName(channel));
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) {
checkVersion(version, Edge.#MIN_VERSION, Edge.#CHANNELS);
if (!Edge.#CHANNELS.includes(version) && platform != "linux")
throw new Error(`${errMsg.unsupportedVersion}: ${version}. Supported versions: ${Edge.#CHANNELS.join(",")}`);
let {versionNumber, channel} = await Edge.#getVersionForChannel(version);
let binary = Edge.#getBinaryPath(channel);
try {
if (await Edge.getInstalledVersion(binary) == versionNumber)
return {binary, versionNumber};
}
catch (e) {}
const [plat, arch, ext] = {
"win32-ia32": ["Windows", "x86", ".msi"],
"win32-x64": ["Windows", "x64", ".msi"],
"linux-x64": ["Linux", "x64", ".deb"],
"darwin-x64": ["MacOS", "universal", ".pkg"],
"darwin-arm64": ["MacOS", "universal", ".pkg"]
}[platformArch];
let url;
let filename;
const downloadChannels = {latest: "Stable", beta: "Beta", dev: "Dev"};
if (!Edge.#CHANNELS.includes(version) && platform == "linux") {
filename = `microsoft-edge-${channel}_${versionNumber}-1_amd64.deb`;
url = `https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edge-${channel}/${filename}`;
}
else {
let products = await got("https://edgeupdates.microsoft.com/api/products?view=enterprise").json();
for (let product of products) {
if (product.Product != downloadChannels[version])
continue;
let release = product.Releases.find(
r => r.Platform == plat && r.Architecture == arch);
if (!release)
continue;
({Location: url} =
release.Artifacts.find(a => a.Location.endsWith(ext)));
filename = url.split("/").pop();
break;
}
}
if (!url)
throw new Error(`${errMsg.browserDownload}: Release not found for Edge ${downloadChannels[version]} on ${plat} ${arch}`);
const archive = path.join(snapshotsBaseDir, "edge", "cache", filename);
try {
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") {
let command;
if (process.env.CI === "true") {
// For some reason installing apps on the home directory fails on macOS
// runners. Using the root target as a workaround
// This may no longer be needed after switching to custom runners
// https://eyeo.atlassian.net/browse/DOPE-8
command = `sudo installer -pkg ${archive} -target /`;
}
else {
await fs.promises.rm(
path.join(process.env.HOME, "Applications", `${Edge.#appName(channel)}.app`),
{force: true, recursive: true}
);
command = `installer -pkg ${archive} -target CurrentUserHomeDirectory`;
}
await promisify(exec)(command);
}
else if (platform == "win32") {
// /S strips quotes and /C executes the runnable file
const installProcess = spawn("cmd", [`/S /C ${archive}`], {
detached: true,
env: process.env
});
await new Promise((resolve, reject) => {
function onClose(code) {
if (code != 0)
reject(new Error(`${errMsg.browserInstall}. Exit code: ${code}`));
resolve();
}
installProcess.on("close", onClose);
});
}
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) {
binary = customBrowserBinary;
versionNumber = await Edge.getInstalledVersion(binary);
}
else {
({binary, versionNumber} =
await Edge.installBrowser(version, downloadTimeout));
}
if (platform == "win32")
// webdriver can't find the binary when subfolders are enclosed by quotes
binary = binary.replaceAll("\"", "");
const majorVersion = getMajorVersion(versionNumber);
const edgeOptions = {};
// https://issues.chromium.org/issues/409441960
if (majorVersion >= 136)
edgeOptions.enableExtensionTargets = true;
let options = new edge.Options({"ms:edgeOptions": edgeOptions})
.addArguments("no-sandbox", ...extraArgs)
.setEdgeChromiumBinaryPath(binary);
if (headless) {
if (versionNumber && majorVersion >= 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");
let builder = new Builder();
builder.forBrowser("MicrosoftEdge");
builder.setEdgeOptions(options);
let driver;
try {
process.env.SE_DRIVER_MIRROR_URL = "https://msedgedriver.microsoft.com";
driver = edge.Driver.createSession(options);
}
catch (err) {
throw new Error(`${errMsg.driverStart}: msedgedriver\n${err}`);
}
// 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}`);
}
}