UNPKG

@eyeo/get-browser-binary

Version:

Install browser binaries and matching webdrivers

387 lines (321 loc) 14.3 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 fs from "fs"; import {expect} from "expect"; import path from "path"; import Jimp from "jimp"; import {By} from "selenium-webdriver"; import {BROWSERS, snapshotsBaseDir, takeFullPageScreenshot} from "../index.js"; import {killDriverProcess} from "../src/utils.js"; import {TEST_SERVER_URL} from "./test-server.js"; const VERSIONS = { chromium: ["latest", "77.0.3865.0", "beta", "dev"], firefox: ["latest", "68.0", "beta", "dev"], edge: ["latest", "114.0.1823.82", "beta", "dev"] }; const TEST_URL_BASIC = `${TEST_SERVER_URL}/basic.html`; const PROXY_URL_BASIC = "http://testpages.adblockplus.org/basic.html"; const TEST_URL_LONG = `${TEST_SERVER_URL}/long.html`; async function switchToHandle(driver, testFn) { for (let handle of await driver.getAllWindowHandles()) { let url; try { await driver.switchTo().window(handle); url = await driver.getCurrentUrl(); } catch (e) { continue; } if (testFn(url)) return handle; } } async function getHandle(driver, page) { let url; let handle = await driver.wait(() => switchToHandle(driver, handleUrl => { if (!handleUrl) return false; url = new URL(handleUrl); return url.pathname == page; }), 8000, `${page} did not open`); return handle; } function normalize(version) { // Discards any numbers after the third dot. For example, "103.0.5060.134" // will return "103.0.5060", and "68.0" will return "68.0" let normalized = version.split(".").slice(0, 3).join("."); // On Windows, Firefox beta versions look like "103.0b9", but the installed // version returned by the browser is actually "103.0" return normalized.split("b")[0]; } function getWindowSize(driver) { return driver.executeScript(() => { return {height: window.innerHeight, width: window.innerWidth}; }); } expect.extend({ toMeasureLessThan(small, big) { let pass = small.width < big.width || small.height < big.height; let message = () => `expected small sizes (w: ${small.width}, h: ${small.height}) ` + `to be smaller than big sizes (w: ${big.width}, h: ${big.height})`; if (pass) return {message, pass: true}; return {message, pass: false}; } }); function getExtension(browser, version) { let manifest = "mv2"; // The latest Edge installed on the Gitlab Windows Shared Runners is Edge 79, // which does not support manifest v3. More info: // https://gitlab.com/eyeo/developer-experience/get-browser-binary/-/issues/29 let windowsSharedRunners = browser == "edge" && process.platform == "win32" && process.env.CI_JOB_ID; if (["chromium", "edge"].includes(browser) && ["latest", "beta", "dev"].includes(version) && !windowsSharedRunners) manifest = "mv3"; let extensionPaths = [path.resolve(process.cwd(), "test", "extension", manifest)]; return {extensionPaths, manifest}; } async function getInstallFileCTime(browser) { // Browsers installed at OS level are not tested if (browser == "edge" && (process.platform == "win32" || process.platform == "darwin")) return null; const installTypes = [".zip", ".dmg", ".bz2", ".xz", ".deb", ".exe"]; let cacheDir = path.join(snapshotsBaseDir, browser, "cache"); let cacheFiles = await fs.promises.readdir(cacheDir); let browserZip = cacheFiles.find(elem => installTypes.some(type => elem.includes(type))); if (!browserZip) throw new Error(`Files in ${cacheDir} don't belong to any known install file types: ${installTypes}`); return (await fs.promises.stat(path.join(cacheDir, browserZip))).ctimeMs; } const browserNames = { chromium: "chrome", firefox: "firefox", edge: /(MicrosoftEdge|msedge)/ }; async function basicUrlTest(driver, browser) { await driver.navigate().to(TEST_URL_BASIC); expect((await driver.getCapabilities()).getBrowserName()) .toEqual(expect.stringMatching(browserNames[browser])); let text = await driver.findElement(By.id("basic")).getText(); expect(text).toEqual("Test server basic page"); } // Adding the browser version to the test title for logging purposes function addVersionToTitle(ctx, version) { ctx.test.title = `${ctx.test.title} [v${version}]`; } function isOldHeadlessMode(browser, version) { // Chromium's old headless mode doesn't support loading extensions return browser != "firefox" && ( !["latest", "beta", "dev"].includes(version) || // Edge on the Windows CI is v79, that is old headless (browser == "edge" && process.platform == "win32")); } for (let browser of Object.keys(BROWSERS)) { describe(`Browser: ${browser}`, () => { before(async() => { if (process.env.TEST_KEEP_SNAPSHOTS == "true") return; await fs.promises.rm(snapshotsBaseDir, {force: true, recursive: true}); }); for (let version of VERSIONS[browser]) { describe(`Version: ${version}`, () => { let driver = null; let firstInstallFileCTime = null; let customBrowserBinary = null; let failedInstall = false; async function quitDriver() { if (!driver) return; await driver.quit(); driver = null; // Some platforms don't immediately kill the driver process if (browser == "chromium") await killDriverProcess("chromedriver"); else if (browser == "firefox") await killDriverProcess("geckodriver"); // https://gitlab.com/eyeo/developer-experience/get-browser-binary/-/issues/77 if (process.platform == "win32" && browser == "chromium") { await killDriverProcess("chrome"); } else if (process.platform == "win32" && browser == "edge") { await killDriverProcess("msedgedriver"); await killDriverProcess("msedge"); } } beforeEach(function() { if (browser == "edge" && process.platform != "linux" && version == "114.0.1823.82") this.skip(); if (failedInstall) this.skip(); }); afterEach(quitDriver); it("installs", async function() { // installing browsers may take a while on the CI this.timeout(150000); let binary; let versionNumber; try { ({binary, versionNumber} = await BROWSERS[browser].installBrowser(version, this.timeout())); } catch (err) { failedInstall = true; throw err; } let browserName = browser == "edge" ? /(edge|Edge)/ : browser; expect(binary).toEqual(expect.stringMatching(browserName)); let installedVersion = await BROWSERS[browser].getInstalledVersion(binary); expect(installedVersion).toEqual( expect.stringContaining(normalize(versionNumber))); addVersionToTitle(this, versionNumber); // Data used in further tests customBrowserBinary = binary; firstInstallFileCTime = await getInstallFileCTime(browser); }); it("runs", async function() { driver = await BROWSERS[browser].getDriver(version); const url = await driver.getCurrentUrl(); // check that default automation tab is in focus: // data;, (chromium) and about:blank (firefox) expect(url).toEqual(expect.stringMatching(/^(data:,|about:blank)$/)); await basicUrlTest(driver, browser); let browserVersion = (await driver.getCapabilities()).getBrowserVersion(); addVersionToTitle(this, browserVersion); }); // This test depends on running the "installs" test it("uses cached install files", async function() { if (!firstInstallFileCTime) this.skip(); // assigning `driver` to allow the afterEach hook quit the driver driver = await BROWSERS[browser].getDriver(version); let secondInstallFileCTime = await getInstallFileCTime(browser); expect(secondInstallFileCTime).toEqual(firstInstallFileCTime); }); // This test depends on running the "installs" test it("runs a custom browser binary", async function() { if (!customBrowserBinary) this.skip(); driver = await BROWSERS[browser].getDriver(version, {customBrowserBinary}); await basicUrlTest(driver, browser); expect((await driver.getCapabilities()).getBrowserName()) .toEqual(expect.stringMatching(browserNames[browser])); }); it("supports extra args", async() => { let headless = false; let extraArgs = browser == "firefox" ? ["-width=600", "-height=400"] : ["window-size=600,400"]; driver = await BROWSERS[browser].getDriver( version, {headless, extraArgs}); await driver.navigate().to(TEST_URL_BASIC); let sizeSmall = await getWindowSize(driver); await quitDriver(); driver = await BROWSERS[browser].getDriver(version, {headless}); await driver.navigate().to(TEST_URL_BASIC); let sizeDefault = await getWindowSize(driver); expect(sizeSmall).toMeasureLessThan(sizeDefault); }); it("supports proxy", async function() { if (browser != "firefox") this.skip(); let headless = true; let proxy = "http://localhost:3000/proxy-config.pac"; let extraArgs = []; driver = await BROWSERS[browser].getDriver( version, {headless, extraArgs, proxy}); await driver.navigate().to(PROXY_URL_BASIC); let text = await driver.findElement(By.id("basic")).getText(); expect(text).toEqual("Test server basic page"); await quitDriver(); }); it("takes a full page screenshot", async() => { driver = await BROWSERS[browser].getDriver(version); await driver.navigate().to(TEST_URL_LONG); let fullImg = await takeFullPageScreenshot(driver); // Taking a regular webdriver screenshot, which should be shorter let data = await driver.takeScreenshot(); let partImg = await Jimp.read(Buffer.from(data, "base64")); expect(fullImg.bitmap.width).toBeGreaterThan(0); expect(fullImg.bitmap.height).toBeGreaterThan(partImg.bitmap.height); }); // The only reason the geckodriver package is required in package.json // is to install a specific version needed by Firefox 68.0, otherwise it // fails to load extensions. When the oldest Firefox version is bumped, // Selenium's automated driver download should be able to manage it. // https://gitlab.com/eyeo/developer-experience/get-browser-binary/-/issues/44 it("loads an extension", async() => { let headless = !isOldHeadlessMode(browser, version); let {extensionPaths} = getExtension(browser, version); driver = await BROWSERS[browser].getDriver( version, {headless, extensionPaths}); await getHandle(driver, "/index.html"); }); it("loads an extension in incognito mode", async function() { if (browser == "firefox" && version == "68.0") this.skip(); let {extensionPaths, manifest} = getExtension(browser, version); driver = await BROWSERS[browser].getDriver( version, {headless: false, extensionPaths, incognito: true}); await BROWSERS[browser].enableExtensionInIncognito( driver, `Browser test extension - ${manifest}` ); await getHandle(driver, "/index.html"); }); it("updates an extension", async() => { let headless = !isOldHeadlessMode(browser, version); let {extensionPaths, manifest} = getExtension(browser, version); let tmpExtensionDir = path.join(snapshotsBaseDir, "extension"); await fs.promises.rm(tmpExtensionDir, {recursive: true, force: true}); await fs.promises.cp(extensionPaths[0], tmpExtensionDir, {recursive: true}); driver = await BROWSERS[browser].getDriver( version, {headless, extensionPaths: [tmpExtensionDir]}); await getHandle(driver, "/index.html"); let text = await driver.findElement(By.id("title")).getText(); expect(text).toEqual(`Browser test extension - ${manifest}`); // The page is modified and reloaded to emulate an extension update await fs.promises.cp(path.join(tmpExtensionDir, "update.html"), path.join(tmpExtensionDir, "index.html")); await driver.navigate().refresh(); text = await driver.findElement(By.id("title")).getText(); expect(text).toEqual(`Updated test extension - ${manifest}`); }); }); } describe("Any version", async() => { it("does not install unsupported versions", async() => { for (let unsupported of ["0.0", "invalid"]) { await expect(BROWSERS[browser].installBrowser(unsupported)) .rejects.toThrow(`Unsupported browser version: ${unsupported}`); } }); it("does not load an invalid extension", async() => { let extensionPaths = [process.cwd()]; await expect(BROWSERS[browser].getDriver("latest", {extensionPaths})) .rejects.toThrow(`Extension manifest file not found: ${extensionPaths[0]}`); }); }); }); }