13 hours ago

In my job, i recently wanted to build to a tool that would help me grade student React projects.

One of the things i wanted to do was compare the visual aspect of the website to the expected baseline.

To do so, I used cypress to take screenshots of the student's websites and the correction.

Then, i used pixelmatch to compare the pictures, and compute a pixel percentage difference.

Taking screenshots with Cypress

Cypress expose the cy.screenshot() method, which is quite straightforward :

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');
  });

In order for cypress to access URL on a different domain than localhost, you need to add the "chromeWebSecurity": false setting in cypress.json. You also must use a webkit-based browser to run the tests.

In two separate tests (as cypress forbid calling .visit() twice in a test), I navigate to both websites, and take screenshots.

Screens are saved by default in ./cypress/screenshots/TESTFOLDER/TESTFILE.spec.js/

Using the pixematch library

With my screenshots ready, I wanted to pipe them into pixelmatch in order to see how similar they were.

it('somewhat ressembles the correction', () => {
		// PNGJS lets me load the picture from disk
    const PNG = require('pngjs').PNG;
		// pixelmatch library will handle comparison
    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 => {
				// load both pictures
        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 });
    
				// calling pixelmatch return how many pixels are different
        const numDiffPixels = pixelmatch(img1.data, img2.data, diff.data, width, height, { threshold: 0.1 });

				// calculating a percent diff
        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);
      });
    });
  });

Seeing those chained .then(), one might be tempted to rewrite this code using async / await. It is not so easy, as cypress does not use Promises, although it really looks as such ! In order to use async / await, one could use a library such as cypress-promise.

At the end of our test, we see our assertion : the difference must be less than 40%. This assertion can be seen in the test runner :

résultat du test runner

Thanks to this code, I can now easily know if a student's code respects the subject's mockup. One could think of other uses outside of teaching : comparing two production environments, see if all browsers render the website the same way...