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' + ]); }); });