Tests/LibWeb: Add tests for @font-face load behavior

This commit is contained in:
Johan Dahlin
2026-04-21 19:46:15 +02:00
committed by Andreas Kling
parent f916b3e11f
commit 0de26af387
Notes: github-actions[bot] 2026-04-25 15:08:05 +00:00
9 changed files with 177 additions and 0 deletions

View File

@@ -14,6 +14,7 @@ Text/input/HTML/media-source-buffered.html
Text/input/HTML/media-source-setup.html
Text/input/css/FontFace-arraybuffer-matching.html
Text/input/wpt-import/mediacapture-streams/idlharness.https.window.html
Text/input/css/font-face-load-dedups-same-url.html
; pushState with path URL requires HTTP(s) scheme.
Text/input/navigation/history-replace-push-then-back.html

View File

@@ -0,0 +1,4 @@
faceA.status: loaded
faceB.status: loaded
resource entries total: 3
resource entries for font url: 2

View File

@@ -0,0 +1 @@
ProbeFont status: error

View File

@@ -0,0 +1,4 @@
MultiFont face count: 3
range=U+41 status=loaded
range=U+42 status=error
range=U+43 status=error

View File

@@ -0,0 +1,4 @@
MultiFont face count: 3
range=U+41 status=loaded
range=U+42 status=error
range=U+43 status=error

View File

@@ -0,0 +1,31 @@
<!DOCTYPE html>
<script src="../include.js"></script>
<script>
promiseTest(async () => {
let fontUrl = new URL(location.href);
fontUrl.search = "";
fontUrl += "/../../../../Assets/HashSans.woff";
// Two FontFace objects referencing the exact same src URL. A single variable
// woff2 served by Google Fonts is commonly declared under multiple
// @font-face rules (one per font-weight), all pointing at the same file.
// With URL-keyed dedup in FontComputer, both faces should share a single
// in-flight FontLoader so only one network fetch occurs.
const faceA = new FontFace("DedupFont", `url(${fontUrl})`, { weight: "400" });
const faceB = new FontFace("DedupFont", `url(${fontUrl})`, { weight: "700" });
await Promise.all([faceA.load(), faceB.load()]);
println(`faceA.status: ${faceA.status}`);
println(`faceB.status: ${faceB.status}`);
// Use the PerformanceResourceTiming API to count how many resource-timing
// entries were recorded for this URL — one means the loaders were deduped,
// two means each FontFace kicked off its own fetch.
const all = performance.getEntriesByType("resource");
const urlStr = String(fontUrl);
const matches = all.filter(e => e.name === urlStr || e.name.endsWith("HashSans.woff"));
println(`resource entries total: ${all.length}`);
println(`resource entries for font url: ${matches.length}`);
});
</script>

View File

@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html>
<head>
<style>
/* Subset face whose unicode-range covers U+0020 — exactly the codepoint used by the
"first-available-font" metric probe. A style-only probe must not load this face. */
@font-face {
font-family: 'ProbeFont';
src: url('unused-probe.woff');
unicode-range: U+0020-007F;
}
/* display:none + empty content: style is computed (metric probe fires via the
em-length in `height`) but nothing is ever shaped, so no codepoint actually
needs to be rendered by ProbeFont. */
.probed { font-family: 'ProbeFont'; display: none; height: 1em; }
</style>
</head>
<body>
<div class="probed"></div>
<script src="../include.js"></script>
<script>
promiseTest(async () => {
// Force style resolution. getComputedStyle goes through the same
// ComputedProperties::first_available_computed_font(' ') path that used to
// eagerly trigger the fetch.
getComputedStyle(document.querySelector('.probed')).fontFamily;
document.body.offsetHeight;
// Give any in-flight font fetch enough time to settle. Before the fix the
// metric probe fetches ProbeFont and the fetch fails fast with NetworkError;
// after the fix no fetch is ever started.
await new Promise(r => setTimeout(r, 50));
const face = [...document.fonts].find(f => f.family === 'ProbeFont');
println(`ProbeFont status: ${face.status}`);
});
</script>
</body>
</html>

View File

@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html>
<head>
<style>
@font-face {
font-family: 'MultiFont';
src: url('../../../Assets/HashSans.woff');
unicode-range: U+0041; /* A */
}
@font-face {
font-family: 'MultiFont';
src: url('unused-b.woff');
unicode-range: U+0042; /* B */
}
@font-face {
font-family: 'MultiFont';
src: url('unused-c.woff');
unicode-range: U+0043; /* C */
}
</style>
</head>
<body>
<script src="../include.js"></script>
<script>
promiseTest(async () => {
const faces = [...document.fonts].filter(f => f.family === 'MultiFont');
faces.sort((a, b) => a.unicodeRange.localeCompare(b.unicodeRange));
// https://drafts.csswg.org/css-font-loading/#find-the-matching-font-faces
// Step 8: "For each font face in matched font faces, if its defined unicode-range
// does not include the codepoint of at least one character in text, remove it from
// the list." So asking the FontFaceSet to load "A" must only kick off faceA's fetch;
// faceB and faceC have disjoint unicode-ranges and must stay unloaded.
// The pre-fix behavior is to pull in faceB and faceC too, which reject with
// NetworkError — catch so the test does not time out reporting "wrong" output.
await document.fonts.load('16px MultiFont', 'A').catch(() => {});
await document.fonts.ready;
println(`MultiFont face count: ${faces.length}`);
for (const f of faces)
println(`range=${f.unicodeRange} status=${f.status}`);
});
</script>
</body>
</html>

View File

@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html>
<head>
<style>
@font-face {
font-family: 'MultiFont';
src: url('../../../Assets/HashSans.woff');
unicode-range: U+0041; /* A */
}
@font-face {
font-family: 'MultiFont';
src: url('unused-b.woff');
unicode-range: U+0042; /* B */
}
@font-face {
font-family: 'MultiFont';
src: url('unused-c.woff');
unicode-range: U+0043; /* C */
}
.text { font-family: 'MultiFont', 'SerenitySans'; font-size: 40px; }
</style>
</head>
<body>
<div class="text">A</div>
<!-- Text inside a display:none element (a <script> in this case) must not trigger font loads.
Here "BBBCCC" sits in a JSON script block and is never laid out; faceB and faceC therefore
stay "unloaded" below even though the content contains codepoints in their unicode-ranges. -->
<script type="application/json" id="blob">{"letters":"BBBCCC","more":"B and C should not be rendered"}</script>
<script src="../include.js"></script>
<script>
promiseTest(async () => {
const faces = [...document.fonts].filter(f => f.family === 'MultiFont');
faces.sort((a, b) => a.unicodeRange.localeCompare(b.unicodeRange));
// Force layout so text shaping runs — this is what triggers the render-path
// load for faceA once deferral is implemented.
document.body.offsetHeight;
// Wait for every in-flight @font-face load to settle. At the pre-fix baseline
// faceB and faceC are eagerly fetched and then reject with NetworkError; after
// the fix only faceA is loaded and ready resolves immediately.
await document.fonts.ready;
println(`MultiFont face count: ${faces.length}`);
for (const f of faces)
println(`range=${f.unicodeRange} status=${f.status}`);
});
</script>
</body>
</html>