Skip to content

Cross-Browser Testing

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.

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
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
Terminal window
npm init playwright@latest
npx playwright install
import { 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'] } },
],
});
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 });
});
  • 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

Pick one answer per question, then press Check answers.

  1. Why is cross-browser testing necessary even with standards?
  2. Which tool bundles Chromium, Firefox and WebKit with per-browser projects?
  3. What is a sensible strategy for the browser matrix?
  4. Why normalise viewport, locale and timezone in browser tests?
  5. What is the main cost concern with real-device clouds?