How to Run Cross-Browser Tests With Selenium
Run one Selenium test across browsers and operating systems with browser options, explicit waits, and Selenium Grid. Includes runnable Python examples and troubleshooting.
Write one test for the behavior you need to verify, then run it in each target browser using that browser’s Selenium options. Run browsers locally for a quick check; use Selenium Grid when you need remote browsers, different operating systems, browser versions, or parallel sessions.
This guide uses Python and Selenium 4. The examples use a placeholder application URL and expected page behavior; replace them with your own test URL, locators, and assertions. Selenium 4 remote sessions require a browser-specific Options object. Grid matches requested capabilities only when a connected node can provide them. Selenium Grid documentation · Browser options
1. Install Selenium and choose a test matrix
Use the Python version already supported by your project. Create and activate a virtual environment, then install Selenium:
python -m venv .venv
# Linux or macOS:
source .venv/bin/activate
# Windows PowerShell:
# .venv\Scripts\Activate.ps1
python -m pip install -U selenium
Pick a small matrix based on your users and risk areas. For example, start with the main browser families your application supports and add operating systems or versions where compatibility risk justifies them. There is no universal browser matrix. Selenium supports browser-specific implementations, and Grid can route sessions to different browser and platform combinations. Selenium browser documentation
| Where the browser runs | Use it for | What you need |
|---|---|---|
| Local WebDriver | Developer smoke tests and a single installed browser | Browser, Selenium binding, and a compatible driver setup |
| Grid standalone | Local remote-session debugging or a quick single-machine suite | Java 11+, Selenium Server JAR, browser(s), and drivers or Selenium Manager configuration |
| Hub and nodes / distributed Grid | Browsers, versions, or operating systems on multiple machines; more capacity | Grid components and nodes configured with the desired browser environments |
2. Write a browser-independent test
Keep the assertion about product behavior independent of browser setup. Use an explicit wait for the condition the assertion needs; reaching the document load state does not guarantee that a dynamic application has finished rendering.
Save this as test_cross_browser.py. The test can run locally by default, or remotely when SELENIUM_REMOTE_URL is set. It supports Chrome, Firefox, and Edge via BROWSER.
import os
import unittest
from urllib.parse import urlparse
from selenium import webdriver
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
BROWSER = os.getenv("BROWSER", "chrome").lower()
BASE_URL = os.getenv("BASE_URL", "https://example.com")
REMOTE_URL = os.getenv("SELENIUM_REMOTE_URL")
def make_options(browser):
if browser == "chrome":
return webdriver.ChromeOptions()
if browser == "firefox":
return webdriver.FirefoxOptions()
if browser == "edge":
return webdriver.EdgeOptions()
raise ValueError("BROWSER must be chrome, firefox, or edge")
def make_driver():
options = make_options(BROWSER)
if REMOTE_URL:
# Capabilities are requests. The remote Grid must have a matching node.
version = os.getenv("BROWSER_VERSION")
platform = os.getenv("BROWSER_PLATFORM")
if version:
options.set_capability("browserVersion", version)
if platform:
options.set_capability("platformName", platform)
return webdriver.Remote(command_executor=REMOTE_URL, options=options)
if BROWSER == "chrome":
return webdriver.Chrome(options=options)
if BROWSER == "firefox":
return webdriver.Firefox(options=options)
return webdriver.Edge(options=options)
class CrossBrowserSmokeTest(unittest.TestCase):
def test_homepage_has_expected_heading(self):
driver = make_driver()
try:
driver.set_page_load_timeout(60)
driver.get(BASE_URL)
# Replace this with the real application condition and assertion.
WebDriverWait(driver, 15).until(
EC.visibility_of_element_located(("tag name", "h1"))
)
heading = driver.find_element("tag name", "h1").text
self.assertTrue(heading.strip(), "Expected a non-empty page heading")
self.assertEqual(urlparse(driver.current_url).hostname,
urlparse(BASE_URL).hostname)
finally:
driver.quit()
if __name__ == "__main__":
unittest.main()
Run locally:
python -m unittest -v test_cross_browser.py
Choose a local browser with an environment variable:
# Linux or macOS
BROWSER=firefox BASE_URL=https://your-app.example python -m unittest -v
# Windows PowerShell
$env:BROWSER="edge"; $env:BASE_URL="https://your-app.example"; python -m unittest -v
Selenium Manager is integrated with Selenium bindings and can manage drivers on most supported platforms and browsers. If automatic management cannot find or obtain a driver in your environment, configure the browser and driver manually and ensure the driver is available on PATH. Selenium Manager documentation
3. Start a local Selenium Grid
For a quick remote session on one machine, install Java 11 or higher, install the browser(s) you intend to use, and download the Selenium Server JAR from the Selenium downloads page. Start standalone mode:
java -jar selenium-server-<version>.jar standalone
Replace <version> with the JAR version you downloaded. The default Grid endpoint is http://localhost:4444. You can open that address in a browser to inspect the Grid UI and available capabilities. Standalone runs the Grid components in one process on one machine; it is useful for local remote-driver debugging and quick suites, but it does not provide multi-machine browser coverage by itself. Grid quick start
4. Run the same test in remote browser sessions
Set SELENIUM_REMOTE_URL to the Grid URL. Set BROWSER to select the Options class. Optional BROWSER_VERSION and BROWSER_PLATFORM values request matching capacity from Grid; they do not install or guarantee a browser environment on their own.
# Linux or macOS, after starting Grid
SELENIUM_REMOTE_URL=http://localhost:4444 \
BROWSER=chrome \
BROWSER_VERSION=stable \
BROWSER_PLATFORM=Linux \
BASE_URL=https://your-app.example \
python -m unittest -v test_cross_browser.py
SELENIUM_REMOTE_URL=http://localhost:4444 BROWSER=firefox python -m unittest -v test_cross_browser.py
SELENIUM_REMOTE_URL=http://localhost:4444 BROWSER=edge python -m unittest -v test_cross_browser.py
Windows PowerShell example:
$env:SELENIUM_REMOTE_URL="http://localhost:4444"
$env:BROWSER="chrome"
$env:BROWSER_VERSION="stable"
$env:BROWSER_PLATFORM="Linux"
$env:BASE_URL="https://your-app.example"
python -m unittest -v test_cross_browser.py
For another language, keep the same flow: create that browser’s Options object, set any supported matching capabilities, pass it to a remote WebDriver pointed at the Grid URL, navigate, wait for the application condition, assert, and always quit the session. See Selenium’s WebDriver documentation and the binding-specific API.
5. Select browser options and capabilities deliberately
| Setting | Purpose | Guidance |
|---|---|---|
| Browser Options class | Chooses the browser implementation and its browser-specific configuration | Use ChromeOptions, FirefoxOptions, or EdgeOptions; Selenium 4 remote sessions require Options. |
browserVersion |
Requests a browser version on a remote end | Optional. Use a value advertised by available Grid capacity; exact matching depends on node configuration. |
platformName |
Requests the operating system/platform for the remote end | Useful with remote providers or configured Grid nodes. The requested platform must exist. |
pageLoadStrategy |
Controls which document ready state navigation waits for | normal waits for complete; eager for interactive; none does not block for ready state. Faster return requires explicit readiness waits. |
acceptInsecureCerts |
Allows invalid or self-signed certificates for the session | Only enable when the test environment intentionally uses such certificates; it changes browser trust behavior. |
| Timeouts | Bound page navigation, scripts, and element lookup behavior | Set page-load and script timeouts to suit the application. Prefer explicit waits for state-specific conditions. |
For example, set a page-load strategy on an Options instance before constructing the driver:
options = webdriver.ChromeOptions()
options.page_load_strategy = "eager" # normal, eager, or none
# For a remote session, pass options to webdriver.Remote(...).
# For a local session, pass options to webdriver.Chrome(options=options).
Use normal unless you have a reason to return earlier. eager and none can reduce waiting for irrelevant assets, but they do not mean the app is ready. Always wait for the specific element, state, or message the next assertion depends on. Avoid mixing implicit waits with explicit waits because their combined timing can be difficult to reason about. Waiting strategies
6. Scale from standalone to multiple Grid nodes
When you need distinct operating systems, browser versions, or more simultaneous sessions, configure Grid with nodes that provide those environments. A Hub and nodes or a distributed deployment gives clients a single routing entry point while browser sessions run on machines with the required capabilities. Selenium Grid can scale up or down; actual capacity depends on concurrent sessions and available resources. Start with measured capacity rather than assuming more workers always shorten the suite. Grid architecture and goals
- List the browser and platform combinations the suite must cover.
- Provide a node for each required environment and verify the node advertises the expected capabilities.
- Point test clients to the Grid router or Hub endpoint and request only capabilities that are available.
- Increase parallel sessions gradually while checking machine load, queue time, session failures, and total suite duration.
- Keep test data and accounts isolated so concurrent runs do not modify the same state.
Parallelism is most useful after each test can run independently. Shared accounts, fixed record IDs, mutable fixtures, and tests that depend on execution order can fail when run concurrently. Retries may hide these issues, so use them as a diagnostic or narrowly scoped policy rather than a substitute for stable test setup.
7. Make cross-browser results reliable
- Assert behavior, not timing. Wait for visible, clickable, or otherwise meaningful application conditions instead of sleeping for a guessed number of seconds.
- Use stable locators. Prefer application-owned IDs or accessible attributes where available; avoid brittle selectors tied to incidental layout.
- Keep setup deterministic. Control test data, account state, locale, and feature flags when they affect the assertion.
- Keep sessions isolated. Create a fresh driver per test or per isolated test worker, and call
quit()in cleanup so browser processes and Grid slots are released. - Compare failures across browsers. A failure limited to one browser can indicate an implementation difference, unsupported feature, browser-specific driver issue, or a race that happens at different speeds.
- Capture diagnostics. On failure, record the browser/version/platform, test name, current URL, relevant page state, and screenshot or browser logs where supported by your setup.
- Protect test environments. Use non-production accounts and avoid exposing sensitive data in logs or artifacts.
8. Troubleshooting common failures
| Symptom | Likely cause | Fix |
|---|---|---|
| Driver executable not found or local session will not start | Browser/driver is missing, unsupported, or not discoverable in the environment | Confirm the browser is installed and accessible. Let Selenium Manager manage the driver where supported, or install a compatible driver and put it on PATH. Check proxy or network restrictions if driver acquisition fails. |
| Remote session creation fails with “session not created” | Requested browser/version/platform is unavailable, or browser and driver versions do not match | Check Grid UI and node configuration. Remove optional version/platform requests to test basic matching, then add a capability only when the node advertises it. |
Connection refused at localhost:4444 |
Grid is not running, is bound elsewhere, or the client uses the wrong endpoint | Start the Server JAR, inspect its startup output and Grid UI, and set SELENIUM_REMOTE_URL to the reachable Grid endpoint. |
| Element not found immediately after navigation | The document loaded before JavaScript rendered the element, or the locator is wrong | Use an explicit wait for the expected state, verify the locator in that browser, and confirm the page actually reached the expected route. |
| Intermittent timeout or flaky assertion | Race condition, variable network/application time, shared test state, or overly short timeout | Wait for the precise condition, isolate test data, inspect failure diagnostics, and set a bounded timeout appropriate to the environment. Avoid using a fixed sleep as the final synchronization strategy. |
| Click intercepted or element not interactable | Overlay, animation, off-screen position, or responsive layout difference | Wait for overlays to disappear and the element to become clickable; scroll or adjust the test viewport if the real user flow requires it. Do not bypass a genuine usability defect with JavaScript clicks. |
| Works in Chrome but fails in another browser | Browser behavior, unsupported web feature, timing difference, or driver-specific issue | Reproduce the same test in the failing browser, inspect browser console and driver logs, and determine whether the product code or browser-specific assumption differs. |
| Sessions queue or run slowly | More concurrent requests than available node slots or machine resources | Reduce parallel workers, add appropriately configured capacity, and measure the end-to-end suite at each concurrency level. |
| Grid is reachable from outside the test network | Network access is broader than intended | Restrict access with firewall rules and trusted network boundaries. Do not expose an unauthenticated Grid endpoint to the public internet. |
Selenium identifies synchronization as a common source of WebDriver errors. Its Grid documentation also warns that exposed Grid infrastructure can give third parties access to internal applications and files or allow custom binaries to run. Restrict Grid network reachability to trusted test clients. Troubleshooting assistance · Grid security warning
9. Performance, reliability, and cost
Performance: Remote sessions add routing and machine scheduling to browser startup and page load. A small stable matrix often gives faster feedback than launching every browser/version combination on every change. Use parallelism to reduce wall-clock time only when nodes have capacity and tests are independent. Measure queue time and total suite time on the actual Grid.
Reliability: Grid is another service in the test path, so monitor whether failures come from the application, browser, driver, node, or Grid. Explicit readiness conditions and clean session teardown reduce avoidable flakes and leaked slots. A remote session does not make a test deterministic by itself.
Cost: A self-managed Grid has no Selenium license charge stated here, but it consumes machine capacity and engineering time for setup, browser images, upgrades, monitoring, and security. Larger matrices and higher concurrency require more browser capacity. Selenium documentation says resource sizing varies by workload and environment; measure before setting a permanent worker count.
Or skip the browser setup
For a rendered page image or PDF, ScreenshotNeo is a website screenshot API and MCP server. It does not run Selenium tests or replace assertions across interactive browser sessions. It can return a screenshot from one GET request without requiring you to install and manage browser drivers. See the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
- Cookie and consent banners, newsletter popups, and chat widgets are removed before capture; each cleanup step can be turned off.
- Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed. Responses identify the page verdict and billing status in headers.
- An MCP server provides
take_screenshot,get_page_info, andcapture_pdftools for Claude, Cursor, and other MCP clients. - The Free plan includes 1,000 screenshots a month without a card. Paid plans start at $5 for 3,000 screenshots; every feature is available on every plan.
Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.
FAQ
Can I run cross-browser tests without Selenium Grid?
Yes. Run separate local sessions for the browsers installed on your machine. Grid is useful when the browser or operating system is remote, when you need more environments, or when you need to distribute sessions.
Does setting a browser version install that browser?
A version capability is a request to the remote end. The Grid must have a node that can satisfy it; the Options capability alone does not configure a complete browser environment.
Should I use a screenshot API to test browser compatibility?
No. A screenshot API is useful for obtaining rendered page images, but Selenium is the fit for interacting with a page and asserting behavior across browser sessions.
Which Selenium version should I install?
Use the current stable binding and Server release shown on the Selenium downloads page, and keep client and server versions compatible. Selenium’s release listing and some documentation version labels can differ; check the release page when choosing a version.


