Back to guides

GUIDE / automation

How to Automate File Download Testing

How to automate file download testing with Playwright and Selenium: wait for files, verify names, types, size, content checks, and common pitfalls.

By The Testing AcademyPublished July 9, 2026Updated July 9, 202616 min read

Exports and invoice PDFs fail in ways a green click path never shows: wrong filename, empty CSV, unauthorized file, or a browser download that never lands. Automating file download testing proves generation, delivery, naming, and permission boundaries under realistic user actions.

This guide walks through what to validate, how to automate downloads in Playwright and Selenium, how to assert file content safely, how to avoid flaky download suites, and how to design cases that match business risk. For broader automation foundations, see the Playwright tutorial and the tool comparison in Selenium vs Playwright vs Cypress.

Why File Download Testing Matters

Downloads sit at the intersection of UI, API, authorization, and document generation. A green "Export" toast does not guarantee a valid file. Common production failures include:

  • Empty CSV with only headers when filters are wrong.
  • PDF invoices with missing line items.
  • Wrong filename that overwrites previous exports.
  • Downloads that work for admins but fail for standard users.
  • Content-Type mismatches that confuse browsers.
  • Files that start downloading then fail mid-stream.

Automated download checks protect revenue and trust in products where documents are part of the contract with the user: finance, healthcare, logistics, education, and B2B SaaS reporting.

What "Done" Means for a Download Test

A complete automated check usually answers several questions:

  1. Did the download start after the user action?
  2. Did it finish without browser errors?
  3. Is the filename correct or acceptably patterned?
  4. Is the extension and rough size correct?
  5. Is the content structure valid?
  6. Are unauthorized users blocked?

Not every test needs deep content parsing. Use layers.

LayerExample assertionCostValue
EventDownload event firedLowConfirms wiring
ArtifactFile exists, size > 0LowConfirms delivery
MetadataName, extension, MIME expectationsLowCatches naming bugs
StructureCSV headers, PDF opens, JSON parsesMediumCatches generator bugs
Business contentTotals, user name, date rangeHigherCatches logic bugs
SecurityOther user cannot download this idMediumCatches auth bugs

Design Test Cases Before Automation

Automation without case design becomes a click demo. Start with scenarios the way you would for manual testing. If you need a refresher on structure, use how to write test cases.

Core Scenarios for Downloads

  • Valid export for an authorized user.
  • Export with filters applied (date range, status, owner).
  • Empty result set behavior (empty file vs message, depending on product rules).
  • Large export behavior (async job vs immediate file).
  • Unsupported format selection handling.
  • Permission denied for unauthorized role.
  • Expired or invalid download link.
  • Multiple sequential downloads without collision.
  • Special characters in filename.
  • Concurrent exports in parallel test runs.

Example Case Table

IDTitleExpected ResultPriority
DL-001Buyer exports own invoice PDFPDF downloads, name contains invoice id, size > 0High
DL-002Buyer cannot export another account invoice403 or safe error, no fileHigh
DL-003Admin exports users CSV for active filterCSV has expected headers and only active usersHigh
DL-004Export with no matching rowsProduct-defined empty behavior, no crashMedium
DL-005Report filename includes date stampName matches agreed patternMedium

How to Automate File Download Testing in Playwright

Playwright has first-class download APIs, which is one reason many teams prefer it for this class of test.

Basic Pattern

import { test, expect } from '@playwright/test';
import fs from 'fs';
import path from 'path';

test('downloads invoice PDF', async ({ page }) => {
  await page.goto('/invoices/inv_123');

  const downloadPromise = page.waitForEvent('download');
  await page.getByTestId('download-invoice').click();
  const download = await downloadPromise;

  expect(download.suggestedFilename()).toMatch(/inv_123.*\.pdf$/i);

  const target = path.join('/tmp', `inv_123-${Date.now()}.pdf`);
  await download.saveAs(target);

  const stats = fs.statSync(target);
  expect(stats.size).toBeGreaterThan(1000);
});

