Browser Compatibility Testing Matrix: What to Test and Why

Browser Compatibility Testing Matrix: What to Test and Why

Most browser compatibility guides tell you to test on Chrome, Firefox, Safari, and Edge. That's a reasonable starting point, but it's not a strategy. A well-designed browser test matrix is based on your actual user analytics, the CSS and JavaScript features your app depends on, and the risk of breaking specific browser/OS combinations.

This guide covers how to build a realistic, maintainable browser compatibility matrix and automate testing against it.

Start With Real Usage Data

Before defining your matrix, pull browser distribution data from your analytics. The numbers vary dramatically by audience:

  • Developer tools / B2B SaaS: Chrome 60-70%, Firefox 15%, Safari 10%, Edge 10%. Firefox matters more than average; IE is irrelevant.
  • Consumer apps (US/EU): Safari/iOS 35-45%, Chrome 40-50%, Chrome/Android 20-30%. Safari matters enormously.
  • Enterprise software: Edge 25-30%, Chrome 45%, older browser versions still in use.
  • Southeast Asia / emerging markets: Samsung Internet 15-20%, older Chrome versions on Android common.

Your real data is the only correct answer. Check Google Analytics, Plausible, or whatever you use. Export browser distribution for the last 90 days and sort by usage percentage.

Building Your Matrix

A practical browser matrix has three tiers:

Tier 1: Must Pass (run on every PR)

  • Browsers representing >10% of your traffic
  • Your primary development browser
  • The latest stable version

Tier 2: Should Pass (run nightly or pre-release)

  • Browsers representing 2-10% of your traffic
  • N-1 versions of Tier 1 browsers (previous major version)

Tier 3: Nice to Have (run quarterly or on demand)

  • Browsers representing <2% of your traffic
  • Legacy versions you're considering dropping
  • Niche browsers you've received specific bug reports about

Example matrix for a typical B2B SaaS:

Browser Version OS Tier % Traffic
Chrome Latest Windows 11 1 45%
Safari Latest macOS 1 15%
Chrome Latest macOS 1 12%
Edge Latest Windows 11 1 10%
Firefox Latest Windows 2 8%
Chrome Latest Android 2 5%
Safari iOS 16/17 iPhone 2 4%
Chrome N-1 Windows 3 1%

Automating the Matrix with Playwright

Configure Playwright projects to match your matrix:

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

export default defineConfig({
  projects: [
    // Tier 1 — run on every PR
    {
      name: 'Chrome-Windows',
      use: {
        ...devices['Desktop Chrome'],
        channel: 'chrome', // Use system Chrome, not bundled Chromium
      },
      testMatch: ['**/*.spec.ts'],
    },
    {
      name: 'Safari-macOS',
      use: { ...devices['Desktop Safari'] },
      testMatch: ['**/*.spec.ts'],
    },
    {
      name: 'Edge-Windows',
      use: {
        ...devices['Desktop Edge'],
        channel: 'msedge',
      },
      testMatch: ['**/*.spec.ts'],
    },
    
    // Tier 2 — nightly
    {
      name: 'Firefox-Windows',
      use: { ...devices['Desktop Firefox'] },
      testMatch: ['**/*.spec.ts'],
    },
    {
      name: 'Chrome-Android',
      use: { ...devices['Pixel 7'] },
      testMatch: ['**/*.spec.ts'],
    },
    {
      name: 'Safari-iOS',
      use: { ...devices['iPhone 14'] },
      testMatch: ['**/*.spec.ts'],
    },
  ],
});

Run tiers separately:

# Tier 1 (PR checks)
npx playwright <span class="hljs-built_in">test --project=Chrome-Windows --project=Safari-macOS --project=Edge-Windows

<span class="hljs-comment"># Tier 1 + 2 (nightly)
npx playwright <span class="hljs-built_in">test

Feature-Based Compatibility Testing

Beyond running your full test suite across browsers, add specific tests for browser compatibility of features you use:

