setInputFiles() de Playwright omite el selector de archivos del sistema operativo configurando los archivos directamente en el elemento , por eso podés automatizar subidas de archivos sin que el navegador abra ningún diálogo. Las descargas funcionan diferente: capturás el evento download con page.waitForEvent('download'), pero tenés que registrarlo antes de disparar la descarga, no después: si la descarga ocurre mientras todavía estás configurando el listener, el evento se pierde. Este artículo cubre subidas de archivos simples y múltiples, buffers de archivos en memoria, drag-and-drop vía DataTransfer, el evento filechooser para botones de subida personalizados, captura de descargas y verificación de contenido, y limpieza con fixtures.

Por qué los diálogos de archivos nativos no se pueden automatizar con clics

El selector de archivos nativo (el diálogo del SO que se abre cuando haces clic en "Elegir archivo") corre fuera del motor de renderizado del navegador. Pertenece al sistema operativo. Playwright controla el navegador; no tiene mecanismo para acceder a una ventana de Windows Explorer o Finder de macOS y seleccionar un archivo.

Esto es intencional desde el punto de vista de la seguridad. Si JavaScript pudiera disparar y completar un diálogo de selector de archivos de forma programática, sería un camino trivial para robar archivos de la máquina de un usuario. Los navegadores aíslan esa interacción específicamente para evitarlo.

Por eso el enfoque estándar para automatizar subidas de archivos evita el diálogo por completo. En lugar de simular un clic que abre el diálogo, interactúas directamente con el elemento subyacente y le decís qué archivo usar. El método setInputFiles() de Playwright hace exactamente eso.

setInputFiles(): el enfoque estándar

La mayoría de los formularios de subida usan un elemento , aunque esté visualmente oculto detrás de un botón estilizado. setInputFiles() configura los archivos en ese input de forma programática, sin pasar por el diálogo.

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

test('sube un archivo único', async ({ page }) => {
  await page.goto('https://lab.becomeqa.com/file-upload');

  const filePath = path.join(__dirname, 'fixtures/muestra.pdf');

  await page.locator('input[type="file"]').setInputFiles(filePath);

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

  await expect(page.getByText('Subida exitosa')).toBeVisible();
});

El patrón path.join(__dirname, ...) es importante. Las rutas absolutas son más confiables que las relativas. __dirname te da el directorio del archivo de test actual, así la ruta del fixture se resuelve correctamente sin importar desde dónde ejecutas el test.

Si el input de archivo tiene un atributo name o id, o está asociado a un label, puedes usar un locator más descriptivo:

// Por texto del label
await page.getByLabel('Adjuntar documento').setInputFiles(filePath);

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

setInputFiles() respeta el atributo accept del input. Si el input está restringido a imágenes (accept="image/*") e intentás configurar un .pdf, Playwright no lanzará un error, pero el navegador puede rechazar el archivo silenciosamente. Siempre usá archivos que coincidan con los tipos aceptados por el input.

Subir múltiples archivos a la vez

Cuando el input permite múltiples archivos (), pasá un array de rutas a setInputFiles():

test('sube múltiples archivos a la vez', async ({ page }) => {
  await page.goto('https://lab.becomeqa.com/file-upload');

  const files = [
    path.join(__dirname, 'fixtures/documento-a.pdf'),
    path.join(__dirname, 'fixtures/documento-b.pdf'),
    path.join(__dirname, 'fixtures/imagen.png'),
  ];

  await page.locator('input[type="file"]').setInputFiles(files);

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

  // Verificar que los tres archivos aparecen en la confirmación
  await expect(page.getByText('documento-a.pdf')).toBeVisible();
  await expect(page.getByText('documento-b.pdf')).toBeVisible();
  await expect(page.getByText('imagen.png')).toBeVisible();
});

Para limpiar la selección y empezar de nuevo, pasa un array vacío:

// Eliminar todos los archivos seleccionados
await page.locator('input[type="file"]').setInputFiles([]);

Si necesitas construir archivos de test dinámicamente en lugar de leerlos del disco, setInputFiles() también acepta un buffer:

test('sube un archivo creado en memoria', async ({ page }) => {
  await page.goto('https://lab.becomeqa.com/file-upload');

  await page.locator('input[type="file"]').setInputFiles({
    name: 'reporte-generado.txt',
    mimeType: 'text/plain',
    buffer: Buffer.from('Contenido del reporte de test\nLínea 2\nLínea 3'),
  });

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

  await expect(page.getByText('reporte-generado.txt')).toBeVisible();
});

El enfoque de buffer es ideal cuando el contenido del archivo importa para el test pero no querés mantener archivos en disco para cada variación.

Subida de archivos por drag-and-drop