Why this pattern works:

  • waitForEvent('download') is tied to the user action.
  • Suggested filename comes from content-disposition style metadata.
  • saveAs puts the file in a path you control.
  • Size assertion catches empty or truncated files.

Verify CSV Content

import { parse } from 'csv-parse/sync';

test('exports orders CSV with headers', async ({ page }) => {
  await page.goto('/orders');
  const downloadPromise = page.waitForEvent('download');
  await page.getByRole('button', { name: 'Export CSV' }).click();
  const download = await downloadPromise;
  const filePath = `/tmp/orders-${Date.now()}.csv`;
  await download.saveAs(filePath);

  const text = fs.readFileSync(filePath, 'utf8');
  const records = parse(text, { columns: true, skip_empty_lines: true });

  expect(Object.keys(records[0] || {})).toEqual(
    expect.arrayContaining(['order_id', 'status', 'total'])
  );
});

Handling New Tab or Direct Navigation Downloads

Some apps open a URL that streams a file. You may still capture a download event, or you may request the file via API with the same auth cookie. Prefer the path closest to the real user while remaining stable in CI.

Playwright Tips

  • Use unique target paths per test and worker.
  • Clean up files in afterEach if disk space matters.
  • Prefer suggested filename assertions with flexible regex when timestamps are included.
  • For auth-sensitive files, always include negative permission cases.

How to Automate File Download Testing in Selenium

Selenium usually needs browser preference configuration so downloads skip the prompt and land in a known folder.

Chrome Example (Java-style options concept in Node)

// conceptual WebDriver Chrome options
const downloadDir = '/tmp/selenium-downloads';
const chromeOptions = {
  prefs: {
    'download.default_directory': downloadDir,
    'download.prompt_for_download': false,
    'download.directory_upgrade': true,
    'safebrowsing.enabled': true,
  },
};

Wait for Completion

Browsers may create temporary files (.crdownload, .part) while downloading. Waiting only for "file exists" is not enough.

// pseudo-helper
async function waitForDownload(dir, pattern, timeoutMs = 30000) {
  const start = Date.now();
  while (Date.now() - start < timeoutMs) {
    const files = fs.readdirSync(dir);
    const match = files.find(
      (f) => pattern.test(f) && !f.endsWith('.crdownload') && !f.endsWith('.part')
    );
    if (match) {
      const full = path.join(dir, match);
      const size1 = fs.statSync(full).size;
      await new Promise((r) => setTimeout(r, 300));
      const size2 = fs.statSync(full).size;
      if (size1 > 0 && size1 === size2) return full;
    }
    await new Promise((r) => setTimeout(r, 200));
  }
  throw new Error('Download not completed in time');
}

Selenium Pitfalls

  • Headless differences by browser version.
  • OS permission issues on CI runners.
  • Parallel tests writing the same default filename.
  • Save As dialogs when prefs are incomplete.
  • Corporate antivirus locking new files briefly.

If you can choose freely for new greenfield UI automation, Playwright's download event model is often simpler. If Selenium is already standard, invest in solid directory isolation and completion waits.

Content Validation Strategies

1. Presence and Size

Cheapest and often enough for smoke:

expect(fs.existsSync(filePath)).toBeTruthy();
expect(fs.statSync(filePath).size).toBeGreaterThan(0);

2. Filename Contracts

Agree with product on a pattern:

  • invoice-<id>.pdf
  • orders-export-YYYYMMDD.csv
  • report-<uuid>.xlsx

Assert with regex, not exact timestamps, unless time is frozen in the test environment.

3. Structural Validation

TypeStructural checks
CSVHeaders, delimiter, row count bounds
JSONParses, required keys present
PDFOpens with a library, page count > 0
XLSXSheet names, header row
ZIPExpected entry names inside archive
PNG/JPGValid image headers, dimensions if relevant

4. Business Content Validation

Examples:

  • Invoice total matches UI total.
  • CSV date range matches selected filters.
  • PDF customer name matches account.
  • Exported count matches table count for the same filter.

Keep a few deep content tests. Do not parse entire multi-megabyte reports in every PR if a structural smoke is enough.

