"""Unit tests for launch_persistent_context() and launch_persistent_context_async(). All tests mock playwright to avoid needing a binary. """ from unittest.mock import AsyncMock, MagicMock, patch import pytest from cloakbrowser.config import DEFAULT_VIEWPORT def _make_mock_pw_and_context(): """Create mock sync_playwright chain returning a mock context.""" context = MagicMock() pw = MagicMock() pw.chromium.launch_persistent_context.return_value = context pw_cm = MagicMock() pw_cm.start.return_value = pw return pw_cm, pw, context # --------------------------------------------------------------------------- # Sync: launch_persistent_context() # --------------------------------------------------------------------------- @patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome") @patch("cloakbrowser.browser.maybe_resolve_geoip", return_value=(None, None, None)) def test_persistent_context_args_built(_mock_geoip, _mock_bin): """Stealth args + extra args combined correctly.""" pw_cm, pw, context = _make_mock_pw_and_context() with patch("playwright.sync_api.sync_playwright", return_value=pw_cm): from cloakbrowser.browser import launch_persistent_context launch_persistent_context("/tmp/profile", args=["--disable-gpu"]) call_kwargs = pw.chromium.launch_persistent_context.call_args[1] assert "--disable-gpu" in call_kwargs["args"] # Stealth args present by default assert any(a.startswith("--fingerprint=") for a in call_kwargs["args"]) @patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome") @patch("cloakbrowser.browser.maybe_resolve_geoip", return_value=(None, None, None)) def test_persistent_context_default_viewport(_mock_geoip, _mock_bin): """DEFAULT_VIEWPORT applied when no viewport given.""" pw_cm, pw, context = _make_mock_pw_and_context() with patch("playwright.sync_api.sync_playwright", return_value=pw_cm): from cloakbrowser.browser import launch_persistent_context launch_persistent_context("/tmp/profile") call_kwargs = pw.chromium.launch_persistent_context.call_args[1] assert call_kwargs["viewport"] == DEFAULT_VIEWPORT @patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome") @patch("cloakbrowser.browser.maybe_resolve_geoip", return_value=(None, None, None)) def test_persistent_context_headed_no_viewport(_mock_geoip, _mock_bin): """Headed (headless=False): no_viewport=True instead of DEFAULT_VIEWPORT so the page tracks the real window (avoids the outerWidth < innerWidth tell).""" pw_cm, pw, context = _make_mock_pw_and_context() with patch("playwright.sync_api.sync_playwright", return_value=pw_cm): from cloakbrowser.browser import launch_persistent_context launch_persistent_context("/tmp/profile", headless=False) call_kwargs = pw.chromium.launch_persistent_context.call_args[1] assert call_kwargs.get("no_viewport") is True assert "viewport" not in call_kwargs @patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome") @patch("cloakbrowser.browser.maybe_resolve_geoip", return_value=(None, None, None)) def test_persistent_context_custom_viewport(_mock_geoip, _mock_bin): """Custom viewport overrides DEFAULT_VIEWPORT.""" pw_cm, pw, context = _make_mock_pw_and_context() custom = {"width": 1280, "height": 720} with patch("playwright.sync_api.sync_playwright", return_value=pw_cm): from cloakbrowser.browser import launch_persistent_context launch_persistent_context("/tmp/profile", viewport=custom) call_kwargs = pw.chromium.launch_persistent_context.call_args[1] assert call_kwargs["viewport"] == custom @patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome") @patch("cloakbrowser.browser.maybe_resolve_geoip", return_value=(None, None, None)) def test_persistent_context_user_agent(_mock_geoip, _mock_bin): """user_agent forwarded to launch_persistent_context().""" pw_cm, pw, context = _make_mock_pw_and_context() with patch("playwright.sync_api.sync_playwright", return_value=pw_cm): from cloakbrowser.browser import launch_persistent_context launch_persistent_context("/tmp/profile", user_agent="Custom/1.0") call_kwargs = pw.chromium.launch_persistent_context.call_args[1] assert call_kwargs["user_agent"] == "Custom/1.0" @patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome") def test_persistent_context_locale_and_timezone(_mock_bin): """Timezone and locale flow to binary args only, NOT to CDP context kwargs.""" pw_cm, pw, context = _make_mock_pw_and_context() with patch("playwright.sync_api.sync_playwright", return_value=pw_cm): from cloakbrowser.browser import launch_persistent_context launch_persistent_context("/tmp/profile", timezone="Asia/Tokyo", locale="ja-JP") call_kwargs = pw.chromium.launch_persistent_context.call_args[1] # Binary args (native, undetectable) assert "--fingerprint-timezone=Asia/Tokyo" in call_kwargs["args"] assert "--lang=ja-JP" in call_kwargs["args"] # NOT in context kwargs (would trigger detectable CDP emulation) assert "timezone_id" not in call_kwargs assert "locale" not in call_kwargs @patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome") @patch("cloakbrowser.browser.maybe_resolve_geoip", return_value=(None, None, None)) def test_persistent_context_color_scheme(_mock_geoip, _mock_bin): """color_scheme forwarded correctly.""" pw_cm, pw, context = _make_mock_pw_and_context() with patch("playwright.sync_api.sync_playwright", return_value=pw_cm): from cloakbrowser.browser import launch_persistent_context launch_persistent_context("/tmp/profile", color_scheme="dark") call_kwargs = pw.chromium.launch_persistent_context.call_args[1] assert call_kwargs["color_scheme"] == "dark" @patch("cloakbrowser.browser.maybe_resolve_geoip", return_value=("Europe/Berlin", "de-DE", "5.6.7.8")) @patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome") def test_persistent_context_geoip(_mock_bin, _mock_geoip): """geoip fills missing tz/locale — flows to binary args, not CDP context.""" pw_cm, pw, context = _make_mock_pw_and_context() with patch("playwright.sync_api.sync_playwright", return_value=pw_cm): from cloakbrowser.browser import launch_persistent_context launch_persistent_context("/tmp/profile", proxy="http://proxy:8080", geoip=True) call_kwargs = pw.chromium.launch_persistent_context.call_args[1] # Binary args assert "--fingerprint-timezone=Europe/Berlin" in call_kwargs["args"] assert "--lang=de-DE" in call_kwargs["args"] # NOT in context kwargs assert "timezone_id" not in call_kwargs assert "locale" not in call_kwargs @patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome") def test_persistent_context_timezone_id_alias(_mock_bin): """timezone_id kwarg accepted as alias for timezone.""" pw_cm, pw, context = _make_mock_pw_and_context() with patch("playwright.sync_api.sync_playwright", return_value=pw_cm): from cloakbrowser.browser import launch_persistent_context launch_persistent_context("/tmp/profile", timezone_id="Europe/Paris") call_kwargs = pw.chromium.launch_persistent_context.call_args[1] assert "--fingerprint-timezone=Europe/Paris" in call_kwargs["args"] assert "timezone_id" not in call_kwargs @patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome") @patch("cloakbrowser.browser.maybe_resolve_geoip", return_value=(None, None, None)) def test_persistent_context_close_stops_pw(_mock_geoip, _mock_bin): """context.close() also calls pw.stop().""" pw_cm, pw, context = _make_mock_pw_and_context() original_close = context.close with patch("playwright.sync_api.sync_playwright", return_value=pw_cm): from cloakbrowser.browser import launch_persistent_context ctx = launch_persistent_context("/tmp/profile") ctx.close() original_close.assert_called_once() pw.stop.assert_called_once() @patch("cloakbrowser.config.get_platform_tag", return_value="darwin-arm64") @patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome") @patch("cloakbrowser.browser.maybe_resolve_geoip", return_value=(None, None, None)) def test_persistent_context_proxy_string(_mock_geoip, _mock_bin, _mock_platform): """Proxy string parsed and passed (unsupported platform → Playwright dict).""" pw_cm, pw, context = _make_mock_pw_and_context() with patch("playwright.sync_api.sync_playwright", return_value=pw_cm): from cloakbrowser.browser import launch_persistent_context launch_persistent_context("/tmp/profile", proxy="http://user:pass@proxy:8080") call_kwargs = pw.chromium.launch_persistent_context.call_args[1] assert call_kwargs["proxy"]["server"] == "http://proxy:8080" assert call_kwargs["proxy"]["username"] == "user" assert call_kwargs["proxy"]["password"] == "pass" @patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome") @patch("cloakbrowser.browser.maybe_resolve_geoip", return_value=(None, None, None)) def test_persistent_context_proxy_dict(_mock_geoip, _mock_bin): """Proxy dict passed through.""" pw_cm, pw, context = _make_mock_pw_and_context() proxy_dict = {"server": "http://proxy:8080", "bypass": ".google.com"} with patch("playwright.sync_api.sync_playwright", return_value=pw_cm): from cloakbrowser.browser import launch_persistent_context launch_persistent_context("/tmp/profile", proxy=proxy_dict) call_kwargs = pw.chromium.launch_persistent_context.call_args[1] assert call_kwargs["proxy"] == proxy_dict # --------------------------------------------------------------------------- # Async: launch_persistent_context_async() # --------------------------------------------------------------------------- def _make_mock_async_pw_and_context(): """Create mock async_playwright chain returning a mock context.""" context = AsyncMock() pw = AsyncMock() pw.chromium.launch_persistent_context.return_value = context pw_cm = AsyncMock() pw_cm.start.return_value = pw return pw_cm, pw, context @pytest.mark.asyncio @patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome") @patch("cloakbrowser.browser.maybe_resolve_geoip", return_value=(None, None, None)) async def test_persistent_context_async_args_built(_mock_geoip, _mock_bin): """Async launch builds args correctly.""" pw_cm, pw, context = _make_mock_async_pw_and_context() with patch("playwright.async_api.async_playwright", return_value=pw_cm): from cloakbrowser.browser import launch_persistent_context_async await launch_persistent_context_async("/tmp/profile", args=["--disable-gpu"]) call_kwargs = pw.chromium.launch_persistent_context.call_args[1] assert "--disable-gpu" in call_kwargs["args"] assert any(a.startswith("--fingerprint=") for a in call_kwargs["args"]) @pytest.mark.asyncio @patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome") @patch("cloakbrowser.browser.maybe_resolve_geoip", return_value=(None, None, None)) async def test_persistent_context_async_close_stops_pw(_mock_geoip, _mock_bin): """await context.close() calls await pw.stop().""" pw_cm, pw, context = _make_mock_async_pw_and_context() original_close = context.close with patch("playwright.async_api.async_playwright", return_value=pw_cm): from cloakbrowser.browser import launch_persistent_context_async ctx = await launch_persistent_context_async("/tmp/profile") await ctx.close() original_close.assert_called_once() pw.stop.assert_called_once() @pytest.mark.asyncio @patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome") async def test_persistent_context_async_timezone_id_alias(_mock_bin): """timezone_id kwarg accepted as alias in async path.""" pw_cm, pw, context = _make_mock_async_pw_and_context() with patch("playwright.async_api.async_playwright", return_value=pw_cm): from cloakbrowser.browser import launch_persistent_context_async await launch_persistent_context_async("/tmp/profile", timezone_id="Europe/Paris") call_kwargs = pw.chromium.launch_persistent_context.call_args[1] assert "--fingerprint-timezone=Europe/Paris" in call_kwargs["args"] assert "timezone_id" not in call_kwargs @patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome") @patch("cloakbrowser.browser.maybe_resolve_geoip", return_value=(None, None, None)) @patch("cloakbrowser.browser.seed_widevine_hint") def test_persistent_context_seeds_widevine(_mock_seed, _mock_geoip, _mock_bin): """Sync persistent launch seeds the Widevine hint with the profile path.""" pw_cm, pw, context = _make_mock_pw_and_context() with patch("playwright.sync_api.sync_playwright", return_value=pw_cm): from cloakbrowser.browser import launch_persistent_context launch_persistent_context("/tmp/profile") _mock_seed.assert_called_once_with("/tmp/profile", "/fake/chrome") @pytest.mark.asyncio @patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome") @patch("cloakbrowser.browser.maybe_resolve_geoip", return_value=(None, None, None)) @patch("cloakbrowser.browser.seed_widevine_hint") async def test_persistent_context_async_seeds_widevine(_mock_seed, _mock_geoip, _mock_bin): """Async persistent launch seeds the Widevine hint with the profile path.""" pw_cm, pw, context = _make_mock_async_pw_and_context() with patch("playwright.async_api.async_playwright", return_value=pw_cm): from cloakbrowser.browser import launch_persistent_context_async await launch_persistent_context_async("/tmp/profile") _mock_seed.assert_called_once_with("/tmp/profile", "/fake/chrome")