import type { CDPSession, Page } from "playwright-core"; import { devices as playwrightDevices } from "playwright-core"; import { ensurePageState, getPageForTargetId } from "./pw-session.js"; async function withCdpSession(page: Page, fn: (session: CDPSession) => Promise): Promise { const session = await page.context().newCDPSession(page); try { return await fn(session); } finally { await session.detach().catch(() => {}); } } export async function setOfflineViaPlaywright(opts: { cdpUrl: string; targetId?: string; offline: boolean; }): Promise { const page = await getPageForTargetId(opts); ensurePageState(page); await page.context().setOffline(Boolean(opts.offline)); } export async function setExtraHTTPHeadersViaPlaywright(opts: { cdpUrl: string; targetId?: string; headers: Record; }): Promise { const page = await getPageForTargetId(opts); ensurePageState(page); await page.context().setExtraHTTPHeaders(opts.headers); } export async function setHttpCredentialsViaPlaywright(opts: { cdpUrl: string; targetId?: string; username?: string; password?: string; clear?: boolean; }): Promise { const page = await getPageForTargetId(opts); ensurePageState(page); if (opts.clear) { await page.context().setHTTPCredentials(null); return; } const username = String(opts.username ?? ""); const password = String(opts.password ?? ""); if (!username) throw new Error("username is required (or set clear=true)"); await page.context().setHTTPCredentials({ username, password }); } export async function setGeolocationViaPlaywright(opts: { cdpUrl: string; targetId?: string; latitude?: number; longitude?: number; accuracy?: number; origin?: string; clear?: boolean; }): Promise { const page = await getPageForTargetId(opts); ensurePageState(page); const context = page.context(); if (opts.clear) { await context.setGeolocation(null); await context.clearPermissions().catch(() => {}); return; } if (typeof opts.latitude !== "number" || typeof opts.longitude !== "number") { throw new Error("latitude and longitude are required (or set clear=true)"); } await context.setGeolocation({ latitude: opts.latitude, longitude: opts.longitude, accuracy: typeof opts.accuracy === "number" ? opts.accuracy : undefined, }); const origin = opts.origin?.trim() || (() => { try { return new URL(page.url()).origin; } catch { return ""; } })(); if (origin) { await context.grantPermissions(["geolocation"], { origin }).catch(() => {}); } } export async function emulateMediaViaPlaywright(opts: { cdpUrl: string; targetId?: string; colorScheme: "dark" | "light" | "no-preference" | null; }): Promise { const page = await getPageForTargetId(opts); ensurePageState(page); await page.emulateMedia({ colorScheme: opts.colorScheme }); } export async function setLocaleViaPlaywright(opts: { cdpUrl: string; targetId?: string; locale: string; }): Promise { const page = await getPageForTargetId(opts); ensurePageState(page); const locale = String(opts.locale ?? "").trim(); if (!locale) throw new Error("locale is required"); await withCdpSession(page, async (session) => { try { await session.send("Emulation.setLocaleOverride", { locale }); } catch (err) { if (String(err).includes("Another locale override is already in effect")) { return; } throw err; } }); } export async function setTimezoneViaPlaywright(opts: { cdpUrl: string; targetId?: string; timezoneId: string; }): Promise { const page = await getPageForTargetId(opts); ensurePageState(page); const timezoneId = String(opts.timezoneId ?? "").trim(); if (!timezoneId) throw new Error("timezoneId is required"); await withCdpSession(page, async (session) => { try { await session.send("Emulation.setTimezoneOverride", { timezoneId }); } catch (err) { const msg = String(err); if (msg.includes("Timezone override is already in effect")) return; if (msg.includes("Invalid timezone")) throw new Error(`Invalid timezone ID: ${timezoneId}`); throw err; } }); } export async function setDeviceViaPlaywright(opts: { cdpUrl: string; targetId?: string; name: string; }): Promise { const page = await getPageForTargetId(opts); ensurePageState(page); const name = String(opts.name ?? "").trim(); if (!name) throw new Error("device name is required"); const descriptor = (playwrightDevices as Record)[name] as | { userAgent?: string; viewport?: { width: number; height: number }; deviceScaleFactor?: number; isMobile?: boolean; hasTouch?: boolean; locale?: string; } | undefined; if (!descriptor) { throw new Error(`Unknown device "${name}".`); } if (descriptor.viewport) { await page.setViewportSize({ width: descriptor.viewport.width, height: descriptor.viewport.height, }); } await withCdpSession(page, async (session) => { if (descriptor.userAgent || descriptor.locale) { await session.send("Emulation.setUserAgentOverride", { userAgent: descriptor.userAgent ?? "", acceptLanguage: descriptor.locale ?? undefined, }); } if (descriptor.viewport) { await session.send("Emulation.setDeviceMetricsOverride", { mobile: Boolean(descriptor.isMobile), width: descriptor.viewport.width, height: descriptor.viewport.height, deviceScaleFactor: descriptor.deviceScaleFactor ?? 1, screenWidth: descriptor.viewport.width, screenHeight: descriptor.viewport.height, }); } if (descriptor.hasTouch) { await session.send("Emulation.setTouchEmulationEnabled", { enabled: true, }); } }); }