visreg-test
Version:
A visual regression testing solution that offers an easy setup with simple yet powerful customisation options, wrapped up in a convenient CLI runner to make assessing and accepting/rejecting diffs a breeze.
309 lines (308 loc) • 13.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.createPassingEndpointTestResult = exports.createFailingEndpointTestResult = exports.getUnchangedEndpoints = exports.getSkippedEndpoints = exports.parseAgenda = exports.parseCypressSummary = exports.includedInTarget = exports.isTargettedTest = exports.getFileType = exports.getDiffingFilesFromTestResult = exports.getAllDiffingFiles = exports.getFileNameWithoutExtension = exports.getSuites = exports.getDirectories = exports.getFilesInDir = exports.cleanUp = exports.removeBackups = exports.BACKUP_RECEIVED_DIR = exports.RECEIVED_DIR = exports.BACKUP_DIFF_DIR = exports.DIFF_DIR = exports.SUITE_SNAPS_DIR = exports.getHumanReadableFileSize = exports.getFileInfo = exports.printColorText = exports.removeDirIfEmpty = exports.createScaffold = exports.parseViewport = exports.hasFiles = exports.pathExists = exports.getSuiteDirOrFail = exports.suitesDirectory = exports.projectRoot = void 0;
const fs = require("fs");
const path = require("path");
const cli_1 = require("./cli");
exports.projectRoot = process.cwd();
exports.suitesDirectory = path.join(exports.projectRoot, 'suites');
const getSuiteDirOrFail = (suiteName) => {
if (!suiteName) {
return '';
}
const specDir = path.join(exports.suitesDirectory, suiteName);
if (!fs.existsSync(specDir)) {
(0, exports.printColorText)('No suite path - see README', '31');
process.exit(1);
}
const files = fs.readdirSync(specDir);
const snapsFile = files.find(file => file.startsWith('snaps'));
if (!snapsFile) {
(0, exports.printColorText)('No snap.js/ts file found - see README', '31');
process.exit(1);
}
return specDir;
};
exports.getSuiteDirOrFail = getSuiteDirOrFail;
const pathExists = (dirPath) => {
return fs.existsSync(dirPath);
};
exports.pathExists = pathExists;
const hasFiles = (dirPath) => fs.readdirSync(dirPath).length > 0;
exports.hasFiles = hasFiles;
const parseViewport = (viewport) => {
const stringedViewport = viewport.toString();
if (!(stringedViewport === null || stringedViewport === void 0 ? void 0 : stringedViewport.includes(','))) {
return viewport;
}
return stringedViewport.split(',').map((pixels) => parseInt(pixels));
};
exports.parseViewport = parseViewport;
const createScaffold = () => {
const typescript = cli_1.program.opts().scaffoldTs;
const scaffoldRoot = path.join(__dirname, 'scaffold');
const fileName = typescript ? 'snaps.ts' : 'snaps.js';
const source = path.join(scaffoldRoot, fileName);
if (!(0, exports.pathExists)(exports.suitesDirectory)) {
fs.mkdirSync(exports.suitesDirectory);
}
const destination = path.join(exports.suitesDirectory, 'test-suite');
if (!(0, exports.pathExists)(destination)) {
fs.mkdirSync(destination);
}
fs.copyFileSync(source, path.join(destination, fileName));
if (typescript) {
fs.copyFileSync(path.join(scaffoldRoot, 'tsconfig-scaffold.json'), path.join(exports.projectRoot, 'tsconfig.json'));
}
};
exports.createScaffold = createScaffold;
const removeDirIfEmpty = (dirPath) => {
if (!(0, exports.pathExists)(dirPath) || (0, exports.hasFiles)(dirPath)) {
return;
}
try {
fs.rmSync(dirPath, { recursive: true });
}
catch (err) {
console.error(err);
}
};
exports.removeDirIfEmpty = removeDirIfEmpty;
const printColorText = (text, colorCode) => {
console.log(`\x1b[${colorCode}m${text}\x1b[0m`);
};
exports.printColorText = printColorText;
const getFileInfo = (filePath) => {
const stats = fs.statSync(filePath);
return {
createdAt: stats.birthtime,
modifiedAt: stats.mtime,
sizeInBytes: stats.size
};
};
exports.getFileInfo = getFileInfo;
const getHumanReadableFileSize = (filePath) => {
const { sizeInBytes } = (0, exports.getFileInfo)(filePath);
const fileSizeInMegabytes = sizeInBytes / 1000000.0;
return fileSizeInMegabytes.toFixed(2) + 'MB';
};
exports.getHumanReadableFileSize = getHumanReadableFileSize;
const SUITE_SNAPS_DIR = () => path.join(exports.suitesDirectory, cli_1.programChoices.suite || '', 'snapshots', 'snaps');
exports.SUITE_SNAPS_DIR = SUITE_SNAPS_DIR;
const DIFF_DIR = () => path.join((0, exports.SUITE_SNAPS_DIR)(), '__diff_output__');
exports.DIFF_DIR = DIFF_DIR;
const BACKUP_DIFF_DIR = () => path.join((0, exports.SUITE_SNAPS_DIR)(), 'backup-diffs');
exports.BACKUP_DIFF_DIR = BACKUP_DIFF_DIR;
const RECEIVED_DIR = () => path.join((0, exports.SUITE_SNAPS_DIR)(), '__received_output__');
exports.RECEIVED_DIR = RECEIVED_DIR;
const BACKUP_RECEIVED_DIR = () => path.join((0, exports.SUITE_SNAPS_DIR)(), 'backup-received');
exports.BACKUP_RECEIVED_DIR = BACKUP_RECEIVED_DIR;
const removeBackups = () => {
(0, exports.pathExists)((0, exports.BACKUP_DIFF_DIR)()) && fs.rmSync((0, exports.BACKUP_DIFF_DIR)(), { recursive: true });
(0, exports.pathExists)((0, exports.BACKUP_RECEIVED_DIR)()) && fs.rmSync((0, exports.BACKUP_RECEIVED_DIR)(), { recursive: true });
};
exports.removeBackups = removeBackups;
const cleanUp = () => {
(0, exports.removeBackups)();
(0, exports.removeDirIfEmpty)((0, exports.DIFF_DIR)());
(0, exports.removeDirIfEmpty)((0, exports.RECEIVED_DIR)());
};
exports.cleanUp = cleanUp;
const getFilesInDir = (dirPath) => {
if (!fs.existsSync(dirPath))
return [];
return fs.readdirSync(dirPath)
.filter((item) => item !== '.DS_Store')
.filter((item) => {
return fs.statSync(path.join(dirPath, item)).isFile();
});
};
exports.getFilesInDir = getFilesInDir;
// Function to get directories
const getDirectories = (source) => {
if (!fs.existsSync(source))
return [];
return fs.readdirSync(source, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);
};
exports.getDirectories = getDirectories;
const getSuites = () => {
const configPath = path.join(exports.projectRoot, 'visreg.config.json');
let visregConfig = {};
if ((0, exports.pathExists)(configPath)) {
const fileContent = fs.readFileSync(configPath, 'utf-8');
try {
visregConfig = JSON.parse(fileContent);
}
catch (e) { }
}
let ignoreDirectories = [];
if (visregConfig.ignoreDirectories) {
ignoreDirectories.push(...visregConfig.ignoreDirectories);
}
const suites = (0, exports.getDirectories)(exports.suitesDirectory)
.filter(dirName => !ignoreDirectories.includes(dirName))
.filter(dirName => {
const fileName = 'snaps';
return (fs.existsSync(path.join(exports.suitesDirectory, dirName, fileName + '.js')) ||
fs.existsSync(path.join(exports.suitesDirectory, dirName, fileName + '.ts')));
});
return suites;
};
exports.getSuites = getSuites;
const getFileNameWithoutExtension = (fileName) => {
const cleanName = fileName.replace(/(-received|.diff|.base)?\.png$/, '');
return cleanName;
};
exports.getFileNameWithoutExtension = getFileNameWithoutExtension;
const getAllDiffingFiles = () => {
const allFiles = (0, exports.getFilesInDir)((0, exports.DIFF_DIR)());
const allDiffingFiles = allFiles.filter(file => file.endsWith('.diff.png'));
return allDiffingFiles;
};
exports.getAllDiffingFiles = getAllDiffingFiles;
const getDiffingFilesFromTestResult = () => {
if (!(0, exports.pathExists)((0, exports.DIFF_DIR)()))
return [];
return fs.readdirSync((0, exports.DIFF_DIR)())
.filter(file => {
if ((0, exports.isTargettedTest)()) {
return (0, exports.includedInTarget)(file);
}
return true;
})
.filter(file => file.endsWith('.diff.png'));
};
exports.getDiffingFilesFromTestResult = getDiffingFilesFromTestResult;
const getFileType = (fileName) => {
if (fileName.endsWith('.diff.png')) {
return 'diff';
}
if (fileName.endsWith('-received.png')) {
return 'received';
}
if (fileName.endsWith('.base.png')) {
return 'baseline';
}
throw new Error('Unknown file type');
};
exports.getFileType = getFileType;
const isTargettedTest = () => !!(cli_1.programChoices.targetViewports.length || cli_1.programChoices.targetEndpointTitles.length);
exports.isTargettedTest = isTargettedTest;
const includedInTarget = (fileName) => {
let viewportScreeningPassed = true;
let endpointScreeningPassed = true;
const { targetViewports, targetEndpointTitles } = cli_1.programChoices;
if (targetViewports.length) {
viewportScreeningPassed = !!targetViewports.find(targetVp => {
return fileName.includes(targetVp.toString());
});
}
if (targetEndpointTitles.length) {
endpointScreeningPassed = !!targetEndpointTitles.find(targetEp => {
const lowercaseTargetEp = targetEp.toLowerCase().replace(/ /g, '-');
const filenameLowercaseAndReplacedSpaces = fileName.toLowerCase().replace(/ /g, '-');
return filenameLowercaseAndReplacedSpaces.includes(lowercaseTargetEp);
});
}
return viewportScreeningPassed && endpointScreeningPassed;
};
exports.includedInTarget = includedInTarget;
const parseCypressSummary = (data) => {
const summaryObject = {};
data.split('\n').slice(1, -1).forEach((line) => {
var _a, _b;
const text = (_a = line.match(/[a-zA-Z]+/)) === null || _a === void 0 ? void 0 : _a[0];
const number = (_b = line.match(/\d+/)) === null || _b === void 0 ? void 0 : _b[0];
if (text && number) {
summaryObject[text.toLowerCase()] = parseInt(number);
}
});
return summaryObject;
};
exports.parseCypressSummary = parseCypressSummary;
const parseAgenda = (data) => {
const agendaData = JSON.parse(data);
const parsedAgenda = [];
agendaData.endpointsToTest.forEach(endpoint => {
agendaData.viewportsToTest.forEach(viewport => {
parsedAgenda.push(`${endpoint.title} @ ${viewport.toString()}`);
});
});
return parsedAgenda;
};
exports.parseAgenda = parseAgenda;
const getSkippedEndpoints = (endpointTestResults, testAgenda) => {
const skipped = testAgenda
.filter(endpoint => {
const passed = endpointTestResults.passing.find(e => e.testTitle === endpoint);
const failed = endpointTestResults.failing.find(e => e.testTitle === endpoint);
const tested = passed || failed;
if (!tested)
return true;
})
.map(endpoint => {
const [endpointTitle, viewport] = endpoint.split(' @ ');
const skippedEndpoint = {
testTitle: endpoint,
endpointTitle,
viewport,
};
return skippedEndpoint;
});
return skipped;
};
exports.getSkippedEndpoints = getSkippedEndpoints;
const getUnchangedEndpoints = (endpointTestResults) => {
const diffList = (0, exports.getDiffingFilesFromTestResult)();
const unchanged = endpointTestResults.passing.filter(endpoint => {
return !diffList.some(diffTitle => {
return endpoint.testTitle === diffTitle.replace('.diff.png', '');
});
});
return unchanged;
};
exports.getUnchangedEndpoints = getUnchangedEndpoints;
const createFailingEndpointTestResult = (payload, errorSignature) => {
const errorMessages = payload.split(errorSignature);
let userTerminated = false;
const failingEndpoints = [];
for (const [index, message] of errorMessages.entries()) {
if (index === 0 || index % 2 !== 0)
continue;
const splitIndex = message.indexOf(':');
const roughTestTitle = message.substring(0, splitIndex).trim();
const testTitle = roughTestTitle.substring(roughTestTitle.lastIndexOf('\n')).trim();
// This Cypress error message occurs when the user terminates the running test, so it's not a real error
if (testTitle.match(/"before each" hook/)) {
userTerminated = true;
return { userTerminated, failingEndpoints };
}
const atSymbolIndex = testTitle.indexOf('@');
const endpointTitle = testTitle.substring(0, atSymbolIndex).trim();
const viewport = testTitle.substring(atSymbolIndex + 1).trim();
const errorMessage = message.substring(splitIndex + 1).trim();
const failedEndpoint = {
testTitle,
errorMessage,
endpointTitle,
viewport,
};
failingEndpoints.push(failedEndpoint);
}
return { userTerminated, failingEndpoints };
};
exports.createFailingEndpointTestResult = createFailingEndpointTestResult;
const createPassingEndpointTestResult = (payload) => {
// E.g. "✓ Start @ samsung-s10 (6090ms)"
const testTitle = payload.replace(/✓| \(\d+ms\)/g, '').trim();
const [endpointTitle, viewport] = testTitle.split(' @ ');
const passedEndpoint = {
testTitle,
endpointTitle,
viewport,
};
return passedEndpoint;
};
exports.createPassingEndpointTestResult = createPassingEndpointTestResult;