setInputFiles() do Playwright contorna o seletor de arquivos do sistema operacional definindo arquivos diretamente no elemento , por isso você consegue automatizar uploads sem que o navegador abra nenhum diálogo. Downloads funcionam diferente: você captura o evento download com page.waitForEvent('download'), mas precisa registrar o listener antes de disparar o download. Se o download acontecer enquanto você ainda está configurando o listener, o evento é perdido.

Por que os diálogos nativos de arquivo não podem ser automatizados com cliques

O seletor de arquivos nativo (o diálogo do sistema operacional que abre ao clicar em "Escolher arquivo") roda fora do motor de renderização do navegador. Pertence ao sistema operacional. O Playwright controla o navegador; não tem mecanismo para entrar em uma janela do Explorador de Arquivos ou do Finder e selecionar um arquivo.

Isso é intencional do ponto de vista de segurança. Se o JavaScript pudesse acionar e completar um diálogo de seleção de arquivo programaticamente, seria um caminho trivial para roubar arquivos da máquina do usuário. Os navegadores isolam essa interação especificamente para impedir isso.

A abordagem padrão para automatizar uploads contorna o diálogo completamente. Em vez de simular um clique que abre o diálogo, você interage diretamente com o elemento subjacente e diz qual arquivo usar. É exatamente o que setInputFiles() faz.

setInputFiles(): a abordagem padrão

A maioria dos formulários de upload usa um elemento , mesmo quando visualmente escondido atrás de um botão estilizado. setInputFiles() define os arquivos nesse input programaticamente, contornando o diálogo completamente.

import { test, expect } from '@playwright/test';
import path from 'path';

test('faz upload de um arquivo único', 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();
});

O padrão path.join(__dirname, ...) é importante aqui. Caminhos absolutos são mais confiáveis do que relativos. __dirname dá o diretório do arquivo de teste atual, então o caminho do fixture resolve corretamente independente de onde você roda o teste.

Se o input tem um atributo name ou id, ou está associado a um label, use um locator mais descritivo:

// Por texto do label
await page.getByLabel('Anexar documento').setInputFiles(filePath);

// Por atributo name
await page.locator('input[name="resume"]').setInputFiles(filePath);

setInputFiles() respeita o atributo accept do input. Se o input aceita apenas imagens (accept="image/*") e você tenta definir um .pdf, o Playwright não vai lançar erro, mas o navegador pode rejeitar o arquivo silenciosamente. Use sempre arquivos que correspondam aos tipos aceitos pelo input.

Fazendo upload de múltiplos arquivos de uma vez

Quando o input permite múltiplos arquivos (), passe um array de caminhos para setInputFiles():

test('faz upload de múltiplos arquivos de uma vez', 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();

  // Verifica que os três arquivos aparecem na confirmação
  await expect(page.getByText('document-a.pdf')).toBeVisible();
  await expect(page.getByText('document-b.pdf')).toBeVisible();
  await expect(page.getByText('image.png')).toBeVisible();
});

Para limpar a seleção e começar de novo, passe um array vazio:

// Remove todos os arquivos selecionados
await page.locator('input[type="file"]').setInputFiles([]);

Útil quando o fluxo do teste envolve mudar a seleção de arquivo antes de enviar, ou quando você precisa resetar o estado do input entre steps.

Se precisar construir arquivos de teste dinamicamente em vez de ler do disco, setInputFiles() também aceita um buffer:

test('faz upload de arquivo criado em memória', async ({ page }) => {
  await page.goto('https://lab.becomeqa.com/file-upload');

  await page.locator('input[type="file"]').setInputFiles({
    name: 'relatorio-gerado.txt',
    mimeType: 'text/plain',
    buffer: Buffer.from('Conteúdo do relatório\nLinha 2\nLinha 3'),
  });

  await page.getByRole('button', { name: 'Upload' }).click();

  await expect(page.getByText('relatorio-gerado.txt')).toBeVisible();
});

