Merge pull request #48 from gmetais/thumbnail

Display a screenshot on dashboard
This commit is contained in:
Gaël Métais
2015-02-05 23:42:15 +01:00
31 changed files with 769 additions and 90 deletions
-1
View File
@@ -330,7 +330,6 @@ module.exports = function(grunt) {
grunt.registerTask('test', [
'build',
'jshint',
'express:testSuite',
'clean:coverage',
'copy-test-server-settings',
+25 -1
View File
@@ -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%;
+38
View File
@@ -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;
}
+7
View File
@@ -0,0 +1,7 @@
.screenshot.board {
text-align: center;
}
.screenshot .screenshotWrapper {
font-size: 2.08333333333333em;
margin-bottom: 0.5em;
}
+5
View File
@@ -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'
+2 -1
View File
@@ -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);
});
+2 -1
View File
@@ -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);
});
+2 -1
View File
@@ -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);
});
@@ -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();
}]);
+2 -1
View File
@@ -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);
});
+29 -1
View File
@@ -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 {
+44
View File
@@ -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 {
+8
View File
@@ -0,0 +1,8 @@
.screenshot.board {
text-align: center;
}
.screenshot .screenshotWrapper {
font-size: 2.08333333333333em;
margin-bottom: 0.5em;
}
+2
View File
@@ -12,6 +12,7 @@
<link rel="stylesheet" type="text/css" href="/css/dashboard.css">
<link rel="stylesheet" type="text/css" href="/css/queue.css">
<link rel="stylesheet" type="text/css" href="/css/rule.css">
<link rel="stylesheet" type="text/css" href="/css/screenshot.css">
<link rel="stylesheet" type="text/css" href="/css/timeline.css">
<link rel="stylesheet" type="text/css" href="/css/about.css">
<!-- endbuild -->
@@ -27,6 +28,7 @@
<script src="/js/controllers/dashboardCtrl.js"></script>
<script src="/js/controllers/queueCtrl.js"></script>
<script src="/js/controllers/ruleCtrl.js"></script>
<script src="/js/controllers/screenshotCtrl.js"></script>
<script src="/js/controllers/timelineCtrl.js"></script>
<script src="/js/models/resultsFactory.js"></script>
<script src="/js/models/runsFactory.js"></script>
+15 -4
View File
@@ -2,10 +2,21 @@
<div class="summary board">
<div class="globalScore" ng-if="globalScore === 0 || globalScore > 0">
<h2> Global score</h2>
<div class="globalScoreDisplay">
<grade score="result.scoreProfiles.generic.globalScore" class="globalGrade"></grade>
<div class="on100">{{globalScore}}/100</div>
<div>
<h2>Global score</h2>
<div class="globalScoreDisplay">
<grade score="result.scoreProfiles.generic.globalScore" class="globalGrade"></grade>
<div class="on100">{{globalScore}}/100</div>
</div>
</div>
<div>
<a href="/result/{{result.runId}}/screenshot">
<div class="screenshotWrapper desktop">
<div>
<img class="screenshotImage" ng-src="{{result.screenshotUrl}}"/>
</div>
</div>
</a>
</div>
</div>
+14
View File
@@ -0,0 +1,14 @@
<div ng-include="'views/resultSubHeader.html'"></div>
<div class="screenshot board">
<h2>Screenshot</h2>
<div class="screenshotWrapper desktop">
<div>
<img class="screenshotImage" ng-src="{{result.screenshotUrl}}"/>
</div>
</div>
<p>(scroll on the screenshot to see under the fold)</p>
<div class="backToDashboard"><a href="#" ng-click="backToDashboard()">Back to dashboard</a></div>
</div>
+2 -2
View File
@@ -416,7 +416,7 @@ var policies = {
"message": "<p>This is the number of different colors defined in CSS.</p><p>Your CSS project will be easier to maintain if you keep a small color set.</p>",
"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'],
+5 -1
View File
@@ -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) {
+135
View File
@@ -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();
+113 -50
View File
@@ -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;
+1 -1
View File
@@ -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');
+40 -9
View File
@@ -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;
+3
View File
@@ -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
+13 -12
View File
@@ -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);
}
});
+3 -1
View File
@@ -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",
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

+46 -2
View File
@@ -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);
}
});
});
});
+47
View File
@@ -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);
});
});
});
+1 -1
View File
@@ -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() {
+128
View File
@@ -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);
});
});
});
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB