Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
}
}
Expand Down
53 changes: 50 additions & 3 deletions src/McpContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
HTTPRequest,
Page,
SerializedAXNode,
PredefinedNetworkConditions,
} from 'puppeteer-core';
import {Context} from './tools/ToolDefinition.js';
import {Debugger} from 'debug';
Expand All @@ -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;
Expand Down Expand Up @@ -136,6 +154,7 @@ export class McpContext implements Context {
} else {
this.#networkConditionsMap.set(page, conditions);
}
this.#updateSelectedPageTimeouts();
}

getNetworkConditions(): string | null {
Expand All @@ -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 {
Expand Down Expand Up @@ -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<ElementHandle<Element>> {
Expand Down Expand Up @@ -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<unknown>): Promise<void> {
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);
}
}
24 changes: 15 additions & 9 deletions src/WaitForHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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');
}),
]);
Expand Down Expand Up @@ -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,
});
}
Expand Down
35 changes: 35 additions & 0 deletions tests/McpContext.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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.getDefaultTimeout();
context.setCpuThrottlingRate(2);
const timeoutAfter = page.getDefaultTimeout();
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);
});
});
});
Loading