mirror of
https://github.com/feder-cr/invisible_playwright.git
synced 2026-06-25 09:28:05 +02:00
Geo-aware locale, audio noise off, font prefs for the bundled-font binary
locale defaults to "auto" and resolves from the proxy egress country the same way timezone does - it reuses the egress IP, maps the country to a BCP-47 locale with the offline mmdb, and falls back to en-US. prefs emit juggler.locale.override (the full Accept-Language list) so the binary keeps navigator.languages and the Intl default locale in sync. The audio fingerprint noise is off in the baseline. Font prefs match the new binary: the sampled whitelist drives font.system.whitelist, system-ui is Segoe UI, the bundled fonts are activated, and the CSS generics are pinned to Windows defaults so they resolve on a non-Windows host too.
This commit is contained in:
parent
8f4b20a19d
commit
3c0efa2d4f
13 changed files with 353 additions and 1015 deletions
|
|
@ -1,19 +1,17 @@
|
|||
"""Canvas / WebGL render-stealth regression tests (binary-level, 2026-06-18).
|
||||
"""Canvas / WebGL render-stealth regression test (binary-level, 2026-06-18).
|
||||
|
||||
Two patched-binary behaviours that must never regress, both needed for the
|
||||
Guards a patched-binary behaviour that must never regress, needed for the
|
||||
fingerprint to look like a real Windows browser to FOSS detectors (CreepJS,
|
||||
FingerprintJS, BrowserLeaks) and to image-dedup font probes / fixed-hash
|
||||
reference checks:
|
||||
FingerprintJS, BrowserLeaks) and fixed-hash reference checks:
|
||||
|
||||
1. Per-font canvas distinctness — whitelisted named fonts are backed by the
|
||||
host list-head glyphs (so measureText widths are host-independent), but each
|
||||
must still rasterise to a DISTINCT image at tiny probe sizes. Otherwise an
|
||||
image-dedup font probe collapses them to ~1 name and the reported font set
|
||||
looks fabricated. (C++: per-font sub-pixel draw offset in DrawText.)
|
||||
2. Solid WebGL readback purity under render-noise — a fixed solid-colour WebGL
|
||||
readback (which reference checks hash against a universal constant) must stay
|
||||
byte-exact even with per-seed render-noise enabled, while high-entropy
|
||||
renders stay noised. (C++: render-noise skips near-uniform WebGL readbacks.)
|
||||
Solid WebGL readback purity under render-noise — a fixed solid-colour WebGL
|
||||
readback (which reference checks hash against a universal constant) must stay
|
||||
byte-exact even with per-seed render-noise enabled, while high-entropy
|
||||
renders stay noised. (C++: render-noise skips near-uniform WebGL readbacks.)
|
||||
|
||||
(Per-font canvas distinctness is no longer guarded here: the font-collapse +
|
||||
per-font draw offset were removed on 2026-06-20 in favour of real bundled
|
||||
Windows fonts, which rasterise to distinct images by nature.)
|
||||
|
||||
Runs against about:blank, no network/proxy. Part of the e2e release gate.
|
||||
Run: pytest tests/test_canvas_render_stealth.py -m e2e -v
|
||||
|
|
@ -23,34 +21,6 @@ from __future__ import annotations
|
|||
import pytest
|
||||
|
||||
from invisible_playwright import InvisiblePlaywright
|
||||
from invisible_playwright import prefs as _prefs
|
||||
from invisible_playwright._fpforge import generate_profile
|
||||
|
||||
# Diverse-codepoint probe string — maximises per-font rendering differences, the
|
||||
# way an image-dedup font probe drives a tiny canvas.
|
||||
_PROBE = ("\U0001f6cd1>'`amlρiюदे來˦"
|
||||
"\U00025578に◌\U0002003eԩԨ")
|
||||
|
||||
|
||||
def _named_fonts(limit: int = 30) -> list[str]:
|
||||
"""The whitelisted NAMED fonts (absolute collapse-target width >= 10) for the
|
||||
test seed — these are the ones the per-font offset must keep distinct."""
|
||||
prof = generate_profile(42)
|
||||
metrics = _prefs._font_metrics_for_platform(prof._raw.get("font_metrics", "") or "")
|
||||
out: list[str] = []
|
||||
for ent in metrics.split(","):
|
||||
name, _, val = ent.partition("|")
|
||||
if not val:
|
||||
continue
|
||||
try:
|
||||
if float(val.replace("px", "")) >= 10.0:
|
||||
out.append(name)
|
||||
except ValueError:
|
||||
pass
|
||||
return out[:limit]
|
||||
|
||||
|
||||
_FONTS = _named_fonts()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
|
|
@ -68,34 +38,6 @@ def noised_page(firefox_binary):
|
|||
yield p
|
||||
|
||||
|
||||
@pytest.mark.e2e
|
||||
def test_named_fonts_render_distinct_canvas_images(noised_page):
|
||||
"""Each whitelisted named font must produce a DISTINCT tiny-canvas image so an
|
||||
image-dedup font probe keeps every name. Regression: without the per-font draw
|
||||
offset all whitelisted fonts share the list-head glyphs -> ~1-2 distinct
|
||||
images -> degenerate detected-font set."""
|
||||
assert len(_FONTS) >= 10, "expected a non-trivial named-font whitelist to probe"
|
||||
distinct = noised_page.evaluate(
|
||||
"""(args) => {
|
||||
const [fonts, V] = args;
|
||||
const c = document.createElement('canvas'); c.width = 90; c.height = 12;
|
||||
const d = c.getContext('2d'); d.fillStyle = 'red';
|
||||
const seen = new Set();
|
||||
for (const f of fonts) {
|
||||
d.clearRect(0, 0, 90, 12);
|
||||
d.font = 'normal 4px "' + f + '"';
|
||||
d.fillText(V, 5, 8);
|
||||
seen.add(c.toDataURL());
|
||||
}
|
||||
return seen.size;
|
||||
}""",
|
||||
[_FONTS, _PROBE],
|
||||
)
|
||||
# broken (offset removed) collapses to ~1-2; require nearly all distinct.
|
||||
assert distinct >= len(_FONTS) - 2, \
|
||||
f"only {distinct}/{len(_FONTS)} distinct font images (per-font offset regressed?)"
|
||||
|
||||
|
||||
@pytest.mark.e2e
|
||||
def test_solid_webgl_readback_stays_pure_under_noise(noised_page):
|
||||
"""A solid-colour WebGL readback must remain byte-exact (only {0,255}) with
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@ _REQUIRED_PREFS_KEYS = (
|
|||
"media.encoder.webm.enabled",
|
||||
"media.mediasource.webm.enabled",
|
||||
"media.mediasource.mp4.enabled",
|
||||
"zoom.stealth.font.whitelist",
|
||||
"zoom.stealth.font.metrics",
|
||||
"zoom.stealth.font.fontlist",
|
||||
"zoom.stealth.font.system_ui",
|
||||
"ui.systemUsesDarkTheme",
|
||||
"intl.accept_languages",
|
||||
"general.useragent.locale",
|
||||
|
|
@ -197,23 +197,26 @@ def test_http_proxy_returned_unchanged_no_socks_mutations():
|
|||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# IT7: profile.fonts reaches prefs as a comma-joined whitelist
|
||||
# IT7: profile.fonts reaches prefs as a comma-joined fontlist
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_profile_fonts_propagate_to_prefs_whitelist():
|
||||
def test_profile_fonts_propagate_to_prefs_fontlist():
|
||||
"""IT7 — every font in ``profile.fonts`` appears in the comma-joined
|
||||
``zoom.stealth.font.whitelist`` pref, in order."""
|
||||
``zoom.stealth.font.fontlist`` pref, in order. The binary applies this
|
||||
list to the native system font allow-list at construction; system-ui is
|
||||
forced to Segoe UI."""
|
||||
profile = generate_profile(seed=42)
|
||||
prefs = translate_profile_to_prefs(profile)
|
||||
|
||||
assert profile.fonts, "fixture seed=42 produced empty fonts list"
|
||||
whitelist = prefs["zoom.stealth.font.whitelist"]
|
||||
assert isinstance(whitelist, str)
|
||||
assert whitelist == ",".join(profile.fonts)
|
||||
fontlist = prefs["zoom.stealth.font.fontlist"]
|
||||
assert isinstance(fontlist, str)
|
||||
assert fontlist == ",".join(profile.fonts)
|
||||
for font in profile.fonts:
|
||||
assert font in whitelist
|
||||
assert font in fontlist
|
||||
assert prefs["zoom.stealth.font.system_ui"] == "Segoe UI"
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -357,23 +360,3 @@ def test_linux_msaa_pin_propagates_through_pipeline(monkeypatch):
|
|||
assert prefs["webgl.msaa-force"] is True
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# IT13 (extra): Linux font metrics receive the GTK/DejaVu compensation
|
||||
# block. End-to-end check that ``_LINUX_GENERIC_FONT_FACTORS`` is
|
||||
# prepended to the per-font metrics string sampled from the profile.
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_linux_font_metrics_include_generic_factors(monkeypatch):
|
||||
"""IT13 — on Linux the font metrics pref starts with the generic
|
||||
width-scale factors (GTK/DejaVu compensation) so glyph widths match
|
||||
Windows. Without this, Linux sessions leak via metric drift."""
|
||||
from invisible_playwright.prefs import _LINUX_GENERIC_FONT_FACTORS
|
||||
|
||||
monkeypatch.setattr(sys, "platform", "linux")
|
||||
profile = generate_profile(seed=42)
|
||||
prefs = translate_profile_to_prefs(profile)
|
||||
|
||||
metrics = prefs["zoom.stealth.font.metrics"]
|
||||
assert metrics.startswith(_LINUX_GENERIC_FONT_FACTORS)
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ def test_default_context_omits_locale_when_empty():
|
|||
def test_build_env_injects_webrtc_egress_when_discovered():
|
||||
ip = InvisiblePlaywright(seed=42)
|
||||
ip._webrtc_egress_ip = "203.0.113.9" # what __enter__ resolves behind a proxy
|
||||
env = ip._build_env()
|
||||
env = ip._build_env({})
|
||||
assert env["STEALTHFOX_WEBRTC_PUBLIC_IP"] == "203.0.113.9"
|
||||
assert env["STEALTHFOX_WEBRTC_DISABLE_IPV6"] == "1"
|
||||
|
||||
|
|
@ -191,7 +191,7 @@ def test_build_env_no_webrtc_keys_without_proxy(monkeypatch):
|
|||
monkeypatch.delenv("STEALTHFOX_WEBRTC_PUBLIC_IP", raising=False)
|
||||
ip = InvisiblePlaywright(seed=42)
|
||||
ip._webrtc_egress_ip = None # no proxy → real STUN already truthful
|
||||
env = ip._build_env()
|
||||
env = ip._build_env({})
|
||||
assert "STEALTHFOX_WEBRTC_PUBLIC_IP" not in env
|
||||
assert "STEALTHFOX_WEBRTC_DISABLE_IPV6" not in env
|
||||
|
||||
|
|
@ -201,6 +201,29 @@ def test_build_env_caller_env_override_wins(monkeypatch):
|
|||
monkeypatch.setenv("STEALTHFOX_WEBRTC_PUBLIC_IP", "198.51.100.5")
|
||||
ip = InvisiblePlaywright(seed=42)
|
||||
ip._webrtc_egress_ip = "203.0.113.9" # auto-discovered
|
||||
env = ip._build_env()
|
||||
env = ip._build_env({})
|
||||
assert env["STEALTHFOX_WEBRTC_PUBLIC_IP"] == "198.51.100.5" # caller wins
|
||||
assert env["STEALTHFOX_WEBRTC_DISABLE_IPV6"] == "1"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_build_env_injects_font_list_and_system_ui():
|
||||
# The binary reads these at the gfxPlatformFontList constructor (process
|
||||
# start); Playwright delivers firefox_user_prefs over juggler AFTER start, so
|
||||
# the env var is the only at-construction channel. Without it host fonts leak
|
||||
# on Linux/macOS (the wrapper's pref-only delivery was a cross-OS gap).
|
||||
ip = InvisiblePlaywright(seed=42)
|
||||
env = ip._build_env({
|
||||
"zoom.stealth.font.fontlist": "arial,calibri,segoe ui",
|
||||
"zoom.stealth.font.system_ui": "Segoe UI",
|
||||
})
|
||||
assert env["STEALTHFOX_FONTLIST"] == "arial,calibri,segoe ui"
|
||||
assert env["STEALTHFOX_SYSTEMUI"] == "Segoe UI"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_build_env_no_font_keys_when_absent():
|
||||
ip = InvisiblePlaywright(seed=42)
|
||||
env = ip._build_env({})
|
||||
assert "STEALTHFOX_FONTLIST" not in env
|
||||
assert "STEALTHFOX_SYSTEMUI" not in env
|
||||
|
|
|
|||
|
|
@ -5,9 +5,7 @@ import pytest
|
|||
|
||||
from invisible_playwright._fpforge import generate_profile
|
||||
from invisible_playwright.prefs import (
|
||||
_LINUX_GENERIC_FONT_FACTORS,
|
||||
_accept_language,
|
||||
_font_metrics_for_platform,
|
||||
_WIN_LIGHT_COLORS,
|
||||
translate_profile_to_prefs,
|
||||
)
|
||||
|
|
@ -82,29 +80,6 @@ def test_accept_language_underscore_normalized():
|
|||
assert _accept_language("pt_BR") == "pt-BR, pt"
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# _font_metrics_for_platform
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_font_metrics_windows_applies_named_factors(monkeypatch):
|
||||
# FM2: Windows/mac apply the per-NAMED-font factors (so whitelisted named
|
||||
# families don't collapse to the list-head width on the canvas measureText
|
||||
# path), but WITHOUT the Linux generic-family compensation (generics bypass
|
||||
# the whitelist and render native there).
|
||||
monkeypatch.setattr(sys, "platform", "win32")
|
||||
out = _font_metrics_for_platform("Arial|1.0,Verdana|0.9,")
|
||||
assert out == "Arial|1.0,Verdana|0.9,"
|
||||
assert "sans-serif|" not in out # no generic compensation on Windows
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_font_metrics_empty_input_returns_empty():
|
||||
# FM3: Empty input always returns "" regardless of platform.
|
||||
assert _font_metrics_for_platform("") == ""
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# Platform-specific GPU / MSAA (Windows)
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -385,34 +360,6 @@ def test_lan_ip_seed_zero_has_no_zero_octets():
|
|||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_font_metrics_linux_prepends_generic_factors(monkeypatch):
|
||||
# FM1: Linux prepends the GTK/DejaVu compensation block to the
|
||||
# per-font metrics string sampled from the profile.
|
||||
monkeypatch.setattr(sys, "platform", "linux")
|
||||
out = _font_metrics_for_platform("Arial|1.0,Verdana|0.9,")
|
||||
assert out.startswith(_LINUX_GENERIC_FONT_FACTORS)
|
||||
assert out.endswith("Arial|1.0,Verdana|0.9,")
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_font_metrics_linux_empty_input_returns_empty(monkeypatch):
|
||||
# FM1b: even on Linux, empty profile metrics short-circuits before
|
||||
# the prepend so we never emit a metrics pref containing only the
|
||||
# generic block (which would surface as a tampering signal).
|
||||
monkeypatch.setattr(sys, "platform", "linux")
|
||||
assert _font_metrics_for_platform("") == ""
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_font_metrics_linux2_variant_uses_linux_branch(monkeypatch):
|
||||
# FM1c: ``sys.platform`` can be ``linux2`` on older Pythons / odd
|
||||
# WSL builds. ``startswith("linux")`` accepts both.
|
||||
monkeypatch.setattr(sys, "platform", "linux2")
|
||||
out = _font_metrics_for_platform("Verdana|0.9,")
|
||||
assert out.startswith(_LINUX_GENERIC_FONT_FACTORS)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_gpu_renderer_set_from_profile_on_linux(monkeypatch):
|
||||
# PG1: on Linux (as on EVERY host) we apply the camoufox-derived Windows-ANGLE GPU persona,
|
||||
|
|
|
|||
|
|
@ -216,12 +216,15 @@ def test_screen_tier_4200x2000_is_ultrawide_via_width_branch():
|
|||
# ── derive_font_prefs / derive_font_whitelist ───────────────────────────
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_derive_font_prefs_returns_whitelist_and_metrics_keys():
|
||||
"""FP1 [HAPPY]: result has the two expected string keys."""
|
||||
def test_derive_font_prefs_returns_whitelist_key():
|
||||
"""FP1 [HAPPY]: result is a single-key dict with the font family list.
|
||||
|
||||
The per-family ``metrics`` string was removed on 2026-06-20: fonts now
|
||||
render from the bundled real Windows files (genuine widths) and per-session
|
||||
metric uniqueness comes from the HarfBuzz jitter, not fabricated factors."""
|
||||
out = derive_font_prefs("integrated_modern", random.Random(42))
|
||||
assert set(out.keys()) == {"whitelist", "metrics"}
|
||||
assert set(out.keys()) == {"whitelist"}
|
||||
assert isinstance(out["whitelist"], str)
|
||||
assert isinstance(out["metrics"], str)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
|
|
@ -249,15 +252,6 @@ def test_derive_font_prefs_unknown_class_falls_back_to_integrated_modern():
|
|||
assert fallback == expected
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_derive_font_prefs_metrics_and_whitelist_are_coherent():
|
||||
"""FP5 [ECP]: every name in whitelist has a metrics entry and vice versa."""
|
||||
out = derive_font_prefs("mid_range", random.Random(99))
|
||||
wl_names = out["whitelist"].split(",")
|
||||
metrics_names = [s.split("|", 1)[0] for s in out["metrics"].split(",")]
|
||||
assert wl_names == metrics_names
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_derive_font_prefs_whitelist_alphabetically_sorted():
|
||||
"""FP6 [ECP]: whitelist names are sorted (ordering invariant for stable dedup)."""
|
||||
|
|
@ -280,10 +274,12 @@ def test_derive_font_whitelist_legacy_shim_matches_dict_form():
|
|||
# machine never lacks them, so a session that drops one advertises a font set that
|
||||
# doesn't match any real Windows profile (image-dedup font probes then report a
|
||||
# short/degenerate name list → server-side OS-font-set checks fail). Calibri in
|
||||
# particular sat in `optional` (a bug); these five caused the detected set to come
|
||||
# up short on some seeds. Regression guard for the 2026-06-18 optional→core move.
|
||||
# particular sat in `optional` (a bug); these caused the detected set to come up
|
||||
# short on some seeds. Regression guard for the 2026-06-18 optional→core move.
|
||||
# NB: the exact Win11 family is "franklin gothic medium" (there is no bare
|
||||
# "franklin gothic" family); the 2026-06-20 bundle reconciliation uses real names.
|
||||
_STANDARD_WINDOWS_FONTS = [
|
||||
"calibri", "franklin gothic", "gadugi", "javanese text", "myanmar text",
|
||||
"calibri", "franklin gothic medium", "gadugi", "javanese text", "myanmar text",
|
||||
]
|
||||
_ALL_GPU_CLASSES = [
|
||||
"integrated_old", "integrated_modern", "mid_range", "high_end",
|
||||
|
|
@ -294,16 +290,14 @@ _ALL_GPU_CLASSES = [
|
|||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("gpu_class", _ALL_GPU_CLASSES)
|
||||
def test_standard_windows_fonts_always_present_every_class_and_seed(gpu_class):
|
||||
"""FP7 [regression]: the standard-Windows fonts appear in BOTH whitelist and
|
||||
metrics for every gpu_class across many seeds (i.e. they are core, not
|
||||
profile-optional). Guards against a standard font silently becoming optional."""
|
||||
"""FP7 [regression]: the standard-Windows fonts appear in the whitelist for
|
||||
every gpu_class across many seeds (i.e. they are core, not profile-optional).
|
||||
Guards against a standard font silently becoming optional."""
|
||||
for seed in range(40):
|
||||
out = derive_font_prefs(gpu_class, random.Random(seed))
|
||||
wl = set(out["whitelist"].split(","))
|
||||
metrics_names = {s.split("|", 1)[0] for s in out["metrics"].split(",")}
|
||||
for font in _STANDARD_WINDOWS_FONTS:
|
||||
assert font in wl, f"{font!r} missing from whitelist (class={gpu_class}, seed={seed})"
|
||||
assert font in metrics_names, f"{font!r} missing from metrics (class={gpu_class}, seed={seed})"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
|
|
@ -320,33 +314,13 @@ def test_standard_windows_fonts_are_in_core_pool():
|
|||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("gpu_class", _ALL_GPU_CLASSES)
|
||||
def test_derive_font_prefs_no_duplicate_families(gpu_class):
|
||||
"""FP9 [regression]: no family appears twice in whitelist/metrics, even when a
|
||||
"""FP9 [regression]: no family appears twice in the whitelist, even when a
|
||||
profile's optional list also names a core font. Guards the dedup in
|
||||
derive_font_prefs (a duplicate family would emit a malformed pref pair)."""
|
||||
derive_font_prefs (a duplicate family would emit a malformed list)."""
|
||||
for seed in range(30):
|
||||
out = derive_font_prefs(gpu_class, random.Random(seed))
|
||||
wl = out["whitelist"].split(",")
|
||||
metrics_names = [s.split("|", 1)[0] for s in out["metrics"].split(",")]
|
||||
assert len(wl) == len(set(wl)), f"duplicate in whitelist (class={gpu_class}, seed={seed})"
|
||||
assert len(metrics_names) == len(set(metrics_names)), \
|
||||
f"duplicate in metrics (class={gpu_class}, seed={seed})"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("gpu_class", _ALL_GPU_CLASSES)
|
||||
def test_derive_font_prefs_named_fonts_emit_absolute_widths(gpu_class):
|
||||
"""FP10 [regression]: every emitted metrics value is a positive number; named
|
||||
(non-generic) fonts carry an ABSOLUTE collapse-target width (>= 10), which the
|
||||
binary self-calibrates per host. A value < 10 here would mean a font slipped
|
||||
through as a bare multiplicative factor and would render at the wrong width."""
|
||||
out = derive_font_prefs(gpu_class, random.Random(3))
|
||||
for entry in out["metrics"].split(","):
|
||||
name, _, val = entry.partition("|")
|
||||
v = float(val.replace("px", ""))
|
||||
assert v > 0.0, f"non-positive metrics value for {name!r}"
|
||||
# the standard named fonts must be absolute (collapse-target) widths
|
||||
if name in _STANDARD_WINDOWS_FONTS:
|
||||
assert v >= 10.0, f"{name!r} emitted as factor {v} (<10), expected absolute width"
|
||||
|
||||
|
||||
# ── Forge / sample ──────────────────────────────────────────────────────
|
||||
|
|
@ -364,7 +338,7 @@ _EXPECTED_KEYS = {
|
|||
"av1_enabled", "webm_encoder_enabled",
|
||||
"mediasource_webm", "mediasource_mp4", "webspeech_synth",
|
||||
"storage_quota_mb", "dark_theme",
|
||||
"font_whitelist", "font_metrics",
|
||||
"font_whitelist",
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -453,10 +427,9 @@ def test_forge_sample_avail_h_defaults_to_h_minus_40_when_missing(monkeypatch):
|
|||
|
||||
@pytest.mark.unit
|
||||
def test_forge_sample_includes_font_keys():
|
||||
"""FS9 [ECP]: font_whitelist + font_metrics present and non-empty."""
|
||||
"""FS9 [ECP]: font_whitelist present and non-empty (the joined family list)."""
|
||||
out = sample(42)
|
||||
assert out["font_whitelist"]
|
||||
assert out["font_metrics"]
|
||||
assert "," in out["font_whitelist"] # at least the core fonts joined
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue