315 lines
11 KiB
TypeScript
315 lines
11 KiB
TypeScript
/**
|
|
* Copyright (c) Microsoft Corporation.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
import { test, expect, parseResponse } from './fixtures';
|
|
|
|
test('browser_network_requests', async ({ client, server }) => {
|
|
server.setContent('/', `
|
|
<button onclick="fetch('/json')">Click me</button>
|
|
<img src="/image.png" />
|
|
`, 'text/html');
|
|
|
|
server.setContent('/json', JSON.stringify({ name: 'John Doe' }), 'application/json');
|
|
|
|
await client.callTool({
|
|
name: 'browser_navigate',
|
|
arguments: {
|
|
url: server.PREFIX,
|
|
},
|
|
});
|
|
|
|
await client.callTool({
|
|
name: 'browser_click',
|
|
arguments: {
|
|
element: 'Click me button',
|
|
target: 'e2',
|
|
},
|
|
});
|
|
|
|
{
|
|
const response = parseResponse(await client.callTool({
|
|
name: 'browser_network_requests',
|
|
}));
|
|
expect(response.result).not.toContain(`[GET] ${`${server.PREFIX}/`} => [200] OK`);
|
|
expect(response.result).toMatch(new RegExp(String.raw`^\d+\. \[GET\] ${escapeRegExp(`${server.PREFIX}/json`)} => \[200\] OK$`, 'm'));
|
|
expect(response.result).toMatch(new RegExp(String.raw`^\d+\. \[GET\] ${escapeRegExp(`${server.PREFIX}/image.png`)} => \[404\]`, 'm'));
|
|
expect(response.result).toContain('Note: 1 static request not shown, run with "static" option to see it.');
|
|
}
|
|
|
|
{
|
|
const response = parseResponse(await client.callTool({
|
|
name: 'browser_network_requests',
|
|
arguments: {
|
|
static: true,
|
|
},
|
|
}));
|
|
expect(response.result).toMatch(new RegExp(String.raw`^\d+\. \[GET\] ${escapeRegExp(`${server.PREFIX}/`)} => \[200\] OK$`, 'm'));
|
|
expect(response.result).toMatch(new RegExp(String.raw`^\d+\. \[GET\] ${escapeRegExp(`${server.PREFIX}/json`)} => \[200\] OK$`, 'm'));
|
|
expect(response.result).toMatch(new RegExp(String.raw`^\d+\. \[GET\] ${escapeRegExp(`${server.PREFIX}/image.png`)} => \[404\]`, 'm'));
|
|
expect(response.result).not.toContain('not shown');
|
|
}
|
|
});
|
|
|
|
test('browser_network_requests filter', async ({ client, server }) => {
|
|
server.setContent('/', '', 'text/html');
|
|
|
|
await client.callTool({
|
|
name: 'browser_navigate',
|
|
arguments: { url: server.PREFIX },
|
|
});
|
|
|
|
await client.callTool({
|
|
name: 'browser_evaluate',
|
|
arguments: {
|
|
function: `async () => {
|
|
await Promise.all([fetch('/api/users'), fetch('/api/orders'), fetch('/static/image.png')]);
|
|
}`,
|
|
},
|
|
});
|
|
|
|
{
|
|
const response = parseResponse(await client.callTool({
|
|
name: 'browser_network_requests',
|
|
arguments: { filter: '/api/', static: true },
|
|
}));
|
|
expect(response.result).toContain(`${server.PREFIX}/api/users`);
|
|
expect(response.result).toContain(`${server.PREFIX}/api/orders`);
|
|
expect(response.result).not.toContain(`${server.PREFIX}/static/image.png`);
|
|
}
|
|
});
|
|
|
|
test('browser_network_requests rejects invalid regex filter', async ({ client, server }) => {
|
|
server.setContent('/', '', 'text/html');
|
|
|
|
await client.callTool({
|
|
name: 'browser_navigate',
|
|
arguments: { url: server.PREFIX },
|
|
});
|
|
|
|
const response = await client.callTool({
|
|
name: 'browser_network_requests',
|
|
arguments: { filter: '[invalid(' },
|
|
});
|
|
expect(response.isError).toBe(true);
|
|
});
|
|
|
|
test('browser_network_requests numbers requests with stable indexes', async ({ client, server }) => {
|
|
server.setContent('/', '', 'text/html');
|
|
|
|
await client.callTool({
|
|
name: 'browser_navigate',
|
|
arguments: { url: server.PREFIX },
|
|
});
|
|
|
|
await client.callTool({
|
|
name: 'browser_evaluate',
|
|
arguments: {
|
|
function: `async () => {
|
|
await fetch('/api/users');
|
|
await fetch('/api/orders');
|
|
}`,
|
|
},
|
|
});
|
|
|
|
// Index assignment is stable across calls — the same request keeps the same number.
|
|
const response = parseResponse(await client.callTool({
|
|
name: 'browser_network_requests',
|
|
arguments: { static: true },
|
|
}));
|
|
const lines = response.result.split('\n').filter(Boolean);
|
|
expect(lines[0]).toMatch(/^1\. \[GET\] /);
|
|
expect(lines).toHaveLength(3);
|
|
});
|
|
|
|
test('browser_network_request shows full request and response details', async ({ client, server }) => {
|
|
server.setContent('/', `
|
|
<button onclick="fetch('/api', { method: 'POST', headers: { 'X-Custom-Header': 'test-value' }, body: JSON.stringify({ key: 'value' }) })">Click me</button>
|
|
`, 'text/html');
|
|
server.setRoute('/api', (_req, res) => {
|
|
res.setHeader('X-Custom-Response', 'response-value');
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify({ name: 'John Doe' }));
|
|
});
|
|
|
|
await client.callTool({
|
|
name: 'browser_navigate',
|
|
arguments: { url: server.PREFIX },
|
|
});
|
|
|
|
await client.callTool({
|
|
name: 'browser_click',
|
|
arguments: { element: 'Click me button', target: 'e2' },
|
|
});
|
|
|
|
// Find the index of the /api request from the listing.
|
|
const list = parseResponse(await client.callTool({
|
|
name: 'browser_network_requests',
|
|
}));
|
|
const match = list.result.match(/^(\d+)\. \[POST\] [^ ]+\/api =>/m);
|
|
expect(match).not.toBeNull();
|
|
const index = Number(match![1]);
|
|
|
|
const detail = parseResponse(await client.callTool({
|
|
name: 'browser_network_request',
|
|
arguments: { index },
|
|
}));
|
|
expect(detail.result).toContain(`#${index} [POST] ${server.PREFIX}/api`);
|
|
expect(detail.result).toContain('General');
|
|
expect(detail.result).toContain('status: [200] OK');
|
|
expect(detail.result).toContain('mimeType: application/json');
|
|
expect(detail.result).toContain('Request headers');
|
|
expect(detail.result).toContain('x-custom-header: test-value');
|
|
expect(detail.result).toContain('Response headers');
|
|
expect(detail.result).toContain('x-custom-response: response-value');
|
|
expect(detail.result).toContain('Call browser_network_request with part="request-body" to read the request body.');
|
|
expect(detail.result).toContain('Call browser_network_request with part="response-body" to read the response body.');
|
|
expect(detail.result).not.toContain('Request body');
|
|
expect(detail.result).not.toContain('Response body');
|
|
expect(detail.result).not.toContain('{"key":"value"}');
|
|
});
|
|
|
|
test('browser_network_request reports failed requests', async ({ client, server }) => {
|
|
server.setContent('/', `<img src="/missing.png" />`, 'text/html');
|
|
|
|
await client.callTool({
|
|
name: 'browser_navigate',
|
|
arguments: { url: server.PREFIX },
|
|
});
|
|
|
|
const list = parseResponse(await client.callTool({
|
|
name: 'browser_network_requests',
|
|
arguments: { static: true },
|
|
}));
|
|
const match = list!.result!.match(/^(\d+)\. \[GET\] [^ ]+\/missing\.png =>/m);
|
|
expect(match).not.toBeNull();
|
|
|
|
const detail = parseResponse(await client.callTool({
|
|
name: 'browser_network_request',
|
|
arguments: { index: Number(match![1]) },
|
|
}));
|
|
expect(detail!.result).toContain(`#${match![1]} [GET] ${server.PREFIX}/missing.png`);
|
|
expect(detail!.result).toContain('status: [404]');
|
|
});
|
|
|
|
test('browser_network_request returns individual parts', async ({ client, server }) => {
|
|
server.setContent('/', `
|
|
<button onclick="fetch('/api', { method: 'POST', headers: { 'X-Custom-Header': 'test-value' }, body: JSON.stringify({ key: 'value' }) })">Click me</button>
|
|
`, 'text/html');
|
|
server.setRoute('/api', (_req, res) => {
|
|
res.setHeader('X-Custom-Response', 'response-value');
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify({ name: 'John Doe' }));
|
|
});
|
|
|
|
await client.callTool({
|
|
name: 'browser_navigate',
|
|
arguments: { url: server.PREFIX },
|
|
});
|
|
|
|
await client.callTool({
|
|
name: 'browser_click',
|
|
arguments: { element: 'Click me button', target: 'e2' },
|
|
});
|
|
|
|
const list = parseResponse(await client.callTool({
|
|
name: 'browser_network_requests',
|
|
}));
|
|
const match = list!.result!.match(/^(\d+)\. \[POST\] [^ ]+\/api =>/m);
|
|
expect(match).not.toBeNull();
|
|
const index = Number(match![1]);
|
|
|
|
const requestHeaders = parseResponse(await client.callTool({
|
|
name: 'browser_network_request',
|
|
arguments: { index, part: 'request-headers' },
|
|
}));
|
|
expect(requestHeaders!.result).toContain('x-custom-header: test-value');
|
|
expect(requestHeaders!.result).not.toContain('General');
|
|
|
|
const requestBody = parseResponse(await client.callTool({
|
|
name: 'browser_network_request',
|
|
arguments: { index, part: 'request-body' },
|
|
}));
|
|
expect(requestBody!.result).toBe('{"key":"value"}');
|
|
|
|
const responseHeaders = parseResponse(await client.callTool({
|
|
name: 'browser_network_request',
|
|
arguments: { index, part: 'response-headers' },
|
|
}));
|
|
expect(responseHeaders!.result).toContain('x-custom-response: response-value');
|
|
|
|
const responseBody = parseResponse(await client.callTool({
|
|
name: 'browser_network_request',
|
|
arguments: { index, part: 'response-body' },
|
|
}));
|
|
expect(responseBody!.result).toBe('{"name":"John Doe"}');
|
|
});
|
|
|
|
test('browser_network_request response-body part saves binary to a file', async ({ client, server }) => {
|
|
const pngBytes = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
server.setContent('/', `
|
|
<button onclick="fetch('/image.png')">Click me</button>
|
|
`, 'text/html');
|
|
server.setRoute('/image.png', (_req, res) => {
|
|
res.setHeader('Content-Type', 'image/png');
|
|
res.end(pngBytes);
|
|
});
|
|
|
|
await client.callTool({
|
|
name: 'browser_navigate',
|
|
arguments: { url: server.PREFIX },
|
|
});
|
|
|
|
await client.callTool({
|
|
name: 'browser_click',
|
|
arguments: { element: 'Click me button', target: 'e2' },
|
|
});
|
|
|
|
const list = parseResponse(await client.callTool({
|
|
name: 'browser_network_requests',
|
|
arguments: { static: true },
|
|
}));
|
|
const match = list!.result!.match(/^(\d+)\. \[GET\] [^ ]+\/image\.png =>/m);
|
|
expect(match).not.toBeNull();
|
|
|
|
const detail = parseResponse(await client.callTool({
|
|
name: 'browser_network_request',
|
|
arguments: { index: Number(match![1]), part: 'response-body' },
|
|
}));
|
|
const bodyPath = path.resolve(test.info().outputPath(), detail!.result!.trim());
|
|
expect(fs.readFileSync(bodyPath)).toEqual(pngBytes);
|
|
});
|
|
|
|
test('browser_network_request rejects out-of-range index', async ({ client, server }) => {
|
|
await client.callTool({
|
|
name: 'browser_navigate',
|
|
arguments: { url: server.PREFIX },
|
|
});
|
|
|
|
const response = await client.callTool({
|
|
name: 'browser_network_request',
|
|
arguments: { index: 999 },
|
|
});
|
|
const parsed = parseResponse(response);
|
|
expect(parsed!.error).toContain('Request #999 not found');
|
|
});
|
|
|
|
function escapeRegExp(text: string): string {
|
|
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
}
|