Files
2026-07-13 21:36:47 +08:00

31 KiB
Raw Permalink Blame History

第三方集成

何时使用:测试应用程序与外部服务(OAuth 提供商、支付网关、分析工具、聊天组件、地图、社交登录和 CAPTCHA)的交互。核心原则:模拟第三方边界,而非你自己的应用程序代码。 前置要求core/network-mocking.mdcore/when-to-mock.mdcore/authentication.md

快速参考

// 模拟 OAuth 回调以绕过真实的提供商
await page.route("**/auth/callback*", (route) => {
  route.fulfill({
    status: 302,
    headers: { Location: "/dashboard?token=mock-jwt-token" },
  })
})

// 阻止分析脚本加载
await page.route("**/*.google-analytics.com/**", (route) => route.abort())
await page.route("**/segment.io/**", (route) => route.abort())

// 模拟第三方组件端点
await page.route("**/api.stripe.com/**", (route) => {
  route.fulfill({ status: 200, contentType: "application/json", body: '{"id":"pi_mock"}' })
})

核心原则:在 E2E 测试中,模拟外部服务,而非你自己的应用程序。你的代码应原样运行;只有第三方响应是被伪造的。

模式

模拟 OAuth 提供商(Google、GitHub 等)

何时使用:测试完整的登录流程,而不依赖真实 OAuth 提供商的可用性、速率限制或测试账号。 避免使用:当你需要验证实际的 OAuth 集成端到端工作时(请使用专门的集成测试)。

TypeScript

import { test, expect } from "@playwright/test"

test("Google OAuth 登录 —— 通过路由模拟", async ({ page }) => {
  // 拦截重定向到 Google 并模拟回调
  await page.route("**/accounts.google.com/**", async (route) => {
    // 从请求 URL 中提取 redirect_uri
    const url = new URL(route.request().url())
    const redirectUri = url.searchParams.get("redirect_uri") || "/auth/callback"

    // 模拟 Google 重定向回来并附带授权码
    await route.fulfill({
      status: 302,
      headers: {
        Location: `${redirectUri}?code=mock-auth-code&state=${url.searchParams.get("state") || ""}`,
      },
    })
  })

  // 模拟你自己后端的令牌交换端点
  await page.route("**/api/auth/google/callback*", async (route) => {
    await route.fulfill({
      status: 200,
      contentType: "application/json",
      body: JSON.stringify({
        token: "mock-jwt-token",
        user: {
          id: "1",
          name: "Test User",
          email: "testuser@gmail.com",
          avatar: "https://placehold.co/100x100",
        },
      }),
    })
  })

  await page.goto("/login")
  await page.getByRole("button", { name: "Sign in with Google" }).click()

  // 应进入仪表盘页面并显示用户信息
  await page.waitForURL("/dashboard")
  await expect(page.getByText("Test User")).toBeVisible()
})

test("GitHub OAuth 登录 —— 模拟提供商", async ({ page }) => {
  await page.route("**/github.com/login/oauth/**", async (route) => {
    const url = new URL(route.request().url())
    const redirectUri = url.searchParams.get("redirect_uri") || "/auth/callback"
    await route.fulfill({
      status: 302,
      headers: {
        Location: `${redirectUri}?code=mock-github-code`,
      },
    })
  })

  await page.route("**/api/auth/github/callback*", async (route) => {
    await route.fulfill({
      status: 200,
      contentType: "application/json",
      body: JSON.stringify({
        token: "mock-jwt-token",
        user: { id: "2", name: "GitHub User", email: "user@github.com" },
      }),
    })
  })

  await page.goto("/login")
  await page.getByRole("button", { name: "Sign in with GitHub" }).click()
  await page.waitForURL("/dashboard")
  await expect(page.getByText("GitHub User")).toBeVisible()
})

JavaScript

const { test, expect } = require("@playwright/test")

test("Google OAuth 登录 —— 通过路由模拟", async ({ page }) => {
  await page.route("**/accounts.google.com/**", async (route) => {
    const url = new URL(route.request().url())
    const redirectUri = url.searchParams.get("redirect_uri") || "/auth/callback"
    await route.fulfill({
      status: 302,
      headers: {
        Location: `${redirectUri}?code=mock-auth-code&state=${url.searchParams.get("state") || ""}`,
      },
    })
  })

  await page.route("**/api/auth/google/callback*", async (route) => {
    await route.fulfill({
      status: 200,
      contentType: "application/json",
      body: JSON.stringify({
        token: "mock-jwt-token",
        user: { id: "1", name: "Test User", email: "testuser@gmail.com" },
      }),
    })
  })

  await page.goto("/login")
  await page.getByRole("button", { name: "Sign in with Google" }).click()
  await page.waitForURL("/dashboard")
  await expect(page.getByText("Test User")).toBeVisible()
})

支付网关测试(Stripe Elements

何时使用:测试使用 Stripe Elements、PayPal 按钮或类似嵌入式支付 UI 的结账流程。 避免使用:当 Stripe 提供带有测试卡号的测试模式,且你想要完整的集成覆盖时。

TypeScript

import { test, expect } from "@playwright/test"

test("使用测试卡完成 Stripe 结账", async ({ page }) => {
  await page.goto("/checkout")

  // Stripe Elements 在 iframe 中加载
  const stripeFrame = page.frameLocator('iframe[name*="__privateStripeFrame"]').first()

  // 在 Stripe iframe 中填写卡号详情
  await stripeFrame.getByPlaceholder("Card number").fill("4242424242424242")
  await stripeFrame.getByPlaceholder("MM / YY").fill("12/28")
  await stripeFrame.getByPlaceholder("CVC").fill("123")
  await stripeFrame.getByPlaceholder("ZIP").fill("10001")

  await page.getByRole("button", { name: "Pay" }).click()
  await expect(page.getByText("Payment successful")).toBeVisible({ timeout: 15000 })
})

test("Stripe 结账 —— 模拟 API 以提高速度", async ({ page }) => {
  // 模拟 Stripe API 调用以获得更快、更可靠的测试
  await page.route("**/api.stripe.com/v1/payment_intents*", async (route) => {
    await route.fulfill({
      status: 200,
      contentType: "application/json",
      body: JSON.stringify({
        id: "pi_mock_123",
        status: "succeeded",
        client_secret: "pi_mock_123_secret_456",
      }),
    })
  })

  await page.route("**/api.stripe.com/v1/payment_methods*", async (route) => {
    await route.fulfill({
      status: 200,
      contentType: "application/json",
      body: JSON.stringify({
        id: "pm_mock_789",
        type: "card",
        card: { brand: "visa", last4: "4242" },
      }),
    })
  })

  await page.goto("/checkout")
  // 使用模拟的 Stripe,你的应用级支付表单可能无需 iframe 即可工作
  await page.getByRole("button", { name: "Pay" }).click()
  await expect(page.getByText("Payment successful")).toBeVisible()
})

test("优雅处理卡片被拒的情况", async ({ page }) => {
  await page.goto("/checkout")

  const stripeFrame = page.frameLocator('iframe[name*="__privateStripeFrame"]').first()
  // Stripe 测试卡 —— 始终被拒
  await stripeFrame.getByPlaceholder("Card number").fill("4000000000000002")
  await stripeFrame.getByPlaceholder("MM / YY").fill("12/28")
  await stripeFrame.getByPlaceholder("CVC").fill("123")
  await stripeFrame.getByPlaceholder("ZIP").fill("10001")

  await page.getByRole("button", { name: "Pay" }).click()
  await expect(page.getByText(/declined|failed/i)).toBeVisible({ timeout: 15000 })
})

JavaScript

const { test, expect } = require("@playwright/test")

test("使用测试卡完成 Stripe 结账", async ({ page }) => {
  await page.goto("/checkout")

  const stripeFrame = page.frameLocator('iframe[name*="__privateStripeFrame"]').first()
  await stripeFrame.getByPlaceholder("Card number").fill("4242424242424242")
  await stripeFrame.getByPlaceholder("MM / YY").fill("12/28")
  await stripeFrame.getByPlaceholder("CVC").fill("123")
  await stripeFrame.getByPlaceholder("ZIP").fill("10001")

  await page.getByRole("button", { name: "Pay" }).click()
  await expect(page.getByText("Payment successful")).toBeVisible({ timeout: 15000 })
})

分析工具拦截

何时使用:阻止分析脚本在测试期间执行,以提高速度、避免污染真实分析数据,并消除第三方脚本故障带来的不稳定性。 避免使用:当你专门需要测试分析事件是否正确触发时。

TypeScript

import { test, expect } from "@playwright/test"

// 在 fixture 中为所有测试拦截分析工具
import { test as base } from "@playwright/test"

export const test = base.extend({
  page: async ({ page }, use) => {
    // 拦截常见的分析和追踪脚本
    await page.route(
      /(google-analytics|googletagmanager|segment\.io|hotjar|mixpanel|amplitude)/,
      (route) => route.abort()
    )
    await page.route("**/collect?**", (route) => route.abort()) // GA 信标
    await page.route("**/analytics/**", (route) => route.abort())
    await use(page)
  },
})

export { expect }
// 用于验证分析事件确实被发送的测试
import { test, expect } from "@playwright/test"

test("结账完成时触发购买事件", async ({ page }) => {
  const analyticsRequests: { url: string; body: string }[] = []

  // 拦截分析调用而非阻止
  await page.route("**/collect**", async (route) => {
    analyticsRequests.push({
      url: route.request().url(),
      body: route.request().postData() || "",
    })
    await route.fulfill({ status: 200 }) // 响应但不发送到真实分析平台
  })

  await page.goto("/checkout")
  await page.getByRole("button", { name: "Complete order" }).click()
  await page.waitForURL("/order/confirmation")

  // 验证购买事件已被发送
  const purchaseEvent = analyticsRequests.find(
    (r) => r.body.includes("purchase") || r.url.includes("purchase")
  )
  expect(purchaseEvent).toBeDefined()
})

JavaScript

const { test: base, expect } = require("@playwright/test")

const test = base.extend({
  page: async ({ page }, use) => {
    await page.route(/(google-analytics|googletagmanager|segment\.io|hotjar|mixpanel)/, (route) =>
      route.abort()
    )
    await use(page)
  },
})

module.exports = { test, expect }

聊天组件测试(Intercom、Drift 等)

何时使用:你的应用嵌入了第三方聊天组件,需要测试交互或验证它不会破坏你的 UI。 避免使用:当聊天组件仅是装饰性的,不属于关键用户流程时。

TypeScript

import { test, expect } from "@playwright/test"

test("Intercom 聊天组件打开并接受消息", async ({ page }) => {
  await page.goto("/")

  // 等待聊天组件加载(通常有延迟)
  const chatLauncher = page
    .frameLocator('iframe[name*="intercom"]')
    .getByRole("button", { name: /chat|message/i })
  await expect(chatLauncher).toBeVisible({ timeout: 15000 })

  // 打开聊天
  await chatLauncher.click()

  // 聊天对话的 iframe 加载
  const chatFrame = page.frameLocator('iframe[name*="intercom-messenger"]')
  await expect(chatFrame.getByText(/How can we help/i)).toBeVisible()

  // 输入消息
  await chatFrame.getByRole("textbox").fill("I need help with my order")
  await chatFrame.getByRole("button", { name: /send/i }).click()

  await expect(chatFrame.getByText("I need help with my order")).toBeVisible()
})

test("聊天组件不遮挡关键 UI", async ({ page }) => {
  await page.goto("/checkout")

  // 获取聊天组件位置
  const chatWidget = page.locator('iframe[name*="intercom"]').first()
  if (await chatWidget.isVisible()) {
    const chatBox = await chatWidget.boundingBox()
    const payButton = await page.getByRole("button", { name: "Pay" }).boundingBox()

    // 确保支付按钮没有被聊天组件遮挡
    if (chatBox && payButton) {
      const overlaps =
        payButton.x < chatBox.x + chatBox.width &&
        payButton.x + payButton.width > chatBox.x &&
        payButton.y < chatBox.y + chatBox.height &&
        payButton.y + payButton.height > chatBox.y
      expect(overlaps).toBe(false)
    }
  }
})

// 在大多数测试中拦截聊天组件以提高速度
test("在非聊天测试中拦截聊天组件", async ({ page }) => {
  await page.route("**/widget.intercom.io/**", (route) => route.abort())
  await page.route("**/js.intercomcdn.com/**", (route) => route.abort())

  await page.goto("/dashboard")
  // 聊天组件不会加载 —— 测试运行更快
  await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible()
})

JavaScript

const { test, expect } = require("@playwright/test")

test("拦截聊天组件以提高速度", async ({ page }) => {
  await page.route("**/widget.intercom.io/**", (route) => route.abort())
  await page.route("**/js.intercomcdn.com/**", (route) => route.abort())

  await page.goto("/dashboard")
  await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible()
})

地图集成测试(Google Maps

何时使用:你的应用嵌入了 Google Maps 或类似地图提供商,需要验证与地图相关的交互。 避免使用:当地图是装饰性的,不属于用户工作流程的一部分时。

TypeScript

import { test, expect } from "@playwright/test"

test("门店定位器在地图上显示标记", async ({ page }) => {
  await page.goto("/store-locator")

  // Google Maps 在 iframe 中或直接加载在页面上
  await page.getByLabel("Search location").fill("New York")
  await page.getByRole("button", { name: "Search" }).click()

  // 等待地图标记出现
  await expect(page.locator('[data-testid="map-marker"]')).toHaveCount(5, {
    timeout: 10000,
  })

  // 点击标记查看门店详情
  await page.locator('[data-testid="map-marker"]').first().click()
  await expect(page.getByTestId("store-info")).toBeVisible()
  await expect(page.getByTestId("store-info")).toContainText("New York")
})

test("模拟 Google Maps API 进行离线测试", async ({ page }) => {
  // 拦截真实的 Google Maps 并提供模拟响应
  await page.route("**/maps.googleapis.com/**", async (route) => {
    const url = route.request().url()

    if (url.includes("/geocode/")) {
      await route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({
          results: [
            {
              geometry: { location: { lat: 40.7128, lng: -74.006 } },
              formatted_address: "New York, NY, USA",
            },
          ],
          status: "OK",
        }),
      })
    } else if (url.includes("/places/")) {
      await route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({
          results: [
            { name: "Store A", geometry: { location: { lat: 40.71, lng: -74.0 } } },
            { name: "Store B", geometry: { location: { lat: 40.72, lng: -74.01 } } },
          ],
          status: "OK",
        }),
      })
    } else {
      await route.continue()
    }
  })

  await page.goto("/store-locator")
  await page.getByLabel("Search location").fill("New York")
  await page.getByRole("button", { name: "Search" }).click()

  await expect(page.getByText("Store A")).toBeVisible()
  await expect(page.getByText("Store B")).toBeVisible()
})

