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",
|
||||
}
|
||||
|
||||
# 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
|
||||
# "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
|
||||
|
|
@ -97,8 +119,17 @@ DETECT_JS = r"""(arg) => {
|
|||
for (const g of arg.generics) gen[g] = size(g);
|
||||
const genref = {};
|
||||
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);
|
||||
return { present, gen, genref };
|
||||
return { present, gen, genref, ttcload };
|
||||
}"""
|
||||
|
||||
# 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,
|
||||
"generics": list(GENERICS.keys()),
|
||||
"targets": list(GENERICS.values()),
|
||||
"ttc": TTC_FAMILIES,
|
||||
"cjk": "中文字体測試あア漢字",
|
||||
}
|
||||
with sync_playwright() as p:
|
||||
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}")
|
||||
if 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:
|
||||
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
|
||||
print("FONT GATE FAILED - the exposed set does not match the Windows "
|
||||
"persona on this OS (see the diff above).")
|
||||
|
|
|
|||
|
|
@ -244,6 +244,14 @@ class InvisiblePlaywright:
|
|||
def _resolve_headless(self) -> bool:
|
||||
if not self._headless:
|
||||
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()
|
||||
# Linux: Xvfb to start. Windows/macOS: make_virtual_display() returns
|
||||
# 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"])
|
||||
def test_archive_name_arm64_supported(machine):
|
||||
"""ARM64 is shipped now (issue #6): both Linux aarch64 and macOS arm64.
|
||||
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"
|
||||
ARCHIVE_NAME must map both machine spellings to the canonical -arm64 asset.
|
||||
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue