mirror of
https://github.com/garrytan/gstack.git
synced 2026-06-18 15:50:11 +02:00
test(browse): runtime + extended-mode coverage for the stealth blend
The stealth tests were all static string-shape assertions; nothing executed the script in a real page. Add real-Chromium runtime checks via applyStealth + page.evaluate: - Layer C runtime: window.chrome.* rich shape, Notification.permission='default' paired with permissions.query notifications='prompt' (guards the shim now running on every path), and patched getters reporting [native code]. - Per-install hardware: navigator.hardwareConcurrency/deviceMemory reflect the GSTACK_* env profile. - Extended-mode blend: navigator.plugins is faked when GSTACK_STEALTH=extended, Layer C still wins window.chrome.runtime, and navigator.webdriver stays false (own-prop getter survives extended's prototype delete). - Persistent-context (launchHeaded/handoff) parity now uses a page created AFTER applyStealth — the old test checked pages()[0], which predates the init script, so webdriver was false only via the launch arg, not Layer C. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -98,11 +98,165 @@ describe('applyStealth — context level', () => {
|
||||
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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyStealth — persistent context (headed-mode parity)', () => {
|
||||
test('webdriver mask applies to launchPersistentContext too (D7)', async () => {
|
||||
// Simulate the launchHeaded path: launchPersistentContext + applyStealth
|
||||
describe('applyStealth — per-install hardware from env', () => {
|
||||
let ctx: BrowserContext;
|
||||
let savedHw: string | undefined;
|
||||
let savedMem: string | undefined;
|
||||
|
||||
beforeAll(async () => {
|
||||
savedHw = process.env.GSTACK_HW_CONCURRENCY;
|
||||
savedMem = process.env.GSTACK_DEVICE_MEMORY;
|
||||
process.env.GSTACK_HW_CONCURRENCY = '12';
|
||||
process.env.GSTACK_DEVICE_MEMORY = '4';
|
||||
ctx = await browser.newContext();
|
||||
await applyStealth(ctx); // readHostProfile() reads env at call time
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await ctx.close();
|
||||
if (savedHw === undefined) delete process.env.GSTACK_HW_CONCURRENCY;
|
||||
else process.env.GSTACK_HW_CONCURRENCY = savedHw;
|
||||
if (savedMem === undefined) delete process.env.GSTACK_DEVICE_MEMORY;
|
||||
else process.env.GSTACK_DEVICE_MEMORY = savedMem;
|
||||
});
|
||||
|
||||
test('navigator.hardwareConcurrency and deviceMemory reflect the env profile', async () => {
|
||||
const page = await ctx.newPage();
|
||||
try {
|
||||
const hw = await page.evaluate(() => ({
|
||||
cores: navigator.hardwareConcurrency,
|
||||
mem: (navigator as any).deviceMemory,
|
||||
}));
|
||||
expect(hw.cores).toBe(12);
|
||||
expect(hw.mem).toBe(4);
|
||||
} finally {
|
||||
await page.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyStealth — extended mode layered on Layer C (GSTACK_STEALTH=extended)', () => {
|
||||
let ctx: BrowserContext;
|
||||
let savedStealth: string | undefined;
|
||||
|
||||
beforeAll(async () => {
|
||||
savedStealth = process.env.GSTACK_STEALTH;
|
||||
process.env.GSTACK_STEALTH = 'extended';
|
||||
ctx = await browser.newContext();
|
||||
await applyStealth(ctx);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await ctx.close();
|
||||
if (savedStealth === undefined) delete process.env.GSTACK_STEALTH;
|
||||
else process.env.GSTACK_STEALTH = savedStealth;
|
||||
});
|
||||
|
||||
test('extended actually runs: navigator.plugins is the faked PluginArray', async () => {
|
||||
const page = await ctx.newPage();
|
||||
try {
|
||||
const names = await page.evaluate(() => Array.from(navigator.plugins).map((p) => p.name));
|
||||
expect(names).toContain('PDF Viewer');
|
||||
} finally {
|
||||
await page.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('blend: Layer C wins window.chrome.runtime (rich shape, not extended skeletal)', async () => {
|
||||
// extended's chrome.runtime is {OnInstalledReason, OnRestartRequiredReason}
|
||||
// and is if(!)-guarded, so Layer C (applied first, with PlatformArch) must
|
||||
// win. This pins the coexistence ordering the blend depends on.
|
||||
const page = await ctx.newPage();
|
||||
try {
|
||||
const hasRich = await page.evaluate(() => !!(window as any).chrome?.runtime?.PlatformArch);
|
||||
expect(hasRich).toBe(true);
|
||||
} finally {
|
||||
await page.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('blend: navigator.webdriver stays false (Layer C own-prop survives extended prototype delete)', async () => {
|
||||
const page = await ctx.newPage();
|
||||
try {
|
||||
const wd = await page.evaluate(() => (navigator as any).webdriver);
|
||||
expect(wd).toBe(false);
|
||||
} finally {
|
||||
await page.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyStealth — persistent context (headed + handoff parity)', () => {
|
||||
test('full Layer C applies to launchPersistentContext (the launchHeaded/handoff path)', async () => {
|
||||
// Simulate the launchHeaded/handoff path: launchPersistentContext +
|
||||
// applyStealth. Verifies the persistent-context path gets the SAME Layer C
|
||||
// as newContext, not just the webdriver mask.
|
||||
const fs = await import('fs');
|
||||
const os = await import('os');
|
||||
const path = await import('path');
|
||||
@@ -114,9 +268,17 @@ describe('applyStealth — persistent context (headed-mode parity)', () => {
|
||||
});
|
||||
try {
|
||||
await applyStealth(ctx);
|
||||
const page = ctx.pages()[0] ?? await ctx.newPage();
|
||||
const webdriver = await page.evaluate(() => (navigator as any).webdriver);
|
||||
expect(webdriver).toBe(false);
|
||||
// Use a page created AFTER applyStealth. launchPersistentContext opens an
|
||||
// initial page at launch time, before addInitScript is registered, so
|
||||
// init scripts never run on pages()[0] (its webdriver is only false
|
||||
// because of the --disable-blink-features launch arg, not Layer C).
|
||||
const page = await ctx.newPage();
|
||||
const probe = await page.evaluate(() => ({
|
||||
webdriver: (navigator as any).webdriver,
|
||||
hasChromeRuntime: !!(window as any).chrome?.runtime?.PlatformArch,
|
||||
}));
|
||||
expect(probe.webdriver).toBe(false);
|
||||
expect(probe.hasChromeRuntime).toBe(true);
|
||||
} finally {
|
||||
await ctx.close();
|
||||
fs.rmSync(userDataDir, { recursive: true, force: true });
|
||||
|
||||
Reference in New Issue
Block a user