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.
875 lines (874 loc) • 36.8 kB
JavaScript
;
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.startWebTest = exports.startWebSuiteQueue = exports.getDiffsForWeb = exports.getVersion = void 0;
const fs = require("fs");
const path = require("path");
const child_process_1 = require("child_process");
const readline = require("readline");
const cli_1 = require("./cli");
const server_1 = require("./server");
const diff_assessment_web_1 = require("./diff-assessment-web");
const utils_1 = require("./utils");
const diff_assessment_terminal_1 = require("./diff-assessment-terminal");
const summarize_1 = require("./summarize");
const defaultBrowser = 'electron';
const configPath = path.join(utils_1.projectRoot, 'visreg.config.json');
// Store the project-level config immutably so it can be cloned for each suite run
let projectLevelConfig = {};
if ((0, utils_1.pathExists)(configPath)) {
const fileContent = fs.readFileSync(configPath, 'utf-8');
try {
projectLevelConfig = JSON.parse(fileContent);
}
catch (e) { }
}
// Working config that gets merged with suite-level config per run. Reset from projectLevelConfig between suite runs.
let visregConfig = Object.assign({}, projectLevelConfig);
let failed = false;
let userTerminatedTest = false;
let cypressSummary = {};
let testAgenda = [];
const endpointTestResults = {
passing: [],
failing: [],
skipped: [],
unchanged: [],
};
const typesList = [
{
name: 'Full',
slug: 'full-test',
description: 'Run a full visual regression test of all endpoints and viewports (previous diffs are deleted)'
},
{
name: 'Retest diffs only',
slug: 'diffs-only',
description: 'Run only the tests which failed in the last run'
},
{
name: 'Targetted',
slug: 'targetted',
description: 'Run a test for a specific endpoint and/or viewport'
},
{
name: 'Assess diffs',
slug: 'assess-existing-diffs',
description: 'Assess the existing diffs (no tests are run)'
},
{
name: 'Lab',
slug: 'lab',
description: 'Run the tests in lab mode'
},
];
const getVersion = () => {
// Read the "version" field from package.json and print it out:
const packageJsonPath = path.join(__dirname, '..', 'package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8') || '{}');
return packageJson.version;
};
exports.getVersion = getVersion;
// Print config path
if ((0, utils_1.pathExists)(configPath)) {
(0, utils_1.printColorText)(`\nProject config: ${configPath}`, '2');
}
// Print header
(0, utils_1.printColorText)(`\n _ _ __ ____ ____ ____ ___ \n/ )( \\( )/ ___)( _ \\( __)/ __)\n\\ \\/ / )( \\___ \\ ) / ) _)( (_ \\ \n \\__/ (__)(____/(__\\_)(____)\\___/ \x1b[2mv${(0, exports.getVersion)()}\x1b[0m\n`, '36;1');
const promptForEndpointTitle = () => __awaiter(void 0, void 0, void 0, function* () {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
yield new Promise((resolve) => {
if (cli_1.programChoices.targetEndpointTitles.length)
resolve();
rl.question('Enter endpoint title (replace spaces with "-"): ', (endpointTitle) => {
cli_1.programChoices.targetEndpointTitles = [endpointTitle];
resolve();
});
});
rl.close();
});
const promptForViewport = () => __awaiter(void 0, void 0, void 0, function* () {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
yield new Promise((resolve) => {
if (cli_1.programChoices.targetViewports.length)
resolve();
rl.question('Enter viewport (e.g. 1920,1080, or iphone-6): ', (viewport) => {
const parsedViewport = (0, utils_1.parseViewport)(viewport) || [];
cli_1.programChoices.targetViewports = [parsedViewport];
resolve();
});
});
rl.close();
});
const startLabMode = (programChoices) => __awaiter(void 0, void 0, void 0, function* () {
const { targetEndpointTitles, targetViewports } = programChoices;
const requiredTargets = targetEndpointTitles.length && targetViewports.length;
if (!requiredTargets) {
(0, utils_1.printColorText)('Lab mode requires both endpoint title and viewport to be specified\n', '2');
yield promptForEndpointTitle();
yield promptForViewport();
if (!requiredTargets) {
(0, utils_1.printColorText)('Lab mode requires both endpoint title and viewport to be specified\n', '31');
return;
}
}
yield runCypressTest();
if (programChoices.gui) {
process.exit();
}
(0, utils_1.printColorText)(`Lab mode summary\n`, '4');
(0, utils_1.printColorText)(`Duration: ${cypressSummary.duration} seconds`, '2');
(0, utils_1.printColorText)('GUI: off', '2');
(0, utils_1.printColorText)(`Snapshots: ${programChoices.snap ? 'on' : 'off'}`, '2');
(0, utils_1.printColorText)(`\n(Tip: use the GUI for hot-reloading!)\n`, '2');
process.exit();
});
const main = () => __awaiter(void 0, void 0, void 0, function* () {
yield selectSuites();
yield selectType();
const { testType } = cli_1.programChoices;
if (testType === 'lab') {
startLabMode(cli_1.programChoices);
return;
}
if (testType === 'assess-existing-diffs') {
assessExistingDiffImages((0, utils_1.getAllDiffingFiles)());
return;
}
// Queue mode: run multiple suites sequentially
if (isQueueMode()) {
yield runSuiteQueue();
return;
}
// Single-suite mode (original behavior)
if (testType === 'diffs-only') {
exitIfNoDIffs();
}
const allCurrentDiffs = createTemporaryDiffList();
backupDiffs();
backupReceived();
const testResultDiffs = yield runCypressTest(allCurrentDiffs);
if (testResultDiffs) {
assessExistingDiffImages(testResultDiffs);
}
return;
});
const selectSuite = () => __awaiter(void 0, void 0, void 0, function* () {
const suites = (0, utils_1.getSuites)();
if (suites.length === 0) {
(0, utils_1.printColorText)('No test suites found - see README', '31');
process.exit(1);
}
if (suites.length === 1) {
cli_1.programChoices.suite = suites[0];
return;
}
if (cli_1.programChoices.suite && suites.includes(cli_1.programChoices.suite)) {
return;
}
console.log('Select suite:\n');
suites.forEach((suite, index) => {
console.log(`${index + 1} ${suite}`);
});
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
yield new Promise((resolve) => {
rl.question('\nEnter number of suite: ', (targetNum) => {
cli_1.programChoices.suite = suites[parseInt(targetNum) - 1];
resolve();
});
});
rl.close();
});
const isQueueMode = () => {
return !!(cli_1.programChoices.allSuites || (cli_1.programChoices.suites && cli_1.programChoices.suites.length > 1));
};
/**
* Resolve the suite queue. Populates programChoices.suites with the list of suites to run.
* In single-suite mode, wraps the selected suite in an array for uniform handling.
*/
const selectSuites = () => __awaiter(void 0, void 0, void 0, function* () {
const allSuites = (0, utils_1.getSuites)();
if (allSuites.length === 0) {
(0, utils_1.printColorText)('No test suites found - see README', '31');
process.exit(1);
}
// --all-suites flag: run all discovered suites
if (cli_1.programChoices.allSuites) {
cli_1.programChoices.suites = allSuites;
return;
}
// --suites flag: validate provided suite names
if (cli_1.programChoices.suites && cli_1.programChoices.suites.length > 0) {
const invalid = cli_1.programChoices.suites.filter(s => !allSuites.includes(s));
if (invalid.length > 0) {
(0, utils_1.printColorText)(`Unknown suite(s): ${invalid.join(', ')}`, '31');
(0, utils_1.printColorText)(`Available suites: ${allSuites.join(', ')}`, '2');
process.exit(1);
}
return;
}
// Fall back to single-suite selection (existing behavior)
yield selectSuite();
cli_1.programChoices.suites = cli_1.programChoices.suite ? [cli_1.programChoices.suite] : [];
});
/**
* Run multiple suites sequentially, accumulating diffs for assessment at the end.
*/
const runSuiteQueue = () => __awaiter(void 0, void 0, void 0, function* () {
const suites = cli_1.programChoices.suites || [];
const suiteResults = [];
(0, utils_1.printColorText)(`\nQueue: running ${suites.length} suite(s) sequentially\n`, '36');
for (let i = 0; i < suites.length; i++) {
const suite = suites[i];
resetForNextSuite();
cli_1.programChoices.suite = suite;
(0, utils_1.printColorText)(`\n${'─'.repeat(60)}`, '36');
(0, utils_1.printColorText)(`Suite ${i + 1}/${suites.length}: ${suite}`, '36');
(0, utils_1.printColorText)(`${'─'.repeat(60)}\n`, '36');
if (cli_1.programChoices.testType === 'diffs-only') {
if (!(0, utils_1.pathExists)((0, utils_1.DIFF_DIR)()) || !(0, utils_1.hasFiles)((0, utils_1.DIFF_DIR)())) {
(0, utils_1.printColorText)(`No diffs found for suite "${suite}", skipping`, '2');
suiteResults.push({ suite, diffs: [], failed: false });
continue;
}
}
const allCurrentDiffs = createTemporaryDiffList();
backupDiffs();
backupReceived();
try {
const testResultDiffs = yield runCypressTest(allCurrentDiffs);
suiteResults.push({
suite,
diffs: testResultDiffs || [],
failed,
});
}
catch (error) {
(0, utils_1.printColorText)(`Error running suite "${suite}": ${error}`, '31');
suiteResults.push({ suite, diffs: [], failed: true });
}
}
// Print combined queue summary
(0, summarize_1.summarizeSuiteQueue)(suiteResults);
// Collect all diffs across suites for assessment
const allDiffs = suiteResults.flatMap(r => r.diffs);
const anyFailed = suiteResults.some(r => r.failed);
if (allDiffs.length > 0) {
// Set suite to the first suite with diffs for the assessment UI context
const firstSuiteWithDiffs = suiteResults.find(r => r.diffs.length > 0);
if (firstSuiteWithDiffs) {
cli_1.programChoices.suite = firstSuiteWithDiffs.suite;
}
assessExistingDiffImages(allDiffs);
}
else {
(0, summarize_1.summarizeResultsAndQuit)([], [], anyFailed);
}
});
const selectType = () => __awaiter(void 0, void 0, void 0, function* () {
const specifiedType = typesList.find(type => type.slug === cli_1.programChoices.testType);
if (specifiedType) {
return;
}
console.log('\nSelect type of test:\n');
typesList.forEach((type, index) => {
if (cli_1.programChoices.containerized) {
if (type.slug === 'lab' || type.slug === 'assess-existing-diffs')
return;
}
(0, utils_1.printColorText)(`${index + 1} ${type.name}\x1b[2m - ${type.description}\x1b[0m`, '0');
});
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
yield new Promise((resolve) => {
rl.question('\nEnter number of type: ', (id) => {
const slug = typesList[parseInt(id) - 1].slug;
cli_1.programChoices.testType = slug;
resolve();
});
});
rl.close();
});
const prepareConfig = (diffList) => {
const suiteRoot = path.join(utils_1.suitesDirectory, cli_1.programChoices.suite || '');
const suiteConfigPath = path.join(suiteRoot, 'visreg.config.json');
let suiteConfig = {};
if ((0, utils_1.pathExists)(suiteConfigPath)) {
const fileContent = fs.readFileSync(suiteConfigPath, 'utf-8');
try {
suiteConfig = JSON.parse(fileContent);
}
catch (e) { }
}
(0, utils_1.pathExists)(suiteConfigPath) && (0, utils_1.printColorText)(`\nSuite config: ${suiteConfigPath}`, '2');
// Merge project config with suite config without mutating the project-level config
visregConfig = Object.assign(Object.assign({}, projectLevelConfig), suiteConfig);
const { screenshotOptions, comparisonOptions, requestOptions, visitOptions, maxViewport, browser, } = visregConfig;
const snapshotSettings = Object.assign(Object.assign({ failureThreshold: 0.001, failureThresholdType: 'percent', capture: 'fullPage', disableTimersAndAnimations: false }, screenshotOptions), comparisonOptions);
const visitSettings = Object.assign({ scrollDuration: 750, devicePixelRatio: 1, waitForNetworkIdle: true }, visitOptions);
const requestSettings = Object.assign({}, requestOptions);
process.env.CYPRESS_VISREG_SETTINGS = JSON.stringify({ maxViewport });
process.env.CYPRESS_SNAPSHOT_SETTINGS = JSON.stringify(snapshotSettings);
process.env.CYPRESS_VISIT_SETTINGS = JSON.stringify(visitSettings);
process.env.CYPRESS_REQUEST_SETTINGS = JSON.stringify(requestSettings);
process.env.SEND_SUITE_CONF = 'false';
const specPath = (0, utils_1.getSuiteDirOrFail)(cli_1.programChoices.suite);
const testSettings = {
testType: cli_1.programChoices.testType,
suite: cli_1.programChoices.suite,
diffList: diffList || [],
targetViewports: cli_1.programChoices.targetViewports,
targetEndpointTitles: cli_1.programChoices.targetEndpointTitles,
noSnap: !cli_1.programChoices.snap,
};
const labMode = cli_1.programChoices.testType === 'lab';
const nonOverridableSettings = {
suitesDirectory: utils_1.suitesDirectory,
useRelativeSnapshotsDir: true,
storeReceivedOnFailure: true,
snapFilenameExtension: labMode ? '.lab' : '.base',
customSnapshotsDir: labMode ? 'lab' : '',
};
process.env.CYPRESS_failOnSnapshotDiff = 'false';
process.env.CYPRESS_updateSnapshots = labMode ? 'true' : 'false';
process.env.CYPRESS_TEST_SETTINGS = Buffer.from(JSON.stringify(testSettings)).toString('base64');
process.env.CYPRESS_NON_OVERRIDABLE_SETTINGS = JSON.stringify(nonOverridableSettings);
return {
specPath,
browser,
};
};
const getInitMessage = (labModeOn, browser) => {
let labModeText = '- lab mode';
labModeText += (cli_1.programChoices === null || cli_1.programChoices === void 0 ? void 0 : cli_1.programChoices.gui) ? ' (GUI)' : '';
labModeText += !(cli_1.programChoices === null || cli_1.programChoices === void 0 ? void 0 : cli_1.programChoices.snap) ? ' (no snapshot)' : '';
if (labModeOn) {
return `Starting Cypress ${labModeText}`;
}
return `Starting Cypress (${browser || defaultBrowser})`;
};
const runCypressTest = (diffList) => __awaiter(void 0, void 0, void 0, function* () {
return new Promise((resolve) => {
var _a;
const conf = prepareConfig(diffList);
process.chdir(__dirname);
const labModeOn = cli_1.programChoices.testType === 'lab';
const message = getInitMessage(labModeOn, conf.browser);
(0, utils_1.printColorText)(`\n${message}\n`, '2');
let cypressCommand;
if (labModeOn && cli_1.programChoices.gui) {
cypressCommand = 'npx cypress open --env HEADED=true';
}
else {
cypressCommand = `npx cypress run --env CLI=true --spec "${conf.specPath}" ${conf.browser ? `--browser ${conf.browser}` : defaultBrowser}`;
}
const parts = cypressCommand.split(' ');
const command = parts[0];
const args = parts.slice(1);
const child = (0, child_process_1.spawn)(`DEBUG=cypress ${command}`, args, { shell: true });
(_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (data) => onTerminalDataOut(data, diffList));
child.on('error', (error) => console.error(`exec error: ${error}`));
child.on('close', (code) => {
if (labModeOn) {
resolve();
return;
}
const testDiffList = onTerminalCypressClose();
resolve(testDiffList);
});
});
});
const onTerminalDataOut = (data, diffList) => {
const dataString = data.toString();
if (dataString.includes('✓')) {
const passing = (0, utils_1.createPassingEndpointTestResult)(dataString);
endpointTestResults.passing.push(passing);
(0, utils_1.printColorText)(`${dataString}`, '32');
return;
}
const failedRegexp = new RegExp(`(\\d+\\)\\s*${cli_1.programChoices.suite})`, 'g'); // e.g. 1) suiteSlug
if (dataString.match(failedRegexp)) {
const { userTerminated, failingEndpoints } = (0, utils_1.createFailingEndpointTestResult)(dataString, failedRegexp);
userTerminatedTest = userTerminated;
endpointTestResults.failing = [...endpointTestResults.failing, ...failingEndpoints];
(0, utils_1.printColorText)(`${dataString}`, '31');
return;
}
const failedInline = new RegExp(`(\\d+\\)\\s*[\\w\\s-]+\\s*\\@\\s*[\\w\\s-]+)`, 'g'); // e.g. 1) Start page @ samsung-s10
if (dataString.match(failedInline)) {
(0, utils_1.printColorText)(`${dataString}`, '31');
return;
}
if (dataString.includes('visreg-test-agenda')) {
if (!testAgenda.length) {
if (cli_1.programChoices.testType === 'diffs-only') {
testAgenda = (diffList === null || diffList === void 0 ? void 0 : diffList.map(diff => diff.replace('.diff.png', ''))) || [];
return;
}
testAgenda = (0, utils_1.parseAgenda)(dataString);
}
return;
}
if (dataString.includes('Spec Ran')) {
cypressSummary = (0, utils_1.parseCypressSummary)(dataString);
// console.log(`${data}`)
(0, utils_1.printColorText)(`${dataString}`, '2');
return;
}
if (dataString.match(/Spec\s+Tests\s+Passing\s+Failing\s+Pending\s+Skipped/)) {
// We don't want to show Cypress' summary because we interpret and summarize the results ourselves
return;
}
console.log(`${dataString}`);
};
const onTerminalCypressClose = () => {
endpointTestResults.skipped = (0, utils_1.getSkippedEndpoints)(endpointTestResults, testAgenda);
endpointTestResults.unchanged = (0, utils_1.getUnchangedEndpoints)(endpointTestResults);
const testDiffList = (0, utils_1.getDiffingFilesFromTestResult)();
restoreBackups();
const allDiffList = (0, utils_1.getAllDiffingFiles)();
const summary = {
name: 'visreg-summary',
testType: cli_1.programChoices.testType,
testDiffList,
allDiffList,
userTerminatedTest,
endpointTestResults,
programChoices: cli_1.programChoices,
cypressSummary,
testAgenda,
createdAt: new Date(),
terminated: userTerminatedTest
};
(0, utils_1.printColorText)(`\nTest duration: ${cypressSummary.duration} seconds\n`, '2');
(0, utils_1.printColorText)('\nTested:', '2');
summary.testAgenda.forEach(testTitle => {
(0, utils_1.printColorText)(`${testTitle}`, '0');
});
if (summary.endpointTestResults.failing.length) {
(0, utils_1.printColorText)('\nError:', '2');
summary.endpointTestResults.failing.forEach(endpoint => {
(0, utils_1.printColorText)(`${endpoint.testTitle}`, '31');
});
}
if (testDiffList.length) {
(0, utils_1.printColorText)('\nDiffs:', '2');
testDiffList.forEach(diff => {
(0, utils_1.printColorText)(`${diff}`, '33');
});
}
if (summary.endpointTestResults.unchanged.length) {
(0, utils_1.printColorText)('\nNo change:', '2');
summary.endpointTestResults.unchanged.forEach(endpoint => {
(0, utils_1.printColorText)(`${endpoint.testTitle}`, '32');
});
}
return testDiffList;
};
const onDataOut = (data, ws, diffList) => {
// Remove ASCII escape codes
const dataString = data
.toString()
.replace(/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]/g, '');
const dataPackage = {
name: '',
type: 'text',
stdout: dataString,
color: ''
};
if (dataString.includes('✓')) {
const passing = (0, utils_1.createPassingEndpointTestResult)(dataString);
endpointTestResults.passing.push(passing);
dataPackage.color = '#749C75';
ws.send(JSON.stringify(dataPackage));
return;
}
const failedRegexp = new RegExp(`(\\d+\\)\\s*Suite: "${cli_1.programChoices.suite}")`, 'g'); // e.g. 1) Suite: "suiteSlug"
if (dataString.match(failedRegexp)) {
const { userTerminated, failingEndpoints } = (0, utils_1.createFailingEndpointTestResult)(dataString, failedRegexp);
userTerminatedTest = userTerminated;
endpointTestResults.failing = [...endpointTestResults.failing, ...failingEndpoints];
return;
}
const failedInline = /^\s*(\d+\)\s*[\w\s-]+\s*\@\s*[\w\s-]+)/; // e.g. 1) Start page @ samsung-s10
if (dataString.match(failedInline)) {
dataPackage.color = '#FE4A49';
ws.send(JSON.stringify(dataPackage));
return;
}
if (dataString.includes('visreg-test-agenda')) {
if (!testAgenda.length) {
if (cli_1.programChoices.testType === 'diffs-only') {
testAgenda = (diffList === null || diffList === void 0 ? void 0 : diffList.map(diff => diff.replace('.diff.png', ''))) || [];
return;
}
testAgenda = (0, utils_1.parseAgenda)(dataString);
}
return;
}
if (dataString.includes('Spec Ran')) {
cypressSummary = (0, utils_1.parseCypressSummary)(dataString);
return;
}
if (dataString.match(/(Results)/)) {
return;
}
if (dataString.match(/Spec\s+Tests\s+Passing\s+Failing\s+Pending\s+Skipped/)) {
// We don't want to show Cypress' summary because we interpret and summarize the results ourselves
return;
}
ws.send(JSON.stringify(dataPackage));
};
const onErrOut = (data, ws) => {
if (data.includes('DevTools listening on ws://127.0.0.1')
|| data.includes('NSApplicationDelegate'))
return;
ws.send(JSON.stringify({ type: 'error', payload: `${data}` }));
};
const onCypressClose = (ws, resolve) => {
endpointTestResults.skipped = (0, utils_1.getSkippedEndpoints)(endpointTestResults, testAgenda);
endpointTestResults.unchanged = (0, utils_1.getUnchangedEndpoints)(endpointTestResults);
const testDiffList = (0, utils_1.getDiffingFilesFromTestResult)();
restoreBackups();
const allDiffList = (0, utils_1.getAllDiffingFiles)();
const summary = JSON.stringify({
type: 'data',
payload: {
name: 'visreg-summary',
testType: cli_1.programChoices.testType,
testDiffList,
allDiffList,
userTerminatedTest,
endpointTestResults,
programChoices: cli_1.programChoices,
cypressSummary,
testAgenda,
createdAt: new Date(),
terminated: userTerminatedTest,
}
});
(0, utils_1.printColorText)(`Test complete\n`, '2');
ws.send(summary);
resolve();
return;
};
const runWebCypressTest = (ws, diffList) => __awaiter(void 0, void 0, void 0, function* () {
return new Promise((resolve, reject) => {
var _a, _b;
const conf = prepareConfig(diffList);
process.chdir(__dirname);
const initMessage = `Starting Cypress (${conf.browser || defaultBrowser})`;
(0, utils_1.printColorText)(`\n${initMessage}\n`, '2');
const initMessagePackage = {
name: '',
type: 'text',
stdout: initMessage,
color: '#7D7D7D'
};
ws.send(JSON.stringify(initMessagePackage));
let cypressCommand;
cypressCommand = `npx cypress run --spec "${conf.specPath}" ${conf.browser ? `--browser ${conf.browser}` : defaultBrowser}`;
const parts = cypressCommand.split(' ');
const command = parts[0];
const args = parts.slice(1);
const child = (0, child_process_1.spawn)(`DEBUG=cypress ${command}`, args, { shell: true });
(_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (data) => onDataOut(data, ws, diffList));
(_b = child.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (data) => onErrOut(data, ws));
child.on('error', (error) => console.log(`exec error: ${error}`));
child.on('close', (code) => onCypressClose(ws, resolve));
});
});
const isNotTargetOfTest = (fileName) => {
const res = !(0, utils_1.includedInTarget)(fileName);
return res;
};
const couldNotTest = (fileName, notTested) => {
if (notTested.length === 0)
return false;
const untested = notTested.some(endpoint => (fileName.includes(endpoint.endpointTitle) && fileName.includes(endpoint.viewport)));
return untested;
};
const restoreBackups = () => {
const backupDiffDir = (0, utils_1.BACKUP_DIFF_DIR)();
const backupReceivedDir = (0, utils_1.BACKUP_RECEIVED_DIR)();
const skipped = (0, utils_1.getSkippedEndpoints)(endpointTestResults, testAgenda);
const notTested = [...endpointTestResults.failing, ...skipped];
const restoreCondition = (fileName) => (isNotTargetOfTest(fileName) || couldNotTest(fileName, notTested));
if ((0, utils_1.pathExists)(backupDiffDir)) {
fs.readdirSync(backupDiffDir)
.filter(restoreCondition)
.forEach(fileName => {
fs.renameSync(path.join(backupDiffDir, fileName), path.join((0, utils_1.DIFF_DIR)(), fileName));
});
}
if ((0, utils_1.pathExists)(backupReceivedDir)) {
fs.readdirSync(backupReceivedDir)
.filter(restoreCondition)
.forEach(fileName => {
fs.renameSync(path.join(backupReceivedDir, fileName), path.join((0, utils_1.RECEIVED_DIR)(), fileName));
});
}
(0, utils_1.cleanUp)();
};
const exitIfNoDIffs = () => {
if (cli_1.programChoices.testType === 'lab') {
process.exit();
}
if (!(0, utils_1.pathExists)((0, utils_1.DIFF_DIR)()) || !(0, utils_1.hasFiles)((0, utils_1.DIFF_DIR)())) {
(0, summarize_1.summarizeResultsAndQuit)([], [], failed);
process.exit();
}
};
const getDiffsForWeb = (suiteSlug, conf) => {
cli_1.programChoices.suite = suiteSlug;
cli_1.programChoices.testType = 'assess-existing-diffs';
cli_1.programChoices.webTesting = true;
visregConfig = conf || Object.assign({}, projectLevelConfig);
let files = (0, utils_1.getDiffingFilesFromTestResult)();
const diffFiles = files.map((file, index) => {
return (0, diff_assessment_web_1.processImageViaWeb)(file, index, files.length, suiteSlug);
});
return diffFiles;
};
exports.getDiffsForWeb = getDiffsForWeb;
const resetPreviousTest = () => {
testAgenda = [];
failed = false;
cypressSummary = {};
userTerminatedTest = false;
cli_1.programChoices.targetViewports = [];
cli_1.programChoices.targetEndpointTitles = [];
endpointTestResults.passing = [];
endpointTestResults.failing = [];
endpointTestResults.skipped = [];
endpointTestResults.unchanged = [];
};
/**
* More thorough reset for multi-suite queue runs.
* Resets all module-level state that could bleed between suite runs.
*/
const resetForNextSuite = () => {
resetPreviousTest();
// Reset working config to project-level defaults
visregConfig = Object.assign({}, projectLevelConfig);
// Reset state in other modules
(0, diff_assessment_web_1.resetWebAssessmentState)();
(0, diff_assessment_terminal_1.resetTerminalAssessmentState)();
(0, summarize_1.resetSummaryState)();
};
/**
* Run a queue of suites via the web UI, streaming progress over WebSocket.
*/
const startWebSuiteQueue = (ws, suites, testType, progChoices, conf) => __awaiter(void 0, void 0, void 0, function* () {
var _a;
const suiteResults = [];
for (let i = 0; i < suites.length; i++) {
const suite = suites[i];
resetForNextSuite();
cli_1.programChoices.suite = suite;
cli_1.programChoices.testType = testType;
cli_1.programChoices.webTesting = true;
visregConfig = conf || Object.assign({}, projectLevelConfig);
if (progChoices.testType === 'targetted') {
const parsedViewports = progChoices.targetViewports
? (_a = progChoices.targetViewports) === null || _a === void 0 ? void 0 : _a.map(vp => (0, utils_1.parseViewport)(vp))
: [];
cli_1.programChoices.targetViewports = parsedViewports || [];
cli_1.programChoices.targetEndpointTitles = progChoices.targetEndpointTitles || [];
}
// Notify frontend of queue progress
ws.send(JSON.stringify({
type: 'queue-progress',
payload: { suite, index: i, total: suites.length, status: 'running' },
}));
if (cli_1.programChoices.testType === 'diffs-only') {
if (!(0, utils_1.pathExists)((0, utils_1.DIFF_DIR)()) || !(0, utils_1.hasFiles)((0, utils_1.DIFF_DIR)())) {
ws.send(JSON.stringify({
type: 'queue-progress',
payload: { suite, index: i, total: suites.length, status: 'skipped' },
}));
suiteResults.push({ suite, diffs: [], failed: false });
continue;
}
}
const diffList = createTemporaryDiffList();
backupDiffs();
backupReceived();
try {
yield runWebCypressTest(ws, diffList);
const diffs = (0, utils_1.getDiffingFilesFromTestResult)();
suiteResults.push({ suite, diffs, failed });
}
catch (error) {
suiteResults.push({ suite, diffs: [], failed: true });
}
ws.send(JSON.stringify({
type: 'queue-progress',
payload: { suite, index: i, total: suites.length, status: 'complete', diffs: suiteResults[suiteResults.length - 1].diffs },
}));
}
// Send final queue summary
const allDiffs = suiteResults.flatMap(r => r.diffs);
ws.send(JSON.stringify({
type: 'queue-complete',
payload: { suiteResults, allDiffs },
}));
});
exports.startWebSuiteQueue = startWebSuiteQueue;
const startWebTest = (ws, progChoices, conf) => __awaiter(void 0, void 0, void 0, function* () {
var _b;
if (!progChoices.suite || !progChoices.testType) {
return;
}
resetPreviousTest();
cli_1.programChoices.suite = progChoices.suite;
cli_1.programChoices.testType = progChoices.testType;
cli_1.programChoices.webTesting = true;
visregConfig = conf || Object.assign({}, projectLevelConfig);
if (cli_1.programChoices.testType === 'targetted') {
const parsedViewports = progChoices.targetViewports
? (_b = progChoices.targetViewports) === null || _b === void 0 ? void 0 : _b.map(vp => (0, utils_1.parseViewport)(vp))
: [];
cli_1.programChoices.targetViewports = parsedViewports || [];
cli_1.programChoices.targetEndpointTitles = progChoices.targetEndpointTitles || [];
}
const diffList = createTemporaryDiffList();
backupDiffs();
backupReceived();
yield runWebCypressTest(ws, diffList);
});
exports.startWebTest = startWebTest;
const backupDiffs = () => {
const dir = (0, utils_1.DIFF_DIR)();
const backupDir = (0, utils_1.BACKUP_DIFF_DIR)();
if ((0, utils_1.pathExists)(dir) && (0, utils_1.hasFiles)(dir)) {
if (!(0, utils_1.pathExists)(backupDir)) {
fs.mkdirSync(backupDir);
}
const files = fs.readdirSync(dir);
files.forEach(file => {
fs.renameSync(path.join(dir, file), path.join(backupDir, file));
});
}
};
const backupReceived = () => {
const dir = (0, utils_1.RECEIVED_DIR)();
const backupDir = (0, utils_1.BACKUP_RECEIVED_DIR)();
if ((0, utils_1.pathExists)(dir) && (0, utils_1.hasFiles)(dir)) {
if (!(0, utils_1.pathExists)(backupDir)) {
fs.mkdirSync(backupDir);
}
const files = fs.readdirSync(dir);
files.forEach(file => {
fs.renameSync(path.join(dir, file), path.join(backupDir, file));
});
}
};
const createTemporaryDiffList = () => {
if (!(0, utils_1.pathExists)((0, utils_1.DIFF_DIR)()))
return [];
return fs.readdirSync((0, utils_1.DIFF_DIR)()).filter(file => file.endsWith('.diff.png'));
};
const selectWhereToAssess = () => __awaiter(void 0, void 0, void 0, function* () {
console.log(`Press SPACE to assess diffs in the browser`);
console.log('...or ENTER to continue in the terminal\n');
const rl = readline.createInterface({
input: process.stdin,
});
let answer;
while (true) {
answer = yield new Promise(resolve => {
// Enable raw mode to get individual keypresses
readline.emitKeypressEvents(process.stdin);
if (process.stdin.isTTY)
process.stdin.setRawMode(true);
process.stdin.on('keypress', (str, key) => {
if (key.name === 'space') {
resolve('web');
}
if (key.name === 'return') {
resolve('cli');
}
if (key.ctrl && key.name === 'c') {
process.exit();
}
});
});
if (answer === 'web' || answer === 'cli') {
break;
}
}
if (process.stdin.isTTY)
process.stdin.setRawMode(false);
rl.close();
return answer;
});
const getTargetText = () => {
if (!cli_1.programChoices.targetViewports.length && !cli_1.programChoices.targetEndpointTitles.length) {
return '';
}
let targetText = ' from test (limited to ';
targetText += cli_1.programChoices.targetEndpointTitles.length
? `"${cli_1.programChoices.targetEndpointTitles.join(', ')}"`
: '';
targetText += cli_1.programChoices.targetViewports.length
? ' @ ' + cli_1.programChoices.targetViewports.join(', ')
: '';
targetText += ')';
return targetText;
};
const assessExistingDiffImages = (files) => __awaiter(void 0, void 0, void 0, function* () {
if (cli_1.programChoices.testType === 'lab') {
process.exit();
}
if (files.length === 0) {
(0, summarize_1.summarizeResultsAndQuit)([], [], failed);
process.exit();
}
console.log('\n\n');
const targetText = getTargetText();
(0, utils_1.printColorText)(`🚨 Detected ${files.length} diffs${targetText}\n`, '33');
const answer = yield selectWhereToAssess();
if (answer === 'web') {
(0, diff_assessment_web_1.assessInWeb)({ files, failed });
return;
}
if (cli_1.programChoices.containerized) {
(0, utils_1.printColorText)(`Assess the changes by running: \x1b[4mnpx visreg-test -a ${cli_1.programChoices.suite}\x1b[0m (i.e. not from the container)`, '33');
return;
}
(0, diff_assessment_terminal_1.assessInCLI)({ files, targetText, failed, visregConfig });
});
process.on('SIGINT', () => {
// console.log('\n\nTerminated by user');
// console.log('Restoring backups\n');
restoreBackups();
process.exit();
});
// Only start the server if the user has specified the --server-start flag (this is just as I'm working on it)
if (cli_1.programChoices.serverStart) {
(0, server_1.startServer)(cli_1.programChoices);
}
else {
main();
}