https://d226lax1qjow5r.cloudfront.net/blog/blogposts/how-to-build-a-visual-regression-test-system-using-playwright/visual-regression-test.png

Comment construire un système visuel de test de régression à l'aide de Playwright

Publié le August 23, 2022

Temps de lecture : 11 minutes

Supposons que Joe travaille sur le composant réutilisable "bouton", qu'il apporte une modification et qu'il s'assure que le bouton fonctionne comme prévu. Joe sait-il que les autres composants sont compatibles avec cette modification ? Joe devrait savoir quels composants dépendent du bouton et les passer tous en revue - en s'assurant visuellement que les boutons ont un aspect correct - dans différents scénarios. Et qu'en est-il des différents navigateurs ? La matrice de test ne cesse de croître...

Aidons Joe à mieux utiliser son temps en automatisant le processus de test visuel !

Tout comme Joe, vous avez une application en cours d'exécution ou vous avez créé vos composants et souhaitez les partager avec le monde entier. À chaque modification, vous (ou un autre membre de votre équipe) passez des heures à examiner l'interface utilisateur pour vous assurer qu'elle est parfaite au pixel près. Même dans ce cas, il arrive que des éléments échappent à l'attention humaine. Ne nous voilons pas la face : les machines sont meilleures que nous pour faire correspondre les pixels. Elles coûtent également moins cher et peuvent le faire encore et encore.

C'est ici que commence notre histoire. Nous avons un plusieurs composants d'interface utilisateur. Nous avons également une documentation pour nos composants dans laquelle nous pouvons "jouer" avec eux (c'est-à-dire les tester manuellement).

La documentation est créée à partir de fichiers readme qui se trouvent dans le dossier de chaque composant. Voici un exemple de fichier.

Notez que les exemples sont de simples extraits HTML :

<vwc-icon type="heart-solid" connotation="accent"></vwc-icon>
<vwc-icon type="heart-solid" connotation="announcement"></vwc-icon>
<vwc-icon type="heart-solid" connotation="cta"></vwc-icon>
<vwc-icon type="heart-solid" connotation="success"></vwc-icon>
<vwc-icon type="heart-solid" connotation="alert"></vwc-icon>
<vwc-icon type="heart-solid" connotation="info"></vwc-icon>

Nous y reviendrons plus tard.

Nous disposons donc d'une cinquantaine de composants et d'un moyen de les visualiser tout en les développant et en les testant manuellement. C'est très bien ! Vous voulez être en mesure de voir les composants avant de les expédier :).

Certains de ces composants sont constitués de plusieurs composants atomiques. Cela signifie que la modification du composant "bouton" peut affecter plusieurs composants complexes qui utilisent le bouton.

Comment mettre en place un dramaturge dans un projet ?

La première étape consiste à courir :

npm i -D playwright

Cela va permettre de faire un certain nombre de choses :

  1. Ajouter playwright à notre fichier package.json.

  2. Installer playwright localement.

  3. Installer playwrightlocalement.

Nous devrons également créer un fichier playwright.config fichier. Pour cette introduction (et parce que c'est ce que je fais personnellement), nous utiliserons la version typescript mais vous pouvez utiliser du simple javascript. Nous allons créer un fichier playwright.config.ts avec le contenu suivant :

import type {
  PlaywrightTestConfig
} from '@playwright/test';

const config: PlaywrightTestConfig = {
  testMatch: 'src/**/*.test.ts',
  projects: [{
      name: 'Chrome Stable',
      use: {
        browserName: 'chromium',
        channel: 'chrome',
      },
    },
    {
      name: 'Desktop Safari',
      use: {
        browserName: 'webkit',
      }
    },
    {
      name: 'Desktop Firefox',
      use: {
        browserName: 'firefox',
      }
    },
  ]
};

export default config;

La configuration ci-dessus permet d'obtenir les résultats suivants :

  1. testMatch: Indique à playwright où obtenir les fichiers de test. Notez que le chemin est relatif à l'emplacement du fichier. Dans le référentiel vivant, le fichier de configuration est situé dans le dossier components, et tous les composants sont dans le dossier src . Nous avons pris l'habitude d'appeler les fichiers de test *.test.ts - de sorte que chaque fichier qui suit ce modèle sera inclus dans l'exécution du test.

  2. projects: Indique à playwright dans quelles configurations exécuter les tests. Dans ce cas, nous avons 3 configurations pour chacun des principaux navigateurs que nous souhaitons tester.

Il y a bien d'autres choses à faire dans la playwright configuration. Vous pouvez en savoir plus ici.

Maintenant que playwright "sait où trouver les fichiers et sur quels navigateurs exécuter nos tests, nous pouvons commencer à mettre en place les tests eux-mêmes.

Comment rédiger des tests de dramaturgie

Pour créer un test, nous devons créer un fichier qui sera trouvé par la commande testMatches. C'est pourquoi dans notre référentiel, nous créons un fichier ui.test.ts pour chaque composant. Voici un exemple de fichier de test :

test('should show the component', async ({
  page
}: {
  page: Page
}) => {
  const template = `...`;

  await loadComponents({
    page,
    components,
  });
  await loadTemplate({
    page,
    template,
  });

  const testWrapper = await page.locator('#wrapper');
  await page.locator('#modal');

  await page.waitForLoadState('networkidle');

  await page.evaluate(() => {
    const modal = (document.getElementById('modal') as Dialog);
    modal.showModal();
    return modal;
  });

  expect(await testWrapper?.screenshot()).toMatchSnapshot(
    './snapshots/dialog.png'
  );
});

Pour le dossier complet, cliquez ici.

Voyons cela en détail.

Bloc d'essai du dramaturge

La première chose que nous remarquons après que toutes les importations ont été effectuées est que nous avons un test bloc. La fonction envoyée au bloc test accepte un objet test. L'une des propriétés de l'objet est la propriété page est l'une des propriétés de l'objet. Cette propriété page est la représentation page représentation dans Playwright. Nous l'utilisons pour manipuler et observer la page testée.

Préparation de la page de test

Dans ce fichier, nous avons un test qui génère un modèle (la variable template ). Il charge ensuite les composants nécessaires et le modèle dans la page de test à l'aide des fonctions utilitaires que nous avons créées (nous y reviendrons plus tard).

Attente du chargement de l'HTML

Les lignes suivantes utilisent la méthode page.locator pour obtenir les éléments recherchés. Un autre "effet secondaire" de l'utilisation du localisateur est qu'il se résout lorsque nos éléments sont visibles sur la page. Cela permet de s'assurer que lorsque nous exécutons les tests, nos éléments sont déjà dans le DOM.

Attendre que les ressources soient recherchées

Un autre mode d'attente consiste à attendre que tout le trafic réseau soit inactif. C'est important pour nous car nous voulons parfois attendre que les icônes ou les images se chargent à partir d'un CDN.

Déclencher des éléments DOM API programmatique

Enfin, il y a la evaluate de l'objet page . Cette méthode évalue le code JavaScript à l'intérieur de la page. Dans ce cas, il s'agit du code évalué :

await page.evaluate(() => {
  const modal = (document.getElementById('modal') as Dialog);
  modal.showModal();
  return modal;
});

Dans ce cas, nous obtenons l'élément modal et évoquons sa méthode showModal et évoquons sa méthode. De cette manière, nous pouvons tester l'API programmatique des éléments du DOM.

Générer et comparer des instantanés

La dernière ligne est la "magie" de la régression visuelle du dramaturge :

expect(await testWrapper?.screenshot()).toMatchSnapshot(
  './snapshots/dialog.png'
);

L'objet locator objet résolu testWrapper possède une méthode screenshot . Lorsque nous invoquons cette méthode et que nous utilisons l'attente toMatchSnapshot elle effectue l'une des opérations suivantes :

  1. S'il existe un instantané dans le chemin d'accès donné à toMatchSnapshotil compare l'instantané pris à celui qui existe dans le système de fichiers. S'ils ne correspondent pas, le test échoue.

  2. S'il n'y a pas d'instantané dans le chemin, il en génère un nouveau.

Comment charger la page de test pendant un test ?

Dans la dernière section, nous avons vu un exemple de la manière dont les tests peuvent être écrits. Nous avons utilisé un objet page pour manipuler notre page de test, mais cela soulève quelques questions : quelle est cette mystérieuse page ? D'où vient le contenu HTML/CSS/JS ?

En substance, l'objet page a une méthode goto. Cette méthode accepte une URL et la charge sur le serveur page. C'est parfait si vous avez une application qui fonctionne. Dans ce cas, tout ce que vous avez à faire est de servir votre application. Cela peut être fait en utilisant le serveur de développement de votre bundler, en utilisant un serveur HTML statique ou même en testant votre environnement QA, de développement ou de production.

Vos tests commencent généralement par un goto et ressembleraient à ceci :

test('should show text hello world', async ({page}) => {
	await page.goto(myAppUrl);
	const headerText = await manipulateApp(page);
	expect(headerText).toEqual('Hello World');
});

Nous goto l'application ou la page de test, nous la manipulons d'une manière ou d'une autre et nous nous attendons à ce qu'une fonctionnalité existe ou soit équivalente à quelque chose.

Comment servir des composants uniques pour une comparaison instantanée ?

Dans les bibliothèques de composants d'interface utilisateur, il n'y a pas d'application réelle à tester. vividcomme dans d'autres bibliothèques de composants d'interface utilisateur, il n'y a pas de véritable application à tester. Nous avons notre documentation - mais la tester reviendrait à tester d'autres choses qui ne sont pas liées à nos composants. En fait, la plupart des données visuelles de la documentation n'ont rien à voir avec les composants.

La meilleure chose à faire est de créer une page de test dédiée à chaque composant. Pour cela, nous servons l'ensemble de notre projet à l'aide de la commande http-server. Notre commande de régression visuelle ressemble à ceci : npm run build & npx http-server -s & npx playwright test.

Cela signifie que nous construisons nos composants, servons le référentiel et donc notre construction, puis lançons les tests. Nous avons également un fichier HTML vide qui est servi, et nous l'utilisons dans la commande page.goto comme suit :

await page.goto('[http://127.0.0.1:8080/scripts/visual-tests/index.html](http://127.0.0.1:8080/scripts/visual-tests/index.html)');

Ce fichier est un fichier HTML vide. Nous pourrions créer manuellement un fichier HTML pour chaque test, mais quel est l'intérêt d'une telle démarche ? Voyons comment nous pouvons commencer à automatiser la génération de nos pages de test à partir de notre code.

Comment injecter des fichiers JS et des styles dans notre page avec Playwright ?

Nous avons maintenant une page HTML pour goto. Elle est encore inutile car nous devons pouvoir injecter le code de nos composants.

Pour cela, nous pouvons regarder la fonction utilitaire que nous avons vue dans l'exemple du fichier de test - loadComponents.

loadComponents est une fonction qui accepte un tableau de noms de composants et utilise la méthode de playwright addScriptTag pour ajouter les composants :

export async function loadComponents({
    page,
    components,
    styleUrls = defaultStyles,
}: {
    page: Page,
    components: string[],
    styleUrls ? : string[]
}) {
    await page.goto('http://127.0.0.1:8080/scripts/visual-tests/index.html');

    (async function() {
        for (const component of components) {
            await page.addScriptTag({
                url: `http://127.0.0.1:8080/dist/libs/components/${component}/index.js`,
                type: 'module',
            });
        }
    })();

    const styleTags$ = styleUrls.map(url => page.addStyleTag({
        url
    }));
    await Promise.all(styleTags$);
}

La magie opère dans la boucle for asynchrone au milieu de la fonction : elle passe en revue chaque composant du tableau et utilise la fonction addScriptTag pour les charger dans notre page de démonstration.

Elle le fait également pour les balises de style situées une ligne plus bas. La fonction est résolue lorsque toutes les balises de style se trouvent dans le DOM.

Ainsi, dans chaque test, nous prenons la même page vierge et nous chargeons les composants que nous voulons tester. Par exemple, si nous voulons tester les éléments d'un formulaire, nous définissons un tableau avec text-field, text-area, select, button et d'autres éléments de formulaire. La fonction loadComponents les injectera dans la page de test pour nous.

Notre trafic réseau va ressembler à ceci dans le cas où notre tableau serait configuré comme ceci : [icon, button, focus, dialog, text-field,layout]:

The JavaScript files loaded in the test page as shown in the network tab of chrome dev toolsA list of JS files from the network tab

N'oubliez pas que nous avons également chargé des modèles :

The CSS files loaded in the test page as shown in the network tab of chrome dev toolsA list of css files from the network tab

Whoopy - JS et CSS sont chargés !

Comment afficher les composants sur la page HTML ?

Nous disposons à présent d'une page HTML dans laquelle ont été injectés les scripts et les styles nécessaires. Il nous reste à injecter le code HTML qui utilisera effectivement nos composants.

Supposons que nous voulions tester un bouton. Nous allons créer une chaîne de caractères qui ressemble à ceci :

<vwc-button label=”Click Me”></vwc-button>

Nous pouvons ajouter d'autres éléments au bouton, comme la connotation ou l'apparence, ou même choisir un composant différent (je veux dire que si nous voulions tester le dialogue, créer un extrait de bouton serait plutôt inutile).

Nous voulons maintenant que ce code HTML soit affiché sur notre page de test. Pour cela, nous allons utiliser une astuce avec playwright's page.addScriptTag. Dans la fonction utilitaire loadTemplatenous allons accepter la page et la chaîne du modèle. Nous ajouterons ensuite un script à la page qui insérera le modèle HTML dans la page :

export async function loadTemplate({
    page,
    template
}: {
    page: Page,
    template: string
}) {
    const wrappedTemplate = `<div id="wrapper">${template}</div>`;
    await page.addScriptTag({
        content: `
            document.body.innerHTML = \`${wrappedTemplate}\`;
        `,
    });
}

La fonction commence par envelopper notre modèle avec une chaîne div puis ajoute un script à la page qui remplace l'innerHTML du corps par notre chaîne HTML enveloppée.

Ainsi, si nous avons une chaîne HTML comme celle-ci :

<vwc-dialog id="modal"
		icon="info" 
		headline="Headline"
		text="This is the content that I want to show, and I will show it!!!"
>				
</vwc-dialog>

Nous obtiendrons le résultat suivant dans le navigateur :

A web browser with a modal dialog open. To the side the chrome dev tools are open showing the HTML we expectedThe test page and the devtools

Affichage de notre modale. À droite de la modale, vous pouvez voir le code HTML que nous avons injecté dans la page.

Génération et comparaison de captures d'écran dans Playwright

Maintenant que notre page de test est prête, nous pouvons prendre un instantané. Nous l'avons déjà fait dans la vue d'ensemble, mais voici un rappel :

const testWrapper = await page.locator('#wrapper');
await page.locator('#modal');

await page.waitForLoadState('networkidle');

await page.evaluate(() => {
  const modal = (document.getElementById('modal') as Dialog);
  modal.showModal();
  return modal;
});

expect(await testWrapper?.screenshot()).toMatchSnapshot(
  './snapshots/dialog.png'
);

Étape 1 : Attendre que la page soit prête

Nous attendons que le wrapper et la fenêtre modale soient sur la page en utilisant la méthode page.locator et à l'événement networkidle pour nous assurer que toutes nos ressources sont chargées.

Étape 2 : Manipuler la page

Une fois la page prête, nous évaluons le script qui utilise l'API showModal sur l'élément modal.

Étape 3 : Prendre une capture d'écran et la valider

Maintenant, si tout fonctionne correctement, nous arrivons à notre ligne expect qui prend l'instantané et le compare à un instantané antérieur.

La méthode testWrapper.screenshot fait partie de l playwright.locator API. Elle prend une capture d'écran de l'élément et nous permet d'utiliser l'API toMatchSnapshot et nous permet d'utiliser l'API d'attente. Dans ce cas, nous la faisons correspondre à un chemin de fichier (l'élément match path) - c'est là que la capture d'écran est enregistrée.

Si c'est la première fois que nous exécutons le test, une capture d'écran est générée dans le fichier match path. Toutes les modifications apportées seront comparées au résultat original, ce qui permet de s'assurer que toute modification de l'apparence de notre composant échouera au test et que nous n'expédierons pas de modifications visuelles non souhaitées.

Étape 4 : Mise à jour des captures d'écran

Dans ce cas, nous voulons changer l'apparence, playwright dispose d'un drapeau update-snapshots qui met à jour l'instantané. Pour cela, nous avons une tâche update dans notre référentiel, mais dans Playwright, cela fonctionne comme suit :

npx playwright test --update-snapshots

Résumé

Nous disposons à présent d'un système de régression visuelle entièrement fonctionnel pour les composants de l'interface utilisateur. Construire ce type de tests pour les composants de l'interface utilisateur est un peu plus compliqué que de tester une application.

Lors du test d'une application, il suffit de charger l'application via un serveur externe et d'utiliser ensuite goto à l'adresse. Lorsque vous testez des composants, vous avez besoin de ce même serveur, mais cette fois-ci, il pointera vers une page vide que nous devons construire pendant le test lui-même.

Les deux fonctions d'utilité, loadComponents et loadTemplate qui ont utilisé les fonctions d'utilité du dramaturge addScriptTag et addStyleTagsont la sauce secrète qui nous permet de tester chaque composant de manière isolée en créant simplement nos extraits HTML.

Nous utilisons un code très similaire pour créer un test pour chaque composant de notre bibliothèque ou page/récit utilisateur de notre application. De cette manière, nous créons un instantané pour chaque composant/page/récit utilisateur et nous nous assurons que l'utilisateur obtiendra le même résultat pour la même procédure. Tout cela se fait automatiquement dans les navigateurs que nous avons définis dans notre configuration (chrome, firefox et Safari). Meilleure qualité - gain de temps.

Nous disposons désormais d'un mécanisme de test de régression visuel automatisé et local. La raison pour laquelle nous l'avons créé est de pouvoir exécuter ces tests localement, afin que le développeur puisse obtenir un retour d'information rapide et corriger les erreurs avant même de pousser ses changements.

Mais nous pouvons encore améliorer bien d'autres choses :

  • Comment automatiser la génération de tests ?

  • Comment se protéger de l'instabilité (un problème connu dans les tests e2e) ?

  • Comment intégrer et optimiser nos tests dans les actions GitHub ?

  • Qu'en est-il du mode de développement et de débogage pour les tests visuels ?

Je reviendrai sur ces sujets dans le prochain article. En attendant, vous pouvez consulter notre référentiel Vividnotre configuration de l'auteur et la documentation sur les tests de l'interface utilisateur pour le voir en action.

Partager:

https://a.storyblok.com/f/270183/400x400/7bf76cb05c/yonatankra.png
Yonatan KraArchitecte logiciel Vonage

Yonatan a participé à des projets impressionnants à l'université et dans l'industrie - de C/C++ à PHP et javascript en passant par Matlab. Il a été directeur technique chez Webiks et architecte logiciel chez WalkMe. Il est actuellement architecte logiciel chez Vonage et instructeur.