@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.
Files changed (89) hide show
  1. package/README.md +293 -0
  2. package/extensions/dino.ts +70 -0
  3. package/extensions/mabl-mcp.ts +306 -0
  4. package/extensions/playwright-mcp.ts +142 -0
  5. package/package.json +47 -0
  6. package/prompts/create-pr.md +110 -0
  7. package/prompts/generate-bdd-from-url.md +65 -0
  8. package/prompts/generate-pom-from-url-cli.md +48 -0
  9. package/prompts/generate-pom-from-url.md +45 -0
  10. package/prompts/generate-spec-from-feature.md +64 -0
  11. package/prompts/heal-playwright-failures.md +106 -0
  12. package/prompts/setup-playwright.md +81 -0
  13. package/scripts/start-dino.cmd +8 -0
  14. package/scripts/start-dino.mjs +226 -0
  15. package/scripts/start-dino.ps1 +9 -0
  16. package/scripts/start-dino.sh +10 -0
  17. package/skills/acli/SKILL.md +292 -0
  18. package/skills/bdd-gherkin/SKILL.md +552 -0
  19. package/skills/build-review-interface/SKILL.md +96 -0
  20. package/skills/error-analysis/SKILL.md +164 -0
  21. package/skills/eval-audit/SKILL.md +183 -0
  22. package/skills/evaluate-rag/SKILL.md +177 -0
  23. package/skills/generate-synthetic-data/SKILL.md +131 -0
  24. package/skills/mabl-mcp/SKILL.md +79 -0
  25. package/skills/playwright-cli/SKILL.md +246 -0
  26. package/skills/playwright-cli/advanced-workflows.md +679 -0
  27. package/skills/playwright-cli/core-commands.md +375 -0
  28. package/skills/playwright-cli/device-emulation.md +475 -0
  29. package/skills/playwright-cli/request-mocking.md +373 -0
  30. package/skills/playwright-cli/running-custom-code.md +553 -0
  31. package/skills/playwright-cli/screenshots-and-media.md +457 -0
  32. package/skills/playwright-cli/session-management.md +425 -0
  33. package/skills/playwright-cli/storage-and-auth.md +487 -0
  34. package/skills/playwright-cli/test-generation.md +336 -0
  35. package/skills/playwright-cli/tracing-and-debugging.md +501 -0
  36. package/skills/playwright-core/SKILL.md +101 -0
  37. package/skills/playwright-core/accessibility.md +1517 -0
  38. package/skills/playwright-core/angular.md +1086 -0
  39. package/skills/playwright-core/api-testing.md +1623 -0
  40. package/skills/playwright-core/assertions-and-waiting.md +703 -0
  41. package/skills/playwright-core/auth-flows.md +1081 -0
  42. package/skills/playwright-core/authentication.md +1409 -0
  43. package/skills/playwright-core/browser-apis.md +673 -0
  44. package/skills/playwright-core/browser-extensions.md +319 -0
  45. package/skills/playwright-core/canvas-and-webgl.md +494 -0
  46. package/skills/playwright-core/clock-and-time-mocking.md +427 -0
  47. package/skills/playwright-core/common-pitfalls.md +1318 -0
  48. package/skills/playwright-core/component-testing.md +1179 -0
  49. package/skills/playwright-core/configuration.md +731 -0
  50. package/skills/playwright-core/crud-testing.md +945 -0
  51. package/skills/playwright-core/debugging.md +827 -0
  52. package/skills/playwright-core/drag-and-drop.md +992 -0
  53. package/skills/playwright-core/electron-testing.md +622 -0
  54. package/skills/playwright-core/error-and-edge-cases.md +1181 -0
  55. package/skills/playwright-core/error-index.md +1903 -0
  56. package/skills/playwright-core/file-operations.md +749 -0
  57. package/skills/playwright-core/file-upload-download.md +1012 -0
  58. package/skills/playwright-core/fixtures-and-hooks.md +1072 -0
  59. package/skills/playwright-core/flaky-tests.md +883 -0
  60. package/skills/playwright-core/forms-and-validation.md +1056 -0
  61. package/skills/playwright-core/i18n-and-localization.md +622 -0
  62. package/skills/playwright-core/iframes-and-shadow-dom.md +488 -0
  63. package/skills/playwright-core/locator-strategy.md +588 -0
  64. package/skills/playwright-core/locators.md +743 -0
  65. package/skills/playwright-core/mobile-and-responsive.md +1669 -0
  66. package/skills/playwright-core/multi-context-and-popups.md +581 -0
  67. package/skills/playwright-core/multi-user-and-collaboration.md +477 -0
  68. package/skills/playwright-core/network-mocking.md +1395 -0
  69. package/skills/playwright-core/nextjs.md +1013 -0
  70. package/skills/playwright-core/performance-testing.md +608 -0
  71. package/skills/playwright-core/react.md +1082 -0
  72. package/skills/playwright-core/search-and-filter.md +1366 -0
  73. package/skills/playwright-core/security-testing.md +624 -0
  74. package/skills/playwright-core/service-workers-and-pwa.md +524 -0
  75. package/skills/playwright-core/test-architecture.md +569 -0
  76. package/skills/playwright-core/test-data-management.md +1209 -0
  77. package/skills/playwright-core/test-organization.md +947 -0
  78. package/skills/playwright-core/third-party-integrations.md +754 -0
  79. package/skills/playwright-core/trace-analysis.md +161 -0
  80. package/skills/playwright-core/visual-regression.md +1027 -0
  81. package/skills/playwright-core/vue.md +1162 -0
  82. package/skills/playwright-core/websockets-and-realtime.md +623 -0
  83. package/skills/playwright-core/when-to-mock.md +827 -0
  84. package/skills/playwright-pom/SKILL.md +94 -0
  85. package/skills/playwright-pom/page-object-model.md +921 -0
  86. package/skills/playwright-pom/pom-vs-fixtures-vs-helpers.md +937 -0
  87. package/skills/validate-evaluator/SKILL.md +215 -0
  88. package/skills/write-judge-prompt/SKILL.md +144 -0
  89. 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