How to Fix Playwright Product Screenshots with Missing Currency Symbols
Diagnose missing currency symbols in Playwright screenshots by checking page text, font coverage, loading, and environment differences—then apply the right fix.
If a price such as $19.99, €19.99, or £19.99 loses its currency symbol in a Playwright screenshot, first find out where it disappears: is it missing from the page text, missing in the browser rendering before capture, or present in the browser but absent from the captured image? Each points to a different fix. Screenshot timing options can stabilize a capture, but they cannot add a glyph that is absent from the page data or its available fonts.
This guide follows that diagnostic path, with runnable Playwright examples and checks you can use in local and CI environments. No single cause can be identified without the page, font stack, browser, and runtime details.
1. Check whether the currency symbol is in the page text
Inspect both textContent and innerText for the exact price element. textContent reads the text nodes; innerText reflects rendered text and visibility. If the symbol is missing from both, investigate the application before changing screenshot settings.
import { test, expect } from '@playwright/test';
test('product price contains its currency symbol', async ({ page }) => {
await page.goto('http://localhost:3000/products/widget');
const price = page.locator('[data-testid="product-price"]');
await expect(price).toBeVisible();
const textContent = await price.evaluate(el => el.textContent);
const innerText = await price.innerText();
console.log({ textContent, innerText });
expect(textContent).toContain('€');
});
Replace the URL, selector, and expected symbol with those used by your app. If this check fails, trace the value from the product data through localization/number formatting to the price template. Check whether the currency code and locale are correct, and whether the template intentionally places the symbol in a separate element or uses an icon or pseudo-element instead of a text character.
If the symbol is in the DOM text but the screenshot shows an empty gap, box, or replacement character, move to font rendering. If the browser looks correct but the screenshot does not, compare the exact capture state and runtime next.
2. Verify the browser rendering and font coverage
Open the page in the same browser engine and runtime environment used for the failing test. Check the price before taking a screenshot. A blank space or missing-glyph box at this point points toward the selected font or its fallback, rather than screenshot encoding.
Inspect the computed font stack and the fonts the browser knows about:
const price = page.locator('[data-testid="product-price"]');
const fontInfo = await price.evaluate(el => {
const style = getComputedStyle(el);
return {
fontFamily: style.fontFamily,
fontWeight: style.fontWeight,
fontStyle: style.fontStyle,
text: el.textContent,
fontsStatus: document.fonts.status,
euroAvailable: document.fonts.check('16px ' + style.fontFamily, '€'),
poundAvailable: document.fonts.check('16px ' + style.fontFamily, '£'),
dollarAvailable: document.fonts.check('16px ' + style.fontFamily, '$'),
};
});
console.log(fontInfo);
document.fonts.check() is a diagnostic signal, not proof that a particular font file contains the glyph: fallback fonts may participate, and a comma-separated family stack is not a direct report of the exact font used for each character. Inspect the actual font requests in browser developer tools or the Playwright trace/network log, then verify the required glyph in the intended font asset. Do not assume every typeface covers every currency symbol.
Check CSS rules for the price and nested spans, including font family, weight, style, and any selector that applies a different font only to the symbol. Also check whether the font file request succeeds in the test environment and whether the correct weight file is served. If the font has no required glyph, choose a font with coverage or provide an appropriate fallback stack.
3. Wait for required fonts before capturing
A screenshot taken while the page is still loading can capture a fallback font or incomplete page state. Wait for the page’s relevant application state and font loading before capturing. This does not repair a missing glyph in every available font; it makes the capture wait for assets that should load.
import { test, expect } from '@playwright/test';
test('product price screenshot after fonts load', async ({ page }) => {
await page.goto('http://localhost:3000/products/widget', {
waitUntil: 'networkidle',
});
const price = page.locator('[data-testid="product-price"]');
await expect(price).toBeVisible();
await page.evaluate(async () => {
await document.fonts.ready;
});
await expect(price).toHaveScreenshot('product-price.png');
});
networkidle can be unsuitable for pages with persistent network activity. In that case, wait for a specific application-ready selector or state and then await document.fonts.ready. If a font request fails, waiting cannot make it succeed; fix the serving, URL, access, or environment issue. Avoid blocking all font requests as a generic workaround, since that can force fallback fonts and alter the rendering.
Playwright’s screenshot assertion waits for two consecutive page screenshots to match before comparison, which can help with a changing page. That stability check does not guarantee that the right font loaded or that the chosen font supports the character. See the Playwright visual comparisons documentation.
4. Compare local and CI environments
When a screenshot differs between a developer machine and CI, record the browser engine and version, Playwright version, operating system or container image, locale, font files, and relevant font network responses. Compare them alongside the three observations: whether the DOM contains the symbol, whether the browser renders it before capture, and whether the screenshot preserves it.
Playwright documents that visual results can vary across browsers and platforms, including from rendering and fonts. Keep screenshot baselines tied to the same browser and operating system environment that generates them. If your project tests multiple engines, investigate each independently instead of assuming one font/rendering result applies to all.
5. Use screenshot settings only for capture-state problems
Playwright’s screenshot options control capture behavior and appearance. For example, you can set a timeout, choose scale, or inject a stylesheet. These options can help when capture timing or a deliberate visual override is the demonstrated problem; they do not supply a missing currency glyph.
await page.screenshot({
path: 'product.png',
fullPage: true,
timeout: 30_000,
scale: 'css',
style: '[data-testid="debug-overlay"] { display: none !important; }',
});
Remove options you do not need. fullPage is for capturing the full page, scale controls output scaling, and style injects CSS for the capture. Avoid using injected CSS to conceal a glyph problem: first establish which font rendered the symbol and why. Refer to the Page screenshot API for the current option definitions.
6. Narrowly evaluate reported font workarounds
A Chromium issue discussion describes a case where fonts were requested again around screenshot capture. The reporter later said their workaround involved local font files, installing them in CI, and clearing the system font cache. This is a report about one case, not a universal Playwright fix. See Playwright issue #29968.
If your symptoms match, create a minimal reproduction with your actual font and capture flow, then test the reported steps individually. Confirm whether the font request, installed font, or cache is the variable. Do not apply the workaround blindly to unrelated missing symbols.
7. Troubleshooting checklist
| Symptom | Likely area to inspect | Next action |
|---|---|---|
Symbol absent from both textContent and innerText |
Application data, locale, formatting, template | Trace the price value and formatting code; confirm the expected currency and locale. |
| DOM has the character; browser shows a box or blank | Selected font, glyph coverage, font fallback | Inspect computed styles and loaded font files; verify that a chosen font covers the character. |
| Local looks right; CI is wrong | Different OS/container fonts, browser version, locale, or failed font request | Compare environment details and network responses; align the screenshot runtime and required assets. |
| Only the screenshot differs from the pre-capture browser view | Capture state, timing, or a capture-specific style | Wait for application readiness and fonts, then compare the same element immediately before capture. |
| Screenshot assertion is flaky while content changes | Page has not reached a stable visual state | Wait for a specific ready condition; use screenshot assertion stabilization while remembering it cannot fix font coverage. |
| Capture times out while waiting for fonts | Font loading is stalled or unavailable, or capture is waiting on pending font work | Inspect font requests and reproduce with a minimal page. A reported timeout symptom appears in issue #29417; it does not establish a universal cause. |
8. Performance, reliability, and cost considerations
Waiting for fonts and application readiness makes a capture more representative, but waiting for all network activity can add delay or never complete on pages with persistent requests. Prefer the narrowest reliable readiness condition for your page, then wait for the specific font and element state that matters. Keep the browser, operating system, locale, and font assets consistent for visual baselines to reduce environment-driven differences.
For CI reliability, make font assets available through the same supported route each run and inspect failed requests rather than adding arbitrary delays. A fixed sleep can waste time and still miss a slow or failed font request. The research sources provide no general benchmark or universal timing threshold for this particular issue; choose timeouts based on your application’s behavior.
With a self-hosted Playwright suite, the practical cost is the compute and maintenance needed to run a stable browser environment and supply its fonts. There is no universal cost figure. If the goal is simply to obtain screenshots rather than maintain browser infrastructure, ScreenshotNeo offers a one-call screenshot API, with 1,000 shots per month free and paid plans starting at $5 for 3,000. Its billing rules and features are described below.
Or skip the browser setup
If you need a screenshot without maintaining a Playwright browser environment, ScreenshotNeo is a website screenshot API and MCP server. One GET request returns a screenshot or PDF. Its capture accepts cookie and consent banners like a visitor, then removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks/CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and the response identifies the page verdict and billing status in headers. AI agents can use its MCP server tools to take screenshots, get page information, or capture PDFs.
It supports PNG, JPEG, or WebP images and PDF, plus full-page capture, element capture by CSS selector, device and viewport settings, dark mode, retina scale, custom CSS and JavaScript, click-before-capture, hidden selectors, wait conditions, request blocking, headers, cookies, user agent, authorization, timezone, geolocation, transparent backgrounds, resizing, configurable cache TTL, signed image links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, usage API, and an OpenAPI spec. Every feature is on every plan. The parameter names used by other screenshot APIs also work to make switching easier. See the ScreenshotNeo API documentation for request options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Replace the sample URL with the product page you want to capture and supply your API key. Cookie banners, popups, and chat widgets are removed before the shot; bot checks, blank pages, and failed loads are never billed. The MCP server lets AI agents take screenshots. The Free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000, with yearly billing giving two months free. Sign up for 1,000 free screenshots a month with no card.
FAQ
Can toHaveScreenshot() restore a missing symbol?
No. It can wait for visual stability before comparison, but it does not add application text or glyph coverage to a font.
Should I block font requests to make screenshots deterministic?
Not as a general fix. Blocking fonts can change the page to a fallback font and create a different rendering. Make the required font available and verify the actual result.
Why does the same page show a symbol locally but not in CI?
The environments may differ in installed fonts, browser/runtime, locale, or font loading. Compare those details and inspect the rendered page before capture in both environments.
Does a different image format solve the missing glyph?
First confirm whether the browser rendered the glyph before capture. Screenshot format selection does not provide a missing font character.
What details help identify a specific root cause?
The currency character, price element and text, CSS font stack, loaded font file, Playwright and browser versions, operating system/container, locale, and whether the browser view is already wrong before capture.