Algunas interfaces de subida usan una zona de drop en lugar de un input de archivo: un

que escucha eventos dragover y drop. No hay en el que llamar setInputFiles().

El enfoque aquí es despachar los eventos de drag manualmente usando page.dispatchEvent() con un objeto DataTransfer poblado vía page.evaluate():

test('sube un archivo por 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/muestra.pdf');

  // Leer el contenido del archivo y crear un DataTransfer con él
  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: 'muestra.pdf', mimeType: 'application/pdf' }
  );

  await expect(page.getByText('muestra.pdf')).toBeVisible();
});

Es más complejo que setInputFiles(), pero es el enfoque correcto para zonas de drop controladas por JavaScript. Si la zona de drop también tiene un input de archivo oculto que se activa después del drop, el enfoque más simple de setInputFiles() puede funcionar. Inspecciona el DOM para averiguarlo.

Antes de recurrir al enfoque de drag-and-drop, inspeccioná el HTML de la zona de drop. Muchas áreas de "arrastrá y soltá" siguen conteniendo un oculto que podés apuntar con setInputFiles(). Revisá primero con DevTools; te ahorra bastante complejidad.

Escuchar el evento filechooser

Algunos botones de subida son elementos

El orden importa: registra el listener de filechooser antes de hacer clic en el botón. Si el diálogo se abre y resuelve antes de que empieces a escuchar, esperarás para siempre.

fileChooser.setFiles() acepta los mismos argumentos que setInputFiles(): una ruta, un array de rutas, o un objeto buffer. Para múltiples archivos:

const fileChooserPromise = page.waitForEvent('filechooser');
await page.getByRole('button', { name: 'Adjuntar archivos' }).click();
const fileChooser = await fileChooserPromise;

await fileChooser.setFiles([
  path.join(__dirname, 'fixtures/adjunto-1.pdf'),
  path.join(__dirname, 'fixtures/adjunto-2.pdf'),
]);

También puedes verificar si el input de archivos acepta múltiples archivos antes de configurarlos:

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

Descargas: capturar el evento de descarga

Las descargas funcionan mediante un mecanismo diferente. Cuando el navegador dispara una descarga, Playwright genera un evento download en la página. Lo interceptás con page.waitForEvent('download') y obtenés un objeto Download con el que podés trabajar.

test('descarga un archivo de reporte', async ({ page }) => {
  await page.goto('https://lab.becomeqa.com/reports');

  // Registrar el listener antes de disparar la descarga
  const downloadPromise = page.waitForEvent('download');

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

  const download = await downloadPromise;

  // Guardar el archivo en una ubicación conocida
  const savePath = path.join(__dirname, 'downloads/reporte.csv');
  await download.saveAs(savePath);

  // Verificar que el archivo existe
  const { existsSync } = require('fs');
  expect(existsSync(savePath)).toBe(true);
});

Por defecto, Playwright guarda las descargas en un directorio temporal que se limpia cuando el contexto del navegador se cierra. saveAs() copia el archivo a una ubicación que controlás para poder inspeccionarlo después de que complete la descarga.

El objeto download expone el nombre de archivo sugerido antes de que guardes el archivo:

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

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

suggestedFilename() devuelve lo que el servidor envió en el header Content-Disposition, que es exactamente lo que un usuario vería como nombre de archivo por defecto en el diálogo de guardar.

Verificar el contenido de la descarga

Capturar la descarga es el primer paso. Asertar que el contenido es correcto es lo que hace al test significativo.

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

test('el CSV exportado contiene los datos correctos', async ({ page }) => {
  await page.goto('https://lab.becomeqa.com/reports');

  const downloadPromise = page.waitForEvent('download');
  await page.getByRole('button', { name: 'Exportar CSV' }).click();
  const download = await downloadPromise;

  // Asertar el nombre de archivo
  expect(download.suggestedFilename()).toMatch(/^reporte-\d{4}-\d{2}-\d{2}\.csv$/);

  // Guardar y leer el contenido
  const savePath = path.join(__dirname, 'downloads/reporte.csv');
  await download.saveAs(savePath);

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

  // Asertar la fila de encabezado
  expect(content).toContain('id,destino,estado,notas');

  // Asertar que el archivo no está vacío más allá del encabezado
  const rows = content.trim().split('\n');
  expect(rows.length).toBeGreaterThan(1);
});

Para archivos binarios, podés verificar el tamaño en lugar del contenido:

