mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 03:35:09 +02:00
test(stealth): comprehensive unit + e2e tests, fix platform mismatch
Tests (52 total, 0 failures): Unit tests (33): - Module exports validation (stealthArgs shape, applyStealthPatches type) - Launch args content (AutomationControlled, no-first-run, no forbidden flags) - Init script source analysis (all 10 patch vectors verified present) - applyStealthPatches API (mock context, GPU args, serialization, idempotency) - Adversarial edge cases (array spread safety, extension compat, GPU plausibility) - Import integration (browser-manager.ts correctly imports and calls both paths) - Old inline patches removal verification E2E tests (19): - Real Chromium launch with stealth patches applied - navigator.webdriver value AND property existence - WebGL1 + WebGL2 renderer spoofing - PluginArray instanceof + shape verification - Complete chrome object (app, runtime, loadTimes, csi) - Languages, permissions, CDP artifacts, Playwright globals - Platform/UA consistency - Patches survive page navigation Bug fix: navigator.platform now spoofed to 'MacIntel' when UA claims Macintosh. Previously reported 'Linux x86_64' in containers, which contradicts the Mac user agent and is a detectable fingerprint mismatch. Caught by the e2e test.
This commit is contained in:
+11
-1
@@ -188,7 +188,7 @@ export async function applyStealthPatches(
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 5. LANGUAGES
|
||||
// 5. LANGUAGES + PLATFORM
|
||||
// ========================================
|
||||
Object.defineProperty(navigator, 'languages', {
|
||||
get: () => ['en-US', 'en'],
|
||||
@@ -196,6 +196,16 @@ export async function applyStealthPatches(
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
// Platform must match the user agent. If UA says Mac, platform must be MacIntel.
|
||||
// navigator.platform is 'Linux x86_64' in containers which contradicts a Mac UA.
|
||||
if (navigator.userAgent.includes('Macintosh')) {
|
||||
Object.defineProperty(navigator, 'platform', {
|
||||
get: () => 'MacIntel',
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 6. CDP ARTIFACT CLEANUP
|
||||
// ========================================
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
/**
|
||||
* stealth-e2e.test.ts — End-to-end stealth verification
|
||||
*
|
||||
* Launches a real Chromium instance with stealth patches applied,
|
||||
* navigates to a page, and verifies all fingerprint vectors are clean.
|
||||
*
|
||||
* Requires: Chromium binary (Playwright's bundled or system)
|
||||
* Slower than unit tests (~5-10s). Run with: bun test stealth-e2e
|
||||
*/
|
||||
|
||||
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
||||
import { chromium, type Browser, type BrowserContext, type Page } from 'playwright';
|
||||
import { stealthArgs, applyStealthPatches } from '../src/stealth';
|
||||
|
||||
let browser: Browser;
|
||||
let context: BrowserContext;
|
||||
let page: Page;
|
||||
|
||||
beforeAll(async () => {
|
||||
browser = await chromium.launch({
|
||||
headless: true, // headless is fine for fingerprint checks
|
||||
args: [...stealthArgs, '--no-sandbox'],
|
||||
});
|
||||
context = await browser.newContext({
|
||||
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36',
|
||||
viewport: { width: 1920, height: 1080 },
|
||||
});
|
||||
await applyStealthPatches(context);
|
||||
page = await context.newPage();
|
||||
// Navigate to a blank page to initialize the browser context
|
||||
await page.goto('about:blank');
|
||||
}, 30_000);
|
||||
|
||||
afterAll(async () => {
|
||||
await context?.close().catch(() => {});
|
||||
await browser?.close().catch(() => {});
|
||||
});
|
||||
|
||||
describe('stealth e2e — fingerprint verification', () => {
|
||||
// ─── Webdriver ────────────────────────────────────────
|
||||
|
||||
test('navigator.webdriver is undefined', async () => {
|
||||
const val = await page.evaluate(() => navigator.webdriver);
|
||||
expect(val).toBeUndefined();
|
||||
});
|
||||
|
||||
test('"webdriver" is not in navigator (property existence check)', async () => {
|
||||
const exists = await page.evaluate(() => 'webdriver' in navigator);
|
||||
expect(exists).toBe(false);
|
||||
});
|
||||
|
||||
// ─── WebGL ────────────────────────────────────────────
|
||||
|
||||
test('WebGL vendor is spoofed (not SwiftShader)', async () => {
|
||||
const vendor = await page.evaluate(() => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const gl = canvas.getContext('webgl');
|
||||
if (!gl) return null;
|
||||
const ext = gl.getExtension('WEBGL_debug_renderer_info');
|
||||
if (!ext) return null;
|
||||
return gl.getParameter(ext.UNMASKED_VENDOR_WEBGL);
|
||||
});
|
||||
expect(vendor).toBeTruthy();
|
||||
expect(vendor).toContain('Apple');
|
||||
expect(vendor).not.toContain('SwiftShader');
|
||||
});
|
||||
|
||||
test('WebGL renderer is spoofed to Apple M1 Pro', async () => {
|
||||
const renderer = await page.evaluate(() => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const gl = canvas.getContext('webgl');
|
||||
if (!gl) return null;
|
||||
const ext = gl.getExtension('WEBGL_debug_renderer_info');
|
||||
if (!ext) return null;
|
||||
return gl.getParameter(ext.UNMASKED_RENDERER_WEBGL);
|
||||
});
|
||||
expect(renderer).toBeTruthy();
|
||||
expect(renderer).toContain('Apple M1 Pro');
|
||||
expect(renderer).not.toContain('SwiftShader');
|
||||
expect(renderer).not.toContain('llvmpipe');
|
||||
});
|
||||
|
||||
test('WebGL2 renderer is also spoofed', async () => {
|
||||
const renderer = await page.evaluate(() => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const gl = canvas.getContext('webgl2');
|
||||
if (!gl) return null;
|
||||
const ext = gl.getExtension('WEBGL_debug_renderer_info');
|
||||
if (!ext) return null;
|
||||
return gl.getParameter(ext.UNMASKED_RENDERER_WEBGL);
|
||||
});
|
||||
// WebGL2 might not be available in all environments
|
||||
if (renderer !== null) {
|
||||
expect(renderer).toContain('Apple M1 Pro');
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Plugins ──────────────────────────────────────────
|
||||
|
||||
test('navigator.plugins has 5 entries', async () => {
|
||||
const len = await page.evaluate(() => navigator.plugins.length);
|
||||
expect(len).toBe(5);
|
||||
});
|
||||
|
||||
test('navigator.plugins passes instanceof PluginArray', async () => {
|
||||
const isPluginArray = await page.evaluate(() => navigator.plugins instanceof PluginArray);
|
||||
expect(isPluginArray).toBe(true);
|
||||
});
|
||||
|
||||
test('navigator.plugins[0] is a Plugin with correct shape', async () => {
|
||||
const info = await page.evaluate(() => {
|
||||
const p = navigator.plugins[0];
|
||||
return {
|
||||
name: p?.name,
|
||||
filename: p?.filename,
|
||||
hasItem: typeof p?.item === 'function',
|
||||
hasNamedItem: typeof p?.namedItem === 'function',
|
||||
};
|
||||
});
|
||||
expect(info.name).toBe('Chrome PDF Plugin');
|
||||
expect(info.filename).toBe('internal-pdf-viewer');
|
||||
expect(info.hasItem).toBe(true);
|
||||
expect(info.hasNamedItem).toBe(true);
|
||||
});
|
||||
|
||||
// ─── Chrome Object ────────────────────────────────────
|
||||
|
||||
test('window.chrome exists and has app', async () => {
|
||||
const hasApp = await page.evaluate(() => !!(window as any).chrome?.app);
|
||||
expect(hasApp).toBe(true);
|
||||
});
|
||||
|
||||
test('window.chrome.app has correct shape', async () => {
|
||||
const shape = await page.evaluate(() => {
|
||||
const app = (window as any).chrome?.app;
|
||||
return {
|
||||
hasInstallState: !!app?.InstallState,
|
||||
hasRunningState: !!app?.RunningState,
|
||||
getDetails: typeof app?.getDetails,
|
||||
};
|
||||
});
|
||||
expect(shape.hasInstallState).toBe(true);
|
||||
expect(shape.hasRunningState).toBe(true);
|
||||
expect(shape.getDetails).toBe('function');
|
||||
});
|
||||
|
||||
test('window.chrome.runtime exists', async () => {
|
||||
const exists = await page.evaluate(() => !!(window as any).chrome?.runtime);
|
||||
expect(exists).toBe(true);
|
||||
});
|
||||
|
||||
test('window.chrome.loadTimes returns object', async () => {
|
||||
const result = await page.evaluate(() => {
|
||||
const lt = (window as any).chrome?.loadTimes;
|
||||
return typeof lt === 'function' ? typeof lt() : 'not a function';
|
||||
});
|
||||
expect(result).toBe('object');
|
||||
});
|
||||
|
||||
// ─── Languages ────────────────────────────────────────
|
||||
|
||||
test('navigator.languages is [en-US, en]', async () => {
|
||||
const langs = await page.evaluate(() => [...navigator.languages]);
|
||||
expect(langs).toEqual(['en-US', 'en']);
|
||||
});
|
||||
|
||||
// ─── Permissions ──────────────────────────────────────
|
||||
|
||||
test('notification permission returns prompt', async () => {
|
||||
const state = await page.evaluate(async () => {
|
||||
const result = await navigator.permissions.query({ name: 'notifications' as any });
|
||||
return result.state;
|
||||
});
|
||||
expect(state).toBe('prompt');
|
||||
});
|
||||
|
||||
// ─── CDP Artifacts ────────────────────────────────────
|
||||
|
||||
test('no cdc_ properties on window', async () => {
|
||||
const cdcKeys = await page.evaluate(() =>
|
||||
Object.keys(window).filter(k => k.startsWith('cdc_') || k.startsWith('$cdc_'))
|
||||
);
|
||||
expect(cdcKeys).toEqual([]);
|
||||
});
|
||||
|
||||
test('no __webdriver properties on document', async () => {
|
||||
const wdKeys = await page.evaluate(() =>
|
||||
Object.keys(document).filter(k => k.startsWith('__webdriver') || k.startsWith('__selenium'))
|
||||
);
|
||||
expect(wdKeys).toEqual([]);
|
||||
});
|
||||
|
||||
// ─── Automation Frameworks ────────────────────────────
|
||||
|
||||
test('no Playwright globals leaked', async () => {
|
||||
const leaked = await page.evaluate(() => ({
|
||||
__playwright: !!(window as any).__playwright,
|
||||
__pw_manual: !!(window as any).__pw_manual,
|
||||
_phantom: !!(window as any)._phantom,
|
||||
__nightmare: !!(window as any).__nightmare,
|
||||
_selenium: !!(window as any)._selenium,
|
||||
}));
|
||||
expect(leaked.__playwright).toBe(false);
|
||||
expect(leaked.__pw_manual).toBe(false);
|
||||
expect(leaked._phantom).toBe(false);
|
||||
expect(leaked.__nightmare).toBe(false);
|
||||
expect(leaked._selenium).toBe(false);
|
||||
});
|
||||
|
||||
// ─── Platform Consistency ─────────────────────────────
|
||||
|
||||
test('navigator.platform matches user agent (MacIntel)', async () => {
|
||||
const platform = await page.evaluate(() => navigator.platform);
|
||||
// Our UA says Mac, so platform should be MacIntel
|
||||
expect(platform).toBe('MacIntel');
|
||||
});
|
||||
|
||||
// ─── Stealth survives navigation ──────────────────────
|
||||
|
||||
test('patches survive page navigation', async () => {
|
||||
// Navigate to a data: URL (new document load)
|
||||
await page.goto('data:text/html,<h1>test</h1>');
|
||||
|
||||
const checks = await page.evaluate(() => ({
|
||||
webdriverUndef: navigator.webdriver === undefined,
|
||||
webdriverNotIn: !('webdriver' in navigator),
|
||||
pluginsLength: navigator.plugins.length,
|
||||
hasChrome: !!(window as any).chrome?.app,
|
||||
langs: [...navigator.languages],
|
||||
}));
|
||||
|
||||
expect(checks.webdriverUndef).toBe(true);
|
||||
expect(checks.webdriverNotIn).toBe(true);
|
||||
expect(checks.pluginsLength).toBe(5);
|
||||
expect(checks.hasChrome).toBe(true);
|
||||
expect(checks.langs).toEqual(['en-US', 'en']);
|
||||
});
|
||||
}, 30_000);
|
||||
@@ -0,0 +1,323 @@
|
||||
/**
|
||||
* stealth.test.ts — Unit + integration tests for anti-bot stealth patches
|
||||
*
|
||||
* Tests:
|
||||
* 1. Module exports (stealthArgs, applyStealthPatches)
|
||||
* 2. Launch args correctness
|
||||
* 3. Init script content validation (parsed from source)
|
||||
* 4. Integration with BrowserManager (import path, no crash)
|
||||
* 5. Adversarial: prototype pollution, toString traps, WebGL spoof values
|
||||
*/
|
||||
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import { stealthArgs, applyStealthPatches } from '../src/stealth';
|
||||
|
||||
// ─── 1. Module Exports ──────────────────────────────────
|
||||
|
||||
describe('stealth module exports', () => {
|
||||
test('stealthArgs is a non-empty array of strings', () => {
|
||||
expect(Array.isArray(stealthArgs)).toBe(true);
|
||||
expect(stealthArgs.length).toBeGreaterThan(0);
|
||||
for (const arg of stealthArgs) {
|
||||
expect(typeof arg).toBe('string');
|
||||
expect(arg.startsWith('--')).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('applyStealthPatches is an async function', () => {
|
||||
expect(typeof applyStealthPatches).toBe('function');
|
||||
// Should accept a context-like object without crashing at import time
|
||||
});
|
||||
});
|
||||
|
||||
// ─── 2. Launch Args ─────────────────────────────────────
|
||||
|
||||
describe('stealthArgs content', () => {
|
||||
test('includes AutomationControlled disable', () => {
|
||||
expect(stealthArgs).toContain('--disable-blink-features=AutomationControlled');
|
||||
});
|
||||
|
||||
test('includes no-first-run to avoid welcome page', () => {
|
||||
expect(stealthArgs).toContain('--no-first-run');
|
||||
});
|
||||
|
||||
test('does not include --headless (that is a separate concern)', () => {
|
||||
expect(stealthArgs.some(a => a.includes('headless'))).toBe(false);
|
||||
});
|
||||
|
||||
test('does not include --no-sandbox (environment-specific)', () => {
|
||||
expect(stealthArgs.some(a => a.includes('no-sandbox'))).toBe(false);
|
||||
});
|
||||
|
||||
test('does not include proxy args (runtime-specific)', () => {
|
||||
expect(stealthArgs.some(a => a.includes('proxy'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── 3. Init Script Content (source analysis) ──────────
|
||||
|
||||
describe('init script coverage', () => {
|
||||
// Read the source to verify all patches are present
|
||||
const source = require('fs').readFileSync(
|
||||
require('path').join(__dirname, '../src/stealth.ts'),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
test('patches navigator.webdriver via prototype deletion', () => {
|
||||
expect(source).toContain('Navigator.prototype');
|
||||
expect(source).toContain('webdriver');
|
||||
expect(source).toContain('delete');
|
||||
});
|
||||
|
||||
test('patches WebGL renderer (both WebGL1 and WebGL2)', () => {
|
||||
expect(source).toContain('WebGLRenderingContext.prototype.getParameter');
|
||||
expect(source).toContain('WebGL2RenderingContext');
|
||||
expect(source).toContain('0x9245'); // UNMASKED_VENDOR
|
||||
expect(source).toContain('0x9246'); // UNMASKED_RENDERER
|
||||
});
|
||||
|
||||
test('creates proper PluginArray (not raw array)', () => {
|
||||
expect(source).toContain('PluginArray.prototype');
|
||||
expect(source).toContain('MimeType.prototype');
|
||||
expect(source).toContain('Plugin.prototype');
|
||||
expect(source).toContain('Symbol.iterator');
|
||||
});
|
||||
|
||||
test('sets up complete chrome object with app', () => {
|
||||
expect(source).toContain('chrome.app');
|
||||
expect(source).toContain('InstallState');
|
||||
expect(source).toContain('RunningState');
|
||||
expect(source).toContain('chrome.runtime');
|
||||
expect(source).toContain('chrome.csi');
|
||||
expect(source).toContain('chrome.loadTimes');
|
||||
});
|
||||
|
||||
test('cleans CDP artifacts', () => {
|
||||
expect(source).toContain('cdc_');
|
||||
expect(source).toContain('$cdc_');
|
||||
expect(source).toContain('__webdriver');
|
||||
expect(source).toContain('__selenium');
|
||||
});
|
||||
|
||||
test('patches Permissions API for notifications', () => {
|
||||
expect(source).toContain('permissions');
|
||||
expect(source).toContain('notifications');
|
||||
expect(source).toContain('prompt');
|
||||
});
|
||||
|
||||
test('patches Function.prototype.toString', () => {
|
||||
expect(source).toContain('Function.prototype.toString');
|
||||
expect(source).toContain('[native code]');
|
||||
});
|
||||
|
||||
test('handles mediaDevices for containers', () => {
|
||||
expect(source).toContain('mediaDevices');
|
||||
expect(source).toContain('enumerateDevices');
|
||||
expect(source).toContain('getUserMedia');
|
||||
});
|
||||
|
||||
test('spoofs navigator.platform to match UA', () => {
|
||||
expect(source).toContain('navigator.platform');
|
||||
expect(source).toContain('MacIntel');
|
||||
expect(source).toContain('Macintosh');
|
||||
});
|
||||
|
||||
test('passes GPU vendor/renderer as args (not hardcoded in browser context)', () => {
|
||||
// The function signature should accept args for GPU strings
|
||||
expect(source).toContain('gpuVendor');
|
||||
expect(source).toContain('gpuRenderer');
|
||||
// And pass them to addInitScript as the second arg
|
||||
expect(source).toContain('[gpuVendor, gpuRenderer]');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── 4. applyStealthPatches API ─────────────────────────
|
||||
|
||||
describe('applyStealthPatches API', () => {
|
||||
test('rejects when called without a context', async () => {
|
||||
// @ts-expect-error - intentionally passing null
|
||||
await expect(applyStealthPatches(null)).rejects.toThrow();
|
||||
});
|
||||
|
||||
test('rejects when context has no addInitScript', async () => {
|
||||
// @ts-expect-error - intentionally passing incomplete mock
|
||||
await expect(applyStealthPatches({})).rejects.toThrow();
|
||||
});
|
||||
|
||||
test('calls addInitScript on a mock context', async () => {
|
||||
let called = false;
|
||||
let receivedArg: unknown;
|
||||
const mockContext = {
|
||||
addInitScript: async (fn: unknown, arg: unknown) => {
|
||||
called = true;
|
||||
receivedArg = arg;
|
||||
},
|
||||
};
|
||||
// @ts-expect-error - mock
|
||||
await applyStealthPatches(mockContext);
|
||||
expect(called).toBe(true);
|
||||
});
|
||||
|
||||
test('passes GPU args as [vendor, renderer] tuple', async () => {
|
||||
let receivedArg: unknown;
|
||||
const mockContext = {
|
||||
addInitScript: async (_fn: unknown, arg: unknown) => {
|
||||
receivedArg = arg;
|
||||
},
|
||||
};
|
||||
// @ts-expect-error - mock
|
||||
await applyStealthPatches(mockContext, {
|
||||
gpuVendor: 'TestVendor',
|
||||
gpuRenderer: 'TestRenderer',
|
||||
});
|
||||
expect(receivedArg).toEqual(['TestVendor', 'TestRenderer']);
|
||||
});
|
||||
|
||||
test('uses default GPU strings when no options provided', async () => {
|
||||
let receivedArg: unknown;
|
||||
const mockContext = {
|
||||
addInitScript: async (_fn: unknown, arg: unknown) => {
|
||||
receivedArg = arg;
|
||||
},
|
||||
};
|
||||
// @ts-expect-error - mock
|
||||
await applyStealthPatches(mockContext);
|
||||
const [vendor, renderer] = receivedArg as [string, string];
|
||||
expect(vendor).toContain('Apple');
|
||||
expect(renderer).toContain('M1 Pro');
|
||||
});
|
||||
|
||||
test('init script function is serializable (no closures over Node APIs)', async () => {
|
||||
let capturedFn: Function | null = null;
|
||||
const mockContext = {
|
||||
addInitScript: async (fn: unknown, _arg: unknown) => {
|
||||
capturedFn = fn as Function;
|
||||
},
|
||||
};
|
||||
// @ts-expect-error - mock
|
||||
await applyStealthPatches(mockContext);
|
||||
expect(capturedFn).not.toBeNull();
|
||||
// The function should be serializable via toString (Playwright does this)
|
||||
const str = capturedFn!.toString();
|
||||
expect(str).toContain('Navigator.prototype');
|
||||
// Should NOT reference any Node.js APIs (require, process, Buffer, etc.)
|
||||
expect(str).not.toContain('require(');
|
||||
expect(str).not.toContain('process.');
|
||||
expect(str).not.toContain('Buffer.');
|
||||
expect(str).not.toContain('__dirname');
|
||||
expect(str).not.toContain('__filename');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── 5. Adversarial: Edge Cases ─────────────────────────
|
||||
|
||||
describe('adversarial edge cases', () => {
|
||||
test('stealthArgs are safe to spread into existing arrays', () => {
|
||||
const existing = ['--no-sandbox', '--disable-gpu'];
|
||||
const combined = [...existing, ...stealthArgs];
|
||||
expect(combined.length).toBe(existing.length + stealthArgs.length);
|
||||
// No duplicates of safety-critical flags
|
||||
const unique = new Set(combined);
|
||||
expect(unique.size).toBe(combined.length);
|
||||
});
|
||||
|
||||
test('stealthArgs do not contain flags that break extension loading', () => {
|
||||
// These flags would break GStack's headed mode with extension
|
||||
const forbidden = ['--disable-extensions', '--disable-component-extensions-with-background-pages'];
|
||||
for (const flag of forbidden) {
|
||||
expect(stealthArgs).not.toContain(flag);
|
||||
}
|
||||
});
|
||||
|
||||
test('GPU spoof strings are plausible (not detectable as fake)', () => {
|
||||
// The default GPU strings should match what a real Mac reports
|
||||
let receivedArg: [string, string] | null = null;
|
||||
const mockContext = {
|
||||
addInitScript: async (_fn: unknown, arg: unknown) => {
|
||||
receivedArg = arg as [string, string];
|
||||
},
|
||||
};
|
||||
// @ts-expect-error - mock
|
||||
applyStealthPatches(mockContext).then(() => {
|
||||
const [vendor, renderer] = receivedArg!;
|
||||
// Should look like a real Apple GPU report
|
||||
expect(vendor).toMatch(/Google Inc\./);
|
||||
expect(renderer).toMatch(/ANGLE.*Apple.*M1/);
|
||||
// Should NOT contain SwiftShader, llvmpipe, or Mesa
|
||||
expect(renderer).not.toMatch(/SwiftShader|llvmpipe|Mesa|Subzero/i);
|
||||
});
|
||||
});
|
||||
|
||||
test('applyStealthPatches can be called multiple times without error', async () => {
|
||||
let callCount = 0;
|
||||
const mockContext = {
|
||||
addInitScript: async () => { callCount++; },
|
||||
};
|
||||
// @ts-expect-error - mock
|
||||
await applyStealthPatches(mockContext);
|
||||
// @ts-expect-error - mock
|
||||
await applyStealthPatches(mockContext);
|
||||
// Should be called twice (no guard against double-apply)
|
||||
// This is fine — Playwright deduplicates addInitScript internally
|
||||
expect(callCount).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── 6. Import Integration ──────────────────────────────
|
||||
|
||||
describe('import integration', () => {
|
||||
test('browser-manager.ts imports stealth module', () => {
|
||||
const bmSource = require('fs').readFileSync(
|
||||
require('path').join(__dirname, '../src/browser-manager.ts'),
|
||||
'utf-8',
|
||||
);
|
||||
expect(bmSource).toContain("import { stealthArgs, applyStealthPatches } from './stealth'");
|
||||
});
|
||||
|
||||
test('browser-manager.ts uses stealthArgs in launch()', () => {
|
||||
const bmSource = require('fs').readFileSync(
|
||||
require('path').join(__dirname, '../src/browser-manager.ts'),
|
||||
'utf-8',
|
||||
);
|
||||
// In launch() — headless path
|
||||
expect(bmSource).toContain('...stealthArgs');
|
||||
});
|
||||
|
||||
test('browser-manager.ts calls applyStealthPatches in launch()', () => {
|
||||
const bmSource = require('fs').readFileSync(
|
||||
require('path').join(__dirname, '../src/browser-manager.ts'),
|
||||
'utf-8',
|
||||
);
|
||||
expect(bmSource).toContain('await applyStealthPatches(this.context)');
|
||||
});
|
||||
|
||||
test('browser-manager.ts uses stealthArgs in launchHeaded()', () => {
|
||||
const bmSource = require('fs').readFileSync(
|
||||
require('path').join(__dirname, '../src/browser-manager.ts'),
|
||||
'utf-8',
|
||||
);
|
||||
expect(bmSource).toContain('...stealthArgs');
|
||||
});
|
||||
|
||||
test('browser-manager.ts calls applyStealthPatches in launchHeaded()', () => {
|
||||
const bmSource = require('fs').readFileSync(
|
||||
require('path').join(__dirname, '../src/browser-manager.ts'),
|
||||
'utf-8',
|
||||
);
|
||||
// Should have exactly 2 calls to applyStealthPatches (launch + launchHeaded)
|
||||
const matches = bmSource.match(/applyStealthPatches/g) || [];
|
||||
expect(matches.length).toBeGreaterThanOrEqual(3); // import + 2 calls
|
||||
});
|
||||
|
||||
test('old inline stealth patches are removed from browser-manager.ts', () => {
|
||||
const bmSource = require('fs').readFileSync(
|
||||
require('path').join(__dirname, '../src/browser-manager.ts'),
|
||||
'utf-8',
|
||||
);
|
||||
// Old inline patches should be gone
|
||||
expect(bmSource).not.toContain("name: 'PDF Viewer'");
|
||||
expect(bmSource).not.toContain("Fake plugins array");
|
||||
expect(bmSource).not.toContain("Fake languages");
|
||||
expect(bmSource).not.toContain("key.startsWith('cdc_')");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user