creevey
Version:
Cross-browser screenshot testing tool for Storybook with fancy UI Runner
186 lines • 8.2 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getPixelmatchAssert = getPixelmatchAssert;
exports.getOdiffAssert = getOdiffAssert;
const path_1 = __importDefault(require("path"));
const assert_1 = __importDefault(require("assert"));
const pngjs_1 = require("pngjs");
const promises_1 = require("fs/promises");
async function getStat(filePath) {
try {
return await (0, promises_1.stat)(filePath);
}
catch (error) {
if (typeof error == 'object' && error && error.code === 'ENOENT') {
return null;
}
throw error;
}
}
async function getLastImageNumber(imageDir, imageName) {
const actualImagesRegexp = new RegExp(`${imageName}-actual-(\\d+)\\.png`);
try {
return ((await (0, promises_1.readdir)(imageDir))
.map((filename) => filename.replace(actualImagesRegexp, '$1'))
.map(Number)
.filter((x) => !isNaN(x))
.sort((a, b) => b - a)[0] ?? 0);
}
catch {
return 0;
}
}
async function readExpected(expectImageDir, imageName) {
const expected = await (0, promises_1.readFile)(path_1.default.join(expectImageDir, `${imageName}.png`));
return expected;
}
async function saveImages(imageDir, images) {
const files = [];
await (0, promises_1.mkdir)(imageDir, { recursive: true });
for (const { name, data } of images) {
const filePath = path_1.default.join(imageDir, name);
await (0, promises_1.writeFile)(filePath, data);
files.push(filePath);
}
return files;
}
async function getImagePaths(config, testFullPath, assertImageName) {
const testPath = [...testFullPath];
const imageName = assertImageName ?? testPath.pop();
(0, assert_1.default)(typeof imageName === 'string', `Can't get image name from empty test scope`);
const expectImageDir = path_1.default.join(config.screenDir, ...testPath);
const reportImageDir = path_1.default.join(config.reportDir, ...testPath);
const imageNumber = (await getLastImageNumber(reportImageDir, imageName)) + 1;
const actualImageName = `${imageName}-actual-${imageNumber}.png`;
const expectImageName = `${imageName}-expect-${imageNumber}.png`;
const diffImageName = `${imageName}-diff-${imageNumber}.png`;
return { imageName, actualImageName, expectImageName, diffImageName, expectImageDir, reportImageDir };
}
async function getExpected(ctx, { imageName, actualImageName, expectImageName, diffImageName, expectImageDir, reportImageDir }) {
const onCompare = async (actual, expect, diff) => {
const imagesMeta = [];
const image = (ctx.images[imageName] = ctx.images[imageName] ?? { actual: actualImageName });
imagesMeta.push({ name: image.actual, data: actual });
if (diff && expect) {
image.expect = expectImageName;
image.diff = diffImageName;
imagesMeta.push({ name: image.expect, data: expect });
imagesMeta.push({ name: image.diff, data: diff });
}
ctx.attachments = await saveImages(reportImageDir, imagesMeta);
};
const expectImageStat = await getStat(path_1.default.join(expectImageDir, `${imageName}.png`));
if (!expectImageStat)
return { expected: null, onCompare };
const expected = await readExpected(expectImageDir, imageName);
return { expected, onCompare };
}
async function getOdiffExpected(ctx, actual, { imageName, actualImageName, expectImageName, diffImageName, expectImageDir, reportImageDir }) {
const expected = await readExpected(expectImageDir, imageName);
const image = (ctx.images[imageName] = ctx.images[imageName] ?? { actual: actualImageName });
image.expect = expectImageName;
image.diff = diffImageName;
const imagesMeta = [
{ name: image.actual, data: actual },
{ name: expectImageName, data: expected },
];
ctx.attachments = await saveImages(reportImageDir, imagesMeta);
return {
actual: path_1.default.join(reportImageDir, actualImageName),
expect: path_1.default.join(reportImageDir, expectImageName),
diff: path_1.default.join(reportImageDir, diffImageName),
};
}
function normalizeImageSize(image, width, height) {
const normalizedImage = Buffer.alloc(4 * width * height);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const i = (y * width + x) * 4;
if (x < image.width && y < image.height) {
const j = (y * image.width + x) * 4;
normalizedImage[i + 0] = image.data[j + 0];
normalizedImage[i + 1] = image.data[j + 1];
normalizedImage[i + 2] = image.data[j + 2];
normalizedImage[i + 3] = image.data[j + 3];
}
else {
normalizedImage[i + 0] = 0;
normalizedImage[i + 1] = 0;
normalizedImage[i + 2] = 0;
normalizedImage[i + 3] = 0;
}
}
}
return normalizedImage;
}
function hasDiffPixels(diff) {
for (let i = 0; i < diff.length; i += 4) {
if (diff[i + 0] == 255 && diff[i + 1] == 0 && diff[i + 2] == 0 && diff[i + 3] == 255)
return true;
}
return false;
}
function compareImages(expect, actual, pixelmatch, diffOptions) {
const expectImage = pngjs_1.PNG.sync.read(expect);
const actualImage = pngjs_1.PNG.sync.read(actual);
const width = Math.max(actualImage.width, expectImage.width);
const height = Math.max(actualImage.height, expectImage.height);
const diffImage = new pngjs_1.PNG({ width, height });
let actualImageData = actualImage.data;
if (actualImage.width < width || actualImage.height < height) {
actualImageData = normalizeImageSize(actualImage, width, height);
}
let expectImageData = expectImage.data;
if (expectImage.width < width || expectImage.height < height) {
expectImageData = normalizeImageSize(expectImage, width, height);
}
pixelmatch(expectImageData, actualImageData, diffImage.data, width, height, diffOptions);
return {
isEqual: !hasDiffPixels(diffImage.data),
diff: pngjs_1.PNG.sync.write(diffImage),
};
}
function getPixelmatchAssert(pixelmatch, ctx, config) {
return async function assertImagePixelmatch(actual, imageName) {
const { expected, onCompare } = await getExpected(ctx, await getImagePaths(config, ctx.testFullPath, imageName));
if (expected == null) {
await onCompare(actual);
return imageName ? `Expected image '${imageName}' does not exists` : 'Expected image does not exists';
}
if (actual.equals(expected)) {
if (!config.reportOnlyFailedTests) {
await onCompare(actual);
}
return;
}
const { isEqual, diff } = compareImages(expected, actual, pixelmatch, config.diffOptions);
if (isEqual) {
if (!config.reportOnlyFailedTests) {
await onCompare(actual);
}
return;
}
await onCompare(actual, expected, diff);
return imageName ? `Expected image '${imageName}' to match` : 'Expected image to match';
};
}
function getOdiffAssert(compare, ctx, config) {
const diffOptions = {
...config.odiffOptions,
noFailOnFsErrors: true,
};
return async function assertImage(image, imageName) {
const { actual, expect, diff } = await getOdiffExpected(ctx, image, await getImagePaths(config, ctx.testFullPath, imageName));
const result = await compare(actual, expect, diff, diffOptions);
if (!result.match) {
if (result.reason == 'file-not-exists') {
return imageName ? `Expected image '${imageName}' does not exists` : 'Expected image does not exists';
}
return imageName ? `Expected image '${imageName}' to match` : 'Expected image to match';
}
};
}
//# sourceMappingURL=compare.js.map