# Third-Party Integrations

> **When to use**: Testing your application's interaction with external services -- OAuth providers, payment gateways, analytics, chat widgets, maps, social login, and CAPTCHAs. The core principle: mock the third-party boundary, not your own application code.
> **Prerequisites**: [core/network-mocking.md](network-mocking.md), [core/when-to-mock.md](when-to-mock.md), [core/authentication.md](authentication.md)

## Quick Reference

```typescript
// Mock an OAuth callback to bypass the real provider
await page.route("**/auth/callback*", (route) => {
  route.fulfill({
    status: 302,
    headers: { Location: "/dashboard?token=mock-jwt-token" },
  })
})

// Block analytics scripts from loading
await page.route("**/*.google-analytics.com/**", (route) => route.abort())
await page.route("**/segment.io/**", (route) => route.abort())

// Mock a third-party widget endpoint
await page.route("**/api.stripe.com/**", (route) => {
  route.fulfill({ status: 200, contentType: "application/json", body: '{"id":"pi_mock"}' })
})
```

**Core principle**: In E2E tests, mock the external service, not your own application. Your code should run as-is; only the third-party responses are faked.

## Patterns

### Mocking OAuth Providers (Google, GitHub, etc.)

**Use when**: Testing the full login flow without depending on real OAuth provider availability, rate limits, or test accounts.
**Avoid when**: You need to verify the actual OAuth integration works end-to-end (use a dedicated integration test for that).

**TypeScript**

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

test("Google OAuth login via route mocking", async ({ page }) => {
  // Intercept the redirect to Google and simulate the callback
  await page.route("**/accounts.google.com/**", async (route) => {
    // Extract the redirect_uri from the request URL
    const url = new URL(route.request().url())
    const redirectUri = url.searchParams.get("redirect_uri") || "/auth/callback"

    // Simulate Google redirecting back with an auth code
    await route.fulfill({
      status: 302,
      headers: {
        Location: `${redirectUri}?code=mock-auth-code&state=${url.searchParams.get("state") || ""}`,
      },
    })
  })

  // Mock your own backend's token exchange endpoint
  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()

  // Should land on dashboard with user info
  await page.waitForURL("/dashboard")
  await expect(page.getByText("Test User")).toBeVisible()
})

test("GitHub OAuth login with mocked provider", 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 login via route mocking", 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()
})
```

### Payment Gateway Testing (Stripe Elements)

**Use when**: Testing checkout flows that use Stripe Elements, PayPal buttons, or similar embedded payment UIs.
**Avoid when**: Stripe provides test mode with test card numbers and you want full integration coverage.

**TypeScript**

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

test("complete Stripe checkout with test card", async ({ page }) => {
  await page.goto("/checkout")

  // Stripe Elements load in an iframe
  const stripeFrame = page.frameLocator('iframe[name*="__privateStripeFrame"]').first()

  // Fill card details inside the 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 checkout with mocked API for speed", async ({ page }) => {
  // Mock Stripe API calls for faster, more reliable tests
  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")
  // With mocked Stripe, your app-level payment form may work without the iframe
  await page.getByRole("button", { name: "Pay" }).click()
  await expect(page.getByText("Payment successful")).toBeVisible()
})

test("handles declined card gracefully", async ({ page }) => {
  await page.goto("/checkout")

  const stripeFrame = page.frameLocator('iframe[name*="__privateStripeFrame"]').first()
  // Stripe test card that always declines
  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("complete Stripe checkout with test card", 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 })
})
```

### Analytics Blocking

**Use when**: Preventing analytics scripts from executing during tests to improve speed, avoid polluting analytics data, and eliminate flakiness from third-party script failures.
**Avoid when**: You specifically need to test that analytics events are fired correctly.

**TypeScript**

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

// Block analytics in a fixture for all tests
import { test as base } from "@playwright/test"

export const test = base.extend({
  page: async ({ page }, use) => {
    // Block common analytics and tracking scripts
    await page.route(
      /(google-analytics|googletagmanager|segment\.io|hotjar|mixpanel|amplitude)/,
      (route) => route.abort()
    )
    await page.route("**/collect?**", (route) => route.abort()) // GA beacon
    await page.route("**/analytics/**", (route) => route.abort())
    await use(page)
  },
})

export { expect }
```

```typescript
// For tests that verify analytics events ARE sent
import { test, expect } from "@playwright/test"

