Maîtriser l'Automatisation de Navigateurs avec Playwright et Puppeteer
Maîtriser l'Automatisation de Navigateurs avec Playwright et Puppeteer

Interactions Avancées avec Playwright : Gestion des Formulaires et Attentes Asynchrones

Introduction

Dans le monde de l'automatisation de navigateurs, interagir avec les formulaires web et gérer les temps de chargement asynchrones sont deux des défis les plus courants et les plus cruciaux. Un script d'automatisation robuste doit non seulement être capable de remplir des champs de texte, de sélectionner des options dans des listes déroulantes ou de cocher des cases, mais il doit aussi le faire de manière fiable, en tenant compte de la nature dynamique et asynchrone des applications web modernes.

Ce chapitre, s'inscrivant dans le cadre de notre cours "Maîtriser l'Automatisation de Navigateurs avec Playwright et Puppeteer", se concentre sur les techniques avancées offertes par Playwright pour une gestion efficace des formulaires et une stratégie d'attentes asynchrones qui garantit la stabilité et la fiabilité de vos scripts d'automatisation. Nous verrons comment Playwright, avec son architecture moderne, simplifie ces tâches complexes, en allant au-delà des interactions de base.

1. Gestion des Formulaires avec Playwright

Les formulaires sont l'épine dorsale de l'interactivité sur le web. Qu'il s'agisse d'une connexion, d'une inscription, d'un formulaire de recherche ou d'un processus de commande, l'automatisation de ces interactions est fondamentale. Playwright fournit un ensemble d'APIs intuitives pour interagir avec tous les types d'éléments de formulaire.

1.1 Saisie de texte : page.fill() et page.type()

Playwright offre deux méthodes principales pour saisir du texte dans des champs de saisie (<input type="text">, <textarea>, etc.).

  • page.fill(selector, value) :
    • C'est la méthode préférée pour la plupart des cas.
    • Elle simule la saisie de texte en remplissant directement la valeur du champ.
    • Elle est plus rapide car elle ne simule pas chaque frappe de touche.
    • Elle déclenche les événements input et change après avoir rempli le champ.
    • Playwright attend automatiquement que l'élément soit visible, modifiable et reçoive des événements.
  • page.type(selector, text, [options]) :
    • Cette méthode simule la frappe de chaque caractère un par un.
    • Elle est utile si votre application web a des gestionnaires d'événements JavaScript qui réagissent à chaque frappe (par exemple, des suggestions de recherche en temps réel, des masques de saisie).
    • Vous pouvez spécifier un délai entre chaque frappe (delay dans les options).
import { test, expect } from '@playwright/test';

test('Automatisation de la saisie de texte dans un formulaire', async ({ page }) => {
    await page.goto('https://example.com/login'); // Remplacez par une URL de formulaire réelle

    // Utilisation de page.fill() pour la rapidité et la fiabilité
    await page.fill('input[name="username"]', 'monUtilisateurTest');
    await page.fill('input[type="password"]', 'motDePasseSecret123');

    // Exemple de page.type() avec un délai (pour des cas spécifiques)
    // Utile si le champ réagit à chaque frappe
    // await page.type('input[name="search_query]', 'Playwright', { delay: 100 }); 

    console.log("Champs de texte remplis avec succès.");

    // Pour soumettre le formulaire, on pourrait cliquer sur le bouton Soumettre
    await page.click('button[type="submit"]');

    // ... puis attendre que la page se mette à jour ou navigue
    // await page.waitForURL('https://example.com/dashboard'); 
});

Explication du code : Nous navigons vers une page de connexion hypothétique. Nous utilisons page.fill() pour remplir les champs de nom d'utilisateur et de mot de passe, ce qui est la méthode la plus courante et efficace. Un exemple commenté de page.type() est inclus pour montrer son utilisation lorsque la simulation de frappe caractère par caractère est nécessaire. Enfin, nous simulons la soumission du formulaire en cliquant sur le bouton.

1.2 Sélection dans les listes déroulantes : page.selectOption()

