From 792f7d8e21ef31827fa5833a229683031046f110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Sun, 14 Dec 2014 15:47:58 +0100 Subject: [PATCH 1/4] Implements a general score calculator --- Gruntfile.js | 2 +- lib/rulesChecker.js | 1 - lib/runner.js | 28 ++- lib/scoreCalculator.js | 83 +++++++ lib/server/middlewares/apiLimitsMiddleware.js | 4 +- test/api/apiTest.js | 210 +++++++++++++++++- test/core/scoreCalculatorTest.js | 18 ++ test/core/yellowlabtoolsTest.js | 15 +- test/fixtures/scoreInput.json | 68 ++++++ test/fixtures/scoreOutput.json | 34 +++ test/fixtures/scoreProfile.json | 35 +++ 11 files changed, 478 insertions(+), 20 deletions(-) create mode 100644 lib/scoreCalculator.js create mode 100644 test/core/scoreCalculatorTest.js create mode 100644 test/fixtures/scoreInput.json create mode 100644 test/fixtures/scoreOutput.json create mode 100644 test/fixtures/scoreProfile.json diff --git a/Gruntfile.js b/Gruntfile.js index ac5dc6a..81702ea 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -88,7 +88,7 @@ module.exports = function(grunt) { options: { reporter: 'spec', }, - src: ['coverage/test/api/apiTest.js'] + src: ['coverage/test/core/scoreCalculatorTest.js'] }, coverage: { options: { diff --git a/lib/rulesChecker.js b/lib/rulesChecker.js index c576c72..9033f0e 100644 --- a/lib/rulesChecker.js +++ b/lib/rulesChecker.js @@ -7,7 +7,6 @@ var RulesChecker = function() { this.check = function(data, policies) { var results = {}; - var err = null; debug('Starting checking rules'); diff --git a/lib/runner.js b/lib/runner.js index c9083c1..caa81a4 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -1,8 +1,10 @@ -var Q = require('q'); -var debug = require('debug')('ylt:yellowlabtools'); +var Q = require('q'); +var debug = require('debug')('ylt:runner'); + +var phantomasWrapper = require('./tools/phantomasWrapper'); +var rulesChecker = require('./rulesChecker'); +var scoreCalculator = require('./scoreCalculator'); -var phantomasWrapper = require('./tools/phantomasWrapper'); -var rulesChecker = require('./rulesChecker'); var Runner = function(params) { 'use strict'; @@ -27,6 +29,24 @@ var Runner = function(params) { data.rules = rulesChecker.check(data, policies); + // Scores calculator + var scoreProfileGeneric = require('./metadata/scoreProfileGeneric.json'); + data.scoreProfiles = { + generic : scoreCalculator.calculate(data, scoreProfileGeneric) + }; + + + // Get the JS Execution Tree from offenders and put in the main object + try { + data.javascriptExecutionTree = JSON.parse(data.toolsResults.phantomas.offenders.javascriptExecutionTree[0]); + } catch(e) { + debug('Could not find nor parse phantomas.offenders.javascriptExecutionTree'); + } + + delete data.toolsResults.phantomas.metrics.javascriptExecutionTree; + delete data.toolsResults.phantomas.offenders.javascriptExecutionTree; + + //Finished! deferred.resolve(data); }).fail(function(err) { diff --git a/lib/scoreCalculator.js b/lib/scoreCalculator.js new file mode 100644 index 0000000..8a0a399 --- /dev/null +++ b/lib/scoreCalculator.js @@ -0,0 +1,83 @@ +var Q = require('q'); +var debug = require('debug')('ylt:scoreCalculator'); + +var ScoreCalculator = function() { + 'use strict'; + + this.calculate = function(data, profile) { + + var results = { + categories: {} + }; + var weight; + var categoryName; + + debug('Starting calculating scores'); + + // Calculate categories + for (categoryName in profile.categories) { + var categoryResult = { + label: profile.categories[categoryName].label + }; + + var sum = 0; + var totalWeight = 0; + var rules = []; + + for (var policyName in profile.categories[categoryName].policies) { + weight = profile.categories[categoryName].policies[policyName]; + + if (data.rules[policyName]) { + sum += data.rules[policyName].score * weight; + } else { + // Max value if rule is not here + sum += 100 * weight; + debug('Warning: could not find rule %s', policyName); + } + + totalWeight += weight; + rules.push(policyName); + } + + if (totalWeight === 0) { + categoryResult.categoryScore = 100; + } else { + categoryResult.categoryScore = Math.round(sum / totalWeight); + } + + categoryResult.rules = rules; + results.categories[categoryName] = categoryResult; + } + + + // Calculate general score + var globalSum = 0; + var globalTotalWeight = 0; + + for (categoryName in profile.globalScore) { + weight = profile.globalScore[categoryName]; + + if (results.categories[categoryName]) { + globalSum += results.categories[categoryName].categoryScore * weight; + } else { + globalSum += 100 * weight; + } + globalTotalWeight += profile.globalScore[categoryName]; + } + + if (globalTotalWeight === 0) { + results.globalScore = 100; + } else { + results.globalScore = Math.round(globalSum / globalTotalWeight); + } + + + + debug('Score calculation finished:'); + debug(results); + + return results; + }; +}; + +module.exports = new ScoreCalculator(); \ No newline at end of file diff --git a/lib/server/middlewares/apiLimitsMiddleware.js b/lib/server/middlewares/apiLimitsMiddleware.js index 7c17015..cce5df2 100644 --- a/lib/server/middlewares/apiLimitsMiddleware.js +++ b/lib/server/middlewares/apiLimitsMiddleware.js @@ -16,7 +16,7 @@ var apiLimitsMiddleware = function(req, res, next) { if (!runsTable.accepts(req.connection.remoteAddress)) { // Sorry :/ debug('Too many tests launched from IP address %s', req.connection.remoteAddress); - res.status(429).send('Too Many Requests'); + res.status(429).send('Too many requests'); return; } @@ -25,7 +25,7 @@ var apiLimitsMiddleware = function(req, res, next) { if (!callsTable.accepts(req.connection.remoteAddress)) { // Sorry :/ debug('Too many API requests from IP address %s', req.connection.remoteAddress); - res.status(429).send('Too Many Requests'); + res.status(429).send('Too many requests'); return; } diff --git a/test/api/apiTest.js b/test/api/apiTest.js index da46653..44c599f 100644 --- a/test/api/apiTest.js +++ b/test/api/apiTest.js @@ -8,25 +8,30 @@ var config = { } }; -var apiUrl = 'http://localhost:8387/api'; +var serverUrl = 'http://localhost:8387'; var wwwUrl = 'http://localhost:8388'; describe('api', function() { - var runId; + + var syncRunResultUrl; + var asyncRunId; var apiServer; + + // Start the server before(function(done) { apiServer = require('../../bin/server.js'); apiServer.startTests = done; }); + it('should refuse a query with an invalid key', function(done) { this.timeout(5000); request({ method: 'POST', - url: apiUrl + '/runs', + url: serverUrl + '/api/runs', body: { url: wwwUrl + '/simple-page.html', waitForResponse: false @@ -44,12 +49,67 @@ describe('api', function() { }); }); - it('should accept a query with a valid key', function(done) { + + it('should launch a synchronous run', function(done) { + this.timeout(15000); + + request({ + method: 'POST', + url: serverUrl + '/api/runs', + body: { + url: wwwUrl + '/simple-page.html', + waitForResponse: true + }, + json: true, + headers: { + 'X-Api-Key': Object.keys(config.authorizedKeys)[0] + } + }, function(error, response, body) { + if (!error && response.statusCode === 302) { + + response.headers.should.have.a.property('location').that.is.a('string'); + syncRunResultUrl = response.headers.location; + + done(); + } else { + done(error || response.statusCode); + } + }); + }); + + + it('should retrieve the results for the synchronous run', function(done) { + this.timeout(15000); + + request({ + method: 'GET', + url: serverUrl + syncRunResultUrl, + json: true, + }, function(error, response, body) { + if (!error && response.statusCode === 200) { + + body.should.have.a.property('runId').that.is.a('string'); + body.should.have.a.property('params').that.is.an('object'); + body.should.have.a.property('scoreProfiles').that.is.an('object'); + body.should.have.a.property('rules').that.is.an('object'); + body.should.have.a.property('toolsResults').that.is.an('object'); + body.should.have.a.property('javascriptExecutionTree').that.is.an('object'); + + done(); + + } else { + done(error || response.statusCode); + } + }); + }); + + + it('should launch a run without waiting for the response', function(done) { this.timeout(5000); request({ method: 'POST', - url: apiUrl + '/runs', + url: serverUrl + '/api/runs', body: { url: wwwUrl + '/simple-page.html', waitForResponse: false @@ -61,8 +121,35 @@ describe('api', function() { }, function(error, response, body) { if (!error && response.statusCode === 200) { - runId = body.runId; - runId.should.be.a('string'); + asyncRunId = body.runId; + asyncRunId.should.be.a('string'); + done(); + + } else { + done(error || response.statusCode); + } + }); + }); + + + it('should respond run status: running', function(done) { + this.timeout(5000); + + request({ + method: 'GET', + url: serverUrl + '/api/runs/' + asyncRunId, + json: true, + headers: { + 'X-Api-Key': Object.keys(config.authorizedKeys)[0] + } + }, function(error, response, body) { + if (!error && response.statusCode === 200) { + + body.runId.should.equal(asyncRunId); + body.status.should.deep.equal({ + statusCode: 'running' + }); + done(); } else { @@ -79,13 +166,16 @@ describe('api', function() { request({ method: 'POST', - url: apiUrl + '/runs', + url: serverUrl + '/api/runs', body: { url: wwwUrl + '/simple-page.html', waitForResponse: false }, json: true }, function(error, response, body) { + + lastRunId = body.runId; + if (error) { deferred.reject(error); } else { @@ -131,6 +221,110 @@ describe('api', function() { }); + + it('should respond 404 to unknown runId', function(done) { + this.timeout(5000); + + request({ + method: 'GET', + url: serverUrl + '/api/runs/unknown', + json: true + }, function(error, response, body) { + if (!error && response.statusCode === 404) { + done(); + } else { + done(error || response.statusCode); + } + }); + }); + + + it('should respond 404 to unknown result', function(done) { + this.timeout(5000); + + request({ + method: 'GET', + url: serverUrl + '/api/results/unknown', + json: true + }, function(error, response, body) { + if (!error && response.statusCode === 404) { + done(); + } else { + done(error || response.statusCode); + } + }); + }); + + + it('should respond status complete to the first run', function(done) { + this.timeout(12000); + + function checkStatus() { + request({ + method: 'GET', + url: serverUrl + '/api/runs/' + asyncRunId, + json: true + }, function(error, response, body) { + if (!error && response.statusCode === 200) { + + body.runId.should.equal(asyncRunId); + + if (body.status.statusCode === 'running') { + setTimeout(checkStatus, 250); + } else if (body.status.statusCode === 'complete') { + done(); + } else { + done(body.status.statusCode); + } + + } else { + done(error || response.statusCode); + } + }); + } + + checkStatus(); + }); + + + it('should find the result of the async run', function(done) { + this.timeout(5000); + + request({ + method: 'GET', + url: serverUrl + '/api/results/' + asyncRunId, + json: true, + }, function(error, response, body) { + if (!error && response.statusCode === 200) { + + body.should.have.a.property('runId').that.equals(asyncRunId); + body.should.have.a.property('params').that.is.an('object'); + body.params.url.should.equal(wwwUrl + '/simple-page.html'); + + body.should.have.a.property('scoreProfiles').that.is.an('object'); + body.scoreProfiles.should.have.a.property('generic').that.is.an('object'); + body.scoreProfiles.generic.should.have.a.property('globalScore').that.is.a('number'); + body.scoreProfiles.generic.should.have.a.property('categories').that.is.an('object'); + + body.should.have.a.property('rules').that.is.an('object'); + + body.should.have.a.property('toolsResults').that.is.an('object'); + body.toolsResults.should.have.a.property('phantomas').that.is.an('object'); + + body.should.have.a.property('javascriptExecutionTree').that.is.an('object'); + body.javascriptExecutionTree.should.have.a.property('data').that.is.an('object'); + body.javascriptExecutionTree.data.should.have.a.property('type').that.equals('main'); + + done(); + + } else { + done(error || response.statusCode); + } + }); + }); + + + // Stop the server after(function() { console.log('Closing the server'); apiServer.close(); diff --git a/test/core/scoreCalculatorTest.js b/test/core/scoreCalculatorTest.js new file mode 100644 index 0000000..171fe93 --- /dev/null +++ b/test/core/scoreCalculatorTest.js @@ -0,0 +1,18 @@ +var should = require('chai').should(); +var scoreCalculator = require('../../lib/scoreCalculator'); + +describe('scoreCalculator', function() { + + it('should have a method calculate', function() { + scoreCalculator.should.have.property('calculate').that.is.a('function'); + }); + + it('should produce a nice rules object', function() { + var data = require('../fixtures/scoreInput.json'); + var profile = require('../fixtures/scoreProfile.json'); + var expected = require('../fixtures/scoreOutput.json'); + + var results = scoreCalculator.calculate(data, profile); + results.should.deep.equals(expected); + }); +}); diff --git a/test/core/yellowlabtoolsTest.js b/test/core/yellowlabtoolsTest.js index b1dd85e..eea43e5 100644 --- a/test/core/yellowlabtoolsTest.js +++ b/test/core/yellowlabtoolsTest.js @@ -9,28 +9,28 @@ chai.use(sinonChai); describe('yellowlabtools', function() { - it('returns a promise', function() { + it('should return a promise', function() { var ylt = new YellowLabTools(); ylt.should.have.property('then').that.is.a('function'); ylt.should.have.property('fail').that.is.a('function'); }); - it('fails an undefined url', function(done) { + it('should fail an undefined url', function(done) { var ylt = new YellowLabTools().fail(function(err) { err.should.be.a('string').that.equals('URL missing'); done(); }); }); - it('fails with an empty url string', function(done) { + it('should fail with an empty url string', function(done) { var ylt = new YellowLabTools('').fail(function(err) { err.should.be.a('string').that.equals('URL missing'); done(); }); }); - it('succeeds on simple-page.html', function(done) { + it('should succeeds on simple-page.html', function(done) { this.timeout(15000); // Check if console.log is called @@ -75,6 +75,13 @@ describe('yellowlabtools', function() { "offenders": ["body > h1[1]"] }); + // Test javascriptExecutionTree + data.toolsResults.phantomas.metrics.should.not.have.a.property('javascriptExecutionTree'); + data.toolsResults.phantomas.offenders.should.not.have.a.property('javascriptExecutionTree'); + data.should.have.a.property('javascriptExecutionTree').that.is.an('object'); + data.javascriptExecutionTree.should.have.a.property('data'); + data.javascriptExecutionTree.data.should.have.a.property('type').that.equals('main'); + /*jshint expr: true*/ console.log.should.not.have.been.called; diff --git a/test/fixtures/scoreInput.json b/test/fixtures/scoreInput.json new file mode 100644 index 0000000..81e6a4f --- /dev/null +++ b/test/fixtures/scoreInput.json @@ -0,0 +1,68 @@ +{ + "rules": { + "metric1": { + "policy": { + "tool": "tool1", + "label": "The metric 1", + "message": "A great message", + "isOkThreshold": 1000, + "isBadThreshold": 3000, + "isAbnormalThreshold": 5000 + }, + "value": 1236, + "bad": true, + "abnormal": false, + "score": 88, + "abnormalityScore": 0 + }, + "metric2": { + "value": 222, + "bad": false, + "abnormal": false, + "score": 100, + "abnormalityScore": 0 + }, + "metric3": { + "value": 6666, + "bad": true, + "abnormal": true, + "score": 0, + "abnormalityScore": -42 + }, + "metric4": { + "value": 1000, + "bad": false, + "abnormal": false, + "score": 100, + "abnormalityScore": 0 + }, + "metric5": { + "value": 3000, + "bad": true, + "abnormal": false, + "score": 0, + "abnormalityScore": 0 + }, + "metric6": { + "value": 0, + "bad": false, + "abnormal": false, + "score": 100, + "abnormalityScore": 0 + }, + "metric7": { + "value": 5000, + "bad": true, + "abnormal": true, + "score": 0, + "abnormalityScore": 0 + }, + "metric8": { + "value": 22, + "bad": true, + "abnormal": true, + "score": 0, + "abnormalityScore": -100 + } + } +} \ No newline at end of file diff --git a/test/fixtures/scoreOutput.json b/test/fixtures/scoreOutput.json new file mode 100644 index 0000000..4630bbf --- /dev/null +++ b/test/fixtures/scoreOutput.json @@ -0,0 +1,34 @@ +{ + "globalScore": 69, + "categories": { + "category1": { + "label": "Category 1", + "categoryScore": 87, + "rules": [ + "metric1", + "metric2", + "metric3", + "metric4" + ] + }, + "category2": { + "label": "Category 2", + "categoryScore": 31, + "rules": [ + "metric5", + "metric6", + "metric7", + "metric8", + "unexistantMetric1" + ] + }, + "category3": { + "label": "Category 3", + "categoryScore": 100, + "rules": [ + "unexistantMetric1", + "unexistantMetric2" + ] + } + } +} \ No newline at end of file diff --git a/test/fixtures/scoreProfile.json b/test/fixtures/scoreProfile.json new file mode 100644 index 0000000..8856126 --- /dev/null +++ b/test/fixtures/scoreProfile.json @@ -0,0 +1,35 @@ +{ + "categories": { + "category1": { + "label": "Category 1", + "policies": { + "metric1": 2, + "metric2": 1, + "metric3": 0.5, + "metric4": 2 + } + }, + "category2": { + "label": "Category 2", + "policies": { + "metric5": 2, + "metric6": 1, + "metric7": 0.5, + "metric8": 2, + "unexistantMetric1": 1 + } + }, + "category3": { + "label": "Category 3", + "policies": { + "unexistantMetric1": 2, + "unexistantMetric2": 1 + } + } + }, + "globalScore": { + "category1": 2, + "category2": 1, + "category3": 0.1 + } +} \ No newline at end of file From 90dac7dd19efef1d04335bafad90385577e6dcb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Sun, 14 Dec 2014 21:35:46 +0100 Subject: [PATCH 2/4] Created the generic score profile --- lib/metadata/policies.json | 28 +++---- lib/metadata/scoreProfileGeneric.json | 105 +++++++++++++++++++++++++- 2 files changed, 118 insertions(+), 15 deletions(-) diff --git a/lib/metadata/policies.json b/lib/metadata/policies.json index 40df848..c858342 100644 --- a/lib/metadata/policies.json +++ b/lib/metadata/policies.json @@ -2,7 +2,7 @@ "DOMelementsCount": { "tool": "phantomas", "label": "DOM elements count", - "message": "

A high number of DOM elements means a lot of work for the browser to render the page.

It also slows down Javascript DOM queries, as there are more elements to search through.

", + "message": "

A high number of DOM elements means a lot of work for the browser to render the page.

It also slows down JavaScript DOM queries, as there are more elements to search through.

", "isOkThreshold": 1000, "isBadThreshold": 3000, "isAbnormalThreshold": 5000 @@ -10,7 +10,7 @@ "DOMelementMaxDepth": { "tool": "phantomas", "label": "DOM max depth", - "message": "

A deep DOM makes the CSS matching with DOM elements difficult.

It also slows down Javascript modifications to the DOM because changing the dimensions of an element makes the browser re-calculate the dimensions of it's parents. Same thing for Javascript events, that bubble up to the document root.

", + "message": "

A deep DOM makes the CSS matching with DOM elements difficult.

It also slows down JavaScript modifications to the DOM because changing the dimensions of an element makes the browser re-calculate the dimensions of it's parents. Same thing for JavaScript events, that bubble up to the document root.

", "isOkThreshold": 10, "isBadThreshold": 20, "isAbnormalThreshold": 30 @@ -34,7 +34,7 @@ "DOMinserts": { "tool": "phantomas", "label": "DOM inserts", - "message": "

Working with the DOM in Javascript triggers layout calculations and slows down the page.

Try, as much as possible, to have an HTML page fully generated by the server instead of making changes with JS.

", + "message": "

Working with the DOM in JavaScript triggers layout calculations and slows down the page.

Try, as much as possible, to have an HTML page fully generated by the server instead of making changes with JS.

", "isOkThreshold": 10, "isBadThreshold": 400, "isAbnormalThreshold": 1000 @@ -42,7 +42,7 @@ "DOMqueries": { "tool": "phantomas", "label": "DOM queries", - "message": "

DOM queries are like looking in a large catalog of items. Even if the browsers made progress on the performances of queries, websites often make hundreds of them.

Try to reduce the number of queries by refactoring your Javascript code.

Avoid also to have a read query between two write queries. To be able to reduce the number repaints and optimize performances, browsers buffer the DOM writing operations and treat them in bulk. But each time a DOM reading is asked, the browser needs to empty the buffer. This can be particularly slow inside a loop.

", + "message": "

DOM queries are like looking in a large catalog of items. Even if the browsers made progress on the performances of queries, websites often make hundreds of them.

Try to reduce the number of queries by refactoring your JavaScript code.

Avoid also to have a read query between two write queries. To be able to reduce the number repaints and optimize performances, browsers buffer the DOM writing operations and treat them in bulk. But each time a DOM reading is asked, the browser needs to empty the buffer. This can be particularly slow inside a loop.

", "isOkThreshold": 50, "isBadThreshold": 1000, "isAbnormalThreshold": 2000 @@ -65,7 +65,7 @@ }, "jsErrors": { "tool": "phantomas", - "label": "Javascript errors", + "label": "JavaScript errors", "message": "

Just to let you know there are some errors on the page.

Please note that some errors only occur in the PhantomJS browser, so you might need to double check on other browsers.

", "isOkThreshold": 0, "isBadThreshold": 1, @@ -106,7 +106,7 @@ "inBodyDomManipulations": { "tool": "ylt", "label": "DOM manipulations in body", - "message": "

This metric counts the number of DOM queries, DOM inserts, binds, etc. made by the Javascript before the DOMContentLoaded event.

Wait for this event before manipulating the DOM. Do not execute Javascript in the middle of the BODY as it slows down the construction of the DOM and makes a poor maintainability. This is what i call spaghetti code.

The JS Timeline tab can help you identify what's happening.

", + "message": "

This metric counts the number of DOM queries, DOM inserts, binds, etc. made by the JavaScript before the DOMContentLoaded event.

Wait for this event before manipulating the DOM. Do not execute JavaScript in the middle of the BODY as it slows down the construction of the DOM and makes a poor maintainability. This is what i call spaghetti code.

The JS Timeline tab can help you identify what's happening.

", "isOkThreshold": 10, "isBadThreshold": 50, "isAbnormalThreshold": 100 @@ -125,6 +125,14 @@ "message": "

jQuery is a heavy library. You should never load jQuery more than one on the same page.

", "isOkThreshold": 1, "isBadThreshold": 2, + "isAbnormalThreshold": 2 + }, + "cssParsingErrors": { + "tool": "phantomas", + "label": "CSS syntax error", + "message": "

Yellow Lab Tools failed to parse a CSS file. I doubt the problem comes from the css parser.

Maybe a CSS validator can help you.

", + "isOkThreshold": 1, + "isBadThreshold": 2, "isAbnormalThreshold": 3 }, "cssRules": { @@ -151,14 +159,6 @@ "isBadThreshold": 50, "isAbnormalThreshold": 100 }, - "cssParsingErrors": { - "tool": "phantomas", - "label": "CSS syntax error", - "message": "

Yellow Lab Tools failed to parse a CSS file. I doubt the problem comes from the css parser.

Maybe a CSS validator can help you.

", - "isOkThreshold": 1, - "isBadThreshold": 2, - "isAbnormalThreshold": 3 - }, "cssImports": { "tool": "phantomas", "label": "Uses of @import", diff --git a/lib/metadata/scoreProfileGeneric.json b/lib/metadata/scoreProfileGeneric.json index 544b7b4..15bf1e0 100644 --- a/lib/metadata/scoreProfileGeneric.json +++ b/lib/metadata/scoreProfileGeneric.json @@ -1,3 +1,106 @@ { - + "categories": { + "domComplexity": { + "label": "DOM complexity", + "policies": { + "DOMelementsCount": 1, + "DOMelementMaxDepth": 1, + "iframesCount": 1, + "DOMidDuplicated": 1 + } + }, + "domManipulations": { + "label": "DOM manipulations", + "policies": { + "DOMinserts": 2, + "DOMqueries": 1, + "DOMqueriesAvoidable": 2, + "eventsBound": 1 + } + }, + "badJavascript": { + "label": "Bad JavaScript", + "policies": { + "jsErrors": 2, + "evalCalls": 1, + "documentWriteCalls": 2, + "consoleMessages": 0.5, + "globalVariables": 0.5, + "inBodyDomManipulations": 1 + } + }, + "jQueryVersion": { + "label": "jQuery version", + "policies": { + "jQueryVersion": 1, + "jQueryDifferentVersions": 5 + } + }, + "cssSyntaxError": { + "label": "CSS syntax errors", + "policies": { + "cssParsingErrors": 1 + } + }, + "cssComplexity": { + "label": "CSS complexity", + "policies": { + "cssRules": 2, + "cssComplexSelectors": 2, + "cssComplexSelectorsByAttribute": 1.5 + } + }, + "badCSS": { + "label": "Bad CSS", + "policies": { + "cssImports": 3, + "cssDuplicatedSelectors": 2, + "cssDuplicatedProperties": 1, + "cssEmptyRules": 2, + "cssExpressions": 1, + "cssImportants": 3, + "cssOldIEFixes": 1, + "cssOldPropertyPrefixes": 1, + "cssUniversalSelectors": 1, + "cssRedundantBodySelectors": 1, + "cssRedundantChildNodesSelectors": 1 + } + }, + "requests": { + "label": "Requests number", + "policies": { + "request": 5, + "htmlCount": 0, + "jsCount": 1, + "cssCount": 1, + "imageCount": 0, + "webfontCount": 2, + "videoCount": 0, + "jsonCount": 0, + "otherCount": 0 + } + }, + "network": { + "label": "Network", + "policies": { + "notFound": 3, + "closedConnections": 3, + "multipleRequests": 3, + "cachingDisabled": 1, + "cachingTooShort": 1, + "domains": 1 + } + } + }, + "globalScore": { + "domComplexity": 1, + "domManipulations": 2, + "badJavascript": 1, + "jQueryVersion": 1, + "cssSyntaxError": 1, + "cssComplexity": 1, + "badCSS": 1, + "requests": 3, + "network": 2 + } } \ No newline at end of file From 2ab843efdbc55ebb67b92d987fed224e6fada82b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Sun, 14 Dec 2014 22:36:25 +0100 Subject: [PATCH 3/4] Added some sub-requests to the API --- Gruntfile.js | 2 +- lib/server/controllers/apiController.js | 53 ++++++++-- test/api/apiTest.js | 128 ++++++++++++++++++++++++ 3 files changed, 174 insertions(+), 9 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 81702ea..ac5dc6a 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -88,7 +88,7 @@ module.exports = function(grunt) { options: { reporter: 'spec', }, - src: ['coverage/test/core/scoreCalculatorTest.js'] + src: ['coverage/test/api/apiTest.js'] }, coverage: { options: { diff --git a/lib/server/controllers/apiController.js b/lib/server/controllers/apiController.js index a1c0952..0f976d7 100644 --- a/lib/server/controllers/apiController.js +++ b/lib/server/controllers/apiController.js @@ -60,7 +60,6 @@ var ApiController = function(app) { // Send result if the user was waiting if (run.params.waitForResponse) { - res.redirect(302, '/api/results/' + run.runId); } @@ -133,21 +132,59 @@ var ApiController = function(app) { // Retrive one result by id app.get('/api/results/:id', function(req, res) { - var runId = req.params.id; + getPartialResults(req.params.id, res, function(data) { + return data; + }); + }); + // Retrieve one result and return only the generalScores part of the response + app.get('/api/results/:id/generalScores', function(req, res) { + getPartialResults(req.params.id, res, function(data) { + return data.scoreProfiles.generic; + }); + }); + + app.get('/api/results/:id/generalScores/:scoreProfile', function(req, res) { + getPartialResults(req.params.id, res, function(data) { + return data.scoreProfiles[req.params.scoreProfile]; + }); + }); + + app.get('/api/results/:id/rules', function(req, res) { + getPartialResults(req.params.id, res, function(data) { + return data.rules; + }); + }); + + app.get('/api/results/:id/javascriptExecutionTree', function(req, res) { + getPartialResults(req.params.id, res, function(data) { + return data.javascriptExecutionTree; + }); + }); + + app.get('/api/results/:id/toolsResults/phantomas', function(req, res) { + getPartialResults(req.params.id, res, function(data) { + return data.toolsResults.phantomas; + }); + }); + + function getPartialResults(runId, res, partialGetterFn) { resultsDatastore.getResult(runId) .then(function(data) { - // This is the pivot format, we might need to clean it first? - - // Hide phantomas results - data.toolsResults.phantomas = {}; + var results = partialGetterFn(data); + if (typeof results === 'undefined') { + res.status(404).send('Not found'); + return; + } + res.setHeader('Content-Type', 'application/json'); - res.send(JSON.stringify(data, null, 2)); + res.send(JSON.stringify(results, null, 2)); + }).fail(function() { res.status(404).send('Not found'); }); - }); + } }; diff --git a/test/api/apiTest.js b/test/api/apiTest.js index 44c599f..f7ff020 100644 --- a/test/api/apiTest.js +++ b/test/api/apiTest.js @@ -324,6 +324,134 @@ describe('api', function() { }); + it('should return the generic score object', function(done) { + this.timeout(5000); + + request({ + method: 'GET', + url: serverUrl + '/api/results/' + asyncRunId + '/generalScores', + json: true, + }, function(error, response, body) { + if (!error && response.statusCode === 200) { + body.should.have.a.property('globalScore').that.is.a('number'); + body.should.have.a.property('categories').that.is.an('object'); + done(); + + } else { + done(error || response.statusCode); + } + }); + }); + + + it('should return the generic score object also', function(done) { + this.timeout(5000); + + request({ + method: 'GET', + url: serverUrl + '/api/results/' + asyncRunId + '/generalScores/generic', + json: true, + }, function(error, response, body) { + if (!error && response.statusCode === 200) { + body.should.have.a.property('globalScore').that.is.a('number'); + body.should.have.a.property('categories').that.is.an('object'); + done(); + + } else { + done(error || response.statusCode); + } + }); + }); + + + it('should not find an unknown score object', function(done) { + this.timeout(5000); + + request({ + method: 'GET', + url: serverUrl + '/api/results/' + asyncRunId + '/generalScores/unknown', + json: true, + }, function(error, response, body) { + if (!error && response.statusCode === 404) { + done(); + } else { + done(error || response.statusCode); + } + }); + }); + + + it('should return the rules', function(done) { + this.timeout(5000); + + request({ + method: 'GET', + url: serverUrl + '/api/results/' + asyncRunId + '/rules', + json: true, + }, function(error, response, body) { + if (!error && response.statusCode === 200) { + + var firstRule = body[Object.keys(body)[0]]; + firstRule.should.have.a.property('policy').that.is.an('object'); + firstRule.should.have.a.property('value').that.is.a('number'); + firstRule.should.have.a.property('bad').that.is.a('boolean'); + firstRule.should.have.a.property('abnormal').that.is.a('boolean'); + firstRule.should.have.a.property('score').that.is.a('number'); + firstRule.should.have.a.property('abnormalityScore').that.is.a('number'); + + done(); + + } else { + done(error || response.statusCode); + } + }); + }); + + + it('should return the javascript execution tree', function(done) { + this.timeout(5000); + + request({ + method: 'GET', + url: serverUrl + '/api/results/' + asyncRunId + '/javascriptExecutionTree', + json: true, + }, function(error, response, body) { + if (!error && response.statusCode === 200) { + + body.should.have.a.property('data').that.is.an('object'); + body.data.should.have.a.property('type').that.equals('main'); + + done(); + + } else { + done(error || response.statusCode); + } + }); + }); + + + it('should return the phantomas results', function(done) { + this.timeout(5000); + + request({ + method: 'GET', + url: serverUrl + '/api/results/' + asyncRunId + '/toolsResults/phantomas', + json: true, + }, function(error, response, body) { + if (!error && response.statusCode === 200) { + + body.should.have.a.property('metrics').that.is.an('object'); + body.should.have.a.property('offenders').that.is.an('object'); + + done(); + + } else { + done(error || response.statusCode); + } + }); + }); + + // Stop the server after(function() { console.log('Closing the server'); From 1385e28911fb871f77edc389faa0f1696dea9219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Sun, 14 Dec 2014 22:50:56 +0100 Subject: [PATCH 4/4] Fix tests --- test/core/yellowlabtoolsTest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/core/yellowlabtoolsTest.js b/test/core/yellowlabtoolsTest.js index eea43e5..952abb2 100644 --- a/test/core/yellowlabtoolsTest.js +++ b/test/core/yellowlabtoolsTest.js @@ -62,7 +62,7 @@ describe('yellowlabtools', function() { policy: { "tool": "phantomas", "label": "DOM max depth", - "message": "

A deep DOM makes the CSS matching with DOM elements difficult.

It also slows down Javascript modifications to the DOM because changing the dimensions of an element makes the browser re-calculate the dimensions of it's parents. Same thing for Javascript events, that bubble up to the document root.

", + "message": "

A deep DOM makes the CSS matching with DOM elements difficult.

It also slows down JavaScript modifications to the DOM because changing the dimensions of an element makes the browser re-calculate the dimensions of it's parents. Same thing for JavaScript events, that bubble up to the document root.

", "isOkThreshold": 10, "isBadThreshold": 20, "isAbnormalThreshold": 30