31 KiB
第三方集成
何时使用:测试应用程序与外部服务(OAuth 提供商、支付网关、分析工具、聊天组件、地图、社交登录和 CAPTCHA)的交互。核心原则:模拟第三方边界,而非你自己的应用程序代码。 前置要求:core/network-mocking.md、core/when-to-mock.md、core/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 }
决策指南
| 集成方式 | 模拟策略 | 何时使用真实服务 | 何时模拟 |
|---|---|---|---|
| OAuth(Google、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') 并在弹窗页面上模拟路由 |
相关文档
- core/network-mocking.md —— 基础路由拦截模式
- core/authentication.md —— 认证状态管理和登录绕过
- core/when-to-mock.md —— 模拟与真实服务的决策框架
- core/multi-context-and-popups.md —— 处理 OAuth 弹窗
- core/security-testing.md —— 测试认证安全方面
- core/iframes-and-shadow-dom.md —— 与支付组件 iframe 交互