Cross-Browser Testing Guide

How to Test Cross Browser with Playwright (Chrome, Firefox, WebKit): Complete 2026 Guide

A scenario-by-scenario walkthrough of cross-browser testing with Playwright. Engine-specific rendering quirks, conditional test skips, browser-specific selectors, CI matrix configuration, viewport differences, and the pitfalls that silently break your test suite on browsers you forgot to check.

3 engines

β€œPlaywright supports Chromium, Firefox, and WebKit out of the box, covering approximately 96% of global browser market share according to StatCounter's 2025 browser usage data.”

StatCounter GlobalStats

0Browser engines covered
0%Global market share covered
0Cross-browser scenarios
0%Fewer lines with Assrt

Cross-Browser Test Execution Flow

DeveloperPlaywright CLIChromiumFirefoxWebKitnpx playwright testLaunch Chromium projectLaunch Firefox projectLaunch WebKit projectChromium resultsFirefox resultsWebKit resultsMerged HTML report

1. Why Cross-Browser Testing Is Harder Than It Looks

Playwright ships with three browser engines: Chromium (the engine behind Chrome and Edge), Firefox (Gecko), and WebKit (the engine behind Safari). Installing all three is a single command, and running tests across them requires just a config change. That simplicity is deceptive. The real difficulty is not launching browsers; it is handling the behavioral differences that emerge once your tests interact with real web platform features.

Chromium, Firefox, and WebKit implement the same web standards, but they do so with different timing, different default behaviors, and different edge-case handling. A CSS property that works identically in Chrome and Firefox may render differently in WebKit. A JavaScript API that resolves synchronously in Chromium may resolve asynchronously in Firefox. A file upload dialog that exposes certain attributes in one engine may omit them in another. These are not theoretical concerns. They are the issues that surface at 2 AM when your CI pipeline goes red on the WebKit leg and green everywhere else.

There are five structural reasons cross-browser testing creates unique challenges. First, each engine has its own event loop semantics, which means race conditions that never appear in Chromium can manifest in Firefox or WebKit. Second, CSS layout engines differ in their handling of flexbox gaps, grid auto-placement, and scrollbar widths, producing pixel-level differences that affect visual regression tests. Third, network stack implementations vary in how they handle HTTP/2 push, preflight CORS caching, and certificate validation, so network interception tests may behave differently per engine. Fourth, WebKit on Linux (the version Playwright uses in CI) does not perfectly mirror Safari on macOS, so some platform-specific bugs require additional attention. Fifth, browser-specific APIs like the Web Bluetooth API or the File System Access API may not exist in all engines, requiring conditional test logic.

Cross-Browser Test Pipeline

🌐

Write Tests

Engine-agnostic by default

βš™οΈ

Configure Projects

Chromium + Firefox + WebKit

βœ…

Run Locally

Spot engine-specific failures

βš™οΈ

CI Matrix

Parallel execution per engine

βœ…

Merge Report

Unified HTML report

A robust cross-browser test suite accounts for all of these differences without duplicating test logic. The sections below walk through each scenario with runnable Playwright TypeScript that you can paste directly into your project.

2. Configuring Playwright for Three Browser Engines

Before writing any cross-browser scenario, you need the right project configuration. Playwright uses the concept of β€œprojects” to define browser targets. Each project runs your entire test suite (or a subset) against a specific browser engine with its own settings for viewport, user agent, and device emulation.

Installing Playwright Browsers

Cross-Browser Setup Checklist

  • Install all three browser engines with npx playwright install
  • Define separate projects for chromium, firefox, and webkit in playwright.config.ts
  • Set consistent viewport sizes across all projects (or intentionally vary them)
  • Configure CI to install system dependencies with npx playwright install-deps
  • Add a shared global setup for test data seeding
  • Enable the HTML reporter for merged cross-browser results
  • Set retries to 1 for flake detection across engines

Playwright Configuration with Three Projects

The key insight is that each project inherits from use at the top level but can override any setting. This lets you set a shared viewport and timeout while customizing per-engine behavior where needed.

playwright.config.ts

Running Tests Against a Single Engine

During development, you will often want to run against just one engine to iterate quickly, then validate the full matrix before pushing. Playwright supports this with the --project flag.

Targeting Specific Browsers

That two-test failure on WebKit is exactly the kind of engine-specific issue the following scenarios teach you to handle.