A abordagem com buffer é ideal quando o conteúdo do arquivo importa para o teste, mas você não quer manter arquivos em disco para cada variação.

Upload por drag-and-drop

Algumas interfaces de upload usam uma drop zone em vez de um input de arquivo: uma

que escuta eventos de dragover e drop. Não tem para chamar setInputFiles().

A abordagem aqui é disparar os eventos de drag manualmente usando page.dispatchEvent() com um objeto DataTransfer populado via page.evaluate():

test('faz upload de arquivo 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');

  // Lê o conteúdo do arquivo e cria um DataTransfer com ele
  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();
});

Mais trabalhoso do que setInputFiles(), mas é a abordagem correta para drop zones controladas por JavaScript. Se a drop zone também tiver um input de arquivo oculto que é ativado após o drop, a abordagem mais simples com setInputFiles() pode funcionar. Inspecione o DOM para verificar.

Antes de usar drag-and-drop, inspecione o HTML da drop zone. Muitas áreas de "arrastar e soltar" ainda contêm um oculto que você pode apontar com setInputFiles(). Confira no DevTools primeiro — economiza bastante complexidade.

Escutando eventos filechooser

Alguns botões de upload são elementos

A ordem importa: registre o listener de filechooser antes de clicar no botão. Se o diálogo disparar e resolver antes de você começar a escutar, você vai esperar para sempre.

fileChooser.setFiles() aceita os mesmos argumentos que setInputFiles(): um caminho único, um array de caminhos, ou um objeto de buffer. Para múltiplos arquivos:

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'),
]);

Você também pode verificar se o input aceita múltiplos arquivos antes de defini-los:

const fileChooser = await fileChooserPromise;
console.log('Aceita múltiplos:', fileChooser.isMultiple());
await fileChooser.setFiles(filePath);

Downloads: capturando o evento de download

Downloads funcionam por um mecanismo diferente. Quando o navegador dispara um download, o Playwright emite um evento download na página. Você o intercepta com page.waitForEvent('download') e recebe um objeto Download para trabalhar.

test('faz download de arquivo de relatório', async ({ page }) => {
  await page.goto('https://lab.becomeqa.com/reports');

  // Registra o listener antes de disparar o download
  const downloadPromise = page.waitForEvent('download');

  await page.getByRole('button', { name: 'Export CSV' }).click();

  const download = await downloadPromise;

  // Salva o arquivo em um local conhecido
  const savePath = path.join(__dirname, 'downloads/report.csv');
  await download.saveAs(savePath);

  // Verifica que o arquivo existe
  const { existsSync } = require('fs');
  expect(existsSync(savePath)).toBe(true);
});

Por padrão, o Playwright salva downloads em um diretório temporário que é limpo quando o contexto do navegador fecha. saveAs() copia o arquivo para um local que você controla, para inspecionar depois que o download completa.

O objeto download expõe o nome de arquivo sugerido antes de você salvar:

const download = await downloadPromise;
console.log('Nome sugerido:', download.suggestedFilename());

await download.saveAs(
  path.join(__dirname, `downloads/${download.suggestedFilename()}`)
);

suggestedFilename() retorna o que o servidor enviou no header Content-Disposition, que é exatamente o que um usuário veria como nome de arquivo padrão no diálogo de salvar.

Verificando o conteúdo do download

Capturar o download é o primeiro passo. Fazer assertions sobre o conteúdo é o que torna o teste significativo.

import { test, expect } from '@playwright/test';
import path from 'path';
import fs from 'fs';

test('CSV exportado contém os dados corretos', 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;

  // Verifica o nome do arquivo
  expect(download.suggestedFilename()).toMatch(/^report-\d{4}-\d{2}-\d{2}\.csv$/);

  // Salva e lê o conteúdo
  const savePath = path.join(__dirname, 'downloads/report.csv');
  await download.saveAs(savePath);

  const content = fs.readFileSync(savePath, 'utf-8');

  // Verifica linha de cabeçalho
  expect(content).toContain('id,destination,status,notes');

  // Verifica que o arquivo não está vazio além do cabeçalho
  const rows = content.trim().split('\n');
  expect(rows.length).toBeGreaterThan(1);
});

