Hydration Proof

Search documentation

Find a page or section

Test locales and timezones for hydration errors

Run every scenario in combinations of locales, timezones, themes, screens and browsers.

To test locales and timezones for hydration errors, list the values in the matrix option. hydration-proof runs each scenario in combinations of them, and of themes, viewports, browsers, network and CPU speeds, then says which value a finding depends on. Pairwise selection keeps the run small: every pair of values is tested at least once.

Why test more than one environment?

Most hydration bugs are environment bugs: a date formatted in the browser's timezone or locale, a theme read from prefers-color-scheme, or a layout that branches on screen size. The server renders one version and the visitor's browser another.

Without a matrix, hydration-proof tests one scenario named default. It uses this machine's locale and timezone and a light color scheme, so a server started on the same machine renders with the same settings. That is the one combination where these bugs never show up.

How to test locales and timezones for hydration errors

Add a matrix to the config. Pick values that differ in ways your code cares about:

hydration-proof.config.ts
import { defineConfig } from "hydration-proof";
 
export default defineConfig({
  matrix: {
    // A right-to-left locale, and timezones on both sides of UTC.
    locale: ["en-US", "de-DE", "ar-EG"],
    timezoneId: ["UTC", "Asia/Karachi", "America/Los_Angeles"],
    colorScheme: ["light", "dark"],
    viewport: ["desktop", "mobile"],
    browser: ["chromium", "firefox", "webkit"],
    // A returning visitor: scripts arrive in a different order.
    cache: ["cold", "warm"],
    // Chromium only; other browsers skip it with a note.
    cpu: [1, 4],
 
    // Anything the tool cannot guess. Each value is scenario settings.
    axes: {
      flags: {
        "new-checkout": { cookies: [{ name: "flag_checkout", value: "new" }] },
        "old-checkout": {},
      },
      currency: {
        eur: { localStorage: { currency: "EUR" } },
        jpy: { localStorage: { currency: "JPY" } },
      },
    },
 
    strategy: "pairwise",
    max: 16,
  },
});

Then run the tests as usual:

npx hydration-proof test

This config has 1,728 combinations. Pairwise testing covers every pair of values with about a dozen of them (11 here), because pairs of values are what usually breaks.

Which axes can you vary?

AxisValues
localeLocales such as 'de-DE'. Also sets Accept-Language
timezoneIdIANA timezones such as 'Asia/Karachi'
colorScheme'light', 'dark', 'no-preference'
reducedMotion'reduce', 'no-preference'
viewport'mobile', 'tablet', 'desktop', or { width, height }
browser'chromium', 'firefox', 'webkit'
network'fast' (no throttling), 'fast-3g', 'slow-3g', or { downloadKbps, uploadKbps, latencyMs }
cpuSlowdown factors such as [1, 4], where 1 is no slowdown (Chromium only)
cache'cold', and 'warm', which loads the page once before testing it
axesCustom axes: axis name, then value name, then scenario settings

cache: ['cold', 'warm'] is a cheap axis worth adding: a warm cache changes the order scripts arrive in, which changes when hydration starts.

Custom axes

Use axes for anything the tool cannot know: feature flags, tenants, a currency cookie, an A/B bucket. Each value applies scenario settings:

OptionTypeDefaultDescription
cookiesCookieConfig[]Cookies this axis value sets.
headersRecord<string, string>Request headers this axis value sets.
localStorageRecord<string, string>localStorage entries this axis value sets.
sessionStorageRecord<string, string>sessionStorage entries this axis value sets.
initScriptsstring[]Scripts this axis value runs before page scripts.
queryRecord<string, string>Query parameters this axis value adds to every URL.

Choose a strategy

strategyWhat it tests
'pairwise' (default)Every pair of values at least once
'full'Every combination. A matrix larger than max falls back to pairwise, with a note
'sample'A random subset of max environments, chosen from seed (default 1)

max (default 16) is the most environments per scenario. If pairwise needs more than that, the first max are tested and a note tells you to raise it for full pair coverage.

The first value of every axis is the baseline, and the combination of all baselines is always tested when the browsers can run it. Put the value most of your users have first.

matrix.scenarios limits the matrix to some scenarios; the others run as configured. The login of a scenario runs once and is shared by all of its environments (see scenarios and sign-in).

Which combinations are skipped?

Some combinations cannot run. They are left out with a note instead of failing the run:

  • CPU slowdown needs Chromium.
  • Firefox and WebKit have no network throttling. There, every request except the document is delayed by the latency instead, and a warm cache cannot be combined with a throttled network.
  • Firefox has no mobile emulation, so 'mobile' only sets the viewport size and touch.
  • Mocks turn the HTTP cache off, so 'warm' only repeats the visit.

Read the results

Each environment is a scenario named after the values it varies, such as guest (de-DE, Asia/Karachi, firefox). Axes with a single value are left out of the name, and custom axes appear as name=value (flags=new-checkout). --scenario guest selects every environment of guest.

When a finding appears in some environments of a route and not in others, the report names the values that separate them. The terminal lists these at the end of the run:

  Only in some environments
    HP1001 /checkout #total  Only found with locale de-DE.

With --mode both, the build is an axis too: "Only found with the production build." The HTML report shows the other environments of the same route next to each finding. To go from "only in de-DE" to a proven cause, add probes.

Turn the matrix off with --no-matrix

A full matrix is slower than you want for a quick local check. --no-matrix tests the scenarios as configured:

npx hydration-proof test --no-matrix

Find flaky findings with --repeat

A finding in one environment out of twelve is usually a real bug that needs that environment, not flakiness. --repeat tells the two apart: it loads every page several times.

npx hydration-proof test --repeat 3

Findings that appear in only some runs are marked flaky ("Seen in 2 of 3 runs of this page.") and still count, because an intermittent mismatch is a real bug. Each page gets a flakiness score: the share of runs whose findings differ from the most common result. The same option is repeat: 3 in the config.

Options

OptionTypeDefaultDescription
localestring[]Locales to test, e.g. ['en-US', 'de-DE', 'ar-EG'].
timezoneIdstring[]Timezones to test, e.g. ['UTC', 'Asia/Karachi', 'America/Los_Angeles'].
colorSchemeColorScheme[]Color schemes to test, e.g. ['light', 'dark'].
reducedMotion("reduce" | "no-preference")[]Reduced-motion settings to test.
viewportViewportOption[]Viewports to test: sizes or the presets mobile, tablet, desktop.
browserBrowserName[]Browsers to test: chromium, firefox, webkit.
networkNetworkProfile[]Network profiles to test: fast (no throttling), fast-3g, slow-3g or custom.
cpunumber[]CPU slowdown factors (Chromium only); 1 is no slowdown.
cacheCacheState[]Cache states to test: cold and warm.
axesRecord<string, Record<string, ScenarioVariant>>Custom axes: axis name → value name → scenario settings. { flags: { 'new-checkout': { cookies: [{ name: 'flag', value: 'on' }] }, 'old-checkout': {} } }
strategy"pairwise" | "full" | "sample"pairwise (default) covers every pair of values, full every combination, sample a random subset.
maxnumber16Most environments per scenario.
seednumber1Seed for sample.
scenariosstring[]allScenarios the matrix applies to.