5. Checksums When Comparing Known Fixtures

When output is deterministic:

import crypto from 'crypto';

function sha256(filePath: string) {
  const data = fs.readFileSync(filePath);
  return crypto.createHash('sha256').update(data).digest('hex');
}

expect(sha256(actualPath)).toBe(EXPECTED_HASH);

Use checksums only when generators are stable. Dynamic timestamps inside PDFs will break naive hashes. In those cases, assert selected fields instead.

Authorization and Security Cases

Download bugs are often auth bugs.

Critical checks:

  • User A cannot download user B private file by guessing URL.
  • Expired signed URL returns an error.
  • Role without export permission does not receive a file.
  • Soft-deleted resources are not downloadable.
  • Audit logs record export events if the product requires that.

Automation approach:

  1. Create two users and one private resource.
  2. Authenticate as the wrong user.
  3. Attempt download via UI and direct URL if exposed.
  4. Assert forbidden response and no local file artifact.

These cases belong in every suite that handles personal data or paid documents.

Not all downloads are immediate. Many systems queue a job and notify later.

Test strategies:

  • Poll job status API until complete, then download.
  • Intercept or read a test mailbox for the link.
  • Use a test-only endpoint that returns the generated file id quickly.
  • Bound wait times and surface job failure messages.
test('async report becomes downloadable', async ({ page, request }) => {
  await page.goto('/reports');
  await page.getByTestId('generate-report').click();
  await expect(page.getByTestId('report-status')).toHaveText(/queued|processing/i);

  // poll through UI or API
  await expect(page.getByTestId('report-status')).toHaveText(/ready/i, {
    timeout: 60_000,
  });

  const downloadPromise = page.waitForEvent('download');
  await page.getByTestId('download-report').click();
  const download = await downloadPromise;
  expect(download.suggestedFilename()).toMatch(/report/i);
});

CI Considerations

Disk and Cleanup

CI runners can fill disk if large files accumulate. Delete artifacts after assertions, or save only on failure.

Browser Sandboxes

Ensure the environment allows writing to your chosen directory. Prefer workspace temp dirs over assuming ~/Downloads.

Parallel Safety

RiskMitigation
Same filenameInclude test id, worker index, timestamp
Shared folder racesPer-test subfolder
Heavy CPU PDF generationLimit concurrent download tests
External storage lagRetry completion wait, not random sleep only

What to Run Where

  • PR: 2 to 5 critical download smokes.
  • Nightly: format matrix, large files, async jobs, deeper content checks.

Mapping Manual Expectations to Automation

Manual testers often open files and "look right." Automation needs observable rules.

Translate subjective checks:

Manual observationAutomatable rule
"PDF looks fine"Page count, contains invoice id, total text found
"CSV opens in Excel"Valid delimiter, utf-8 BOM policy, headers stable
"Name makes sense"Regex contract agreed with product
"Felt fast enough"Soft threshold on generation time in non-functional suite

Document these translations so product and QA share the same definition of correct.

Common Mistakes When Automating Downloads

Mistake 1: Asserting Only That a Button Is Clickable

The button can work while generation fails. Always assert the artifact or a clear failure state.

Mistake 2: Not Waiting for File Completion

Partial files cause random size and parse errors. Wait for stable size and absence of temp extensions.

Mistake 3: Hard-Coding One Download Path for All Tests

Parallel runs will collide. Unique paths are mandatory.

Mistake 4: Over-Precise Filename Assertions

If the server includes milliseconds, exact string asserts will flake. Use patterns.

Mistake 5: Ignoring Empty and Error Paths

Happy path PDF tests miss many defects. Include zero-result and forbidden cases.

Mistake 6: Parsing Full Content for Every Suite Run

Deep parsing is valuable, but expensive. Tier your checks.

Mistake 7: Testing Only Chromium Locally

Download behavior can differ by browser policy. Include at least one additional browser in nightly if your users are diverse.

