From 04255cf412021ed22816e4cf8474105664859a76 Mon Sep 17 00:00:00 2001 From: CloakHQ Date: Tue, 10 Mar 2026 03:29:38 +0100 Subject: [PATCH] fix: use binary flags for timezone/locale instead of detectable CDP emulation - Remove locale and timezone_id from Playwright context kwargs (CDP) - Pass timezone via --fingerprint-timezone binary flag (process-wide) - Pass locale via --lang + --fingerprint-locale binary flags - Accept both timezone and timezone_id param names silently (no deprecation) - Update all wrapper tests to verify binary args, not CDP context params --- cloakbrowser/browser.py | 48 +++++++++++---------------- js/src/args.ts | 11 ++++--- js/src/playwright.ts | 23 ++++++------- js/src/types.ts | 2 +- js/tests/config.test.ts | 27 +++++++++------ js/tests/launch.test.ts | 21 ++++++------ tests/test_build_args.py | 56 ++++++++++++++------------------ tests/test_launch_context.py | 52 ++++++++++++++--------------- tests/test_persistent_context.py | 47 +++++++++++++-------------- 9 files changed, 135 insertions(+), 152 deletions(-) diff --git a/cloakbrowser/browser.py b/cloakbrowser/browser.py index b82a41f..27d57dd 100644 --- a/cloakbrowser/browser.py +++ b/cloakbrowser/browser.py @@ -16,7 +16,6 @@ from __future__ import annotations import logging import os -import warnings from typing import Any, Literal, TypedDict from urllib.parse import unquote, urlparse, urlunparse @@ -26,10 +25,9 @@ from .download import ensure_binary logger = logging.getLogger("cloakbrowser") -def _migrate_timezone_id(timezone: str | None, kwargs: dict[str, Any]) -> str | None: - """Pop deprecated timezone_id from kwargs, warn, return resolved timezone.""" +def _resolve_timezone(timezone: str | None, kwargs: dict[str, Any]) -> str | None: + """Accept both timezone and timezone_id — either works, no warning.""" if "timezone_id" in kwargs: - warnings.warn("timezone_id is deprecated, use timezone instead", FutureWarning, stacklevel=3) if timezone is None: timezone = kwargs.pop("timezone_id") else: @@ -279,7 +277,7 @@ def launch_persistent_context( """ sync_playwright = _import_sync_playwright(_resolve_backend(backend)) - timezone = _migrate_timezone_id(timezone, kwargs) + timezone = _resolve_timezone(timezone, kwargs) binary_path = ensure_binary() timezone, locale = _maybe_resolve_geoip(geoip, proxy, timezone, locale) @@ -291,14 +289,12 @@ def launch_persistent_context( user_data_dir, ) + # locale and timezone are set via binary flags (--lang, --fingerprint-timezone) + # — NOT via Playwright context kwargs which use detectable CDP emulation. context_kwargs: dict[str, Any] = {} if user_agent: context_kwargs["user_agent"] = user_agent context_kwargs["viewport"] = viewport or DEFAULT_VIEWPORT - if locale: - context_kwargs["locale"] = locale - if timezone: - context_kwargs["timezone_id"] = timezone if color_scheme: context_kwargs["color_scheme"] = color_scheme context_kwargs.update(kwargs) @@ -394,7 +390,7 @@ async def launch_persistent_context_async( """ async_playwright = _import_async_playwright(_resolve_backend(backend)) - timezone = _migrate_timezone_id(timezone, kwargs) + timezone = _resolve_timezone(timezone, kwargs) binary_path = ensure_binary() timezone, locale = _maybe_resolve_geoip(geoip, proxy, timezone, locale) @@ -406,14 +402,12 @@ async def launch_persistent_context_async( user_data_dir, ) + # locale and timezone are set via binary flags (--lang, --fingerprint-timezone) + # — NOT via Playwright context kwargs which use detectable CDP emulation. context_kwargs: dict[str, Any] = {} if user_agent: context_kwargs["user_agent"] = user_agent context_kwargs["viewport"] = viewport or DEFAULT_VIEWPORT - if locale: - context_kwargs["locale"] = locale - if timezone: - context_kwargs["timezone_id"] = timezone if color_scheme: context_kwargs["color_scheme"] = color_scheme context_kwargs.update(kwargs) @@ -491,25 +485,21 @@ def launch_context( Returns: Playwright BrowserContext object. """ - timezone = _migrate_timezone_id(timezone, kwargs) + timezone = _resolve_timezone(timezone, kwargs) # Resolve geoip BEFORE launch() to avoid double-resolution and ensure - # resolved values flow to both binary flags AND context params + # resolved values flow to binary flags timezone, locale = _maybe_resolve_geoip(geoip, proxy, timezone, locale) - # Skip --fingerprint-timezone binary flag: it only applies to the default - # context and interferes with Playwright's timezone_id on new contexts. - # Timezone is set via browser.new_context(timezone_id=...) below instead. + # --fingerprint-timezone is process-wide (reads CommandLine in renderer), + # so it applies to ALL contexts, not just the default one. + # locale and timezone are set via binary flags only — no CDP emulation. browser = launch(headless=headless, proxy=proxy, args=args, stealth_args=stealth_args, - timezone=None, locale=locale, backend=backend) + timezone=timezone, locale=locale, backend=backend) context_kwargs: dict[str, Any] = {} if user_agent: context_kwargs["user_agent"] = user_agent context_kwargs["viewport"] = viewport or DEFAULT_VIEWPORT - if locale: - context_kwargs["locale"] = locale - if timezone: - context_kwargs["timezone_id"] = timezone if color_scheme: context_kwargs["color_scheme"] = color_scheme context_kwargs.update(kwargs) @@ -646,11 +636,11 @@ def _build_args( logger.debug("Arg override: %s -> %s", seen[key], flag) seen[key] = flag if locale: - key = "--lang" - flag = f"{key}={locale}" - if key in seen: - logger.debug("Arg override: %s -> %s", seen[key], flag) - seen[key] = flag + for key in ("--lang", "--fingerprint-locale"): + flag = f"{key}={locale}" + if key in seen: + logger.debug("Arg override: %s -> %s", seen[key], flag) + seen[key] = flag return list(seen.values()) diff --git a/js/src/args.ts b/js/src/args.ts index 0df410a..0318ac8 100644 --- a/js/src/args.ts +++ b/js/src/args.ts @@ -38,12 +38,13 @@ export function buildArgs(options: LaunchOptions): string[] { seen.set(key, flag); } if (options.locale) { - const key = "--lang"; - const flag = `${key}=${options.locale}`; - if (seen.has(key)) { - if (DEBUG) console.debug(`[cloakbrowser] Arg override: ${seen.get(key)} -> ${flag}`); + for (const k of ["--lang", "--fingerprint-locale"] as const) { + const flag = `${k}=${options.locale}`; + if (seen.has(k)) { + if (DEBUG) console.debug(`[cloakbrowser] Arg override: ${seen.get(k)} -> ${flag}`); + } + seen.set(k, flag); } - seen.set(key, flag); } return [...seen.values()]; } diff --git a/js/src/playwright.ts b/js/src/playwright.ts index 673c369..4ddf501 100644 --- a/js/src/playwright.ts +++ b/js/src/playwright.ts @@ -11,10 +11,9 @@ import { ensureBinary } from "./download.js"; import { parseProxyUrl } from "./proxy.js"; import { maybeResolveGeoip } from "./geoip.js"; -/** @internal Migrate deprecated timezoneId → timezone, warn once. Exported for testing. */ -export function migrateTimezoneId(options: T): T { +/** @internal Accept both timezone and timezoneId — either works, no warning. Exported for testing. */ +export function resolveTimezone(options: T): T { if (options.timezoneId != null) { - console.warn("[cloakbrowser] timezoneId is deprecated, use timezone instead"); const merged = { ...options, timezone: options.timezone ?? options.timezoneId }; delete (merged as any).timezoneId; return merged; @@ -86,21 +85,19 @@ export async function launch(options: LaunchOptions = {}): Promise { export async function launchContext( options: LaunchContextOptions = {} ): Promise { - options = migrateTimezoneId(options); + options = resolveTimezone(options); // Resolve geoip BEFORE launch() to avoid double-resolution const resolved = await maybeResolveGeoip(options); - // Skip --fingerprint-timezone binary flag: it only applies to the default - // context and interferes with Playwright's timezoneId on new contexts. - // Timezone is set via browser.newContext(timezoneId: ...) below instead. - const browser = await launch({ ...options, ...resolved, geoip: false, timezone: undefined }); + // --fingerprint-timezone is process-wide (reads CommandLine in renderer), + // so it applies to ALL contexts, not just the default one. + // locale and timezone are set via binary flags only — no CDP emulation. + const browser = await launch({ ...options, ...resolved, geoip: false }); let context: BrowserContext; try { context = await browser.newContext({ ...(options.userAgent ? { userAgent: options.userAgent } : {}), viewport: options.viewport ?? DEFAULT_VIEWPORT, - ...(resolved.locale ? { locale: resolved.locale } : {}), - ...(resolved.timezone ? { timezoneId: resolved.timezone } : {}), ...(options.colorScheme ? { colorScheme: options.colorScheme } : {}), }); } catch (err) { @@ -153,13 +150,15 @@ export async function launchContext( export async function launchPersistentContext( options: LaunchPersistentContextOptions ): Promise { - options = migrateTimezoneId(options); + options = resolveTimezone(options); const { chromium } = await import("playwright-core"); const binaryPath = process.env.CLOAKBROWSER_BINARY_PATH || (await ensureBinary()); const resolved = await maybeResolveGeoip(options); const args = buildArgs({ ...options, ...resolved }); + // locale and timezone are set via binary flags (--lang, --fingerprint-timezone) + // — NOT via Playwright context kwargs which use detectable CDP emulation. const context = await chromium.launchPersistentContext(options.userDataDir, { executablePath: binaryPath, headless: options.headless ?? true, @@ -170,8 +169,6 @@ export async function launchPersistentContext( : {}), ...(options.userAgent ? { userAgent: options.userAgent } : {}), viewport: options.viewport ?? DEFAULT_VIEWPORT, - ...(resolved.locale ? { locale: resolved.locale } : {}), - ...(resolved.timezone ? { timezoneId: resolved.timezone } : {}), ...(options.colorScheme ? { colorScheme: options.colorScheme } : {}), ...options.launchOptions, }); diff --git a/js/src/types.ts b/js/src/types.ts index a539c1d..6ca60af 100644 --- a/js/src/types.ts +++ b/js/src/types.ts @@ -39,7 +39,7 @@ export interface LaunchContextOptions extends LaunchOptions { viewport?: { width: number; height: number }; /** Browser locale, e.g. "en-US". */ locale?: string; - /** @deprecated Use `timezone` (inherited from LaunchOptions) instead. */ + /** IANA timezone — alias for `timezone`. Either works. */ timezoneId?: string; /** Color scheme preference — 'light', 'dark', or 'no-preference'. */ colorScheme?: "light" | "dark" | "no-preference"; diff --git a/js/tests/config.test.ts b/js/tests/config.test.ts index 12e9376..b8ee529 100644 --- a/js/tests/config.test.ts +++ b/js/tests/config.test.ts @@ -9,7 +9,7 @@ import { getDownloadUrl, getFallbackDownloadUrl, } from "../src/config.js"; -import { _buildArgsForTest, migrateTimezoneId } from "../src/playwright.js"; +import { _buildArgsForTest, resolveTimezone } from "../src/playwright.js"; describe("config", () => { it("CHROMIUM_VERSION matches expected format", () => { @@ -97,21 +97,24 @@ describe("buildArgs timezone/locale", () => { expect(args).toContain("--fingerprint-timezone=America/New_York"); }); - it("injects --lang when locale is set", () => { + it("injects --lang and --fingerprint-locale when locale is set", () => { const args = _buildArgsForTest({ locale: "en-US" }); expect(args).toContain("--lang=en-US"); + expect(args).toContain("--fingerprint-locale=en-US"); }); it("injects both when both are set", () => { const args = _buildArgsForTest({ timezone: "Europe/Berlin", locale: "de-DE" }); expect(args).toContain("--fingerprint-timezone=Europe/Berlin"); expect(args).toContain("--lang=de-DE"); + expect(args).toContain("--fingerprint-locale=de-DE"); }); it("injects timezone/locale even when stealthArgs=false", () => { const args = _buildArgsForTest({ stealthArgs: false, timezone: "America/New_York", locale: "en-US" }); expect(args).toContain("--fingerprint-timezone=America/New_York"); expect(args).toContain("--lang=en-US"); + expect(args).toContain("--fingerprint-locale=en-US"); expect(args.some(a => a.startsWith("--fingerprint="))).toBe(false); }); @@ -119,6 +122,7 @@ describe("buildArgs timezone/locale", () => { const args = _buildArgsForTest({}); expect(args.some(a => a.startsWith("--fingerprint-timezone="))).toBe(false); expect(args.some(a => a.startsWith("--lang="))).toBe(false); + expect(args.some(a => a.startsWith("--fingerprint-locale="))).toBe(false); }); }); @@ -147,14 +151,17 @@ describe("buildArgs deduplication", () => { expect(tzArgs[0]).toBe("--fingerprint-timezone=America/New_York"); }); - it("locale param overrides user --lang arg", () => { + it("locale param overrides user --lang and --fingerprint-locale args", () => { const args = _buildArgsForTest({ - args: ["--lang=de-DE"], + args: ["--lang=de-DE", "--fingerprint-locale=de-DE"], locale: "en-US", }); const langArgs = args.filter(a => a.startsWith("--lang=")); expect(langArgs).toHaveLength(1); expect(langArgs[0]).toBe("--lang=en-US"); + const localeArgs = args.filter(a => a.startsWith("--fingerprint-locale=")); + expect(localeArgs).toHaveLength(1); + expect(localeArgs[0]).toBe("--fingerprint-locale=en-US"); }); it("no duplicate flag keys in output", () => { @@ -175,29 +182,29 @@ describe("buildArgs deduplication", () => { }); }); -describe("migrateTimezoneId deprecation", () => { - it("migrates timezoneId to timezone", () => { - const result = migrateTimezoneId({ timezoneId: "Europe/Paris" }); +describe("resolveTimezone alias", () => { + it("resolves timezoneId to timezone", () => { + const result = resolveTimezone({ timezoneId: "Europe/Paris" }); expect(result.timezone).toBe("Europe/Paris"); expect(result).not.toHaveProperty("timezoneId"); }); it("preserves explicit timezone over timezoneId", () => { - const result = migrateTimezoneId({ timezone: "UTC", timezoneId: "Europe/Paris" }); + const result = resolveTimezone({ timezone: "UTC", timezoneId: "Europe/Paris" }); expect(result.timezone).toBe("UTC"); expect(result).not.toHaveProperty("timezoneId"); }); it("returns options unchanged when no timezoneId", () => { const opts = { timezone: "UTC" }; - const result = migrateTimezoneId(opts); + const result = resolveTimezone(opts); expect(result).toBe(opts); // same reference, no copy expect(result.timezone).toBe("UTC"); }); it("returns options unchanged when neither is set", () => { const opts = {}; - const result = migrateTimezoneId(opts); + const result = resolveTimezone(opts); expect(result).toBe(opts); }); }); diff --git a/js/tests/launch.test.ts b/js/tests/launch.test.ts index 1dbe91f..d9f4d25 100644 --- a/js/tests/launch.test.ts +++ b/js/tests/launch.test.ts @@ -96,20 +96,20 @@ describe("launchContext (unit)", () => { expect(ctxArgs.userAgent).toBe("Custom/1.0"); }); - it("passes timezone to context timezoneId, not to launch", async () => { + it("passes timezone via binary flag, not CDP context", async () => { const { launchContext } = await import("../src/playwright.js"); await launchContext({ timezone: "America/New_York" }); - // launch() called with timezone: undefined (skipped for binary flag) + // launch() called with --fingerprint-timezone binary flag const launchArgs = mockChromium.launch.mock.calls[0][0]; const hasTimezoneFlag = launchArgs.args.some((a: string) => - a.startsWith("--fingerprint-timezone=") + a.startsWith("--fingerprint-timezone=America/New_York") ); - expect(hasTimezoneFlag).toBe(false); + expect(hasTimezoneFlag).toBe(true); - // newContext() gets timezoneId + // NOT in newContext() — no CDP emulation const ctxArgs = mockBrowser.newContext.mock.calls[0][0]; - expect(ctxArgs.timezoneId).toBe("America/New_York"); + expect(ctxArgs.timezoneId).toBeUndefined(); }); it("forwards colorScheme to newContext", async () => { @@ -165,7 +165,7 @@ describe("launchPersistentContext (unit)", () => { expect(args.viewport).toEqual(DEFAULT_VIEWPORT); }); - it("passes timezone and locale to context", async () => { + it("passes timezone and locale via binary args, not CDP context", async () => { const { launchPersistentContext } = await import("../src/playwright.js"); await launchPersistentContext({ userDataDir: "/tmp/profile", @@ -174,11 +174,12 @@ describe("launchPersistentContext (unit)", () => { }); const args = mockChromium.launchPersistentContext.mock.calls[0][1]; - expect(args.timezoneId).toBe("Asia/Tokyo"); - expect(args.locale).toBe("ja-JP"); - // Also in binary args + // Binary args (native, undetectable) expect(args.args).toContain("--fingerprint-timezone=Asia/Tokyo"); expect(args.args).toContain("--lang=ja-JP"); + // NOT in context kwargs (would trigger detectable CDP emulation) + expect(args.timezoneId).toBeUndefined(); + expect(args.locale).toBeUndefined(); }); it("forwards proxy string", async () => { diff --git a/tests/test_build_args.py b/tests/test_build_args.py index 9ab588f..d38ea7c 100644 --- a/tests/test_build_args.py +++ b/tests/test_build_args.py @@ -1,8 +1,6 @@ -"""Unit tests for _build_args timezone/locale injection and deprecation compat.""" +"""Unit tests for _build_args timezone/locale injection and timezone alias.""" -import warnings - -from cloakbrowser.browser import _build_args, _migrate_timezone_id +from cloakbrowser.browser import _build_args, _resolve_timezone def test_timezone_injected(): @@ -12,9 +10,10 @@ def test_timezone_injected(): def test_locale_injected(): - """--lang flag should appear when locale is set.""" + """--lang and --fingerprint-locale flags should appear when locale is set.""" args = _build_args(stealth_args=True, extra_args=None, locale="en-US") assert "--lang=en-US" in args + assert "--fingerprint-locale=en-US" in args def test_both_injected(): @@ -22,6 +21,7 @@ def test_both_injected(): args = _build_args(stealth_args=True, extra_args=None, timezone="Europe/Berlin", locale="de-DE") assert "--fingerprint-timezone=Europe/Berlin" in args assert "--lang=de-DE" in args + assert "--fingerprint-locale=de-DE" in args def test_timezone_independent_of_stealth_args(): @@ -29,15 +29,17 @@ def test_timezone_independent_of_stealth_args(): args = _build_args(stealth_args=False, extra_args=None, timezone="America/New_York", locale="en-US") assert "--fingerprint-timezone=America/New_York" in args assert "--lang=en-US" in args + assert "--fingerprint-locale=en-US" in args # No stealth fingerprint args assert not any(a.startswith("--fingerprint=") for a in args) def test_no_flags_when_not_set(): - """No timezone/lang flags when params are None.""" + """No timezone/lang/fingerprint-locale flags when params are None.""" args = _build_args(stealth_args=True, extra_args=None) assert not any(a.startswith("--fingerprint-timezone=") for a in args) assert not any(a.startswith("--lang=") for a in args) + assert not any(a.startswith("--fingerprint-locale=") for a in args) def test_extra_args_preserved(): @@ -46,52 +48,41 @@ def test_extra_args_preserved(): assert "--disable-gpu" in args assert "--fingerprint-timezone=Asia/Tokyo" in args assert "--lang=ja-JP" in args + assert "--fingerprint-locale=ja-JP" in args -# --- _migrate_timezone_id deprecation compat --- +# --- _resolve_timezone alias --- -def test_migrate_old_param_only(): +def test_resolve_timezone_id_alias(): """timezone_id in kwargs should be promoted to timezone.""" kwargs = {"timezone_id": "Europe/Paris"} - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - result = _migrate_timezone_id(None, kwargs) + result = _resolve_timezone(None, kwargs) assert result == "Europe/Paris" assert "timezone_id" not in kwargs - assert len(w) == 1 and issubclass(w[0].category, FutureWarning) -def test_migrate_new_param_wins(): +def test_resolve_timezone_wins_over_alias(): """Explicit timezone takes precedence; timezone_id is still popped.""" kwargs = {"timezone_id": "Europe/Paris"} - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - result = _migrate_timezone_id("UTC", kwargs) + result = _resolve_timezone("UTC", kwargs) assert result == "UTC" assert "timezone_id" not in kwargs - assert len(w) == 1 -def test_migrate_no_old_param(): - """No warning when timezone_id is absent.""" +def test_resolve_no_alias(): + """No-op when timezone_id is absent.""" kwargs = {"other": "value"} - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - result = _migrate_timezone_id("UTC", kwargs) + result = _resolve_timezone("UTC", kwargs) assert result == "UTC" assert "other" in kwargs - assert len(w) == 0 -def test_migrate_both_none(): - """Neither param set — returns None, no warning.""" +def test_resolve_both_none(): + """Neither param set — returns None.""" kwargs = {} - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - result = _migrate_timezone_id(None, kwargs) + result = _resolve_timezone(None, kwargs) assert result is None - assert len(w) == 0 # --- Deduplication tests --- @@ -126,15 +117,18 @@ def test_timezone_param_overrides_user_arg(): def test_locale_param_overrides_user_arg(): - """Dedicated locale param should override user --lang arg.""" + """Dedicated locale param should override user --lang and --fingerprint-locale args.""" args = _build_args( stealth_args=True, - extra_args=["--lang=de-DE"], + extra_args=["--lang=de-DE", "--fingerprint-locale=de-DE"], locale="en-US", ) lang_args = [a for a in args if a.startswith("--lang=")] assert len(lang_args) == 1 assert lang_args[0] == "--lang=en-US" + locale_args = [a for a in args if a.startswith("--fingerprint-locale=")] + assert len(locale_args) == 1 + assert locale_args[0] == "--fingerprint-locale=en-US" def test_no_duplicate_flags(): diff --git a/tests/test_launch_context.py b/tests/test_launch_context.py index 78299f1..8c80f22 100644 --- a/tests/test_launch_context.py +++ b/tests/test_launch_context.py @@ -1,6 +1,5 @@ """Unit tests for launch_context() — context kwargs, viewport defaults, close cleanup.""" -import warnings from unittest.mock import MagicMock, call, patch import pytest @@ -66,7 +65,7 @@ def test_user_agent(mock_launch, _mock_bin): @patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome") @patch("cloakbrowser.browser.launch") def test_locale_forwarded(mock_launch, _mock_bin): - """locale flows to both launch() binary args AND new_context().""" + """locale flows to launch() for --lang binary flag, NOT to new_context() CDP.""" browser, context = _make_mock_browser() mock_launch.return_value = browser @@ -75,18 +74,18 @@ def test_locale_forwarded(mock_launch, _mock_bin): # Locale in launch() call (for --lang binary flag) assert mock_launch.call_args[1]["locale"] == "de-DE" - # Locale in new_context() call + # NOT in new_context() — would trigger detectable CDP emulation ctx_kwargs = browser.new_context.call_args - assert ctx_kwargs[1]["locale"] == "de-DE" + assert "locale" not in ctx_kwargs[1] @patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome") @patch("cloakbrowser.browser.launch") -def test_timezone_via_context_not_binary(mock_launch, _mock_bin): - """timezone passed to new_context(timezone_id=...) but NOT to launch(timezone=...). +def test_timezone_via_binary_not_cdp(mock_launch, _mock_bin): + """timezone passed to launch() for binary flag, NOT to new_context() CDP. - This is intentional: the --fingerprint-timezone binary flag only applies to the - default context and would conflict with Playwright's timezone_id on new contexts. + --fingerprint-timezone is process-wide (reads CommandLine in renderer), + so it applies to ALL contexts, not just the default one. """ browser, context = _make_mock_browser() mock_launch.return_value = browser @@ -94,11 +93,11 @@ def test_timezone_via_context_not_binary(mock_launch, _mock_bin): from cloakbrowser.browser import launch_context launch_context(timezone="America/New_York") - # timezone=None in launch() — binary flag skipped - assert mock_launch.call_args[1]["timezone"] is None - # timezone_id in new_context() + # timezone in launch() — binary flag set + assert mock_launch.call_args[1]["timezone"] == "America/New_York" + # NOT in new_context() — no CDP emulation ctx_kwargs = browser.new_context.call_args - assert ctx_kwargs[1]["timezone_id"] == "America/New_York" + assert "timezone_id" not in ctx_kwargs[1] @patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome") @@ -119,40 +118,37 @@ def test_color_scheme(mock_launch, _mock_bin): @patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome") @patch("cloakbrowser.browser.launch") def test_geoip_resolution(mock_launch, _mock_bin, _mock_geoip): - """geoip fills timezone+locale, both flow to correct places.""" + """geoip fills timezone+locale, both flow to binary args only.""" browser, context = _make_mock_browser() mock_launch.return_value = browser from cloakbrowser.browser import launch_context launch_context(proxy="http://proxy:8080", geoip=True) - # Locale goes to launch() for binary flag + # Both go to launch() for binary flags assert mock_launch.call_args[1]["locale"] == "de-DE" - # Timezone goes to context, not binary - assert mock_launch.call_args[1]["timezone"] is None + assert mock_launch.call_args[1]["timezone"] == "Europe/Berlin" + # Neither in context — no CDP emulation ctx_kwargs = browser.new_context.call_args - assert ctx_kwargs[1]["timezone_id"] == "Europe/Berlin" - assert ctx_kwargs[1]["locale"] == "de-DE" + assert "timezone_id" not in ctx_kwargs[1] + assert "locale" not in ctx_kwargs[1] @patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome") @patch("cloakbrowser.browser.launch") -def test_timezone_id_deprecation(mock_launch, _mock_bin): - """timezone_id kwarg triggers FutureWarning, value migrated to timezone.""" +def test_timezone_id_alias(mock_launch, _mock_bin): + """timezone_id kwarg accepted as alias for timezone.""" browser, context = _make_mock_browser() mock_launch.return_value = browser from cloakbrowser.browser import launch_context - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - launch_context(timezone_id="Europe/Paris") + launch_context(timezone_id="Europe/Paris") - assert len(w) == 1 - assert issubclass(w[0].category, FutureWarning) - assert "timezone_id" in str(w[0].message) - # Migrated value flows to context + # Resolved value flows to launch() for binary flag + assert mock_launch.call_args[1]["timezone"] == "Europe/Paris" + # NOT in context — no CDP emulation ctx_kwargs = browser.new_context.call_args - assert ctx_kwargs[1]["timezone_id"] == "Europe/Paris" + assert "timezone_id" not in ctx_kwargs[1] @patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome") diff --git a/tests/test_persistent_context.py b/tests/test_persistent_context.py index 72f244e..cb58c22 100644 --- a/tests/test_persistent_context.py +++ b/tests/test_persistent_context.py @@ -3,7 +3,6 @@ All tests mock playwright to avoid needing a binary. """ -import warnings from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -87,7 +86,7 @@ def test_persistent_context_user_agent(_mock_geoip, _mock_bin): @patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome") def test_persistent_context_locale_and_timezone(_mock_bin): - """Both timezone and locale flow to context kwargs and binary args.""" + """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): @@ -95,12 +94,12 @@ def test_persistent_context_locale_and_timezone(_mock_bin): launch_persistent_context("/tmp/profile", timezone="Asia/Tokyo", locale="ja-JP") call_kwargs = pw.chromium.launch_persistent_context.call_args[1] - # Context kwargs - assert call_kwargs["timezone_id"] == "Asia/Tokyo" - assert call_kwargs["locale"] == "ja-JP" - # Binary args + # 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") @@ -120,7 +119,7 @@ def test_persistent_context_color_scheme(_mock_geoip, _mock_bin): @patch("cloakbrowser.browser._maybe_resolve_geoip", return_value=("Europe/Berlin", "de-DE")) @patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome") def test_persistent_context_geoip(_mock_bin, _mock_geoip): - """geoip fills missing tz/locale.""" + """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): @@ -128,25 +127,26 @@ def test_persistent_context_geoip(_mock_bin, _mock_geoip): launch_persistent_context("/tmp/profile", proxy="http://proxy:8080", geoip=True) call_kwargs = pw.chromium.launch_persistent_context.call_args[1] - assert call_kwargs["timezone_id"] == "Europe/Berlin" - assert call_kwargs["locale"] == "de-DE" + # 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_deprecation(_mock_bin): - """Old timezone_id kwarg migrated with warning.""" +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 - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - launch_persistent_context("/tmp/profile", timezone_id="Europe/Paris") + launch_persistent_context("/tmp/profile", timezone_id="Europe/Paris") - assert len(w) == 1 - assert issubclass(w[0].category, FutureWarning) call_kwargs = pw.chromium.launch_persistent_context.call_args[1] - assert call_kwargs["timezone_id"] == "Europe/Paris" + 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") @@ -246,17 +246,14 @@ async def test_persistent_context_async_close_stops_pw(_mock_geoip, _mock_bin): @pytest.mark.asyncio @patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome") -async def test_persistent_context_async_timezone_id_deprecation(_mock_bin): - """Deprecated timezone_id kwarg migrated with warning in async path.""" +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 - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - await launch_persistent_context_async("/tmp/profile", timezone_id="Europe/Paris") + await launch_persistent_context_async("/tmp/profile", timezone_id="Europe/Paris") - assert len(w) == 1 - assert issubclass(w[0].category, FutureWarning) call_kwargs = pw.chromium.launch_persistent_context.call_args[1] - assert call_kwargs["timezone_id"] == "Europe/Paris" + assert "--fingerprint-timezone=Europe/Paris" in call_kwargs["args"] + assert "timezone_id" not in call_kwargs