Les listes déroulantes (<select>) sont gérées avec la méthode page.selectOption(). Vous pouvez sélectionner une option par sa valeur, son label visible ou son index.

  • Par value : La valeur de l'attribut value de l'option.
  • Par label : Le texte visible de l'option.
  • Par index : La position de l'option (commençant à 0).
import { test, expect } from '@playwright/test';

test('Sélection dans une liste déroulante', async ({ page }) => {
    await page.goto('https://example.com/registration'); // Page avec un formulaire d'inscription

    // Supposons une liste déroulante pour le pays:
    // <select id="country">
    //   <option value="fr">France</option>
    //   <option value="de">Allemagne</option>
    //   <option value="us">États-Unis</option>
    // </select>

    // Sélection par la valeur de l'option
    await page.selectOption('#country', 'de');
    console.log("Pays sélectionné par valeur : Allemagne");

    // Sélection par le label visible de l'option
    // await page.selectOption('#country', { label: 'France' });
    // console.log("Pays sélectionné par label : France");

    // Sélection par l'index de l'option (attention, peut être moins robuste si l'ordre change)
    // await page.selectOption('#country', { index: 2 }); // Sélectionne 'États-Unis'
    // console.log("Pays sélectionné par index : États-Unis");

    // Si la liste déroulante permet des sélections multiples
    // await page.selectOption('#multi-select-field', ['option1_value', 'option3_value']);
});

Explication du code : Après avoir navigué vers une page d'inscription, nous utilisons page.selectOption() pour choisir une option dans une liste déroulante identifiée par son ID #country. Les exemples commentés montrent les différentes manières de sélectionner (par label, par index) et comment gérer les listes à sélection multiple.

1.3 Cases à cocher et boutons radio : page.check(), page.uncheck(), page.setChecked()

Ces méthodes sont utilisées pour interagir avec les checkbox et les radio buttons.

  • page.check(selector) : Coche un élément s'il n'est pas déjà coché.
  • page.uncheck(selector) : Décoche un élément s'il n'est pas déjà décoché.
  • page.setChecked(selector, checked) : Définit l'état de l'élément (true pour cocher, false pour décocher). Plus générique.
import { test, expect } from '@playwright/test';

test('Interaction avec cases à cocher et boutons radio', async ({ page }) => {
    await page.goto('https://example.com/settings'); // Page avec des préférences utilisateur

    // Supposons une case à cocher pour les notifications :
    // <input type="checkbox" id="notifications" checked>
    await page.uncheck('#notifications'); // Décoche les notifications
    console.log("Notifications décochées.");

    // Coche la case des newsletters, en s'assurant qu'elle est cochée
    // <input type="checkbox" name="newsletter">
    await page.check('input[name="newsletter"]');
    console.log("Newsletter cochée.");

    // Supposons un groupe de boutons radio pour le thème de l'interface :
    // <input type="radio" name="theme" value="dark"> Dark
    // <input type="radio" name="theme" value="light" checked> Light
    await page.click('input[name="theme"][value="dark"]'); // Sélectionne le thème "Dark"
    // Ou de manière plus directe
    // await page.setChecked('input[name="theme"][value="dark"]', true);
    console.log("Thème 'Dark' sélectionné.");

    // Vous pouvez vérifier l'état d'une case à cocher/bouton radio
    const isNotificationsChecked = await page.isChecked('#notifications');
    expect(isNotificationsChecked).toBe(false);
});

Explication du code : L'exemple montre comment interagir avec des cases à cocher (#notifications, input[name="newsletter"]) en utilisant uncheck() et check(). Pour les boutons radio, un click() sur le bouton souhaité est suffisant pour le sélectionner, ou setChecked() peut être utilisé. Enfin, page.isChecked() permet de vérifier l'état actuel d'un élément.

1.4 Envoi de fichiers : page.setInputFiles()

Pour télécharger un fichier via un champ <input type="file">, utilisez page.setInputFiles(). Vous devez fournir le sélecteur de l'input et le chemin du fichier (ou un tableau de chemins si l'input supporte plusieurs fichiers).

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

