# 第三方集成 > **何时使用**:测试应用程序与外部服务(OAuth 提供商、支付网关、分析工具、聊天组件、地图、社交登录和 CAPTCHA)的交互。核心原则:模拟第三方边界,而非你自己的应用程序代码。 > **前置要求**:[core/network-mocking.md](network-mocking.md)、[core/when-to-mock.md](when-to-mock.md)、[core/authentication.md](authentication.md) ## 快速参考 ```typescript // 模拟 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** ```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** ```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** ```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** ```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** ```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 } ``` ```typescript // 用于验证分析事件确实被发送的测试 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** ```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** ```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** ```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** ```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** ```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** ```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** ```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** ```typescript import { test as base, expect } from "@playwright/test" // 可复用的 fixture,用于模拟任何 OAuth 提供商 type OAuthFixtures = { mockOAuth: (provider: string, userData: Record) => Promise } export const test = base.extend({ mockOAuth: async ({ page }, use) => { const mock = async (provider: string, userData: Record) => { const providerPatterns: Record = { 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** ```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](network-mocking.md) —— 基础路由拦截模式 - [core/authentication.md](authentication.md) —— 认证状态管理和登录绕过 - [core/when-to-mock.md](when-to-mock.md) —— 模拟与真实服务的决策框架 - [core/multi-context-and-popups.md](multi-context-and-popups.md) —— 处理 OAuth 弹窗 - [core/security-testing.md](security-testing.md) —— 测试认证安全方面 - [core/iframes-and-shadow-dom.md](iframes-and-shadow-dom.md) —— 与支付组件 iframe 交互