Skip to content
End To End Tester
Platformspractical

Cross-Browser Testing

Which browsers actually need testing in 2026, what still differs between engines, and a strategy that catches real defects without tripling your pipeline.

2 min read · updated 19 September 2026

Cross-browser testing has changed shape. Three engines remain, JavaScript semantics have converged, and the differences that break real applications have moved to layout, fonts, input and platform integration.

#What genuinely differs

Safari and WebKit is where most of it lives:

  • Date parsing — new Date('2026-01-01 12:00') is invalid in Safari and fine in Chromium, which is still a real and recurring production bug.
  • Intl output differs in spacing and separators between engines and versions. Asserting on exact formatted strings is a trap.
  • IndexedDB and storage eviction behave differently, especially in private browsing.
  • Scroll behaviour, momentum, 100vh under a collapsing toolbar.
  • Autoplay, media and PWA support.
  • Newer CSS features land on a different schedule.

Firefox is an independent implementation, which is its value: it catches places where you have coded to Chromium's interpretation of a spec rather than the spec. Its developer tools also surface accessibility issues that others do not.

Chromium is the majority everywhere and the one you develop against, which means it is the one where you will not notice a bug.

#A strategy that is worth the cost

typescript
// playwright.config.ts
projects: [
  // Everything, on the engine most of your users have.
  { name: 'chromium', use: { ...devices['Desktop Chrome'] } },

  // The journeys that would end the business, on the others.
  {
    name: 'webkit-critical',
    use: { ...devices['Desktop Safari'] },
    grep: /@critical/
  },
  {
    name: 'firefox-critical',
    use: { ...devices['Desktop Firefox'] },
    grep: /@critical/
  },

  // Mobile viewport, which is a layout concern more than an engine one.
  {
    name: 'mobile-safari',
    use: { ...devices['iPhone 15'] },
    grep: /@critical/
  }
]
typescript
test('a customer can check out @critical', async ({ page }) => { /* ... */ });

Tagging ten to twenty journeys @critical and running those everywhere costs a fraction of a full matrix and catches nearly everything a full matrix would. The full suite on three engines triples cost and triples flakiness for a small marginal signal.

#Writing tests that do not care which engine they are on

Most "cross-browser failures" are test failures, not application failures.

typescript
// Brittle: exact formatted output differs between engines and ICU versions.
await expect(page.getByTestId('total')).toHaveText('£1,234.56');

// Durable: assert the value, or a tolerant pattern.
await expect(page.getByTestId('total')).toHaveText(/1[,\s]?234[.,]56/);
typescript
// Brittle: a pixel position.
expect((await button.boundingBox())!.y).toBe(412);

// Durable: the property that matters.
await expect(button).toBeInViewport();

Other rules that transfer: query by role and accessible name rather than by CSS structure; never assert on exact font metrics; and use web-first assertions so timing differences between engines do not become failures.

#Real devices

An engine is not a device. Real hardware brings its own font stack, input model, memory limits, network stack and OS integration, and emulation reproduces none of it.

The honest split:

  • Emulated viewports for responsive layout — cheap, run them often.
  • Real devices for a handful of journeys — BrowserStack, Sauce Labs, LambdaTest, AWS Device Farm. All speak WebDriver, which is one of the better arguments for keeping a Selenium path available even in a Playwright shop.
typescript
// Playwright against a device cloud
const browser = await chromium.connect(
  `wss://cdp.browserstack.com/playwright?caps=${encodeURIComponent(JSON.stringify({
    browser: 'playwright-webkit',
    os: 'ios',
    device: 'iPhone 15',
    'browserstack.username': process.env.BS_USER,
    'browserstack.accessKey': process.env.BS_KEY
  }))}`
);

#Visual differences

Cross-engine visual regression is mostly a trap: font rendering differs between platforms, so a WebKit screenshot will never match a Chromium baseline. Keep a baseline per project, and generate all of them in the same container that will compare them:

typescript
await expect(page).toHaveScreenshot(`checkout-${test.info().project.name}.png`, {
  maxDiffPixelRatio: 0.01
});

See screenshots.

#Choosing the matrix

From your own analytics. A template matrix copied from a blog post costs real money and tests browsers your users do not have.

Sessions, last 90 days
  Chrome desktop      52%   ← full suite
  Safari iOS          23%   ← critical journeys, real device for 3 of them
  Chrome Android      12%   ← critical journeys
  Safari desktop       6%   ← critical journeys
  Edge                 4%   ← Chromium; covered
  Firefox              2%   ← critical journeys
  everything else      1%   ← not tested; fix on report

That last line is a decision, not an oversight. Write it down, so it is a choice rather than an omission somebody discovers during an incident.

Common questions

Do I still need to test in multiple browsers?
Yes, but far less than a decade ago and in a different shape. JavaScript semantics rarely differ now; layout, fonts, input handling, date and number formatting, and Safari-specific quirks still do. Run everything on one engine and a small critical subset on the others.
Is testing WebKit the same as testing Safari?
Not quite. Playwright's WebKit is built from the same upstream source, so it catches engine-level differences, but it is not Safari's shipping build and it is not Safari on iOS. For the last mile you need a real device.
How do I choose which browsers to test?
From your analytics, not from a template. Take the browsers covering the top 95% of your actual sessions, add anything with unusual business value, and ignore the long tail — a browser used by 0.2% of visitors rarely justifies a permanent pipeline cost.

Runnable samples for this page

last test results ↗

Working tests, not fragments — they run in CI on every push to 8exgh/endtoendtester-samples.

Was this page useful?