Implementation Checklist

  • Product filename and format contracts are written down.
  • Critical positive download tests exist.
  • Authorization negative tests exist.
  • Framework-level download wait is used (not only blind sleep).
  • Files are saved to unique paths.
  • Size and extension assertions are present.
  • At least one structural content check exists for each critical format.
  • CI cleans up or isolates artifacts.
  • Async jobs have bounded, explicit waits.
  • Failures capture server job ids or network logs when possible.

Worked Example: Report Export Acceptance Pack

Requirement:

An organization admin can export a filtered users report as CSV. The file name includes the organization slug and date. Only admins can export. The CSV includes email, role, and status columns.

Suggested automated pack:

  1. Admin with active filter downloads CSV, headers match, only active users included.
  2. Non-admin does not receive a file.
  3. Empty filter result follows product rule.
  4. Filename matches users-<org>-<date>.csv.
  5. Direct API export URL enforces the same auth as UI.

This pack is small, high value, and suitable for PR level CI.

Organizing Code With Page Objects

If export actions repeat, encapsulate UI triggers, not file asserts.

// downloads are system-level; keep assertions in the test or a download helper
export class ReportsPage {
  constructor(private page: import('@playwright/test').Page) {}

  async exportUsersCsv() {
    await this.page.getByTestId('export-users').click();
  }
}
const downloadPromise = page.waitForEvent('download');
await new ReportsPage(page).exportUsersCsv();
const download = await downloadPromise;

Keep page object layers focused on UI collaboration. File validation helpers can live in utils/downloads.ts.

Practice Path

  1. Automate one PDF or CSV happy path with filename and size checks.
  2. Add a permission-denied case.
  3. Add header or content validation.
  4. Run under parallel workers and fix path collisions.
  5. Add the smoke to CI.

Practice building reliable automation habits in QABattle battles, then apply the same completion-wait and isolation discipline to download flows in your product.

Final Workflow

  1. Define what correct means for name, format, and content.
  2. Write manual-style cases for positive, empty, and auth paths.
  3. Choose framework APIs that wait for downloads explicitly.
  4. Save to unique paths and assert completion.
  5. Add structural and selective business content checks.
  6. Tier tests across PR and nightly pipelines.
  7. Monitor flake sources: dialogs, locks, slow generation, collisions.

Mastering how to automate file download testing is less about clicking Export and more about proving a trustworthy artifact reached disk with the right security boundaries. Once those assertions are in place, download coverage becomes one of the highest confidence suites in your regression pack.

Browser Differences That Affect Download Automation

Even when the product is correct, browsers behave differently around downloads.

TopicChromiumFirefoxWebKit notes
Download APIs in PlaywrightStrongStrongVerify in your version matrix
Save dialogs in SeleniumOften controllable via prefsProfile prefs differCI constraints vary
Multiple downloadsWatch filename collisionsSameSame
PDF inline vs attachmentContent-Disposition dependentDependentDependent

Always assert server headers in at least one API-level test:

Content-Disposition: attachment; filename="invoice-inv_123.pdf"
Content-Type: application/pdf

If the UI click is flaky but headers and bytes are correct, split coverage: one UI trigger smoke, deeper content checks via authenticated HTTP download.

Testing Content-Disposition and Filename Encoding

Filenames with spaces, unicode, or long names often break.

Cases:

  • ASCII simple name
  • Spaces encoded correctly
  • Unicode customer names in filename if product supports them
  • Very long names truncated per policy
  • Reserved characters stripped
expect(download.suggestedFilename()).toMatch(/^report-[\w.-]+\.csv$/);

Coordinate with product on sanitization rules so tests do not assert accidental current behavior.

Virus Scanning and Security Gate Delays

Enterprise environments may scan downloads and delay file availability. In CI, scanners or endpoint protection can lock files briefly.

Mitigations:

  • Prefer isolated temp directories.
  • Retry stable-size waits.
  • Avoid reading a file at the exact millisecond it appears.
  • Document known environment delays in test helpers.

Large File Strategy

Large exports can timeout browsers or fill disks.

