@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,581 @@
1
+ # Multi-Context, Popups, and New Windows
2
+
3
+ > **When to use**: Handling popup windows, new tabs, OAuth authorization flows, payment gateway redirects, multi-tab coordination, and any scenario where your application opens additional browser windows or tabs.
4
+ > **Prerequisites**: [core/assertions-and-waiting.md](assertions-and-waiting.md), [core/fixtures-and-hooks.md](fixtures-and-hooks.md)
5
+
6
+ ## Quick Reference
7
+
8
+ ```typescript
9
+ // Catch a popup triggered by a click
10
+ const popupPromise = page.waitForEvent('popup');
11
+ await page.getByRole('button', { name: 'Open preview' }).click();
12
+ const popup = await popupPromise;
13
+ await popup.waitForLoadState();
14
+ await expect(popup.getByRole('heading')).toContainText('Preview');
15
+
16
+ // List all open pages in a context
17
+ const allPages = context.pages();
18
+ console.log(`Open tabs: ${allPages.length}`);
19
+ ```
20
+
21
+ **Key concept**: In Playwright, a "popup" is any new page opened by `window.open()`, `target="_blank"` links, or JavaScript-triggered new windows. They all fire the `popup` event on the originating page.
22
+
23
+ ## Patterns
24
+
25
+ ### Handling Basic Popups
26
+
27
+ **Use when**: A user action opens a new tab or window and you need to interact with it.
28
+ **Avoid when**: The "popup" is actually a modal dialog within the same page -- use `getByRole('dialog')` instead.
29
+
30
+ **TypeScript**
31
+ ```typescript
32
+ import { test, expect } from '@playwright/test';
33
+
34
+ test('handle popup opened by target="_blank" link', async ({ page }) => {
35
+ await page.goto('/help');
36
+
37
+ // Set up the popup listener BEFORE the action that triggers it
38
+ const popupPromise = page.waitForEvent('popup');
39
+ await page.getByRole('link', { name: 'Documentation' }).click();
40
+ const popup = await popupPromise;
41
+
42
+ // Wait for the popup to load
43
+ await popup.waitForLoadState();
44
+
45
+ // Interact with the popup
46
+ await expect(popup.getByRole('heading', { level: 1 })).toContainText('Documentation');
47
+ expect(popup.url()).toContain('/docs');
48
+
49
+ // Close the popup when done
50
+ await popup.close();
51
+ });
52
+
53
+ test('handle popup opened by window.open()', async ({ page }) => {
54
+ await page.goto('/reports');
55
+
56
+ const popupPromise = page.waitForEvent('popup');
57
+ await page.getByRole('button', { name: 'Print report' }).click();
58
+ const popup = await popupPromise;
59
+
60
+ await popup.waitForLoadState();
61
+ await expect(popup.getByTestId('print-preview')).toBeVisible();
62
+
63
+ // The original page is still accessible
64
+ await expect(page.getByRole('heading', { name: 'Reports' })).toBeVisible();
65
+ });
66
+ ```
67
+
68
+ **JavaScript**
69
+ ```javascript
70
+ const { test, expect } = require('@playwright/test');
71
+
72
+ test('handle popup opened by target="_blank" link', async ({ page }) => {
73
+ await page.goto('/help');
74
+
75
+ const popupPromise = page.waitForEvent('popup');
76
+ await page.getByRole('link', { name: 'Documentation' }).click();
77
+ const popup = await popupPromise;
78
+
79
+ await popup.waitForLoadState();
80
+ await expect(popup.getByRole('heading', { level: 1 })).toContainText('Documentation');
81
+
82
+ await popup.close();
83
+ });
84
+ ```
85
+
86
+ ### OAuth Popup Flows
87
+
88
+ **Use when**: Your app opens a third-party OAuth window (Google, GitHub, Microsoft, etc.) for authentication.
89
+ **Avoid when**: You can bypass OAuth entirely by injecting auth tokens directly -- see [core/authentication.md](authentication.md).
90
+
91
+ **TypeScript**
92
+ ```typescript
93
+ import { test, expect } from '@playwright/test';
94
+
95
+ test('Google OAuth popup flow', async ({ page }) => {
96
+ await page.goto('/login');
97
+
98
+ // Listen for the popup before clicking the OAuth button
99
+ const popupPromise = page.waitForEvent('popup');
100
+ await page.getByRole('button', { name: 'Sign in with Google' }).click();
101
+ const oauthPopup = await popupPromise;
102
+
103
+ // Wait for the OAuth provider page to load
104
+ await oauthPopup.waitForLoadState();
105
+ expect(oauthPopup.url()).toContain('accounts.google.com');
106
+
107
+ // Fill in credentials on the OAuth provider page
108
+ await oauthPopup.getByLabel('Email or phone').fill('testuser@gmail.com');
109
+ await oauthPopup.getByRole('button', { name: 'Next' }).click();
110
+ await oauthPopup.getByLabel('Enter your password').fill('test-password');
111
+ await oauthPopup.getByRole('button', { name: 'Next' }).click();
112
+
113
+ // The popup closes automatically after authorization
114
+ // Wait for the original page to receive the auth callback
115
+ await page.waitForURL('/dashboard');
116
+ await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
117
+ });
118
+
119
+ test('GitHub OAuth popup flow', async ({ page }) => {
120
+ await page.goto('/login');
121
+
122
+ const popupPromise = page.waitForEvent('popup');
123
+ await page.getByRole('button', { name: 'Sign in with GitHub' }).click();
124
+ const popup = await popupPromise;
125
+
126
+ await popup.waitForLoadState();
127
+ expect(popup.url()).toContain('github.com');
128
+
129
+ await popup.getByLabel('Username or email address').fill('testuser');
130
+ await popup.getByLabel('Password').fill('test-password');
131
+ await popup.getByRole('button', { name: 'Sign in' }).click();
132
+
133
+ // Authorize the app if prompted
134
+ const authorizeButton = popup.getByRole('button', { name: 'Authorize' });
135
+ if (await authorizeButton.isVisible({ timeout: 3000 }).catch(() => false)) {
136
+ await authorizeButton.click();
137
+ }
138
+
139
+ // Popup closes, original page redirects
140
+ await page.waitForURL('/dashboard');
141
+ await expect(page.getByText(/Welcome/)).toBeVisible();
142
+ });
143
+ ```
144
+
145
+ **JavaScript**
146
+ ```javascript
147
+ const { test, expect } = require('@playwright/test');
148
+
149
+ test('GitHub OAuth popup flow', async ({ page }) => {
150
+ await page.goto('/login');
151
+
152
+ const popupPromise = page.waitForEvent('popup');
153
+ await page.getByRole('button', { name: 'Sign in with GitHub' }).click();
154
+ const popup = await popupPromise;
155
+
156
+ await popup.waitForLoadState();
157
+ await popup.getByLabel('Username or email address').fill('testuser');
158
+ await popup.getByLabel('Password').fill('test-password');
159
+ await popup.getByRole('button', { name: 'Sign in' }).click();
160
+
161
+ await page.waitForURL('/dashboard');
162
+ await expect(page.getByText(/Welcome/)).toBeVisible();
163
+ });
164
+ ```
165
+
166
+ ### Payment Gateway Popups
167
+
168
+ **Use when**: Checkout flows open a payment provider in a new window (PayPal, 3D Secure verification).
169
+ **Avoid when**: The payment gateway loads in an iframe on the same page -- use `frameLocator` instead.
170
+
171
+ **TypeScript**
172
+ ```typescript
173
+ import { test, expect } from '@playwright/test';
174
+
175
+ test('PayPal popup checkout flow', async ({ page }) => {
176
+ await page.goto('/checkout');
177
+
178
+ await page.getByLabel('Email').fill('buyer@example.com');
179
+ await page.getByRole('button', { name: 'Proceed to payment' }).click();
180
+
181
+ // PayPal opens in a popup
182
+ const popupPromise = page.waitForEvent('popup');
183
+ await page.getByRole('button', { name: 'Pay with PayPal' }).click();
184
+ const paypalPopup = await popupPromise;
185
+
186
+ await paypalPopup.waitForLoadState();
187
+ expect(paypalPopup.url()).toContain('paypal.com');
188
+
189
+ // Complete PayPal flow
190
+ await paypalPopup.getByLabel('Email').fill('buyer@paypal-test.com');
191
+ await paypalPopup.getByRole('button', { name: 'Next' }).click();
192
+ await paypalPopup.getByLabel('Password').fill('test-password');
193
+ await paypalPopup.getByRole('button', { name: 'Log In' }).click();
194
+ await paypalPopup.getByRole('button', { name: 'Complete Purchase' }).click();
195
+
196
+ // Popup closes, return to original page
197
+ await page.waitForURL('/order/confirmation');
198
+ await expect(page.getByText('Payment successful')).toBeVisible();
199
+ });
200
+
201
+ test('3D Secure verification popup', async ({ page }) => {
202
+ await page.goto('/checkout');
203
+ await page.getByLabel('Card number').fill('4000000000003220'); // 3DS test card
204
+ await page.getByLabel('Expiry').fill('12/28');
205
+ await page.getByLabel('CVC').fill('123');
206
+ await page.getByRole('button', { name: 'Pay' }).click();
207
+
208
+ // 3DS challenge opens in popup or iframe -- handle both
209
+ const popupPromise = page.waitForEvent('popup', { timeout: 5000 }).catch(() => null);
210
+ const popup = await popupPromise;
211
+
212
+ if (popup) {
213
+ await popup.waitForLoadState();
214
+ await popup.getByRole('button', { name: 'Complete authentication' }).click();
215
+ } else {
216
+ // Fallback: 3DS in iframe
217
+ const frame = page.frameLocator('iframe[name*="challenge"]');
218
+ await frame.getByRole('button', { name: 'Complete authentication' }).click();
219
+ }
220
+
221
+ await expect(page.getByText('Payment successful')).toBeVisible();
222
+ });
223
+ ```
224
+
225
+ **JavaScript**
226
+ ```javascript
227
+ const { test, expect } = require('@playwright/test');
228
+
229
+ test('PayPal popup checkout flow', async ({ page }) => {
230
+ await page.goto('/checkout');
231
+ await page.getByRole('button', { name: 'Proceed to payment' }).click();
232
+
233
+ const popupPromise = page.waitForEvent('popup');
234
+ await page.getByRole('button', { name: 'Pay with PayPal' }).click();
235
+ const paypalPopup = await popupPromise;
236
+
237
+ await paypalPopup.waitForLoadState();
238
+ await paypalPopup.getByLabel('Email').fill('buyer@paypal-test.com');
239
+ await paypalPopup.getByRole('button', { name: 'Next' }).click();
240
+ await paypalPopup.getByLabel('Password').fill('test-password');
241
+ await paypalPopup.getByRole('button', { name: 'Log In' }).click();
242
+ await paypalPopup.getByRole('button', { name: 'Complete Purchase' }).click();
243
+
244
+ await page.waitForURL('/order/confirmation');
245
+ await expect(page.getByText('Payment successful')).toBeVisible();
246
+ });
247
+ ```
248
+
249
+ ### Multi-Tab Coordination
250
+
251
+ **Use when**: Testing scenarios where multiple tabs share state -- real-time collaboration, shopping cart sync, or session management across tabs.
252
+ **Avoid when**: Each tab is independent and can be tested in separate test cases.
253
+
254
+ **TypeScript**
255
+ ```typescript
256
+ import { test, expect } from '@playwright/test';
257
+
258
+ test('cart updates reflect across tabs', async ({ context }) => {
259
+ // Open two tabs in the same context (shared cookies/storage)
260
+ const page1 = await context.newPage();
261
+ const page2 = await context.newPage();
262
+
263
+ await page1.goto('/products');
264
+ await page2.goto('/cart');
265
+
266
+ // Add item in tab 1
267
+ await page1.getByRole('button', { name: 'Add to cart' }).first().click();
268
+
269
+ // Tab 2 should reflect the update (via WebSocket, polling, or storage event)
270
+ await expect(page2.getByTestId('cart-count')).toHaveText('1', { timeout: 5000 });
271
+ });
272
+
273
+ test('logout in one tab logs out all tabs', async ({ context }) => {
274
+ const page1 = await context.newPage();
275
+ const page2 = await context.newPage();
276
+
277
+ // Both tabs are on authenticated pages
278
+ await page1.goto('/dashboard');
279
+ await page2.goto('/settings');
280
+
281
+ // Log out from tab 1
282
+ await page1.getByRole('button', { name: 'Log out' }).click();
283
+ await page1.waitForURL('/login');
284
+
285
+ // Tab 2 should redirect to login on next action or automatically
286
+ await page2.reload();
287
+ expect(page2.url()).toContain('/login');
288
+ });
289
+
290
+ test('manage multiple tabs with context.pages()', async ({ context, page }) => {
291
+ await page.goto('/dashboard');
292
+
293
+ // Open several new tabs
294
+ const popupPromise1 = page.waitForEvent('popup');
295
+ await page.getByRole('link', { name: 'Report A' }).click();
296
+ const tab1 = await popupPromise1;
297
+
298
+ const popupPromise2 = page.waitForEvent('popup');
299
+ await page.getByRole('link', { name: 'Report B' }).click();
300
+ const tab2 = await popupPromise2;
301
+
302
+ // List all pages in this context
303
+ const allPages = context.pages();
304
+ expect(allPages).toHaveLength(3); // original + 2 popups
305
+
306
+ // Interact with specific tabs
307
+ await tab1.waitForLoadState();
308
+ await expect(tab1.getByRole('heading')).toContainText('Report A');
309
+
310
+ await tab2.waitForLoadState();
311
+ await expect(tab2.getByRole('heading')).toContainText('Report B');
312
+
313
+ // Close tabs when done
314
+ await tab2.close();
315
+ await tab1.close();
316
+ expect(context.pages()).toHaveLength(1);
317
+ });
318
+ ```
319
+
320
+ **JavaScript**
321
+ ```javascript
322
+ const { test, expect } = require('@playwright/test');
323
+
324
+ test('cart updates reflect across tabs', async ({ context }) => {
325
+ const page1 = await context.newPage();
326
+ const page2 = await context.newPage();
327
+
328
+ await page1.goto('/products');
329
+ await page2.goto('/cart');
330
+
331
+ await page1.getByRole('button', { name: 'Add to cart' }).first().click();
332
+ await expect(page2.getByTestId('cart-count')).toHaveText('1', { timeout: 5000 });
333
+ });
334
+
335
+ test('manage tabs with context.pages()', async ({ context, page }) => {
336
+ await page.goto('/dashboard');
337
+
338
+ const popupPromise = page.waitForEvent('popup');
339
+ await page.getByRole('link', { name: 'Report A' }).click();
340
+ const newTab = await popupPromise;
341
+
342
+ expect(context.pages()).toHaveLength(2);
343
+ await newTab.close();
344
+ expect(context.pages()).toHaveLength(1);
345
+ });
346
+ ```
347
+
348
+ ### Download Triggers in New Tabs
349
+
350
+ **Use when**: A download is triggered by opening a new tab (e.g., PDF generation that opens in a new window before downloading).
351
+ **Avoid when**: The download starts directly without opening a new tab -- use `page.waitForEvent('download')` directly.
352
+
353
+ **TypeScript**
354
+ ```typescript
355
+ import { test, expect } from '@playwright/test';
356
+
357
+ test('PDF download from new tab', async ({ page }) => {
358
+ await page.goto('/invoices');
359
+
360
+ // Some apps open the PDF in a new tab, which then triggers a download
361
+ const popupPromise = page.waitForEvent('popup');
362
+ await page.getByRole('link', { name: 'Download Invoice #123' }).click();
363
+ const popup = await popupPromise;
364
+
365
+ // The popup may directly trigger a download
366
+ const downloadPromise = popup.waitForEvent('download');
367
+ const download = await downloadPromise;
368
+
369
+ expect(download.suggestedFilename()).toContain('invoice');
370
+ const path = await download.path();
371
+ expect(path).toBeTruthy();
372
+
373
+ await popup.close();
374
+ });
375
+
376
+ test('export opens in new tab then auto-downloads', async ({ page }) => {
377
+ await page.goto('/reports');
378
+
379
+ // Handle both: popup that downloads AND popup that shows content
380
+ const popupPromise = page.waitForEvent('popup');
381
+ await page.getByRole('button', { name: 'Export CSV' }).click();
382
+ const popup = await popupPromise;
383
+
384
+ // Try to catch a download; if no download, the popup has the content
385
+ try {
386
+ const download = await popup.waitForEvent('download', { timeout: 5000 });
387
+ const filename = download.suggestedFilename();
388
+ expect(filename).toMatch(/\.csv$/);
389
+ await download.saveAs(`./downloads/${filename}`);
390
+ } catch {
391
+ // No download event — content displayed in the popup
392
+ const content = await popup.textContent('body');
393
+ expect(content).toContain('Report Data');
394
+ }
395
+
396
+ await popup.close();
397
+ });
398
+ ```
399
+
400
+ **JavaScript**
401
+ ```javascript
402
+ const { test, expect } = require('@playwright/test');
403
+
404
+ test('PDF download from new tab', async ({ page }) => {
405
+ await page.goto('/invoices');
406
+
407
+ const popupPromise = page.waitForEvent('popup');
408
+ await page.getByRole('link', { name: 'Download Invoice #123' }).click();
409
+ const popup = await popupPromise;
410
+
411
+ const downloadPromise = popup.waitForEvent('download');
412
+ const download = await downloadPromise;
413
+
414
+ expect(download.suggestedFilename()).toContain('invoice');
415
+ await popup.close();
416
+ });
417
+ ```
418
+
419
+ ### Multi-Context for Isolated Sessions
420
+
421
+ **Use when**: Testing interactions between different users (e.g., admin and regular user, two chat participants).
422
+ **Avoid when**: You only need a single user perspective -- a single context is sufficient.
423
+
424
+ **TypeScript**
425
+ ```typescript
426
+ import { test, expect } from '@playwright/test';
427
+
428
+ test('admin sees user changes in real-time', async ({ browser }) => {
429
+ // Create separate contexts for two users (separate sessions)
430
+ const adminContext = await browser.newContext();
431
+ const userContext = await browser.newContext();
432
+
433
+ const adminPage = await adminContext.newPage();
434
+ const userPage = await userContext.newPage();
435
+
436
+ // Admin logs in and watches the user list
437
+ await adminPage.goto('/admin/users');
438
+
439
+ // User signs up
440
+ await userPage.goto('/register');
441
+ await userPage.getByLabel('Name').fill('New User');
442
+ await userPage.getByLabel('Email').fill('newuser@example.com');
443
+ await userPage.getByLabel('Password').fill('password123');
444
+ await userPage.getByRole('button', { name: 'Register' }).click();
445
+
446
+ // Admin should see the new user appear
447
+ await expect(adminPage.getByText('newuser@example.com')).toBeVisible({ timeout: 10000 });
448
+
449
+ await adminContext.close();
450
+ await userContext.close();
451
+ });
452
+ ```
453
+
454
+ **JavaScript**
455
+ ```javascript
456
+ const { test, expect } = require('@playwright/test');
457
+
458
+ test('admin sees user changes in real-time', async ({ browser }) => {
459
+ const adminContext = await browser.newContext();
460
+ const userContext = await browser.newContext();
461
+
462
+ const adminPage = await adminContext.newPage();
463
+ const userPage = await userContext.newPage();
464
+
465
+ await adminPage.goto('/admin/users');
466
+
467
+ await userPage.goto('/register');
468
+ await userPage.getByLabel('Name').fill('New User');
469
+ await userPage.getByLabel('Email').fill('newuser@example.com');
470
+ await userPage.getByLabel('Password').fill('password123');
471
+ await userPage.getByRole('button', { name: 'Register' }).click();
472
+
473
+ await expect(adminPage.getByText('newuser@example.com')).toBeVisible({ timeout: 10000 });
474
+
475
+ await adminContext.close();
476
+ await userContext.close();
477
+ });
478
+ ```
479
+
480
+ ### Observing New Contexts and Mirrored Events (Playwright 1.60+)
481
+
482
+ **Use when**: You're orchestrating many contexts/pages (multi-user suites, agent-driven sessions) and want a single place to react when a new context is created, or to listen for page lifecycle events at the context level rather than wiring a listener onto every page.
483
+ **Avoid when**: You have one context and one page — listen on the `page` directly.
484
+
485
+ Playwright 1.60 adds `browser.on('context')`, fired whenever a new context is created, and makes `BrowserContext` mirror its pages' lifecycle events (`download`, frame events, page close/load). This means you can attach logging or diagnostics once at the browser/context level.
486
+
487
+ **TypeScript**
488
+ ```typescript
489
+ import { test, expect } from '@playwright/test';
490
+
491
+ test('log every context and download across the browser', async ({ browser }) => {
492
+ // Fires for each new context created on this browser (1.60+)
493
+ browser.on('context', (context) => {
494
+ console.log('New context created');
495
+
496
+ // Context mirrors page-level events — no per-page wiring needed (1.60+)
497
+ context.on('download', (download) => {
498
+ console.log('Download started:', download.suggestedFilename());
499
+ });
500
+ });
501
+
502
+ const userA = await browser.newContext();
503
+ const userB = await browser.newContext();
504
+
505
+ const pageA = await userA.newPage();
506
+ await pageA.goto('/reports');
507
+ await pageA.getByRole('button', { name: 'Export CSV' }).click();
508
+
509
+ await userA.close();
510
+ await userB.close();
511
+ });
512
+ ```
513
+
514
+ **JavaScript**
515
+ ```javascript
516
+ const { test } = require('@playwright/test');
517
+
518
+ test('log every context and download across the browser', async ({ browser }) => {
519
+ browser.on('context', (context) => {
520
+ context.on('download', (download) => {
521
+ console.log('Download started:', download.suggestedFilename());
522
+ });
523
+ });
524
+
525
+ const ctx = await browser.newContext();
526
+ const page = await ctx.newPage();
527
+ await page.goto('/reports');
528
+ await page.getByRole('button', { name: 'Export CSV' }).click();
529
+ await ctx.close();
530
+ });
531
+ ```
532
+
533
+ > Context-level mirrored events are for cross-cutting concerns (logging, artifacts, counters). For assertions tied to a specific page, still listen on that `page`.
534
+
535
+ ## Decision Guide
536
+
537
+ | Scenario | Approach | Why |
538
+ |---|---|---|
539
+ | `target="_blank"` link | `page.waitForEvent('popup')` | Playwright fires `popup` for all new windows/tabs |
540
+ | `window.open()` call | `page.waitForEvent('popup')` | Same mechanism regardless of how the window opens |
541
+ | OAuth login popup | `waitForEvent('popup')` + interact + wait for close | OAuth providers redirect back and close the popup |
542
+ | Payment popup (PayPal) | `waitForEvent('popup')` + complete flow | Same as OAuth but with payment-specific UI |
543
+ | Download in new tab | `popup.waitForEvent('download')` | Catch the download event on the popup page |
544
+ | Multiple tabs, same user | Open pages in the same `context` | Shared cookies, localStorage, session |
545
+ | Multiple users (separate sessions) | Create separate `browser.newContext()` per user | Isolated cookies, storage, auth state |
546
+ | Tab sync testing | Multiple `context.newPage()` + assert shared state | Tests real-time state synchronization |
547
+ | Popup that may or may not appear | `waitForEvent('popup', { timeout })` with try/catch | Graceful handling of conditional popups |
548
+
549
+ ## Anti-Patterns
550
+
551
+ | Don't Do This | Problem | Do This Instead |
552
+ |---|---|---|
553
+ | Clicking then calling `waitForEvent('popup')` | Popup may open before listener is registered (race condition) | Set up `waitForEvent` BEFORE the click |
554
+ | Using `context.pages()[1]` to get a popup | Index order is not guaranteed; brittle | Use `waitForEvent('popup')` which returns the exact page |
555
+ | Forgetting `popup.waitForLoadState()` | Popup page may not be loaded when you interact with it | Always call `waitForLoadState()` after receiving the popup |
556
+ | Not closing popups after test | Leaked pages consume memory and may affect subsequent tests | Close popups with `popup.close()` in the test or use fixtures |
557
+ | Using separate `browser.newContext()` when tabs should share state | Separate contexts have separate cookies/sessions | Use `context.newPage()` for tabs in the same session |
558
+ | Using `context.newPage()` for isolated users | Pages in the same context share state | Use `browser.newContext()` for separate user sessions |
559
+ | `page.waitForTimeout()` after popup trigger | Popup timing is unpredictable | Use `page.waitForEvent('popup')` which resolves when the popup opens |
560
+ | Catching popup on the wrong page | Popup event fires on the page that triggered it | Listen for `popup` on the page where the click/action happens |
561
+
562
+ ## Troubleshooting
563
+
564
+ | Symptom | Likely Cause | Fix |
565
+ |---|---|---|
566
+ | `waitForEvent('popup')` times out | The action did not open a new window; it navigated in the same tab | Check if the link has `target="_blank"` or if `window.open` is called |
567
+ | Popup opens but is blank | Popup is still loading; you interacted too early | Add `await popup.waitForLoadState()` before interacting |
568
+ | Popup URL is `about:blank` | Popup is created empty, then navigated by JavaScript | Wait for `popup.waitForURL()` with the expected URL pattern |
569
+ | OAuth popup is blocked by browser | Pop-up blocker is active | Playwright disables pop-up blocking by default; check if a browser arg re-enables it |
570
+ | Two popups open but only one is caught | `waitForEvent` resolves for the first event only | Use `page.on('popup', callback)` to catch all popups, or chain two `waitForEvent` calls |
571
+ | `context.pages()` returns unexpected count | Previous test left pages open | Close all extra pages in `afterEach` or use per-test contexts |
572
+ | Popup closes before interaction completes | The app or OAuth provider auto-closes after timeout | Increase test speed or remove `slowMo`; interact with the popup immediately |
573
+ | Cross-origin popup interaction fails | Popup navigated to a different origin | Playwright handles cross-origin popups; ensure you are not setting `--disable-web-security` |
574
+
575
+ ## Related
576
+
577
+ - [core/authentication.md](authentication.md) -- bypassing OAuth popups with stored auth state
578
+ - [core/multi-user-and-collaboration.md](multi-user-and-collaboration.md) -- multi-user real-time collaboration testing
579
+ - [core/file-operations.md](file-operations.md) -- file download handling without popups
580
+ - [core/third-party-integrations.md](third-party-integrations.md) -- mocking OAuth providers to avoid real popups
581
+ - [core/fixtures-and-hooks.md](fixtures-and-hooks.md) -- fixtures for multi-context setups