# 请求拦截(Request Mocking) > **适用场景**:在浏览器自动化过程中拦截、模拟、修改或阻断网络请求——API 桩代码、模拟错误、测试离线行为、移除追踪,或通过拦截大资源来加速页面加载。 > **前置条件**:请先阅读 [core-commands.md](core-commands.md) 了解基本 CLI 用法 ## 快速参考 ```bash # 拦截所有图片 playwright-cli route "**/*.jpg" --status=404 # 以 JSON 响应体模拟 API playwright-cli route "**/api/users" --body='[{"id":1,"name":"Alice"}]' --content-type=application/json # 添加自定义响应头 playwright-cli route "**/api/data" --body='{"ok":true}' --header="X-Custom: value" # 移除请求头(例如测试时去掉认证信息) playwright-cli route "**/*" --remove-header=cookie,authorization # 列出当前活跃的路由规则 playwright-cli route-list # 移除某条路由规则 playwright-cli unroute "**/*.jpg" # 移除所有路由规则 playwright-cli unroute ``` ## URL 匹配模式 playwright-cli 使用 glob 模式进行 URL 匹配: | 模式 | 匹配规则 | 示例 URL | | ------------------------------- | -------------------------------- | --------------------------------------------- | | `**/api/users` | 匹配任意来源的精确路径 | `https://api.example.com/api/users` | | `**/api/*/details` | 路径段中的通配符 | `https://api.example.com/api/123/details` | | `**/*.{png,jpg,jpeg}` | 多个文件扩展名 | `https://cdn.example.com/hero.png` | | `**/search?q=*` | 查询参数 | `https://example.com/search?q=test` | | `https://api.example.com/**` | 匹配特定来源的所有请求 | `https://api.example.com/v2/users` | | `**/*` | 匹配所有请求 | 所有请求 | ## CLI 路由命令 ### 模拟状态码 ```bash # 对所有图片请求返回 404 playwright-cli route "**/*.jpg" --status=404 # 返回 503 Service Unavailable playwright-cli route "**/api/health" --status=503 # 返回 401 Unauthorized playwright-cli route "**/api/protected" --status=401 ``` ### 模拟 JSON 响应 ```bash # 简单的 JSON 响应体 playwright-cli route "**/api/users" --body='[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]' --content-type=application/json # 模拟单个资源 playwright-cli route "**/api/users/1" --body='{"id":1,"name":"Alice","email":"alice@example.com"}' --content-type=application/json # 返回空数组(无结果场景) playwright-cli route "**/api/search*" --body='[]' --content-type=application/json ``` ### 模拟自定义响应头 ```bash # 测试时设置 CORS 头 playwright-cli route "**/api/**" --body='{"ok":true}' --header="Access-Control-Allow-Origin: *" --header="X-Request-Id: mock-123" # 设置缓存头 playwright-cli route "**/static/**" --header="Cache-Control: max-age=3600" ``` ### 移除请求头 从发出的请求中移除指定请求头——适用于测试未认证访问: ```bash # 移除认证头 playwright-cli route "**/*" --remove-header=cookie,authorization # 移除追踪头 playwright-cli route "**/*" --remove-header=x-tracking-id ``` ### 管理活跃路由 ```bash # 查看当前拦截了哪些请求 playwright-cli route-list # 移除某条特定路由 playwright-cli unroute "**/*.jpg" # 清除所有路由 playwright-cli unroute ``` ## 使用 run-code 实现高级模拟 如需条件响应、请求体检查、响应修改或定时延迟,可使用 `run-code` 调用完整的 Playwright 路由 API。 ### 基于请求体的条件响应 ```bash playwright-cli run-code "async page => { await page.route('**/api/login', route => { const body = route.request().postDataJSON(); if (body.username === 'admin' && body.password === 'secret') { route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ token: 'mock-jwt-token', user: { role: 'admin' } }) }); } else { route.fulfill({ status: 401, contentType: 'application/json', body: JSON.stringify({ error: 'Invalid credentials' }) }); } }); }" ``` ### 基于 HTTP 方法的条件响应 ```bash playwright-cli run-code "async page => { await page.route('**/api/users', route => { const method = route.request().method(); switch (method) { case 'GET': route.fulfill({ contentType: 'application/json', body: JSON.stringify([{ id: 1, name: 'Alice' }]) }); break; case 'POST': route.fulfill({ status: 201, contentType: 'application/json', body: JSON.stringify({ id: 2, name: 'New User' }) }); break; case 'DELETE': route.fulfill({ status: 204 }); break; default: route.continue(); } }); }" ``` ### 修改真实响应 让请求实际发出,然后在页面接收到响应之前对其进行修改: ```bash playwright-cli run-code "async page => { await page.route('**/api/user/profile', async route => { const response = await route.fetch(); const json = await response.json(); // 覆盖特定字段 json.isPremium = true; json.subscription = 'enterprise'; await route.fulfill({ response, json }); }); }" ``` ### 为真实响应添加响应头 ```bash playwright-cli run-code "async page => { await page.route('**/api/**', async route => { const response = await route.fetch(); const headers = { ...response.headers(), 'x-mock': 'true' }; await route.fulfill({ response, headers }); }); }" ``` ### 模拟网络故障 ```bash # 网络断开 playwright-cli run-code "async page => { await page.route('**/api/offline', route => route.abort('internetdisconnected')); }" # 连接被拒绝 playwright-cli run-code "async page => { await page.route('**/api/down', route => route.abort('connectionrefused')); }" # 超时 playwright-cli run-code "async page => { await page.route('**/api/slow', route => route.abort('timedout')); }" # 连接重置 playwright-cli run-code "async page => { await page.route('**/api/reset', route => route.abort('connectionreset')); }" ``` 可用的中止原因:`connectionrefused`, `timedout`, `connectionreset`, `internetdisconnected`, `blockedbyclient`, `failed` ### 模拟慢响应(延迟) ```bash playwright-cli run-code "async page => { await page.route('**/api/slow-endpoint', async route => { await new Promise(resolve => setTimeout(resolve, 3000)); route.fulfill({ contentType: 'application/json', body: JSON.stringify({ data: 'finally loaded' }) }); }); }" ``` ### 从文件读取响应进行模拟 ```bash playwright-cli run-code "async page => { const fs = require('fs'); await page.route('**/api/config', route => { const body = fs.readFileSync('./fixtures/mock-config.json', 'utf8'); route.fulfill({ contentType: 'application/json', body }); }); }" ``` ### 请求计数与验证 ```bash playwright-cli run-code "async page => { let apiCallCount = 0; await page.route('**/api/analytics', route => { apiCallCount++; route.continue(); }); // ... 执行页面操作 ... // 之后检查计数: return \`API was called \${apiCallCount} times\`; }" ``` ## 常见模式 ### 拦截大资源以提升速度 ```bash # 拦截图片、字体和样式表 playwright-cli route "**/*.{png,jpg,jpeg,gif,svg,webp}" --status=404 playwright-cli route "**/*.{woff,woff2,ttf,eot}" --status=404 playwright-cli route "**/*.css" --status=404 ``` ### 拦截第三方脚本 ```bash # 拦截分析及追踪脚本 playwright-cli run-code "async page => { await page.route('**/*', route => { const url = route.request().url(); const blocked = [ 'google-analytics.com', 'googletagmanager.com', 'facebook.net', 'hotjar.com', 'segment.io' ]; if (blocked.some(domain => url.includes(domain))) { route.abort('blockedbyclient'); } else { route.continue(); } }); }" ``` ### 模拟 GraphQL 请求 ```bash playwright-cli run-code "async page => { await page.route('**/graphql', route => { const { query } = route.request().postDataJSON(); if (query.includes('GetUser')) { route.fulfill({ contentType: 'application/json', body: JSON.stringify({ data: { user: { id: '1', name: 'Alice', email: 'alice@example.com' } } }) }); } else if (query.includes('ListProducts')) { route.fulfill({ contentType: 'application/json', body: JSON.stringify({ data: { products: [{ id: '1', name: 'Widget', price: 9.99 }] } }) }); } else { route.continue(); } }); }" ``` ### 基于 HAR 文件的重放 录制真实网络流量,之后回放——非常适合确定性测试: ```bash # 将所有网络流量录制到 HAR 文件 playwright-cli run-code "async page => { await page.routeFromHAR('./recordings/api-traffic.har', { update: true, url: '**/api/**' }); await page.goto('https://example.com'); // 在页面上执行操作——所有匹配的请求都会被录制 }" # 之后从 HAR 文件回放(无需真实网络) playwright-cli run-code "async page => { await page.routeFromHAR('./recordings/api-traffic.har', { url: '**/api/**' }); await page.goto('https://example.com'); // API 响应来自 HAR 文件 }" ``` ### 模拟 WebSocket 消息 ```bash playwright-cli run-code "async page => { const ws = await page.waitForEvent('websocket'); ws.on('framereceived', event => { console.log('WS received:', event.payload); }); ws.on('framesent', event => { console.log('WS sent:', event.payload); }); }" ``` ## 提示 - **顺序很重要**:路由按照添加的顺序依次评估。更具体的模式应放在全匹配模式之前添加。 - **`route.continue()`**:让请求继续发往真实服务器——在条件路由中用于"透传"场景。 - **`route.fetch()`**:发起真实请求并返回响应,以便你在兑现前检查或修改它。 - **性能**:拦截图片和第三方脚本可以大幅提升自动化过程中的页面加载速度。 - **清理**:使用完毕后务必执行 `unroute`,否则路由规则在整个会话中持续生效,可能会干扰后续操作。