diff --git a/Gruntfile.js b/Gruntfile.js index 819db4f..9525960 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -330,7 +330,6 @@ module.exports = function(grunt) { grunt.registerTask('test', [ 'build', - 'jshint', 'express:testSuite', 'clean:coverage', 'copy-test-server-settings', diff --git a/front/src/css/dashboard.css b/front/src/css/dashboard.css index 407887b..658202e 100644 --- a/front/src/css/dashboard.css +++ b/front/src/css/dashboard.css @@ -5,7 +5,14 @@ text-align: center; } .summary .globalScore { - margin-bottom: 3em; + display: table; + width: 60%; + margin: 3em auto; +} +.summary .globalScore > div { + display: table-cell; + width: 50%; + vertical-align: middle; } .summary .globalScore .globalGrade { margin: 0.5 auto; @@ -22,6 +29,23 @@ font-weight: bold; margin: 0.5em 0 1em; } +.summary .globalScore .screenshotWrapper:hover { + opacity: 0.75; +} +.summary .globalScore .screenshotWrapper:hover:after { + position: absolute; + width: 1.25em; + height: 1.25em; + top: 0.7em; + left: 1.55em; + font-size: 3em; + color: #FFF; + background: #000; + border-radius: 0.2em; + text-align: center; + content: "+"; + opacity: 0.6; +} .summary .notations { display: table; width: 80%; diff --git a/front/src/css/main.css b/front/src/css/main.css index 8cab1bf..0a6b4bf 100644 --- a/front/src/css/main.css +++ b/front/src/css/main.css @@ -113,6 +113,44 @@ h1 span { margin-top: 4em; color: black; } +.screenshotWrapper.desktop { + display: inline-block; + position: relative; + border: 0.2em solid #AAA; + background: #000; + padding: 0.5em; + border-top-left-radius: 0.4em; + border-top-right-radius: 0.4em; +} +.screenshotWrapper.desktop:before { + position: absolute; + width: 15em; + height: 0.6em; + bottom: -0.75em; + left: -1em; + background: #CCC; + border-bottom-left-radius: 0.2em; + border-bottom-right-radius: 0.2em; + content: " "; +} +.screenshotWrapper.desktop:after { + position: absolute; + width: 0.4em; + height: 0.2em; + bottom: -0.55em; + left: 12.5em; + background: lime; + content: " "; +} +.screenshotWrapper.desktop > div { + width: 12em; + height: 6.75em; + overflow: scroll; + position: relative; +} +.screenshotWrapper.desktop .screenshotImage { + width: 100%; +} .star { font-weight: bold; } diff --git a/front/src/css/screenshot.css b/front/src/css/screenshot.css new file mode 100644 index 0000000..55af543 --- /dev/null +++ b/front/src/css/screenshot.css @@ -0,0 +1,7 @@ +.screenshot.board { + text-align: center; +} +.screenshot .screenshotWrapper { + font-size: 2.08333333333333em; + margin-bottom: 0.5em; +} diff --git a/front/src/js/app.js b/front/src/js/app.js index 7ddabb4..4c4f43b 100644 --- a/front/src/js/app.js +++ b/front/src/js/app.js @@ -6,6 +6,7 @@ var yltApp = angular.module('YellowLabTools', [ 'dashboardCtrl', 'queueCtrl', 'ruleCtrl', + 'screenshotCtrl', 'timelineCtrl', 'runsFactory', 'resultsFactory', @@ -46,6 +47,10 @@ yltApp.config(['$routeProvider', '$locationProvider', templateUrl: 'views/timeline.html', controller: 'TimelineCtrl' }). + when('/result/:runId/screenshot', { + templateUrl: 'views/screenshot.html', + controller: 'ScreenshotCtrl' + }). when('/result/:runId/rule/:policy', { templateUrl: 'views/rule.html', controller: 'RuleCtrl' diff --git a/front/src/js/controllers/dashboardCtrl.js b/front/src/js/controllers/dashboardCtrl.js index c1b2ba1..4243e7b 100644 --- a/front/src/js/controllers/dashboardCtrl.js +++ b/front/src/js/controllers/dashboardCtrl.js @@ -36,7 +36,8 @@ dashboardCtrl.controller('DashboardCtrl', ['$scope', '$rootScope', '$routeParams $scope.testAgain = function() { Runs.save({ url: $scope.result.params.url, - waitForResponse: false + waitForResponse: false, + screenshot: true }, function(data) { $location.path('/queue/' + data.runId); }); diff --git a/front/src/js/controllers/indexCtrl.js b/front/src/js/controllers/indexCtrl.js index bec1da3..e460d83 100644 --- a/front/src/js/controllers/indexCtrl.js +++ b/front/src/js/controllers/indexCtrl.js @@ -5,7 +5,8 @@ indexCtrl.controller('IndexCtrl', ['$scope', '$location', 'Runs', function($scop if ($scope.url) { Runs.save({ url: $scope.url, - waitForResponse: false + waitForResponse: false, + screenshot: true }, function(data) { $location.path('/queue/' + data.runId); }); diff --git a/front/src/js/controllers/ruleCtrl.js b/front/src/js/controllers/ruleCtrl.js index 7ff0622..27e9dcd 100644 --- a/front/src/js/controllers/ruleCtrl.js +++ b/front/src/js/controllers/ruleCtrl.js @@ -31,7 +31,8 @@ ruleCtrl.controller('RuleCtrl', ['$scope', '$rootScope', '$routeParams', '$locat $scope.testAgain = function() { Runs.save({ url: $scope.result.params.url, - waitForResponse: false + waitForResponse: false, + screenshot: true }, function(data) { $location.path('/queue/' + data.runId); }); diff --git a/front/src/js/controllers/screenshotCtrl.js b/front/src/js/controllers/screenshotCtrl.js new file mode 100644 index 0000000..57d7632 --- /dev/null +++ b/front/src/js/controllers/screenshotCtrl.js @@ -0,0 +1,37 @@ +var screenshotCtrl = angular.module('screenshotCtrl', ['resultsFactory', 'menuService']); + +screenshotCtrl.controller('ScreenshotCtrl', ['$scope', '$rootScope', '$routeParams', '$location', 'Results', 'Runs', 'Menu', function($scope, $rootScope, $routeParams, $location, Results, Runs, Menu) { + $scope.runId = $routeParams.runId; + $scope.Menu = Menu.setCurrentPage(null, $scope.runId); + + function loadResults() { + // Load result if needed + if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) { + Results.get({runId: $routeParams.runId}, function(result) { + $rootScope.loadedResult = result; + $scope.result = result; + init(); + }, function(err) { + $scope.error = true; + }); + } else { + $scope.result = $rootScope.loadedResult; + } + } + + $scope.backToDashboard = function() { + $location.path('/result/' + $scope.runId); + }; + + $scope.testAgain = function() { + Runs.save({ + url: $scope.result.params.url, + waitForResponse: false, + screenshot: true + }, function(data) { + $location.path('/queue/' + data.runId); + }); + }; + + loadResults(); +}]); \ No newline at end of file diff --git a/front/src/js/controllers/timelineCtrl.js b/front/src/js/controllers/timelineCtrl.js index da813f9..2dd1943 100644 --- a/front/src/js/controllers/timelineCtrl.js +++ b/front/src/js/controllers/timelineCtrl.js @@ -134,7 +134,8 @@ timelineCtrl.controller('TimelineCtrl', ['$scope', '$rootScope', '$routeParams', $scope.testAgain = function() { Runs.save({ url: $scope.result.params.url, - waitForResponse: false + waitForResponse: false, + screenshot: true }, function(data) { $location.path('/queue/' + data.runId); }); diff --git a/front/src/less/dashboard.less b/front/src/less/dashboard.less index 9813f0e..5b99342 100644 --- a/front/src/less/dashboard.less +++ b/front/src/less/dashboard.less @@ -7,7 +7,16 @@ } .summary .globalScore { - margin-bottom: 3em; + display: table; + width: 60%; + margin: 3em auto; + + > div { + display: table-cell; + width: 50%; + vertical-align: middle; + } + .globalGrade { margin: 0.5 auto; width: 2.5em; @@ -23,6 +32,25 @@ font-weight: bold; margin: 0.5em 0 1em; } + + .screenshotWrapper:hover { + opacity: 0.75; + + &:after { + position: absolute; + width: 1.25em; + height: 1.25em; + top: 0.7em; + left: 1.55em; + font-size: 3em; + color: #FFF; + background: #000; + border-radius: 0.2em; + text-align: center; + content: "+"; + opacity: 0.6; + } + } } .summary .notations { diff --git a/front/src/less/main.less b/front/src/less/main.less index ae2cc21..b949cd8 100644 --- a/front/src/less/main.less +++ b/front/src/less/main.less @@ -113,6 +113,50 @@ h1 span { } +.screenshotWrapper.desktop { + display: inline-block; + position: relative; + border: 0.2em solid #AAA; + background: #000; + padding: 0.5em; + border-top-left-radius: 0.4em; + border-top-right-radius: 0.4em; + + &:before { + position: absolute; + width: 15em; + height: 0.6em; + bottom: -0.75em; + left: -1em; + background: #CCC; + border-bottom-left-radius: 0.2em; + border-bottom-right-radius: 0.2em; + content: " "; + } + + &:after { + position: absolute; + width: 0.4em; + height: 0.2em; + bottom: -0.55em; + left: 12.5em; + background: lime; + content: " "; + } + + > div { + width: 12em; + height: 6.75em; + overflow: scroll; + position: relative; + } + + .screenshotImage { + width: 100%; + } +} + + .star { font-weight: bold; span { diff --git a/front/src/less/screenshot.less b/front/src/less/screenshot.less new file mode 100644 index 0000000..a214659 --- /dev/null +++ b/front/src/less/screenshot.less @@ -0,0 +1,8 @@ +.screenshot.board { + text-align: center; +} + +.screenshot .screenshotWrapper { + font-size: 2.08333333333333em; + margin-bottom: 0.5em; +} \ No newline at end of file diff --git a/front/src/main.html b/front/src/main.html index fe5ec86..5d6f738 100644 --- a/front/src/main.html +++ b/front/src/main.html @@ -12,6 +12,7 @@ + @@ -27,6 +28,7 @@ + diff --git a/front/src/views/dashboard.html b/front/src/views/dashboard.html index 7d5a7b6..1d163bc 100644 --- a/front/src/views/dashboard.html +++ b/front/src/views/dashboard.html @@ -2,10 +2,21 @@
(scroll on the screenshot to see under the fold)
+ + +This is the number of different colors defined in CSS.
Your CSS project will be easier to maintain if you keep a small color set.
", "isOkThreshold": 30, "isBadThreshold": 150, - "isAbnormalThreshold": 300, + "isAbnormalThreshold": 400, "hasOffenders": true, "offendersTransformFn": function(offenders, ruleObject) { var deduplicatedObj = {}; @@ -771,7 +771,7 @@ var policies = { list: offenders.map(function(offender) { var splittedOffender = offendersHelpers.cssOffenderPattern(offender); - var rule = splittedOffender.css; + var rule = splittedOffender.css || ''; var redundanters = [ ['ul', 'li'], ['ol', 'li'], diff --git a/lib/runner.js b/lib/runner.js index ab6ea6d..adb8864 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -43,7 +43,11 @@ var Runner = function(params) { delete data.toolsResults.phantomas.metrics.javascriptExecutionTree; delete data.toolsResults.phantomas.offenders.javascriptExecutionTree; - //Finished! + return data; + + }).then(function(data) { + + // Finished! deferred.resolve(data); }).fail(function(err) { diff --git a/lib/screenshotHandler.js b/lib/screenshotHandler.js new file mode 100644 index 0000000..5651a12 --- /dev/null +++ b/lib/screenshotHandler.js @@ -0,0 +1,135 @@ +var debug = require('debug')('ylt:screenshotHandler'); +var lwip = require('lwip'); +var tmp = require('temporary'); +var Q = require('q'); +var fs = require('fs'); +var path = require('path'); + + +var screenshotHandler = function() { + + this.getScreenshotTempFile = function() { + + var screenshotTmpFolder = new tmp.Dir(); + var tmpFilePath = path.join(screenshotTmpFolder.path, 'screenshot.jpg'); + var that = this; + + return { + + getTmpFolder: function() { + return screenshotTmpFolder; + }, + + getTmpFilePath: function() { + return tmpFilePath; + }, + + toThumbnail: function(width) { + return that.optimize(tmpFilePath, width); + }, + + deleteTmpFile: function() { + return that.deleteTmpFileAndFolder(tmpFilePath, screenshotTmpFolder); + } + }; + }; + + + this.optimize = function(imagePath, width) { + var that = this; + + debug('Starting screenshot transformation'); + + return this.openImage(imagePath) + + .then(function(image) { + + return that.resizeImage(image, width); + + }) + + .then(this.toBuffer); + }; + + + this.openImage = function(imagePath) { + var deferred = Q.defer(); + + lwip.open(imagePath, function(err, image){ + if (err) { + debug('Could not open imagePath %s', imagePath); + debug(err); + + deferred.reject(err); + } else { + debug('Image correctly open'); + deferred.resolve(image); + } + }); + + return deferred.promise; + }; + + + this.resizeImage = function(image, newWidth) { + var deferred = Q.defer(); + + var currentWidth = image.width(); + var ratio = newWidth / currentWidth; + + image.scale(ratio, function(err, image){ + if (err) { + debug('Could not resize image'); + debug(err); + + deferred.reject(err); + } else { + debug('Image correctly resized'); + deferred.resolve(image); + } + }); + + return deferred.promise; + }; + + + this.toBuffer = function(image) { + var deferred = Q.defer(); + + image.toBuffer('jpg', {quality: 90}, function(err, buffer){ + if (err) { + debug('Could not save image to buffer'); + debug(err); + + deferred.reject(err); + } else { + debug('Image correctly transformed to buffer'); + deferred.resolve(buffer); + } + }); + + return deferred.promise; + }; + + + this.deleteTmpFileAndFolder = function(tmpFilePath, screenshotTmpFolder) { + var deferred = Q.defer(); + + fs.unlink(tmpFilePath, function (err) { + if (err) { + debug('Screenshot file not found, could not be deleted. But it is not a problem.'); + } else { + debug('Screenshot file deleted.'); + } + + screenshotTmpFolder.rmdir(); + debug('Screenshot temp folder deleted'); + + deferred.resolve(); + }); + + return deferred.promise; + }; +}; + +module.exports = new screenshotHandler(); \ No newline at end of file diff --git a/lib/server/controllers/apiController.js b/lib/server/controllers/apiController.js index 4ab4142..940c870 100644 --- a/lib/server/controllers/apiController.js +++ b/lib/server/controllers/apiController.js @@ -1,6 +1,8 @@ var debug = require('debug')('ylt:server'); +var Q = require('q'); var ylt = require('../../index'); +var ScreenshotHandler = require('../../screenshotHandler'); var RunsQueue = require('../datastores/runsQueue'); var RunsDatastore = require('../datastores/runsDatastore'); var ResultsDatastore = require('../datastores/resultsDatastore'); @@ -24,10 +26,17 @@ var ApiController = function(app) { params: { url: req.body.url, waitForResponse: req.body.waitForResponse !== false && req.body.waitForResponse !== 'false' && req.body.waitForResponse !== 0, - partialResult: req.body.partialResult || null + partialResult: req.body.partialResult || null, + screenshot: req.body.screenshot || false } }; + // Create a temporary folder to save the screenshot + var screenshot; + if (run.params.screenshot) { + screenshot = ScreenshotHandler.getScreenshotTempFile(); + } + // Add test to the testQueue debug('Adding test %s to the queue', run.runId); var queuePromise = queue.push(run.runId); @@ -49,81 +58,120 @@ var ApiController = function(app) { debug('Launching test %s on %s', run.runId, run.params.url); - ylt(run.params.url) + var runOptions = { + screenshot: run.params.screenshot ? screenshot.getTmpFilePath() : false + }; - .then(function(data) { + return ylt(run.params.url, runOptions); - debug('Success'); - + }) + // Phantomas completed, let's save the screenshot if any + .then(function(data) { - // Save result in datastore - data.runId = run.runId; - resultsDatastore.saveResult(data) - .then(function() { + debug('Success'); + data.runId = run.runId; - runsDatastore.markAsComplete(run.runId); - - // Send result if the user was waiting - if (run.params.waitForResponse) { + + // Some conditional steps are made if there is a screenshot + var screenshotPromise = Q.resolve(); - // If the user only wants a portion of the result (partialResult option) - switch(run.params.partialResult) { - case 'generalScores': - res.redirect(302, '/api/results/' + run.runId + '/generalScores'); - break; - case 'rules': - res.redirect(302, '/api/results/' + run.runId + '/rules'); - break; - case 'javascriptExecutionTree': - res.redirect(302, '/api/results/' + run.runId + '/javascriptExecutionTree'); - break; - case 'phantomas': - res.redirect(302, '/api/results/' + run.runId + '/toolsResults/phantomas'); - break; - default: - res.redirect(302, '/api/results/' + run.runId); - } - } - - }) - .fail(function(err) { - debug('Saving results to resultsDatastore failed:'); - debug(err); + if (run.params.screenshot) { + + // Replace the empty promise created earlier with Q.resolve() + screenshotPromise = screenshot.toThumbnail(400) + + // Read screenshot + .then(function(screenshotBuffer) { + + if (screenshotBuffer) { + debug('Image optimized'); + data.screenshotBuffer = screenshotBuffer; - res.status(500).send('Saving results failed'); - }); + // Official path to get the image + data.screenshotUrl = '/api/results/' + data.runId + '/screenshot.jpg'; + } + }) + // Delete screenshot temporary file + .then(screenshot.deleteTmpFile); + + } + + // Let's continue + screenshotPromise + + // Save results + .then(function() { + delete data.params.options.screenshot; + return resultsDatastore.saveResult(data); }) + // Mark as the run as complete and send the response if the request is still waiting + .then(function() { + + debug('Result saved in datastore'); + + runsDatastore.markAsComplete(run.runId); + + if (run.params.waitForResponse) { + + // If the user only wants a portion of the result (partialResult option) + switch(run.params.partialResult) { + case 'generalScores': + res.redirect(302, '/api/results/' + run.runId + '/generalScores'); + break; + case 'rules': + res.redirect(302, '/api/results/' + run.runId + '/rules'); + break; + case 'javascriptExecutionTree': + res.redirect(302, '/api/results/' + run.runId + '/javascriptExecutionTree'); + break; + case 'phantomas': + res.redirect(302, '/api/results/' + run.runId + '/toolsResults/phantomas'); + break; + default: + res.redirect(302, '/api/results/' + run.runId); + } + } + + }) .fail(function(err) { - console.error('Test failed for URL: %s', run.params.url); console.error(err.toString()); runsDatastore.markAsFailed(run.runId, err.toString()); - res.status(400).send('Bad request'); - - }) - - .finally(function() { - queue.remove(run.runId); + res.status(500).send('An error occured'); }); - }).fail(function(err) { - console.error('Error or YLT\'s core instanciation'); - console.error(err); - console.error(err.stack); + }) + + .fail(function(err) { + + console.error('Test failed for URL: %s', run.params.url); + console.error(err.toString()); + + runsDatastore.markAsFailed(run.runId, err.toString()); + + res.status(400).send('Bad request'); + + }) + + .finally(function() { + queue.remove(run.runId); }); - // The user doesn't not want to wait for the response, sending the run ID only + + // The user doesn't want to wait for the response, sending the run ID only if (!run.params.waitForResponse) { console.log('Sending response without waiting.'); res.setHeader('Content-Type', 'application/json'); res.send(JSON.stringify({runId: run.runId})); } + }); + // Retrive one run by id app.get('/api/runs/:id', function(req, res) { var runId = req.params.id; @@ -221,6 +269,21 @@ var ApiController = function(app) { }); } + // Retrive one result by id + app.get('/api/results/:id/screenshot.jpg', function(req, res) { + var runId = req.params.id; + + resultsDatastore.getScreenshot(runId) + .then(function(screenshotBuffer) { + + res.setHeader('Content-Type', 'image/jpeg'); + res.send(screenshotBuffer); + + }).fail(function() { + res.status(404).send('Not found'); + }); + }); + }; module.exports = ApiController; \ No newline at end of file diff --git a/lib/server/controllers/frontController.js b/lib/server/controllers/frontController.js index 9a19a67..4d5f531 100644 --- a/lib/server/controllers/frontController.js +++ b/lib/server/controllers/frontController.js @@ -7,7 +7,7 @@ var FrontController = function(app) { var cacheDuration = 365 * 24 * 60 * 60 * 1000; // One year var assetsPath = (app.get('env') === 'development') ? '../../../front/src' : '../../../front/build'; - var routes = ['/', '/about', '/result/:runId', '/result/:runId/timeline', '/result/:runId/rule/:policy', '/queue/:runId']; + var routes = ['/', '/about', '/result/:runId', '/result/:runId/timeline', '/result/:runId/screenshot', '/result/:runId/rule/:policy', '/queue/:runId']; routes.forEach(function(route) { app.get(route, function(req, res) { res.setHeader('Cache-Control', 'public, max-age=20'); diff --git a/lib/server/datastores/resultsDatastore.js b/lib/server/datastores/resultsDatastore.js index 3392337..a8119f9 100644 --- a/lib/server/datastores/resultsDatastore.js +++ b/lib/server/datastores/resultsDatastore.js @@ -9,24 +9,28 @@ function ResultsDatastore() { 'use strict'; var resultFileName = 'results.json'; + var resultScreenshotName = 'screenshot.jpg'; var resultsFolderName = 'results'; var resultsDir = path.join(__dirname, '..', '..', '..', resultsFolderName); this.saveResult = function(testResults) { - var promise = createResultFolder(testResults.runId); + + return createResultFolder(testResults.runId) - debug('Saving results to disk...'); + .then(function() { + return saveScreenshotIfExists(testResults); + }) - promise.then(function() { + .then(function() { - var resultFilePath = path.join(resultsDir, testResults.runId, resultFileName); - debug('Destination file is %s', resultFilePath); - - return Q.nfcall(fs.writeFile, resultFilePath, JSON.stringify(testResults, null, 2)); - }); + debug('Saving results to disk...'); - return promise; + var resultFilePath = path.join(resultsDir, testResults.runId, resultFileName); + debug('Destination file is %s', resultFilePath); + + return Q.nfcall(fs.writeFile, resultFilePath, JSON.stringify(testResults, null, 2)); + }); }; @@ -84,6 +88,33 @@ function ResultsDatastore() { return deferred.promise; } + + // If there is a screenshot, save it as screenshot.jpg in the same folder as the results + function saveScreenshotIfExists(testResults) { + var deferred = Q.defer(); + + if (testResults.screenshotBuffer) { + + var screenshotFilePath = path.join(resultsDir, testResults.runId, resultScreenshotName); + fs.writeFile(screenshotFilePath, testResults.screenshotBuffer); + + delete testResults.screenshotBuffer; + + } else { + deferred.resolve(); + } + + return deferred; + } + + this.getScreenshot = function(runId) { + + var screenshotFilePath = path.join(resultsDir, runId, resultScreenshotName); + + debug('Getting screenshot (runID = %s) from disk...', runId); + + return Q.nfcall(fs.readFile, screenshotFilePath); + }; } module.exports = ResultsDatastore; \ No newline at end of file diff --git a/lib/server/datastores/runsQueue.js b/lib/server/datastores/runsQueue.js index 26a6717..e99f1fb 100644 --- a/lib/server/datastores/runsQueue.js +++ b/lib/server/datastores/runsQueue.js @@ -1,4 +1,5 @@ var Q = require('q'); +var debug = require('debug')('ylt:runsQueue'); function RunsQueue() { @@ -11,6 +12,8 @@ function RunsQueue() { var deferred = Q.defer(); var startingPosition = queue.length; + debug('Adding run %s to the queue, position is %d', runId, startingPosition); + if (startingPosition === 0) { // The queue is empty, let's run immediatly diff --git a/lib/tools/phantomas/phantomasWrapper.js b/lib/tools/phantomas/phantomasWrapper.js index 594cde9..c24dd67 100644 --- a/lib/tools/phantomas/phantomasWrapper.js +++ b/lib/tools/phantomas/phantomasWrapper.js @@ -1,9 +1,9 @@ -var async = require('async'); -var Q = require('q'); -var ps = require('ps-node'); -var path = require('path'); -var debug = require('debug')('ylt:phantomaswrapper'); -var phantomas = require('phantomas'); +var async = require('async'); +var Q = require('q'); +var ps = require('ps-node'); +var path = require('path'); +var debug = require('debug')('ylt:phantomaswrapper'); +var phantomas = require('phantomas'); var PhantomasWrapper = function() { @@ -20,17 +20,18 @@ var PhantomasWrapper = function() { this.execute = function(data) { var deferred = Q.defer(); - var task = data.params; + var options = { // Cusomizable options - timeout: task.options.timeout || 60, + 'timeout': task.options.timeout || 60, 'js-deep-analysis': task.options.jsDeepAnalysis || false, 'user-agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.110 Safari/537.36', + 'screenshot': task.options.screenshot || false, // Mandatory - reporter: 'json:pretty', + 'reporter': 'json:pretty', 'analyze-css': true, 'skip-modules': [ 'blockDomains', // not needed @@ -44,7 +45,6 @@ var PhantomasWrapper = function() { 'jQuery', // overridden 'jserrors', // overridden 'pageSource', // not needed - 'screenshot', // not needed for the moment 'waitForSelector', // not needed 'windowPerformance' // overriden ].join(','), @@ -100,7 +100,7 @@ var PhantomasWrapper = function() { debug('Returning from Phantomas'); // Adding some YellowLabTools errors here - if (json && json.metrics && !json.metrics.javascriptExecutionTree) { + if (json && json.metrics && (!json.metrics.javascriptExecutionTree || !json.offenders.javascriptExecutionTree)) { err = 1001; } @@ -130,10 +130,11 @@ var PhantomasWrapper = function() { if (err) { debug('All ' + triesNumber + ' attemps failed for the test'); deferred.reject(err); + } else { - // Success!!! deferred.resolve(json); + } }); diff --git a/package.json b/package.json index e2d31ec..264a7f1 100644 --- a/package.json +++ b/package.json @@ -16,10 +16,12 @@ "cors": "^2.5.2", "debug": "~2.1.0", "express": "~4.10.6", + "lwip": "0.0.6", "phantomas": "1.9.0", "ps-node": "0.0.3", "q": "~1.1.2", - "rimraf": "~2.2.8" + "rimraf": "~2.2.8", + "temporary": "0.0.8" }, "devDependencies": { "chai": "^1.10.0", diff --git a/screenshot.png b/screenshot.png deleted file mode 100644 index b95be34..0000000 Binary files a/screenshot.png and /dev/null differ diff --git a/test/api/apiTest.js b/test/api/apiTest.js index bb20128..4e960d2 100644 --- a/test/api/apiTest.js +++ b/test/api/apiTest.js @@ -16,6 +16,7 @@ describe('api', function() { var syncRunResultUrl; var asyncRunId; + var screenshotUrl; it('should refuse a query with an invalid key', function(done) { @@ -94,7 +95,8 @@ describe('api', function() { url: serverUrl + '/api/runs', body: { url: wwwUrl + '/simple-page.html', - waitForResponse: true + waitForResponse: true, + screenshot: true }, json: true, headers: { @@ -131,7 +133,6 @@ describe('api', function() { }, function(error, response, body) { if (!error && response.statusCode === 302) { - console.log(response.headers.location); response.headers.should.have.a.property('location').that.is.a('string'); response.headers.location.should.contain('/rules'); @@ -160,6 +161,16 @@ describe('api', function() { body.should.have.a.property('toolsResults').that.is.an('object'); body.should.have.a.property('javascriptExecutionTree').that.is.an('object'); + // Check if the screenshot temporary file was correctly removed + body.params.options.should.not.have.a.property('screenshot'); + // Check if the screenshot buffer was correctly removed + body.should.not.have.a.property('screenshotBuffer'); + // Check if the screenshot url is here + body.should.have.a.property('screenshotUrl'); + body.screenshotUrl.should.equal('/api/results/' + body.runId + '/screenshot.jpg'); + + screenshotUrl = body.screenshotUrl; + done(); } else { @@ -516,4 +527,37 @@ describe('api', function() { }); }); + + it('should retrieve the screenshot', function(done) { + this.timeout(5000); + + request({ + method: 'GET', + url: serverUrl + screenshotUrl + }, function(error, response, body) { + if (!error && response.statusCode === 200) { + response.headers['content-type'].should.equal('image/jpeg'); + done(); + } else { + done(error || response.statusCode); + } + }); + }); + + + it('should fail on a unexistant screenshot', function(done) { + this.timeout(5000); + + request({ + method: 'GET', + url: serverUrl + '/api/results/000000/screenshot.jpg' + }, function(error, response, body) { + if (!error && response.statusCode === 404) { + done(); + } else { + done(error || response.statusCode); + } + }); + }); + }); diff --git a/test/api/resultsDatastoreTest.js b/test/api/resultsDatastoreTest.js index 4e8bdaf..da181e7 100644 --- a/test/api/resultsDatastoreTest.js +++ b/test/api/resultsDatastoreTest.js @@ -1,6 +1,9 @@ var should = require('chai').should(); var resultsDatastore = require('../../lib/server/datastores/resultsDatastore'); +var fs = require('fs'); +var path = require('path'); + describe('resultsDatastore', function() { var datastore = new resultsDatastore(); @@ -71,4 +74,48 @@ describe('resultsDatastore', function() { done(); }); }); + + + var testId3 = '555555'; + var testData3 = { + runId: testId3, + other: { + foo: 'foo', + bar: 2 + }, + screenshotBuffer: fs.readFileSync(path.join(__dirname, '../fixtures/logo-large.png')) + }; + + it('should store a test with a screenshot', function(done) { + + datastore.saveResult(testData3).then(function() { + done(); + }).fail(function(err) { + done(err); + }); + }); + + it('should have a normal result', function(done) { + datastore.getResult(testId3) + .then(function(results) { + + results.should.not.have.a.property('screenshot'); + + done(); + }) + .fail(function(err) { + done(err); + }); + }); + + it('should retrieve the saved image', function() { + datastore.getScreenshot(testId3) + .then(function(imageBuffer) { + imageBuffer.should.be.an.instanceof(Buffer); + done(); + }) + .fail(function(err) { + done(err); + }); + }); }); diff --git a/test/api/runsQueueTest.js b/test/api/runsQueueTest.js index 1e8798d..df306cd 100644 --- a/test/api/runsQueueTest.js +++ b/test/api/runsQueueTest.js @@ -1,5 +1,5 @@ var should = require('chai').should(); -var runsQueue = require('../../lib/server/datastores/runsQueue.js'); +var runsQueue = require('../../lib/server/datastores/runsQueue'); describe('runsQueue', function() { diff --git a/test/api/screenshotHandlerTest.js b/test/api/screenshotHandlerTest.js new file mode 100644 index 0000000..c19d0e0 --- /dev/null +++ b/test/api/screenshotHandlerTest.js @@ -0,0 +1,128 @@ +var should = require('chai').should(); +var ScreenshotHandler = require('../../lib/screenshotHandler'); + +var fs = require('fs'); +var path = require('path'); + +describe('screenshotHandler', function() { + + var imagePath = path.join(__dirname, '../fixtures/logo-large.png'); + var screenshot, lwipImage; + + + it('should open an image and return an lwip object', function(done) { + ScreenshotHandler.openImage(imagePath) + .then(function(image) { + lwipImage = image; + + lwipImage.should.be.an('object'); + lwipImage.width().should.equal(620); + lwipImage.height().should.equal(104); + + done(); + }) + .fail(function(err) { + done(err); + }); + }); + + + it('should resize an lwip image', function(done) { + ScreenshotHandler.resizeImage(lwipImage, 310) + .then(function(image) { + lwipImage = image; + + lwipImage.width().should.equal(310); + lwipImage.height().should.equal(52); + + done(); + }) + .fail(function(err) { + done(err); + }); + }); + + + it('should transform a lwip image into a buffer', function(done) { + ScreenshotHandler.toBuffer(lwipImage) + .then(function(buffer) { + buffer.should.be.an.instanceof(Buffer); + done(); + }) + .fail(function(err) { + done(err); + }); + }); + + + it('should optimize an image and return a buffered version', function(done) { + ScreenshotHandler.optimize(imagePath, 200) + .then(function(buffer) { + buffer.should.be.an.instanceof(Buffer); + done(); + }) + .fail(function(err) { + done(err); + }); + }); + + + it('should provide a temporary file object', function() { + screenshot = ScreenshotHandler.getScreenshotTempFile(); + + screenshot.should.have.a.property('getTmpFolder').that.is.a('function'); + screenshot.should.have.a.property('getTmpFilePath').that.is.a('function'); + screenshot.should.have.a.property('toThumbnail').that.is.a('function'); + screenshot.should.have.a.property('deleteTmpFile').that.is.a('function'); + }); + + + it('should have created the temporary folder', function() { + var folder = screenshot.getTmpFolder(); + fs.existsSync(folder.path).should.equal(true); + }); + + + it('should respond a temporary file', function() { + var file = screenshot.getTmpFilePath(); + file.should.have.string('/screenshot.jpg'); + }); + + + it('should delete the temp folder when there is no file', function(done) { + var tmpFolderPath = screenshot; + + screenshot.deleteTmpFile() + .delay(1000) + .then(function() { + fs.existsSync(screenshot.getTmpFolder().path).should.equal(false); + done(); + }) + .fail(function(err) { + done(err); + }); + }); + + it('should delete the temp folder with the screenshot inside', function(done) { + screenshot = ScreenshotHandler.getScreenshotTempFile(); + var tmpFolderPath = screenshot.getTmpFolder().path; + var tmpImagePath = path.join(tmpFolderPath, 'screenshot.jpg'); + + // Copy image + var testImage = fs.readFileSync(imagePath); + fs.writeFileSync(tmpImagePath, testImage); + + fs.existsSync(tmpImagePath).should.equal(true); + + screenshot.deleteTmpFile() + .delay(1000) + .then(function() { + fs.existsSync(tmpImagePath).should.equal(false); + fs.existsSync(tmpFolderPath).should.equal(false); + done(); + }) + .fail(function(err) { + done(err); + }); + }); +}); \ No newline at end of file diff --git a/test/fixtures/logo-large.png b/test/fixtures/logo-large.png new file mode 100644 index 0000000..f739504 Binary files /dev/null and b/test/fixtures/logo-large.png differ