JavaScript

const { test, expect } = require("@playwright/test")

test("模拟 Google Maps 地理编码 API", async ({ page }) => {
  await page.route("**/maps.googleapis.com/maps/api/geocode/**", async (route) => {
    await route.fulfill({
      status: 200,
      contentType: "application/json",
      body: JSON.stringify({
        results: [
          {
            geometry: { location: { lat: 40.7128, lng: -74.006 } },
            formatted_address: "New York, NY, USA",
          },
        ],
        status: "OK",
      }),
    })
  })

  await page.goto("/store-locator")
  await page.getByLabel("Search location").fill("New York")
  await page.getByRole("button", { name: "Search" }).click()
  await expect(page.getByText("New York")).toBeVisible()
})

测试中绕过 reCAPTCHA

何时使用:你的应用使用 reCAPTCHA 或 hCaptcha,需要测试在不解决验证挑战的情况下继续进行。 避免使用:当你可以通过服务端标志在测试环境中禁用 CAPTCHA 时(这是推荐做法)。

TypeScript

import { test, expect } from "@playwright/test"

test.describe("reCAPTCHA 处理", () => {
  // 最佳做法:使用 Google 提供的测试密钥
  // Site key: 6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI(始终通过)
  // Secret key: 6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe

  test("通过测试密钥绕过 reCAPTCHA 提交表单", async ({ page }) => {
    // 你的测试环境应配置为使用 Google 的测试密钥
    await page.goto("/contact")

    await page.getByLabel("Name").fill("Test User")
    await page.getByLabel("Email").fill("test@example.com")
    await page.getByLabel("Message").fill("Test message")

    // 使用测试密钥,reCAPTCHA 自动通过 —— 只需点击提交
    await page.getByRole("button", { name: "Send" }).click()
    await expect(page.getByText("Message sent")).toBeVisible()
  })

  test("模拟 reCAPTCHA 验证端点", async ({ page }) => {
    // 模拟 Google 的 reCAPTCHA 验证 API
    await page.route("**/recaptcha/api/siteverify**", async (route) => {
      await route.fulfill({
        status: 200,
        contentType: "application/json",
        body: JSON.stringify({ success: true, score: 0.9 }),
      })
    })

    // 模拟 reCAPTCHA 脚本以提供虚假令牌
    await page.addInitScript(() => {
      ;(window as any).grecaptcha = {
        ready: (cb: () => void) => cb(),
        execute: () => Promise.resolve("mock-recaptcha-token"),
        render: () => "mock-widget-id",
        getResponse: () => "mock-recaptcha-token",
        reset: () => {},
      }
    })

    await page.goto("/contact")
    await page.getByLabel("Name").fill("Test User")
    await page.getByLabel("Email").fill("test@example.com")
    await page.getByLabel("Message").fill("Test message")
    await page.getByRole("button", { name: "Send" }).click()

    await expect(page.getByText("Message sent")).toBeVisible()
  })

  test("不可见 reCAPTCHA v3 绕过", async ({ page }) => {
    // 对于 reCAPTCHA v3,模拟企业 API
    await page.route("**/recaptcha/**", async (route) => {
      if (route.request().url().includes("api.js")) {
        // 提供一个模拟脚本来提供 grecaptcha 对象
        await route.fulfill({
          status: 200,
          contentType: "application/javascript",
          body: `
            window.grecaptcha = {
              ready: function(cb) { cb(); },
              execute: function() { return Promise.resolve('mock-token'); },
              enterprise: {
                ready: function(cb) { cb(); },
                execute: function() { return Promise.resolve('mock-token'); },
              }
            };
          `,
        })
      } else {
        await route.abort()
      }
    })

    await page.goto("/login")
    await page.getByLabel("Email").fill("user@example.com")
    await page.getByLabel("Password").fill("password123")
    await page.getByRole("button", { name: "Sign in" }).click()
    await page.waitForURL("/dashboard")
  })
})

