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, core/when-to-mock.md, core/authentication.md
Quick Reference
// 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
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
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
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
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
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 }// 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
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
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
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
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
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
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
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
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
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 -- foundational route interception patterns
- core/authentication.md -- auth state management and login bypasses
- core/when-to-mock.md -- decision framework for mocking vs real services
- core/multi-context-and-popups.md -- handling OAuth popup windows
- core/security-testing.md -- testing auth security aspects
- core/iframes-and-shadow-dom.md -- interacting with payment widget iframes