Playwright gère les uploads de fichiers avec setInputFiles(), qui contourne entièrement la boîte de dialogue native du système en définissant les fichiers directement sur l'élément . Les téléchargements sont capturés via l'événement Download pour obtenir une référence au fichier avant qu'il ne soit enregistré sur disque.
Pourquoi les boîtes de dialogue natives ne peuvent pas être automatisées par clic
Le sélecteur de fichier natif (la boîte de dialogue du système qui s'ouvre quand vous cliquez sur "Choisir un fichier") s'exécute en dehors du moteur de rendu du navigateur. Il appartient au système d'exploitation. Playwright contrôle le navigateur et n'a aucun mécanisme pour accéder à une fenêtre Windows Explorer ou macOS Finder.
C'est intentionnel pour des raisons de sécurité. Si JavaScript pouvait déclencher et compléter un sélecteur de fichiers par programmation, cela ouvrirait une voie triviale pour voler des fichiers d'une machine utilisateur.
L'approche standard contourne entièrement la boîte de dialogue. Au lieu de simuler un clic qui l'ouvre, vous interagissez directement avec l'élément sous-jacent et lui indiquez quel fichier utiliser. La méthode setInputFiles() de Playwright fait exactement cela.
setInputFiles() : l'approche standard
La plupart des formulaires d'upload utilisent un élément , même quand il est visuellement caché derrière un bouton stylisé. setInputFiles() définit les fichiers sur cet input par programmation, contournant complètement la boîte de dialogue.
import { test, expect } from '@playwright/test';
import path from 'path';
test('upload d\'un seul fichier', 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 réussi')).toBeVisible();
});Le schéma path.join(__dirname, ...) est important. Les chemins absolus sont plus fiables que les relatifs. __dirname donne le répertoire du fichier de test courant, donc le chemin du fixture se résout correctement quel que soit l'endroit depuis lequel vous lancez le test.
Si l'input de fichier a un attribut name ou id, ou est associé à un label, utilisez un locator plus descriptif :
// Par texte du label
await page.getByLabel('Joindre un document').setInputFiles(filePath);
// Par attribut name
await page.locator('input[name="resume"]').setInputFiles(filePath);setInputFiles() respecte l'attribut accept de l'input. Si l'input est limité aux images (accept="image/*") et que vous tentez de définir un .pdf, Playwright ne lèvera pas d'erreur, mais le navigateur peut rejeter le fichier silencieusement. Utilisez toujours des fichiers correspondant aux types acceptés par l'input.Uploader plusieurs fichiers à la fois
Quand l'input accepte plusieurs fichiers (), passez un tableau de chemins à setInputFiles() :
test('upload de plusieurs fichiers à la fois', 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();
});Pour effacer la sélection et recommencer, passez un tableau vide :
// Supprimer tous les fichiers sélectionnés
await page.locator('input[type="file"]').setInputFiles([]);Si vous avez besoin de construire des fichiers de test dynamiquement plutôt que de les lire depuis le disque, setInputFiles() accepte aussi un buffer :
test('upload d\'un fichier créé en mémoire', async ({ page }) => {
await page.goto('https://lab.becomeqa.com/file-upload');
await page.locator('input[type="file"]').setInputFiles({
name: 'rapport-genere.txt',
mimeType: 'text/plain',
buffer: Buffer.from('Contenu du rapport de test\nLigne 2\nLigne 3'),
});
await page.getByRole('button', { name: 'Upload' }).click();
await expect(page.getByText('rapport-genere.txt')).toBeVisible();
});L'approche buffer est idéale quand le contenu du fichier compte pour le test mais que vous ne voulez pas maintenir des fichiers sur disque pour chaque variation.
Upload par glisser-déposer
Certaines interfaces d'upload utilisent une zone de dépôt plutôt qu'un input de fichier : un L'approche consiste à déclencher les événements de glisser-déposer manuellement via Certains boutons d'upload sont des éléments L'ordre compte : enregistrez l'écouteur Les téléchargements fonctionnent selon un mécanisme différent. Quand le navigateur déclenche un téléchargement, Playwright émet un événement Par défaut, Playwright sauvegarde les téléchargements dans un répertoire temporaire nettoyé à la fermeture du contexte de navigateur. L'objet Capturer le téléchargement est la première étape. Vérifier que le contenu est correct est ce qui rend le test utile. Pour les fichiers binaires, vérifiez la taille plutôt que le contenu : Pour les formats structurés comme JSON, désérialisez et vérifiez la structure : Les tests d'upload et de téléchargement laissent des fichiers sur disque. En exécution parallèle ou en CI, les fichiers résiduels d'une exécution peuvent contaminer la suivante. La bonne solution est une fixture Playwright qui gère le nettoyage automatiquement. Créez une fixture qui fournit un répertoire de téléchargement propre et le supprime après chaque test : Les tests qui utilisent cette fixture obtiennent un répertoire isolé et propre pour chaque exécution : Ce schéma garde l'infrastructure de test cohérente. Si un test échoue et que le nettoyage ne s'exécute pas dans un Cliquez d'abord sur le bouton, attendez que l'input apparaisse, puis appelez Oui. Interceptez l'URL de téléchargement et retournez un statut d'erreur : Oui, il utilise le timeout d'action par défaut (généralement 30 secondes). Si votre téléchargement prend plus longtemps, passez un timeout personnalisé : Si le téléchargement s'ouvre dans une nouvelle page, écoutez sur le contexte du navigateur : dragover et drop. Il n'y a pas de sur lequel appeler setInputFiles().
page.dispatchEvent() avec un objet DataTransfer peuplé via page.evaluate() :test('upload d\'un fichier par glisser-déposer', 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');
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();
}); caché que vous pouvez cibler avec setInputFiles(). Vérifiez avec les DevTools en premier ; cela évite une complexité significative.Écouter les événements filechooser
ou page.waitForEvent('filechooser') de Playwright permet d'intercepter la boîte de dialogue qui s'ouvre quand ce bouton est cliqué :
test('gestion d\'un bouton d\'upload personnalisé', async ({ page }) => {
await page.goto('https://lab.becomeqa.com/file-upload');
const filePath = path.join(__dirname, 'fixtures/sample.pdf');
// Configurer l'écouteur AVANT le clic
const fileChooserPromise = page.waitForEvent('filechooser');
await page.getByRole('button', { name: 'Choisir un fichier' }).click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(filePath);
await expect(page.getByText('sample.pdf')).toBeVisible();
});filechooser avant de cliquer sur le bouton. Si la boîte de dialogue se déclenche et se résout avant que vous commenciez à écouter, vous attendrez indéfiniment.fileChooser.setFiles() accepte les mêmes arguments que setInputFiles(), soit un seul chemin, un tableau de chemins ou un objet buffer. Pour plusieurs fichiers :
const fileChooserPromise = page.waitForEvent('filechooser');
await page.getByRole('button', { name: 'Joindre des fichiers' }).click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles([
path.join(__dirname, 'fixtures/piece-jointe-1.pdf'),
path.join(__dirname, 'fixtures/piece-jointe-2.pdf'),
]);Téléchargements : capturer l'événement download
download sur la page. Interceptez-le avec page.waitForEvent('download') et obtenez un objet Download exploitable.test('téléchargement d\'un fichier rapport', async ({ page }) => {
await page.goto('https://lab.becomeqa.com/reports');
// Enregistrer l'écouteur avant de déclencher le téléchargement
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: 'Exporter CSV' }).click();
const download = await downloadPromise;
// Sauvegarder le fichier à un emplacement connu
const savePath = path.join(__dirname, 'downloads/rapport.csv');
await download.saveAs(savePath);
const { existsSync } = require('fs');
expect(existsSync(savePath)).toBe(true);
});saveAs() copie le fichier vers un emplacement que vous contrôlez pour pouvoir l'inspecter après.download expose le nom de fichier suggéré avant la sauvegarde :const download = await downloadPromise;
console.log('Nom suggéré :', download.suggestedFilename());
await download.saveAs(
path.join(__dirname, `downloads/${download.suggestedFilename()}`)
);suggestedFilename() retourne ce que le serveur a envoyé dans l'en-tête Content-Disposition, soit exactement ce qu'un utilisateur verrait comme nom de fichier par défaut dans la boîte de dialogue.
Vérifier le contenu des téléchargements
import { test, expect } from '@playwright/test';
import path from 'path';
import fs from 'fs';
test('le CSV exporté contient les bonnes données', async ({ page }) => {
await page.goto('https://lab.becomeqa.com/reports');
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: 'Exporter CSV' }).click();
const download = await downloadPromise;
// Vérifier le nom du fichier
expect(download.suggestedFilename()).toMatch(/^rapport-\d{4}-\d{2}-\d{2}\.csv$/);
// Sauvegarder et lire le contenu
const savePath = path.join(__dirname, 'downloads/rapport.csv');
await download.saveAs(savePath);
const content = fs.readFileSync(savePath, 'utf-8');
// Vérifier la ligne d'en-tête
expect(content).toContain('id,destination,statut,notes');
// Vérifier que le fichier n'est pas vide au-delà de l'en-tête
const rows = content.trim().split('\n');
expect(rows.length).toBeGreaterThan(1);
});test('le PDF téléchargé n\'est pas vide', async ({ page }) => {
await page.goto('https://lab.becomeqa.com/reports');
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: 'Exporter PDF' }).click();
const download = await downloadPromise;
const savePath = path.join(__dirname, 'downloads/rapport.pdf');
await download.saveAs(savePath);
const stats = fs.statSync(savePath);
expect(download.suggestedFilename()).toBe('rapport.pdf');
expect(stats.size).toBeGreaterThan(1000); // Un vrai PDF fait plus d'1 Ko
});test('le JSON exporté a la structure attendue', async ({ page }) => {
await page.goto('https://lab.becomeqa.com/reports');
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: 'Exporter 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('statut');
});Nettoyer les fichiers de test avec des 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);
// Nettoyage après le 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('téléchargement du rapport dans un répertoire isolé', async ({ page, downloadDir, testFilePath }) => {
await page.goto('https://lab.becomeqa.com/reports');
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: 'Exporter 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 et son contenu sont supprimés automatiquement après ce test
});beforeAll/afterAll, le teardown de la fixture s'exécute quand même. Playwright appelle le teardown des fixtures même quand les tests échouent.FAQ
Et si l'input de fichier est caché ou a display: none ?
setInputFiles() fonctionne sur les inputs cachés. Il contourne l'état visuel de l'élément. Si vous obtenez une erreur de visibilité, utilisez { force: true } en option : await page.locator('input[type="file"]').setInputFiles(filePath, { force: true }).
Comment gérer un upload qui nécessite de cliquer un bouton d'abord pour révéler l'input ?
setInputFiles(). Si le bouton ouvre la boîte de dialogue native, utilisez page.waitForEvent('filechooser') avant le clic.await page.route('/export/csv', route => route.fulfill({ status: 500 })). Vérifiez ensuite que l'interface affiche un message d'erreur approprié. L'événement download ne se déclenchera pas dans ce cas.page.waitForEvent('download') peut-il expirer ?
page.waitForEvent('download', { timeout: 60000 }).const download = await browserContext.waitForEvent('download'). Cela capture les téléchargements de n'importe quelle page dans le contexte.