Merge pull request #42 from gmetais/offenders

Fixes #40.
This commit is contained in:
Gaël Métais
2015-01-26 11:50:51 +01:00
22 changed files with 2177 additions and 118 deletions
+9 -6
View File
@@ -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'
]);
+2 -1
View File
@@ -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"
}
}
+66
View File
@@ -82,3 +82,69 @@
font-size: 3em;
margin-bottom: 1em;
}
.offenders .offenderButton {
display: inline-block;
position: relative;
background: #efe;
padding: 0 0.5em;
margin: 0.2em 0;
border-radius: 0.4em;
z-index: 1;
}
.offenders .offenderButton.opens {
padding-right: 0.75em;
}
.offenders .offenderButton.opens:after {
position: relative;
left: 0.5em;
content: '\25BC';
font-size: 0.8em;
}
.offenders .offenderButton > 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 .offenderButton .domTree {
text-align: left;
white-space: nowrap;
}
.offenders .offenderButton .domTree > div {
margin: 0.5em;
}
.offenders .offenderButton .domTree > div div {
margin-left: 1em;
}
.offenders .offenderButton .backtrace,
.offenders .offenderButton .cssFileAndLine {
white-space: nowrap;
padding: 0.5em;
}
.offenders .offenderButton.opens.mouseOver {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
background: #ffe0cc;
z-index: 2;
}
.offenders .offenderButton.opens.mouseOver > div {
display: block;
}
.offendersHtml {
display: inline-block;
}
.domTree div {
text-align: left;
margin-left: 1em;
}
.domTree div span:only-child {
font-weight: bold;
}
.domTree div span:only-child span {
font-style: italic;
font-weight: normal;
}
+2
View File
@@ -1,5 +1,6 @@
var yltApp = angular.module('YellowLabTools', [
'ngRoute',
'ngSanitize',
'indexCtrl',
'aboutCtrl',
'dashboardCtrl',
@@ -10,6 +11,7 @@ var yltApp = angular.module('YellowLabTools', [
'resultsFactory',
'menuService',
'gradeDirective',
'offendersDirectives'
]);
yltApp.run(['$rootScope', '$location', function($rootScope, $location) {
-1
View File
@@ -22,7 +22,6 @@ ruleCtrl.controller('RuleCtrl', ['$scope', '$rootScope', '$routeParams', '$locat
function init() {
$scope.rule = $scope.result.rules[$scope.policyName];
$scope.message = $sce.trustAsHtml($scope.rule.policy.message);
}
$scope.backToDashboard = function() {
@@ -0,0 +1,120 @@
var offendersDirectives = angular.module('offendersDirectives', []);
offendersDirectives.directive('domTree', function() {
return {
restrict: 'E',
scope: {
tree: '='
},
template: '<div class="domTree"></div>',
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 += '<div><span>' + key + '</span>' + recursiveHtmlBuilder(tree[key]) + '</div>';
} else if (tree[key] > 1) {
html += '<div><span>' + key + ' <span>(x' + tree[key] + ')</span></span></div>';
} else {
html += '<div><span>' + key + '</span></div>';
}
});
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: '<a href="{{url}}" target="_blank" title="{{url}}">{{url | shortenUrl:maxLength}}</a>',
replace: true
};
});
offendersDirectives.filter('encodeURIComponent', function() {
return window.encodeURIComponent;
});
offendersDirectives.directive('fileAndLine', function() {
return {
restrict: 'E',
scope: {
file: '=',
line: '=',
column: '='
},
template: '<span><span ng-if="file"><url-link url="file" max-length="60"></url-link></span><span ng-if="!file">&lt;inline CSS&gt;</span> @ {{line}}:{{column}}</span>',
replace: true
};
});
offendersDirectives.directive('fileAndLineButton', function() {
return {
restrict: 'E',
scope: {
file: '=',
line: '=',
column: '='
},
template: '<div class="offenderButton opens">css file<div class="cssFileAndLine"><file-and-line file="file" line="line" column="column" button="true"></file-and-line></div></div>',
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();
});
}
};
});
+80
View File
@@ -88,4 +88,84 @@
font-size: 3em;
margin-bottom: 1em;
}
}
.offenders {
.offenderButton {
display: inline-block;
position: relative;
background: #efe;
padding: 0 0.5em;
margin: 0.2em 0;
border-radius: 0.4em;
z-index: 1;
&.opens {
padding-right: 0.75em;
&: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;
}
}
}
.backtrace, .cssFileAndLine {
white-space: nowrap;
padding: 0.5em;
}
&.opens.mouseOver {
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;
}
}
}
+2
View File
@@ -20,6 +20,7 @@
<script src="/bower_components/angular/angular.min.js"></script>
<script src="/bower_components/angular-route/angular-route.min.js"></script>
<script src="/bower_components/angular-resource/angular-resource.min.js"></script>
<script src="/bower_components/angular-sanitize/angular-sanitize.min.js"></script>
<script src="/js/app.js"></script>
<script src="/js/controllers/indexCtrl.js"></script>
<script src="/js/controllers/aboutCtrl.js"></script>
@@ -31,6 +32,7 @@
<script src="/js/models/runsFactory.js"></script>
<script src="/js/services/menuService.js"></script>
<script src="/js/directives/gradeDirective.js"></script>
<script src="/js/directives/offendersDirectives.js"></script>
<!-- endbuild -->
<head>
+7
View File
@@ -0,0 +1,7 @@
<div class="offenderButton" ng-class="{opens: obj.tree}">
<span ng-if="obj.type == 'html' || obj.type == 'body' || obj.type == 'head' || obj.type == 'window' || obj.type == 'document' || obj.type == 'fragment'"><b>{{obj.type}}</b></span>
<span ng-if="obj.type == 'domElement'">DOM element <b>{{obj.element}}</b></span>
<span ng-if="obj.type == 'fragmentElement'">Fragment element <b>{{obj.element}}</b></span>
<span ng-if="obj.type == 'createdElement'">Created element <b>{{obj.element}}</b></span>
<dom-tree ng-if="obj.tree" tree="obj.tree"></dom-tree>
</div>
+127 -9
View File
@@ -10,23 +10,141 @@
</div>
<div class="right">
<h3>Value: {{rule.value}}</h3>
<div ng-bind-html="message" class="message"></div>
<div ng-bind-html="rule.policy.message" class="message"></div>
</div>
</div>
<div ng-if="rule.abnormal" class="warning">
<h3>Warning</h3>
<p>This rule reached the abnormality threshold, which means there is a real problem you should care about.</p>
</div>
<div class="offenders">
<h3>
<ng-pluralize count="rule.offenders.length || 0" when="{'0': 'No offenders', 'one': '1 offender', 'other': '{} offenders'}">
</ng-pluralize>
</h3>
<div class="offendersTable">
<div ng-repeat="offender in rule.offenders track by $index">
<div>{{offender}}</div>
<div class="offenders" ng-if="rule.policy.hasOffenders">
<h3><ng-pluralize count="rule.offendersObj.count" when="{'0': 'No offenders', 'one': '1 offender', 'other': '{} offenders'}"></ng-pluralize></h3>
<div ng-if="rule.offendersObj.list" class="offendersTable">
<div ng-repeat="offender in rule.offendersObj.list track by $index">
<div ng-if="offender.parseError">
{{offender.parseError}}
</div>
<div ng-if="!offender.parseError">
<div ng-if="policyName === 'DOMidDuplicated'">
<b>{{offender.id}}</b>: {{offender.occurrences}} occurrences
</div>
<div ng-if="policyName === 'DOMinserts'">
<dom-element-button obj="offender.insertedElement"></dom-element-button> appended to <dom-element-button obj="offender.receiverElement"></dom-element-button>
</div>
<div ng-if="policyName === 'DOMqueriesWithoutResults'">
<b>{{offender.query}}</b> (in <dom-element-button obj="offender.context"></dom-element-button>) using {{offender.fn}}
</div>
<div ng-if="policyName === 'DOMqueriesAvoidable'">
<b>{{offender.query}}</b> (in <dom-element-button obj="offender.context"></dom-element-button>) using {{offender.fn}}: <b>{{offender.count}} queries</b>
</div>
<div ng-if="policyName === 'eventsBound'">
<b>{{offender.eventName}}</b> bound to <dom-element-button obj="offender.element"></dom-element-button>
</div>
<div ng-if="policyName === 'jsErrors'">
<b>{{offender.error}}</b>
<div class="offenderButton" ng-if="offender.backtrace.length == 0">no backtrace</div>
<div class="offenderButton opens" ng-if="offender.backtrace.length > 0">
backtrace
<div class="backtrace">
<div ng-repeat="obj in offender.backtrace track by $index">
<span ng-if="obj.functionName">{{obj.functionName}}()</span>
<url-link url="obj.file" max-length="60"></url-link>
line {{obj.line}}
</div>
</div>
</div>
</div>
<div ng-if="policyName === 'cssParsingErrors'">
<b>{{offender.error}}</b>
<file-and-line file="offender.file" line="offender.line" column="offender.column"></file-and-line>
<span ng-if="offender.file">(<a href="http://jigsaw.w3.org/css-validator/validator?profile=css3&usermedium=all&warning=no&uri={{offender.file | encodeURIComponent}}" target="_blank">Check on the W3C validator</a>)</span>
</div>
<div ng-if="policyName === 'cssComplexSelectors' || policyName === 'cssComplexSelectorsByAttribute' || policyName === 'cssImports' || policyName === 'cssUniversalSelectors' || policyName === 'cssRedundantBodySelectors' || policyName === 'cssRedundantChildNodesSelectors'">
<span ng-if="offender.bolded" ng-bind-html="offender.bolded"></span>
<b ng-if="!offender.bolded">{{offender.css}}</b>
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
</div>
<div ng-if="policyName === 'cssDuplicatedSelectors'">
{{offender.rule}} (<b>x{{offender.occurrences}}</b>)
</div>
<div ng-if="policyName === 'cssDuplicatedProperties'">
Property <b>{{offender.property}}</b> duplicated in <b>{{offender.rule}} { }</b>
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
</div>
<div ng-if="policyName === 'cssEmptyRules'">
<b>{{offender.css}} { }</b>
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
</div>
<div ng-if="policyName === 'cssExpressions'">
{{offender.rule}} {{ '{' + offender.property}}: <b>expression(</b>{{offender.expression}}<b>)</b>}
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
</div>
<div ng-if="policyName === 'cssImportants'">
{{offender.rule}} {{ '{' + offender.property}}: {{offender.value}} <b>!important</b>}
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
</div>
<div ng-if="policyName === 'cssOldIEFixes'">
<span ng-if="offender.browser"><b>{{offender.browser}} fix:</b></span>
<span ng-bind-html="offender.bolded"></span>
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
</div>
<div ng-if="policyName === 'cssOldPropertyPrefixes'">
{{offender.rule}} {<b>{{offender.property}}</b>: {{offender.value + '}' }}
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
</div>
<div ng-if="policyName === 'requests' || policyName === 'htmlCount' || policyName === 'jsCount' || policyName === 'cssCount' || policyName === 'imageCount' || policyName === 'webfontCount' || policyName === 'videoCount' || policyName === 'jsonCount' || policyName === 'otherCount' || policyName === 'smallJsFiles' || policyName === 'smallCssFiles' || policyName === 'smallImages'">
<url-link url="offender.file" max-length="100"></url-link>
<span ng-if="offender.size || offender.size === 0">({{offender.size}} kB)</span>
</div>
<div ng-if="policyName === 'notFound' || policyName === 'closedConnections' || policyName === 'multipleRequests' || policyName === 'cachingDisabled' || policyName === 'cachingNotSpecified'">
<url-link url="offender" max-length="100"></url-link>
</div>
<div ng-if="policyName === 'cachingTooShort'">
<url-link url="offender.file" max-length="100"></url-link>
cached for <b>{{offender.ttlWithUnit}} {{offender.unit}}</b>
</div>
<div ng-if="policyName === 'domains'">
<b>{{offender.domain}}</b>
(<ng-pluralize count="offender.requests" when="{'one':'1 request','other':'{} requests'}"></ng-pluralize>)
</div>
<div ng-if="policyName === 'globalVariables' || policyName === 'jQueryDifferentVersions'">
{{offender}}
</div>
</div>
</div>
</div>
<div ng-if="!rule.offendersObj.list" class="offendersHtml">
<div ng-if="policyName === 'DOMelementMaxDepth'">
<dom-tree tree="rule.offendersObj.tree"></dom-tree>
</div>
</div>
</div>
<div ng-if="!rule && rule !== null" class="notFound">
<h2>404</h2>
+699 -60
View File
File diff suppressed because it is too large Load Diff
+178
View File
@@ -0,0 +1,178 @@
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();
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.domPathToDomElementObj = function(domPath) {
var domArray = this.domPathToArray(domPath);
var domTree = this.listOfDomArraysToTree([this.domPathToArray(domPath)]);
if (domArray[0] === 'html') {
return {
type: 'html'
};
}
if (domArray[0] === 'body') {
if (domArray.length === 1) {
return {
type: 'body'
};
} else {
return {
type: 'domElement',
element: domArray[domArray.length - 1],
tree: domTree
};
}
}
if (domArray[0] === 'head') {
return {
type: 'head'
};
}
if (domArray[0] === '#document') {
return {
type: 'document'
};
}
if (domArray[0] === 'window') {
return {
type: 'window'
};
}
if (domArray[0] === 'DocumentFragment') {
if (domArray.length === 1) {
return {
type: 'fragment'
};
} else {
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 {
type: 'createdElement',
element: domPath
};
} else {
return {
type: 'createdElement',
element: domArray[domArray.length - 1],
tree: domTree
};
}
};
this.backtraceToArray = function(str) {
var traceArray = str.split(/ \/ /);
if (traceArray) {
var results = [];
var parts = null;
for (var i=0 ; i<traceArray.length ; i++) {
parts = /^(([\w$]+) )?([^ ]+):(\d+)$/.exec(traceArray[i]);
if (parts) {
var obj = {
file: parts[3],
line: parseInt(parts[4], 10)
};
if (parts[2]) {
obj.functionName = parts[2];
}
results.push(obj);
} else {
return null;
}
}
return results;
} else {
return null;
}
};
this.sortVarsLikeChromeDevTools = function(vars) {
return vars.sort(function(a, b) {
return (a < b) ? -1 : 1;
});
};
this.urlToLink = function(url) {
var shortUrl = (url.length > 110) ? url.substr(0, 47) + ' ... ' + url.substr(-48) : url;
return '<a href="' + url + '" target="_blank" title="' + url + '">' + shortUrl + '</a>';
};
this.cssOffenderPattern = function(offender) {
var parts = /^(.*) (?:<([^ \(]*)>|\[inline CSS\]) @ (\d+):(\d+)$/.exec(offender);
if (!parts) {
return {
offender: offender
};
} else {
return {
css: parts[1],
file: parts[2] || null,
line: parseInt(parts[3], 10),
column: parseInt(parts[4], 10)
};
}
};
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();
+55 -17
View File
@@ -1,3 +1,5 @@
var extend = require('util')._extend;
var debug = require('debug')('ylt:ruleschecker');
var RulesChecker = function() {
@@ -21,31 +23,67 @@ var RulesChecker = function() {
rule = {
value: data.toolsResults[policy.tool].metrics[metricName],
policy: policy
policy: extend({}, policy) // Clone object policy instead of reference
};
// Take DOMqueriesAvoidable's offenders from DOMqueriesDuplicated, for example.
if (policy.takeOffendersFrom) {
var fromList = policy.takeOffendersFrom;
// Deal with offenders
if (policy.hasOffenders) {
var offenders = [];
// takeOffendersFrom option can be a string or an array of strings.
if (typeof fromList === 'string') {
fromList = [fromList];
// 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;
} 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 = {};
fromList.forEach(function(from) {
offenders = offenders.concat(data.toolsResults[policy.tool].offenders[from]);
});
// 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) {
data.toolsResults[policy.tool].offenders[metricName] = offenders;
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 (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];
}
rule.bad = rule.value > policy.isOkThreshold;
rule.abnormal = policy.isAbnormalThreshold && rule.value >= policy.isAbnormalThreshold;
@@ -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');
}
}
});
};
+3 -1
View File
@@ -34,12 +34,14 @@ var PhantomasWrapper = function() {
'analyze-css': true,
'skip-modules': [
'blockDomains', // not needed
'caching', // overriden
'domMutations', // not compatible with webkit
'domQueries', // overriden
'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
+1 -2
View File
@@ -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"
+395 -1
View File
@@ -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": [
"<http://www.sudexpress.com/skin/frontend/sudexpress/default/css/styles.css> (Error: CSS parsing failed: missing '}' @ 4:1)",
"<http://www.sudexpress.com/skin/frontend/sudexpress/default/css/reset.css> (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...
});
+10 -2
View File
@@ -65,14 +65,22 @@ describe('index.js', function() {
"message": "<p>A deep DOM makes the CSS matching with DOM elements difficult.</p><p>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.</p>",
"isOkThreshold": 10,
"isBadThreshold": 20,
"isAbnormalThreshold": 28
"isAbnormalThreshold": 28,
"hasOffenders": true
},
"value": 1,
"bad": false,
"abnormal": false,
"score": 100,
"abnormalityScore": 0,
"offenders": ["body > h1[1]"]
"offendersObj": {
"count": 1,
"tree": {
"body": {
"h1[1]": 1
}
}
}
});
// Test javascriptExecutionTree
+283
View File
@@ -0,0 +1,283 @@
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 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': {
'div.alternate-color': {
'ul.retroGuide': {
'li[0]': {
'div.retro-chaine.france2': 2
},
'li[1]': {
'div.retro-chaine.france2': 1
}
}
}
}
}
});
input.should.deep.equal(inputClone);
});
});
describe('domPathToDomElementObj', function() {
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
}
}
}
});
});
it('should transform a domFragment element', function() {
var result = offendersHelpers.domPathToDomElementObj('DocumentFragment');
result.should.deep.equal({
type: 'fragment'
});
});
it('should transform a domFragment element', function() {
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.domPathToDomElementObj('div#sizcache');
result.should.deep.equal({
type: 'createdElement',
element: 'div#sizcache'
});
});
it('should transform an not-attached element path', function() {
var result = offendersHelpers.domPathToDomElementObj('div > div#sizcache');
result.should.deep.equal({
type: 'createdElement',
element: 'div#sizcache',
tree: {
'div': {
'div#sizcache': 1
}
}
});
});
});
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 / callback 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
},
{
functionName: 'callback',
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('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'
]);
});
});
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('<a href="http://www.google.com/js/main.js" target="_blank" title="http://www.google.com/js/main.js">http://www.google.com/js/main.js</a>');
});
it('should ellypsis the url if too long', function() {
var result = offendersHelpers.urlToLink('http://www.google.com/js/longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong/main.js');
result.should.equal('<a href="http://www.google.com/js/longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong/main.js" target="_blank" title="http://www.google.com/js/longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong/main.js">http://www.google.com/js/longlonglonglonglonglo ... longlonglonglonglonglonglonglonglonglong/main.js</a>');
});
});
describe('cssOffenderPattern', function() {
it('should transform a css offender into an object', function() {
var result = offendersHelpers.cssOffenderPattern('.pagination .plus ul li <http://www.pouet.com/css/main.css> @ 30:31862');
result.should.deep.equal({
css: '.pagination .plus ul li',
file: 'http://www.pouet.com/css/main.css',
line: 30,
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
});
});
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'
});
});
});
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
});
});
});
});
+1 -1
View File
@@ -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);
+26 -8
View File
@@ -6,7 +6,8 @@
"message": "A great message",
"isOkThreshold": 1000,
"isBadThreshold": 3000,
"isAbnormalThreshold": 5000
"isAbnormalThreshold": 5000,
"hasOffenders": false
},
"value": 1236,
"bad": true,
@@ -22,10 +23,14 @@
"isOkThreshold": 1000,
"isBadThreshold": 3000,
"isAbnormalThreshold": 5000,
"hasOffenders": true,
"takeOffendersFrom": "metric3"
},
"value": 222,
"offenders": ["offender1", "offender2"],
"offendersObj": {
"count": 2,
"str": "offender1 - offender2"
},
"bad": false,
"abnormal": false,
"score": 100,
@@ -38,10 +43,14 @@
"message": "A great message",
"isOkThreshold": 1000,
"isBadThreshold": 3000,
"isAbnormalThreshold": 5000
"isAbnormalThreshold": 5000,
"hasOffenders": true
},
"value": 6666,
"offenders": ["offender1", "offender2"],
"offendersObj": {
"count": 2,
"test": "offender1/offender2"
},
"bad": true,
"abnormal": true,
"score": 0,
@@ -54,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,
@@ -71,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,
@@ -118,7 +135,8 @@
"message": "<p>This is from another tool!</p>",
"isOkThreshold": 0,
"isBadThreshold": 3,
"isAbnormalThreshold": 11
"isAbnormalThreshold": 11,
"hasOffenders": false
},
"value": 22,
"bad": true,
@@ -1,11 +1,13 @@
{
var policies = {
"metric1": {
"tool": "tool1",
"label": "The metric 1",
"message": "A great message",
"isOkThreshold": 1000,
"isBadThreshold": 3000,
"isAbnormalThreshold": 5000
"isAbnormalThreshold": 5000,
"hasOffenders": false
},
"metric2": {
"tool": "tool1",
@@ -14,7 +16,14 @@
"isOkThreshold": 1000,
"isBadThreshold": 3000,
"isAbnormalThreshold": 5000,
"takeOffendersFrom": "metric3"
"takeOffendersFrom": "metric3",
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return {
count: 2,
str: offenders.join(' - ')
};
}
},
"metric3": {
"tool": "tool1",
@@ -22,7 +31,14 @@
"message": "A great message",
"isOkThreshold": 1000,
"isBadThreshold": 3000,
"isAbnormalThreshold": 5000
"isAbnormalThreshold": 5000,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return {
count: 2,
test: offenders.join('/')
};
}
},
"metric4": {
"tool": "tool1",
@@ -30,7 +46,8 @@
"message": "A great message",
"isOkThreshold": 1000,
"isBadThreshold": 3000,
"isAbnormalThreshold": 5000
"isAbnormalThreshold": 5000,
"hasOffenders": true,
},
"metric5": {
"tool": "tool1",
@@ -39,6 +56,7 @@
"isOkThreshold": 1000,
"isBadThreshold": 3000,
"isAbnormalThreshold": 5000,
"hasOffenders": true,
"takeOffendersFrom": ["metric3", "metric4"]
},
"metric6": {
@@ -64,7 +82,8 @@
"message": "<p>This is from another tool!</p>",
"isOkThreshold": 0,
"isBadThreshold": 3,
"isAbnormalThreshold": 11
"isAbnormalThreshold": 11,
"hasOffenders": false,
},
"unexistantMetric": {
@@ -73,12 +92,16 @@
"message": "",
"isOkThreshold": 1000,
"isBadThreshold": 3000,
"isAbnormalThreshold": 5000
"isAbnormalThreshold": 5000,
"hasOffenders": true
},
"unexistantTool": {
"tool": "unexistant",
"isOkThreshold": 1000,
"isBadThreshold": 3000,
"isAbnormalThreshold": 5000
"isAbnormalThreshold": 5000,
"hasOffenders": false
}
}
};
module.exports = policies;