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