testophobia
Version:
Web application snapshot tester
163 lines (143 loc) • 6.62 kB
JavaScript
import express from 'express';
const app = express();
import fs from 'fs';
import path from 'path';
import opn from 'opn';
import chalk from 'chalk';
import {glob} from 'glob';
import {resolveNodeModuleFile, deleteGlob} from '../file/file.js';
import {readTestFileToObject} from '../test/format-tests.js';
import {writeGoldensManifest} from '../generate/generate-screenshot.js';
import {getGoldenImagesForViewer, getGoldenDirectoriesForViewer, getBrowsersForViewer} from './get-goldens-for-viewer.js';
const moduleURL = new URL(import.meta.url);
const __dirname = path.dirname(moduleURL.pathname);
/**
* Backend for the Testophobia Viewer
*
* @param {object} conf The Testophobia config file
* @param {string} resultsDir Path containing the results.json file(s)
*/
/* istanbul ignore next */
export const serveViewer = async (conf, resultsDir) => {
const projectDir = conf.projectDir;
let resultsJSON;
if (!conf.golden) {
try {
const results = await glob.sync(path.join(resultsDir, 'results*.json'))
results.forEach(r => {
if (!resultsJSON) resultsJSON = JSON.parse(fs.readFileSync(r));
else resultsJSON.failures = resultsJSON.failures.concat(JSON.parse(fs.readFileSync(r)).failures);
})
} catch (e) {
console.log('Could not locate the results file!', resultsDir);
return;
}
}
/* try to prevent any client side caching */
app.use((req, res, next) => {
res.setHeader('Surrogate-Control', 'no-store');
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
next();
});
/* serve the viewer config */
app.get('/viewer-config', (req, res) => {
res.header('Content-Type', 'application/json');
res.send(JSON.stringify({browser: conf.currentBrowser}));
});
/* serve the results json file or golden images when requested */
app.get('/test-results', (req, res) => {
res.header('Content-Type', 'application/json');
res.send(JSON.stringify(resultsJSON));
});
/* serve the results json file or golden images when requested */
app.get('/test-results/*splat', (req, res) => {
res.header('Content-Type', 'application/json');
res.send(JSON.stringify(getGoldenImagesForViewer(conf, req.params.splat.join('/'))));
});
/* serve the list of golden directories */
app.get('/golden-dirs', async (req, res) => {
res.header('Content-Type', 'application/json');
const dirs = await getGoldenDirectoriesForViewer(conf);
res.send(JSON.stringify(dirs));
});
/* serve the test images when requested */
app.get('/images/:testIdx/:imageType', (req, res) => {
if (resultsJSON.failures.length === 0) return res.sendStatus(200);
const imgPath = resultsJSON.failures[req.params.testIdx][`${req.params.imageType}FileLocation`];
if (imgPath.includes('.png')) res.header('Content-Type', 'image/png');
else res.header('Content-Type', 'image/jpeg');
res.sendFile(path.join(projectDir, imgPath));
});
/* serve the golden images when requested */
app.get('/goldens/*splat', (req, res) => {
const imgPath = req.params.splat.join('/');
if (imgPath.includes('.png')) res.header('Content-Type', 'image/png');
else res.header('Content-Type', 'image/jpeg');
res.sendFile(path.join(projectDir, imgPath));
});
const skipFailure = async testIdx => {
const diffPath = resultsJSON.failures[testIdx][`diffFileLocation`];
const testPath = resultsJSON.failures[testIdx][`testFileLocation`];
if (conf.verbose) console.log(chalk.dim(`Removing test failure: ${testPath}`));
if (diffPath) fs.unlinkSync(path.join(projectDir, diffPath));
resultsJSON.failures.splice(testIdx, 1);
--resultsJSON.totalTests;
--resultsJSON.totalFailures;
await deleteGlob(path.join(resultsDir, 'results*.json'));
fs.writeFileSync(path.join(resultsDir, 'results.json'), JSON.stringify(resultsJSON));
}
const applyGolden = async testIdx => {
const diffPath = resultsJSON.failures[testIdx][`diffFileLocation`];
const testPath = resultsJSON.failures[testIdx][`testFileLocation`];
const gldnPath = resultsJSON.failures[testIdx][`goldenFileLocation`];
const testDefPath = resultsJSON.failures[testIdx].testDefinitionPath;
const testDef = !isNaN(testDefPath) ? conf.tests[testDefPath] : await readTestFileToObject(testDefPath);
if (conf.verbose) console.log(chalk.dim(`Apply new golden image from: ${testPath}`));
fs.copyFileSync(path.join(projectDir, testPath), path.join(projectDir, gldnPath));
if (diffPath) fs.unlinkSync(path.join(projectDir, diffPath));
resultsJSON.failures.splice(testIdx, 1);
--resultsJSON.totalTests;
--resultsJSON.totalFailures;
await deleteGlob(path.join(resultsDir, 'results*.json'));
fs.writeFileSync(path.join(resultsDir, 'results.json'), JSON.stringify(resultsJSON));
writeGoldensManifest(path.resolve(gldnPath, '..'), testDef);
};
/* copy the test image over as the new golden image for this test index, and remove the failure */
app.post('/skip-failure/:testIdx', async (req, res) => {
await skipFailure(req.params.testIdx);
res.sendStatus(200);
});
/* copy the test image over as the new golden image for this test index, and remove the failure */
app.post('/apply-golden/:testIdx', async (req, res) => {
await applyGolden(req.params.testIdx);
res.sendStatus(200);
});
/* copy all test images over as the new golden images, and remove the failure */
app.post('/apply-all-goldens', async (req, res) => {
const numFailures = resultsJSON.failures.length;
for (let i = numFailures - 1; i >= 0; i--) {
await applyGolden(i);
}
res.sendStatus(200);
});
/* (not used anymore) viewer produces a heartbeat, if we don't receive it anymore, stop the server */
app.post('/heartbeat', (req, res) => {
res.sendStatus(200);
});
/* set the main viewer root directory */
app.use(express.static(path.join(__dirname, '../../../viewer')));
/* map any calls to node_modules to the main testophobia node_modules dir */
app.get('/node_modules/*splat', (req, res) => {
res.sendFile(resolveNodeModuleFile(req.path));
});
/* map all other routes back to index */
app.get('*splat', function (req, res) {
res.sendFile(path.resolve(__dirname, '../../../viewer/' + (conf.golden ? 'golden.html' : 'index.html')));
});
/* start the server */
app.listen(8090);
/* open the viewer in the default browser */
opn('http://localhost:8090/' + (conf.golden ? 'golden.html' : 'index.html'));
};