From 41c02cb7324d3cf76bf32ee924dd725df99148d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Fri, 16 Jan 2015 15:50:56 +0100 Subject: [PATCH 01/14] Remove coverage from 'grunt test-current-work' command --- Gruntfile.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 475359a..819db4f 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -141,7 +141,7 @@ module.exports = function(grunt) { options: { reporter: 'spec', }, - src: ['coverage/test/api/apiTest.js'] + src: ['test/core/offendersHelpersTest.js'] }, coverage: { options: { @@ -184,6 +184,13 @@ module.exports = function(grunt) { showStack: true } }, + 'test-current-work': { + options: { + port: 8387, + server: './bin/server.js', + showStack: true + } + }, testSuite: { options: { port: 8388, @@ -342,11 +349,7 @@ module.exports = function(grunt) { 'express:testSuite', 'clean:coverage', 'copy-test-server-settings', - 'lineremover:beforeCoverage', - 'copy:beforeCoverage', - 'blanket', - 'copy:coverage', - 'express:test', + 'express:test-current-work', 'mochaTest:test-current-work', 'clean:tmp' ]); From 49a6e4633a2d0f3c50f72ef194b743f70ff4e462 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Fri, 16 Jan 2015 15:51:45 +0100 Subject: [PATCH 02/14] Include angular-sanitize module --- bower.json | 3 ++- front/src/js/app.js | 1 + front/src/main.html | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/bower.json b/bower.json index 8609ca8..eb2c5d9 100644 --- a/bower.json +++ b/bower.json @@ -3,6 +3,7 @@ "dependencies": { "angular": "~1.3.8", "angular-route": "~1.3.8", - "angular-resource": "~1.3.7" + "angular-resource": "~1.3.7", + "angular-sanitize": "~1.4.0-beta.0" } } diff --git a/front/src/js/app.js b/front/src/js/app.js index a7f73d3..0adc65f 100644 --- a/front/src/js/app.js +++ b/front/src/js/app.js @@ -1,5 +1,6 @@ var yltApp = angular.module('YellowLabTools', [ 'ngRoute', + 'ngSanitize', 'indexCtrl', 'aboutCtrl', 'dashboardCtrl', diff --git a/front/src/main.html b/front/src/main.html index d966016..88f1dd6 100644 --- a/front/src/main.html +++ b/front/src/main.html @@ -20,6 +20,7 @@ + From bce7a1ad9a0481ec68892f7a87eaf412bae93819 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Fri, 16 Jan 2015 15:52:56 +0100 Subject: [PATCH 03/14] Add the possibility to change offenders' layout --- front/src/css/rule.css | 14 ++++ front/src/js/controllers/ruleCtrl.js | 5 +- front/src/less/rule.less | 17 ++++ front/src/views/rule.html | 13 ++- lib/metadata/policies.js | 14 +++- lib/offendersHelpers.js | 65 +++++++++++++++ lib/rulesChecker.js | 29 +++++-- test/core/indexTest.js | 3 +- test/core/offendersHelpersTest.js | 83 +++++++++++++++++++ test/core/rulesCheckerTest.js | 2 +- test/fixtures/rulesCheckerOutput.json | 6 +- ...rPolicies.json => rulesCheckerPolicies.js} | 17 +++- 12 files changed, 248 insertions(+), 20 deletions(-) create mode 100644 lib/offendersHelpers.js create mode 100644 test/core/offendersHelpersTest.js rename test/fixtures/{rulesCheckerPolicies.json => rulesCheckerPolicies.js} (86%) diff --git a/front/src/css/rule.css b/front/src/css/rule.css index 9fea690..57d9b17 100644 --- a/front/src/css/rule.css +++ b/front/src/css/rule.css @@ -82,3 +82,17 @@ font-size: 3em; margin-bottom: 1em; } +.offendersHtml { + display: inline-block; +} +.offendersHtml .domTree div { + text-align: left; + margin-left: 1em; +} +.offendersHtml .domTree div span:only-child { + font-weight: bold; +} +.offendersHtml .domTree div span:only-child span { + font-style: italic; + font-weight: normal; +} diff --git a/front/src/js/controllers/ruleCtrl.js b/front/src/js/controllers/ruleCtrl.js index 63fa2ef..e63adc1 100644 --- a/front/src/js/controllers/ruleCtrl.js +++ b/front/src/js/controllers/ruleCtrl.js @@ -22,7 +22,10 @@ ruleCtrl.controller('RuleCtrl', ['$scope', '$rootScope', '$routeParams', '$locat function init() { $scope.rule = $scope.result.rules[$scope.policyName]; - $scope.message = $sce.trustAsHtml($scope.rule.policy.message); + + if (angular.isString($scope.rule.offenders)) { + $scope.htmlOffenders = $scope.rule.offenders; + } } $scope.backToDashboard = function() { diff --git a/front/src/less/rule.less b/front/src/less/rule.less index a12463a..feddc09 100644 --- a/front/src/less/rule.less +++ b/front/src/less/rule.less @@ -88,4 +88,21 @@ font-size: 3em; margin-bottom: 1em; } +} + +.offendersHtml { + display: inline-block; + + .domTree div { + text-align: left; + margin-left: 1em; + + span:only-child { + font-weight: bold; + span { + font-style: italic; + font-weight: normal; + } + } + } } \ No newline at end of file diff --git a/front/src/views/rule.html b/front/src/views/rule.html index ee540ce..639ed91 100644 --- a/front/src/views/rule.html +++ b/front/src/views/rule.html @@ -10,24 +10,31 @@

Value: {{rule.value}}

-
+

Warning

This rule reached the abnormality threshold, which means there is a real problem you should care about.

-
+

-
{{offender}}
+
+
+

+ + +

+
+

404

Rule "{{policyName}}"" not found diff --git a/lib/metadata/policies.js b/lib/metadata/policies.js index b2b70fd..9c12353 100644 --- a/lib/metadata/policies.js +++ b/lib/metadata/policies.js @@ -1,4 +1,5 @@ var debug = require('debug')('ylt:policies'); +var offendersHelpers = require('../offendersHelpers'); var policies = { "DOMelementsCount": { @@ -15,7 +16,10 @@ var policies = { "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": 28 + "isAbnormalThreshold": 28, + "offendersTransformFn": function(offenders) { + return offendersHelpers.listOfDomPathsToHTML(offenders); + } }, "iframesCount": { "tool": "phantomas", @@ -31,7 +35,13 @@ var policies = { "message": "

IDs of HTML elements must be document-wide unique. This can cause problems with getElementById returning the wrong element.

", "isOkThreshold": 0, "isBadThreshold": 5, - "isAbnormalThreshold": 10 + "isAbnormalThreshold": 10, + "offendersTransformFn": function(offenders) { + return offenders.map(function(offender) { + var results = /^(.*): (\d) occurrences$/.exec(offender); + return '#' + results[1] + ': ' + results[2] + ' occurrences'; + }); + } }, "DOMinserts": { "tool": "phantomas", diff --git a/lib/offendersHelpers.js b/lib/offendersHelpers.js new file mode 100644 index 0000000..bbab2bf --- /dev/null +++ b/lib/offendersHelpers.js @@ -0,0 +1,65 @@ + + +var OffendersHelpers = function() { + + + this.domPathToArray = function(str) { + return str.split(/\s?>\s?/); + }; + + this.listOfDomArraysToTree = function(listOfDomArrays) { + var result = {}; + + function recursiveTreeBuilder(tree, domArray) { + if (domArray.length > 0) { + var currentDomElement = domArray.shift(domArray); + if (tree === null) { + tree = {}; + } + tree[currentDomElement] = recursiveTreeBuilder(tree[currentDomElement] || null, domArray); + return tree; + } else if (tree === null) { + return 1; + } else { + return tree + 1; + } + } + + listOfDomArrays.forEach(function(domArray) { + result = recursiveTreeBuilder(result, domArray); + }); + + return result; + }; + + this.domTreeToHTML = function(domTree) { + + function recursiveHtmlBuilder(tree) { + var html = ''; + var keys = Object.keys(tree); + + keys.forEach(function(key) { + if (isNaN(tree[key])) { + html += '
' + key + '' + recursiveHtmlBuilder(tree[key]) + '
'; + } else if (tree[key] > 1) { + html += '
' + key + ' (x' + tree[key] + ')
'; + } else { + html += '
' + key + '
'; + } + }); + + return html; + } + + return '
' + recursiveHtmlBuilder(domTree) + '
'; + }; + + this.listOfDomPathsToHTML = function(domPaths) { + var domArrays = domPaths.map(this.domPathToArray); + var domTree = this.listOfDomArraysToTree(domArrays); + return this.domTreeToHTML(domTree); + }; + +}; + +module.exports = new OffendersHelpers(); \ No newline at end of file diff --git a/lib/rulesChecker.js b/lib/rulesChecker.js index f8eabd1..7fb238d 100644 --- a/lib/rulesChecker.js +++ b/lib/rulesChecker.js @@ -24,10 +24,12 @@ var RulesChecker = function() { policy: policy }; + var offenders = []; + // Take DOMqueriesAvoidable's offenders from DOMqueriesDuplicated, for example. if (policy.takeOffendersFrom) { + var fromList = policy.takeOffendersFrom; - var offenders = []; // takeOffendersFrom option can be a string or an array of strings. if (typeof fromList === 'string') { @@ -35,16 +37,31 @@ var RulesChecker = function() { } fromList.forEach(function(from) { - offenders = offenders.concat(data.toolsResults[policy.tool].offenders[from]); + if (data.toolsResults[policy.tool] && + data.toolsResults[policy.tool].offenders && + data.toolsResults[policy.tool].offenders[from]) { + offenders = offenders.concat(data.toolsResults[policy.tool].offenders[from]); + } }); data.toolsResults[policy.tool].offenders[metricName] = offenders; + + } else if (data.toolsResults[policy.tool] && + data.toolsResults[policy.tool].offenders && + data.toolsResults[policy.tool].offenders[metricName]) { + offenders = data.toolsResults[policy.tool].offenders[metricName]; + } + + // It is possible to declare a transformation function for the offenders. + // The function should take an array of strings as single parameter and return a string. + if (policy.offendersTransformFn) { + rule.offendersCount = offenders.length; + offenders = policy.offendersTransformFn(offenders); + delete policy.offendersTransformFn; } - if (data.toolsResults[policy.tool].offenders && - data.toolsResults[policy.tool].offenders[metricName] && - data.toolsResults[policy.tool].offenders[metricName].length > 0) { - rule.offenders = data.toolsResults[policy.tool].offenders[metricName]; + if (offenders && offenders.length > 0) { + rule.offenders = offenders; } rule.bad = rule.value > policy.isOkThreshold; diff --git a/test/core/indexTest.js b/test/core/indexTest.js index 6207241..715f2f9 100644 --- a/test/core/indexTest.js +++ b/test/core/indexTest.js @@ -72,7 +72,8 @@ describe('index.js', function() { "abnormal": false, "score": 100, "abnormalityScore": 0, - "offenders": ["body > h1[1]"] + "offenders": "
body
h1[1]
", + "offendersCount": 1 }); // Test javascriptExecutionTree diff --git a/test/core/offendersHelpersTest.js b/test/core/offendersHelpersTest.js new file mode 100644 index 0000000..5c1a72c --- /dev/null +++ b/test/core/offendersHelpersTest.js @@ -0,0 +1,83 @@ +var should = require('chai').should(); +var offendersHelpers = require('../../lib/offendersHelpers'); + +describe('offendersHelpers', function() { + + describe('domPathToArray', function() { + + it('should transform a path to an array', function() { + var result = offendersHelpers.domPathToArray('body > section#page > div.alternate-color > ul.retroGuide > li[0] > div.retro-chaine.france2'); + result.should.deep.equal(['body', 'section#page', 'div.alternate-color', 'ul.retroGuide', 'li[0]', 'div.retro-chaine.france2']); + }); + + it('should work even if a space is missing', function() { + var result = offendersHelpers.domPathToArray('body > section#page> div.alternate-color > ul.retroGuide >li[0] > div.retro-chaine.france2'); + result.should.deep.equal(['body', 'section#page', 'div.alternate-color', 'ul.retroGuide', 'li[0]', 'div.retro-chaine.france2']); + }); + + }); + + describe('listOfDomArraysToTree', function() { + + it('should transform a list of arrays into a tree', function() { + var result = offendersHelpers.listOfDomArraysToTree([ + ['body', 'section#page', 'div.alternate-color', 'ul.retroGuide', 'li[0]', 'div.retro-chaine.france2'], + ['body', 'section#page', 'div.alternate-color', 'ul.retroGuide', 'li[0]', 'div.retro-chaine.france2'], + ['body', 'section#page', 'div.alternate-color', 'ul.retroGuide', 'li[1]', 'div.retro-chaine.france2'] + ]); + result.should.deep.equal({ + 'body': { + 'section#page': { + 'div.alternate-color': { + 'ul.retroGuide': { + 'li[0]': { + 'div.retro-chaine.france2': 2 + }, + 'li[1]': { + 'div.retro-chaine.france2': 1 + } + } + } + } + } + }); + }); + + }); + + describe('domTreeToHTML', function() { + + it('should transform a dom tree into HTML with the awaited format', function() { + var result = offendersHelpers.domTreeToHTML({ + 'body': { + 'ul.retroGuide': { + 'li[0]': { + 'div.retro-chaine.france2': 2 + }, + 'li[1]': { + 'div.retro-chaine.france2': 1 + } + } + } + }); + + result.should.equal('
body
ul.retroGuide
li[0]
div.retro-chaine.france2 (x2)
li[1]
div.retro-chaine.france2
'); + }); + + }); + + describe('listOfDomPathsToHTML', function() { + + it('should transform a list of path strings into HTML', function() { + var result = offendersHelpers.listOfDomPathsToHTML([ + 'body > ul.retroGuide > li[0] > div.retro-chaine.france2', + 'body > ul.retroGuide > li[1] > div.retro-chaine.france2', + 'body > ul.retroGuide > li[0] > div.retro-chaine.france2', + ]); + + result.should.equal('
body
ul.retroGuide
li[0]
div.retro-chaine.france2 (x2)
li[1]
div.retro-chaine.france2
'); + }); + + }); + +}); diff --git a/test/core/rulesCheckerTest.js b/test/core/rulesCheckerTest.js index 841c1ab..f979109 100644 --- a/test/core/rulesCheckerTest.js +++ b/test/core/rulesCheckerTest.js @@ -9,7 +9,7 @@ describe('rulesChecker', function() { it('should produce a nice rules object', function() { var data = require('../fixtures/rulesCheckerInput.json'); - var policies = require('../fixtures/rulesCheckerPolicies.json'); + var policies = require('../fixtures/rulesCheckerPolicies'); var expected = require('../fixtures/rulesCheckerOutput.json'); var results = rulesChecker.check(data, policies); diff --git a/test/fixtures/rulesCheckerOutput.json b/test/fixtures/rulesCheckerOutput.json index 5327aa5..9f6ddf4 100644 --- a/test/fixtures/rulesCheckerOutput.json +++ b/test/fixtures/rulesCheckerOutput.json @@ -25,7 +25,8 @@ "takeOffendersFrom": "metric3" }, "value": 222, - "offenders": ["offender1", "offender2"], + "offenders": "offender1 - offender2", + "offendersCount": 2, "bad": false, "abnormal": false, "score": 100, @@ -41,7 +42,8 @@ "isAbnormalThreshold": 5000 }, "value": 6666, - "offenders": ["offender1", "offender2"], + "offenders": "offender1/offender2", + "offendersCount": 2, "bad": true, "abnormal": true, "score": 0, diff --git a/test/fixtures/rulesCheckerPolicies.json b/test/fixtures/rulesCheckerPolicies.js similarity index 86% rename from test/fixtures/rulesCheckerPolicies.json rename to test/fixtures/rulesCheckerPolicies.js index 9147e01..a4fecab 100644 --- a/test/fixtures/rulesCheckerPolicies.json +++ b/test/fixtures/rulesCheckerPolicies.js @@ -1,4 +1,5 @@ -{ +var policies = { + "metric1": { "tool": "tool1", "label": "The metric 1", @@ -14,7 +15,10 @@ "isOkThreshold": 1000, "isBadThreshold": 3000, "isAbnormalThreshold": 5000, - "takeOffendersFrom": "metric3" + "takeOffendersFrom": "metric3", + "offendersTransformFn": function(offenders) { + return offenders.join(' - '); + } }, "metric3": { "tool": "tool1", @@ -22,7 +26,10 @@ "message": "A great message", "isOkThreshold": 1000, "isBadThreshold": 3000, - "isAbnormalThreshold": 5000 + "isAbnormalThreshold": 5000, + "offendersTransformFn": function(offenders) { + return offenders.join('/'); + } }, "metric4": { "tool": "tool1", @@ -81,4 +88,6 @@ "isBadThreshold": 3000, "isAbnormalThreshold": 5000 } -} \ No newline at end of file +}; + +module.exports = policies; \ No newline at end of file From a4f22a85426d52460d9ecac6ecda23638cef3927 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Sat, 17 Jan 2015 09:21:50 +0100 Subject: [PATCH 04/14] Offenders layout for DOM inserts --- front/src/css/rule.css | 53 +++++++++++++++++++-- front/src/less/rule.less | 78 +++++++++++++++++++++++++++---- lib/metadata/policies.js | 13 ++++-- lib/offendersHelpers.js | 34 +++++++++++++- test/core/offendersHelpersTest.js | 44 +++++++++++++++++ 5 files changed, 205 insertions(+), 17 deletions(-) diff --git a/front/src/css/rule.css b/front/src/css/rule.css index 57d9b17..cbdcfee 100644 --- a/front/src/css/rule.css +++ b/front/src/css/rule.css @@ -82,17 +82,64 @@ font-size: 3em; margin-bottom: 1em; } +.offenders .eltButton { + display: inline-block; + position: relative; + background: #efe; + padding: 0 0.5em; + margin: 0.2em 0; + border-radius: 0.4em; + z-index: 1; +} +.offenders .eltButton.opens { + padding-right: 0.75em; +} +.offenders .eltButton.opens:after { + position: relative; + left: 0.5em; + content: '\25BC'; + font-size: 0.8em; +} +.offenders .eltButton > div { + display: none; + position: absolute; + right: 0; + min-width: 100%; + background: inherit; + border-bottom-left-radius: 0.4em; + border-bottom-right-radius: 0.4em; + border-top: 1px solid #999; +} +.offenders .eltButton .domTree { + text-align: left; + white-space: nowrap; +} +.offenders .eltButton .domTree > div { + margin: 0.5em; +} +.offenders .eltButton .domTree > div div { + margin-left: 1em; +} +.offenders .eltButton:hover { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + background: #ffe0cc; + z-index: 2; +} +.offenders .eltButton:hover > div { + display: block; +} .offendersHtml { display: inline-block; } -.offendersHtml .domTree div { +.domTree div { text-align: left; margin-left: 1em; } -.offendersHtml .domTree div span:only-child { +.domTree div span:only-child { font-weight: bold; } -.offendersHtml .domTree div span:only-child span { +.domTree div span:only-child span { font-style: italic; font-weight: normal; } diff --git a/front/src/less/rule.less b/front/src/less/rule.less index feddc09..a54a753 100644 --- a/front/src/less/rule.less +++ b/front/src/less/rule.less @@ -90,19 +90,77 @@ } } -.offendersHtml { - display: inline-block; +.offenders { + .eltButton { + display: inline-block; + position: relative; + background: #efe; + padding: 0 0.5em; + margin: 0.2em 0; + border-radius: 0.4em; + z-index: 1; - .domTree div { - text-align: left; - margin-left: 1em; + &.opens { + padding-right: 0.75em; - span:only-child { - font-weight: bold; - span { - font-style: italic; - font-weight: normal; + &:after { + position: relative; + left: 0.5em; + content: '\25BC'; + font-size: 0.8em; + } + } + + > div { + display: none; + position: absolute; + right: 0; + min-width: 100%; + background: inherit; + border-bottom-left-radius: 0.4em; + border-bottom-right-radius: 0.4em; + border-top: 1px solid #999; + } + + .domTree { + text-align: left; + white-space: nowrap; + + > div { + margin: 0.5em; + + div { + margin-left: 1em; + } + } + } + + &:hover { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + background: #ffe0cc; + z-index: 2; + + & > div { + display: block; } } } +} + +.offendersHtml { + display: inline-block; +} + +.domTree div { + text-align: left; + margin-left: 1em; + + span:only-child { + font-weight: bold; + span { + font-style: italic; + font-weight: normal; + } + } } \ No newline at end of file diff --git a/lib/metadata/policies.js b/lib/metadata/policies.js index 9c12353..f0cdb18 100644 --- a/lib/metadata/policies.js +++ b/lib/metadata/policies.js @@ -38,8 +38,8 @@ var policies = { "isAbnormalThreshold": 10, "offendersTransformFn": function(offenders) { return offenders.map(function(offender) { - var results = /^(.*): (\d) occurrences$/.exec(offender); - return '#' + results[1] + ': ' + results[2] + ' occurrences'; + var parts = /^(.*): (\d) occurrences$/.exec(offender); + return '#' + parts[1] + ': ' + parts[2] + ' occurrences'; }); } }, @@ -49,7 +49,14 @@ var policies = { "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 + "isAbnormalThreshold": 1000, + "offendersTransformFn": function(offenders) { + return offenders.map(function(offender) { + var parts = /^"(.*)" appended to "(.*)"$/.exec(offender); + + return offendersHelpers.domPathToButton(parts[1]) + ' appended to ' + offendersHelpers.domPathToButton(parts[2]); + }); + } }, "DOMqueries": { "tool": "phantomas", diff --git a/lib/offendersHelpers.js b/lib/offendersHelpers.js index bbab2bf..24a523e 100644 --- a/lib/offendersHelpers.js +++ b/lib/offendersHelpers.js @@ -1,7 +1,6 @@ var OffendersHelpers = function() { - this.domPathToArray = function(str) { return str.split(/\s?>\s?/); @@ -60,6 +59,39 @@ var OffendersHelpers = function() { return this.domTreeToHTML(domTree); }; + this.domPathToButton = function(domPath) { + var domArray = this.domPathToArray(domPath); + var domTree = this.listOfDomPathsToHTML([domPath]); + + if (domArray[0] === 'html') { + return '
html
'; + } + if (domArray[0] === 'body') { + if (domArray.length === 1) { + return '
body
'; + } else { + return '
DOM element ' + domArray[domArray.length - 1] + '' + domTree + '
'; + } + } + if (domArray[0] === 'head') { + return '
head
'; + } + if (domArray[0] === 'DocumentFragment') { + if (domArray.length === 1) { + return '
Fragment
'; + } else { + return '
Fragment element ' + domArray[domArray.length - 1] + '' + domTree + '
'; + } + } + + // Not attached element, such as just created with document.createElement() + if (domArray.length === 1) { + return '
Created element ' + domPath + '
'; + } else { + return '
Created element ' + domArray[domArray.length - 1] + '' + domTree + '
'; + } + }; + }; module.exports = new OffendersHelpers(); \ No newline at end of file diff --git a/test/core/offendersHelpersTest.js b/test/core/offendersHelpersTest.js index 5c1a72c..30c2d15 100644 --- a/test/core/offendersHelpersTest.js +++ b/test/core/offendersHelpersTest.js @@ -80,4 +80,48 @@ describe('offendersHelpers', function() { }); + describe('domPathToButton', function() { + + it('should transform html', function() { + var result = offendersHelpers.domPathToButton('html'); + result.should.equal('
html
'); + }); + + it('should transform body', function() { + var result = offendersHelpers.domPathToButton('body'); + result.should.equal('
body
'); + }); + + it('should transform head', function() { + var result = offendersHelpers.domPathToButton('head'); + result.should.equal('
head
'); + }); + + it('should transform a standard in-body element', function() { + var result = offendersHelpers.domPathToButton('body > div#colorbox > div#cboxContent'); + result.should.equal('
DOM element div#cboxContent
body
div#colorbox
div#cboxContent
'); + }); + + it('should transform a domFragment element', function() { + var result = offendersHelpers.domPathToButton('DocumentFragment'); + result.should.equal('
Fragment
'); + }); + + it('should transform a domFragment element', function() { + var result = offendersHelpers.domPathToButton('DocumentFragment > div#colorbox > div#cboxContent'); + result.should.equal('
Fragment element div#cboxContent
DocumentFragment
div#colorbox
div#cboxContent
'); + }); + + it('should transform an not-attached element', function() { + var result = offendersHelpers.domPathToButton('div#sizcache'); + result.should.equal('
Created element div#sizcache
'); + }); + + it('should transform an not-attached element path', function() { + var result = offendersHelpers.domPathToButton('div > div#sizcache'); + result.should.equal('
Created element div#sizcache
div
div#sizcache
'); + }); + + }); + }); From 6fc502b0b2f3932bad5d59e8748a0f216a037959 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Sat, 17 Jan 2015 09:44:50 +0100 Subject: [PATCH 05/14] Offenders layout for DOM queries without results --- lib/metadata/policies.js | 25 ++++++++++++++++++++++++- lib/offendersHelpers.js | 3 +++ test/core/offendersHelpersTest.js | 5 +++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/lib/metadata/policies.js b/lib/metadata/policies.js index f0cdb18..046d9a2 100644 --- a/lib/metadata/policies.js +++ b/lib/metadata/policies.js @@ -39,6 +39,12 @@ var policies = { "offendersTransformFn": function(offenders) { return offenders.map(function(offender) { var parts = /^(.*): (\d) occurrences$/.exec(offender); + + if (!parts) { + debug('DOMidDuplicated offenders transform function error with "%s"', offender); + return offender; + } + return '#' + parts[1] + ': ' + parts[2] + ' occurrences'; }); } @@ -54,6 +60,11 @@ var policies = { return offenders.map(function(offender) { var parts = /^"(.*)" appended to "(.*)"$/.exec(offender); + if (!parts) { + debug('DOMinserts offenders transform function error with "%s"', offender); + return offender; + } + return offendersHelpers.domPathToButton(parts[1]) + ' appended to ' + offendersHelpers.domPathToButton(parts[2]); }); } @@ -81,7 +92,19 @@ var policies = { "message": "

Number of queries that return no result.

It suggests the query is not used on the page, probably because it is some dead code.

Or maybe the code is trying to find an HTML block that is not always here. Look at the JS Timeline to see if the scripts correctly figures out the HTML block is not here and immediatly stops interacting further with the DOM.

", "isOkThreshold": 0, "isBadThreshold": 100, - "isAbnormalThreshold": 200 + "isAbnormalThreshold": 200, + "offendersTransformFn": function(offenders) { + return offenders.map(function(offender) { + var parts = /^(.*) \(in (.*)\) using (.*)$/.exec(offender); + + if (!parts) { + debug('DOMqueriesWithoutResults offenders transform function error with "%s"', offender); + return offender; + } + + return '' + parts[1] + ' (in ' + offendersHelpers.domPathToButton(parts[2]) + ') using ' + parts[3] + ''; + }); + } }, "eventsBound": { "tool": "phantomas", diff --git a/lib/offendersHelpers.js b/lib/offendersHelpers.js index 24a523e..1ce47ef 100644 --- a/lib/offendersHelpers.js +++ b/lib/offendersHelpers.js @@ -76,6 +76,9 @@ var OffendersHelpers = function() { if (domArray[0] === 'head') { return '
head
'; } + if (domArray[0] === '#document') { + return '
document
'; + } if (domArray[0] === 'DocumentFragment') { if (domArray.length === 1) { return '
Fragment
'; diff --git a/test/core/offendersHelpersTest.js b/test/core/offendersHelpersTest.js index 30c2d15..4cea34c 100644 --- a/test/core/offendersHelpersTest.js +++ b/test/core/offendersHelpersTest.js @@ -97,6 +97,11 @@ describe('offendersHelpers', function() { result.should.equal('
head
'); }); + it('should transform #document', function() { + var result = offendersHelpers.domPathToButton('#document'); + result.should.equal('
document
'); + }); + it('should transform a standard in-body element', function() { var result = offendersHelpers.domPathToButton('body > div#colorbox > div#cboxContent'); result.should.equal('
DOM element div#cboxContent
body
div#colorbox
div#cboxContent
'); From 23dc46d697528d6d3b7baed52d288e6b360e9d12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Mon, 19 Jan 2015 11:24:16 +0100 Subject: [PATCH 06/14] Offenders layout for DOMqueriesAvoidable, jsErrors and globalVariables --- front/src/css/rule.css | 21 +++--- front/src/less/rule.less | 6 +- lib/metadata/policies.js | 74 +++++++++++++++---- lib/offendersHelpers.js | 68 +++++++++++++++--- test/core/offendersHelpersTest.js | 115 +++++++++++++++++++++++++++--- 5 files changed, 240 insertions(+), 44 deletions(-) diff --git a/front/src/css/rule.css b/front/src/css/rule.css index cbdcfee..4f5f41d 100644 --- a/front/src/css/rule.css +++ b/front/src/css/rule.css @@ -82,7 +82,7 @@ font-size: 3em; margin-bottom: 1em; } -.offenders .eltButton { +.offenders .offenderButton { display: inline-block; position: relative; background: #efe; @@ -91,16 +91,16 @@ border-radius: 0.4em; z-index: 1; } -.offenders .eltButton.opens { +.offenders .offenderButton.opens { padding-right: 0.75em; } -.offenders .eltButton.opens:after { +.offenders .offenderButton.opens:after { position: relative; left: 0.5em; content: '\25BC'; font-size: 0.8em; } -.offenders .eltButton > div { +.offenders .offenderButton > div { display: none; position: absolute; right: 0; @@ -110,23 +110,26 @@ border-bottom-right-radius: 0.4em; border-top: 1px solid #999; } -.offenders .eltButton .domTree { +.offenders .offenderButton .domTree { text-align: left; white-space: nowrap; } -.offenders .eltButton .domTree > div { +.offenders .offenderButton .domTree > div { margin: 0.5em; } -.offenders .eltButton .domTree > div div { +.offenders .offenderButton .domTree > div div { margin-left: 1em; } -.offenders .eltButton:hover { +.offenders .offenderButton .backtrace { + white-space: nowrap; +} +.offenders .offenderButton:hover { border-bottom-left-radius: 0; border-bottom-right-radius: 0; background: #ffe0cc; z-index: 2; } -.offenders .eltButton:hover > div { +.offenders .offenderButton:hover > div { display: block; } .offendersHtml { diff --git a/front/src/less/rule.less b/front/src/less/rule.less index a54a753..7a19d52 100644 --- a/front/src/less/rule.less +++ b/front/src/less/rule.less @@ -91,7 +91,7 @@ } .offenders { - .eltButton { + .offenderButton { display: inline-block; position: relative; background: #efe; @@ -135,6 +135,10 @@ } } + .backtrace { + white-space: nowrap; + } + &:hover { border-bottom-left-radius: 0; border-bottom-right-radius: 0; diff --git a/lib/metadata/policies.js b/lib/metadata/policies.js index 046d9a2..8016d11 100644 --- a/lib/metadata/policies.js +++ b/lib/metadata/policies.js @@ -44,7 +44,7 @@ var policies = { debug('DOMidDuplicated offenders transform function error with "%s"', offender); return offender; } - + return '#' + parts[1] + ': ' + parts[2] + ' occurrences'; }); } @@ -77,15 +77,6 @@ var policies = { "isBadThreshold": 1000, "isAbnormalThreshold": 2000 }, - "DOMqueriesAvoidable": { - "tool": "phantomas", - "label": "Duplicated DOM queries", - "message": "

This is the number of queries that could be avoided by removing all duplicated queries.

Simply save the result of a query in a variable. Ok it is not always simple, especially with third-party scripts, but at least do it with your own code.

", - "isOkThreshold": 0, - "isBadThreshold": 200, - "isAbnormalThreshold": 500, - "takeOffendersFrom": "DOMqueriesDuplicated" - }, "DOMqueriesWithoutResults": { "tool": "phantomas", "label": "DOM queries without result", @@ -102,7 +93,28 @@ var policies = { return offender; } - return '' + parts[1] + ' (in ' + offendersHelpers.domPathToButton(parts[2]) + ') using ' + parts[3] + ''; + return '' + parts[1] + ' (in ' + offendersHelpers.domPathToButton(parts[2]) + ') using ' + parts[3]; + }); + } + }, + "DOMqueriesAvoidable": { + "tool": "phantomas", + "label": "Duplicated DOM queries", + "message": "

This is the number of queries that could be avoided by removing all duplicated queries.

Simply save the result of a query in a variable. Ok it is not always simple, especially with third-party scripts, but at least do it with your own code.

", + "isOkThreshold": 0, + "isBadThreshold": 200, + "isAbnormalThreshold": 500, + "takeOffendersFrom": "DOMqueriesDuplicated", + "offendersTransformFn": function(offenders) { + return offenders.map(function(offender) { + var parts = /^.* "(.*)" with (.*) \(in context (.*)\): (.*)\s?queries$/.exec(offender); + + if (!parts) { + debug('DOMqueriesAvoidable offenders transform function error with "%s"', offender); + return offender; + } + + return '' + parts[1] + ' (in ' + offendersHelpers.domPathToButton(parts[3]) + ') using ' + parts[2] + ': ' + parts[4] + ' queries'; }); } }, @@ -112,7 +124,19 @@ var policies = { "message": "

Binding too many events has a cost.

It can be avoided by using \"event delegation\". Instead of binding events on each element one by one, events delegation binds them on the top level document element and uses the bubbling principle. It will imperceptibly slow down the event when it occurs, but the loading of the page will speed-up.

", "isOkThreshold": 100, "isBadThreshold": 800, - "isAbnormalThreshold": 1500 + "isAbnormalThreshold": 1500, + "offendersTransformFn": function(offenders) { + return offenders.map(function(offender) { + var parts = /^"(.*)" bound to "(.*)"$/.exec(offender); + + if (!parts) { + debug('eventsBound offenders transform function error with "%s"', offender); + return offender; + } + + return '' + parts[1] + ' bound to ' + offendersHelpers.domPathToButton(parts[2]); + }); + } }, "jsErrors": { "tool": "phantomas", @@ -120,7 +144,24 @@ var policies = { "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, - "isAbnormalThreshold": 4 + "isAbnormalThreshold": 4, + "offendersTransformFn": function(offenders) { + return offenders.map(function(offender) { + var parts = /^(.*) - (.*)$/.exec(offender); + + if (!parts) { + debug('jsErrors offenders transform function error with "%s"', offender); + return offender; + } + + var backtraceArray = offendersHelpers.backtraceToArray(parts[2]); + if (backtraceArray === null) { + return offender; + } else { + return '' + parts[1] + ' ' + offendersHelpers.backtraceArrayToHtml(backtraceArray); + } + }); + } }, "evalCalls": { "tool": "phantomas", @@ -152,7 +193,10 @@ var policies = { "message": "

It is a bad practice because they clutter up the global namespace. If two scripts use the same variable name in the global scope, it can cause conflicts and it is generally hard to debug.

Global variables also take a (very) little bit longer to be accessed than variables in the local scope of a function.

", "isOkThreshold": 10, "isBadThreshold": 50, - "isAbnormalThreshold": 200 + "isAbnormalThreshold": 200, + "offendersTransformFn": function(offenders) { + return offendersHelpers.sortVarsLikeChromeDevTools(offenders); + } }, "jQueryVersion": { "label": "jQuery version", @@ -210,7 +254,7 @@ var policies = { "jQueryDifferentVersions": { "tool": "phantomas", "label": "Several versions loaded", - "message": "

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

", + "message": "

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

", "isOkThreshold": 1, "isBadThreshold": 2, "isAbnormalThreshold": 2 diff --git a/lib/offendersHelpers.js b/lib/offendersHelpers.js index 1ce47ef..6b427ab 100644 --- a/lib/offendersHelpers.js +++ b/lib/offendersHelpers.js @@ -11,7 +11,7 @@ var OffendersHelpers = function() { function recursiveTreeBuilder(tree, domArray) { if (domArray.length > 0) { - var currentDomElement = domArray.shift(domArray); + var currentDomElement = domArray.shift(); if (tree === null) { tree = {}; } @@ -64,37 +64,85 @@ var OffendersHelpers = function() { var domTree = this.listOfDomPathsToHTML([domPath]); if (domArray[0] === 'html') { - return '
html
'; + return '
html
'; } if (domArray[0] === 'body') { if (domArray.length === 1) { - return '
body
'; + return '
body
'; } else { - return '
DOM element ' + domArray[domArray.length - 1] + '' + domTree + '
'; + return '
DOM element ' + domArray[domArray.length - 1] + '' + domTree + '
'; } } if (domArray[0] === 'head') { - return '
head
'; + return '
head
'; } if (domArray[0] === '#document') { - return '
document
'; + return '
document
'; + } + if (domArray[0] === 'window') { + return '
window
'; } if (domArray[0] === 'DocumentFragment') { if (domArray.length === 1) { - return '
Fragment
'; + return '
Fragment
'; } else { - return '
Fragment element ' + domArray[domArray.length - 1] + '' + domTree + '
'; + return '
Fragment element ' + domArray[domArray.length - 1] + '' + domTree + '
'; } } // Not attached element, such as just created with document.createElement() if (domArray.length === 1) { - return '
Created element ' + domPath + '
'; + return '
Created element ' + domPath + '
'; } else { - return '
Created element ' + domArray[domArray.length - 1] + '' + domTree + '
'; + return '
Created element ' + domArray[domArray.length - 1] + '' + domTree + '
'; } }; + + this.backtraceToArray = function(str) { + var traceArray = str.split(/ \/ /); + + if (traceArray) { + var results = []; + var parts = null; + + for (var i=0 ; ino backtrace
'; + } + + var html = '
backtrace
'; + backtraceArray.forEach(function(backtraceObj) { + html += '
' + backtraceObj.file + ' line ' + backtraceObj.line + '
'; + }); + return html + '
'; + }; + + + this.sortVarsLikeChromeDevTools = function(vars) { + return vars.sort(function(a, b) { + return (a < b) ? -1 : 1; + }); + }; + }; module.exports = new OffendersHelpers(); \ No newline at end of file diff --git a/test/core/offendersHelpersTest.js b/test/core/offendersHelpersTest.js index 4cea34c..bf1a887 100644 --- a/test/core/offendersHelpersTest.js +++ b/test/core/offendersHelpersTest.js @@ -84,47 +84,144 @@ describe('offendersHelpers', function() { it('should transform html', function() { var result = offendersHelpers.domPathToButton('html'); - result.should.equal('
html
'); + result.should.equal('
html
'); }); it('should transform body', function() { var result = offendersHelpers.domPathToButton('body'); - result.should.equal('
body
'); + result.should.equal('
body
'); }); it('should transform head', function() { var result = offendersHelpers.domPathToButton('head'); - result.should.equal('
head
'); + result.should.equal('
head
'); }); it('should transform #document', function() { var result = offendersHelpers.domPathToButton('#document'); - result.should.equal('
document
'); + result.should.equal('
document
'); + }); + + it('should transform window', function() { + var result = offendersHelpers.domPathToButton('window'); + result.should.equal('
window
'); }); it('should transform a standard in-body element', function() { var result = offendersHelpers.domPathToButton('body > div#colorbox > div#cboxContent'); - result.should.equal('
DOM element div#cboxContent
body
div#colorbox
div#cboxContent
'); + result.should.equal('
DOM element div#cboxContent
body
div#colorbox
div#cboxContent
'); }); it('should transform a domFragment element', function() { var result = offendersHelpers.domPathToButton('DocumentFragment'); - result.should.equal('
Fragment
'); + result.should.equal('
Fragment
'); }); it('should transform a domFragment element', function() { var result = offendersHelpers.domPathToButton('DocumentFragment > div#colorbox > div#cboxContent'); - result.should.equal('
Fragment element div#cboxContent
DocumentFragment
div#colorbox
div#cboxContent
'); + result.should.equal('
Fragment element div#cboxContent
DocumentFragment
div#colorbox
div#cboxContent
'); }); it('should transform an not-attached element', function() { var result = offendersHelpers.domPathToButton('div#sizcache'); - result.should.equal('
Created element div#sizcache
'); + result.should.equal('
Created element div#sizcache
'); }); it('should transform an not-attached element path', function() { var result = offendersHelpers.domPathToButton('div > div#sizcache'); - result.should.equal('
Created element div#sizcache
div
div#sizcache
'); + result.should.equal('
Created element div#sizcache
div
div#sizcache
'); + }); + + }); + + describe('backtraceToArray', function() { + + it('should transform a backtrace into an array', function() { + var result = offendersHelpers.backtraceToArray('http://pouet.com/js/jquery.footer-transverse-min-v1.0.20.js:1 / http://pouet.com/js/main.js:1'); + + result.should.deep.equal([ + { + file: 'http://pouet.com/js/jquery.footer-transverse-min-v1.0.20.js', + line: 1 + }, + { + file: 'http://pouet.com/js/main.js', + line: 1 + } + ]); + }); + + it('should return null if it fails', function() { + var result = offendersHelpers.backtraceToArray('http://pouet.com/js/jquery.footer-transverse-min-v1.0.20.js:1 /http://pouet.com/js/main.js:1'); + + should.equal(result, null); + }); + + }); + + describe('backtraceArrayToHtml', function() { + + it('should create a button from a backtrace array', function() { + var result = offendersHelpers.backtraceArrayToHtml([ + { + file: 'http://pouet.com/js/jquery.footer-transverse-min-v1.0.20.js', + line: 1 + }, + { + file: 'http://pouet.com/js/main.js', + line: 1 + } + ]); + + result.should.equal(''); + }); + + it('should display "no backtrace"', function() { + var result = offendersHelpers.backtraceArrayToHtml([]); + + result.should.equal('
no backtrace
'); + }); + + }); + + describe('sortVarsLikeChromeDevTools', function() { + + it('should sort in the same strange order', function() { + var result = offendersHelpers.sortVarsLikeChromeDevTools([ + 'a', + 'aaa', + 'a2', + 'b', + 'A', + 'AAA', + 'B', + '_a', + '_aaa', + '__a', + 'a_a', + 'aA', + 'a__', + '$', + '$a' + ]); + + result.should.deep.equal([ + '$', + '$a', + 'A', + 'AAA', + 'B', + '__a', + '_a', + '_aaa', + 'a', + 'a2', + 'aA', + 'a__', + 'a_a', + 'aaa', + 'b' + ]); }); }); From 4ef83a844fc81f4b9bb3d39fe39105a90d133b13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Mon, 19 Jan 2015 12:15:05 +0100 Subject: [PATCH 07/14] Fix offendersTransformFn deleted after the first run --- lib/rulesChecker.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/rulesChecker.js b/lib/rulesChecker.js index 7fb238d..8f82b68 100644 --- a/lib/rulesChecker.js +++ b/lib/rulesChecker.js @@ -1,3 +1,5 @@ +var extend = require('util')._extend; + var debug = require('debug')('ylt:ruleschecker'); var RulesChecker = function() { @@ -21,7 +23,7 @@ var RulesChecker = function() { rule = { value: data.toolsResults[policy.tool].metrics[metricName], - policy: policy + policy: extend({}, policy) // Clone object policy instead of reference }; var offenders = []; @@ -56,8 +58,15 @@ var RulesChecker = function() { // The function should take an array of strings as single parameter and return a string. if (policy.offendersTransformFn) { rule.offendersCount = offenders.length; - offenders = policy.offendersTransformFn(offenders); - delete policy.offendersTransformFn; + + try { + offenders = policy.offendersTransformFn(offenders); + } catch(err) { + debug('Error while transforming offenders for %s', metricName); + debug(err); + } + + delete rule.policy.offendersTransformFn; } if (offenders && offenders.length > 0) { From e6bd0d0006bc0cf5deda7cf77254724be897dc37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Mon, 19 Jan 2015 12:16:19 +0100 Subject: [PATCH 08/14] Strange spaces disapearance in offenders --- lib/metadata/policies.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/metadata/policies.js b/lib/metadata/policies.js index 8016d11..f1c9d7d 100644 --- a/lib/metadata/policies.js +++ b/lib/metadata/policies.js @@ -38,7 +38,7 @@ var policies = { "isAbnormalThreshold": 10, "offendersTransformFn": function(offenders) { return offenders.map(function(offender) { - var parts = /^(.*): (\d) occurrences$/.exec(offender); + var parts = /^(.*): ?(\d) ?occurrences$/.exec(offender); if (!parts) { debug('DOMidDuplicated offenders transform function error with "%s"', offender); @@ -58,7 +58,7 @@ var policies = { "isAbnormalThreshold": 1000, "offendersTransformFn": function(offenders) { return offenders.map(function(offender) { - var parts = /^"(.*)" appended to "(.*)"$/.exec(offender); + var parts = /^"(.*)" ?appended ?to ?"(.*)"$/.exec(offender); if (!parts) { debug('DOMinserts offenders transform function error with "%s"', offender); @@ -86,7 +86,7 @@ var policies = { "isAbnormalThreshold": 200, "offendersTransformFn": function(offenders) { return offenders.map(function(offender) { - var parts = /^(.*) \(in (.*)\) using (.*)$/.exec(offender); + var parts = /^(.*) ?\(in ?(.*)\) ?using ?(.*)$/.exec(offender); if (!parts) { debug('DOMqueriesWithoutResults offenders transform function error with "%s"', offender); @@ -107,7 +107,7 @@ var policies = { "takeOffendersFrom": "DOMqueriesDuplicated", "offendersTransformFn": function(offenders) { return offenders.map(function(offender) { - var parts = /^.* "(.*)" with (.*) \(in context (.*)\): (.*)\s?queries$/.exec(offender); + var parts = /^.* "(.*)" ?with ?(.*) ?\(in ?context ?(.*)\): ?(.*)\s?queries$/.exec(offender); if (!parts) { debug('DOMqueriesAvoidable offenders transform function error with "%s"', offender); @@ -127,7 +127,7 @@ var policies = { "isAbnormalThreshold": 1500, "offendersTransformFn": function(offenders) { return offenders.map(function(offender) { - var parts = /^"(.*)" bound to "(.*)"$/.exec(offender); + var parts = /^"(.*)" ?bound ?to ?"(.*)"$/.exec(offender); if (!parts) { debug('eventsBound offenders transform function error with "%s"', offender); From cac36727e8ba9367767e93849f9702afc6a07ddc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Mon, 19 Jan 2015 12:16:56 +0100 Subject: [PATCH 09/14] Handle function names in backtraces --- front/src/css/rule.css | 1 + front/src/less/rule.less | 1 + lib/offendersHelpers.js | 19 +++++++++++++------ test/core/offendersHelpersTest.js | 6 ++++-- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/front/src/css/rule.css b/front/src/css/rule.css index 4f5f41d..80474b4 100644 --- a/front/src/css/rule.css +++ b/front/src/css/rule.css @@ -122,6 +122,7 @@ } .offenders .offenderButton .backtrace { white-space: nowrap; + padding: 0.5em; } .offenders .offenderButton:hover { border-bottom-left-radius: 0; diff --git a/front/src/less/rule.less b/front/src/less/rule.less index 7a19d52..71bb103 100644 --- a/front/src/less/rule.less +++ b/front/src/less/rule.less @@ -137,6 +137,7 @@ .backtrace { white-space: nowrap; + padding: 0.5em; } &:hover { diff --git a/lib/offendersHelpers.js b/lib/offendersHelpers.js index 6b427ab..39a048f 100644 --- a/lib/offendersHelpers.js +++ b/lib/offendersHelpers.js @@ -107,13 +107,19 @@ var OffendersHelpers = function() { var parts = null; for (var i=0 ; i' + backtraceObj.file + ' line ' + backtraceObj.line + '
'; + var functionName = (backtraceObj.functionName) ? backtraceObj.functionName + '() ' : ''; + html += '
' + functionName + '' + backtraceObj.file + ' line ' + backtraceObj.line + '
'; }); return html + ''; }; diff --git a/test/core/offendersHelpersTest.js b/test/core/offendersHelpersTest.js index bf1a887..ce23d07 100644 --- a/test/core/offendersHelpersTest.js +++ b/test/core/offendersHelpersTest.js @@ -137,7 +137,7 @@ describe('offendersHelpers', function() { describe('backtraceToArray', function() { it('should transform a backtrace into an array', function() { - var result = offendersHelpers.backtraceToArray('http://pouet.com/js/jquery.footer-transverse-min-v1.0.20.js:1 / http://pouet.com/js/main.js:1'); + var result = offendersHelpers.backtraceToArray('http://pouet.com/js/jquery.footer-transverse-min-v1.0.20.js:1 / callback http://pouet.com/js/main.js:1'); result.should.deep.equal([ { @@ -145,6 +145,7 @@ describe('offendersHelpers', function() { line: 1 }, { + functionName: 'callback', file: 'http://pouet.com/js/main.js', line: 1 } @@ -168,12 +169,13 @@ describe('offendersHelpers', function() { line: 1 }, { + functionName: 'callback', file: 'http://pouet.com/js/main.js', line: 1 } ]); - result.should.equal(''); + result.should.equal(''); }); it('should display "no backtrace"', function() { From fd2271f6d6c7a24369a65ce042e7b0893a01384f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Mon, 19 Jan 2015 20:31:06 +0100 Subject: [PATCH 10/14] Offenders layout for various CSS metrics --- lib/metadata/policies.js | 192 ++++++++++++++++++++++++++++-- lib/offendersHelpers.js | 24 +++- test/core/offendersHelpersTest.js | 41 ++++++- 3 files changed, 242 insertions(+), 15 deletions(-) diff --git a/lib/metadata/policies.js b/lib/metadata/policies.js index f1c9d7d..82ce5b7 100644 --- a/lib/metadata/policies.js +++ b/lib/metadata/policies.js @@ -38,7 +38,7 @@ var policies = { "isAbnormalThreshold": 10, "offendersTransformFn": function(offenders) { return offenders.map(function(offender) { - var parts = /^(.*): ?(\d) ?occurrences$/.exec(offender); + var parts = /^(.*): ?(\d+) ?occurrences$/.exec(offender); if (!parts) { debug('DOMidDuplicated offenders transform function error with "%s"', offender); @@ -107,7 +107,7 @@ var policies = { "takeOffendersFrom": "DOMqueriesDuplicated", "offendersTransformFn": function(offenders) { return offenders.map(function(offender) { - var parts = /^.* "(.*)" ?with ?(.*) ?\(in ?context ?(.*)\): ?(.*)\s?queries$/.exec(offender); + var parts = /^[^"]* ?"(.*)" ?with ?(.*) ?\(in ?context ?(.*)\): ?(.*)\s?queries$/.exec(offender); if (!parts) { debug('DOMqueriesAvoidable offenders transform function error with "%s"', offender); @@ -265,7 +265,34 @@ var policies = { "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": 0, "isBadThreshold": 1, - "isAbnormalThreshold": 1 + "isAbnormalThreshold": 1, + "offendersTransformFn": function(offenders) { + return offenders.map(function(offender) { + var parts = /^(([^ \(]*|) ?)?(?:\((((?! @ ).)*)( @ (\d+):(\d+))?\))?$/.exec(offender); + + if (!parts) { + debug('cssParsingErrors offenders transform function error with "%s"', offender); + return offender; + } + + var html = (parts[3]) ? parts[3] : ''; + + if (parts[2]) { + html += (parts[2] === '') ? ' <inline CSS>' : ' ' + offendersHelpers.urlToLink(parts[2]); + } + + if (parts[5]) { + html += ' @ ' + parts[6] + ':' + parts[7]; + } + + if (parts[2] && parts[2] !== '') { + var w3cDirectUrl = 'http://jigsaw.w3.org/css-validator/validator?uri=' + encodeURIComponent(parts[2]) + '&profile=css3&usermedium=all&warning=no'; + html += ' (Check on the W3C validator)'; + } + + return html; + }); + } }, "cssRules": { "tool": "phantomas", @@ -281,7 +308,14 @@ var policies = { "message": "

Complex selectors are CSS selectors with 4 or more expressions, like \"#header ul li .foo\".

They are adding more work for the browser, and this could be avoided by simplifying selectors.

", "isOkThreshold": 0, "isBadThreshold": 500, - "isAbnormalThreshold": 2000 + "isAbnormalThreshold": 2000, + "offendersTransformFn": function(offenders) { + return offenders.map(function(offender) { + var splittedOffender = offendersHelpers.cssOffenderPattern(offender); + + return '' + splittedOffender.offender + ' @ ' + splittedOffender.line + ':' + splittedOffender.character; + }); + } }, "cssComplexSelectorsByAttribute": { "tool": "phantomas", @@ -289,7 +323,16 @@ var policies = { "message": "

Complex attributes selectors are one of these:

  • .foo[type*=bar] (contains bar)
  • .foo[type^=bar] (starts with bar)
  • .foo[type|=bar] (starts with bar or bar-)
  • .foo[type$=bar] (ends with bar)
  • .foo[type~=bar baz] (bar or baz)

Their matching process needs more CPU and it has a cost on performances.

", "isOkThreshold": 0, "isBadThreshold": 50, - "isAbnormalThreshold": 100 + "isAbnormalThreshold": 100, + "offendersTransformFn": function(offenders) { + return offenders.map(function(offender) { + var splittedOffender = offendersHelpers.cssOffenderPattern(offender); + + var boldedOffender = splittedOffender.offender.replace(/(\[[^ ]+[~\|\^\$\*]="[^"]+"\])/g, '$1'); + + return boldedOffender + ' @ ' + splittedOffender.line + ':' + splittedOffender.character; + }); + } }, "cssImports": { "tool": "phantomas", @@ -297,7 +340,14 @@ var policies = { "message": "

It’s bad for performance to use @import because CSS files don't get downloaded in parallel.

You should use <link rel='stylesheet' href='a.css'> instead.

", "isOkThreshold": 0, "isBadThreshold": 1, - "isAbnormalThreshold": 1 + "isAbnormalThreshold": 1, + "offendersTransformFn": function(offenders) { + return offenders.map(function(offender) { + var splittedOffender = offendersHelpers.cssOffenderPattern(offender); + + return '' + splittedOffender.offender + ' @ ' + splittedOffender.line + ':' + splittedOffender.character; + }); + } }, "cssDuplicatedSelectors": { "tool": "phantomas", @@ -305,7 +355,19 @@ var policies = { "message": "

This is when two or more selectors are strictly identical and should be merged.

", "isOkThreshold": 0, "isBadThreshold": 40, - "isAbnormalThreshold": 80 + "isAbnormalThreshold": 80, + "offendersTransformFn": function(offenders) { + return offenders.map(function(offender) { + var parts = /^(.*) \((\d+) times\)$/.exec(offender); + + if (!parts) { + debug('cssDuplicatedSelectors offenders transform function error with "%s"', offender); + return offender; + } + + return parts[1] + ' (x' + parts[2] + ')'; + }); + } }, "cssDuplicatedProperties": { "tool": "phantomas", @@ -313,7 +375,21 @@ var policies = { "message": "

This is the number of property definitions duplicated within a selector.

", "isOkThreshold": 0, "isBadThreshold": 50, - "isAbnormalThreshold": 100 + "isAbnormalThreshold": 100, + "offendersTransformFn": function(offenders) { + return offenders.map(function(offender) { + var splittedOffender = offendersHelpers.cssOffenderPattern(offender); + + var parts = /^([^{]+) {([^ ]+): (.+)}$/.exec(splittedOffender.offender); + + if (!parts) { + debug('cssDuplicatedProperties offenders transform function error with "%s"', offender); + return offender; + } + + return 'Property ' + parts[2] + ' duplicated in ' + parts[1] + ' { } @ ' + splittedOffender.line + ':' + splittedOffender.character; + }); + } }, "cssEmptyRules": { "tool": "phantomas", @@ -321,7 +397,13 @@ var policies = { "message": "

Very easy to fix: remove all empty rules.

", "isOkThreshold": 0, "isBadThreshold": 40, - "isAbnormalThreshold": 100 + "isAbnormalThreshold": 100, + "offendersTransformFn": function(offenders) { + return offenders.map(function(offender) { + var splittedOffender = offendersHelpers.cssOffenderPattern(offender); + return '' + splittedOffender.offender + ' { } @ ' + splittedOffender.line + ':' + splittedOffender.character; + }); + } }, "cssExpressions": { "tool": "phantomas", @@ -329,7 +411,21 @@ var policies = { "message": "

Such as: expression( document.body.clientWidth > 600 ? \"600px\" : \"auto\" )

This is a bad practice as it slows down browsers. There are some simpler CSS3 methods for doing this.

", "isOkThreshold": 0, "isBadThreshold": 1, - "isAbnormalThreshold": 20 + "isAbnormalThreshold": 20, + "offendersTransformFn": function(offenders) { + return offenders.map(function(offender) { + var splittedOffender = offendersHelpers.cssOffenderPattern(offender); + + var parts = /^(.*) {([^ ]+): expression\((.*)\)}$/.exec(splittedOffender.offender); + + if (!parts) { + debug('cssExpressions offenders transform function error with "%s"', offender); + return offender; + } + + return parts[1] + ' {' + parts[2] + ': expression(' + parts[3] + ')} @ ' + splittedOffender.line + ':' + splittedOffender.character; + }); + } }, "cssImportants": { "tool": "phantomas", @@ -337,7 +433,21 @@ var policies = { "message": "

It can be useful, but only as a last resort. It is a bad practice because it overrides the normal cascading logic. The more you use !important, the more you need it again to over-override. This conducts to a poor maintainability.

", "isOkThreshold": 0, "isBadThreshold": 50, - "isAbnormalThreshold": 150 + "isAbnormalThreshold": 150, + "offendersTransformFn": function(offenders) { + return offenders.map(function(offender) { + var splittedOffender = offendersHelpers.cssOffenderPattern(offender); + + var parts = /^(.*) {([^ ]+): (.*) ?\!important}$/.exec(splittedOffender.offender); + + if (!parts) { + debug('cssImportants offenders transform function error with "%s"', offender); + return offender; + } + + return parts[1] + ' {' + parts[2] + ': ' + parts[3] + ' !important} @ ' + splittedOffender.line + ':' + splittedOffender.character; + }); + } }, "cssOldIEFixes": { "tool": "phantomas", @@ -345,7 +455,49 @@ var policies = { "message": "

What browser do you need to support? Once you've got the answer, take a look at these old rules that pollute your CSS code and remove them.

IE6:

  • * html
  • html > body (everything but IE6)

IE7:

  • *height: 123px;
  • height: 123px !ie;

IE9:

  • -ms-filter
  • progid:DXImageTransform.Microsoft

", "isOkThreshold": 0, "isBadThreshold": 50, - "isAbnormalThreshold": 300 + "isAbnormalThreshold": 300, + "offendersTransformFn": function(offenders) { + return offenders.map(function(offender) { + var splittedOffender = offendersHelpers.cssOffenderPattern(offender); + + var parts = /^([^{]*)( {([^ ]+): (.*)})?$/.exec(splittedOffender.offender); + + if (!parts) { + debug('cssOldIEFixes offenders transform function error with "%s"', offender); + return offender; + } + + var rule = parts[1]; + var property = parts[3]; + var value = parts[4]; + var browser = null; + + if (rule.indexOf('* html') === 0) { + rule = rule.replace(/^\* html/, '* html'); + browser = 'IE6'; + } else if (rule.indexOf('html>body') === 0) { + rule = rule.replace(/^html>body/, 'html>body'); + browser = 'IE6'; + } else if (property.indexOf('*') === 0) { + property = '' + property + ''; + browser = 'IE7'; + } else if (value.match(/\!ie$/)) { + value = value.replace(/\!ie$/, '!ie'); + browser = 'IE7'; + } else if (property === '-ms-filter') { + property = '-ms-filter'; + browser = 'IE9'; + } else if (value.indexOf('progid:DXImageTransform.Microsoft') === 0) { + value = value.replace(/^progid:DXImageTransform\.Microsoft/, 'progid:DXImageTransform.Microsoft'); + browser = 'IE9'; + } + + browser = browser ? '' + browser + ' fix: ' : ''; + var propertyAndValue = (property && value) ? ' {' + property + ': ' + value + '}' : ''; + + return browser + rule + propertyAndValue + ' @ ' + splittedOffender.line + ':' + splittedOffender.character; + }); + } }, "cssOldPropertyPrefixes": { "tool": "phantomas", @@ -353,7 +505,21 @@ var policies = { "message": "

Many property prefixes such as -moz- or -webkit- are not needed anymore, or by very few people. You can remove them or replace them with the non-prefixed version. This will help reducing your stylesheets weight.

", "isOkThreshold": 0, "isBadThreshold": 50, - "isAbnormalThreshold": 300 + "isAbnormalThreshold": 300, + "offendersTransformFn": function(offenders) { + return offenders.map(function(offender) { + var splittedOffender = offendersHelpers.cssOffenderPattern(offender); + + var parts = /^([^{]*)(?: { ([^ ]+): (.*) }) \/\/ (.*)$/.exec(splittedOffender.offender); + + if (!parts) { + debug('cssOldPropertyPrefixes offenders transform function error with "%s"', offender); + return offender; + } + + return '
' + parts[1] + ' {' + parts[2] + ': ' + parts[3] + '} @ ' + splittedOffender.line + ':' + splittedOffender.character + '
' + parts[4] + '
'; + }); + } }, "cssUniversalSelectors": { "tool": "phantomas", diff --git a/lib/offendersHelpers.js b/lib/offendersHelpers.js index 39a048f..66b11f3 100644 --- a/lib/offendersHelpers.js +++ b/lib/offendersHelpers.js @@ -136,9 +136,10 @@ var OffendersHelpers = function() { } var html = '
backtrace
'; + var that = this; backtraceArray.forEach(function(backtraceObj) { var functionName = (backtraceObj.functionName) ? backtraceObj.functionName + '() ' : ''; - html += '
' + functionName + '' + backtraceObj.file + ' line ' + backtraceObj.line + '
'; + html += '
' + functionName + that.urlToLink(backtraceObj.file) + ' line ' + backtraceObj.line + '
'; }); return html + '
'; }; @@ -150,6 +151,27 @@ var OffendersHelpers = function() { }); }; + this.urlToLink = function(url) { + var shortUrl = (url.length > 110) ? url.substr(0, 47) + ' ... ' + url.substr(-48) : url; + return '' + shortUrl + ''; + }; + + this.cssOffenderPattern = function(offender) { + var parts = /^(.*) @ (\d+):(\d+)$/.exec(offender); + + if (!parts) { + return { + offender: offender + }; + } else { + return { + offender: parts[1], + line: parseInt(parts[2], 10), + character: parseInt(parts[3], 10) + }; + } + }; + }; module.exports = new OffendersHelpers(); \ No newline at end of file diff --git a/test/core/offendersHelpersTest.js b/test/core/offendersHelpersTest.js index ce23d07..072d2d1 100644 --- a/test/core/offendersHelpersTest.js +++ b/test/core/offendersHelpersTest.js @@ -175,7 +175,7 @@ describe('offendersHelpers', function() { } ]); - result.should.equal(''); + result.should.equal(''); }); it('should display "no backtrace"', function() { @@ -228,4 +228,43 @@ describe('offendersHelpers', function() { }); + describe('urlToLink', function() { + + it('should transform an url into an html link', function() { + var result = offendersHelpers.urlToLink('http://www.google.com/js/main.js'); + + result.should.equal('http://www.google.com/js/main.js'); + }); + + it('should ellypsis the url if too long', function() { + var result = offendersHelpers.urlToLink('http://www.google.com/js/longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong/main.js'); + + result.should.equal('http://www.google.com/js/longlonglonglonglonglo ... longlonglonglonglonglonglonglonglonglong/main.js'); + }); + + }); + + + describe('cssOffenderPattern', function() { + + it('should transform a css offender into an object', function() { + var result = offendersHelpers.cssOffenderPattern('.pagination .plus ul li @ 30:31862'); + + result.should.deep.equal({ + offender: '.pagination .plus ul li', + line: 30, + character: 31862 + }); + }); + + it('should handle the case where line and char are not here', function() { + var result = offendersHelpers.cssOffenderPattern('.pagination .plus ul li'); + + result.should.deep.equal({ + offender: '.pagination .plus ul li' + }); + }); + + }); + }); From 4a8a5829d74834db3ce3ae9339fdde5460a90000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Thu, 22 Jan 2015 14:54:51 +0100 Subject: [PATCH 11/14] End of the offender transform functions --- lib/metadata/policies.js | 231 ++++++++++++++++++++++++++---- lib/offendersHelpers.js | 15 ++ test/core/offendersHelpersTest.js | 13 ++ 3 files changed, 235 insertions(+), 24 deletions(-) diff --git a/lib/metadata/policies.js b/lib/metadata/policies.js index 82ce5b7..b6f2c35 100644 --- a/lib/metadata/policies.js +++ b/lib/metadata/policies.js @@ -510,7 +510,7 @@ var policies = { return offenders.map(function(offender) { var splittedOffender = offendersHelpers.cssOffenderPattern(offender); - var parts = /^([^{]*)(?: { ([^ ]+): (.*) }) \/\/ (.*)$/.exec(splittedOffender.offender); + var parts = /^([^{]*)(?: ?{ ?([^ ]+): (.*) ?}) \/\/ (.*)$/.exec(splittedOffender.offender); if (!parts) { debug('cssOldPropertyPrefixes offenders transform function error with "%s"', offender); @@ -535,7 +535,17 @@ var policies = { "message": "

This is one way to remove complexity from a CSS rule. Generally, when \"body\" is specified in a rule it can be removed, because an element is necessarily inside the body.

", "isOkThreshold": 0, "isBadThreshold": 50, - "isAbnormalThreshold": 200 + "isAbnormalThreshold": 200, + "offendersTransformFn": function(offenders) { + return offenders.map(function(offender) { + var splittedOffender = offendersHelpers.cssOffenderPattern(offender); + + var rule = splittedOffender.offender; + rule = rule.replace(/body/, 'body'); + + return rule + ' @ ' + splittedOffender.line + ':' + splittedOffender.character; + }); + } }, "cssRedundantChildNodesSelectors": { "tool": "phantomas", @@ -543,7 +553,27 @@ var policies = { "message": "

Some tags included inside other tags are obvious. For example, when \"ul li\" is specified in a rule, \"ul\" can be removed because the \"li\" element is always inside a \"ul\". Same thing for \"tr td\", \"select option\", ...

Lowering compexity in CSS selectors can make the page load a little faster.

", "isOkThreshold": 0, "isBadThreshold": 50, - "isAbnormalThreshold": 200 + "isAbnormalThreshold": 200, + "offendersTransformFn": function(offenders) { + return offenders.map(function(offender) { + var splittedOffender = offendersHelpers.cssOffenderPattern(offender); + + var rule = splittedOffender.offender; + var redundanters = [ + ['ul', 'li'], + ['ol', 'li'], + ['select', 'option'], + ['table', 'tr'], + ['table', 'th'], + ]; + + redundanters.forEach(function(couple) { + rule = rule.replace(new RegExp('(^| |>)' + couple[0] + '([^ >]*)?([ >]| > )' + couple[1] + '([^\\w-]|$)', 'g'), '$1' + couple[0] + '$2$3' + couple[1] + '$4'); + }); + + return rule + ' @ ' + splittedOffender.line + ':' + splittedOffender.character; + }); + } }, "requests": { "tool": "phantomas", @@ -552,7 +582,17 @@ var policies = { "isOkThreshold": 15, "isBadThreshold": 100, "isAbnormalThreshold": 200, - "takeOffendersFrom": ["htmlCount", "jsCount", "cssCount", "imageCount", "webfontCount", "videoCount", "jsonCount", "jsonCount"] + "takeOffendersFrom": ["htmlCount", "jsCount", "cssCount", "imageCount", "webfontCount", "videoCount", "jsonCount", "jsonCount"], + "offendersTransformFn": function(offenders) { + return offenders + .map(function(offender) { + return offendersHelpers.fileWithSizePattern(offender); + }).sort(function(a, b) { + return b.size - a.size; + }).map(function(fileObj) { + return offendersHelpers.urlToLink(fileObj.file) + ((fileObj.size !== undefined) ? ' (' + fileObj.size + ' kB)' : ''); + }); + } }, "htmlCount": { "tool": "phantomas", @@ -560,7 +600,13 @@ var policies = { "message": "

The number of HTML pages requests, HTML fragments or iframes.

", "isOkThreshold": 10, "isBadThreshold": 20, - "isAbnormalThreshold": 30 + "isAbnormalThreshold": 30, + "offendersTransformFn": function(offenders) { + return offenders.map(function(offender) { + var fileObj = offendersHelpers.fileWithSizePattern(offender); + return offendersHelpers.urlToLink(fileObj.file) + ((fileObj.size !== undefined) ? ' (' + fileObj.size + ' kB)' : ''); + }); + } }, "jsCount": { "tool": "phantomas", @@ -568,7 +614,13 @@ var policies = { "message": "

Reduce the number of scripts by concatenating them.

", "isOkThreshold": 5, "isBadThreshold": 15, - "isAbnormalThreshold": 30 + "isAbnormalThreshold": 30, + "offendersTransformFn": function(offenders) { + return offenders.map(function(offender) { + var fileObj = offendersHelpers.fileWithSizePattern(offender); + return offendersHelpers.urlToLink(fileObj.file) + ((fileObj.size !== undefined) ? ' (' + fileObj.size + ' kB)' : ''); + }); + } }, "cssCount": { "tool": "phantomas", @@ -576,7 +628,13 @@ var policies = { "message": "

Reduce the number of stylesheets by concatenating them.

", "isOkThreshold": 3, "isBadThreshold": 10, - "isAbnormalThreshold": 22 + "isAbnormalThreshold": 22, + "offendersTransformFn": function(offenders) { + return offenders.map(function(offender) { + var fileObj = offendersHelpers.fileWithSizePattern(offender); + return offendersHelpers.urlToLink(fileObj.file) + ((fileObj.size !== undefined) ? ' (' + fileObj.size + ' kB)' : ''); + }); + } }, "imageCount": { "tool": "phantomas", @@ -584,7 +642,13 @@ var policies = { "message": "

Reduce the number of images by lazyloading them, by spriting them or by creating an icons font.

", "isOkThreshold": 15, "isBadThreshold": 40, - "isAbnormalThreshold": 70 + "isAbnormalThreshold": 70, + "offendersTransformFn": function(offenders) { + return offenders.map(function(offender) { + var fileObj = offendersHelpers.fileWithSizePattern(offender); + return offendersHelpers.urlToLink(fileObj.file) + ((fileObj.size !== undefined) ? ' (' + fileObj.size + ' kB)' : ''); + }); + } }, "webfontCount": { "tool": "phantomas", @@ -592,15 +656,27 @@ var policies = { "message": "

Fonts are loaded on the critical path of the head. Load as many as possible.

", "isOkThreshold": 0, "isBadThreshold": 3, - "isAbnormalThreshold": 5 + "isAbnormalThreshold": 5, + "offendersTransformFn": function(offenders) { + return offenders.map(function(offender) { + var fileObj = offendersHelpers.fileWithSizePattern(offender); + return offendersHelpers.urlToLink(fileObj.file) + ((fileObj.size !== undefined) ? ' (' + fileObj.size + ' kB)' : ''); + }); + } }, "videoCount": { "tool": "phantomas", - "label": "Videos count", + "label": "Video count", "message": "

The number of videos loaded.

", "isOkThreshold": 1, "isBadThreshold": 5, - "isAbnormalThreshold": 15 + "isAbnormalThreshold": 15, + "offendersTransformFn": function(offenders) { + return offenders.map(function(offender) { + var fileObj = offendersHelpers.fileWithSizePattern(offender); + return offendersHelpers.urlToLink(fileObj.file) + ((fileObj.size !== undefined) ? ' (' + fileObj.size + ' kB)' : ''); + }); + } }, "jsonCount": { "tool": "phantomas", @@ -608,7 +684,13 @@ var policies = { "message": "

The number of AJAX requests to JSON files or webservices.

", "isOkThreshold": 2, "isBadThreshold": 10, - "isAbnormalThreshold": 25 + "isAbnormalThreshold": 25, + "offendersTransformFn": function(offenders) { + return offenders.map(function(offender) { + var fileObj = offendersHelpers.fileWithSizePattern(offender); + return offendersHelpers.urlToLink(fileObj.file) + ((fileObj.size !== undefined) ? ' (' + fileObj.size + ' kB)' : ''); + }); + } }, "otherCount": { "tool": "phantomas", @@ -616,7 +698,13 @@ var policies = { "message": "

They can be Flash, XML, music or any unknown format.

", "isOkThreshold": 5, "isBadThreshold": 20, - "isAbnormalThreshold": 40 + "isAbnormalThreshold": 40, + "offendersTransformFn": function(offenders) { + return offenders.map(function(offender) { + var fileObj = offendersHelpers.fileWithSizePattern(offender); + return offendersHelpers.urlToLink(fileObj.file) + ((fileObj.size !== undefined) ? ' (' + fileObj.size + ' kB)' : ''); + }); + } }, "smallJsFiles": { "tool": "phantomas", @@ -624,7 +712,13 @@ var policies = { "message": "

Number of JS assets smaller than 2 KB that could probably be inlined or merged.

", "isOkThreshold": 2, "isBadThreshold": 10, - "isAbnormalThreshold": 16 + "isAbnormalThreshold": 16, + "offendersTransformFn": function(offenders) { + return offenders.map(function(offender) { + var fileObj = offendersHelpers.fileWithSizePattern(offender); + return offendersHelpers.urlToLink(fileObj.file) + ((fileObj.size !== undefined) ? ' (' + fileObj.size + ' kB)' : ''); + }); + } }, "smallCssFiles": { "tool": "phantomas", @@ -632,7 +726,13 @@ var policies = { "message": "

Number of CSS assets smaller than 2 KB that could probably be inlined or merged.

", "isOkThreshold": 0, "isBadThreshold": 8, - "isAbnormalThreshold": 12 + "isAbnormalThreshold": 12, + "offendersTransformFn": function(offenders) { + return offenders.map(function(offender) { + var fileObj = offendersHelpers.fileWithSizePattern(offender); + return offendersHelpers.urlToLink(fileObj.file) + ((fileObj.size !== undefined) ? ' (' + fileObj.size + ' kB)' : ''); + }); + } }, "smallImages": { "tool": "phantomas", @@ -640,7 +740,13 @@ var policies = { "message": "

Images smaller than 2 KB that could be base64 encoded or merged into a sprite.

", "isOkThreshold": 2, "isBadThreshold": 17, - "isAbnormalThreshold": 30 + "isAbnormalThreshold": 30, + "offendersTransformFn": function(offenders) { + return offenders.map(function(offender) { + var fileObj = offendersHelpers.fileWithSizePattern(offender); + return offendersHelpers.urlToLink(fileObj.file) + ((fileObj.size !== undefined) ? ' (' + fileObj.size + ' kB)' : ''); + }); + } }, "notFound": { "tool": "phantomas", @@ -648,7 +754,12 @@ var policies = { "message": "

404 errors are never cached, so each time a page ask for it, it hits se server. Even if it is behind a CDN or a reverse-proxy cache.

", "isOkThreshold": 0, "isBadThreshold": 1, - "isAbnormalThreshold": 1 + "isAbnormalThreshold": 1, + "offendersTransformFn": function(offenders) { + return offenders.map(function(offender) { + return offendersHelpers.urlToLink(offender); + }); + } }, "closedConnections": { "tool": "phantomas", @@ -656,15 +767,25 @@ var policies = { "message": "

This counts the number of requests not keeping the connection alive (specifying \"Connection: close\" in the response headers). It is only counting a request if it is followed by another request on the same domain.

This is slowing down the next request, because the brower needs to open a new connection to the server, which means a additional round-trip.

Correct the problem by setting a Keep-Alive header on the guilty server.

", "isOkThreshold": 0, "isBadThreshold": 8, - "isAbnormalThreshold": 20 + "isAbnormalThreshold": 20, + "offendersTransformFn": function(offenders) { + return offenders.map(function(offender) { + return offendersHelpers.urlToLink(offender); + }); + } }, "multipleRequests": { "tool": "phantomas", "label": "Duplicated requests", "message": "

This only happens when the asset has no cache and is requested more than once on the same page. Be very careful about it.

", "isOkThreshold": 0, - "isBadThreshold": 5, - "isAbnormalThreshold": 10 + "isBadThreshold": 3, + "isAbnormalThreshold": 10, + "offendersTransformFn": function(offenders) { + return offenders.map(function(offender) { + return offendersHelpers.urlToLink(offender); + }); + } }, "cachingDisabled": { "tool": "phantomas", @@ -672,7 +793,12 @@ var policies = { "message": "

Counts responses with caching disabled (max-age=0)

Fix immediatly if on static assets.

", "isOkThreshold": 0, "isBadThreshold": 12, - "isAbnormalThreshold": 25 + "isAbnormalThreshold": 25, + "offendersTransformFn": function(offenders) { + return offenders.map(function(offender) { + return offendersHelpers.urlToLink(offender); + }); + } }, "cachingNotSpecified": { "tool": "phantomas", @@ -680,7 +806,12 @@ var policies = { "message": "

When no caching is specified, each browser will handle it differently. Most of the time, it will automatically add a cache for you, but a poor one. You'd better handle it yourself.

", "isOkThreshold": 5, "isBadThreshold": 20, - "isAbnormalThreshold": 40 + "isAbnormalThreshold": 40, + "offendersTransformFn": function(offenders) { + return offenders.map(function(offender) { + return offendersHelpers.urlToLink(offender); + }); + } }, "cachingTooShort": { "tool": "phantomas", @@ -688,7 +819,47 @@ var policies = { "message": "

Responses with too short caching time (less than a week).

The longer you cache, the better. Add versionning to your static assets, if it's not already done, and set their cache time to one year.

", "isOkThreshold": 5, "isBadThreshold": 20, - "isAbnormalThreshold": 40 + "isAbnormalThreshold": 40, + "offendersTransformFn": function(offenders) { + return offenders + .map(function(offender) { + var parts = /^([^ ]*) cached for (-?\d+(\.\d+)?) s$/.exec(offender); + + if (!parts) { + debug('cachingTooShort offenders transform function error with "%s"', offender); + return { + file: offender + }; + } + + return { + file: parts[1], + ttl: Math.round(parseFloat(parts[2])) + }; + }).sort(function(a, b) { + return a.ttl - b.ttl; + }).map(function(obj) { + var duration = obj.ttl; + var unit = 'seconds'; + + if (duration >= 120) { + duration = Math.round(duration / 60); + unit = 'minutes'; + } + + if (duration >= 120) { + duration = Math.round(duration / 60); + unit = 'hours'; + } + + if (duration >= 48) { + duration = Math.round(duration / 24); + unit = 'days'; + } + + return offendersHelpers.urlToLink(obj.file) + ' cached for ' + duration + ' ' + unit + ''; + }); + } }, "domains": { "tool": "phantomas", @@ -696,7 +867,19 @@ var policies = { "message": "

For each domain met, the browser needs to make a DNS look-up, which is slow. Avoid having to many different domains and the page should render faster.

By the way, domain sharding is not a good practice anymore.

", "isOkThreshold": 10, "isBadThreshold": 25, - "isAbnormalThreshold": 50 + "isAbnormalThreshold": 50, + "offendersTransformFn": function(offenders) { + return offenders.map(function(offender) { + var parts = /^([^ ]*): (\d+) request\(s\)$/.exec(offender); + + if (!parts) { + debug('domains offenders transform function error with "%s"', offender); + return offender; + } + + return '' + parts[1] + ' (' + parts[2] + ' ' + ((parseInt(parts[2], 10) > 1) ? 'requests' : 'request') + ')'; + }); + } } }; diff --git a/lib/offendersHelpers.js b/lib/offendersHelpers.js index 66b11f3..36151c6 100644 --- a/lib/offendersHelpers.js +++ b/lib/offendersHelpers.js @@ -172,6 +172,21 @@ var OffendersHelpers = function() { } }; + this.fileWithSizePattern = function(fileWithSize) { + var parts = /^([^ ]*) \((\d+\.\d{2}) kB\)$/.exec(fileWithSize); + + if (!parts) { + return { + file: fileWithSize + }; + } else { + return { + file: parts[1], + size: parseFloat(parts[2]) + }; + } + }; + }; module.exports = new OffendersHelpers(); \ No newline at end of file diff --git a/test/core/offendersHelpersTest.js b/test/core/offendersHelpersTest.js index 072d2d1..d6632f0 100644 --- a/test/core/offendersHelpersTest.js +++ b/test/core/offendersHelpersTest.js @@ -267,4 +267,17 @@ describe('offendersHelpers', function() { }); + describe('fileWithSizePattern', function() { + + it('should return an object', function() { + var result = offendersHelpers.fileWithSizePattern('http://img3.pouet.com/2008/portail/js/jq-timer.js (1.72 kB)'); + + result.should.deep.equal({ + file: 'http://img3.pouet.com/2008/portail/js/jq-timer.js', + size: 1.72 + }); + }); + + }); + }); From ee666a3a6f707f5bc276ca1265beab1aaca506ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Thu, 22 Jan 2015 15:15:38 +0100 Subject: [PATCH 12/14] Temporary fix for negative ttl in cachingTooShort --- .../custom_modules/modules/cachYLT/cachYLT.js | 79 +++++++++++++++++++ lib/tools/phantomas/phantomasWrapper.js | 1 + 2 files changed, 80 insertions(+) create mode 100644 lib/tools/phantomas/custom_modules/modules/cachYLT/cachYLT.js diff --git a/lib/tools/phantomas/custom_modules/modules/cachYLT/cachYLT.js b/lib/tools/phantomas/custom_modules/modules/cachYLT/cachYLT.js new file mode 100644 index 0000000..213edfe --- /dev/null +++ b/lib/tools/phantomas/custom_modules/modules/cachYLT/cachYLT.js @@ -0,0 +1,79 @@ +/** + * Analyzes HTTP caching headers + * + * @see https://developers.google.com/speed/docs/best-practices/caching + */ + +exports.version = '0.2.a'; + +exports.module = function(phantomas) { + 'use strict'; + + var cacheControlRegExp = /max-age=(\d+)/; + + function getCachingTime(url, headers) { + // false means "no caching" + var ttl = false, + headerName, + now = new Date(), + headerDate; + + for (headerName in headers) { + var value = headers[headerName]; + + switch (headerName.toLowerCase()) { + // parse max-age=... + // + // max-age=2592000 + // public, max-age=300, must-revalidate + case 'cache-control': + var matches = value.match(cacheControlRegExp); + + if (matches) { + ttl = parseInt(matches[1], 10); + } + break; + + // catch Expires and Pragma headers + case 'expires': + case 'pragma': + // and Varnish specific headers + case 'x-pass-expires': + case 'x-pass-cache-control': + phantomas.incrMetric('oldCachingHeaders'); // @desc number of responses with old, HTTP 1.0 caching headers (Expires and Pragma) + phantomas.addOffender('oldCachingHeaders', url + ' - ' + headerName + ': ' + value); + headerDate = Date.parse(value); + if (headerDate) ttl = Math.round((headerDate - now) / 1000); + break; + } + } + + //console.log(JSON.stringify(headers)); console.log("TTL: " + ttl + ' s'); + + return ttl; + } + + phantomas.setMetric('cachingNotSpecified'); // @desc number of responses with no caching header sent (no Cache-Control header) + phantomas.setMetric('cachingTooShort'); // @desc number of responses with too short (less than a week) caching time + phantomas.setMetric('cachingDisabled'); // @desc number of responses with caching disabled (max-age=0) + + phantomas.setMetric('oldCachingHeaders'); + + phantomas.on('recv', function(entry, res) { + var ttl = getCachingTime(entry.url, entry.headers); + + // static assets + if (entry.isImage || entry.isJS || entry.isCSS) { + if (ttl === false) { + phantomas.incrMetric('cachingNotSpecified'); + phantomas.addOffender('cachingNotSpecified', entry.url); + } else if (ttl <= 0) { + phantomas.incrMetric('cachingDisabled'); + phantomas.addOffender('cachingDisabled', entry.url); + } else if (ttl < 7 * 86400) { + phantomas.incrMetric('cachingTooShort'); + phantomas.addOffender('cachingTooShort', entry.url + ' cached for ' + ttl + ' s'); + } + } + }); +}; diff --git a/lib/tools/phantomas/phantomasWrapper.js b/lib/tools/phantomas/phantomasWrapper.js index 6c7937f..4706201 100644 --- a/lib/tools/phantomas/phantomasWrapper.js +++ b/lib/tools/phantomas/phantomasWrapper.js @@ -34,6 +34,7 @@ var PhantomasWrapper = function() { 'analyze-css': true, 'skip-modules': [ 'blockDomains', // not needed + 'caching', // overriden 'domMutations', // not compatible with webkit 'domQueries', // overriden 'eventListeners', // overridden From d829525ab8aa0a14ecb89728f8f6efd599048bec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Mon, 26 Jan 2015 11:32:37 +0100 Subject: [PATCH 13/14] Update phantomas to v1.9 --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index fa04850..e2d31ec 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "cors": "^2.5.2", "debug": "~2.1.0", "express": "~4.10.6", - "phantomas": "1.8.0", + "phantomas": "1.9.0", "ps-node": "0.0.3", "q": "~1.1.2", "rimraf": "~2.2.8" @@ -44,7 +44,6 @@ "grunt-usemin": "^3.0.0", "matchdep": "^0.3.0", "mocha": "^2.1.0", - "phantomjs": "^1.9.13", "request": "^2.51.0", "sinon": "^1.12.1", "sinon-chai": "^2.6.0" From 901116fde662e64b46deed737daefe9529f764e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Mon, 26 Jan 2015 11:34:38 +0100 Subject: [PATCH 14/14] Separate offender analyze, parsing in the core, display in the front --- front/src/css/rule.css | 7 +- front/src/js/app.js | 1 + front/src/js/controllers/ruleCtrl.js | 4 - .../src/js/directives/offendersDirectives.js | 120 +++ front/src/less/rule.less | 6 +- front/src/main.html | 1 + front/src/views/domElementButton.html | 7 + front/src/views/rule.html | 141 ++- lib/metadata/policies.js | 848 +++++++++++------- lib/offendersHelpers.js | 102 +-- lib/rulesChecker.js | 88 +- lib/tools/phantomas/phantomasWrapper.js | 3 +- test/core/customPoliciesTest.js | 396 +++++++- test/core/indexTest.js | 13 +- test/core/offendersHelpersTest.js | 198 ++-- test/fixtures/rulesCheckerOutput.json | 36 +- test/fixtures/rulesCheckerPolicies.js | 28 +- 17 files changed, 1436 insertions(+), 563 deletions(-) create mode 100644 front/src/js/directives/offendersDirectives.js create mode 100644 front/src/views/domElementButton.html diff --git a/front/src/css/rule.css b/front/src/css/rule.css index 80474b4..b66c470 100644 --- a/front/src/css/rule.css +++ b/front/src/css/rule.css @@ -120,17 +120,18 @@ .offenders .offenderButton .domTree > div div { margin-left: 1em; } -.offenders .offenderButton .backtrace { +.offenders .offenderButton .backtrace, +.offenders .offenderButton .cssFileAndLine { white-space: nowrap; padding: 0.5em; } -.offenders .offenderButton:hover { +.offenders .offenderButton.opens.mouseOver { border-bottom-left-radius: 0; border-bottom-right-radius: 0; background: #ffe0cc; z-index: 2; } -.offenders .offenderButton:hover > div { +.offenders .offenderButton.opens.mouseOver > div { display: block; } .offendersHtml { diff --git a/front/src/js/app.js b/front/src/js/app.js index 0adc65f..7ddabb4 100644 --- a/front/src/js/app.js +++ b/front/src/js/app.js @@ -11,6 +11,7 @@ var yltApp = angular.module('YellowLabTools', [ 'resultsFactory', 'menuService', 'gradeDirective', + 'offendersDirectives' ]); yltApp.run(['$rootScope', '$location', function($rootScope, $location) { diff --git a/front/src/js/controllers/ruleCtrl.js b/front/src/js/controllers/ruleCtrl.js index e63adc1..7ff0622 100644 --- a/front/src/js/controllers/ruleCtrl.js +++ b/front/src/js/controllers/ruleCtrl.js @@ -22,10 +22,6 @@ ruleCtrl.controller('RuleCtrl', ['$scope', '$rootScope', '$routeParams', '$locat function init() { $scope.rule = $scope.result.rules[$scope.policyName]; - - if (angular.isString($scope.rule.offenders)) { - $scope.htmlOffenders = $scope.rule.offenders; - } } $scope.backToDashboard = function() { diff --git a/front/src/js/directives/offendersDirectives.js b/front/src/js/directives/offendersDirectives.js new file mode 100644 index 0000000..cb691d0 --- /dev/null +++ b/front/src/js/directives/offendersDirectives.js @@ -0,0 +1,120 @@ +var offendersDirectives = angular.module('offendersDirectives', []); + +offendersDirectives.directive('domTree', function() { + return { + restrict: 'E', + scope: { + tree: '=' + }, + template: '
', + replace: true, + link: function(scope, element, attrs) { + + function recursiveHtmlBuilder(tree) { + var html = ''; + var keys = Object.keys(tree); + + keys.forEach(function(key) { + if (isNaN(tree[key])) { + html += '
' + key + '' + recursiveHtmlBuilder(tree[key]) + '
'; + } else if (tree[key] > 1) { + html += '
' + key + ' (x' + tree[key] + ')
'; + } else { + html += '
' + key + '
'; + } + }); + + return html; + } + + element.append(recursiveHtmlBuilder(scope.tree)); + } + }; +}); + +offendersDirectives.directive('domElementButton', function() { + return { + restrict: 'E', + scope: { + obj: '=' + }, + templateUrl: 'views/domElementButton.html', + replace: true + }; +}); + +offendersDirectives.filter('shortenUrl', function() { + return function(url, maxLength) { + if (!maxLength) { + maxLength = 110; + } + + // Why dividing by 2.1? Because it adds a 5% margin. + var leftLength = Math.floor((maxLength - 5) / 2.1); + var rightLength = Math.ceil((maxLength - 5) / 2.1); + + return (url.length > maxLength) ? url.substr(0, leftLength) + ' ... ' + url.substr(-rightLength) : url; + }; +}); + +offendersDirectives.directive('urlLink', function() { + return { + restrict: 'E', + scope: { + url: '=', + maxLength: '=' + }, + template: '{{url | shortenUrl:maxLength}}', + replace: true + }; +}); + +offendersDirectives.filter('encodeURIComponent', function() { + return window.encodeURIComponent; +}); + +offendersDirectives.directive('fileAndLine', function() { + return { + restrict: 'E', + scope: { + file: '=', + line: '=', + column: '=' + }, + template: '<inline CSS> @ {{line}}:{{column}}', + replace: true + }; +}); + +offendersDirectives.directive('fileAndLineButton', function() { + return { + restrict: 'E', + scope: { + file: '=', + line: '=', + column: '=' + }, + template: '
css file
', + replace: true + }; +}); + +offendersDirectives.directive('offenderButton', function() { + return { + restrict: 'C', + link: function(scope, element, attrs) { + + console.log('initializing touchstart'); + + element.bind('touchstart mouseenter', function(e) { + element.addClass('mouseOver'); + e.preventDefault(); + }); + + element.bind('touchend mouseleave click', function(e) { + element.removeClass('mouseOver'); + e.preventDefault(); + }); + } + }; +}); \ No newline at end of file diff --git a/front/src/less/rule.less b/front/src/less/rule.less index 71bb103..ccf862a 100644 --- a/front/src/less/rule.less +++ b/front/src/less/rule.less @@ -135,18 +135,18 @@ } } - .backtrace { + .backtrace, .cssFileAndLine { white-space: nowrap; padding: 0.5em; } - &:hover { + &.opens.mouseOver { border-bottom-left-radius: 0; border-bottom-right-radius: 0; background: #ffe0cc; z-index: 2; - & > div { + > div { display: block; } } diff --git a/front/src/main.html b/front/src/main.html index 88f1dd6..fe5ec86 100644 --- a/front/src/main.html +++ b/front/src/main.html @@ -32,6 +32,7 @@ + diff --git a/front/src/views/domElementButton.html b/front/src/views/domElementButton.html new file mode 100644 index 0000000..78dda24 --- /dev/null +++ b/front/src/views/domElementButton.html @@ -0,0 +1,7 @@ +
+ {{obj.type}} + DOM element {{obj.element}} + Fragment element {{obj.element}} + Created element {{obj.element}} + +
\ No newline at end of file diff --git a/front/src/views/rule.html b/front/src/views/rule.html index 639ed91..28e3c31 100644 --- a/front/src/views/rule.html +++ b/front/src/views/rule.html @@ -17,23 +17,134 @@

Warning

This rule reached the abnormality threshold, which means there is a real problem you should care about.

-
-

- - -

-
-
-
+
+

+ +
+
+
+ {{offender.parseError}} +
+
+ +
+ {{offender.id}}: {{offender.occurrences}} occurrences +
+ +
+ appended to +
+ +
+ {{offender.query}} (in ) using {{offender.fn}} +
+ +
+ {{offender.query}} (in ) using {{offender.fn}}: {{offender.count}} queries +
+ +
+ {{offender.eventName}} bound to +
+ +
+ {{offender.error}} +
no backtrace
+
+ backtrace +
+
+ {{obj.functionName}}() + + line {{obj.line}} +
+
+
+
+ +
+ {{offender.error}} + + (Check on the W3C validator) +
+ +
+ + {{offender.css}} + +
+ +
+ {{offender.rule}} (x{{offender.occurrences}}) +
+ +
+ Property {{offender.property}} duplicated in {{offender.rule}} { } + +
+ +
+ {{offender.css}} { } + +
+ +
+ {{offender.rule}} {{ '{' + offender.property}}: expression({{offender.expression}})} + +
+ +
+ {{offender.rule}} {{ '{' + offender.property}}: {{offender.value}} !important} + +
+ +
+ {{offender.browser}} fix: + + +
+ +
+ {{offender.rule}} {{{offender.property}}: {{offender.value + '}' }} + +
+ +
+ + ({{offender.size}} kB) +
+ +
+ +
+ +
+ + cached for {{offender.ttlWithUnit}} {{offender.unit}} +
+ +
+ {{offender.domain}} + () +
+ +
+ {{offender}} +
+ +
-
-
-

- - -

-
+ + +
+ +
+ +
+ +
+

404

diff --git a/lib/metadata/policies.js b/lib/metadata/policies.js index b6f2c35..00e667b 100644 --- a/lib/metadata/policies.js +++ b/lib/metadata/policies.js @@ -8,7 +8,8 @@ var policies = { "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": 2500, - "isAbnormalThreshold": 4000 + "isAbnormalThreshold": 4000, + "hasOffenders": false }, "DOMelementMaxDepth": { "tool": "phantomas", @@ -17,8 +18,14 @@ var policies = { "isOkThreshold": 10, "isBadThreshold": 20, "isAbnormalThreshold": 28, + "hasOffenders": true, "offendersTransformFn": function(offenders) { - return offendersHelpers.listOfDomPathsToHTML(offenders); + var domArrays = offenders.map(offendersHelpers.domPathToArray); + + return { + count: offenders.length, + tree: offendersHelpers.listOfDomArraysToTree(domArrays) + }; } }, "iframesCount": { @@ -27,7 +34,8 @@ var policies = { "message": "

iFrames are the most complex HTML elements. They are pages, just like the main page, and the browser needs to create a new page context, which has a cost.

", "isOkThreshold": 2, "isBadThreshold": 15, - "isAbnormalThreshold": 30 + "isAbnormalThreshold": 30, + "hasOffenders": false }, "DOMidDuplicated": { "tool": "phantomas", @@ -36,17 +44,26 @@ var policies = { "isOkThreshold": 0, "isBadThreshold": 5, "isAbnormalThreshold": 10, + "hasOffenders": true, "offendersTransformFn": function(offenders) { - return offenders.map(function(offender) { - var parts = /^(.*): ?(\d+) ?occurrences$/.exec(offender); + return { + count: offenders.length, + list: offenders.map(function(offender) { + var parts = /^(.*): ?(\d+) ?occurrences$/.exec(offender); - if (!parts) { - debug('DOMidDuplicated offenders transform function error with "%s"', offender); - return offender; - } + if (!parts) { + debug('DOMidDuplicated offenders transform function error with "%s"', offender); + return { + parseError: offender + }; + } - return '#' + parts[1] + ': ' + parts[2] + ' occurrences'; - }); + return { + id: parts[1], + occurrences: parseInt(parts[2], 10) + }; + }) + }; } }, "DOMinserts": { @@ -56,17 +73,26 @@ var policies = { "isOkThreshold": 10, "isBadThreshold": 400, "isAbnormalThreshold": 1000, + "hasOffenders": true, "offendersTransformFn": function(offenders) { - return offenders.map(function(offender) { - var parts = /^"(.*)" ?appended ?to ?"(.*)"$/.exec(offender); + return { + count: offenders.length, + list: offenders.map(function(offender) { + var parts = /^"(.*)" ?appended ?to ?"(.*)"$/.exec(offender); - if (!parts) { - debug('DOMinserts offenders transform function error with "%s"', offender); - return offender; - } + if (!parts) { + debug('DOMinserts offenders transform function error with "%s"', offender); + return { + parseError: offender + }; + } - return offendersHelpers.domPathToButton(parts[1]) + ' appended to ' + offendersHelpers.domPathToButton(parts[2]); - }); + return { + insertedElement: offendersHelpers.domPathToDomElementObj(parts[1]), + receiverElement: offendersHelpers.domPathToDomElementObj(parts[2]) + }; + }) + }; } }, "DOMqueries": { @@ -75,7 +101,8 @@ var policies = { "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 + "isAbnormalThreshold": 2000, + "hasOffenders": false }, "DOMqueriesWithoutResults": { "tool": "phantomas", @@ -84,17 +111,27 @@ var policies = { "isOkThreshold": 0, "isBadThreshold": 100, "isAbnormalThreshold": 200, + "hasOffenders": true, "offendersTransformFn": function(offenders) { - return offenders.map(function(offender) { - var parts = /^(.*) ?\(in ?(.*)\) ?using ?(.*)$/.exec(offender); + return { + count: offenders.length, + list: offenders.map(function(offender) { + var parts = /^(.*) ?\(in ?(.*)\) ?using ?(.*)$/.exec(offender); - if (!parts) { - debug('DOMqueriesWithoutResults offenders transform function error with "%s"', offender); - return offender; - } + if (!parts) { + debug('DOMqueriesWithoutResults offenders transform function error with "%s"', offender); + return { + parseError: offender + }; + } - return '' + parts[1] + ' (in ' + offendersHelpers.domPathToButton(parts[2]) + ') using ' + parts[3]; - }); + return { + query: parts[1], + context: offendersHelpers.domPathToDomElementObj(parts[2]), + fn: parts[3] + }; + }) + }; } }, "DOMqueriesAvoidable": { @@ -104,18 +141,29 @@ var policies = { "isOkThreshold": 0, "isBadThreshold": 200, "isAbnormalThreshold": 500, + "hasOffenders": true, "takeOffendersFrom": "DOMqueriesDuplicated", "offendersTransformFn": function(offenders) { - return offenders.map(function(offender) { - var parts = /^[^"]* ?"(.*)" ?with ?(.*) ?\(in ?context ?(.*)\): ?(.*)\s?queries$/.exec(offender); + return { + count: offenders.length, + list: offenders.map(function(offender) { + var parts = /^[^"]* ?"(.*)" ?with ?(.*) ?\(in ?context ?(.*)\): ?(.*)\s?queries$/.exec(offender); - if (!parts) { - debug('DOMqueriesAvoidable offenders transform function error with "%s"', offender); - return offender; - } + if (!parts) { + debug('DOMqueriesAvoidable offenders transform function error with "%s"', offender); + return { + parseError: offender + }; + } - return '' + parts[1] + ' (in ' + offendersHelpers.domPathToButton(parts[3]) + ') using ' + parts[2] + ': ' + parts[4] + ' queries'; - }); + return { + query: parts[1], + context: offendersHelpers.domPathToDomElementObj(parts[3]), + fn: parts[2], + count: parseInt(parts[4], 10) + }; + }) + }; } }, "eventsBound": { @@ -125,17 +173,26 @@ var policies = { "isOkThreshold": 100, "isBadThreshold": 800, "isAbnormalThreshold": 1500, + "hasOffenders": true, "offendersTransformFn": function(offenders) { - return offenders.map(function(offender) { - var parts = /^"(.*)" ?bound ?to ?"(.*)"$/.exec(offender); + return { + count: offenders.length, + list: offenders.map(function(offender) { + var parts = /^"(.*)" ?bound ?to ?"(.*)"$/.exec(offender); - if (!parts) { - debug('eventsBound offenders transform function error with "%s"', offender); - return offender; - } + if (!parts) { + debug('eventsBound offenders transform function error with "%s"', offender); + return { + parseError: offender + }; + } - return '' + parts[1] + ' bound to ' + offendersHelpers.domPathToButton(parts[2]); - }); + return { + eventName: parts[1], + element: offendersHelpers.domPathToDomElementObj(parts[2]) + }; + }) + }; } }, "jsErrors": { @@ -145,22 +202,28 @@ var policies = { "isOkThreshold": 0, "isBadThreshold": 1, "isAbnormalThreshold": 4, + "hasOffenders": true, "offendersTransformFn": function(offenders) { - return offenders.map(function(offender) { - var parts = /^(.*) - (.*)$/.exec(offender); + return { + count: offenders.length, + list: offenders.map(function(offender) { + var parts = /^(.*) - (.*)$/.exec(offender); - if (!parts) { - debug('jsErrors offenders transform function error with "%s"', offender); - return offender; - } + if (!parts) { + debug('jsErrors offenders transform function error with "%s"', offender); + return { + parseError: offender + }; + } - var backtraceArray = offendersHelpers.backtraceToArray(parts[2]); - if (backtraceArray === null) { - return offender; - } else { - return '' + parts[1] + ' ' + offendersHelpers.backtraceArrayToHtml(backtraceArray); - } - }); + var backtraceArray = offendersHelpers.backtraceToArray(parts[2]); + + return { + error: parts[1], + backtrace: backtraceArray || [] + }; + }) + }; } }, "evalCalls": { @@ -169,7 +232,8 @@ var policies = { "message": "

The 'eval' function is slow and is a bad coding practice. Try to get rid of it.

", "isOkThreshold": 0, "isBadThreshold": 10, - "isAbnormalThreshold": 20 + "isAbnormalThreshold": 20, + "hasOffenders": false }, "documentWriteCalls": { "tool": "phantomas", @@ -177,7 +241,8 @@ var policies = { "message": "

They slow down the page construction, especially if they are used to insert scripts in the page. Remove them ASAP.

If you cannot remove them because they come from a third-party script (such as ads), have a look at PostScribe.

", "isOkThreshold": 0, "isBadThreshold": 10, - "isAbnormalThreshold": 20 + "isAbnormalThreshold": 20, + "hasOffenders": false }, "consoleMessages": { "tool": "phantomas", @@ -185,7 +250,8 @@ var policies = { "message": "

Try to keep your console clean when in production. Debugging is good for development only.

Writing in the console has a cost, especially when dumping large object variables.

There is also a problem with Internet Explorer 8, not knowing the console object.

", "isOkThreshold": 0, "isBadThreshold": 10, - "isAbnormalThreshold": 25 + "isAbnormalThreshold": 25, + "hasOffenders": false }, "globalVariables": { "tool": "phantomas", @@ -194,17 +260,22 @@ var policies = { "isOkThreshold": 10, "isBadThreshold": 50, "isAbnormalThreshold": 200, + "hasOffenders": true, "offendersTransformFn": function(offenders) { - return offendersHelpers.sortVarsLikeChromeDevTools(offenders); + return { + count: offenders.length, + list: offendersHelpers.sortVarsLikeChromeDevTools(offenders) + }; } }, "jQueryVersion": { "label": "jQuery version", "message": "

Current latest versions of jQuery are 1.11 (with support for old IE versions) and 2.1 (without).

Each new version of jQuery optimizes performances. Do not keep an old version of jQuery. Updating can sometimes break a few things, but it is generally quite easy to fix them up. So don't hesitate.

", + "hasOffenders": false, "scoreFn": function(data) { var differentVersions = data.toolsResults.phantomas.metrics.jQueryDifferentVersions; - if (differentVersions === 0 || differentVersions > 1) { + if (differentVersions === 0 || differentVersions > 1 || !data.toolsResults.phantomas.metrics.jQueryVersion) { // Not applicable return null; } else { @@ -257,7 +328,8 @@ var policies = { "message": "

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

", "isOkThreshold": 1, "isBadThreshold": 2, - "isAbnormalThreshold": 2 + "isAbnormalThreshold": 2, + "hasOffenders": true }, "cssParsingErrors": { "tool": "phantomas", @@ -266,32 +338,28 @@ var policies = { "isOkThreshold": 0, "isBadThreshold": 1, "isAbnormalThreshold": 1, + "hasOffenders": true, "offendersTransformFn": function(offenders) { - return offenders.map(function(offender) { - var parts = /^(([^ \(]*|) ?)?(?:\((((?! @ ).)*)( @ (\d+):(\d+))?\))?$/.exec(offender); + return { + count: offenders.length, + list: offenders.map(function(offender) { + var parts = /^(?:(?:<([^ \(]*)>|\[inline CSS\]) ?)?(?:\((((?! @ ).)*)(?: @ (\d+):(\d+))?\))?$/.exec(offender); - if (!parts) { - debug('cssParsingErrors offenders transform function error with "%s"', offender); - return offender; - } + if (!parts) { + debug('cssParsingErrors offenders transform function error with "%s"', offender); + return { + parseError: offender + }; + } - var html = (parts[3]) ? parts[3] : ''; - - if (parts[2]) { - html += (parts[2] === '') ? ' <inline CSS>' : ' ' + offendersHelpers.urlToLink(parts[2]); - } - - if (parts[5]) { - html += ' @ ' + parts[6] + ':' + parts[7]; - } - - if (parts[2] && parts[2] !== '') { - var w3cDirectUrl = 'http://jigsaw.w3.org/css-validator/validator?uri=' + encodeURIComponent(parts[2]) + '&profile=css3&usermedium=all&warning=no'; - html += ' (Check on the W3C validator)'; - } - - return html; - }); + return { + error: parts[2], + file: parts[1] || null, + line: (parts[4] && parts[5]) ? parseInt(parts[4], 10) : null, + column: (parts[4] && parts[5]) ? parseInt(parts[5], 10) : null + }; + }) + }; } }, "cssRules": { @@ -300,7 +368,8 @@ var policies = { "message": "

Having a huge number of CSS rules hurts performances. If the number of CSS rules is higher than the number of DOM elements, there is clearly a problem.

Huge stylesheets generally occur when the different pages of a website load all the CSS, concatenated in a single stylesheet, even if a large part of the rules are page-specific. Solution is to create one main CSS file with global rules and one custom files per page.

", "isOkThreshold": 500, "isBadThreshold": 2500, - "isAbnormalThreshold": 4000 + "isAbnormalThreshold": 4000, + "hasOffenders": false }, "cssComplexSelectors": { "tool": "phantomas", @@ -309,12 +378,15 @@ var policies = { "isOkThreshold": 0, "isBadThreshold": 500, "isAbnormalThreshold": 2000, + "hasOffenders": true, "offendersTransformFn": function(offenders) { - return offenders.map(function(offender) { - var splittedOffender = offendersHelpers.cssOffenderPattern(offender); - - return '' + splittedOffender.offender + ' @ ' + splittedOffender.line + ':' + splittedOffender.character; - }); + return { + count: offenders.length, + list: offenders.map(function(offender) { + var splittedOffender = offendersHelpers.cssOffenderPattern(offender); + return splittedOffender; + }) + }; } }, "cssComplexSelectorsByAttribute": { @@ -324,14 +396,18 @@ var policies = { "isOkThreshold": 0, "isBadThreshold": 50, "isAbnormalThreshold": 100, + "hasOffenders": true, "offendersTransformFn": function(offenders) { - return offenders.map(function(offender) { - var splittedOffender = offendersHelpers.cssOffenderPattern(offender); + return { + count: offenders.length, + list: offenders.map(function(offender) { + var splittedOffender = offendersHelpers.cssOffenderPattern(offender); - var boldedOffender = splittedOffender.offender.replace(/(\[[^ ]+[~\|\^\$\*]="[^"]+"\])/g, '$1'); + splittedOffender.bolded = splittedOffender.css.replace(/(\[[^ ]+[~\|\^\$\*]="[^"]+"\])/g, '$1'); - return boldedOffender + ' @ ' + splittedOffender.line + ':' + splittedOffender.character; - }); + return splittedOffender; + }) + }; } }, "cssImports": { @@ -341,12 +417,15 @@ var policies = { "isOkThreshold": 0, "isBadThreshold": 1, "isAbnormalThreshold": 1, + "hasOffenders": true, "offendersTransformFn": function(offenders) { - return offenders.map(function(offender) { - var splittedOffender = offendersHelpers.cssOffenderPattern(offender); - - return '' + splittedOffender.offender + ' @ ' + splittedOffender.line + ':' + splittedOffender.character; - }); + return { + count: offenders.length, + list: offenders.map(function(offender) { + var splittedOffender = offendersHelpers.cssOffenderPattern(offender); + return splittedOffender; + }) + }; } }, "cssDuplicatedSelectors": { @@ -356,17 +435,26 @@ var policies = { "isOkThreshold": 0, "isBadThreshold": 40, "isAbnormalThreshold": 80, + "hasOffenders": true, "offendersTransformFn": function(offenders) { - return offenders.map(function(offender) { - var parts = /^(.*) \((\d+) times\)$/.exec(offender); + return { + count: offenders.length, + list: offenders.map(function(offender) { + var parts = /^(.*) \((\d+) times\)$/.exec(offender); - if (!parts) { - debug('cssDuplicatedSelectors offenders transform function error with "%s"', offender); - return offender; - } + if (!parts) { + debug('cssDuplicatedSelectors offenders transform function error with "%s"', offender); + return { + parseError: offender + }; + } - return parts[1] + ' (x' + parts[2] + ')'; - }); + return { + rule: parts[1], + occurrences: parseInt(parts[2], 10) + }; + }) + }; } }, "cssDuplicatedProperties": { @@ -376,19 +464,31 @@ var policies = { "isOkThreshold": 0, "isBadThreshold": 50, "isAbnormalThreshold": 100, + "hasOffenders": true, "offendersTransformFn": function(offenders) { - return offenders.map(function(offender) { - var splittedOffender = offendersHelpers.cssOffenderPattern(offender); + return { + count: offenders.length, + list: offenders.map(function(offender) { + var splittedOffender = offendersHelpers.cssOffenderPattern(offender); - var parts = /^([^{]+) {([^ ]+): (.+)}$/.exec(splittedOffender.offender); + var parts = /^([^{]+) {([^ ]+): (.+)}$/.exec(splittedOffender.css); - if (!parts) { - debug('cssDuplicatedProperties offenders transform function error with "%s"', offender); - return offender; - } + if (!parts) { + debug('cssDuplicatedProperties offenders transform function error with "%s"', offender); + return { + parseError: offender + }; + } - return 'Property ' + parts[2] + ' duplicated in ' + parts[1] + ' { } @ ' + splittedOffender.line + ':' + splittedOffender.character; - }); + return { + property: parts[2], + rule: parts[1], + file: splittedOffender.file, + line: splittedOffender.line, + column: splittedOffender.column + }; + }) + }; } }, "cssEmptyRules": { @@ -398,11 +498,16 @@ var policies = { "isOkThreshold": 0, "isBadThreshold": 40, "isAbnormalThreshold": 100, + "hasOffenders": true, "offendersTransformFn": function(offenders) { - return offenders.map(function(offender) { - var splittedOffender = offendersHelpers.cssOffenderPattern(offender); - return '' + splittedOffender.offender + ' { } @ ' + splittedOffender.line + ':' + splittedOffender.character; - }); + return { + count: offenders.length, + list: offenders.map(function(offender) { + var splittedOffender = offendersHelpers.cssOffenderPattern(offender); + + return splittedOffender; + }) + }; } }, "cssExpressions": { @@ -412,19 +517,32 @@ var policies = { "isOkThreshold": 0, "isBadThreshold": 1, "isAbnormalThreshold": 20, + "hasOffenders": true, "offendersTransformFn": function(offenders) { - return offenders.map(function(offender) { - var splittedOffender = offendersHelpers.cssOffenderPattern(offender); + return { + count: offenders.length, + list: offenders.map(function(offender) { + var splittedOffender = offendersHelpers.cssOffenderPattern(offender); - var parts = /^(.*) {([^ ]+): expression\((.*)\)}$/.exec(splittedOffender.offender); + var parts = /^(.*) {([^ ]+): expression\((.*)\)}$/.exec(splittedOffender.css); - if (!parts) { - debug('cssExpressions offenders transform function error with "%s"', offender); - return offender; - } + if (!parts) { + debug('cssExpressions offenders transform function error with "%s"', offender); + return { + parseError: offender + }; + } - return parts[1] + ' {' + parts[2] + ': expression(' + parts[3] + ')} @ ' + splittedOffender.line + ':' + splittedOffender.character; - }); + return { + rule: parts[1], + property: parts[2], + expression: parts[3], + file: splittedOffender.file, + line: splittedOffender.line, + column: splittedOffender.column + }; + }) + }; } }, "cssImportants": { @@ -434,19 +552,32 @@ var policies = { "isOkThreshold": 0, "isBadThreshold": 50, "isAbnormalThreshold": 150, + "hasOffenders": true, "offendersTransformFn": function(offenders) { - return offenders.map(function(offender) { - var splittedOffender = offendersHelpers.cssOffenderPattern(offender); + return { + count: offenders.length, + list: offenders.map(function(offender) { + var splittedOffender = offendersHelpers.cssOffenderPattern(offender); - var parts = /^(.*) {([^ ]+): (.*) ?\!important}$/.exec(splittedOffender.offender); + var parts = /^(.*) {([^ ]+): (.*) ?\!important}$/.exec(splittedOffender.css); - if (!parts) { - debug('cssImportants offenders transform function error with "%s"', offender); - return offender; - } + if (!parts) { + debug('cssImportants offenders transform function error with "%s"', offender); + return { + parseError: offender + }; + } - return parts[1] + ' {' + parts[2] + ': ' + parts[3] + ' !important} @ ' + splittedOffender.line + ':' + splittedOffender.character; - }); + return { + rule: parts[1], + property: parts[2], + value: parts[3], + file: splittedOffender.file, + line: splittedOffender.line, + column: splittedOffender.column + }; + }) + }; } }, "cssOldIEFixes": { @@ -456,47 +587,54 @@ var policies = { "isOkThreshold": 0, "isBadThreshold": 50, "isAbnormalThreshold": 300, + "hasOffenders": true, "offendersTransformFn": function(offenders) { - return offenders.map(function(offender) { - var splittedOffender = offendersHelpers.cssOffenderPattern(offender); + return { + count: offenders.length, + list: offenders.map(function(offender) { + var splittedOffender = offendersHelpers.cssOffenderPattern(offender); - var parts = /^([^{]*)( {([^ ]+): (.*)})?$/.exec(splittedOffender.offender); + var parts = /^([^{]*)( {([^ ]+): (.*)})?$/.exec(splittedOffender.css); - if (!parts) { - debug('cssOldIEFixes offenders transform function error with "%s"', offender); - return offender; - } + if (!parts) { + debug('cssOldIEFixes offenders transform function error with "%s"', offender); + return { + parseError: offender + }; + } - var rule = parts[1]; - var property = parts[3]; - var value = parts[4]; - var browser = null; + var rule = parts[1]; + var property = parts[3]; + var value = parts[4]; + var browser = null; - if (rule.indexOf('* html') === 0) { - rule = rule.replace(/^\* html/, '* html'); - browser = 'IE6'; - } else if (rule.indexOf('html>body') === 0) { - rule = rule.replace(/^html>body/, 'html>body'); - browser = 'IE6'; - } else if (property.indexOf('*') === 0) { - property = '' + property + ''; - browser = 'IE7'; - } else if (value.match(/\!ie$/)) { - value = value.replace(/\!ie$/, '!ie'); - browser = 'IE7'; - } else if (property === '-ms-filter') { - property = '-ms-filter'; - browser = 'IE9'; - } else if (value.indexOf('progid:DXImageTransform.Microsoft') === 0) { - value = value.replace(/^progid:DXImageTransform\.Microsoft/, 'progid:DXImageTransform.Microsoft'); - browser = 'IE9'; - } + if (rule.indexOf('* html') === 0) { + rule = rule.replace(/^\* html/, '* html'); + browser = 'IE6'; + } else if (rule.indexOf('html>body') === 0) { + rule = rule.replace(/^html>body/, 'html>body'); + browser = 'IE6'; + } else if (property.indexOf('*') === 0) { + property = '' + property + ''; + browser = 'IE7'; + } else if (value.match(/\!ie$/)) { + value = value.replace(/\!ie$/, '!ie'); + browser = 'IE7'; + } else if (property === '-ms-filter') { + property = '-ms-filter'; + browser = 'IE9'; + } else if (value.indexOf('progid:DXImageTransform.Microsoft') >= 0) { + value = value.replace(/progid:DXImageTransform\.Microsoft/, 'progid:DXImageTransform.Microsoft'); + browser = 'IE9'; + } - browser = browser ? '' + browser + ' fix: ' : ''; - var propertyAndValue = (property && value) ? ' {' + property + ': ' + value + '}' : ''; + var propertyAndValue = (property && value) ? ' {' + property + ': ' + value + '}' : ''; + splittedOffender.bolded = rule + propertyAndValue; + splittedOffender.browser = browser; - return browser + rule + propertyAndValue + ' @ ' + splittedOffender.line + ':' + splittedOffender.character; - }); + return splittedOffender; + }) + }; } }, "cssOldPropertyPrefixes": { @@ -506,19 +644,32 @@ var policies = { "isOkThreshold": 0, "isBadThreshold": 50, "isAbnormalThreshold": 300, + "hasOffenders": true, "offendersTransformFn": function(offenders) { - return offenders.map(function(offender) { - var splittedOffender = offendersHelpers.cssOffenderPattern(offender); + return { + count: offenders.length, + list: offenders.map(function(offender) { + var splittedOffender = offendersHelpers.cssOffenderPattern(offender); - var parts = /^([^{]*)(?: ?{ ?([^ ]+): (.*) ?}) \/\/ (.*)$/.exec(splittedOffender.offender); + var parts = /^([^{]*)(?: ?{ ?([^ ]+): (.*) ?}) \/\/ (.*)$/.exec(splittedOffender.css); - if (!parts) { - debug('cssOldPropertyPrefixes offenders transform function error with "%s"', offender); - return offender; - } + if (!parts) { + debug('cssOldPropertyPrefixes offenders transform function error with "%s"', offender); + return { + parseError: offender + }; + } - return '
' + parts[1] + ' {' + parts[2] + ': ' + parts[3] + '} @ ' + splittedOffender.line + ':' + splittedOffender.character + '
' + parts[4] + '
'; - }); + return { + rule: parts[1], + property: parts[2], + value: parts[3], + file: splittedOffender.file, + line: splittedOffender.line, + column: splittedOffender.column + }; + }) + }; } }, "cssUniversalSelectors": { @@ -527,7 +678,17 @@ var policies = { "message": "

Universal selectors are the most expensive CSS selectors.

More informations here.

", "isOkThreshold": 0, "isBadThreshold": 40, - "isAbnormalThreshold": 150 + "isAbnormalThreshold": 150, + "hasOffenders": true, + "offendersTransformFn": function(offenders) { + return { + count: offenders.length, + list: offenders.map(function(offender) { + var splittedOffender = offendersHelpers.cssOffenderPattern(offender); + return splittedOffender; + }) + }; + } }, "cssRedundantBodySelectors": { "tool": "phantomas", @@ -536,15 +697,18 @@ var policies = { "isOkThreshold": 0, "isBadThreshold": 50, "isAbnormalThreshold": 200, + "hasOffenders": true, "offendersTransformFn": function(offenders) { - return offenders.map(function(offender) { - var splittedOffender = offendersHelpers.cssOffenderPattern(offender); + return { + count: offenders.length, + list: offenders.map(function(offender) { + var splittedOffender = offendersHelpers.cssOffenderPattern(offender); - var rule = splittedOffender.offender; - rule = rule.replace(/body/, 'body'); + splittedOffender.bolded = splittedOffender.css.replace(/body/, 'body'); - return rule + ' @ ' + splittedOffender.line + ':' + splittedOffender.character; - }); + return splittedOffender; + }) + }; } }, "cssRedundantChildNodesSelectors": { @@ -554,25 +718,31 @@ var policies = { "isOkThreshold": 0, "isBadThreshold": 50, "isAbnormalThreshold": 200, + "hasOffenders": true, "offendersTransformFn": function(offenders) { - return offenders.map(function(offender) { - var splittedOffender = offendersHelpers.cssOffenderPattern(offender); + return { + count: offenders.length, + list: offenders.map(function(offender) { + var splittedOffender = offendersHelpers.cssOffenderPattern(offender); - var rule = splittedOffender.offender; - var redundanters = [ - ['ul', 'li'], - ['ol', 'li'], - ['select', 'option'], - ['table', 'tr'], - ['table', 'th'], - ]; + var rule = splittedOffender.css; + var redundanters = [ + ['ul', 'li'], + ['ol', 'li'], + ['select', 'option'], + ['table', 'tr'], + ['table', 'th'], + ]; - redundanters.forEach(function(couple) { - rule = rule.replace(new RegExp('(^| |>)' + couple[0] + '([^ >]*)?([ >]| > )' + couple[1] + '([^\\w-]|$)', 'g'), '$1' + couple[0] + '$2$3' + couple[1] + '$4'); - }); + redundanters.forEach(function(couple) { + rule = rule.replace(new RegExp('(^| |>)' + couple[0] + '([^ >]*)?([ >]| > )' + couple[1] + '([^\\w-]|$)', 'g'), '$1' + couple[0] + '$2$3' + couple[1] + '$4'); + }); - return rule + ' @ ' + splittedOffender.line + ':' + splittedOffender.character; - }); + splittedOffender.bolded = rule; + + return splittedOffender; + }) + }; } }, "requests": { @@ -582,16 +752,18 @@ var policies = { "isOkThreshold": 15, "isBadThreshold": 100, "isAbnormalThreshold": 200, + "hasOffenders": true, "takeOffendersFrom": ["htmlCount", "jsCount", "cssCount", "imageCount", "webfontCount", "videoCount", "jsonCount", "jsonCount"], "offendersTransformFn": function(offenders) { - return offenders - .map(function(offender) { - return offendersHelpers.fileWithSizePattern(offender); - }).sort(function(a, b) { - return b.size - a.size; - }).map(function(fileObj) { - return offendersHelpers.urlToLink(fileObj.file) + ((fileObj.size !== undefined) ? ' (' + fileObj.size + ' kB)' : ''); - }); + return { + count: offenders.length, + list: offenders + .map(function(offender) { + return offendersHelpers.fileWithSizePattern(offender); + }).sort(function(a, b) { + return b.size - a.size; + }) + }; } }, "htmlCount": { @@ -601,11 +773,14 @@ var policies = { "isOkThreshold": 10, "isBadThreshold": 20, "isAbnormalThreshold": 30, + "hasOffenders": true, "offendersTransformFn": function(offenders) { - return offenders.map(function(offender) { - var fileObj = offendersHelpers.fileWithSizePattern(offender); - return offendersHelpers.urlToLink(fileObj.file) + ((fileObj.size !== undefined) ? ' (' + fileObj.size + ' kB)' : ''); - }); + return { + count: offenders.length, + list: offenders.map(function(offender) { + return offendersHelpers.fileWithSizePattern(offender); + }) + }; } }, "jsCount": { @@ -615,11 +790,14 @@ var policies = { "isOkThreshold": 5, "isBadThreshold": 15, "isAbnormalThreshold": 30, + "hasOffenders": true, "offendersTransformFn": function(offenders) { - return offenders.map(function(offender) { - var fileObj = offendersHelpers.fileWithSizePattern(offender); - return offendersHelpers.urlToLink(fileObj.file) + ((fileObj.size !== undefined) ? ' (' + fileObj.size + ' kB)' : ''); - }); + return { + count: offenders.length, + list: offenders.map(function(offender) { + return offendersHelpers.fileWithSizePattern(offender); + }) + }; } }, "cssCount": { @@ -629,11 +807,14 @@ var policies = { "isOkThreshold": 3, "isBadThreshold": 10, "isAbnormalThreshold": 22, + "hasOffenders": true, "offendersTransformFn": function(offenders) { - return offenders.map(function(offender) { - var fileObj = offendersHelpers.fileWithSizePattern(offender); - return offendersHelpers.urlToLink(fileObj.file) + ((fileObj.size !== undefined) ? ' (' + fileObj.size + ' kB)' : ''); - }); + return { + count: offenders.length, + list: offenders.map(function(offender) { + return offendersHelpers.fileWithSizePattern(offender); + }) + }; } }, "imageCount": { @@ -643,11 +824,14 @@ var policies = { "isOkThreshold": 15, "isBadThreshold": 40, "isAbnormalThreshold": 70, + "hasOffenders": true, "offendersTransformFn": function(offenders) { - return offenders.map(function(offender) { - var fileObj = offendersHelpers.fileWithSizePattern(offender); - return offendersHelpers.urlToLink(fileObj.file) + ((fileObj.size !== undefined) ? ' (' + fileObj.size + ' kB)' : ''); - }); + return { + count: offenders.length, + list: offenders.map(function(offender) { + return offendersHelpers.fileWithSizePattern(offender); + }) + }; } }, "webfontCount": { @@ -657,11 +841,14 @@ var policies = { "isOkThreshold": 0, "isBadThreshold": 3, "isAbnormalThreshold": 5, + "hasOffenders": true, "offendersTransformFn": function(offenders) { - return offenders.map(function(offender) { - var fileObj = offendersHelpers.fileWithSizePattern(offender); - return offendersHelpers.urlToLink(fileObj.file) + ((fileObj.size !== undefined) ? ' (' + fileObj.size + ' kB)' : ''); - }); + return { + count: offenders.length, + list: offenders.map(function(offender) { + return offendersHelpers.fileWithSizePattern(offender); + }) + }; } }, "videoCount": { @@ -671,11 +858,14 @@ var policies = { "isOkThreshold": 1, "isBadThreshold": 5, "isAbnormalThreshold": 15, + "hasOffenders": true, "offendersTransformFn": function(offenders) { - return offenders.map(function(offender) { - var fileObj = offendersHelpers.fileWithSizePattern(offender); - return offendersHelpers.urlToLink(fileObj.file) + ((fileObj.size !== undefined) ? ' (' + fileObj.size + ' kB)' : ''); - }); + return { + count: offenders.length, + list: offenders.map(function(offender) { + return offendersHelpers.fileWithSizePattern(offender); + }) + }; } }, "jsonCount": { @@ -685,11 +875,14 @@ var policies = { "isOkThreshold": 2, "isBadThreshold": 10, "isAbnormalThreshold": 25, + "hasOffenders": true, "offendersTransformFn": function(offenders) { - return offenders.map(function(offender) { - var fileObj = offendersHelpers.fileWithSizePattern(offender); - return offendersHelpers.urlToLink(fileObj.file) + ((fileObj.size !== undefined) ? ' (' + fileObj.size + ' kB)' : ''); - }); + return { + count: offenders.length, + list: offenders.map(function(offender) { + return offendersHelpers.fileWithSizePattern(offender); + }) + }; } }, "otherCount": { @@ -699,11 +892,14 @@ var policies = { "isOkThreshold": 5, "isBadThreshold": 20, "isAbnormalThreshold": 40, + "hasOffenders": true, "offendersTransformFn": function(offenders) { - return offenders.map(function(offender) { - var fileObj = offendersHelpers.fileWithSizePattern(offender); - return offendersHelpers.urlToLink(fileObj.file) + ((fileObj.size !== undefined) ? ' (' + fileObj.size + ' kB)' : ''); - }); + return { + count: offenders.length, + list: offenders.map(function(offender) { + return offendersHelpers.fileWithSizePattern(offender); + }) + }; } }, "smallJsFiles": { @@ -713,11 +909,14 @@ var policies = { "isOkThreshold": 2, "isBadThreshold": 10, "isAbnormalThreshold": 16, + "hasOffenders": true, "offendersTransformFn": function(offenders) { - return offenders.map(function(offender) { - var fileObj = offendersHelpers.fileWithSizePattern(offender); - return offendersHelpers.urlToLink(fileObj.file) + ((fileObj.size !== undefined) ? ' (' + fileObj.size + ' kB)' : ''); - }); + return { + count: offenders.length, + list: offenders.map(function(offender) { + return offendersHelpers.fileWithSizePattern(offender); + }) + }; } }, "smallCssFiles": { @@ -727,11 +926,14 @@ var policies = { "isOkThreshold": 0, "isBadThreshold": 8, "isAbnormalThreshold": 12, + "hasOffenders": true, "offendersTransformFn": function(offenders) { - return offenders.map(function(offender) { - var fileObj = offendersHelpers.fileWithSizePattern(offender); - return offendersHelpers.urlToLink(fileObj.file) + ((fileObj.size !== undefined) ? ' (' + fileObj.size + ' kB)' : ''); - }); + return { + count: offenders.length, + list: offenders.map(function(offender) { + return offendersHelpers.fileWithSizePattern(offender); + }) + }; } }, "smallImages": { @@ -741,11 +943,14 @@ var policies = { "isOkThreshold": 2, "isBadThreshold": 17, "isAbnormalThreshold": 30, + "hasOffenders": true, "offendersTransformFn": function(offenders) { - return offenders.map(function(offender) { - var fileObj = offendersHelpers.fileWithSizePattern(offender); - return offendersHelpers.urlToLink(fileObj.file) + ((fileObj.size !== undefined) ? ' (' + fileObj.size + ' kB)' : ''); - }); + return { + count: offenders.length, + list: offenders.map(function(offender) { + return offendersHelpers.fileWithSizePattern(offender); + }) + }; } }, "notFound": { @@ -755,11 +960,7 @@ var policies = { "isOkThreshold": 0, "isBadThreshold": 1, "isAbnormalThreshold": 1, - "offendersTransformFn": function(offenders) { - return offenders.map(function(offender) { - return offendersHelpers.urlToLink(offender); - }); - } + "hasOffenders": true }, "closedConnections": { "tool": "phantomas", @@ -768,11 +969,7 @@ var policies = { "isOkThreshold": 0, "isBadThreshold": 8, "isAbnormalThreshold": 20, - "offendersTransformFn": function(offenders) { - return offenders.map(function(offender) { - return offendersHelpers.urlToLink(offender); - }); - } + "hasOffenders": true }, "multipleRequests": { "tool": "phantomas", @@ -781,11 +978,7 @@ var policies = { "isOkThreshold": 0, "isBadThreshold": 3, "isAbnormalThreshold": 10, - "offendersTransformFn": function(offenders) { - return offenders.map(function(offender) { - return offendersHelpers.urlToLink(offender); - }); - } + "hasOffenders": true }, "cachingDisabled": { "tool": "phantomas", @@ -794,11 +987,7 @@ var policies = { "isOkThreshold": 0, "isBadThreshold": 12, "isAbnormalThreshold": 25, - "offendersTransformFn": function(offenders) { - return offenders.map(function(offender) { - return offendersHelpers.urlToLink(offender); - }); - } + "hasOffenders": true }, "cachingNotSpecified": { "tool": "phantomas", @@ -807,11 +996,7 @@ var policies = { "isOkThreshold": 5, "isBadThreshold": 20, "isAbnormalThreshold": 40, - "offendersTransformFn": function(offenders) { - return offenders.map(function(offender) { - return offendersHelpers.urlToLink(offender); - }); - } + "hasOffenders": true }, "cachingTooShort": { "tool": "phantomas", @@ -820,45 +1005,56 @@ var policies = { "isOkThreshold": 5, "isBadThreshold": 20, "isAbnormalThreshold": 40, + "hasOffenders": true, "offendersTransformFn": function(offenders) { - return offenders - .map(function(offender) { - var parts = /^([^ ]*) cached for (-?\d+(\.\d+)?) s$/.exec(offender); + return { + count: offenders.length, + list: offenders + .map(function(offender) { + var parts = /^([^ ]*) cached for (-?\d+(\.\d+)?) s$/.exec(offender); + + if (!parts) { + debug('cachingTooShort offenders transform function error with "%s"', offender); + return { + file: offender + }; + } - if (!parts) { - debug('cachingTooShort offenders transform function error with "%s"', offender); return { - file: offender + file: parts[1], + ttl: Math.round(parseFloat(parts[2])) }; - } - return { - file: parts[1], - ttl: Math.round(parseFloat(parts[2])) - }; - }).sort(function(a, b) { - return a.ttl - b.ttl; - }).map(function(obj) { - var duration = obj.ttl; - var unit = 'seconds'; + }).sort(function(a, b) { - if (duration >= 120) { - duration = Math.round(duration / 60); - unit = 'minutes'; - } + return a.ttl - b.ttl; - if (duration >= 120) { - duration = Math.round(duration / 60); - unit = 'hours'; - } + }).map(function(obj) { - if (duration >= 48) { - duration = Math.round(duration / 24); - unit = 'days'; - } + var duration = obj.ttl; + var unit = 'seconds'; - return offendersHelpers.urlToLink(obj.file) + ' cached for ' + duration + ' ' + unit + ''; - }); + if (duration >= 120) { + duration = Math.round(duration / 60); + unit = 'minutes'; + } + + if (duration >= 120) { + duration = Math.round(duration / 60); + unit = 'hours'; + } + + if (duration >= 48) { + duration = Math.round(duration / 24); + unit = 'days'; + } + + obj.ttlWithUnit = duration; + obj.unit = unit; + + return obj; + }) + }; } }, "domains": { @@ -868,17 +1064,27 @@ var policies = { "isOkThreshold": 10, "isBadThreshold": 25, "isAbnormalThreshold": 50, + "hasOffenders": true, "offendersTransformFn": function(offenders) { - return offenders.map(function(offender) { - var parts = /^([^ ]*): (\d+) request\(s\)$/.exec(offender); + return { + count: offenders.length, + list: offenders.map(function(offender) { + var parts = /^([^ ]*): (\d+) request\(s\)$/.exec(offender); - if (!parts) { - debug('domains offenders transform function error with "%s"', offender); - return offender; - } + if (!parts) { + debug('domains offenders transform function error with "%s"', offender); + return { + file: offender + }; + } - return '' + parts[1] + ' (' + parts[2] + ' ' + ((parseInt(parts[2], 10) > 1) ? 'requests' : 'request') + ')'; - }); + return { + domain: parts[1], + requests: parseInt(parts[2]) + }; + + }) + }; } } }; diff --git a/lib/offendersHelpers.js b/lib/offendersHelpers.js index 36151c6..f2dde54 100644 --- a/lib/offendersHelpers.js +++ b/lib/offendersHelpers.js @@ -31,70 +31,69 @@ var OffendersHelpers = function() { return result; }; - this.domTreeToHTML = function(domTree) { - - function recursiveHtmlBuilder(tree) { - var html = ''; - var keys = Object.keys(tree); - - keys.forEach(function(key) { - if (isNaN(tree[key])) { - html += '
' + key + '' + recursiveHtmlBuilder(tree[key]) + '
'; - } else if (tree[key] > 1) { - html += '
' + key + ' (x' + tree[key] + ')
'; - } else { - html += '
' + key + '
'; - } - }); - - return html; - } - - return '
' + recursiveHtmlBuilder(domTree) + '
'; - }; - - this.listOfDomPathsToHTML = function(domPaths) { - var domArrays = domPaths.map(this.domPathToArray); - var domTree = this.listOfDomArraysToTree(domArrays); - return this.domTreeToHTML(domTree); - }; - - this.domPathToButton = function(domPath) { + this.domPathToDomElementObj = function(domPath) { var domArray = this.domPathToArray(domPath); - var domTree = this.listOfDomPathsToHTML([domPath]); + var domTree = this.listOfDomArraysToTree([this.domPathToArray(domPath)]); if (domArray[0] === 'html') { - return '
html
'; + return { + type: 'html' + }; } if (domArray[0] === 'body') { if (domArray.length === 1) { - return '
body
'; + return { + type: 'body' + }; } else { - return '
DOM element ' + domArray[domArray.length - 1] + '' + domTree + '
'; + return { + type: 'domElement', + element: domArray[domArray.length - 1], + tree: domTree + }; } } if (domArray[0] === 'head') { - return '
head
'; + return { + type: 'head' + }; } if (domArray[0] === '#document') { - return '
document
'; + return { + type: 'document' + }; } if (domArray[0] === 'window') { - return '
window
'; + return { + type: 'window' + }; } if (domArray[0] === 'DocumentFragment') { if (domArray.length === 1) { - return '
Fragment
'; + return { + type: 'fragment' + }; } else { - return '
Fragment element ' + domArray[domArray.length - 1] + '' + domTree + '
'; + return { + type: 'fragmentElement', + element: domArray[domArray.length - 1], + tree: domTree + }; } } // Not attached element, such as just created with document.createElement() if (domArray.length === 1) { - return '
Created element ' + domPath + '
'; + return { + type: 'createdElement', + element: domPath + }; } else { - return '
Created element ' + domArray[domArray.length - 1] + '' + domTree + '
'; + return { + type: 'createdElement', + element: domArray[domArray.length - 1], + tree: domTree + }; } }; @@ -130,20 +129,6 @@ var OffendersHelpers = function() { } }; - this.backtraceArrayToHtml = function(backtraceArray) { - if (backtraceArray.length === 0) { - return '
no backtrace
'; - } - - var html = '
backtrace
'; - var that = this; - backtraceArray.forEach(function(backtraceObj) { - var functionName = (backtraceObj.functionName) ? backtraceObj.functionName + '() ' : ''; - html += '
' + functionName + that.urlToLink(backtraceObj.file) + ' line ' + backtraceObj.line + '
'; - }); - return html + '
'; - }; - this.sortVarsLikeChromeDevTools = function(vars) { return vars.sort(function(a, b) { @@ -157,7 +142,7 @@ var OffendersHelpers = function() { }; this.cssOffenderPattern = function(offender) { - var parts = /^(.*) @ (\d+):(\d+)$/.exec(offender); + var parts = /^(.*) (?:<([^ \(]*)>|\[inline CSS\]) @ (\d+):(\d+)$/.exec(offender); if (!parts) { return { @@ -165,9 +150,10 @@ var OffendersHelpers = function() { }; } else { return { - offender: parts[1], - line: parseInt(parts[2], 10), - character: parseInt(parts[3], 10) + css: parts[1], + file: parts[2] || null, + line: parseInt(parts[3], 10), + column: parseInt(parts[4], 10) }; } }; diff --git a/lib/rulesChecker.js b/lib/rulesChecker.js index 8f82b68..b82b7d7 100644 --- a/lib/rulesChecker.js +++ b/lib/rulesChecker.js @@ -26,52 +26,64 @@ var RulesChecker = function() { policy: extend({}, policy) // Clone object policy instead of reference }; - var offenders = []; - // Take DOMqueriesAvoidable's offenders from DOMqueriesDuplicated, for example. - if (policy.takeOffendersFrom) { - - var fromList = policy.takeOffendersFrom; - - // takeOffendersFrom option can be a string or an array of strings. - if (typeof fromList === 'string') { - fromList = [fromList]; - } - - fromList.forEach(function(from) { - if (data.toolsResults[policy.tool] && - data.toolsResults[policy.tool].offenders && - data.toolsResults[policy.tool].offenders[from]) { - offenders = offenders.concat(data.toolsResults[policy.tool].offenders[from]); + // Deal with offenders + if (policy.hasOffenders) { + + var offenders = []; + + // Take DOMqueriesAvoidable's offenders from DOMqueriesDuplicated, for example. + if (policy.takeOffendersFrom) { + + var fromList = policy.takeOffendersFrom; + + // takeOffendersFrom option can be a string or an array of strings. + if (typeof fromList === 'string') { + fromList = [fromList]; } - }); + + fromList.forEach(function(from) { + if (data.toolsResults[policy.tool] && + data.toolsResults[policy.tool].offenders && + data.toolsResults[policy.tool].offenders[from]) { + offenders = offenders.concat(data.toolsResults[policy.tool].offenders[from]); + } + }); - data.toolsResults[policy.tool].offenders[metricName] = offenders; + data.toolsResults[policy.tool].offenders[metricName] = offenders; - } else if (data.toolsResults[policy.tool] && - data.toolsResults[policy.tool].offenders && - data.toolsResults[policy.tool].offenders[metricName]) { - offenders = data.toolsResults[policy.tool].offenders[metricName]; - } - - // It is possible to declare a transformation function for the offenders. - // The function should take an array of strings as single parameter and return a string. - if (policy.offendersTransformFn) { - rule.offendersCount = offenders.length; - - try { - offenders = policy.offendersTransformFn(offenders); - } catch(err) { - debug('Error while transforming offenders for %s', metricName); - debug(err); + } else if (data.toolsResults[policy.tool] && + data.toolsResults[policy.tool].offenders && + data.toolsResults[policy.tool].offenders[metricName]) { + offenders = data.toolsResults[policy.tool].offenders[metricName]; } + + var offendersObj = {}; - delete rule.policy.offendersTransformFn; + // It is possible to declare a transformation function for the offenders. + // The function should take an array of strings as single parameter and return a string. + if (policy.offendersTransformFn) { + + try { + offendersObj = policy.offendersTransformFn(offenders); + } catch(err) { + debug('Error while transforming offenders for %s', metricName); + debug(err); + } + + delete rule.policy.offendersTransformFn; + + } else { + + offendersObj = { + count: offenders.length, + list: offenders + }; + } + + rule.offendersObj = offendersObj; } - if (offenders && offenders.length > 0) { - rule.offenders = offenders; - } rule.bad = rule.value > policy.isOkThreshold; rule.abnormal = policy.isAbnormalThreshold && rule.value >= policy.isAbnormalThreshold; diff --git a/lib/tools/phantomas/phantomasWrapper.js b/lib/tools/phantomas/phantomasWrapper.js index 4706201..594cde9 100644 --- a/lib/tools/phantomas/phantomasWrapper.js +++ b/lib/tools/phantomas/phantomasWrapper.js @@ -40,7 +40,8 @@ var PhantomasWrapper = function() { 'eventListeners', // overridden 'filmStrip', // not needed 'har', // not needed for the moment - 'javaScriptBottlenecks', // needs to be launched after custom module scopeYLT, + 'javaScriptBottlenecks', // needs to be launched after custom module scopeYLT + 'jQuery', // overridden 'jserrors', // overridden 'pageSource', // not needed 'screenshot', // not needed for the moment diff --git a/test/core/customPoliciesTest.js b/test/core/customPoliciesTest.js index 5593f2f..302b1c4 100644 --- a/test/core/customPoliciesTest.js +++ b/test/core/customPoliciesTest.js @@ -4,10 +4,362 @@ var rulesChecker = require('../../lib/rulesChecker'); describe('rulesChecker', function() { var policies = require('../../lib/metadata/policies.js'); + var results; + it('should transform DOMelementMaxDepth offenders', function() { + results = rulesChecker.check({ + "toolsResults": { + "phantomas": { + "metrics": { + "DOMelementMaxDepth": 3 + }, + "offenders": { + "DOMelementMaxDepth": [ + "body > div#foo > span.bar" + ] + } + } + } + }, policies); + + results.should.have.a.property('DOMelementMaxDepth'); + results.DOMelementMaxDepth.should.have.a.property('offendersObj').that.deep.equals({ + "count": 1, + "tree": { + "body": { + "div#foo": { + "span.bar": 1 + } + } + } + }); + }); + + + it('should transform DOMidDuplicated offenders', function() { + results = rulesChecker.check({ + "toolsResults": { + "phantomas": { + "metrics": { + "DOMidDuplicated": 2 + }, + "offenders": { + "DOMidDuplicated": [ + "colorswitch-30883-30865: 4 occurrences", + "foo: 1 occurrences" + ] + } + } + } + }, policies); + + results.should.have.a.property('DOMidDuplicated'); + results.DOMidDuplicated.should.have.a.property('offendersObj').that.deep.equals({ + "count": 2, + "list": [ + { + "id": "colorswitch-30883-30865", + "occurrences": 4 + }, + { + "id": "foo", + "occurrences": 1 + } + ] + }); + }); + + + it('should transform DOMinserts offenders', function() { + results = rulesChecker.check({ + "toolsResults": { + "phantomas": { + "metrics": { + "DOMinserts": 4 + }, + "offenders": { + "DOMinserts": [ + "\"div\" appended to \"html\"", + "\"DocumentFragment > link[0]\" appended to \"head\"", + "\"div#Netaff-yh1XbS0vK3NaRGu\" appended to \"body > div#Global\"", + "\"img\" appended to \"body\"" + ] + } + } + } + }, policies); + + results.should.have.a.property('DOMinserts'); + results.DOMinserts.should.have.a.property('offendersObj').that.deep.equals({ + "count": 4, + "list": [ + { + "insertedElement": { + "type": "createdElement", + "element": "div" + }, + "receiverElement": { + "type": "html" + } + }, + { + "insertedElement": { + "type": "fragmentElement", + "element": "link[0]", + "tree": { + "DocumentFragment": { + "link[0]": 1 + } + } + }, + "receiverElement": { + "type": "head" + } + }, + { + "insertedElement": { + "type": "createdElement", + "element": "div#Netaff-yh1XbS0vK3NaRGu" + }, + "receiverElement": { + "type": "domElement", + "element": "div#Global", + "tree": { + "body": { + "div#Global": 1 + } + } + } + }, + { + "insertedElement": { + "type": "createdElement", + "element": "img" + }, + "receiverElement": { + "type": "body" + } + } + ] + }); + }); + + + it('should transform DOMqueriesWithoutResults offenders', function() { + results = rulesChecker.check({ + "toolsResults": { + "phantomas": { + "metrics": { + "DOMqueriesWithoutResults": 2 + }, + "offenders": { + "DOMqueriesWithoutResults": [ + "#SearchMenu (in #document) using getElementById", + ".partnership-link (in body > div#Global > div#Header > ul#MainMenu) using getElementsByClassName" + ] + } + } + } + }, policies); + + results.should.have.a.property('DOMqueriesWithoutResults'); + results.DOMqueriesWithoutResults.should.have.a.property('offendersObj').that.deep.equals({ + "count": 2, + "list": [ + { + "context": { + "type": "document" + }, + "fn": "getElementById", + "query": "#SearchMenu " + }, + { + "context": { + "element": "ul#MainMenu", + "tree": { + "body": { + "div#Global": { + "div#Header": { + "ul#MainMenu": 1 + } + } + } + }, + "type": "domElement" + }, + "fn": "getElementsByClassName", + "query": ".partnership-link " + } + ] + }); + }); + + + it('should transform DOMqueriesAvoidable offenders', function() { + results = rulesChecker.check({ + "toolsResults": { + "phantomas": { + "metrics": { + "DOMqueriesAvoidable": 2 + }, + "offenders": { + "DOMqueriesDuplicated": [ + "id \"#j2t-top-cart\" with getElementById (in context #document): 4 queries", + "class \".listingResult\" with getElementsByClassName (in context body > div#Global > div#Listing): 4 queries" + ] + } + } + } + }, policies); + + results.should.have.a.property('DOMqueriesAvoidable'); + results.DOMqueriesAvoidable.should.have.a.property('offendersObj').that.deep.equals({ + "count": 2, + "list": [ + { + "query": "#j2t-top-cart", + "context": { + "type": "document" + }, + "fn": "getElementById ", + "count": 4 + }, + { + "query": ".listingResult", + "context": { + "type": "domElement", + "element": "div#Listing", + "tree": { + "body": { + "div#Global": { + "div#Listing": 1 + } + } + } + }, + "fn": "getElementsByClassName ", + "count": 4 + } + ] + }); + }); + + + it('should transform eventsBound offenders', function() { + results = rulesChecker.check({ + "toolsResults": { + "phantomas": { + "metrics": { + "eventsBound": 2 + }, + "offenders": { + "eventsBound": [ + "\"DOMContentLoaded\" bound to \"#document\"", + "\"unload\" bound to \"window\"", + "\"submit\" bound to \"body > div#Global > div#Header > form#search_mini_form\"" + ] + } + } + } + }, policies); + + results.should.have.a.property('eventsBound'); + results.eventsBound.should.have.a.property('offendersObj').that.deep.equals({ + "count": 3, + "list": [ + { + "element": { + "type": "document" + }, + "eventName": "DOMContentLoaded" + }, + { + "element": { + "type": "window" + }, + "eventName": "unload" + }, + { + "element": { + "element": "form#search_mini_form", + "tree": { + "body": { + "div#Global": { + "div#Header": { + "form#search_mini_form": 1 + } + } + } + }, + "type": "domElement" + }, + "eventName": "submit" + } + ] + }); + }); + + + it('should transform jsErrors offenders', function() { + results = rulesChecker.check({ + "toolsResults": { + "phantomas": { + "metrics": { + "jsErrors": 2 + }, + "offenders": { + "jsErrors": [ + "TypeError: 'undefined' is not a function (evaluating 'this.successfullyCollected.bind(this)') - http://asset.easydmp.net/js/collect.js:1160 / callCollecte http://asset.easydmp.net/js/collect.js:1203 / callbackUpdateParams http://asset.easydmp.net/js/collect.js:1135 / http://asset.easydmp.net/js/collect.js:1191", + "TypeError: 'undefined' is not an object (evaluating 'd.readyState') - http://me.hunkal.com/p/:3" + ] + } + } + } + }, policies); + + results.should.have.a.property('jsErrors'); + results.jsErrors.should.have.a.property('offendersObj').that.deep.equals({ + "count": 2, + "list": [ + { + "error": "TypeError: 'undefined' is not a function (evaluating 'this.successfullyCollected.bind(this)')", + "backtrace": [ + { + "file": "http://asset.easydmp.net/js/collect.js", + "line": 1160 + }, + { + "file": "http://asset.easydmp.net/js/collect.js", + "line": 1203, + "functionName": "callCollecte" + }, + { + "file": "http://asset.easydmp.net/js/collect.js", + "line": 1135, + "functionName": "callbackUpdateParams" + }, + { + "file": "http://asset.easydmp.net/js/collect.js", + "line": 1191 + } + ] + }, + { + "error": "TypeError: 'undefined' is not an object (evaluating 'd.readyState')", + "backtrace": [ + { + "file": "http://me.hunkal.com/p/", + "line": 3 + } + ] + } + ] + }); + }); + + it('should grade correctly jQuery versions', function() { - var versions = { '1.2.9': 0, @@ -84,4 +436,46 @@ describe('rulesChecker', function() { results.jQueryDifferentVersions.should.have.a.property('score').that.equals(0); results.jQueryDifferentVersions.should.have.a.property('abnormal').that.equals(true); }); + + + it('should transform cssParsingErrors offenders', function() { + results = rulesChecker.check({ + "toolsResults": { + "phantomas": { + "metrics": { + "cssParsingErrors": 2 + }, + "offenders": { + "cssParsingErrors": [ + " (Error: CSS parsing failed: missing '}' @ 4:1)", + " (Empty CSS was provided)" + ] + } + } + } + }, policies); + + results.should.have.a.property('cssParsingErrors'); + results.cssParsingErrors.should.have.a.property('offendersObj').that.deep.equals({ + "count": 2, + "list": [ + { + "error": "Error: CSS parsing failed: missing '}'", + "file": "http://www.sudexpress.com/skin/frontend/sudexpress/default/css/styles.css", + "line": 4, + "column": 1 + }, + { + "error": "Empty CSS was provided", + "file": "http://www.sudexpress.com/skin/frontend/sudexpress/default/css/reset.css", + "line": null, + "column": null + } + ] + }); + }); + + + // Enough for the moment, to be complete... + }); diff --git a/test/core/indexTest.js b/test/core/indexTest.js index 715f2f9..2801b85 100644 --- a/test/core/indexTest.js +++ b/test/core/indexTest.js @@ -65,15 +65,22 @@ describe('index.js', function() { "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": 28 + "isAbnormalThreshold": 28, + "hasOffenders": true }, "value": 1, "bad": false, "abnormal": false, "score": 100, "abnormalityScore": 0, - "offenders": "
body
h1[1]
", - "offendersCount": 1 + "offendersObj": { + "count": 1, + "tree": { + "body": { + "h1[1]": 1 + } + } + } }); // Test javascriptExecutionTree diff --git a/test/core/offendersHelpersTest.js b/test/core/offendersHelpersTest.js index d6632f0..6bf59c9 100644 --- a/test/core/offendersHelpersTest.js +++ b/test/core/offendersHelpersTest.js @@ -20,11 +20,15 @@ describe('offendersHelpers', function() { describe('listOfDomArraysToTree', function() { it('should transform a list of arrays into a tree', function() { - var result = offendersHelpers.listOfDomArraysToTree([ + var input = [ ['body', 'section#page', 'div.alternate-color', 'ul.retroGuide', 'li[0]', 'div.retro-chaine.france2'], ['body', 'section#page', 'div.alternate-color', 'ul.retroGuide', 'li[0]', 'div.retro-chaine.france2'], ['body', 'section#page', 'div.alternate-color', 'ul.retroGuide', 'li[1]', 'div.retro-chaine.france2'] - ]); + ]; + + var inputClone = input.slice(); + + var result = offendersHelpers.listOfDomArraysToTree(input); result.should.deep.equal({ 'body': { 'section#page': { @@ -41,95 +45,105 @@ describe('offendersHelpers', function() { } } }); + + input.should.deep.equal(inputClone); }); }); - describe('domTreeToHTML', function() { + describe('domPathToDomElementObj', function() { - it('should transform a dom tree into HTML with the awaited format', function() { - var result = offendersHelpers.domTreeToHTML({ - 'body': { - 'ul.retroGuide': { - 'li[0]': { - 'div.retro-chaine.france2': 2 - }, - 'li[1]': { - 'div.retro-chaine.france2': 1 + it('should transform html', function() { + var result = offendersHelpers.domPathToDomElementObj('html'); + result.should.deep.equal({ + type: 'html' + }); + }); + + it('should transform body', function() { + var result = offendersHelpers.domPathToDomElementObj('body'); + result.should.deep.equal({ + type: 'body' + }); + }); + + it('should transform head', function() { + var result = offendersHelpers.domPathToDomElementObj('head'); + result.should.deep.equal({ + type: 'head' + }); + }); + + it('should transform #document', function() { + var result = offendersHelpers.domPathToDomElementObj('#document'); + result.should.deep.equal({ + type: 'document' + }); + }); + + it('should transform window', function() { + var result = offendersHelpers.domPathToDomElementObj('window'); + result.should.deep.equal({ + type: 'window' + }); + }); + + it('should transform a standard in-body element', function() { + var result = offendersHelpers.domPathToDomElementObj('body > div#colorbox > div#cboxContent'); + result.should.deep.equal({ + type: 'domElement', + element: 'div#cboxContent', + tree: { + 'body': { + 'div#colorbox': { + 'div#cboxContent': 1 } } } }); - - result.should.equal('
body
ul.retroGuide
li[0]
div.retro-chaine.france2 (x2)
li[1]
div.retro-chaine.france2
'); - }); - - }); - - describe('listOfDomPathsToHTML', function() { - - it('should transform a list of path strings into HTML', function() { - var result = offendersHelpers.listOfDomPathsToHTML([ - 'body > ul.retroGuide > li[0] > div.retro-chaine.france2', - 'body > ul.retroGuide > li[1] > div.retro-chaine.france2', - 'body > ul.retroGuide > li[0] > div.retro-chaine.france2', - ]); - - result.should.equal('
body
ul.retroGuide
li[0]
div.retro-chaine.france2 (x2)
li[1]
div.retro-chaine.france2
'); - }); - - }); - - describe('domPathToButton', function() { - - it('should transform html', function() { - var result = offendersHelpers.domPathToButton('html'); - result.should.equal('
html
'); - }); - - it('should transform body', function() { - var result = offendersHelpers.domPathToButton('body'); - result.should.equal('
body
'); - }); - - it('should transform head', function() { - var result = offendersHelpers.domPathToButton('head'); - result.should.equal('
head
'); - }); - - it('should transform #document', function() { - var result = offendersHelpers.domPathToButton('#document'); - result.should.equal('
document
'); - }); - - it('should transform window', function() { - var result = offendersHelpers.domPathToButton('window'); - result.should.equal('
window
'); - }); - - it('should transform a standard in-body element', function() { - var result = offendersHelpers.domPathToButton('body > div#colorbox > div#cboxContent'); - result.should.equal('
DOM element div#cboxContent
body
div#colorbox
div#cboxContent
'); }); it('should transform a domFragment element', function() { - var result = offendersHelpers.domPathToButton('DocumentFragment'); - result.should.equal('
Fragment
'); + var result = offendersHelpers.domPathToDomElementObj('DocumentFragment'); + result.should.deep.equal({ + type: 'fragment' + }); }); it('should transform a domFragment element', function() { - var result = offendersHelpers.domPathToButton('DocumentFragment > div#colorbox > div#cboxContent'); - result.should.equal('
Fragment element div#cboxContent
DocumentFragment
div#colorbox
div#cboxContent
'); + var result = offendersHelpers.domPathToDomElementObj('DocumentFragment > div#colorbox > div#cboxContent'); + result.should.deep.equal({ + type: 'fragmentElement', + element: 'div#cboxContent', + tree: { + 'DocumentFragment': { + 'div#colorbox': { + 'div#cboxContent': 1 + } + } + } + }); }); it('should transform an not-attached element', function() { - var result = offendersHelpers.domPathToButton('div#sizcache'); - result.should.equal('
Created element div#sizcache
'); + var result = offendersHelpers.domPathToDomElementObj('div#sizcache'); + result.should.deep.equal({ + type: 'createdElement', + element: 'div#sizcache' + }); }); it('should transform an not-attached element path', function() { - var result = offendersHelpers.domPathToButton('div > div#sizcache'); - result.should.equal('
Created element div#sizcache
div
div#sizcache
'); + var result = offendersHelpers.domPathToDomElementObj('div > div#sizcache'); + result.should.deep.equal({ + type: 'createdElement', + element: 'div#sizcache', + tree: { + 'div': { + 'div#sizcache': 1 + } + } + }); }); }); @@ -160,32 +174,6 @@ describe('offendersHelpers', function() { }); - describe('backtraceArrayToHtml', function() { - - it('should create a button from a backtrace array', function() { - var result = offendersHelpers.backtraceArrayToHtml([ - { - file: 'http://pouet.com/js/jquery.footer-transverse-min-v1.0.20.js', - line: 1 - }, - { - functionName: 'callback', - file: 'http://pouet.com/js/main.js', - line: 1 - } - ]); - - result.should.equal(''); - }); - - it('should display "no backtrace"', function() { - var result = offendersHelpers.backtraceArrayToHtml([]); - - result.should.equal('
no backtrace
'); - }); - - }); - describe('sortVarsLikeChromeDevTools', function() { it('should sort in the same strange order', function() { @@ -248,12 +236,24 @@ describe('offendersHelpers', function() { describe('cssOffenderPattern', function() { it('should transform a css offender into an object', function() { - var result = offendersHelpers.cssOffenderPattern('.pagination .plus ul li @ 30:31862'); + var result = offendersHelpers.cssOffenderPattern('.pagination .plus ul li @ 30:31862'); result.should.deep.equal({ - offender: '.pagination .plus ul li', + css: '.pagination .plus ul li', + file: 'http://www.pouet.com/css/main.css', line: 30, - character: 31862 + column: 31862 + }); + }); + + it('should work with an inline css', function() { + var result = offendersHelpers.cssOffenderPattern('.pagination .plus ul li [inline CSS] @ 1:32'); + + result.should.deep.equal({ + css: '.pagination .plus ul li', + file: null, + line: 1, + column: 32 }); }); diff --git a/test/fixtures/rulesCheckerOutput.json b/test/fixtures/rulesCheckerOutput.json index 9f6ddf4..e12ad22 100644 --- a/test/fixtures/rulesCheckerOutput.json +++ b/test/fixtures/rulesCheckerOutput.json @@ -6,7 +6,8 @@ "message": "A great message", "isOkThreshold": 1000, "isBadThreshold": 3000, - "isAbnormalThreshold": 5000 + "isAbnormalThreshold": 5000, + "hasOffenders": false }, "value": 1236, "bad": true, @@ -22,11 +23,14 @@ "isOkThreshold": 1000, "isBadThreshold": 3000, "isAbnormalThreshold": 5000, + "hasOffenders": true, "takeOffendersFrom": "metric3" }, "value": 222, - "offenders": "offender1 - offender2", - "offendersCount": 2, + "offendersObj": { + "count": 2, + "str": "offender1 - offender2" + }, "bad": false, "abnormal": false, "score": 100, @@ -39,11 +43,14 @@ "message": "A great message", "isOkThreshold": 1000, "isBadThreshold": 3000, - "isAbnormalThreshold": 5000 + "isAbnormalThreshold": 5000, + "hasOffenders": true }, "value": 6666, - "offenders": "offender1/offender2", - "offendersCount": 2, + "offendersObj": { + "count": 2, + "test": "offender1/offender2" + }, "bad": true, "abnormal": true, "score": 0, @@ -56,10 +63,14 @@ "message": "A great message", "isOkThreshold": 1000, "isBadThreshold": 3000, - "isAbnormalThreshold": 5000 + "isAbnormalThreshold": 5000, + "hasOffenders": true }, "value": 1000, - "offenders": ["offender3"], + "offendersObj": { + "count": 1, + "list": ["offender3"] + }, "bad": false, "abnormal": false, "score": 100, @@ -73,10 +84,14 @@ "isOkThreshold": 1000, "isBadThreshold": 3000, "isAbnormalThreshold": 5000, + "hasOffenders": true, "takeOffendersFrom": ["metric3", "metric4"] }, "value": 3000, - "offenders": ["offender1", "offender2", "offender3"], + "offendersObj": { + "count": 3, + "list": ["offender1", "offender2", "offender3"] + }, "bad": true, "abnormal": false, "score": 0, @@ -120,7 +135,8 @@ "message": "

This is from another tool!

", "isOkThreshold": 0, "isBadThreshold": 3, - "isAbnormalThreshold": 11 + "isAbnormalThreshold": 11, + "hasOffenders": false }, "value": 22, "bad": true, diff --git a/test/fixtures/rulesCheckerPolicies.js b/test/fixtures/rulesCheckerPolicies.js index a4fecab..2391781 100644 --- a/test/fixtures/rulesCheckerPolicies.js +++ b/test/fixtures/rulesCheckerPolicies.js @@ -6,7 +6,8 @@ var policies = { "message": "A great message", "isOkThreshold": 1000, "isBadThreshold": 3000, - "isAbnormalThreshold": 5000 + "isAbnormalThreshold": 5000, + "hasOffenders": false }, "metric2": { "tool": "tool1", @@ -16,8 +17,12 @@ var policies = { "isBadThreshold": 3000, "isAbnormalThreshold": 5000, "takeOffendersFrom": "metric3", + "hasOffenders": true, "offendersTransformFn": function(offenders) { - return offenders.join(' - '); + return { + count: 2, + str: offenders.join(' - ') + }; } }, "metric3": { @@ -27,8 +32,12 @@ var policies = { "isOkThreshold": 1000, "isBadThreshold": 3000, "isAbnormalThreshold": 5000, + "hasOffenders": true, "offendersTransformFn": function(offenders) { - return offenders.join('/'); + return { + count: 2, + test: offenders.join('/') + }; } }, "metric4": { @@ -37,7 +46,8 @@ var policies = { "message": "A great message", "isOkThreshold": 1000, "isBadThreshold": 3000, - "isAbnormalThreshold": 5000 + "isAbnormalThreshold": 5000, + "hasOffenders": true, }, "metric5": { "tool": "tool1", @@ -46,6 +56,7 @@ var policies = { "isOkThreshold": 1000, "isBadThreshold": 3000, "isAbnormalThreshold": 5000, + "hasOffenders": true, "takeOffendersFrom": ["metric3", "metric4"] }, "metric6": { @@ -71,7 +82,8 @@ var policies = { "message": "

This is from another tool!

", "isOkThreshold": 0, "isBadThreshold": 3, - "isAbnormalThreshold": 11 + "isAbnormalThreshold": 11, + "hasOffenders": false, }, "unexistantMetric": { @@ -80,13 +92,15 @@ var policies = { "message": "", "isOkThreshold": 1000, "isBadThreshold": 3000, - "isAbnormalThreshold": 5000 + "isAbnormalThreshold": 5000, + "hasOffenders": true }, "unexistantTool": { "tool": "unexistant", "isOkThreshold": 1000, "isBadThreshold": 3000, - "isAbnormalThreshold": 5000 + "isAbnormalThreshold": 5000, + "hasOffenders": false } };