Downloads and files

Upload fixtures, verify generated files, and keep file-related browser tests deterministic in CI.

File flows cross several boundaries: browser controls, native operating system dialogs, HTTP responses, temporary directories, application storage, and CI artifact upload.

Reliable file tests make those boundaries explicit. Use committed fixtures for uploads. Save generated files into known artifact directories. Assert the user-visible result before inspecting the filesystem.

Mental model: uploads are input, downloads are artifacts

Uploads behave like form input. The test supplies a known file to a file input.

Downloads behave like output. The product creates a file after a user action, and the test keeps enough evidence to prove the file was produced.

Those flows need different assertions:

Flow Primary assertion Supporting evidence
upload accepted success message, preview, filename fixture path and file size
upload rejected validation message invalid fixture
download started UI confirmation or response status saved artifact
generated file content stable text, rows, metadata parser or file size

Upload with the file input, not the OS dialog

Do not automate the operating system file picker. Set files on the input:

php
use function Playwright\Testing\expect;

$page
    ->getByLabel('Avatar')
    ->setInputFiles(__DIR__.'/fixtures/avatar.png');

$page->getByRole('button', ['name' => 'Save profile'])->click();

expect($page->getByText('Profile updated'))->toBeVisible();
expect($page->getByText('avatar.png'))->toBeVisible();

This works in headless CI because it stays inside the browser automation surface.

Use arrays for multiple files:

php
$page->getByLabel('Attachments')->setInputFiles([
    __DIR__.'/fixtures/invoice.pdf',
    __DIR__.'/fixtures/receipt.pdf',
]);

Use file chooser only when the flow needs it

The FileChooserInterface can set files on the chooser it represents, but the normal, stable path is the locator file input. Use file chooser APIs only when the product behavior specifically depends on the chooser opening.

For most forms, this is clearer:

php
$page->getByLabel('Invoice PDF')->setInputFiles(__DIR__.'/fixtures/invoice.pdf');

It states the input contract directly.

There is currently no PageInterface::waitForFileChooser() helper to build the common JavaScript-style pattern around. That is another reason the Guide path targets the input directly.

Keep upload fixtures deterministic

Good fixtures are:

  • committed with the test suite;
  • small enough to copy quickly;
  • named by purpose;
  • valid or invalid on purpose;
  • safe to upload to CI logs or artifacts.

Avoid files from /tmp, the local Downloads folder, or user-specific paths. They make tests pass on one machine and fail elsewhere.

Name fixtures by behavior, not by accident:

text
tests/Fixtures/uploads/
├── avatar-valid.png
├── avatar-too-large.png
├── avatar-wrong-type.txt
└── invoice-two-pages.pdf

The name should tell the next reader why that file exists.

Assert the application response first

After an upload, the product behavior matters more than the input value:

php
$page->getByLabel('Avatar')->setInputFiles(__DIR__.'/fixtures/not-an-image.txt');
$page->getByRole('button', ['name' => 'Save profile'])->click();

expect($page->getByText('Upload a PNG or JPEG image'))->toBeVisible();

Test invalid file behavior deliberately. It often covers validation, error placement, and recovery better than another happy-path upload.

Handle downloads as artifacts

The core-safe path is to assert the UI and response evidence, then save files through the runner or integration when a DownloadInterface is available.

php
$fileStatus = null;

$page->events()->onResponse(static function ($response) use (&$fileStatus): void {
    if (str_contains($response->url(), '/invoice.pdf')) {
        $fileStatus = $response->status();
    }
});

$page->getByRole('link', ['name' => 'Download invoice'])->click();

self::assertSame(200, $fileStatus);
expect($page->getByText('Invoice ready'))->toBeVisible();

When you have a DownloadInterface, copy it to your artifact directory:

php
$download->saveAs(__DIR__.'/var/playwright-artifacts/monthly-report.csv');

Do not assume a JS-style waitForDownload() helper exists on PageInterface. Use the API exposed by your runner/integration, or collect response evidence and application state.

When you control context creation, configure downloads explicitly:

php
$context = Playwright::chromium([
    'context' => [
        'acceptDownloads' => true,
        'downloadsPath' => __DIR__.'/var/playwright-artifacts/downloads',
    ],
]);

For lower-level setup, BrowserContextBuilder::withDownloadsPath() builds the same kind of context options, and PlaywrightConfigBuilder::withDownloadsDir() configures the browser launch path for a factory-created client.

Check generated files with stable facts

For generated files, prefer durable checks:

  • expected filename pattern;
  • non-empty file size;
  • MIME type or extension;
  • CSV header or row count;
  • PDF text when your parser makes it stable;
  • application audit entry or visible status.

Avoid byte-for-byte comparisons unless the generator is deterministic. PDFs and office documents often contain timestamps, object IDs, or metadata that change every run.

For CSV files, parse rows and assert headings or key values. For PDFs, prefer stable text extraction or product-side metadata. For images, assert that the file exists and is non-empty unless visual comparison is explicitly part of the product quality gate.

CI file policy

Put file artifacts in one ignored directory:

text
var/playwright-artifacts/

CI should upload that directory on failure, or on success only when the file is the product output being tested.

Keep downloaded reports and uploaded fixtures separate:

text
tests/Fixtures/uploads/
var/playwright-artifacts/downloads/

Fixtures are inputs and belong in the repository. Downloads are outputs and belong in artifacts.

Do not upload fixtures as artifacts unless the CI job generated them. Fixtures already live in the repository; uploading them only adds noise. Upload outputs, logs, traces, screenshots, and files that explain the run.

Security and cleanup

Files can contain personal data. Treat uploads and downloads as sensitive by default:

  • do not use real customer files as fixtures;
  • strip secrets from generated reports before storing them;
  • keep artifact retention short;
  • delete local artifacts when they are no longer needed;
  • never write test downloads under public/.

If a file must be retained for compliance or manual review, document that separately from ordinary browser debugging artifacts.

Common mistakes

Using local files outside the repository. They disappear on CI.

Trying to drive the OS file picker. It is outside the web page and fails in headless runs.

Assuming temporary download paths are stable. Save files into a path you control.

Asserting only HTTP status for a UI feature. The user still needs visible confirmation.

Comparing generated files byte-for-byte. Use stable content or metadata instead.

Verification checklist

  • Upload fixtures are committed and intentionally chosen.
  • Upload tests assert visible success or validation.
  • Download evidence is registered before the user action when needed.
  • Download outputs are copied into the artifact directory.
  • Generated-file checks ignore unstable metadata.
  • CI uploads the artifact directory when it matters.

Go next