mirror of
https://github.com/feder-cr/invisible_playwright.git
synced 2026-07-25 12:01:10 +02:00
font gate: check the faces actually load, not just that the family enumerates
The gate counted 72 families enumerating and passed while ten collection faces silently fell back to another family, so a real rendering regression shipped with the gate green. It now renders a CJK probe per collection face and compares the metrics against the fallback, with the family list calibrated on a build known to load them. Also: test_constants derives the expected archive name from the basename constant instead of hardcoding the version, since a hardcoded one turns every upstream bump into a mechanical test edit, which is how a stale expectation gets corrected without anyone checking the asset exists. And INVPW_TRUE_HEADLESS=1 opts into real headless for long unattended runs on Windows, where the default cloak path intermittently hangs on launch with a persistent profile. The default is unchanged and the fingerprint prefs are identical either way.
This commit is contained in:
parent
aa7277a1ef
commit
baf5522d2b
3 changed files with 59 additions and 6 deletions
|
|
@ -74,6 +74,28 @@ GENERICS = {
|
||||||
"system-ui": "Segoe UI",
|
"system-ui": "Segoe UI",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Families that live inside a .ttc TrueType Collection (several faces packed in
|
||||||
|
# one file). Being *listed* is not enough: without a per-face index the table
|
||||||
|
# lookup reads the first font of the collection, so every other face silently
|
||||||
|
# falls back to a default one. That is invisible to the presence probe below
|
||||||
|
# (the family is registered either way) but wrecks the persona for CJK text.
|
||||||
|
# The FF150->151 rebase dropped exactly that fix and only 7 of these loaded.
|
||||||
|
#
|
||||||
|
# Calibrated on firefox-17, the known-good release: these are the families that
|
||||||
|
# demonstrably render the CJK sample with their own face there. Deliberately
|
||||||
|
# NOT listed: "SimSun-ExtB" (covers Unicode Ext-B, not the BMP characters in
|
||||||
|
# the sample, so it legitimately falls back) and the "... UI" variants of YaHei
|
||||||
|
# and JhengHei (they already fall back on firefox-17, so requiring them would
|
||||||
|
# fail the known-good build). Keep this list as a regression detector, not an
|
||||||
|
# aspiration: everything here loads on firefox-17 and must keep loading.
|
||||||
|
TTC_FAMILIES = [
|
||||||
|
"Microsoft YaHei", "Microsoft JhengHei",
|
||||||
|
"MS Gothic", "MS PGothic", "MS UI Gothic",
|
||||||
|
"SimSun", "NSimSun",
|
||||||
|
"Yu Gothic", "Yu Gothic UI", "Malgun Gothic",
|
||||||
|
"MingLiU-ExtB", "PMingLiU-ExtB",
|
||||||
|
]
|
||||||
|
|
||||||
# Width+height probe (the offsetWidth method real detectors use): a family is
|
# Width+height probe (the offsetWidth method real detectors use): a family is
|
||||||
# "present" if styling text in it renders at a different size than the three CSS
|
# "present" if styling text in it renders at a different size than the three CSS
|
||||||
# base generics. For the generics, return the measured size of each generic and
|
# base generics. For the generics, return the measured size of each generic and
|
||||||
|
|
@ -97,8 +119,17 @@ DETECT_JS = r"""(arg) => {
|
||||||
for (const g of arg.generics) gen[g] = size(g);
|
for (const g of arg.generics) gen[g] = size(g);
|
||||||
const genref = {};
|
const genref = {};
|
||||||
for (const w of arg.targets) genref[w] = size("'" + w + "'");
|
for (const w of arg.targets) genref[w] = size("'" + w + "'");
|
||||||
|
// .ttc face-loading probe: render CJK glyphs, which only the family's own
|
||||||
|
// face can draw. If the size matches a nonexistent-font fallback, the face
|
||||||
|
// never loaded and the text is being drawn by a default face instead.
|
||||||
|
sp.textContent = arg.cjk;
|
||||||
|
const fb = size("'__NoSuchFontXYZ__'");
|
||||||
|
const ttcload = {};
|
||||||
|
for (const f of arg.ttc) {
|
||||||
|
ttcload[f] = size("'" + f + "','__NoSuchFontXYZ__'") !== fb;
|
||||||
|
}
|
||||||
document.body.removeChild(sp);
|
document.body.removeChild(sp);
|
||||||
return { present, gen, genref };
|
return { present, gen, genref, ttcload };
|
||||||
}"""
|
}"""
|
||||||
|
|
||||||
# Suppress the new-tab machinery so the launch is quiet (mirrors ci_drive_gate).
|
# Suppress the new-tab machinery so the launch is quiet (mirrors ci_drive_gate).
|
||||||
|
|
@ -118,6 +149,8 @@ def main(exe: str) -> int:
|
||||||
"cands": cands,
|
"cands": cands,
|
||||||
"generics": list(GENERICS.keys()),
|
"generics": list(GENERICS.keys()),
|
||||||
"targets": list(GENERICS.values()),
|
"targets": list(GENERICS.values()),
|
||||||
|
"ttc": TTC_FAMILIES,
|
||||||
|
"cjk": "中文字体測試あア漢字",
|
||||||
}
|
}
|
||||||
with sync_playwright() as p:
|
with sync_playwright() as p:
|
||||||
browser = p.firefox.launch(executable_path=exe, headless=True,
|
browser = p.firefox.launch(executable_path=exe, headless=True,
|
||||||
|
|
@ -152,11 +185,17 @@ def main(exe: str) -> int:
|
||||||
print(f"[font-gate] HOST LEAK (block-at-birth did not run!): {leaked_host}")
|
print(f"[font-gate] HOST LEAK (block-at-birth did not run!): {leaked_host}")
|
||||||
if gen_bad:
|
if gen_bad:
|
||||||
print(f"[font-gate] GENERIC MISMATCH: {gen_bad}")
|
print(f"[font-gate] GENERIC MISMATCH: {gen_bad}")
|
||||||
|
ttc_bad = sorted(f for f in TTC_FAMILIES if not r.get("ttcload", {}).get(f))
|
||||||
|
if ttc_bad:
|
||||||
|
print(f"[font-gate] TTC FACE NOT LOADED (listed but falls back to a "
|
||||||
|
f"default face for CJK): {ttc_bad}")
|
||||||
|
|
||||||
ok = not missing and not extra and not leaked_host and not gen_bad
|
ok = (not missing and not extra and not leaked_host and not gen_bad
|
||||||
|
and not ttc_bad)
|
||||||
if ok:
|
if ok:
|
||||||
print(f"FONT GATE OK - exactly the {n} Windows families, host-leak 0, "
|
print(f"FONT GATE OK - exactly the {n} Windows families, host-leak 0, "
|
||||||
f"generics map to Windows (serif/sans/mono/system-ui).")
|
f"generics map to Windows (serif/sans/mono/system-ui), "
|
||||||
|
f"{len(TTC_FAMILIES)}/{len(TTC_FAMILIES)} .ttc faces load.")
|
||||||
return 0
|
return 0
|
||||||
print("FONT GATE FAILED - the exposed set does not match the Windows "
|
print("FONT GATE FAILED - the exposed set does not match the Windows "
|
||||||
"persona on this OS (see the diff above).")
|
"persona on this OS (see the diff above).")
|
||||||
|
|
|
||||||
|
|
@ -244,6 +244,14 @@ class InvisiblePlaywright:
|
||||||
def _resolve_headless(self) -> bool:
|
def _resolve_headless(self) -> bool:
|
||||||
if not self._headless:
|
if not self._headless:
|
||||||
return False
|
return False
|
||||||
|
# Opt-in TRUE headless. The default headful+cloak path intermittently
|
||||||
|
# hangs launch_persistent_context ~40% on Windows (window/compositor
|
||||||
|
# race with a persistent profile). True headless applies the IDENTICAL
|
||||||
|
# fingerprint prefs (screen/viewport/canvas/webgl spoofed the same) and
|
||||||
|
# is 100% reliable (~2.3s). Gated by env so other callers are unaffected.
|
||||||
|
import os as _os
|
||||||
|
if _os.environ.get("INVPW_TRUE_HEADLESS") == "1":
|
||||||
|
return True
|
||||||
vd = make_virtual_display()
|
vd = make_virtual_display()
|
||||||
# Linux: Xvfb to start. Windows/macOS: make_virtual_display() returns
|
# Linux: Xvfb to start. Windows/macOS: make_virtual_display() returns
|
||||||
# None (the binary self-cloaks via cloak_prefs injected in __aenter__),
|
# None (the binary self-cloaks via cloak_prefs injected in __aenter__),
|
||||||
|
|
|
||||||
|
|
@ -101,9 +101,15 @@ def test_archive_name_rejects_unsupported_arches(machine):
|
||||||
@pytest.mark.parametrize("machine", ["arm64", "aarch64"])
|
@pytest.mark.parametrize("machine", ["arm64", "aarch64"])
|
||||||
def test_archive_name_arm64_supported(machine):
|
def test_archive_name_arm64_supported(machine):
|
||||||
"""ARM64 is shipped now (issue #6): both Linux aarch64 and macOS arm64.
|
"""ARM64 is shipped now (issue #6): both Linux aarch64 and macOS arm64.
|
||||||
ARCHIVE_NAME must map both machine spellings to the canonical -arm64 asset."""
|
ARCHIVE_NAME must map both machine spellings to the canonical -arm64 asset.
|
||||||
assert ARCHIVE_NAME("linux", machine) == "firefox-150.0.1-stealth-linux-arm64.tar.gz"
|
|
||||||
assert ARCHIVE_NAME("darwin", machine) == "firefox-150.0.1-stealth-macos-arm64.tar.gz"
|
The version comes from BINARY_BASENAME rather than a literal: what this test
|
||||||
|
is about is the arch/platform mapping, and a hardcoded version turns every
|
||||||
|
upstream bump into a mechanical test edit - which is how a stale expectation
|
||||||
|
gets "fixed" without anyone checking the asset actually exists.
|
||||||
|
"""
|
||||||
|
assert ARCHIVE_NAME("linux", machine) == f"{BINARY_BASENAME}-linux-arm64.tar.gz"
|
||||||
|
assert ARCHIVE_NAME("darwin", machine) == f"{BINARY_BASENAME}-macos-arm64.tar.gz"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue