Created
November 20, 2020 14:18
-
-
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)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | |
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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