ScreenshotNeo

BlogHow-to

How to Prevent Failed Web Fonts from Changing Visual Regression Screenshots

Wait for used fonts, check required faces for failures, and make the test’s font policy explicit before capturing a visual regression screenshot.

By the ScreenshotNeo team4 October 20269 min read

To prevent failed web fonts from silently changing a visual regression screenshot, wait for the page’s used fonts to settle, inspect the font faces the test requires, and assert an explicit policy before capture. If the intended custom font is mandatory, fail the test when it is missing or failed. If a fallback is acceptable, make that fallback deterministic and compare the fallback rendering consistently. Waiting for network idle alone does not prove that the intended font rendered.

This guide uses Playwright with JavaScript. It covers the font readiness signals, a runnable test pattern, fallback policies, diagnosis, and reproducibility. See the CSS Font Loading API and MDN’s Document.fonts reference for the browser APIs.

1. Understand what font-ready means

document.fonts exposes the document’s FontFaceSet. Each face can be unloaded, loading, loaded, or failed. The set’s ready promise fulfills after loading and layout operations for fonts used by the document have completed.

That promise is a readiness signal, not an assertion that every declared font loaded. A face declared in CSS may not be used by the current page state, and the used-font set can differ from all declared faces—for example, with font-display: optional. Therefore, await readiness and separately check the faces or rendering policy that matter to the test.

Font loading can also affect what appears during a transient capture. Google’s technical notes describe browser-dependent behavior while web fonts load: Chrome and Safari may temporarily show blank space for text using an unloaded font, while Firefox may show a default font first and later render again. Treat these as documented examples, not guarantees for every current browser version. See Google Fonts technical considerations.

2. Choose a font policy for the test

Policy Use it when What to assert
Custom font required The baseline represents a branded or otherwise essential typeface. The required family and relevant weight/style loaded. Fail with diagnostics if they did not.
Fallback accepted The page is allowed to render without the remote or custom face. The fallback is explicitly selected and available in the test environment; capture and compare that known rendering.

Do not turn every font error into success or suppress failures indiscriminately. That can hide a real asset regression. Likewise, aborting font requests changes the rendered page, so it is not a neutral way to stabilize a baseline.

3. Run a Playwright test that checks font state

The following example is a Node.js ES module test. It navigates to a page, waits for the relevant content, bounds the font readiness wait, checks the expected font faces, and captures only if the required-font policy passes.

Install Playwright and its Chromium browser in the project, then save this as font-visual.spec.js. Run it with npx playwright test font-visual.spec.js. Set TEST_URL to the page under test and adjust the family, weight, style, and selector to match the application.

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

const url = process.env.TEST_URL ?? 'http://127.0.0.1:3000';
const requiredFamily = 'Brand Sans';
const requiredWeight = '400';
const requiredStyle = 'normal';

test('captures only after the required font is loaded', async ({ page, browserName }) => {
  const fontFailures = [];
  page.on('requestfailed', request => {
    if (/\.(woff2?|ttf|otf)(\?|$)/i.test(request.url())) {
      fontFailures.push({ url: request.url(), error: request.failure()?.errorText });
    }
  });

  await page.goto(url, { waitUntil: 'domcontentloaded' });
  await page.locator('[data-testid="headline"]').waitFor({ state: 'visible' });

  // Make sure the text and face under test are actually used by this page state.
  const isAvailable = await page.evaluate(async ({ family, weight, style }) => {
    return document.fonts.check(`${style} ${weight} 16px "${family}"`, 'Visual regression sample');
  }, { family: requiredFamily, weight: requiredWeight, style: requiredStyle });
  expect(isAvailable, `The required face ${requiredFamily} ${requiredWeight} ${requiredStyle} is not available`).toBe(true);

  // Bound the wait so a stuck font load produces diagnostics instead of hanging indefinitely.
  const ready = await Promise.race([
    page.evaluate(() => document.fonts.ready.then(() => 'ready')),
    page.waitForTimeout(10000).then(() => 'timeout'),
  ]);
  const fontState = await page.evaluate(() => ({
    status: document.fonts.status,
    faces: [...document.fonts].map(face => ({
      family: face.family,
      weight: face.weight,
      style: face.style,
      status: face.status,
    })),
  }));

  expect(ready, `Font readiness timed out. Browser: ${browserName}; state: ${JSON.stringify(fontState)}; failed requests: ${JSON.stringify(fontFailures)}`).toBe('ready');
  expect(fontState.faces.filter(face => face.family.replaceAll('"', '') === requiredFamily)
    .some(face => face.weight === requiredWeight && face.style === requiredStyle && face.status === 'loaded'),
    `Required face did not reach loaded state. Browser: ${browserName}; state: ${JSON.stringify(fontState)}; failed requests: ${JSON.stringify(fontFailures)}`)
    .toBe(true);

  await expect(page).toHaveScreenshot('headline.png', { fullPage: true });
});

Important: document.fonts.check() answers whether the requested font shorthand can be used without pending loads for the supplied text; it should not be treated by itself as proof that the intended branded file is the face actually rendered. The example also checks the face set and request failures. Adapt the check to the app’s actual font declarations and verify the behavior in the supported browser. The family string in a FontFace can include quotes, so normalize or compare it according to the page’s declarations.

The readiness timeout above uses Playwright’s timeout utility. A timed-out Promise.race does not cancel the underlying document.fonts.ready promise; it simply lets the test report a bounded failure. The test reports browser name, face state, and failed font requests to make the failure actionable.