3. Scenario: Handling Engine-Specific Rendering Differences

The most common cross-browser failures come from CSS rendering differences. Chromium, Firefox, and WebKit each implement their own layout engine, and edge cases in flexbox, grid, scroll behavior, and font rendering produce visible differences. Your tests need to account for these without becoming brittle or engine-specific.

1

Detecting and Handling CSS Rendering Differences

Moderate

Goal

Write assertions that validate layout behavior without breaking on minor pixel differences between engines. Verify that a responsive navigation menu collapses correctly on all three browsers.

Preconditions

  • App running with a responsive navigation component
  • Tests configured with three browser projects
  • Visual comparison baseline images generated per browser (if using screenshot assertions)

Playwright Implementation

cross-browser-layout.spec.ts

What to Assert Beyond the UI

  • Bounding box values confirm layout without pixel-perfect screenshots
  • Use browserName fixture for engine-specific wait logic, not engine-specific assertions
  • Prefer tolerance-based checks (within 2px) over exact pixel matching

Layout Test: Playwright vs Assrt

import { test, expect } from '@playwright/test';

test('responsive nav collapses at mobile', async ({ page, browserName }) => {
  await page.goto('/');
  await page.setViewportSize({ width: 1280, height: 720 });
  const navLinks = page.getByRole('navigation').getByRole('link');
  await expect(navLinks.first()).toBeVisible();
  await page.setViewportSize({ width: 375, height: 667 });
  if (browserName === 'webkit') {
    await page.waitForTimeout(100);
  }
  const hamburger = page.getByRole('button', { name: /menu/i });
  await expect(hamburger).toBeVisible();
  await expect(navLinks.first()).toBeHidden();
  await hamburger.click();
  await expect(navLinks.first()).toBeVisible();
});
33% fewer lines

4. Scenario: Conditional Test Skips per Browser

Some features genuinely do not work in all browsers. The File System Access API is Chromium-only. The Web Bluetooth API has no Firefox support. Certain video codecs play in WebKit but not in Chromium without flags. For these cases, you need conditional skips that clearly document why a test is skipped rather than leaving a mysterious failure in your CI report.

2

Conditional Skips for Unsupported Browser APIs

Straightforward

Goal

Skip tests that rely on browser-specific APIs with clear annotations, so your test report shows intentional skips rather than false failures.

Playwright Implementation

conditional-skips.spec.ts

Skip Annotation Patterns

Playwright provides several skip mechanisms. Use test.skip(condition, reason) inside the test body for dynamic skips. Use test.fixme(condition, reason) when you know the test should eventually work but the engine has a known bug. Use test.describe.configure({ mode: 'serial' }) when tests within a describe block depend on each other and a skip in one should skip subsequent tests.

skip-patterns.spec.ts

Try Assrt for free

Open-source AI testing framework. No signup required.

Get Started β†’

5. Scenario: Browser-Specific Selectors and DOM Behavior

Most Playwright selectors work identically across engines because Playwright abstracts over the underlying DOM APIs. However, certain edge cases expose differences. Shadow DOM piercing, ARIA role computation, and form element behavior can vary. The :has() CSS pseudo-class, for example, was supported in Chromium long before Firefox shipped it. Native <dialog> elements behave slightly differently across engines in terms of focus management and backdrop click handling.

3

Handling DOM and Selector Differences Across Engines

Moderate

Goal

Write selectors that work reliably across all three engines, handle shadow DOM components, and verify native dialog behavior consistently.

Playwright Implementation

cross-browser-selectors.spec.ts

What to Assert Beyond the UI

  • Validate ARIA state attributes (aria-expanded, aria-hidden) rather than relying on visual checks alone
  • Check the Constraint Validation API for form validity instead of matching engine-specific validation message strings
  • Use Playwright's built-in shadow DOM piercing rather than engine-specific workarounds

6. Scenario: Viewport and Device Emulation Across Engines

Playwright's device descriptors include viewport size, user agent string, device scale factor, and whether the device supports touch events. These descriptors vary by engine because real devices use different engines. An iPhone uses WebKit, a Pixel uses Chromium, and no mainstream mobile device uses Firefox (though Firefox for Android exists). Testing mobile viewports requires choosing the right engine for realistic emulation.

4

Multi-Device Viewport Testing Strategy

Moderate

Goal

Configure device emulation projects that pair realistic viewport sizes with the correct browser engine, and write tests that adapt to touch vs. pointer interactions.

Playwright Implementation

playwright.config.ts
viewport-tests.spec.ts

What to Assert Beyond the UI

  • Verify isMobile fixture reflects the correct device configuration
  • Confirm viewport dimensions match the device descriptor using page.viewportSize()
  • Check that touch events fire correctly on mobile projects and mouse events on desktop projects

Device Emulation Decision Tree

🌐

iPhone/iPad

Use WebKit engine

🌐

Android Pixel

Use Chromium engine

🌐

Desktop Safari

Use WebKit engine

🌐

Desktop Chrome

Use Chromium engine

🌐

Desktop Firefox

Use Firefox engine

7. Scenario: Network Behavior Differences and Request Interception

Playwright's page.route() API lets you intercept and modify network requests across all three engines. The API surface is consistent, but the underlying network stacks differ. Firefox and WebKit handle CORS preflight requests differently from Chromium. Service worker registration timing varies across engines. HTTP/2 server push behavior is engine-dependent. These differences matter when your tests rely on network interception for mocking APIs or simulating offline conditions.

5

Network Interception and API Mocking Across Engines

Complex

Goal

Mock API responses and simulate network conditions consistently across Chromium, Firefox, and WebKit, handling engine-specific timing differences in request interception.

Playwright Implementation

cross-browser-network.spec.ts

What to Assert Beyond the UI

  • Monitor the number of intercepted requests to confirm no duplicate calls across engines
  • Verify CORS headers are handled correctly; Firefox is stricter about preflight caching than Chromium
  • Check service worker registration status, as WebKit service worker lifecycle events fire at different points than Chromium

API Mocking: Playwright vs Assrt

import { test, expect } from '@playwright/test';

test('mock API and verify rendering', async ({ page }) => {
  await page.route('**/api/products', async (route) => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({
        products: [
          { id: 1, name: 'Test Widget', price: 29.99 },
          { id: 2, name: 'Test Gadget', price: 49.99 },
        ],
      }),
    });
  });
  await page.goto('/products');
  await expect(page.getByText('Test Widget')).toBeVisible();
  await expect(page.getByText('$29.99')).toBeVisible();
});
25% fewer lines

8. Scenario: CI Matrix Configuration for Parallel Browser Runs

Running all three browsers sequentially in CI triples your pipeline time. A well-configured CI matrix runs each browser project in parallel using separate jobs that share the same test artifacts. This section covers GitHub Actions configuration, but the same pattern applies to GitLab CI, CircleCI, and other providers.

6

GitHub Actions Matrix for Parallel Cross-Browser Testing

Complex

Goal

Configure a CI pipeline that runs Chromium, Firefox, and WebKit tests in parallel, shards large test suites for speed, and merges results into a single report.

GitHub Actions Configuration

.github/workflows/e2e.yml

Playwright Configuration for Sharding

To produce blob reports that the merge step can combine, update your Playwright config to output blob reports when running in CI.

playwright.config.ts
CI Matrix Execution

CI Config: Manual YAML vs Assrt

# .github/workflows/e2e.yml
name: E2E Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        browser: [chromium, firefox, webkit]
        shard: [1/3, 2/3, 3/3]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npx playwright install --with-deps ${{ matrix.browser }}
      - run: npx playwright test --project=${{ matrix.browser }} --shard=${{ matrix.shard }}
      - uses: actions/upload-artifact@v4
        with:
          name: blob-${{ matrix.browser }}-${{ strategy.job-index }}
          path: blob-report/
50% fewer lines

9. Common Pitfalls That Break Cross-Browser Test Suites

These are not theoretical warnings. Each pitfall below comes from real issues reported in the Playwright GitHub repository and Stack Overflow threads about cross-browser testing failures.

7

Cross-Browser Pitfalls and How to Fix Them

Complex

