@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,673 @@
|
|
|
1
|
+
# Browser APIs
|
|
2
|
+
|
|
3
|
+
> **When to use**: When testing features that depend on browser-native APIs -- geolocation, permissions, clipboard, notifications, camera/microphone, localStorage, sessionStorage, IndexedDB.
|
|
4
|
+
> **Prerequisites**: [core/configuration.md](configuration.md), [core/fixtures-and-hooks.md](fixtures-and-hooks.md)
|
|
5
|
+
|
|
6
|
+
## Quick Reference
|
|
7
|
+
|
|
8
|
+
```typescript
|
|
9
|
+
// Geolocation — set via context options
|
|
10
|
+
const context = await browser.newContext({
|
|
11
|
+
geolocation: { latitude: 40.7128, longitude: -74.0060 },
|
|
12
|
+
permissions: ['geolocation'],
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Permissions — grant at context level
|
|
16
|
+
const context = await browser.newContext({
|
|
17
|
+
permissions: ['clipboard-read', 'clipboard-write', 'notifications'],
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// localStorage / sessionStorage — access via page.evaluate
|
|
21
|
+
await page.evaluate(() => localStorage.setItem('theme', 'dark'));
|
|
22
|
+
const value = await page.evaluate(() => localStorage.getItem('theme'));
|
|
23
|
+
|
|
24
|
+
// Clipboard — read/write in page context
|
|
25
|
+
await page.evaluate(() => navigator.clipboard.writeText('copied text'));
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Patterns
|
|
29
|
+
|
|
30
|
+
### Geolocation
|
|
31
|
+
|
|
32
|
+
**Use when**: Your app uses `navigator.geolocation` for maps, store locators, delivery tracking, or location-based features.
|
|
33
|
+
**Avoid when**: Your app never reads the user's location.
|
|
34
|
+
|
|
35
|
+
Geolocation is set at the context level. You can also update it mid-test with `context.setGeolocation()`.
|
|
36
|
+
|
|
37
|
+
**TypeScript**
|
|
38
|
+
```typescript
|
|
39
|
+
import { test, expect } from '@playwright/test';
|
|
40
|
+
|
|
41
|
+
test('shows nearest store based on geolocation', async ({ browser }) => {
|
|
42
|
+
const context = await browser.newContext({
|
|
43
|
+
geolocation: { latitude: 40.7128, longitude: -74.0060 }, // New York
|
|
44
|
+
permissions: ['geolocation'],
|
|
45
|
+
});
|
|
46
|
+
const page = await context.newPage();
|
|
47
|
+
|
|
48
|
+
await page.goto('/store-locator');
|
|
49
|
+
await page.getByRole('button', { name: 'Find nearby stores' }).click();
|
|
50
|
+
|
|
51
|
+
await expect(page.getByText('Manhattan Store')).toBeVisible();
|
|
52
|
+
await context.close();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('update location mid-test for moving user', async ({ browser }) => {
|
|
56
|
+
const context = await browser.newContext({
|
|
57
|
+
geolocation: { latitude: 37.7749, longitude: -122.4194 }, // San Francisco
|
|
58
|
+
permissions: ['geolocation'],
|
|
59
|
+
});
|
|
60
|
+
const page = await context.newPage();
|
|
61
|
+
|
|
62
|
+
await page.goto('/delivery-tracker');
|
|
63
|
+
await expect(page.getByTestId('current-city')).toHaveText('San Francisco');
|
|
64
|
+
|
|
65
|
+
// Simulate user moving to a new location
|
|
66
|
+
await context.setGeolocation({ latitude: 34.0522, longitude: -118.2437 }); // Los Angeles
|
|
67
|
+
|
|
68
|
+
// Trigger location refresh
|
|
69
|
+
await page.getByRole('button', { name: 'Update location' }).click();
|
|
70
|
+
await expect(page.getByTestId('current-city')).toHaveText('Los Angeles');
|
|
71
|
+
|
|
72
|
+
await context.close();
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**JavaScript**
|
|
77
|
+
```javascript
|
|
78
|
+
const { test, expect } = require('@playwright/test');
|
|
79
|
+
|
|
80
|
+
test('shows nearest store based on geolocation', async ({ browser }) => {
|
|
81
|
+
const context = await browser.newContext({
|
|
82
|
+
geolocation: { latitude: 40.7128, longitude: -74.0060 },
|
|
83
|
+
permissions: ['geolocation'],
|
|
84
|
+
});
|
|
85
|
+
const page = await context.newPage();
|
|
86
|
+
|
|
87
|
+
await page.goto('/store-locator');
|
|
88
|
+
await page.getByRole('button', { name: 'Find nearby stores' }).click();
|
|
89
|
+
|
|
90
|
+
await expect(page.getByText('Manhattan Store')).toBeVisible();
|
|
91
|
+
await context.close();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('update location mid-test', async ({ browser }) => {
|
|
95
|
+
const context = await browser.newContext({
|
|
96
|
+
geolocation: { latitude: 37.7749, longitude: -122.4194 },
|
|
97
|
+
permissions: ['geolocation'],
|
|
98
|
+
});
|
|
99
|
+
const page = await context.newPage();
|
|
100
|
+
|
|
101
|
+
await page.goto('/delivery-tracker');
|
|
102
|
+
await expect(page.getByTestId('current-city')).toHaveText('San Francisco');
|
|
103
|
+
|
|
104
|
+
await context.setGeolocation({ latitude: 34.0522, longitude: -118.2437 });
|
|
105
|
+
await page.getByRole('button', { name: 'Update location' }).click();
|
|
106
|
+
await expect(page.getByTestId('current-city')).toHaveText('Los Angeles');
|
|
107
|
+
|
|
108
|
+
await context.close();
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
You can also set geolocation globally in `playwright.config`:
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
// playwright.config.ts
|
|
116
|
+
import { defineConfig } from '@playwright/test';
|
|
117
|
+
|
|
118
|
+
export default defineConfig({
|
|
119
|
+
use: {
|
|
120
|
+
geolocation: { latitude: 51.5074, longitude: -0.1278 }, // London
|
|
121
|
+
permissions: ['geolocation'],
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Permissions
|
|
127
|
+
|
|
128
|
+
**Use when**: Your app requests browser permissions -- notifications, camera, microphone, geolocation, clipboard.
|
|
129
|
+
**Avoid when**: Your app does not use the Permissions API.
|
|
130
|
+
|
|
131
|
+
Grant permissions at context creation. Playwright does not show permission dialogs; you pre-grant or deny them.
|
|
132
|
+
|
|
133
|
+
**TypeScript**
|
|
134
|
+
```typescript
|
|
135
|
+
import { test, expect } from '@playwright/test';
|
|
136
|
+
|
|
137
|
+
test('notification permission granted shows notification UI', async ({ browser }) => {
|
|
138
|
+
const context = await browser.newContext({
|
|
139
|
+
permissions: ['notifications'],
|
|
140
|
+
});
|
|
141
|
+
const page = await context.newPage();
|
|
142
|
+
|
|
143
|
+
await page.goto('/settings/notifications');
|
|
144
|
+
|
|
145
|
+
// App checks Notification.permission and shows the toggle
|
|
146
|
+
await expect(page.getByRole('switch', { name: 'Enable notifications' })).toBeEnabled();
|
|
147
|
+
|
|
148
|
+
await context.close();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('notification permission denied shows upgrade prompt', async ({ browser }) => {
|
|
152
|
+
// No 'notifications' in permissions = denied
|
|
153
|
+
const context = await browser.newContext({
|
|
154
|
+
permissions: [],
|
|
155
|
+
});
|
|
156
|
+
const page = await context.newPage();
|
|
157
|
+
|
|
158
|
+
await page.goto('/settings/notifications');
|
|
159
|
+
|
|
160
|
+
await expect(page.getByText('Notifications are blocked')).toBeVisible();
|
|
161
|
+
await context.close();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('grant permissions mid-test', async ({ browser }) => {
|
|
165
|
+
const context = await browser.newContext();
|
|
166
|
+
const page = await context.newPage();
|
|
167
|
+
await page.goto('/camera-app');
|
|
168
|
+
|
|
169
|
+
// Initially no camera permission
|
|
170
|
+
await expect(page.getByText('Camera access needed')).toBeVisible();
|
|
171
|
+
|
|
172
|
+
// Grant permission dynamically
|
|
173
|
+
await context.grantPermissions(['camera'], { origin: 'https://localhost:3000' });
|
|
174
|
+
|
|
175
|
+
await page.getByRole('button', { name: 'Enable camera' }).click();
|
|
176
|
+
await expect(page.getByTestId('camera-preview')).toBeVisible();
|
|
177
|
+
|
|
178
|
+
// Revoke all permissions
|
|
179
|
+
await context.clearPermissions();
|
|
180
|
+
await context.close();
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**JavaScript**
|
|
185
|
+
```javascript
|
|
186
|
+
const { test, expect } = require('@playwright/test');
|
|
187
|
+
|
|
188
|
+
test('notification permission granted shows notification UI', async ({ browser }) => {
|
|
189
|
+
const context = await browser.newContext({
|
|
190
|
+
permissions: ['notifications'],
|
|
191
|
+
});
|
|
192
|
+
const page = await context.newPage();
|
|
193
|
+
|
|
194
|
+
await page.goto('/settings/notifications');
|
|
195
|
+
await expect(page.getByRole('switch', { name: 'Enable notifications' })).toBeEnabled();
|
|
196
|
+
await context.close();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('grant permissions mid-test', async ({ browser }) => {
|
|
200
|
+
const context = await browser.newContext();
|
|
201
|
+
const page = await context.newPage();
|
|
202
|
+
await page.goto('/camera-app');
|
|
203
|
+
|
|
204
|
+
await context.grantPermissions(['camera'], { origin: 'https://localhost:3000' });
|
|
205
|
+
await page.getByRole('button', { name: 'Enable camera' }).click();
|
|
206
|
+
await expect(page.getByTestId('camera-preview')).toBeVisible();
|
|
207
|
+
|
|
208
|
+
await context.clearPermissions();
|
|
209
|
+
await context.close();
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Clipboard API
|
|
214
|
+
|
|
215
|
+
**Use when**: Testing copy/paste functionality, "Copy to clipboard" buttons, or paste-from-clipboard features.
|
|
216
|
+
**Avoid when**: Your app does not interact with the clipboard.
|
|
217
|
+
|
|
218
|
+
**TypeScript**
|
|
219
|
+
```typescript
|
|
220
|
+
import { test, expect } from '@playwright/test';
|
|
221
|
+
|
|
222
|
+
test('copy button puts text on clipboard', async ({ browser }) => {
|
|
223
|
+
const context = await browser.newContext({
|
|
224
|
+
permissions: ['clipboard-read', 'clipboard-write'],
|
|
225
|
+
});
|
|
226
|
+
const page = await context.newPage();
|
|
227
|
+
|
|
228
|
+
await page.goto('/share');
|
|
229
|
+
await page.getByRole('button', { name: 'Copy link' }).click();
|
|
230
|
+
|
|
231
|
+
// Read clipboard content
|
|
232
|
+
const clipboardText = await page.evaluate(() => navigator.clipboard.readText());
|
|
233
|
+
expect(clipboardText).toContain('https://example.com/share/');
|
|
234
|
+
|
|
235
|
+
await context.close();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('paste from clipboard into editor', async ({ browser }) => {
|
|
239
|
+
const context = await browser.newContext({
|
|
240
|
+
permissions: ['clipboard-read', 'clipboard-write'],
|
|
241
|
+
});
|
|
242
|
+
const page = await context.newPage();
|
|
243
|
+
|
|
244
|
+
await page.goto('/editor');
|
|
245
|
+
|
|
246
|
+
// Write to clipboard programmatically
|
|
247
|
+
await page.evaluate(() => navigator.clipboard.writeText('Pasted content from clipboard'));
|
|
248
|
+
|
|
249
|
+
// Focus the editor and trigger paste
|
|
250
|
+
const editor = page.getByRole('textbox', { name: 'Editor' });
|
|
251
|
+
await editor.focus();
|
|
252
|
+
await page.keyboard.press('ControlOrMeta+v');
|
|
253
|
+
|
|
254
|
+
await expect(editor).toContainText('Pasted content from clipboard');
|
|
255
|
+
|
|
256
|
+
await context.close();
|
|
257
|
+
});
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
**JavaScript**
|
|
261
|
+
```javascript
|
|
262
|
+
const { test, expect } = require('@playwright/test');
|
|
263
|
+
|
|
264
|
+
test('copy button puts text on clipboard', async ({ browser }) => {
|
|
265
|
+
const context = await browser.newContext({
|
|
266
|
+
permissions: ['clipboard-read', 'clipboard-write'],
|
|
267
|
+
});
|
|
268
|
+
const page = await context.newPage();
|
|
269
|
+
|
|
270
|
+
await page.goto('/share');
|
|
271
|
+
await page.getByRole('button', { name: 'Copy link' }).click();
|
|
272
|
+
|
|
273
|
+
const clipboardText = await page.evaluate(() => navigator.clipboard.readText());
|
|
274
|
+
expect(clipboardText).toContain('https://example.com/share/');
|
|
275
|
+
await context.close();
|
|
276
|
+
});
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Camera and Microphone Mocking
|
|
280
|
+
|
|
281
|
+
**Use when**: Testing video calls, QR code scanners, voice recording, or any feature using `getUserMedia`.
|
|
282
|
+
**Avoid when**: Your app does not use camera or microphone.
|
|
283
|
+
|
|
284
|
+
Chromium can use fake media devices. This is not available in Firefox or WebKit.
|
|
285
|
+
|
|
286
|
+
**TypeScript**
|
|
287
|
+
```typescript
|
|
288
|
+
import { test, expect, chromium } from '@playwright/test';
|
|
289
|
+
|
|
290
|
+
test('video call shows local preview with fake camera', async () => {
|
|
291
|
+
// Launch Chromium with fake media streams
|
|
292
|
+
const browser = await chromium.launch({
|
|
293
|
+
args: [
|
|
294
|
+
'--use-fake-device-for-media-stream',
|
|
295
|
+
'--use-fake-ui-for-media-stream',
|
|
296
|
+
],
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const context = await browser.newContext({
|
|
300
|
+
permissions: ['camera', 'microphone'],
|
|
301
|
+
});
|
|
302
|
+
const page = await context.newPage();
|
|
303
|
+
|
|
304
|
+
await page.goto('/video-call');
|
|
305
|
+
await page.getByRole('button', { name: 'Start camera' }).click();
|
|
306
|
+
|
|
307
|
+
// Verify the video element is playing
|
|
308
|
+
const isPlaying = await page.evaluate(() => {
|
|
309
|
+
const video = document.querySelector('video#local-preview') as HTMLVideoElement;
|
|
310
|
+
return video && !video.paused && video.readyState >= 2;
|
|
311
|
+
});
|
|
312
|
+
expect(isPlaying).toBe(true);
|
|
313
|
+
|
|
314
|
+
await context.close();
|
|
315
|
+
await browser.close();
|
|
316
|
+
});
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
**JavaScript**
|
|
320
|
+
```javascript
|
|
321
|
+
const { test, expect, chromium } = require('@playwright/test');
|
|
322
|
+
|
|
323
|
+
test('video call shows local preview with fake camera', async () => {
|
|
324
|
+
const browser = await chromium.launch({
|
|
325
|
+
args: [
|
|
326
|
+
'--use-fake-device-for-media-stream',
|
|
327
|
+
'--use-fake-ui-for-media-stream',
|
|
328
|
+
],
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const context = await browser.newContext({
|
|
332
|
+
permissions: ['camera', 'microphone'],
|
|
333
|
+
});
|
|
334
|
+
const page = await context.newPage();
|
|
335
|
+
|
|
336
|
+
await page.goto('/video-call');
|
|
337
|
+
await page.getByRole('button', { name: 'Start camera' }).click();
|
|
338
|
+
|
|
339
|
+
const isPlaying = await page.evaluate(() => {
|
|
340
|
+
const video = document.querySelector('video#local-preview');
|
|
341
|
+
return video && !video.paused && video.readyState >= 2;
|
|
342
|
+
});
|
|
343
|
+
expect(isPlaying).toBe(true);
|
|
344
|
+
|
|
345
|
+
await context.close();
|
|
346
|
+
await browser.close();
|
|
347
|
+
});
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### localStorage and sessionStorage
|
|
351
|
+
|
|
352
|
+
**Use when**: Your app persists state, tokens, preferences, or feature flags in web storage.
|
|
353
|
+
**Avoid when**: You can set the state through the UI or API instead. Prefer those approaches for realism.
|
|
354
|
+
|
|
355
|
+
Playwright 1.61 adds a first-class Web Storage API — `page.localStorage` and `page.sessionStorage` — with `getItem`, `setItem`, `removeItem`, `clear`, and `items()`. Prefer it over `page.evaluate()`: no serialization boilerplate, and it works even when the page has a strict CSP that blocks script evaluation.
|
|
356
|
+
|
|
357
|
+
**TypeScript**
|
|
358
|
+
```typescript
|
|
359
|
+
import { test, expect } from '@playwright/test';
|
|
360
|
+
|
|
361
|
+
test('app loads dark theme from localStorage preference', async ({ page }) => {
|
|
362
|
+
// Set localStorage before the app reads it (Playwright 1.61+)
|
|
363
|
+
await page.goto('/');
|
|
364
|
+
await page.localStorage.setItem('theme', 'dark');
|
|
365
|
+
|
|
366
|
+
// Reload to pick up the stored preference
|
|
367
|
+
await page.reload();
|
|
368
|
+
|
|
369
|
+
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test('clear localStorage between scenarios', async ({ page }) => {
|
|
373
|
+
await page.goto('/');
|
|
374
|
+
|
|
375
|
+
// Seed some data
|
|
376
|
+
await page.localStorage.setItem('cart', JSON.stringify([{ id: 1, qty: 2 }]));
|
|
377
|
+
await page.localStorage.setItem('user_prefs', JSON.stringify({ currency: 'EUR' }));
|
|
378
|
+
|
|
379
|
+
// Read and verify
|
|
380
|
+
const cart = JSON.parse((await page.localStorage.getItem('cart')) ?? '[]');
|
|
381
|
+
expect(cart).toHaveLength(1);
|
|
382
|
+
|
|
383
|
+
// List everything stored
|
|
384
|
+
const all = await page.localStorage.items();
|
|
385
|
+
|
|
386
|
+
// Clear specific keys
|
|
387
|
+
await page.localStorage.removeItem('cart');
|
|
388
|
+
|
|
389
|
+
// Or clear everything
|
|
390
|
+
await page.localStorage.clear();
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test('sessionStorage survives navigations within the session', async ({ page }) => {
|
|
394
|
+
await page.goto('/step-1');
|
|
395
|
+
await page.sessionStorage.setItem('wizard_step', '1');
|
|
396
|
+
|
|
397
|
+
await page.goto('/step-2');
|
|
398
|
+
expect(await page.sessionStorage.getItem('wizard_step')).toBe('1');
|
|
399
|
+
});
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
**JavaScript**
|
|
403
|
+
```javascript
|
|
404
|
+
const { test, expect } = require('@playwright/test');
|
|
405
|
+
|
|
406
|
+
test('app loads dark theme from localStorage preference', async ({ page }) => {
|
|
407
|
+
await page.goto('/');
|
|
408
|
+
await page.localStorage.setItem('theme', 'dark');
|
|
409
|
+
await page.reload();
|
|
410
|
+
|
|
411
|
+
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test('sessionStorage survives navigations within the session', async ({ page }) => {
|
|
415
|
+
await page.goto('/step-1');
|
|
416
|
+
await page.sessionStorage.setItem('wizard_step', '1');
|
|
417
|
+
|
|
418
|
+
await page.goto('/step-2');
|
|
419
|
+
expect(await page.sessionStorage.getItem('wizard_step')).toBe('1');
|
|
420
|
+
});
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
**On Playwright < 1.61**, fall back to `page.evaluate()`:
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
await page.evaluate(() => localStorage.setItem('theme', 'dark'));
|
|
427
|
+
const step = await page.evaluate(() => sessionStorage.getItem('wizard_step'));
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### IndexedDB Testing
|
|
431
|
+
|
|
432
|
+
**Use when**: Your app uses IndexedDB for offline storage, caching, or large datasets (progressive web apps, offline-first apps).
|
|
433
|
+
**Avoid when**: Your app only uses localStorage or server-side storage.
|
|
434
|
+
|
|
435
|
+
**TypeScript**
|
|
436
|
+
```typescript
|
|
437
|
+
import { test, expect } from '@playwright/test';
|
|
438
|
+
|
|
439
|
+
test('offline-first app stores data in IndexedDB', async ({ page }) => {
|
|
440
|
+
await page.goto('/notes');
|
|
441
|
+
|
|
442
|
+
// Create a note through the UI
|
|
443
|
+
await page.getByRole('button', { name: 'New note' }).click();
|
|
444
|
+
await page.getByRole('textbox', { name: 'Title' }).fill('Test Note');
|
|
445
|
+
await page.getByRole('textbox', { name: 'Content' }).fill('This is stored in IndexedDB');
|
|
446
|
+
await page.getByRole('button', { name: 'Save' }).click();
|
|
447
|
+
|
|
448
|
+
// Verify it is stored in IndexedDB
|
|
449
|
+
const storedNotes = await page.evaluate(() => {
|
|
450
|
+
return new Promise((resolve, reject) => {
|
|
451
|
+
const request = indexedDB.open('NotesDB', 1);
|
|
452
|
+
request.onsuccess = () => {
|
|
453
|
+
const db = request.result;
|
|
454
|
+
const tx = db.transaction('notes', 'readonly');
|
|
455
|
+
const store = tx.objectStore('notes');
|
|
456
|
+
const getAll = store.getAll();
|
|
457
|
+
getAll.onsuccess = () => resolve(getAll.result);
|
|
458
|
+
getAll.onerror = () => reject(getAll.error);
|
|
459
|
+
};
|
|
460
|
+
request.onerror = () => reject(request.error);
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
expect(storedNotes).toHaveLength(1);
|
|
465
|
+
expect(storedNotes[0]).toMatchObject({
|
|
466
|
+
title: 'Test Note',
|
|
467
|
+
content: 'This is stored in IndexedDB',
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
test('clear IndexedDB for a clean test state', async ({ page }) => {
|
|
472
|
+
await page.goto('/');
|
|
473
|
+
|
|
474
|
+
// Delete the entire database
|
|
475
|
+
await page.evaluate(() => {
|
|
476
|
+
return new Promise((resolve, reject) => {
|
|
477
|
+
const request = indexedDB.deleteDatabase('NotesDB');
|
|
478
|
+
request.onsuccess = () => resolve(undefined);
|
|
479
|
+
request.onerror = () => reject(request.error);
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
await page.reload();
|
|
484
|
+
await expect(page.getByText('No notes yet')).toBeVisible();
|
|
485
|
+
});
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
**JavaScript**
|
|
489
|
+
```javascript
|
|
490
|
+
const { test, expect } = require('@playwright/test');
|
|
491
|
+
|
|
492
|
+
test('offline-first app stores data in IndexedDB', async ({ page }) => {
|
|
493
|
+
await page.goto('/notes');
|
|
494
|
+
|
|
495
|
+
await page.getByRole('button', { name: 'New note' }).click();
|
|
496
|
+
await page.getByRole('textbox', { name: 'Title' }).fill('Test Note');
|
|
497
|
+
await page.getByRole('textbox', { name: 'Content' }).fill('This is stored in IndexedDB');
|
|
498
|
+
await page.getByRole('button', { name: 'Save' }).click();
|
|
499
|
+
|
|
500
|
+
const storedNotes = await page.evaluate(() => {
|
|
501
|
+
return new Promise((resolve, reject) => {
|
|
502
|
+
const request = indexedDB.open('NotesDB', 1);
|
|
503
|
+
request.onsuccess = () => {
|
|
504
|
+
const db = request.result;
|
|
505
|
+
const tx = db.transaction('notes', 'readonly');
|
|
506
|
+
const store = tx.objectStore('notes');
|
|
507
|
+
const getAll = store.getAll();
|
|
508
|
+
getAll.onsuccess = () => resolve(getAll.result);
|
|
509
|
+
getAll.onerror = () => reject(getAll.error);
|
|
510
|
+
};
|
|
511
|
+
request.onerror = () => reject(request.error);
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
expect(storedNotes).toHaveLength(1);
|
|
516
|
+
expect(storedNotes[0]).toMatchObject({
|
|
517
|
+
title: 'Test Note',
|
|
518
|
+
content: 'This is stored in IndexedDB',
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
### Notifications
|
|
524
|
+
|
|
525
|
+
**Use when**: Your app uses the browser Notification API to show desktop notifications.
|
|
526
|
+
**Avoid when**: Notifications are purely server-side (push without Notification API).
|
|
527
|
+
|
|
528
|
+
Playwright cannot capture the actual system notification. Instead, mock the Notification constructor and verify the app calls it correctly.
|
|
529
|
+
|
|
530
|
+
**TypeScript**
|
|
531
|
+
```typescript
|
|
532
|
+
import { test, expect } from '@playwright/test';
|
|
533
|
+
|
|
534
|
+
test('app triggers a browser notification on new message', async ({ browser }) => {
|
|
535
|
+
const context = await browser.newContext({
|
|
536
|
+
permissions: ['notifications'],
|
|
537
|
+
});
|
|
538
|
+
const page = await context.newPage();
|
|
539
|
+
|
|
540
|
+
// Intercept Notification constructor to capture calls
|
|
541
|
+
await page.evaluate(() => {
|
|
542
|
+
(window as any).__notifications = [];
|
|
543
|
+
const OriginalNotification = window.Notification;
|
|
544
|
+
(window as any).Notification = class MockNotification {
|
|
545
|
+
constructor(title: string, options?: NotificationOptions) {
|
|
546
|
+
(window as any).__notifications.push({ title, ...options });
|
|
547
|
+
}
|
|
548
|
+
static get permission() { return 'granted'; }
|
|
549
|
+
static requestPermission() { return Promise.resolve('granted' as NotificationPermission); }
|
|
550
|
+
};
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
await page.goto('/chat');
|
|
554
|
+
|
|
555
|
+
// Simulate receiving a message that triggers a notification
|
|
556
|
+
await page.evaluate(() => {
|
|
557
|
+
window.dispatchEvent(new CustomEvent('new-message', {
|
|
558
|
+
detail: { from: 'Alice', text: 'Hey there!' },
|
|
559
|
+
}));
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
// Check the notification was created with correct content
|
|
563
|
+
const notifications = await page.evaluate(() => (window as any).__notifications);
|
|
564
|
+
expect(notifications).toHaveLength(1);
|
|
565
|
+
expect(notifications[0].title).toBe('New message from Alice');
|
|
566
|
+
expect(notifications[0].body).toBe('Hey there!');
|
|
567
|
+
|
|
568
|
+
await context.close();
|
|
569
|
+
});
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
**JavaScript**
|
|
573
|
+
```javascript
|
|
574
|
+
const { test, expect } = require('@playwright/test');
|
|
575
|
+
|
|
576
|
+
test('app triggers a browser notification on new message', async ({ browser }) => {
|
|
577
|
+
const context = await browser.newContext({
|
|
578
|
+
permissions: ['notifications'],
|
|
579
|
+
});
|
|
580
|
+
const page = await context.newPage();
|
|
581
|
+
|
|
582
|
+
await page.evaluate(() => {
|
|
583
|
+
window.__notifications = [];
|
|
584
|
+
window.Notification = class MockNotification {
|
|
585
|
+
constructor(title, options) {
|
|
586
|
+
window.__notifications.push({ title, ...options });
|
|
587
|
+
}
|
|
588
|
+
static get permission() { return 'granted'; }
|
|
589
|
+
static requestPermission() { return Promise.resolve('granted'); }
|
|
590
|
+
};
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
await page.goto('/chat');
|
|
594
|
+
|
|
595
|
+
await page.evaluate(() => {
|
|
596
|
+
window.dispatchEvent(new CustomEvent('new-message', {
|
|
597
|
+
detail: { from: 'Alice', text: 'Hey there!' },
|
|
598
|
+
}));
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
const notifications = await page.evaluate(() => window.__notifications);
|
|
602
|
+
expect(notifications).toHaveLength(1);
|
|
603
|
+
expect(notifications[0].title).toBe('New message from Alice');
|
|
604
|
+
await context.close();
|
|
605
|
+
});
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
### Preserving a Real Browser's Behavior on Attach (`connectOverCDP({ noDefaults })`, Playwright 1.60+)
|
|
609
|
+
|
|
610
|
+
**Use when**: You attach to an existing, user-owned browser over CDP and need it to keep behaving exactly as the user left it — Playwright otherwise overrides the default context's download, focus, and media emulation.
|
|
611
|
+
**Avoid when**: You launched the browser for testing. The default overrides give you deterministic downloads and focus/media emulation, which is usually what tests want.
|
|
612
|
+
|
|
613
|
+
Playwright 1.60 adds `noDefaults` to `browserType.connectOverCDP()` to disable those default-context overrides:
|
|
614
|
+
|
|
615
|
+
```typescript
|
|
616
|
+
import { chromium } from 'playwright';
|
|
617
|
+
|
|
618
|
+
test('drive a real browser without changing its focus/media behavior', async () => {
|
|
619
|
+
const browser = await chromium.connectOverCDP('http://localhost:9222', {
|
|
620
|
+
noDefaults: true, // don't override download/focus/media emulation
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
const context = browser.contexts()[0];
|
|
624
|
+
const page = context.pages()[0] ?? (await context.newPage());
|
|
625
|
+
await page.goto('https://example.com');
|
|
626
|
+
|
|
627
|
+
await browser.close(); // detaches without closing the user's browser
|
|
628
|
+
});
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
See [playwright-cli/session-management.md](../playwright-cli/session-management.md#attaching-to-a-real-browser-without-overrides-connectovercdp-nodefaults-playwright-160) for the CLI/agent attach workflow.
|
|
632
|
+
|
|
633
|
+
## Decision Guide
|
|
634
|
+
|
|
635
|
+
| Browser API | How to Test | Key Configuration |
|
|
636
|
+
|---|---|---|
|
|
637
|
+
| Geolocation | `geolocation` context option + `permissions: ['geolocation']` | `context.setGeolocation()` for mid-test changes |
|
|
638
|
+
| Permissions (any) | `permissions` array in context options | `context.grantPermissions()` / `context.clearPermissions()` |
|
|
639
|
+
| Clipboard | Grant `clipboard-read`/`clipboard-write` + `page.evaluate(navigator.clipboard...)` | Requires secure context (HTTPS or localhost) |
|
|
640
|
+
| Notifications | Mock `Notification` constructor via `page.evaluate` | Grant `notifications` permission; capture constructor calls |
|
|
641
|
+
| Camera / Microphone | Chromium `--use-fake-device-for-media-stream` launch arg | Only works in Chromium; grant `camera`/`microphone` permissions |
|
|
642
|
+
| localStorage | `page.evaluate(() => localStorage.getItem/setItem(...))` | Set before navigation or reload to take effect |
|
|
643
|
+
| sessionStorage | `page.evaluate(() => sessionStorage.getItem/setItem(...))` | Scoped to the browsing session; cleared on context close |
|
|
644
|
+
| IndexedDB | `page.evaluate` with `indexedDB.open()` | Wrap in Promises for async operations |
|
|
645
|
+
|
|
646
|
+
## Anti-Patterns
|
|
647
|
+
|
|
648
|
+
| Don't Do This | Problem | Do This Instead |
|
|
649
|
+
|---|---|---|
|
|
650
|
+
| Set `geolocation` without granting the permission | `getCurrentPosition` returns permission error | Always pair `geolocation` with `permissions: ['geolocation']` |
|
|
651
|
+
| Test clipboard without secure context | `navigator.clipboard` throws in non-HTTPS contexts | Use `localhost` or configure HTTPS in your test server |
|
|
652
|
+
| Access localStorage before navigating to the origin | `page.evaluate` runs in `about:blank` context initially | `page.goto('/')` first, then set localStorage, then reload |
|
|
653
|
+
| Store test tokens in localStorage directly | Bypasses auth flow; may mask real login bugs | Use `storageState` or proper auth fixtures |
|
|
654
|
+
| Rely on IndexedDB state from a previous test | Tests must be independent | Clear or delete the database in test setup |
|
|
655
|
+
| Skip cleanup of injected mocks (Notification, etc.) | Mock leaks into subsequent tests if using the same context | Each test gets a fresh context by default; only a concern with shared contexts |
|
|
656
|
+
|
|
657
|
+
## Troubleshooting
|
|
658
|
+
|
|
659
|
+
| Symptom | Cause | Fix |
|
|
660
|
+
|---|---|---|
|
|
661
|
+
| `geolocation` returns `undefined` in the app | Permission not granted | Add `permissions: ['geolocation']` to context options |
|
|
662
|
+
| Clipboard `readText()` throws `DOMException` | Missing clipboard permissions or non-secure context | Grant `clipboard-read`; ensure HTTPS or localhost |
|
|
663
|
+
| `localStorage.getItem()` returns `null` after `setItem` | Set was done on a different origin or before navigation | Verify you navigated to the correct origin before calling `setItem` |
|
|
664
|
+
| Camera not working in Firefox/WebKit | Fake media devices are Chromium-only | Skip camera tests on non-Chromium browsers or mock `getUserMedia` via `page.evaluate` |
|
|
665
|
+
| IndexedDB `evaluate` returns `undefined` | Forgot to return the Promise | Ensure the `page.evaluate` callback returns `new Promise(...)` |
|
|
666
|
+
| Permissions change not reflected | App caches permission state on load | Reload the page after `grantPermissions()` or `clearPermissions()` |
|
|
667
|
+
|
|
668
|
+
## Related
|
|
669
|
+
|
|
670
|
+
- [core/configuration.md](configuration.md) -- global geolocation/permissions in config
|
|
671
|
+
- [core/service-workers-and-pwa.md](service-workers-and-pwa.md) -- offline and cache testing using IndexedDB and service workers
|
|
672
|
+
- [core/fixtures-and-hooks.md](fixtures-and-hooks.md) -- wrap browser API setup in reusable fixtures
|
|
673
|
+
- [core/debugging.md](debugging.md) -- inspecting storage and permissions in Playwright traces
|