From 103e8c8eacf98a4d12106c1567fcff325bfc21f2 Mon Sep 17 00:00:00 2001 From: Nikolay Vitkov Date: Mon, 22 Sep 2025 17:36:47 +0200 Subject: [PATCH 1/2] fix: increase timeouts in case of Emulation --- CONTRIBUTING.md | 4 +-- src/McpContext.ts | 53 +++++++++++++++++++++++++++++++++++++--- src/WaitForHelper.ts | 24 +++++++++++------- tests/McpContext.test.ts | 35 ++++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 14 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 80a40c05..cad5d3ef 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -56,12 +56,12 @@ npx @modelcontextprotocol/inspector node build/src/index.js Add the MCP server to your client's config. -``` +```json { "mcpServers": { "chrome-devtools": { "command": "node", - "args": ["/path-to/build/src/index.js"], + "args": ["/path-to/build/src/index.js"] } } } diff --git a/src/McpContext.ts b/src/McpContext.ts index b2b2883c..77b2f25a 100644 --- a/src/McpContext.ts +++ b/src/McpContext.ts @@ -11,6 +11,7 @@ import { HTTPRequest, Page, SerializedAXNode, + PredefinedNetworkConditions, } from 'puppeteer-core'; import {Context} from './tools/ToolDefinition.js'; import {Debugger} from 'debug'; @@ -36,6 +37,23 @@ export interface TextSnapshot { const DEFAULT_TIMEOUT = 5_000; const NAVIGATION_TIMEOUT = 10_000; +function getNetworkMultiplierFromString(condition: string | null): number { + const puppeteerCondition = + condition as keyof typeof PredefinedNetworkConditions; + + switch (puppeteerCondition) { + case 'Fast 4G': + return 1; + case 'Slow 4G': + return 2.5; + case 'Fast 3G': + return 5; + case 'Slow 3G': + return 10; + } + return 1; +} + export class McpContext implements Context { browser: Browser; logger: Debugger; @@ -136,6 +154,7 @@ export class McpContext implements Context { } else { this.#networkConditionsMap.set(page, conditions); } + this.#updateSelectedPageTimeouts(); } getNetworkConditions(): string | null { @@ -146,6 +165,7 @@ export class McpContext implements Context { setCpuThrottlingRate(rate: number): void { const page = this.getSelectedPage(); this.#cpuThrottlingRateMap.set(page, rate); + this.#updateSelectedPageTimeouts(); } getCpuThrottlingRate(): number { @@ -205,12 +225,22 @@ export class McpContext implements Context { this.#selectedPageIdx = idx; const newPage = this.getSelectedPage(); newPage.on('dialog', this.#dialogHandler); + this.#updateSelectedPageTimeouts(); + } + #updateSelectedPageTimeouts() { + const page = this.getSelectedPage(); // For waiters 5sec timeout should be sufficient. - newPage.setDefaultTimeout(DEFAULT_TIMEOUT); + // Increased in case we throttle the CPU + const cpuMultiplier = this.getCpuThrottlingRate(); + page.setDefaultTimeout(DEFAULT_TIMEOUT * cpuMultiplier); // 10sec should be enough for the load event to be emitted during // navigations. - newPage.setDefaultNavigationTimeout(NAVIGATION_TIMEOUT); + // Increased in case we throttle the network requests + const networkMultiplier = getNetworkMultiplierFromString( + this.getNetworkConditions(), + ); + page.setDefaultNavigationTimeout(NAVIGATION_TIMEOUT * networkMultiplier); } async getElementByUid(uid: string): Promise> { @@ -315,9 +345,26 @@ export class McpContext implements Context { return this.#traceResults; } + getWaitForHelper( + page: Page, + cpuMultiplier: number, + networkMultiplier: number, + ) { + return new WaitForHelper(page, cpuMultiplier, networkMultiplier); + } + waitForEventsAfterAction(action: () => Promise): Promise { const page = this.getSelectedPage(); - const waitForHelper = new WaitForHelper(page); + const cpuMultiplier = this.getCpuThrottlingRate(); + const networkMultiplier = getNetworkMultiplierFromString( + this.getNetworkConditions(), + ); + + const waitForHelper = this.getWaitForHelper( + page, + cpuMultiplier, + networkMultiplier, + ); return waitForHelper.waitForEventsAfterAction(action); } } diff --git a/src/WaitForHelper.ts b/src/WaitForHelper.ts index d5221bff..d9ff0f44 100644 --- a/src/WaitForHelper.ts +++ b/src/WaitForHelper.ts @@ -9,15 +9,21 @@ import {logger} from './logger.js'; export class WaitForHelper { #abortController = new AbortController(); - #genericTimeout: number; + #page: CdpPage; + #stableDomTimeout: number; #stableDomFor: number; #expectNavigationIn: number; - #page: CdpPage; - - constructor(page: Page) { - this.#genericTimeout = 3000; - this.#stableDomFor = 100; - this.#expectNavigationIn = 100; + #navigationTimeout: number; + + constructor( + page: Page, + cpuTimeoutMultiplier: number, + networkTimeoutMultiplier: number, + ) { + this.#stableDomTimeout = 3000 * cpuTimeoutMultiplier; + this.#stableDomFor = 100 * cpuTimeoutMultiplier; + this.#expectNavigationIn = 100 * cpuTimeoutMultiplier; + this.#navigationTimeout = 3000 * networkTimeoutMultiplier; this.#page = page as unknown as CdpPage; } @@ -69,7 +75,7 @@ export class WaitForHelper { stableDomObserver.evaluate(async observer => { return await observer.resolver.promise; }), - this.timeout(this.#genericTimeout).then(() => { + this.timeout(this.#stableDomTimeout).then(() => { throw new Error('Timeout'); }), ]); @@ -128,7 +134,7 @@ export class WaitForHelper { const navigationStated = await navigationStartedPromise; if (navigationStated) { await this.#page.waitForNavigation({ - timeout: this.#genericTimeout, + timeout: this.#navigationTimeout, signal: this.#abortController.signal, }); } diff --git a/tests/McpContext.test.ts b/tests/McpContext.test.ts index c75d300a..9fc32595 100644 --- a/tests/McpContext.test.ts +++ b/tests/McpContext.test.ts @@ -7,6 +7,7 @@ import {describe, it} from 'node:test'; import assert from 'assert'; import {TraceResult} from '../src/trace-processing/parse.js'; import {withBrowser} from './utils.js'; +import sinon from 'sinon'; describe('McpContext', () => { it('list pages', async () => { @@ -38,4 +39,38 @@ describe('McpContext', () => { assert.deepEqual(context.recordedTraces(), [fakeTrace1, fakeTrace2]); }); }); + + it('should update default timeout when cpu throttling changes', async () => { + await withBrowser(async (_response, context) => { + const page = await context.newPage(); + const timeoutBefore = page.getDefaultNavigationTimeout(); + context.setCpuThrottlingRate(2); + const timeoutAfter = page.getDefaultNavigationTimeout(); + assert(timeoutBefore < timeoutAfter, 'Timeout was less then expected'); + }); + }); + + it('should update default timeout when network conditions changes', async () => { + await withBrowser(async (_response, context) => { + const page = await context.newPage(); + const timeoutBefore = page.getDefaultNavigationTimeout(); + context.setNetworkConditions('Slow 3G'); + const timeoutAfter = page.getDefaultNavigationTimeout(); + assert(timeoutBefore < timeoutAfter, 'Timeout was less then expected'); + }); + }); + + it('should call waitForEventsAfterAction with correct multipliers', async () => { + await withBrowser(async (_response, context) => { + const page = await context.newPage(); + + context.setCpuThrottlingRate(2); + context.setNetworkConditions('Slow 3G'); + const stub = sinon.spy(context, 'getWaitForHelper'); + + await context.waitForEventsAfterAction(async () => {}); + + sinon.assert.calledWithExactly(stub, page, 2, 10); + }); + }); }); From 9d9be64955eb455b73ef9571b9db36d3dbd260b9 Mon Sep 17 00:00:00 2001 From: Nikolay Vitkov Date: Mon, 22 Sep 2025 17:51:48 +0200 Subject: [PATCH 2/2] fix --- tests/McpContext.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/McpContext.test.ts b/tests/McpContext.test.ts index 9fc32595..2aa35f5a 100644 --- a/tests/McpContext.test.ts +++ b/tests/McpContext.test.ts @@ -43,9 +43,9 @@ describe('McpContext', () => { it('should update default timeout when cpu throttling changes', async () => { await withBrowser(async (_response, context) => { const page = await context.newPage(); - const timeoutBefore = page.getDefaultNavigationTimeout(); + const timeoutBefore = page.getDefaultTimeout(); context.setCpuThrottlingRate(2); - const timeoutAfter = page.getDefaultNavigationTimeout(); + const timeoutAfter = page.getDefaultTimeout(); assert(timeoutBefore < timeoutAfter, 'Timeout was less then expected'); }); });