瀏覽代碼

Merge pull request #81 from gmetais/develop

v1.6.0
Gaël Métais 10 年之前
父節點
當前提交
e67a0e0bc0

+ 2 - 0
bin/server.js

@@ -7,12 +7,14 @@ var cors                    = require('cors');
 
 var authMiddleware          = require('../lib/server/middlewares/authMiddleware');
 var apiLimitsMiddleware     = require('../lib/server/middlewares/apiLimitsMiddleware');
+var wwwRedirectMiddleware   = require('../lib/server/middlewares/wwwRedirectMiddleware');
 
 
 // Middlewares
 app.use(compress());
 app.use(bodyParser.json());
 app.use(cors());
+app.use(wwwRedirectMiddleware);
 app.use(authMiddleware);
 app.use(apiLimitsMiddleware);
 

File diff suppressed because it is too large
+ 0 - 0
front/src/css/icons.css


File diff suppressed because it is too large
+ 0 - 0
front/src/css/main.css


+ 24 - 2
front/src/css/timeline.css

@@ -143,6 +143,10 @@
   border: 1px dotted #aaa;
   text-align: left;
 }
+.subFilters {
+  margin-left: 3em;
+  font-size: 0.9em;
+}
 .table {
   display: table;
   width: 100%;
@@ -306,9 +310,27 @@
   color: #e74c3c;
   cursor: pointer;
 }
