Playwright & Web Automation Hub

Playwright architecture, selector reliability, and advanced interaction patterns.

Running Chromium vs Firefox vs WebKit in Playwright

Playwright ships its own builds of three rendering engines — Chromium (Blink), Firefox (Gecko), and WebKit (the engine behind Safari) — and drives all of them through one unified API. The promise is that a test written once runs on every engine, but the reality is that Blink, Gecko, and WebKit differ in font loading, cookie partitioning, content-security-policy enforcement, and layout timing. This page shows how to declare all three engines in a single config, run one engine or the full matrix on demand, and write tests that branch on browserName only where an engine genuinely behaves differently. It is the hands-on companion to Cross-Browser Execution, which sits under Playwright Setup & Core Architecture.

One spec running across three engines A single test spec is dispatched by the Playwright runner to three projects, Chromium, Firefox, and WebKit, each producing its own result. One spec test(...) Runner projects[] Chromium (Blink) CDP Firefox (Gecko) cookie partition WebKit (Safari) strict CSP
The runner dispatches the same spec to each configured project; the engines diverge in protocol, cookie handling, and CSP enforcement.

Root cause: cross-engine failures are environment divergence, not engine defects

When a test passes on Chromium but fails on WebKit, the instinct is to blame the engine. Almost always the real cause is that the test relies on behavior that only one engine happens to provide for free. WebKit does not always fire font-load events at the same point Blink does, so a screenshot taken too early renders with a fallback face. Firefox's Total Cookie Protection partitions cookies per top-level site, so a session restored from a cross-site cookie silently fails to authenticate. WebKit enforces content-security-policy more strictly, so an injected script that Chromium tolerates is blocked. The fix is never to special-case the engine everywhere; it is to declare every engine in the config, let the runner fan the spec out, and add a narrow browserName branch only at the exact point where the engines genuinely differ.

Minimal reproducible example

First, declare the three engines as projects so one command can cover all of them. The devices presets supply the right browserName, viewport, and user agent for each engine.

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true, // run files across the matrix concurrently
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
  ],
});

The spec below runs unchanged on all three engines, with a single guarded branch for WebKit's font timing. The browserName fixture tells the test which engine it is currently running under.

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

test('header renders consistently across engines', async ({ page, browserName }) => {
  await page.goto('https://example.com/dashboard');

  // Auto-waiting assertion works identically on every engine.
  const heading = page.getByRole('heading', { level: 1 });
  await expect(heading).toBeVisible();

  // WebKit may not have finished loading web fonts when the heading appears;
  // wait for fonts explicitly ONLY there so the other engines are not slowed.
  if (browserName === 'webkit') {
    await page.evaluate(() => document.fonts.ready);
  }

  await expect(heading).toContainText('Dashboard');
});

Step-by-step fix

  1. Declare every engine as a project. Add chromium, firefox, and webkit entries to the projects array in playwright.config.ts, spreading the matching devices preset into each use block so engine, viewport, and user agent stay consistent.
  2. Install the engine binaries. Run npx playwright install so Playwright's version-pinned builds of all three engines are present; the bundled Chromium is separate from a system-installed Chrome.
  3. Run one engine or the whole matrix. Use npx playwright test --project=webkit to isolate a single engine while debugging, and omit --project to run the full matrix in CI.
  4. Branch only where engines truly differ. Read the browserName fixture and add a narrow conditional — such as awaiting document.fonts.ready on WebKit — instead of writing three separate copies of the test.
  5. Inject session state explicitly for Firefox. Because Firefox partitions cookies, restore authentication with context.addCookies() or a storageState file rather than relying on cross-site cookie persistence.
  6. Cache binaries in CI. Cache the Playwright browser download path between runs to avoid re-downloading three engines on every job, and align worker count with available CPU to prevent out-of-memory kills on WebKit.

Troubleshooting variants

WebKit blocks an injected script or fails on a self-signed certificate

WebKit enforces content-security-policy more aggressively than Blink, so scripts that Chromium tolerates get rejected. Strip or rewrite the Content-Security-Policy header with page.route() for the page under test, and set ignoreHTTPSErrors: true in the context when a staging host serves a self-signed certificate. Both of these build directly on Network Interception Basics.

Firefox loses the logged-in session between navigations

Firefox's Total Cookie Protection partitions cookies by top-level site, which breaks any flow that depends on a cross-site cookie. Stop relying on the browser to carry the cookie across origins and instead seed the session deterministically with context.addCookies() or a saved storageState, ideally created once per worker as described in Setting Up Global Fixtures for Parallel Tests.

One engine is flaky in CI but green locally

Flakiness that appears only on one engine in CI is usually a synchronization gap, not an engine fault. Replace any fixed delay with an auto-waiting assertion, capture the failing run with --trace on, and inspect it engine by engine in the Playwright Trace Viewer. Reserve retries for genuinely transient network errors rather than papering over a missing wait.

Verification

Prove cross-engine stability three ways. First, run the matrix repeatedly (npx playwright test --repeat-each=5) and confirm every project stays green. Second, run each project in isolation with --project=firefox and --project=webkit so an engine-specific failure is not hidden by a passing Chromium run. Third, open a trace from a WebKit run and confirm the font-ready branch fired and the heading rendered with the intended typeface, not a fallback. Wiring this matrix into continuous integration is covered by CI/CD Integration.

Frequently Asked Questions

Does Playwright use my installed Chrome, Firefox, and Safari?

By default no. Playwright downloads its own version-pinned builds of Chromium, Firefox, and WebKit so results are reproducible across machines. You can target a system browser with the channel option, such as channel: 'chrome', but that requires the browser to be installed separately.

How do I run a single engine while debugging?

Pass the project name to the CLI, for example npx playwright test --project=webkit. Omitting the --project flag runs every project declared in the config, which is what you want in CI for full matrix coverage.

Why does the same test pass on Chromium but fail on WebKit?

It usually relies on behavior only one engine provides for free, such as font load timing, cookie partitioning, or relaxed content-security-policy. Add a narrow browserName branch at the exact point of divergence rather than rewriting the whole test.

Back to overview