JavaScript

const { test, expect } = require("@playwright/test")

test("模拟 reCAPTCHA 以便提交表单", async ({ page }) => {
  await page.route("**/recaptcha/api/siteverify**", async (route) => {
    await route.fulfill({
      status: 200,
      contentType: "application/json",
      body: JSON.stringify({ success: true, score: 0.9 }),
    })
  })

  await page.addInitScript(() => {
    window.grecaptcha = {
      ready: (cb) => cb(),
      execute: () => Promise.resolve("mock-recaptcha-token"),
      render: () => "mock-widget-id",
      getResponse: () => "mock-recaptcha-token",
      reset: () => {},
    }
  })

  await page.goto("/contact")
  await page.getByLabel("Name").fill("Test User")
  await page.getByLabel("Email").fill("test@example.com")
  await page.getByLabel("Message").fill("Test message")
  await page.getByRole("button", { name: "Send" }).click()
  await expect(page.getByText("Message sent")).toBeVisible()
})

社交登录模拟

何时使用:你的应用提供多种社交登录选项,需要测试登录流程而无需使用真实的提供商账号。 避免使用:当你拥有后端绕过方式可以直接设置认证状态时。

TypeScript

import { test as base, expect } from "@playwright/test"

// 可复用的 fixture,用于模拟任何 OAuth 提供商
type OAuthFixtures = {
  mockOAuth: (provider: string, userData: Record<string, string>) => Promise<void>
}