-.warningsFilterOn > div {
+.queryWithoutResultsFilterOn > div {
+  display: none;
+}
+.queryWithoutResultsFilterOn > div.queryWithoutResults {
+  display: table-row;
+}
+.jQueryCallOnEmptyObjectFilterOn > div {
+  display: none;
+}
+.jQueryCallOnEmptyObjectFilterOn > div.jQueryCallOnEmptyObject {
+  display: table-row;
+}
+.eventNotDelegatedFilterOn > div {
+  display: none;
+}
+.eventNotDelegatedFilterOn > div.eventNotDelegated {
+  display: table-row;
+}
+.jsErrorFilterOn > div {
   display: none;
 }
-.warningsFilterOn > div.warning {
+.jsErrorFilterOn > div.jsError {
   display: table-row;
 }

File diff suppressed because it is too large
+ 0 - 0
front/src/fonts/svg-icons/arrow-left3.svg


File diff suppressed because it is too large
+ 0 - 0
front/src/fonts/svg-icons/bars.svg


File diff suppressed because it is too large
+ 0 - 0
front/src/fonts/svg-icons/lab.svg


File diff suppressed because it is too large
+ 0 - 0
front/src/fonts/svg-icons/list.svg


File diff suppressed because it is too large
+ 0 - 0
front/src/fonts/svg-icons/loop.svg


File diff suppressed because it is too large
+ 0 - 0
front/src/fonts/svg-icons/mobile.svg


File diff suppressed because it is too large
+ 0 - 0
front/src/fonts/svg-icons/question.svg


File diff suppressed because it is too large
+ 0 - 0
front/src/fonts/svg-icons/screen.svg


File diff suppressed because it is too large
+ 0 - 0
front/src/fonts/svg-icons/tablet.svg


File diff suppressed because it is too large
+ 0 - 0
front/src/fonts/svg-icons/warning.svg


+ 12 - 0
front/src/js/app.js

@@ -19,8 +19,20 @@ var yltApp = angular.module('YellowLabTools', [
 ]);
 
 yltApp.run(['$rootScope', '$location', function($rootScope, $location) {
+    $rootScope.isTouchDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(window.navigator.userAgent);
     $rootScope.loadedRunId = null;
 
+    var oldHash;
+
+    // We don't want the hash to be kept between two pages
+    $rootScope.$on('$locationChangeStart', function(param1, param2, param3, param4){
+        var newHash = $location.hash();
+        if (newHash === oldHash) {
+            $location.hash(null);
+        }
+        oldHash = newHash;
+    });
+
     // Google Analytics
     $rootScope.$on('$routeChangeSuccess', function(){
         ga('send', 'pageview', {'page': $location.path()});

+ 4 - 5
front/src/js/controllers/dashboardCtrl.js

@@ -25,8 +25,7 @@ dashboardCtrl.controller('DashboardCtrl', ['$scope', '$rootScope', '$routeParams
         // By default, Angular sorts object's attributes alphabetically. Countering this problem by retrieving the keys order here.
         $scope.categoriesOrder = Object.keys($scope.result.scoreProfiles.generic.categories);
         
-        $scope.globalScore = Math.max($scope.result.scoreProfiles.generic.globalScore, 0);
-        $scope.tweetText = 'My website\'s score is ' + $scope.globalScore + '/100 on #YellowLabTools!';
+        $scope.tweetText = 'I\'ve discovered this cool open-source tool that audits the front-end quality of a web page: ';
     }
 
     $scope.showRulePage = function(ruleName) {
@@ -37,18 +36,18 @@ dashboardCtrl.controller('DashboardCtrl', ['$scope', '$rootScope', '$routeParams
         API.relaunchTest($scope.result);
     };
 
-    /// When comming from a social shared link, the user needs to click on "See full report" button to display the full dashboard.
+    // When comming from a social shared link, the user needs to click on "See full report" button to display the full dashboard.
     $scope.seeFullReport = function() {
         $scope.fromSocialShare = false;
         $location.search({});
     };
 
     $scope.shareOnTwitter = function(message) {
-        openSocialPopup('https://twitter.com/intent/tweet?url=' + document.URL + '%3Fshare&text=' + encodeURIComponent(message));
+        openSocialPopup('https://twitter.com/intent/tweet?text=' + encodeURIComponent(message + 'http://yellowlab.tools'));
     };
 
     $scope.shareOnLinkedin = function(message) {
-        openSocialPopup('https://www.linkedin.com/shareArticle?mini=true&url=' + document.URL + '%3Fshare&title=' + encodeURIComponent(message) + '&summary=' + encodeURIComponent('YellowLabTools is a free online tool that analyzes performance and front-end quality of a webpage.'));
+        openSocialPopup('https://www.linkedin.com/shareArticle?mini=true&url=http://yellowlab.tools&title=' + encodeURIComponent(message) + '&summary=' + encodeURIComponent('YellowLabTools is a free online tool that analyzes performance and front-end quality of a webpage.'));
     };
 
     function openSocialPopup(url) {

+ 18 - 0
front/src/js/controllers/timelineCtrl.js

@@ -19,12 +19,30 @@ timelineCtrl.controller('TimelineCtrl', ['$scope', '$rootScope', '$routeParams',
     }
 
     function render() {
+        initFilters();
         initScriptFiltering();
         initExecutionTree();
         initTimeline();
         $timeout(initProfiler, 100);
     }
 
+    function initFilters() {
+        var hash = $location.hash();
+        var filter = null;
+        
+        if (hash.indexOf('filter=') === 0) {
+            filter = hash.substr(7);
+        }
+
+        $scope.warningsFilterOn = (filter !== null);
+        $scope.warningsFilters = {
+            queryWithoutResults: (filter === null || filter === 'queryWithoutResults'),
+            jQueryCallOnEmptyObject: (filter === null || filter === 'jQueryCallOnEmptyObject'),
+            eventNotDelegated: (filter === null || filter === 'eventNotDelegated'),
+            jsError: (filter === null || filter === 'jsError')
+        };
+    }
+
     function initScriptFiltering() {
         var offenders = $scope.result.rules.jsCount.offendersObj.list;
         $scope.scripts = [];

+ 14 - 1
front/src/js/directives/offendersDirectives.js

@@ -766,10 +766,23 @@
                 element.append(getProfilerLineHTML(scope.index, scope.node));
                 element[0].id = 'line_' + scope.index;
 
-                if (scope.node.warning || scope.node.error) {
+                if (scope.node.warning) {
                     element[0].classList.add('warning');
+
+                    if (scope.node.queryWithoutResults) {
+                        element[0].classList.add('queryWithoutResults');
+                    }
+
+                    if (scope.node.jQueryCallOnEmptyObject) {
+                        element[0].classList.add('jQueryCallOnEmptyObject');
+                    }
+
+                    if (scope.node.eventNotDelegated) {
+                        element[0].classList.add('eventNotDelegated');
+                    }
                 }
 
+
                 // Bind click on the details icon
                 var detailsIcon = element[0].querySelector('.details div');
                 if (detailsIcon) {

File diff suppressed because it is too large
+ 0 - 0
front/src/less/icons.less


+ 31 - 2
front/src/less/timeline.less

@@ -162,6 +162,11 @@
     text-align: left;
 }
 
+.subFilters {
+    margin-left: 3em;
+    font-size: 0.9em;
+}
+
 .table {
     display: table;
     width: 100%;
@@ -346,10 +351,34 @@
     color: #e74c3c;
     cursor: pointer;
 }
-.warningsFilterOn {
+.queryWithoutResultsFilterOn {
+    > div {
+        display: none;
+        &.queryWithoutResults {
+            display: table-row;
+        }
+    }
+}
+.jQueryCallOnEmptyObjectFilterOn {
+    > div {
+        display: none;
+        &.jQueryCallOnEmptyObject {
+            display: table-row;
+        }
+    }
+}
+.eventNotDelegatedFilterOn {
+    > div {
+        display: none;
+        &.eventNotDelegated {
+            display: table-row;
+        }
+    }
+}
+.jsErrorFilterOn {
     > div {
         display: none;
-        &.warning {
+        &.jsError {
             display: table-row;
         }
     }

+ 2 - 2
front/src/views/index.html

@@ -2,7 +2,7 @@
 <p class="price">Free and open source!</p>
 
 <form ng-submit="launchTest()" >
-    <input type="text" name="url" ng-model="url" placeholder="http://www.mysite.com" class="url" />
+    <input type="{{$root.isTouchDevice? 'url' : 'text'}}" name="url" ng-model="url" placeholder="http://www.mysite.com" class="url" />
     <input type="submit" value="Launch test" class="launchBtn" ng-class="{disabled: !url}" />
     <div class="settings">
         <div class="device">
@@ -90,4 +90,4 @@
         <h3>JS Profiling</h3>
         <p>Untangles the JavaScript spaghetti code</p>
     </div>
-</div>
+</div>

+ 20 - 28
front/src/views/rule.html

@@ -31,22 +31,10 @@
                         <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 === 'eventsScrollBound'">
                         <span ng-if="offender.target == 'window'">Scroll event bound on <b>window</b></span>
                         <span ng-if="offender.target == '#document'">Scroll event bound on <b>document</b></span>
@@ -83,21 +71,6 @@
                         function <b>{{offender.functionName}}</b> used {{offender.count}} times
                     </div>
 
-                    <div ng-if="policyName === 'jQueryNotDelegatedEvents'">
-                        function <b>{{offender.functionName}}</b> used on {{offender.contextLength}} DOM elements without event delegation
-                        <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 === 'documentWriteCalls'">
                         <b>{{offender.writeFn}}</b>
                         <span ng-if="offender.from">
@@ -218,10 +191,29 @@
                     <div ng-repeat="offender in rule.offendersObj.palette" style="background-color: {{offender.color}}; width: {{offender.occurrences * 100 / rule.offendersObj.palette[0].occurrences}}%"><div>{{offender.color}} ({{offender.occurrences}} times)</div></div>
                 </div>
             </div>
-
         </div>
+    </div>
 
+    <div ng-if="policyName === 'DOMaccesses'">
+        <h3>{{rule.value}} offenders</h3>
+        Please open the <a href="/result/{{runId}}/timeline">JS timeline</a>
     </div>
+
+    <div ng-if="policyName === 'queriesWithoutResults'">
+        <h3>{{rule.value}} offenders</h3>
+        Please open the <a href="/result/{{runId}}/timeline#filter=queryWithoutResults">JS timeline, filtered by "Queries without results"</a>
+    </div>
+
+    <div ng-if="policyName === 'jQueryCallsOnEmptyObject'">
+        <h3>{{rule.value}} offenders</h3>
+        Please open the <a href="/result/{{runId}}/timeline#filter=jQueryCallOnEmptyObject">JS timeline, filtered by "jQuery calls on empty object"</a>
+    </div>
+
+    <div ng-if="policyName === 'jQueryNotDelegatedEvents'">
+        <h3>{{rule.value}} offenders</h3>
+        Please open the <a href="/result/{{runId}}/timeline#filter=eventNotDelegated">JS timeline, filtered by "Events not delegated"</a>
+    </div>
+
     <div ng-if="!rule && rule !== null" class="notFound">
         <h2>404</h2>
         Rule "{{policyName}}"" not found

+ 30 - 11
front/src/views/timeline.html

@@ -16,14 +16,14 @@
                 <div ng-repeat="duration in timeline track by $index"
                      class="interval"
                      ng-class="{
-                        domCreation: $index * timelineIntervalDuration < result.toolsResults.phantomas.metrics.domInteractive,
-                        domInteractive: $index * timelineIntervalDuration >= result.toolsResults.phantomas.metrics.domInteractive
-                            && $index * timelineIntervalDuration < result.toolsResults.phantomas.metrics.domContentLoaded,
-                        domContentLoaded: $index * timelineIntervalDuration >= result.toolsResults.phantomas.metrics.domContentLoaded
-                            && $index * timelineIntervalDuration < result.toolsResults.phantomas.metrics.domContentLoadedEnd,
-                        domContentLoadedEnd: $index * timelineIntervalDuration >= result.toolsResults.phantomas.metrics.domContentLoadedEnd
-                            && $index * timelineIntervalDuration < result.toolsResults.phantomas.metrics.domComplete,
-                        domComplete: $index * timelineIntervalDuration >= result.toolsResults.phantomas.metrics.domComplete
+                        domCreation: $index * timelineIntervalDuration < result.javascriptExecutionTree.data.domInteractive,
+                        domInteractive: $index * timelineIntervalDuration >= result.javascriptExecutionTree.data.domInteractive
+                            && $index * timelineIntervalDuration < result.javascriptExecutionTree.data.domContentLoaded,
+                        domContentLoaded: $index * timelineIntervalDuration >= result.javascriptExecutionTree.data.domContentLoaded
+                            && $index * timelineIntervalDuration < result.javascriptExecutionTree.data.domContentLoadedEnd,
+                        domContentLoadedEnd: $index * timelineIntervalDuration >= result.javascriptExecutionTree.data.domContentLoadedEnd
+                            && $index * timelineIntervalDuration < result.javascriptExecutionTree.data.domComplete,
+                        domComplete: $index * timelineIntervalDuration >= result.javascriptExecutionTree.data.domComplete
                      }">
                     <div style="height: {{100 * duration / timelineMax | number: 0}}px" class="color" ng-class="{clickable: duration > 0}" scroll-on-click="{{$index * timelineIntervalDuration}}"></div>
                     <div class="tooltip detailsOverlay">
@@ -60,11 +60,30 @@
     </p>
     <div class="filters">
         <div>
-            <input type="checkbox" ng-model="warningsFilterOn" id="warningsFilterOn" />
-            <label for="warningsFilterOn">Show warnings only</label>
+            <input type="checkbox" name="warningsFilter" ng-model="warningsFilterOn" id="warningsFilterOn" />
+            <label for="warningsFilterOn">Filter on warnings and errors</label>
+            <div class="subFilters" ng-if="warningsFilterOn">
+                <div>
+                    <input type="checkbox" name="filters" ng-model="warningsFilters.queryWithoutResults" id="queryWithoutResultsFilterOn"/>
+                    <label for="queryWithoutResultsFilterOn">Queries without results</label>
+                </div>
+                <div>
+                    <input type="checkbox" name="filters" ng-model="warningsFilters.jQueryCallOnEmptyObject" id="jQueryCallOnEmptyObjectFilterOn" />
+                    <label for="jQueryCallOnEmptyObjectFilterOn">jQuery calls on empty object</label>
+                </div>
+                <div>
+                    <input type="checkbox" name="filters" ng-model="warningsFilters.eventNotDelegated" id="eventNotDelegatedFilterOn" />
+                    <label for="eventNotDelegatedFilterOn">Events not delegated</label>
+                </div>
+                <div>
+                    <input type="checkbox" name="filters" ng-model="warningsFilters.jsError" id="jsErrorFilterOn" />
+                    <label for="jsErrorFilterOn">Errors</label>
+                </div>
+            </div>
         </div>
+        {{queryWithoutResultsFilterOn}}
     </div>
-    <div class="table" ng-class="{warningsFilterOn: warningsFilterOn}">
+    <div class="table" ng-class="{queryWithoutResultsFilterOn: warningsFilterOn && warningsFilters.queryWithoutResults, jQueryCallOnEmptyObjectFilterOn: warningsFilterOn && warningsFilters.jQueryCallOnEmptyObject, eventNotDelegatedFilterOn: warningsFilterOn && warningsFilters.eventNotDelegated, jsErrorFilterOn: warningsFilterOn && warningsFilters.jsError}">
         <div class="headers">
             <div><!-- index --></div>
             <div>Type</div>

+ 29 - 108
lib/metadata/policies.js

@@ -66,82 +66,23 @@ var policies = {
             };
         }
     },
-    /*"DOMaccesses": {
+    "DOMaccesses": {
         "tool": "jsExecutionTransformer",
         "label": "DOM access",
-        "message": "<p>TODO</p><p>TODO</p>",
+        "message": "<p>This metric counts the number of calls to DOM related functions (both native DOM functions and jQuery functions) on page load.</p><p>The more your JavaScript code accesses the DOM, the slower the page will load.</p><p>Try, as much as possible, to have an HTML page fully generated by the server instead of making changes with JS.</p><p>Try to reduce the number of queries by refactoring your JavaScript code.</p><p>Binding too many events also has a cost. Try to use <a href=\"https://learn.jquery.com/events/event-delegation/\" target=\"_blank\">event delegation</a> as much as possible.</p>",
         "isOkThreshold": 50,
-        "isBadThreshold": 2000,
+        "isBadThreshold": 1500,
         "isAbnormalThreshold": 3000,
         "hasOffenders": false
-    },*/
-    "DOMinserts": {
-        "tool": "phantomas",
-        "label": "DOM inserts",
-        "message": "<p>Working with the DOM in JavaScript triggers layout calculations and slows down the page.</p><p>Try, as much as possible, to have an HTML page fully generated by the server instead of making changes with JS.</p>",
-        "isOkThreshold": 10,
-        "isBadThreshold": 400,
-        "isAbnormalThreshold": 1000,
-        "hasOffenders": true,
-        "offendersTransformFn": function(offenders) {
-            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 {
-                            parseError: offender
-                        };
-                    }
-
-                    return {
-                        insertedElement: offendersHelpers.domPathToDomElementObj(parts[1]),
-                        receiverElement: offendersHelpers.domPathToDomElementObj(parts[2])
-                    };
-                })
-            };
-        }
     },
-    "DOMqueries": {
-        "tool": "phantomas",
-        "label": "DOM queries",
-        "message": "<p>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.</p><p>Try to reduce the number of queries by refactoring your JavaScript code.</p><p>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.</p>",
-        "isOkThreshold": 50,
-        "isBadThreshold": 1000,
-        "isAbnormalThreshold": 2000,
-        "hasOffenders": false
-    },
-    "DOMqueriesWithoutResults": {
-        "tool": "phantomas",
-        "label": "DOM queries without result",
-        "message": "<p>Number of queries that return no result.</p><p>It suggests the query is not used on the page, probably because it is some dead code.</p><p>Or maybe the code is trying to find an HTML block that is not always here. Look at the JS Timeline to see if the scripts correctly figures out the HTML block is not here and immediatly stops interacting further with the DOM.</p>",
+    "queriesWithoutResults": {
+        "tool": "jsExecutionTransformer",
+        "label": "Queries without result",
+        "message": "<p>Number of queries that return no result. Both native and jQuery DOM requests are counted.</p><p>It suggests the query is not used on the page, probably because it is some dead code.</p><p>Or maybe the code is trying to find an HTML block that is not always here. Look at the JS Timeline to see if the scripts correctly figures out the HTML block is not here and immediatly stops interacting further with the DOM.</p>",
         "isOkThreshold": 0,
         "isBadThreshold": 100,
         "isAbnormalThreshold": 200,
-        "hasOffenders": true,
-        "offendersTransformFn": function(offenders) {
-            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 {
-                            parseError: offender
-                        };
-                    }
-
-                    return {
-                        query: parts[1],
-                        context: offendersHelpers.domPathToDomElementObj(parts[2]),
-                        fn: parts[3]
-                    };
-                })
-            };
-        }
+        "hasOffenders": false
     },
     "DOMqueriesAvoidable": {
         "tool": "phantomas",
@@ -175,35 +116,6 @@ var policies = {
             };
         }
     },
-    "eventsBound": {
-        "tool": "phantomas",
-        "label": "Events bound",
-        "message": "<p>Binding too many events has a cost.</p><p>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.</p>",
-        "isOkThreshold": 100,
-        "isBadThreshold": 800,
-        "isAbnormalThreshold": 1500,
-        "hasOffenders": true,
-        "offendersTransformFn": function(offenders) {
-            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 {
-                            parseError: offender
-                        };
-                    }
-
-                    return {
-                        eventName: parts[1],
-                        element: offendersHelpers.domPathToDomElementObj(parts[2])
-                    };
-                })
-            };
-        }
-    },
     "eventsScrollBound": {
         "tool": "phantomas",
         "label": "Scroll events bound",
@@ -351,9 +263,9 @@ var policies = {
         "tool": "phantomas",
         "label": "Global variables",
         "message": "<p>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.</p><p>Global variables also take a (very) little bit longer to be accessed than variables in the local scope of a function.</p>",
-        "isOkThreshold": 10,
-        "isBadThreshold": 50,
-        "isAbnormalThreshold": 200,
+        "isOkThreshold": 30,
+        "isBadThreshold": 100,
+        "isAbnormalThreshold": 400,
         "hasOffenders": true,
         "offendersTransformFn": function(offenders) {
             return {
@@ -389,17 +301,17 @@ var policies = {
                     score = 70;
                 } else if (value.indexOf('1.8.') === 0) {
                     score = 50;
-                } else if (value.indexOf('1.7.') === 0) {
+                } else if (value.indexOf('1.7') === 0) {
                     score = 40;
-                } else if (value.indexOf('1.6.') === 0) {
+                } else if (value.indexOf('1.6') === 0) {
                     score = 30;
-                } else if (value.indexOf('1.5.') === 0) {
+                } else if (value.indexOf('1.5') === 0) {
                     score = 20;
-                } else if (value.indexOf('1.4.') === 0) {
+                } else if (value.indexOf('1.4') === 0) {
                     score = 10;
-                } else if (value.indexOf('1.3.') === 0) {
+                } else if (value.indexOf('1.3') === 0) {
                     score = 0;
-                } else if (value.indexOf('1.2.') === 0) {
+                } else if (value.indexOf('1.2') === 0) {
                     score = 0;
                 } else {
                     debug('Unknown jQuery version "%s"', value);
@@ -433,12 +345,21 @@ var policies = {
     "jQueryFunctionsUsed": {
         "tool": "jsExecutionTransformer",
         "label": "jQuery usage",
-        "message": "<p>This is the number of different jQuery functions called on load. This rule is not trying to blame you for using jQuery too much, but the opposite.</p><p>If only a few functions are used, why not trying to get rid of jQuery? Have a look at <a href=\"http://youmightnotneedjquery.com/\" target=\"_blank\">http://youmightnotneedjquery.com</a>.</p>",
+        "message": "<p>This is the number of different core jQuery functions called on load. This rule is not trying to blame you for using jQuery too much, but the opposite.</p><p>If only a few functions are used, why not trying to get rid of jQuery? Have a look at <a href=\"http://youmightnotneedjquery.com/\" target=\"_blank\">http://youmightnotneedjquery.com</a>.</p>",
         "isOkThreshold": 15,
         "isBadThreshold": 6,
         "isAbnormalThreshold": 0,
         "hasOffenders": true
     },
+    "jQueryCallsOnEmptyObject": {
+        "tool": "jsExecutionTransformer",
+        "label": "Calls on empty objects",
+        "message": "<p>This metric counts the number of jQuery functions called on an empty jQuery object. The call was useless.</p><p>This can be helpful to detect dead or unused code.</p>",
+        "isOkThreshold": 1,
+        "isBadThreshold": 100,
+        "isAbnormalThreshold": 180,
+        "hasOffenders": false
+    },
     "jQueryNotDelegatedEvents": {
         "tool": "jsExecutionTransformer",
         "label": "Events not delegated",
@@ -446,7 +367,7 @@ var policies = {
         "isOkThreshold": 1,
         "isBadThreshold": 100,
         "isAbnormalThreshold": 180,
-        "hasOffenders": true
+        "hasOffenders": false
     },
     "cssParsingErrors": {
         "tool": "phantomas",
@@ -1024,7 +945,7 @@ var policies = {
         "tool": "phantomas",
         "label": "Font count",
         "message": "<p>Fonts are loaded on the critical path of the head. Load as few as possible.</p>",
-        "isOkThreshold": 0,
+        "isOkThreshold": 1,
         "isBadThreshold": 3,
         "isAbnormalThreshold": 5,
         "hasOffenders": true,

+ 6 - 7
lib/metadata/scoreProfileGeneric.json

@@ -12,11 +12,9 @@
         "domManipulations": {
             "label": "DOM manipulations",
             "policies": {
-                "DOMinserts": 2,
-                "DOMqueries": 1,
-                "DOMqueriesWithoutResults": 2,
-                "DOMqueriesAvoidable": 2,
-                "eventsBound": 1
+                "DOMaccesses": 3,
+                "queriesWithoutResults": 1,
+                "DOMqueriesAvoidable": 1
             }
         },
         "scroll": {
@@ -38,9 +36,10 @@
         "jQuery": {
             "label": "jQuery",
             "policies": {
-                "jQueryVersion": 1,
-                "jQueryVersionsLoaded": 1,
+                "jQueryVersion": 2,
+                "jQueryVersionsLoaded": 2,
                 "jQueryFunctionsUsed": 1,
+                "jQueryCallsOnEmptyObject": 1,
                 "jQueryNotDelegatedEvents": 1
             }
         },

+ 6 - 0
lib/server/datastores/runsDatastore.js

@@ -58,6 +58,9 @@ function RunsDatastore() {
 
         var errorMessage;
         switch(err) {
+            case '1':
+                errorMessage = "Error 1: unknown error";
+                break;
             case '252':
                 errorMessage = "Error 252: page timeout in Phantomas";
                 break;
@@ -76,6 +79,9 @@ function RunsDatastore() {
             case '1002':
                 errorMessage = "Error 1002: missing Phantomas metrics";
                 break;
+            case '1003':
+                errorMessage = "Error 1003: Phantomas not returning";
+                break;
             default:
                 errorMessage = err;
         }

+ 12 - 0
lib/server/middlewares/wwwRedirectMiddleware.js

@@ -0,0 +1,12 @@
+var wwwRedirectMiddleware = function(req, res, next) {
+    'use strict';
+
+    // Redirect www.yellowlab.tools to yellowlab.tools without "www" (for SEO purpose)
+    if(/^www\.yellowlab\.tools/.test(req.headers.host)) {
+        res.redirect(301, req.protocol + '://' + req.headers.host.replace(/^www\./, '') + req.url);
+    } else {
+        next();
+    }
+};
+
+module.exports = wwwRedirectMiddleware;

+ 23 - 24
lib/tools/jsExecutionTransformer.js

@@ -10,14 +10,17 @@ var jsExecutionTransformer = function() {
         var jQueryFunctionsCollection = new Collection();
         
         var metrics = {
+            domInteractive: 0,
+            domContentLoaded: 0,
+            domContentLoadedEnd: 0,
+            domComplete: 0,
+
             DOMaccesses: 0,
             DOMaccessesOnScroll: 0,
             queriesWithoutResults: 0
         };
 
-        var offenders = {
-
-        };
+        var offenders = {};
 
         var hasjQuery = (data.toolsResults.phantomas.metrics.jQueryVersionsLoaded > 0);
         if (hasjQuery) {
@@ -27,7 +30,6 @@ var jsExecutionTransformer = function() {
             metrics.jQueryNotDelegatedEvents = 0;
 
             offenders.jQueryFunctionsUsed = [];
-            offenders.jQueryNotDelegatedEvents = [];
         }
 
         try {
@@ -42,22 +44,19 @@ var jsExecutionTransformer = function() {
 
                     if (isABindWithoutEventDelegation(node, contextLength)) {
                         metrics.jQueryNotDelegatedEvents += contextLength;
-                        offenders.jQueryNotDelegatedEvents.push({
-                            functionName: node.data.type.substring(9),
-                            contextLength: contextLength,
-                            backtrace: offendersHelpers.backtraceToArray(node.data.backtrace)
-                        });
                         node.warning = true;
                         node.eventNotDelegated = true;
                     }
 
                     if (node.data.resultsNumber === 0) {
                         metrics.queriesWithoutResults ++;
+                        node.queryWithoutResults = true;
                         node.warning = true;
                     }
 
                     if (contextLength === 0) {
                         metrics.jQueryCallsOnEmptyObject ++;
+                        node.jQueryCallOnEmptyObject = true;
                         node.warning = true;
                     }
 
@@ -74,22 +73,22 @@ var jsExecutionTransformer = function() {
                     // Mark a performance flag
                     if (['domInteractive', 'domContentLoaded', 'domContentLoadedEnd', 'domComplete'].indexOf(node.data.type) >= 0) {
                         node.windowPerformance = true;
-                    }
 
-                    // Read the execution tree and adjust the navigation timings (cause their not very well synchronised)
-                    switch(node.data.type) {
-                        case 'domInteractive':
-                            data.toolsResults.phantomas.metrics.domInteractive = node.data.timestamp;
-                            break;
-                        case 'domContentLoaded':
-                            data.toolsResults.phantomas.metrics.domContentLoaded = node.data.timestamp;
-                            break;
-                        case 'domContentLoadedEnd':
-                            data.toolsResults.phantomas.metrics.domContentLoadedEnd = node.data.timestamp;
-                            break;
-                        case 'domComplete':
-                            data.toolsResults.phantomas.metrics.domComplete = node.data.timestamp;
-                            break;
+                        // Adjust the navigation timings (cause their not very well synchronised)
+                        switch(node.data.type) {
+                            case 'domInteractive':
+                                javascriptExecutionTree.data.domInteractive = node.data.timestamp;
+                                break;
+                            case 'domContentLoaded':
+                                javascriptExecutionTree.data.domContentLoaded = node.data.timestamp;
+                                break;
+                            case 'domContentLoadedEnd':
+                                javascriptExecutionTree.data.domContentLoadedEnd = node.data.timestamp;
+                                break;
+                            case 'domComplete':
+                                javascriptExecutionTree.data.domComplete = node.data.timestamp;
+                                break;
+                        }
                     }
 
                     // Transform domPaths into objects

+ 6 - 1
lib/tools/phantomas/custom_modules/modules/domQYLT/domQYLT.js

@@ -365,7 +365,12 @@ exports.module = function(phantomas) {
         phantomas.log('DOM query: by %s - "%s" (using %s) in %s', type, query, fnName, context);
         phantomas.incrMetric('DOMqueries');
 
-        if (context && context.indexOf('DocumentFragment') === -1) {
+        if (context && (
+                context.indexOf('html') === 0 ||
+                context.indexOf('body') === 0 ||
+                context.indexOf('head') === 0 ||
+                context.indexOf('#document') === 0
+            )) {
             DOMqueries.push(type + ' "' + query + '" with ' + fnName + ' (in context ' + context + ')');
         }
     });

+ 28 - 9
lib/tools/phantomas/phantomasWrapper.js

@@ -19,6 +19,7 @@ var PhantomasWrapper = function() {
 
 
         var options = {
+            
             // Cusomizable options
             'timeout': task.options.timeout || 45,
             'js-deep-analysis': task.options.jsDeepAnalysis || false,
@@ -75,34 +76,52 @@ var PhantomasWrapper = function() {
         }
         debug('node node_modules/phantomas/bin/phantomas.js --url=' + task.url + optionsString + ' --verbose');
 
-        // Kill the application if nothing happens
+
         var phantomasPid;
+        var isKilled = false;
+
+        // Kill phantomas if nothing happens
         var killer = setTimeout(function() {
-            debug('Killing the app because the test on %s was launched %d seconds ago', task.url, 5*options.timeout);
-            // If in server mode, forever will restart the server
+            console.log('Killing phantomas because the test on ' + task.url + ' was launched ' + 5*options.timeout + ' seconds ago');
             
-            // Kill the Phantomas process first
             if (phantomasPid) {
                 ps.kill(phantomasPid, function(err) {
+                    
                     if (err) {
                         debug('Could not kill Phantomas process %s', phantomasPid);
+
+                        // Suicide
+                        process.exit(1);
+                        // If in server mode, forever will restart the server
                     }
-                    else {
-                        debug('Phantomas process %s was correctly killed', phantomasPid);
-                    }
 
-                    // Then suicide.
-                    process.exit(1);
+                    debug('Phantomas process %s was correctly killed', phantomasPid);
+
+                    // Then mark the test as failed
+                    // Error 1003 = Phantomas not answering
+                    deferred.reject(1003);
+                    isKilled = true;
                 });
+            } else {
+                // Suicide
+                process.exit(1);
             }
 
         }, 5*options.timeout*1000);
 
+
         // It's time to launch the test!!!
         var triesNumber = 2;
 
         async.retry(triesNumber, function(cb) {
             var process = phantomas(task.url, options, function(err, json, results) {
+                
+                if (isKilled) {
+                    debug('Process was killed, too late Phantomas, sorry...');
+                    return;
+                }
+
+
                 debug('Returning from Phantomas');
 
                 // Adding some YellowLabTools errors here

+ 7 - 7
package.json

@@ -1,6 +1,6 @@
 {
   "name": "yellowlabtools",
-  "version": "1.5.1",
+  "version": "1.6.1",
   "description": "Online tool to audit a webpage for performance and front-end quality issues",
   "repository": {
     "type": "git",
@@ -11,18 +11,18 @@
   },
   "main": "./lib/index.js",
   "dependencies": {
-    "async": "~0.9.0",
+    "async": "~1.0.0",
     "body-parser": "~1.12.4",
-    "compression": "~1.4.3",
+    "compression": "~1.4.4",
     "cors": "^2.6.0",
     "debug": "~2.2.0",
-    "express": "~4.12.3",
+    "express": "~4.12.4",
     "lwip": "0.0.6",
     "meow": "^3.1.0",
     "phantomas": "1.10.2",
     "ps-node": "0.0.4",
-    "q": "~1.4.0",
-    "rimraf": "~2.3.3",
+    "q": "~1.4.1",
+    "rimraf": "~2.3.4",
     "temporary": "0.0.8"
   },
   "devDependencies": {
@@ -47,7 +47,7 @@
     "grunt-usemin": "^3.0.0",
     "grunt-webfont": "^0.5.3",
     "matchdep": "^0.3.0",
-    "mocha": "^2.2.4",
+    "mocha": "^2.2.5",
     "request": "^2.55.0",
     "sinon": "^1.14.1",
     "sinon-chai": "^2.7.0"

+ 1 - 181
test/core/customPoliciesTest.js

@@ -1,7 +1,7 @@
 var should = require('chai').should();
 var rulesChecker = require('../../lib/rulesChecker');
 
-describe('rulesChecker', function() {
+describe('customPolicies', function() {
     
     var policies = require('../../lib/metadata/policies.js');
     var results;
@@ -70,131 +70,6 @@ describe('rulesChecker', function() {
         });
     });
 
-    
-    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({
@@ -246,61 +121,6 @@ describe('rulesChecker', function() {
     });
 
 
-    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": {

+ 9 - 3
test/core/indexTest.js

@@ -53,8 +53,9 @@ describe('index.js', function() {
                 data.toolsResults.phantomas.metrics.should.have.a.property('requests').that.equals(1);
                 data.toolsResults.phantomas.should.have.a.property('offenders').that.is.an('object');
                 data.toolsResults.phantomas.offenders.should.have.a.property('DOMelementMaxDepth');
-                data.toolsResults.phantomas.offenders.DOMelementMaxDepth.should.have.length(1);
+                data.toolsResults.phantomas.offenders.DOMelementMaxDepth.should.have.length(2);
                 data.toolsResults.phantomas.offenders.DOMelementMaxDepth[0].should.equal('body > h1[0]');
+                data.toolsResults.phantomas.offenders.DOMelementMaxDepth[1].should.equal('body > script[1]');
 
                 // Test rules
                 data.should.have.a.property('rules').that.is.an('object');
@@ -76,10 +77,11 @@ describe('index.js', function() {
                     "score": 100,
                     "abnormalityScore": 0,
                     "offendersObj": {
-                        "count": 1,
+                        "count": 2,
                         "tree": {
                             "body": {
-                                "h1[0]": 1
+                                "h1[0]": 1,
+                                "script[1]": 1
                             }
                         }
                     }
@@ -91,6 +93,10 @@ describe('index.js', function() {
                 data.should.have.a.property('javascriptExecutionTree').that.is.an('object');
                 data.javascriptExecutionTree.should.have.a.property('data');
                 data.javascriptExecutionTree.data.should.have.a.property('type').that.equals('main');
+                data.javascriptExecutionTree.data.should.have.a.property('domInteractive').that.is.a('number');
+                data.javascriptExecutionTree.data.should.have.a.property('domContentLoaded').that.is.a('number');
+                data.javascriptExecutionTree.data.should.have.a.property('domContentLoadedEnd').that.is.a('number');
+                data.javascriptExecutionTree.data.should.have.a.property('domComplete').that.is.a('number');
 
                 /*jshint expr: true*/
                 console.log.should.not.have.been.called;

+ 4 - 0
test/www/simple-page.html

@@ -4,5 +4,9 @@
 </head>
 <body>
     <h1>Simple page</h1>
+
+    <script>
+        document.getElementById('foo');
+    </script>
 </body>
 </html>

Some files were not shown because too many files changed in this diff