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:
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 testThis 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?
| Axis | Values |
|---|---|
locale | Locales such as 'de-DE'. Also sets Accept-Language |
timezoneId | IANA 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 } |
cpu | Slowdown factors such as [1, 4], where 1 is no slowdown (Chromium only) |
cache | 'cold', and 'warm', which loads the page once before testing it |
axes | Custom 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:
| Option | Type | Default | Description |
|---|---|---|---|
| cookies | CookieConfig[] | — | Cookies this axis value sets. |
| headers | Record<string, string> | — | Request headers this axis value sets. |
| localStorage | Record<string, string> | — | localStorage entries this axis value sets. |
| sessionStorage | Record<string, string> | — | sessionStorage entries this axis value sets. |
| initScripts | string[] | — | Scripts this axis value runs before page scripts. |
| query | Record<string, string> | — | Query parameters this axis value adds to every URL. |
Choose a strategy
strategy | What 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-matrixFind 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 3Findings 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
| Option | Type | Default | Description |
|---|---|---|---|
| locale | string[] | — | Locales to test, e.g. ['en-US', 'de-DE', 'ar-EG']. |
| timezoneId | string[] | — | Timezones to test, e.g. ['UTC', 'Asia/Karachi', 'America/Los_Angeles']. |
| colorScheme | ColorScheme[] | — | Color schemes to test, e.g. ['light', 'dark']. |
| reducedMotion | ("reduce" | "no-preference")[] | — | Reduced-motion settings to test. |
| viewport | ViewportOption[] | — | Viewports to test: sizes or the presets mobile, tablet, desktop. |
| browser | BrowserName[] | — | Browsers to test: chromium, firefox, webkit. |
| network | NetworkProfile[] | — | Network profiles to test: fast (no throttling), fast-3g, slow-3g or custom. |
| cpu | number[] | — | CPU slowdown factors (Chromium only); 1 is no slowdown. |
| cache | CacheState[] | — | Cache states to test: cold and warm. |
| axes | Record<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. |
| max | number | 16 | Most environments per scenario. |
| seed | number | 1 | Seed for sample. |
| scenarios | string[] | all | Scenarios the matrix applies to. |
Related
- Scenarios and sign-in: the environments the matrix expands
- Prove which factor causes a finding
- Timezone hydration mismatches
- Locale-dependent formatting
- The
matrixoption in the configuration reference