mirror of
https://github.com/CloakHQ/CloakBrowser.git
synced 2026-06-23 11:41:46 +02:00
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
This commit is contained in:
+19
-29
@@ -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())
|
||||
|
||||
|
||||
+6
-5
@@ -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()];
|
||||
}
|
||||
|
||||
+10
-13
@@ -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<T extends { timezone?: string; timezoneId?: string }>(options: T): T {
|
||||
/** @internal Accept both timezone and timezoneId — either works, no warning. Exported for testing. */
|
||||
export function resolveTimezone<T extends { timezone?: string; timezoneId?: string }>(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<Browser> {
|
||||
export async function launchContext(
|
||||
options: LaunchContextOptions = {}
|
||||
): Promise<BrowserContext> {
|
||||
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<BrowserContext> {
|
||||
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,
|
||||
});
|
||||
|
||||
+1
-1
@@ -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";
|
||||
|
||||
+17
-10
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
+11
-10
@@ -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 () => {
|
||||
|
||||
+25
-31
@@ -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():
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user