## TL;DR
Screenshots race webfont loading: the screenshot fires before the font arrives, so text renders in a fallback font and the pixel diff flakes. Locally the font is usually cached, which is why CI is where this appears. Wait for document.fonts.ready, or for document.fonts.status to equal loaded, before taking the screenshot, self-host the fonts so CI does not depend on a font CDN, and mask font-sensitive regions only as a last resort.

## The query
```text
playwright fonts not loaded before screenshot flake in CI  -  how to wait for fonts
```

## Use this when
- screenshot specs flake in CI with text in the wrong typeface
- the same specs are stable locally where fonts are cached
- the diff shows glyph-level differences, not layout shifts

## Not for
- every screenshot differs from the baseline consistently (regenerate the baseline; the font changed deliberately)
- layout shifts or animations cause the diff (stabilize those instead)
- the font never loads even locally (the font URL is broken; fix the asset)

## Steps

### 1. Confirm the flake is font-related
Compare a failing screenshot against a passing one. Fallback-font rendering, slightly different glyph shapes and spacing, is unmistakable once you look.
```bash
# inspect the last two screenshots in the test-results directory
ls test-results/ | tail -5
```
Expected output: the diff shows text in a different typeface rather than a layout or content change.

### 2. Wait for fonts before the screenshot
Block the test until the document's fonts are loaded. This is the direct fix and it belongs in the test, not the config.
```js
await page.goto('/pricing');
await page.evaluate(() => document.fonts.ready);
await expect(page).toHaveScreenshot('pricing.png');
```
Expected output: screenshots are taken with the real fonts and the flake disappears.

### 3. Self-host the fonts so CI does not depend on a CDN
A font CDN that is slow or blocked in CI reintroduces the race. Serving the font files from the app itself removes the network variable.
```css
/* serve from your own static assets */
@font-face {
  font-family: 'AppFont';
  src: url('/fonts/appfont.woff2') format('woff2');
  font-display: block;
}
```
Expected output: font loading is deterministic in CI with no third-party request in the path.

### 4. Mask font-sensitive regions as a last resort
If a third-party widget's fonts cannot be controlled, mask that region so its rendering cannot fail the comparison.
```js
await expect(page).toHaveScreenshot('pricing.png', {
  mask: [page.getByTestId('third-party-widget')],
});
```
Expected output: the uncontrollable region is excluded and the rest of the page is still compared.

## Tool compatibility
- Playwright 1.40 - 1.55
- any browser with webfont support

## Variant phrasings
- fonts not loaded before screenshot flake in CI
- playwright screenshot font fallback flake
- document.fonts.ready before toHaveScreenshot

## Why it happens
Webfonts load asynchronously after the document is otherwise ready, and Playwright's auto-waiting does not cover font loading: waitForLoadState('networkidle') can even fire before a late font request starts. CI's cold cache and slower network stretch the gap between first paint and font arrival, so the screenshot lands in the fallback-font window intermittently.

## Edge cases / pitfalls
- document.fonts.ready resolves when initial fonts load, but late-added weights can still arrive after; wait for the specific weights if needed
- icon fonts that load lazily cause the same flake on icon-heavy pages; include them in the wait
- some CI environments block external font CDNs entirely, which turns the flake into a permanent fallback; self-hosting fixes both
- font-display: swap is good for users but bad for screenshots; block is the safer value on test pages

## Provenance

Resolved from the public thread: https://vectle.com/posts/pst_kg9TcD9C4z0bLEB8H01c5A
