@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,749 @@
1
+ # File Operations
2
+
3
+ > **When to use**: Testing file uploads, downloads, drag-and-drop file interactions, file type validation, and download verification.
4
+ > **Prerequisites**: [core/locators.md](locators.md), [core/assertions-and-waiting.md](assertions-and-waiting.md)
5
+
6
+ ## Quick Reference
7
+
8
+ ```typescript
9
+ // Upload — single file
10
+ await page.getByLabel('Upload').setInputFiles('fixtures/resume.pdf');
11
+
12
+ // Upload — multiple files
13
+ await page.getByLabel('Upload').setInputFiles(['fixtures/a.png', 'fixtures/b.png']);
14
+
15
+ // Upload — clear selection
16
+ await page.getByLabel('Upload').setInputFiles([]);
17
+
18
+ // Download — wait and save
19
+ const download = await page.waitForEvent('download');
20
+ await page.getByRole('button', { name: 'Export CSV' }).click();
21
+ const path = await download.path(); // temp path
22
+ await download.saveAs('test-results/export.csv'); // permanent path
23
+
24
+ // File chooser dialog — non-input uploads
25
+ const fileChooser = await page.waitForEvent('filechooser');
26
+ await page.getByRole('button', { name: 'Choose file' }).click();
27
+ await fileChooser.setFiles('fixtures/photo.jpg');
28
+ ```
29
+
30
+ ## Patterns
31
+
32
+ ### Single File Upload
33
+
34
+ **Use when**: A form has a standard `<input type="file">` element.
35
+ **Avoid when**: The upload uses a drag-and-drop zone with no underlying file input.
36
+
37
+ **TypeScript**
38
+ ```typescript
39
+ import { test, expect } from '@playwright/test';
40
+ import path from 'path';
41
+
42
+ test('upload a single document', async ({ page }) => {
43
+ await page.goto('/settings/profile');
44
+
45
+ const filePath = path.join(__dirname, '../fixtures/avatar.png');
46
+ await page.getByLabel('Profile picture').setInputFiles(filePath);
47
+
48
+ // Verify the file name appears in the UI
49
+ await expect(page.getByText('avatar.png')).toBeVisible();
50
+
51
+ await page.getByRole('button', { name: 'Save' }).click();
52
+ await expect(page.getByText('Profile updated')).toBeVisible();
53
+ });
54
+ ```
55
+
56
+ **JavaScript**
57
+ ```javascript
58
+ const { test, expect } = require('@playwright/test');
59
+ const path = require('path');
60
+
61
+ test('upload a single document', async ({ page }) => {
62
+ await page.goto('/settings/profile');
63
+
64
+ const filePath = path.join(__dirname, '../fixtures/avatar.png');
65
+ await page.getByLabel('Profile picture').setInputFiles(filePath);
66
+
67
+ await expect(page.getByText('avatar.png')).toBeVisible();
68
+
69
+ await page.getByRole('button', { name: 'Save' }).click();
70
+ await expect(page.getByText('Profile updated')).toBeVisible();
71
+ });
72
+ ```
73
+
74
+ ### Multiple File Upload
75
+
76
+ **Use when**: The file input accepts `multiple` and you need to attach several files at once.
77
+ **Avoid when**: The UI only allows one file. Use single file upload.
78
+
79
+ **TypeScript**
80
+ ```typescript
81
+ import { test, expect } from '@playwright/test';
82
+ import path from 'path';
83
+
84
+ test('upload multiple attachments', async ({ page }) => {
85
+ await page.goto('/tickets/new');
86
+
87
+ const fixtures = ['doc1.pdf', 'doc2.pdf', 'screenshot.png'].map(
88
+ (f) => path.join(__dirname, '../fixtures', f)
89
+ );
90
+
91
+ await page.getByLabel('Attachments').setInputFiles(fixtures);
92
+
93
+ // Verify all files are listed
94
+ await expect(page.getByTestId('file-list').getByRole('listitem')).toHaveCount(3);
95
+
96
+ // Remove one file by clearing and re-setting
97
+ await page.getByLabel('Attachments').setInputFiles(fixtures.slice(0, 2));
98
+ await expect(page.getByTestId('file-list').getByRole('listitem')).toHaveCount(2);
99
+ });
100
+ ```
101
+
102
+ **JavaScript**
103
+ ```javascript
104
+ const { test, expect } = require('@playwright/test');
105
+ const path = require('path');
106
+
107
+ test('upload multiple attachments', async ({ page }) => {
108
+ await page.goto('/tickets/new');
109
+
110
+ const fixtures = ['doc1.pdf', 'doc2.pdf', 'screenshot.png'].map(
111
+ (f) => path.join(__dirname, '../fixtures', f)
112
+ );
113
+
114
+ await page.getByLabel('Attachments').setInputFiles(fixtures);
115
+ await expect(page.getByTestId('file-list').getByRole('listitem')).toHaveCount(3);
116
+
117
+ await page.getByLabel('Attachments').setInputFiles(fixtures.slice(0, 2));
118
+ await expect(page.getByTestId('file-list').getByRole('listitem')).toHaveCount(2);
119
+ });
120
+ ```
121
+
122
+ ### File Chooser Dialog
123
+
124
+ **Use when**: The upload is triggered by a button click that opens the native file picker, not by a visible `<input type="file">`. Common with drag-and-drop libraries and custom upload components.
125
+ **Avoid when**: There is a visible `<input type="file">` — use `setInputFiles` directly.
126
+
127
+ **TypeScript**
128
+ ```typescript
129
+ import { test, expect } from '@playwright/test';
130
+
131
+ test('upload via file chooser dialog', async ({ page }) => {
132
+ await page.goto('/documents');
133
+
134
+ // Register the listener BEFORE the click that opens the dialog
135
+ const fileChooserPromise = page.waitForEvent('filechooser');
136
+ await page.getByRole('button', { name: 'Upload document' }).click();
137
+ const fileChooser = await fileChooserPromise;
138
+
139
+ // Verify dialog properties
140
+ expect(fileChooser.isMultiple()).toBe(false);
141
+
142
+ await fileChooser.setFiles('fixtures/report.pdf');
143
+ await expect(page.getByText('report.pdf')).toBeVisible();
144
+ });
145
+
146
+ test('upload multiple via file chooser', async ({ page }) => {
147
+ await page.goto('/gallery');
148
+
149
+ const fileChooserPromise = page.waitForEvent('filechooser');
150
+ await page.getByRole('button', { name: 'Add photos' }).click();
151
+ const fileChooser = await fileChooserPromise;
152
+
153
+ expect(fileChooser.isMultiple()).toBe(true);
154
+ await fileChooser.setFiles(['fixtures/photo1.jpg', 'fixtures/photo2.jpg']);
155
+
156
+ await expect(page.getByRole('img')).toHaveCount(2);
157
+ });
158
+ ```
159
+
160
+ **JavaScript**
161
+ ```javascript
162
+ const { test, expect } = require('@playwright/test');
163
+
164
+ test('upload via file chooser dialog', async ({ page }) => {
165
+ await page.goto('/documents');
166
+
167
+ const fileChooserPromise = page.waitForEvent('filechooser');
168
+ await page.getByRole('button', { name: 'Upload document' }).click();
169
+ const fileChooser = await fileChooserPromise;
170
+
171
+ expect(fileChooser.isMultiple()).toBe(false);
172
+
173
+ await fileChooser.setFiles('fixtures/report.pdf');
174
+ await expect(page.getByText('report.pdf')).toBeVisible();
175
+ });
176
+
177
+ test('upload multiple via file chooser', async ({ page }) => {
178
+ await page.goto('/gallery');
179
+
180
+ const fileChooserPromise = page.waitForEvent('filechooser');
181
+ await page.getByRole('button', { name: 'Add photos' }).click();
182
+ const fileChooser = await fileChooserPromise;
183
+
184
+ expect(fileChooser.isMultiple()).toBe(true);
185
+ await fileChooser.setFiles(['fixtures/photo1.jpg', 'fixtures/photo2.jpg']);
186
+
187
+ await expect(page.getByRole('img')).toHaveCount(2);
188
+ });
189
+ ```
190
+
191
+ ### Drag-and-Drop File Upload
192
+
193
+ **Use when**: The UI has a drop zone that accepts files via the HTML5 Drag and Drop API and has no `<input type="file">` fallback.
194
+ **Avoid when**: A file input exists — even hidden ones work with `setInputFiles`.
195
+
196
+ **TypeScript**
197
+ ```typescript
198
+ import { test, expect } from '@playwright/test';
199
+ import fs from 'fs';
200
+ import path from 'path';
201
+
202
+ test('drop file onto upload zone', async ({ page }) => {
203
+ await page.goto('/upload');
204
+
205
+ // Read the file into a buffer
206
+ const filePath = path.join(__dirname, '../fixtures/data.csv');
207
+ const buffer = fs.readFileSync(filePath);
208
+
209
+ // Create a DataTransfer with the file and dispatch drop event
210
+ const dropZone = page.getByTestId('drop-zone');
211
+
212
+ await dropZone.dispatchEvent('drop', {
213
+ dataTransfer: {
214
+ files: [
215
+ { name: 'data.csv', mimeType: 'text/csv', buffer },
216
+ ],
217
+ },
218
+ });
219
+
220
+ await expect(page.getByText('data.csv')).toBeVisible();
221
+ await expect(page.getByText('Upload complete')).toBeVisible();
222
+ });
223
+
224
+ test('drag-and-drop with hidden input fallback', async ({ page }) => {
225
+ await page.goto('/upload');
226
+
227
+ // Many drag-and-drop libraries still use a hidden <input type="file">
228
+ // Check for it first — this is more reliable than simulating DnD events
229
+ const hiddenInput = page.locator('input[type="file"]');
230
+
231
+ if (await hiddenInput.count() > 0) {
232
+ await hiddenInput.setInputFiles('fixtures/data.csv');
233
+ }
234
+
235
+ await expect(page.getByText('data.csv')).toBeVisible();
236
+ });
237
+ ```
238
+
239
+ **JavaScript**
240
+ ```javascript
241
+ const { test, expect } = require('@playwright/test');
242
+ const fs = require('fs');
243
+ const path = require('path');
244
+
245
+ test('drop file onto upload zone', async ({ page }) => {
246
+ await page.goto('/upload');
247
+
248
+ const filePath = path.join(__dirname, '../fixtures/data.csv');
249
+ const buffer = fs.readFileSync(filePath);
250
+
251
+ const dropZone = page.getByTestId('drop-zone');
252
+
253
+ await dropZone.dispatchEvent('drop', {
254
+ dataTransfer: {
255
+ files: [
256
+ { name: 'data.csv', mimeType: 'text/csv', buffer },
257
+ ],
258
+ },
259
+ });
260
+
261
+ await expect(page.getByText('data.csv')).toBeVisible();
262
+ await expect(page.getByText('Upload complete')).toBeVisible();
263
+ });
264
+
265
+ test('drag-and-drop with hidden input fallback', async ({ page }) => {
266
+ await page.goto('/upload');
267
+
268
+ const hiddenInput = page.locator('input[type="file"]');
269
+
270
+ if (await hiddenInput.count() > 0) {
271
+ await hiddenInput.setInputFiles('fixtures/data.csv');
272
+ }
273
+
274
+ await expect(page.getByText('data.csv')).toBeVisible();
275
+ });
276
+ ```
277
+
278
+ ### File Download — Wait and Verify
279
+
280
+ **Use when**: Testing export buttons, report generation, or any action that triggers a browser download.
281
+ **Avoid when**: The file is served as a page navigation (opens in a new tab). Use multi-tab patterns instead.
282
+
283
+ **TypeScript**
284
+ ```typescript
285
+ import { test, expect } from '@playwright/test';
286
+ import fs from 'fs';
287
+
288
+ test('download and verify file content', async ({ page }) => {
289
+ await page.goto('/reports');
290
+
291
+ // Register BEFORE the click that triggers the download
292
+ const downloadPromise = page.waitForEvent('download');
293
+ await page.getByRole('button', { name: 'Export CSV' }).click();
294
+ const download = await downloadPromise;
295
+
296
+ // Verify download metadata
297
+ expect(download.suggestedFilename()).toBe('report-2025.csv');
298
+
299
+ // Save to a known location
300
+ const savePath = 'test-results/report.csv';
301
+ await download.saveAs(savePath);
302
+
303
+ // Read and verify content
304
+ const content = fs.readFileSync(savePath, 'utf-8');
305
+ expect(content).toContain('Name,Email,Status');
306
+ expect(content).toContain('Jane Doe,jane@example.com,Active');
307
+
308
+ // Verify file size is reasonable
309
+ const stats = fs.statSync(savePath);
310
+ expect(stats.size).toBeGreaterThan(100);
311
+ });
312
+
313
+ test('download triggered by a link', async ({ page }) => {
314
+ await page.goto('/files');
315
+
316
+ const downloadPromise = page.waitForEvent('download');
317
+ await page.getByRole('link', { name: 'Download invoice' }).click();
318
+ const download = await downloadPromise;
319
+
320
+ expect(download.suggestedFilename()).toMatch(/invoice-\d+\.pdf/);
321
+
322
+ // Use download.path() for the temp file path
323
+ const tempPath = await download.path();
324
+ expect(tempPath).toBeTruthy();
325
+ });
326
+ ```
327
+
328
+ **JavaScript**
329
+ ```javascript
330
+ const { test, expect } = require('@playwright/test');
331
+ const fs = require('fs');
332
+
333
+ test('download and verify file content', async ({ page }) => {
334
+ await page.goto('/reports');
335
+
336
+ const downloadPromise = page.waitForEvent('download');
337
+ await page.getByRole('button', { name: 'Export CSV' }).click();
338
+ const download = await downloadPromise;
339
+
340
+ expect(download.suggestedFilename()).toBe('report-2025.csv');
341
+
342
+ const savePath = 'test-results/report.csv';
343
+ await download.saveAs(savePath);
344
+
345
+ const content = fs.readFileSync(savePath, 'utf-8');
346
+ expect(content).toContain('Name,Email,Status');
347
+ expect(content).toContain('Jane Doe,jane@example.com,Active');
348
+
349
+ const stats = fs.statSync(savePath);
350
+ expect(stats.size).toBeGreaterThan(100);
351
+ });
352
+
353
+ test('download triggered by a link', async ({ page }) => {
354
+ await page.goto('/files');
355
+
356
+ const downloadPromise = page.waitForEvent('download');
357
+ await page.getByRole('link', { name: 'Download invoice' }).click();
358
+ const download = await downloadPromise;
359
+
360
+ expect(download.suggestedFilename()).toMatch(/invoice-\d+\.pdf/);
361
+
362
+ const tempPath = await download.path();
363
+ expect(tempPath).toBeTruthy();
364
+ });
365
+ ```
366
+
367
+ ### Configuring Download Paths
368
+
369
+ **Use when**: You need downloads to go to a specific directory, or you need to disable the download dialog prompt.
370
+ **Avoid when**: Default temp paths via `download.path()` or `download.saveAs()` are sufficient.
371
+
372
+ **TypeScript**
373
+ ```typescript
374
+ // playwright.config.ts — global download behavior
375
+ import { defineConfig } from '@playwright/test';
376
+
377
+ export default defineConfig({
378
+ use: {
379
+ // Accept all downloads without prompting
380
+ acceptDownloads: true, // default is true
381
+ },
382
+ });
383
+ ```
384
+
385
+ ```typescript
386
+ import { test, expect } from '@playwright/test';
387
+ import fs from 'fs';
388
+ import path from 'path';
389
+
390
+ // Per-test download directory via a custom fixture
391
+ const downloadTest = test.extend<{ downloadDir: string }>({
392
+ downloadDir: async ({}, use, testInfo) => {
393
+ const dir = path.join('test-results', 'downloads', testInfo.title.replace(/\s+/g, '-'));
394
+ fs.mkdirSync(dir, { recursive: true });
395
+ await use(dir);
396
+ },
397
+ });
398
+
399
+ downloadTest('save downloads to organized directories', async ({ page, downloadDir }) => {
400
+ await page.goto('/exports');
401
+
402
+ const downloadPromise = page.waitForEvent('download');
403
+ await page.getByRole('button', { name: 'Export' }).click();
404
+ const download = await downloadPromise;
405
+
406
+ const savePath = path.join(downloadDir, download.suggestedFilename());
407
+ await download.saveAs(savePath);
408
+
409
+ expect(fs.existsSync(savePath)).toBe(true);
410
+ });
411
+ ```
412
+
413
+ **JavaScript**
414
+ ```javascript
415
+ // playwright.config.js
416
+ const { defineConfig } = require('@playwright/test');
417
+
418
+ module.exports = defineConfig({
419
+ use: {
420
+ acceptDownloads: true,
421
+ },
422
+ });
423
+ ```
424
+
425
+ ```javascript
426
+ const { test, expect } = require('@playwright/test');
427
+ const fs = require('fs');
428
+ const path = require('path');
429
+
430
+ const downloadTest = test.extend({
431
+ downloadDir: async ({}, use, testInfo) => {
432
+ const dir = path.join('test-results', 'downloads', testInfo.title.replace(/\s+/g, '-'));
433
+ fs.mkdirSync(dir, { recursive: true });
434
+ await use(dir);
435
+ },
436
+ });
437
+
438
+ downloadTest('save downloads to organized directories', async ({ page, downloadDir }) => {
439
+ await page.goto('/exports');
440
+
441
+ const downloadPromise = page.waitForEvent('download');
442
+ await page.getByRole('button', { name: 'Export' }).click();
443
+ const download = await downloadPromise;
444
+
445
+ const savePath = path.join(downloadDir, download.suggestedFilename());
446
+ await download.saveAs(savePath);
447
+
448
+ expect(fs.existsSync(savePath)).toBe(true);
449
+ });
450
+ ```
451
+
452
+ ### File Type Validation
453
+
454
+ **Use when**: Testing that the application rejects invalid file types and accepts valid ones.
455
+ **Avoid when**: The application does no client-side validation and relies entirely on server-side checks (test via API instead).
456
+
457
+ **TypeScript**
458
+ ```typescript
459
+ import { test, expect } from '@playwright/test';
460
+
461
+ test('rejects unsupported file types', async ({ page }) => {
462
+ await page.goto('/upload');
463
+
464
+ // Upload an invalid file type
465
+ await page.getByLabel('Upload image').setInputFiles({
466
+ name: 'malware.exe',
467
+ mimeType: 'application/octet-stream',
468
+ buffer: Buffer.from('fake-exe-content'),
469
+ });
470
+
471
+ await expect(page.getByText('Only JPG, PNG, and GIF files are allowed')).toBeVisible();
472
+ await expect(page.getByRole('button', { name: 'Submit' })).toBeDisabled();
473
+ });
474
+
475
+ test('accepts valid file types', async ({ page }) => {
476
+ await page.goto('/upload');
477
+
478
+ await page.getByLabel('Upload image').setInputFiles({
479
+ name: 'photo.jpg',
480
+ mimeType: 'image/jpeg',
481
+ buffer: Buffer.from('fake-jpg-content'),
482
+ });
483
+
484
+ await expect(page.getByText('photo.jpg')).toBeVisible();
485
+ await expect(page.getByRole('button', { name: 'Submit' })).toBeEnabled();
486
+ });
487
+
488
+ test('validates file size limits', async ({ page }) => {
489
+ await page.goto('/upload');
490
+
491
+ // Create a buffer that exceeds the 5MB limit
492
+ const largeBuffer = Buffer.alloc(6 * 1024 * 1024, 'x');
493
+
494
+ await page.getByLabel('Upload document').setInputFiles({
495
+ name: 'huge-file.pdf',
496
+ mimeType: 'application/pdf',
497
+ buffer: largeBuffer,
498
+ });
499
+
500
+ await expect(page.getByText('File size must be under 5MB')).toBeVisible();
501
+ });
502
+ ```
503
+
504
+ **JavaScript**
505
+ ```javascript
506
+ const { test, expect } = require('@playwright/test');
507
+
508
+ test('rejects unsupported file types', async ({ page }) => {
509
+ await page.goto('/upload');
510
+
511
+ await page.getByLabel('Upload image').setInputFiles({
512
+ name: 'malware.exe',
513
+ mimeType: 'application/octet-stream',
514
+ buffer: Buffer.from('fake-exe-content'),
515
+ });
516
+
517
+ await expect(page.getByText('Only JPG, PNG, and GIF files are allowed')).toBeVisible();
518
+ await expect(page.getByRole('button', { name: 'Submit' })).toBeDisabled();
519
+ });
520
+
521
+ test('accepts valid file types', async ({ page }) => {
522
+ await page.goto('/upload');
523
+
524
+ await page.getByLabel('Upload image').setInputFiles({
525
+ name: 'photo.jpg',
526
+ mimeType: 'image/jpeg',
527
+ buffer: Buffer.from('fake-jpg-content'),
528
+ });
529
+
530
+ await expect(page.getByText('photo.jpg')).toBeVisible();
531
+ await expect(page.getByRole('button', { name: 'Submit' })).toBeEnabled();
532
+ });
533
+
534
+ test('validates file size limits', async ({ page }) => {
535
+ await page.goto('/upload');
536
+
537
+ const largeBuffer = Buffer.alloc(6 * 1024 * 1024, 'x');
538
+
539
+ await page.getByLabel('Upload document').setInputFiles({
540
+ name: 'huge-file.pdf',
541
+ mimeType: 'application/pdf',
542
+ buffer: largeBuffer,
543
+ });
544
+
545
+ await expect(page.getByText('File size must be under 5MB')).toBeVisible();
546
+ });
547
+ ```
548
+
549
+ ### Large File Handling
550
+
551
+ **Use when**: Testing uploads or downloads of large files where timeouts and progress indicators matter.
552
+ **Avoid when**: Every test. Large file tests are slow. Run them in a separate suite or tag them for nightly runs.
553
+
554
+ **TypeScript**
555
+ ```typescript
556
+ import { test, expect } from '@playwright/test';
557
+ import fs from 'fs';
558
+ import path from 'path';
559
+
560
+ test.describe('large file operations', () => {
561
+ // Increase timeout for the entire describe block
562
+ test.slow(); // Triples the default timeout
563
+
564
+ test('upload large file with progress tracking', async ({ page }) => {
565
+ await page.goto('/upload');
566
+
567
+ // Create a test file on disk (avoid Buffer.alloc for very large files)
568
+ const largePath = path.join('test-results', 'large-test-file.bin');
569
+ const stream = fs.createWriteStream(largePath);
570
+ for (let i = 0; i < 100; i++) {
571
+ stream.write(Buffer.alloc(1024 * 1024, 'a')); // 100MB total
572
+ }
573
+ stream.end();
574
+ await new Promise((resolve) => stream.on('finish', resolve));
575
+
576
+ await page.getByLabel('Upload file').setInputFiles(largePath);
577
+
578
+ // Wait for progress indicator
579
+ await expect(page.getByRole('progressbar')).toBeVisible();
580
+
581
+ // Wait for completion — extended timeout
582
+ await expect(page.getByText('Upload complete')).toBeVisible({ timeout: 120_000 });
583
+
584
+ // Cleanup
585
+ fs.unlinkSync(largePath);
586
+ });
587
+
588
+ test('download large file', async ({ page }) => {
589
+ await page.goto('/exports');
590
+
591
+ const downloadPromise = page.waitForEvent('download');
592
+ await page.getByRole('button', { name: 'Export full dataset' }).click();
593
+ const download = await downloadPromise;
594
+
595
+ const savePath = 'test-results/large-export.zip';
596
+ await download.saveAs(savePath);
597
+
598
+ // Verify file size is reasonable (at least 10MB)
599
+ const stats = fs.statSync(savePath);
600
+ expect(stats.size).toBeGreaterThan(10 * 1024 * 1024);
601
+
602
+ fs.unlinkSync(savePath);
603
+ });
604
+ });
605
+ ```
606
+
607
+ **JavaScript**
608
+ ```javascript
609
+ const { test, expect } = require('@playwright/test');
610
+ const fs = require('fs');
611
+ const path = require('path');
612
+
613
+ test.describe('large file operations', () => {
614
+ test.slow();
615
+
616
+ test('upload large file with progress tracking', async ({ page }) => {
617
+ await page.goto('/upload');
618
+
619
+ const largePath = path.join('test-results', 'large-test-file.bin');
620
+ const stream = fs.createWriteStream(largePath);
621
+ for (let i = 0; i < 100; i++) {
622
+ stream.write(Buffer.alloc(1024 * 1024, 'a'));
623
+ }
624
+ stream.end();
625
+ await new Promise((resolve) => stream.on('finish', resolve));
626
+
627
+ await page.getByLabel('Upload file').setInputFiles(largePath);
628
+
629
+ await expect(page.getByRole('progressbar')).toBeVisible();
630
+ await expect(page.getByText('Upload complete')).toBeVisible({ timeout: 120_000 });
631
+
632
+ fs.unlinkSync(largePath);
633
+ });
634
+
635
+ test('download large file', async ({ page }) => {
636
+ await page.goto('/exports');
637
+
638
+ const downloadPromise = page.waitForEvent('download');
639
+ await page.getByRole('button', { name: 'Export full dataset' }).click();
640
+ const download = await downloadPromise;
641
+
642
+ const savePath = 'test-results/large-export.zip';
643
+ await download.saveAs(savePath);
644
+
645
+ const stats = fs.statSync(savePath);
646
+ expect(stats.size).toBeGreaterThan(10 * 1024 * 1024);
647
+
648
+ fs.unlinkSync(savePath);
649
+ });
650
+ });
651
+ ```
652
+
653
+ ## Decision Guide
654
+
655
+ | Scenario | Approach | Key API |
656
+ |---|---|---|
657
+ | Standard `<input type="file">` | `setInputFiles()` on the locator | `locator.setInputFiles(path)` |
658
+ | Hidden file input | Find the input even if hidden, call `setInputFiles()` | `page.locator('input[type="file"]').setInputFiles()` |
659
+ | Custom button opens file picker | Listen for `filechooser` event before clicking | `page.waitForEvent('filechooser')` |
660
+ | Drag-and-drop zone with no input | Dispatch `drop` event with `DataTransfer` | `locator.dispatchEvent('drop', ...)` |
661
+ | DnD zone with hidden input fallback | Prefer `setInputFiles()` on the hidden input | Check `input[type="file"]` count first |
662
+ | Multiple files at once | Pass array to `setInputFiles()` | `setInputFiles([path1, path2])` |
663
+ | In-memory test file (no disk) | Pass object with `name`, `mimeType`, `buffer` | `setInputFiles({ name, mimeType, buffer })` |
664
+ | Download — verify filename | `download.suggestedFilename()` | `page.waitForEvent('download')` |
665
+ | Download — verify content | `download.saveAs()` then read with `fs` | `fs.readFileSync()` |
666
+ | Download — temp file only | `download.path()` returns temp location | Auto-deleted after test |
667
+ | Large file upload/download | Use `test.slow()`, increase assertion timeouts | `{ timeout: 120_000 }` |
668
+
669
+ ## Anti-Patterns
670
+
671
+ | Don't Do This | Problem | Do This Instead |
672
+ |---|---|---|
673
+ | `await page.waitForTimeout(3000)` after upload | Arbitrary delay; flaky | `await expect(page.getByText('Upload complete')).toBeVisible()` |
674
+ | `await page.setInputFiles('#file', path)` using CSS selector | Fragile selector; breaks on ID changes | `await page.getByLabel('Upload').setInputFiles(path)` |
675
+ | Clicking download then immediately reading the file | Race condition; file may not be written yet | Use `page.waitForEvent('download')` then `download.saveAs()` |
676
+ | Creating large test files in `beforeAll` and never cleaning up | Fills disk in CI, slows down subsequent runs | Clean up in `afterAll` or use fixture teardown |
677
+ | Using `page.on('filechooser')` for every upload | Unnecessary complexity when `<input type="file">` exists | Use `setInputFiles()` directly |
678
+ | Hardcoding absolute file paths | Breaks across machines and CI | Use `path.join(__dirname, ...)` or relative to project root |
679
+ | Testing file upload with empty buffer | Does not test real validation behavior | Use realistic file content or minimum valid file size |
680
+ | Using `download.path()` for permanent storage | Temp files are cleaned up after test context closes | Use `download.saveAs()` for permanent paths |
681
+ | Uploading real 100MB files in every test run | Slows entire suite, wastes CI resources | Tag large file tests separately; run on schedule, not every PR |
682
+
683
+ ## Troubleshooting
684
+
685
+ ### "FileChooser event was not emitted"
686
+
687
+ **Cause**: The click did not open a native file picker dialog. The upload component may use a different mechanism.
688
+
689
+ ```typescript
690
+ // Debug: check if there is a hidden <input type="file"> you can target directly
691
+ const fileInputCount = await page.locator('input[type="file"]').count();
692
+ console.log(`Found ${fileInputCount} file inputs`);
693
+
694
+ // If input exists, skip the file chooser approach entirely
695
+ if (fileInputCount > 0) {
696
+ await page.locator('input[type="file"]').setInputFiles('fixtures/file.pdf');
697
+ }
698
+ ```
699
+
700
+ ### "Download event was not emitted"
701
+
702
+ **Cause**: The link opens in a new tab or navigates to the file URL instead of triggering a download.
703
+
704
+ ```typescript
705
+ // Fix 1: Ensure acceptDownloads is true in config
706
+ // Fix 2: Check if the link opens a new tab — handle it as a new page
707
+ const pagePromise = page.context().waitForEvent('page');
708
+ await page.getByRole('link', { name: 'Download' }).click();
709
+ const newPage = await pagePromise;
710
+ // Then wait for the download event on the new page
711
+ const download = await newPage.waitForEvent('download');
712
+ ```
713
+
714
+ ### Upload works locally but fails in CI
715
+
716
+ **Cause**: File paths are wrong in CI, or the fixture files are not included in the repo/build.
717
+
718
+ ```typescript
719
+ // Fix: always resolve paths relative to the test file
720
+ import path from 'path';
721
+ const fixturePath = path.join(__dirname, '..', 'fixtures', 'test-file.pdf');
722
+
723
+ // Verify the file exists before uploading
724
+ import fs from 'fs';
725
+ if (!fs.existsSync(fixturePath)) {
726
+ throw new Error(`Fixture file missing: ${fixturePath}`);
727
+ }
728
+ ```
729
+
730
+ ### `setInputFiles` does nothing — no file appears
731
+
732
+ **Cause**: The input element is detached, inside a Shadow DOM, or in an iframe.
733
+
734
+ ```typescript
735
+ // Check for iframe
736
+ const frame = page.frameLocator('iframe[title="Upload"]');
737
+ await frame.locator('input[type="file"]').setInputFiles('fixtures/file.pdf');
738
+
739
+ // Check for Shadow DOM — Playwright pierces open shadow roots automatically
740
+ // but the input may be in a closed shadow root (rare). Use the file chooser approach instead.
741
+ ```
742
+
743
+ ## Related
744
+
745
+ - [core/locators.md](locators.md) -- locator strategies for finding upload/download elements
746
+ - [core/assertions-and-waiting.md](assertions-and-waiting.md) -- assertion patterns for verifying upload/download results
747
+ - [core/fixtures-and-hooks.md](fixtures-and-hooks.md) -- creating reusable download directory fixtures
748
+ - [core/network-mocking.md](network-mocking.md) -- mocking upload endpoints for faster tests
749
+ - [core/error-and-edge-cases.md](error-and-edge-cases.md) -- testing upload failure states and error handling