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 загрузка файлов

Некоторые интерфейсы загрузки используют зону сброса вместо файлового инпута:

который слушает события dragover и drop. Элемента для setInputFiles() нет.

Подход здесь: отправить drag-события вручную через 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 чтобы выяснить.

Прежде чем прибегать к drag-and-drop подходу, проинспектируй HTML зоны сброса. Многие «drag-and-drop» области загрузки всё равно содержат скрытый который можно таргетировать через setInputFiles(). Сначала проверь DevTools: это сэкономит значительную сложность.

Событие filechooser

Некоторые кнопки загрузки: стилизованные

Порядок важен: регистрируй слушатель 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);

Скачивание файлов: перехват события

Скачивания работают через другой механизм. Когда браузер запускает скачивание файла, Playwright генерирует событие 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);
});

По умолчанию Playwright сохраняет скачанные файлы во временную директорию которая очищается при закрытии браузерного контекста. 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
});

Для структурированных форматов вроде JSON десериализуй и проверяй структуру:

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');
});

Не проверяй точный размер файла для генерируемых файлов. Таймстампы, ID и контент генерируемый сервером могут вызывать побайтовые различия между прогонами. Для бинарных файлов проверяй минимальный размер, для текстовых форматов проверяй структуру содержимого.

Очистка тест-файлов через фикстуры

Тесты загрузки и скачивания оставляют файлы на диске. При параллельном запуске или в CI-окружении файлы от одного прогона могут загрязнять следующий. Правильный инструмент: фикстура Playwright которая автоматически убирает за собой.

Создай фикстуру которая предоставляет чистую директорию для скачиваний и удаляет её после каждого теста:

// 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 и его содержимое удаляются автоматически после теста
});

Для фикстур загрузки можно генерировать временные тест-файлы при setup и удалять их после:

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') перед кликом.

Можно протестировать что скачивание не удалось или было заблокировано?

Да. Перехвати URL скачивания и верни статус ошибки: await page.route('/export/csv', route => route.fulfill({ status: 500 })). Затем проверяй что UI показывает подходящее сообщение об ошибке. Событие download в этом случае не сработает.

Есть ли у page.waitForEvent('download') таймаут?

Да, используется дефолтный таймаут действия (обычно 30 секунд). Если скачивание занимает дольше, передай кастомный таймаут: page.waitForEvent('download', { timeout: 60000 }).

Как обработать скачивание в отдельной вкладке браузера?

Если скачивание открывается на новой странице, слушай на уровне браузерного контекста: const download = await browserContext.waitForEvent('download'). Это перехватывает скачивания с любой страницы контекста.

Что происходит с временной директорией скачивания если не вызвать saveAs()?

Файл находится во внутренней временной директории Playwright до закрытия браузерного контекста, затем удаляется. Всегда вызывай saveAs() если нужно проинспектировать файл.

→ See also: Перехват сети, моки и стабы в Playwright | Кастомные фикстуры в Playwright: паттерн, который делает тесты читаемыми | API-тестирование в Playwright: выходим за рамки UI | Assertions в Playwright: полное руководство