@assertrx/dino 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +293 -0
- package/extensions/dino.ts +70 -0
- package/extensions/mabl-mcp.ts +306 -0
- package/extensions/playwright-mcp.ts +142 -0
- package/package.json +47 -0
- package/prompts/create-pr.md +110 -0
- package/prompts/generate-bdd-from-url.md +65 -0
- package/prompts/generate-pom-from-url-cli.md +48 -0
- package/prompts/generate-pom-from-url.md +45 -0
- package/prompts/generate-spec-from-feature.md +64 -0
- package/prompts/heal-playwright-failures.md +106 -0
- package/prompts/setup-playwright.md +81 -0
- package/scripts/start-dino.cmd +8 -0
- package/scripts/start-dino.mjs +226 -0
- package/scripts/start-dino.ps1 +9 -0
- package/scripts/start-dino.sh +10 -0
- package/skills/acli/SKILL.md +292 -0
- package/skills/bdd-gherkin/SKILL.md +552 -0
- package/skills/build-review-interface/SKILL.md +96 -0
- package/skills/error-analysis/SKILL.md +164 -0
- package/skills/eval-audit/SKILL.md +183 -0
- package/skills/evaluate-rag/SKILL.md +177 -0
- package/skills/generate-synthetic-data/SKILL.md +131 -0
- package/skills/mabl-mcp/SKILL.md +79 -0
- package/skills/playwright-cli/SKILL.md +246 -0
- package/skills/playwright-cli/advanced-workflows.md +679 -0
- package/skills/playwright-cli/core-commands.md +375 -0
- package/skills/playwright-cli/device-emulation.md +475 -0
- package/skills/playwright-cli/request-mocking.md +373 -0
- package/skills/playwright-cli/running-custom-code.md +553 -0
- package/skills/playwright-cli/screenshots-and-media.md +457 -0
- package/skills/playwright-cli/session-management.md +425 -0
- package/skills/playwright-cli/storage-and-auth.md +487 -0
- package/skills/playwright-cli/test-generation.md +336 -0
- package/skills/playwright-cli/tracing-and-debugging.md +501 -0
- package/skills/playwright-core/SKILL.md +101 -0
- package/skills/playwright-core/accessibility.md +1517 -0
- package/skills/playwright-core/angular.md +1086 -0
- package/skills/playwright-core/api-testing.md +1623 -0
- package/skills/playwright-core/assertions-and-waiting.md +703 -0
- package/skills/playwright-core/auth-flows.md +1081 -0
- package/skills/playwright-core/authentication.md +1409 -0
- package/skills/playwright-core/browser-apis.md +673 -0
- package/skills/playwright-core/browser-extensions.md +319 -0
- package/skills/playwright-core/canvas-and-webgl.md +494 -0
- package/skills/playwright-core/clock-and-time-mocking.md +427 -0
- package/skills/playwright-core/common-pitfalls.md +1318 -0
- package/skills/playwright-core/component-testing.md +1179 -0
- package/skills/playwright-core/configuration.md +731 -0
- package/skills/playwright-core/crud-testing.md +945 -0
- package/skills/playwright-core/debugging.md +827 -0
- package/skills/playwright-core/drag-and-drop.md +992 -0
- package/skills/playwright-core/electron-testing.md +622 -0
- package/skills/playwright-core/error-and-edge-cases.md +1181 -0
- package/skills/playwright-core/error-index.md +1903 -0
- package/skills/playwright-core/file-operations.md +749 -0
- package/skills/playwright-core/file-upload-download.md +1012 -0
- package/skills/playwright-core/fixtures-and-hooks.md +1072 -0
- package/skills/playwright-core/flaky-tests.md +883 -0
- package/skills/playwright-core/forms-and-validation.md +1056 -0
- package/skills/playwright-core/i18n-and-localization.md +622 -0
- package/skills/playwright-core/iframes-and-shadow-dom.md +488 -0
- package/skills/playwright-core/locator-strategy.md +588 -0
- package/skills/playwright-core/locators.md +743 -0
- package/skills/playwright-core/mobile-and-responsive.md +1669 -0
- package/skills/playwright-core/multi-context-and-popups.md +581 -0
- package/skills/playwright-core/multi-user-and-collaboration.md +477 -0
- package/skills/playwright-core/network-mocking.md +1395 -0
- package/skills/playwright-core/nextjs.md +1013 -0
- package/skills/playwright-core/performance-testing.md +608 -0
- package/skills/playwright-core/react.md +1082 -0
- package/skills/playwright-core/search-and-filter.md +1366 -0
- package/skills/playwright-core/security-testing.md +624 -0
- package/skills/playwright-core/service-workers-and-pwa.md +524 -0
- package/skills/playwright-core/test-architecture.md +569 -0
- package/skills/playwright-core/test-data-management.md +1209 -0
- package/skills/playwright-core/test-organization.md +947 -0
- package/skills/playwright-core/third-party-integrations.md +754 -0
- package/skills/playwright-core/trace-analysis.md +161 -0
- package/skills/playwright-core/visual-regression.md +1027 -0
- package/skills/playwright-core/vue.md +1162 -0
- package/skills/playwright-core/websockets-and-realtime.md +623 -0
- package/skills/playwright-core/when-to-mock.md +827 -0
- package/skills/playwright-pom/SKILL.md +94 -0
- package/skills/playwright-pom/page-object-model.md +921 -0
- package/skills/playwright-pom/pom-vs-fixtures-vs-helpers.md +937 -0
- package/skills/validate-evaluator/SKILL.md +215 -0
- package/skills/write-judge-prompt/SKILL.md +144 -0
- package/themes/dino.json +66 -0
|
@@ -0,0 +1,754 @@
|
|
|
1
|
+
# Third-Party Integrations
|
|
2
|
+
|
|
3
|
+
> **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.
|
|
4
|
+
> **Prerequisites**: [core/network-mocking.md](network-mocking.md), [core/when-to-mock.md](when-to-mock.md), [core/authentication.md](authentication.md)
|
|
5
|
+
|
|
6
|
+
## Quick Reference
|
|
7
|
+
|
|
8
|
+
```typescript
|
|
9
|
+
// Mock an OAuth callback to bypass the real provider
|
|
10
|
+
await page.route('**/auth/callback*', (route) => {
|
|
11
|
+
route.fulfill({
|
|
12
|
+
status: 302,
|
|
13
|
+
headers: { Location: '/dashboard?token=mock-jwt-token' },
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// Block analytics scripts from loading
|
|
18
|
+
await page.route('**/*.google-analytics.com/**', (route) => route.abort());
|
|
19
|
+
await page.route('**/segment.io/**', (route) => route.abort());
|
|
20
|
+
|
|
21
|
+
// Mock a third-party widget endpoint
|
|
22
|
+
await page.route('**/api.stripe.com/**', (route) => {
|
|
23
|
+
route.fulfill({ status: 200, contentType: 'application/json', body: '{"id":"pi_mock"}' });
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**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.
|
|
28
|
+
|
|
29
|
+
## Patterns
|
|
30
|
+
|
|
31
|
+
### Mocking OAuth Providers (Google, GitHub, etc.)
|
|
32
|
+
|
|
33
|
+
**Use when**: Testing the full login flow without depending on real OAuth provider availability, rate limits, or test accounts.
|
|
34
|
+
**Avoid when**: You need to verify the actual OAuth integration works end-to-end (use a dedicated integration test for that).
|
|
35
|
+
|
|
36
|
+
**TypeScript**
|
|
37
|
+
```typescript
|
|
38
|
+
import { test, expect } from '@playwright/test';
|
|
39
|
+
|
|
40
|
+
test('Google OAuth login via route mocking', async ({ page }) => {
|
|
41
|
+
// Intercept the redirect to Google and simulate the callback
|
|
42
|
+
await page.route('**/accounts.google.com/**', async (route) => {
|
|
43
|
+
// Extract the redirect_uri from the request URL
|
|
44
|
+
const url = new URL(route.request().url());
|
|
45
|
+
const redirectUri = url.searchParams.get('redirect_uri') || '/auth/callback';
|
|
46
|
+
|
|
47
|
+
// Simulate Google redirecting back with an auth code
|
|
48
|
+
await route.fulfill({
|
|
49
|
+
status: 302,
|
|
50
|
+
headers: {
|
|
51
|
+
Location: `${redirectUri}?code=mock-auth-code&state=${url.searchParams.get('state') || ''}`,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Mock your own backend's token exchange endpoint
|
|
57
|
+
await page.route('**/api/auth/google/callback*', async (route) => {
|
|
58
|
+
await route.fulfill({
|
|
59
|
+
status: 200,
|
|
60
|
+
contentType: 'application/json',
|
|
61
|
+
body: JSON.stringify({
|
|
62
|
+
token: 'mock-jwt-token',
|
|
63
|
+
user: {
|
|
64
|
+
id: '1',
|
|
65
|
+
name: 'Test User',
|
|
66
|
+
email: 'testuser@gmail.com',
|
|
67
|
+
avatar: 'https://placehold.co/100x100',
|
|
68
|
+
},
|
|
69
|
+
}),
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await page.goto('/login');
|
|
74
|
+
await page.getByRole('button', { name: 'Sign in with Google' }).click();
|
|
75
|
+
|
|
76
|
+
// Should land on dashboard with user info
|
|
77
|
+
await page.waitForURL('/dashboard');
|
|
78
|
+
await expect(page.getByText('Test User')).toBeVisible();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('GitHub OAuth login with mocked provider', async ({ page }) => {
|
|
82
|
+
await page.route('**/github.com/login/oauth/**', async (route) => {
|
|
83
|
+
const url = new URL(route.request().url());
|
|
84
|
+
const redirectUri = url.searchParams.get('redirect_uri') || '/auth/callback';
|
|
85
|
+
await route.fulfill({
|
|
86
|
+
status: 302,
|
|
87
|
+
headers: {
|
|
88
|
+
Location: `${redirectUri}?code=mock-github-code`,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await page.route('**/api/auth/github/callback*', async (route) => {
|
|
94
|
+
await route.fulfill({
|
|
95
|
+
status: 200,
|
|
96
|
+
contentType: 'application/json',
|
|
97
|
+
body: JSON.stringify({
|
|
98
|
+
token: 'mock-jwt-token',
|
|
99
|
+
user: { id: '2', name: 'GitHub User', email: 'user@github.com' },
|
|
100
|
+
}),
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
await page.goto('/login');
|
|
105
|
+
await page.getByRole('button', { name: 'Sign in with GitHub' }).click();
|
|
106
|
+
await page.waitForURL('/dashboard');
|
|
107
|
+
await expect(page.getByText('GitHub User')).toBeVisible();
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**JavaScript**
|
|
112
|
+
```javascript
|
|
113
|
+
const { test, expect } = require('@playwright/test');
|
|
114
|
+
|
|
115
|
+
test('Google OAuth login via route mocking', async ({ page }) => {
|
|
116
|
+
await page.route('**/accounts.google.com/**', async (route) => {
|
|
117
|
+
const url = new URL(route.request().url());
|
|
118
|
+
const redirectUri = url.searchParams.get('redirect_uri') || '/auth/callback';
|
|
119
|
+
await route.fulfill({
|
|
120
|
+
status: 302,
|
|
121
|
+
headers: {
|
|
122
|
+
Location: `${redirectUri}?code=mock-auth-code&state=${url.searchParams.get('state') || ''}`,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
await page.route('**/api/auth/google/callback*', async (route) => {
|
|
128
|
+
await route.fulfill({
|
|
129
|
+
status: 200,
|
|
130
|
+
contentType: 'application/json',
|
|
131
|
+
body: JSON.stringify({
|
|
132
|
+
token: 'mock-jwt-token',
|
|
133
|
+
user: { id: '1', name: 'Test User', email: 'testuser@gmail.com' },
|
|
134
|
+
}),
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
await page.goto('/login');
|
|
139
|
+
await page.getByRole('button', { name: 'Sign in with Google' }).click();
|
|
140
|
+
await page.waitForURL('/dashboard');
|
|
141
|
+
await expect(page.getByText('Test User')).toBeVisible();
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Payment Gateway Testing (Stripe Elements)
|
|
146
|
+
|
|
147
|
+
**Use when**: Testing checkout flows that use Stripe Elements, PayPal buttons, or similar embedded payment UIs.
|
|
148
|
+
**Avoid when**: Stripe provides test mode with test card numbers and you want full integration coverage.
|
|
149
|
+
|
|
150
|
+
**TypeScript**
|
|
151
|
+
```typescript
|
|
152
|
+
import { test, expect } from '@playwright/test';
|
|
153
|
+
|
|
154
|
+
test('complete Stripe checkout with test card', async ({ page }) => {
|
|
155
|
+
await page.goto('/checkout');
|
|
156
|
+
|
|
157
|
+
// Stripe Elements load in an iframe
|
|
158
|
+
const stripeFrame = page.frameLocator('iframe[name*="__privateStripeFrame"]').first();
|
|
159
|
+
|
|
160
|
+
// Fill card details inside the Stripe iframe
|
|
161
|
+
await stripeFrame.getByPlaceholder('Card number').fill('4242424242424242');
|
|
162
|
+
await stripeFrame.getByPlaceholder('MM / YY').fill('12/28');
|
|
163
|
+
await stripeFrame.getByPlaceholder('CVC').fill('123');
|
|
164
|
+
await stripeFrame.getByPlaceholder('ZIP').fill('10001');
|
|
165
|
+
|
|
166
|
+
await page.getByRole('button', { name: 'Pay' }).click();
|
|
167
|
+
await expect(page.getByText('Payment successful')).toBeVisible({ timeout: 15000 });
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('Stripe checkout with mocked API for speed', async ({ page }) => {
|
|
171
|
+
// Mock Stripe API calls for faster, more reliable tests
|
|
172
|
+
await page.route('**/api.stripe.com/v1/payment_intents*', async (route) => {
|
|
173
|
+
await route.fulfill({
|
|
174
|
+
status: 200,
|
|
175
|
+
contentType: 'application/json',
|
|
176
|
+
body: JSON.stringify({
|
|
177
|
+
id: 'pi_mock_123',
|
|
178
|
+
status: 'succeeded',
|
|
179
|
+
client_secret: 'pi_mock_123_secret_456',
|
|
180
|
+
}),
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
await page.route('**/api.stripe.com/v1/payment_methods*', async (route) => {
|
|
185
|
+
await route.fulfill({
|
|
186
|
+
status: 200,
|
|
187
|
+
contentType: 'application/json',
|
|
188
|
+
body: JSON.stringify({
|
|
189
|
+
id: 'pm_mock_789',
|
|
190
|
+
type: 'card',
|
|
191
|
+
card: { brand: 'visa', last4: '4242' },
|
|
192
|
+
}),
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
await page.goto('/checkout');
|
|
197
|
+
// With mocked Stripe, your app-level payment form may work without the iframe
|
|
198
|
+
await page.getByRole('button', { name: 'Pay' }).click();
|
|
199
|
+
await expect(page.getByText('Payment successful')).toBeVisible();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('handles declined card gracefully', async ({ page }) => {
|
|
203
|
+
await page.goto('/checkout');
|
|
204
|
+
|
|
205
|
+
const stripeFrame = page.frameLocator('iframe[name*="__privateStripeFrame"]').first();
|
|
206
|
+
// Stripe test card that always declines
|
|
207
|
+
await stripeFrame.getByPlaceholder('Card number').fill('4000000000000002');
|
|
208
|
+
await stripeFrame.getByPlaceholder('MM / YY').fill('12/28');
|
|
209
|
+
await stripeFrame.getByPlaceholder('CVC').fill('123');
|
|
210
|
+
await stripeFrame.getByPlaceholder('ZIP').fill('10001');
|
|
211
|
+
|
|
212
|
+
await page.getByRole('button', { name: 'Pay' }).click();
|
|
213
|
+
await expect(page.getByText(/declined|failed/i)).toBeVisible({ timeout: 15000 });
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**JavaScript**
|
|
218
|
+
```javascript
|
|
219
|
+
const { test, expect } = require('@playwright/test');
|
|
220
|
+
|
|
221
|
+
test('complete Stripe checkout with test card', async ({ page }) => {
|
|
222
|
+
await page.goto('/checkout');
|
|
223
|
+
|
|
224
|
+
const stripeFrame = page.frameLocator('iframe[name*="__privateStripeFrame"]').first();
|
|
225
|
+
await stripeFrame.getByPlaceholder('Card number').fill('4242424242424242');
|
|
226
|
+
await stripeFrame.getByPlaceholder('MM / YY').fill('12/28');
|
|
227
|
+
await stripeFrame.getByPlaceholder('CVC').fill('123');
|
|
228
|
+
await stripeFrame.getByPlaceholder('ZIP').fill('10001');
|
|
229
|
+
|
|
230
|
+
await page.getByRole('button', { name: 'Pay' }).click();
|
|
231
|
+
await expect(page.getByText('Payment successful')).toBeVisible({ timeout: 15000 });
|
|
232
|
+
});
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Analytics Blocking
|
|
236
|
+
|
|
237
|
+
**Use when**: Preventing analytics scripts from executing during tests to improve speed, avoid polluting analytics data, and eliminate flakiness from third-party script failures.
|
|
238
|
+
**Avoid when**: You specifically need to test that analytics events are fired correctly.
|
|
239
|
+
|
|
240
|
+
**TypeScript**
|
|
241
|
+
```typescript
|
|
242
|
+
import { test, expect } from '@playwright/test';
|
|
243
|
+
|
|
244
|
+
// Block analytics in a fixture for all tests
|
|
245
|
+
import { test as base } from '@playwright/test';
|
|
246
|
+
|
|
247
|
+
export const test = base.extend({
|
|
248
|
+
page: async ({ page }, use) => {
|
|
249
|
+
// Block common analytics and tracking scripts
|
|
250
|
+
await page.route(/(google-analytics|googletagmanager|segment\.io|hotjar|mixpanel|amplitude)/, (route) =>
|
|
251
|
+
route.abort()
|
|
252
|
+
);
|
|
253
|
+
await page.route('**/collect?**', (route) => route.abort()); // GA beacon
|
|
254
|
+
await page.route('**/analytics/**', (route) => route.abort());
|
|
255
|
+
await use(page);
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
export { expect };
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
// For tests that verify analytics events ARE sent
|
|
264
|
+
import { test, expect } from '@playwright/test';
|
|
265
|
+
|
|
266
|
+
test('purchase event fires on checkout completion', async ({ page }) => {
|
|
267
|
+
const analyticsRequests: { url: string; body: string }[] = [];
|
|
268
|
+
|
|
269
|
+
// Intercept analytics calls instead of blocking them
|
|
270
|
+
await page.route('**/collect**', async (route) => {
|
|
271
|
+
analyticsRequests.push({
|
|
272
|
+
url: route.request().url(),
|
|
273
|
+
body: route.request().postData() || '',
|
|
274
|
+
});
|
|
275
|
+
await route.fulfill({ status: 200 }); // Respond but don't send to real analytics
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
await page.goto('/checkout');
|
|
279
|
+
await page.getByRole('button', { name: 'Complete order' }).click();
|
|
280
|
+
await page.waitForURL('/order/confirmation');
|
|
281
|
+
|
|
282
|
+
// Verify the purchase event was sent
|
|
283
|
+
const purchaseEvent = analyticsRequests.find(
|
|
284
|
+
(r) => r.body.includes('purchase') || r.url.includes('purchase')
|
|
285
|
+
);
|
|
286
|
+
expect(purchaseEvent).toBeDefined();
|
|
287
|
+
});
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
**JavaScript**
|
|
291
|
+
```javascript
|
|
292
|
+
const { test: base, expect } = require('@playwright/test');
|
|
293
|
+
|
|
294
|
+
const test = base.extend({
|
|
295
|
+
page: async ({ page }, use) => {
|
|
296
|
+
await page.route(/(google-analytics|googletagmanager|segment\.io|hotjar|mixpanel)/, (route) =>
|
|
297
|
+
route.abort()
|
|
298
|
+
);
|
|
299
|
+
await use(page);
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
module.exports = { test, expect };
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Chat Widget Testing (Intercom, Drift, etc.)
|
|
307
|
+
|
|
308
|
+
**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.
|
|
309
|
+
**Avoid when**: The chat widget is cosmetic and not part of critical user flows.
|
|
310
|
+
|
|
311
|
+
**TypeScript**
|
|
312
|
+
```typescript
|
|
313
|
+
import { test, expect } from '@playwright/test';
|
|
314
|
+
|
|
315
|
+
test('Intercom chat widget opens and accepts messages', async ({ page }) => {
|
|
316
|
+
await page.goto('/');
|
|
317
|
+
|
|
318
|
+
// Wait for the chat widget to load (often delayed)
|
|
319
|
+
const chatLauncher = page.frameLocator('iframe[name*="intercom"]')
|
|
320
|
+
.getByRole('button', { name: /chat|message/i });
|
|
321
|
+
await expect(chatLauncher).toBeVisible({ timeout: 15000 });
|
|
322
|
+
|
|
323
|
+
// Open the chat
|
|
324
|
+
await chatLauncher.click();
|
|
325
|
+
|
|
326
|
+
// The chat conversation iframe loads
|
|
327
|
+
const chatFrame = page.frameLocator('iframe[name*="intercom-messenger"]');
|
|
328
|
+
await expect(chatFrame.getByText(/How can we help/i)).toBeVisible();
|
|
329
|
+
|
|
330
|
+
// Type a message
|
|
331
|
+
await chatFrame.getByRole('textbox').fill('I need help with my order');
|
|
332
|
+
await chatFrame.getByRole('button', { name: /send/i }).click();
|
|
333
|
+
|
|
334
|
+
await expect(chatFrame.getByText('I need help with my order')).toBeVisible();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test('chat widget does not overlap critical UI', async ({ page }) => {
|
|
338
|
+
await page.goto('/checkout');
|
|
339
|
+
|
|
340
|
+
// Get chat widget position
|
|
341
|
+
const chatWidget = page.locator('iframe[name*="intercom"]').first();
|
|
342
|
+
if (await chatWidget.isVisible()) {
|
|
343
|
+
const chatBox = await chatWidget.boundingBox();
|
|
344
|
+
const payButton = await page.getByRole('button', { name: 'Pay' }).boundingBox();
|
|
345
|
+
|
|
346
|
+
// Ensure the pay button is not hidden behind the chat widget
|
|
347
|
+
if (chatBox && payButton) {
|
|
348
|
+
const overlaps =
|
|
349
|
+
payButton.x < chatBox.x + chatBox.width &&
|
|
350
|
+
payButton.x + payButton.width > chatBox.x &&
|
|
351
|
+
payButton.y < chatBox.y + chatBox.height &&
|
|
352
|
+
payButton.y + payButton.height > chatBox.y;
|
|
353
|
+
expect(overlaps).toBe(false);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// Block chat widget in most tests for speed
|
|
359
|
+
test('block chat widget for non-chat tests', async ({ page }) => {
|
|
360
|
+
await page.route('**/widget.intercom.io/**', (route) => route.abort());
|
|
361
|
+
await page.route('**/js.intercomcdn.com/**', (route) => route.abort());
|
|
362
|
+
|
|
363
|
+
await page.goto('/dashboard');
|
|
364
|
+
// Chat widget will not load — test runs faster
|
|
365
|
+
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
|
|
366
|
+
});
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
**JavaScript**
|
|
370
|
+
```javascript
|
|
371
|
+
const { test, expect } = require('@playwright/test');
|
|
372
|
+
|
|
373
|
+
test('block chat widget for speed', async ({ page }) => {
|
|
374
|
+
await page.route('**/widget.intercom.io/**', (route) => route.abort());
|
|
375
|
+
await page.route('**/js.intercomcdn.com/**', (route) => route.abort());
|
|
376
|
+
|
|
377
|
+
await page.goto('/dashboard');
|
|
378
|
+
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
|
|
379
|
+
});
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### Map Integration Testing (Google Maps)
|
|
383
|
+
|
|
384
|
+
**Use when**: Your app embeds Google Maps or a similar map provider and you need to verify map-related interactions.
|
|
385
|
+
**Avoid when**: The map is decorative and not part of a user workflow.
|
|
386
|
+
|
|
387
|
+
**TypeScript**
|
|
388
|
+
```typescript
|
|
389
|
+
import { test, expect } from '@playwright/test';
|
|
390
|
+
|
|
391
|
+
test('store locator shows markers on the map', async ({ page }) => {
|
|
392
|
+
await page.goto('/store-locator');
|
|
393
|
+
|
|
394
|
+
// Google Maps loads in an iframe or directly on the page
|
|
395
|
+
await page.getByLabel('Search location').fill('New York');
|
|
396
|
+
await page.getByRole('button', { name: 'Search' }).click();
|
|
397
|
+
|
|
398
|
+
// Wait for map markers to appear
|
|
399
|
+
await expect(page.locator('[data-testid="map-marker"]')).toHaveCount(5, {
|
|
400
|
+
timeout: 10000,
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// Click a marker to see store details
|
|
404
|
+
await page.locator('[data-testid="map-marker"]').first().click();
|
|
405
|
+
await expect(page.getByTestId('store-info')).toBeVisible();
|
|
406
|
+
await expect(page.getByTestId('store-info')).toContainText('New York');
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test('mock Google Maps API for offline testing', async ({ page }) => {
|
|
410
|
+
// Block real Google Maps and provide mock responses
|
|
411
|
+
await page.route('**/maps.googleapis.com/**', async (route) => {
|
|
412
|
+
const url = route.request().url();
|
|
413
|
+
|
|
414
|
+
if (url.includes('/geocode/')) {
|
|
415
|
+
await route.fulfill({
|
|
416
|
+
status: 200,
|
|
417
|
+
contentType: 'application/json',
|
|
418
|
+
body: JSON.stringify({
|
|
419
|
+
results: [{
|
|
420
|
+
geometry: { location: { lat: 40.7128, lng: -74.0060 } },
|
|
421
|
+
formatted_address: 'New York, NY, USA',
|
|
422
|
+
}],
|
|
423
|
+
status: 'OK',
|
|
424
|
+
}),
|
|
425
|
+
});
|
|
426
|
+
} else if (url.includes('/places/')) {
|
|
427
|
+
await route.fulfill({
|
|
428
|
+
status: 200,
|
|
429
|
+
contentType: 'application/json',
|
|
430
|
+
body: JSON.stringify({
|
|
431
|
+
results: [
|
|
432
|
+
{ name: 'Store A', geometry: { location: { lat: 40.71, lng: -74.00 } } },
|
|
433
|
+
{ name: 'Store B', geometry: { location: { lat: 40.72, lng: -74.01 } } },
|
|
434
|
+
],
|
|
435
|
+
status: 'OK',
|
|
436
|
+
}),
|
|
437
|
+
});
|
|
438
|
+
} else {
|
|
439
|
+
await route.continue();
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
await page.goto('/store-locator');
|
|
444
|
+
await page.getByLabel('Search location').fill('New York');
|
|
445
|
+
await page.getByRole('button', { name: 'Search' }).click();
|
|
446
|
+
|
|
447
|
+
await expect(page.getByText('Store A')).toBeVisible();
|
|
448
|
+
await expect(page.getByText('Store B')).toBeVisible();
|
|
449
|
+
});
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
**JavaScript**
|
|
453
|
+
```javascript
|
|
454
|
+
const { test, expect } = require('@playwright/test');
|
|
455
|
+
|
|
456
|
+
test('mock Google Maps geocode API', async ({ page }) => {
|
|
457
|
+
await page.route('**/maps.googleapis.com/maps/api/geocode/**', async (route) => {
|
|
458
|
+
await route.fulfill({
|
|
459
|
+
status: 200,
|
|
460
|
+
contentType: 'application/json',
|
|
461
|
+
body: JSON.stringify({
|
|
462
|
+
results: [{
|
|
463
|
+
geometry: { location: { lat: 40.7128, lng: -74.0060 } },
|
|
464
|
+
formatted_address: 'New York, NY, USA',
|
|
465
|
+
}],
|
|
466
|
+
status: 'OK',
|
|
467
|
+
}),
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
await page.goto('/store-locator');
|
|
472
|
+
await page.getByLabel('Search location').fill('New York');
|
|
473
|
+
await page.getByRole('button', { name: 'Search' }).click();
|
|
474
|
+
await expect(page.getByText('New York')).toBeVisible();
|
|
475
|
+
});
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### reCAPTCHA Bypass for Testing
|
|
479
|
+
|
|
480
|
+
**Use when**: Your app uses reCAPTCHA or hCaptcha and you need tests to proceed without solving challenges.
|
|
481
|
+
**Avoid when**: You can disable CAPTCHA in your test environment through a server-side flag (preferred approach).
|
|
482
|
+
|
|
483
|
+
**TypeScript**
|
|
484
|
+
```typescript
|
|
485
|
+
import { test, expect } from '@playwright/test';
|
|
486
|
+
|
|
487
|
+
test.describe('reCAPTCHA handling', () => {
|
|
488
|
+
// Best approach: Use test keys provided by Google
|
|
489
|
+
// Site key: 6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI (always passes)
|
|
490
|
+
// Secret key: 6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe
|
|
491
|
+
|
|
492
|
+
test('form submission with reCAPTCHA bypassed via test keys', async ({ page }) => {
|
|
493
|
+
// Your test environment should be configured with Google's test keys
|
|
494
|
+
await page.goto('/contact');
|
|
495
|
+
|
|
496
|
+
await page.getByLabel('Name').fill('Test User');
|
|
497
|
+
await page.getByLabel('Email').fill('test@example.com');
|
|
498
|
+
await page.getByLabel('Message').fill('Test message');
|
|
499
|
+
|
|
500
|
+
// With test keys, reCAPTCHA auto-passes — just click submit
|
|
501
|
+
await page.getByRole('button', { name: 'Send' }).click();
|
|
502
|
+
await expect(page.getByText('Message sent')).toBeVisible();
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
test('mock reCAPTCHA verification endpoint', async ({ page }) => {
|
|
506
|
+
// Mock Google's reCAPTCHA verification API
|
|
507
|
+
await page.route('**/recaptcha/api/siteverify**', async (route) => {
|
|
508
|
+
await route.fulfill({
|
|
509
|
+
status: 200,
|
|
510
|
+
contentType: 'application/json',
|
|
511
|
+
body: JSON.stringify({ success: true, score: 0.9 }),
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// Mock the reCAPTCHA script to provide a fake token
|
|
516
|
+
await page.addInitScript(() => {
|
|
517
|
+
(window as any).grecaptcha = {
|
|
518
|
+
ready: (cb: () => void) => cb(),
|
|
519
|
+
execute: () => Promise.resolve('mock-recaptcha-token'),
|
|
520
|
+
render: () => 'mock-widget-id',
|
|
521
|
+
getResponse: () => 'mock-recaptcha-token',
|
|
522
|
+
reset: () => {},
|
|
523
|
+
};
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
await page.goto('/contact');
|
|
527
|
+
await page.getByLabel('Name').fill('Test User');
|
|
528
|
+
await page.getByLabel('Email').fill('test@example.com');
|
|
529
|
+
await page.getByLabel('Message').fill('Test message');
|
|
530
|
+
await page.getByRole('button', { name: 'Send' }).click();
|
|
531
|
+
|
|
532
|
+
await expect(page.getByText('Message sent')).toBeVisible();
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
test('invisible reCAPTCHA v3 bypass', async ({ page }) => {
|
|
536
|
+
// For reCAPTCHA v3, mock the enterprise API
|
|
537
|
+
await page.route('**/recaptcha/**', async (route) => {
|
|
538
|
+
if (route.request().url().includes('api.js')) {
|
|
539
|
+
// Serve a mock script that provides the grecaptcha object
|
|
540
|
+
await route.fulfill({
|
|
541
|
+
status: 200,
|
|
542
|
+
contentType: 'application/javascript',
|
|
543
|
+
body: `
|
|
544
|
+
window.grecaptcha = {
|
|
545
|
+
ready: function(cb) { cb(); },
|
|
546
|
+
execute: function() { return Promise.resolve('mock-token'); },
|
|
547
|
+
enterprise: {
|
|
548
|
+
ready: function(cb) { cb(); },
|
|
549
|
+
execute: function() { return Promise.resolve('mock-token'); },
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
`,
|
|
553
|
+
});
|
|
554
|
+
} else {
|
|
555
|
+
await route.abort();
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
await page.goto('/login');
|
|
560
|
+
await page.getByLabel('Email').fill('user@example.com');
|
|
561
|
+
await page.getByLabel('Password').fill('password123');
|
|
562
|
+
await page.getByRole('button', { name: 'Sign in' }).click();
|
|
563
|
+
await page.waitForURL('/dashboard');
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
**JavaScript**
|
|
569
|
+
```javascript
|
|
570
|
+
const { test, expect } = require('@playwright/test');
|
|
571
|
+
|
|
572
|
+
test('mock reCAPTCHA for form submission', async ({ page }) => {
|
|
573
|
+
await page.route('**/recaptcha/api/siteverify**', async (route) => {
|
|
574
|
+
await route.fulfill({
|
|
575
|
+
status: 200,
|
|
576
|
+
contentType: 'application/json',
|
|
577
|
+
body: JSON.stringify({ success: true, score: 0.9 }),
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
await page.addInitScript(() => {
|
|
582
|
+
window.grecaptcha = {
|
|
583
|
+
ready: (cb) => cb(),
|
|
584
|
+
execute: () => Promise.resolve('mock-recaptcha-token'),
|
|
585
|
+
render: () => 'mock-widget-id',
|
|
586
|
+
getResponse: () => 'mock-recaptcha-token',
|
|
587
|
+
reset: () => {},
|
|
588
|
+
};
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
await page.goto('/contact');
|
|
592
|
+
await page.getByLabel('Name').fill('Test User');
|
|
593
|
+
await page.getByLabel('Email').fill('test@example.com');
|
|
594
|
+
await page.getByLabel('Message').fill('Test message');
|
|
595
|
+
await page.getByRole('button', { name: 'Send' }).click();
|
|
596
|
+
await expect(page.getByText('Message sent')).toBeVisible();
|
|
597
|
+
});
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
### Social Login Mocking
|
|
601
|
+
|
|
602
|
+
**Use when**: Your app offers multiple social login options and you need to test the login flow without real provider accounts.
|
|
603
|
+
**Avoid when**: You have a backend bypass that sets auth state directly.
|
|
604
|
+
|
|
605
|
+
**TypeScript**
|
|
606
|
+
```typescript
|
|
607
|
+
import { test as base, expect } from '@playwright/test';
|
|
608
|
+
|
|
609
|
+
// Reusable fixture that mocks any OAuth provider
|
|
610
|
+
type OAuthFixtures = {
|
|
611
|
+
mockOAuth: (provider: string, userData: Record<string, string>) => Promise<void>;
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
export const test = base.extend<OAuthFixtures>({
|
|
615
|
+
mockOAuth: async ({ page }, use) => {
|
|
616
|
+
const mock = async (provider: string, userData: Record<string, string>) => {
|
|
617
|
+
const providerPatterns: Record<string, string> = {
|
|
618
|
+
google: '**/accounts.google.com/**',
|
|
619
|
+
github: '**/github.com/login/oauth/**',
|
|
620
|
+
facebook: '**/facebook.com/v*/dialog/oauth**',
|
|
621
|
+
apple: '**/appleid.apple.com/**',
|
|
622
|
+
microsoft: '**/login.microsoftonline.com/**',
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
const pattern = providerPatterns[provider];
|
|
626
|
+
if (!pattern) throw new Error(`Unknown provider: ${provider}`);
|
|
627
|
+
|
|
628
|
+
// Intercept the OAuth redirect
|
|
629
|
+
await page.route(pattern, async (route) => {
|
|
630
|
+
const url = new URL(route.request().url());
|
|
631
|
+
const redirectUri = url.searchParams.get('redirect_uri') || '/auth/callback';
|
|
632
|
+
await route.fulfill({
|
|
633
|
+
status: 302,
|
|
634
|
+
headers: { Location: `${redirectUri}?code=mock-${provider}-code` },
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
// Mock the token exchange
|
|
639
|
+
await page.route(`**/api/auth/${provider}/callback*`, async (route) => {
|
|
640
|
+
await route.fulfill({
|
|
641
|
+
status: 200,
|
|
642
|
+
contentType: 'application/json',
|
|
643
|
+
body: JSON.stringify({ token: `mock-${provider}-jwt`, user: userData }),
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
await use(mock);
|
|
649
|
+
},
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
// Usage in tests
|
|
653
|
+
test('login with multiple social providers', async ({ page, mockOAuth }) => {
|
|
654
|
+
// Test Google login
|
|
655
|
+
await mockOAuth('google', { name: 'Google User', email: 'user@gmail.com' });
|
|
656
|
+
await page.goto('/login');
|
|
657
|
+
await page.getByRole('button', { name: 'Sign in with Google' }).click();
|
|
658
|
+
await page.waitForURL('/dashboard');
|
|
659
|
+
await expect(page.getByText('Google User')).toBeVisible();
|
|
660
|
+
});
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
**JavaScript**
|
|
664
|
+
```javascript
|
|
665
|
+
const { test: base, expect } = require('@playwright/test');
|
|
666
|
+
|
|
667
|
+
const test = base.extend({
|
|
668
|
+
mockOAuth: async ({ page }, use) => {
|
|
669
|
+
const mock = async (provider, userData) => {
|
|
670
|
+
const patterns = {
|
|
671
|
+
google: '**/accounts.google.com/**',
|
|
672
|
+
github: '**/github.com/login/oauth/**',
|
|
673
|
+
facebook: '**/facebook.com/v*/dialog/oauth**',
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
await page.route(patterns[provider], async (route) => {
|
|
677
|
+
const url = new URL(route.request().url());
|
|
678
|
+
const redirectUri = url.searchParams.get('redirect_uri') || '/auth/callback';
|
|
679
|
+
await route.fulfill({
|
|
680
|
+
status: 302,
|
|
681
|
+
headers: { Location: `${redirectUri}?code=mock-${provider}-code` },
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
await page.route(`**/api/auth/${provider}/callback*`, async (route) => {
|
|
686
|
+
await route.fulfill({
|
|
687
|
+
status: 200,
|
|
688
|
+
contentType: 'application/json',
|
|
689
|
+
body: JSON.stringify({ token: `mock-${provider}-jwt`, user: userData }),
|
|
690
|
+
});
|
|
691
|
+
});
|
|
692
|
+
};
|
|
693
|
+
await use(mock);
|
|
694
|
+
},
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
test('login with Google', async ({ page, mockOAuth }) => {
|
|
698
|
+
await mockOAuth('google', { name: 'Google User', email: 'user@gmail.com' });
|
|
699
|
+
await page.goto('/login');
|
|
700
|
+
await page.getByRole('button', { name: 'Sign in with Google' }).click();
|
|
701
|
+
await page.waitForURL('/dashboard');
|
|
702
|
+
await expect(page.getByText('Google User')).toBeVisible();
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
module.exports = { test, expect };
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
## Decision Guide
|
|
709
|
+
|
|
710
|
+
| Integration | Mock Strategy | When to Use Real | When to Mock |
|
|
711
|
+
|---|---|---|---|
|
|
712
|
+
| OAuth (Google, GitHub, etc.) | Route intercept on provider URL + mock callback | Dedicated integration test with test accounts | All E2E feature tests that require login |
|
|
713
|
+
| Stripe/payment gateway | Use Stripe test mode with test cards OR mock API | Checkout flow integration tests | All non-payment E2E tests |
|
|
714
|
+
| Google Analytics / Segment | `route.abort()` to block entirely | Dedicated analytics verification tests | Every other test (block for speed) |
|
|
715
|
+
| Chat widgets (Intercom, Drift) | `route.abort()` to block, or interact via iframe | Tests specifically about chat functionality | Every other test (block for speed/stability) |
|
|
716
|
+
| Google Maps | Mock geocoding/places API responses | Tests verifying real map rendering | Tests that only need location data |
|
|
717
|
+
| reCAPTCHA | Google test keys (preferred) or mock `grecaptcha` | Never in automated tests | Always -- CAPTCHA cannot be solved by automation |
|
|
718
|
+
| Social login | Reusable OAuth mock fixture per provider | Periodic integration check | All E2E tests requiring auth |
|
|
719
|
+
| Email services (SendGrid, SES) | Mock API endpoints, verify calls were made | End-to-end email delivery tests (separate suite) | All tests that trigger emails |
|
|
720
|
+
|
|
721
|
+
## Anti-Patterns
|
|
722
|
+
|
|
723
|
+
| Don't Do This | Problem | Do This Instead |
|
|
724
|
+
|---|---|---|
|
|
725
|
+
| Using real OAuth provider accounts in tests | Rate limits, 2FA requirements, account lockouts, test flakiness | Mock the OAuth flow or use provider test modes |
|
|
726
|
+
| 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 |
|
|
727
|
+
| Solving reCAPTCHA with image recognition | Fragile, slow, violates CAPTCHA terms of service | Use Google's test keys or mock the `grecaptcha` object |
|
|
728
|
+
| Mocking your own application code instead of the third-party | Does not test your integration logic | Mock only the external boundary (API endpoints, scripts) |
|
|
729
|
+
| Hardcoding mock responses for every test | Duplicated mock setup, hard to maintain | Build reusable mock fixtures (see Social Login pattern) |
|
|
730
|
+
| Not testing error paths from third parties | Misses how your app handles OAuth failures, payment declines | Mock error responses: `{ error: 'access_denied' }`, declined cards |
|
|
731
|
+
| 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 |
|
|
732
|
+
| Trusting `networkidle` for third-party scripts | Third-party scripts may poll indefinitely | Wait for specific app-level indicators, not `networkidle` |
|
|
733
|
+
|
|
734
|
+
## Troubleshooting
|
|
735
|
+
|
|
736
|
+
| Symptom | Likely Cause | Fix |
|
|
737
|
+
|---|---|---|
|
|
738
|
+
| 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 |
|
|
739
|
+
| Stripe iframe not found | Stripe.js loads async; iframe name varies by version | Use a flexible selector: `iframe[name*="__privateStripeFrame"]` with a long timeout |
|
|
740
|
+
| reCAPTCHA mock does not work | Script loads before `addInitScript` runs | Use `route` to intercept the reCAPTCHA script and serve a mock version |
|
|
741
|
+
| 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(){}' })` |
|
|
742
|
+
| Chat widget iframe is not accessible | Cross-origin iframe restrictions | Use `frameLocator` with the correct iframe name/selector |
|
|
743
|
+
| 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 |
|
|
744
|
+
| 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 |
|
|
745
|
+
| 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 |
|
|
746
|
+
|
|
747
|
+
## Related
|
|
748
|
+
|
|
749
|
+
- [core/network-mocking.md](network-mocking.md) -- foundational route interception patterns
|
|
750
|
+
- [core/authentication.md](authentication.md) -- auth state management and login bypasses
|
|
751
|
+
- [core/when-to-mock.md](when-to-mock.md) -- decision framework for mocking vs real services
|
|
752
|
+
- [core/multi-context-and-popups.md](multi-context-and-popups.md) -- handling OAuth popup windows
|
|
753
|
+
- [core/security-testing.md](security-testing.md) -- testing auth security aspects
|
|
754
|
+
- [core/iframes-and-shadow-dom.md](iframes-and-shadow-dom.md) -- interacting with payment widget iframes
|