Pitfalls to Avoid

  • Hardcoding waitForTimeout values that only work in Chromium. WebKit and Firefox have different event loop timing. Use waitForSelector, waitForURL, or expect assertions with auto-retry instead.
  • Matching exact validation messages from native form elements. Each engine produces different wording (Chrome: 'Please fill out this field', Firefox: 'Please fill in this field', Safari: 'Fill out this field'). Assert on validity state, not message text.
  • Pixel-perfect screenshot assertions without per-browser baselines. Font rendering, anti-aliasing, and sub-pixel positioning differ across engines. Generate separate baseline snapshots per browser using the browserName fixture.
  • Forgetting to install system dependencies for WebKit on Linux CI. WebKit requires additional packages (libwoff1, libvpx, libevent, libopus, etc.) that Chromium and Firefox do not need. Use npx playwright install-deps webkit.
  • Testing Chromium-only APIs (File System Access, Web USB, Web Bluetooth) without conditional skips. These tests will hard-fail in Firefox and WebKit. Always use test.skip(browserName !== 'chromium', reason).
  • Assuming scrollbar width is zero. Chromium uses overlay scrollbars by default on most platforms; Firefox shows classic scrollbars that take up 15-17px of layout width. This shifts elements and breaks position assertions.
  • Using page.mouse.wheel() and expecting identical scroll distances across engines. WebKit interprets delta values differently than Chromium. Use locator-based scrollIntoView or keyboard-based scrolling instead.
  • Running CI without the --with-deps flag. Bare Ubuntu images lack libgbm, libatk, and other libraries. The test process crashes before any test runs, producing confusing 'browser not found' errors.

Handling Scrollbar Width Differences

scrollbar-aware-test.spec.ts

10. Writing Cross-Browser Tests in Plain English with Assrt

Every scenario above required careful attention to engine-specific behavior: conditional skips, tolerance values, browser-specific wait logic, and CI matrix configuration. Assrt lets you express cross-browser test intent in plain English and handles the engine-specific complexity in the compiled output. You declare which browsers a scenario targets, and Assrt generates the correct Playwright TypeScript with the appropriate conditional logic built in.

The Assrt Approach

Here is how the responsive navigation scenario from Section 3 looks as an Assrt file. Instead of writing browserName conditionals, you declare the expected behavior and let Assrt figure out the engine-specific implementation details.

# cross-browser-layout.assrt

config:
  browsers: [chromium, firefox, webkit]
  baseURL: http://localhost:3000

---
scenario: Responsive nav collapses at mobile breakpoint
steps:
  - set viewport to 1280x720
  - I see navigation links in the nav bar
  - set viewport to 375x667
  - navigation links are hidden
  - I see a "Menu" button
  - click "Menu"
expect:
  - navigation links are visible

---
scenario: Product cards do not overlap in grid layout
steps:
  - go to /dashboard
  - there are at least 3 cards in the grid
expect:
  - no cards overlap each other horizontally

---
scenario: Native dialog opens and closes with Escape
steps:
  - go to /settings
  - click "Delete Account"
  - a dialog appears
  - press Escape
expect:
  - the dialog is hidden

---
scenario: Mock API shows product data
mock:
  GET /api/products:
    products:
      - { id: 1, name: "Test Widget", price: 29.99 }
      - { id: 2, name: "Test Gadget", price: 49.99 }
steps:
  - go to /products
expect:
  - I see "Test Widget"
  - I see "$29.99"
  - I see "Test Gadget"

---
scenario: Offline mode shows error state
steps:
  - go to /dashboard
  - I see "Dashboard loaded"
  - go offline
  - click "Refresh"
expect:
  - I see an offline or network error message
  - go online
  - click "Retry"
  - I see "Dashboard loaded"

Assrt compiles each scenario block into the Playwright TypeScript shown in the preceding sections. When you specify browsers: [chromium, firefox, webkit] at the top level, Assrt generates tests that run across all three engines. It automatically adds engine-specific waits where needed (like the extra frame wait for WebKit after viewport changes), tolerance-based assertions for layout checks, and conditional skips for engine-specific API tests.

The compiled output includes the correct CI matrix configuration for your provider (GitHub Actions, GitLab CI, or CircleCI). When a browser engine update changes behavior, Assrt detects the failure, analyzes the engine-specific DOM difference, and opens a pull request with the updated implementation. Your scenario files remain unchanged because the test intent did not change; only the engine-specific implementation needed updating.

Start with the responsive layout scenario. Once it passes across all three engines in your CI pipeline, add the network interception scenario, then the viewport and device emulation tests, then the native dialog and form validation scenarios. Within a single afternoon, you will have comprehensive cross-browser coverage that catches the engine-specific regressions most teams only discover when users report bugs in production.

Related Guides

Ready to automate your testing?

Assrt discovers test scenarios, writes Playwright tests from plain English, and self-heals when your UI changes.

$npm install @assrt/sdk