UNPKG

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
"use strict"; 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;