Pārlūkot izejas kodu

Merge pull request #120 from gmetais/develop

v1.8.1
Gaël Métais 9 gadi atpakaļ
vecāks
revīzija
9ff24918e2

+ 5 - 0
front/src/css/rule.css

@@ -101,6 +101,7 @@
   margin: 0.2em 0;
   margin: 0.2em 0;
   border-radius: 0.4em;
   border-radius: 0.4em;
   z-index: 1;
   z-index: 1;
+  cursor: pointer;
 }
 }
 .offendersTable .offenderButton.opens,
 .offendersTable .offenderButton.opens,
 .value .offenderButton.opens {
 .value .offenderButton.opens {
@@ -157,6 +158,10 @@
   display: block;
   display: block;
   background: #ffe0cc;
   background: #ffe0cc;
 }
 }
+.offendersTable .smallerOffenders,
+.value .smallerOffenders {
+  font-size: 0.9em;
+}
 .offendersHtml {
 .offendersHtml {
   display: inline-block;
   display: inline-block;
 }
 }

+ 5 - 0
front/src/less/rule.less

@@ -112,6 +112,7 @@
         margin: 0.2em 0;
         margin: 0.2em 0;
         border-radius: 0.4em;
         border-radius: 0.4em;
         z-index: 1;
         z-index: 1;
+        cursor: pointer;
 
 
         &.opens {
         &.opens {
             padding-right: 0.75em;
             padding-right: 0.75em;
@@ -166,6 +167,10 @@
             }
             }
         }
         }
     }
     }
