diff --git a/src/browser-installer/chrome/browser.ts b/src/browser-installer/chrome/browser.ts index 2a8d49d66..8eaaaae17 100644 --- a/src/browser-installer/chrome/browser.ts +++ b/src/browser-installer/chrome/browser.ts @@ -1,10 +1,12 @@ +import _ from "lodash"; import { resolveBuildId, canDownload, install as puppeteerInstall } from "@puppeteer/browsers"; -import { MIN_CHROME_FOR_TESTING_VERSION } from "../constants"; +import { CHROME_FOR_TESTING_LATEST_STABLE_API_URL, MIN_CHROME_FOR_TESTING_VERSION } from "../constants"; import { browserInstallerDebug, getBrowserPlatform, getBrowsersDir, getMilestone, + retryFetch, type DownloadProgressCallback, } from "../utils"; import registry from "../registry"; @@ -74,3 +76,20 @@ export const installChrome = async ( return browserPath; }; + +export const resolveLatestChromeVersion = _.memoize(async (force = false): Promise => { + if (!force) { + const platform = getBrowserPlatform(); + const existingLocallyBrowserVersion = registry.getMatchedBrowserVersion(BrowserName.CHROME, platform); + + if (existingLocallyBrowserVersion) { + return existingLocallyBrowserVersion; + } + } + + return retryFetch(CHROME_FOR_TESTING_LATEST_STABLE_API_URL) + .then(res => res.text()) + .catch(() => { + throw new Error("Couldn't resolve latest chrome version"); + }); +}); diff --git a/src/browser-installer/chrome/index.ts b/src/browser-installer/chrome/index.ts index 5ec7456c0..301551d03 100644 --- a/src/browser-installer/chrome/index.ts +++ b/src/browser-installer/chrome/index.ts @@ -4,11 +4,11 @@ import waitPort from "wait-port"; import { pipeLogsWithPrefix } from "../../dev-server/utils"; import { DRIVER_WAIT_TIMEOUT } from "../constants"; import { getMilestone } from "../utils"; -import { installChrome } from "./browser"; +import { installChrome, resolveLatestChromeVersion } from "./browser"; import { installChromeDriver } from "./driver"; import { isUbuntu, getUbuntuLinkerEnv } from "../ubuntu-packages"; -export { installChrome, installChromeDriver }; +export { installChrome, resolveLatestChromeVersion, installChromeDriver }; export const runChromeDriver = async ( chromeVersion: string, diff --git a/src/browser-installer/constants.ts b/src/browser-installer/constants.ts index dc3d4b86c..9f8bbc18f 100644 --- a/src/browser-installer/constants.ts +++ b/src/browser-installer/constants.ts @@ -1,6 +1,17 @@ export const CHROMEDRIVER_STORAGE_API = "https://chromedriver.storage.googleapis.com"; + +const CHROME_FOR_TESTING_VERSIONS_API_URL = "https://googlechromelabs.github.io/chrome-for-testing"; +export const CHROME_FOR_TESTING_MILESTONES_API_URL = `${CHROME_FOR_TESTING_VERSIONS_API_URL}/latest-versions-per-milestone.json`; +export const CHROME_FOR_TESTING_LATEST_STABLE_API_URL = `${CHROME_FOR_TESTING_VERSIONS_API_URL}/LATEST_RELEASE_STABLE`; + export const GECKODRIVER_CARGO_TOML = "https://raw.githubusercontent.com/mozilla/geckodriver/release/Cargo.toml"; + +const FIREFOX_VERSIONS_VERSIONS_API_URL = "https://product-details.mozilla.org/1.0"; +export const FIREFOX_VERSIONS_ALL_VERSIONS_API_URL = `${FIREFOX_VERSIONS_VERSIONS_API_URL}/firefox.json`; +export const FIREFOX_VERSIONS_LATEST_VERSIONS_API_URL = `${FIREFOX_VERSIONS_VERSIONS_API_URL}/firefox_versions.json`; + export const MSEDGEDRIVER_API = "https://msedgedriver.azureedge.net"; + export const SAFARIDRIVER_PATH = "/usr/bin/safaridriver"; export const MIN_CHROME_FOR_TESTING_VERSION = 113; export const MIN_CHROMEDRIVER_FOR_TESTING_VERSION = 115; diff --git a/src/browser-installer/edge/browser.ts b/src/browser-installer/edge/browser.ts new file mode 100644 index 000000000..08973cc18 --- /dev/null +++ b/src/browser-installer/edge/browser.ts @@ -0,0 +1,67 @@ +import _ from "lodash"; +import { exec } from "child_process"; +import { BrowserPlatform } from "@puppeteer/browsers"; +import { getBrowserPlatform } from "../utils"; + +const extractBrowserVersion = (cmd: string): Promise => + new Promise((resolve, reject) => { + exec(cmd, (err, stdout) => { + if (err) { + const errorMessage = "Couldn't retrive edge version. Looks like its not installed"; + + reject(new Error(errorMessage)); + + return; + } + + const edgeVersionRegExp = /\d+\.\d+\.\d+\.\d+/; + const version = edgeVersionRegExp.exec(stdout); + + if (version && version[0]) { + resolve(version[0]); + } else { + const errorMessage = `Couldn't retrive edge version. Expected browser version, but got "${stdout}"`; + + reject(new Error(errorMessage)); + } + }); + }); + +const resolveLinuxEdgeVersion = (): Promise => { + const getMsEdgeStableVersion = "which microsoft-edge-stable > /dev/null && microsoft-edge-stable --version"; + const getMsEdgeVersion = "which microsoft-edge > /dev/null && microsoft-edge --version"; + + return extractBrowserVersion(`${getMsEdgeStableVersion} || ${getMsEdgeVersion}`); +}; + +const resolveWindowsEdgeVersion = (): Promise => { + const getMsEdgeVersion = 'reg query "HKEY_CURRENT_USER\\Software\\Microsoft\\Edge\\BLBeacon" /v version'; + + return extractBrowserVersion(getMsEdgeVersion); +}; + +const resolveMacEdgeVersion = (): Promise => { + const getMsEdgeVersion = "/Applications/Microsoft\\ Edge.app/Contents/MacOS/Microsoft\\ Edge --version"; + + return extractBrowserVersion(getMsEdgeVersion); +}; + +export const resolveEdgeVersion = _.once(async () => { + const platform = getBrowserPlatform(); + + switch (platform) { + case BrowserPlatform.LINUX: + return resolveLinuxEdgeVersion(); + + case BrowserPlatform.WIN32: + case BrowserPlatform.WIN64: + return resolveWindowsEdgeVersion(); + + case BrowserPlatform.MAC: + case BrowserPlatform.MAC_ARM: + return resolveMacEdgeVersion(); + + default: + throw new Error(`Unsupported platform: "${platform}"`); + } +}); diff --git a/src/browser-installer/edge/index.ts b/src/browser-installer/edge/index.ts index 5860f5b2b..62d9abff1 100644 --- a/src/browser-installer/edge/index.ts +++ b/src/browser-installer/edge/index.ts @@ -5,6 +5,7 @@ import waitPort from "wait-port"; import { pipeLogsWithPrefix } from "../../dev-server/utils"; import { DRIVER_WAIT_TIMEOUT } from "../constants"; +export { resolveEdgeVersion } from "./browser"; export { installEdgeDriver }; export const runEdgeDriver = async ( diff --git a/src/browser-installer/firefox/browser.ts b/src/browser-installer/firefox/browser.ts index 140df5619..b3b022a86 100644 --- a/src/browser-installer/firefox/browser.ts +++ b/src/browser-installer/firefox/browser.ts @@ -1,10 +1,18 @@ +import _ from "lodash"; import { canDownload, install as puppeteerInstall } from "@puppeteer/browsers"; -import { browserInstallerDebug, getBrowserPlatform, getBrowsersDir, type DownloadProgressCallback } from "../utils"; +import { + browserInstallerDebug, + getBrowserPlatform, + getBrowsersDir, + retryFetch, + type DownloadProgressCallback, +} from "../utils"; import registry from "../registry"; import { getFirefoxBuildId, normalizeFirefoxVersion } from "./utils"; import { installLatestGeckoDriver } from "./driver"; import { installUbuntuPackageDependencies } from "../ubuntu-packages"; import { BrowserName } from "../../browser/types"; +import { FIREFOX_VERSIONS_LATEST_VERSIONS_API_URL } from "../constants"; const installFirefoxBrowser = async (version: string, { force = false } = {}): Promise => { const platform = getBrowserPlatform(); @@ -59,3 +67,21 @@ export const installFirefox = async ( return browserPath; }; + +export const resolveLatestFirefoxVersion = _.memoize(async (force = false): Promise => { + if (!force) { + const platform = getBrowserPlatform(); + const existingLocallyBrowserVersion = registry.getMatchedBrowserVersion(BrowserName.FIREFOX, platform); + + if (existingLocallyBrowserVersion) { + return existingLocallyBrowserVersion; + } + } + + return retryFetch(FIREFOX_VERSIONS_LATEST_VERSIONS_API_URL) + .then(res => res.json()) + .then(({ LATEST_FIREFOX_VERSION }) => LATEST_FIREFOX_VERSION) + .catch(() => { + throw new Error("Couldn't resolve latest firefox version"); + }); +}); diff --git a/src/browser-installer/firefox/index.ts b/src/browser-installer/firefox/index.ts index 682ec2373..c661eb9b3 100644 --- a/src/browser-installer/firefox/index.ts +++ b/src/browser-installer/firefox/index.ts @@ -2,13 +2,13 @@ import type { ChildProcess } from "child_process"; import { start as startGeckoDriver } from "geckodriver"; import getPort from "get-port"; import waitPort from "wait-port"; -import { installFirefox } from "./browser"; +import { installFirefox, resolveLatestFirefoxVersion } from "./browser"; import { installLatestGeckoDriver } from "./driver"; import { pipeLogsWithPrefix } from "../../dev-server/utils"; import { DRIVER_WAIT_TIMEOUT } from "../constants"; import { getUbuntuLinkerEnv, isUbuntu } from "../ubuntu-packages"; -export { installFirefox, installLatestGeckoDriver }; +export { installFirefox, resolveLatestFirefoxVersion, installLatestGeckoDriver }; export const runGeckoDriver = async ( firefoxVersion: string, diff --git a/src/browser-installer/index.ts b/src/browser-installer/index.ts index c8c035e93..04cad87a4 100644 --- a/src/browser-installer/index.ts +++ b/src/browser-installer/index.ts @@ -1,3 +1,4 @@ export { installBrowser, installBrowsersWithDrivers, BrowserInstallStatus } from "./install"; export { runBrowserDriver } from "./run"; +export { resolveBrowserVersion } from "./resolve-browser-version"; export type { SupportedBrowser, SupportedDriver } from "./utils"; diff --git a/src/browser-installer/install.ts b/src/browser-installer/install.ts index dafec2a08..62ce0c030 100644 --- a/src/browser-installer/install.ts +++ b/src/browser-installer/install.ts @@ -11,12 +11,6 @@ export const installBrowser = async ( browserVersion?: string, { force = false, shouldInstallWebDriver = false, shouldInstallUbuntuPackages = false } = {}, ): Promise => { - if (!browserVersion) { - throw new Error( - `Couldn't install browser '${browserName}' because it has invalid version: '${browserVersion}'`, - ); - } - const { isUbuntu } = await import("./ubuntu-packages"); const needUbuntuPackages = shouldInstallUbuntuPackages && (await isUbuntu()); @@ -33,22 +27,25 @@ export const installBrowser = async ( switch (browserName) { case BrowserName.CHROME: case BrowserName.CHROMIUM: { - const { installChrome } = await import("./chrome"); + const { installChrome, resolveLatestChromeVersion } = await import("./chrome"); + const version = browserVersion || (await resolveLatestChromeVersion(force)); - return installChrome(browserVersion, { force, needUbuntuPackages, needWebDriver: shouldInstallWebDriver }); + return installChrome(version, { force, needUbuntuPackages, needWebDriver: shouldInstallWebDriver }); } case BrowserName.FIREFOX: { - const { installFirefox } = await import("./firefox"); + const { installFirefox, resolveLatestFirefoxVersion } = await import("./firefox"); + const version = browserVersion || (await resolveLatestFirefoxVersion(force)); - return installFirefox(browserVersion, { force, needUbuntuPackages, needWebDriver: shouldInstallWebDriver }); + return installFirefox(version, { force, needUbuntuPackages, needWebDriver: shouldInstallWebDriver }); } case BrowserName.EDGE: { - const { installEdgeDriver } = await import("./edge"); + const { installEdgeDriver, resolveEdgeVersion } = await import("./edge"); + const version = browserVersion || (await resolveEdgeVersion()); if (shouldInstallWebDriver) { - await installEdgeDriver(browserVersion, { force }); + await installEdgeDriver(version, { force }); } return null; @@ -113,7 +110,9 @@ export const installBrowsersWithDrivers = async ( for (const { browserName, browserVersion } of uniqBrowsers) { installPromises.push( forceInstallBinaries(installBrowser, browserName, browserVersion).then(result => { - browsersInstallResult[`${browserName}@${browserVersion}`] = result; + const key = browserVersion ? `${browserName}@${browserVersion}` : String(browserName); + + browsersInstallResult[key] = result; }), ); } diff --git a/src/browser-installer/registry/index.ts b/src/browser-installer/registry/index.ts index e12eec26f..a656066bf 100644 --- a/src/browser-installer/registry/index.ts +++ b/src/browser-installer/registry/index.ts @@ -47,6 +47,22 @@ const logDownloadingBrowsersWarningOnce = _.once(() => { logger.warn("Note: this is one-time action. It may take a while..."); }); +const getBuildPrefix = (browserName: SupportedBrowser, browserVersion: string): string | null => { + switch (browserName) { + case BrowserName.CHROME: + return normalizeChromeVersion(browserVersion); + + case BrowserName.CHROMIUM: + return getMilestone(browserVersion); + + case BrowserName.FIREFOX: + return getFirefoxBuildId(browserVersion); + + default: + return null; + } +}; + class Registry { private registryPath = getRegistryPath(); private registry = this.readRegistry(); @@ -123,7 +139,7 @@ class Registry { public getMatchedBrowserVersion( browserName: SupportedBrowser, platform: BrowserPlatform, - browserVersion: string, + browserVersion?: string, ): string | null { const registryKey = getRegistryBinaryKey(browserName, platform); @@ -131,27 +147,21 @@ class Registry { return null; } - let buildPrefix: string; - - switch (browserName) { - case BrowserName.CHROME: - buildPrefix = normalizeChromeVersion(browserVersion); - break; + const buildIds = this.getBinaryVersions(browserName, platform); - case BrowserName.CHROMIUM: - buildPrefix = getMilestone(browserVersion); - break; + let suitableBuildIds; - case BrowserName.FIREFOX: - buildPrefix = getFirefoxBuildId(browserVersion); - break; + if (!browserVersion) { + suitableBuildIds = buildIds; + } else { + const buildPrefix = getBuildPrefix(browserName, browserVersion); - default: + if (buildPrefix === null) { return null; - } + } - const buildIds = this.getBinaryVersions(browserName, platform); - const suitableBuildIds = buildIds.filter(buildId => buildId.startsWith(buildPrefix)); + suitableBuildIds = buildIds.filter(buildId => buildId.startsWith(buildPrefix)); + } if (!suitableBuildIds.length) { return null; diff --git a/src/browser-installer/resolve-browser-version.ts b/src/browser-installer/resolve-browser-version.ts new file mode 100644 index 000000000..b952410ab --- /dev/null +++ b/src/browser-installer/resolve-browser-version.ts @@ -0,0 +1,14 @@ +import { BrowserName, type W3CBrowserName } from "../browser/types"; + +export const resolveBrowserVersion = (browserName: W3CBrowserName, { force = false } = {}): Promise => { + switch (browserName) { + case BrowserName.CHROME: + return import("./chrome").then(module => module.resolveLatestChromeVersion(force)); + case BrowserName.FIREFOX: + return import("./firefox").then(module => module.resolveLatestFirefoxVersion(force)); + case BrowserName.EDGE: + return import("./edge").then(module => module.resolveEdgeVersion()); + case BrowserName.SAFARI: + return import("./safari").then(module => module.resolveSafariVersion()); + } +}; diff --git a/src/browser-installer/safari/browser.ts b/src/browser-installer/safari/browser.ts new file mode 100644 index 000000000..72b39907f --- /dev/null +++ b/src/browser-installer/safari/browser.ts @@ -0,0 +1,24 @@ +import _ from "lodash"; +import { exec } from "child_process"; + +export const resolveSafariVersion = _.once( + () => + new Promise((resolve, reject) => { + const getSafariVersionError = new Error("Couldn't retrive safari version."); + + exec("mdls -name kMDItemVersion /Applications/Safari.app", (err, stdout) => { + if (err) { + reject(getSafariVersionError); + return; + } + + const regExpResult = /kMDItemVersion = "(.*)"/.exec(stdout); + + if (regExpResult && regExpResult[1]) { + resolve(regExpResult[1]); + } else { + reject(getSafariVersionError); + } + }); + }), +); diff --git a/src/browser-installer/safari/index.ts b/src/browser-installer/safari/index.ts index d41338ea8..e406170e6 100644 --- a/src/browser-installer/safari/index.ts +++ b/src/browser-installer/safari/index.ts @@ -4,6 +4,8 @@ import waitPort from "wait-port"; import { pipeLogsWithPrefix } from "../../dev-server/utils"; import { DRIVER_WAIT_TIMEOUT, SAFARIDRIVER_PATH } from "../constants"; +export { resolveSafariVersion } from "./browser"; + export const runSafariDriver = async ({ debug = false }: { debug?: boolean } = {}): Promise<{ gridUrl: string; process: ChildProcess; diff --git a/src/browser-installer/ubuntu-packages/collect-dependencies/browser-versions/chrome.ts b/src/browser-installer/ubuntu-packages/collect-dependencies/browser-versions/chrome.ts index 9507f727b..2305a39df 100644 --- a/src/browser-installer/ubuntu-packages/collect-dependencies/browser-versions/chrome.ts +++ b/src/browser-installer/ubuntu-packages/collect-dependencies/browser-versions/chrome.ts @@ -1,5 +1,5 @@ +import { CHROME_FOR_TESTING_MILESTONES_API_URL } from "../../../constants"; import { retryFetch } from "../../../utils"; -import { CHROME_FOR_TESTING_VERSIONS_API_URL } from "../constants"; type ChromeVersionInfo = { milestone: `${number}`; @@ -7,12 +7,14 @@ type ChromeVersionInfo = { revision: `${number}`; }; -type ChromeVersionsApiResponse = { milestones: Record<`${number}`, ChromeVersionInfo> }; +type ChromeMilestonesApiResponse = { milestones: Record<`${number}`, ChromeVersionInfo> }; export const fetchChromeMilestoneVersions = async (): Promise => { try { - const response = await retryFetch(CHROME_FOR_TESTING_VERSIONS_API_URL); - const data = (await response.json()) as ChromeVersionsApiResponse; + const response = await retryFetch(CHROME_FOR_TESTING_MILESTONES_API_URL); + + const data = (await response.json()) as ChromeMilestonesApiResponse; + return Object.values(data.milestones).map(({ version }) => version); } catch (err) { throw new Error(`Couldn't get chrome versions: ${err}`); diff --git a/src/browser-installer/ubuntu-packages/collect-dependencies/browser-versions/firefox.ts b/src/browser-installer/ubuntu-packages/collect-dependencies/browser-versions/firefox.ts index ad6e16552..1fe34e73c 100644 --- a/src/browser-installer/ubuntu-packages/collect-dependencies/browser-versions/firefox.ts +++ b/src/browser-installer/ubuntu-packages/collect-dependencies/browser-versions/firefox.ts @@ -1,7 +1,6 @@ import _ from "lodash"; import { getMilestone, retryFetch } from "../../../utils"; -import { FIREFOX_VERSIONS_API_URL } from "../constants"; -import { MIN_FIREFOX_VERSION } from "../../../constants"; +import { FIREFOX_VERSIONS_ALL_VERSIONS_API_URL, MIN_FIREFOX_VERSION } from "../../../constants"; type FirefoxVersionInfo = { category: "major" | "esr" | "stability" | "dev"; @@ -13,7 +12,7 @@ type FirefoxVersionsApiResponse = { releases: Record export const fetchFirefoxMilestoneVersions = async (): Promise => { try { - const response = await retryFetch(FIREFOX_VERSIONS_API_URL); + const response = await retryFetch(FIREFOX_VERSIONS_ALL_VERSIONS_API_URL); const data = (await response.json()) as FirefoxVersionsApiResponse; const stableVersions = Object.values(data.releases) .filter(data => ["stability", "esr"].includes(data.category)) diff --git a/src/browser-installer/ubuntu-packages/collect-dependencies/constants.ts b/src/browser-installer/ubuntu-packages/collect-dependencies/constants.ts index 56d8b0d49..ac1643ea8 100644 --- a/src/browser-installer/ubuntu-packages/collect-dependencies/constants.ts +++ b/src/browser-installer/ubuntu-packages/collect-dependencies/constants.ts @@ -1,5 +1,2 @@ -export const FIREFOX_VERSIONS_API_URL = "https://product-details.mozilla.org/1.0/firefox.json"; -export const CHROME_FOR_TESTING_VERSIONS_API_URL = - "https://googlechromelabs.github.io/chrome-for-testing/latest-versions-per-milestone.json"; // Those are couldn't be seen with readelf -d export const EXTRA_FIREFOX_SHARED_OBJECTS = ["libdbus-glib-1.so.2", "libXt.so.6"]; diff --git a/src/browser-installer/utils.ts b/src/browser-installer/utils.ts index 74e4cdb62..43cdd4fc6 100644 --- a/src/browser-installer/utils.ts +++ b/src/browser-installer/utils.ts @@ -22,7 +22,7 @@ export const DriverName = { export type SupportedBrowser = (typeof BrowserName)[keyof typeof BrowserName]; export type SupportedDriver = (typeof DriverName)[keyof typeof DriverName]; -export const createBrowserLabel = (browserName: string, version = "latest"): string => browserName + "@" + version; +export const createBrowserLabel = (browserName: string, version: string): string => browserName + "@" + version; export const getMilestone = (version: string | number): string => { if (typeof version === "number") { diff --git a/src/browser-pool/webdriver-pool.ts b/src/browser-pool/webdriver-pool.ts index 0466e5083..da60b2d67 100644 --- a/src/browser-pool/webdriver-pool.ts +++ b/src/browser-pool/webdriver-pool.ts @@ -1,5 +1,5 @@ import type { ChildProcess } from "child_process"; -import { runBrowserDriver } from "../browser-installer"; +import { resolveBrowserVersion, runBrowserDriver } from "../browser-installer"; import { getNormalizedBrowserName } from "../utils/browser"; import type { SupportedBrowser } from "../browser-installer"; @@ -33,11 +33,9 @@ export class WebdriverPool { ); } - if (!browserVersion) { - throw new Error(`Couldn't run browser driver for "${browserName}" because its version is undefined`); - } + const browserVersionNormalized = browserVersion || (await resolveBrowserVersion(browserNameNormalized)); - const wdProcesses = this.driverProcess.get(browserNameNormalized)?.get(browserVersion) ?? {}; + const wdProcesses = this.driverProcess.get(browserNameNormalized)?.get(browserVersionNormalized) ?? {}; for (const port in wdProcesses) { if (!wdProcesses[port].isBusy) { @@ -46,12 +44,12 @@ export class WebdriverPool { return { gridUrl: wdProcesses[port].gridUrl, free: () => this.freeWebdriver(port), - kill: () => this.killWebdriver(browserNameNormalized, browserVersion, port), + kill: () => this.killWebdriver(browserNameNormalized, browserVersionNormalized, port), }; } } - return this.createWebdriverProcess(browserNameNormalized, browserVersion, { debug }); + return this.createWebdriverProcess(browserNameNormalized, browserVersionNormalized, { debug }); } private freeWebdriver(port: Port): void { diff --git a/test/src/browser-installer/chrome/browser.ts b/test/src/browser-installer/chrome/browser.ts index 5da350501..fc89912ea 100644 --- a/test/src/browser-installer/chrome/browser.ts +++ b/test/src/browser-installer/chrome/browser.ts @@ -1,12 +1,16 @@ import proxyquire from "proxyquire"; import sinon, { type SinonStub } from "sinon"; import { BrowserName } from "../../../../src/browser/types"; -import type { installChrome as InstallChromeType } from "../../../../src/browser-installer/chrome/browser"; +import type { + installChrome as InstallChromeType, + resolveLatestChromeVersion as ResolveLatestChromeVersionType, +} from "../../../../src/browser-installer/chrome/browser"; describe("browser-installer/chrome/browser", () => { const sandbox = sinon.createSandbox(); let installChrome: typeof InstallChromeType; + let resolveLatestChromeVersion: typeof ResolveLatestChromeVersionType; let installChromiumStub: SinonStub; @@ -14,6 +18,8 @@ describe("browser-installer/chrome/browser", () => { let puppeteerInstallStub: SinonStub; let canDownloadStub: SinonStub; + let retryFetchStub: SinonStub; + let getBinaryPathStub: SinonStub; let getMatchedBrowserVersionStub: SinonStub; let installBinaryStub: SinonStub; @@ -32,102 +38,159 @@ describe("browser-installer/chrome/browser", () => { getMatchedBrowserVersionStub = sandbox.stub().returns(null); installBinaryStub = sandbox.stub(); + retryFetchStub = sandbox.stub().resolves({ text: () => Promise.resolve("") }); + installChromeDriverStub = sandbox.stub(); installUbuntuPackageDependenciesStub = sandbox.stub(); - installChrome = proxyquire("../../../../src/browser-installer/chrome/browser", { - "./driver": { installChromeDriver: installChromeDriverStub }, - "../chromium": { installChromium: installChromiumStub }, - "../ubuntu-packages": { installUbuntuPackageDependencies: installUbuntuPackageDependenciesStub }, - "@puppeteer/browsers": { - resolveBuildId: resolveBuildIdStub, - install: puppeteerInstallStub, - canDownload: canDownloadStub, - }, - "../registry": { - default: { - getBinaryPath: getBinaryPathStub, - getMatchedBrowserVersion: getMatchedBrowserVersionStub, - installBinary: installBinaryStub, + ({ installChrome, resolveLatestChromeVersion } = proxyquire( + "../../../../src/browser-installer/chrome/browser", + { + "./driver": { installChromeDriver: installChromeDriverStub }, + "../chromium": { installChromium: installChromiumStub }, + "../ubuntu-packages": { installUbuntuPackageDependencies: installUbuntuPackageDependenciesStub }, + "@puppeteer/browsers": { + resolveBuildId: resolveBuildIdStub, + install: puppeteerInstallStub, + canDownload: canDownloadStub, + }, + "../utils": { + ...require("src/browser-installer/utils"), + retryFetch: retryFetchStub, + }, + "../registry": { + default: { + getBinaryPath: getBinaryPathStub, + getMatchedBrowserVersion: getMatchedBrowserVersionStub, + installBinary: installBinaryStub, + }, }, }, - }).installChrome; + )); }); afterEach(() => sandbox.restore()); - it("should try to resolve browser path locally by default", async () => { - getMatchedBrowserVersionStub.withArgs(BrowserName.CHROME, sinon.match.string, "115").returns("115.0"); - getBinaryPathStub.withArgs(BrowserName.CHROME, sinon.match.string, "115.0").returns("/browser/path"); + describe("installChrome", () => { + it("should try to resolve browser path locally by default", async () => { + getMatchedBrowserVersionStub.withArgs(BrowserName.CHROME, sinon.match.string, "115").returns("115.0"); + getBinaryPathStub.withArgs(BrowserName.CHROME, sinon.match.string, "115.0").returns("/browser/path"); - const binaryPath = await installChrome("115"); + const binaryPath = await installChrome("115"); - assert.equal(binaryPath, "/browser/path"); - assert.notCalled(resolveBuildIdStub); - assert.notCalled(installBinaryStub); - }); + assert.equal(binaryPath, "/browser/path"); + assert.notCalled(resolveBuildIdStub); + assert.notCalled(installBinaryStub); + }); - it("should not try to resolve browser path locally with 'force' flag", async () => { - getMatchedBrowserVersionStub.withArgs(BrowserName.CHROME, sinon.match.string, "115").returns("115.0"); - resolveBuildIdStub.withArgs(BrowserName.CHROME, sinon.match.string, "115").resolves("115.0.5678.170"); + it("should not try to resolve browser path locally with 'force' flag", async () => { + getMatchedBrowserVersionStub.withArgs(BrowserName.CHROME, sinon.match.string, "115").returns("115.0"); + resolveBuildIdStub.withArgs(BrowserName.CHROME, sinon.match.string, "115").resolves("115.0.5678.170"); - installBinaryStub - .withArgs(BrowserName.CHROME, sinon.match.string, "115.0.5678.170", sinon.match.func) - .resolves("/new/downloaded/browser/path"); + installBinaryStub + .withArgs(BrowserName.CHROME, sinon.match.string, "115.0.5678.170", sinon.match.func) + .resolves("/new/downloaded/browser/path"); - const binaryPath = await installChrome("115", { force: true }); + const binaryPath = await installChrome("115", { force: true }); - assert.notCalled(getBinaryPathStub); - assert.equal(binaryPath, "/new/downloaded/browser/path"); - }); + assert.notCalled(getBinaryPathStub); + assert.equal(binaryPath, "/new/downloaded/browser/path"); + }); - it("should download browser if it is not downloaded", async () => { - getMatchedBrowserVersionStub.withArgs(BrowserName.CHROME, sinon.match.string, "115").returns(null); - resolveBuildIdStub.withArgs(BrowserName.CHROME, sinon.match.string, "115").resolves("115.0.5678.170"); - installBinaryStub - .withArgs(BrowserName.CHROME, sinon.match.string, "115.0.5678.170", sinon.match.func) - .resolves("/new/downloaded/browser/path"); + it("should download browser if it is not downloaded", async () => { + getMatchedBrowserVersionStub.withArgs(BrowserName.CHROME, sinon.match.string, "115").returns(null); + resolveBuildIdStub.withArgs(BrowserName.CHROME, sinon.match.string, "115").resolves("115.0.5678.170"); + installBinaryStub + .withArgs(BrowserName.CHROME, sinon.match.string, "115.0.5678.170", sinon.match.func) + .resolves("/new/downloaded/browser/path"); - const binaryPath = await installChrome("115"); + const binaryPath = await installChrome("115"); - assert.equal(binaryPath, "/new/downloaded/browser/path"); - }); + assert.equal(binaryPath, "/new/downloaded/browser/path"); + }); - it("should use chromium browser download if version is too low", async () => { - getMatchedBrowserVersionStub.returns(null); - installChromiumStub.withArgs("80").resolves("/browser/chromium/path"); + it("should use chromium browser download if version is too low", async () => { + getMatchedBrowserVersionStub.returns(null); + installChromiumStub.withArgs("80").resolves("/browser/chromium/path"); - const result = await installChrome("80"); + const result = await installChrome("80"); - assert.equal(result, "/browser/chromium/path"); - assert.notCalled(resolveBuildIdStub); - assert.notCalled(installBinaryStub); - }); + assert.equal(result, "/browser/chromium/path"); + assert.notCalled(resolveBuildIdStub); + assert.notCalled(installBinaryStub); + }); - it("should throw an error if can't download the browser", async () => { - getMatchedBrowserVersionStub.withArgs(BrowserName.CHROME, sinon.match.string, "115").returns(null); - resolveBuildIdStub.withArgs(BrowserName.CHROME, sinon.match.string, "115").resolves("115"); - canDownloadStub.resolves(false); - - await assert.isRejected( - installChrome("115"), - [ - `chrome@115 can't be installed.`, - `Probably the version '115' is invalid, please try another version.`, - "Version examples: '120', '120.0'", - ].join("\n"), - ); - }); + it("should throw an error if can't download the browser", async () => { + getMatchedBrowserVersionStub.withArgs(BrowserName.CHROME, sinon.match.string, "115").returns(null); + resolveBuildIdStub.withArgs(BrowserName.CHROME, sinon.match.string, "115").resolves("115"); + canDownloadStub.resolves(false); + + await assert.isRejected( + installChrome("115"), + [ + `chrome@115 can't be installed.`, + `Probably the version '115' is invalid, please try another version.`, + "Version examples: '120', '120.0'", + ].join("\n"), + ); + }); + + it("should try to install chromedriver if 'needWebDriver' is set", async () => { + await installChrome("115", { needWebDriver: true }); + + assert.calledOnceWith(installChromeDriverStub, "115", { force: false }); + }); - it("should try to install chromedriver if 'needWebDriver' is set", async () => { - await installChrome("115", { needWebDriver: true }); + it("should try to install ubuntu dependencies if 'needWebDriver' is set", async () => { + await installChrome("115", { needUbuntuPackages: true }); - assert.calledOnceWith(installChromeDriverStub, "115", { force: false }); + assert.calledOnceWith(installUbuntuPackageDependenciesStub); + }); }); - it("should try to install ubuntu dependencies if 'needWebDriver' is set", async () => { - await installChrome("115", { needUbuntuPackages: true }); + describe("resolveLatestChromeVersion", () => { + beforeEach(() => { + const apiUrl = "https://googlechromelabs.github.io/chrome-for-testing/LATEST_RELEASE_STABLE"; + + retryFetchStub.withArgs(apiUrl).resolves({ text: () => Promise.resolve("100.0.500.0") }); + }); + + it("should resolve local version", async () => { + getMatchedBrowserVersionStub.withArgs(BrowserName.CHROME, sinon.match.string).returns("500.0.100.0"); + + const version = await resolveLatestChromeVersion(); + + assert.equal(version, "500.0.100.0"); + assert.notCalled(retryFetchStub); + }); + + it("should resolve network version if local does not exist", async () => { + getMatchedBrowserVersionStub.withArgs(BrowserName.CHROME, sinon.match.string).returns(null); + + const version = await resolveLatestChromeVersion(); + + assert.equal(version, "100.0.500.0"); + assert.calledOnce(retryFetchStub); + }); + + it("should resolve network version on force mode", async () => { + getMatchedBrowserVersionStub.withArgs(BrowserName.CHROME, sinon.match.string).returns("500.0.100.0"); + + const version = await resolveLatestChromeVersion(true); + + assert.equal(version, "100.0.500.0"); + assert.calledOnce(retryFetchStub); + assert.notCalled(getMatchedBrowserVersionStub); + }); + + it("should memoize result", async () => { + getMatchedBrowserVersionStub.withArgs(BrowserName.CHROME, sinon.match.string).returns(null); + + await resolveLatestChromeVersion(true); + await resolveLatestChromeVersion(true); + await resolveLatestChromeVersion(true); - assert.calledOnceWith(installUbuntuPackageDependenciesStub); + assert.calledOnce(retryFetchStub); + }); }); }); diff --git a/test/src/browser-installer/edge/browser.ts b/test/src/browser-installer/edge/browser.ts new file mode 100644 index 000000000..c335347db --- /dev/null +++ b/test/src/browser-installer/edge/browser.ts @@ -0,0 +1,102 @@ +import proxyquire from "proxyquire"; +import sinon, { type SinonStub } from "sinon"; +import { BrowserPlatform } from "@puppeteer/browsers"; +import type { resolveEdgeVersion as ResolveEdgeVersionType } from "../../../../src/browser-installer/edge/browser"; + +describe("browser-installer/chrome/browser", () => { + const sandbox = sinon.createSandbox(); + + let resolveEdgeVersion: typeof ResolveEdgeVersionType; + + let execStub: SinonStub; + let getBrowserPlatformStub: SinonStub; + + beforeEach(() => { + execStub = sinon.stub(); + + getBrowserPlatformStub = sinon.stub().returns(BrowserPlatform.LINUX); + + resolveEdgeVersion = proxyquire("../../../../src/browser-installer/edge/browser", { + // eslint-disable-next-line camelcase + child_process: { exec: execStub }, + "../utils": { getBrowserPlatform: getBrowserPlatformStub }, + }).resolveEdgeVersion; + }); + + afterEach(() => sandbox.restore()); + + const execFailStub_ = (stub: SinonStub, err: Error): void => { + stub.callsFake((_: string, cb: (err: Error) => void) => { + cb(err); + }); + }; + + const execSuccessStub_ = (stub: SinonStub, stdout: string): void => { + stub.callsFake((_: string, cb: (err: null, stdout: string) => void) => { + cb(null, stdout); + }); + }; + + describe("resolveEdgeVersion", () => { + it("should throw error if exec command has failed", async () => { + execFailStub_(execStub, new Error("Can't run the command")); + + const errorMessage = "Couldn't retrive edge version. Looks like its not installed"; + await assert.isRejected(resolveEdgeVersion(), errorMessage); + }); + + it("should throw error if exec command returned invalid output", async () => { + execSuccessStub_(execStub, "some invalid output"); + + const errorMessage = + 'Couldn\'t retrive edge version. Expected browser version, but got "some invalid output"'; + await assert.isRejected(resolveEdgeVersion(), errorMessage); + }); + + it("should resolve windows output", async () => { + const windowsSuccessOutput = ` + HKEY_CURRENT_USER\\Software\\Microsoft\\Edge\\BLBeacon + version REG_SZ 114.0.1823.67 + `; + + getBrowserPlatformStub.returns(BrowserPlatform.WIN64); + execSuccessStub_(execStub, windowsSuccessOutput); + + const version = await resolveEdgeVersion(); + + assert.equal(version, "114.0.1823.67"); + }); + + it("should resolve linux output", async () => { + const linuxSuccessOutput = "Microsoft Edge 131.0.2903.112"; + + getBrowserPlatformStub.returns(BrowserPlatform.LINUX); + execSuccessStub_(execStub, linuxSuccessOutput); + + const version = await resolveEdgeVersion(); + + assert.equal(version, "131.0.2903.112"); + }); + + it("should resolve mac output", async () => { + const macOsSuccessOutput = "Microsoft Edge 131.0.2903.112"; + + getBrowserPlatformStub.returns(BrowserPlatform.MAC); + execSuccessStub_(execStub, macOsSuccessOutput); + + const version = await resolveEdgeVersion(); + + assert.equal(version, "131.0.2903.112"); + }); + + it("should memoize result", async () => { + execSuccessStub_(execStub, "Microsoft Edge 131.0.2903.112"); + + await resolveEdgeVersion(); + await resolveEdgeVersion(); + await resolveEdgeVersion(); + + assert.calledOnce(execStub); + }); + }); +}); diff --git a/test/src/browser-installer/firefox/browser.ts b/test/src/browser-installer/firefox/browser.ts index 08fd6d358..5bb745c73 100644 --- a/test/src/browser-installer/firefox/browser.ts +++ b/test/src/browser-installer/firefox/browser.ts @@ -1,16 +1,22 @@ import proxyquire from "proxyquire"; import sinon, { type SinonStub } from "sinon"; -import type { installFirefox as InstallFirefoxType } from "../../../../src/browser-installer/firefox/browser"; import { BrowserName } from "../../../../src/browser/types"; +import type { + installFirefox as InstallFirefoxType, + resolveLatestFirefoxVersion as ResolveLatestFirefoxVersionType, +} from "../../../../src/browser-installer/firefox/browser"; describe("browser-installer/firefox/browser", () => { const sandbox = sinon.createSandbox(); let installFirefox: typeof InstallFirefoxType; + let resolveLatestFirefoxVersion: typeof ResolveLatestFirefoxVersionType; let puppeteerInstallStub: SinonStub; let canDownloadStub: SinonStub; + let retryFetchStub: SinonStub; + let getBinaryPathStub: SinonStub; let getMatchedBrowserVersionStub: SinonStub; let installBinaryStub: SinonStub; @@ -22,6 +28,8 @@ describe("browser-installer/firefox/browser", () => { puppeteerInstallStub = sandbox.stub().resolves({ executablePath: "/firefox/browser/path" }); canDownloadStub = sandbox.stub().resolves(true); + retryFetchStub = sandbox.stub().resolves({ json: () => Promise.resolve({}) }); + getBinaryPathStub = sandbox.stub().returns(null); getMatchedBrowserVersionStub = sandbox.stub().returns(null); installBinaryStub = sandbox.stub(); @@ -29,81 +37,140 @@ describe("browser-installer/firefox/browser", () => { installLatestGeckoDriverStub = sandbox.stub(); installUbuntuPackageDependenciesStub = sandbox.stub(); - installFirefox = proxyquire("../../../../src/browser-installer/firefox/browser", { - "./driver": { installLatestGeckoDriver: installLatestGeckoDriverStub }, - "../ubuntu-packages": { installUbuntuPackageDependencies: installUbuntuPackageDependenciesStub }, - "@puppeteer/browsers": { - install: puppeteerInstallStub, - canDownload: canDownloadStub, - }, - "../registry": { - default: { - getBinaryPath: getBinaryPathStub, - getMatchedBrowserVersion: getMatchedBrowserVersionStub, - installBinary: installBinaryStub, + ({ installFirefox, resolveLatestFirefoxVersion } = proxyquire( + "../../../../src/browser-installer/firefox/browser", + { + "./driver": { installLatestGeckoDriver: installLatestGeckoDriverStub }, + "../ubuntu-packages": { installUbuntuPackageDependencies: installUbuntuPackageDependenciesStub }, + "@puppeteer/browsers": { + install: puppeteerInstallStub, + canDownload: canDownloadStub, + }, + "../utils": { + ...require("src/browser-installer/utils"), + retryFetch: retryFetchStub, + }, + "../registry": { + default: { + getBinaryPath: getBinaryPathStub, + getMatchedBrowserVersion: getMatchedBrowserVersionStub, + installBinary: installBinaryStub, + }, }, }, - }).installFirefox; + )); }); afterEach(() => sandbox.restore()); - it("should try to resolve browser path locally by default", async () => { - getMatchedBrowserVersionStub.withArgs(BrowserName.FIREFOX, sinon.match.string, "115").returns("115.0"); - getBinaryPathStub.withArgs(BrowserName.FIREFOX, sinon.match.string, "115.0").returns("/browser/path"); + describe("installFirefox", () => { + it("should try to resolve browser path locally by default", async () => { + getMatchedBrowserVersionStub.withArgs(BrowserName.FIREFOX, sinon.match.string, "115").returns("115.0"); + getBinaryPathStub.withArgs(BrowserName.FIREFOX, sinon.match.string, "115.0").returns("/browser/path"); - const binaryPath = await installFirefox("115"); + const binaryPath = await installFirefox("115"); - assert.equal(binaryPath, "/browser/path"); - assert.notCalled(installBinaryStub); - }); + assert.equal(binaryPath, "/browser/path"); + assert.notCalled(installBinaryStub); + }); - it("should not try to resolve browser path locally with 'force' flag", async () => { - getMatchedBrowserVersionStub.withArgs(BrowserName.FIREFOX, sinon.match.string, "115").returns("stable_115.0"); - installBinaryStub - .withArgs(BrowserName.FIREFOX, sinon.match.string, "stable_115.0", sinon.match.func) - .resolves("/new/downloaded/browser/path"); + it("should not try to resolve browser path locally with 'force' flag", async () => { + getMatchedBrowserVersionStub + .withArgs(BrowserName.FIREFOX, sinon.match.string, "115") + .returns("stable_115.0"); + installBinaryStub + .withArgs(BrowserName.FIREFOX, sinon.match.string, "stable_115.0", sinon.match.func) + .resolves("/new/downloaded/browser/path"); - const binaryPath = await installFirefox("115", { force: true }); + const binaryPath = await installFirefox("115", { force: true }); - assert.notCalled(getBinaryPathStub); - assert.equal(binaryPath, "/new/downloaded/browser/path"); - }); + assert.notCalled(getBinaryPathStub); + assert.equal(binaryPath, "/new/downloaded/browser/path"); + }); - it("should download browser if it is not downloaded", async () => { - getMatchedBrowserVersionStub.withArgs(BrowserName.FIREFOX, sinon.match.string, "115").returns(null); - installBinaryStub - .withArgs(BrowserName.FIREFOX, sinon.match.string, "stable_115.0", sinon.match.func) - .resolves("/new/downloaded/browser/path"); + it("should download browser if it is not downloaded", async () => { + getMatchedBrowserVersionStub.withArgs(BrowserName.FIREFOX, sinon.match.string, "115").returns(null); + installBinaryStub + .withArgs(BrowserName.FIREFOX, sinon.match.string, "stable_115.0", sinon.match.func) + .resolves("/new/downloaded/browser/path"); - const binaryPath = await installFirefox("115"); + const binaryPath = await installFirefox("115"); - assert.equal(binaryPath, "/new/downloaded/browser/path"); - }); + assert.equal(binaryPath, "/new/downloaded/browser/path"); + }); - it("should throw an error if can't download the browser", async () => { - getMatchedBrowserVersionStub.withArgs(BrowserName.FIREFOX, sinon.match.string, "115").returns(null); - canDownloadStub.resolves(false); - - await assert.isRejected( - installFirefox("115"), - [ - `firefox@115 can't be installed.`, - `Probably the version '115' is invalid, please try another version.`, - "Version examples: '120', '130.0', '131.0'", - ].join("\n"), - ); - }); + it("should throw an error if can't download the browser", async () => { + getMatchedBrowserVersionStub.withArgs(BrowserName.FIREFOX, sinon.match.string, "115").returns(null); + canDownloadStub.resolves(false); + + await assert.isRejected( + installFirefox("115"), + [ + `firefox@115 can't be installed.`, + `Probably the version '115' is invalid, please try another version.`, + "Version examples: '120', '130.0', '131.0'", + ].join("\n"), + ); + }); + + it("should try to install geckodriver if 'needWebDriver' is set", async () => { + await installFirefox("115", { needWebDriver: true }); + + assert.calledOnceWith(installLatestGeckoDriverStub, "115", { force: false }); + }); - it("should try to install geckodriver if 'needWebDriver' is set", async () => { - await installFirefox("115", { needWebDriver: true }); + it("should try to install ubuntu dependencies if 'needWebDriver' is set", async () => { + await installFirefox("115", { needUbuntuPackages: true }); - assert.calledOnceWith(installLatestGeckoDriverStub, "115", { force: false }); + assert.calledOnceWith(installUbuntuPackageDependenciesStub); + }); }); - it("should try to install ubuntu dependencies if 'needWebDriver' is set", async () => { - await installFirefox("115", { needUbuntuPackages: true }); + describe("resolveLatestFirefoxVersion", () => { + beforeEach(() => { + const apiUrl = "https://product-details.mozilla.org/1.0/firefox_versions.json"; + + retryFetchStub + .withArgs(apiUrl) + .resolves({ json: () => Promise.resolve({ LATEST_FIREFOX_VERSION: "100.500" }) }); + }); + + it("should resolve local version", async () => { + getMatchedBrowserVersionStub.withArgs(BrowserName.FIREFOX, sinon.match.string).returns("500.100"); + + const version = await resolveLatestFirefoxVersion(); + + assert.equal(version, "500.100"); + assert.notCalled(retryFetchStub); + }); + + it("should resolve network version if local does not exist", async () => { + getMatchedBrowserVersionStub.withArgs(BrowserName.FIREFOX, sinon.match.string).returns(null); + + const version = await resolveLatestFirefoxVersion(); + + assert.equal(version, "100.500"); + assert.calledOnce(retryFetchStub); + }); + + it("should resolve network version on force mode", async () => { + getMatchedBrowserVersionStub.withArgs(BrowserName.FIREFOX, sinon.match.string).returns("500.100"); + + const version = await resolveLatestFirefoxVersion(true); + + assert.equal(version, "100.500"); + assert.calledOnce(retryFetchStub); + assert.notCalled(getMatchedBrowserVersionStub); + }); + + it("should memoize result", async () => { + getMatchedBrowserVersionStub.withArgs(BrowserName.FIREFOX, sinon.match.string).returns(null); + + await resolveLatestFirefoxVersion(true); + await resolveLatestFirefoxVersion(true); + await resolveLatestFirefoxVersion(true); - assert.calledOnceWith(installUbuntuPackageDependenciesStub); + assert.calledOnce(retryFetchStub); + }); }); }); diff --git a/test/src/browser-installer/install.ts b/test/src/browser-installer/install.ts index 6a8a155f0..e35b7d844 100644 --- a/test/src/browser-installer/install.ts +++ b/test/src/browser-installer/install.ts @@ -141,13 +141,6 @@ describe("browser-installer/install", () => { assert.equal(binaryPath, null); }); }); - - it("should throw exception on empty browser version", async () => { - await assert.isRejected( - installBrowser(BrowserName.CHROME, "", { force }), - /Couldn't install browser 'chrome' because it has invalid version: ''/, - ); - }); }); }); }); diff --git a/test/src/browser-installer/safari/browser.ts b/test/src/browser-installer/safari/browser.ts new file mode 100644 index 000000000..25ec89c7e --- /dev/null +++ b/test/src/browser-installer/safari/browser.ts @@ -0,0 +1,69 @@ +import proxyquire from "proxyquire"; +import sinon, { type SinonStub } from "sinon"; +import type { resolveSafariVersion as ResolveSafariVersionType } from "../../../../src/browser-installer/safari/browser"; + +describe("browser-installer/chrome/browser", () => { + const sandbox = sinon.createSandbox(); + + let resolveSafariVersion: typeof ResolveSafariVersionType; + + let execStub: SinonStub; + + beforeEach(() => { + execStub = sinon.stub(); + + resolveSafariVersion = proxyquire("../../../../src/browser-installer/safari/browser", { + // eslint-disable-next-line camelcase + child_process: { exec: execStub }, + }).resolveSafariVersion; + }); + + afterEach(() => sandbox.restore()); + + const execFailStub_ = (stub: SinonStub, err: Error): void => { + stub.callsFake((_: string, cb: (err: Error) => void) => { + cb(err); + }); + }; + + const execSuccessStub_ = (stub: SinonStub, stdout: string): void => { + stub.callsFake((_: string, cb: (err: null, stdout: string) => void) => { + cb(null, stdout); + }); + }; + + describe("resolveEdgeVersion", () => { + it("should throw error if exec command has failed", async () => { + execFailStub_(execStub, new Error("Can't run the command")); + + const errorMessage = "Couldn't retrive safari version."; + await assert.isRejected(resolveSafariVersion(), errorMessage); + }); + + it("should throw error if exec command returned invalid output", async () => { + execSuccessStub_(execStub, "some invalid output"); + + const errorMessage = "Couldn't retrive safari version."; + await assert.isRejected(resolveSafariVersion(), errorMessage); + }); + + it("should resolve safari version", async () => { + const successOutput = 'kMDItemVersion = "16.4"'; + + execSuccessStub_(execStub, successOutput); + + const version = await resolveSafariVersion(); + + assert.equal(version, "16.4"); + }); + + it("should memoize result", async () => { + execSuccessStub_(execStub, 'kMDItemVersion = "16.4"'); + + await resolveSafariVersion(); + await resolveSafariVersion(); + + assert.calledOnce(execStub); + }); + }); +});