mirror of
https://github.com/CloakHQ/CloakBrowser.git
synced 2026-06-23 11:41:46 +02:00
fix(wrapper): track real window geometry on headed launches
Headed launches applied a fixed emulated viewport on top of the real browser window, yielding outerWidth < innerWidth (an impossible window). Default headed new_page()/new_context() to no_viewport so the page tracks the real window; headless keeps a deterministic viewport. Covers Python launch/launch_context/launch_persistent_context (+async) and the JS Playwright/Puppeteer wrappers. Explicit viewport still honored.
This commit is contained in:
+55
-2
@@ -52,15 +52,43 @@ function filterStealthCtxOptions(ctx?: BrowserContextOptions): Partial<BrowserCo
|
||||
* Useful when integrating CloakBrowser with an existing Playwright Browser while
|
||||
* keeping the wrapper's stealth-safe defaults for `newContext()`.
|
||||
*/
|
||||
/**
|
||||
* Effective headless mode for viewport decisions. buildLaunchOptions() spreads
|
||||
* `...options.launchOptions` LAST, so a raw `launchOptions.headless` overrides the
|
||||
* top-level field at the actual chromium.launch() call. Viewport logic must read
|
||||
* the same effective value — otherwise a headed browser gets a fixed viewport
|
||||
* (reintroducing the impossible-window tell). Playwright-specific (Puppeteer
|
||||
* resolves headless the opposite way).
|
||||
*/
|
||||
function effectiveHeadless(options: LaunchOptions): boolean {
|
||||
return (
|
||||
(options.launchOptions as { headless?: boolean } | undefined)?.headless ??
|
||||
options.headless ??
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
export function buildContextOptions(
|
||||
options: LaunchContextOptions = {}
|
||||
): BrowserContextOptions {
|
||||
// Headed: viewport=null (no emulation) so the page tracks the real window and
|
||||
// outerWidth >= innerWidth stays coherent — CDP viewport emulation forces
|
||||
// inner > outer = a physically impossible window = bot tell. Headless has no
|
||||
// window chrome (outer == inner), so a fixed viewport stays coherent and keeps
|
||||
// dimensions deterministic. Explicit viewport (incl. null) is always honored.
|
||||
const headless = effectiveHeadless(options);
|
||||
const viewport =
|
||||
options.viewport !== undefined
|
||||
? options.viewport
|
||||
: headless
|
||||
? DEFAULT_VIEWPORT
|
||||
: null;
|
||||
return {
|
||||
// contextOptions first — explicit wrapper fields below override it.
|
||||
// filterStealthCtxOptions strips locale/timezoneId to prevent CDP detection.
|
||||
...filterStealthCtxOptions(options.contextOptions),
|
||||
...(options.userAgent ? { userAgent: options.userAgent } : {}),
|
||||
viewport: options.viewport === undefined ? DEFAULT_VIEWPORT : options.viewport,
|
||||
viewport,
|
||||
...(options.colorScheme ? { colorScheme: options.colorScheme } : {}),
|
||||
} as BrowserContextOptions;
|
||||
}
|
||||
@@ -127,10 +155,33 @@ export async function humanizeBrowser(
|
||||
export async function launch(options: LaunchOptions = {}): Promise<Browser> {
|
||||
const { chromium } = await import("playwright-core");
|
||||
const browser = await chromium.launch(await buildLaunchOptions(options));
|
||||
// Headed: a bare browser.newPage() would inherit Playwright's emulated 1280x720
|
||||
// viewport -> outerWidth < innerWidth (impossible window = bot tell). Default
|
||||
// newPage()/newContext() to viewport:null so the page tracks the real window.
|
||||
// Headless keeps Playwright's default viewport (coherent there).
|
||||
if (!effectiveHeadless(options)) {
|
||||
applyDefaultNoViewport(browser);
|
||||
}
|
||||
await humanizeBrowser(browser, options);
|
||||
return browser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a Browser's newContext()/newPage() to default to viewport:null (no
|
||||
* emulation) when the caller didn't specify a viewport. setdefault-style: an
|
||||
* explicit viewport (including null) is always honored. Apply before humanize's
|
||||
* patchBrowser so the wraps compose.
|
||||
*/
|
||||
function applyDefaultNoViewport(browser: Browser): void {
|
||||
const origNewContext = browser.newContext.bind(browser);
|
||||
(browser as any).newContext = (options?: Parameters<typeof origNewContext>[0]) =>
|
||||
origNewContext(options?.viewport === undefined ? { ...options, viewport: null } : options);
|
||||
|
||||
const origNewPage = browser.newPage.bind(browser);
|
||||
(browser as any).newPage = (options?: Parameters<typeof origNewPage>[0]) =>
|
||||
origNewPage(options?.viewport === undefined ? { ...options, viewport: null } : options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch stealth browser and return a BrowserContext with common options pre-set.
|
||||
* Closing the context also closes the browser.
|
||||
@@ -161,7 +212,9 @@ export async function launchContext(
|
||||
// --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, args: launchArgs, geoip: false });
|
||||
// humanize:false on the inner launch — patchContext below applies humanize
|
||||
// exactly once (else launch()'s humanizeBrowser would patch it a second time).
|
||||
const browser = await launch({ ...options, ...resolved, args: launchArgs, geoip: false, humanize: false });
|
||||
|
||||
let context: BrowserContext;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user