test(e2e): add 4 Linux launcher-routing tests for Phase 9

E9-E12 exercise the launcher's Linux code paths without spawning a
real Firefox binary or Xvfb. They monkeypatch ``sys.platform`` and
stub ``make_virtual_display`` / ``_binary_on_path`` so the tests run
on any host:

- E9 ``_build_prefs(headless=True)`` on Linux passes
  ``virtual_display=False`` to the translator, so the Win32-only
  ``security.sandbox.gpu.level`` workaround never leaks into Linux
  prefs (Xvfb handles window hiding instead).
- E10 ``_resolve_headless`` on Linux + headless=True invokes the
  dispatcher and stores the returned object on ``self._virtual_display``.
- E11 ``_teardown`` stops the Linux virtual display, clears the
  reference, and is idempotent on a second call.
- E12 With Xvfb missing from PATH, ``_resolve_headless`` raises a
  clear ``RuntimeError`` mentioning ``Xvfb`` instead of a cryptic
  FileNotFoundError.

Suite on Linux/WSL: 286 passed, 5 skipped (4 binary-gated E2E
lifecycle tests + 1 Win32 ctypes test). Binary-gated E1/E2/E5/E8
remain ready to run on Linux once the patched Firefox tar.gz is
fetched locally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
chrissbaumann 2026-05-14 14:07:03 +02:00
parent b8139c2873
commit 70c1ca464f

View file

@ -141,3 +141,103 @@ def test_e8_new_context_defaults_from_profile(firefox_binary):
assert vp["height"] > 0
finally:
ctx.close()
# ────────────────────────────────────────────────────────────────────
# Linux-specific lifecycle tests (no Firefox binary required).
#
# These exercise the launcher's Linux code paths without spawning real
# Firefox or Xvfb. They monkeypatch ``sys.platform`` and (where needed)
# the ``make_virtual_display`` dispatcher so the tests run on any host
# — including Windows hosts that ship the production CI for this repo.
# ────────────────────────────────────────────────────────────────────
@pytest.mark.e2e
def test_e9_linux_build_prefs_omits_windows_sandbox_key(monkeypatch):
"""E9: ``_build_prefs(headless=True)`` on Linux must pass
``virtual_display=False`` to the prefs translator. The Win32-only
``security.sandbox.gpu.level`` workaround targets the alt-desktop
GPU sandbox bug and MUST NOT leak into Linux prefs, where Xvfb
handles window hiding instead."""
import sys as _sys
monkeypatch.setattr(_sys, "platform", "linux")
ip = InvisiblePlaywright(seed=42, headless=True)
prefs = ip._build_prefs()
assert "security.sandbox.gpu.level" not in prefs
@pytest.mark.e2e
def test_e10_linux_resolve_headless_invokes_xvfb_dispatcher(monkeypatch):
"""E10: ``_resolve_headless`` with ``headless=True`` on Linux must
call ``make_virtual_display().start()`` and store the result on
``self._virtual_display``. We stub the dispatcher so no real Xvfb
is spawned the dispatcher's platform routing is covered separately
in ``test_headless.py``."""
import sys as _sys
monkeypatch.setattr(_sys, "platform", "linux")
events: list[str] = []
class _FakeDisplay:
def start(self) -> None:
events.append("start")
def stop(self) -> None:
events.append("stop")
from invisible_playwright import launcher as _l
monkeypatch.setattr(_l, "make_virtual_display", lambda: _FakeDisplay())
ip = InvisiblePlaywright(seed=42, headless=True)
result = ip._resolve_headless()
assert result is False
assert events == ["start"]
assert ip._virtual_display is not None
@pytest.mark.e2e
def test_e11_linux_teardown_stops_virtual_display_and_is_idempotent(monkeypatch):
"""E11: ``_teardown`` stops the Linux virtual display, clears the
reference, and a second invocation is a no-op. Guards the cleanup
path used by ``__exit__`` so a failed ``__enter__`` cannot leak Xvfb."""
import sys as _sys
monkeypatch.setattr(_sys, "platform", "linux")
stops: list[bool] = []
class _FakeDisplay:
def start(self) -> None:
pass
def stop(self) -> None:
stops.append(True)
from invisible_playwright import launcher as _l
monkeypatch.setattr(_l, "make_virtual_display", lambda: _FakeDisplay())
ip = InvisiblePlaywright(seed=42, headless=True)
ip._resolve_headless()
ip._teardown()
assert stops == [True]
assert ip._virtual_display is None
ip._teardown()
assert stops == [True]
@pytest.mark.e2e
def test_e12_linux_resolve_headless_without_xvfb_raises_clear_error(monkeypatch):
"""E12: On Linux with ``headless=True`` and ``Xvfb`` missing from
``PATH``, ``_resolve_headless`` must surface a clear, actionable
``RuntimeError`` instead of a cryptic FileNotFoundError. Verifies
the early-check path in ``_LinuxVirtualDisplay.start``."""
import sys as _sys
monkeypatch.setattr(_sys, "platform", "linux")
from invisible_playwright import _headless as _h
monkeypatch.setattr(_h, "_binary_on_path", lambda name: False)
ip = InvisiblePlaywright(seed=42, headless=True)
with pytest.raises(RuntimeError, match="Xvfb"):
ip._resolve_headless()
assert ip._virtual_display is None