export const test = base.extend<OAuthFixtures>({
  mockOAuth: async ({ page }, use) => {
    const mock = async (provider: string, userData: Record<string, string>) => {
      const providerPatterns: Record<string, string> = {
        google: "**/accounts.google.com/**",
        github: "**/github.com/login/oauth/**",
        facebook: "**/facebook.com/v*/dialog/oauth**",
        apple: "**/appleid.apple.com/**",
        microsoft: "**/login.microsoftonline.com/**",
      }

      const pattern = providerPatterns[provider]
      if (!pattern) throw new Error(`Unknown provider: ${provider}`)

      // 拦截 OAuth 重定向
      await page.route(pattern, async (route) => {
        const url = new URL(route.request().url())
        const redirectUri = url.searchParams.get("redirect_uri") || "/auth/callback"
        await route.fulfill({
          status: 302,
          headers: { Location: `${redirectUri}?code=mock-${provider}-code` },
        })
      })

      // 模拟令牌交换
      await page.route(`**/api/auth/${provider}/callback*`, async (route) => {
        await route.fulfill({
          status: 200,
          contentType: "application/json",
          body: JSON.stringify({ token: `mock-${provider}-jwt`, user: userData }),
        })
      })
    }

    await use(mock)
  },
})

// 在测试中使用
test("使用多个社交提供商登录", async ({ page, mockOAuth }) => {
  // 测试 Google 登录
  await mockOAuth("google", { name: "Google User", email: "user@gmail.com" })
  await page.goto("/login")
  await page.getByRole("button", { name: "Sign in with Google" }).click()
  await page.waitForURL("/dashboard")
  await expect(page.getByText("Google User")).toBeVisible()
})

