/** * 网络拦截和 Mock 工具 * 用于深度验证 API 调用、模拟响应和网络错误 */ import { Page, Route, Request } from '@playwright/test'; export interface CapturedRequest { url: string; method: string; body?: unknown; headers: Record; timestamp: number; } export interface MockResponse { status?: number; body?: unknown; headers?: Record; delay?: number; } /** * 网络拦截和 Mock 工具集 */ export const networkHelpers = { /** * 拦截并记录所有 API 请求 * 返回请求列表,用于后续断言 */ async interceptAllAPI(page: Page): Promise { const requests: CapturedRequest[] = []; await page.route('**/api/**', async (route: Route) => { const request = route.request(); const body = request.postData(); requests.push({ url: request.url(), method: request.method(), body: body ? safeParseJSON(body) : undefined, headers: request.headers(), timestamp: Date.now(), }); await route.continue(); }); return requests; }, /** * Mock API 响应 * @param path - API 路径(不含 /api 前缀) * @param response - Mock 响应配置 */ async mockAPI( page: Page, path: string, response: MockResponse | ((request: Request) => MockResponse) ): Promise { await page.route(`**/api/${path}**`, async (route: Route) => { const request = route.request(); const mockConfig = typeof response === 'function' ? response(request) : response; if (mockConfig.delay) { await new Promise((r) => setTimeout(r, mockConfig.delay)); } await route.fulfill({ status: mockConfig.status ?? 200, contentType: 'application/json', body: JSON.stringify(mockConfig.body ?? {}), headers: mockConfig.headers, }); }); }, /** * Mock 多个 API 响应 */ async mockMultipleAPIs( page: Page, mocks: Record ): Promise { for (const [path, response] of Object.entries(mocks)) { await this.mockAPI(page, path, response); } }, /** * 模拟网络错误 */ async simulateNetworkError(page: Page, path: string): Promise { await page.route(`**/api/${path}**`, async (route: Route) => { await route.abort('failed'); }); }, /** * 模拟连接超时 */ async simulateTimeout(page: Page, path: string, timeoutMs: number = 60000): Promise { await page.route(`**/api/${path}**`, async (route: Route) => { await new Promise((r) => setTimeout(r, timeoutMs)); }); }, /** * 模拟延迟响应 */ async simulateDelay(page: Page, path: string, delayMs: number): Promise { await page.route(`**/api/${path}**`, async (route: Route) => { await new Promise((r) => setTimeout(r, delayMs)); await route.continue(); }); }, /** * 模拟 HTTP 错误状态码 */ async simulateHTTPError(page: Page, path: string, status: number, message?: string): Promise { await page.route(`**/api/${path}**`, async (route: Route) => { await route.fulfill({ status, contentType: 'application/json', body: JSON.stringify({ error: true, message: message || `HTTP ${status} Error`, }), }); }); }, /** * 模拟限流 (429 Too Many Requests) */ async simulateRateLimit(page: Page, path: string, retryAfter: number = 60): Promise { await page.route(`**/api/${path}**`, async (route: Route) => { await route.fulfill({ status: 429, headers: { 'Retry-After': String(retryAfter), }, contentType: 'application/json', body: JSON.stringify({ error: true, message: 'Too Many Requests', retryAfter, }), }); }); }, /** * 拦截 WebSocket 连接 */ async interceptWebSocket(page: Page): Promise<{ messages: Array<{ direction: 'sent' | 'received'; data: unknown }>; isConnected: () => boolean; }> { const messages: Array<{ direction: 'sent' | 'received'; data: unknown }> = []; let connected = false; // Playwright 的 WebSocket 拦截需要特殊处理 page.on('websocket', (ws) => { connected = true; ws.on('framereceived', (frame) => { try { const data = safeParseJSON(frame.payload as string); messages.push({ direction: 'received', data }); } catch { messages.push({ direction: 'received', data: frame.payload }); } }); ws.on('framesent', (frame) => { try { const data = safeParseJSON(frame.payload as string); messages.push({ direction: 'sent', data }); } catch { messages.push({ direction: 'sent', data: frame.payload }); } }); ws.on('close', () => { connected = false; }); }); return { messages, isConnected: () => connected, }; }, /** * Mock 流式响应(用于聊天功能) */ async mockStreamResponse( page: Page, path: string, chunks: Array<{ delta?: string; phase?: string; content?: string }>, chunkDelay: number = 100 ): Promise { await page.route(`**/api/${path}**`, async (route: Route) => { // 创建流式响应 const encoder = new TextEncoder(); const stream = new ReadableStream({ async start(controller) { for (const chunk of chunks) { const data = `data: ${JSON.stringify(chunk)}\n\n`; controller.enqueue(encoder.encode(data)); await new Promise((r) => setTimeout(r, chunkDelay)); } controller.enqueue(encoder.encode('data: [DONE]\n\n')); controller.close(); }, }); await route.fulfill({ status: 200, headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive', }, body: stream as any, }); }); }, /** * 等待特定 API 请求 */ async waitForAPIRequest( page: Page, pathPattern: string | RegExp, options?: { timeout?: number } ): Promise { return page.waitForRequest( (request) => { const url = request.url(); if (typeof pathPattern === 'string') { return url.includes(pathPattern); } return pathPattern.test(url); }, { timeout: options?.timeout ?? 30000 } ); }, /** * 等待特定 API 响应 */ async waitForAPIResponse( page: Page, pathPattern: string | RegExp, options?: { timeout?: number } ): Promise<{ status: number; body: unknown }> { const response = await page.waitForResponse( (response) => { const url = response.url(); if (typeof pathPattern === 'string') { return url.includes(pathPattern); } return pathPattern.test(url); }, { timeout: options?.timeout ?? 30000 } ); let body: unknown; try { body = await response.json(); } catch { body = await response.text(); } return { status: response.status(), body, }; }, /** * 清除所有路由拦截 */ async clearRoutes(page: Page): Promise { await page.unrouteAll(); }, }; /** * 安全解析 JSON */ function safeParseJSON(text: string): unknown { try { return JSON.parse(text); } catch { return text; } } /** * 请求匹配器 - 用于断言 */ export const requestMatchers = { /** * 验证请求包含特定字段 */ hasField(request: CapturedRequest, field: string, value?: unknown): boolean { if (!request.body || typeof request.body !== 'object') return false; const body = request.body as Record; if (value === undefined) return field in body; return body[field] === value; }, /** * 验证请求方法 */ isMethod(request: CapturedRequest, method: string): boolean { return request.method.toUpperCase() === method.toUpperCase(); }, /** * 验证请求路径 */ matchesPath(request: CapturedRequest, pattern: string | RegExp): boolean { if (typeof pattern === 'string') { return request.url.includes(pattern); } return pattern.test(request.url); }, /** * 查找匹配的请求 */ findRequests( requests: CapturedRequest[], predicate: (req: CapturedRequest) => boolean ): CapturedRequest[] { return requests.filter(predicate); }, /** * 获取特定路径的所有请求 */ getRequestsForPath(requests: CapturedRequest[], path: string): CapturedRequest[] { return requests.filter((r) => r.url.includes(path)); }, /** * 获取 POST 请求 */ getPostRequests(requests: CapturedRequest[]): CapturedRequest[] { return requests.filter((r) => r.method === 'POST'); }, /** * 获取 GET 请求 */ getGetRequests(requests: CapturedRequest[]): CapturedRequest[] { return requests.filter((r) => r.method === 'GET'); }, };