Playwright’s file upload and download capabilities are surprisingly robust, allowing for seamless testing of user interactions that involve file system operations.

Let’s see it in action. Imagine we have a simple web page with an input of type file for uploading and a link that, when clicked, triggers a file download.

<!DOCTYPE html>
<html>
<head>
    <title>File Interaction Test</title>
</head>
<body>
    <h1>File Upload</h1>
    <input type="file" id="file-upload-input">
    <p id="upload-status"></p>

    <h1>File Download</h1>
    <a href="/download/sample.txt" id="download-link">Download Sample File</a>
    <p id="download-status"></p>

    <script>
        document.getElementById('file-upload-input').addEventListener('change', function(event) {
            const file = event.target.files[0];
            if (file) {
                document.getElementById('upload-status').innerText = `Uploaded: ${file.name} (${file.size} bytes)`;
            }
        });

        document.getElementById('download-link').addEventListener('click', function(event) {
            // In a real scenario, this would trigger a server-side download.
            // For this example, we'll simulate it.
            event.preventDefault();
            document.getElementById('download-status').innerText = 'Download initiated...';
            setTimeout(() => {
                document.getElementById('download-status').innerText = 'Download complete!';
            }, 1000);
        });
    </script>
</body>
</html>

To test this with Playwright, we can use the setInputFiles method for uploads and waitForDownload for downloads.

import asyncio
import os
from playwright.sync_api import sync_playwright

# Create a dummy file for upload
upload_file_path = "my_document.txt"
with open(upload_file_path, "w") as f:
    f.write("This is a test document for upload.\n")

# Create a dummy file for download (if it doesn't exist)
download_dir = "downloads"
os.makedirs(download_dir, exist_ok=True)
download_file_path = os.path.join(download_dir, "sample.txt")
if not os.path.exists(download_file_path):
    with open(download_file_path, "w") as f:
        f.write("This is a sample file for download.\n")

def run():
    with sync_playwright() as p:
        browser = p.chromium.launch()
        page = browser.new_page()

        # Navigate to a local HTML file for testing
        # For a real website, you'd use page.goto("http://your-website.com")
        page.goto(f"file:///{os.path.abspath('index.html')}")

        # --- File Upload Test ---
        print("Testing file upload...")
        upload_input = page.locator("#file-upload-input")
        upload_input.set_input_files(upload_file_path)
        page.wait_for_selector("#upload-status:has-text('Uploaded: my_document.txt')")
        print("File upload successful.")

        # --- File Download Test ---
        print("Testing file download...")
        download_link = page.locator("#download-link")

        # Prepare to wait for the download
        # The download event needs to be listened to *before* triggering the action
        with page.expect_download() as download_info:
            download_link.click()

        download = download_info.value
        # Specify where to save the downloaded file
        download_path = download.save_as(os.path.join(download_dir, download.suggested_filename))
        print(f"File downloaded to: {download_path}")
        assert os.path.exists(download_path)
        print("File download successful.")

        browser.close()

run()

# Clean up dummy files
# os.remove(upload_file_path)
# os.remove(download_file_path)

This script first sets up two dummy files: my_document.txt for uploading and sample.txt within a downloads directory for downloading. It then launches Playwright, navigates to a local HTML file containing the input and link elements.

For the upload, page.locator("#file-upload-input").set_input_files("my_document.txt") directly associates the specified file with the file input element. Playwright handles the underlying DOM manipulation and event triggering. We then assert that the upload status message updates correctly.

For the download, page.expect_download() is crucial. This context manager tells Playwright to listen for a download event. The download is then initiated by clicking the #download-link. Once the download completes, download_info.value provides a Download object. We use download.save_as() to specify the local path where the file should be saved, using download.suggested_filename to respect the server’s suggested filename. Finally, we assert that the file exists at the saved location.

The core mechanism Playwright uses for file uploads is by directly interacting with the <input type="file"> element. When you use set_input_files(), Playwright bypasses the typical user workflow of clicking "Choose File" and then selecting a file from the OS’s file dialog. Instead, it programmatically sets the files property of the input element, as if the user had selected those files. For downloads, Playwright hooks into the browser’s download events. When a navigation or click action triggers a download, Playwright intercepts this event, allowing you to capture the download object, inspect its properties (like filename and MIME type), and decide where to save it.

The expect_download context manager is particularly powerful because it ensures your test doesn’t proceed until a download has been initiated and completed, preventing race conditions where your script might try to save a file before it’s fully transferred.

A common pitfall is forgetting to wrap the action that triggers the download (like link.click()) within the page.expect_download() context. If you click the link first and then try to expect_download, the download event might have already fired and been missed, leading to a timeout.

The next hurdle you’ll likely encounter is handling file uploads or downloads in more complex scenarios, such as those involving drag-and-drop interfaces or API-driven downloads that don’t directly involve a click on a link.

Want structured learning?

Take the full Playwright course →