Skip to content

Instantly share code, notes, and snippets.

@gamerxl
Created November 20, 2020 14:18
Show Gist options
  • Save gamerxl/1de0d2d5d08110dbe9ab4befc0836c25 to your computer and use it in GitHub Desktop.
Save gamerxl/1de0d2d5d08110dbe9ab4befc0836c25 to your computer and use it in GitHub Desktop.
Visual site comparison (Core for visual regression testing with puppeteer and Node.js)
const fs = require('fs');
const { promisify } = require('util');
const puppeteer = require('puppeteer');
const looksSame = require('looks-same');
const mkdir = promisify(fs.mkdir);
const readdir = promisify(fs.readdir);
/**
* Returns a normalized link for usage as a valid file name.
*
* @param {String} link
* @return {String} Valid file name
*/
const normalizeLink = (link) => {
link = link.replace('https://', '');
link = link.replace(/[/#?]$/g, '');
link = link.replace(/[./#?[\]=]/g, '-');
link = link.replace(/(%5B|%5D)/g, '-');
return link;
};
// https://stackoverflow.com/questions/51529332/puppeteer-scroll-down-until-you-cant-anymore
async function scrollToBottom() {
await new Promise(resolve => {
const distance = 100; // should be less than or equal to window.innerHeight
const delay = 100;
const timer = setInterval(() => {
document.scrollingElement.scrollBy(0, distance);
if (document.scrollingElement.scrollTop + window.innerHeight >= document.scrollingElement.scrollHeight) {
clearInterval(timer);
resolve();
}
}, delay);
});
}
/**
* Visit a page and makes a screenshot.
*
* @param {String} browser
* @param {String} link
* @param {String} directory
*/
const visitPage = async (browser, link, directory) => {
const page = await browser.newPage();
await page.goto(link);
await page.evaluate(scrollToBottom);
await page.waitFor(2500);
if (!fs.existsSync(directory)) {
await mkdir(directory);
}
await page.screenshot({ path: directory + '/' + normalizeLink(link) + '.png', fullPage: true });
await page.close();
};
/**
* Visit a site and visit all local links on it and makes a screenshot of each of those.
*
* @param {String} browser
* @param {String} siteLink
* @param {String} directory
* @param {Number} max
*/
const visitSite = async (browser, siteLink, directory, max) => {
const page = await browser.newPage();
await page.goto(siteLink);
await page.evaluate(scrollToBottom);
await page.waitFor(2500);
if (!fs.existsSync(directory)) {
await mkdir(directory);
}
await page.screenshot({ path: directory + '/' + normalizeLink(siteLink) + '.png', fullPage: true });
let c = 1;
const siteLinks = await page.evaluate((siteLink) => {
const links = Array.from(document.querySelectorAll('a'));
return links.filter(link => link.href.includes(siteLink)).map(link => link.href);
}, siteLink);
await page.close();
for (const link of siteLinks) {
await visitPage(browser, link, directory);
c++;
if (c === max) {
break;
}
}
};
/**
* Compares images of a directory with another directory
* based on same name and puts diff images in another directory.
*
* @param {String} directoryA
* @param {String} directoryB
* @param {String} diffsDirectory
*/
const compareDirectories = async (directoryA, directoryB, diffsDirectory) => {
const names = await readdir(directoryA);
if (!fs.existsSync(directoryA) || !fs.existsSync(directoryB)) {
return;
}
if (!fs.existsSync(diffsDirectory)) {
await mkdir(diffsDirectory);
}
for (const name of names) {
const pathA = directoryA + '/' + name;
const pathB = directoryB + '/' + name;
if (fs.existsSync(pathB)) {
// Mostly same options as for diff operation later on in the process
const compareOptions = {
strict: false,
tolerance: 2.5,
ignoreAntialiasing: true,
ignoreCaret: true,
antialiasingTolerance: 0
};
// Compare image A (pathA) with image B (pathB)
looksSame(pathA, pathB, compareOptions, (error, { equal }) => {
console.log(pathA, pathB, equal);
// Mostly same options for earlier in the process
looksSame.createDiff({
reference: pathA,
current: pathB,
diff: diffsDirectory + '/' + name,
highlightColor: '#ff00ff', // color to highlight the differences
strict: false, // strict comparsion
tolerance: 2.5,
antialiasingTolerance: 0,
ignoreAntialiasing: true, // ignore antialising by default
ignoreCaret: true // ignore caret by default
}, function(error) {
});
});
}
}
};
/**
* Create and returns a headless puppeteer browser instance.
*
* @param {Number} viewportWidth
* @param {Number} viewportHeight
* @return Puppeteer browser instance
*/
const createPuppeteerBrowser = async (viewportWidth = 1920, viewportHeight = 1080) => {
return await puppeteer.launch({
args: ['--no-sandbox', '--disable-setuid-sandbox'],
headless: true,
defaultViewport: {
width: viewportWidth,
height: viewportHeight
}
});
};
(async () => {
const browser = await createPuppeteerBrowser();
await visitSite(browser, 'https://site-version-a.domain/', 'version-a', 3);
await visitSite(browser, 'https://site-version-b.domain/', 'version-b', 3);
await browser.close();
await compareDirectories('version-a', 'version-b', 'version-ab-compared');
})();
{
"name": "site-compare",
"version": "1.0.0",
"description": "Script for comparison of sites for visual regression testing.",
"main": "compare.js",
"dependencies": {
"looks-same": "^7.2.1",
"puppeteer": "^1.18.1"
},
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "GamerXL (Martin)",
"license": "ISC"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment