Interactions Avancées avec Puppeteer : Gestion des Formulaires et Attentes Asynchrones
Bienvenue à cette leçon avancée de notre cours "Maîtriser l'Automatisation de Navigateurs avec Playwright et Puppeteer". Après avoir couvert les bases de la navigation, il est temps de plonger dans des scénarios d'automatisation plus complexes et interactifs. Aujourd'hui, nous allons explorer deux piliers fondamentaux pour toute automatisation robuste : la gestion des formulaires et les attentes asynchrones avancées.
L'interaction avec des formulaires est au cœur de nombreuses tâches d'automatisation, qu'il s'agisse de tests, de remplissage de données ou de scraping authentifié. De même, la nature asynchrone des applications web modernes rend les attentes explicites et intelligentes absolument cruciales pour éviter les erreurs et garantir la fiabilité de vos scripts.
À la fin de cette leçon, vous serez capable de :
- Interagir avec divers types de champs de formulaire (texte, cases à cocher, radios, listes déroulantes).
- Soumettre des formulaires et gérer les réponses.
- Mettre en œuvre des stratégies d'attente sophistiquées pour des éléments dynamiques et des requêtes réseau.
- Développer des scripts Puppeteer plus résilients et fiables.
1. Gestion des Formulaires avec Puppeteer
Les formulaires web sont omniprésents. Puppeteer offre des méthodes puissantes pour interagir avec eux de manière programmatique. La clé est de sélectionner correctement les éléments et d'appliquer les bonnes actions.
1.1. Sélection d'Éléments de Formulaire
Avant d'interagir, il faut trouver l'élément. Puppeteer utilise des sélecteurs CSS (comme dans JavaScript côté client) et peut également travailler avec XPath.
page.$('sélecteur'): Retourne unElementHandledu premier élément correspondant au sélecteur.page.$$('sélecteur'): Retourne un tableau d'ElementHandlede tous les éléments correspondants.page.waitForSelector('sélecteur'): Attends qu'un élément soit présent dans le DOM et visible. C'est la méthode préférable avant toute interaction pour s'assurer que l'élément est prêt.
Conseil pour les sélecteurs robustes :
- Privilégiez les
iduniques. - Utilisez des attributs
nameoudata-test-idsi disponibles et stables. - Les sélecteurs basés sur des classes peuvent être fragiles si les classes changent souvent.
- L'XPath est une alternative puissante pour des sélections plus complexes ou lorsque les sélecteurs CSS sont insuffisants.
1.2. Interaction avec les Champs de Saisie (Texte, textarea)
Pour remplir des champs de texte, vous avez deux options principales :
page.type(selector, text, [options]): Tape le texte caractère par caractère (simulant un utilisateur réel). Utile pour déclencher des événementsinputoukeyup.elementHandle.type(text, [options]): Similaire àpage.type, mais sur unElementHandledéjà obtenu.elementHandle.evaluate((el, text) => el.value = text, text): Change directement la valeur de l'élément via une fonctionevaluatedans le contexte du navigateur. C'est plus rapide mais ne déclenche pas les événements clavier.
// Exemple d'interaction avec un champ de texte
(async () => {
const puppeteer = require('puppeteer');
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
// Imagine que nous sommes sur une page avec ces champs:
// <input id="username" type="text">
// <input name="password" type="password">
// <textarea id="comment"></textarea>
await page.goto('http://localhost:8080/form_test.html'); // Remplacez par votre URL
// Option 1: page.type pour simuler la frappe
await page.type('#username', 'utilisateur_test');
console.log("Nom d'utilisateur tapé.");
// Option 2: Utiliser un ElementHandle pour plus de granularité
const passwordInput = await page.waitForSelector('input[name="password"]');
await passwordInput.type('motdepasse_secret');
console.log("Mot de passe tapé.");
// Option 3: Utiliser evaluate pour définir directement la valeur
// Utile pour les textareas ou de grands blocs de texte si les événements clavier ne sont pas critiques
await page.evaluate(() => {
document.getElementById('comment').value = 'Ceci est un commentaire long et détaillé.';
});
console.log("Commentaire ajouté via evaluate.");
await browser.close();
})();
1.3. Interaction avec les Checkboxes et Radio Buttons
Ces éléments sont généralement manipulés via la méthode click(). Il est important de vérifier leur état actuel si vous souhaitez les basculer.
page.click(selector, [options]): Clique sur l'élément.elementHandle.click([options]): Clique sur unElementHandle.
Pour les checkboxes, vous pouvez vérifier leur état avec elementHandle.getProperty('checked').
// Exemple de checkboxes et radios
(async () => {
const puppeteer = require('puppeteer');
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
// Imagine que nous avons:
// <input type="checkbox" id="newsletter" checked>
// <input type="radio" name="option" value="a" checked>
// <input type="radio" name="option" value="b">
await page.goto('http://localhost:8080/form_test.html'); // Votre URL
// Désélectionner la case à cocher 'newsletter' si elle est cochée
const newsletterCheckbox = await page.waitForSelector('#newsletter');
const isChecked = await newsletterCheckbox.evaluate(el => el.checked);
if (isChecked) {
await newsletterCheckbox.click();
console.log("Case à cocher 'newsletter' désélectionnée.");
} else {
console.log("Case à cocher 'newsletter' déjà désélectionnée.");
}
// Sélectionner l'option radio 'b'
await page.click('input[name="option"][value="b"]');
console.log("Option radio 'b' sélectionnée.");
await browser.close();
})();
1.4. Gestion des Dropdowns (Select Menus)
Les éléments <select> ont une méthode dédiée dans Puppeteer pour simplifier leur manipulation.
page.select(selector, ...values): Sélectionne une ou plusieurs options dans un élément<select>par leur attributvalue.
// Exemple de menu déroulant
(async () => {
const puppeteer = require('puppeteer');
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
// Imagine que nous avons:
// <select id="country">
// <option value="fr">France</option>
// <option value="de">Allemagne</option>
// <option value="us">États-Unis</option>
// </select>
await page.goto('http://localhost:8080/form_test.html'); // Votre URL
// Sélectionner l'option avec la valeur "us"
await page.select('#country', 'us');
console.log("Pays sélectionné: États-Unis.");
await browser.close();
})();
1.5. Soumission de Formulaires
La soumission d'un formulaire peut se faire de plusieurs manières :
- Cliquer sur un bouton de soumission :
page.click('button[type="submit"]')ouelementHandle.click(). C'est l'approche la plus courante et qui simule le mieux l'utilisateur. - Appuyer sur
Entrée: Si le focus est sur un champ de texte et que le formulaire est configuré pour se soumettre avecEntrée, vous pouvez utiliserpage.keyboard.press('Enter'). - Appeler
submit()sur l'élément<form>: Si vous avez unElementHandlede l'élément<form>lui-même, vous pouvez évaluerformHandle.evaluate(form => form.submit()). Cette méthode ne déclenche pas toujours tous les événementsclickousubmitcomme le ferait un clic utilisateur.
Très important : Après la soumission d'un formulaire, attendez toujours une indication que l'action a été complétée. Cela peut être une nouvelle navigation, l'apparition d'un message de succès, ou la disparition du formulaire. Nous y reviendrons dans la section sur les attentes asynchrones.
2. Attentes Asynchrones Avancées
Les applications web modernes sont hautement dynamiques. Le contenu se charge via AJAX, des animations sont déclenchées, et des éléments apparaissent ou disparaissent. Attendre le bon moment est fondamental pour la stabilité de vos scripts Puppeteer.
2.1. Pourquoi les Attentes sont Cruciales
Sans attentes appropriées, vos scripts échoueront car ils tenteront d'interagir avec des éléments qui n'existent pas encore dans le DOM, ne sont pas visibles, ou ne sont pas interactifs. C'est la cause numéro un des "flaky tests" (tests intermittents).
2.2. Rappel des Attentes de Base
Vous connaissez déjà probablement ces méthodes :
page.waitForNavigation([options]): Attends que la navigation soit terminée. Options courantes :waitUntil: 'networkidle0'(pas plus de 0 connexion réseau pendant 500ms) ou'networkidle2'(pas plus de 2 connexions réseau pendant 500ms).page.waitForSelector(selector, [options]): Attends qu'un élément correspondant au sélecteur soit ajouté au DOM et, par défaut, visible.page.waitForTimeout(milliseconds): À éviter autant que possible. C'est une pause statique et inefficace qui rend les tests lents et fragiles. Utilisez-la uniquement en dernier recours ou pour du débogage temporaire.
2.3. Attendre des Conditions Spécifiques avec page.waitForFunction()
C'est l'une des méthodes d'attente les plus puissantes et flexibles de Puppeteer. Elle permet d'exécuter une fonction JavaScript dans le contexte du navigateur jusqu'à ce qu'elle retourne une valeur "truthy".
page.waitForFunction(pageFunction, [options, ...args]):pageFunctionest la fonction à exécuter.options.polling:'raf'(requestAnimationFrame, par défaut),'mutation'(utilise MutationObserver), ou un nombre (intervalle en ms).options.timeout: Temps maximal d'attente en ms (par défaut 30s).
Cas d'utilisation courants :
- Attendre qu'une variable JavaScript côté client ait une certaine valeur.
- Attendre qu'un élément ait un certain texte ou attribut.
- Attendre qu'une animation se termine.
- Attendre que la hauteur d'un élément change.
// Exemple de page.waitForFunction()
(async () => {
const puppeteer = require('puppeteer');
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
// Imagine une page qui charge dynamiquement un message de succès après 3 secondes
// <div id="status">Chargement...</div>
// Après 3s, devient <div id="status">Succès !</div>
await page.goto('http://localhost:8080/dynamic_content.html'); // Votre URL avec contenu dynamique
console.log("Attente du message de succès...");
// Attendre que le texte de l'élément #status devienne "Succès !"
await page.waitForFunction(
() => document.querySelector('#status') && document.querySelector('#status').innerText.includes('Succès !'),
{ polling: 100 } // Vérifie toutes les 100ms
);
console.log("Message de succès affiché !");
// Autre exemple : attendre qu'une variable JS globale soit définie
// window.dataLoaded = false; (initialement)
// Après AJAX: window.dataLoaded = true;
// await page.waitForFunction('window.dataLoaded === true');
await browser.close();
})();
2.4. Attendre des Requêtes Réseau
Pour des interactions encore plus fines, Puppeteer permet d'attendre des requêtes ou des réponses réseau spécifiques. C'est particulièrement utile pour tester des API ou des chargements de données asynchrones.
page.waitForRequest(urlOrPredicate, [options]): Attends qu'une requête réseau corresponde à l'URL ou à la fonction de prédicat fournie.page.waitForResponse(urlOrPredicate, [options]): Attends qu'une réponse réseau corresponde à l'URL ou à la fonction de prédicat fournie.
urlOrPredicate peut être une chaîne (correspondance exacte ou avec *), une RegExp, ou une fonction qui prend un objet Request ou Response et retourne true si elle correspond.
// Exemple d'attente de requête/réponse réseau
(async () => {
const puppeteer = require('puppeteer');
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
// Imagine une page qui fait une requête API pour charger des produits
// La requête est POST vers /api/products
await page.goto('http://localhost:8080/api_interaction.html'); // Votre URL
console.log("Attente de la requête POST /api/products...");
// Attendre la requête POST vers /api/products
const request = await page.waitForRequest(request =>
request.url().includes('/api/products') && request.method() === 'POST'
);
console.log(`Requête détectée: URL = ${request.url()}, Méthode = ${request.method()}`);
// Vous pouvez inspecter les postData si nécessaire: request.postData()
console.log("Attente de la réponse de la requête POST /api/products...");
// Attendre la réponse de la même requête
const response = await page.waitForResponse(response =>
response.url().includes('/api/products') && response.request().method() === 'POST'
);
console.log(`Réponse reçue: Statut = ${response.status()}`);
// Vous pouvez inspecter le corps de la réponse: const data = await response.json();
await browser.close();
})();
2.5. Gestion des Pop-ups et Dialogues (Alertes, Confirmations, Prompts)
Les dialogues JavaScript natifs (alert, confirm, prompt) peuvent bloquer l'exécution de Puppeteer. Il est essentiel de les gérer.
page.on('dialog', handler): Permet de définir un écouteur d'événements pour les dialogues.- L'objet
dialogpassé au handler a des méthodes :dialog.accept([promptText])(pour confirmer ou entrer du texte),dialog.dismiss()(pour annuler),dialog.message(),dialog.type().
- L'objet
// Exemple de gestion de dialogue
(async () => {
const puppeteer = require('puppeteer');
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.goto('http://localhost:8080/dialog_test.html'); // Votre URL avec un bouton qui déclenche un alert/confirm/prompt
// Ajouter un écouteur d'événements pour les dialogues
page.on('dialog', async dialog => {
console.log(`Type de dialogue: ${dialog.type()}`);
console.log(`Message de dialogue: ${dialog.message()}`);
if (dialog.type() === 'alert') {
await dialog.accept(); // Cliquez sur OK
console.log("Alerte acceptée.");
} else if (dialog.type() === 'confirm') {
await dialog.accept(); // Ou dialog.dismiss() pour annuler
console.log("Confirmation acceptée.");
} else if (dialog.type() === 'prompt') {
await dialog.accept('Mon texte de prompt');
console.log("Prompt accepté avec texte.");
}
});
// Déclenchez un dialogue (par exemple, en cliquant sur un bouton)
await page.click('#showAlertButton'); // Assurez-vous d'avoir un bouton avec cet ID
await page.click('#showConfirmButton');
await page.click('#showPromptButton');
await browser.close();
})();
3. Exemple Pratique Complet : Connexion et Mise à Jour de Profil
Mettons tout cela ensemble dans un scénario réaliste : un script qui se connecte à une application web, navigue vers une page de profil, met à jour un champ, puis soumet le formulaire et vérifie le succès.
Structure HTML conceptuelle :
login.html:
<form id="loginForm">
<input type="text" id="username" name="username">
<input type="password" id="password" name="password">
<button type="submit">Se connecter</button>
</form>
<div id="loginMessage" style="display:none;"></div>
profile.html:
<h1>Mon Profil</h1>
<form id="profileForm">
<input type="text" id="bio" name="bio" value="Bio actuelle...">
<select id="country" name="country">
<option value="fr">France</option>
<option value="de">Allemagne</option>
<option value="us">États-Unis</option>
</select>
<input type="checkbox" id="email_notifications" name="email_notifications"> Recevoir les notifications
<button type="submit">Mettre à jour</button>
</form>
<div id="successMessage" style="display:none;">Profil mis à jour avec succès !</div>
Script Puppeteer :
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: false, slowMo: 50 }); // Visualiser l'exécution avec slowMo
const page = await browser.newPage();
try {
// 1. Navigation vers la page de connexion
console.log("Navigating to login page...");
await page.goto('http://localhost:8080/login.html', { waitUntil: 'networkidle2' });
// 2. Remplir le formulaire de connexion
console.log("Filling login form...");
await page.type('#username', 'monutilisateur');
await page.type('#password', 'monmotdepasse');
// 3. Soumettre le formulaire et attendre la navigation vers la page de profil
console.log("Submitting login form and waiting for navigation...");
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle2' }), // Attendre que la nouvelle page soit chargée
page.click('button[type="submit"]')
]);
console.log("Logged in and navigated to profile page.");
// Vérifier que nous sommes bien sur la page de profil
const currentPageTitle = await page.title();
if (!currentPageTitle.includes('Profil')) {
throw new Error('La connexion a échoué ou la redirection est incorrecte.');
}
// 4. Interagir avec le formulaire de profil
console.log("Updating profile information...");
await page.waitForSelector('#profileForm', { visible: true }); // S'assurer que le formulaire de profil est présent
// Mettre à jour la bio
await page.type('#bio', 'Développeur passionné par l\'automatisation web.', { delay: 10 });
// Sélectionner un nouveau pays
await page.select('#country', 'de'); // Allemagne
// Cocher la case des notifications si elle n'est pas cochée
const emailNotifications = await page.$('#email_notifications');
const isChecked = await emailNotifications.evaluate(el => el.checked);
if (!isChecked) {
await emailNotifications.click();
console.log("Notifications par e-mail activées.");
} else {
console.log("Notifications par e-mail déjà activées.");
}
// 5. Soumettre le formulaire de profil
console.log("Submitting profile form...");
await Promise.all([
// Pas de navigation attendue ici, mais l'apparition d'un message de succès
// Nous utilisons waitForFunction pour un contenu dynamique
page.waitForFunction(
() => document.getElementById('successMessage') && window.getComputedStyle(document.getElementById('successMessage')).display !== 'none',
{ timeout: 5000 } // Attendre au maximum 5 secondes
),
page.click('button[type="submit"]')
]);
console.log("Profile updated and success message displayed!");
// 6. Vérifier le message de succès
const successMessage = await page.$eval('#successMessage', el => el.innerText);
console.log(`Message de succès: "${successMessage}"`);
} catch (error) {
console.error("Une erreur est survenue:", error);
} finally {
await browser.close();
}
})();
4. Bonnes Pratiques et Pièges à Éviter
- Sélecteurs Robustes : Toujours privilégier les
id,name, ou attributsdata-*pour les sélecteurs. Les sélecteurs basés sur des positions (ex:div > div > input:nth-child(2)) ou des classes génériques sont très fragiles. - Éviter
waitForTimeout(): C'est le signe d'un script fragile. Utilisez des attentes explicites commewaitForSelector,waitForFunction,waitForNavigation,waitForRequest/Response. - Gérer les Cas d'Erreur : Utilisez des blocs
try...catchpour intercepter les erreurs (comme un sélecteur non trouvé ou un timeout) et fermer proprement le navigateur. - Anticiper l'Asynchronisme : Supposer que chaque action prend du temps. Ne partez jamais du principe qu'un élément est immédiatement disponible après un clic ou une navigation.
- Débogage Efficace :
- Utilisez
headless: falseetslowMolors du développement pour voir ce que Puppeteer fait. - Utilisez
page.screenshot()à des points clés pour capturer l'état de la page. - Activez la console du navigateur dans Puppeteer avec
page.on('console', msg => console.log('BROWSER LOG:', msg.text())). - Utilisez
page.evaluate()pour exécuter du JS dans le contexte du navigateur et inspecter le DOM ou les variables.
- Utilisez
Conclusion
Félicitations ! Vous avez maintenant une compréhension approfondie de la gestion des formulaires et des stratégies d'attente asynchrones avancées avec Puppeteer. Ces compétences sont essentielles pour construire des scripts d'automatisation fiables, efficaces et capables de gérer la complexité des applications web modernes.
Nous avons couvert :
- Les différentes méthodes pour interagir avec les champs de formulaire (texte, cases à cocher, radios, listes déroulantes).
- L'importance des sélecteurs robustes et des techniques de soumission de formulaires.
- Les méthodes d'attente asynchrone de Puppeteer, y compris l'utilisation puissante de
page.waitForFunction()pour les conditions personnalisées etpage.waitForRequest()/waitForResponse()pour le contrôle du réseau. - La gestion des dialogues natifs du navigateur.
- Un exemple complet combinant plusieurs de ces techniques.
N'oubliez pas que la clé de l'automatisation réside dans l'observation attentive du comportement de l'application web et la traduction de ce comportement en code Puppeteer précis et résilient. Entraînez-vous avec ces concepts sur vos propres sites web pour consolider votre apprentissage. Dans la prochaine leçon, nous explorerons d'autres interactions avancées, comme la manipulation des iframes et le téléchargement de fichiers.