# 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) => 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}`) // 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