UNPKG

@eyeo/get-browser-binary

Version:

Install browser binaries and matching webdrivers

308 lines (270 loc) 11.1 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 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}`); } }