diff --git a/tests/test_e2e.py b/tests/test_e2e.py index e5f4e94..35fad98 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -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