Para arquivos binários, você pode verificar o tamanho em vez do conteúdo:

test('PDF baixado não está vazio', 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); // Um PDF real tem mais de 1KB
});

Para formatos estruturados como JSON, deserialize e faça assertions na estrutura:

test('JSON exportado tem a estrutura esperada', 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');
});

Não faça assertions de tamanho exato para arquivos gerados. Timestamps, IDs ou conteúdo gerado pelo servidor podem causar diferenças em nível de byte entre execuções. Faça assertions de tamanho mínimo para arquivos binários e de estrutura de conteúdo para formatos de texto.

Limpando arquivos de teste com fixtures

Testes de upload e download deixam arquivos em disco. Ao rodar testes em paralelo ou em ambiente de CI, arquivos sobrando de uma execução podem contaminar outra. A ferramenta certa é uma fixture do Playwright que faz o cleanup automaticamente.

Crie uma fixture que fornece um diretório limpo de downloads e o remove após cada teste:

// 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);

    // Cleanup após o teste
    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';

Testes que usam essa fixture recebem um diretório isolado e novo para cada execução:

// tests/download.spec.ts
import { test, expect } from '../fixtures/file-test';
import path from 'path';

test('faz download para diretório isolado', 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 e seu conteúdo são removidos automaticamente após este teste
});

Para fixtures de upload, você pode gerar arquivos de teste temporários no setup da fixture e limpá-los depois:

export const test = base.extend<{
  uploadFixturePath: string;
}>({
  uploadFixturePath: async ({}, use) => {
    const filePath = path.join(__dirname, `../fixtures/temp-${Date.now()}.txt`);
    fs.writeFileSync(filePath, 'Conteúdo do arquivo de teste temporário');

    await use(filePath);

    fs.rmSync(filePath, { force: true });
  },
});

Esse padrão mantém a infraestrutura de teste honesta: se um teste falha e o cleanup nunca roda em um beforeAll/afterAll, o teardown da fixture ainda roda. O Playwright chama o teardown das fixtures mesmo quando os testes falham.

FAQ

E se o input de arquivo estiver oculto ou com display: none? setInputFiles() funciona em inputs ocultos: ignora o estado visual do elemento. Se você receber um erro de visibilidade, use { force: true } como opção: await page.locator('input[type="file"]').setInputFiles(filePath, { force: true }). Como lidar com upload que exige clicar em um botão primeiro para revelar o input?

Clique no botão primeiro, aguarde o input aparecer, depois chame setInputFiles(). Se o botão abre o diálogo nativo em vez disso, use page.waitForEvent('filechooser') antes do clique.

Posso testar que um download falha ou é bloqueado?

Sim. Intercepte a URL do download e retorne um status de erro: await page.route('/export/csv', route => route.fulfill({ status: 500 })). Depois verifique que a UI mostra uma mensagem de erro adequada. O evento download não vai disparar nesse caso.

page.waitForEvent('download') tem timeout?

Sim, usa o timeout de action padrão (geralmente 30 segundos). Se seu download demorar mais, passe um timeout customizado: page.waitForEvent('download', { timeout: 60000 }).

Como lidar com downloads em uma aba separada do navegador?

Se o download abre em uma nova página, escute no contexto do navegador: const download = await browserContext.waitForEvent('download'). Isso captura downloads de qualquer página no contexto.

O que acontece com o diretório temporário de download se eu não chamar saveAs()?

O arquivo fica no diretório interno temporário do Playwright até o contexto do navegador fechar, depois é deletado. Sempre chame saveAs() se precisar inspecionar o arquivo.

→ Veja também: Interceptação de Rede, Mocking e Stubbing no Playwright | Fixtures Personalizados no Playwright: O Padrão que Torna os Testes Legíveis | Testes de API com Playwright: Além da Interface | Assertions no Playwright: O Guia Completo