Uploading a profile picture, importing a CSV, exporting a report: file transfer is part of many test flows. Playwright handles both directions without touching the operating system’s file dialogs. In this lesson you will upload real and in-memory files to the SQA Tools practice page, use the file chooser event, and capture, save and verify downloads, including files the page generates on the fly.
📖 What You’ll Learn: set_input_files with a path, several files and in-memory content, checking what was selected, clearing a file input, expect_file_chooser for upload buttons that hide the input, expect_download, suggested_filename, save_as and path, verifying the downloaded content, downloads generated by JavaScript, and where Playwright stores downloads.
📤 Uploading Files: set_input_files
A file upload on a web page is an <input type="file">. Instead of opening the operating system’s file dialog (which automation cannot control), Playwright sets the input’s files directly with set_input_files(). The practice page has one: <input type="file" id="fileUpload">.
📄 tests/conftest.py
import pytest
from playwright.sync_api import Page
@pytest.fixture
def practice(page: Page) -> Page:
page.goto("https://sqatools.in/automation-practice-page/")
return page
📄 tests/test_upload.py
from pathlib import Path
from playwright.sync_api import Page, expect
SELECTED_NAMES = "el => [...el.files].map(f => f.name + ' (' + f.size + ' bytes)')"
def test_upload_file_from_disk(practice: Page, tmp_path: Path):
# Create a real file to upload (in a real project, keep test files in a test_data folder)
report = tmp_path / "report.csv"
report.write_text("id,name\n1,Deepesh\n")
upload = practice.locator("#fileUpload")
upload.set_input_files(report)
print("\nselected:", upload.evaluate(SELECTED_NAMES))
expect(upload).to_have_value("C:\\fakepath\\report.csv")
def test_upload_in_memory_file(practice: Page):
upload = practice.locator("#fileUpload")
upload.set_input_files({
"name": "notes.txt",
"mimeType": "text/plain",
"buffer": b"created in memory by the test",
})
print("\nin memory:", upload.evaluate(SELECTED_NAMES))
assert upload.evaluate("el => el.files[0].name") == "notes.txt"
def test_clear_selection(practice: Page, tmp_path: Path):
file = tmp_path / "a.txt"
file.write_text("a")
upload = practice.locator("#fileUpload")
upload.set_input_files(file)
upload.set_input_files([]) # an empty list removes the selection
expect(upload).to_have_value("")
Line by line:
set_input_files(path)accepts a path as a string orpathlib.Path. Relative paths are resolved from the folder where you run pytest.- To see what was selected, the test reads the input’s
fileslist in the browser withevaluate: name and size of each file. - Browsers never reveal the real path of an uploaded file, so the input’s value is always
C:\fakepath\<name>, on every operating system.to_have_valuechecks exactly that. - Instead of a file on disk, you can pass a dictionary with
name,mimeTypeandbuffer(bytes). This is handy for generated content and avoids test-data files. set_input_files([])clears the selection.- For inputs with the
multipleattribute, pass a list of paths to upload several files at once.
🖥️ Output
selected: ['report.csv (20 bytes)']
in memory: ['notes.txt (29 bytes)']
(The CSV is 20 bytes on Windows, where Python’s write_text stores each line break as two characters, \r\n; on Linux and macOS the same file is 18 bytes.)
📝 Note: set_input_files only selects the file, exactly like a user picking it in the dialog. Whatever the page does next (uploading on change, or on clicking an Upload button) you trigger and verify as usual.
🗂️ Upload Buttons That Hide the Input: expect_file_chooser
Many sites hide the real file input behind a styled button. Clicking the button opens the native file dialog, which Playwright intercepts as a file chooser event:
📄 tests/test_upload.py
def test_file_chooser(page: Page, tmp_path: Path):
page.set_content("""
<input type="file" id="hidden-input" style="display:none">
<button onclick="document.getElementById('hidden-input').click()">Choose photo</button>
<p id="status"></p>
<script>
document.getElementById('hidden-input').onchange = e =>
document.getElementById('status').textContent = 'Selected: ' + e.target.files[0].name;
</script>
""")
photo = tmp_path / "avatar.png"
photo.write_bytes(b"\x89PNG fake image bytes")
with page.expect_file_chooser() as chooser_info:
page.get_by_role("button", name="Choose photo").click()
chooser = chooser_info.value
chooser.set_files(photo)
expect(page.locator("#status")).to_have_text("Selected: avatar.png")
page.expect_file_chooser()starts waiting before the click, likeexpect_popup()in Lesson 34.chooser.set_files(...)accepts the same arguments asset_input_files. The page’schangeevent fires, so the page reacts exactly as it would for a real user.- You could also call
set_input_filesdirectly on the hidden input; it works on hidden inputs too. The file chooser approach is useful when you do not know where the input is.
📥 Downloading Files: expect_download
When a click starts a download, wrap it in page.expect_download(). The Download object lets you read the file name, save the file and check its content:
📄 tests/test_download.py
import csv
from pathlib import Path
from playwright.sync_api import Page
PAGE = """
<a id="static" href="data:text/csv;charset=utf-8,id%2Cname%0A1%2CDeepesh%0A2%2CRahul" download="users.csv">Export users</a>
<button id="generate">Generate report</button>
<script>
document.getElementById('generate').onclick = () => {
const text = 'Total tests: 42\\nPassed: 40\\nFailed: 2';
const url = URL.createObjectURL(new Blob([text], { type: 'text/plain' }));
const a = document.createElement('a');
a.href = url; a.download = 'summary.txt'; a.click();
};
</script>
"""
def test_download_csv(page: Page, tmp_path: Path):
page.set_content(PAGE)
with page.expect_download() as download_info:
page.get_by_role("link", name="Export users").click()
download = download_info.value
print("\nsuggested name:", download.suggested_filename)
target = tmp_path / download.suggested_filename
download.save_as(target)
with open(target, newline="", encoding="utf-8") as f:
rows = list(csv.DictReader(f))
print("rows:", rows)
assert rows == [{"id": "1", "name": "Deepesh"}, {"id": "2", "name": "Rahul"}]
def test_download_generated_by_javascript(page: Page):
page.set_content(PAGE)
with page.expect_download() as download_info:
page.get_by_role("button", name="Generate report").click()
download = download_info.value
assert download.failure() is None # None means the download succeeded
content = Path(download.path()).read_text()
print("\n" + download.suggested_filename + ":", content.splitlines())
assert "Failed: 2" in content
Line by line:
page.expect_download()waits for a download that starts during thewithblock.download.suggested_filenameis the name the site proposed (from thedownloadattribute or the server’s headers).download.save_as(path)copies the file to a location you choose. Here it goes to pytest’stmp_path, which is cleaned up automatically.download.path()returns where Playwright saved the file temporarily; it waits until the download has finished.download.failure()returnsNoneon success or an error message.- The second download is generated by JavaScript in the browser (a
Blobturned into a temporary URL), the same technique many “Export” buttons use. Playwright captures it the same way. - Python’s
csv.DictReaderturns the CSV into a list of dictionaries, so the test checks the content, not just that a file arrived.
🖥️ Output
suggested name: users.csv
rows: [{'id': '1', 'name': 'Deepesh'}, {'id': '2', 'name': 'Rahul'}]
summary.txt: ['Total tests: 42', 'Passed: 40', 'Failed: 2']
📁 Where Do Downloads Go?
- By default, Playwright saves downloads in a temporary folder that is deleted when the browser context closes. With pytest-playwright that happens at the end of each test, so call
save_as()if you need the file afterwards. - Downloads are accepted by default in pytest-playwright contexts (
accept_downloads=True). - To keep all downloads in a fixed folder, pass
downloads_pathinbrowser_type_launch_args(Lesson 6), butsave_as()per test is usually clearer.
⌨️ Terminal
pytest -q -s tests
🖥️ Output
suggested name: users.csv
rows: [{'id': '1', 'name': 'Deepesh'}, {'id': '2', 'name': 'Rahul'}]
.
summary.txt: ['Total tests: 42', 'Passed: 40', 'Failed: 2']
.
selected: ['report.csv (20 bytes)']
.
in memory: ['notes.txt (29 bytes)']
...
6 passed, 1 warning in 7.86s
❓ Frequently Asked Questions
🤔 Can Playwright click “Browse” and pick a file in the Windows dialog?
No automation tool should drive the native dialog. Playwright intercepts it instead: use set_input_files() on the input, or expect_file_chooser() around the click.
🤔 Why is the input value C:\fakepath\… on Linux too?
It is a browser security rule: the real path is never revealed to the page, on any operating system.
🤔 How do I check that the download was a valid Excel or PDF file?
Save it, then open it with a Python library: openpyxl for Excel, pypdf for PDF, csv or json for text formats. Module 7 covers reading test data files.
🤔 My download test fails with a timeout. Why?
The click did not start a download (it may open the file in a new tab instead), or the download started before the with page.expect_download() block. Start waiting first, then click inside the block.
🏁 Summary
set_input_files()selects files on an<input type="file">: paths, lists, in-memory buffers, or[]to clear.expect_file_chooser()handles upload buttons that open the native dialog.expect_download()captures downloads; usesuggested_filename,save_as(),path()andfailure(), then verify the content.- Downloads live in a temporary folder that disappears with the context, so save what you need.
That completes Module 4. Test yourself with the Module 4 mock interview, then continue to Module 5: assertions, waits and screenshots.
🎬 Watch the Playwright tutorial series on YouTube!
Subscribe to SqaToolsByDY • Practice on the SQA Tools Automation Practice Page