Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 64 additions & 4 deletions packages/host/tests/helpers/percy-snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,59 @@ export default async function percySnapshot(
// Load every @font-face the page has declared, not just a hard-coded list.
// This covers IBM Plex Sans, IBM Plex Mono (used by Monaco), IBM Plex Serif
// and any future additions, without needing to keep the helper in sync.
await Promise.all(Array.from(document.fonts, (f) => f.load()));
//
// `allSettled` (not `all`) because Chrome rejects FontFace.load() with a
// generic `DOMException: A network error occurred.` when the font fetch
// fails — typically a transient hiccup pulling a non-critical font over the
// wire in CI. Letting that bubble out turns the *whole* test red with no URL
// attached. The hard-coded IBM Plex Sans `document.fonts.check` below stays
// the load-bearing assertion: if the font that actually moves Percy pixels
// is missing, fail there with a clear message.
let faces = Array.from(document.fonts);
let fontResults = await Promise.allSettled(
faces.map((f) => f.load().then(() => f)),
);
Comment thread
habdelra marked this conversation as resolved.
await document.fonts.ready;

// The hard-coded Sans check remains as a load-bearing assertion: this font
// is the default body font, and a capture without it shifts every text
// element by a fraction of a pixel and turns Percy red across the board.
let failedFonts = fontResults.flatMap((result, idx) => {
if (result.status !== 'rejected') {
return [];
}
let face = faces[idx];
let descriptor = face
? `${face.weight} ${face.style} "${face.family}"`
: `<font#${idx}>`;
return [{ face, descriptor, reason: describeFontLoadError(result.reason) }];
Comment thread
habdelra marked this conversation as resolved.
});

// If IBM Plex Sans itself failed to load, fail loud here — `document.fonts
// .check(..., '')` below cannot be relied on to catch this: per the WHATWG
// spec, `FontFaceSet.check` treats faces in `error` status as settled, and
// with empty text it can still return `true` while the required face is
// actually unrenderable. Without this explicit guard, Percy would capture
// the page with a fallback font silently substituted.
let failedRequired = failedFonts.find(
({ face }) => face?.family === 'IBM Plex Sans',
);
if (failedRequired) {
throw new Error(
`Required font failed to load: ${failedRequired.descriptor}: ${failedRequired.reason}`,
);
}

if (failedFonts.length) {
let lines = failedFonts.map(
({ descriptor, reason }) => `${descriptor}: ${reason}`,
);
console.warn(
`[percy-snapshot] ${failedFonts.length} @font-face load(s) failed; continuing snapshot. Failures:\n ${lines.join('\n ')}`,
);
}

// Belt-and-suspenders: even if no Sans face entered the `error` status, the
// page may still be missing a Sans weight (e.g. never declared, or evicted
// after a teardown). A capture without it shifts every text element by a
// fraction of a pixel and turns Percy red across the board.
for (const weight of ['400', '500', '600', '700']) {
const descriptor = `${weight} 1em IBM Plex Sans`;
if (!document.fonts.check(descriptor, '')) {
Expand Down Expand Up @@ -70,3 +117,16 @@ export default async function percySnapshot(

await originalPercySnapshot(...args);
}

function describeFontLoadError(reason: unknown): string {
if (reason instanceof Error) {
let header = reason.name
? `${reason.name}: ${reason.message}`
: reason.message;
return header || 'Unknown error';
}
if (typeof reason === 'string') {
return reason;
}
return String(reason);
}
Loading