/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import assert from 'node:assert'; import {afterEach, describe, it} from 'node:test'; import sinon from 'sinon'; import {NetworkFormatter} from '../src/formatters/NetworkFormatter.js'; import type {HTTPResponse} from '../src/third_party/index.js'; import type {TraceResult} from '../src/trace-processing/parse.js'; import {getMockRequest, html, withMcpContext} from './utils.js'; describe('McpContext', () => { afterEach(() => { sinon.restore(); }); it('list pages', async () => { await withMcpContext(async (_response, context) => { const page = context.getSelectedMcpPage(); await page.pptrPage.setContent( html` `, ); await context.createTextSnapshot(context.getSelectedMcpPage()); assert.ok(await page.getElementByUid('1_1')); await context.createTextSnapshot(context.getSelectedMcpPage()); await page.getElementByUid('1_1'); }); }); it('can store and retrieve the latest performance trace', async () => { await withMcpContext(async (_response, context) => { const fakeTrace1 = {} as unknown as TraceResult; const fakeTrace2 = {} as unknown as TraceResult; context.storeTraceRecording(fakeTrace1); context.storeTraceRecording(fakeTrace2); assert.deepEqual(context.recordedTraces(), [fakeTrace2]); }); }); it('should update default timeout when cpu throttling changes', async () => { await withMcpContext(async (_response, context) => { const page = await context.newPage(); const timeoutBefore = page.pptrPage.getDefaultTimeout(); await context.emulate({cpuThrottlingRate: 2}); const timeoutAfter = page.pptrPage.getDefaultTimeout(); assert(timeoutBefore < timeoutAfter, 'Timeout was less then expected'); }); }); it('should update default timeout when network conditions changes', async () => { await withMcpContext(async (_response, context) => { const page = await context.newPage(); const timeoutBefore = page.pptrPage.getDefaultNavigationTimeout(); await context.emulate({networkConditions: 'Slow 3G'}); const timeoutAfter = page.pptrPage.getDefaultNavigationTimeout(); assert(timeoutBefore < timeoutAfter, 'Timeout was less then expected'); }); }); it('should call waitForEventsAfterAction with correct multipliers', async () => { await withMcpContext(async (_response, context) => { const page = await context.newPage(); await context.emulate({ cpuThrottlingRate: 2, networkConditions: 'Slow 3G', }); const stub = sinon.spy(page, 'createWaitForHelper'); await page.waitForEventsAfterAction(async () => { // trigger the waiting only }); sinon.assert.calledWithExactly(stub, 2, 10); }); }); it('should should detect open DevTools pages', async () => { await withMcpContext( async (_response, context) => { const page = await context.newPage(); await context.createPagesSnapshot(); assert.ok(context.getDevToolsPage(page.pptrPage)); }, { autoOpenDevTools: true, }, ); }); it('resolves uid from a non-selected page snapshot', async () => { await withMcpContext(async (_response, context) => { // Page 1: set content and snapshot const page1 = context.getSelectedMcpPage(); await page1.pptrPage.setContent(html``); await context.createTextSnapshot(page1, false, undefined); // Capture a uid from page1's snapshot (snapshotId=1, button is node 1) const page1Uid = '1_1'; const page1Node = context.getAXNodeByUid(page1Uid); assert.ok(page1Node, 'uid should resolve from page1 snapshot'); // Page 2: new page, set content, snapshot const page2 = await context.newPage(); context.selectPage(page2); await page2.pptrPage.setContent(html``); await context.createTextSnapshot(page2, false, undefined); // Page 2 is now selected. Page 1's uid should still resolve. const node = context.getAXNodeByUid(page1Uid); assert.ok(node, 'page1 uid should still resolve after page2 snapshot'); assert.strictEqual(node?.name, 'Page1 Button'); // The element should also be retrievable when the target page is provided. const element = await page1.getElementByUid(page1Uid); assert.ok(element, 'should get element handle from page1 snapshot uid'); }); }); it('should include network requests in structured content', async t => { await withMcpContext(async (response, context) => { const mockRequest = getMockRequest({ url: 'http://example.com/api', stableId: 123, }); sinon.stub(context, 'getNetworkRequests').returns([mockRequest]); sinon.stub(context, 'getNetworkRequestStableId').returns(123); response.setIncludeNetworkRequests(true); const result = await response.handle('test', context); t.assert.snapshot?.(JSON.stringify(result.structuredContent, null, 2)); }); }); it('should include detailed network request in structured content', async t => { await withMcpContext(async (response, context) => { const mockRequest = getMockRequest({ url: 'http://example.com/detail', stableId: 456, }); sinon.stub(context, 'getNetworkRequestById').returns(mockRequest); sinon.stub(context, 'getNetworkRequestStableId').returns(456); response.attachNetworkRequest(456); const result = await response.handle('test', context); t.assert.snapshot?.(JSON.stringify(result.structuredContent, null, 2)); }); }); it('should include file paths in structured content when saving to file', async t => { await withMcpContext(async (response, context) => { const mockRequest = getMockRequest({ url: 'http://example.com/file-save', stableId: 789, hasPostData: true, postData: 'some detailed data', response: { status: () => 200, headers: () => ({'content-type': 'text/plain'}), buffer: async () => Buffer.from('some response data'), } as unknown as HTTPResponse, }); sinon.stub(context, 'getNetworkRequestById').returns(mockRequest); sinon.stub(context, 'getNetworkRequestStableId').returns(789); // We stub NetworkFormatter.from to avoid actual file system writes and verify arguments const fromStub = sinon .stub(NetworkFormatter, 'from') .callsFake(async (_req, opts) => { // Verify we received the file paths assert.strictEqual(opts?.requestFilePath, '/tmp/req.txt'); assert.strictEqual(opts?.responseFilePath, '/tmp/res.txt'); // Return a dummy formatter that behaves as if it saved files // We need to create a real instance or mock one. // Since constructor is private, we can't easily new it up. // But we can return a mock object. return { toStringDetailed: () => 'Detailed string', toJSONDetailed: () => ({ requestBody: '/tmp/req.txt', responseBody: '/tmp/res.txt', }), } as unknown as NetworkFormatter; }); response.attachNetworkRequest(789, { requestFilePath: '/tmp/req.txt', responseFilePath: '/tmp/res.txt', }); const result = await response.handle('test', context); t.assert.snapshot?.(JSON.stringify(result.structuredContent, null, 2)); fromStub.restore(); }); }); });