Two new media queries related rules

This commit is contained in:
Gaël Métais
2015-11-26 14:57:47 +01:00
parent 4f9589fb6b
commit 394d3a5b25
8 changed files with 338 additions and 2 deletions
+48
View File
@@ -48,6 +48,54 @@ ruleCtrl.controller('RuleCtrl', ['$scope', '$rootScope', '$routeParams', '$locat
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() {
+21
View File
@@ -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>
<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'">
<b>{{offender.error}}</b>
<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>
</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">
<h2>404</h2>
Rule "{{policyName}}"" not found
+38 -1
View File
@@ -464,7 +464,7 @@ var policies = {
"cssColors": {
"tool": "phantomas",
"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,
"isBadThreshold": 150,
"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 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": 600,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return {
count: offenders.length,
list: offenders
};
}
},
"cssImports": {
"tool": "phantomas",
"label": "Uses of @import",
+3 -1
View File
@@ -71,7 +71,9 @@
"cssComplexSelectors": 2,
"cssComplexSelectorsByAttribute": 1.5,
"cssColors": 0.5,
"similarColors": 0.5
"similarColors": 0.5,
"cssBreakpoints": 1,
"cssMobileFirst": 1
}
},
"badCSS": {
+4
View File
@@ -4,6 +4,7 @@ var debug = require('debug')('ylt:runner');
var phantomasWrapper = require('./tools/phantomas/phantomasWrapper');
var jsExecutionTransformer = require('./tools/jsExecutionTransformer');
var colorDiff = require('./tools/colorDiff');
var mediaQueriesChecker = require('./tools/mediaQueriesChecker');
var weightChecker = require('./tools/weightChecker/weightChecker');
var rulesChecker = require('./rulesChecker');
var scoreCalculator = require('./scoreCalculator');
@@ -32,6 +33,9 @@ var Runner = function(params) {
// Compare colors
data = colorDiff.compareAllColors(data);
// Check media queries
data = mediaQueriesChecker.analyzeMediaQueries(data);
// Redownload every file
return weightChecker.recheckAllFiles(data);
+168
View File
@@ -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();
+1
View File
@@ -34,6 +34,7 @@
"color-diff": "0.1.7",
"compression": "1.6.0",
"cors": "2.7.1",
"css-mq-parser": "0.0.3",
"debug": "2.2.0",
"express": "4.13.3",
"imagemin": "3.2.2",
+55
View File
@@ -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);
});
});