invisible_playwright/tests/test_async_api.py
chrissbaumann 4f6254469e test(launcher, headless, async_api): add 32 Phase 10 gap-coverage tests
Final sweep adds unit tests for the modules left at 0% direct coverage
after Phases 1-9:

- launcher._tz_env: 7 tests covering the IANA -> POSIX mapping
  including the Phoenix / Honolulu no-DST regression cases
- launcher._humanize_max_seconds, _default_context_kwargs: 11 tests
  on the constructor-side helpers (no browser launch)
- _headless.make_virtual_display dispatcher + _WindowsVirtualDesktop
  init/teardown: 8 tests (Linux dispatch branch covered without
  spawning Xvfb, since __init__ does no I/O)
- async_api.InvisiblePlaywright constructor parity with sync: 8 tests
  guarding against drift between the two APIs

Suite: 230 -> 264 passing. Pyramid stays clean: 243 unit / 12
integration / 9 e2e.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:59:47 +02:00

83 lines
2.9 KiB
Python

"""Constructor-parity tests for the async ``InvisiblePlaywright``.
The async API mirrors the sync launcher (same prefs pipeline, same
profile generation, same proxy handling). The only async-specific
surface is ``__aenter__`` / ``__aexit__`` and an awaitable ``new_page``
patch — both require a real Firefox binary to exercise meaningfully and
are covered by the sync E2E tests via parity arguments.
What we test here without launching a browser: the constructor builds
the same eager Profile, clamps the seed identically, and surfaces pin
validation errors at construction time. These guards keep the async
class from silently drifting away from the sync class as features land.
"""
from __future__ import annotations
import pytest
from invisible_playwright.async_api import InvisiblePlaywright as AsyncIP
from invisible_playwright.launcher import InvisiblePlaywright as SyncIP
@pytest.mark.unit
def test_async_explicit_seed_is_stored():
ip = AsyncIP(seed=42)
assert ip.seed == 42
@pytest.mark.unit
def test_async_random_seed_is_positive_int31():
"""Same int31 contract as sync: the C++ side rejects ``seed <= 0`` and
a 32-bit value risks the high bit looking negative."""
ip = AsyncIP()
assert isinstance(ip.seed, int)
assert 0 < ip.seed < 2**31
@pytest.mark.unit
def test_async_random_seed_varies_across_instances():
seeds = {AsyncIP().seed for _ in range(5)}
assert len(seeds) > 1
@pytest.mark.unit
def test_async_profile_built_eagerly_in_constructor():
"""Pin validation must fire before ``__aenter__`` — otherwise a user
only learns their pin is wrong when the browser launch starts."""
ip = AsyncIP(seed=42)
assert ip._profile is not None
assert ip._profile.seed == 42
@pytest.mark.unit
def test_async_invalid_pin_raises_in_constructor():
with pytest.raises(ValueError):
AsyncIP(seed=42, pin={"not_a_real_field": 1})
@pytest.mark.unit
def test_async_and_sync_share_seed_for_same_input():
"""Same seed → identical Profile across the two APIs. Both lean on
``generate_profile(seed)``; if they diverge it means one of them
started doing extra sampling."""
seed = 12345
a = AsyncIP(seed=seed)
s = SyncIP(seed=seed)
assert a._profile == s._profile
@pytest.mark.unit
def test_async_seed_coerced_from_float():
"""``int(seed)`` truncation — matches sync clamping behaviour."""
ip = AsyncIP(seed=42.9)
assert ip.seed == 42
@pytest.mark.unit
def test_async_default_context_kwargs_match_sync():
"""The two ``_default_context_kwargs`` implementations must produce
the same dict for the same inputs. Guards against the async copy
drifting away when sync adds new keys."""
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()