Migrating from Selenium 3 to 4
A step-by-step upgrade guide: dependency changes, the capabilities rewrite, removed APIs, Duration-based timeouts, Service objects, and a checklist for suites written against Selenium 3.
Selenium 3.141.59 was the last 3.x release, in 2018. Suites still running on it exist in every large company, and upgrading them is one of the most common tasks handed to a new automation engineer. The good news: most 3.x code compiles and runs unchanged on 4.x. The bad news: the parts that break are exactly the parts that were never idiomatic to begin with.
Step 1: Bump the Dependency
Move straight to the current version. Do not stop at 4.0; the 4.0 to 4.48 changes are additive and you get Selenium Manager, which removes an entire class of upgrade problems.
<dependency> <groupId>org.seleniumhq.selenium</groupId> <artifactId>selenium-java</artifactId> <version>4.48.0</version></dependency>
<!-- Remove these if present: they are no longer needed --><!-- io.github.bonigarcia:webdrivermanager (Selenium Manager replaces it) --><!-- org.seleniumhq.selenium:htmlunit-driver (not shipped with Selenium 4) -->selenium>=4.48
# Remove: webdriver-manager (Selenium Manager replaces it)# Ensure: Python 3.10 or newer (3.9 support ended in Selenium 4.37){"dependencies": { "selenium-webdriver": "^4.48.0"}}// Remove chromedriver / geckodriver npm packages; Selenium Manager handles drivers.<PackageReference Include="Selenium.WebDriver" Version="4.48.0" /><PackageReference Include="Selenium.Support" Version="4.48.0" />
<!-- Remove Selenium.WebDriver.ChromeDriver and similar driver packages --><!-- Assemblies are strongly signed from 4.44 -->Runtime floors: Java 11, Python 3.10, .NET 8 or .NET Framework 4.6.2 via netstandard2.0, Node.js 18.
Step 2: Replace DesiredCapabilities with Options
This is the change that breaks the most suites. Selenium 4 speaks only the W3C protocol, and W3C rejects unknown top-level capability keys. Cloud providers moved their custom keys under vendor prefixes such as sauce:options or bstack:options.
// SELENIUM 3DesiredCapabilities caps = DesiredCapabilities.chrome();caps.setCapability("version", "92");caps.setCapability("platform", "Windows 10");caps.setCapability("build", "nightly-123"); // vendor key at top level: rejected by W3CWebDriver driver = new RemoteWebDriver(gridUrl, caps);
// SELENIUM 4ChromeOptions options = new ChromeOptions();options.setBrowserVersion("92");options.setPlatformName("Windows 10");Map<String, Object> cloudOptions = new HashMap<>();cloudOptions.put("build", "nightly-123");options.setCapability("cloud:options", cloudOptions); // vendor prefixWebDriver driver = new RemoteWebDriver(gridUrl, options);# SELENIUM 3caps = DesiredCapabilities.CHROME.copy()caps["version"] = "92"caps["platform"] = "Windows 10"caps["build"] = "nightly-123" # rejected by W3Cdriver = webdriver.Remote(command_executor=grid_url, desired_capabilities=caps)
# SELENIUM 4 (desired_capabilities argument was removed in 4.10)options = webdriver.ChromeOptions()options.browser_version = "92"options.platform_name = "Windows 10"options.set_capability("cloud:options", {"build": "nightly-123"})driver = webdriver.Remote(command_executor=grid_url, options=options)// SELENIUM 3const caps = { browserName: 'chrome', version: '92', platform: 'Windows 10', build: 'nightly-123' };const driver = new Builder().usingServer(gridUrl).withCapabilities(caps).build();
// SELENIUM 4const chrome = require('selenium-webdriver/chrome');const options = new chrome.Options().setBrowserVersion('92').setPlatformName('Windows 10').set('cloud:options', { build: 'nightly-123' });const driver = await new Builder().usingServer(gridUrl).forBrowser('chrome').setChromeOptions(options).build();// SELENIUM 3var caps = new DesiredCapabilities("chrome", "92", new Platform(PlatformType.Windows));caps.SetCapability("build", "nightly-123");var driver = new RemoteWebDriver(gridUri, caps);
// SELENIUM 4var options = new ChromeOptions { BrowserVersion = "92", PlatformName = "Windows 10" };options.AddAdditionalOption("cloud:options", new Dictionary<string, object> { ["build"] = "nightly-123" });// AddAdditionalCapability was replaced by AddAdditionalOptionvar driver = new RemoteWebDriver(gridUri, options);If you must merge capabilities, remember Java’s merge no longer mutates: write options = options.merge(caps).
Step 3: Timeouts and Waits Take Duration
Java removed the (long, TimeUnit) overloads. Anything that compiled with a warning in 4.0 fails to compile now.
import java.time.Duration;
// SELENIUM 3driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);driver.manage().timeouts().pageLoadTimeout(30, TimeUnit.SECONDS);WebDriverWait wait = new WebDriverWait(driver, 10);
// SELENIUM 4driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(30));WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));# Python signatures did not change; seconds are still floatsdriver.implicitly_wait(10)driver.set_page_load_timeout(30)wait = WebDriverWait(driver, 10)
# Removed in 4.x: driver.find_element_by_id(...) and friends (deprecated in 4.0, removed 4.3)from selenium.webdriver.common.by import Bydriver.find_element(By.ID, "username")// Timeouts are set through manage().setTimeouts with millisecondsawait driver.manage().setTimeouts({ implicit: 10000, pageLoad: 30000, script: 30000 });
// Explicit waits unchangedawait driver.wait(until.elementLocated(By.id('result')), 10000);// TimeSpan was already the C# convention; unchangeddriver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(10);driver.Manage().Timeouts().PageLoad = TimeSpan.FromSeconds(30);var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(10));
// ExpectedConditions moved to the DotNetSeleniumExtras.WaitHelpers package,// or write lambdas: wait.Until(d => d.FindElement(By.Id("result")).Displayed);Step 4: Driver Paths Become Service Objects (or Disappear)
// SELENIUM 3System.setProperty("webdriver.chrome.driver", "/opt/drivers/chromedriver");WebDriver driver = new ChromeDriver();
// SELENIUM 4: preferred, let Selenium Manager resolve the driverWebDriver driver = new ChromeDriver();
// SELENIUM 4: explicit path when you must (air-gapped CI)ChromeDriverService service = new ChromeDriverService.Builder() .usingDriverExecutable(new File("/opt/drivers/chromedriver")) .build();WebDriver driver = new ChromeDriver(service, new ChromeOptions());# SELENIUM 3driver = webdriver.Chrome(executable_path="/opt/drivers/chromedriver")
# SELENIUM 4: preferreddriver = webdriver.Chrome()
# SELENIUM 4: explicit pathfrom selenium.webdriver.chrome.service import Serviceservice = Service(executable_path="/opt/drivers/chromedriver")driver = webdriver.Chrome(service=service)// SELENIUM 3: required the chromedriver npm package on PATH// SELENIUM 4: preferredconst driver = await new Builder().forBrowser('chrome').build();
// Explicit pathconst chrome = require('selenium-webdriver/chrome');const service = new chrome.ServiceBuilder('/opt/drivers/chromedriver');const driver = await new Builder().forBrowser('chrome').setChromeService(service).build();// SELENIUM 3var driver = new ChromeDriver(@"C:\drivers");
// SELENIUM 4: preferredvar driver = new ChromeDriver();
// Explicit pathvar service = ChromeDriverService.CreateDefaultService(@"C:\drivers", "chromedriver.exe");var driver = new ChromeDriver(service, new ChromeOptions());Step 5: Fix the Long Tail
Search your codebase for these and replace them:
| Selenium 3 | Selenium 4 |
|---|---|
driver.findElementById("x") (Java) | driver.findElement(By.id("x")) |
driver.find_element_by_css_selector(...) (Python) | driver.find_element(By.CSS_SELECTOR, ...) |
new Actions(driver).moveToElement(e).build().perform() | build() is optional; perform() alone works |
driver.executeScript("window.open()") to open tabs | driver.switchTo().newWindow(WindowType.TAB) |
FirefoxOptions.setLegacy(true) | Delete; only geckodriver is supported |
Augmenter to get screenshots from RemoteWebDriver | Not needed; RemoteWebDriver implements TakesScreenshot |
FirefoxProfile + FirefoxBinary (Python) | options.binary_location, options.set_preference |
driver.get_log("browser") (Python) | BiDi console handlers; classic log API is Chromium-only |
ExpectedConditions in OpenQA.Selenium.Support.UI (C#) | SeleniumExtras.WaitHelpers package or lambdas |
htmlunit-driver | Not bundled; add separately or use headless Chrome |
Step 6: Reconsider What 3.x Made You Write
An upgrade is a chance to delete code, not just port it:
- Driver download utilities (WebDriverManager, shell scripts, checked-in binaries): Selenium Manager replaces them.
- JavaScript scroll-and-click helpers: usually written to dodge Selenium 3 click failures; try plain
click()again, it improved. - Custom “wait for console error” hacks that polled
driver.manage().logs(): replace with BiDiaddJavaScriptErrorHandler. - Thread.sleep: was never right; see Explicit Waits.
Migration Checklist
- Upgrade the dependency to the latest 4.x and fix compile errors (Java, C#) or run the suite and fix
TypeError/AttributeError(Python, JavaScript). - Rewrite every
DesiredCapabilitiesblock asOptions; move vendor keys under a prefix. - Replace
TimeUnittimeouts withDuration(Java). - Delete driver path management and the libraries that did it.
- Run the whole suite in CI twice. Selenium 4’s W3C actions and stricter element interactability can surface real timing bugs the 3.x driver masked.
- Enable BiDi on one test class and start capturing JavaScript errors; you will find product bugs immediately.
Summary
- The dependency bump is trivial; the capabilities rewrite is not. Budget time for cloud provider keys.
- Java timeouts require
Duration; Python lostexecutable_pathandfind_element_by_*. - Let Selenium Manager handle drivers and remove the tooling that used to.
- Treat the migration as a cleanup: most Selenium 3 workarounds have a native Selenium 4 replacement.