Files
skillhub-147-playwright-skill/playwright-cli/request-mocking.md
T
2026-07-13 21:36:47 +08:00

10 KiB
Raw Blame History

请求拦截(Request Mocking

适用场景:在浏览器自动化过程中拦截、模拟、修改或阻断网络请求——API 桩代码、模拟错误、测试离线行为、移除追踪,或通过拦截大资源来加速页面加载。 前置条件:请先阅读 core-commands.md 了解基本 CLI 用法

快速参考

# 拦截所有图片
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 路由命令

模拟状态码

# 对所有图片请求返回 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 响应

# 简单的 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

模拟自定义响应头

# 测试时设置 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"

移除请求头

从发出的请求中移除指定请求头——适用于测试未认证访问:

# 移除认证头
playwright-cli route "**/*" --remove-header=cookie,authorization

# 移除追踪头
playwright-cli route "**/*" --remove-header=x-tracking-id

管理活跃路由

# 查看当前拦截了哪些请求
playwright-cli route-list

# 移除某条特定路由
playwright-cli unroute "**/*.jpg"

# 清除所有路由
playwright-cli unroute

使用 run-code 实现高级模拟

如需条件响应、请求体检查、响应修改或定时延迟,可使用 run-code 调用完整的 Playwright 路由 API。

基于请求体的条件响应

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 方法的条件响应

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();
    }
  });
}"

修改真实响应

让请求实际发出,然后在页面接收到响应之前对其进行修改:

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 });
  });
}"

为真实响应添加响应头

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 });
  });
}"

模拟网络故障

# 网络断开
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

模拟慢响应(延迟)

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' })
    });
  });
}"

从文件读取响应进行模拟

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
    });
  });
}"

请求计数与验证

playwright-cli run-code "async page => {
  let apiCallCount = 0;
  await page.route('**/api/analytics', route => {
    apiCallCount++;
    route.continue();
  });

  // ... 执行页面操作 ...
  // 之后检查计数:
  return \`API was called \${apiCallCount} times\`;
}"

常见模式

拦截大资源以提升速度

# 拦截图片、字体和样式表
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

拦截第三方脚本

# 拦截分析及追踪脚本
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 请求

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 文件的重放

录制真实网络流量,之后回放——非常适合确定性测试:

# 将所有网络流量录制到 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 消息

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,否则路由规则在整个会话中持续生效,可能会干扰后续操作。