stably 4.10.4 → 4.10.5

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 (38) hide show
  1. package/dist/index.mjs +1 -1
  2. package/dist/stably-plugin-cli/skills/browser-interaction-guide/SKILL.md +21 -0
  3. package/dist/stably-plugin-cli/skills/playwright-best-practices/references/accessibility.md +359 -0
  4. package/dist/stably-plugin-cli/skills/playwright-best-practices/references/annotations.md +526 -0
  5. package/dist/stably-plugin-cli/skills/playwright-best-practices/references/assertions-waiting.md +361 -0
  6. package/dist/stably-plugin-cli/skills/playwright-best-practices/references/browser-apis.md +391 -0
  7. package/dist/stably-plugin-cli/skills/playwright-best-practices/references/browser-extensions.md +506 -0
  8. package/dist/stably-plugin-cli/skills/playwright-best-practices/references/canvas-webgl.md +493 -0
  9. package/dist/stably-plugin-cli/skills/playwright-best-practices/references/ci-cd.md +407 -0
  10. package/dist/stably-plugin-cli/skills/playwright-best-practices/references/clock-mocking.md +364 -0
  11. package/dist/stably-plugin-cli/skills/playwright-best-practices/references/component-testing.md +500 -0
  12. package/dist/stably-plugin-cli/skills/playwright-best-practices/references/console-errors.md +420 -0
  13. package/dist/stably-plugin-cli/skills/playwright-best-practices/references/debugging.md +491 -0
  14. package/dist/stably-plugin-cli/skills/playwright-best-practices/references/electron.md +509 -0
  15. package/dist/stably-plugin-cli/skills/playwright-best-practices/references/error-testing.md +360 -0
  16. package/dist/stably-plugin-cli/skills/playwright-best-practices/references/file-operations.md +375 -0
  17. package/dist/stably-plugin-cli/skills/playwright-best-practices/references/fixtures-hooks.md +417 -0
  18. package/dist/stably-plugin-cli/skills/playwright-best-practices/references/flaky-tests.md +494 -0
  19. package/dist/stably-plugin-cli/skills/playwright-best-practices/references/global-setup.md +434 -0
  20. package/dist/stably-plugin-cli/skills/playwright-best-practices/references/i18n.md +508 -0
  21. package/dist/stably-plugin-cli/skills/playwright-best-practices/references/iframes.md +403 -0
  22. package/dist/stably-plugin-cli/skills/playwright-best-practices/references/locators.md +242 -0
  23. package/dist/stably-plugin-cli/skills/playwright-best-practices/references/mobile-testing.md +409 -0
  24. package/dist/stably-plugin-cli/skills/playwright-best-practices/references/multi-context.md +288 -0
  25. package/dist/stably-plugin-cli/skills/playwright-best-practices/references/multi-user.md +393 -0
  26. package/dist/stably-plugin-cli/skills/playwright-best-practices/references/network-advanced.md +452 -0
  27. package/dist/stably-plugin-cli/skills/playwright-best-practices/references/page-object-model.md +315 -0
  28. package/dist/stably-plugin-cli/skills/playwright-best-practices/references/performance-testing.md +476 -0
  29. package/dist/stably-plugin-cli/skills/playwright-best-practices/references/performance.md +453 -0
  30. package/dist/stably-plugin-cli/skills/playwright-best-practices/references/projects-dependencies.md +456 -0
  31. package/dist/stably-plugin-cli/skills/playwright-best-practices/references/security-testing.md +430 -0
  32. package/dist/stably-plugin-cli/skills/playwright-best-practices/references/service-workers.md +504 -0
  33. package/dist/stably-plugin-cli/skills/playwright-best-practices/references/test-coverage.md +495 -0
  34. package/dist/stably-plugin-cli/skills/playwright-best-practices/references/test-data.md +492 -0
  35. package/dist/stably-plugin-cli/skills/playwright-best-practices/references/test-organization.md +361 -0
  36. package/dist/stably-plugin-cli/skills/playwright-best-practices/references/third-party.md +464 -0
  37. package/dist/stably-plugin-cli/skills/playwright-best-practices/references/websockets.md +403 -0
  38. package/package.json +1 -1
@@ -0,0 +1,375 @@
1
+ # File Upload & Download Testing
2
+
3
+ ## Table of Contents
4
+
5
+ 1. [File Downloads](#file-downloads)
6
+ 2. [File Uploads](#file-uploads)
7
+ 3. [Drag and Drop](#drag-and-drop)
8
+ 4. [File Content Verification](#file-content-verification)
9
+
10
+ ## File Downloads
11
+
12
+ ### Basic Download
13
+
14
+ ```typescript
15
+ test("download PDF report", async ({ page }) => {
16
+ await page.goto("/reports");
17
+
18
+ // Start waiting for download before clicking
19
+ const downloadPromise = page.waitForEvent("download");
20
+ await page.getByRole("button", { name: "Download PDF" }).click();
21
+ const download = await downloadPromise;
22
+
23
+ // Verify filename
24
+ expect(download.suggestedFilename()).toBe("report.pdf");
25
+
26
+ // Save to specific path
27
+ await download.saveAs("./downloads/report.pdf");
28
+ });
29
+ ```
30
+
31
+ ### Download with Custom Path
32
+
33
+ ```typescript
34
+ test("download to temp directory", async ({ page }, testInfo) => {
35
+ await page.goto("/exports");
36
+
37
+ const downloadPromise = page.waitForEvent("download");
38
+ await page.getByRole("link", { name: "Export CSV" }).click();
39
+ const download = await downloadPromise;
40
+
41
+ // Save to test output directory
42
+ const path = testInfo.outputPath(download.suggestedFilename());
43
+ await download.saveAs(path);
44
+
45
+ // Attach to test report
46
+ await testInfo.attach("downloaded-file", { path });
47
+ });
48
+ ```
49
+
50
+ ### Verify Download Content
51
+
52
+ ```typescript
53
+ import fs from "fs";
54
+ import path from "path";
55
+
56
+ test("verify CSV content", async ({ page }, testInfo) => {
57
+ await page.goto("/data");
58
+
59
+ const downloadPromise = page.waitForEvent("download");
60
+ await page.getByRole("button", { name: "Export" }).click();
61
+ const download = await downloadPromise;
62
+
63
+ const filePath = testInfo.outputPath("export.csv");
64
+ await download.saveAs(filePath);
65
+
66
+ // Read and verify content
67
+ const content = fs.readFileSync(filePath, "utf-8");
68
+ expect(content).toContain("Name,Email,Status");
69
+ expect(content).toContain("John Doe");
70
+
71
+ // Verify row count
72
+ const rows = content.trim().split("\n");
73
+ expect(rows.length).toBeGreaterThan(1);
74
+ });
75
+ ```
76
+
77
+ ### Multiple Downloads
78
+
79
+ ```typescript
80
+ test("download multiple files", async ({ page }) => {
81
+ await page.goto("/batch-export");
82
+
83
+ await page.getByRole("checkbox", { name: "Select All" }).check();
84
+
85
+ // Collect all downloads
86
+ const downloads: Download[] = [];
87
+ page.on("download", (download) => downloads.push(download));
88
+
89
+ await page.getByRole("button", { name: "Download Selected" }).click();
90
+
91
+ // Wait for all downloads
92
+ await expect.poll(() => downloads.length, { timeout: 30000 }).toBe(5);
93
+
94
+ // Verify each download
95
+ for (const download of downloads) {
96
+ expect(download.suggestedFilename()).toMatch(/\.pdf$/);
97
+ }
98
+ });
99
+ ```
100
+
101
+ ### Download Fixture
102
+
103
+ ```typescript
104
+ // fixtures/download.fixture.ts
105
+ import { test as base, Download } from "@playwright/test";
106
+ import fs from "fs";
107
+ import path from "path";
108
+
109
+ type DownloadFixtures = {
110
+ downloadDir: string;
111
+ downloadAndVerify: (
112
+ trigger: () => Promise<void>,
113
+ expectedFilename: string,
114
+ ) => Promise<string>;
115
+ };
116
+
117
+ export const test = base.extend<DownloadFixtures>({
118
+ downloadDir: async ({}, use, testInfo) => {
119
+ const dir = testInfo.outputPath("downloads");
120
+ fs.mkdirSync(dir, { recursive: true });
121
+ await use(dir);
122
+ },
123
+
124
+ downloadAndVerify: async ({ page, downloadDir }, use) => {
125
+ await use(async (trigger, expectedFilename) => {
126
+ const downloadPromise = page.waitForEvent("download");
127
+ await trigger();
128
+ const download = await downloadPromise;
129
+
130
+ expect(download.suggestedFilename()).toBe(expectedFilename);
131
+
132
+ const filePath = path.join(downloadDir, expectedFilename);
133
+ await download.saveAs(filePath);
134
+ return filePath;
135
+ });
136
+ },
137
+ });
138
+ ```
139
+
140
+ ## File Uploads
141
+
142
+ ### Basic Upload
143
+
144
+ ```typescript
145
+ test("upload profile picture", async ({ page }) => {
146
+ await page.goto("/settings/profile");
147
+
148
+ // Upload file
149
+ await page
150
+ .getByLabel("Profile Picture")
151
+ .setInputFiles("./fixtures/avatar.png");
152
+
153
+ // Verify preview
154
+ await expect(page.getByAltText("Profile preview")).toBeVisible();
155
+
156
+ await page.getByRole("button", { name: "Save" }).click();
157
+ await expect(page.getByText("Profile updated")).toBeVisible();
158
+ });
159
+ ```
160
+
161
+ ### Multiple File Upload
162
+
163
+ ```typescript
164
+ test("upload multiple documents", async ({ page }) => {
165
+ await page.goto("/documents/upload");
166
+
167
+ await page
168
+ .getByLabel("Documents")
169
+ .setInputFiles([
170
+ "./fixtures/doc1.pdf",
171
+ "./fixtures/doc2.pdf",
172
+ "./fixtures/doc3.pdf",
173
+ ]);
174
+
175
+ // Verify all files listed
176
+ await expect(page.getByText("doc1.pdf")).toBeVisible();
177
+ await expect(page.getByText("doc2.pdf")).toBeVisible();
178
+ await expect(page.getByText("doc3.pdf")).toBeVisible();
179
+
180
+ await page.getByRole("button", { name: "Upload All" }).click();
181
+ await expect(page.getByText("3 files uploaded")).toBeVisible();
182
+ });
183
+ ```
184
+
185
+ ### Upload with File Chooser
186
+
187
+ ```typescript
188
+ test("upload via file chooser dialog", async ({ page }) => {
189
+ await page.goto("/upload");
190
+
191
+ // Handle file chooser
192
+ const fileChooserPromise = page.waitForEvent("filechooser");
193
+ await page.getByRole("button", { name: "Choose File" }).click();
194
+ const fileChooser = await fileChooserPromise;
195
+
196
+ await fileChooser.setFiles("./fixtures/document.pdf");
197
+
198
+ await expect(page.getByText("document.pdf")).toBeVisible();
199
+ });
200
+ ```
201
+
202
+ ### Clear and Re-upload
203
+
204
+ ```typescript
205
+ test("replace uploaded file", async ({ page }) => {
206
+ await page.goto("/upload");
207
+
208
+ const input = page.getByLabel("Document");
209
+
210
+ // Upload first file
211
+ await input.setInputFiles("./fixtures/old.pdf");
212
+ await expect(page.getByText("old.pdf")).toBeVisible();
213
+
214
+ // Clear selection
215
+ await input.setInputFiles([]);
216
+
217
+ // Upload new file
218
+ await input.setInputFiles("./fixtures/new.pdf");
219
+ await expect(page.getByText("new.pdf")).toBeVisible();
220
+ await expect(page.getByText("old.pdf")).toBeHidden();
221
+ });
222
+ ```
223
+
224
+ ### Upload from Buffer
225
+
226
+ ```typescript
227
+ test("upload generated file", async ({ page }) => {
228
+ await page.goto("/upload");
229
+
230
+ // Create file content dynamically
231
+ const content = "Name,Email\nJohn,john@example.com";
232
+
233
+ await page.getByLabel("CSV File").setInputFiles({
234
+ name: "users.csv",
235
+ mimeType: "text/csv",
236
+ buffer: Buffer.from(content),
237
+ });
238
+
239
+ await expect(page.getByText("users.csv")).toBeVisible();
240
+ });
241
+ ```
242
+
243
+ ## Drag and Drop
244
+
245
+ ### Drag and Drop Upload
246
+
247
+ ```typescript
248
+ test("drag and drop file upload", async ({ page }) => {
249
+ await page.goto("/upload");
250
+
251
+ const dropzone = page.getByTestId("dropzone");
252
+
253
+ // Create a DataTransfer with the file
254
+ const dataTransfer = await page.evaluateHandle(() => new DataTransfer());
255
+
256
+ // Read file and add to DataTransfer
257
+ const buffer = fs.readFileSync("./fixtures/image.png");
258
+ await page.evaluate(
259
+ async ([dataTransfer, data]) => {
260
+ const file = new File([new Uint8Array(data)], "image.png", {
261
+ type: "image/png",
262
+ });
263
+ dataTransfer.items.add(file);
264
+ },
265
+ [dataTransfer, [...buffer]] as const,
266
+ );
267
+
268
+ // Dispatch drop event
269
+ await dropzone.dispatchEvent("drop", { dataTransfer });
270
+
271
+ await expect(page.getByText("image.png uploaded")).toBeVisible();
272
+ });
273
+ ```
274
+
275
+ ### Simpler Drag and Drop
276
+
277
+ ```typescript
278
+ test("drag and drop with setInputFiles", async ({ page }) => {
279
+ await page.goto("/upload");
280
+
281
+ // Most dropzones have a hidden file input
282
+ const input = page.locator('input[type="file"]');
283
+
284
+ // This works even if the input is hidden
285
+ await input.setInputFiles("./fixtures/document.pdf");
286
+
287
+ await expect(page.getByText("document.pdf")).toBeVisible();
288
+ });
289
+ ```
290
+
291
+ ## File Content Verification
292
+
293
+ ### Verify PDF Content
294
+
295
+ ```typescript
296
+ import pdf from "pdf-parse";
297
+
298
+ test("verify PDF content", async ({ page }, testInfo) => {
299
+ await page.goto("/invoice/123");
300
+
301
+ const downloadPromise = page.waitForEvent("download");
302
+ await page.getByRole("button", { name: "Download Invoice" }).click();
303
+ const download = await downloadPromise;
304
+
305
+ const path = testInfo.outputPath("invoice.pdf");
306
+ await download.saveAs(path);
307
+
308
+ // Parse PDF
309
+ const dataBuffer = fs.readFileSync(path);
310
+ const data = await pdf(dataBuffer);
311
+
312
+ expect(data.text).toContain("Invoice #123");
313
+ expect(data.text).toContain("Total: $99.99");
314
+ });
315
+ ```
316
+
317
+ ### Verify Excel Content
318
+
319
+ ```typescript
320
+ import XLSX from "xlsx";
321
+
322
+ test("verify Excel export", async ({ page }, testInfo) => {
323
+ await page.goto("/reports");
324
+
325
+ const downloadPromise = page.waitForEvent("download");
326
+ await page.getByRole("button", { name: "Export Excel" }).click();
327
+ const download = await downloadPromise;
328
+
329
+ const path = testInfo.outputPath("report.xlsx");
330
+ await download.saveAs(path);
331
+
332
+ // Parse Excel
333
+ const workbook = XLSX.readFile(path);
334
+ const sheet = workbook.Sheets[workbook.SheetNames[0]];
335
+ const data = XLSX.utils.sheet_to_json(sheet);
336
+
337
+ expect(data).toHaveLength(10);
338
+ expect(data[0]).toHaveProperty("Name");
339
+ expect(data[0]).toHaveProperty("Email");
340
+ });
341
+ ```
342
+
343
+ ### Verify JSON Download
344
+
345
+ ```typescript
346
+ test("verify JSON export", async ({ page }, testInfo) => {
347
+ await page.goto("/api-data");
348
+
349
+ const downloadPromise = page.waitForEvent("download");
350
+ await page.getByRole("button", { name: "Export JSON" }).click();
351
+ const download = await downloadPromise;
352
+
353
+ const path = testInfo.outputPath("data.json");
354
+ await download.saveAs(path);
355
+
356
+ const content = JSON.parse(fs.readFileSync(path, "utf-8"));
357
+
358
+ expect(content.users).toHaveLength(5);
359
+ expect(content.exportDate).toBeDefined();
360
+ });
361
+ ```
362
+
363
+ ## Anti-Patterns to Avoid
364
+
365
+ | Anti-Pattern | Problem | Solution |
366
+ | ------------------------------------- | ------------------------------- | --------------------------------------------- |
367
+ | Not waiting for download | Race condition, test fails | Always use `waitForEvent("download")` |
368
+ | Hardcoded download paths | Conflicts in parallel runs | Use `testInfo.outputPath()` |
369
+ | Skipping content verification | Download might be empty/corrupt | Verify file content when possible |
370
+ | Using `force: true` for hidden inputs | May not trigger proper events | Use `setInputFiles` on hidden inputs directly |
371
+
372
+ ## Related References
373
+
374
+ - **Fixtures**: See [fixtures-hooks.md](fixtures-hooks.md) for download fixture patterns
375
+ - **Debugging**: See [debugging.md](debugging.md) for troubleshooting download issues