Cross-Browser Testing
What it is
Section titled “What it is”Cross-browser testing runs the same web UI suite against multiple rendering engines and device profiles, so behaviour that differs between them is caught before users find it. The core problem is that browsers are not interchangeable: layout, CSS support, fonts, and JavaScript APIs all vary.
┌──────────────┐ suite ──▶│ Chromium │ Blink (Chrome, Edge, Opera) │ Firefox │ Gecko │ WebKit │ Safari (macOS, iOS) └──────────────┘ ▲ local engines ▲ real devices + OS (cloud)A pragmatic strategy is the test pyramid for browsers: run the full suite on one primary engine on every commit, and a smaller smoke suite on the remaining engines and real devices.
When to use it / when to avoid it
Section titled “When to use it / when to avoid it”Use it for
- User-facing journeys on browsers your analytics say matter (Chrome, Safari, mobile Safari)
- CSS/layout features with known engine differences (grid, container queries,
:has()) - File uploads, clipboard, media playback and other capability-sensitive features
- A release gate before shipping a UI change
Avoid it when
- You are running a slow suite on every engine on every commit — sample the matrix instead
- The feature is backend-only and has no rendering surface
- A single engine already has full coverage and you are only chasing a coverage number
- You need native app behaviour — that is mobile
Frameworks & tools
Section titled “Frameworks & tools”| Tool | Ecosystem | Notes |
|---|---|---|
| Playwright | Node / Multi | Bundles Chromium, Firefox, WebKit; projects per browser |
| BrowserStack | Multi | Real browsers and devices in the cloud |
| Sauce Labs | Multi | Cross-browser and mobile cloud, rich analytics |
| LambdaTest | Multi | Cloud grid with live and automated sessions |
| Selenium Grid | Multi | Self-hosted hub for parallel browser nodes |
| Cypress | Multi | Same suite in Chrome, Edge, Firefox, Electron |
| TestCafe | Node | Runs in any browser without WebDriver |
| WebdriverIO | Node | Multi-browser + cloud service integrations |
Quick setup
Section titled “Quick setup”npm init playwright@latestnpx playwright installimport { defineConfig, devices } from '@playwright/test';
export default defineConfig({ projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, { name: 'firefox', use: { ...devices['Desktop Firefox'] } }, { name: 'webkit', use: { ...devices['Desktop Safari'] } }, { name: 'mobile-safari', use: { ...devices['iPhone 15'] } }, ],});pip install pytest-playwrightplaywright installgo get github.com/playwright-community/playwright-go<dependency> <groupId>com.microsoft.playwright</groupId> <artifactId>playwright</artifactId> <version>1.48.0</version> <scope>test</scope></dependency>dotnet add package Microsoft.Playwright.NUnitpwsh bin/Debug/net8.0/playwright.ps1 installExample
Section titled “Example”import { test, expect } from '@playwright/test';
test('checkout works on this engine', async ({ page, browserName }) => { await page.goto('/checkout'); await page.getByRole('button', { name: 'Pay' }).click(); await expect(page.getByText('Order confirmed')).toBeVisible(); test.info().annotations.push({ type: 'engine', description: browserName });});import pytestfrom playwright.sync_api import sync_playwright
@pytest.mark.parametrize("engine", ["chromium", "firefox", "webkit"])def test_checkout(engine): with sync_playwright() as p: browser = getattr(p, engine).launch() page = browser.new_page() page.goto("http://localhost:3000/checkout") page.get_by_role("button", name="Pay").click() assert page.get_by_text("Order confirmed").is_visible() browser.close(){ "browserName": "safari", "bstack:options": { "os": "OS X", "osVersion": "Sonoma", "deviceName": "iPhone 15", "realMobile": true }}from selenium import webdriverfrom selenium.webdriver.common.options import ArgOptions
options = ArgOptions()options.browser_name = "firefox"driver = webdriver.Remote( command_executor="http://grid:4444/wd/hub", options=options,)MutableCapabilities caps = new MutableCapabilities();caps.setCapability("browserName", "firefox");caps.setCapability("browserVersion", "latest");caps.setCapability("platformName", "Windows 11");WebDriver driver = new RemoteWebDriver( new URL("https://ondemand.saucelabs.com/wd/hub"), caps);var options = new ChromeOptions();options.AddAdditionalOption("browserName", "chrome");options.AddAdditionalOption("platformName", "Windows 11");options.BrowserVersion = "latest";var driver = new RemoteWebDriver( new Uri("https://hub.lambdatest.com/wd/hub"), options);Anti-patterns & best practices
Section titled “Anti-patterns & best practices”- Define the matrix from data. Use analytics to pick the engines and devices worth testing, not personal preference.
- Run the full suite on one primary engine per commit and a smoke subset on the rest.
- Do not use engine-specific hacks as a fix. A CSS workaround that only helps one browser should be understood first.
- Normalise viewport and timezone. Set them explicitly so failures are about code, not the environment.
- Capture screenshots and traces per engine. A failure you cannot reproduce locally needs artifacts.
- Budget cloud minutes. Real-device sessions are metered; keep them on critical journeys.
Self-assessment quiz
Section titled “Self-assessment quiz”Self-assessment quiz
Pick one answer per question, then press Check answers.