// tests/compat/css-features.spec.ts
test.describe('CSS feature compatibility', () => {
  test('CSS Grid renders layout correctly', async ({ page }) => {
    await page.goto('/dashboard');
    
    const gridContainer = page.locator('[data-testid="metrics-grid"]');
    await expect(gridContainer).toBeVisible();
    
    // Verify grid layout actually applied
    const gridStyle = await gridContainer.evaluate(el => 
      window.getComputedStyle(el).display
    );
    expect(gridStyle).toBe('grid');
    
    // Check columns are rendered (4-column layout)
    const gridColumns = await gridContainer.evaluate(el =>
      window.getComputedStyle(el).gridTemplateColumns
    );
    expect(gridColumns.split(' ')).toHaveLength(4);
  });
  
  test('CSS custom properties (variables) work', async ({ page }) => {
    await page.goto('/');
    
    const primaryButton = page.locator('[data-testid="cta-button"]');
    const backgroundColor = await primaryButton.evaluate(el =>
      window.getComputedStyle(el).backgroundColor
    );
    
    // Should not be empty or inherit — variable should have resolved
    expect(backgroundColor).not.toBe('rgba(0, 0, 0, 0)');
    expect(backgroundColor).not.toBe('');
  });
  
  test('CSS container queries work', async ({ page }) => {
    // Only run this test if your app uses container queries
    await page.goto('/');
    
    // Resize to trigger container query
    await page.setViewportSize({ width: 800, height: 600 });
    
    const card = page.locator('[data-testid="responsive-card"]');
    const layout = await card.evaluate(el => el.getAttribute('data-layout'));
    expect(layout).toBe('compact'); // Container query should set data-layout
  });
});
// tests/compat/js-features.spec.ts
test.describe('JavaScript API compatibility', () => {
  test('Intersection Observer works', async ({ page }) => {
    await page.goto('/features');
    
    // Scroll to trigger lazy loading
    await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
    await page.waitForTimeout(500);
    
    // Lazy-loaded images should now be visible
    const lazyImages = await page.locator('img[loading="lazy"]').all();
    for (const img of lazyImages.slice(0, 3)) {
      await expect(img).toBeVisible();
    }
  });
  
  test('Clipboard API write works (with permission)', async ({ page, context }) => {
    await context.grantPermissions(['clipboard-read', 'clipboard-write']);
    await page.goto('/docs/api-reference');
    
    await page.click('[data-testid="copy-code-button"]');
    
    const clipboardContent = await page.evaluate(() => navigator.clipboard.readText());
    expect(clipboardContent.length).toBeGreaterThan(0);
  });
  
  test('Web Share API is handled when unavailable', async ({ page }) => {
    // WebKit on desktop doesn't support Web Share — verify graceful fallback
    await page.goto('/blog/post/1');
    
    // Share button should exist
    const shareButton = page.locator('[data-testid="share-button"]');
    await expect(shareButton).toBeVisible();
    
    // Click it — should either open Web Share or show fallback
    await shareButton.click();
    
    // Either share dialog or fallback copy-link modal should appear
    const shareDialog = page.locator('[data-testid="share-dialog"], [data-testid="copy-link-modal"]');
    await expect(shareDialog).toBeVisible();
  });
});

Handling Known Browser Differences

Some behaviors legitimately differ between browsers. Handle them with conditional assertions rather than separate tests:

test('date input formats correctly', async ({ page, browserName }) => {
  await page.goto('/events/create');
  
  const dateInput = page.locator('[data-testid="event-date"]');
  await dateInput.fill('2024-12-25');
  
  const inputValue = await dateInput.inputValue();
  
  // Firefox and WebKit may format dates differently
  if (browserName === 'webkit') {
    // Safari may format as 12/25/2024 on US locale
    expect(inputValue).toMatch(/12\/25\/2024|2024-12-25/);
  } else {
    expect(inputValue).toBe('2024-12-25');
  }
});

Maintaining the Matrix Over Time

Browser compatibility matrices require maintenance as browser usage changes and new browser versions ship:

Quarterly review tasks:

  1. Pull updated browser analytics — have any browsers crossed the 10% threshold (tier 1) or dropped below 2% (tier 3)?
  2. Update browser version targets in your Playwright config
  3. Remove browsers from the matrix that have dropped off entirely
  4. Check for new CSS/JS features you're using that have compatibility implications

When to drop a browser:

  • Traffic drops below 1% of your audience
  • The browser vendor has ended support (IE 11 support ended June 2022)
  • Supporting it requires maintaining incompatible code paths that slow development

When to add a browser:

  • Traffic exceeds 5% (add to Tier 2)
  • You receive multiple bug reports about a specific browser
  • You're entering a new market where a specific browser is dominant

Caniuse Integration

For CSS/JS features, use caniuse.com data to know before coding whether a feature needs a fallback:

// CI check — flag if any CSS features are used that don't support your matrix
// This can be integrated as an eslint plugin or build step:

// browserslist (.browserslistrc):
// > 1% in US
// last 2 Chrome versions
// last 2 Firefox versions  
// last 2 Safari versions
// last 2 Edge versions

// Then use postcss-preset-env or babel to auto-add prefixes/fallbacks
// based on your actual browser targets

Automating Cross-Browser Failure Analysis

When cross-browser tests fail, you need to know quickly which browsers are affected and whether it's a real bug or flakiness:

// Parse test results to show browser-specific failures
// playwright-report/index.html groups by project (browser) automatically

// CLI command to see failures by browser
npx playwright test --reporter=list | grep FAILED

For systematic analysis, use Playwright's JSON reporter and parse the results:

npx playwright test --reporter=json > results.json
<span class="hljs-built_in">cat results.json <span class="hljs-pipe">| jq <span class="hljs-string">'
  .suites[].suites[] | 
  select(.specs[].tests[].results[].status == "failed") <span class="hljs-pipe">|
  {browser: .title, test: .specs[].title}
'

The Minimum Viable Browser Matrix

If you're just getting started with cross-browser testing and need a starting point before you have analytics data:

Browser Why
Chrome (latest) ~65% of global web traffic
Safari/WebKit ~19% globally, renders differently, iOS requirement
Firefox (latest) ~4% globally, Gecko engine reveals real compatibility issues

These three cover Chromium (Chrome/Edge/Opera), WebKit (Safari/iOS), and Gecko (Firefox) — the three distinct browser engines. Testing all three catches engine-level compatibility issues. Edge and Chrome share the Chromium engine, so an additional Edge project is only necessary if you have significant Edge traffic or need to test Edge-specific features.

A well-defined, analytics-driven browser matrix is the difference between cross-browser testing that's comprehensive and cross-browser testing that's expensive theater. Test the browsers your users actually use, at a depth proportional to that usage, and automate the matrix maintenance so it stays current as browser usage evolves.

Read more