il y a 13 heures

Dans mon travail, j'ai récemment voulu créer des outils automatisés pour m'aider a corriger les exercices en React de mes étudiants.

Un de mes besoins était de comparer le rendu visuel du code de l'étudiant, a celui de la correction "officielle" du sujet.

Pour ce faire, j'ai utiliser cypress pour prendre des captures d'écran des deux sites : étudiant et correction. Ensuite, j'ai utilisé la librairie pixelmatch pour comparer les deux images, et déterminer un pourcentage de correspondance.

Dans ce bref tutoriel, je vais vous montrer comment je m'y suis pris !

Prendre des captures d'écran avec Cypress

Cypress expose la méthode cy.screenshot(), qui est assez simple à utiliser :

it('takes screenshots of the correction', () => {
    cy.visit('<https://therapeutic-sidewalk.surge.sh>');

    cy.get('input').type('salut');
    cy.get('form').submit();
    cy.wait(500);

    cy.screenshot('baseline-home');
  });

  it('takes screenshots of the students project', () => {
    cy.visit('<http://localhost:8080>');

    cy.get('input').type('salut');
    cy.get('form').submit();
    cy.wait(500);

    cy.screenshot('student-home');
  });

Notons que pour que cypress puisse accéder à des URL autres que localhost, il est nécessaire d'ajouter l'option "chromeWebSecurity": false dans cypress.json. Il faudra également que cypress utilise un navigateur basé sur webkit.

Dans deux tests distincts (car cypress interdit de .visit() deux sites dans le même test) , on visite tantôt le projet étudiant, tantôt la correction, et l'on crée des screenshots.

Les screenshots seront sauvegardés par défaut dans ./cypress/screenshots/DOSSIERDETEST/FICHIERDUTEST.spec.js/

Utiliser la librairie pixelmatch

Je souhaitais ensuite comparer les deux screenshots. Pour ce faire, on peut utiliser la librairie pixelmatch de la sorte :

it('somewhat ressembles the correction', () => {
		// PNGJS me permet de charger l'image depuis le disque
    const PNG = require('pngjs').PNG;
		// la librairie pixelmatch se chargera de la comparaison
    const pixelmatch = require('pixelmatch');

    cy.readFile(
      './cypress/screenshots/github-workshop/base.spec.js/baseline-home.png', 'base64'
    ).then(baseImage => {
      cy.readFile(
        './cypress/screenshots/github-workshop/base.spec.js/student-home.png', 'base64'
      ).then(studentImage => {
				// on charge les deux images
        const img1 = PNG.sync.read(Buffer.from(baseImage, 'base64'));
        const img2 = PNG.sync.read(Buffer.from(studentImage, 'base64'));
    
        const { width, height } = img1;
        const diff = new PNG({ width, height });
    
				// l'appel a pixelmatch nous renvoie un nombre de pixels de différence
        const numDiffPixels = pixelmatch(img1.data, img2.data, diff.data, width, height, { threshold: 0.1 });

				// on fait enfin une petite règle de trois pour obtenir un pourcentage
        const diffPercent = (numDiffPixels / (width * height) * 100);

        cy.task('log', `Found a ${diffPercent.toFixed(2)}% pixel difference`);
        cy.log(`Found a ${diffPercent.toFixed(2)}% pixel difference`);
        //cy.writeFile('diff.png', PNG.sync.write(diff));

        expect(diffPercent).to.be.below(40);
      });
    });
  });

A la vue des .then() chainés, on pourrait être tenté de réécrire le code de façon plus lisible avec des async / await . Ce n'est pas faisable si simplement, car cypress ne manipule pas des promesses Javascript, même si cela y ressemble beaucoup ! Une solution serait d'utiliser une librairie comme cypress-promise.

A la fin du test, on retrouve notre assertion : la différence doit être de moins de 40%. Cette assertion est visible dans le test runner de cypress :

résultat du test runner

Grâce a ce petit outil, je suis désormais capable de déterminer rapidement si le code d'un étudiant respecte la maquette fournie. On pourrait imaginer des applications en dehors de la pédagogie : comparer différents environnement de déploiement, valider un design, comparer le visuel entre deux navigateurs...