# 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