test("purchase event fires on checkout completion", async ({ page }) => {
  const analyticsRequests: { url: string; body: string }[] = []

  // Intercept analytics calls instead of blocking them
  await page.route("**/collect**", async (route) => {
    analyticsRequests.push({
      url: route.request().url(),
      body: route.request().postData() || "",
    })
    await route.fulfill({ status: 200 }) // Respond but don't send to real analytics
  })

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

  // Verify the purchase event was sent
  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 }
```

### Chat Widget Testing (Intercom, Drift, etc.)

**Use when**: Your app embeds a third-party chat widget and you need to test the interaction or verify it does not break your UI.
**Avoid when**: The chat widget is cosmetic and not part of critical user flows.

**TypeScript**

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

test("Intercom chat widget opens and accepts messages", async ({ page }) => {
  await page.goto("/")

  // Wait for the chat widget to load (often delayed)
  const chatLauncher = page
    .frameLocator('iframe[name*="intercom"]')
    .getByRole("button", { name: /chat|message/i })
  await expect(chatLauncher).toBeVisible({ timeout: 15000 })

  // Open the chat
  await chatLauncher.click()

  // The chat conversation iframe loads
  const chatFrame = page.frameLocator('iframe[name*="intercom-messenger"]')
  await expect(chatFrame.getByText(/How can we help/i)).toBeVisible()

  // Type a message
  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("chat widget does not overlap critical UI", async ({ page }) => {
  await page.goto("/checkout")

  // Get chat widget position
  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()

    // Ensure the pay button is not hidden behind the chat widget
    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)
    }
  }
})

// Block chat widget in most tests for speed
test("block chat widget for non-chat tests", async ({ page }) => {
  await page.route("**/widget.intercom.io/**", (route) => route.abort())
  await page.route("**/js.intercomcdn.com/**", (route) => route.abort())

  await page.goto("/dashboard")
  // Chat widget will not load — test runs faster
  await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible()
})
```

**JavaScript**

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

test("block chat widget for speed", 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()
})
```

### Map Integration Testing (Google Maps)

**Use when**: Your app embeds Google Maps or a similar map provider and you need to verify map-related interactions.
**Avoid when**: The map is decorative and not part of a user workflow.

**TypeScript**

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

test("store locator shows markers on the map", async ({ page }) => {
  await page.goto("/store-locator")

  // Google Maps loads in an iframe or directly on the page
  await page.getByLabel("Search location").fill("New York")
  await page.getByRole("button", { name: "Search" }).click()

  // Wait for map markers to appear
  await expect(page.locator('[data-testid="map-marker"]')).toHaveCount(5, {
    timeout: 10000,
  })

  // Click a marker to see store details
  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("mock Google Maps API for offline testing", async ({ page }) => {
  // Block real Google Maps and provide mock responses
  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("mock Google Maps geocode 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 Bypass for Testing

**Use when**: Your app uses reCAPTCHA or hCaptcha and you need tests to proceed without solving challenges.
**Avoid when**: You can disable CAPTCHA in your test environment through a server-side flag (preferred approach).

**TypeScript**

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

test.describe("reCAPTCHA handling", () => {
  // Best approach: Use test keys provided by Google
  // Site key: 6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI (always passes)
  // Secret key: 6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe

  test("form submission with reCAPTCHA bypassed via test keys", async ({ page }) => {
    // Your test environment should be configured with Google's test keys
    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")

    // With test keys, reCAPTCHA auto-passes — just click submit
    await page.getByRole("button", { name: "Send" }).click()
    await expect(page.getByText("Message sent")).toBeVisible()
  })

  test("mock reCAPTCHA verification endpoint", async ({ page }) => {
    // Mock Google's reCAPTCHA verification 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 }),
      })
    })

    // Mock the reCAPTCHA script to provide a fake token
    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("invisible reCAPTCHA v3 bypass", async ({ page }) => {
    // For reCAPTCHA v3, mock the enterprise API
    await page.route("**/recaptcha/**", async (route) => {
      if (route.request().url().includes("api.js")) {
        // Serve a mock script that provides the grecaptcha object
        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("mock reCAPTCHA for form submission", 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()
})
```

### Social Login Mocking

**Use when**: Your app offers multiple social login options and you need to test the login flow without real provider accounts.
**Avoid when**: You have a backend bypass that sets auth state directly.

**TypeScript**

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

// Reusable fixture that mocks any OAuth provider
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}`)

      // Intercept the OAuth redirect
      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` },
        })
      })

      // Mock the token exchange
      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)
  },
})

