@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,1012 @@
1
+ # File Upload and Download Recipes
2
+
3
+ > **When to use**: You need to test file uploads (single, multiple, drag-and-drop), file downloads (verify content, filename, type), upload progress, or file type restrictions.
4
+
5
+ ---
6
+
7
+ ## Recipe 1: Single File Upload via Input
8
+
9
+ ### Complete Example
10
+
11
+ **TypeScript**
12
+
13
+ ```typescript
14
+ import { test, expect } from '@playwright/test';
15
+ import path from 'path';
16
+
17
+ test('uploads a single file via file input', async ({ page }) => {
18
+ await page.goto('/documents');
19
+
20
+ // Click the upload button which triggers the hidden file input
21
+ const fileInput = page.locator('input[type="file"]');
22
+
23
+ // Upload a file from the test fixtures directory
24
+ await fileInput.setInputFiles(path.resolve(__dirname, '../fixtures/report.pdf'));
25
+
26
+ // Verify the file name appears in the UI
27
+ await expect(page.getByText('report.pdf')).toBeVisible();
28
+
29
+ // Click the submit/upload button
30
+ await page.getByRole('button', { name: 'Upload' }).click();
31
+
32
+ // Wait for the upload to complete
33
+ await expect(page.getByRole('alert')).toContainText('File uploaded successfully');
34
+
35
+ // Verify the file appears in the documents list
36
+ await expect(page.getByRole('link', { name: 'report.pdf' })).toBeVisible();
37
+ });
38
+
39
+ test('uploads a file created in-memory (no fixture file needed)', async ({ page }) => {
40
+ await page.goto('/documents');
41
+
42
+ const fileInput = page.locator('input[type="file"]');
43
+
44
+ // Create a file from a buffer -- no fixture file required
45
+ await fileInput.setInputFiles({
46
+ name: 'test-data.csv',
47
+ mimeType: 'text/csv',
48
+ buffer: Buffer.from('name,email,role\nJane,jane@example.com,admin\nBob,bob@example.com,user'),
49
+ });
50
+
51
+ await expect(page.getByText('test-data.csv')).toBeVisible();
52
+
53
+ await page.getByRole('button', { name: 'Upload' }).click();
54
+
55
+ await expect(page.getByRole('alert')).toContainText('File uploaded successfully');
56
+ });
57
+
58
+ test('clears a selected file before uploading', async ({ page }) => {
59
+ await page.goto('/documents');
60
+
61
+ const fileInput = page.locator('input[type="file"]');
62
+
63
+ // Select a file
64
+ await fileInput.setInputFiles({
65
+ name: 'wrong-file.txt',
66
+ mimeType: 'text/plain',
67
+ buffer: Buffer.from('wrong content'),
68
+ });
69
+
70
+ await expect(page.getByText('wrong-file.txt')).toBeVisible();
71
+
72
+ // Clear the selection
73
+ await fileInput.setInputFiles([]);
74
+
75
+ // Or click a "Remove" button in the UI
76
+ // await page.getByRole('button', { name: 'Remove' }).click();
77
+
78
+ await expect(page.getByText('wrong-file.txt')).not.toBeVisible();
79
+ });
80
+ ```
81
+
82
+ **JavaScript**
83
+
84
+ ```javascript
85
+ const { test, expect } = require('@playwright/test');
86
+ const path = require('path');
87
+
88
+ test('uploads a single file via file input', async ({ page }) => {
89
+ await page.goto('/documents');
90
+
91
+ const fileInput = page.locator('input[type="file"]');
92
+ await fileInput.setInputFiles(path.resolve(__dirname, '../fixtures/report.pdf'));
93
+
94
+ await expect(page.getByText('report.pdf')).toBeVisible();
95
+
96
+ await page.getByRole('button', { name: 'Upload' }).click();
97
+
98
+ await expect(page.getByRole('alert')).toContainText('File uploaded successfully');
99
+ await expect(page.getByRole('link', { name: 'report.pdf' })).toBeVisible();
100
+ });
101
+
102
+ test('uploads a file created in-memory', async ({ page }) => {
103
+ await page.goto('/documents');
104
+
105
+ const fileInput = page.locator('input[type="file"]');
106
+
107
+ await fileInput.setInputFiles({
108
+ name: 'test-data.csv',
109
+ mimeType: 'text/csv',
110
+ buffer: Buffer.from('name,email,role\nJane,jane@example.com,admin'),
111
+ });
112
+
113
+ await expect(page.getByText('test-data.csv')).toBeVisible();
114
+ await page.getByRole('button', { name: 'Upload' }).click();
115
+ await expect(page.getByRole('alert')).toContainText('File uploaded successfully');
116
+ });
117
+ ```
118
+
119
+ ---
120
+
121
+ ## Recipe 2: Multiple File Upload
122
+
123
+ ### Complete Example
124
+
125
+ **TypeScript**
126
+
127
+ ```typescript
128
+ import { test, expect } from '@playwright/test';
129
+ import path from 'path';
130
+
131
+ test('uploads multiple files at once', async ({ page }) => {
132
+ await page.goto('/documents');
133
+
134
+ const fileInput = page.locator('input[type="file"]');
135
+
136
+ // Pass an array to upload multiple files
137
+ await fileInput.setInputFiles([
138
+ path.resolve(__dirname, '../fixtures/report.pdf'),
139
+ path.resolve(__dirname, '../fixtures/photo.jpg'),
140
+ path.resolve(__dirname, '../fixtures/data.csv'),
141
+ ]);
142
+
143
+ // Verify all file names appear
144
+ await expect(page.getByText('report.pdf')).toBeVisible();
145
+ await expect(page.getByText('photo.jpg')).toBeVisible();
146
+ await expect(page.getByText('data.csv')).toBeVisible();
147
+
148
+ // Verify the count indicator
149
+ await expect(page.getByText('3 files selected')).toBeVisible();
150
+
151
+ await page.getByRole('button', { name: 'Upload all' }).click();
152
+
153
+ await expect(page.getByRole('alert')).toContainText('3 files uploaded');
154
+ });
155
+
156
+ test('uploads multiple files with in-memory buffers', async ({ page }) => {
157
+ await page.goto('/documents');
158
+
159
+ const fileInput = page.locator('input[type="file"]');
160
+
161
+ await fileInput.setInputFiles([
162
+ {
163
+ name: 'notes.txt',
164
+ mimeType: 'text/plain',
165
+ buffer: Buffer.from('Meeting notes for Q4 planning'),
166
+ },
167
+ {
168
+ name: 'config.json',
169
+ mimeType: 'application/json',
170
+ buffer: Buffer.from(JSON.stringify({ theme: 'dark', lang: 'en' })),
171
+ },
172
+ ]);
173
+
174
+ await expect(page.getByText('notes.txt')).toBeVisible();
175
+ await expect(page.getByText('config.json')).toBeVisible();
176
+
177
+ await page.getByRole('button', { name: 'Upload all' }).click();
178
+ await expect(page.getByRole('alert')).toContainText('2 files uploaded');
179
+ });
180
+
181
+ test('removes one file from a multi-file selection', async ({ page }) => {
182
+ await page.goto('/documents');
183
+
184
+ const fileInput = page.locator('input[type="file"]');
185
+
186
+ await fileInput.setInputFiles([
187
+ { name: 'keep.txt', mimeType: 'text/plain', buffer: Buffer.from('keep') },
188
+ { name: 'remove.txt', mimeType: 'text/plain', buffer: Buffer.from('remove') },
189
+ { name: 'also-keep.txt', mimeType: 'text/plain', buffer: Buffer.from('keep') },
190
+ ]);
191
+
192
+ // Remove a specific file from the preview list
193
+ const removeItem = page.getByText('remove.txt').locator('..');
194
+ await removeItem.getByRole('button', { name: /remove|delete|×/i }).click();
195
+
196
+ await expect(page.getByText('remove.txt')).not.toBeVisible();
197
+ await expect(page.getByText('keep.txt')).toBeVisible();
198
+ await expect(page.getByText('also-keep.txt')).toBeVisible();
199
+ await expect(page.getByText('2 files selected')).toBeVisible();
200
+ });
201
+ ```
202
+
203
+ **JavaScript**
204
+
205
+ ```javascript
206
+ const { test, expect } = require('@playwright/test');
207
+ const path = require('path');
208
+
209
+ test('uploads multiple files at once', async ({ page }) => {
210
+ await page.goto('/documents');
211
+
212
+ const fileInput = page.locator('input[type="file"]');
213
+
214
+ await fileInput.setInputFiles([
215
+ path.resolve(__dirname, '../fixtures/report.pdf'),
216
+ path.resolve(__dirname, '../fixtures/photo.jpg'),
217
+ path.resolve(__dirname, '../fixtures/data.csv'),
218
+ ]);
219
+
220
+ await expect(page.getByText('report.pdf')).toBeVisible();
221
+ await expect(page.getByText('photo.jpg')).toBeVisible();
222
+ await expect(page.getByText('data.csv')).toBeVisible();
223
+
224
+ await page.getByRole('button', { name: 'Upload all' }).click();
225
+ await expect(page.getByRole('alert')).toContainText('3 files uploaded');
226
+ });
227
+ ```
228
+
229
+ ---
230
+
231
+ ## Recipe 3: Drag-and-Drop File Upload
232
+
233
+ > **Playwright 1.60+**: `locator.drop({ files })` simulates a real external file drop (native `dragenter`/`dragover`/`drop` with synthetic `DataTransfer`). Use it for zones that listen for `drop` events and have no usable `input[type=file]`. On Playwright < 1.60, target the underlying file input with `setInputFiles` (shown after).
234
+
235
+ ### Complete Example
236
+
237
+ **TypeScript**
238
+
239
+ ```typescript
240
+ import { test, expect } from '@playwright/test';
241
+ import path from 'path';
242
+
243
+ test('uploads a file via a native drop (Playwright 1.60+)', async ({ page }) => {
244
+ await page.goto('/documents');
245
+
246
+ const dropZone = page.locator('[data-testid="drop-zone"]');
247
+ await expect(dropZone).toContainText(/drag.*here|drop.*files/i);
248
+
249
+ // Native external file drop — fires the drop events the zone listens for
250
+ await dropZone.drop({ files: path.resolve(__dirname, '../fixtures/report.pdf') });
251
+
252
+ await expect(dropZone.getByText('report.pdf')).toBeVisible();
253
+
254
+ await page.getByRole('button', { name: 'Upload' }).click();
255
+ await expect(page.getByRole('alert')).toContainText('uploaded successfully');
256
+ });
257
+
258
+ test('drops multiple files at once (Playwright 1.60+)', async ({ page }) => {
259
+ await page.goto('/documents');
260
+
261
+ await page.locator('[data-testid="drop-zone"]').drop({
262
+ files: [
263
+ { name: 'image1.png', mimeType: 'image/png', buffer: Buffer.from('fake-png-1') },
264
+ { name: 'image2.png', mimeType: 'image/png', buffer: Buffer.from('fake-png-2') },
265
+ ],
266
+ });
267
+
268
+ await expect(page.getByText('image1.png')).toBeVisible();
269
+ await expect(page.getByText('image2.png')).toBeVisible();
270
+ });
271
+
272
+ test('uploads a file via the underlying input (Playwright < 1.60)', async ({ page }) => {
273
+ await page.goto('/documents');
274
+
275
+ const dropZone = page.locator('[data-testid="drop-zone"]');
276
+ await expect(dropZone).toContainText(/drag.*here|drop.*files/i);
277
+
278
+ // Older Playwright can't simulate OS file drops — drive the input the zone falls back to
279
+ const fileInput = page.locator('input[type="file"]');
280
+
281
+ await fileInput.setInputFiles(path.resolve(__dirname, '../fixtures/report.pdf'));
282
+
283
+ // File preview should appear in the drop zone
284
+ await expect(dropZone.getByText('report.pdf')).toBeVisible();
285
+
286
+ // Trigger upload
287
+ await page.getByRole('button', { name: 'Upload' }).click();
288
+
289
+ await expect(page.getByRole('alert')).toContainText('uploaded successfully');
290
+ });
291
+
292
+ test('shows visual feedback during drag-over', async ({ page }) => {
293
+ await page.goto('/documents');
294
+
295
+ const dropZone = page.locator('[data-testid="drop-zone"]');
296
+
297
+ // Simulate dragenter to show visual feedback
298
+ await dropZone.dispatchEvent('dragenter', {
299
+ dataTransfer: { types: ['Files'], files: [] },
300
+ });
301
+
302
+ // Drop zone should show an active state
303
+ await expect(dropZone).toHaveClass(/active|highlight|drag-over/);
304
+ await expect(dropZone).toContainText(/release|drop now/i);
305
+
306
+ // Simulate dragleave to reset
307
+ await dropZone.dispatchEvent('dragleave');
308
+
309
+ await expect(dropZone).not.toHaveClass(/active|highlight|drag-over/);
310
+ });
311
+
312
+ test('handles multiple files dropped at once', async ({ page }) => {
313
+ await page.goto('/documents');
314
+
315
+ const fileInput = page.locator('input[type="file"]');
316
+
317
+ await fileInput.setInputFiles([
318
+ { name: 'image1.png', mimeType: 'image/png', buffer: Buffer.from('fake-png-1') },
319
+ { name: 'image2.png', mimeType: 'image/png', buffer: Buffer.from('fake-png-2') },
320
+ { name: 'image3.png', mimeType: 'image/png', buffer: Buffer.from('fake-png-3') },
321
+ ]);
322
+
323
+ await expect(page.getByText('image1.png')).toBeVisible();
324
+ await expect(page.getByText('image2.png')).toBeVisible();
325
+ await expect(page.getByText('image3.png')).toBeVisible();
326
+ });
327
+ ```
328
+
329
+ **JavaScript**
330
+
331
+ ```javascript
332
+ const { test, expect } = require('@playwright/test');
333
+ const path = require('path');
334
+
335
+ test('uploads a file via drag-and-drop zone', async ({ page }) => {
336
+ await page.goto('/documents');
337
+
338
+ const dropZone = page.locator('[data-testid="drop-zone"]');
339
+ const fileInput = page.locator('input[type="file"]');
340
+
341
+ await fileInput.setInputFiles(path.resolve(__dirname, '../fixtures/report.pdf'));
342
+
343
+ await expect(dropZone.getByText('report.pdf')).toBeVisible();
344
+
345
+ await page.getByRole('button', { name: 'Upload' }).click();
346
+ await expect(page.getByRole('alert')).toContainText('uploaded successfully');
347
+ });
348
+ ```
349
+
350
+ ---
351
+
352
+ ## Recipe 4: Download and Verify File Content
353
+
354
+ ### Complete Example
355
+
356
+ **TypeScript**
357
+
358
+ ```typescript
359
+ import { test, expect } from '@playwright/test';
360
+ import fs from 'fs';
361
+ import path from 'path';
362
+
363
+ test('downloads a file and verifies its content', async ({ page }) => {
364
+ await page.goto('/documents');
365
+
366
+ // Start waiting for the download event BEFORE clicking
367
+ const downloadPromise = page.waitForEvent('download');
368
+
369
+ await page.getByRole('link', { name: 'report.csv' }).click();
370
+
371
+ const download = await downloadPromise;
372
+
373
+ // Save to a temporary location
374
+ const downloadPath = path.join(__dirname, '../downloads', download.suggestedFilename());
375
+ await download.saveAs(downloadPath);
376
+
377
+ // Read and verify the content
378
+ const content = fs.readFileSync(downloadPath, 'utf-8');
379
+
380
+ expect(content).toContain('name,email,role');
381
+ expect(content).toContain('Jane,jane@example.com,admin');
382
+
383
+ // Verify line count
384
+ const lines = content.trim().split('\n');
385
+ expect(lines.length).toBeGreaterThan(1); // header + at least one data row
386
+
387
+ // Clean up
388
+ fs.unlinkSync(downloadPath);
389
+ });
390
+
391
+ test('downloads a JSON file and verifies structure', async ({ page }) => {
392
+ await page.goto('/api-docs');
393
+
394
+ const downloadPromise = page.waitForEvent('download');
395
+ await page.getByRole('button', { name: 'Export API spec' }).click();
396
+
397
+ const download = await downloadPromise;
398
+ const downloadPath = path.join(__dirname, '../downloads', download.suggestedFilename());
399
+ await download.saveAs(downloadPath);
400
+
401
+ const content = JSON.parse(fs.readFileSync(downloadPath, 'utf-8'));
402
+
403
+ expect(content).toHaveProperty('openapi');
404
+ expect(content.paths).toBeDefined();
405
+ expect(Object.keys(content.paths).length).toBeGreaterThan(0);
406
+
407
+ fs.unlinkSync(downloadPath);
408
+ });
409
+
410
+ test('downloads a file via the stream (no disk save needed)', async ({ page }) => {
411
+ await page.goto('/documents');
412
+
413
+ const downloadPromise = page.waitForEvent('download');
414
+ await page.getByRole('link', { name: 'report.csv' }).click();
415
+
416
+ const download = await downloadPromise;
417
+
418
+ // Read directly from the download stream without saving to disk
419
+ const readable = await download.createReadStream();
420
+ const chunks: Buffer[] = [];
421
+
422
+ for await (const chunk of readable!) {
423
+ chunks.push(Buffer.from(chunk));
424
+ }
425
+
426
+ const content = Buffer.concat(chunks).toString('utf-8');
427
+ expect(content).toContain('name,email,role');
428
+ });
429
+ ```
430
+
431
+ **JavaScript**
432
+
433
+ ```javascript
434
+ const { test, expect } = require('@playwright/test');
435
+ const fs = require('fs');
436
+ const path = require('path');
437
+
438
+ test('downloads a file and verifies its content', async ({ page }) => {
439
+ await page.goto('/documents');
440
+
441
+ const downloadPromise = page.waitForEvent('download');
442
+ await page.getByRole('link', { name: 'report.csv' }).click();
443
+
444
+ const download = await downloadPromise;
445
+
446
+ const downloadPath = path.join(__dirname, '../downloads', download.suggestedFilename());
447
+ await download.saveAs(downloadPath);
448
+
449
+ const content = fs.readFileSync(downloadPath, 'utf-8');
450
+ expect(content).toContain('name,email,role');
451
+ expect(content).toContain('Jane,jane@example.com,admin');
452
+
453
+ fs.unlinkSync(downloadPath);
454
+ });
455
+
456
+ test('downloads a file via the stream', async ({ page }) => {
457
+ await page.goto('/documents');
458
+
459
+ const downloadPromise = page.waitForEvent('download');
460
+ await page.getByRole('link', { name: 'report.csv' }).click();
461
+
462
+ const download = await downloadPromise;
463
+
464
+ const readable = await download.createReadStream();
465
+ const chunks = [];
466
+
467
+ for await (const chunk of readable) {
468
+ chunks.push(Buffer.from(chunk));
469
+ }
470
+
471
+ const content = Buffer.concat(chunks).toString('utf-8');
472
+ expect(content).toContain('name,email,role');
473
+ });
474
+ ```
475
+
476
+ ---
477
+
478
+ ## Recipe 5: Download and Verify Filename
479
+
480
+ ### Complete Example
481
+
482
+ **TypeScript**
483
+
484
+ ```typescript
485
+ import { test, expect } from '@playwright/test';
486
+
487
+ test('download has correct filename and extension', async ({ page }) => {
488
+ await page.goto('/reports');
489
+
490
+ const downloadPromise = page.waitForEvent('download');
491
+ await page.getByRole('button', { name: 'Export as PDF' }).click();
492
+
493
+ const download = await downloadPromise;
494
+
495
+ // Verify filename pattern
496
+ expect(download.suggestedFilename()).toMatch(/^report-\d{4}-\d{2}-\d{2}\.pdf$/);
497
+ });
498
+
499
+ test('download filename changes based on selected format', async ({ page }) => {
500
+ await page.goto('/reports');
501
+
502
+ // Select CSV format
503
+ await page.getByLabel('Export format').selectOption('csv');
504
+
505
+ const csvDownload = page.waitForEvent('download');
506
+ await page.getByRole('button', { name: 'Download' }).click();
507
+ const csv = await csvDownload;
508
+ expect(csv.suggestedFilename()).toMatch(/\.csv$/);
509
+
510
+ // Select Excel format
511
+ await page.getByLabel('Export format').selectOption('xlsx');
512
+
513
+ const xlsxDownload = page.waitForEvent('download');
514
+ await page.getByRole('button', { name: 'Download' }).click();
515
+ const xlsx = await xlsxDownload;
516
+ expect(xlsx.suggestedFilename()).toMatch(/\.xlsx$/);
517
+ });
518
+
519
+ test('download has correct MIME type via response headers', async ({ page }) => {
520
+ await page.goto('/reports');
521
+
522
+ // Intercept the download request to check response headers
523
+ const responsePromise = page.waitForResponse('**/api/reports/export**');
524
+ const downloadPromise = page.waitForEvent('download');
525
+
526
+ await page.getByRole('button', { name: 'Export as PDF' }).click();
527
+
528
+ const response = await responsePromise;
529
+ expect(response.headers()['content-type']).toContain('application/pdf');
530
+ expect(response.headers()['content-disposition']).toContain('attachment');
531
+
532
+ await downloadPromise; // Consume the download event
533
+ });
534
+
535
+ test('download failure shows error to user', async ({ page }) => {
536
+ // Mock the download endpoint to fail
537
+ await page.route('**/api/reports/export**', async (route) => {
538
+ await route.fulfill({
539
+ status: 500,
540
+ contentType: 'application/json',
541
+ body: JSON.stringify({ error: 'Report generation failed' }),
542
+ });
543
+ });
544
+
545
+ await page.goto('/reports');
546
+ await page.getByRole('button', { name: 'Export as PDF' }).click();
547
+
548
+ await expect(page.getByRole('alert')).toContainText(/failed|error/i);
549
+ });
550
+ ```
551
+
552
+ **JavaScript**
553
+
554
+ ```javascript
555
+ const { test, expect } = require('@playwright/test');
556
+
557
+ test('download has correct filename and extension', async ({ page }) => {
558
+ await page.goto('/reports');
559
+
560
+ const downloadPromise = page.waitForEvent('download');
561
+ await page.getByRole('button', { name: 'Export as PDF' }).click();
562
+
563
+ const download = await downloadPromise;
564
+ expect(download.suggestedFilename()).toMatch(/^report-\d{4}-\d{2}-\d{2}\.pdf$/);
565
+ });
566
+
567
+ test('download filename changes based on selected format', async ({ page }) => {
568
+ await page.goto('/reports');
569
+
570
+ await page.getByLabel('Export format').selectOption('csv');
571
+
572
+ const csvDownload = page.waitForEvent('download');
573
+ await page.getByRole('button', { name: 'Download' }).click();
574
+ const csv = await csvDownload;
575
+ expect(csv.suggestedFilename()).toMatch(/\.csv$/);
576
+ });
577
+ ```
578
+
579
+ ---
580
+
581
+ ## Recipe 6: Large File Upload with Progress
582
+
583
+ ### Complete Example
584
+
585
+ **TypeScript**
586
+
587
+ ```typescript
588
+ import { test, expect } from '@playwright/test';
589
+
590
+ test('shows upload progress for a large file', async ({ page }) => {
591
+ await page.goto('/documents');
592
+
593
+ const fileInput = page.locator('input[type="file"]');
594
+
595
+ // Create a large in-memory file (5 MB)
596
+ const largeBuffer = Buffer.alloc(5 * 1024 * 1024, 'x');
597
+
598
+ await fileInput.setInputFiles({
599
+ name: 'large-dataset.bin',
600
+ mimeType: 'application/octet-stream',
601
+ buffer: largeBuffer,
602
+ });
603
+
604
+ await page.getByRole('button', { name: 'Upload' }).click();
605
+
606
+ // Verify progress bar appears
607
+ const progressBar = page.getByRole('progressbar');
608
+ await expect(progressBar).toBeVisible();
609
+
610
+ // Verify progress percentage updates
611
+ // Use polling to check that progress increases
612
+ await expect(async () => {
613
+ const value = await progressBar.getAttribute('aria-valuenow');
614
+ expect(Number(value)).toBeGreaterThan(0);
615
+ }).toPass({ timeout: 10000 });
616
+
617
+ // Wait for upload to complete
618
+ await expect(progressBar).not.toBeVisible({ timeout: 60000 });
619
+ await expect(page.getByRole('alert')).toContainText('uploaded successfully');
620
+ });
621
+
622
+ test('cancels an in-progress upload', async ({ page }) => {
623
+ // Slow down the upload API to simulate a long upload
624
+ await page.route('**/api/documents/upload', async (route) => {
625
+ // Delay for 10 seconds to give us time to cancel
626
+ await new Promise((resolve) => setTimeout(resolve, 10000));
627
+ await route.continue();
628
+ });
629
+
630
+ await page.goto('/documents');
631
+
632
+ const fileInput = page.locator('input[type="file"]');
633
+ const largeBuffer = Buffer.alloc(5 * 1024 * 1024, 'x');
634
+
635
+ await fileInput.setInputFiles({
636
+ name: 'large-file.bin',
637
+ mimeType: 'application/octet-stream',
638
+ buffer: largeBuffer,
639
+ });
640
+
641
+ await page.getByRole('button', { name: 'Upload' }).click();
642
+
643
+ // Wait for upload to start
644
+ await expect(page.getByRole('progressbar')).toBeVisible();
645
+
646
+ // Cancel the upload
647
+ await page.getByRole('button', { name: 'Cancel upload' }).click();
648
+
649
+ // Verify upload was cancelled
650
+ await expect(page.getByRole('progressbar')).not.toBeVisible();
651
+ await expect(page.getByText(/cancelled|aborted/i)).toBeVisible();
652
+
653
+ // Verify the file did not appear in the documents list
654
+ await expect(page.getByRole('link', { name: 'large-file.bin' })).not.toBeVisible();
655
+ });
656
+
657
+ test('retries a failed upload', async ({ page }) => {
658
+ let attempt = 0;
659
+
660
+ await page.route('**/api/documents/upload', async (route) => {
661
+ attempt++;
662
+ if (attempt === 1) {
663
+ // First attempt fails
664
+ await route.fulfill({
665
+ status: 500,
666
+ contentType: 'application/json',
667
+ body: JSON.stringify({ error: 'Server error' }),
668
+ });
669
+ } else {
670
+ // Second attempt succeeds
671
+ await route.fulfill({
672
+ status: 200,
673
+ contentType: 'application/json',
674
+ body: JSON.stringify({ id: '123', name: 'data.csv' }),
675
+ });
676
+ }
677
+ });
678
+
679
+ await page.goto('/documents');
680
+
681
+ const fileInput = page.locator('input[type="file"]');
682
+ await fileInput.setInputFiles({
683
+ name: 'data.csv',
684
+ mimeType: 'text/csv',
685
+ buffer: Buffer.from('col1,col2\nval1,val2'),
686
+ });
687
+
688
+ await page.getByRole('button', { name: 'Upload' }).click();
689
+
690
+ // First attempt fails
691
+ await expect(page.getByText(/upload failed|error/i)).toBeVisible();
692
+
693
+ // Retry
694
+ await page.getByRole('button', { name: /retry/i }).click();
695
+
696
+ // Second attempt succeeds
697
+ await expect(page.getByRole('alert')).toContainText('uploaded successfully');
698
+ expect(attempt).toBe(2);
699
+ });
700
+ ```
701
+
702
+ **JavaScript**
703
+
704
+ ```javascript
705
+ const { test, expect } = require('@playwright/test');
706
+
707
+ test('shows upload progress for a large file', async ({ page }) => {
708
+ await page.goto('/documents');
709
+
710
+ const fileInput = page.locator('input[type="file"]');
711
+
712
+ const largeBuffer = Buffer.alloc(5 * 1024 * 1024, 'x');
713
+
714
+ await fileInput.setInputFiles({
715
+ name: 'large-dataset.bin',
716
+ mimeType: 'application/octet-stream',
717
+ buffer: largeBuffer,
718
+ });
719
+
720
+ await page.getByRole('button', { name: 'Upload' }).click();
721
+
722
+ const progressBar = page.getByRole('progressbar');
723
+ await expect(progressBar).toBeVisible();
724
+
725
+ await expect(progressBar).not.toBeVisible({ timeout: 60000 });
726
+ await expect(page.getByRole('alert')).toContainText('uploaded successfully');
727
+ });
728
+
729
+ test('cancels an in-progress upload', async ({ page }) => {
730
+ await page.route('**/api/documents/upload', async (route) => {
731
+ await new Promise((resolve) => setTimeout(resolve, 10000));
732
+ await route.continue();
733
+ });
734
+
735
+ await page.goto('/documents');
736
+
737
+ const fileInput = page.locator('input[type="file"]');
738
+ await fileInput.setInputFiles({
739
+ name: 'large-file.bin',
740
+ mimeType: 'application/octet-stream',
741
+ buffer: Buffer.alloc(5 * 1024 * 1024, 'x'),
742
+ });
743
+
744
+ await page.getByRole('button', { name: 'Upload' }).click();
745
+ await expect(page.getByRole('progressbar')).toBeVisible();
746
+
747
+ await page.getByRole('button', { name: 'Cancel upload' }).click();
748
+
749
+ await expect(page.getByRole('progressbar')).not.toBeVisible();
750
+ await expect(page.getByText(/cancelled|aborted/i)).toBeVisible();
751
+ });
752
+ ```
753
+
754
+ ---
755
+
756
+ ## Recipe 7: Testing File Type Restrictions
757
+
758
+ ### Complete Example
759
+
760
+ **TypeScript**
761
+
762
+ ```typescript
763
+ import { test, expect } from '@playwright/test';
764
+
765
+ test('accepts allowed file types', async ({ page }) => {
766
+ await page.goto('/documents');
767
+
768
+ const fileInput = page.locator('input[type="file"]');
769
+
770
+ // Verify the accept attribute on the file input
771
+ await expect(fileInput).toHaveAttribute('accept', /\.pdf|\.doc|\.docx|\.txt/);
772
+
773
+ // Upload an allowed file type
774
+ await fileInput.setInputFiles({
775
+ name: 'valid-document.pdf',
776
+ mimeType: 'application/pdf',
777
+ buffer: Buffer.from('fake-pdf-content'),
778
+ });
779
+
780
+ // Should be accepted without error
781
+ await expect(page.getByText('valid-document.pdf')).toBeVisible();
782
+ await expect(page.getByText(/not allowed|invalid/i)).not.toBeVisible();
783
+ });
784
+
785
+ test('rejects disallowed file types with clear error message', async ({ page }) => {
786
+ await page.goto('/documents');
787
+
788
+ const fileInput = page.locator('input[type="file"]');
789
+
790
+ // Upload a disallowed file type
791
+ // Note: setInputFiles bypasses the browser's accept attribute filter,
792
+ // so the app's JavaScript validation is what we are testing here
793
+ await fileInput.setInputFiles({
794
+ name: 'script.exe',
795
+ mimeType: 'application/x-msdownload',
796
+ buffer: Buffer.from('fake-exe-content'),
797
+ });
798
+
799
+ // App should show a rejection message
800
+ await expect(page.getByRole('alert')).toContainText(
801
+ /not allowed|unsupported file type|only .pdf, .doc/i
802
+ );
803
+
804
+ // File should not appear in the upload queue
805
+ await expect(page.getByText('script.exe')).not.toBeVisible();
806
+ });
807
+
808
+ test('enforces maximum file size', async ({ page }) => {
809
+ await page.goto('/documents');
810
+
811
+ const fileInput = page.locator('input[type="file"]');
812
+
813
+ // Create a file that exceeds the max size (e.g., 11 MB when limit is 10 MB)
814
+ const oversizedBuffer = Buffer.alloc(11 * 1024 * 1024, 'x');
815
+
816
+ await fileInput.setInputFiles({
817
+ name: 'huge-file.pdf',
818
+ mimeType: 'application/pdf',
819
+ buffer: oversizedBuffer,
820
+ });
821
+
822
+ await expect(page.getByRole('alert')).toContainText(/file.*too large|exceeds.*10 ?MB/i);
823
+ await expect(page.getByText('huge-file.pdf')).not.toBeVisible();
824
+ });
825
+
826
+ test('enforces maximum number of files', async ({ page }) => {
827
+ await page.goto('/documents');
828
+
829
+ const fileInput = page.locator('input[type="file"]');
830
+
831
+ // Try to upload more files than the limit (e.g., limit is 5)
832
+ const files = Array.from({ length: 6 }, (_, i) => ({
833
+ name: `file-${i + 1}.txt`,
834
+ mimeType: 'text/plain' as const,
835
+ buffer: Buffer.from(`content ${i + 1}`),
836
+ }));
837
+
838
+ await fileInput.setInputFiles(files);
839
+
840
+ await expect(page.getByRole('alert')).toContainText(/maximum.*5 files|too many files/i);
841
+ });
842
+
843
+ test('validates image dimensions for avatar upload', async ({ page }) => {
844
+ await page.goto('/settings/profile');
845
+
846
+ const fileInput = page.locator('input[type="file"]');
847
+
848
+ // Create a tiny 1x1 PNG (valid format but too small)
849
+ // Minimal PNG buffer
850
+ const tinyPng = Buffer.from(
851
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
852
+ 'base64'
853
+ );
854
+
855
+ await fileInput.setInputFiles({
856
+ name: 'tiny-avatar.png',
857
+ mimeType: 'image/png',
858
+ buffer: tinyPng,
859
+ });
860
+
861
+ await expect(page.getByRole('alert')).toContainText(/minimum.*dimensions|too small/i);
862
+ });
863
+ ```
864
+
865
+ **JavaScript**
866
+
867
+ ```javascript
868
+ const { test, expect } = require('@playwright/test');
869
+
870
+ test('rejects disallowed file types with clear error message', async ({ page }) => {
871
+ await page.goto('/documents');
872
+
873
+ const fileInput = page.locator('input[type="file"]');
874
+
875
+ await fileInput.setInputFiles({
876
+ name: 'script.exe',
877
+ mimeType: 'application/x-msdownload',
878
+ buffer: Buffer.from('fake-exe-content'),
879
+ });
880
+
881
+ await expect(page.getByRole('alert')).toContainText(
882
+ /not allowed|unsupported file type|only .pdf, .doc/i
883
+ );
884
+ await expect(page.getByText('script.exe')).not.toBeVisible();
885
+ });
886
+
887
+ test('enforces maximum file size', async ({ page }) => {
888
+ await page.goto('/documents');
889
+
890
+ const fileInput = page.locator('input[type="file"]');
891
+
892
+ const oversizedBuffer = Buffer.alloc(11 * 1024 * 1024, 'x');
893
+
894
+ await fileInput.setInputFiles({
895
+ name: 'huge-file.pdf',
896
+ mimeType: 'application/pdf',
897
+ buffer: oversizedBuffer,
898
+ });
899
+
900
+ await expect(page.getByRole('alert')).toContainText(/file.*too large|exceeds.*10 ?MB/i);
901
+ });
902
+
903
+ test('enforces maximum number of files', async ({ page }) => {
904
+ await page.goto('/documents');
905
+
906
+ const fileInput = page.locator('input[type="file"]');
907
+
908
+ const files = Array.from({ length: 6 }, (_, i) => ({
909
+ name: `file-${i + 1}.txt`,
910
+ mimeType: 'text/plain',
911
+ buffer: Buffer.from(`content ${i + 1}`),
912
+ }));
913
+
914
+ await fileInput.setInputFiles(files);
915
+
916
+ await expect(page.getByRole('alert')).toContainText(/maximum.*5 files|too many files/i);
917
+ });
918
+ ```
919
+
920
+ ---
921
+
922
+ ## Variations
923
+
924
+ ### Upload via File Chooser Dialog
925
+
926
+ ```typescript
927
+ test('uploads via the native file chooser dialog', async ({ page }) => {
928
+ await page.goto('/documents');
929
+
930
+ // Listen for the file chooser event before clicking the trigger
931
+ const fileChooserPromise = page.waitForEvent('filechooser');
932
+ await page.getByRole('button', { name: 'Choose file' }).click();
933
+
934
+ const fileChooser = await fileChooserPromise;
935
+
936
+ // Verify it accepts only certain types
937
+ expect(fileChooser.isMultiple()).toBe(false);
938
+
939
+ await fileChooser.setFiles({
940
+ name: 'chosen-file.pdf',
941
+ mimeType: 'application/pdf',
942
+ buffer: Buffer.from('pdf-content'),
943
+ });
944
+
945
+ await expect(page.getByText('chosen-file.pdf')).toBeVisible();
946
+ });
947
+ ```
948
+
949
+ ### Upload with Image Preview
950
+
951
+ ```typescript
952
+ test('shows image preview after selecting a file', async ({ page }) => {
953
+ await page.goto('/settings/profile');
954
+
955
+ const fileInput = page.locator('input[type="file"]');
956
+
957
+ await fileInput.setInputFiles(path.resolve(__dirname, '../fixtures/avatar.jpg'));
958
+
959
+ // Verify the image preview is displayed
960
+ const preview = page.getByRole('img', { name: /preview|avatar/i });
961
+ await expect(preview).toBeVisible();
962
+
963
+ // Verify the preview src is a blob or data URL
964
+ const src = await preview.getAttribute('src');
965
+ expect(src).toMatch(/^(blob:|data:image)/);
966
+ });
967
+ ```
968
+
969
+ ### Download with Authentication
970
+
971
+ ```typescript
972
+ test('downloads a file that requires authentication', async ({ page, request }) => {
973
+ // Some downloads go through an API that needs auth cookies
974
+ await page.goto('/documents');
975
+
976
+ // The UI download works because the browser sends cookies
977
+ const downloadPromise = page.waitForEvent('download');
978
+ await page.getByRole('link', { name: 'confidential-report.pdf' }).click();
979
+
980
+ const download = await downloadPromise;
981
+ expect(download.suggestedFilename()).toBe('confidential-report.pdf');
982
+
983
+ // Alternatively, verify via API request (which carries the auth context)
984
+ const response = await request.get('/api/documents/123/download');
985
+ expect(response.ok()).toBeTruthy();
986
+ expect(response.headers()['content-type']).toContain('application/pdf');
987
+ });
988
+ ```
989
+
990
+ ---
991
+
992
+ ## Tips
993
+
994
+ 1. **Pick the right upload primitive for your Playwright version**. For a plain `input[type="file"]`, `setInputFiles()` is the most reliable path. For drag-and-drop zones that listen for `drop` events, prefer `locator.drop({ files })` on Playwright 1.60+ (it fires native `dragenter`/`dragover`/`drop`); on older versions, fall back to `setInputFiles()` on the zone's underlying `input[type="file"]`.
995
+
996
+ 2. **Prefer in-memory buffers over fixture files**. Creating files with `Buffer.from()` keeps tests self-contained and eliminates dependencies on external fixture files. Use fixture files only when you need real file content (e.g., a valid PDF that your app parses).
997
+
998
+ 3. **Always set up the download listener before clicking**. Call `page.waitForEvent('download')` before the click that triggers the download. If you click first, you may miss the event.
999
+
1000
+ 4. **Use `createReadStream()` to verify content without disk I/O**. The `download.createReadStream()` method lets you read file content directly in memory, which is faster and avoids cleanup of temporary files.
1001
+
1002
+ 5. **Test the accept attribute AND the JavaScript validation separately**. The HTML `accept` attribute only filters the OS file dialog -- it does not prevent uploads via other means. Your app's JavaScript validation is the real gatekeeper, and `setInputFiles()` bypasses the `accept` filter, which is exactly what you want to test.
1003
+
1004
+ ---
1005
+
1006
+ ## Related
1007
+
1008
+ - [Playwright Upload Docs](https://playwright.dev/docs/input#upload-files)
1009
+ - [Playwright Download Docs](https://playwright.dev/docs/downloads)
1010
+ - `recipes/drag-and-drop.md` -- General drag and drop patterns
1011
+ - `recipes/crud-testing.md` -- File uploads as part of resource creation
1012
+ - `foundations/actions.md` -- Input interaction fundamentals