Retour au blog
Guides
Mihnea-Octavian ManolacheLast updated on May 8, 202615 min read

Formulaire de soumission Puppeteer : Guide Node.js pour 2026

Formulaire de soumission Puppeteer : Guide Node.js pour 2026
En bref : utilisez page.locator(selector).fill(value) pour des scripts Puppeteer de soumission de formulaire rapides et déterministes, et page.type() lorsque la page surveille les frappes réelles (autocomplétion, anti-bot, validation en temps réel). Envoyez le formulaire en cliquant sur le bouton, en appuyant sur Entrée ou en appelant form.requestSubmit(), et attendez toujours un signal de réussite concret plutôt qu'un délai d'expiration fixe.

Les formulaires sont au cœur du fonctionnement de la plupart des pages utiles. Connexions, barres de recherche, processus de paiement, outils de téléchargement de fichiers, assistants d’intégration en plusieurs étapes : si vous automatisez le Web à des fins de test ou de scraping, tôt ou tard, vous devrez gérer un formulaire. Un workflow de soumission de formulaire avec Puppeteer semble d'une simplicité trompeuse au premier abord, puis se heurte aux réalités d'un site moderne : le réaffichage des applications monopages, les pièges cachés, les champs de saisie sans valeur, les éditeurs piégés dans des iframes, et le JavaScript qui jette discrètement votre saisie à la poubelle parce qu'il n'a jamais vu d'événement keydown .

Un formulaire HTML est un <form> élément <input>, <select>, <textarea>et des contrôles similaires, avec un action attribut et un déclencheur de soumission qui envoie les données collectées pour traitement. C'est la partie facile. La partie difficile consiste à faire en sorte qu'un script Chrome sans interface utilisateur se comporte suffisamment comme une personne pour que la page accepte effectivement la soumission et vous renvoie une réponse utilisable.

Ce guide est l'aide-mémoire que j'aurais aimé avoir lorsque j'ai commencé à déployer des scripts Puppeteer en production. Nous choisirons l'API adaptée au type de données, définirons des sélecteurs stables, passerons en revue trois stratégies de soumission et les cas où chacune d'entre elles échoue, aborderons tous les types d'entrée courants (y compris les sélecteurs de fichiers personnalisés et les éditeurs de texte enrichi), attendrons le bon signal de réussite, validerons le résultat et terminerons par une liste de contrôle de débogage pour les redoutables échecs silencieux.

Pourquoi l'automatisation de l'envoi de formulaires avec Puppeteer est plus difficile qu'il n'y paraît

Les formulaires contrôlent les parties les plus précieuses du Web moderne : création de compte, résultats de recherche, tableaux de bord, téléchargements payants. Ils concentrent également tous les points faibles de l’automatisation des navigateurs en un seul endroit. Un simple script Puppeteer de soumission de formulaire peut devoir gérer des entrées React ou Vue qui ignorent une value , une validation qui se déclenche à chaque frappe, des libellés ARIA uniquement sans id, des champs honeypot cachés, des éléments hors écran sur lesquels vous ne pouvez pas cliquer et des sandbox iframe pour le texte enrichi. Si vous partez du principe qu’un formulaire n’est qu’un simple HTML statique, votre script échouera silencieusement. Les modèles ci-dessous partent du principe que ce n’est pas le cas.

Configuration du projet : Node.js, ESM et une installation Puppeteer opérationnelle

Créez un nouveau dossier et exécutez npm init -y. Définissez "type": "module" en package.json la syntaxe import fonctionne, puis installez le paquet complet avec npm install puppeteer. Cela fournit un binaire Chromium adapté, vous n'avez donc pas besoin d'un navigateur séparé. Utilisez puppeteer-core à la place si vous prévoyez de vous connecter à une installation Chrome existante. Avant d'écrire le moindre sélecteur, effectuez un test de base pour vérifier que tout est bien configuré :

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({ headless: 'new' });
const page = await browser.newPage();
await page.goto('https://example.com');
console.log(await page.title());
await browser.close();

Si le titre d'une page réelle s'affiche, tout va bien. Exécutez avec headless: false pendant le débogage, puis passez à 'new' une fois que le script est stable.

Choisir la bonne méthode de saisie : page.type vs Locator.fill vs injection de valeur brute

