Skip to content

Commit

Permalink
Merge pull request #1046 from gemini-testing/TESTPLANE-386.latest_loc…
Browse files Browse the repository at this point in the history
…al_browser

feat: add ability to run local browsers without specified browserVersion
  • Loading branch information
KuznetsovRoman authored Jan 13, 2025
2 parents 9610610 + 7827fd0 commit b946892
Show file tree
Hide file tree
Showing 23 changed files with 653 additions and 189 deletions.
21 changes: 20 additions & 1 deletion src/browser-installer/chrome/browser.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -74,3 +76,20 @@ export const installChrome = async (

return browserPath;
};

export const resolveLatestChromeVersion = _.memoize(async (force = false): Promise<string> => {
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");
});
});
4 changes: 2 additions & 2 deletions src/browser-installer/chrome/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
11 changes: 11 additions & 0 deletions src/browser-installer/constants.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
67 changes: 67 additions & 0 deletions src/browser-installer/edge/browser.ts
Original file line number Diff line number Diff line change
@@ -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<string> =>
new Promise<string>((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<string> => {
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<string> => {
const getMsEdgeVersion = 'reg query "HKEY_CURRENT_USER\\Software\\Microsoft\\Edge\\BLBeacon" /v version';

return extractBrowserVersion(getMsEdgeVersion);
};

const resolveMacEdgeVersion = (): Promise<string> => {
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}"`);
}
});
1 change: 1 addition & 0 deletions src/browser-installer/edge/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
28 changes: 27 additions & 1 deletion src/browser-installer/firefox/browser.ts
Original file line number Diff line number Diff line change
@@ -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<string> => {
const platform = getBrowserPlatform();
Expand Down Expand Up @@ -59,3 +67,21 @@ export const installFirefox = async (

return browserPath;
};

export const resolveLatestFirefoxVersion = _.memoize(async (force = false): Promise<string> => {
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");
});
});
4 changes: 2 additions & 2 deletions src/browser-installer/firefox/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/browser-installer/index.ts
Original file line number Diff line number Diff line change
@@ -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";
25 changes: 12 additions & 13 deletions src/browser-installer/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,6 @@ export const installBrowser = async (
browserVersion?: string,
{ force = false, shouldInstallWebDriver = false, shouldInstallUbuntuPackages = false } = {},
): Promise<string | null> => {
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());
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}),
);
}
Expand Down
44 changes: 27 additions & 17 deletions src/browser-installer/registry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -123,35 +139,29 @@ class Registry {
public getMatchedBrowserVersion(
browserName: SupportedBrowser,
platform: BrowserPlatform,
browserVersion: string,
browserVersion?: string,
): string | null {
const registryKey = getRegistryBinaryKey(browserName, platform);

if (!this.registry.binaries[registryKey]) {
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;
Expand Down
14 changes: 14 additions & 0 deletions src/browser-installer/resolve-browser-version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { BrowserName, type W3CBrowserName } from "../browser/types";

export const resolveBrowserVersion = (browserName: W3CBrowserName, { force = false } = {}): Promise<string> => {
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());
}
};
24 changes: 24 additions & 0 deletions src/browser-installer/safari/browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import _ from "lodash";
import { exec } from "child_process";

export const resolveSafariVersion = _.once(
() =>
new Promise<string>((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);
}
});
}),
);
2 changes: 2 additions & 0 deletions src/browser-installer/safari/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit b946892

Please sign in to comment.