chromedevtools--chrome-devtools-mcp
327a3884d8
LLM are trained to know the common status code so we can rely on that when displaying the status code and fall back to text in case of errors or pending reponse.
472 行
14 KiB
TypeScript
472 行
14 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import assert from 'node:assert';
|
|
import {mkdtemp, readFile, rm, writeFile} from 'node:fs/promises';
|
|
import {tmpdir} from 'node:os';
|
|
import {join} from 'node:path';
|
|
import {afterEach, beforeEach, describe, it} from 'node:test';
|
|
|
|
import {NetworkFormatter} from '../../src/formatters/NetworkFormatter.js';
|
|
import type {HTTPRequest} from '../../src/third_party/index.js';
|
|
import {getMockRequest, getMockResponse} from '../utils.js';
|
|
|
|
describe('NetworkFormatter', () => {
|
|
let tmpDir: string;
|
|
|
|
beforeEach(async () => {
|
|
tmpDir = await mkdtemp(join(tmpdir(), 'network-formatter-test-'));
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await rm(tmpDir, {recursive: true, force: true});
|
|
});
|
|
|
|
describe('toString', () => {
|
|
it('works', async () => {
|
|
const request = getMockRequest();
|
|
const formatter = await NetworkFormatter.from(request, {
|
|
requestId: 1,
|
|
saveFile: async () => ({filename: ''}),
|
|
});
|
|
|
|
assert.equal(
|
|
formatter.toString(),
|
|
'reqid=1 GET http://example.com [pending]',
|
|
);
|
|
});
|
|
it('shows correct method', async () => {
|
|
const request = getMockRequest({method: 'POST'});
|
|
const formatter = await NetworkFormatter.from(request, {
|
|
requestId: 1,
|
|
saveFile: async () => ({filename: ''}),
|
|
});
|
|
|
|
assert.equal(
|
|
formatter.toString(),
|
|
'reqid=1 POST http://example.com [pending]',
|
|
);
|
|
});
|
|
it('shows correct status for request with response code in 200', async () => {
|
|
const response = getMockResponse();
|
|
const request = getMockRequest({response});
|
|
const formatter = await NetworkFormatter.from(request, {
|
|
requestId: 1,
|
|
saveFile: async () => ({filename: ''}),
|
|
});
|
|
|
|
assert.equal(
|
|
formatter.toString(),
|
|
'reqid=1 GET http://example.com [200]',
|
|
);
|
|
});
|
|
it('shows correct status for request with response code in 100', async () => {
|
|
const response = getMockResponse({
|
|
status: 199,
|
|
});
|
|
const request = getMockRequest({response});
|
|
const formatter = await NetworkFormatter.from(request, {
|
|
requestId: 1,
|
|
saveFile: async () => ({filename: ''}),
|
|
});
|
|
|
|
assert.equal(
|
|
formatter.toString(),
|
|
'reqid=1 GET http://example.com [199]',
|
|
);
|
|
});
|
|
it('shows correct status for request with response code above 200', async () => {
|
|
const response = getMockResponse({
|
|
status: 300,
|
|
});
|
|
const request = getMockRequest({response});
|
|
const formatter = await NetworkFormatter.from(request, {
|
|
requestId: 1,
|
|
saveFile: async () => ({filename: ''}),
|
|
});
|
|
|
|
assert.equal(
|
|
formatter.toString(),
|
|
'reqid=1 GET http://example.com [300]',
|
|
);
|
|
});
|
|
it('shows correct status for request that failed', async () => {
|
|
const request = getMockRequest({
|
|
failure() {
|
|
return {
|
|
errorText: 'Error in Network',
|
|
};
|
|
},
|
|
});
|
|
const formatter = await NetworkFormatter.from(request, {
|
|
requestId: 1,
|
|
saveFile: async () => ({filename: ''}),
|
|
});
|
|
|
|
assert.equal(
|
|
formatter.toString(),
|
|
'reqid=1 GET http://example.com [Error in Network]',
|
|
);
|
|
});
|
|
|
|
it('marks requests selected in DevTools UI', async () => {
|
|
const request = getMockRequest();
|
|
const formatter = await NetworkFormatter.from(request, {
|
|
requestId: 1,
|
|
selectedInDevToolsUI: true,
|
|
saveFile: async () => ({filename: ''}),
|
|
});
|
|
|
|
assert.equal(
|
|
formatter.toString(),
|
|
'reqid=1 GET http://example.com [pending] [selected in the DevTools Network panel]',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('toStringDetailed', () => {
|
|
it('works with request body from fetchPostData', async () => {
|
|
const request = getMockRequest({
|
|
hasPostData: true,
|
|
postData: undefined,
|
|
fetchPostData: Promise.resolve('test'),
|
|
});
|
|
const formatter = await NetworkFormatter.from(request, {
|
|
requestId: 200,
|
|
fetchData: true,
|
|
saveFile: async () => ({filename: ''}),
|
|
});
|
|
const result = formatter.toStringDetailed();
|
|
assert.match(result, /test/);
|
|
});
|
|
|
|
it('works with request body from postData', async () => {
|
|
const request = getMockRequest({
|
|
postData: JSON.stringify({
|
|
request: 'body',
|
|
}),
|
|
hasPostData: true,
|
|
});
|
|
const formatter = await NetworkFormatter.from(request, {
|
|
requestId: 200,
|
|
fetchData: true,
|
|
saveFile: async () => ({filename: ''}),
|
|
});
|
|
const result = formatter.toStringDetailed();
|
|
|
|
assert.match(
|
|
result,
|
|
new RegExp(
|
|
JSON.stringify({
|
|
request: 'body',
|
|
}),
|
|
),
|
|
);
|
|
});
|
|
|
|
it('truncates request body', async () => {
|
|
const request = getMockRequest({
|
|
postData: 'some text that is longer than expected',
|
|
hasPostData: true,
|
|
});
|
|
const formatter = await NetworkFormatter.from(request, {
|
|
requestId: 20,
|
|
fetchData: true,
|
|
saveFile: async () => ({filename: ''}),
|
|
});
|
|
const result = formatter.toStringDetailed();
|
|
assert.match(result, /some text/);
|
|
});
|
|
|
|
it('should save bodies to file when file paths are provided', async () => {
|
|
const request = {
|
|
method: () => 'POST',
|
|
url: () => 'http://example.com',
|
|
headers: () => ({}),
|
|
hasPostData: () => true,
|
|
postData: () => 'request body',
|
|
response: () => ({
|
|
status: () => 200,
|
|
headers: () => ({}),
|
|
buffer: async () => Buffer.from('response body'),
|
|
}),
|
|
failure: () => null,
|
|
redirectChain: () => [],
|
|
fetchPostData: async () => undefined,
|
|
} as unknown as HTTPRequest;
|
|
|
|
const reqPath = join(tmpDir, 'test_req_' + Date.now());
|
|
const resPath = join(tmpDir, 'test_res_' + Date.now());
|
|
|
|
const formatter = await NetworkFormatter.from(request, {
|
|
fetchData: true,
|
|
requestFilePath: reqPath,
|
|
responseFilePath: resPath,
|
|
saveFile: async (data, filename) => {
|
|
await writeFile(filename, data);
|
|
return {filename};
|
|
},
|
|
});
|
|
|
|
const json = formatter.toJSONDetailed() as {
|
|
requestBody: string;
|
|
responseBody: string;
|
|
requestBodyFilePath: string;
|
|
responseBodyFilePath: string;
|
|
};
|
|
assert.strictEqual(json.requestBodyFilePath, reqPath);
|
|
assert.strictEqual(json.responseBodyFilePath, resPath);
|
|
assert.strictEqual(json.requestBody, undefined);
|
|
assert.strictEqual(json.responseBody, undefined);
|
|
});
|
|
|
|
it('should not truncate large bodies when saving to file', async () => {
|
|
const largeBody = 'a'.repeat(10005);
|
|
const request = {
|
|
method: () => 'POST',
|
|
url: () => 'http://example.com',
|
|
headers: () => ({}),
|
|
hasPostData: () => true,
|
|
postData: () => largeBody,
|
|
response: () => ({
|
|
status: () => 200,
|
|
headers: () => ({}),
|
|
buffer: async () => Buffer.from(largeBody),
|
|
}),
|
|
failure: () => null,
|
|
redirectChain: () => [],
|
|
fetchPostData: async () => undefined,
|
|
} as unknown as HTTPRequest;
|
|
|
|
const reqPath = join(tmpDir, 'test_req_large_' + Date.now());
|
|
const resPath = join(tmpDir, 'test_res_large_' + Date.now());
|
|
|
|
await NetworkFormatter.from(request, {
|
|
fetchData: true,
|
|
requestFilePath: reqPath,
|
|
responseFilePath: resPath,
|
|
saveFile: async (data, filename) => {
|
|
await writeFile(filename, data);
|
|
return {filename};
|
|
},
|
|
});
|
|
|
|
const reqContent = await readFile(reqPath, 'utf8');
|
|
const resContent = await readFile(resPath, 'utf8');
|
|
|
|
assert.strictEqual(reqContent, largeBody);
|
|
assert.strictEqual(resContent, largeBody);
|
|
});
|
|
|
|
it('handles response body', async () => {
|
|
const response = getMockResponse();
|
|
response.buffer = () => {
|
|
return Promise.resolve(Buffer.from(JSON.stringify({response: 'body'})));
|
|
};
|
|
const request = getMockRequest({response});
|
|
|
|
const formatter = await NetworkFormatter.from(request, {
|
|
requestId: 200,
|
|
fetchData: true,
|
|
saveFile: async () => ({filename: ''}),
|
|
});
|
|
const result = formatter.toStringDetailed();
|
|
|
|
assert.match(result, /"response":"body"/);
|
|
});
|
|
|
|
it('handles redirect chain', async () => {
|
|
const redirectRequest = getMockRequest({
|
|
url: 'http://example.com/redirect',
|
|
});
|
|
const request = getMockRequest({
|
|
redirectChain: [redirectRequest],
|
|
});
|
|
const formatter = await NetworkFormatter.from(request, {
|
|
requestId: 1,
|
|
requestIdResolver: () => 2,
|
|
saveFile: async () => ({filename: ''}),
|
|
});
|
|
const result = formatter.toStringDetailed();
|
|
assert.match(result, /Redirect chain/);
|
|
assert.match(result, /reqid=2/);
|
|
});
|
|
it('shows saved to file message in toStringDetailed', async () => {
|
|
const request = {
|
|
method: () => 'POST',
|
|
url: () => 'http://example.com',
|
|
headers: () => ({}),
|
|
hasPostData: () => true,
|
|
postData: () => 'request body',
|
|
response: () => ({
|
|
status: () => 200,
|
|
headers: () => ({}),
|
|
buffer: async () => Buffer.from('response body'),
|
|
}),
|
|
failure: () => null,
|
|
redirectChain: () => [],
|
|
fetchPostData: async () => undefined,
|
|
} as unknown as HTTPRequest;
|
|
|
|
const reqPath = join(tmpDir, 'req.txt');
|
|
const resPath = join(tmpDir, 'res.txt');
|
|
|
|
const formatter = await NetworkFormatter.from(request, {
|
|
fetchData: true,
|
|
requestFilePath: reqPath,
|
|
responseFilePath: resPath,
|
|
saveFile: async (data, filename) => {
|
|
await writeFile(filename, data);
|
|
return {filename};
|
|
},
|
|
});
|
|
|
|
const result = formatter.toStringDetailed();
|
|
assert.ok(result.includes(`Saved to ${reqPath}.`));
|
|
assert.ok(result.includes(`Saved to ${resPath}.`));
|
|
});
|
|
|
|
it('handles missing bodies with filepath', async () => {
|
|
const request = {
|
|
method: () => 'POST',
|
|
url: () => 'http://example.com',
|
|
headers: () => ({}),
|
|
hasPostData: () => true, // Claim we have data
|
|
postData: () => null, // But returns null
|
|
response: () => ({
|
|
status: () => 200,
|
|
headers: () => ({}),
|
|
buffer: async () => {
|
|
throw new Error('Body not available');
|
|
},
|
|
}),
|
|
failure: () => null,
|
|
redirectChain: () => [],
|
|
fetchPostData: async () => {
|
|
throw new Error('Body not available');
|
|
},
|
|
} as unknown as HTTPRequest;
|
|
|
|
const reqPath = join(tmpDir, 'req_missing.txt');
|
|
const resPath = join(tmpDir, 'res_missing.txt');
|
|
|
|
const formatter = await NetworkFormatter.from(request, {
|
|
fetchData: true,
|
|
requestFilePath: reqPath,
|
|
responseFilePath: resPath,
|
|
saveFile: async (data, filename) => {
|
|
await writeFile(filename, data);
|
|
return {filename};
|
|
},
|
|
});
|
|
|
|
const result = formatter.toStringDetailed();
|
|
assert.ok(
|
|
result.includes(
|
|
`### Response Body\n<Response body not available anymore>`,
|
|
),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('toJSON', () => {
|
|
it('returns structured data', async () => {
|
|
const request = getMockRequest();
|
|
const formatter = await NetworkFormatter.from(request, {
|
|
requestId: 1,
|
|
selectedInDevToolsUI: true,
|
|
saveFile: async () => ({filename: ''}),
|
|
});
|
|
const result = formatter.toJSON();
|
|
assert.deepEqual(result, {
|
|
requestId: 1,
|
|
method: 'GET',
|
|
url: 'http://example.com',
|
|
status: 'pending',
|
|
selectedInDevToolsUI: true,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('toJSONDetailed', () => {
|
|
it('returns structured detailed data', async () => {
|
|
const response = getMockResponse();
|
|
response.buffer = () => Promise.resolve(Buffer.from('response'));
|
|
const request = getMockRequest({
|
|
response,
|
|
postData: 'request',
|
|
hasPostData: true,
|
|
});
|
|
const formatter = await NetworkFormatter.from(request, {
|
|
requestId: 1,
|
|
fetchData: true,
|
|
saveFile: async () => ({filename: ''}),
|
|
});
|
|
const result = formatter.toJSONDetailed();
|
|
assert.deepEqual(result, {
|
|
requestId: 1,
|
|
method: 'GET',
|
|
url: 'http://example.com',
|
|
status: '200',
|
|
selectedInDevToolsUI: undefined,
|
|
requestHeaders: {
|
|
'content-size': '10',
|
|
},
|
|
requestBody: 'request',
|
|
requestBodyFilePath: undefined,
|
|
responseHeaders: {},
|
|
responseBody: 'response',
|
|
responseBodyFilePath: undefined,
|
|
failure: undefined,
|
|
redirectChain: undefined,
|
|
});
|
|
});
|
|
|
|
it('returns file paths in structured detailed data', async () => {
|
|
const request = {
|
|
method: () => 'POST',
|
|
url: () => 'http://example.com',
|
|
headers: () => ({}),
|
|
hasPostData: () => true,
|
|
postData: () => 'request body',
|
|
response: () => ({
|
|
status: () => 200,
|
|
headers: () => ({}),
|
|
buffer: async () => Buffer.from('response body'),
|
|
}),
|
|
failure: () => null,
|
|
redirectChain: () => [],
|
|
fetchPostData: async () => undefined,
|
|
} as unknown as HTTPRequest;
|
|
|
|
const reqPath = join(tmpDir, 'req_json.txt');
|
|
const resPath = join(tmpDir, 'res_json.txt');
|
|
|
|
const formatter = await NetworkFormatter.from(request, {
|
|
fetchData: true,
|
|
requestFilePath: reqPath,
|
|
responseFilePath: resPath,
|
|
saveFile: async (data, filename) => {
|
|
await writeFile(filename, data);
|
|
return {filename};
|
|
},
|
|
});
|
|
|
|
const result = formatter.toJSONDetailed() as {
|
|
requestBodyFilePath: string;
|
|
responseBodyFilePath: string;
|
|
requestBody?: string;
|
|
responseBody?: string;
|
|
};
|
|
|
|
assert.strictEqual(result.requestBodyFilePath, reqPath);
|
|
assert.strictEqual(result.responseBodyFilePath, resPath);
|
|
assert.strictEqual(result.requestBody, undefined);
|
|
assert.strictEqual(result.responseBody, undefined);
|
|
});
|
|
});
|
|
});
|