Puppeteer vous offre trois façons d’insérer du texte dans un champ, et ce choix a des conséquences réelles tant sur la vitesse que sur la détection des bots. Notez que, d’après la documentation actuelle de Puppeteer au moment de la rédaction de cet article, il n’existe pas de méthode de niveau supérieur page.fill() dans la Page classe comme le fait Playwright ; l'action équivalente se trouve dans l'API Locator de Puppeteer via page.locator(selector).fill(value).

Méthode

Événements déclenchés

Vitesse

Quand l'utiliser

page.type(selector, value)

keydown, keypress, input, keyup par personnage

Lente

Validation en temps réel, saisie semi-automatique, surveillance anti-bot, suggestions de recherche

page.locator(sel).fill(value)

input, change (en une seule fois)

Rapide

Vous n'avez besoin que de la valeur finale dans le champ

$eval(sel, el => el.value = ...)

Aucun, sauf si vous les déclenchez

Le plus rapide

Formulaires en masse où la page n'écoute pas les frappes

Si vous suivez la $eval , effectuez le dispatch new Event('input', { bubbles: true }) ensuite pour que React ou Vue détecte bien le changement.

Cibler les champs de formulaire avec des sélecteurs stables

Un script Puppeteer de soumission de formulaire ne fonctionne que si ses sélecteurs survivent à un redéploiement. Classez vos options par ordre de priorité :

  1. #id lorsqu'un id existe et semble stable.
  2. [name="..."] pour tout <input name> qui envoie une requête POST vers un backend, puisque le nom fait partie du contrat.
  3. [data-testid="..."] ou d'autres data-* hooks ajoutés explicitement pour l'automatisation.
  4. aria-label et label[for] des chaînes pour les interfaces utilisateur axées sur l'accessibilité.
  5. Les sélecteurs d'attributs CSS tels que input[type="email"] uniquement lorsque le formulaire comporte exactement un champ de ce type.
  6. XPath en dernier recours, lorsque vous avez besoin d'une correspondance de texte telle que //button[contains(., "Sign in")].

Évitez les noms de classe générés automatiquement comme .css-1q8r9j. Privilégier le CSS plutôt que l'XPath est généralement plus efficace en termes de clarté et de rapidité, mais l'XPath est indispensable lorsque vous devez vous ancrer sur du texte visible.

Exemple complet : recherche sur Yelp par lieu

La barre de recherche de Yelp utilise deux champs de saisie : #find_desc pour ce que vous recherchez et #dropperText_Mast pour le lieu. Locator fill convient ici ; le formulaire n'a pas besoin d'événements par touche.

await page.goto('https://www.yelp.com');
await page.locator('#find_desc').fill('coffee');
await page.locator('#dropperText_Mast').fill('Berlin, Germany');

await Promise.all([
  page.waitForNavigation({ waitUntil: 'networkidle2' }),
  page.click('button[type="submit"]'),
]);

await page.waitForSelector('h3 a.businessName__09f24__HG_pC', { timeout: 10000 });

Le Promise.all modèle déclenche le click et l'écouteur de navigation de manière atomique, vous ne manquez donc jamais l'événement de navigation car il s'est résolu avant que l'attente ne soit enregistrée.

Exemple complet : connexion à GitHub avec des sélecteurs mixtes

La page de connexion de GitHub est un bon exercice, car les trois champs utilisent tous des styles de sélecteurs différents : id sur le nom d'utilisateur, name sur le mot de passe, et type sur le bouton de soumission.

await page.goto('https://github.com/login');
await page.type('input[id="login_field"]', process.env.GH_USER);
await page.type('input[name="password"]', process.env.GH_PASS);

await Promise.all([
  page.waitForNavigation(),
  page.click('input[type="submit"]'),
]);

J'utilise délibérément page.type ici. Les pages de connexion ont tendance à identifier les sessions qui saisissent les identifiants trop rapidement, et les frappes caractère par caractère laissent une trace plus humaine. Ne codez jamais les identifiants en dur ; récupérez-les à partir de variables d'environnement.

