374 lines
10 KiB
Markdown
374 lines
10 KiB
Markdown
# 请求拦截(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`,否则路由规则在整个会话中持续生效,可能会干扰后续操作。
|