# File Upload and Download Recipes > **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. --- ## Recipe 1: Single File Upload via Input ### Complete Example **TypeScript** ```typescript import { test, expect } from "@playwright/test" import path from "path" test("uploads a single file via file input", async ({ page }) => { await page.goto("/documents") // Click the upload button which triggers the hidden file input const fileInput = page.locator('input[type="file"]') // Upload a file from the test fixtures directory await fileInput.setInputFiles(path.resolve(__dirname, "../fixtures/report.pdf")) // Verify the file name appears in the UI await expect(page.getByText("report.pdf")).toBeVisible() // Click the submit/upload button await page.getByRole("button", { name: "Upload" }).click() // Wait for the upload to complete await expect(page.getByRole("alert")).toContainText("File uploaded successfully") // Verify the file appears in the documents list await expect(page.getByRole("link", { name: "report.pdf" })).toBeVisible() }) test("uploads a file created in-memory (no fixture file needed)", async ({ page }) => { await page.goto("/documents") const fileInput = page.locator('input[type="file"]') // Create a file from a buffer -- no fixture file required await fileInput.setInputFiles({ name: "test-data.csv", mimeType: "text/csv", buffer: Buffer.from("name,email,role\nJane,jane@example.com,admin\nBob,bob@example.com,user"), }) await expect(page.getByText("test-data.csv")).toBeVisible() await page.getByRole("button", { name: "Upload" }).click() await expect(page.getByRole("alert")).toContainText("File uploaded successfully") }) test("clears a selected file before uploading", async ({ page }) => { await page.goto("/documents") const fileInput = page.locator('input[type="file"]') // Select a file await fileInput.setInputFiles({ name: "wrong-file.txt", mimeType: "text/plain", buffer: Buffer.from("wrong content"), }) await expect(page.getByText("wrong-file.txt")).toBeVisible() // Clear the selection await fileInput.setInputFiles([]) // Or click a "Remove" button in the UI // await page.getByRole('button', { name: 'Remove' }).click(); await expect(page.getByText("wrong-file.txt")).not.toBeVisible() }) ``` **JavaScript** ```javascript const { test, expect } = require("@playwright/test") const path = require("path") test("uploads a single file via file input", async ({ page }) => { await page.goto("/documents") const fileInput = page.locator('input[type="file"]') await fileInput.setInputFiles(path.resolve(__dirname, "../fixtures/report.pdf")) await expect(page.getByText("report.pdf")).toBeVisible() await page.getByRole("button", { name: "Upload" }).click() await expect(page.getByRole("alert")).toContainText("File uploaded successfully") await expect(page.getByRole("link", { name: "report.pdf" })).toBeVisible() }) test("uploads a file created in-memory", async ({ page }) => { await page.goto("/documents") const fileInput = page.locator('input[type="file"]') await fileInput.setInputFiles({ name: "test-data.csv", mimeType: "text/csv", buffer: Buffer.from("name,email,role\nJane,jane@example.com,admin"), }) await expect(page.getByText("test-data.csv")).toBeVisible() await page.getByRole("button", { name: "Upload" }).click() await expect(page.getByRole("alert")).toContainText("File uploaded successfully") }) ``` --- ## Recipe 2: Multiple File Upload ### Complete Example **TypeScript** ```typescript import { test, expect } from "@playwright/test" import path from "path" test("uploads multiple files at once", async ({ page }) => { await page.goto("/documents") const fileInput = page.locator('input[type="file"]') // Pass an array to upload multiple files await fileInput.setInputFiles([ path.resolve(__dirname, "../fixtures/report.pdf"), path.resolve(__dirname, "../fixtures/photo.jpg"), path.resolve(__dirname, "../fixtures/data.csv"), ]) // Verify all file names appear await expect(page.getByText("report.pdf")).toBeVisible() await expect(page.getByText("photo.jpg")).toBeVisible() await expect(page.getByText("data.csv")).toBeVisible() // Verify the count indicator await expect(page.getByText("3 files selected")).toBeVisible() await page.getByRole("button", { name: "Upload all" }).click() await expect(page.getByRole("alert")).toContainText("3 files uploaded") }) test("uploads multiple files with in-memory buffers", async ({ page }) => { await page.goto("/documents") const fileInput = page.locator('input[type="file"]') await fileInput.setInputFiles([ { name: "notes.txt", mimeType: "text/plain", buffer: Buffer.from("Meeting notes for Q4 planning"), }, { name: "config.json", mimeType: "application/json", buffer: Buffer.from(JSON.stringify({ theme: "dark", lang: "en" })), }, ]) await expect(page.getByText("notes.txt")).toBeVisible() await expect(page.getByText("config.json")).toBeVisible() await page.getByRole("button", { name: "Upload all" }).click() await expect(page.getByRole("alert")).toContainText("2 files uploaded") }) test("removes one file from a multi-file selection", async ({ page }) => { await page.goto("/documents") const fileInput = page.locator('input[type="file"]') await fileInput.setInputFiles([ { name: "keep.txt", mimeType: "text/plain", buffer: Buffer.from("keep") }, { name: "remove.txt", mimeType: "text/plain", buffer: Buffer.from("remove") }, { name: "also-keep.txt", mimeType: "text/plain", buffer: Buffer.from("keep") }, ]) // Remove a specific file from the preview list const removeItem = page.getByText("remove.txt").locator("..") await removeItem.getByRole("button", { name: /remove|delete|×/i }).click() await expect(page.getByText("remove.txt")).not.toBeVisible() await expect(page.getByText("keep.txt")).toBeVisible() await expect(page.getByText("also-keep.txt")).toBeVisible() await expect(page.getByText("2 files selected")).toBeVisible() }) ``` **JavaScript** ```javascript const { test, expect } = require("@playwright/test") const path = require("path") test("uploads multiple files at once", async ({ page }) => { await page.goto("/documents") const fileInput = page.locator('input[type="file"]') await fileInput.setInputFiles([ path.resolve(__dirname, "../fixtures/report.pdf"), path.resolve(__dirname, "../fixtures/photo.jpg"), path.resolve(__dirname, "../fixtures/data.csv"), ]) await expect(page.getByText("report.pdf")).toBeVisible() await expect(page.getByText("photo.jpg")).toBeVisible() await expect(page.getByText("data.csv")).toBeVisible() await page.getByRole("button", { name: "Upload all" }).click() await expect(page.getByRole("alert")).toContainText("3 files uploaded") }) ``` --- ## Recipe 3: Drag-and-Drop File Upload ### Complete Example **TypeScript** ```typescript import { test, expect } from "@playwright/test" import path from "path" test("uploads a file via drag-and-drop zone", async ({ page }) => { await page.goto("/documents") const dropZone = page.locator('[data-testid="drop-zone"]') await expect(dropZone).toContainText(/drag.*here|drop.*files/i) // Drag-and-drop from the OS is not natively supported in Playwright, // but drop zones always have an underlying input[type=file] -- use that const fileInput = page.locator('input[type="file"]') await fileInput.setInputFiles(path.resolve(__dirname, "../fixtures/report.pdf")) // File preview should appear in the drop zone await expect(dropZone.getByText("report.pdf")).toBeVisible() // Trigger upload await page.getByRole("button", { name: "Upload" }).click() await expect(page.getByRole("alert")).toContainText("uploaded successfully") }) test("shows visual feedback during drag-over", async ({ page }) => { await page.goto("/documents") const dropZone = page.locator('[data-testid="drop-zone"]') // Simulate dragenter to show visual feedback await dropZone.dispatchEvent("dragenter", { dataTransfer: { types: ["Files"], files: [] }, }) // Drop zone should show an active state await expect(dropZone).toHaveClass(/active|highlight|drag-over/) await expect(dropZone).toContainText(/release|drop now/i) // Simulate dragleave to reset await dropZone.dispatchEvent("dragleave") await expect(dropZone).not.toHaveClass(/active|highlight|drag-over/) }) test("handles multiple files dropped at once", async ({ page }) => { await page.goto("/documents") const fileInput = page.locator('input[type="file"]') await fileInput.setInputFiles([ { name: "image1.png", mimeType: "image/png", buffer: Buffer.from("fake-png-1") }, { name: "image2.png", mimeType: "image/png", buffer: Buffer.from("fake-png-2") }, { name: "image3.png", mimeType: "image/png", buffer: Buffer.from("fake-png-3") }, ]) await expect(page.getByText("image1.png")).toBeVisible() await expect(page.getByText("image2.png")).toBeVisible() await expect(page.getByText("image3.png")).toBeVisible() }) ``` **JavaScript** ```javascript const { test, expect } = require("@playwright/test") const path = require("path") test("uploads a file via drag-and-drop zone", async ({ page }) => { await page.goto("/documents") const dropZone = page.locator('[data-testid="drop-zone"]') const fileInput = page.locator('input[type="file"]') await fileInput.setInputFiles(path.resolve(__dirname, "../fixtures/report.pdf")) await expect(dropZone.getByText("report.pdf")).toBeVisible() await page.getByRole("button", { name: "Upload" }).click() await expect(page.getByRole("alert")).toContainText("uploaded successfully") }) ``` --- ## Recipe 4: Download and Verify File Content ### Complete Example **TypeScript** ```typescript import { test, expect } from "@playwright/test" import fs from "fs" import path from "path" test("downloads a file and verifies its content", async ({ page }) => { await page.goto("/documents") // Start waiting for the download event BEFORE clicking const downloadPromise = page.waitForEvent("download") await page.getByRole("link", { name: "report.csv" }).click() const download = await downloadPromise // Save to a temporary location const downloadPath = path.join(__dirname, "../downloads", download.suggestedFilename()) await download.saveAs(downloadPath) // Read and verify the content const content = fs.readFileSync(downloadPath, "utf-8") expect(content).toContain("name,email,role") expect(content).toContain("Jane,jane@example.com,admin") // Verify line count const lines = content.trim().split("\n") expect(lines.length).toBeGreaterThan(1) // header + at least one data row // Clean up fs.unlinkSync(downloadPath) }) test("downloads a JSON file and verifies structure", async ({ page }) => { await page.goto("/api-docs") const downloadPromise = page.waitForEvent("download") await page.getByRole("button", { name: "Export API spec" }).click() const download = await downloadPromise const downloadPath = path.join(__dirname, "../downloads", download.suggestedFilename()) await download.saveAs(downloadPath) const content = JSON.parse(fs.readFileSync(downloadPath, "utf-8")) expect(content).toHaveProperty("openapi") expect(content.paths).toBeDefined() expect(Object.keys(content.paths).length).toBeGreaterThan(0) fs.unlinkSync(downloadPath) }) test("downloads a file via the stream (no disk save needed)", async ({ page }) => { await page.goto("/documents") const downloadPromise = page.waitForEvent("download") await page.getByRole("link", { name: "report.csv" }).click() const download = await downloadPromise // Read directly from the download stream without saving to disk const readable = await download.createReadStream() const chunks: Buffer[] = [] for await (const chunk of readable!) { chunks.push(Buffer.from(chunk)) } const content = Buffer.concat(chunks).toString("utf-8") expect(content).toContain("name,email,role") }) ``` **JavaScript** ```javascript const { test, expect } = require("@playwright/test") const fs = require("fs") const path = require("path") test("downloads a file and verifies its content", async ({ page }) => { await page.goto("/documents") const downloadPromise = page.waitForEvent("download") await page.getByRole("link", { name: "report.csv" }).click() const download = await downloadPromise const downloadPath = path.join(__dirname, "../downloads", download.suggestedFilename()) await download.saveAs(downloadPath) const content = fs.readFileSync(downloadPath, "utf-8") expect(content).toContain("name,email,role") expect(content).toContain("Jane,jane@example.com,admin") fs.unlinkSync(downloadPath) }) test("downloads a file via the stream", async ({ page }) => { await page.goto("/documents") const downloadPromise = page.waitForEvent("download") await page.getByRole("link", { name: "report.csv" }).click() const download = await downloadPromise const readable = await download.createReadStream() const chunks = [] for await (const chunk of readable) { chunks.push(Buffer.from(chunk)) } const content = Buffer.concat(chunks).toString("utf-8") expect(content).toContain("name,email,role") }) ``` --- ## Recipe 5: Download and Verify Filename ### Complete Example **TypeScript** ```typescript import { test, expect } from "@playwright/test" test("download has correct filename and extension", async ({ page }) => { await page.goto("/reports") const downloadPromise = page.waitForEvent("download") await page.getByRole("button", { name: "Export as PDF" }).click() const download = await downloadPromise // Verify filename pattern expect(download.suggestedFilename()).toMatch(/^report-\d{4}-\d{2}-\d{2}\.pdf$/) }) test("download filename changes based on selected format", async ({ page }) => { await page.goto("/reports") // Select CSV format await page.getByLabel("Export format").selectOption("csv") const csvDownload = page.waitForEvent("download") await page.getByRole("button", { name: "Download" }).click() const csv = await csvDownload expect(csv.suggestedFilename()).toMatch(/\.csv$/) // Select Excel format await page.getByLabel("Export format").selectOption("xlsx") const xlsxDownload = page.waitForEvent("download") await page.getByRole("button", { name: "Download" }).click() const xlsx = await xlsxDownload expect(xlsx.suggestedFilename()).toMatch(/\.xlsx$/) }) test("download has correct MIME type via response headers", async ({ page }) => { await page.goto("/reports") // Intercept the download request to check response headers const responsePromise = page.waitForResponse("**/api/reports/export**") const downloadPromise = page.waitForEvent("download") await page.getByRole("button", { name: "Export as PDF" }).click() const response = await responsePromise expect(response.headers()["content-type"]).toContain("application/pdf") expect(response.headers()["content-disposition"]).toContain("attachment") await downloadPromise // Consume the download event }) test("download failure shows error to user", async ({ page }) => { // Mock the download endpoint to fail await page.route("**/api/reports/export**", async (route) => { await route.fulfill({ status: 500, contentType: "application/json", body: JSON.stringify({ error: "Report generation failed" }), }) }) await page.goto("/reports") await page.getByRole("button", { name: "Export as PDF" }).click() await expect(page.getByRole("alert")).toContainText(/failed|error/i) }) ``` **JavaScript** ```javascript const { test, expect } = require("@playwright/test") test("download has correct filename and extension", async ({ page }) => { await page.goto("/reports") const downloadPromise = page.waitForEvent("download") await page.getByRole("button", { name: "Export as PDF" }).click() const download = await downloadPromise expect(download.suggestedFilename()).toMatch(/^report-\d{4}-\d{2}-\d{2}\.pdf$/) }) test("download filename changes based on selected format", async ({ page }) => { await page.goto("/reports") await page.getByLabel("Export format").selectOption("csv") const csvDownload = page.waitForEvent("download") await page.getByRole("button", { name: "Download" }).click() const csv = await csvDownload expect(csv.suggestedFilename()).toMatch(/\.csv$/) }) ``` --- ## Recipe 6: Large File Upload with Progress ### Complete Example **TypeScript** ```typescript import { test, expect } from "@playwright/test" test("shows upload progress for a large file", async ({ page }) => { await page.goto("/documents") const fileInput = page.locator('input[type="file"]') // Create a large in-memory file (5 MB) const largeBuffer = Buffer.alloc(5 * 1024 * 1024, "x") await fileInput.setInputFiles({ name: "large-dataset.bin", mimeType: "application/octet-stream", buffer: largeBuffer, }) await page.getByRole("button", { name: "Upload" }).click() // Verify progress bar appears const progressBar = page.getByRole("progressbar") await expect(progressBar).toBeVisible() // Verify progress percentage updates // Use polling to check that progress increases await expect(async () => { const value = await progressBar.getAttribute("aria-valuenow") expect(Number(value)).toBeGreaterThan(0) }).toPass({ timeout: 10000 }) // Wait for upload to complete await expect(progressBar).not.toBeVisible({ timeout: 60000 }) await expect(page.getByRole("alert")).toContainText("uploaded successfully") }) test("cancels an in-progress upload", async ({ page }) => { // Slow down the upload API to simulate a long upload await page.route("**/api/documents/upload", async (route) => { // Delay for 10 seconds to give us time to cancel await new Promise((resolve) => setTimeout(resolve, 10000)) await route.continue() }) await page.goto("/documents") const fileInput = page.locator('input[type="file"]') const largeBuffer = Buffer.alloc(5 * 1024 * 1024, "x") await fileInput.setInputFiles({ name: "large-file.bin", mimeType: "application/octet-stream", buffer: largeBuffer, }) await page.getByRole("button", { name: "Upload" }).click() // Wait for upload to start await expect(page.getByRole("progressbar")).toBeVisible() // Cancel the upload await page.getByRole("button", { name: "Cancel upload" }).click() // Verify upload was cancelled await expect(page.getByRole("progressbar")).not.toBeVisible() await expect(page.getByText(/cancelled|aborted/i)).toBeVisible() // Verify the file did not appear in the documents list await expect(page.getByRole("link", { name: "large-file.bin" })).not.toBeVisible() }) test("retries a failed upload", async ({ page }) => { let attempt = 0 await page.route("**/api/documents/upload", async (route) => { attempt++ if (attempt === 1) { // First attempt fails await route.fulfill({ status: 500, contentType: "application/json", body: JSON.stringify({ error: "Server error" }), }) } else { // Second attempt succeeds await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ id: "123", name: "data.csv" }), }) } }) await page.goto("/documents") const fileInput = page.locator('input[type="file"]') await fileInput.setInputFiles({ name: "data.csv", mimeType: "text/csv", buffer: Buffer.from("col1,col2\nval1,val2"), }) await page.getByRole("button", { name: "Upload" }).click() // First attempt fails await expect(page.getByText(/upload failed|error/i)).toBeVisible() // Retry await page.getByRole("button", { name: /retry/i }).click() // Second attempt succeeds await expect(page.getByRole("alert")).toContainText("uploaded successfully") expect(attempt).toBe(2) }) ``` **JavaScript** ```javascript const { test, expect } = require("@playwright/test") test("shows upload progress for a large file", async ({ page }) => { await page.goto("/documents") const fileInput = page.locator('input[type="file"]') const largeBuffer = Buffer.alloc(5 * 1024 * 1024, "x") await fileInput.setInputFiles({ name: "large-dataset.bin", mimeType: "application/octet-stream", buffer: largeBuffer, }) await page.getByRole("button", { name: "Upload" }).click() const progressBar = page.getByRole("progressbar") await expect(progressBar).toBeVisible() await expect(progressBar).not.toBeVisible({ timeout: 60000 }) await expect(page.getByRole("alert")).toContainText("uploaded successfully") }) test("cancels an in-progress upload", async ({ page }) => { await page.route("**/api/documents/upload", async (route) => { await new Promise((resolve) => setTimeout(resolve, 10000)) await route.continue() }) await page.goto("/documents") const fileInput = page.locator('input[type="file"]') await fileInput.setInputFiles({ name: "large-file.bin", mimeType: "application/octet-stream", buffer: Buffer.alloc(5 * 1024 * 1024, "x"), }) await page.getByRole("button", { name: "Upload" }).click() await expect(page.getByRole("progressbar")).toBeVisible() await page.getByRole("button", { name: "Cancel upload" }).click() await expect(page.getByRole("progressbar")).not.toBeVisible() await expect(page.getByText(/cancelled|aborted/i)).toBeVisible() }) ``` --- ## Recipe 7: Testing File Type Restrictions ### Complete Example **TypeScript** ```typescript import { test, expect } from "@playwright/test" test("accepts allowed file types", async ({ page }) => { await page.goto("/documents") const fileInput = page.locator('input[type="file"]') // Verify the accept attribute on the file input await expect(fileInput).toHaveAttribute("accept", /\.pdf|\.doc|\.docx|\.txt/) // Upload an allowed file type await fileInput.setInputFiles({ name: "valid-document.pdf", mimeType: "application/pdf", buffer: Buffer.from("fake-pdf-content"), }) // Should be accepted without error await expect(page.getByText("valid-document.pdf")).toBeVisible() await expect(page.getByText(/not allowed|invalid/i)).not.toBeVisible() }) test("rejects disallowed file types with clear error message", async ({ page }) => { await page.goto("/documents") const fileInput = page.locator('input[type="file"]') // Upload a disallowed file type // Note: setInputFiles bypasses the browser's accept attribute filter, // so the app's JavaScript validation is what we are testing here await fileInput.setInputFiles({ name: "script.exe", mimeType: "application/x-msdownload", buffer: Buffer.from("fake-exe-content"), }) // App should show a rejection message await expect(page.getByRole("alert")).toContainText( /not allowed|unsupported file type|only .pdf, .doc/i ) // File should not appear in the upload queue await expect(page.getByText("script.exe")).not.toBeVisible() }) test("enforces maximum file size", async ({ page }) => { await page.goto("/documents") const fileInput = page.locator('input[type="file"]') // Create a file that exceeds the max size (e.g., 11 MB when limit is 10 MB) const oversizedBuffer = Buffer.alloc(11 * 1024 * 1024, "x") await fileInput.setInputFiles({ name: "huge-file.pdf", mimeType: "application/pdf", buffer: oversizedBuffer, }) await expect(page.getByRole("alert")).toContainText(/file.*too large|exceeds.*10 ?MB/i) await expect(page.getByText("huge-file.pdf")).not.toBeVisible() }) test("enforces maximum number of files", async ({ page }) => { await page.goto("/documents") const fileInput = page.locator('input[type="file"]') // Try to upload more files than the limit (e.g., limit is 5) const files = Array.from({ length: 6 }, (_, i) => ({ name: `file-${i + 1}.txt`, mimeType: "text/plain" as const, buffer: Buffer.from(`content ${i + 1}`), })) await fileInput.setInputFiles(files) await expect(page.getByRole("alert")).toContainText(/maximum.*5 files|too many files/i) }) test("validates image dimensions for avatar upload", async ({ page }) => { await page.goto("/settings/profile") const fileInput = page.locator('input[type="file"]') // Create a tiny 1x1 PNG (valid format but too small) // Minimal PNG buffer const tinyPng = Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", "base64" ) await fileInput.setInputFiles({ name: "tiny-avatar.png", mimeType: "image/png", buffer: tinyPng, }) await expect(page.getByRole("alert")).toContainText(/minimum.*dimensions|too small/i) }) ``` **JavaScript** ```javascript const { test, expect } = require("@playwright/test") test("rejects disallowed file types with clear error message", async ({ page }) => { await page.goto("/documents") const fileInput = page.locator('input[type="file"]') await fileInput.setInputFiles({ name: "script.exe", mimeType: "application/x-msdownload", buffer: Buffer.from("fake-exe-content"), }) await expect(page.getByRole("alert")).toContainText( /not allowed|unsupported file type|only .pdf, .doc/i ) await expect(page.getByText("script.exe")).not.toBeVisible() }) test("enforces maximum file size", async ({ page }) => { await page.goto("/documents") const fileInput = page.locator('input[type="file"]') const oversizedBuffer = Buffer.alloc(11 * 1024 * 1024, "x") await fileInput.setInputFiles({ name: "huge-file.pdf", mimeType: "application/pdf", buffer: oversizedBuffer, }) await expect(page.getByRole("alert")).toContainText(/file.*too large|exceeds.*10 ?MB/i) }) test("enforces maximum number of files", async ({ page }) => { await page.goto("/documents") const fileInput = page.locator('input[type="file"]') const files = Array.from({ length: 6 }, (_, i) => ({ name: `file-${i + 1}.txt`, mimeType: "text/plain", buffer: Buffer.from(`content ${i + 1}`), })) await fileInput.setInputFiles(files) await expect(page.getByRole("alert")).toContainText(/maximum.*5 files|too many files/i) }) ``` --- ## Variations ### Upload via File Chooser Dialog ```typescript test("uploads via the native file chooser dialog", async ({ page }) => { await page.goto("/documents") // Listen for the file chooser event before clicking the trigger const fileChooserPromise = page.waitForEvent("filechooser") await page.getByRole("button", { name: "Choose file" }).click() const fileChooser = await fileChooserPromise // Verify it accepts only certain types expect(fileChooser.isMultiple()).toBe(false) await fileChooser.setFiles({ name: "chosen-file.pdf", mimeType: "application/pdf", buffer: Buffer.from("pdf-content"), }) await expect(page.getByText("chosen-file.pdf")).toBeVisible() }) ``` ### Upload with Image Preview ```typescript test("shows image preview after selecting a file", async ({ page }) => { await page.goto("/settings/profile") const fileInput = page.locator('input[type="file"]') await fileInput.setInputFiles(path.resolve(__dirname, "../fixtures/avatar.jpg")) // Verify the image preview is displayed const preview = page.getByRole("img", { name: /preview|avatar/i }) await expect(preview).toBeVisible() // Verify the preview src is a blob or data URL const src = await preview.getAttribute("src") expect(src).toMatch(/^(blob:|data:image)/) }) ``` ### Download with Authentication ```typescript test("downloads a file that requires authentication", async ({ page, request }) => { // Some downloads go through an API that needs auth cookies await page.goto("/documents") // The UI download works because the browser sends cookies const downloadPromise = page.waitForEvent("download") await page.getByRole("link", { name: "confidential-report.pdf" }).click() const download = await downloadPromise expect(download.suggestedFilename()).toBe("confidential-report.pdf") // Alternatively, verify via API request (which carries the auth context) const response = await request.get("/api/documents/123/download") expect(response.ok()).toBeTruthy() expect(response.headers()["content-type"]).toContain("application/pdf") }) ``` --- ## Tips 1. **Use `setInputFiles` for upload testing**. Even if the UI uses a drag-and-drop zone, there is always an underlying `input[type="file"]` element. Target it directly with `setInputFiles()` instead of trying to simulate OS-level drag events. 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). 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. 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. 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. --- ## Related - [Playwright Upload Docs](https://playwright.dev/docs/input#upload-files) - [Playwright Download Docs](https://playwright.dev/docs/downloads) - `recipes/drag-and-drop.md` -- General drag and drop patterns - `recipes/crud-testing.md` -- File uploads as part of resource creation - `foundations/actions.md` -- Input interaction fundamentals