Trois méthodes fiables de soumission de formulaire avec Puppeteer (et quand chacune d'entre elles échoue)

Une fois les champs remplis, vous disposez de trois options fiables :

  1. Cliquez sur le bouton de soumission avec page.click('button[type="submit"]'). C'est l'option par défaut. Elle échoue lorsque le bouton est masqué, hors de l'écran ou recouvert par une bannière fixe. Résolvez d'abord le problème avec page.waitForSelector(sel, { visible: true }) la première.
  2. Appuyez sur Entrée avec await page.keyboard.press('Enter') après avoir mis un champ en surbrillance. Fonctionne pour presque tous les champs de recherche et formulaires de connexion. Échoue lorsque la page intercepte la touche Entrée pour la saisie automatique, ou lorsqu'aucun champ n'est en surbrillance.
  3. Appeler form.requestSubmit() through page.$eval('form', f => f.requestSubmit()). Contourne entièrement les gestionnaires de clic et exécute la validation native, ce qui est utile lorsque le bouton visible est personnalisé et peu fiable. Échoue lorsqu'un gestionnaire JS personnalisé court-circuite la soumission réelle et n'écoute que le clic.

Choisissez en fonction du comportement, pas de l'habitude.

Gérer tous les types d'entrée courants

Au-delà des simples champs de texte, les formulaires réels combinent des cases à cocher, des boutons radio, des menus déroulants, des curseurs, des dates, des fichiers et du texte enrichi. Chacun dispose d'un chemin de réussite adapté à Puppeteer et de quelques pièges. Les quatre sous-sections suivantes les traitent à l'aide de modèles copiables-collables.

Cases à cocher et boutons radio

Pour les cases à cocher et les boutons radio natifs, page.click est votre allié ; il bascule l'état et déclenche les événements appropriés. Traitez les groupes de boutons radio par leur attribut, et non pas uniquement par leur position.

await page.click('input[type="checkbox"][name="newsletter"]');
await page.click('input[type="radio"][name="plan"][value="pro"]');

const isChecked = await page.$eval(
  'input[name="newsletter"]',
  el => el.checked,
)

Lisez toujours checked ; les conteneurs stylisés peuvent absorber le clic sans modifier l'état.

Sélectionnez les menus déroulants (sélection unique et multiple)

Les <select> sont le cas le plus simple. Utilisez page.select, qui prend l'option value, et non le libellé visible. Pour les sélections multiples, transmettez un tableau. Les sélecteurs de pays peuvent être très volumineux ; un exemple courant du tutoriel ScrapeOps utilise une liste d’environ 248 options de pays, et la même signature d’appel les gère toutes.

await page.select('select#country', 'DE');
await page.select('select#languages', 'en', 'de', 'fr');

Les menus déroulants JS personnalisés (pensez à <div role="listbox">) nécessitent une séquence de clics : cliquez sur le déclencheur, attendez que le panneau d'options s'affiche, cliquez sur l'option correspondante en fonction du texte visible via XPath.

Sélecteurs de date et curseurs de plage

Native <input type="date"> accepte YYYY-MM-DD et fonctionne parfaitement avec page.type ou Locator fill. Les widgets de calendrier personnalisés nécessitent une séquence de clics dans la fenêtre contextuelle. Pour un curseur de plage, définissez la valeur via le DOM et déclenchez les événements, sinon la page ne se rafraîchit jamais. Dans l'exemple de curseur ci-dessous, nous avons réglé le curseur sur 85 % avant de prendre la capture d'écran :

await page.$eval('input[type="range"]', el => {
  el.value = 85;
  el.dispatchEvent(new Event('input', { bubbles: true }));
  el.dispatchEvent(new Event('change', { bubbles: true }));
});

Éditeurs de texte enrichi Contenteditable et basés sur des iframes

Les éditeurs de texte enrichi se présentent sous deux formes. Une balise contenteditable div prend Locator fill directement. Les éditeurs hébergés dans des iframes, comme CKEditor ou TinyMCE, sont en mode sandbox ; vous devez d'abord changer de contexte via ElementHandle.contentFrame() avant de pouvoir trouver quoi que ce soit à l'intérieur.

const frameHandle = await page.$('iframe.cke_wysiwyg_frame');
const frame = await frameHandle.contentFrame();
await frame.locator('body').fill('Hello from Puppeteer.');

Si un sélecteur renvoie null à l'intérieur de la page principale, pensez à un iframe avant de soupçonner une faute de frappe.

Téléchargement de fichiers : champs de saisie visibles vs boutons de navigation personnalisés

Pour un champ visible <input type="file">, récupérez son identifiant natif avec page.$ et appelez uploadFile avec des chemins absolus. Les fichiers multiples ne sont que des arguments supplémentaires. Avertissement important : uploadFile ne vérifie pas si le fichier existe réellement. Une faute de frappe dans le chemin d'accès échoue silencieusement, le formulaire est soumis sans pièce jointe, et vous passez deux heures à blâmer vos sélecteurs. Validez les chemins d'accès dans le code.

import { existsSync } from 'node:fs';
import { resolve } from 'node:path';

const file = resolve('./uploads/report.pdf');
if (!existsSync(file)) throw new Error(`Missing: ${file}`);

const input = await page.$('input[type="file"]');
await input.uploadFile(file);

Lorsque l'interface utilisateur visible est un bouton « Parcourir » personnalisé qui masque le champ de saisie réel, utilisez page.waitForFileChooser. Enregistrez d'abord l'écouteur, puis déclenchez le clic qui ouvre la boîte de dialogue du système d'exploitation :

const [chooser] = await Promise.all([
  page.waitForFileChooser(),
  page.click('button.upload-trigger'),
]);
await chooser.accept([file]);

Stratégies d'attente après la soumission

setTimeout et page.waitForTimeout ne sont pas des stratégies d'attente ; ce sont des aimants à bugs. Choisissez un signal de réussite concret :

  • waitForNavigation: rechargement complet classique de la page après la soumission. Enveloppez-le avec Promise.all afin de devancer le clic et l'attente.
  • waitForResponse: les POST SPA vers une API. Attendez que l'URL ou le statut correspondant revienne.
  • waitForSelector: une bannière de réussite, un élément de redirection ou une nouvelle ligne dans une liste.
  • waitForNetworkIdle: la solution fourre-tout de Puppeteer moderne lorsque le signal de réussite est flou et que la page vient juste de se stabiliser.

Pour une soumission de recherche classique, surveillez l'élément de résultat ; pour une connexion, surveillez l'élément de navigation du tableau de bord. Ces deux éléments constituent des signaux plus fiables qu'un changement d'URL.

Valider la réussite ou l'échec par programmation

Une soumission qui renvoie un code 200 n'est pas synonyme de soumission réussie. Lisez la page après.

  • Inspectez un conteneur d'erreur connu, par exemple .error-message, et considérez tout contenu textuel comme un échec cuisant : Epic sadface: Username is required est un véritable message de validation que vous verrez sur le site de démonstration de Sauce Labs.
  • Exécutez une validation native via el.checkValidity() via $eval pour détecter les champs que l'utilisateur a mal remplis avant de cliquer.
  • Comparez page.url() avant et après la soumission lorsque vous vous attendez à une redirection.
  • En cas d'échec, effectuez une capture d'écran avec await page.screenshot({ path: 'fail.png', fullPage: true }) afin de disposer d'une preuve dans CI.

Gestion des boîtes de dialogue, confirmations et alertes JS lors de la soumission

Certains formulaires génèrent encore une confirm() avant la soumission. Puppeteer les fait apparaître sous forme d' dialog événements, et vous devez enregistrer l'écouteur avant le clic qui déclenche la boîte de dialogue, sinon celle-ci bloquera la page.

page.on('dialog', async dialog => {
  console.log('dialog:', dialog.message());
  await dialog.accept();
});
await page.click('button#delete-account');

Utilisez dialog.dismiss() pour annuler et dialog.message() pour enregistrer ce que la page a réellement demandé.

Éviter les blocages : anti-bot, honeypots et CAPTCHA sur les pages de formulaire

Les formulaires de connexion et d'inscription sont les principaux lieux où s'applique la logique anti-bot. Trois menaces réelles :

  1. Les honeypots. Des champs de texte cachés <input type="hidden"> ou visuellement masqués qu'un utilisateur réel ne touche jamais. Si votre script remplit aveuglément tous les champs, le serveur vous rejette. Lisez le style calculé du champ ou type et ignorez tout ce qui n'est pas visible.
  2. Empreintes digitales. Vanilla Puppeteer divulgue navigator.webdriver = true et d'autres indices. D'après les tests de la communauté au moment de la rédaction de cet article, puppeteer-extra-plugin-stealth la plupart sont corrigés, bien que les fournisseurs de solutions de détection continuent de mettre à jour leurs systèmes.
  3. CAPTCHA. Selon la documentation actuelle des projets concernés, vous pouvez associer puppeteer-extra avec puppeteer-extra-plugin-recaptcha et un jeton payant de type 2captcha pour gérer reCAPTCHA et hCaptcha, mais la couverture et la fiabilité varient au fil du temps. Si vous ne parvenez pas à surmonter cet obstacle, notre API Scraper constitue une solution plus rapide que de régler chaque semaine les paramètres de furtivité.

Recette de débogage : que faire lorsqu'un formulaire refuse d'être envoyé

Lorsqu'un script Puppeteer de soumission de formulaire ne fait rien sans avertissement, suivez cette liste dans l'ordre :

  1. Exécutez avec headless: false et slowMo: 100 afin de voir ce que fait réellement le navigateur.
  2. Ouvrez devtools: true et surveillez les onglets Réseau et Console pour repérer les requêtes bloquées ou les erreurs générées.
  3. Vérifiez required et pattern les attributs ainsi que checkValidity() sur chaque champ ; la validation native peut bloquer l'envoi avant le déclenchement de tout gestionnaire.
  4. Vérifiez s’il y a des éléments hors écran ou désactivés ; faites défiler pour les afficher à l’aide de el.scrollIntoView() avant de cliquer.
  5. Vérifiez la présence d'un wrapper iframe ; si c'est le cas, changez de contexte avec contentFrame().
  6. Activez l'interception des requêtes pour consigner chaque POST sortant et vérifier si la requête de soumission a bien quitté le navigateur.

Liste de contrôle de production pour un script Puppeteer de soumission de formulaire

Avant la mise en production :

  • Utilisez des sélecteurs stables, privilégiez id, name, et data-testid.
  • Enveloppez chaque navigation dans Promise.all avec un délai concret.
  • Définissez des valeurs par action timeout ; ne prenez jamais « infini » par défaut.
  • Enveloppez l'exécution dans des tentatives de réessai avec un recul exponentiel.
  • Effectuez une capture d'écran à chaque échec et envoyez-la vers votre magasin de journaux.
  • Générez des journaux structurés, exécutez headless: 'new'et faites tourner les proxys pour toute cible accessible au public.

Conclusion et prochaines étapes

Choisissez la méthode de saisie en fonction de ce que la page attend, sélectionnez le chemin de soumission correspondant au comportement du formulaire, attendez un signal de réussite réel et capturez chaque échec. À partir de là, explorez les tutoriels Puppeteer connexes sur les téléchargements de fichiers, les principes fondamentaux des navigateurs sans interface graphique et le choix entre les sélecteurs XPath et CSS.

Points clés

  • Utilisez page.locator(selector).fill(value) pour la rapidité et page.type lorsque la page surveille les frappes (saisie automatique, anti-bot, validation en temps réel).
  • Soumettez en cliquant sur le bouton, en appuyant sur Entrée ou en appelant form.requestSubmit(); choisissez en fonction du comportement du formulaire, et non par habitude.
  • Associez toujours l'action de soumission à un délai concret (waitForNavigation, waitForResponse, waitForSelector, ou waitForNetworkIdle) à l'intérieur de Promise.all.
  • Pour les téléchargements de fichiers, validez vous-même le chemin d'accès ; uploadFile ne le fera pas, et une faute de frappe échouera silencieusement.
  • Lorsqu'un formulaire refuse silencieusement d'être soumis, exécutez headful avec slowMo, vérifiez la required/pattern la validation, recherchez les honeypots et vérifiez la présence de wrappers iframe.

FAQ

Puppeteer dispose-t-il d'une méthode page.fill() comme Playwright ?

Pas dans la Page classe. D'après la documentation actuelle de Puppeteer au moment de la rédaction de cet article, l' fill action se trouve dans l'API Locator, vous devez donc appeler await page.locator(selector).fill(value) au lieu de await page.fill(selector, value). Locator fill prendrait en charge input, textarea, selectet checkbox , et attend que l'élément soit modifiable avant d'assigner la valeur.

Comment puis-je soumettre un formulaire Puppeteer sans bouton de soumission visible ?

Utilisez form.requestSubmit() via page.$eval('form#login', f => f.requestSubmit()). Cela déclenche la validation HTML5 native et déclenche l' submit sans nécessiter d'élément cliquable. À défaut, placez le focus sur n'importe quel champ du formulaire avec page.focus() et appelez await page.keyboard.press('Enter'), ce que la plupart des formulaires de recherche et de connexion acceptent.

Comment attendre la fin de l'envoi d'un formulaire dans une application monopage ?

Attendez l'appel de l'API sous-jacente plutôt que la navigation. Utilisez await page.waitForResponse(res => res.url().includes('/api/submit') && res.status() === 200), ou page.waitForNetworkIdle({ idleTime: 500 }) si l'application SPA déclenche plusieurs requêtes en parallèle. Associez l'un ou l'autre à waitForSelector sur l'élément de réussite afin de vous assurer que l'interface utilisateur a bien affiché le résultat.

Comment puis-je télécharger plusieurs fichiers vers un seul champ de saisie avec Puppeteer ?

Transmettez chaque chemin absolu en tant qu'argument distinct à uploadFile: await input.uploadFile(file1, file2, file3). La cible <input type="file"> doit comporter l' multiple , sinon le navigateur ne conserve que la dernière entrée. Pour les boutons Parcourir personnalisés, appelez chooser.accept([file1, file2]) sur le sélecteur de fichiers renvoyé par waitForFileChooser.

Puppeteer peut-il remplir des formulaires à l'intérieur d'un iframe ?

Oui, mais vous devez d'abord changer de contexte. Récupérez l'élément iframe avec page.$('iframe#payment'), puis appelez await handle.contentFrame() pour obtenir un Frame objet. À partir de là, toutes les méthodes que vous appelleriez sur page (type, click, locator, waitForSelector) est disponible sur le cadre et s'exécute dans le périmètre de son document.

Conclusion

Un script Puppeteer fiable pour la soumission de formulaires est avant tout une question de choix. Choisissez la méthode de saisie qui correspond à ce que la page attend, le chemin de soumission qui correspond à la façon dont le formulaire se déclenche réellement, et le délai d'attente qui correspond au signal de réussite que vous pouvez réellement observer. Les mécanismes ne sont pas complexes ; la discipline consiste à ne négliger aucun de ces trois choix.

Les modèles présentés dans ce guide couvrent les cas que vous rencontrerez sur 90 % des sites publics. Les 10 % restants, à savoir les pages de connexion avec une identification par empreinte digitale agressive, les processus de paiement protégés par CAPTCHA et les pare-feu anti-bot (WAF) qui modifient leur comportement chaque semaine, relèvent d’un tout autre domaine. Le réglage des paramètres de furtivité sur votre propre parc de navigateurs est un véritable travail d’ingénierie, et les coûts de maintenance s’accumulent.

Si vous préférez consacrer ce temps au flux de données plutôt qu’à la couche de requêtes, jetez un œil à WebScrapingAPI. Il gère la rotation des proxys, l’empreinte digitale des navigateurs et la résolution des CAPTCHA derrière un seul point de terminaison, afin que votre script Puppeteer puisse conserver sa logique de remplissage de formulaires et simplement déléguer les parties qui sont plus difficiles à maintenir qu’elles ne sont intéressantes. Quoi qu’il en soit, prenez dès maintenant l’habitude de soumettre et de vérifier, et votre futur vous en sera reconnaissant.

À propos de l'auteur
Mihnea-Octavian Manolache, Développeur Full Stack @ WebScrapingAPI
Mihnea-Octavian ManolacheDéveloppeur Full Stack

Mihnea-Octavian Manolache est ingénieur Full Stack et DevOps chez WebScrapingAPI, où il développe des fonctionnalités pour les produits et assure la maintenance de l'infrastructure qui garantit le bon fonctionnement de la plateforme.

Commencez à créer

Prêt à faire évoluer votre système de collecte de données ?

Rejoignez plus de 2 000 entreprises qui utilisent WebScrapingAPI pour extraire des données Web à l'échelle de l'entreprise, sans aucun coût d'infrastructure.