test('Envoi de fichier', async ({ page }) => {
    await page.goto('https://example.com/upload'); // Page avec un formulaire d'upload de fichier

    // Créez un fichier temporaire pour le test si nécessaire
    // ou utilisez un fichier existant dans votre projet de test
    const filePath = path.join(__dirname, 'test-file.txt');
    // Si 'test-file.txt' n'existe pas, vous pouvez le créer programmatiquement pour le test.
    // fs.writeFileSync(filePath, 'Contenu du fichier de test');

    // <input type="file" id="document-upload">
    await page.setInputFiles('#document-upload', filePath);
    console.log(`Fichier ${filePath} envoyé avec succès.`);

    // Si vous devez attendre que l'upload soit terminé ou que des indicateurs disparaissent
    // await page.waitForSelector('.upload-success-message');
});

Explication du code : Nous naviguons vers une page d'upload. La méthode page.setInputFiles() est utilisée pour simuler la sélection d'un fichier (test-file.txt) depuis le système de fichiers local et l'associer à l'élément <input type="file">. Le module path est utilisé pour construire un chemin de fichier robuste.

1.5 Soumission de formulaire

La soumission d'un formulaire se fait généralement de deux manières :

  • page.click(selector) sur le bouton de soumission (<button type="submit">, <input type="submit">). C'est la méthode la plus courante.
  • page.press(selector, 'Enter') sur un champ de saisie du formulaire. Ceci simule la pression de la touche "Entrée" après avoir rempli les champs.
import { test, expect } from '@playwright/test';

test('Soumission de formulaire', async ({ page }) => {
    await page.goto('https://example.com/search'); // Page avec un formulaire de recherche

    // Remplir le champ de recherche
    await page.fill('input[name="query"]', 'Playwright automation');

    // Option 1: Cliquer sur le bouton de soumission
    await page.click('button[type="submit"]');
    console.log("Formulaire soumis en cliquant sur le bouton.");

    // Option 2 (alternative): Appuyer sur 'Enter' après avoir rempli un champ
    // Note: Reviendrait à la page de recherche pour cet exemple.
    // await page.fill('input[name="query"]', 'Another search term');
    // await page.press('input[name="query"]', 'Enter');
    // console.log("Formulaire soumis en appuyant sur Entrée.");

    // Attendre la navigation ou l'affichage des résultats
    await page.waitForURL('https://example.com/search?query=*'); // Attendre une URL qui correspond au pattern
    await expect(page.locator('.search-results')).toBeVisible();
});

Explication du code : Un formulaire de recherche est utilisé pour démontrer la soumission. Après avoir rempli le champ de recherche, le formulaire est soumis soit en cliquant sur le bouton submit, soit en simulant la pression de la touche Entrée dans le champ de saisie. La fiabilité est assurée en attendant que l'URL change et qu'un élément de résultats de recherche devienne visible.

2. Attentes Asynchrones et Stabilité de l'Automatisation

Les applications web modernes sont hautement dynamiques. Les éléments apparaissent et disparaissent, les données sont chargées de manière asynchrone, et les animations peuvent prendre du temps. Sans une gestion appropriée des attentes, vos scripts d'automatisation deviendront instables (flaky) et échoueront fréquemment de manière imprévisible. Playwright excelle dans ce domaine grâce à ses mécanismes d'attente robustes.

2.1 Pourquoi les attentes sont cruciales

  • Race Conditions : Un script tente d'interagir avec un élément avant qu'il ne soit entièrement chargé ou rendu par le navigateur.
  • Éléments non visibles/cliquables : Un élément peut être présent dans le DOM mais non visible ou cliquable à cause de styles CSS, d'animations ou de superpositions.
  • Données manquantes : Une section de la page peut être chargée, mais les données dynamiques qu'elle doit afficher ne sont pas encore arrivées via une requête API.
  • Navigation incomplète : Le script tente d'interagir avec la nouvelle page avant que toutes les ressources nécessaires n'aient été chargées après une navigation.

