UNPKG

japp

Version:

An npm package to compare images and snapshots

778 lines (662 loc) 18.7 kB
//var lib = require("./lib.js"); var child_process = require("child_process"); var fs = require("fs"); var path = require("path"); var http = require("http"); var phantom = require("phantomjs"); var logger = require("./lib/logger.js"); //var imagemagick = require("./lib/imagemagick.js"); // default options var options = { specGenerationFileName: "mergedSpec.json" }; var exitCodes = { allPassing: 0, failing: 1, refMissing: 2, noSnapshot: 4, savePathError: 8, specPathError: 9, sourcePathError: 10 }; // the server created to serve compare images as the canvas // element needed by resemble module cannot work if the var resembleServer = null; var buildComparer = false; var callbacks = { completedCount: 0, checkComplete: function () { this.completedCount += 1; return (this.completedCount === options.phantomThreads); }, complete: [], eachpass: [], eachfail: [] }; var out = { total: 0, passing: 0, failing: 0, ssfailing: 0, missing: 0, report: { passing: [], ssfailing: [], failing: [], missing: [], status: "" } }; var getFolderMap = function (dir, store, callback) { var mod = this, subFolderCount = 0, successCount = 0, failCount = 0, directSubFolders = [], subfolderCB = function (status) { if (status === "success") { successCount += 1; } else { failCount += 1; } if (((successCount + failCount) === subFolderCount) && callback) { if (failCount) { callback("error"); } else { callback("success"); } } }; fs.readdir(dir, function (err, files) { if (err) { callback("error"); return false; } var i = 0, ii = files.length, subfolders = {}, file, fspath; for (; i < ii; i += 1) { file = files[i].replace(/\r/, ''); file = file.replace(/\n/, ''); fspath = path.join(dir, file); if (fs.statSync(fspath).isDirectory()) { subfolders[file] = []; subFolderCount += 1; directSubFolders.push(file); } else { store.push(file); } } i = 0; ii = directSubFolders.length; if (!ii) { if (callback) { callback("success"); } } else { store.push(subfolders); for (; i < ii; i += 1) { getFolderMap(path.join(dir, directSubFolders[i]), subfolders[directSubFolders[i]], subfolderCB); } } }); }; var splitSpecs = function (specArray, prefix) { prefix = prefix || "."; var item, subitem, count = 0, num = options.phantomThreads, specFiles = specArray || options.specFiles, i = 0, ii = specFiles.length, returnArr = options.splitSpecs || (options.splitSpecs = []), index = 0; if (!num) { return []; } for (; i < ii; i += 1) { item = specFiles[i]; if (typeof item === "object") { for (subitem in item) { splitSpecs(item[subitem], path.join(prefix, subitem)); } } else if (specFiles[i].indexOf("Spec.json") !== -1) { index = i % num; if (!returnArr[index]) { returnArr[index] = []; } returnArr[index].push(path.join(prefix, specFiles[i])); } } return returnArr; }; var processPhantomMessage = function (msg) { var parts; if (msg.indexOf("__exitPhantomMismatch__") !== -1) { out.total += 1; out.failing += 1; parts = msg.split(","); out.report.failing.push({ id: parts[1], path: parts[2], mismatch: parts[3], snapshot: parts[4], refpath: parts[5], diff: parts[7] }); logger.error(msg); } else if (msg.indexOf("__exitPhantomMatch__") !== -1) { out.total += 1; out.passing += 1; parts = msg.split(","); out.report.passing.push({ id: parts[1], path: parts[2], mismatch: parts[3], snapshot: parts[4], refpath: parts[5], }); logger.info(msg); } else if (msg.indexOf("__NotFound__") !== -1) { out.total += 1; out.missing += 1; parts = msg.split(","); out.report.missing.push({ refpath: parts[1], id: parts[2], path: parts[3], snapshot: parts[4] }); logger.error(msg); } else if (msg.indexOf("__buildComparer__") !== -1) { buildComparer = true; } else if (msg.indexOf("__noSnapshot__") !== -1) { out.total += 1; out.ssfailing += 1; parts = msg.split(","); out.report.ssfailing.push({ id: parts[1], path: parts[2] }); logger.error(msg); } else { logger.info(msg); } }; module.exports = { cli: function (program) { logger.startTime("full"); var fspath, sPath, exists, source, merge, destination, confObj; if (fs.existsSync(path.join(process.cwd(), program.config))) { confObj = JSON.parse(fs.readFileSync(path.join(process.cwd(), program.config))); } else { confObj = {}; } options.phantomThreads = Number(program.maxThreads) || confObj.maxThreads || 1; if (program.generateSpecs) { source = program.dataSourceFolder || confObj.dataSourceFolder || "./tests/data/"; merge = Boolean(program.mergeSpecs); this.specGeneratorFS(source, merge); return; } if (program.save) { sPath = program.savePath || confObj.savePath || "./saved/"; fspath = path.join(process.cwd(), sPath); exists = fs.existsSync(fspath); options.overwriteRef = program.overwriteRef; if (exists) { options.saveImages = true; options.savePath = fspath; } else { options.saveImages = false; options.savePath = null; logger.error("The path provided to save the images does not exist."); process.exit(exitCodes.savePathError); } } sPath = (program.specSource || confObj.specSource || "./tests/specs/"); fspath = path.join(process.cwd(), sPath); exists = fs.existsSync(fspath); if (exists) { options.specSource = fspath; options.specSourceRelative = sPath; } else { options.specSource = null; logger.error("The path provided to source the charts does not exist."); process.exit(exitCodes.sourcePathError); } sPath = (program.refImagesRoot || confObj.refImagesRoot || "./"); fspath = path.join(process.cwd(), sPath); exists = fs.existsSync(fspath); if (exists) { options.refImages = sPath; } else { options.refImages = null; logger.warn("The root provided for the reference images does not exist."); } sPath = (program.jsSourceRoot || confObj.jsSourceRoot || "./src/"); fspath = path.join(process.cwd(), sPath); exists = fs.existsSync(fspath); if (exists) { options.jsSource = sPath; } else { options.jsSource = null; logger.warn("The path provided for the source files does not exist."); } var mPath = (program.mapsPathPrefix || confObj.mapsPathPrefix || "maps/"); fspath = path.join(fspath, mPath); exists = fs.existsSync(fspath); if (exists) { options.mapsPathPrefix = mPath; } else { options.mapsPathPrefix = "/"; logger.warn("The path provided for the MAPS source files does not exist."); } if (program.debugMode) { logger.setWriteStream("japp_log.txt"); } else { logger.info = function () { // do nothing }; } options.threshold = Number(program.threshold) || Number(confObj.threshold)|| 0; this.start(); }, /** * This function converts the chart json data into a spec json and either stores it in the store * provided or returns the spec json for writing. If added to the store the consumer of * this method has the responsibility of writing the specs in the store into the filesystem. * If not added to the store, then the method returns an object that is should be immediately written * to the file system (also by the consumer of the method). * * @param {[type]} data The chart json data * @param {[type]} id id to hash the spec json that gets created * @param {[type]} store The spec store * @return {[type]} [description] */ convertJSONToSpec: function (data, id, store) { if (!data) { return null; } var chartConstructionOptions = { width: "600", height: "400", type: "column2d", dataFormat: "json", dataSource: data }, toWriteImmediately = {}, toSave = false; if (data.__constructorParams) { chartConstructionOptions.width = (data.__constructorParams.width || "600"); chartConstructionOptions.height = (data.__constructorParams.height || "400"); chartConstructionOptions.type = (data.__constructorParams.type || data.__constructorParams.swfUrl || "column2d"); delete data.__constructorParams; } if (!store || store[id]) { toWriteImmediately[id] = { description: "Auto generated spec", options: chartConstructionOptions }; } else { toSave = true; store[id] = { description: "Auto generated spec", options: chartConstructionOptions }; } return { addedToStore: toSave, writeNow: toWriteImmediately }; }, /** * Gets FusionCharts chart data in JSON format from an array of files and converts them into * specs. * If there are more than data files provided in the array then the corresponding specs * are save in a single file obtained from targetSpec. * If there is a single file in the array then the generated spec is stored either based * on the targetSpec parameter (if provided), OR the file name of the json data file. * * @param {[type]} folderPath [description] * @param {[type]} dataFileArr [description] * @param {[type]} targetSpec [description] * @return {[type]} [description] */ createSpecsFromFiles: function (folderPath, dataFileArr, targetSpec) { var i = 0, ii = dataFileArr.length, specData = {}, toSave = false, temp, key, specKey, convertResult, file; if (ii < 1) { return false; } else if (ii > 1) { targetSpec = targetSpec || options.specGenerationFileName; } else { // ii === 1 specData = null; } for (; i < ii; i += 1) { file = dataFileArr[i]; data = fs.readFileSync(path.join(folderPath, file)); key = file.split(".")[0]; specKey = (key && key.toString()) || "noname"; convertResult = this.convertJSONToSpec(JSON.parse(data), specKey, specData); if (convertResult.addedToStore) { toSave = true; } if (convertResult.writeNow) { for (var item in convertResult.writeNow) { if (targetSpec) { temp = targetSpec + "Spec.json"; } else { temp = item + "Spec.json"; } fs.writeFileSync( path.join(folderPath, temp), JSON.stringify(convertResult.writeNow, null, 4), {flag: "w"} ); } } } if (toSave) { // targetSpec should be defined. If for some reason it is not defined, there is a problem // with the logic. if (targetSpec) { fs.writeFileSync( path.join(folderPath, targetSpec), JSON.stringify(specData, null, 4), {flag: "w"} ); } else { logger.error('The target file in which to save the merged specs was not found.'); } } }, /** * Read the source folder from the filesystem, fetches all the files (assuming all the files contain only FusionCharts * specific data), converts them into spec jsons and writes them back to the file system. * * @param {[type]} source [description] * @param {[type]} merge [description] * @return {[type]} [description] */ specGeneratorFS: function (source, merge) { var mod = this, dataStore = []; getFolderMap(source, dataStore, function (status) { if (status !== "success") { logger.error("Unable to open the folder " + source); return; } var converter = function (folder, tree) { var i = 0, ii = tree.length, fileArray = [], item; for (; i < ii; i += 1) { item = tree[i]; if (typeof item === "object") { for (var each in item) { converter(path.join(source, each), item[each]); } } else if (item.indexOf("Spec.json") === -1) { if (merge) { fileArray.push(item); } else { mod.createSpecsFromFiles(folder, [item]); } } } if (merge) { mod.createSpecsFromFiles(folder, fileArray); } }; converter(source, dataStore); }); }, configure: function (opts) { // Might be needed later }, start: function () { var mod = this; if (options.specSource) { logger.info("Reading the specs from the file system"); getFolderMap(options.specSource, (options.specFiles = []), function (status) { if (status !== "success") { logger.error("Spec path provided does not exist / cannot be opened"); process.exit(exitCodes.specPathError); } logger.info("Starting the tests. Please wait."); mod.startHTTP(); var resultBatch = splitSpecs(null, options.specSourceRelative), i = 0, ii = resultBatch.length, checkCB = function () { if (callbacks.checkComplete()) { logger.info("Testing has finished executing!"); mod.finished(); } }; options.phantomThreads = ii; if (!ii) { logger.error("No spec files found"); process.exit(1); } for (; i < ii; i += 1) { if (resultBatch[i].length) { mod.runTests(resultBatch[i], checkCB); } } }); } }, startHTTP: function () { if (resembleServer) { return resembleServer; } logger.info("Creating a server to serve resemble.js"); var resourcePrefix = path.relative(process.cwd(), __dirname); resembleServer = http.createServer(function (req, res) { var url = req.url.toString(), filename; if (url.indexOf('.js') !== -1) { filename = path.join(process.cwd(), ("." + url)); fs.readFile(filename, function (err, data) { if (err) { res.writeHead(404); res.end("Error reading file. Check if the file is present"); } else { res.setHeader("Content-type", "text/javascript"); res.writeHead(200); res.write(data); res.end(); } }); } else if (url.indexOf(".png") !== -1) { filename = path.join(process.cwd(), "." + url); fs.readFile(filename, function (err, data) { if (err) { res.writeHead(404); res.end("Error reading file. Check if the file is present"); } else { res.setHeader("Content-type", "image/png"); res.writeHead(200); res.end(data, 'binary'); } }); } else { res.write([ "<html>", "<head>", "<script type='text/javascript' src='" + resourcePrefix + "/lib/resemble.js'></script>", "<script type='text/javascript' src='" + options.jsSource + "/FusionCharts.js'></script>", "<script type='text/javascript'>", "FusionCharts.options.scriptBaseUri = '" + options.jsSource + "';", "FusionCharts.options.html5ScriptNamePrefix = '" + options.mapsPathPrefix + "/FusionCharts.HC.';", "</script>", "</head>", "<body><div id='chart-container'></div><div id='diffHolder'></div></body>", "</html>" ].join("")); res.end(); } }); resembleServer.listen(8090); }, stopHTTP: function () { if (resembleServer) { resembleServer.close(); resembleServer = null; logger.info("Resemble server closed."); } }, setPhantomCount: function (num) { options.phantomThreads = num; }, runTests: function (specFileArray, callback) { var args; if (options.saveImages) { args = [ path.join(__dirname, "node_modules/phantomjs/bin/phantomjs"), path.join(__dirname, "src/phantomFCRunner.js"), JSON.stringify(specFileArray), options.jsSource, "1", options.savePath, options.overwriteRef, options.refImages ]; } else { args = [ path.join(__dirname, "node_modules/phantomjs/bin/phantomjs"), path.join(__dirname, "src/phantomFCRunner.js"), JSON.stringify(specFileArray), options.jsSource, "0", options.threshold ]; } logger.info("Creating instance of phantom"); var ls = child_process.spawn("node", args); ls.stdout.on('data', function (data) { processPhantomMessage(data.toString()); }); ls.stderr.on('data', function (data) { logger.error('STDERR: ' + data); }); ls.on('close', function (code) { if (callback) { callback(); } }); }, buildResultHTML: function () { var htmlstr = "<html><head></head><body>"; htmlstr += "<h3>Failing Tests</h3>"; for (i = 0, ii = out.report.failing.length; i < ii; i += 1) { htmlstr += "<div style='border:1px solid red;padding: 5px; margin: 5px;'>"; htmlstr += "<img src='" + out.report.failing[i].refpath + "' />"; htmlstr += "<img src='" + out.report.failing[i].snapshot + "' />"; htmlstr += "<img src='" + out.report.failing[i].diff + "' />"; htmlstr += "</div>"; } htmlstr += "</body></html>"; fs.writeFileSync("japp_mismatch.html", htmlstr); htmlstr = "<html><head></head><body>"; htmlstr += "<h3>Passing Tests</h3>"; for (i = 0, ii = out.report.passing.length; i < ii; i += 1) { htmlstr += "<div style='border:1px solid red;padding: 5px; margin: 5px;'>"; htmlstr += "<img src='" + out.report.passing[i].refpath + "' />"; htmlstr += "<img src='" + out.report.passing[i].snapshot + "' />"; htmlstr += "</div>"; } htmlstr += "</body></html>"; fs.writeFileSync("japp_match.html", htmlstr); }, help: function () { }, stop: function () { }, /** Events to listen to **/ finished: function () { var i = 0, ii = callbacks.complete.length, exitCode = 0; for (; i < ii; i += 1) { callbacks.complete[i].call(); } logger.endTime("full"); console.log("\nResults\n==========="); console.log("Total Tests Executed: " + out.total); console.log("Tests Passing: " + out.passing); console.log("Tests Failing: " + out.failing); fs.writeFileSync("japp_result.json", JSON.stringify(out, null, 4)); if (buildComparer) { this.buildResultHTML(); } this.stopHTTP(); if (out.total === out.passing) { exitCode = 0; } if (out.failing) { exitCode += 1; } if (out.missing) { exitCode += 2; } if(out.ssfailing) { exitCode += 4; } process.exit(exitCode); }, onEachPass: function () { }, onEachFail: function () { }, onComplete: function () { } };