@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,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
|