Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51e4265109 | ||
|
|
eaebb3adfa | ||
|
|
76b4ca6ea5 | ||
|
|
b8560c6e15 | ||
|
|
8eec2549a4 | ||
|
|
aaa1ad7eaa | ||
|
|
74c57fc11c | ||
|
|
b7e25c4e1c | ||
|
|
3f97333423 | ||
|
|
71152f64a5 | ||
|
|
531d125dcc | ||
|
|
299e79e890 | ||
|
|
14f60d8dd3 | ||
|
|
a00b1a96c7 | ||
|
|
2847e02c5d | ||
|
|
57b30a1838 | ||
|
|
f2a637ada7 | ||
|
|
b3804586dc | ||
|
|
113c1f0565 | ||
|
|
3948eeaad4 | ||
|
|
f843184970 | ||
|
|
2aa9edeb8f | ||
|
|
5e37cf8224 | ||
|
|
8bd3a1edde | ||
|
|
c68962e2f2 | ||
|
|
6a9a247a87 | ||
|
|
e238370bc8 | ||
|
|
178f2fd135 | ||
|
|
55cd1160db | ||
|
|
06bdbd929c | ||
|
|
100eb01f27 | ||
|
|
00ade4ad29 | ||
|
|
f3a5779102 | ||
|
|
6f0b97e760 | ||
|
|
28f9ed64ff | ||
|
|
b6924f70d4 | ||
|
|
79ecae34c8 | ||
|
|
f4843baa0e | ||
|
|
cd97a275fe | ||
|
|
f2e77a5c3a | ||
|
|
23f4f9bd0b | ||
|
|
e8eb6964db | ||
|
|
9643c5bfcd | ||
|
|
56ebe6bb8b | ||
|
|
5f7c2f07b8 | ||
|
|
bbe965e7a3 | ||
|
|
ebd03c7b1a | ||
|
|
0d02a57600 | ||
|
|
394d3a5b25 | ||
|
|
4f9589fb6b | ||
|
|
048e3396de | ||
|
|
e3d378a4ba | ||
|
|
91f8d9c2b0 | ||
|
|
2285c896d2 | ||
|
|
9ff24918e2 | ||
|
|
e78e794240 | ||
|
|
0eb359542b | ||
|
|
c54cf5a2ba | ||
|
|
6f7f05b456 | ||
|
|
da6a8834dd | ||
|
|
7ed16d63f3 | ||
|
|
d8730cc67b | ||
|
|
b67efe0cac | ||
|
|
835cb2f90f | ||
|
|
f75e99f703 |
+13
-4
@@ -1,9 +1,18 @@
|
||||
language: node_js
|
||||
sudo: false
|
||||
node_js:
|
||||
- "0.12.4"
|
||||
- "5.8"
|
||||
- "0.12"
|
||||
env:
|
||||
- CXX=g++-4.8
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
- g++-4.8
|
||||
before_install:
|
||||
- "npm install -g npm"
|
||||
- "npm install -g grunt-cli"
|
||||
- "npm install -g npm"
|
||||
- "npm install -g grunt-cli"
|
||||
install:
|
||||
- "npm install"
|
||||
- "npm install"
|
||||
@@ -1,6 +1,6 @@
|
||||
<img src="./doc/img/logo.png" alt="Yellow Lab Tools"></img>
|
||||
|
||||
Analyzes a webpage and detects **performance** or **front-end code quality** issues. Easy to launch, easy to understand, in-depth details.
|
||||
Analyzes a webpage and detects **performance** or **front-end code quality** issues. Free, easy to launch, easy to understand, in-depth details.
|
||||
|
||||
|
||||
#
|
||||
@@ -75,16 +75,10 @@ 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).
|
||||
|
||||
|
||||
## 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
|
||||
Gaël Métais. I'm a webperf freelance based in Paris. If you understand French, you can visit [my website](http://www.gaelmetais.com).
|
||||
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!
|
||||
|
||||
If you understand French, you can visit [my website](http://www.gaelmetais.com) (will be soon in English too).
|
||||
|
||||
|
||||
## Contributors
|
||||
|
||||
+3
-8
@@ -3,7 +3,7 @@
|
||||
var debug = require('debug')('ylt:cli');
|
||||
var meow = require('meow');
|
||||
var path = require('path');
|
||||
var jstoxml = require('jstoxml');
|
||||
var EasyXml = require('easyxml');
|
||||
|
||||
var ylt = require('../lib/index');
|
||||
|
||||
@@ -15,7 +15,6 @@ var cli = meow({
|
||||
'Options:',
|
||||
' --device Use "phone" or "tablet" to simulate a mobile device (by user-agent and viewport size).',
|
||||
' --screenshot Will take a screenshot and use this value as the output path. It needs to end with ".png".',
|
||||
' --js-deep-analysis When activated, the javascriptExecutionTree will contain sub-requests.',
|
||||
' --wait-for-selector Once the page is loaded, Phantomas will wait until the given CSS selector matches some elements.',
|
||||
' --cookie Adds a cookie on the main domain.',
|
||||
' --auth-user Basic HTTP authentication username.',
|
||||
@@ -51,11 +50,6 @@ if (screenshot) {
|
||||
options.screenshot = cli.flags.screenshot;
|
||||
}
|
||||
|
||||
// Deep JS analysis option
|
||||
if (cli.flags.jsDeepAnalysis === true || cli.flags.jsDeepAnalysis === 'true') {
|
||||
options.jsDeepAnalysis = true;
|
||||
}
|
||||
|
||||
// Device simulation
|
||||
options.device = cli.flags.device || 'desktop';
|
||||
|
||||
@@ -86,7 +80,8 @@ if (cli.flags.reporter && cli.flags.reporter !== 'json' && cli.flags.reporter !=
|
||||
debug('Success');
|
||||
switch(cli.flags.reporter) {
|
||||
case 'xml':
|
||||
console.log(jstoxml.toXML(data, {indent: ' '}));
|
||||
var serializer = new EasyXml();
|
||||
console.log(serializer.render(data));
|
||||
break;
|
||||
default:
|
||||
console.log(JSON.stringify(data, null, 2));
|
||||
|
||||
@@ -34,7 +34,8 @@
|
||||
font-size: 1em;
|
||||
}
|
||||
.settings input[type=text],
|
||||
.settings input[type=password] {
|
||||
.settings input[type=password],
|
||||
.settings textarea {
|
||||
width: 100%;
|
||||
min-width: 4em;
|
||||
}
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
margin: 0.2em 0;
|
||||
border-radius: 0.4em;
|
||||
z-index: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
.offendersTable .offenderButton.opens,
|
||||
.value .offenderButton.opens {
|
||||
@@ -157,6 +158,10 @@
|
||||
display: block;
|
||||
background: #ffe0cc;
|
||||
}
|
||||
.offendersTable .smallerOffenders,
|
||||
.value .smallerOffenders {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.offendersHtml {
|
||||
display: inline-block;
|
||||
}
|
||||
@@ -174,7 +179,7 @@
|
||||
.checker {
|
||||
/* Checkerboard background */
|
||||
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-position: 0 0, 0.5em 0.5em;
|
||||
}
|
||||
|
||||
+15
-15
@@ -62,34 +62,34 @@
|
||||
background: #F04DA7;
|
||||
}
|
||||
.timeline .domComplete.interval {
|
||||
background: #ede3ff;
|
||||
background: #EDE3FF;
|
||||
}
|
||||
.timeline .domComplete .color {
|
||||
background: #c2a3ff;
|
||||
background: #C2A3FF;
|
||||
}
|
||||
.timeline .domContentLoadedEnd.interval {
|
||||
background: #d8f0f0;
|
||||
background: #D8F0F0;
|
||||
}
|
||||
.timeline .domContentLoadedEnd .color {
|
||||
background: #7ecccc;
|
||||
background: #7ECCCC;
|
||||
}
|
||||
.timeline .domContentLoaded.interval {
|
||||
background: #e0ffd1;
|
||||
background: #E0FFD1;
|
||||
}
|
||||
.timeline .domContentLoaded .color {
|
||||
background: #a7e846;
|
||||
background: #A7E846;
|
||||
}
|
||||
.timeline .domInteractive.interval {
|
||||
background: #fffccc;
|
||||
background: #FFFCCC;
|
||||
}
|
||||
.timeline .domInteractive .color {
|
||||
background: #ffe433;
|
||||
background: #FFE433;
|
||||
}
|
||||
.timeline .domCreation.interval {
|
||||
background: #ffe0cc;
|
||||
background: #FFE0CC;
|
||||
}
|
||||
.timeline .domCreation .color {
|
||||
background: #ff6600;
|
||||
background: #FF6600;
|
||||
}
|
||||
.timeline .tooltip.detailsOverlay {
|
||||
position: absolute;
|
||||
@@ -294,19 +294,19 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
.table > div > .startTime.domComplete {
|
||||
background: #ede3ff;
|
||||
background: #EDE3FF;
|
||||
}
|
||||
.table > div > .startTime.domContentLoadedEnd {
|
||||
background: #d8f0f0;
|
||||
background: #D8F0F0;
|
||||
}
|
||||
.table > div > .startTime.domContentLoaded {
|
||||
background: #e0ffd1;
|
||||
background: #E0FFD1;
|
||||
}
|
||||
.table > div > .startTime.domInteractive {
|
||||
background: #fffccc;
|
||||
background: #FFFCCC;
|
||||
}
|
||||
.table > div > .startTime.domCreation {
|
||||
background: #ffe0cc;
|
||||
background: #FFE0CC;
|
||||
}
|
||||
.execution .icon-warning {
|
||||
color: #e74c3c;
|
||||
|
||||
@@ -48,6 +48,55 @@ 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.breakpointsSeries = ['Number of CSS rules per breakpoint'];
|
||||
$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() {
|
||||
|
||||
@@ -9,25 +9,26 @@ apiService.factory('API', ['$location', 'Runs', 'Results', function($location, R
|
||||
url: url,
|
||||
waitForResponse: false,
|
||||
screenshot: true,
|
||||
jsTimeline: true,
|
||||
device: settings.device,
|
||||
waitForSelector: settings.waitForSelector,
|
||||
cookie: settings.cookie,
|
||||
authUser: settings.authUser,
|
||||
authPass: settings.authPass
|
||||
authPass: settings.authPass,
|
||||
blockDomain: settings.blockDomain,
|
||||
allowedDomains: settings.allowedDomains,
|
||||
noExternals: settings.noExternals
|
||||
};
|
||||
|
||||
if (settings.waitForSelector && settings.waitForSelector !== '') {
|
||||
runObject.waitForSelector = settings.waitForSelector;
|
||||
}
|
||||
|
||||
if (settings.cookie && settings.cookie !== '') {
|
||||
runObject.cookie = settings.cookie;
|
||||
}
|
||||
|
||||
if (settings.authUser && settings.authUser !== '' && settings.authPass && settings.authPass !== '') {
|
||||
runObject.authUser = settings.authUser;
|
||||
runObject.authPass = settings.authPass;
|
||||
|
||||
if (settings.domainsBlackOrWhite === 'black') {
|
||||
runObject.blockDomain = this.parseDomains(settings.domains);
|
||||
} else if (settings.domainsBlackOrWhite === 'white') {
|
||||
var allowedDomains = this.parseDomains(settings.domains);
|
||||
if (allowedDomains.length > 0) {
|
||||
runObject.allowDomain = allowedDomains;
|
||||
} else {
|
||||
runObject.noExternals = true;
|
||||
}
|
||||
}
|
||||
|
||||
Runs.save(runObject, function(data) {
|
||||
@@ -43,6 +44,17 @@ apiService.factory('API', ['$location', 'Runs', 'Results', function($location, R
|
||||
|
||||
relaunchTest: function(result) {
|
||||
this.launchTest(result.params.url, result.params.options);
|
||||
},
|
||||
|
||||
parseDomains: function(textareaContent) {
|
||||
var lines = textareaContent.split('\n');
|
||||
|
||||
function removeEmptyLines (line) {
|
||||
return line.trim() !== '';
|
||||
}
|
||||
|
||||
// Remove empty lines
|
||||
return lines.filter(removeEmptyLines).join(',');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
input[type=text], input[type=password] {
|
||||
input[type=text], input[type=password], textarea {
|
||||
width: 100%;
|
||||
min-width: 4em;
|
||||
}
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
margin: 0.2em 0;
|
||||
border-radius: 0.4em;
|
||||
z-index: 1;
|
||||
cursor: pointer;
|
||||
|
||||
&.opens {
|
||||
padding-right: 0.75em;
|
||||
@@ -166,6 +167,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.smallerOffenders {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
|
||||
.offendersHtml {
|
||||
|
||||
+18
-10
@@ -19,6 +19,24 @@
|
||||
<link rel="stylesheet" type="text/css" href="/node_modules/angular-chart.js/dist/angular-chart.css">
|
||||
<!-- endbuild -->
|
||||
|
||||
<link rel="preconnect" href="//www.google-analytics.com">
|
||||
<link rel="dns-prefetch" href="//www.google-analytics.com">
|
||||
<link rel="preconnect" href="//ghbtns.com">
|
||||
<link rel="dns-prefetch" href="//ghbtns.com">
|
||||
<link rel="preconnect" href="https://api.github.com">
|
||||
<link rel="dns-prefetch" href="https://api.github.com">
|
||||
|
||||
<head>
|
||||
|
||||
<body ng-app="YellowLabTools">
|
||||
<div id="header"><h1>Yellow Lab <span class="icon-lab"></span> Tools</h1></div>
|
||||
<div id="body" ng-view autoscroll="true"></div>
|
||||
<div class="footer">
|
||||
<span class="version">@@version</span>
|
||||
<br><a href="/about">More about Yellow Lab Tools</a><br>
|
||||
<div class="github"><iframe id="ghbtn" frameborder="0" scrolling="0" width="145px" height="30px"></iframe></div>
|
||||
</div>
|
||||
|
||||
<!-- build:js /js/all.js -->
|
||||
<script src="/node_modules/angular/angular.min.js"></script>
|
||||
<script src="/node_modules/chart.js/Chart.min.js"></script>
|
||||
@@ -44,16 +62,6 @@
|
||||
<script src="/js/directives/gradeDirective.js"></script>
|
||||
<script src="/js/directives/offendersDirectives.js"></script>
|
||||
<!-- endbuild -->
|
||||
<head>
|
||||
|
||||
<body ng-app="YellowLabTools">
|
||||
<div id="header"><h1>Yellow Lab <span class="icon-lab"></span> Tools</h1></div>
|
||||
<div id="body" ng-view autoscroll="true"></div>
|
||||
<div class="footer">
|
||||
<span class="version">@@version</span>
|
||||
<br><a href="/about">More about Yellow Lab Tools</a><br>
|
||||
<div class="github"><iframe id="ghbtn" frameborder="0" scrolling="0" width="145px" height="30px"></iframe></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','//www.google-analytics.com/analytics.js','ga');if('@@googleAnalyticsId'.indexOf('UA-')===0){ga('create','@@googleAnalyticsId','auto');}
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
<div ng-include="'views/resultSubHeader.html'"></div>
|
||||
<div class="summary board">
|
||||
|
||||
<div ng-if="result.blockedRequests">
|
||||
<b><ng-pluralize count="result.blockedRequests.length" when="{'0': 'No blocked request', 'one': '1 blocked request', 'other': '{} blocked requests'}"></ng-pluralize>:</b>
|
||||
<div ng-repeat="request in result.blockedRequests">
|
||||
{{request}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="globalScore" ng-if="globalScore === 0 || globalScore > 0">
|
||||
<div>
|
||||
<h2>Global score</h2>
|
||||
|
||||
@@ -61,15 +61,22 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--<div>
|
||||
<div class="label">Blocked domains</div>
|
||||
<div>
|
||||
<div class="label">
|
||||
Block domains
|
||||
<span class="settingsTooltip">
|
||||
<span class="icon-question"></span>
|
||||
<div><b>Block some domains</b><br><br>One line per domain or subdomain.<br><br><i><b>Example:</b><br>google-analytics.com<br>ads.yahoo.com<br>ajax.googleapis.com</i><br><br>An empty whitelist will block all domains except the main domain.</div>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
Blacklist / Whitelist
|
||||
<input type="radio" name="blackOrWhite" ng-model="settings.domainsBlackOrWhite" value="black" />blacklist
|
||||
<input type="radio" name="blackOrWhite" ng-model="settings.domainsBlackOrWhite" value="white" />whitelist
|
||||
</div>
|
||||
<textarea name=""></textarea>
|
||||
<textarea name="domains" ng-model="settings.domains" rows="5"></textarea>
|
||||
</div>
|
||||
</div>-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -96,10 +96,21 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'cssRules'">
|
||||
<span ng-if="offender.file === 'inline CSS'">inline CSS</span>
|
||||
<span ng-if="offender.file !== 'inline CSS'"><url-link url="offender.file" max-length="80"></url-link></span>
|
||||
: <ng-pluralize count="offender.rules" when="{'0': '0 rule', 'one':'1 rule','other':'{} rules'}"></ng-pluralize>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'similarColors'">
|
||||
<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>
|
||||
@@ -143,10 +154,18 @@
|
||||
</div>
|
||||
|
||||
<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 ng-if="policyName === 'lazyLoadableImagesBelowTheFold' || policyName === 'hiddenImages'">
|
||||
@@ -340,6 +359,34 @@
|
||||
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" chart-series="breakpointsSeries" 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="policyName === 'http2'">
|
||||
<h3>Protocols advertised by the server</h3>
|
||||
<div class="offendersTable">
|
||||
<div ng-repeat="protocol in rule.offendersObj.list">
|
||||
<div>{{protocol}}</div>
|
||||
</div>
|
||||
<div ng-if="!rule.offendersObj || rule.offendersObj.count == 0">
|
||||
<div>none</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="!rule && rule !== null" class="notFound">
|
||||
<h2>404</h2>
|
||||
Rule "{{policyName}}"" not found
|
||||
|
||||
+159
-39
@@ -267,7 +267,7 @@ var policies = {
|
||||
},
|
||||
"jQueryVersion": {
|
||||
"label": "jQuery version",
|
||||
"message": "<p>Current latest versions of jQuery are 1.11 (with support for old IE versions) and 2.1 (without).</p><p>Each new version of jQuery optimizes performances. Do not keep an old version of jQuery. Updating can sometimes break a few things, but it is generally quite easy to fix them up. So don't hesitate.</p>",
|
||||
"message": "<p>Current latest versions of jQuery are 1.12 (with support for old IE versions) and 2.2 (without).</p><p>Each new version of jQuery optimizes performances. Do not keep an old version of jQuery. Updating can sometimes break a few things, but it is generally quite easy to fix them up. So don't hesitate.</p>",
|
||||
"hasOffenders": false,
|
||||
"scoreFn": function(data) {
|
||||
var differentVersions = data.toolsResults.phantomas.metrics.jQueryVersionsLoaded;
|
||||
@@ -279,27 +279,31 @@ var policies = {
|
||||
var value = data.toolsResults.phantomas.metrics.jQueryVersion;
|
||||
var score;
|
||||
|
||||
if (value.indexOf('1.11.') === 0 ||
|
||||
value.indexOf('1.12.') === 0 ||
|
||||
value.indexOf('2.1.') === 0 ||
|
||||
if (value.indexOf('1.12.') === 0 ||
|
||||
value.indexOf('2.2.') === 0 ||
|
||||
value.indexOf('3.0.') === 0) {
|
||||
value.indexOf('1.13.') === 0 ||
|
||||
value.indexOf('2.3.') === 0 ||
|
||||
value.indexOf('3.0.') === 0 ||
|
||||
value.indexOf('3.1.') === 0) {
|
||||
score = 100;
|
||||
} else if (value.indexOf('1.11.') === 0 ||
|
||||
value.indexOf('2.1.') === 0) {
|
||||
score = 90;
|
||||
} else if (value.indexOf('1.10.') === 0 ||
|
||||
value.indexOf('2.0.') === 0) {
|
||||
score = 90;
|
||||
} else if (value.indexOf('1.9.') === 0) {
|
||||
score = 70;
|
||||
} else if (value.indexOf('1.8.') === 0) {
|
||||
} else if (value.indexOf('1.9.') === 0) {
|
||||
score = 50;
|
||||
} else if (value.indexOf('1.7') === 0) {
|
||||
} else if (value.indexOf('1.8.') === 0) {
|
||||
score = 40;
|
||||
} else if (value.indexOf('1.6') === 0) {
|
||||
} else if (value.indexOf('1.7') === 0) {
|
||||
score = 30;
|
||||
} else if (value.indexOf('1.5') === 0) {
|
||||
} else if (value.indexOf('1.6') === 0) {
|
||||
score = 20;
|
||||
} else if (value.indexOf('1.4') === 0) {
|
||||
} else if (value.indexOf('1.5') === 0) {
|
||||
score = 10;
|
||||
} else if (value.indexOf('1.4') === 0) {
|
||||
score = 0;
|
||||
} else if (value.indexOf('1.3') === 0) {
|
||||
score = 0;
|
||||
} else if (value.indexOf('1.2') === 0) {
|
||||
@@ -420,7 +424,40 @@ var policies = {
|
||||
"isOkThreshold": 750,
|
||||
"isBadThreshold": 3000,
|
||||
"isAbnormalThreshold": 4500,
|
||||
"hasOffenders": false
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
var hasInline = false;
|
||||
var inlineCount = 0;
|
||||
var files = [];
|
||||
|
||||
offenders.forEach(function(line) {
|
||||
if (line.indexOf('[inline CSS]: ') === 0) {
|
||||
hasInline = true;
|
||||
inlineCount += parseInt(line.substr(14));
|
||||
} else {
|
||||
var parts = /^<(.*)>: (\d+)$/.exec(line);
|
||||
|
||||
if (parts) {
|
||||
files.push({
|
||||
file: parts[1],
|
||||
rules: parseInt(parts[2], 10)
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (hasInline) {
|
||||
files.push({
|
||||
file: 'inline CSS',
|
||||
rules: inlineCount
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
count: files.length,
|
||||
list: files
|
||||
};
|
||||
}
|
||||
},
|
||||
"cssComplexSelectors": {
|
||||
"tool": "phantomas",
|
||||
@@ -464,7 +501,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 +559,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": {
|
||||
"tool": "phantomas",
|
||||
"label": "Uses of @import",
|
||||
@@ -752,36 +826,53 @@ var policies = {
|
||||
"cssOldPropertyPrefixes": {
|
||||
"tool": "phantomas",
|
||||
"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,
|
||||
"isBadThreshold": 75,
|
||||
"isAbnormalThreshold": 300,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
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 propertyName = parts[2];
|
||||
|
||||
if (!properties[propertyName]) {
|
||||
properties[propertyName] = {
|
||||
property: propertyName,
|
||||
message: parts[4],
|
||||
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: offenders.map(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
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
rule: parts[1],
|
||||
property: parts[2],
|
||||
value: parts[3],
|
||||
message: parts[4],
|
||||
file: splittedOffender.file,
|
||||
line: splittedOffender.line,
|
||||
column: splittedOffender.column
|
||||
};
|
||||
})
|
||||
list: list
|
||||
};
|
||||
}
|
||||
},
|
||||
@@ -871,7 +962,7 @@ var policies = {
|
||||
"imageOptimization": {
|
||||
"tool": "weightChecker",
|
||||
"label": "Image optimization",
|
||||
"message": "<p>This metric mesures the number of bytes that could be saved by optimizing images.</p><p>Image optimization is generally one of the easiest way to reduce a page weight, and as a result, the page load time. Don't use Photoshop or other image editing tools, they're not very good for optimization. Use specialized tools such as <a href=\"https://kraken.io/\" target=\"_blank\">Kraken.io</a> or the excellent <a href=\"https://imageoptim.com/\" target=\"_blank\">ImageOptim</a> on Mac. For SVG images, you can use <a href=\"https://jakearchibald.github.io/svgomg/\" target=\"_blank\">SVGOMG</a></p><p>The tools in use in YellowLabTools are not set to their maximum optimization power (JPEG quality 85), so you might be able to compress even more!</p><p>Please note that Yellow Lab Tools' engine (PhantomJS) is not compatible with image srcset (unless you use a polyfill). This can lead to incorrect page weight.</p>",
|
||||
"message": "<p>This metric measures the number of bytes that could be saved by optimizing images.</p><p>Image optimization is generally one of the easiest way to reduce a page weight, and as a result, the page load time. Don't use Photoshop or other image editing tools, they're not very good for optimization. Use specialized tools such as <a href=\"https://kraken.io/\" target=\"_blank\">Kraken.io</a> or the excellent <a href=\"https://imageoptim.com/\" target=\"_blank\">ImageOptim</a> on Mac. For SVG images, you can use <a href=\"https://jakearchibald.github.io/svgomg/\" target=\"_blank\">SVGOMG</a></p><p>The tools in use in YellowLabTools are not set to their maximum optimization power (JPEG quality 85), so you might be able to compress even more!</p><p>Please note that Yellow Lab Tools' engine (PhantomJS) is not compatible with image srcset (unless you use a polyfill). This can lead to incorrect page weight.</p>",
|
||||
"isOkThreshold": 10240,
|
||||
"isBadThreshold": 122880,
|
||||
"isAbnormalThreshold": 307200,
|
||||
@@ -881,7 +972,7 @@ var policies = {
|
||||
"gzipCompression": {
|
||||
"tool": "weightChecker",
|
||||
"label": "Gzip compression",
|
||||
"message": "<p>Mesures the number of bytes that could be saved by compressing file transfers.</p><p>Gzip is a powerfull weight reducer and should be enabled on text-based assets in your server's configuration. Note that gzipping small files (< 1 KB) is arguable, and that some assets such as images should not be gzipped as they are already compressed. <a href=\"https://gist.github.com/gmetais/971ce13a1fbeebd88445\" target=\"_blank\">Here</a> is a list of Content-Types that should be gzipped.</p>",
|
||||
"message": "<p>Measures the number of bytes that could be saved by compressing file transfers.</p><p>Gzip is a powerfull weight reducer and should be enabled on text-based assets in your server's configuration. Note that gzipping small files (< 1 KB) is arguable, and that some assets such as images should not be gzipped as they are already compressed. <a href=\"https://gist.github.com/gmetais/971ce13a1fbeebd88445\" target=\"_blank\">Here</a> is a list of Content-Types that should be gzipped.</p>",
|
||||
"isOkThreshold": 5125,
|
||||
"isBadThreshold": 81920,
|
||||
"isAbnormalThreshold": 153600,
|
||||
@@ -990,6 +1081,35 @@ var policies = {
|
||||
"isAbnormalThreshold": 30,
|
||||
"hasOffenders": true
|
||||
},
|
||||
"http2": {
|
||||
"label": "HTTP/2 or SPDY",
|
||||
"message": "<p>HTTP/2 is the latest version of the HTTP protocol and is designed to optimize load speed. SPDY is deprecated but still very well supported.</p><p>The latest versions of all major browsers are now compatible. The difficulty is on the server side, where technologies are not quite ready yet.</p>",
|
||||
"hasOffenders": true,
|
||||
"scoreFn": function(data) {
|
||||
if (!data.toolsResults.http2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var isHttp2 = data.toolsResults.http2.metrics.http2;
|
||||
|
||||
var result = {
|
||||
value: isHttp2 ? 'Yes' : 'No',
|
||||
score: isHttp2 ? 100 : 0,
|
||||
bad: !isHttp2,
|
||||
abnormal: false,
|
||||
abnormalityScore: 0
|
||||
};
|
||||
|
||||
if (data.toolsResults.http2.offenders) {
|
||||
result.offendersObj = {
|
||||
count: data.toolsResults.http2.offenders.http2.length,
|
||||
list: data.toolsResults.http2.offenders.http2
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
},
|
||||
"cachingDisabled": {
|
||||
"tool": "phantomas",
|
||||
"label": "Caching disabled",
|
||||
|
||||
@@ -71,7 +71,9 @@
|
||||
"cssComplexSelectors": 2,
|
||||
"cssComplexSelectorsByAttribute": 1.5,
|
||||
"cssColors": 0.5,
|
||||
"similarColors": 0.5
|
||||
"similarColors": 0.5,
|
||||
"cssBreakpoints": 1,
|
||||
"cssMobileFirst": 1
|
||||
}
|
||||
},
|
||||
"badCSS": {
|
||||
@@ -94,6 +96,7 @@
|
||||
"serverConfig": {
|
||||
"label": "Server config",
|
||||
"policies": {
|
||||
"http2": 2,
|
||||
"closedConnections": 2,
|
||||
"cachingNotSpecified": 1,
|
||||
"cachingDisabled": 1,
|
||||
|
||||
+16
-1
@@ -4,6 +4,8 @@ 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 isHttp2 = require('./tools/isHttp2');
|
||||
var weightChecker = require('./tools/weightChecker/weightChecker');
|
||||
var rulesChecker = require('./rulesChecker');
|
||||
var scoreCalculator = require('./scoreCalculator');
|
||||
@@ -32,11 +34,19 @@ var Runner = function(params) {
|
||||
// Compare colors
|
||||
data = colorDiff.compareAllColors(data);
|
||||
|
||||
// Check media queries
|
||||
data = mediaQueriesChecker.analyzeMediaQueries(data);
|
||||
|
||||
// Redownload every file
|
||||
return weightChecker.recheckAllFiles(data);
|
||||
|
||||
})
|
||||
|
||||
.then(function(data) {
|
||||
// Check if HTTP2
|
||||
return isHttp2.check(data);
|
||||
})
|
||||
|
||||
.then(function(data) {
|
||||
|
||||
// Rules checker
|
||||
@@ -56,9 +66,14 @@ var Runner = function(params) {
|
||||
delete data.toolsResults.phantomas.metrics.scrollExecutionTree;
|
||||
delete data.toolsResults.phantomas.offenders.scrollExecutionTree;
|
||||
|
||||
|
||||
if (data.toolsResults.phantomas.offenders.blockedRequests) {
|
||||
data.blockedRequests = data.toolsResults.phantomas.offenders.blockedRequests;
|
||||
}
|
||||
|
||||
|
||||
// Finished!
|
||||
deferred.resolve(data);
|
||||
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
|
||||
@@ -34,12 +34,14 @@ var ApiController = function(app) {
|
||||
waitForResponse: req.body.waitForResponse !== false && req.body.waitForResponse !== 'false' && req.body.waitForResponse !== 0,
|
||||
partialResult: req.body.partialResult || null,
|
||||
screenshot: req.body.screenshot || false,
|
||||
jsTimeline: req.body.jsTimeline || false,
|
||||
device: req.body.device || 'desktop',
|
||||
waitForSelector: req.body.waitForSelector || null,
|
||||
cookie: req.body.cookie || null,
|
||||
authUser: req.body.authUser || null,
|
||||
authPass: req.body.authPass || null
|
||||
authPass: req.body.authPass || null,
|
||||
blockDomain: req.body.blockDomain || null,
|
||||
allowDomain: req.body.allowDomain || null,
|
||||
noExternals: req.body.noExternals || false
|
||||
}
|
||||
};
|
||||
|
||||
@@ -71,12 +73,14 @@ var ApiController = function(app) {
|
||||
|
||||
var runOptions = {
|
||||
screenshot: run.params.screenshot ? screenshot.getTmpFilePath() : false,
|
||||
jsDeepAnalysis: run.params.jsTimeline,
|
||||
device: run.params.device,
|
||||
waitForSelector: run.params.waitForSelector,
|
||||
cookie: run.params.cookie,
|
||||
authUser: run.params.authUser,
|
||||
authPass: run.params.authPass,
|
||||
blockDomain: run.params.blockDomain,
|
||||
allowDomain: run.params.allowDomain,
|
||||
noExternals: run.params.noExternals,
|
||||
phantomasEngine: serverSettings.phantomasEngine
|
||||
};
|
||||
|
||||
@@ -96,7 +100,7 @@ var ApiController = function(app) {
|
||||
if (run.params.screenshot) {
|
||||
|
||||
// Replace the empty promise created earlier with Q.resolve()
|
||||
screenshotPromise = screenshot.toThumbnail(400)
|
||||
screenshotPromise = screenshot.toThumbnail(serverSettings.screenshotWidth || 400)
|
||||
|
||||
// Read screenshot
|
||||
.then(function(screenshotBuffer) {
|
||||
@@ -129,13 +133,7 @@ var ApiController = function(app) {
|
||||
// Remove uneeded temp screenshot path
|
||||
delete data.params.options.screenshot;
|
||||
|
||||
// Empty javascriptExecutionTree if not needed
|
||||
if (!run.params.jsTimeline) {
|
||||
data.javascriptExecutionTree = {};
|
||||
data.scrollExecutionTree = {};
|
||||
}
|
||||
|
||||
// Remove tools results if not needed
|
||||
// Here we can remove tools results if not needed
|
||||
|
||||
return resultsDatastore.saveResult(data);
|
||||
})
|
||||
|
||||
@@ -46,7 +46,9 @@ var colorDiff = function() {
|
||||
var deduplicatedColors = {};
|
||||
|
||||
parsedOffenders.forEach(function(color) {
|
||||
deduplicatedColors[color] = color;
|
||||
if (color !== null) {
|
||||
deduplicatedColors[color] = color;
|
||||
}
|
||||
});
|
||||
|
||||
return Object.keys(deduplicatedColors).map(this.parseColor);
|
||||
@@ -54,7 +56,7 @@ var colorDiff = function() {
|
||||
|
||||
this.parseOffender = function(offender) {
|
||||
var regexResult = /^(.*) \(\d+ times\)$/.exec(offender);
|
||||
return regexResult[1];
|
||||
return regexResult ? regexResult[1] : null;
|
||||
};
|
||||
|
||||
this.parseColor = function(color) {
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
var debug = require('debug')('ylt:isHttp2');
|
||||
var url = require('url');
|
||||
var Q = require('q');
|
||||
var http2 = require('is-http2');
|
||||
|
||||
var isHttp2 = function() {
|
||||
'use strict';
|
||||
|
||||
this.check = function(data) {
|
||||
debug('Starting to check for HTTP2 support...');
|
||||
|
||||
return this.checkHttp2(data)
|
||||
|
||||
.then(function(result) {
|
||||
|
||||
if (result.isHttp) {
|
||||
debug('The website is not even in HTTPS');
|
||||
|
||||
data.toolsResults.http2 = {
|
||||
metrics: {
|
||||
http2: false
|
||||
}
|
||||
};
|
||||
|
||||
} else if (result.isHttp2) {
|
||||
debug('HTTP/2 (or SPDY) is supported');
|
||||
|
||||
data.toolsResults.http2 = {
|
||||
metrics: {
|
||||
http2: true
|
||||
}
|
||||
};
|
||||
|
||||
} else {
|
||||
debug('HTTP/2 is not supported');
|
||||
|
||||
data.toolsResults.http2 = {
|
||||
metrics: {
|
||||
http2: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Add the supported protocols as offenders
|
||||
if (result.supportedProtocols) {
|
||||
debug('Supported protocols: ' + result.supportedProtocols.join(' '));
|
||||
data.toolsResults.http2.offenders = {
|
||||
http2: result.supportedProtocols
|
||||
};
|
||||
}
|
||||
|
||||
debug('End of HTTP2 support check');
|
||||
|
||||
return data;
|
||||
})
|
||||
|
||||
.fail(function() {
|
||||
return data;
|
||||
});
|
||||
};
|
||||
|
||||
this.getParsedUrl = function(data) {
|
||||
return url.parse(data.toolsResults.phantomas.url);
|
||||
};
|
||||
|
||||
this.getProtocol = function(data) {
|
||||
return this.getParsedUrl(data).protocol;
|
||||
};
|
||||
|
||||
this.getDomain = function(data) {
|
||||
return this.getParsedUrl(data).hostname;
|
||||
};
|
||||
|
||||
this.checkHttp2 = function(data) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
// Check if it's HTTPS first
|
||||
if (this.getProtocol(data) === 'http:') {
|
||||
|
||||
deferred.resolve({
|
||||
isHttp: true
|
||||
});
|
||||
|
||||
} else {
|
||||
|
||||
// To make is-http2 work, you need to have openssl in a version greater than 1.0.0 installed and available in your $path.
|
||||
http2(this.getDomain(data), {includeSpdy: true})
|
||||
|
||||
.then(function(result) {
|
||||
deferred.resolve(result);
|
||||
})
|
||||
|
||||
.catch(function(error) {
|
||||
debug('Error while checking HTTP2 support:');
|
||||
debug(error);
|
||||
deferred.reject('Error while checking for HTTP2 support');
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = new isHttp2();
|
||||
@@ -121,24 +121,28 @@ var jsExecutionTransformer = function() {
|
||||
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) {
|
||||
throw err;
|
||||
|
||||
@@ -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();
|
||||
@@ -18,8 +18,8 @@ exports.module = function(phantomas) {
|
||||
responseEndTime = Date.now();
|
||||
});
|
||||
|
||||
phantomas.once('init', function() {
|
||||
phantomas.evaluate(function(responseEndTime, deepAnalysis) {
|
||||
phantomas.on('init', function() {
|
||||
phantomas.evaluate(function(responseEndTime) {
|
||||
(function(phantomas) {
|
||||
|
||||
// Overwritting phantomas spy function
|
||||
@@ -106,48 +106,85 @@ exports.module = function(phantomas) {
|
||||
var currentContext = root;
|
||||
var depth = 0;
|
||||
|
||||
if (deepAnalysis) {
|
||||
phantomas.log('Entering deep Javascript analysis mode');
|
||||
}
|
||||
|
||||
// Add a child but don't enter his context
|
||||
// Add a child but don't enter its context
|
||||
function pushContext(data) {
|
||||
if (depth === 0 || deepAnalysis) {
|
||||
|
||||
// Some data is not needed on subchildren
|
||||
if (depth === 0) {
|
||||
data.timestamp = Date.now() - responseEndTime;
|
||||
data.loadingStep = phantomas.currentStep || '';
|
||||
currentContext.addChild(data);
|
||||
}
|
||||
|
||||
// Some data is not needed on subchildren
|
||||
if (depth > 0) {
|
||||
if (data.backtrace) {
|
||||
delete data.backtrace;
|
||||
}
|
||||
if (data.resultsNumber) {
|
||||
delete data.resultsNumber;
|
||||
}
|
||||
}
|
||||
|
||||
currentContext.addChild(data);
|
||||
}
|
||||
|
||||
// Add a child to the current context and enter his context
|
||||
// Add a child to the current context and enter its context
|
||||
function enterContext(data) {
|
||||
if (depth === 0 || deepAnalysis) {
|
||||
|
||||
// Some data is not needed on subchildren
|
||||
if (depth === 0) {
|
||||
data.timestamp = Date.now() - responseEndTime;
|
||||
data.loadingStep = phantomas.currentStep || '';
|
||||
currentContext = currentContext.addChild(data);
|
||||
}
|
||||
|
||||
// Some data is not needed on subchildren
|
||||
if (depth > 0) {
|
||||
if (data.backtrace) {
|
||||
delete data.backtrace;
|
||||
}
|
||||
if (data.resultsNumber) {
|
||||
delete data.resultsNumber;
|
||||
}
|
||||
}
|
||||
|
||||
currentContext = currentContext.addChild(data);
|
||||
|
||||
depth ++;
|
||||
}
|
||||
|
||||
// Save given data in the current context and jump change current context to its parent
|
||||
function leaveContext(moreData) {
|
||||
if (depth === 1 || deepAnalysis) {
|
||||
|
||||
// Some data is not needed on subchildren
|
||||
if (depth === 1) {
|
||||
currentContext.data.time = Date.now() - currentContext.data.timestamp - responseEndTime;
|
||||
}
|
||||
|
||||
// Merge previous data with moreData (ovewrites if exists)
|
||||
if (moreData) {
|
||||
for (var key in moreData) {
|
||||
currentContext.data[key] = moreData[key];
|
||||
}
|
||||
// Some data is not needed on subchildren
|
||||
if (depth > 1) {
|
||||
if (moreData && moreData.backtrace) {
|
||||
delete moreData.backtrace;
|
||||
}
|
||||
|
||||
var parent = currentContext.parent;
|
||||
if (parent === null) {
|
||||
console.error('Error: trying to close root context in ContextTree');
|
||||
} else {
|
||||
currentContext = parent;
|
||||
if (moreData && moreData.resultsNumber) {
|
||||
delete moreData.resultsNumber;
|
||||
}
|
||||
}
|
||||
|
||||
// Merge previous data with moreData (ovewrites if exists)
|
||||
if (moreData) {
|
||||
for (var key in moreData) {
|
||||
currentContext.data[key] = moreData[key];
|
||||
}
|
||||
}
|
||||
|
||||
var parent = currentContext.parent;
|
||||
if (parent === null) {
|
||||
console.error('Error: trying to close root context in ContextTree');
|
||||
} else {
|
||||
currentContext = parent;
|
||||
}
|
||||
|
||||
depth --;
|
||||
}
|
||||
|
||||
@@ -161,7 +198,6 @@ exports.module = function(phantomas) {
|
||||
if (root !== currentContext) {
|
||||
return null;
|
||||
}
|
||||
var current = currentContext;
|
||||
|
||||
function recusiveRead(node) {
|
||||
if (node.children.length === 0) {
|
||||
@@ -209,6 +245,6 @@ exports.module = function(phantomas) {
|
||||
})();
|
||||
|
||||
})(window.__phantomas);
|
||||
}, responseEndTime, phantomas.getParam('js-deep-analysis'));
|
||||
}, responseEndTime);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ exports.module = function(phantomas) {
|
||||
phantomas.setMetric('DOMqueriesAvoidable'); // @desc number of repeated uses of a duplicated query
|
||||
|
||||
// fake native DOM functions
|
||||
phantomas.once('init', function() {
|
||||
phantomas.on('init', function() {
|
||||
phantomas.evaluate(function() {
|
||||
(function(phantomas) {
|
||||
function querySpy(type, query, fnName, context, hasNoResults) {
|
||||
|
||||
@@ -12,7 +12,7 @@ exports.module = function(phantomas) {
|
||||
phantomas.setMetric('eventsDispatched'); // @desc number of EventTarget.dispatchEvent calls
|
||||
phantomas.setMetric('eventsScrollBound'); // @desc number of scroll event bounds
|
||||
|
||||
phantomas.once('init', function() {
|
||||
phantomas.on('init', function() {
|
||||
phantomas.evaluate(function() {
|
||||
(function(phantomas) {
|
||||
// spy calls to EventTarget.addEventListener
|
||||
|
||||
@@ -138,10 +138,9 @@ exports.module = function(phantomas) {
|
||||
jQueryFunctions = jQueryFunctions.concat(jQueryTraversalFunctions);
|
||||
|
||||
// spy calls to jQuery functions
|
||||
phantomas.once('init', function() {
|
||||
phantomas.on('init', function() {
|
||||
phantomas.evaluate(function(jQueryFunctions, jQueryTraversalFunctions) {
|
||||
(function(phantomas) {
|
||||
var jQuery;
|
||||
var oldJQuery;
|
||||
|
||||
phantomas.spyGlobalVar('jQuery', function(jQuery) {
|
||||
@@ -371,14 +370,6 @@ exports.module = function(phantomas) {
|
||||
});
|
||||
|
||||
|
||||
// store the last resource that was received
|
||||
// try to report where given jQuery version was loaded from
|
||||
phantomas.on('recv', function(entry) {
|
||||
if (entry.isJS) {
|
||||
lastUrl = entry.url;
|
||||
}
|
||||
});
|
||||
|
||||
phantomas.on('jQueryLoaded', function(version) {
|
||||
phantomas.log('jQuery: loaded v' + version);
|
||||
phantomas.setMetric('jQueryVersion', version);
|
||||
@@ -386,7 +377,5 @@ exports.module = function(phantomas) {
|
||||
// report multiple jQuery "instances" (issue #435)
|
||||
phantomas.incrMetric('jQueryVersionsLoaded');
|
||||
phantomas.addOffender('jQueryVersionsLoaded', 'v%s', version);
|
||||
|
||||
phantomas.log('jQuery: v%s (probably loaded from <%s>)', version, lastUrl);
|
||||
});
|
||||
};
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ exports.module = function(phantomas) {
|
||||
phantomas.log('javaScriptBottlenecks: to spy calls to eval() run phantomas with --spy-eval option');
|
||||
}
|
||||
|
||||
phantomas.once('init', function() {
|
||||
phantomas.on('init', function() {
|
||||
phantomas.evaluate(function(spyEval) {
|
||||
(function(phantomas) {
|
||||
function report(msg, caller, backtrace, metric) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -22,8 +22,7 @@ var PhantomasWrapper = function() {
|
||||
// Cusomizable options
|
||||
'engine': task.options.phantomasEngine || 'webkit',
|
||||
'timeout': task.options.timeout || 30,
|
||||
'js-deep-analysis': task.options.jsDeepAnalysis || false,
|
||||
'user-agent': (task.options.device === 'desktop') ? 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.110 Safari/537.36' : null,
|
||||
'user-agent': (task.options.device === 'desktop') ? 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) YLT Chrome/27.0.1453.110 Safari/537.36' : null,
|
||||
'tablet': (task.options.device === 'tablet'),
|
||||
'phone': (task.options.device === 'phone'),
|
||||
'screenshot': task.options.screenshot || false,
|
||||
@@ -31,12 +30,14 @@ var PhantomasWrapper = function() {
|
||||
'cookie': task.options.cookie,
|
||||
'auth-user': task.options.authUser,
|
||||
'auth-pass': task.options.authPass,
|
||||
'block-domain': task.options.blockDomain,
|
||||
'allow-domain': task.options.allowDomain,
|
||||
'no-externals': task.options.noExternals,
|
||||
|
||||
// Mandatory
|
||||
'reporter': 'json:pretty',
|
||||
'analyze-css': true,
|
||||
'skip-modules': [
|
||||
'blockDomains', // not needed
|
||||
'domHiddenContent', // overriden
|
||||
'domMutations', // not compatible with webkit
|
||||
'domQueries', // overriden
|
||||
@@ -46,6 +47,7 @@ var PhantomasWrapper = function() {
|
||||
'javaScriptBottlenecks', // needs to be launched after custom module scopeYLT
|
||||
'jQuery', // overridden
|
||||
'jserrors', // overridden
|
||||
'lazyLoadableImages', //overriden
|
||||
'pageSource', // not needed
|
||||
'windowPerformance' // overriden
|
||||
].join(','),
|
||||
@@ -112,9 +114,14 @@ var PhantomasWrapper = function() {
|
||||
|
||||
// It's time to launch the test!!!
|
||||
var triesNumber = 2;
|
||||
var currentTry = 0;
|
||||
|
||||
async.retry(triesNumber, function(cb) {
|
||||
|
||||
currentTry ++;
|
||||
|
||||
var process = phantomas(task.url, options, function(err, json, results) {
|
||||
var errorCode = err ? parseInt(err.message, 10) : null;
|
||||
|
||||
if (isKilled) {
|
||||
debug('Process was killed, too late Phantomas, sorry...');
|
||||
@@ -122,28 +129,33 @@ var PhantomasWrapper = function() {
|
||||
}
|
||||
|
||||
|
||||
debug('Returning from Phantomas with error %s', err);
|
||||
debug('Returning from Phantomas with error %s', errorCode);
|
||||
|
||||
// Adding some YellowLabTools errors here
|
||||
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
|
||||
if (err === 252 && json) {
|
||||
if (errorCode === 252 && json) {
|
||||
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);
|
||||
|
||||
}).fail(function() {
|
||||
// This function is useless, but the failing promise needs to be handled,
|
||||
// otherwise the module meow writes in the console in case of a timeout (error code 252).
|
||||
debug('Failing promise handled');
|
||||
});
|
||||
|
||||
phantomasPid = process.pid;
|
||||
|
||||
@@ -408,113 +408,121 @@ var WeightChecker = function() {
|
||||
|
||||
var statusCode;
|
||||
|
||||
request(requestOptions)
|
||||
try {
|
||||
|
||||
.on('response', function(res) {
|
||||
|
||||
// Raw headers were added in NodeJS v0.12
|
||||
// (https://github.com/joyent/node/issues/4844), but let's
|
||||
// reconstruct them for backwards compatibility.
|
||||
var rawHeaders = ('HTTP/' + res.httpVersion + ' ' + res.statusCode +
|
||||
' ' + http.STATUS_CODES[res.statusCode] + '\r\n');
|
||||
Object.keys(res.headers).forEach(function(headerKey) {
|
||||
rawHeaders += headerKey + ': ' + res.headers[headerKey] + '\r\n';
|
||||
});
|
||||
rawHeaders += '\r\n';
|
||||
request(requestOptions)
|
||||
|
||||
var uncompressedSize = 0; // size after uncompression
|
||||
var bodySize = 0; // bytes size over the wire
|
||||
var body = ''; // plain text body (after uncompressing gzip/deflate)
|
||||
var isCompressed = false;
|
||||
.on('response', function(res) {
|
||||
|
||||
// Raw headers were added in NodeJS v0.12
|
||||
// (https://github.com/joyent/node/issues/4844), but let's
|
||||
// reconstruct them for backwards compatibility.
|
||||
var rawHeaders = ('HTTP/' + res.httpVersion + ' ' + res.statusCode +
|
||||
' ' + http.STATUS_CODES[res.statusCode] + '\r\n');
|
||||
Object.keys(res.headers).forEach(function(headerKey) {
|
||||
rawHeaders += headerKey + ': ' + res.headers[headerKey] + '\r\n';
|
||||
});
|
||||
rawHeaders += '\r\n';
|
||||
|
||||
function tally() {
|
||||
var uncompressedSize = 0; // size after uncompression
|
||||
var bodySize = 0; // bytes size over the wire
|
||||
var body = ''; // plain text body (after uncompressing gzip/deflate)
|
||||
var isCompressed = false;
|
||||
|
||||
if (statusCode !== 200) {
|
||||
callback({code: statusCode});
|
||||
return;
|
||||
}
|
||||
function tally() {
|
||||
|
||||
var result = {
|
||||
body: body,
|
||||
headersSize: Buffer.byteLength(rawHeaders, 'utf8'),
|
||||
bodySize: bodySize,
|
||||
isCompressed: isCompressed,
|
||||
uncompressedSize: uncompressedSize
|
||||
};
|
||||
|
||||
callback(null, result);
|
||||
}
|
||||
|
||||
switch (res.headers['content-encoding']) {
|
||||
case 'gzip':
|
||||
|
||||
var gzip = zlib.createGunzip();
|
||||
|
||||
gzip.on('data', function (data) {
|
||||
body += data;
|
||||
uncompressedSize += data.length;
|
||||
}).on('end', function () {
|
||||
isCompressed = true;
|
||||
tally();
|
||||
}).on('error', function(err) {
|
||||
debug('Error while decoding %s', requestOptions.url);
|
||||
debug(err);
|
||||
callback(err);
|
||||
});
|
||||
|
||||
res.on('data', function (data) {
|
||||
bodySize += data.length;
|
||||
}).pipe(gzip);
|
||||
|
||||
break;
|
||||
case 'deflate':
|
||||
res.setEncoding('utf8');
|
||||
|
||||
var deflate = zlib.createInflate();
|
||||
|
||||
deflate.on('data', function (data) {
|
||||
body += data;
|
||||
uncompressedSize += data.length;
|
||||
}).on('end', function () {
|
||||
isCompressed = true;
|
||||
tally();
|
||||
}).on('error', function(err) {
|
||||
debug('Error while decoding %s', requestOptions.url);
|
||||
debug(err);
|
||||
callback(err);
|
||||
});
|
||||
|
||||
res.on('data', function (data) {
|
||||
bodySize += data.length;
|
||||
}).pipe(deflate);
|
||||
|
||||
break;
|
||||
default:
|
||||
if (contentType === 'image/jpeg' || contentType === 'image/png') {
|
||||
res.setEncoding('binary');
|
||||
if (statusCode !== 200) {
|
||||
callback({code: statusCode});
|
||||
return;
|
||||
}
|
||||
|
||||
res.on('data', function (data) {
|
||||
body += data;
|
||||
uncompressedSize += data.length;
|
||||
bodySize += data.length;
|
||||
}).on('end', function () {
|
||||
tally();
|
||||
});
|
||||
var result = {
|
||||
body: body,
|
||||
headersSize: Buffer.byteLength(rawHeaders, 'utf8'),
|
||||
bodySize: bodySize,
|
||||
isCompressed: isCompressed,
|
||||
uncompressedSize: uncompressedSize
|
||||
};
|
||||
|
||||
break;
|
||||
}
|
||||
})
|
||||
callback(null, result);
|
||||
}
|
||||
|
||||
.on('response', function(response) {
|
||||
statusCode = response.statusCode;
|
||||
})
|
||||
switch (res.headers['content-encoding']) {
|
||||
case 'gzip':
|
||||
|
||||
.on('error', function(err) {
|
||||
var gzip = zlib.createGunzip();
|
||||
|
||||
gzip.on('data', function (data) {
|
||||
body += data;
|
||||
uncompressedSize += data.length;
|
||||
}).on('end', function () {
|
||||
isCompressed = true;
|
||||
tally();
|
||||
}).on('error', function(err) {
|
||||
debug('Error while decoding %s', requestOptions.url);
|
||||
debug(err);
|
||||
callback(err);
|
||||
});
|
||||
|
||||
res.on('data', function (data) {
|
||||
bodySize += data.length;
|
||||
}).pipe(gzip);
|
||||
|
||||
break;
|
||||
case 'deflate':
|
||||
res.setEncoding('utf8');
|
||||
|
||||
var deflate = zlib.createInflate();
|
||||
|
||||
deflate.on('data', function (data) {
|
||||
body += data;
|
||||
uncompressedSize += data.length;
|
||||
}).on('end', function () {
|
||||
isCompressed = true;
|
||||
tally();
|
||||
}).on('error', function(err) {
|
||||
debug('Error while decoding %s', requestOptions.url);
|
||||
debug(err);
|
||||
callback(err);
|
||||
});
|
||||
|
||||
res.on('data', function (data) {
|
||||
bodySize += data.length;
|
||||
}).pipe(deflate);
|
||||
|
||||
break;
|
||||
default:
|
||||
if (contentType === 'image/jpeg' || contentType === 'image/png') {
|
||||
res.setEncoding('binary');
|
||||
}
|
||||
|
||||
res.on('data', function (data) {
|
||||
body += data;
|
||||
uncompressedSize += data.length;
|
||||
bodySize += data.length;
|
||||
}).on('end', function () {
|
||||
tally();
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
})
|
||||
|
||||
.on('response', function(response) {
|
||||
statusCode = response.statusCode;
|
||||
})
|
||||
|
||||
.on('error', function(err) {
|
||||
debug('Error while downloading %s', requestOptions.url);
|
||||
debug(err);
|
||||
callback(err);
|
||||
});
|
||||
|
||||
} catch(err) {
|
||||
debug('Error while downloading %s', requestOptions.url);
|
||||
debug(err);
|
||||
callback(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
+45
-39
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "yellowlabtools",
|
||||
"version": "1.8.0",
|
||||
"version": "1.10.0",
|
||||
"description": "Online tool to audit a webpage for performance and front-end quality issues",
|
||||
"license": "GPL-2.0",
|
||||
"author": {
|
||||
@@ -15,51 +15,57 @@
|
||||
"bin": {
|
||||
"yellowlabtools": "./bin/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.12.0"
|
||||
},
|
||||
"main": "./lib/index.js",
|
||||
"dependencies": {
|
||||
"angular": "1.4.5",
|
||||
"angular-animate": "1.4.5",
|
||||
"angular-chart.js": "0.8.4",
|
||||
"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": "1.5.2",
|
||||
"angular-animate": "1.5.2",
|
||||
"angular-chart.js": "0.9.0",
|
||||
"angular-local-storage": "0.2.7",
|
||||
"angular-resource": "1.5.2",
|
||||
"angular-route": "1.5.2",
|
||||
"angular-sanitize": "1.5.2",
|
||||
"async": "1.5.2",
|
||||
"body-parser": "1.15.0",
|
||||
"chart.js": "1.0.2",
|
||||
"clean-css": "3.4.3",
|
||||
"color-diff": "0.1.7",
|
||||
"compression": "1.5.2",
|
||||
"clean-css": "3.4.10",
|
||||
"color-diff": "1.0.0",
|
||||
"compression": "1.6.1",
|
||||
"cors": "2.7.1",
|
||||
"css-mq-parser": "0.0.3",
|
||||
"debug": "2.2.0",
|
||||
"express": "4.13.3",
|
||||
"imagemin": "3.2.0",
|
||||
"imagemin-jpegoptim": "4.0.0",
|
||||
"jstoxml": "0.2.3",
|
||||
"lwip": "0.0.7",
|
||||
"meow": "3.3.0",
|
||||
"minimize": "1.7.1",
|
||||
"easyxml": "2.0.1",
|
||||
"express": "4.13.4",
|
||||
"imagemin": "4.0.0",
|
||||
"imagemin-jpegoptim": "4.1.0",
|
||||
"is-http2": "1.0.4",
|
||||
"lwip": "0.0.8",
|
||||
"meow": "3.7.0",
|
||||
"minimize": "1.8.1",
|
||||
"parse-color": "1.0.0",
|
||||
"phantomas": "1.12.0",
|
||||
"ps-node": "0.0.4",
|
||||
"phantomas": "1.15.1",
|
||||
"ps-node": "0.0.5",
|
||||
"q": "1.4.1",
|
||||
"request": "2.62.0",
|
||||
"rimraf": "2.4.3",
|
||||
"request": "2.69.0",
|
||||
"rimraf": "2.5.2",
|
||||
"temporary": "0.0.8",
|
||||
"uglify-js": "2.4.24"
|
||||
"try-thread-sleep": "1.0.0",
|
||||
"uglify-js": "2.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "~3.2.0",
|
||||
"chai": "~3.5.0",
|
||||
"grunt": "~0.4.5",
|
||||
"grunt-blanket": "~0.0.8",
|
||||
"grunt-contrib-clean": "~0.6.0",
|
||||
"grunt-contrib-concat": "~0.5.1",
|
||||
"grunt-contrib-copy": "~0.8.1",
|
||||
"grunt-contrib-cssmin": "~0.14.0",
|
||||
"grunt-contrib-htmlmin": "~0.4.0",
|
||||
"grunt-contrib-jshint": "~0.11.3",
|
||||
"grunt-contrib-less": "~1.0.1",
|
||||
"grunt-contrib-uglify": "~0.9.2",
|
||||
"grunt-blanket": "~0.0.10",
|
||||
"grunt-contrib-clean": "~1.0.0",
|
||||
"grunt-contrib-concat": "~1.0.0",
|
||||
"grunt-contrib-copy": "~1.0.0",
|
||||
"grunt-contrib-cssmin": "~1.0.1",
|
||||
"grunt-contrib-htmlmin": "~1.1.0",
|
||||
"grunt-contrib-jshint": "~1.0.0",
|
||||
"grunt-contrib-less": "~1.2.0",
|
||||
"grunt-contrib-uglify": "~1.0.1",
|
||||
"grunt-env": "~0.4.4",
|
||||
"grunt-express": "~1.4.1",
|
||||
"grunt-filerev": "~2.3.1",
|
||||
@@ -68,10 +74,10 @@
|
||||
"grunt-mocha-test": "~0.12.7",
|
||||
"grunt-replace": "~0.11.0",
|
||||
"grunt-usemin": "~3.1.1",
|
||||
"grunt-webfont": "~0.5.4",
|
||||
"matchdep": "~0.3.0",
|
||||
"mocha": "~2.3.2",
|
||||
"sinon": "~1.16.1",
|
||||
"grunt-webfont": "~1.2.0",
|
||||
"matchdep": "~1.0.1",
|
||||
"mocha": "~2.4.5",
|
||||
"sinon": "~1.17.3",
|
||||
"sinon-chai": "~2.8.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
var express = require('express');
|
||||
var app = express();
|
||||
var server = require('http').createServer(app);
|
||||
|
||||
var settings = require('./settings.json');
|
||||
|
||||
app.all('*', function(req, res) {
|
||||
res.status(500).send('YellowLabTools is in maintenance. It should come back soon with a new version!');
|
||||
});
|
||||
|
||||
server.listen(settings.serverPort, function() {
|
||||
console.log('Maintenance mode started on port %d', server.address().port);
|
||||
});
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
cd /space/YellowLabTools
|
||||
|
||||
# Stop the server
|
||||
# Stop the server and start the maintenance mode
|
||||
forever stopall
|
||||
forever start server_config/maintenance.js
|
||||
|
||||
# Keep the settings.json file
|
||||
git stash
|
||||
@@ -18,5 +19,6 @@ npm install || exit 1
|
||||
rm -rf front/build
|
||||
grunt build
|
||||
|
||||
# Restart the server
|
||||
# Stop the maintenance mode and restart the server
|
||||
forever stopall
|
||||
NODE_ENV=production forever start -c "node --stack-size=262000" bin/server.js
|
||||
@@ -2,6 +2,7 @@
|
||||
"serverPort": 80,
|
||||
"phantomasEngine": "webkit",
|
||||
"googleAnalyticsId": "",
|
||||
"screenshotWidth": 400,
|
||||
|
||||
"authorizedKeys": {
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"serverPort": 8383,
|
||||
"phantomasEngine": "webkit",
|
||||
"googleAnalyticsId": "",
|
||||
"screenshotWidth": 400,
|
||||
|
||||
"authorizedKeys": {
|
||||
|
||||
|
||||
+10
-4
@@ -31,6 +31,7 @@ describe('api', function() {
|
||||
},
|
||||
json: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Api-Key': 'invalid'
|
||||
}
|
||||
}, function(error, response, body) {
|
||||
@@ -53,6 +54,7 @@ describe('api', function() {
|
||||
},
|
||||
json: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Api-Key': Object.keys(config.authorizedKeys)[0]
|
||||
}
|
||||
}, function(error, response, body) {
|
||||
@@ -76,6 +78,7 @@ describe('api', function() {
|
||||
},
|
||||
json: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Api-Key': Object.keys(config.authorizedKeys)[0]
|
||||
}
|
||||
}, function(error, response, body) {
|
||||
@@ -99,12 +102,13 @@ describe('api', function() {
|
||||
screenshot: true,
|
||||
device: 'tablet',
|
||||
//waitForSelector: '*',
|
||||
cookie: 'foo=bar',
|
||||
cookie: 'foo=bar;domain=google.com',
|
||||
authUser: 'joe',
|
||||
authPass: 'secret'
|
||||
},
|
||||
json: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Api-Key': Object.keys(config.authorizedKeys)[0]
|
||||
}
|
||||
}, function(error, response, body) {
|
||||
@@ -133,6 +137,7 @@ describe('api', function() {
|
||||
},
|
||||
json: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Api-Key': Object.keys(config.authorizedKeys)[0]
|
||||
}
|
||||
}, function(error, response, body) {
|
||||
@@ -165,14 +170,13 @@ describe('api', function() {
|
||||
body.should.have.a.property('rules').that.is.an('object');
|
||||
body.should.have.a.property('toolsResults').that.is.an('object');
|
||||
|
||||
// javascriptExecutionTree should only be filled if option jsTimeline is true
|
||||
body.should.have.a.property('javascriptExecutionTree').that.is.an('object');
|
||||
body.javascriptExecutionTree.should.deep.equal({});
|
||||
body.javascriptExecutionTree.should.not.deep.equal({});
|
||||
|
||||
// Check if settings are correctly sent and retrieved
|
||||
body.params.options.should.have.a.property('device').that.equals('tablet');
|
||||
//body.params.options.should.have.a.property('waitForSelector').that.equals('*');
|
||||
body.params.options.should.have.a.property('cookie').that.equals('foo=bar');
|
||||
body.params.options.should.have.a.property('cookie').that.equals('foo=bar;domain=google.com');
|
||||
body.params.options.should.have.a.property('authUser').that.equals('joe');
|
||||
body.params.options.should.have.a.property('authPass').that.equals('secret');
|
||||
|
||||
@@ -208,6 +212,7 @@ describe('api', function() {
|
||||
},
|
||||
json: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Api-Key': Object.keys(config.authorizedKeys)[0]
|
||||
}
|
||||
}, function(error, response, body) {
|
||||
@@ -232,6 +237,7 @@ describe('api', function() {
|
||||
url: serverUrl + '/api/runs/' + asyncRunId,
|
||||
json: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Api-Key': Object.keys(config.authorizedKeys)[0]
|
||||
}
|
||||
}, function(error, response, body) {
|
||||
|
||||
@@ -183,18 +183,19 @@ describe('customPolicies', function() {
|
||||
|
||||
var versions = {
|
||||
'1.2.9': 0,
|
||||
'1.3.9': 0,
|
||||
'1.4.4': 10,
|
||||
'1.5.0': 20,
|
||||
'1.6.3': 30,
|
||||
'1.7.0': 40,
|
||||
'1.8.3a': 50,
|
||||
'1.9.2': 70,
|
||||
'1.10.1': 90,
|
||||
'2.0.0-rc1': 90,
|
||||
'1.11.1': 100,
|
||||
'2.1.1-beta1': 100,
|
||||
'3.0.0': 100
|
||||
'1.4.4': 0,
|
||||
'1.5.0': 10,
|
||||
'1.6.3': 20,
|
||||
'1.7.0': 30,
|
||||
'1.8.3a': 40,
|
||||
'1.9.2': 50,
|
||||
'1.10.1': 70,
|
||||
'2.0.0-rc1': 70,
|
||||
'1.11.1': 90,
|
||||
'2.1.1-beta1': 90,
|
||||
'1.12.1': 100,
|
||||
'2.3.1': 100,
|
||||
'3.1.0': 100
|
||||
};
|
||||
|
||||
for (var version in versions) {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
var should = require('chai').should();
|
||||
var isHttp2 = require('../../lib/tools/isHttp2');
|
||||
|
||||
describe('isHttp2', function() {
|
||||
|
||||
it('should parse the protocol correctly', function() {
|
||||
isHttp2.getProtocol({
|
||||
toolsResults: {
|
||||
phantomas: {
|
||||
url: 'http://www.yahoo.com'
|
||||
}
|
||||
}
|
||||
}).should.equal('http:');
|
||||
|
||||
|
||||
isHttp2.getProtocol({
|
||||
toolsResults: {
|
||||
phantomas: {
|
||||
url: 'https://www.yahoo.com'
|
||||
}
|
||||
}
|
||||
}).should.equal('https:');
|
||||
});
|
||||
|
||||
it('should parse the domain correctly', function() {
|
||||
isHttp2.getDomain({
|
||||
toolsResults: {
|
||||
phantomas: {
|
||||
url: 'http://www.yahoo.com'
|
||||
}
|
||||
}
|
||||
}).should.equal('www.yahoo.com');
|
||||
|
||||
|
||||
isHttp2.getDomain({
|
||||
toolsResults: {
|
||||
phantomas: {
|
||||
url: 'https://www.yahoo.com'
|
||||
}
|
||||
}
|
||||
}).should.equal('www.yahoo.com');
|
||||
});
|
||||
|
||||
it('should have a function checkHttp2', function() {
|
||||
isHttp2.should.have.a.property('checkHttp2').that.is.a('function');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -51,11 +51,14 @@ describe('phantomasWrapper', function() {
|
||||
done('Error: unwanted success');
|
||||
|
||||
}).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) {
|
||||
/*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) {
|
||||
done(err);
|
||||
});
|
||||
|
||||
Vendored
+1
@@ -2,6 +2,7 @@
|
||||
"serverPort": "8387",
|
||||
"phantomasEngine": "webkit",
|
||||
"googleAnalyticsId": "",
|
||||
"screenshotWidth": 400,
|
||||
|
||||
"authorizedKeys": {
|
||||
"1234567890": "contact@gaelmetais.com"
|
||||
|
||||
Reference in New Issue
Block a user