fix(stealth): 修复 launch 选项与测试基线不一致问题
在 stealth 默认策略下保留自定义 user-agent,不再被 CDP 覆盖。 同步更新 protocol/browser/launch/file-access 相关测试预期,并放宽 browser.test 的 hook 超时以消除偶发超时。
This commit is contained in:
+9
-8
@@ -9,11 +9,11 @@ describe('BrowserManager', () => {
|
|||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
browser = new BrowserManager();
|
browser = new BrowserManager();
|
||||||
await browser.launch({ headless: true });
|
await browser.launch({ headless: true });
|
||||||
});
|
}, 30000);
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await browser.close();
|
await browser.close();
|
||||||
});
|
}, 30000);
|
||||||
|
|
||||||
describe('launch and close', () => {
|
describe('launch and close', () => {
|
||||||
it('should report as launched', () => {
|
it('should report as launched', () => {
|
||||||
@@ -56,7 +56,7 @@ describe('BrowserManager', () => {
|
|||||||
|
|
||||||
it('should report local stealth policy capabilities', async () => {
|
it('should report local stealth policy capabilities', async () => {
|
||||||
const testBrowser = new BrowserManager();
|
const testBrowser = new BrowserManager();
|
||||||
await testBrowser.launch({ headless: true, stealth: true });
|
await testBrowser.launch({ headless: true });
|
||||||
|
|
||||||
const status = testBrowser.getStealthStatus('chromium');
|
const status = testBrowser.getStealthStatus('chromium');
|
||||||
expect(status.enabled).toBe(true);
|
expect(status.enabled).toBe(true);
|
||||||
@@ -84,7 +84,7 @@ describe('BrowserManager', () => {
|
|||||||
const spy = vi.spyOn(chromium, 'connectOverCDP').mockResolvedValue(mockBrowser as any);
|
const spy = vi.spyOn(chromium, 'connectOverCDP').mockResolvedValue(mockBrowser as any);
|
||||||
|
|
||||||
const cdpBrowser = new BrowserManager();
|
const cdpBrowser = new BrowserManager();
|
||||||
await cdpBrowser.launch({ cdpPort: 9222, stealth: true });
|
await cdpBrowser.launch({ cdpPort: 9222 });
|
||||||
|
|
||||||
expect(addInitScript).toHaveBeenCalledTimes(1);
|
expect(addInitScript).toHaveBeenCalledTimes(1);
|
||||||
const status = cdpBrowser.getStealthStatus();
|
const status = cdpBrowser.getStealthStatus();
|
||||||
@@ -97,7 +97,7 @@ describe('BrowserManager', () => {
|
|||||||
spy.mockRestore();
|
spy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should disable stealth capabilities when launch stealth is false in CDP mode', async () => {
|
it('should ignore legacy stealth=false and keep CDP stealth capabilities enabled', async () => {
|
||||||
const addInitScript = vi.fn().mockResolvedValue(undefined);
|
const addInitScript = vi.fn().mockResolvedValue(undefined);
|
||||||
const mockPage = { url: () => 'http://example.com', on: vi.fn(), isClosed: () => false };
|
const mockPage = { url: () => 'http://example.com', on: vi.fn(), isClosed: () => false };
|
||||||
const mockContext = {
|
const mockContext = {
|
||||||
@@ -116,11 +116,12 @@ describe('BrowserManager', () => {
|
|||||||
const cdpBrowser = new BrowserManager();
|
const cdpBrowser = new BrowserManager();
|
||||||
await cdpBrowser.launch({ cdpPort: 9222, stealth: false });
|
await cdpBrowser.launch({ cdpPort: 9222, stealth: false });
|
||||||
|
|
||||||
expect(addInitScript).not.toHaveBeenCalled();
|
expect(addInitScript).toHaveBeenCalledTimes(1);
|
||||||
const status = cdpBrowser.getStealthStatus();
|
const status = cdpBrowser.getStealthStatus();
|
||||||
expect(status.enabled).toBe(false);
|
expect(status.enabled).toBe(true);
|
||||||
expect(status.connectionKind).toBe('cdp');
|
expect(status.connectionKind).toBe('cdp');
|
||||||
expect(status.capabilities).toEqual([]);
|
expect(status.capabilities).toContain('context-init-scripts');
|
||||||
|
expect(status.capabilities).not.toContain('chromium-launch-args');
|
||||||
|
|
||||||
await cdpBrowser.close();
|
await cdpBrowser.close();
|
||||||
spy.mockRestore();
|
spy.mockRestore();
|
||||||
|
|||||||
+7
-2
@@ -445,7 +445,10 @@ export class BrowserManager {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const policy = this.getStealthPolicy();
|
const policy = this.getStealthPolicy();
|
||||||
if (!policy.applyInitScripts) return;
|
if (!policy.applyInitScripts) return;
|
||||||
await applyStealthScripts(context, options);
|
await applyStealthScripts(context, {
|
||||||
|
...options,
|
||||||
|
userAgent: this.contextUserAgent,
|
||||||
|
});
|
||||||
this.logStealthPolicy('init-script applied');
|
this.logStealthPolicy('init-script applied');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1693,7 +1696,9 @@ export class BrowserManager {
|
|||||||
this.cdpEndpoint = null;
|
this.cdpEndpoint = null;
|
||||||
|
|
||||||
if (stealthPolicy.enabled && browserType === 'chromium') {
|
if (stealthPolicy.enabled && browserType === 'chromium') {
|
||||||
await applyBrowserLevelStealth(this.browser);
|
await applyBrowserLevelStealth(this.browser, {
|
||||||
|
userAgent: contextUserAgent,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options.userAgent && stealthPolicy.enabled && browserType === 'chromium') {
|
if (!options.userAgent && stealthPolicy.enabled && browserType === 'chromium') {
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ const cmd = (obj: object) => JSON.stringify(obj);
|
|||||||
|
|
||||||
describe('parseCommand', () => {
|
describe('parseCommand', () => {
|
||||||
describe('launch', () => {
|
describe('launch', () => {
|
||||||
it('should parse launch command with stealth flag', () => {
|
it('should parse launch command and ignore legacy stealth flag', () => {
|
||||||
const result = parseCommand(
|
const result = parseCommand(
|
||||||
cmd({ id: '1', action: 'launch', headless: false, stealth: true })
|
cmd({ id: '1', action: 'launch', headless: false, stealth: true })
|
||||||
);
|
);
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
expect(result.command.action).toBe('launch');
|
expect(result.command.action).toBe('launch');
|
||||||
expect(result.command.stealth).toBe(true);
|
expect((result.command as any).stealth).toBeUndefined();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+21
-9
@@ -10,6 +10,8 @@ import type { Browser, BrowserContext, Page } from 'playwright-core';
|
|||||||
|
|
||||||
export interface StealthScriptOptions {
|
export interface StealthScriptOptions {
|
||||||
locale?: string;
|
locale?: string;
|
||||||
|
userAgent?: string;
|
||||||
|
acceptLanguage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,25 +37,30 @@ export async function applyStealthScripts(
|
|||||||
// Apply CDP-level User-Agent override so Workers also get the patched UA.
|
// Apply CDP-level User-Agent override so Workers also get the patched UA.
|
||||||
// This must be done per-page since CDP sessions are page-scoped.
|
// This must be done per-page since CDP sessions are page-scoped.
|
||||||
for (const page of context.pages()) {
|
for (const page of context.pages()) {
|
||||||
await applyCDPStealthToPage(page);
|
await applyCDPStealthToPage(page, options);
|
||||||
}
|
}
|
||||||
context.on('page', (page: Page) => applyCDPStealthToPage(page));
|
context.on('page', (page: Page) => applyCDPStealthToPage(page, options));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply browser-level CDP overrides that affect all targets (including Workers).
|
* Apply browser-level CDP overrides that affect all targets (including Workers).
|
||||||
* Call this right after browser.launch() and before creating pages.
|
* Call this right after browser.launch() and before creating pages.
|
||||||
*/
|
*/
|
||||||
export async function applyBrowserLevelStealth(browser: Browser): Promise<void> {
|
export async function applyBrowserLevelStealth(
|
||||||
|
browser: Browser,
|
||||||
|
options: StealthScriptOptions = {}
|
||||||
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const cdp = await (browser as any).newBrowserCDPSession();
|
const cdp = await (browser as any).newBrowserCDPSession();
|
||||||
const version = await cdp.send('Browser.getVersion');
|
const version = await cdp.send('Browser.getVersion');
|
||||||
const rawUA = version?.userAgent ?? '';
|
const rawUA = version?.userAgent ?? '';
|
||||||
if (!rawUA.includes('HeadlessChrome')) {
|
const explicitUA = options.userAgent?.trim();
|
||||||
|
if (!explicitUA && !rawUA.includes('HeadlessChrome')) {
|
||||||
await cdp.detach();
|
await cdp.detach();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const patchedUA = rawUA.replace(/HeadlessChrome/g, 'Chrome');
|
const patchedUA = explicitUA || rawUA.replace(/HeadlessChrome/g, 'Chrome');
|
||||||
|
const acceptLanguage = options.acceptLanguage ?? 'en-US,en;q=0.9';
|
||||||
const metadata = buildUserAgentMetadata(patchedUA);
|
const metadata = buildUserAgentMetadata(patchedUA);
|
||||||
|
|
||||||
// Override on all existing targets
|
// Override on all existing targets
|
||||||
@@ -66,7 +73,7 @@ export async function applyBrowserLevelStealth(browser: Browser): Promise<void>
|
|||||||
});
|
});
|
||||||
await cdp.send('Emulation.setUserAgentOverride', {
|
await cdp.send('Emulation.setUserAgentOverride', {
|
||||||
userAgent: patchedUA,
|
userAgent: patchedUA,
|
||||||
acceptLanguage: 'en-US,en;q=0.9',
|
acceptLanguage,
|
||||||
platform: getPlatformString(),
|
platform: getPlatformString(),
|
||||||
userAgentMetadata: metadata,
|
userAgentMetadata: metadata,
|
||||||
});
|
});
|
||||||
@@ -81,17 +88,22 @@ export async function applyBrowserLevelStealth(browser: Browser): Promise<void>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applyCDPStealthToPage(page: Page): Promise<void> {
|
async function applyCDPStealthToPage(
|
||||||
|
page: Page,
|
||||||
|
options: StealthScriptOptions = {}
|
||||||
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const cdp = await page.context().newCDPSession(page);
|
const cdp = await page.context().newCDPSession(page);
|
||||||
const ua = await cdp.send('Browser.getVersion').catch(() => null);
|
const ua = await cdp.send('Browser.getVersion').catch(() => null);
|
||||||
const rawUA = ua?.userAgent ?? '';
|
const rawUA = ua?.userAgent ?? '';
|
||||||
const patchedUA = rawUA.replace(/HeadlessChrome/g, 'Chrome');
|
const explicitUA = options.userAgent?.trim();
|
||||||
|
const patchedUA = explicitUA || rawUA.replace(/HeadlessChrome/g, 'Chrome');
|
||||||
|
const acceptLanguage = options.acceptLanguage ?? 'en-US,en;q=0.9';
|
||||||
const metadata = buildUserAgentMetadata(patchedUA);
|
const metadata = buildUserAgentMetadata(patchedUA);
|
||||||
|
|
||||||
await cdp.send('Emulation.setUserAgentOverride', {
|
await cdp.send('Emulation.setUserAgentOverride', {
|
||||||
userAgent: patchedUA,
|
userAgent: patchedUA,
|
||||||
acceptLanguage: 'en-US,en;q=0.9',
|
acceptLanguage,
|
||||||
platform: getPlatformString(),
|
platform: getPlatformString(),
|
||||||
userAgentMetadata: metadata,
|
userAgentMetadata: metadata,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -146,9 +146,9 @@ describe('File Access (Issue #345)', () => {
|
|||||||
const content = await page.locator('h1').textContent();
|
const content = await page.locator('h1').textContent();
|
||||||
expect(content).toBe('Test File Access');
|
expect(content).toBe('Test File Access');
|
||||||
|
|
||||||
// Verify webdriver is hidden (from custom arg)
|
// Verify webdriver is hidden under stealth defaults
|
||||||
const webdriver = await page.evaluate(() => navigator.webdriver);
|
const webdriver = await page.evaluate(() => navigator.webdriver);
|
||||||
expect(webdriver).toBe(false);
|
expect(webdriver).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ describe('Launch Options', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('browser args', () => {
|
describe('browser args', () => {
|
||||||
it('should launch with custom args to disable webdriver detection', async () => {
|
it('should keep webdriver undefined with custom args under stealth defaults', async () => {
|
||||||
browser = new BrowserManager();
|
browser = new BrowserManager();
|
||||||
await browser.launch({
|
await browser.launch({
|
||||||
headless: true,
|
headless: true,
|
||||||
@@ -21,9 +21,9 @@ describe('Launch Options', () => {
|
|||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
await page.goto('about:blank');
|
await page.goto('about:blank');
|
||||||
|
|
||||||
// Check that navigator.webdriver is false
|
// Under stealth defaults, webdriver is hidden (undefined)
|
||||||
const webdriver = await page.evaluate(() => navigator.webdriver);
|
const webdriver = await page.evaluate(() => navigator.webdriver);
|
||||||
expect(webdriver).toBe(false);
|
expect(webdriver).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should launch with multiple args', async () => {
|
it('should launch with multiple args', async () => {
|
||||||
@@ -39,7 +39,7 @@ describe('Launch Options', () => {
|
|||||||
expect(browser.isLaunched()).toBe(true);
|
expect(browser.isLaunched()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should launch without args (default behavior)', async () => {
|
it('should launch without args and keep webdriver hidden by default', async () => {
|
||||||
browser = new BrowserManager();
|
browser = new BrowserManager();
|
||||||
await browser.launch({
|
await browser.launch({
|
||||||
headless: true,
|
headless: true,
|
||||||
@@ -48,9 +48,9 @@ describe('Launch Options', () => {
|
|||||||
const page = browser.getPage();
|
const page = browser.getPage();
|
||||||
await page.goto('about:blank');
|
await page.goto('about:blank');
|
||||||
|
|
||||||
// Default Playwright behavior - webdriver is true
|
// Stealth default behavior - webdriver is hidden
|
||||||
const webdriver = await page.evaluate(() => navigator.webdriver);
|
const webdriver = await page.evaluate(() => navigator.webdriver);
|
||||||
expect(webdriver).toBe(true);
|
expect(webdriver).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ describe('Launch Options', () => {
|
|||||||
|
|
||||||
// Verify webdriver is hidden
|
// Verify webdriver is hidden
|
||||||
const webdriver = await page.evaluate(() => navigator.webdriver);
|
const webdriver = await page.evaluate(() => navigator.webdriver);
|
||||||
expect(webdriver).toBe(false);
|
expect(webdriver).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user