+
+    .smallerOffenders {
+        font-size: 0.9em;
+    }
 }
 }
 
 
 .offendersHtml {
 .offendersHtml {

+ 12 - 4
front/src/views/rule.html

@@ -143,10 +143,18 @@
                     </div>
                     </div>
 
 
                     <div ng-if="policyName === 'cssOldPropertyPrefixes'">
                     <div ng-if="policyName === 'cssOldPropertyPrefixes'">
-                        {{offender.rule}} {<b>{{offender.property}}</b>: {{offender.value + '}' }}
-                        <br>
-                        <b>{{offender.message}}</b>
-                        <file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
+                        <b>{{offender.property}} {{offender.message}}</b>
+                        <div ng-if="offender.rules.length" ng-click="offender.showMore = !offender.showMore" class="offenderButton">
+                            <span ng-if="!offender.showMore">show</span>
+                            <span ng-if="offender.showMore">hide</span>
+                            <ng-pluralize count="offender.rules.length" when="{'one':'1 offender','other':'{} offenders'}"></ng-pluralize>
+                        </div>
+                        <div ng-if="offender.showMore" class="smallerOffenders">
+                            <div ng-repeat="cssRule in offender.rules">
+                                {{cssRule.rule}} {{'{' + offender.property}}: {{cssRule.value + '}' }}
+                                <file-and-line-button file="cssRule.file" line="cssRule.line" column="cssRule.column"></file-and-line-button>
+                            </div>
+                        </div>
                     </div>
                     </div>
 
 
                     <div ng-if="policyName === 'lazyLoadableImagesBelowTheFold' || policyName === 'hiddenImages'">
                     <div ng-if="policyName === 'lazyLoadableImagesBelowTheFold' || policyName === 'hiddenImages'">

+ 37 - 20
lib/metadata/policies.js

@@ -752,36 +752,53 @@ var policies = {
     "cssOldPropertyPrefixes": {
     "cssOldPropertyPrefixes": {
         "tool": "phantomas",
         "tool": "phantomas",
         "label": "Old prefixes",
         "label": "Old prefixes",
-        "message": "<p>Many property prefixes such as -moz- or -webkit- are not needed anymore, or by very few people. You can remove them or replace them with the non-prefixed version. This will help reducing your stylesheets weight.</p>",
+        "message": "<p>Many property prefixes such as -moz- or -webkit- are not needed anymore, or by very few people. Sometimes, they have never even existed. You can remove them or replace them with the non-prefixed version. This will help reducing your stylesheets weight.</p><p>The prefixes database comes from <a href=\"http://caniuse.com/\" target=\"_blank\">Can I Use</a>.</p>",
         "isOkThreshold": 0,
         "isOkThreshold": 0,
         "isBadThreshold": 75,
         "isBadThreshold": 75,
         "isAbnormalThreshold": 300,
         "isAbnormalThreshold": 300,
         "hasOffenders": true,
         "hasOffenders": true,
         "offendersTransformFn": function(offenders) {
         "offendersTransformFn": function(offenders) {
-            return {
-                count: offenders.length,
-                list: offenders.map(function(offender) {
-                    var splittedOffender = offendersHelpers.cssOffenderPattern(offender);
+            var properties = {};
+            offenders.forEach(function(offender) {
+                var splittedOffender = offendersHelpers.cssOffenderPattern(offender);
 
 
-                    var parts = /^([^{]*)(?: ?{ ?([^ ]+): (.*) ?}) \/\/ (.*)$/.exec(splittedOffender.css);
-
-                    if (!parts) {
-                        debug('cssOldPropertyPrefixes offenders transform function error with "%s"', offender);
-                        return {
-                            parseError: offender
-                        };
-                    }
+                var parts = /^([^{]*)(?: ?{ ?([^ ]+): (.*) ?}) \/\/ (.*)$/.exec(splittedOffender.css);
 
 
+                if (!parts) {
+                    debug('cssOldPropertyPrefixes offenders transform function error with "%s"', offender);
                     return {
                     return {
-                        rule: parts[1],
-                        property: parts[2],
-                        value: parts[3],
+                        parseError: offender
+                    };
+                }
+
+                var propertyName = parts[2];
+
+                if (!properties[propertyName]) {
+                    properties[propertyName] = {
+                        property: propertyName,
                         message: parts[4],
                         message: parts[4],
-                        file: splittedOffender.file,
-                        line: splittedOffender.line,
-                        column: splittedOffender.column
+                        rules: []
                     };
                     };
-                })
+                }
+
+                properties[propertyName].rules.push({
+                    rule: parts[1],
+                    value: parts[3],
+                    file: splittedOffender.file,
+                    line: splittedOffender.line,
+                    column: splittedOffender.column
+                });
+            });
+
+            // Object to array
+            var list = [];
+            for (var propertyName in properties) {
+                list.push(properties[propertyName]);
+            }
+
+            return {
+                count: offenders.length,
+                list: list
             };
             };
         }
         }
     },
     },

+ 20 - 16
lib/tools/jsExecutionTransformer.js

@@ -121,24 +121,28 @@ var jsExecutionTransformer = function() {
             debug('JS execution transformation complete');
             debug('JS execution transformation complete');
 
 
 
 
-            debug('Starting scroll execution transformation');
-            offenders.DOMaccessesOnScroll = JSON.parse(data.toolsResults.phantomas.offenders.scrollExecutionTree[0]);
-            if (offenders.DOMaccessesOnScroll.children) {
-                offenders.DOMaccessesOnScroll.children.forEach(function(node) {
-                    
-                    // Mark a event flag
-                    if (['documentScroll', 'windowScroll', 'window.onscroll'].indexOf(node.data.type) >= 0) {
-                        node.windowPerformance = true;
-                    }
+            if (data.toolsResults.phantomas.offenders.scrollExecutionTree) {
+                debug('Starting scroll execution transformation');
+                offenders.DOMaccessesOnScroll = JSON.parse(data.toolsResults.phantomas.offenders.scrollExecutionTree[0]);
+                if (offenders.DOMaccessesOnScroll.children) {
+                    offenders.DOMaccessesOnScroll.children.forEach(function(node) {
+                        
+                        // Mark a event flag
+                        if (['documentScroll', 'windowScroll', 'window.onscroll'].indexOf(node.data.type) >= 0) {
+                            node.windowPerformance = true;
+                        }
 
 
-                    // Transform domPaths into objects
-                    changeListOfDomPaths(node);
-                    
-                    // Count the number of DOM accesses, by counting the tree leafs
-                    metrics.DOMaccessesOnScroll += countTreeLeafs(node);
-                });
+                        // Transform domPaths into objects
+                        changeListOfDomPaths(node);
+                        
+                        // Count the number of DOM accesses, by counting the tree leafs
+                        metrics.DOMaccessesOnScroll += countTreeLeafs(node);
+                    });
+                }
+                debug('Scroll execution transformation complete');
+            } else {
+                debug('Could not parse scrollExecutionTree');
             }
             }
-            debug('Scroll execution transformation complete');
 
 
         } catch(err) {
         } catch(err) {
             throw err;
             throw err;

+ 79 - 0
lib/tools/phantomas/custom_modules/modules/lazyLoadableYLT/lazyLoadableYLT.js

@@ -0,0 +1,79 @@
+/**
+ * Analyzes images and detects which one can be lazy-loaded (are below the fold)
+ *
+ * @see https://github.com/macbre/phantomas/issues/494
+ */
+/* global document: true, window: true */
+
+exports.version = '1.0.a';
+
+exports.module = function(phantomas) {
+    'use strict';
+    
+    phantomas.setMetric('lazyLoadableImagesBelowTheFold'); // @desc number of images displayed below the fold that can be lazy-loaded
+
+    phantomas.on('report', function() {
+        phantomas.log('lazyLoadableImages: analyzing which images can be lazy-loaded...');
+
+        phantomas.evaluate(function() {
+            (function(phantomas) {
+                phantomas.spyEnabled(false, 'analyzing which images can be lazy-loaded');
+
+                var images = document.body.getElementsByTagName('img'),
+                    i,
+                    len = images.length,
+                    offset,
+                    path,
+                    processedImages = {},
+                    src,
+                    viewportHeight = window.innerHeight;
+
+                phantomas.log('lazyLoadableImages: %d image(s) found, assuming %dpx offset to be the fold', len, viewportHeight);
+
+                for (i = 0; i < len; i++) {
+                    // @see https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect
+                    offset = images[i].getBoundingClientRect().top;
+                    src = images[i].src;
+
+                    // ignore base64-encoded images
+                    if (src === '' || /^data:/.test(src)) {
+                        continue;
+                    }
+
+                    path = phantomas.getDOMPath(images[i]);
+
+                    // get the most top position for a given image (deduplicate by src)
+                    if (typeof processedImages[src] === 'undefined') {
+                        processedImages[src] = {
+                            offset: offset,
+                            path: path
+                        };
+                    }
+
+                    // maybe there's the same image loaded above the fold?
+                    if (offset < processedImages[src].offset) {
+                        processedImages[src] = {
+                            offset: offset,
+                            path: path
+                        };
+                    }
+                }
+
+                phantomas.log('lazyLoadableImages: checking %d unique image(s)', Object.keys(processedImages).length);
+
+                Object.keys(processedImages).forEach(function(src) {
+                    var img = processedImages[src];
+
+                    if (img.offset > viewportHeight) {
+                        phantomas.log('lazyLoadableImages: <%s> image (%s) is below the fold (at %dpx)', src, img.path, img.offset);
+
+                        phantomas.incrMetric('lazyLoadableImagesBelowTheFold');
+                        phantomas.addOffender('lazyLoadableImagesBelowTheFold', src);
+                    }
+                });
+
+                phantomas.spyEnabled(true);
+            })(window.__phantomas);
+        });
+    });
+};

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

@@ -46,6 +46,7 @@ var PhantomasWrapper = function() {
                 'javaScriptBottlenecks', // needs to be launched after custom module scopeYLT
                 'javaScriptBottlenecks', // needs to be launched after custom module scopeYLT
                 'jQuery', // overridden
                 'jQuery', // overridden
                 'jserrors', // overridden
                 'jserrors', // overridden
+                'lazyLoadableImages', //overriden
                 'pageSource', // not needed
                 'pageSource', // not needed
                 'windowPerformance' // overriden
                 'windowPerformance' // overriden
             ].join(','),
             ].join(','),
@@ -112,9 +113,19 @@ var PhantomasWrapper = function() {
 
 
         // It's time to launch the test!!!
         // It's time to launch the test!!!
         var triesNumber = 2;
         var triesNumber = 2;
+        var currentTry = 0;
 
 
         async.retry(triesNumber, function(cb) {
         async.retry(triesNumber, function(cb) {
+
+            currentTry ++;
+            // Fix for https://github.com/gmetais/YellowLabTools/issues/114
+            if (currentTry === 2 && options.engine === 'webkit2') {
+                debug('Launching a second try with the old webkit v1 engine');
+                options.engine = 'webkit';
+            }
+
             var process = phantomas(task.url, options, function(err, json, results) {
             var process = phantomas(task.url, options, function(err, json, results) {
+                var errorCode = err ? parseInt(err.message, 10) : null;
                 
                 
                 if (isKilled) {
                 if (isKilled) {
                     debug('Process was killed, too late Phantomas, sorry...');
                     debug('Process was killed, too late Phantomas, sorry...');
@@ -122,28 +133,28 @@ var PhantomasWrapper = function() {
                 }
                 }
 
 
 
 
-                debug('Returning from Phantomas with error %s', err);
+                debug('Returning from Phantomas with error %s', errorCode);
 
 
                 // Adding some YellowLabTools errors here
                 // Adding some YellowLabTools errors here
                 if (json && json.metrics && (!json.metrics.javascriptExecutionTree || !json.offenders.javascriptExecutionTree)) {
                 if (json && json.metrics && (!json.metrics.javascriptExecutionTree || !json.offenders.javascriptExecutionTree)) {
-                    err = 1001;
+                    errorCode = 1001;
                 }
                 }
 
 
-                if (!err && (!json || !json.metrics)) {
-                    err = 1002;
+                if (!errorCode && (!json || !json.metrics)) {
+                    errorCode = 1002;
                 }
                 }
 
 
                 // Don't cancel test if it is a timeout and we've got some results
                 // Don't cancel test if it is a timeout and we've got some results
-                if (err === 252 && json) {
+                if (errorCode === 252 && json) {
                     debug('Timeout after ' + options.timeout + ' seconds. But it\'s not a problem, the test is valid.');
                     debug('Timeout after ' + options.timeout + ' seconds. But it\'s not a problem, the test is valid.');
-                    err = null;
+                    errorCode = null;
                 }
                 }
 
 
-                if (err) {
-                    debug('Attempt failed. Error code ' + err);
+                if (errorCode) {
+                    debug('Attempt failed. Error code ' + errorCode);
                 }
                 }
 
 
-                cb(err, json);
+                cb(errorCode, json);
             });
             });
             
             
             phantomasPid = process.pid;
             phantomasPid = process.pid;

+ 29 - 25
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "yellowlabtools",
   "name": "yellowlabtools",
-  "version": "1.8.0",
+  "version": "1.8.1",
   "description": "Online tool to audit a webpage for performance and front-end quality issues",
   "description": "Online tool to audit a webpage for performance and front-end quality issues",
   "license": "GPL-2.0",
   "license": "GPL-2.0",
   "author": {
   "author": {
@@ -15,51 +15,55 @@
   "bin": {
   "bin": {
     "yellowlabtools": "./bin/cli.js"
     "yellowlabtools": "./bin/cli.js"
   },
   },
+  "engines": {
+    "node": ">= 0.12.0"
+  },
   "main": "./lib/index.js",
   "main": "./lib/index.js",
   "dependencies": {
   "dependencies": {
-    "angular": "1.4.5",
-    "angular-animate": "1.4.5",
-    "angular-chart.js": "0.8.4",
+    "angular": "1.4.7",
+    "angular-animate": "1.4.7",
+    "angular-chart.js": "0.8.5",
     "angular-local-storage": "0.2.2",
     "angular-local-storage": "0.2.2",
-    "angular-resource": "1.4.5",
-    "angular-route": "1.4.5",
-    "angular-sanitize": "1.4.5",
-    "async": "1.4.2",
-    "body-parser": "1.14.0",
+    "angular-resource": "1.4.7",
+    "angular-route": "1.4.7",
+    "angular-sanitize": "1.4.7",
+    "async": "1.5.0",
+    "body-parser": "1.14.1",
     "chart.js": "1.0.2",
     "chart.js": "1.0.2",
-    "clean-css": "3.4.3",
+    "clean-css": "3.4.6",
     "color-diff": "0.1.7",
     "color-diff": "0.1.7",
-    "compression": "1.5.2",
+    "compression": "1.6.0",
     "cors": "2.7.1",
     "cors": "2.7.1",
     "debug": "2.2.0",
     "debug": "2.2.0",
     "express": "4.13.3",
     "express": "4.13.3",
-    "imagemin": "3.2.0",
+    "imagemin": "3.2.2",
     "imagemin-jpegoptim": "4.0.0",
     "imagemin-jpegoptim": "4.0.0",
     "jstoxml": "0.2.3",
     "jstoxml": "0.2.3",
-    "lwip": "0.0.7",
-    "meow": "3.3.0",
-    "minimize": "1.7.1",
+    "lwip": "0.0.8",
+    "meow": "3.4.2",
+    "minimize": "1.7.4",
     "parse-color": "1.0.0",
     "parse-color": "1.0.0",
-    "phantomas": "1.12.0",
-    "ps-node": "0.0.4",
+    "phantomas": "1.13.0",
+    "ps-node": "0.0.5",
     "q": "1.4.1",
     "q": "1.4.1",
-    "request": "2.62.0",
+    "request": "2.65.0",
     "rimraf": "2.4.3",
     "rimraf": "2.4.3",
     "temporary": "0.0.8",
     "temporary": "0.0.8",
-    "uglify-js": "2.4.24"
+    "try-thread-sleep": "1.0.0",
+    "uglify-js": "2.5.0"
   },
   },
   "devDependencies": {
   "devDependencies": {
-    "chai": "~3.2.0",
+    "chai": "~3.4.0",
     "grunt": "~0.4.5",
     "grunt": "~0.4.5",
     "grunt-blanket": "~0.0.8",
     "grunt-blanket": "~0.0.8",
     "grunt-contrib-clean": "~0.6.0",
     "grunt-contrib-clean": "~0.6.0",
     "grunt-contrib-concat": "~0.5.1",
     "grunt-contrib-concat": "~0.5.1",
-    "grunt-contrib-copy": "~0.8.1",
+    "grunt-contrib-copy": "~0.8.2",
     "grunt-contrib-cssmin": "~0.14.0",
     "grunt-contrib-cssmin": "~0.14.0",
-    "grunt-contrib-htmlmin": "~0.4.0",
+    "grunt-contrib-htmlmin": "~0.6.0",
     "grunt-contrib-jshint": "~0.11.3",
     "grunt-contrib-jshint": "~0.11.3",
     "grunt-contrib-less": "~1.0.1",
     "grunt-contrib-less": "~1.0.1",
-    "grunt-contrib-uglify": "~0.9.2",
+    "grunt-contrib-uglify": "~0.10.0",
     "grunt-env": "~0.4.4",
     "grunt-env": "~0.4.4",
     "grunt-express": "~1.4.1",
     "grunt-express": "~1.4.1",
     "grunt-filerev": "~2.3.1",
     "grunt-filerev": "~2.3.1",
@@ -69,9 +73,9 @@
     "grunt-replace": "~0.11.0",
     "grunt-replace": "~0.11.0",
     "grunt-usemin": "~3.1.1",
     "grunt-usemin": "~3.1.1",
     "grunt-webfont": "~0.5.4",
     "grunt-webfont": "~0.5.4",
-    "matchdep": "~0.3.0",
+    "matchdep": "~1.0.0",
     "mocha": "~2.3.2",
     "mocha": "~2.3.2",
-    "sinon": "~1.16.1",
+    "sinon": "~1.17.2",
     "sinon-chai": "~2.8.0"
     "sinon-chai": "~2.8.0"
   },
   },
   "scripts": {
   "scripts": {

+ 20 - 13
test/core/phantomasWrapperTest.js

@@ -51,11 +51,14 @@ describe('phantomasWrapper', function() {
             done('Error: unwanted success');
             done('Error: unwanted success');
 
 
         }).fail(function(err) {
         }).fail(function(err) {
+            try {
+                should.exist(err);
+                err.should.equal(254);
 
 
-            should.exist(err);
-            err.should.equal(254);
-
-            done();
+                done();
+            } catch(error) {
+                done(error);
+            }
         });
         });
     });
     });
 
 
@@ -72,16 +75,20 @@ describe('phantomasWrapper', function() {
             }
             }
         }).then(function(data) {
         }).then(function(data) {
             /*jshint -W030 */
             /*jshint -W030 */
-            
-            data.should.be.an('object');
-            data.should.have.a.property('generator');
-            data.generator.should.contain('phantomas');
-            data.should.have.a.property('url').that.equals(url);
-            data.should.have.a.property('metrics').that.is.an('object').not.empty;
-            data.should.have.a.property('offenders').that.is.an('object').not.empty;
-            data.offenders.should.have.a.property('javascriptExecutionTree').that.is.a('array').not.empty;
 
 
-            done();
+            try {            
+                data.should.be.an('object');
+                data.should.have.a.property('generator');
+                data.generator.should.contain('phantomas');
+                data.should.have.a.property('url').that.equals(url);
+                data.should.have.a.property('metrics').that.is.an('object').not.empty;
+                data.should.have.a.property('offenders').that.is.an('object').not.empty;
+                data.offenders.should.have.a.property('javascriptExecutionTree').that.is.a('array').not.empty;
+
+                done();
+            } catch(error) {
+                done(error);
+            }
         }).fail(function(err) {
         }).fail(function(err) {
             done(err);
             done(err);
         });
         });