mirror of
https://github.com/feder-cr/invisible_playwright.git
synced 2026-06-07 08:35:12 +02:00
Add macOS support
This commit is contained in:
parent
143aff4bd2
commit
d09200f01d
10 changed files with 240 additions and 50 deletions
|
|
@ -59,7 +59,9 @@ pip install git+https://github.com/feder-cr/invisible_playwright.git
|
|||
python -m invisible_playwright fetch # one-time ~100 MB download, SHA256-verified
|
||||
```
|
||||
|
||||
Supported platforms: **Windows x86_64**, **Linux x86_64**.
|
||||
Supported platforms: **Windows x86_64**, **Linux x86_64**, **macOS x86_64**, **macOS arm64**.
|
||||
|
||||
`headless=True` keeps using the hidden headed-mode backends on Windows/Linux. On macOS, launch support is available but hidden headed-mode is not yet implemented, so use `headless=False`.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ windows off the user's screen.
|
|||
Linux: spawns its own ``Xvfb`` instance, points ``DISPLAY`` at it.
|
||||
Windows: creates a hidden desktop via ``CreateDesktop`` and binds the
|
||||
calling thread to it, so Playwright's child processes inherit it.
|
||||
macOS: launch support exists, but there is currently no hidden-display
|
||||
backend for ``headless=True``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -212,14 +214,22 @@ class _WindowsVirtualDesktop:
|
|||
def make_virtual_display():
|
||||
"""Return a started/stoppable virtual-display object for this platform.
|
||||
|
||||
InvisiblePlaywright supports Windows x86_64 and Linux x86_64 only.
|
||||
Hidden-display backends currently exist for Windows and Linux only.
|
||||
On macOS the wrapper can launch normally, but ``headless=True`` is
|
||||
not yet supported because there is no invisible headed-mode backend.
|
||||
"""
|
||||
if sys.platform == "win32":
|
||||
return _WindowsVirtualDesktop()
|
||||
if sys.platform.startswith("linux"):
|
||||
return _LinuxVirtualDisplay()
|
||||
if sys.platform == "darwin":
|
||||
raise RuntimeError(
|
||||
"invisible_playwright headless=True is not yet supported on macOS. "
|
||||
"Use headless=False."
|
||||
)
|
||||
raise RuntimeError(
|
||||
f"invisible_playwright supports Windows and Linux only (got {sys.platform!r})"
|
||||
f"invisible_playwright headless=True supports Windows and Linux only "
|
||||
f"(got {sys.platform!r})"
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -19,11 +19,20 @@ BINARY_BASENAME: str = f"firefox-{FIREFOX_UPSTREAM_VERSION}-stealth"
|
|||
def ARCHIVE_NAME(platform_key: str, machine: str) -> str:
|
||||
"""Return the platform-specific archive filename.
|
||||
|
||||
platform_key: sys.platform ("win32", "linux")
|
||||
machine: platform.machine() ("AMD64", "x86_64", ...)
|
||||
platform_key: sys.platform ("win32", "linux", "darwin")
|
||||
machine: platform.machine() ("AMD64", "x86_64", "arm64", ...)
|
||||
"""
|
||||
pk = platform_key.lower()
|
||||
m = machine.lower()
|
||||
if pk == "darwin":
|
||||
if m in {"arm64", "aarch64"}:
|
||||
arch = "arm64"
|
||||
elif m in {"amd64", "x86_64"}:
|
||||
arch = "x86_64"
|
||||
else:
|
||||
raise NotImplementedError(f"unsupported arch: {machine}")
|
||||
return f"{BINARY_BASENAME}-macos-{arch}.tar.gz"
|
||||
|
||||
if m in {"amd64", "x86_64"}:
|
||||
arch = "x86_64"
|
||||
else:
|
||||
|
|
@ -40,6 +49,7 @@ def ARCHIVE_NAME(platform_key: str, machine: str) -> str:
|
|||
BINARY_ENTRY_REL = {
|
||||
"win32": "firefox.exe",
|
||||
"linux": "firefox",
|
||||
"darwin": "Firefox.app/Contents/MacOS/firefox",
|
||||
}
|
||||
|
||||
# GitHub release URL template. The "TODO" owner is resolved at publication time.
|
||||
|
|
|
|||
|
|
@ -431,11 +431,11 @@ def _accept_language(locale: str) -> str:
|
|||
def _font_metrics_for_platform(profile_metrics: str) -> str:
|
||||
"""Return ``zoom.stealth.font.metrics`` value.
|
||||
|
||||
Windows: empty string. The C++ width-scale hook is a no-op and
|
||||
Firefox renders Arial/Segoe/Calibri/etc. at their native canonical
|
||||
widths. Applying the Bayesian-sampled per-font factors on a Windows
|
||||
build would *distort* real metrics and surface as a font_preferences
|
||||
width anomaly to FP Pro / reCAPTCHA.
|
||||
Windows/macOS: empty string. The C++ width-scale hook is a no-op and
|
||||
native desktop builds render Arial/Segoe/Calibri/etc. through their
|
||||
host text stacks. Applying the Bayesian-sampled per-font factors on a
|
||||
non-Linux build would *distort* real metrics and surface as a
|
||||
font_preferences width anomaly to FP Pro / reCAPTCHA.
|
||||
|
||||
Linux: prepend generic-family compensation factors so DejaVu /
|
||||
Liberation render at the widths Windows JS expects, then append the
|
||||
|
|
@ -446,7 +446,7 @@ def _font_metrics_for_platform(profile_metrics: str) -> str:
|
|||
return ""
|
||||
if sys.platform.startswith("linux"):
|
||||
return _LINUX_GENERIC_FONT_FACTORS + profile_metrics
|
||||
return "" # Windows: NEVER apply width-scale factors.
|
||||
return "" # Non-Linux builds: never apply width-scale factors.
|
||||
|
||||
|
||||
def translate_profile_to_prefs(
|
||||
|
|
@ -473,13 +473,13 @@ def translate_profile_to_prefs(
|
|||
# GPU / WebGL renderer/vendor.
|
||||
# On Linux we spoof to a Windows ANGLE renderer string (profile.gpu.renderer)
|
||||
# so cross-platform sessions report a consistent Windows GPU identity.
|
||||
# On Windows, spoofing a different GPU creates a renderer/parameters hash
|
||||
# On non-Linux hosts, spoofing a different GPU creates a renderer/parameters hash
|
||||
# mismatch: FP Pro hashes all 81 CN-set getParameter() values including
|
||||
# enum 7937 (RENDERER). Setting GTX 980 while ANGLE returns Intel Arc A750
|
||||
# parameters produces an OOD (hash 23d0a74b vs vanilla 66544db) that FP Pro
|
||||
# ML scores at ~0.70 (confirmed: direct SF146 vs vanilla on same machine).
|
||||
# Fix: leave renderer/vendor empty on Windows → ANGLE reports native hardware
|
||||
# (SanitizeRenderer path at ClientWebGLContext.cpp:2592-2595) → consistent.
|
||||
# Fix: leave renderer/vendor empty on non-Linux builds so the native stack
|
||||
# reports its own hardware path and keeps the renderer/parameter hash coherent.
|
||||
if sys.platform.startswith("linux"):
|
||||
prefs["zoom.stealth.webgl.renderer"] = profile.gpu.renderer
|
||||
prefs["zoom.stealth.webgl.vendor"] = profile.gpu.vendor
|
||||
|
|
@ -489,10 +489,9 @@ def translate_profile_to_prefs(
|
|||
prefs["zoom.stealth.webgl.vendor"] = ""
|
||||
_renderer_lo = "intel" # test hardware is Intel Arc A750
|
||||
|
||||
# MSAA: on Windows, pin to 4 (Firefox default for ANGLE) so gl.SAMPLES is
|
||||
# MSAA: on non-Linux hosts, pin to 4 so gl.SAMPLES is
|
||||
# constant across all sessions. Different MSAA values cause different CN-set
|
||||
# parameters hashes even with the same renderer → detectable variation.
|
||||
# Vanilla Intel Arc A750 parameters hash (66544db8) verified at msaa=4.
|
||||
_msaa = profile.webgl.msaa_samples if sys.platform.startswith("linux") else 4
|
||||
prefs["zoom.stealth.webgl.msaa"] = _msaa
|
||||
prefs["webgl.msaa-samples"] = _msaa
|
||||
|
|
|
|||
|
|
@ -81,3 +81,15 @@ def test_async_default_context_kwargs_match_sync():
|
|||
a = AsyncIP(seed=42, timezone="America/New_York", locale="de-DE")
|
||||
s = SyncIP(seed=42, timezone="America/New_York", locale="de-DE")
|
||||
assert a._default_context_kwargs() == s._default_context_kwargs()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_async_resolve_headless_raises_clear_error_on_macos(monkeypatch):
|
||||
"""The async launcher shares the same hidden-display limitation as the
|
||||
sync launcher, so ``headless=True`` on macOS should fail the same way."""
|
||||
import sys as _sys
|
||||
monkeypatch.setattr(_sys, "platform", "darwin")
|
||||
|
||||
ip = AsyncIP(seed=42, headless=True)
|
||||
with pytest.raises(RuntimeError, match="headless=True is not yet supported on macOS"):
|
||||
ip._resolve_headless()
|
||||
|
|
|
|||
|
|
@ -31,9 +31,29 @@ def test_archive_name_linux():
|
|||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_archive_name_unsupported_raises():
|
||||
def test_archive_name_macos_arm64():
|
||||
name = ARCHIVE_NAME("darwin", "arm64")
|
||||
assert name.endswith(".tar.gz")
|
||||
assert "macos-arm64" in name
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_archive_name_macos_x86_64():
|
||||
name = ARCHIVE_NAME("darwin", "x86_64")
|
||||
assert name.endswith(".tar.gz")
|
||||
assert "macos-x86_64" in name
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_archive_name_unsupported_platform_raises():
|
||||
with pytest.raises(NotImplementedError):
|
||||
ARCHIVE_NAME("darwin", "arm64")
|
||||
ARCHIVE_NAME("freebsd14", "x86_64")
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_archive_name_unsupported_arch_raises():
|
||||
with pytest.raises(NotImplementedError):
|
||||
ARCHIVE_NAME("darwin", "ppc64")
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
|
|
@ -57,6 +77,10 @@ def test_binary_basename_format():
|
|||
("linux", "AMD64", "linux-x86_64.tar.gz"), # odd but plausible
|
||||
("Linux", "x86_64", "linux-x86_64.tar.gz"), # case-insensitive platform
|
||||
("WIN32", "AMD64", "win-x86_64.zip"), # ALL CAPS platform
|
||||
("darwin", "arm64", "macos-arm64.tar.gz"), # Apple Silicon
|
||||
("darwin", "aarch64", "macos-arm64.tar.gz"), # alternate ARM64 spelling
|
||||
("darwin", "AMD64", "macos-x86_64.tar.gz"), # Intel macOS
|
||||
("Darwin", "x86_64", "macos-x86_64.tar.gz"), # case-insensitive platform
|
||||
])
|
||||
def test_archive_name_accepts_case_variations(platform_key, machine, expected_substring):
|
||||
"""sys.platform / platform.machine() return inconsistent casing across
|
||||
|
|
@ -87,7 +111,7 @@ def test_archive_name_arm64_not_yet_supported(machine):
|
|||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("platform_key", ["darwin", "freebsd", "cygwin", "openbsd"])
|
||||
@pytest.mark.parametrize("platform_key", ["freebsd", "cygwin", "openbsd"])
|
||||
def test_archive_name_rejects_unsupported_platforms(platform_key):
|
||||
"""Same logic — non-Linux/non-Windows platforms must raise, not silently
|
||||
pick one of the two."""
|
||||
|
|
@ -104,7 +128,7 @@ def test_archive_name_rejects_unsupported_platforms(platform_key):
|
|||
def test_binary_entry_rel_covers_every_supported_platform():
|
||||
"""If ARCHIVE_NAME accepts a platform key, BINARY_ENTRY_REL must declare
|
||||
where the executable lives inside the archive for it."""
|
||||
for plat in ["win32", "linux"]:
|
||||
for plat in ["win32", "linux", "darwin"]:
|
||||
ARCHIVE_NAME(plat, "x86_64") # must not raise
|
||||
assert plat in BINARY_ENTRY_REL, (
|
||||
f"ARCHIVE_NAME accepts {plat!r} but BINARY_ENTRY_REL has no entry "
|
||||
|
|
@ -114,10 +138,16 @@ def test_binary_entry_rel_covers_every_supported_platform():
|
|||
|
||||
@pytest.mark.unit
|
||||
def test_binary_entry_rel_extension_matches_platform():
|
||||
"""firefox.exe on Windows, plain `firefox` on Linux."""
|
||||
"""firefox.exe on Windows, plain executable names on Linux and macOS."""
|
||||
assert BINARY_ENTRY_REL["win32"].endswith(".exe")
|
||||
assert not BINARY_ENTRY_REL["linux"].endswith(".exe")
|
||||
assert BINARY_ENTRY_REL["linux"] == "firefox"
|
||||
assert not BINARY_ENTRY_REL["darwin"].endswith(".exe")
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_macos_binary_entry_points_into_app_bundle():
|
||||
assert BINARY_ENTRY_REL["darwin"] == "Firefox.app/Contents/MacOS/firefox"
|
||||
|
||||
|
||||
# ---- RELEASE_URL_TEMPLATE shape ------------------------------------------- #
|
||||
|
|
@ -176,7 +206,7 @@ def test_binary_basename_includes_upstream_version():
|
|||
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("plat", ["win32", "linux"])
|
||||
@pytest.mark.parametrize("plat", ["win32", "linux", "darwin"])
|
||||
def test_archive_name_includes_upstream_version(plat):
|
||||
"""Same desync guard, from the other direction."""
|
||||
assert FIREFOX_UPSTREAM_VERSION in ARCHIVE_NAME(plat, "x86_64")
|
||||
|
|
|
|||
|
|
@ -321,6 +321,8 @@ def test_ensure_binary_accepts_binary_mode_checksums(tmp_path, monkeypatch):
|
|||
|
||||
# Force the platform branch the test mocks:
|
||||
monkeypatch.setattr("sys.platform", "win32")
|
||||
import platform
|
||||
monkeypatch.setattr(platform, "machine", lambda: "AMD64")
|
||||
out = ensure_binary()
|
||||
# No RuntimeError means the parser accepted the `*`-prefixed key.
|
||||
assert out.exists()
|
||||
|
|
@ -418,9 +420,9 @@ def test_github_token_none_when_unset(monkeypatch):
|
|||
# Bonus coverage: unsupported platform raises NotImplementedError before any HTTP
|
||||
@pytest.mark.unit
|
||||
def test_ensure_binary_unsupported_platform_raises(monkeypatch):
|
||||
monkeypatch.setattr("sys.platform", "darwin")
|
||||
monkeypatch.setattr("sys.platform", "freebsd14")
|
||||
import platform
|
||||
monkeypatch.setattr(platform, "machine", lambda: "AMD64")
|
||||
monkeypatch.setattr(platform, "machine", lambda: "x86_64")
|
||||
with pytest.raises(NotImplementedError, match="unsupported platform"):
|
||||
ensure_binary()
|
||||
|
||||
|
|
@ -542,6 +544,96 @@ def test_ensure_binary_missing_entry_after_extract_raises_linux(tmp_path, monkey
|
|||
ensure_binary()
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# macOS platform tests — exercise the .app bundle path inside a tar.gz
|
||||
# archive and both download/cache/error paths on darwin arm64.
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@responses.activate
|
||||
def test_ensure_binary_downloads_and_verifies_macos_arm64(tmp_path, monkeypatch):
|
||||
"""macOS happy path: tar.gz download -> SHA256 check -> extract -> return
|
||||
the Firefox executable inside the .app bundle."""
|
||||
cache = tmp_path / "cache"
|
||||
monkeypatch.setattr("invisible_playwright.download.cache_root", lambda: cache)
|
||||
|
||||
archive_path = tmp_path / "archive.tar.gz"
|
||||
inner = "Firefox.app/Contents/MacOS/firefox"
|
||||
archive_bytes = _make_targz(archive_path, inner, b"MACHO!")
|
||||
archive_sha = hashlib.sha256(archive_bytes).hexdigest()
|
||||
from invisible_playwright.constants import ARCHIVE_NAME
|
||||
asset = ARCHIVE_NAME("darwin", "arm64")
|
||||
|
||||
url_archive = f"https://github.com/feder-cr/invisible_playwright/releases/download/{BINARY_VERSION}/{asset}"
|
||||
url_sums = f"https://github.com/feder-cr/invisible_playwright/releases/download/{BINARY_VERSION}/checksums.txt"
|
||||
|
||||
responses.add(responses.GET, url_archive, body=archive_bytes, status=200,
|
||||
content_type="application/gzip")
|
||||
responses.add(responses.GET, url_sums,
|
||||
body=f"{archive_sha} {asset}\n", status=200)
|
||||
|
||||
monkeypatch.setattr("sys.platform", "darwin")
|
||||
import platform
|
||||
monkeypatch.setattr(platform, "machine", lambda: "arm64")
|
||||
|
||||
path = ensure_binary()
|
||||
assert Path(path).exists()
|
||||
assert Path(path).as_posix().endswith("Firefox.app/Contents/MacOS/firefox")
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_ensure_binary_cache_hit_skips_http_macos(tmp_path, monkeypatch):
|
||||
"""macOS cache hit short-circuits before any HTTP and returns the
|
||||
executable nested inside the cached .app bundle."""
|
||||
cache = tmp_path / "cache"
|
||||
version_dir = cache / BINARY_VERSION / "Firefox.app" / "Contents" / "MacOS"
|
||||
version_dir.mkdir(parents=True)
|
||||
pre_cached = version_dir / "firefox"
|
||||
pre_cached.write_text("cached-content")
|
||||
|
||||
monkeypatch.setattr("invisible_playwright.download.cache_root", lambda: cache)
|
||||
monkeypatch.setattr("sys.platform", "darwin")
|
||||
import platform
|
||||
monkeypatch.setattr(platform, "machine", lambda: "arm64")
|
||||
|
||||
def _fail_get(*args, **kwargs):
|
||||
raise AssertionError("HTTP must not be called on cache hit")
|
||||
monkeypatch.setattr("invisible_playwright.download.requests.get", _fail_get)
|
||||
|
||||
path = ensure_binary()
|
||||
assert path == pre_cached
|
||||
assert path.read_text() == "cached-content"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@responses.activate
|
||||
def test_ensure_binary_missing_entry_after_extract_raises_macos(tmp_path, monkeypatch):
|
||||
"""If the macOS archive extracts without the .app executable, raise
|
||||
instead of returning a broken path."""
|
||||
cache = tmp_path / "cache"
|
||||
monkeypatch.setattr("invisible_playwright.download.cache_root", lambda: cache)
|
||||
|
||||
archive_path = tmp_path / "archive.tar.gz"
|
||||
archive_bytes = _make_targz(archive_path, "Firefox.app/Contents/Info.plist", b"plist")
|
||||
archive_sha = hashlib.sha256(archive_bytes).hexdigest()
|
||||
from invisible_playwright.constants import ARCHIVE_NAME
|
||||
asset = ARCHIVE_NAME("darwin", "arm64")
|
||||
|
||||
url_archive = f"https://github.com/feder-cr/invisible_playwright/releases/download/{BINARY_VERSION}/{asset}"
|
||||
url_sums = f"https://github.com/feder-cr/invisible_playwright/releases/download/{BINARY_VERSION}/checksums.txt"
|
||||
|
||||
responses.add(responses.GET, url_archive, body=archive_bytes, status=200)
|
||||
responses.add(responses.GET, url_sums, body=f"{archive_sha} {asset}\n", status=200)
|
||||
|
||||
monkeypatch.setattr("sys.platform", "darwin")
|
||||
import platform
|
||||
monkeypatch.setattr(platform, "machine", lambda: "arm64")
|
||||
|
||||
with pytest.raises(RuntimeError, match="binary not found after extraction"):
|
||||
ensure_binary()
|
||||
|
||||
|
||||
# ========================================================================== #
|
||||
# _resolve_asset_url — public-repo direct URL vs private-repo API resolution
|
||||
# ========================================================================== #
|
||||
|
|
|
|||
|
|
@ -241,3 +241,16 @@ def test_e12_linux_resolve_headless_without_xvfb_raises_clear_error(monkeypatch)
|
|||
with pytest.raises(RuntimeError, match="Xvfb"):
|
||||
ip._resolve_headless()
|
||||
assert ip._virtual_display is None
|
||||
|
||||
|
||||
@pytest.mark.e2e
|
||||
def test_e13_darwin_resolve_headless_raises_clear_error(monkeypatch):
|
||||
"""E13: macOS can launch visibly, but ``headless=True`` should fail
|
||||
early with a clear message until an invisible backend exists."""
|
||||
import sys as _sys
|
||||
monkeypatch.setattr(_sys, "platform", "darwin")
|
||||
|
||||
ip = InvisiblePlaywright(seed=42, headless=True)
|
||||
with pytest.raises(RuntimeError, match="headless=True is not yet supported on macOS"):
|
||||
ip._resolve_headless()
|
||||
assert ip._virtual_display is None
|
||||
|
|
|
|||
|
|
@ -7,9 +7,10 @@ construction's later ``start()`` call, and ``_LinuxVirtualDisplay`` calls
|
|||
``Xvfb`` — both belong in integration/E2E coverage. The dispatcher's
|
||||
job is pure platform routing, which we patch via ``monkeypatch``.
|
||||
|
||||
Per scope: Windows-specific + platform-agnostic only. We still cover
|
||||
the Linux dispatch branch because instantiating ``_LinuxVirtualDisplay``
|
||||
does no I/O — Xvfb is only spawned in ``start()``, which we never call.
|
||||
Per scope: Windows/Linux hidden-display support plus the macOS guardrail.
|
||||
We still cover the Linux dispatch branch because instantiating
|
||||
``_LinuxVirtualDisplay`` does no I/O — Xvfb is only spawned in
|
||||
``start()``, which we never call.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -51,19 +52,17 @@ def test_make_virtual_display_accepts_linux_variants(monkeypatch):
|
|||
|
||||
@pytest.mark.unit
|
||||
def test_make_virtual_display_raises_on_darwin(monkeypatch):
|
||||
"""macOS is unsupported — the dispatcher must raise with a clear
|
||||
message rather than returning a no-op shim. ``InvisiblePlaywright``
|
||||
relies on this to bail before launching Firefox on a system where
|
||||
the patched binary doesn't exist."""
|
||||
"""macOS can launch visibly, but there is no hidden-display backend
|
||||
for ``headless=True`` yet. The dispatcher should say that plainly."""
|
||||
monkeypatch.setattr(headless.sys, "platform", "darwin")
|
||||
with pytest.raises(RuntimeError, match="Windows and Linux only"):
|
||||
with pytest.raises(RuntimeError, match="headless=True is not yet supported on macOS"):
|
||||
make_virtual_display()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_make_virtual_display_raises_on_unsupported_platform(monkeypatch):
|
||||
monkeypatch.setattr(headless.sys, "platform", "freebsd14")
|
||||
with pytest.raises(RuntimeError, match="Windows and Linux only"):
|
||||
with pytest.raises(RuntimeError, match="headless=True supports Windows and Linux only"):
|
||||
make_virtual_display()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -82,16 +82,24 @@ def test_accept_language_underscore_normalized():
|
|||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_font_metrics_windows_returns_empty(monkeypatch):
|
||||
# FM2: Windows never applies width-scale factors.
|
||||
monkeypatch.setattr(sys, "platform", "win32")
|
||||
assert _font_metrics_for_platform("Arial|1.0,Verdana|0.9,") == ""
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_font_metrics_empty_input_returns_empty():
|
||||
# FM3: Empty input always returns "" regardless of platform.
|
||||
assert _font_metrics_for_platform("") == ""
|
||||
def test_font_metrics_windows_returns_empty(monkeypatch):
|
||||
# FM2: Windows never applies width-scale factors.
|
||||
monkeypatch.setattr(sys, "platform", "win32")
|
||||
assert _font_metrics_for_platform("Arial|1.0,Verdana|0.9,") == ""
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_font_metrics_macos_returns_empty(monkeypatch):
|
||||
# FM2b: macOS follows the same "no width-scale factors" path as
|
||||
# Windows; only Linux needs the generic-family compensation block.
|
||||
monkeypatch.setattr(sys, "platform", "darwin")
|
||||
assert _font_metrics_for_platform("Arial|1.0,Verdana|0.9,") == ""
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_font_metrics_empty_input_returns_empty():
|
||||
# FM3: Empty input always returns "" regardless of platform.
|
||||
assert _font_metrics_for_platform("") == ""
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -143,13 +151,28 @@ def test_canvas_noise_mask_windows_uses_intel_path(monkeypatch):
|
|||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_webgl_extensions_cleared_on_windows(monkeypatch):
|
||||
# WE2
|
||||
monkeypatch.setattr(sys, "platform", "win32")
|
||||
p = generate_profile(seed=42)
|
||||
prefs = translate_profile_to_prefs(p)
|
||||
assert prefs["zoom.stealth.webgl.extensions"] == ""
|
||||
assert prefs["zoom.stealth.webgl2.extensions"] == ""
|
||||
def test_webgl_extensions_cleared_on_windows(monkeypatch):
|
||||
# WE2
|
||||
monkeypatch.setattr(sys, "platform", "win32")
|
||||
p = generate_profile(seed=42)
|
||||
prefs = translate_profile_to_prefs(p)
|
||||
assert prefs["zoom.stealth.webgl.extensions"] == ""
|
||||
assert prefs["zoom.stealth.webgl2.extensions"] == ""
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_macos_uses_non_linux_webgl_defaults(monkeypatch):
|
||||
# WE2b / PG2b: macOS follows the non-Linux branch for renderer/vendor
|
||||
# and extension exposure until native macOS tuning lands.
|
||||
monkeypatch.setattr(sys, "platform", "darwin")
|
||||
p = generate_profile(seed=42, pin={"webgl.msaa_samples": 8})
|
||||
prefs = translate_profile_to_prefs(p)
|
||||
assert prefs["zoom.stealth.webgl.renderer"] == ""
|
||||
assert prefs["zoom.stealth.webgl.vendor"] == ""
|
||||
assert prefs["zoom.stealth.webgl.extensions"] == ""
|
||||
assert prefs["zoom.stealth.webgl2.extensions"] == ""
|
||||
assert prefs["zoom.stealth.webgl.msaa"] == 4
|
||||
assert prefs["webgl.msaa-samples"] == 4
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue