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 :
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...