JavaScript

const { test: base, expect } = require("@playwright/test")

const test = base.extend({
  mockOAuth: async ({ page }, use) => {
    const mock = async (provider, userData) => {
      const patterns = {
        google: "**/accounts.google.com/**",
        github: "**/github.com/login/oauth/**",
        facebook: "**/facebook.com/v*/dialog/oauth**",
      }

      await page.route(patterns[provider], async (route) => {
        const url = new URL(route.request().url())
        const redirectUri = url.searchParams.get("redirect_uri") || "/auth/callback"
        await route.fulfill({
          status: 302,
          headers: { Location: `${redirectUri}?code=mock-${provider}-code` },
        })
      })

      await page.route(`**/api/auth/${provider}/callback*`, async (route) => {
        await route.fulfill({
          status: 200,
          contentType: "application/json",
          body: JSON.stringify({ token: `mock-${provider}-jwt`, user: userData }),
        })
      })
    }
    await use(mock)
  },
})

test("使用 Google 登录", async ({ page, mockOAuth }) => {
  await mockOAuth("google", { name: "Google User", email: "user@gmail.com" })
  await page.goto("/login")
  await page.getByRole("button", { name: "Sign in with Google" }).click()
  await page.waitForURL("/dashboard")
  await expect(page.getByText("Google User")).toBeVisible()
})

module.exports = { test, expect }

决策指南

