UNPKG

@eyeo/get-browser-binary

Version:

Install browser binaries and matching webdrivers

366 lines (318 loc) 12.6 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, 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}`); } }