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] 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 } };