[fail] Quels outils pour tester ses web components ?

Les web components sont les éléments d’interface utilisateur personnalisés et réutilisables natifs aux navigateurs et standards sur le web. Ils ont pour avantage d’être autonomes et intégrables à n’importe quelle application frontend, qu’importe la technologie utilisée, et peuvent embarquer des logiques dynamiques JavaScript, ce qui en font de très bons candidats pour les librairies de composants de design systems partagés, et pour des services tiers sous forme de widgets (vous vous dites que votre iframe fait déjà ça ? Attention aux risques de sécurité applicative).

Google avec Youtube, Adobe avec Photoshop, IBM avec Carbon DS, Microsoft avec Edge, Bing, et VSCode, Salesforce, ou encore Adeo avec Mozaic Exemples d’utilisation des web components par les géants du web

Comme dans n’importe quelle base de code, il est important d’y appliquer un harnais de tests automatisés suffisant pour éviter les mauvaises surprises sur les applications qui les consomment en détectant les régressions ou changements cassants, mais aussi pour assurer un critère de qualité propre au frontend : l’accessibilité. C’est même d’autant plus important si ces composants sont amenés à être réutilisés.

Spoiler : cet article n'explique pas les bonnes pratiques de tests automatisés de manière générale, la série Culture Test le fait déjà très bien !

🚲 Début du voyage : @open-wc/testing

Pour pouvoir tester un web component, la première étape est de pouvoir rendre son contenu dans un contexte de test, et malheureusement il ne suffit pas de charger son script dans un fichier de test pour que ça marche.

Si vous cherchez des librairies sur internet à ce sujet, @open-wc/testing est la première - et pratiquement la seule - ressource sur laquelle vous allez tomber. Développée par la communauté open-source Open Web Components avec Lit sous le capot, elle permet de charger un web component, son contenu et ses logiques - car normalement le contenu et les logiques d’un web component ne sont chargés que dans le navigateur.

Associés par défaut à la librairie de tests chai, les utilitaires de @open-wc/testing sont dissociables et utilisables avec tout autre librairie JavaScript de tests automatisés ou directement node:test (préférer alors @open-wc/testing-helpers).

Seule, l’utilisation de cette librairie permet à date de vérifier les contenus, les balises présentes, ou encore les événements déclenchés :

import "./MonFormulaire.js";
import { fixture, html, oneDefaultPreventedEvent } from "@open-wc/testing";

it("attend un événement au submit", async () => {
  // Sachant que
  const formulaire = await fixture(html`<mon-formulaire></mon-formulaire>`);

  // Lorsque
  formulaire.querySelector('button').click();

  // Alors
  const { detail } = await oneDefaultPreventedEvent(formulaire, 'submit');
  expect(detail).to.be.true;
});

Exemple de test inspiré de la documentation : https://open-wc.org/docs/testing/helpers/#events-with-preventdefault

Dans un exemple simple comme celui-ci on peut déduire ce que le test cherche à vérifier mais la lecture nécessite néanmoins un temps de compréhension, même pour un développeur frontend aguerri. Cette approche rend difficile la lecture pour quelqu’un découvrant la base de code, est très couplée à l’implémentation - une refacto de code transparent pour l’utilisateur et sans régression peut malgré tout casser le test correspondant - et, qui plus est, ne simule pas le comportement naturel d’un utilisateur vis-à-vis de notre application.

🧭 Direction Testing Library : les avantages apportés par le Behaviour-Driven Development

Testing library est une librairie fournissant des méthodes utilitaires pour requêter les éléments du DOM et simuler le comportement d’un utilisateur vis-à-vis d’un composant en prenant en compte la navigation à la souris et au clavier, ainsi que les spécifications d’accessibilité ARIA (Accessible Rich Internet Applications). Elle s’est popularisée dans les projets React et se décline aujourd’hui pour s’adapter aux frameworks de composants les plus utilisés, mais on peut également utiliser le cœur de la librairie en JS natif pour l’appliquer dans notre cas, avec @open-wc/testing. Ainsi nous pouvons nous abstraire de l’implémentation tout en écrivant un test compréhensible et représentatif d’un comportement utilisateur :

import "./MonFormulaire.js";
import { fixture, html } from "@open-wc/testing";
import { getByLabelText } from "@testing-library/dom";
import userEvent from "@testing-library/user-event";

it("permet de créer un compte", async () => {
  // Sachant que
  const utilisateur = userEvent.setup();
  const formulaire = await fixture(html`<mon-formulaire></mon-formulaire>`);

  const email = getByLabelText(formulaire, "Adresse e-mail");
  const motDePasse = getByLabelText(formulaire, "Mot de passe");
  const cgus = getByRole(formulaire, "checkbox", {name: "Accepter les conditions générales"});
  const valider = getByRole(formulaire, "button", {name: "Créer mon compte"});
  
  // Lorsque
  await utilisateur.type(email, "test@example.com");
  await utilisateur.type(motDePasse, "MotDeP@sse2024");
  await utilisateur.click(cgus);
  await utilisateur.click(valider);

  // Alors
  const messageDeSucces = getByRole(formulaire, "alert", {name: "Votre compte a bien été créé"})
  expect(messageDeSucces).toHaveFocus()
});

Exemple de test utilisant les fonctionnalités de @open-wc/testing et @testing-library/dom

Cette solution ne change que très peu les habitudes de tests d’un développeur utilisant déjà Testing Library sur un projet React, Angular, Vue… et nous a permis de vérifier en plus du comportement souhaité :

  • ✅ Que chaque champ de notre formulaire avait un libellé accessible ;
  • ✅ Que le champ des Conditions générales se comportait comme une case à cocher ;
  • ✅ Et que notre message de succès était correctement restitué aux lecteurs d’écran et la navigation clavier.

Plutôt efficace comme solution, mais ce n’est pas la seule !

🌳 Étape à Rhodes : le colosse Cypress face au modeste Open Web Components

Une alternative possible à @open-wc/testing est l’utilisation de Cypress en tests de composants ou end-to-end. Tout comme son concurrent, Cypress peut s’utiliser avec ou sans Testing Library et la manière d’écrire un test est quasiment identique :

import "./MonAccordeon.js";
import { html } from "lit";

describe("Design System - Accordeon", () => {
  it("affiche le contenu après clic sur le bouton d'ouverture", () => {
    // Sachant que
    const libelleBoutonOuverture = "Cypress est-elle la solution ?"
    cy.mount(html`
     <mon-accordeon libelle-bouton="${libelleBoutonOuverture}">
       <p>Très bonne question !</p>
     </mon-accordeon>
    `);

    // Lorsque
    cy.findByRole("button", {name: libelleBoutonOuverture}).click();

    // Alors
    cy.findByText("Très bonne question !"}).should("be.visible");
  });
});

Exemple de test de composant Cypress avec @testing-library/cypress

On notera l’utilisation, ici aussi, de la méthode utilitaire html de Lit pour charger le composant dans notre test.

Un avantage majeur de cette technique est qu’elle bénéficie d’une très large communauté avec un vaste écosystème de librairies et de plugins, ce qui assure sa pérennité et peut améliorer l’expérience développeur. Elle permet par ailleurs de visualiser les différents comportements dans une interface, ce qui peut s’avérer utile autant pour un développeur qu’un designer, et donc pertinent dans le cadre d’un Design System.

L’inconvénient principal - parce qu’il y en a toujours - c’est qu’il faille sortir l’artillerie lourde pour des composants qui ont majoritairement vocation à être légers et simples. En conséquence, comparée à une librairie spécifique aux tests unitaires de composants, elle souffre d’un manque de rapidité d’exécution.

🗻 Détour forcé : obstacle aux tests de web components

Les exemples de tests que j’ai partagés ont un point commun : ils testent des web components sans Shadow DOM. Cette spécificité qui fait partie intégrante du concept même du web component fait barrière lors de l’écriture de nos tests. En même temps c’est son rôle, puisqu’elle permet d’encapsuler les styles et les logiques de composant.

En effet dans le cas de @open-wc/testing, le composant rendu par la méthode fixture ne charge pas les web components enfants s’ils ont un shadow DOM. Ça peut être contraignant notamment dans un Design System si l’on considère la philosophie Atomic et qu’on souhaite orchestrer un ou plusieurs web component “molécules” dans un web component “organisme” par exemple.

Par ailleurs dans le cas de Cypress, le sélecteur ne peut pas voir dans un shadow DOM sans passer par la méthode .shadow() qui permet d’entrer dans le composant. Malheureusement en faisant ainsi, le code de test se couple de nouveau à l’implémentation de notre composant.

⛱️ Arrivée à destination : ce qu’on peut en retenir

L’approche proposée par Testing Library qui a fait ses preuves peut s’appliquer aux web components comme démontré précédemment, ce qui est une bonne nouvelle pour le développement accessible, et donc de Design Systems accessibles implémentés avec des web components : “L'avenir des design systems est dans l'accessibilité”.

Entre @open-wc/testing et Cypress (voire peut-être aussi Playwright?), il faudra choisir entre la simple librairie performante et spécifique avec une communauté plus faible mais qui peut continuer de grossir, et le framework lourd et plus lent mais avec un écosystème déjà bien établi.

Le point commun entre ces deux librairies, c’est l’utilisation de Lit pour rendre le composant dans leur contexte de test. La librairie développée par Google, qui en plus d’être légère simplifie la création de web components réactifs, continue son ascension (1 600 000 téléchargements par semaine sur npm en avril 2024 contre 900 000 en juillet 2023); ce qui est rassurant pour la pérennité de la librairie et donc de ces tests.

Par ailleurs, il est important de noter que cette étude montre des tests agnostiques des librairies utilisées pour créer des web components. Certains frameworks front capables de générer des web components doivent pouvoir les tester avant compilation à travers la librairie Testing Library associée (Svelte ou Solid.js par exemple).

Dans tous les cas, la question du Shadow DOM doit se poser non seulement pour vos tests, mais aussi parce que l'encapsulation de web components avec Shadow DOM pose des soucis d’accessibilité, notamment pour la lecture d’écran. La bonne nouvelle c’est que dans la plupart des cas (Design Systems inclus) on peut s’en passer.

🗺️ Pour prolonger le voyage