4. Make fallback rendering deterministic

If the product accepts a fallback, do not let the browser choose an incidental substitute that varies across machines. Define the intended fallback in CSS and ensure it is available in the test environment. For example:

/* Example policy: use a bundled local fallback for the test route. */
.visual-test {
  font-family: "Test Sans", sans-serif;
}

@font-face {
  font-family: "Test Sans";
  src: url("/test-assets/test-sans.woff2") format("woff2");
  font-style: normal;
  font-weight: 400;
  font-display: swap;
}

In a fallback-policy test, assert that the expected fallback face is loaded and capture that state. If you intentionally test failure behavior, create a controlled fixture where the custom font is unavailable, then assert the fallback result. Avoid making production font requests fail by intercepting or blocking them in a test that is intended to represent normal delivery.

5. Control both font delivery requests

When using Google Fonts, the browser first requests generated CSS and then downloads the corresponding font resource selected for its user agent. A diagnosis may need to inspect both requests. A successful stylesheet response does not establish that the font file loaded successfully.

  1. Record failed requests for CSS and font file extensions such as .woff and .woff2.
  2. Inspect the browser console and network log for the CSS response, the font-file response, CORS errors, and blocked or timed-out requests.
  3. Check that the requested family, style, and weight are declared and match the text being captured.
  4. For repeatable baselines, pin the browser/runtime and keep the operating system, installed fonts, and font assets consistent between baseline creation and comparison.

The final environment controls are reproducibility recommendations based on the browser-dependent delivery path and rendering behavior documented above; they are not a guarantee that all font differences disappear.

6. Why network idle is not enough

Playwright defines networkidle as no network connections for at least 500 ms and discourages using it as a testing readiness condition, recommending assertions about the page instead. A quiet network does not say whether a required face loaded, whether the current page uses it, or whether a fallback appeared. Use the application’s real readiness condition, then check font state and the test’s chosen policy. See the Playwright Page API.

7. Troubleshooting

Symptom Likely cause Fix
Screenshot differs only in text width or wrapping The intended face or weight failed, or a different fallback rendered. Inspect the specific family, weight, and style; check both stylesheet and font-file requests; assert the required face before capture.
document.fonts.ready resolves but the branded font is missing Readiness covers loading and layout for used fonts; it is not a guarantee that every declared face loaded. Check required faces and the intended rendering policy separately. Ensure the page state actually uses the text and face being tested.
Font status remains loading A request is stalled, a page state is still triggering font use, or the browser is waiting on a font operation. Use a bounded wait; report face state and failed requests; inspect the font file request and reproduce with the project’s exact browser/runtime.
Font face is failed The file request may have failed or been rejected, or its declaration/resource may be invalid. Inspect the request and console details, verify the URL, response and font declaration, then either repair delivery or explicitly test an accepted fallback.
Works locally but differs in CI Browser/runtime, operating system, installed fonts, or delivered font assets differ. Pin the browser/runtime, provide controlled assets where practical, and use the same environment for baselines and comparisons.
Google Fonts CSS loads, but text uses fallback The later font-file request failed, was blocked, or did not match the face used by the page. Inspect the font-file request separately from the stylesheet, including response and CORS diagnostics.
Playwright screenshot preparation hangs while waiting for fonts A version- and environment-specific hang has been reported for Linux WebKit and Playwright 1.63.0. Check the issue and verify against the exact browser/runtime in use. Do not assume skipping the screenshot font wait repairs font state.

A Playwright issue opened September 29, 2026 reports a Linux WebKit screenshot timeout while waiting for fonts in Playwright 1.63.0, with a successful 1.60.0 control in the reporter’s setup. The report had not isolated a minimal cause or established the first affected version. Treat it as a specific issue report, not a general Playwright defect or a confirmed fix; check the issue for updates before choosing a workaround.

8. Performance, reliability, and cost

  • Performance: Waiting for required fonts adds time when those fonts are still loading. A bounded wait makes stalls visible and keeps the failure report useful; it does not make a slow font service faster.
  • Reliability: Control the browser/runtime and font assets used for both baseline and comparison. A deterministic fallback policy is often more reproducible than relying on whichever fonts happen to be installed.
  • Cost: This approach uses your existing browser test setup. The dossier provides no benchmark or price comparison for browser testing tools, so none is asserted here. If you use a screenshot API for captures, check its billing and failure semantics before relying on it for repeated regression runs.

9. Or skip the browser setup

For a one-call capture, ScreenshotNeo is a website screenshot API and MCP server for developers. Its API returns PNG, JPEG, WebP, or PDF, and supports full-page screenshots and browser options. See the ScreenshotNeo API documentation for parameters and configuration. A basic request is:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and the response identifies page verdict and billing status in headers. An MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.

Create a free ScreenshotNeo account to get 1,000 screenshots a month with no card.

10. FAQ

Does document.fonts.ready reject when a font fails?

It fulfills when loading and layout operations for used fonts have completed. Check the faces and required rendering policy separately to detect failure.

Should I wait for load, domcontentloaded, or networkidle?

Use the navigation condition that suits the application, then wait for the relevant page state and inspect fonts. None of those navigation states alone proves the required font rendered.

Should visual tests block all external font requests?

Only when the test intentionally represents a blocked-font scenario. Blocking changes the page’s appearance and invalidates a baseline meant to represent normal font delivery.

Can this guarantee identical text rendering on every machine?

No. Pinning the browser and controlling fonts reduces environmental variation, but the test should still define and assert the rendering policy it needs.