import { describe, test, expect, beforeAll, afterAll } from 'bun:test'; import { chromium, type Browser, type BrowserContext } from 'playwright'; import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { applyStealth, buildStealthScript, readHostProfile, STEALTH_LAUNCH_ARGS } from '../src/stealth'; let browser: Browser; beforeAll(async () => { // Playwright's default launch timeout is 30s — under the full-suite // --parallel run, ~400 workers contend and a cold Chromium launch can // stall past it (observed: hook death reported as an '(unnamed)' test at // 30006ms). The runner's external wall-clock still bounds the ceiling. browser = await chromium.launch({ headless: true, args: STEALTH_LAUNCH_ARGS, timeout: 120_000 }); }); afterAll(async () => { await browser.close(); }); describe('STEALTH_LAUNCH_ARGS', () => { test('includes --disable-blink-features=AutomationControlled', () => { expect(STEALTH_LAUNCH_ARGS).toContain('--disable-blink-features=AutomationControlled'); }); }); describe('applyStealth — context level', () => { let context: BrowserContext; beforeAll(async () => { context = await browser.newContext(); await applyStealth(context); }); afterAll(async () => { await context.close(); }); test('navigator.webdriver returns false on a fresh page', async () => { const page = await context.newPage(); try { const webdriver = await page.evaluate(() => (navigator as any).webdriver); expect(webdriver).toBe(false); } finally { await page.close(); } }); test('webdriver is false for every new page in the same context (init script applies to all pages)', async () => { const p1 = await context.newPage(); const p2 = await context.newPage(); try { const w1 = await p1.evaluate(() => (navigator as any).webdriver); const w2 = await p2.evaluate(() => (navigator as any).webdriver); expect(w1).toBe(false); expect(w2).toBe(false); } finally { await p1.close(); await p2.close(); } }); test('navigator.plugins is NOT a hardcoded fixed list (D7: let Chromium emit native)', async () => { const page = await context.newPage(); try { const plugins = await page.evaluate(() => Array.from(navigator.plugins).map((p) => p.name)); // We do not assert exact contents — Chromium versions vary. We assert // that we did NOT replace plugins with the wintermute fake list. // The wintermute approach was: get: () => [1, 2, 3, 4, 5] const isFake = plugins.length === 5 && plugins.every((name) => /^[12345]$/.test(String(name))); expect(isFake).toBe(false); } finally { await page.close(); } }); test('navigator.languages is NOT hardcoded by us (D7)', async () => { const page = await context.newPage(); try { const langs = await page.evaluate(() => navigator.languages); // Whatever Chromium emits is fine; we just assert we are not the // ones forcing it to ['en-US', 'en'] (wintermute pattern). // Cannot assert this strictly because Chromium often DOES emit those // values naturally. Instead, assert that languages is an array of // strings — i.e. the property still works (we didn't break it). expect(Array.isArray(langs)).toBe(true); expect(langs.every((l) => typeof l === 'string')).toBe(true); } finally { await page.close(); } }); test('window.chrome.* ships the rich Layer C shape at runtime', async () => { const page = await context.newPage(); try { const shape = await page.evaluate(() => { const c = (window as any).chrome; return { hasRuntime: !!c?.runtime, hasPlatformArch: !!c?.runtime?.PlatformArch, hasOnInstalled: !!c?.runtime?.OnInstalledReason, csiIsFn: typeof c?.csi === 'function', loadTimesIsFn: typeof c?.loadTimes === 'function', appIsObj: typeof c?.app === 'object', }; }); expect(shape.hasRuntime).toBe(true); expect(shape.hasPlatformArch).toBe(true); expect(shape.hasOnInstalled).toBe(true); expect(shape.csiIsFn).toBe(true); expect(shape.loadTimesIsFn).toBe(true); expect(shape.appIsObj).toBe(true); } finally { await page.close(); } }); test('Notification.permission is default AND Permissions API returns prompt for notifications', async () => { // The cdc/Permissions shim now lives in applyStealth, so this pairing // holds on the plain newContext path too — previously it was headed-only, // which left Notification.permission=default mismatched against the native // Permissions answer in headless. Regression guard for that gap. const page = await context.newPage(); try { const result = await page.evaluate(async () => { const perm = typeof Notification !== 'undefined' ? Notification.permission : 'unavailable'; let queryState = 'unavailable'; try { const status = await navigator.permissions.query({ name: 'notifications' } as any); queryState = status.state; } catch {} return { perm, queryState }; }); expect(result.perm).toBe('default'); expect(result.queryState).toBe('prompt'); } finally { await page.close(); } }); test('patched getters report [native code] via the toString proxy', async () => { const page = await context.newPage(); try { const getterSrc = await page.evaluate(() => { const wd = Object.getOwnPropertyDescriptor(navigator, 'webdriver'); return wd && wd.get ? wd.get.toString() : ''; }); // Layer C wraps every patched getter through markNative, so the // Function.prototype.toString Proxy reports native code instead of the // injected source — defeats the toString integrity check. expect(getterSrc).toContain('[native code]'); } finally { await page.close(); } }); test('toString proxy survives the depth-3 recursion trick', async () => { // The headline claim: defeats fn.toString.toString.toString().includes( // '[native code]'). Depth-1 is covered above; this walks the full chain a // detector uses so a regression that only masks one level is caught. const page = await context.newPage(); try { const depth3 = await page.evaluate(() => { const wd = Object.getOwnPropertyDescriptor(navigator, 'webdriver'); const get = wd && wd.get; return get ? (get as any).toString.toString.toString().includes('[native code]') : false; }); expect(depth3).toBe(true); } finally { await page.close(); } }); test('chrome.csi() and chrome.loadTimes() execute without inventing runtime messaging', async () => { const page = await context.newPage(); try { const r = await page.evaluate(() => { const c = (window as any).chrome; return { csiOk: typeof c.csi().onloadT === 'number', loadTimesOk: typeof c.loadTimes().wasFetchedViaSpdy === 'boolean', connect: typeof c.runtime.connect, sendMessage: typeof c.runtime.sendMessage, }; }); expect(r.csiOk).toBe(true); expect(r.loadTimesOk).toBe(true); expect(r.connect).toBe('undefined'); expect(r.sendMessage).toBe('undefined'); } finally { await page.close(); } }); }); describe('extension messaging compatibility', () => { test('plain Chromium and default stealth both select the ordinary web flow without an extension', async () => { const plainBrowser = await chromium.launch({ headless: true }); try { for (const stealth of [false, true]) { const ctx = await plainBrowser.newContext(); try { if (stealth) await applyStealth(ctx); const page = await ctx.newPage(); await page.goto('data:text/html,