screenshot-test-server
Version:
A node server for screenshot testing that takes base64 string for captured screenshot and compares it with exisiting screenshot and generates diff image and a test report as an html file.
366 lines (355 loc) • 18 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.msg = void 0;
exports.compareImages = compareImages;
exports.base64_encode = base64_encode;
exports.generateHtml = generateHtml;
const fs = __importStar(require("fs"));
const resemble = require('./resemble');
function writeDiffImage(data, diffPath, id) {
return __awaiter(this, void 0, void 0, function* () {
const dataString = data.getImageDataUrl().split(',')[1];
const diffFilePath = `${diffPath}/${id}.png`;
try {
yield fs.writeFileSync(diffFilePath, Buffer.from(dataString, 'base64'));
console.log(`Diff File saved as ${diffFilePath}`);
}
catch (err) {
console.error(`Error generating the the diff png for ${id}:`, err);
}
});
}
function compareImages(newFilePath, oldFilePath, diffPath, id, showDiffInGrayScale) {
// Paths to the images you want to compare
if (showDiffInGrayScale) {
resemble(oldFilePath)
.compareTo(newFilePath)
.ignoreColors() // Optional: Ignore colors for strict pixel comparison
.onComplete((data) => writeDiffImage(data, diffPath, id));
}
else {
{
resemble(oldFilePath)
.compareTo(newFilePath)
.onComplete((data) => writeDiffImage(data, diffPath, id));
}
}
}
function base64_encode(file) {
// read binary data
var bitmap = fs.readFileSync(file);
// convert binary data to base64 encoded string
return bitmap.toString('base64');
}
const rowStyle = 'display: flex; margin: 0px 4px; padding: 5px; overflow: scroll;';
// const imageStyle = 'max-width: 300px;'
const getMetaDataHtml = (title, description, id) => `<div style="border: 1px solid #aaa; border-radius: 2px; padding-bottom: 6px">
<div style="background-color: #eee; display: flex;">
<div style="background-color: #ddd; border: 1px solid lightgray; border-radius: 2px; padding: 5px 10px; cursor: pointer" onclick="onShowHide('${id}')"><i id="${id + 'icon'}" class="fa fa-chevron-down"></i></div>
<div style="padding: 6px;">${title}</div>
</div>
<div id="${id}" style="display: none;">
<div style="margin-left: 10px; margin-top: 6px;">
<span style="font-weight: bold; margin-right: 10px;">Description:</span> ${description}
</div>
<div style="margin-left: 10px; margin-top: 6px;">
<span style="font-weight: bold; margin-right: 10px;">ID:</span> ${id}
</div>
</div>
</div>`;
function failedHtml(diffPath, path, maxWidthOuter, metadata) {
return __awaiter(this, void 0, void 0, function* () {
let htmlStr = '', length = 0;
try {
const files = yield fs.readdirSync(diffPath);
length = files.length;
files.forEach((file) => {
const len = file.length;
const id = file.substring(0, len - 4);
const component = metadata.components.find((item) => item.id === id);
let { title, description, backgroundColor, maxWidth } = component;
maxWidth = maxWidth || maxWidthOuter;
const metaDataHtml = getMetaDataHtml(title, description, id);
htmlStr += `<div style="border: 2px solid #D45553; border-radius: 2px; margin: 12px 5px 20px 5px; padding: 5px; overflow: scroll; font-size: 14px">
${metaDataHtml}
<div class="imgContainer" style="${rowStyle} background-color: ${backgroundColor};">
<div><h3>Original:</h3><img style="max-width: ${maxWidth}px;" src="./old/${file}" alt="./old/${file}"/></div>
<div><h3>Modified:</h3><img style="max-width: ${maxWidth}px;" src="./new/${file}" alt="./new/${file}"/><button onclick="accept('${path}','${id}',${maxWidth})" style="background-color: #23569E; color: white; padding: 4px 8px;">Accept Changes</button></div>
<div><h3>Diff:</h3><img style="max-width: ${maxWidth}px;" src="./diff/${file}" alt="./diff/${file}"/></div>
</div>
</div>`;
});
}
catch (err) {
console.error('Unable to scan directory: ' + err);
}
return { htmlStr, length };
});
}
function passedHtml(newPath, oldPath, diffPath, maxWidthOuter, metadata) {
return __awaiter(this, void 0, void 0, function* () {
let htmlStr = '', length = 0;
try {
const files = yield fs.readdirSync(oldPath);
length = files.length;
files.forEach((file) => {
const len = file.length;
const id = file.substring(0, len - 4);
const component = metadata.components.find((item) => item.id === id);
let { title, description, backgroundColor, maxWidth } = component;
maxWidth = maxWidth || maxWidthOuter;
const metaDataHtml = getMetaDataHtml(title, description, id);
if (fs.existsSync(`${newPath}/${file}`) &&
fs.existsSync(`${diffPath}/${file}`)) {
}
else {
htmlStr += `<div style="border: 2px solid #89AB59; border-radius: 2px; margin: 12px 5px 20px 5px; padding: 5px; overflow: scroll; font-size: 14px">
${metaDataHtml}
<div class="imgContainer" style="${rowStyle} background-color: ${backgroundColor};">
<img style="max-width: ${maxWidth}px;" src="./old/${file}" alt="./old/${file}"/>
</div>
</div>`;
}
});
}
catch (err) {
console.error('Unable to scan directory: ' + err);
}
return { htmlStr, length };
});
}
// async function allHtml(
// newPath: string,
// oldPath: string,
// diffPath: string,
// path: string,
// maxWidthOuter: number,
// metadata: any
// ) {
// let htmlStr = '',
// length = 0
// try {
// const files = await fs.readdirSync(oldPath)
// length = files.length
// files.forEach((file) => {
// const len = file.length
// const id = file.substring(0, len - 4)
// const component = metadata.components.find((item: any) => item.id === id)
// let { title, description, backgroundColor, maxWidth } = component
// maxWidth = maxWidth || maxWidthOuter
// const metaDataHtml = getMetaDataHtml(title, description, id)
// if (
// fs.existsSync(`${newPath}/${file}`) &&
// fs.existsSync(`${diffPath}/${file}`)
// ) {
// htmlStr += `<div style="border: 2px solid #D45553; border-radius: 2px; margin: 12px 5px 20px 5px; padding: 5px; overflow: scroll; font-size: 14px">
// ${metaDataHtml}
// <div style="${rowStyle} background-color: ${backgroundColor};">
// <div><h3>Original:</h3><img style="max-width: ${maxWidth}px;" src="./old/${file}" alt="./old/${file}"/><button onclick="accept('${path}','${id}',${maxWidth})" style="background-color: #23569E; color: white; padding: 4px 8px;">Accept Changes</button></div>
// <div><h3>Modified:</h3><img style="max-width: ${maxWidth}px;" src="./new/${file}" alt="./new/${file}"/></div>
// <div><h3>Diff:</h3><img style="max-width: ${maxWidth}px;" src="./diff/${file}" alt="./diff/${file}"/></div>
// </div>
// </div>`
// } else {
// htmlStr += `<div style="border: 2px solid #89AB59; border-radius: 2px; margin: 12px 5px 20px 5px; padding: 5px; overflow: scroll; font-size: 14px">
// ${metaDataHtml}
// <div style="${rowStyle} background-color: ${backgroundColor};">
// <img style="max-width: ${maxWidth}px;" src="./old/${file}" alt="./old/${file}"/>
// </div>
// </div>`
// }
// })
// } catch (err) {
// console.error('Unable to scan directory: ' + err)
// }
// return { htmlStr, length }
// }
function getHtmlContent(diffHtmlString, passedHtmlString, path, maxWidth, all, failed, serverPort, metadata) {
const localhostUrl = 'http://127.0.0.1';
const url = `${localhostUrl}:${serverPort}`;
const urlForUpdate = `${url}/update`;
const urlForReset = `${url}/reset`;
return `<html>
<head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<script>
var diffHtmlString = \`${diffHtmlString}\`
var passedHtmlString = \`${passedHtmlString}\`
async function update (path,id,maxWidth) {
await fetch(\`${urlForUpdate}?path=\${path}&id=\${id}&maxWidth=\${maxWidth}\`)
}
async function accept (path,id,maxWidth) {
const res = confirm('Accept the change?')
if(!res) return
await update(path,id,maxWidth);
alert('Accepted the change. Please reload the page to see changes!')
}
async function reset (path,maxWidth) {
await fetch(\`${urlForReset}?path=\${path}&maxWidth=\${maxWidth}\`)
}
async function acceptAll (path,maxWidth) {
const res = confirm('Accept all changes?')
if(!res) return
await reset(path,maxWidth);
alert('All changes accepted. Please reload the page to see changes!')
}
function onShowHide(id){
const node = document.getElementById(id)
const icon = document.getElementById(id+'icon')
const style = node.getAttribute("style")
if(style){
node.setAttribute('style','')
icon.setAttribute('class','fa fa-chevron-up')
}
else{
node.setAttribute('style','display: none')
icon.setAttribute('class','fa fa-chevron-down')
}
}
function showAll() {
document.getElementById("mainBody").innerHTML = diffHtmlString + passedHtmlString
document.getElementById("chk").checked = false
}
function showDiff() {
document.getElementById("mainBody").innerHTML = diffHtmlString
document.getElementById("chk").checked = false
}
function showPassed() {
document.getElementById("mainBody").innerHTML = passedHtmlString
document.getElementById("chk").checked = false
}
function onloaded () {
let node = document.getElementById('acceptAll')
node.setAttribute("style","${failed ? '' : 'display: none'}")
const bdy = document.getElementsByTagName('body')[0]
let style = bdy.getAttribute('style')
bdy.setAttribute('style',style+\` border: 10px solid ${failed ? '#EEB6B3' : '#C6E0C4'}\`)
node = document.getElementById('header')
style = node.getAttribute('style')
node.setAttribute('style', style+\` background-color: ${failed ? '#EEB6B3' : '#C6E0C4'}\`)
node = document.getElementById('reportBox')
style = node.getAttribute('style')
node.setAttribute('style', style+\` background-color: ${failed ? '#350505' : '#001e03'}\`)
showAll();
}
function toggle() {
const nodes = document.getElementsByClassName('imgContainer')
if(nodes && nodes.length){
Array.from(nodes).forEach(node=>{
style = node.getAttribute('style')
node.setAttribute('style', style.includes('none') ? style.replace('none','flex') : style.replace('flex','none'))
})
}
}
</script>
</head>
<body onload="onloaded()" style="overflow: hidden; margin: 0;">
<div id="header" style="display: flex; justify-content: space-between; padding-bottom: 10px;">
<div>
<input type="radio" name="choice" onclick="showAll()" checked> All </input>
<input type="radio" name="choice" onclick="showDiff()"> Failed </input>
<input type="radio" name="choice" onclick="showPassed()"> Passed </input>
<div id="acceptAll">
<button style="margin-top: 10px; margin-left: 10px; padding: 6px 16px; background-color: #23569E; color: white; font-size: 14px; font-weight: bold; border-radius: 4px; cursor: pointer;" onclick="acceptAll('${path}',${maxWidth})">
Accept all changes
</button>
</div>
<div style="margin-top: 10px;">
<input id="chk" type="checkbox" onchange="toggle()">Hide images</input>
</div>
</div>
<div id="reportBox" style="padding: 10px 24px; border-radius: 2px; width: 140px; margin-left: 10px; font-family: Arial, Helvetica, sans-serif;">
<div style="color: wheat; display: flex; justify-content: space-between; margin-bottom: 4px;">
<div>Total: </div>
<div>${all} </div>
</div>
<div style="color: #7CE77C; display: flex; justify-content: space-between; margin-bottom: 4px;">
<div>Passed: </div>
<div>${all - failed} </div>
</div>
<div style="color: #F87069; display: flex; justify-content: space-between;">
<div>Failed: </div>
<div>${failed} </div>
</div>
</div>
</div>
<div id="mainBody" style="overflow-y: scroll; height: 90%">
</div>
</body>
</html>
`;
}
exports.msg = `
<div>
<h2>Welcome to screenshot Testing!</h2>
<p>
To see the screenshots, open the <span style="color: blue;">test.html</span> file which is generated inside the path you provided in the <b>withScreenShot</b> function.
</p>
</div
`;
function generateHtml(path, maxWidth, serverPort, backgroundColor, metadataParam) {
return __awaiter(this, void 0, void 0, function* () {
const oldPath = path + '/old';
const newPath = path + '/new';
const diffPath = path + '/diff';
let metadata = metadataParam;
if (metadata) {
metadata = Object.assign(Object.assign({}, metadata), { components: metadata.components.map((item) => (Object.assign(Object.assign({}, item), { title: item.title.replaceAll('`', '"').replaceAll("'", '"'), id: item.id.replaceAll('`', '"').replaceAll("'", '"'), backgroundColor: item.backgroundColor || backgroundColor }))) });
try {
const metadataPath = path + '/metadata.json';
yield fs.writeFileSync(metadataPath, JSON.stringify(metadata));
console.log(`Metadata file saved as ${metadataPath}`);
}
catch (err) {
console.error('error while writing metadata file', err);
}
}
else {
const metadataStr = yield fs.readFileSync(path + '/metadata.json');
metadata = JSON.parse(metadataStr.toString());
}
const { htmlStr: diffHtmlString, length: failed } = yield failedHtml(diffPath, path, maxWidth, metadata);
const { htmlStr: passedHtmlString, length: passed } = yield passedHtml(newPath, oldPath, diffPath, maxWidth, metadata);
const all = failed + passed;
const finalHtmlString = getHtmlContent(diffHtmlString, passedHtmlString, path, maxWidth, all, failed, serverPort, metadata);
try {
yield fs.writeFileSync(path + '/test.html', finalHtmlString);
console.log(`HTML File saved as ${path + '/test.html'}`);
}
catch (err) {
console.error('Error writing the test.html file:', err);
}
});
}