From 8ff63ef3215c772c6e532fdca8c1b05ebac965f6 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:14:58 -0700 Subject: [PATCH] fix: resolve timezoneId in standalone builders so --fingerprint-timezone is not dropped Follow-up to #262/#265. buildArgs only reads `options.timezone` to emit --fingerprint-timezone. The launch* wrappers on the Playwright side resolve `timezoneId` -> `timezone` first, but two paths did not: - playwright buildLaunchOptions() is exported and used standalone, yet did not call resolveTimezone, so buildLaunchOptions({ timezoneId }) silently dropped the flag. - the puppeteer adapter's resolveArgs() (shared by launch / launchContext / launchPersistentContext) never resolved timezoneId at all, so the entire puppeteer surface dropped it. Add resolveTimezone() at the top of both. Tests cover timezoneId on the playwright standalone builder and the puppeteer launch path. --- js/src/playwright.ts | 6 ++++++ js/src/puppeteer.ts | 6 ++++++ js/tests/config.test.ts | 34 +++++++++++++++++++++++++++++++++- js/tests/puppeteer.test.ts | 8 ++++++++ 4 files changed, 53 insertions(+), 1 deletion(-) diff --git a/js/src/playwright.ts b/js/src/playwright.ts index 08efd35f..814068c2 100644 --- a/js/src/playwright.ts +++ b/js/src/playwright.ts @@ -110,6 +110,12 @@ export function buildContextOptions( export async function buildLaunchOptions( options: LaunchOptions = {} ): Promise { + // Standalone callers may pass `timezoneId` (Playwright's field name) instead + // of `timezone`. The launch* wrappers resolve this before delegating, but + // buildLaunchOptions is exported and used directly, and buildArgs only reads + // `options.timezone` to emit --fingerprint-timezone — so without this, a + // standalone `buildLaunchOptions({ timezoneId })` silently drops the flag. + options = resolveTimezone(options); const binaryPath = process.env.CLOAKBROWSER_BINARY_PATH || (await ensureBinary(options.licenseKey, options.browserVersion)); diff --git a/js/src/puppeteer.ts b/js/src/puppeteer.ts index e1929451..eccad618 100644 --- a/js/src/puppeteer.ts +++ b/js/src/puppeteer.ts @@ -13,6 +13,7 @@ import { } from "./config.js"; import { buildArgs } from "./args.js"; import { maybeWarnWindowsFonts } from "./fonts.js"; +import { resolveTimezone } from "./playwright.js"; import { ensureBinary } from "./download.js"; import { isSocksProxy, normalizeHttpStringUrl, parseProxyUrl, reconstructHttpUrl, resolveProxyConfig, supportsHttpProxyInlineAuth } from "./proxy.js"; import { maybeResolveGeoip, resolveWebrtcArgs, appendWebrtcExitIp } from "./geoip.js"; @@ -45,6 +46,11 @@ function resolveDefaultViewport(options: LaunchOptions): { width: number; height /** Resolve binary path, geoip, webrtc, and build final Chrome args. */ async function resolveArgs(options: LaunchOptions): Promise<{ binaryPath: string; args: string[] }> { + // Map Playwright's `timezoneId` onto `timezone` before buildArgs, which only + // reads `timezone` to emit --fingerprint-timezone. Every puppeteer entry + // point (launch/launchContext/launchPersistentContext) funnels through here, + // so resolving once covers all of them. + options = resolveTimezone(options); const binaryPath = process.env.CLOAKBROWSER_BINARY_PATH || (await ensureBinary(options.licenseKey, options.browserVersion)); diff --git a/js/tests/config.test.ts b/js/tests/config.test.ts index a933b170..6bd99e4c 100644 --- a/js/tests/config.test.ts +++ b/js/tests/config.test.ts @@ -12,7 +12,7 @@ import { binarySupportsHeadlessNoViewport, binarySupportsMaximizedWindow, } from "../src/config.js"; -import { _buildArgsForTest, resolveTimezone } from "../src/playwright.js"; +import { _buildArgsForTest, resolveTimezone, buildLaunchOptions } from "../src/playwright.js"; describe("config", () => { it("CHROMIUM_VERSION matches expected format", () => { @@ -347,3 +347,35 @@ describe("buildArgs --start-maximized", () => { expect(args.filter(a => a === "--start-maximized")).toHaveLength(1); }); }); + +describe("buildLaunchOptions timezoneId resolution", () => { + // Standalone callers (not going through launch*/launchContext) may pass + // Playwright's `timezoneId`. buildArgs only reads `timezone`, so without + // resolveTimezone inside buildLaunchOptions the --fingerprint-timezone flag + // is silently dropped. Regression guard for that gap. + it("resolves timezoneId to --fingerprint-timezone for standalone callers", async () => { + const prev = process.env.CLOAKBROWSER_BINARY_PATH; + process.env.CLOAKBROWSER_BINARY_PATH = "/tmp/fake-cloak-binary"; + try { + const opts = await buildLaunchOptions({ + timezoneId: "Europe/Paris", + } as unknown as Parameters[0]); + expect(opts.args).toContain("--fingerprint-timezone=Europe/Paris"); + } finally { + if (prev === undefined) delete process.env.CLOAKBROWSER_BINARY_PATH; + else process.env.CLOAKBROWSER_BINARY_PATH = prev; + } + }); + + it("still emits the flag when an explicit timezone is passed", async () => { + const prev = process.env.CLOAKBROWSER_BINARY_PATH; + process.env.CLOAKBROWSER_BINARY_PATH = "/tmp/fake-cloak-binary"; + try { + const opts = await buildLaunchOptions({ timezone: "UTC" }); + expect(opts.args).toContain("--fingerprint-timezone=UTC"); + } finally { + if (prev === undefined) delete process.env.CLOAKBROWSER_BINARY_PATH; + else process.env.CLOAKBROWSER_BINARY_PATH = prev; + } + }); +}); diff --git a/js/tests/puppeteer.test.ts b/js/tests/puppeteer.test.ts index 3f100d16..61586596 100644 --- a/js/tests/puppeteer.test.ts +++ b/js/tests/puppeteer.test.ts @@ -176,6 +176,14 @@ describe("puppeteer launch", () => { expect(callArgs.args).toContain("--lang=ja-JP"); }); + it("resolves timezoneId to --fingerprint-timezone", async () => { + const { launch } = await import("../src/puppeteer.js"); + await launch({ timezoneId: "Asia/Tokyo" } as unknown as Parameters[0]); + + const callArgs = vi.mocked(puppeteerMock.default.launch).mock.calls[0][0]; + expect(callArgs.args).toContain("--fingerprint-timezone=Asia/Tokyo"); + }); + it("merges extra args", async () => { const { launch } = await import("../src/puppeteer.js"); await launch({ args: ["--disable-gpu", "--no-first-run"] });