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 El enfoque aquí es despachar los eventos de drag manualmente usando Es más complejo que Algunos botones de subida son elementos El orden importa: registra el listener de También puedes verificar si el input de archivos acepta múltiples archivos antes de configurarlos: Las descargas funcionan mediante un mecanismo diferente. Cuando el navegador dispara una descarga, Playwright genera un evento Por defecto, Playwright guarda las descargas en un directorio temporal que se limpia cuando el contexto del navegador se cierra. El objeto Capturar la descarga es el primer paso. Asertar que el contenido es correcto es lo que hace al test significativo. Para archivos binarios, podés verificar el tamaño en lugar del contenido: Para formatos estructurados como JSON, deserializá y asertá la estructura: 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: Los tests que usan este fixture obtienen un directorio fresco y aislado para cada ejecución: Para fixtures de subida, podés generar archivos de test temporales en el setup del fixture y limpiarlos después: Este patrón mantiene la infraestructura de test honesta: si un test falla y la limpieza no se ejecuta en Haz clic en el botón primero, espera a que aparezca el input, luego llama Sí. Interceptá la URL de descarga y retorná un estado de error: Sí, usa el timeout de acción por defecto (generalmente 30 segundos). Si tu descarga tarda más, pasá un timeout personalizado: Si la descarga se abre en una nueva página, escuchá en el contexto del navegador: El archivo vive en el directorio temporal interno de Playwright hasta que el contexto del navegador se cierra, y luego se elimina. Siempre llama dragover y drop. No hay en el que llamar setInputFiles().
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();
});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. oculto que podés apuntar con setInputFiles(). Revisá primero con DevTools; te ahorra bastante complejidad.Escuchar el evento filechooser
o page.waitForEvent('filechooser') de Playwright te permite interceptar el diálogo que se abre cuando se hace clic en ese tipo de botón:
test('maneja un botón de subida personalizado', async ({ page }) => {
await page.goto('https://lab.becomeqa.com/file-upload');
const filePath = path.join(__dirname, 'fixtures/muestra.pdf');
// Configurar el listener ANTES del clic
const fileChooserPromise = page.waitForEvent('filechooser');
await page.getByRole('button', { name: 'Elegir archivo' }).click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(filePath);
await expect(page.getByText('muestra.pdf')).toBeVisible();
});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'),
]);const fileChooser = await fileChooserPromise;
console.log('Acepta múltiples:', fileChooser.isMultiple());
await fileChooser.setFiles(filePath);Descargas: capturar el evento de descarga
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);
});saveAs() copia el archivo a una ubicación que controlás para poder inspeccionarlo después de que complete la descarga.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
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);
});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
});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');
});Limpiar archivos de test con fixtures
// 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';// 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
});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 });
},
});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?
setInputFiles(). Si el botón abre el diálogo nativo en su lugar, usa page.waitForEvent('filechooser') antes del clic.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?
page.waitForEvent('download', { timeout: 60000 }).const download = await browserContext.waitForEvent('download'). Esto captura descargas de cualquier página en el contexto.saveAs()?
saveAs() si necesitas inspeccionar el archivo.