From 9a69e467190d021e818222e671ebe677e252a20c Mon Sep 17 00:00:00 2001 From: Mohammad Saddam Hossain Date: Sun, 4 Oct 2026 06:52:47 +0530 Subject: [PATCH] feat(browser): add browser.version to pin local Chrome/Firefox runs (#51) Co-Authored-By: Claude Sonnet 5.5 --- docs-site/docs/configuration.md | 8 +++ .../config/SeleniumBootConfig.java | 4 ++ .../driver/BrowserVersionPin.java | 19 +++++++ .../driver/LocalChromeDriverProvider.java | 2 + .../driver/LocalFirefoxDriverProvider.java | 2 + .../unit/BrowserVersionPinTest.java | 52 +++++++++++++++++++ 6 files changed, 87 insertions(+) create mode 100644 src/main/java/com/seleniumboot/driver/BrowserVersionPin.java create mode 100644 src/test/java/com/seleniumboot/unit/BrowserVersionPinTest.java diff --git a/docs-site/docs/configuration.md b/docs-site/docs/configuration.md index 6bd4d81..e24fa5d 100644 --- a/docs-site/docs/configuration.md +++ b/docs-site/docs/configuration.md @@ -49,6 +49,7 @@ browser: name: chrome # chrome | firefox | edge | safari headless: false # true in CI (auto-forced when CI detected) lifecycle: per-test # per-test (default) | per-suite + # version: "120.0.6099.109" # optional: pin local Chrome/Firefox to an exact build # Optional: extra browser arguments arguments: @@ -184,6 +185,13 @@ The browser to use. Selenium Manager downloads the matching driver automatically ### `headless` Runs the browser without a visible window. Automatically forced to `true` when a CI environment is detected (GitHub Actions, Jenkins, etc.). +### `version` +Pins a local Chrome or Firefox run to an exact browser version, for when "passes on my +laptop, fails in CI" is a browser-version mismatch. Passed to Selenium Manager, which resolves +and downloads the matching build. Optional — omit it to use whatever browser is installed. +Ignored by `remote`, `browserstack` and `saucelabs` modes (those use their own +`browserVersion`). + ### `lifecycle` Controls when the WebDriver session is closed. diff --git a/src/main/java/com/seleniumboot/config/SeleniumBootConfig.java b/src/main/java/com/seleniumboot/config/SeleniumBootConfig.java index f2a90e3..17fa52e 100644 --- a/src/main/java/com/seleniumboot/config/SeleniumBootConfig.java +++ b/src/main/java/com/seleniumboot/config/SeleniumBootConfig.java @@ -110,6 +110,10 @@ public static final class Browser { private boolean failOnConsoleErrors = false; private List matrix = Collections.emptyList(); private String device; // optional device profile name, e.g. "iPhone 14" + private String version; // optional exact browser version for local Chrome/Firefox runs + + public String getVersion() { return version; } + public void setVersion(String version) { this.version = version; } public String getDevice() { return device; } public void setDevice(String device) { this.device = device; } diff --git a/src/main/java/com/seleniumboot/driver/BrowserVersionPin.java b/src/main/java/com/seleniumboot/driver/BrowserVersionPin.java new file mode 100644 index 0000000..56e9070 --- /dev/null +++ b/src/main/java/com/seleniumboot/driver/BrowserVersionPin.java @@ -0,0 +1,19 @@ +package com.seleniumboot.driver; + +import org.openqa.selenium.remote.AbstractDriverOptions; + +/** + * Applies {@code browser.version} to local driver options. Selenium Manager resolves + * (and downloads if needed) the matching browser build. Omitted or blank leaves the options + * untouched, so the browser installed on the machine is used. + */ +final class BrowserVersionPin { + + private BrowserVersionPin() {} + + static void apply(AbstractDriverOptions options, String version) { + if (version != null && !version.isBlank()) { + options.setBrowserVersion(version.trim()); + } + } +} diff --git a/src/main/java/com/seleniumboot/driver/LocalChromeDriverProvider.java b/src/main/java/com/seleniumboot/driver/LocalChromeDriverProvider.java index 24870b8..208a727 100644 --- a/src/main/java/com/seleniumboot/driver/LocalChromeDriverProvider.java +++ b/src/main/java/com/seleniumboot/driver/LocalChromeDriverProvider.java @@ -32,6 +32,8 @@ public WebDriver createDriver() { capabilities.forEach(options::setCapability); } + BrowserVersionPin.apply(options, config.getBrowser().getVersion()); + if (config.getBrowser().isHeadless()) { options.addArguments("--headless=new"); } diff --git a/src/main/java/com/seleniumboot/driver/LocalFirefoxDriverProvider.java b/src/main/java/com/seleniumboot/driver/LocalFirefoxDriverProvider.java index 6306881..6230490 100644 --- a/src/main/java/com/seleniumboot/driver/LocalFirefoxDriverProvider.java +++ b/src/main/java/com/seleniumboot/driver/LocalFirefoxDriverProvider.java @@ -35,6 +35,8 @@ public WebDriver createDriver() { capabilities.forEach(options::setCapability); } + BrowserVersionPin.apply(options, config.getBrowser().getVersion()); + if (config.getBrowser().isHeadless()) { options.addArguments("-headless"); } diff --git a/src/test/java/com/seleniumboot/unit/BrowserVersionPinTest.java b/src/test/java/com/seleniumboot/unit/BrowserVersionPinTest.java new file mode 100644 index 0000000..5bd9773 --- /dev/null +++ b/src/test/java/com/seleniumboot/unit/BrowserVersionPinTest.java @@ -0,0 +1,52 @@ +package com.seleniumboot.unit; + +import com.seleniumboot.config.SeleniumBootConfig; +import org.openqa.selenium.chrome.ChromeOptions; +import org.openqa.selenium.firefox.FirefoxOptions; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; + +/** {@code browser.version} is passed to local driver options only when set. */ +public class BrowserVersionPinTest { + + private static void apply(Object options, String version) throws Exception { + Class pin = Class.forName("com.seleniumboot.driver.BrowserVersionPin"); + var m = pin.getDeclaredMethod("apply", org.openqa.selenium.remote.AbstractDriverOptions.class, String.class); + m.setAccessible(true); + m.invoke(null, options, version); + } + + @Test + public void chrome_versionSet_isPassedThrough() throws Exception { + ChromeOptions o = new ChromeOptions(); + apply(o, "120.0.6099.109"); + assertEquals(o.getBrowserVersion(), "120.0.6099.109"); + } + + @Test + public void firefox_versionSet_isPassedThrough() throws Exception { + FirefoxOptions o = new FirefoxOptions(); + apply(o, " 121.0 "); + assertEquals(o.getBrowserVersion(), "121.0"); + } + + @Test + public void nullOrBlank_leavesVersionUnset() throws Exception { + ChromeOptions a = new ChromeOptions(); + apply(a, null); + ChromeOptions b = new ChromeOptions(); + apply(b, " "); + assertEquals(a.getBrowserVersion(), new ChromeOptions().getBrowserVersion()); + assertEquals(b.getBrowserVersion(), new ChromeOptions().getBrowserVersion()); + } + + @Test + public void config_defaultsToNull_andRoundTrips() { + SeleniumBootConfig.Browser b = new SeleniumBootConfig.Browser(); + assertNull(b.getVersion()); + b.setVersion("120"); + assertEquals(b.getVersion(), "120"); + } +}