Skip to content
Merged
Show file tree
Hide file tree
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
3 changes: 3 additions & 0 deletions .github/workflows/ci-host.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,9 @@ jobs:
env:
PERCY_ENABLED: ${{ needs.check-percy.outputs.percy_needed }}
PERCY_GZIP: true
# Log every asset-discovery request so font woff2 capture (or its
# failure) is visible in the shard log for each snapshot.
PERCY_LOGLEVEL: debug
PERCY_TOKEN: ${{ needs.check-percy.outputs.percy_needed == 'true' && secrets.PERCY_TOKEN_HOST || '' }}
PERCY_PARALLEL_NONCE: ${{ github.run_id }}-${{ github.run_attempt }}
HOST_TEST_PARTITION: ${{ matrix.shardIndex }}
Expand Down
22 changes: 22 additions & 0 deletions packages/host/.percy.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
module.exports = {
version: 2,
/*
* Card fixtures can declare fonts served by Google Fonts. Letting Percy's
* asset discovery fetch those per build makes snapshot rendering depend on
* a live third-party response — which woff2 subsets get captured can vary
* between builds of identical code, flipping text between the themed font
* and its fallback. Blocking the hostnames means those fonts are never
* captured, so every build deterministically renders the fixture's
* fallback stack (fonts Percy's renderer has locally, e.g. Georgia).
*/
discovery: {
'disallowed-hostnames': ['fonts.googleapis.com', 'fonts.gstatic.com'],
/*
* The discovery browser is reused across a shard's snapshots, and
* Chrome's own HTTP cache satisfies repeat font requests without them
* ever reaching Percy's proxy — so only the shard's first snapshot got
* the woff2s attached, and every later snapshot rendered fallback text.
* Disabling the browser cache makes every request visible to the proxy,
* so each snapshot carries its full resource set. Percy's proxy-side
* response cache still prevents refetching from the test server.
*/
'disable-cache': true,
},
snapshot: {
widths: [1280],
percyCSS: `
Expand Down
27 changes: 20 additions & 7 deletions packages/host/tests/helpers/percy-snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,24 @@ export default async function percySnapshot(
// 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.
//
// What these waits do and don't buy: fonts loaded here affect the DOM that
// gets serialized, not the pixels Percy renders. Percy uploads a serialized
// copy of this page and re-renders it in its own browsers, where fonts
// resolve from what the *snapshot's captured resources* provide — see
// `discovery` in packages/host/.percy.js for the settings that govern that.
// Loading fonts here still matters because layout computed in this browser
// is serialized as-is: Monaco in particular measures character widths and
// writes absolute pixel offsets into the markup, so capturing before fonts
// settle bakes fallback-font geometry into the snapshot. Chasing a Percy
// font difference by strengthening the guards below will not help; look at
// whether the fonts were captured as snapshot resources instead.
//
// `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.
// attached, so only a missing IBM Plex Sans — the family that drives layout
// everywhere — throws below; the rest warn.
let faces = Array.from(document.fonts);
const fontStart = performance.now();
let fontResults = await Promise.allSettled(
Expand All @@ -68,8 +79,9 @@ export default async function percySnapshot(
// .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.
// actually unrenderable. Failing here names the face and the fetch error,
// which is far easier to act on than the layout drift a fallback-font
// measurement would otherwise bake into the serialized markup.
let failedRequired = failedFonts.find(
({ face }) => face?.family === 'IBM Plex Sans',
);
Expand All @@ -90,8 +102,9 @@ export default async function percySnapshot(

// 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.
// after a teardown). Text measured against a fallback face lands at
// different offsets, so serializing in that state misreports the layout of
// every element positioned from measured text.
for (const weight of ['400', '500', '600', '700']) {
const descriptor = `${weight} 1em IBM Plex Sans`;
if (!document.fonts.check(descriptor, '')) {
Expand Down
Loading