集成方式 模拟策略 何时使用真实服务 何时模拟
OAuthGoogle、GitHub 等) 对提供商 URL 进行路由拦截 + 模拟回调 使用测试账号的专用集成测试 所有需要登录的 E2E 功能测试
Stripe/支付网关 使用 Stripe 测试模式 + 测试卡,或模拟 API 结账流程集成测试 所有非支付的 E2E 测试
Google Analytics / Segment 使用 route.abort() 完全阻止 专用的分析验证测试 其他所有测试(阻止以提高速度)
聊天组件(Intercom、Drift 使用 route.abort() 阻止,或通过 iframe 交互 专门针对聊天功能的测试 其他所有测试(阻止以提高速度/稳定性)
Google Maps 模拟地理编码/地点 API 响应 验证真实地图渲染的测试 仅需位置数据的测试
reCAPTCHA Google 测试密钥(推荐)或模拟 grecaptcha 自动化测试中从不使用 始终模拟 —— CAPTCHA 无法通过自动化解决
社交登录 按提供商创建可复用的 OAuth 模拟 fixture 定期集成检查 所有需要认证的 E2E 测试
邮件服务(SendGrid、SES 模拟 API 端点,验证调用是否发出 端到端邮件投递测试(独立测试套件) 所有触发邮件的测试

反模式

不要这样做 问题 应该这样做
在测试中使用真实的 OAuth 提供商账号 速率限制、2FA 要求、账号锁定、测试不稳定 模拟 OAuth 流程或使用提供商的测试模式
让分析脚本在所有测试中运行 测试变慢、污染真实分析数据、增加网络不稳定性 在共享 fixture 中使用 route.abort() 阻止分析工具
使用图像识别解决 reCAPTCHA 脆弱、缓慢、违反 CAPTCHA 服务条款 使用 Google 的测试密钥或模拟 grecaptcha 对象
模拟你自己的应用程序代码而非第三方代码 没有测试你的集成逻辑 仅模拟外部边界(API 端点、脚本)
在每个测试中硬编码模拟响应 模拟设置重复、难以维护 构建可复用的模拟 fixture(参见社交登录模式)
不测试第三方服务的错误路径 遗漏应用如何处理 OAuth 失败、支付被拒等情况 模拟错误响应:{ error: 'access_denied' }、被拒卡片等
在每个测试中加载真实的 Stripe.js 初始加载慢、偶发 CDN 故障 为非支付测试模拟 Stripe;仅在结账测试中使用真实 Stripe
对第三方脚本依赖 networkidle 第三方脚本可能无限轮询 等待特定的应用级指示器,而非 networkidle

故障排查

症状 可能原因 修复方法
OAuth 模拟路由从未触发 应用使用重定向链绕过了你的路由模式 使用更宽泛的模式如 **accounts.google.com**,或记录所有请求以查看实际 URL
Stripe iframe 未找到 Stripe.js 异步加载;iframe 名称因版本而异 使用灵活的定位器:iframe[name*="__privateStripeFrame"] 并设置较长的超时时间
reCAPTCHA 模拟不起作用 脚本在 addInitScript 运行之前已加载 使用 route 拦截 reCAPTCHA 脚本并提供模拟版本
分析工具拦截破坏了应用 应用代码依赖分析库存在(例如 window.gtag 不要阻止,而是用一个桩响应来满足:route.fulfill({ body: 'window.gtag=function(){}' })
聊天组件 iframe 无法访问 跨域 iframe 限制 使用 frameLocator 配合正确的 iframe 名称/定位器
模拟 OAuth 返回了错误的重定向 URI redirect_uri 参数是 URL 编码的,或在测试中不同 使用 page.on('request') 记录实际请求 URL 并相应匹配
支付测试本地通过但 CI 失败 Stripe 测试模式有速率限制;CI IP 可能被阻止 在 CI 中模拟 Stripe API,仅在单独的集成流水线中使用真实 Stripe
社交登录模拟未触发重定向 你的应用在新弹窗中打开 OAuth,而非重定向 处理弹窗:page.waitForEvent('popup') 并在弹窗页面上模拟路由

相关文档