setInputFiles() в Playwright обходит системный диалог выбора файла, устанавливая файлы напрямую на элемент . Именно поэтому загрузку файлов можно автоматизировать без того чтобы браузер вообще открывал диалог. Скачивание работает иначе: ты перехватываешь событие download через page.waitForEvent('download'), но зарегистрировать слушатель нужно до того как скачивание запустится, не после. Если скачивание произошло пока ты настраивал слушатель, событие потеряно. Эта статья разбирает одиночную и множественную загрузку файлов, буферы в памяти, drag-and-drop через DataTransfer, событие filechooser для кастомных кнопок загрузки, перехват скачивания с проверкой содержимого и автоматическую очистку через фикстуры.
Почему нативный диалог не автоматизируется кликами
Нативный файловый пикер (диалог ОС который открывается при клике «Выбрать файл») работает вне движка рендеринга браузера. Это часть операционной системы. Playwright управляет браузером; механизма для доступа в окно Windows Explorer или macOS Finder у него нет.
Это сделано намеренно из соображений безопасности. Если бы JavaScript мог программно открывать и закрывать диалог выбора файла, это стало бы тривиальным способом кражи файлов с машины пользователя. Браузеры изолируют это взаимодействие именно для того чтобы такого не было.
Поэтому стандартный подход к автоматизации загрузки файлов обходит диалог полностью. Вместо симуляции клика который открывает диалог, взаимодействуешь напрямую с элементом и говоришь ему какой файл использовать. Именно это делает метод setInputFiles().
setInputFiles(): стандартный подход
Большинство форм загрузки файлов используют элемент , даже когда он визуально скрыт за стилизованной кнопкой. setInputFiles() устанавливает файлы на этот инпут программно, полностью минуя диалог.
import { test, expect } from '@playwright/test';
import path from 'path';
test('uploads a single file', async ({ page }) => {
await page.goto('https://lab.becomeqa.com/file-upload');
const filePath = path.join(__dirname, 'fixtures/sample.pdf');
await page.locator('input[type="file"]').setInputFiles(filePath);
await page.getByRole('button', { name: 'Upload' }).click();
await expect(page.getByText('Upload successful')).toBeVisible();
});Паттерн path.join(__dirname, ...) важен. Абсолютные пути надёжнее относительных. __dirname даёт тебе директорию текущего тест-файла, поэтому путь к фикстуре резолвится корректно независимо от того откуда запускаются тесты.
Если у файлового инпута есть атрибут name, id или он привязан к лейблу, используй более описательный локатор:
// по тексту лейбла
await page.getByLabel('Attach document').setInputFiles(filePath);
// по атрибуту name
await page.locator('input[name="resume"]').setInputFiles(filePath);setInputFiles() уважает атрибут accept на инпуте. Если инпут ограничен изображениями (accept="image/*") а ты пытаешься передать .pdf, Playwright не выбросит ошибку, но браузер может тихо отклонить файл. Всегда используй файлы подходящих типов.Загрузка нескольких файлов сразу
Когда инпут разрешает несколько файлов (), передавай массив путей в setInputFiles():
test('uploads multiple files at once', async ({ page }) => {
await page.goto('https://lab.becomeqa.com/file-upload');
const files = [
path.join(__dirname, 'fixtures/document-a.pdf'),
path.join(__dirname, 'fixtures/document-b.pdf'),
path.join(__dirname, 'fixtures/image.png'),
];
await page.locator('input[type="file"]').setInputFiles(files);
await page.getByRole('button', { name: 'Upload' }).click();
// проверяем что все три файла отображаются в подтверждении
await expect(page.getByText('document-a.pdf')).toBeVisible();
await expect(page.getByText('document-b.pdf')).toBeVisible();
await expect(page.getByText('image.png')).toBeVisible();
});Чтобы очистить выбор и начать заново, передай пустой массив:
// убираем все выбранные файлы
await page.locator('input[type="file"]').setInputFiles([]);Это полезно когда флоу теста предполагает смену файла перед отправкой или нужно сбросить состояние инпута между шагами.
Если нужно конструировать тест-файлы динамически без чтения с диска, setInputFiles() также принимает буфер:
test('uploads a file created in memory', async ({ page }) => {
await page.goto('https://lab.becomeqa.com/file-upload');
await page.locator('input[type="file"]').setInputFiles({
name: 'generated-report.txt',
mimeType: 'text/plain',
buffer: Buffer.from('Test report content\nLine 2\nLine 3'),
});
await page.getByRole('button', { name: 'Upload' }).click();
await expect(page.getByText('generated-report.txt')).toBeVisible();
});Буферный подход идеален когда содержимое файла важно для теста, но ты не хочешь держать файлы на диске для каждого варианта.
Drag-and-drop загрузка файлов
Некоторые интерфейсы загрузки используют зону сброса вместо файлового инпута: Подход здесь: отправить drag-события вручную через Это сложнее чем Некоторые кнопки загрузки: стилизованные Порядок важен: регистрируй слушатель Можно также проверить принимает ли файловый инпут несколько файлов: Скачивания работают через другой механизм. Когда браузер запускает скачивание файла, Playwright генерирует событие По умолчанию Playwright сохраняет скачанные файлы во временную директорию которая очищается при закрытии браузерного контекста. Объект Перехватить скачивание: это первый шаг. Проверка правильности содержимого делает тест осмысленным. Для бинарных файлов проверяй размер вместо содержимого: Для структурированных форматов вроде JSON десериализуй и проверяй структуру: Тесты загрузки и скачивания оставляют файлы на диске. При параллельном запуске или в CI-окружении файлы от одного прогона могут загрязнять следующий. Правильный инструмент: фикстура Playwright которая автоматически убирает за собой. Создай фикстуру которая предоставляет чистую директорию для скачиваний и удаляет её после каждого теста: Тесты которые используют эту фикстуру получают свежую изолированную директорию для каждого прогона: Для фикстур загрузки можно генерировать временные тест-файлы при setup и удалять их после: Этот паттерн держит тест-инфраструктуру честной: если тест падает и очистка не запускается в Нажми кнопку, дождись появления инпута, затем вызывай Да. Перехвати URL скачивания и верни статус ошибки: Да, используется дефолтный таймаут действия (обычно 30 секунд). Если скачивание занимает дольше, передай кастомный таймаут: Если скачивание открывается на новой странице, слушай на уровне браузерного контекста: Файл находится во внутренней временной директории Playwright до закрытия браузерного контекста, затем удаляется. Всегда вызывай dragover и drop. Элемента для setInputFiles() нет.
page.dispatchEvent() с объектом DataTransfer заполненным через page.evaluate():test('uploads a file via drag and drop', async ({ page }) => {
await page.goto('https://lab.becomeqa.com/file-upload');
const dropZone = page.locator('.drop-zone');
const filePath = path.join(__dirname, 'fixtures/sample.pdf');
// читаем содержимое файла и создаём DataTransfer с ним
const fileContent = require('fs').readFileSync(filePath);
const base64 = fileContent.toString('base64');
await dropZone.evaluate(
(element, { base64Content, fileName, mimeType }) => {
const dataTransfer = new DataTransfer();
const byteCharacters = atob(base64Content);
const byteArray = new Uint8Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteArray[i] = byteCharacters.charCodeAt(i);
}
const file = new File([byteArray], fileName, { type: mimeType });
dataTransfer.items.add(file);
element.dispatchEvent(new DragEvent('dragenter', { dataTransfer, bubbles: true }));
element.dispatchEvent(new DragEvent('dragover', { dataTransfer, bubbles: true }));
element.dispatchEvent(new DragEvent('drop', { dataTransfer, bubbles: true }));
},
{ base64Content: base64, fileName: 'sample.pdf', mimeType: 'application/pdf' }
);
await expect(page.getByText('sample.pdf')).toBeVisible();
});setInputFiles(), но правильный подход для JavaScript-driven зон сброса. Если зона сброса также содержит скрытый файловый инпут который активируется после drop, более простой setInputFiles() тоже может сработать. Проверь DOM чтобы выяснить. который можно таргетировать через setInputFiles(). Сначала проверь DevTools: это сэкономит значительную сложность.Событие filechooser
или page.waitForEvent('filechooser') позволяет перехватить диалог который открывается при клике на такую кнопку:
test('handles a custom upload button', async ({ page }) => {
await page.goto('https://lab.becomeqa.com/file-upload');
const filePath = path.join(__dirname, 'fixtures/sample.pdf');
// регистрируем слушатель ДО клика
const fileChooserPromise = page.waitForEvent('filechooser');
await page.getByRole('button', { name: 'Choose File' }).click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(filePath);
await expect(page.getByText('sample.pdf')).toBeVisible();
});filechooser до клика по кнопке. Если диалог откроется и закроется пока ты настраиваешь слушатель, будешь ждать вечно.fileChooser.setFiles() принимает те же аргументы что и setInputFiles(): одиночный путь, массив путей или объект-буфер. Для нескольких файлов:
const fileChooserPromise = page.waitForEvent('filechooser');
await page.getByRole('button', { name: 'Attach Files' }).click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles([
path.join(__dirname, 'fixtures/attachment-1.pdf'),
path.join(__dirname, 'fixtures/attachment-2.pdf'),
]);const fileChooser = await fileChooserPromise;
console.log('Accepts multiple:', fileChooser.isMultiple());
await fileChooser.setFiles(filePath);Скачивание файлов: перехват события
download на странице. Ты перехватываешь его через page.waitForEvent('download') и получаешь объект Download для дальнейшей работы.test('downloads a report file', async ({ page }) => {
await page.goto('https://lab.becomeqa.com/reports');
// регистрируем слушатель до запуска скачивания
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: 'Export CSV' }).click();
const download = await downloadPromise;
// сохраняем файл в известное место
const savePath = path.join(__dirname, 'downloads/report.csv');
await download.saveAs(savePath);
// проверяем что файл существует
const { existsSync } = require('fs');
expect(existsSync(savePath)).toBe(true);
});saveAs() копирует файл в место которое ты контролируешь, чтобы можно было его проверить после завершения скачивания.download отдаёт предлагаемое имя файла до его сохранения:const download = await downloadPromise;
console.log('Suggested filename:', download.suggestedFilename());
await download.saveAs(
path.join(__dirname, `downloads/${download.suggestedFilename()}`)
);suggestedFilename() возвращает то что сервер прислал в заголовке Content-Disposition: именно это пользователь видел бы как имя файла по умолчанию в диалоге сохранения.
Проверка содержимого скачанного файла
import { test, expect } from '@playwright/test';
import path from 'path';
import fs from 'fs';
test('exported CSV contains the correct data', async ({ page }) => {
await page.goto('https://lab.becomeqa.com/reports');
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: 'Export CSV' }).click();
const download = await downloadPromise;
// проверяем имя файла
expect(download.suggestedFilename()).toMatch(/^report-\d{4}-\d{2}-\d{2}\.csv$/);
// сохраняем и читаем содержимое
const savePath = path.join(__dirname, 'downloads/report.csv');
await download.saveAs(savePath);
const content = fs.readFileSync(savePath, 'utf-8');
// проверяем строку заголовков
expect(content).toContain('id,destination,status,notes');
// проверяем что файл не пустой (больше одной строки)
const rows = content.trim().split('\n');
expect(rows.length).toBeGreaterThan(1);
});test('downloaded PDF is not empty', async ({ page }) => {
await page.goto('https://lab.becomeqa.com/reports');
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: 'Export PDF' }).click();
const download = await downloadPromise;
const savePath = path.join(__dirname, 'downloads/report.pdf');
await download.saveAs(savePath);
const stats = fs.statSync(savePath);
expect(download.suggestedFilename()).toBe('report.pdf');
expect(stats.size).toBeGreaterThan(1000); // настоящий PDF больше 1KB
});test('exported JSON has the expected structure', async ({ page }) => {
await page.goto('https://lab.becomeqa.com/reports');
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: 'Export JSON' }).click();
const download = await downloadPromise;
const savePath = path.join(__dirname, 'downloads/export.json');
await download.saveAs(savePath);
const content = JSON.parse(fs.readFileSync(savePath, 'utf-8'));
expect(Array.isArray(content)).toBe(true);
expect(content[0]).toHaveProperty('destination');
expect(content[0]).toHaveProperty('status');
});Очистка тест-файлов через фикстуры
// fixtures/file-test.ts
import { test as base } from '@playwright/test';
import fs from 'fs';
import path from 'path';
type FileTestFixtures = {
downloadDir: string;
testFilePath: (filename: string) => string;
};
export const test = base.extend<FileTestFixtures>({
downloadDir: async ({}, use) => {
const dir = path.join(__dirname, `../downloads/test-${Date.now()}`);
fs.mkdirSync(dir, { recursive: true });
await use(dir);
// очистка после теста
fs.rmSync(dir, { recursive: true, force: true });
},
testFilePath: async ({ downloadDir }, use) => {
const getPath = (filename: string) => path.join(downloadDir, filename);
await use(getPath);
},
});
export { expect } from '@playwright/test';// tests/download.spec.ts
import { test, expect } from '../fixtures/file-test';
import path from 'path';
test('downloads report to isolated directory', async ({ page, downloadDir, testFilePath }) => {
await page.goto('https://lab.becomeqa.com/reports');
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: 'Export CSV' }).click();
const download = await downloadPromise;
const savePath = testFilePath(download.suggestedFilename());
await download.saveAs(savePath);
const content = require('fs').readFileSync(savePath, 'utf-8');
expect(content).toContain('destination');
// downloadDir и его содержимое удаляются автоматически после теста
});export const test = base.extend<{
uploadFixturePath: string;
}>({
uploadFixturePath: async ({}, use) => {
const filePath = path.join(__dirname, `../fixtures/temp-${Date.now()}.txt`);
fs.writeFileSync(filePath, 'Temporary test file content');
await use(filePath);
fs.rmSync(filePath, { force: true });
},
});beforeAll/afterAll, teardown фикстуры всё равно выполняется, потому что Playwright вызывает его даже при падении теста.FAQ
Что если файловый инпут скрыт или имеет
display: none?setInputFiles() работает со скрытыми инпутами. Он обходит визуальное состояние элемента. Если получаешь ошибку о видимости, используй { force: true }: await page.locator('input[type="file"]').setInputFiles(filePath, { force: true }).
Как обработать загрузку файла если сначала нужно нажать кнопку чтобы появился инпут?
setInputFiles(). Если кнопка открывает нативный диалог, используй page.waitForEvent('filechooser') перед кликом.Можно протестировать что скачивание не удалось или было заблокировано?
await page.route('/export/csv', route => route.fulfill({ status: 500 })). Затем проверяй что UI показывает подходящее сообщение об ошибке. Событие download в этом случае не сработает.Есть ли у
page.waitForEvent('download') таймаут?page.waitForEvent('download', { timeout: 60000 }).Как обработать скачивание в отдельной вкладке браузера?
const download = await browserContext.waitForEvent('download'). Это перехватывает скачивания с любой страницы контекста.Что происходит с временной директорией скачивания если не вызвать
saveAs()?saveAs() если нужно проинспектировать файл.