2.2 Attentes implicites (Playwright's Auto-Waiting)

L'une des plus grandes forces de Playwright est son système d'auto-attente. Pour la plupart des actions (par exemple, click(), fill(), waitForSelector()), Playwright attend automatiquement que l'élément cible atteigne un état actionnable.

Playwright attend typiquement que l'élément soit :

  • visible : L'élément est dans la viewport et a des dimensions non nulles.
  • enabled : L'élément n'est pas désactivé (disabled).
  • stable : L'élément ne bouge plus dans la viewport.
  • receives events : L'élément est à l'avant-plan et peut recevoir des événements de souris/clavier (pas d'autres éléments le superposant).

Ce comportement par défaut réduit considérablement le besoin d'ajouter manuellement des attentes explicites, rendant les scripts plus concis et moins sujets aux erreurs.

2.3 Attentes explicites (page.waitFor...)

Malgré l'auto-attente de Playwright, il existe des scénarios où des attentes explicites sont nécessaires pour des conditions plus complexes ou pour synchroniser votre script avec des événements spécifiques du navigateur ou de l'application.

  • page.waitForSelector(selector, [options]) :

    • Attend qu'un élément correspondant au sélecteur apparaisse dans le DOM et atteigne un certain état.
    • options.state:
      • 'visible' (défaut) : L'élément est dans le DOM et visible.
      • 'hidden' : L'élément n'est pas dans le DOM ou est invisible.
      • 'attached' : L'élément est dans le DOM (visible ou non).
      • 'detached' : L'élément n'est plus dans le DOM.
    • Utile pour attendre l'apparition d'un indicateur de chargement, d'un message d'erreur, ou d'un élément de contenu dynamique.
  • page.waitForLoadState(state, [options]) :

    • Attend que la page atteigne un état de chargement spécifique.
    • 'load' : L'événement load de la page est déclenché.
    • 'domcontentloaded' : L'événement DOMContentLoaded est déclenché.
    • 'networkidle' : Aucune requête réseau n'a été en cours pendant au moins 500 ms (souvent le plus robuste pour les SPA).
  • page.waitForResponse(urlOrPredicate, [options]) / page.waitForRequest(urlOrPredicate, [options]) :

    • Attend qu'une requête ou une réponse réseau spécifique se produise.
    • urlOrPredicate peut être une chaîne de caractères (pour une URL exacte ou partielle), un RegExp (pour un motif d'URL), ou une fonction de rappel qui évalue les objets de requête/réponse.
    • Indispensable pour synchroniser l'automatisation avec les appels API asynchrones.
  • page.waitForFunction(pageFunction, [args, options]) :

    • Une méthode très puissante qui attend qu'une fonction JavaScript exécutée dans le contexte du navigateur retourne une valeur truthy.
    • Permet d'attendre des conditions très spécifiques qui ne sont pas couvertes par d'autres méthodes (par exemple, attendre qu'une variable JavaScript atteigne une certaine valeur, qu'une animation se termine, ou qu'une propriété d'un élément change).
  • page.waitForTimeout(timeout) (À utiliser avec parcimonie!) :

    • Attend une durée fixe en millisecondes.
    • Généralement découragée car cela introduit des délais inutiles ou insuffisants, rendant les tests lents et peu fiables.
    • À n'utiliser qu'en dernier recours, lorsque toutes les autres stratégies d'attente ont échoué à cause d'un comportement particulièrement imprévisible ou d'une animation non détectable.

2.4 Exemple de combinaison de formulaires et d'attentes

Voici un exemple plus complexe combinant la gestion des formulaires et diverses stratégies d'attente pour simuler une connexion utilisateur et vérifier le chargement du tableau de bord.

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

test('Processus de connexion robuste avec attentes asynchrones', async ({ page }) => {
    // 1. Naviguer vers la page de connexion
    await page.goto('https://demo.playwright.dev/todomvc/#/'); // Exemple d'application, adaptez l'URL

    // Naviguer vers une page de login pour cet exemple
    await page.goto('https://qa.orangegrp.com/login'); 
    console.log("Navigué vers la page de connexion.");

    // 2. Attendre que les champs de formulaire soient prêts à être remplis
    // Playwright le fait automatiquement pour `fill()`, mais un `waitForSelector`
    // pourrait être utilisé pour s'assurer que le formulaire entier est visible.
    await page.waitForSelector('input[name="username"]', { state: 'visible' });
    await page.waitForSelector('input[name="password"]', { state: 'visible' });
    console.log("Champs de connexion détectés et visibles.");

    // 3. Remplir les champs du formulaire
    await page.fill('input[name="username"]', 'testuser');
    await page.fill('input[name="password"]', 'testpass');
    console.log("Nom d'utilisateur et mot de passe remplis.");

    // 4. Attendre une réponse réseau spécifique après la soumission du formulaire
    // Utile si la connexion déclenche un appel API avant la navigation
    const [response] = await Promise.all([
        page.waitForResponse(response => response.url().includes('/api/login') && response.status() === 200),
        page.click('button[type="submit"]') // Cliquer sur le bouton de connexion
    ]);
    console.log(`Requête de connexion réussie : ${response.url()}`);

    // 5. Attendre la navigation vers la page du tableau de bord
    await page.waitForURL('**/dashboard', { timeout: 10000 }); // Attendre une URL contenant '/dashboard'
    console.log("Navigué vers le tableau de bord.");

    // 6. Attendre qu'un élément clé du tableau de bord soit visible et que les données soient chargées
    await page.waitForSelector('#dashboard-welcome-message', { state: 'visible' });
    await expect(page.locator('#dashboard-welcome-message')).toHaveText(/Bienvenue, testuser/);
    console.log("Message de bienvenue du tableau de bord vérifié.");

    // Autre exemple: Attendre qu'un compteur d'éléments se mette à jour via JS
    // await page.waitForFunction(() => document.querySelector('#item-count').innerText > '0');
    // console.log("Le nombre d'éléments a été mis à jour.");
});

Explication du code : Cet exemple simule un processus de connexion complet et robuste.

  1. Nous naviguons vers une page de connexion.
  2. Bien que page.fill() inclue l'auto-attente, un page.waitForSelector() explicite est ajouté pour s'assurer que les champs sont bien rendus avant toute interaction.
  3. Les champs de texte sont remplis.
  4. Une stratégie d'attente avancée est utilisée avec Promise.all() et page.waitForResponse(). Cela permet d'attendre simultanément que le clic sur le bouton se produise ET qu'une réponse API spécifique soit renvoyée avec un statut 200 (indiquant une connexion réussie). Cela synchronise le script avec la logique backend.
  5. page.waitForURL() est utilisé pour attendre que le navigateur navigue vers l'URL du tableau de bord, avec un timeout pour plus de sécurité.
  6. Enfin, un page.waitForSelector() et une assertion expect().toHaveText() sont utilisés pour vérifier la présence d'un message de bienvenue sur le tableau de bord, garantissant que la page est entièrement chargée et que les données sont affichées correctement. Un exemple commenté de waitForFunction montre comment attendre des conditions très spécifiques basées sur le JavaScript de la page.

Conclusion

La gestion des formulaires et les attentes asynchrones sont des piliers fondamentaux de toute automatisation de navigateur réussie. Playwright, grâce à ses APIs intuitives et son système d'auto-attente intelligent, simplifie grandement ces tâches complexes.

Vous avez appris à :

  • Interagir efficacement avec divers éléments de formulaire : champs de texte (fill, type), listes déroulantes (selectOption), cases à cocher/boutons radio (check, uncheck, setChecked) et téléchargements de fichiers (setInputFiles).
  • Comprendre l'importance cruciale des attentes dans un environnement web dynamique et asynchrone.
  • Tirer parti de l'auto-attente intégrée de Playwright pour des scripts plus concis et fiables.
  • Maîtriser les attentes explicites (waitForSelector, waitForLoadState, waitForResponse, waitForFunction) pour gérer les scénarios les plus complexes et synchroniser vos scripts avec la logique de l'application.

En combinant ces techniques, vous pouvez créer des scripts d'automatisation Playwright qui sont non seulement fonctionnels mais aussi robustes, rapides et fiables, capables de naviguer et d'interagir avec les applications web les plus dynamiques sans "flakiness". Continuez à pratiquer et à expérimenter avec ces méthodes pour maîtriser pleinement l'automatisation de navigateurs.