mirror of
https://github.com/feder-cr/invisible_playwright.git
synced 2026-06-22 09:18:06 +02:00
Merge d09200f01d into db1d3ec359
This commit is contained in:
commit
34d91b3c4b
10 changed files with 240 additions and 50 deletions
|
|
@ -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