// Usage in tests
test("login with multiple social providers", async ({ page, mockOAuth }) => {
  // Test Google login
  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("login with 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 }
```

## Decision Guide

| Integration                    | Mock Strategy                                     | When to Use Real                                 | When to Mock                                     |
| ------------------------------ | ------------------------------------------------- | ------------------------------------------------ | ------------------------------------------------ |
| OAuth (Google, GitHub, etc.)   | Route intercept on provider URL + mock callback   | Dedicated integration test with test accounts    | All E2E feature tests that require login         |
| Stripe/payment gateway         | Use Stripe test mode with test cards OR mock API  | Checkout flow integration tests                  | All non-payment E2E tests                        |
| Google Analytics / Segment     | `route.abort()` to block entirely                 | Dedicated analytics verification tests           | Every other test (block for speed)               |
| Chat widgets (Intercom, Drift) | `route.abort()` to block, or interact via iframe  | Tests specifically about chat functionality      | Every other test (block for speed/stability)     |
| Google Maps                    | Mock geocoding/places API responses               | Tests verifying real map rendering               | Tests that only need location data               |
| reCAPTCHA                      | Google test keys (preferred) or mock `grecaptcha` | Never in automated tests                         | Always -- CAPTCHA cannot be solved by automation |
| Social login                   | Reusable OAuth mock fixture per provider          | Periodic integration check                       | All E2E tests requiring auth                     |
| Email services (SendGrid, SES) | Mock API endpoints, verify calls were made        | End-to-end email delivery tests (separate suite) | All tests that trigger emails                    |

## Anti-Patterns

| Don't Do This                                                | Problem                                                            | Do This Instead                                                            |
| ------------------------------------------------------------ | ------------------------------------------------------------------ | -------------------------------------------------------------------------- |
| Using real OAuth provider accounts in tests                  | Rate limits, 2FA requirements, account lockouts, test flakiness    | Mock the OAuth flow or use provider test modes                             |
| Letting analytics scripts run in all tests                   | Slower tests, pollutes real analytics data, adds network flakiness | Block analytics with `route.abort()` in a shared fixture                   |
| Solving reCAPTCHA with image recognition                     | Fragile, slow, violates CAPTCHA terms of service                   | Use Google's test keys or mock the `grecaptcha` object                     |
| Mocking your own application code instead of the third-party | Does not test your integration logic                               | Mock only the external boundary (API endpoints, scripts)                   |
| Hardcoding mock responses for every test                     | Duplicated mock setup, hard to maintain                            | Build reusable mock fixtures (see Social Login pattern)                    |
| Not testing error paths from third parties                   | Misses how your app handles OAuth failures, payment declines       | Mock error responses: `{ error: 'access_denied' }`, declined cards         |
| Loading real Stripe.js in every test                         | Slow initial load, occasional CDN failures                         | Mock Stripe for non-payment tests; use real Stripe only for checkout tests |
| Trusting `networkidle` for third-party scripts               | Third-party scripts may poll indefinitely                          | Wait for specific app-level indicators, not `networkidle`                  |

## Troubleshooting

| Symptom                                     | Likely Cause                                                              | Fix                                                                                             |
| ------------------------------------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| OAuth mock route never fires                | The app uses a redirect chain that bypasses your route pattern            | Use a broader pattern like `**accounts.google.com**` or log all requests to see the actual URL  |
| Stripe iframe not found                     | Stripe.js loads async; iframe name varies by version                      | Use a flexible selector: `iframe[name*="__privateStripeFrame"]` with a long timeout             |
| reCAPTCHA mock does not work                | Script loads before `addInitScript` runs                                  | Use `route` to intercept the reCAPTCHA script and serve a mock version                          |
| Analytics blocking breaks the app           | App code depends on analytics library being present (e.g., `window.gtag`) | Instead of blocking, fulfill with a stub: `route.fulfill({ body: 'window.gtag=function(){}' })` |
| Chat widget iframe is not accessible        | Cross-origin iframe restrictions                                          | Use `frameLocator` with the correct iframe name/selector                                        |
| Mock OAuth returns wrong redirect URI       | The `redirect_uri` parameter is URL-encoded or different in test          | Log the actual request URL with `page.on('request')` and match accordingly                      |
| Payment test passes locally but fails in CI | Stripe test mode has rate limits; CI IP may be blocked                    | Mock Stripe API in CI, use real Stripe only in a separate integration pipeline                  |
| Social login mock does not redirect         | Your app opens OAuth in a popup, not a redirect                           | Handle the popup: `page.waitForEvent('popup')` and mock routes on the popup page                |

## Related

- [core/network-mocking.md](network-mocking.md) -- foundational route interception patterns
- [core/authentication.md](authentication.md) -- auth state management and login bypasses
- [core/when-to-mock.md](when-to-mock.md) -- decision framework for mocking vs real services
- [core/multi-context-and-popups.md](multi-context-and-popups.md) -- handling OAuth popup windows
- [core/security-testing.md](security-testing.md) -- testing auth security aspects
- [core/iframes-and-shadow-dom.md](iframes-and-shadow-dom.md) -- interacting with payment widget iframes