test('el PDF descargado no está vacío', async ({ page }) => {
  await page.goto('https://lab.becomeqa.com/reports');

  const downloadPromise = page.waitForEvent('download');
  await page.getByRole('button', { name: 'Exportar PDF' }).click();
  const download = await downloadPromise;

  const savePath = path.join(__dirname, 'downloads/reporte.pdf');
  await download.saveAs(savePath);

  const stats = fs.statSync(savePath);

  expect(download.suggestedFilename()).toBe('reporte.pdf');
  expect(stats.size).toBeGreaterThan(1000); // Un PDF real pesa más de 1KB
});

Para formatos estructurados como JSON, deserializá y asertá la estructura:

test('el JSON exportado tiene la estructura esperada', async ({ page }) => {
  await page.goto('https://lab.becomeqa.com/reports');

  const downloadPromise = page.waitForEvent('download');
  await page.getByRole('button', { name: 'Exportar JSON' }).click();
  const download = await downloadPromise;

  const savePath = path.join(__dirname, 'downloads/exportacion.json');
  await download.saveAs(savePath);

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

  expect(Array.isArray(content)).toBe(true);
  expect(content[0]).toHaveProperty('destino');
  expect(content[0]).toHaveProperty('estado');
});

No asertes tamaños exactos de archivos generados. Timestamps, IDs o contenido generado por el servidor pueden causar diferencias a nivel de bytes entre ejecuciones. Asertá tamaño mínimo para archivos binarios y estructura de contenido para formatos de texto.

Limpiar archivos de test con fixtures

Los tests de subida y descarga dejan archivos en disco. Si corrés tests en paralelo o en CI, los archivos sobrantes de una ejecución pueden contaminar otra. La herramienta correcta es un fixture de Playwright que maneja la limpieza automáticamente.

Crea un fixture que provea un directorio de descargas limpio y lo elimine después de cada test:

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

    // Limpiar después del test
    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';

Los tests que usan este fixture obtienen un directorio fresco y aislado para cada ejecución:

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

test('descarga el reporte a un directorio aislado', async ({ page, downloadDir, testFilePath }) => {
  await page.goto('https://lab.becomeqa.com/reports');

  const downloadPromise = page.waitForEvent('download');
  await page.getByRole('button', { name: 'Exportar 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('destino');
  // downloadDir y su contenido se eliminan automáticamente después del test
});

Para fixtures de subida, podés generar archivos de test temporales en el setup del fixture y limpiarlos después:

export const test = base.extend<{
  uploadFixturePath: string;
}>({
  uploadFixturePath: async ({}, use) => {
    const filePath = path.join(__dirname, `../fixtures/temp-${Date.now()}.txt`);
    fs.writeFileSync(filePath, 'Contenido de archivo de test temporal');

    await use(filePath);

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

Este patrón mantiene la infraestructura de test honesta: si un test falla y la limpieza no se ejecuta en beforeAll/afterAll, el teardown del fixture igual corre porque Playwright lo llama incluso cuando los tests fallan.

FAQ

¿Qué pasa si el input de archivo está oculto o tiene display: none? setInputFiles() funciona con inputs ocultos. Omite el estado visual del elemento. Si obtenés un error sobre visibilidad, usá { force: true } como opción: await page.locator('input[type="file"]').setInputFiles(filePath, { force: true }). ¿Cómo manejo una subida que requiere hacer clic en un botón primero para revelar el input?

Haz clic en el botón primero, espera a que aparezca el input, luego llama setInputFiles(). Si el botón abre el diálogo nativo en su lugar, usa page.waitForEvent('filechooser') antes del clic.

¿Puedo testear que una descarga falla o está bloqueada?

Sí. Interceptá la URL de descarga y retorná un estado de error: await page.route('/export/csv', route => route.fulfill({ status: 500 })). Luego verificá que la UI muestre un mensaje de error apropiado. El evento download no se disparará en este caso.

¿page.waitForEvent('download') tiene timeout?

Sí, usa el timeout de acción por defecto (generalmente 30 segundos). Si tu descarga tarda más, pasá un timeout personalizado: page.waitForEvent('download', { timeout: 60000 }).

¿Cómo manejo descargas en una pestaña separada del navegador?

Si la descarga se abre en una nueva página, escuchá en el contexto del navegador: const download = await browserContext.waitForEvent('download'). Esto captura descargas de cualquier página en el contexto.

¿Qué pasa con el directorio temporal de descargas si no llamo saveAs()?

El archivo vive en el directorio temporal interno de Playwright hasta que el contexto del navegador se cierra, y luego se elimina. Siempre llama saveAs() si necesitas inspeccionar el archivo.

→ See also: Interceptación de Red, Mocks y Stubs en Playwright | Fixtures Personalizados en Playwright: El Patrón que Hace los Tests Legibles | API Testing con Playwright: Más Allá de la UI | Assertions en Playwright: La Guía Completa