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