Prechádzať zdrojové kódy

Merge pull request #128 from gmetais/develop

Pushing to production soon
Gaël Métais 9 rokov pred
rodič
commit
9643c5bfcd

+ 0 - 8
README.md

@@ -75,14 +75,6 @@ You can use [ngrok](https://ngrok.com/), a tool that creates a secure tunnel bet
 If your project is not accessible from outside, or if you want to fork and improve the tool, you can build your own instance. The documentation is [here](https://github.com/gmetais/YellowLabTools/wiki/Install-your-private-server).
 If your project is not accessible from outside, or if you want to fork and improve the tool, you can build your own instance. The documentation is [here](https://github.com/gmetais/YellowLabTools/wiki/Install-your-private-server).
 
 
 
 
-## Help needed!
-There are so many things left to do, **your help would be greatly appreciated**! Please report bugs, ask for evolutions and come code with me.
-
-
-## License
-Please read the [license](LICENSE). Icons are the property of [IcoMoon.io](https://icomoon.io/)
-
-
 ## Author
 ## Author
 Gaël Métais. I'm a webperf freelance. Follow me on Twitter [@gaelmetais](https://twitter.com/gaelmetais), I tweet about Web Performances, Front-end and new versions of YellowLabTools!
 Gaël Métais. I'm a webperf freelance. Follow me on Twitter [@gaelmetais](https://twitter.com/gaelmetais), I tweet about Web Performances, Front-end and new versions of YellowLabTools!
 
 

+ 1 - 1
front/src/css/rule.css

@@ -179,7 +179,7 @@
 .checker {
 .checker {
   /* Checkerboard background */
   /* Checkerboard background */
   background-color: #ddd;
   background-color: #ddd;
-  background-image: linear-gradient(45deg, #aaaaaa 25%, transparent 25%, transparent 75%, #aaaaaa 75%, #aaaaaa), linear-gradient(45deg, #aaaaaa 25%, transparent 25%, transparent 75%, #aaaaaa 75%, #aaaaaa);
+  background-image: linear-gradient(45deg, #AAA 25%, transparent 25%, transparent 75%, #AAA 75%, #AAA), linear-gradient(45deg, #AAA 25%, transparent 25%, transparent 75%, #AAA 75%, #AAA);
   background-size: 1em 1em;
   background-size: 1em 1em;
   background-position: 0 0, 0.5em 0.5em;
   background-position: 0 0, 0.5em 0.5em;
 }
 }

+ 15 - 15
front/src/css/timeline.css

@@ -62,34 +62,34 @@
   background: #F04DA7;
   background: #F04DA7;
 }
 }
 .timeline .domComplete.interval {
 .timeline .domComplete.interval {
-  background: #ede3ff;
+  background: #EDE3FF;
 }
 }
 .timeline .domComplete .color {
 .timeline .domComplete .color {
-  background: #c2a3ff;
+  background: #C2A3FF;
 }
 }
 .timeline .domContentLoadedEnd.interval {
 .timeline .domContentLoadedEnd.interval {
-  background: #d8f0f0;
+  background: #D8F0F0;
 }
 }
 .timeline .domContentLoadedEnd .color {
 .timeline .domContentLoadedEnd .color {
-  background: #7ecccc;
+  background: #7ECCCC;
 }
 }
 .timeline .domContentLoaded.interval {
 .timeline .domContentLoaded.interval {
-  background: #e0ffd1;
+  background: #E0FFD1;
 }
 }
 .timeline .domContentLoaded .color {
 .timeline .domContentLoaded .color {
-  background: #a7e846;
+  background: #A7E846;
 }
 }
 .timeline .domInteractive.interval {
 .timeline .domInteractive.interval {
-  background: #fffccc;
+  background: #FFFCCC;
 }
 }
 .timeline .domInteractive .color {
 .timeline .domInteractive .color {
-  background: #ffe433;
+  background: #FFE433;
 }
 }
 .timeline .domCreation.interval {
 .timeline .domCreation.interval {
-  background: #ffe0cc;
+  background: #FFE0CC;
 }
 }
 .timeline .domCreation .color {
 .timeline .domCreation .color {
-  background: #ff6600;
+  background: #FF6600;
 }
 }
 .timeline .tooltip.detailsOverlay {
 .timeline .tooltip.detailsOverlay {
   position: absolute;
   position: absolute;
@@ -294,19 +294,19 @@
   white-space: nowrap;
   white-space: nowrap;
 }
 }
 .table > div > .startTime.domComplete {
 .table > div > .startTime.domComplete {
-  background: #ede3ff;
+  background: #EDE3FF;
 }
 }
 .table > div > .startTime.domContentLoadedEnd {
 .table > div > .startTime.domContentLoadedEnd {
-  background: #d8f0f0;
+  background: #D8F0F0;
 }
 }
 .table > div > .startTime.domContentLoaded {
 .table > div > .startTime.domContentLoaded {
-  background: #e0ffd1;
+  background: #E0FFD1;
 }
 }
 .table > div > .startTime.domInteractive {
 .table > div > .startTime.domInteractive {
-  background: #fffccc;
+  background: #FFFCCC;
 }
 }
 .table > div > .startTime.domCreation {
 .table > div > .startTime.domCreation {
-  background: #ffe0cc;
+  background: #FFE0CC;
 }
 }
 .execution .icon-warning {
 .execution .icon-warning {
   color: #e74c3c;
   color: #e74c3c;

+ 48 - 0
front/src/js/controllers/ruleCtrl.js

@@ -48,6 +48,54 @@ ruleCtrl.controller('RuleCtrl', ['$scope', '$rootScope', '$routeParams', '$locat
                 tooltipTemplate: '<%=label%>: <%=value%> KB'
                 tooltipTemplate: '<%=label%>: <%=value%> KB'
             };
             };
         }
         }
+
+        // Init "Breakpoints" chart
+        if ($scope.policyName === 'cssBreakpoints' && $scope.rule.value > 0) {
+
+            // Seek for the biggest breakpoint
+            var max = 0;
+            $scope.rule.offendersObj.forEach(function(offender) {
+                if (offender.pixels > max) {
+                    max = offender.pixels;
+                }
+            });
+            max = Math.max(max + 100, 1400);
+
+            // We group offenders 10px by 10px
+            var GROUP_SIZE = 20;
+
+            // Generate an empty array of values
+            $scope.breakpointsLabels = [];
+            $scope.breakpointsData = [[]];
+            for (var i = 0; i <= max / GROUP_SIZE; i++) {
+                $scope.breakpointsLabels[i] = '';
+                $scope.breakpointsData[0][i] = 0;
+            }
+
+            // Fill it with results
+            $scope.rule.offendersObj.forEach(function(offender) {
+                var group = Math.floor((offender.pixels + 1) / GROUP_SIZE);
+
+                if ($scope.breakpointsLabels[group] !== '') {
+                    $scope.breakpointsLabels[group] += '/';
+                }
+                $scope.breakpointsLabels[group] += offender.breakpoint;
+
+                $scope.breakpointsData[0][group] += offender.count;
+            });
+
+            $scope.breakpointsColours = ['#9c4274'];
+            $scope.breakpointsOptions = {
+                scaleShowGridLines: false,
+                barShowStroke: false,
+                showTooltips: false,
+                pointDot: false,
+                responsive: true,
+                maintainAspectRatio: true,
+                strokeColor: 'rgba(20, 200, 20, 1)',
+                scaleFontSize: 9
+            };
+        }
     }
     }
 
 
     $scope.backToDashboard = function() {
     $scope.backToDashboard = function() {

+ 21 - 0
front/src/views/rule.html

@@ -100,6 +100,11 @@
                         <div class="similarColors checker"><div ng-style="{'background-color': offender.color1, 'color': offender.isDark ? '#FFF' : '#000'}">{{offender.color1}}</div><div ng-style="{'background-color': offender.color2, 'color': offender.isDark ? '#FFF' : '#000'}">{{offender.color2}}</div></div>
                         <div class="similarColors checker"><div ng-style="{'background-color': offender.color1, 'color': offender.isDark ? '#FFF' : '#000'}">{{offender.color1}}</div><div ng-style="{'background-color': offender.color2, 'color': offender.isDark ? '#FFF' : '#000'}">{{offender.color2}}</div></div>
                     </div>
                     </div>
 
 
+                    <div ng-if="policyName === 'cssMobileFirst'">
+                        <b>{{offender.query}}</b> for <ng-pluralize count="offender.rules" when="{'one':'1 rule','other':'{} rules'}"></ng-pluralize>
+                        <file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
+                    </div>
+
                     <div ng-if="policyName === 'cssParsingErrors'">
                     <div ng-if="policyName === 'cssParsingErrors'">
                         <b>{{offender.error}}</b>
                         <b>{{offender.error}}</b>
                         <file-and-line file="offender.file" line="offender.line" column="offender.column"></file-and-line>
                         <file-and-line file="offender.file" line="offender.line" column="offender.column"></file-and-line>
@@ -348,6 +353,22 @@
         Please open the <a href="/result/{{runId}}/timeline#filter=eventNotDelegated">JS timeline, filtered by "Events not delegated"</a>
         Please open the <a href="/result/{{runId}}/timeline#filter=eventNotDelegated">JS timeline, filtered by "Events not delegated"</a>
     </div>
     </div>
 
 
+    <div ng-if="policyName === 'cssBreakpoints'">
+        <div ng-if="rule.value > 0" class="cssBreakpointsGraph">
+            <h3>Breakpoints distribution graph</h3>
+            <canvas class="chart chart-line" chart-data="breakpointsData" chart-labels="breakpointsLabels" chart-options="breakpointsOptions" chart-legend="true" chart-colours="breakpointsColours" width="600" height="250"></canvas>
+            <h3>Breakpoints list</h3>
+            <div class="offendersTable">
+                <div ng-repeat="offender in rule.offendersObj | orderBy:'pixels'">
+                    <div>Breakpoint <b>{{offender.breakpoint}}</b> involves <ng-pluralize count="offender.count" when="{'one': '1 rule', 'other': '{} rules'}"></ng-pluralize></div>
+                </div>
+            </div>
+        </div>
+        <div ng-if="rule.value === 0">
+        No breakpoint
+        </div>
+    </div>
+
     <div ng-if="!rule && rule !== null" class="notFound">
     <div ng-if="!rule && rule !== null" class="notFound">
         <h2>404</h2>
         <h2>404</h2>
         Rule "{{policyName}}"" not found
         Rule "{{policyName}}"" not found

+ 38 - 1
lib/metadata/policies.js

@@ -464,7 +464,7 @@ var policies = {
     "cssColors": {
     "cssColors": {
         "tool": "phantomas",
         "tool": "phantomas",
         "label": "Colors count",
         "label": "Colors count",
-        "message": "<p>This is the number of different colors defined in CSS.</p><p>Your CSS project will be easier to maintain if you keep a small color set.</p>",
+        "message": "<p>This is the number of different colors defined in CSS.</p><p>Your CSS will be easier to maintain if you keep a small color set.</p>",
         "isOkThreshold": 30,
         "isOkThreshold": 30,
         "isBadThreshold": 150,
         "isBadThreshold": 150,
         "isAbnormalThreshold": 400,
         "isAbnormalThreshold": 400,
@@ -522,6 +522,43 @@ var policies = {
             };
             };
         }
         }
     },
     },
+    "cssBreakpoints": {
+        "tool": "mediaQueriesChecker",
+        "label": "Breakpoints count",
+        "message": "<p>This is the number of different breakpoints found in the stylesheets' media queries.</p><p>Please note this rule is based on <i>min-width</i>, <i>max-width</i>, <i>min-device-width</i> and <i>max-device-width</i> media queries only.</p><p>Your CSS will be easier to maintain if you keep a reasonable number of breakpoints. Try to make a fluid design - using percents - to avoid the creation of numerous breakpoints.</p>",
+        "isOkThreshold": 6,
+        "isBadThreshold": 40,
+        "isAbnormalThreshold": 60,
+        "hasOffenders": true,
+        "offendersTransformFn": function(offenders) {
+            var offendersTable = [];
+
+            for (var offender in offenders) {
+                offendersTable.push({
+                    breakpoint: offender,
+                    count: offenders[offender].count,
+                    pixels: offenders[offender].pixels
+                });
+            }
+
+            return offendersTable;
+        }
+    },
+    "cssMobileFirst": {
+        "tool": "mediaQueriesChecker",
+        "label": "Not mobile-first media queries",
+        "message": "<p>This is the number of CSS rules inside media queries that address small screens.</p><p>The common good practice, when creating a responsive website, is to write it \"mobile-first\". More explanation in <a href=\"http://www.sitepoint.com/introduction-mobile-first-media-queries\" target=\"_blank\">this great article</a>.</p>",
+        "isOkThreshold": 25,
+        "isBadThreshold": 200,
+        "isAbnormalThreshold": 1000,
+        "hasOffenders": true,
+        "offendersTransformFn": function(offenders) {
+            return {
+                count: offenders.length,
+                list: offenders
+            };
+        }
+    },
     "cssImports": {
     "cssImports": {
         "tool": "phantomas",
         "tool": "phantomas",
         "label": "Uses of @import",
         "label": "Uses of @import",

+ 3 - 1
lib/metadata/scoreProfileGeneric.json

@@ -71,7 +71,9 @@
                 "cssComplexSelectors": 2,
                 "cssComplexSelectors": 2,
                 "cssComplexSelectorsByAttribute": 1.5,
                 "cssComplexSelectorsByAttribute": 1.5,
                 "cssColors": 0.5,
                 "cssColors": 0.5,
-                "similarColors": 0.5
+                "similarColors": 0.5,
+                "cssBreakpoints": 1,
+                "cssMobileFirst": 1
             }
             }
         },
         },
         "badCSS": {
         "badCSS": {

+ 4 - 0
lib/runner.js

@@ -4,6 +4,7 @@ var debug                   = require('debug')('ylt:runner');
 var phantomasWrapper        = require('./tools/phantomas/phantomasWrapper');
 var phantomasWrapper        = require('./tools/phantomas/phantomasWrapper');
 var jsExecutionTransformer  = require('./tools/jsExecutionTransformer');
 var jsExecutionTransformer  = require('./tools/jsExecutionTransformer');
 var colorDiff               = require('./tools/colorDiff');
 var colorDiff               = require('./tools/colorDiff');
+var mediaQueriesChecker     = require('./tools/mediaQueriesChecker');
 var weightChecker           = require('./tools/weightChecker/weightChecker');
 var weightChecker           = require('./tools/weightChecker/weightChecker');
 var rulesChecker            = require('./rulesChecker');
 var rulesChecker            = require('./rulesChecker');
 var scoreCalculator         = require('./scoreCalculator');
 var scoreCalculator         = require('./scoreCalculator');
@@ -32,6 +33,9 @@ var Runner = function(params) {
         // Compare colors
         // Compare colors
         data = colorDiff.compareAllColors(data);
         data = colorDiff.compareAllColors(data);
 
 
+        // Check media queries
+        data = mediaQueriesChecker.analyzeMediaQueries(data);
+
         // Redownload every file
         // Redownload every file
         return weightChecker.recheckAllFiles(data);
         return weightChecker.recheckAllFiles(data);
 
 

+ 4 - 2
lib/tools/colorDiff.js

@@ -46,7 +46,9 @@ var colorDiff = function() {
         var deduplicatedColors = {};
         var deduplicatedColors = {};
 
 
         parsedOffenders.forEach(function(color) {
         parsedOffenders.forEach(function(color) {
-            deduplicatedColors[color] = color;
+            if (color !== null) {
+                deduplicatedColors[color] = color;
+            }
         });
         });
 
 
         return Object.keys(deduplicatedColors).map(this.parseColor);
         return Object.keys(deduplicatedColors).map(this.parseColor);
@@ -54,7 +56,7 @@ var colorDiff = function() {
 
 
     this.parseOffender = function(offender) {
     this.parseOffender = function(offender) {
         var regexResult = /^(.*) \(\d+ times\)$/.exec(offender);
         var regexResult = /^(.*) \(\d+ times\)$/.exec(offender);
-        return regexResult[1];
+        return regexResult ? regexResult[1] : null;
     };
     };
 
 
     this.parseColor = function(color) {
     this.parseColor = function(color) {

+ 168 - 0
lib/tools/mediaQueriesChecker.js

@@ -0,0 +1,168 @@
+var debug   = require('debug')('ylt:mediaQueriesChecker');
+var parseMediaQuery = require('css-mq-parser');
+var offendersHelpers = require('../offendersHelpers');
+
+
+var mediaQueriesChecker = function() {
+    'use strict';
+
+    var MOBILE_MIN_BREAKPOINT = 200;
+    var MOBILE_MAX_BREAKPOINT = 300;
+
+    this.analyzeMediaQueries = function(data) {
+        debug('Starting to check all media queries...');
+
+        var offenders = data.toolsResults.phantomas.offenders.cssMediaQueries;
+        var mediaQueries = (offenders) ? this.parseAllMediaQueries(offenders) : [];
+        
+        var notMobileFirstCount = 0;
+        var notMobileFirstOffenders = [];
+
+        var breakpointsOffenders = {};
+
+        for (var i = 0; i < mediaQueries.length; i++) {
+            var item = mediaQueries[i];
+
+            if (!item) {
+                continue;
+            }
+
+            if (item.isForMobile) {
+                notMobileFirstCount += item.mediaQuery.rules;
+                notMobileFirstOffenders.push(item.mediaQuery);
+            }
+
+            for (var j = 0; j < item.breakpoints.length; j++) {
+                var breakpointString = item.breakpoints[j].string;
+                if (!breakpointsOffenders[breakpointString]) {
+                    breakpointsOffenders[breakpointString] = {
+                        count: 1,
+                        pixels: item.breakpoints[j].pixels
+                    };
+                } else {
+                    breakpointsOffenders[breakpointString].count += 1;
+                }
+            }
+        }
+
+        data.toolsResults.mediaQueriesChecker = {
+            metrics: {
+                cssMobileFirst: notMobileFirstCount,
+                cssBreakpoints: Object.keys(breakpointsOffenders).length
+            },
+            offenders: {
+                cssMobileFirst: notMobileFirstOffenders,
+                cssBreakpoints: breakpointsOffenders
+            }
+        };
+
+        debug('End of media queries check');
+
+        return data;
+    };
+
+    this.parseAllMediaQueries = function(offenders) {
+        return offenders.map(this.parseOneMediaQuery);
+    };
+
+    this.parseOneMediaQuery = function(offender) {
+        var splittedOffender = offendersHelpers.cssOffenderPattern(offender);
+        var parts = /^@media (.*) \((\d+ rules)\)$/.exec(splittedOffender.css);
+
+        if (!parts) {
+            debug('Failed to parse media query ' + offender);
+            return false;
+        }
+
+        var rulesCount = parseInt(parts[2], 10);
+        var query = parts[1];
+
+        var isForMobile = false;
+        var breakpoints = [];
+
+        try {
+
+            var ast = parseMediaQuery(query);
+
+            var min = 0;
+            var max = Infinity;
+            var pixels;
+
+            ast.forEach(function(astItem) {
+                astItem.expressions.forEach(function(expression) {
+                    if (expression.feature === 'width' || expression.feature === 'device-width') {
+                        if (astItem.inverse === false) {
+                            if (expression.modifier === 'max') {
+                                pixels = toPixels(expression.value);
+                                max = Math.min(max, pixels);
+                                breakpoints.push({
+                                    string: expression.value,
+                                    pixels: pixels
+                                });
+                            } else if (expression.modifier === 'min') {
+                                pixels = toPixels(expression.value);
+                                min = Math.max(min, pixels);
+                                breakpoints.push({
+                                    string: expression.value,
+                                    pixels: pixels
+                                });
+                            }
+                        } else if (astItem.inverse === true) {
+                            if (expression.modifier === 'max') {
+                                pixels = toPixels(expression.value);
+                                min = Math.max(min, pixels);
+                                breakpoints.push({
+                                    string: expression.value,
+                                    pixels: pixels
+                                });
+                            } else if (expression.modifier === 'min') {
+                                pixels = toPixels(expression.value);
+                                max = Math.min(max, pixels);
+                                breakpoints.push({
+                                    string: expression.value,
+                                    pixels: pixels
+                                });
+                            }
+                        }
+                    }
+                });
+            });
+
+            isForMobile = (min <= MOBILE_MIN_BREAKPOINT && max >= MOBILE_MAX_BREAKPOINT && max !== Infinity);
+
+        } catch(error) {
+            debug('Failed to parse media query ' + offender);
+        }
+
+        return {
+            mediaQuery: {
+                query: query,
+                rules: rulesCount,
+                file: splittedOffender.file,
+                line: splittedOffender.line,
+                column: splittedOffender.column
+            },
+            isForMobile: isForMobile,
+            breakpoints: breakpoints
+        };
+    };
+
+    // Parses a size in em, pt (or px) and returns it in px
+    function toPixels(size) {
+        var splittedSize = /^([\d\.]+)(.*)/.exec(size);
+        var value = parseFloat(splittedSize[1]);
+        var unit = splittedSize[2];
+
+        if (unit === 'em') {
+            return value * 16;
+        }
+
+        if (unit === 'pt') {
+            return value / 12 * 16;
+        }
+
+        return value;
+    }
+};
+
+module.exports = new mediaQueriesChecker();

+ 20 - 19
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "yellowlabtools",
   "name": "yellowlabtools",
-  "version": "1.8.2",
+  "version": "1.9.0",
   "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": {
@@ -20,50 +20,51 @@
   },
   },
   "main": "./lib/index.js",
   "main": "./lib/index.js",
   "dependencies": {
   "dependencies": {
-    "angular": "1.4.7",
-    "angular-animate": "1.4.7",
-    "angular-chart.js": "0.8.5",
+    "angular": "1.4.8",
+    "angular-animate": "1.4.8",
+    "angular-chart.js": "0.8.6",
     "angular-local-storage": "0.2.2",
     "angular-local-storage": "0.2.2",
-    "angular-resource": "1.4.7",
-    "angular-route": "1.4.7",
-    "angular-sanitize": "1.4.7",
+    "angular-resource": "1.4.8",
+    "angular-route": "1.4.8",
+    "angular-sanitize": "1.4.8",
     "async": "1.5.0",
     "async": "1.5.0",
     "body-parser": "1.14.1",
     "body-parser": "1.14.1",
     "chart.js": "1.0.2",
     "chart.js": "1.0.2",
-    "clean-css": "3.4.6",
+    "clean-css": "3.4.8",
     "color-diff": "0.1.7",
     "color-diff": "0.1.7",
     "compression": "1.6.0",
     "compression": "1.6.0",
     "cors": "2.7.1",
     "cors": "2.7.1",
+    "css-mq-parser": "0.0.3",
     "debug": "2.2.0",
     "debug": "2.2.0",
     "express": "4.13.3",
     "express": "4.13.3",
-    "imagemin": "3.2.2",
-    "imagemin-jpegoptim": "4.0.0",
+    "imagemin": "4.0.0",
+    "imagemin-jpegoptim": "4.1.0",
     "jstoxml": "0.2.3",
     "jstoxml": "0.2.3",
     "lwip": "0.0.8",
     "lwip": "0.0.8",
-    "meow": "3.4.2",
+    "meow": "3.6.0",
     "minimize": "1.7.4",
     "minimize": "1.7.4",
     "parse-color": "1.0.0",
     "parse-color": "1.0.0",
     "phantomas": "1.13.0",
     "phantomas": "1.13.0",
     "ps-node": "0.0.5",
     "ps-node": "0.0.5",
     "q": "1.4.1",
     "q": "1.4.1",
-    "request": "2.65.0",
-    "rimraf": "2.4.3",
+    "request": "2.67.0",
+    "rimraf": "2.4.4",
     "temporary": "0.0.8",
     "temporary": "0.0.8",
     "try-thread-sleep": "1.0.0",
     "try-thread-sleep": "1.0.0",
-    "uglify-js": "2.5.0"
+    "uglify-js": "2.6.1"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "chai": "~3.4.0",
     "chai": "~3.4.0",
     "grunt": "~0.4.5",
     "grunt": "~0.4.5",
-    "grunt-blanket": "~0.0.8",
-    "grunt-contrib-clean": "~0.6.0",
+    "grunt-blanket": "~0.0.10",
+    "grunt-contrib-clean": "~0.7.0",
     "grunt-contrib-concat": "~0.5.1",
     "grunt-contrib-concat": "~0.5.1",
     "grunt-contrib-copy": "~0.8.2",
     "grunt-contrib-copy": "~0.8.2",
     "grunt-contrib-cssmin": "~0.14.0",
     "grunt-contrib-cssmin": "~0.14.0",
     "grunt-contrib-htmlmin": "~0.6.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-uglify": "~0.10.0",
+    "grunt-contrib-less": "~1.1.0",
+    "grunt-contrib-uglify": "~0.11.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",
@@ -72,7 +73,7 @@
     "grunt-mocha-test": "~0.12.7",
     "grunt-mocha-test": "~0.12.7",
     "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": "~1.0.2",
     "matchdep": "~1.0.0",
     "matchdep": "~1.0.0",
     "mocha": "~2.3.2",
     "mocha": "~2.3.2",
     "sinon": "~1.17.2",
     "sinon": "~1.17.2",

+ 55 - 0
test/core/mediaQueriesCheckerTest.js

@@ -0,0 +1,55 @@
+var should = require('chai').should();
+var mediaQueriesChecker = require('../../lib/tools/mediaQueriesChecker');
+
+describe('mediaQueriesChecker', function() {
+    
+    it('should parse mediaQueryes correctly', function() {
+        mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 1024px) (1 rules) <http://domain.com/css/stylesheet.css> @ 269:1').should.deep.equal({
+            mediaQuery: {
+                query: 'screen and (max-width: 1024px)',
+                rules: 1,
+                file: 'http://domain.com/css/stylesheet.css',
+                line: 269,
+                column: 1
+            },
+            isForMobile: true,
+            breakpoints: [{string: '1024px', pixels: 1024}]
+        });
+    });
+
+    it('should determine if it\'s for mobile correctly', function() {
+        mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 1024px) (1 rules) <file> @ 1:1').isForMobile.should.equal(true);
+        mediaQueriesChecker.parseOneMediaQuery('@media (max-width:1024px) (1 rules) <file> @ 1:1').isForMobile.should.equal(true);
+        mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 320px) (1 rules) <file> @ 1:1').isForMobile.should.equal(true);
+        mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 290px) (1 rules) <file> @ 1:1').isForMobile.should.equal(false);
+        mediaQueriesChecker.parseOneMediaQuery('@media screen and (min-width: 320px) (1 rules) <file> @ 1:1').isForMobile.should.equal(false);
+        mediaQueriesChecker.parseOneMediaQuery('@media screen and (min-width: 600px) (1 rules) <file> @ 1:1').isForMobile.should.equal(false);
+        mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 20em) (1 rules) <file> @ 1:1').isForMobile.should.equal(true);
+        mediaQueriesChecker.parseOneMediaQuery('@media screen and (min-width: 40em) (1 rules) <file> @ 1:1').isForMobile.should.equal(false);
+        mediaQueriesChecker.parseOneMediaQuery('@media (min-width: 600px) and (max-width: 1000px) (1 rules) <file> @ 1:1').isForMobile.should.equal(false);
+        mediaQueriesChecker.parseOneMediaQuery('@media (min-width: 180px) and (max-width: 400px) (1 rules) <file> @ 1:1').isForMobile.should.equal(true);
+        mediaQueriesChecker.parseOneMediaQuery('@media (min-width: 180px) and (max-width: 290px) (1 rules) <file> @ 1:1').isForMobile.should.equal(false);
+        mediaQueriesChecker.parseOneMediaQuery('@media not all and (min-width: 600px) (1 rules) <file> @ 1:1').isForMobile.should.equal(true);
+        mediaQueriesChecker.parseOneMediaQuery('@media not all and (min-width: 180px) (1 rules) <file> @ 1:1').isForMobile.should.equal(false);
+        mediaQueriesChecker.parseOneMediaQuery('@media not all and (min-width: 1000px) and (max-width: 600px) (1 rules) <file> @ 1:1').isForMobile.should.equal(false);
+        mediaQueriesChecker.parseOneMediaQuery('@media not all and (min-width: 400px) and (max-width: 180px) (1 rules) <file> @ 1:1').isForMobile.should.equal(true);
+    });
+
+    it('should count breakpoints correctly', function() {
+        mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 1024px) (1 rules) <file> @ 1:1').breakpoints.should.deep.equal([{string: '1024px', pixels: 1024}]);
+        mediaQueriesChecker.parseOneMediaQuery('@media (max-width:1024px) (1 rules) <file> @ 1:1').breakpoints.should.deep.equal([{string: '1024px', pixels: 1024}]);
+        mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 320px) (1 rules) <file> @ 1:1').breakpoints.should.deep.equal([{string: '320px', pixels: 320}]);
+        mediaQueriesChecker.parseOneMediaQuery('@media (min-width: 600px) and (max-width: 1000px) (1 rules) <file> @ 1:1').breakpoints.should.deep.equal([{string: '600px', pixels: 600}, {string: '1000px', pixels: 1000}]);
+        mediaQueriesChecker.parseOneMediaQuery('@media not all and (min-width: 180px) (1 rules) <file> @ 1:1').breakpoints.should.deep.equal([{string: '180px', pixels: 180}]);
+        mediaQueriesChecker.parseOneMediaQuery('@media not all and (min-width: 1000px) and (max-width: 600px) (1 rules) <file> @ 1:1').breakpoints.should.deep.equal([{string: '1000px', pixels: 1000}, {string: '600px', pixels: 600}]);
+        mediaQueriesChecker.parseOneMediaQuery('@media (max-height:500px) (1 rules) <file> @ 1:1').breakpoints.should.deep.equal([]);
+        mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 100em) (1 rules) <file> @ 1:1').breakpoints.should.deep.equal([{string: '100em', pixels: 1600}]);
+        mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 120pt) (1 rules) <file> @ 1:1').breakpoints.should.deep.equal([{string: '120pt', pixels: 160}]);
+        mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 40.2em) (1 rules) <file> @ 1:1').breakpoints.should.deep.equal([{string: '40.2em', pixels: 643.2}]);
+    });
+
+    it('should fail silently', function() {
+        mediaQueriesChecker.parseOneMediaQuery('@media bad syntax (1 rules) <file> @ 1:1').isForMobile.should.equal(false); 
+    });
+
+});