Approach:

  1. Smoke with small fixtures in PR.
  2. Nightly large-file test with higher timeout.
  3. Assert streaming success and final size range, not full in-memory parse when unnecessary.
  4. For multi-GB files, prefer API job status plus checksum of a server-side artifact when available.

Email Attachment Downloads

Some products email files instead of browser download.

Automation options:

  1. Test mail catcher APIs (Mailhog, Mailpit, provider test APIs).
  2. Parse the message for a signed URL.
  3. Download via request context with the link.
  4. Validate content the same way as browser downloads.
const link = await getDownloadLinkFromTestInbox(email);
const res = await request.get(link);
expect(res.ok()).toBeTruthy();
const buf = await res.body();
expect(buf.byteLength).toBeGreaterThan(500);

This is still download testing. The transport changed, not the quality goal.

Accessibility and UX Adjacent Checks

Automation can catch some UX failures around downloads:

  • Button remains disabled until prerequisites met.
  • Error text appears when export fails.
  • Progress indicator appears for long jobs.
  • Success toast or ready state appears when complete.

These do not replace file assertions, but they improve the user-visible story.

Mapping Risks to Automation Depth

Business riskMinimum automation depth
Paid invoice PDF wrongContent fields + authz
Marketing brochure PDFFilename + size smoke
Regulatory exportStructure + selected content + audit
Admin CSV dumpHeaders + authz + row filter rules
Temporary debug exportManual or low priority

Depth should follow blast radius. How to automate file download testing well includes knowing when a size check is enough and when it is negligent.

Sample Helper Module

import fs from 'fs';
import path from 'path';
import { Download, expect } from '@playwright/test';

export async function saveUnique(download: Download, dir: string, prefix: string) {
  fs.mkdirSync(dir, { recursive: true });
  const suggested = download.suggestedFilename();
  const target = path.join(dir, `${prefix}-${Date.now()}-${suggested}`);
  await download.saveAs(target);
  return target;
}

export function expectNonEmptyFile(filePath: string, minBytes = 1) {
  expect(fs.existsSync(filePath)).toBeTruthy();
  expect(fs.statSync(filePath).size).toBeGreaterThanOrEqual(minBytes);
}

Shared helpers reduce copy-paste and standardize completion expectations.

Rollout Plan for Teams New to Download Automation

  1. Week 1: one happy path PDF/CSV with Playwright download event.
  2. Week 2: auth negative and empty-state case.
  3. Week 3: structural content validation for the critical format.
  4. Week 4: CI smoke + nightly extended.
  5. Ongoing: add cases from production incidents.

This progressive rollout builds confidence without boiling the ocean.

Closing

File downloads combine UI triggers, HTTP metadata, generation logic, and permissions. Automate them with explicit completion waits, unique storage paths, layered assertions, and risk-based depth. Do that consistently and export regressions become rare, diagnosable events instead of customer surprises.

FAQ

Questions testers ask

How do you automate file download testing in a browser?

Trigger the download in an automated browser, wait until the file is fully written, then assert file name, extension, size, MIME-related expectations, and content where needed. Prefer framework download APIs over guessing OS download folders.

How does Playwright handle downloads?

Playwright can wait for a download event, save the file to a chosen path, and let you assert suggested filename and file contents. This is more reliable than polling a default Downloads directory with timing guesses.

Can Selenium automate file downloads?

Yes. Configure browser preferences for a fixed download directory, disable save dialogs when possible, trigger the download, wait for the file to finish, then validate the artifact. Chrome options and Firefox profiles are the usual approach.

What should you assert about a downloaded file?

At minimum: the download completed, file name pattern, extension, and non-zero size. For higher risk files, validate headers, PDF page count, CSV columns, checksums, or critical content strings. Match depth to business risk.

Why do download tests flake?

Common causes are save dialogs, antivirus scanners locking files, slow generation on the server, parallel tests writing the same filename, and asserting before the browser finished writing. Use unique paths and completion waits.

Should download tests run in every CI pipeline?

Run a small smoke set on pull requests for critical export paths. Keep heavier content validation and multi-format matrices for nightly jobs if they are slow or environment sensitive.