This commit is contained in:
Danny Ward 2026-06-06 00:54:44 -04:00 committed by GitHub
commit 34d91b3c4b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 240 additions and 50 deletions

View file

@ -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()

View file

@ -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")

View file

@ -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
# ========================================================================== #

View file

@ -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

View file

@ -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()

View file

@ -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
# ──────────────────────────────────────────────────────────────────────