mirror of
https://github.com/feder-cr/invisible_playwright.git
synced 2026-06-07 08:35:12 +02:00
test(e2e): add 9 launcher lifecycle tests for Phase 9
Five test the constructor only (seed handling, eager profile build, fail-fast pin validation) and always run. Four spin up the patched Firefox and exercise the full `with InvisiblePlaywright(...)` lifecycle, gated on a locally cached binary so CI without the binary skips cleanly. All 230 tests pass on Windows with the binary fetched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9c8d24408b
commit
234fe7e406
1 changed files with 143 additions and 0 deletions
143
tests/test_e2e.py
Normal file
143
tests/test_e2e.py
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
"""E2E tests for the launcher lifecycle.
|
||||
|
||||
Tests requiring the patched Firefox binary are gated behind the
|
||||
``firefox_binary`` fixture, which skips the test cleanly when the
|
||||
binary is not cached locally and cannot be downloaded (e.g. no
|
||||
network or no release token). The constructor-only tests (seed
|
||||
handling) do not need a binary and always run.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
from invisible_playwright import InvisiblePlaywright
|
||||
from invisible_playwright.constants import BINARY_ENTRY_REL
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def firefox_binary():
|
||||
"""Locate the patched Firefox binary or skip the calling test.
|
||||
|
||||
We do NOT trigger a network download here: ``ensure_binary`` would
|
||||
pull a multi-hundred-megabyte archive from a private release,
|
||||
which is not appropriate inside a unit/E2E test run. Instead we
|
||||
look for an already-cached binary; if missing we skip.
|
||||
"""
|
||||
if sys.platform not in BINARY_ENTRY_REL:
|
||||
pytest.skip(f"unsupported platform: {sys.platform}")
|
||||
from invisible_playwright.download import cache_dir_for_version
|
||||
entry = cache_dir_for_version() / BINARY_ENTRY_REL[sys.platform]
|
||||
if not entry.exists():
|
||||
pytest.skip(
|
||||
"patched Firefox binary not cached; run `invisible-playwright fetch` "
|
||||
"to enable E2E tests"
|
||||
)
|
||||
return str(entry)
|
||||
|
||||
|
||||
# ────────────────────────────────────────────────────────────────────
|
||||
# Constructor-only tests (no browser launch required)
|
||||
# ────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.e2e
|
||||
def test_e3_seed_is_accessible():
|
||||
"""E3: explicit seed is stored on the instance after construction."""
|
||||
ip = InvisiblePlaywright(seed=42)
|
||||
assert ip.seed == 42
|
||||
|
||||
|
||||
@pytest.mark.e2e
|
||||
def test_e4_random_seed_when_none():
|
||||
"""E4: omitting seed → a fresh positive int31 is chosen."""
|
||||
ip = InvisiblePlaywright()
|
||||
assert isinstance(ip.seed, int)
|
||||
assert ip.seed > 0
|
||||
assert ip.seed < 2**31
|
||||
|
||||
|
||||
@pytest.mark.e2e
|
||||
def test_e4b_random_seed_varies_across_instances():
|
||||
"""E4 extension: two no-seed instances pick different seeds with
|
||||
overwhelming probability. ``secrets.randbits(31)`` collisions are
|
||||
~1 in 2 billion, so we accept the negligible flake risk."""
|
||||
seeds = {InvisiblePlaywright().seed for _ in range(5)}
|
||||
assert len(seeds) > 1
|
||||
|
||||
|
||||
@pytest.mark.e2e
|
||||
def test_e6_profile_built_eagerly():
|
||||
"""The constructor materializes the Profile up front so seed-driven
|
||||
fields are accessible without launching a browser. Guards against
|
||||
a regression where Profile generation is deferred into ``__enter__``
|
||||
and an invalid pin therefore raises only at launch time.
|
||||
"""
|
||||
ip = InvisiblePlaywright(seed=42)
|
||||
assert ip._profile is not None
|
||||
assert ip._profile.seed == 42
|
||||
|
||||
|
||||
@pytest.mark.e2e
|
||||
def test_e7_invalid_pin_raises_in_constructor():
|
||||
"""Invalid pin keys fail fast at construction, not at __enter__."""
|
||||
with pytest.raises(ValueError):
|
||||
InvisiblePlaywright(seed=42, pin={"not_a_real_field": 1})
|
||||
|
||||
|
||||
# ────────────────────────────────────────────────────────────────────
|
||||
# Lifecycle tests (require Firefox binary)
|
||||
# ────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.e2e
|
||||
def test_e1_sync_context_manager_lifecycle(firefox_binary):
|
||||
"""E1: ``with InvisiblePlaywright(...) as browser`` yields a real
|
||||
Playwright Browser object that exposes ``new_context``."""
|
||||
with InvisiblePlaywright(seed=42, binary_path=firefox_binary) as browser:
|
||||
assert browser is not None
|
||||
assert hasattr(browser, "new_context")
|
||||
assert callable(browser.new_context)
|
||||
|
||||
|
||||
@pytest.mark.e2e
|
||||
def test_e2_create_context_and_page(firefox_binary):
|
||||
"""E2: a context spawned from the patched browser can create a page."""
|
||||
with InvisiblePlaywright(seed=42, binary_path=firefox_binary) as browser:
|
||||
ctx = browser.new_context()
|
||||
try:
|
||||
page = ctx.new_page()
|
||||
assert page is not None
|
||||
assert hasattr(page, "goto")
|
||||
finally:
|
||||
ctx.close()
|
||||
|
||||
|
||||
@pytest.mark.e2e
|
||||
def test_e5_teardown_does_not_raise(firefox_binary):
|
||||
"""E5: ``__exit__`` cleans up Playwright + virtual display without raising."""
|
||||
ip = InvisiblePlaywright(seed=42, binary_path=firefox_binary)
|
||||
browser = ip.__enter__()
|
||||
try:
|
||||
assert browser is not None
|
||||
finally:
|
||||
ip.__exit__(None, None, None)
|
||||
# second teardown is idempotent
|
||||
ip.__exit__(None, None, None)
|
||||
|
||||
|
||||
@pytest.mark.e2e
|
||||
def test_e8_new_context_defaults_from_profile(firefox_binary):
|
||||
"""new_context() without kwargs should inherit profile-derived
|
||||
viewport/screen. Guards the monkey-patch installed in __enter__."""
|
||||
with InvisiblePlaywright(seed=42, binary_path=firefox_binary) as browser:
|
||||
ctx = browser.new_context()
|
||||
try:
|
||||
page = ctx.new_page()
|
||||
vp = page.viewport_size
|
||||
assert vp is not None
|
||||
assert vp["width"] > 0
|
||||
assert vp["height"] > 0
|
||||
finally:
|
||||
ctx.close()
|
||||
Loading…
Add table
Add a link
Reference in a new issue