Merge pull request #77 from gmetais/develop

v1.5.0
This commit is contained in:
Gaël Métais
2015-05-12 09:49:53 +02:00
44 changed files with 1145 additions and 196 deletions
-1
View File
@@ -3,5 +3,4 @@ node_js:
- "0.10.33" - "0.10.33"
before_install: before_install:
- "npm install -g grunt-cli" - "npm install -g grunt-cli"
- "npm install -g phantomjs"
install: npm install install: npm install
+15 -2
View File
@@ -15,6 +15,10 @@ var cli = meow({
' --device Use "phone" or "tablet" to simulate a mobile device (by user-agent and viewport size).', ' --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".', ' --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.', ' --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.',
' --auth-pass Basic HTTP authentication password.',
'' ''
].join('\n'), ].join('\n'),
pkg: '../package.json' pkg: '../package.json'
@@ -24,8 +28,7 @@ var cli = meow({
// Check parameters // Check parameters
if (cli.input.length < 1) { if (cli.input.length < 1) {
console.error('Incorrect parameters: url not provided'); cli.showHelp();
process.exit(1);
} }
var url = cli.input[0]; var url = cli.input[0];
@@ -54,6 +57,16 @@ if (cli.flags.jsDeepAnalysis === true || cli.flags.jsDeepAnalysis === 'true') {
// Device simulation // Device simulation
options.device = cli.flags.device || 'desktop'; options.device = cli.flags.device || 'desktop';
// Wait for CSS selector
options.waitForSelector = cli.flags.waitForSelector || null;
// Cookie
options.cookie = cli.flags.cookie || null;
// HTTP basic auth
options.authUser = cli.flags.authUser || null;
options.authPass = cli.flags.authPass || null;
(function execute(url, options) { (function execute(url, options) {
'use strict'; 'use strict';
+1 -1
View File
@@ -6,7 +6,7 @@
"angular-resource": "~1.3.15", "angular-resource": "~1.3.15",
"angular-sanitize": "~1.3.15", "angular-sanitize": "~1.3.15",
"angular-animate": "~1.3.15", "angular-animate": "~1.3.15",
"angular-local-storage": "~0.1.5" "angular-local-storage": "~0.2.0"
}, },
"resolutions": { "resolutions": {
"angular": "~1.3.8" "angular": "~1.3.8"
File diff suppressed because one or more lines are too long
+91 -3
View File
@@ -13,11 +13,11 @@
width: 50%; width: 50%;
} }
.launchBtn { .launchBtn {
background: #e74c3c; background: #ffa319;
color: #fff; color: #fff;
} }
.launchBtn:focus { .launchBtn:focus {
background: #ffa319; background: #e74c3c;
} }
.launchBtn.disabled { .launchBtn.disabled {
background: #deaca6; background: #deaca6;
@@ -25,6 +25,18 @@
.launchBtn.disabled:focus { .launchBtn.disabled:focus {
color: #ddd; color: #ddd;
} }
.settings {
width: 50%;
margin: 0 auto;
}
.settings input,
.settings select {
font-size: 1em;
}
.settings input[type=text] {
width: 100%;
min-width: 4em;
}
.device { .device {
margin-top: 3em; margin-top: 3em;
} }
@@ -53,10 +65,86 @@
margin: 0.2em 0 0.1em; margin: 0.2em 0 0.1em;
font-size: 3em; font-size: 3em;
} }
.settingsTooltip {
position: relative;
}
.settingsTooltip span {
font-size: 0.8em;
vertical-align: text-top;
}
.settingsTooltip div {
display: none;
position: absolute;
padding: 0.5em;
width: 25em;
background: #FFF;
color: #000;
font-size: 0.8em;
border-radius: 1em;
border: 2px solid #ffa319;
white-space: normal;
z-index: 2;
}
.settingsTooltip:hover div {
display: block;
}
.showAdvanced {
display: inline-block;
margin-top: 2em;
color: #FFF;
text-decoration: none;
font-size: 0.9em;
}
.showAdvanced:hover {
color: #ffa319;
}
.currentSettings {
font-size: 0.9em;
}
.currentSettings span {
color: #ffa319;
}
.currentSettings span:after {
color: #FFF;
content: ",";
}
.currentSettings span:last-child:after {
content: "";
}
.advanced {
margin: 1em 0 0;
display: table;
width: 100%;
text-align: left;
border-spacing: 0.75em;
}
.advanced > div {
display: table-row;
}
.advanced > div > div {
display: table-cell;
width: 75%;
}
.advanced > div > div.label {
width: 25%;
white-space: nowrap;
}
.advanced .subTable {
display: table;
border-spacing: 0;
width: 100%;
}
.advanced .subTable > div {
display: table-row;
}
.advanced .subTable > div > div {
display: table-cell;
padding: 0 0 0.75em;
}
.features { .features {
display: table; display: table;
width: 50%; width: 50%;
margin: 8em auto 0; margin: 6em auto 0;
font-size: 0.9em; font-size: 0.9em;
color: #413; color: #413;
} }
File diff suppressed because one or more lines are too long
+25 -11
View File
@@ -82,7 +82,11 @@
font-size: 3em; font-size: 3em;
margin-bottom: 1em; margin-bottom: 1em;
} }
.offenders .offenderButton { .rule .startTime {
display: none;
}
.offendersTable .offenderButton,
.value .offenderButton {
display: inline-block; display: inline-block;
position: relative; position: relative;
background: #efe; background: #efe;
@@ -91,16 +95,19 @@
border-radius: 0.4em; border-radius: 0.4em;
z-index: 1; z-index: 1;
} }
.offenders .offenderButton.opens { .offendersTable .offenderButton.opens,
.value .offenderButton.opens {
padding-right: 0.75em; padding-right: 0.75em;
} }
.offenders .offenderButton.opens:after { .offendersTable .offenderButton.opens:after,
.value .offenderButton.opens:after {
position: relative; position: relative;
left: 0.5em; left: 0.5em;
content: '\25BC'; content: '\25BC';
font-size: 0.8em; font-size: 0.8em;
} }
.offenders .offenderButton > div { .offendersTable .offenderButton > div,
.value .offenderButton > div {
display: none; display: none;
position: absolute; position: absolute;
right: 0; right: 0;
@@ -111,28 +118,35 @@
border-top: 1px solid #999; border-top: 1px solid #999;
z-index: 2; z-index: 2;
} }
.offenders .offenderButton .domTree { .offendersTable .offenderButton .domTree,
.value .offenderButton .domTree {
text-align: left; text-align: left;
white-space: nowrap; white-space: nowrap;
} }
.offenders .offenderButton .domTree > div { .offendersTable .offenderButton .domTree > div,
.value .offenderButton .domTree > div {
margin: 0.5em; margin: 0.5em;
} }
.offenders .offenderButton .domTree > div div { .offendersTable .offenderButton .domTree > div div,
.value .offenderButton .domTree > div div {
margin-left: 1em; margin-left: 1em;
} }
.offenders .offenderButton .backtrace, .offendersTable .offenderButton .backtrace,
.offenders .offenderButton .cssFileAndLine { .value .offenderButton .backtrace,
.offendersTable .offenderButton .cssFileAndLine,
.value .offenderButton .cssFileAndLine {
white-space: nowrap; white-space: nowrap;
padding: 0.5em; padding: 0.5em;
} }
.offenders .offenderButton.opens:hover { .offendersTable .offenderButton.opens:hover,
.value .offenderButton.opens:hover {
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
background: #ffe0cc; background: #ffe0cc;
z-index: 2; z-index: 2;
} }
.offenders .offenderButton.opens:hover > div { .offendersTable .offenderButton.opens:hover > div,
.value .offenderButton.opens:hover > div {
display: block; display: block;
background: #ffe0cc; background: #ffe0cc;
} }
+1 -1
View File
@@ -8,7 +8,7 @@ dashboardCtrl.controller('DashboardCtrl', ['$scope', '$rootScope', '$routeParams
function loadResults() { function loadResults() {
// Load result if needed // Load result if needed
if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) { if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
Results.get({runId: $routeParams.runId}, function(result) { Results.get({runId: $routeParams.runId, exclude: 'toolsResults'}, function(result) {
$rootScope.loadedResult = result; $rootScope.loadedResult = result;
$scope.result = result; $scope.result = result;
init(); init();
-1
View File
@@ -3,7 +3,6 @@ var indexCtrl = angular.module('indexCtrl', []);
indexCtrl.controller('IndexCtrl', ['$scope', 'Settings', 'API', function($scope, Settings, API) { indexCtrl.controller('IndexCtrl', ['$scope', 'Settings', 'API', function($scope, Settings, API) {
$scope.settings = Settings.getMergedSettings(); $scope.settings = Settings.getMergedSettings();
console.log($scope.settings);
$scope.launchTest = function() { $scope.launchTest = function() {
if ($scope.url) { if ($scope.url) {
+1 -1
View File
@@ -9,7 +9,7 @@ ruleCtrl.controller('RuleCtrl', ['$scope', '$rootScope', '$routeParams', '$locat
function loadResults() { function loadResults() {
// Load result if needed // Load result if needed
if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) { if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
Results.get({runId: $routeParams.runId}, function(result) { Results.get({runId: $routeParams.runId, exclude: 'toolsResults'}, function(result) {
$rootScope.loadedResult = result; $rootScope.loadedResult = result;
$scope.result = result; $scope.result = result;
init(); init();
+1 -1
View File
@@ -7,7 +7,7 @@ screenshotCtrl.controller('ScreenshotCtrl', ['$scope', '$rootScope', '$routePara
function loadResults() { function loadResults() {
// Load result if needed // Load result if needed
if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) { if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
Results.get({runId: $routeParams.runId}, function(result) { Results.get({runId: $routeParams.runId, exclude: 'toolsResults'}, function(result) {
$rootScope.loadedResult = result; $rootScope.loadedResult = result;
$scope.result = result; $scope.result = result;
}, function(err) { }, function(err) {
+1 -1
View File
@@ -7,7 +7,7 @@ timelineCtrl.controller('TimelineCtrl', ['$scope', '$rootScope', '$routeParams',
function loadResults() { function loadResults() {
// Load result if needed // Load result if needed
if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) { if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
Results.get({runId: $routeParams.runId}, function(result) { Results.get({runId: $routeParams.runId, exclude: 'toolsResults'}, function(result) {
$rootScope.loadedResult = result; $rootScope.loadedResult = result;
$scope.result = result; $scope.result = result;
render(); render();
+35 -8
View File
@@ -109,7 +109,7 @@
} else if (context.length > 3) { } else if (context.length > 3) {
html += ' and ' + (context.length - 2) + ' more...'; html += ' and ' + (context.length - 2) + ' more...';
} }
return html + '}'; return html + ')';
} }
function isJQuery(node) { function isJQuery(node) {
@@ -119,6 +119,22 @@
function getNonJQueryHTML(node, onASingleLine) { function getNonJQueryHTML(node, onASingleLine) {
var type = node.data.type; var type = node.data.type;
if (node.windowPerformance) {
switch (type) {
case 'documentScroll':
return '(triggering the scroll event on <b>document</b>)';
case 'windowScroll':
return '(triggering the scroll event on <b>window</b>)';
case 'window.onscroll':
return '(calling the <b>window.onscroll</b> function)';
default:
return '';
}
}
if (!node.data.callDetails) { if (!node.data.callDetails) {
return ''; return '';
} }
@@ -153,6 +169,18 @@
case 'error': case 'error':
return args[0]; return args[0];
case 'jQuery - onDOMReady':
return '(function)';
case 'documentScroll':
return 'The scroll event just triggered on document';
case 'windowScroll':
return 'The scroll event just triggered on window';
case 'window.onscroll':
return 'The window.onscroll function just got called';
default: default:
return ''; return '';
} }
@@ -318,7 +346,9 @@
case 'jQuery - on': case 'jQuery - on':
case 'jQuery - one': case 'jQuery - one':
if (args[1]) { if (isStringOfObject(args[0])) {
return '<b>' + args[0].replace(/&quot;\(function\)&quot;/g, '(function)') + '</b>';
} else if (args[1] && isPureString(args[1])) {
return 'bind <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[1] + '</b>'; return 'bind <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[1] + '</b>';
} else { } else {
return 'bind <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine); return 'bind <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
@@ -518,9 +548,6 @@
} }
break; break;
case 'jQuery - onDOMReady':
return '(function)';
default: default:
return ''; return '';
} }
@@ -643,8 +670,8 @@
if (node.data.callDetails.context && node.data.callDetails.context.length === 0) { if (node.data.callDetails.context && node.data.callDetails.context.length === 0) {
html += '<h4>Called on 0 jQuery element</h4><p class="advice">Useless function call, as the jQuery object is empty.</p>'; html += '<h4>Called on 0 jQuery element</h4><p class="advice">Useless function call, as the jQuery object is empty.</p>';
} else if (node.data.type === 'jQuery - bind' && node.data.callDetails.context.length > 5) { } else if (node.eventNotDelegated) {
html += '<p class="advice">The .bind() method attaches the event listener to each jQuery element one by one. Using the .on() method is preferable if available (from v1.7).</p>'; html += '<p class="advice">This binding should use Event Delegation instead of binding each element one by one.</p>';
} }
if (node.data.resultsNumber === 0) { if (node.data.resultsNumber === 0) {
@@ -674,7 +701,7 @@
function getProfilerLineHTML(index, node) { function getProfilerLineHTML(index, node) {
return '<div class="index">' + (index + 1) + '</div>' + return '<div class="index">' + (index + 1) + '</div>' +
'<div class="type">' + node.data.type + (node.children ? '<div class="children">' + recursiveChildrenHTML(node) + '</div>' : '') + '</div>' + '<div class="type">' + node.data.type + (node.children ? '<div class="children">' + recursiveChildrenHTML(node) + '</div>' : '') + '</div>' +
'<div class="value offenders">' + getTimelineParamsHTML(node, false) + '</div>' + '<div class="value">' + getTimelineParamsHTML(node, false) + '</div>' +
'<div class="details">' + getTimelineDetailsHTML(node) + '</div>' + '<div class="details">' + getTimelineDetailsHTML(node) + '</div>' +
'<div class="startTime ' + node.data.loadingStep + '">' + numberWithCommas(node.data.timestamp, 0) + ' ms</div>'; '<div class="startTime ' + node.data.loadingStep + '">' + numberWithCommas(node.data.timestamp, 0) + ' ms</div>';
} }
+22 -3
View File
@@ -5,13 +5,32 @@ apiService.factory('API', ['$location', 'Runs', 'Results', function($location, R
return { return {
launchTest: function(url, settings) { launchTest: function(url, settings) {
Runs.save({ var runObject = {
url: url, url: url,
waitForResponse: false, waitForResponse: false,
screenshot: true, screenshot: true,
jsTimeline: true, jsTimeline: true,
device: settings.device device: settings.device,
}, function(data) { waitForSelector: settings.waitForSelector,
cookie: settings.cookie,
authUser: settings.authUser,
authPass: settings.authPass,
};
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;
}
Runs.save(runObject, function(data) {
$location.path('/queue/' + data.runId); $location.path('/queue/' + data.runId);
}, function(response) { }, function(response) {
if (response.status === 429) { if (response.status === 429) {
+2 -1
View File
@@ -6,7 +6,8 @@ settingsService.factory('Settings', ['localStorageService', function(localStorag
getMergedSettings: function() { getMergedSettings: function() {
var defaultSettings = { var defaultSettings = {
device: 'desktop' device: 'desktop',
showAdvanced: false
}; };
var savedValues = localStorageService.get('settings'); var savedValues = localStorageService.get('settings');
File diff suppressed because one or more lines are too long
+106 -3
View File
@@ -16,10 +16,10 @@
} }
.launchBtn { .launchBtn {
background: #e74c3c; background: #ffa319;
color: #fff; color: #fff;
&:focus { &:focus {
background: #ffa319; background: #e74c3c;
} }
&.disabled { &.disabled {
background: #deaca6; background: #deaca6;
@@ -30,6 +30,20 @@
} }
.settings {
width: 50%;
margin: 0 auto;
input, select {
font-size: 1em;
}
input[type=text] {
width: 100%;
min-width: 4em;
}
}
.device { .device {
margin-top: 3em; margin-top: 3em;
.item { .item {
@@ -62,10 +76,99 @@
} }
} }
.settingsTooltip {
position: relative;
span {
font-size: 0.8em;
vertical-align: text-top;
}
div {
display: none;
position: absolute;
padding: 0.5em;
width: 25em;
background: #FFF;
color: #000;
font-size: 0.8em;
border-radius: 1em;
border: 2px solid #ffa319;
white-space: normal;
z-index: 2;
}
&:hover div {
display: block;
}
}
.showAdvanced {
display: inline-block;
margin-top: 2em;
color: #FFF;
text-decoration: none;
font-size: 0.9em;
&:hover {
color: #ffa319;
}
}
.currentSettings {
font-size: 0.9em;
span {
color: #ffa319;
&:after {
color: #FFF;
content: ",";
}
&:last-child:after {
content: "";
}
}
}
.advanced {
margin: 1em 0 0;
display: table;
width: 100%;
text-align: left;
border-spacing: 0.75em;
> div {
display: table-row;
> div {
display: table-cell;
width: 75%;
&.label {
width: 25%;
white-space: nowrap;
}
}
}
.subTable {
display: table;
border-spacing: 0;
width: 100%;
> div {
display: table-row;
> div {
display: table-cell;
padding: 0 0 0.75em;
}
}
}
}
.features { .features {
display: table; display: table;
width: 50%; width: 50%;
margin: 8em auto 0; margin: 6em auto 0;
font-size: 0.9em; font-size: 0.9em;
color: #413; color: #413;
+4
View File
@@ -139,6 +139,10 @@ a.linkButton {
.screenshotImage { .screenshotImage {
width: 100%; width: 100%;
} }
.screenshotError {
color: #fff;
}
} }
.screenshotWrapper.desktop { .screenshotWrapper.desktop {
+5 -1
View File
@@ -90,7 +90,11 @@
} }
} }
.offenders { .rule .startTime {
display: none;
}
.offendersTable, .value {
.offenderButton { .offenderButton {
display: inline-block; display: inline-block;
position: relative; position: relative;
+3 -2
View File
@@ -11,9 +11,10 @@
</div> </div>
<div> <div>
<a href="/result/{{result.runId}}/screenshot"> <a href="/result/{{result.runId}}/screenshot">
<div class="screenshotWrapper" ng-class="result.params.options.device"> <div class="screenshotWrapper" ng-class="result.params.options.device || 'desktop'">
<div> <div>
<img class="screenshotImage" ng-src="{{result.screenshotUrl}}"/> <img ng-if="result.screenshotUrl" class="screenshotImage" ng-src="{{result.screenshotUrl}}"/>
<span ng-if="!result.screenshotUrl" class="screenshotError">Screenshot not available</span>
</div> </div>
</div> </div>
</a> </a>
+63 -3
View File
@@ -7,9 +7,69 @@
<div class="settings"> <div class="settings">
<div class="device"> <div class="device">
<div>Choose the simulated device:</div> <div>Choose the simulated device:</div>
<div class="item" ng-class="{active: settings.device == 'desktop'}" ng-click="settings.device = 'desktop'"><div class="icon-screen"></div>Desktop</div> <a href="" class="item" ng-class="{active: settings.device == 'desktop'}" ng-click="settings.device = 'desktop'"><div class="icon-screen"></div>Desktop</a>
<div class="item" ng-class="{active: settings.device == 'tablet'}" ng-click="settings.device = 'tablet'"><div class="icon-tablet"></div>Tablet</div> <a href="" class="item" ng-class="{active: settings.device == 'tablet'}" ng-click="settings.device = 'tablet'"><div class="icon-tablet"></div>Tablet</a>
<div class="item" ng-class="{active: settings.device == 'phone'}" ng-click="settings.device = 'phone'"><div class="icon-mobile"></div>Phone</div> <a href="" class="item" ng-class="{active: settings.device == 'phone'}" ng-click="settings.device = 'phone'"><div class="icon-mobile"></div>Phone</a>
</div>
[ <a href="" class="showAdvanced" ng-click="settings.showAdvanced = !settings.showAdvanced">
<span ng-if="!settings.showAdvanced">Advanced settings &nbsp;✚</span>
<span ng-if="settings.showAdvanced">Hide advanced settings &nbsp;✖</span>
</a> ]
<span class="currentSettings" ng-if="!settings.showAdvanced && (settings.waitForSelector || settings.cookie || settings.authUser || settings.authPass)">
Currently set:
<span ng-if="settings.waitForSelector">wait for selector</span>
<span ng-if="settings.cookie">cookie</span>
<span ng-if="settings.authUser || settings.authPass">authentication</span>
</span>
<div class="advanced" ng-show="settings.showAdvanced">
<!--<div>
<div class="label">
Wait selector
<span class="settingsTooltip">
<span class="icon-question"></span>
<div><b>Wait for a CSS selector</b><br><br>Once the page is considered loaded, PhantomJS will repeatedly try to match the given CSS selector until it is found in the page. A 60 seconds timeout still applies anyway.<br><br>Example: "body.loaded"</div>
</span>
</div>
<div><input type="text" name="waitForSelector" ng-model="settings.waitForSelector" /></div>
</div>-->
<div>
<div class="label">
Cookie
<span class="settingsTooltip">
<span class="icon-question"></span>
<div><b>Cookie</b><br><br>Adds a cookie on the main domain.<br><br>Example: "bar=foo;domain=url"</div>
</span>
</div>
<div><input type="text" name="cookie" ng-model="settings.cookie" /></div>
</div>
<!--<div>
<div class="label">
Authent
<span class="settingsTooltip">
<span class="icon-question"></span>
<div><b>Basic HTTP authentication</b><br><br>Enter your credentials here if you need to bypass a basic authentication.<br><br><i>PS: if your authentication is not basic, you might be able to copy the session cookie from your browser, paste it in the "Cookie" setting and launch a run before your cookie expires.</i></div>
</span>
</div>
<div class="subTable">
<div>
<div>username</div>
<div><input type="text" class="authField" name="authUser" ng-model="settings.authUser" /></div>
</div>
<div>
<div><span>password</div>
<div><input type="text" class="authField" name="authPass" ng-model="settings.authPass" /></div>
</div>
</div>
</div>-->
<!--<div>
<div class="label">Blocked domains</div>
<div>
<div>
Blacklist / Whitelist
</div>
<textarea name=""></textarea>
</div>
</div>-->
</div> </div>
</div> </div>
</form> </form>
+54
View File
@@ -47,6 +47,23 @@
<b>{{offender.eventName}}</b> bound to <dom-element-button obj="offender.element"></dom-element-button> <b>{{offender.eventName}}</b> bound to <dom-element-button obj="offender.element"></dom-element-button>
</div> </div>
<div ng-if="policyName === 'eventsScrollBound'">
<span ng-if="offender.target == 'window'">Scroll event bound on <b>window</b></span>
<span ng-if="offender.target == '#document'">Scroll event bound on <b>document</b></span>
<span ng-if="offender.target == 'window.onscroll'"><b>window.onscroll</b> function declared</span>
<div class="offenderButton" ng-if="offender.backtrace.length == 0">no backtrace</div>
<div class="offenderButton opens" ng-if="offender.backtrace.length > 0">
backtrace
<div class="backtrace">
<div ng-repeat="obj in offender.backtrace track by $index">
<span ng-if="obj.functionName">{{obj.functionName}}()</span>
<url-link url="obj.file" max-length="60"></url-link>
line {{obj.line}}
</div>
</div>
</div>
</div>
<div ng-if="policyName === 'jsErrors'"> <div ng-if="policyName === 'jsErrors'">
<b>{{offender.error}}</b> <b>{{offender.error}}</b>
<div class="offenderButton" ng-if="offender.backtrace.length == 0">no backtrace</div> <div class="offenderButton" ng-if="offender.backtrace.length == 0">no backtrace</div>
@@ -62,6 +79,25 @@
</div> </div>
</div> </div>
<div ng-if="policyName === 'jQueryFunctionsUsed'">
Function <b>{{offender.functionName}}</b> used {{offender.count}} times
</div>
<div ng-if="policyName === 'jQueryNotDelegatedEvents'">
function <b>{{offender.functionName}}</b> used on {{offender.contextLength}} DOM elements without event delegation
<div class="offenderButton" ng-if="offender.backtrace.length == 0">no backtrace</div>
<div class="offenderButton opens" ng-if="offender.backtrace.length > 0">
backtrace
<div class="backtrace">
<div ng-repeat="obj in offender.backtrace track by $index">
<span ng-if="obj.functionName">{{obj.functionName}}()</span>
<url-link url="obj.file" max-length="60"></url-link>
line {{obj.line}}
</div>
</div>
</div>
</div>
<div ng-if="policyName === 'documentWriteCalls'"> <div ng-if="policyName === 'documentWriteCalls'">
<b>{{offender.writeFn}}</b> <b>{{offender.writeFn}}</b>
<span ng-if="offender.from"> <span ng-if="offender.from">
@@ -133,6 +169,11 @@
<url-link url="offender" max-length="100"></url-link> <url-link url="offender" max-length="100"></url-link>
</div> </div>
<div ng-if="policyName === 'assetsNotGzipped'">
<url-link url="offender.file" max-length="100"></url-link>
({{offender.type}})
</div>
<div ng-if="policyName === 'cachingTooShort'"> <div ng-if="policyName === 'cachingTooShort'">
<url-link url="offender.file" max-length="100"></url-link> <url-link url="offender.file" max-length="100"></url-link>
cached for <b>{{offender.ttlWithUnit}} {{offender.unit}}</b> cached for <b>{{offender.ttlWithUnit}} {{offender.unit}}</b>
@@ -158,6 +199,19 @@
<dom-tree tree="rule.offendersObj.tree"></dom-tree> <dom-tree tree="rule.offendersObj.tree"></dom-tree>
</div> </div>
<div ng-if="policyName === 'DOMaccessesOnScroll' && rule.offendersObj.children.length > 0">
<p>The table below shows the interactions between the JavaScript and the DOM on a scroll event.</p>
<div class="table" ng-class="{warningsFilterOn: warningsFilterOn}">
<div class="headers">
<div><!-- index --></div>
<div>Type</div>
<div>Params</div>
<div><!-- details --></div>
</div>
<profiler-line ng-repeat="node in rule.offendersObj.children" data-index="$index" node="node"></profiler-line>
</div>
</div>
<div ng-if="policyName === 'cssColors' && rule.offendersObj.count > 0"> <div ng-if="policyName === 'cssColors' && rule.offendersObj.count > 0">
<p>This is the colors palette, sized by total occurrences:</p> <p>This is the colors palette, sized by total occurrences:</p>
<div class="colorPalette"> <div class="colorPalette">
+3 -2
View File
@@ -2,9 +2,10 @@
<div class="screenshot board"> <div class="screenshot board">
<h2>Screenshot</h2> <h2>Screenshot</h2>
<div class="screenshotWrapper" ng-class="result.params.options.device"> <div class="screenshotWrapper" ng-class="result.params.options.device || 'desktop'">
<div> <div>
<img class="screenshotImage" ng-src="{{result.screenshotUrl}}"/> <img ng-if="result.screenshotUrl" class="screenshotImage" ng-src="{{result.screenshotUrl}}"/>
<span ng-if="!result.screenshotUrl" class="screenshotError">Screenshot not available</span>
</div> </div>
</div> </div>
+5 -5
View File
@@ -7,8 +7,8 @@
</select> </select>
</div> </div>
<h2>Javascript Timeline</h2> <h2>JavaScript Timeline</h2>
<p>This graph gives a quick view of when the Javascript interactions with the DOM occur during the loading of the page.</p> <p>This graph gives a quick view of when the JavaScript interactions with the DOM occur during the loading of the page.</p>
<div class="timeline"> <div class="timeline">
<div class="chart"> <div class="chart">
@@ -45,7 +45,7 @@
<div class="domComplete"><div class="color"></div>Page is complete</div> <div class="domComplete"><div class="color"></div>Page is complete</div>
</div> </div>
<div class="tips"> <div class="tips">
<div>Executing Javascript and DOM queries here is a <b>bad practice</b> and slows down the DOM construction.</div> <div>Executing JavaScript and DOM queries here is a <b>bad practice</b> and slows down the DOM construction.</div>
<div>Some frameworks do things here, but it's not reliable and should be avoided.</div> <div>Some frameworks do things here, but it's not reliable and should be avoided.</div>
<div>Also known as "document ready". This is where you should execute <b>top-priority</b> scripts, like binding action buttons or launch a video player.</div> <div>Also known as "document ready". This is where you should execute <b>top-priority</b> scripts, like binding action buttons or launch a video player.</div>
<div>Here you can execute <b>mid-priority</b> tasks. Loading a script with createElement('script') is one way to do so.</div> <div>Here you can execute <b>mid-priority</b> tasks. Loading a script with createElement('script') is one way to do so.</div>
@@ -54,9 +54,9 @@
</div> </div>
</div> </div>
<h2>Javascript Profiler</h2> <h2>JavaScript Profiler</h2>
<p> <p>
The table below shows the interactions between Javascript and the DOM. It is useful to understand what happens while the page loads. The table below shows the interactions between the JavaScript and the DOM. It is useful to understand what happens while the page loads.
</p> </p>
<div class="filters"> <div class="filters">
<div> <div>
+150 -30
View File
@@ -66,6 +66,15 @@ var policies = {
}; };
} }
}, },
/*"DOMaccesses": {
"tool": "jsExecutionTransformer",
"label": "DOM access",
"message": "<p>TODO</p><p>TODO</p>",
"isOkThreshold": 50,
"isBadThreshold": 2000,
"isAbnormalThreshold": 3000,
"hasOffenders": false
},*/
"DOMinserts": { "DOMinserts": {
"tool": "phantomas", "tool": "phantomas",
"label": "DOM inserts", "label": "DOM inserts",
@@ -195,6 +204,49 @@ var policies = {
}; };
} }
}, },
"eventsScrollBound": {
"tool": "phantomas",
"label": "Scroll events bound",
"message": "<p>Number of 'scroll' event listeners binded to 'window' or 'document'.</p><p>Asking too much work to the browser on scroll hurts the smoothness of the scroll. Merging all your event listeners into an unique listener can help you factorize their code and reduce their footprint on scroll.</p>",
"isOkThreshold": 1,
"isBadThreshold": 7,
"isAbnormalThreshold": 12,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return {
count: offenders.length,
list: offenders.map(function(offender) {
var parts = /^bound by (.*) on ([^ ]+)$/.exec(offender);
if (!parts) {
debug('eventsScrollBound offenders transform function error with "%s"', offender);
return {
parseError: offender
};
}
var backtraceArray = offendersHelpers.backtraceToArray(parts[1]);
return {
backtrace: backtraceArray || [],
target: parts[2]
};
})
};
}
},
"DOMaccessesOnScroll": {
"tool": "jsExecutionTransformer",
"label": "DOM access on scroll",
"message": "<p>This rule counts the number of DOM-accessing functions calls, such as queries, readings, writings, bindings and jQuery functions.</p><p>Two scroll events are triggered quickly, one after the other, and only the second one is analyzed so throttled functions are ignored.</p><p>One of the main reasons of a poor scrolling experience is when too much JS is executed on each scroll event. Note that some devices such as smartphones and MacBooks send more scroll events than others.</p><p>Reduce the number of DOM accesses inside scroll listeners. Put DOM queries outside them when possible. Use <a href=\"http://blogorama.nerdworks.in/javascriptfunctionthrottlingan/\" target=\"_blank\">throttling or deboucing</a>.</p>",
"isOkThreshold": 1,
"isBadThreshold": 12,
"isAbnormalThreshold": 25,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return offenders;
}
},
"jsErrors": { "jsErrors": {
"tool": "phantomas", "tool": "phantomas",
"label": "JavaScript errors", "label": "JavaScript errors",
@@ -371,39 +423,79 @@ var policies = {
}, },
"jQueryVersionsLoaded": { "jQueryVersionsLoaded": {
"tool": "phantomas", "tool": "phantomas",
"label": "Several versions loaded", "label": "Several jQuery loaded",
"message": "<p>jQuery is a heavy library. You should <b>never</b> load jQuery more than once on the same page.</p>", "message": "<p>jQuery is a heavy library. You should <b>never</b> load jQuery more than once on the same page.</p>",
"isOkThreshold": 1, "isOkThreshold": 1,
"isBadThreshold": 2, "isBadThreshold": 2,
"isAbnormalThreshold": 2, "isAbnormalThreshold": 2,
"hasOffenders": true "hasOffenders": true
}, },
"jQueryFunctionsUsed": {
"tool": "jsExecutionTransformer",
"label": "jQuery usage",
"message": "<p>This is the number of different jQuery functions called on load. This rule is not trying to blame you for using jQuery too much, but the opposite.</p><p>If only a few functions are used, why not trying to get rid of jQuery? Have a look at <a href=\"http://youmightnotneedjquery.com/\" target=\"_blank\">http://youmightnotneedjquery.com</a>.</p>",
"isOkThreshold": 15,
"isBadThreshold": 6,
"isAbnormalThreshold": 0,
"hasOffenders": true
},
"jQueryNotDelegatedEvents": {
"tool": "jsExecutionTransformer",
"label": "Events not delegated",
"message": "<p>This is the number of events that are bound with the .bind() or the .on() function without using <a href=\"https://learn.jquery.com/events/event-delegation/\" target=\"_blank\">event delegation</a>.</p><p>This means jQuery binds each element contained in the object one by one. This is bad for performance.</p>",
"isOkThreshold": 1,
"isBadThreshold": 100,
"isAbnormalThreshold": 180,
"hasOffenders": true
},
"cssParsingErrors": { "cssParsingErrors": {
"tool": "phantomas", "tool": "phantomas",
"label": "CSS syntax error", "label": "CSS syntax error",
"message": "<p>Yellow Lab Tools failed to parse a CSS file. I doubt the problem comes from the css parser.</p><p>Maybe a <a href=\"http://jigsaw.w3.org/css-validator\" target=\"_blank\">CSS validator</a> can help you.</p>", "message": "<p>Yellow Lab Tools failed to parse a CSS file. I doubt the problem comes from the css parser.</p><p>Maybe a <a href=\"http://jigsaw.w3.org/css-validator\" target=\"_blank\">CSS validator</a> can help you.</p>",
"isOkThreshold": 0, "isOkThreshold": 0,
"isBadThreshold": 1, "isBadThreshold": 1,
"isAbnormalThreshold": 1, "isAbnormalThreshold": 8,
"hasOffenders": true, "hasOffenders": true,
"offendersTransformFn": function(offenders) { "offendersTransformFn": function(offenders) {
return { return {
count: offenders.length, count: offenders.length,
list: offenders.map(function(offender) { list: offenders.map(function(offender) {
var parts = /^(?:(?:<([^ \(]*)>|\[inline CSS\]) ?)?(?:\((((?! @ ).)*)(?: @ (\d+):(\d+))?\))?$/.exec(offender); if (offender === '[inline CSS] (Empty CSS was provided)') {
if (!parts) {
debug('cssParsingErrors offenders transform function error with "%s"', offender);
return { return {
parseError: offender error: 'Empty style tag',
file: null,
line: null,
column: null
}; };
} }
var parts = /^(?:(?:<([^ \(]*)>|\[inline CSS\]) ?)?(?:\((((?! @ ).)*)(?: @ (\d+):(\d+))?\))?$/.exec(offender);
if (parts) {
return {
error: parts[2] || 'Unknown parsing error' + (parts[1] ? '. The entire file was ignored. As a result, the other CSS metrics and scores are miscalculated.' : ''),
file: parts[1] || null,
line: (parts[4] && parts[5]) ? parseInt(parts[4], 10) : null,
column: (parts[4] && parts[5]) ? parseInt(parts[5], 10) : null
};
}
// Try another syntax
parts = /^(.*) <(.*)> @ (\d+):(\d+)$/.exec(offender);
if (parts) {
return {
error: parts[1] || 'Unknown parsing error',
file: parts[2] || null,
line: parseInt(parts[3], 10),
column: parseInt(parts[4], 10)
};
}
debug('cssParsingErrors offenders transform function error with "%s"', offender);
return { return {
error: parts[2], parseError: offender
file: parts[1] || null,
line: (parts[4] && parts[5]) ? parseInt(parts[4], 10) : null,
column: (parts[4] && parts[5]) ? parseInt(parts[5], 10) : null
}; };
}) })
}; };
@@ -413,9 +505,9 @@ var policies = {
"tool": "phantomas", "tool": "phantomas",
"label": "Rules count", "label": "Rules count",
"message": "<p>Having a huge number of CSS rules hurts performances. If the number of CSS rules is higher than the number of DOM elements, there is clearly a problem.</p><p>Huge stylesheets generally occur when the different pages of a website load all the CSS, concatenated in a single stylesheet, even if a large part of the rules are page-specific. Solution is to create one main CSS file with global rules and one custom file per page.</p>", "message": "<p>Having a huge number of CSS rules hurts performances. If the number of CSS rules is higher than the number of DOM elements, there is clearly a problem.</p><p>Huge stylesheets generally occur when the different pages of a website load all the CSS, concatenated in a single stylesheet, even if a large part of the rules are page-specific. Solution is to create one main CSS file with global rules and one custom file per page.</p>",
"isOkThreshold": 500, "isOkThreshold": 750,
"isBadThreshold": 2500, "isBadThreshold": 3000,
"isAbnormalThreshold": 4000, "isAbnormalThreshold": 4500,
"hasOffenders": false "hasOffenders": false
}, },
"cssComplexSelectors": { "cssComplexSelectors": {
@@ -423,7 +515,7 @@ var policies = {
"label": "Complex selectors", "label": "Complex selectors",
"message": "<p>Complex selectors are CSS selectors with 4 or more expressions, like \"#header ul li .foo\".</p><p>They are adding more work for the browser, and this could be avoided by simplifying selectors.</p>", "message": "<p>Complex selectors are CSS selectors with 4 or more expressions, like \"#header ul li .foo\".</p><p>They are adding more work for the browser, and this could be avoided by simplifying selectors.</p>",
"isOkThreshold": 0, "isOkThreshold": 0,
"isBadThreshold": 500, "isBadThreshold": 600,
"isAbnormalThreshold": 2000, "isAbnormalThreshold": 2000,
"hasOffenders": true, "hasOffenders": true,
"offendersTransformFn": function(offenders) { "offendersTransformFn": function(offenders) {
@@ -441,8 +533,8 @@ var policies = {
"label": "Complex attributes selector", "label": "Complex attributes selector",
"message": "<p>Complex attributes selectors are one of these:<ul><li>.foo[type*=bar] (contains bar)</li><li>.foo[type^=bar] (starts with bar)</li><li>.foo[type|=bar] (starts with bar or bar-)</li><li>.foo[type$=bar] (ends with bar)</li><li>.foo[type~=bar baz] (bar or baz)</li></ul></p><p>Their matching process needs more CPU and it has a cost on performances.</p>", "message": "<p>Complex attributes selectors are one of these:<ul><li>.foo[type*=bar] (contains bar)</li><li>.foo[type^=bar] (starts with bar)</li><li>.foo[type|=bar] (starts with bar or bar-)</li><li>.foo[type$=bar] (ends with bar)</li><li>.foo[type~=bar baz] (bar or baz)</li></ul></p><p>Their matching process needs more CPU and it has a cost on performances.</p>",
"isOkThreshold": 0, "isOkThreshold": 0,
"isBadThreshold": 50, "isBadThreshold": 75,
"isAbnormalThreshold": 100, "isAbnormalThreshold": 150,
"hasOffenders": true, "hasOffenders": true,
"offendersTransformFn": function(offenders) { "offendersTransformFn": function(offenders) {
return { return {
@@ -526,8 +618,8 @@ var policies = {
"label": "Duplicated selectors", "label": "Duplicated selectors",
"message": "<p>This is when two or more selectors are strictly identical and should be merged.</p>", "message": "<p>This is when two or more selectors are strictly identical and should be merged.</p>",
"isOkThreshold": 0, "isOkThreshold": 0,
"isBadThreshold": 40, "isBadThreshold": 50,
"isAbnormalThreshold": 80, "isAbnormalThreshold": 100,
"hasOffenders": true, "hasOffenders": true,
"offendersTransformFn": function(offenders) { "offendersTransformFn": function(offenders) {
return { return {
@@ -555,8 +647,8 @@ var policies = {
"label": "Duplicated properties", "label": "Duplicated properties",
"message": "<p>This is the number of property definitions duplicated within a selector.</p>", "message": "<p>This is the number of property definitions duplicated within a selector.</p>",
"isOkThreshold": 0, "isOkThreshold": 0,
"isBadThreshold": 50, "isBadThreshold": 60,
"isAbnormalThreshold": 100, "isAbnormalThreshold": 120,
"hasOffenders": true, "hasOffenders": true,
"offendersTransformFn": function(offenders) { "offendersTransformFn": function(offenders) {
return { return {
@@ -589,7 +681,7 @@ var policies = {
"label": "Empty rules", "label": "Empty rules",
"message": "<p>Very easy to fix: remove all empty rules.</p>", "message": "<p>Very easy to fix: remove all empty rules.</p>",
"isOkThreshold": 0, "isOkThreshold": 0,
"isBadThreshold": 40, "isBadThreshold": 50,
"isAbnormalThreshold": 100, "isAbnormalThreshold": 100,
"hasOffenders": true, "hasOffenders": true,
"offendersTransformFn": function(offenders) { "offendersTransformFn": function(offenders) {
@@ -643,8 +735,8 @@ var policies = {
"label": "Uses of !important", "label": "Uses of !important",
"message": "<p>It can be useful, but only as a last resort. It is a bad practice because it overrides the normal cascading logic. The more you use !important, the more you need it again to over-override. This conducts to a poor maintainability.</p>", "message": "<p>It can be useful, but only as a last resort. It is a bad practice because it overrides the normal cascading logic. The more you use !important, the more you need it again to over-override. This conducts to a poor maintainability.</p>",
"isOkThreshold": 0, "isOkThreshold": 0,
"isBadThreshold": 50, "isBadThreshold": 75,
"isAbnormalThreshold": 150, "isAbnormalThreshold": 200,
"hasOffenders": true, "hasOffenders": true,
"offendersTransformFn": function(offenders) { "offendersTransformFn": function(offenders) {
return { return {
@@ -678,7 +770,7 @@ var policies = {
"label": "Old IE fixes", "label": "Old IE fixes",
"message": "<p>What browser do you need to support? Once you've got the answer, take a look at these old rules that pollute your CSS code and remove them.</p><p>IE6:<ul><li>* html</li><li>html > body (everything but IE6)</li></ul><p><p>IE7:<ul><li><b>*</b>height: 123px;</li><li>height: 123px <b>!ie</b>;</li></ul><p><p>IE9:<ul><li>-ms-filter</li><li>progid:DXImageTransform.Microsoft</li></ul></p>", "message": "<p>What browser do you need to support? Once you've got the answer, take a look at these old rules that pollute your CSS code and remove them.</p><p>IE6:<ul><li>* html</li><li>html > body (everything but IE6)</li></ul><p><p>IE7:<ul><li><b>*</b>height: 123px;</li><li>height: 123px <b>!ie</b>;</li></ul><p><p>IE9:<ul><li>-ms-filter</li><li>progid:DXImageTransform.Microsoft</li></ul></p>",
"isOkThreshold": 0, "isOkThreshold": 0,
"isBadThreshold": 50, "isBadThreshold": 75,
"isAbnormalThreshold": 300, "isAbnormalThreshold": 300,
"hasOffenders": true, "hasOffenders": true,
"offendersTransformFn": function(offenders) { "offendersTransformFn": function(offenders) {
@@ -735,7 +827,7 @@ var policies = {
"label": "Old prefixes", "label": "Old prefixes",
"message": "<p>Many property prefixes such as -moz- or -webkit- are not needed anymore, or by very few people. You can remove them or replace them with the non-prefixed version. This will help reducing your stylesheets weight.</p>", "message": "<p>Many property prefixes such as -moz- or -webkit- are not needed anymore, or by very few people. You can remove them or replace them with the non-prefixed version. This will help reducing your stylesheets weight.</p>",
"isOkThreshold": 0, "isOkThreshold": 0,
"isBadThreshold": 50, "isBadThreshold": 75,
"isAbnormalThreshold": 300, "isAbnormalThreshold": 300,
"hasOffenders": true, "hasOffenders": true,
"offendersTransformFn": function(offenders) { "offendersTransformFn": function(offenders) {
@@ -771,7 +863,7 @@ var policies = {
"label": "Universal selectors", "label": "Universal selectors",
"message": "<p>Universal selectors are the most expensive CSS selectors.</p><p>More informations <a href=\"http://perfectionkills.com/profiling-css-for-fun-and-profit-optimization-notes/\" target=\"_blank\">here</a>.</p>", "message": "<p>Universal selectors are the most expensive CSS selectors.</p><p>More informations <a href=\"http://perfectionkills.com/profiling-css-for-fun-and-profit-optimization-notes/\" target=\"_blank\">here</a>.</p>",
"isOkThreshold": 0, "isOkThreshold": 0,
"isBadThreshold": 40, "isBadThreshold": 50,
"isAbnormalThreshold": 150, "isAbnormalThreshold": 150,
"hasOffenders": true, "hasOffenders": true,
"offendersTransformFn": function(offenders) { "offendersTransformFn": function(offenders) {
@@ -789,7 +881,7 @@ var policies = {
"label": "Redundant body selectors", "label": "Redundant body selectors",
"message": "<p>This is one way to remove complexity from a CSS rule. Generally, when \"body\" is specified in a rule it can be removed, because an element is necessarily inside the body.</p>", "message": "<p>This is one way to remove complexity from a CSS rule. Generally, when \"body\" is specified in a rule it can be removed, because an element is necessarily inside the body.</p>",
"isOkThreshold": 0, "isOkThreshold": 0,
"isBadThreshold": 50, "isBadThreshold": 60,
"isAbnormalThreshold": 200, "isAbnormalThreshold": 200,
"hasOffenders": true, "hasOffenders": true,
"offendersTransformFn": function(offenders) { "offendersTransformFn": function(offenders) {
@@ -810,7 +902,7 @@ var policies = {
"label": "Redundant tags selectors", "label": "Redundant tags selectors",
"message": "<p>Some tags included inside other tags are obvious. For example, when \"ul li\" is specified in a rule, \"ul\" can be removed because the \"li\" element is <b>always</b> inside a \"ul\". Same thing for \"tr td\", \"select option\", ...</p><p>Lowering compexity in CSS selectors can make the page load a little faster.</p>", "message": "<p>Some tags included inside other tags are obvious. For example, when \"ul li\" is specified in a rule, \"ul\" can be removed because the \"li\" element is <b>always</b> inside a \"ul\". Same thing for \"tr td\", \"select option\", ...</p><p>Lowering compexity in CSS selectors can make the page load a little faster.</p>",
"isOkThreshold": 0, "isOkThreshold": 0,
"isBadThreshold": 50, "isBadThreshold": 60,
"isAbnormalThreshold": 200, "isAbnormalThreshold": 200,
"hasOffenders": true, "hasOffenders": true,
"offendersTransformFn": function(offenders) { "offendersTransformFn": function(offenders) {
@@ -1056,6 +1148,35 @@ var policies = {
"isAbnormalThreshold": 1, "isAbnormalThreshold": 1,
"hasOffenders": true "hasOffenders": true
}, },
"assetsNotGzipped": {
"tool": "phantomas",
"label": "Not gzipped",
"message": "<p>This is the number of requests that should be compressed with gzip but aren't.</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": 0,
"isBadThreshold": 12,
"isAbnormalThreshold": 20,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return {
count: offenders.length,
list: offenders.map(function(offender) {
var parts = /^([^ ]*) \((.+)\)$/.exec(offender);
if (!parts) {
debug('assetsNotGzipped offenders transform function error with "%s"', offender);
return {
parseError: offender
};
}
return {
file: parts[1],
type: parts[2]
};
})
};
}
},
"closedConnections": { "closedConnections": {
"tool": "phantomas", "tool": "phantomas",
"label": "Connections closed", "label": "Connections closed",
@@ -1176,7 +1297,6 @@ var policies = {
domain: parts[1], domain: parts[1],
requests: parseInt(parts[2]) requests: parseInt(parts[2])
}; };
}) })
}; };
} }
+17 -11
View File
@@ -19,6 +19,13 @@
"eventsBound": 1 "eventsBound": 1
} }
}, },
"scroll": {
"label": "Scroll bottlenecks",
"policies": {
"eventsScrollBound": 1,
"DOMaccessesOnScroll": 4
}
},
"badJavascript": { "badJavascript": {
"label": "Bad JavaScript", "label": "Bad JavaScript",
"policies": { "policies": {
@@ -28,17 +35,13 @@
"globalVariables": 0.5 "globalVariables": 0.5
} }
}, },
"jQueryVersion": { "jQuery": {
"label": "jQuery version", "label": "jQuery",
"policies": { "policies": {
"jQueryVersion": 5, "jQueryVersion": 1,
"jQueryVersionsLoaded": 0.1 "jQueryVersionsLoaded": 1,
} "jQueryFunctionsUsed": 1,
}, "jQueryNotDelegatedEvents": 1
"cssSyntaxError": {
"label": "CSS syntax errors",
"policies": {
"cssParsingErrors": 1
} }
}, },
"cssComplexity": { "cssComplexity": {
@@ -53,6 +56,7 @@
"badCSS": { "badCSS": {
"label": "Bad CSS", "label": "Bad CSS",
"policies": { "policies": {
"cssParsingErrors": 4,
"cssImports": 3, "cssImports": 3,
"cssDuplicatedSelectors": 2, "cssDuplicatedSelectors": 2,
"cssDuplicatedProperties": 1, "cssDuplicatedProperties": 1,
@@ -92,6 +96,7 @@
"label": "Network", "label": "Network",
"policies": { "policies": {
"notFound": 2, "notFound": 2,
"assetsNotGzipped": 1.5,
"closedConnections": 2, "closedConnections": 2,
"multipleRequests": 2, "multipleRequests": 2,
"cachingNotSpecified": 1, "cachingNotSpecified": 1,
@@ -104,8 +109,9 @@
"globalScore": { "globalScore": {
"domComplexity": 1, "domComplexity": 1,
"domManipulations": 2, "domManipulations": 2,
"scroll": 1,
"badJavascript": 1, "badJavascript": 1,
"jQueryVersion": 1, "jQuery": 1,
"cssSyntaxError": 1, "cssSyntaxError": 1,
"cssComplexity": 1, "cssComplexity": 1,
"badCSS": 1, "badCSS": 1,
+1 -1
View File
@@ -115,7 +115,7 @@ var OffendersHelpers = function() {
var parts = null; var parts = null;
for (var i=0 ; i<traceArray.length ; i++) { for (var i=0 ; i<traceArray.length ; i++) {
parts = /^(([\w$]+) )?([^ ]+):(\d+)$/.exec(traceArray[i]); parts = /^(([\w$]+) )?\(?([^ ]+):(\d+)\)?$/.exec(traceArray[i]);
if (parts) { if (parts) {
var obj = { var obj = {
+2
View File
@@ -42,6 +42,8 @@ var Runner = function(params) {
delete data.toolsResults.phantomas.metrics.javascriptExecutionTree; delete data.toolsResults.phantomas.metrics.javascriptExecutionTree;
delete data.toolsResults.phantomas.offenders.javascriptExecutionTree; delete data.toolsResults.phantomas.offenders.javascriptExecutionTree;
delete data.toolsResults.phantomas.metrics.scrollExecutionTree;
delete data.toolsResults.phantomas.offenders.scrollExecutionTree;
return data; return data;
+30 -3
View File
@@ -34,7 +34,11 @@ var ApiController = function(app) {
partialResult: req.body.partialResult || null, partialResult: req.body.partialResult || null,
screenshot: req.body.screenshot || false, screenshot: req.body.screenshot || false,
jsTimeline: req.body.jsTimeline || false, jsTimeline: req.body.jsTimeline || false,
device: req.body.device || 'desktop' 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
} }
}; };
@@ -68,7 +72,11 @@ var ApiController = function(app) {
var runOptions = { var runOptions = {
screenshot: run.params.screenshot ? screenshot.getTmpFilePath() : false, screenshot: run.params.screenshot ? screenshot.getTmpFilePath() : false,
jsDeepAnalysis: run.params.jsTimeline, jsDeepAnalysis: run.params.jsTimeline,
device: run.params.device device: run.params.device,
waitForSelector: run.params.waitForSelector,
cookie: run.params.cookie,
authUser: run.params.authUser,
authPass: run.params.authPass
}; };
return ylt(run.params.url, runOptions); return ylt(run.params.url, runOptions);
@@ -101,8 +109,14 @@ var ApiController = function(app) {
} }
}) })
// Delete screenshot temporary file // Delete screenshot temporary file
.then(screenshot.deleteTmpFile); .then(screenshot.deleteTmpFile)
// Don't worry if there's an error
.fail(function(err) {
debug('An error occured while creating the screenshot\'s thumbnail. Ignoring and continuing...');
});
} }
@@ -117,6 +131,7 @@ var ApiController = function(app) {
// Empty javascriptExecutionTree if not needed // Empty javascriptExecutionTree if not needed
if (!run.params.jsTimeline) { if (!run.params.jsTimeline) {
data.javascriptExecutionTree = {}; data.javascriptExecutionTree = {};
data.scrollExecutionTree = {};
} }
// Remove tools results if not needed // Remove tools results if not needed
@@ -234,6 +249,18 @@ var ApiController = function(app) {
// Retrive one result by id // Retrive one result by id
app.get('/api/results/:id', function(req, res) { app.get('/api/results/:id', function(req, res) {
getPartialResults(req.params.id, res, function(data) { getPartialResults(req.params.id, res, function(data) {
// Some fields can be excluded from the response, this way:
// /api/results/:id?exclude=field1,field2
if (req.query.exclude && typeof req.query.exclude === 'string') {
var excludedFields = req.query.exclude.split(',');
excludedFields.forEach(function(fieldName) {
if (data[fieldName]) {
delete data[fieldName];
}
});
}
return data; return data;
}); });
}); });
+146 -26
View File
@@ -1,34 +1,70 @@
var debug = require('debug')('ylt:jsExecutionTransformer'); var debug = require('debug')('ylt:jsExecutionTransformer');
var offendersHelpers = require('../offendersHelpers'); var offendersHelpers = require('../offendersHelpers');
var Collection = require('./phantomas/custom_modules/util/collection');
var jsExecutionTransformer = function() { var jsExecutionTransformer = function() {
this.transform = function(data) { this.transform = function(data) {
var javascriptExecutionTree = {}; var javascriptExecutionTree = {};
var scrollExecutionTree = {};
var jQueryFunctionsCollection = new Collection();
var metrics = { var metrics = {
domManipulations: 0, DOMaccesses: 0,
queriesWithoutResults: 0, DOMaccessesOnScroll: 0,
jQueryCalls: 0, queriesWithoutResults: 0
jQueryCallsOnEmptyObject: 0
}; };
debug('Starting JS execution transformation'); var offenders = {
};
var hasjQuery = (data.toolsResults.phantomas.metrics.jQueryVersionsLoaded > 0);
if (hasjQuery) {
metrics.jQueryCalls = 0;
metrics.jQueryFunctionsUsed = 0;
metrics.jQueryCallsOnEmptyObject = 0;
metrics.jQueryNotDelegatedEvents = 0;
offenders.jQueryFunctionsUsed = [];
offenders.jQueryNotDelegatedEvents = [];
}
try { try {
debug('Starting JS execution transformation');
javascriptExecutionTree = JSON.parse(data.toolsResults.phantomas.offenders.javascriptExecutionTree[0]); javascriptExecutionTree = JSON.parse(data.toolsResults.phantomas.offenders.javascriptExecutionTree[0]);
if (javascriptExecutionTree.children) { if (javascriptExecutionTree.children) {
javascriptExecutionTree.children.forEach(function(node) { javascriptExecutionTree.children.forEach(function(node) {
// Mark abnormal things with a warning flag var contextLength = (node.data.callDetails && node.data.callDetails.context) ? node.data.callDetails.context.length : null;
var contextLenght = (node.data.callDetails && node.data.callDetails.context) ? node.data.callDetails.context.length : null;
if ((node.data.type === 'jQuery - bind' && contextLenght > 5) || if (isABindWithoutEventDelegation(node, contextLength)) {
node.data.resultsNumber === 0 || metrics.jQueryNotDelegatedEvents += contextLength;
contextLenght === 0) { offenders.jQueryNotDelegatedEvents.push({
functionName: node.data.type.substring(9),
contextLength: contextLength,
backtrace: offendersHelpers.backtraceToArray(node.data.backtrace)
});
node.warning = true; node.warning = true;
node.eventNotDelegated = true;
}
if (node.data.resultsNumber === 0) {
metrics.queriesWithoutResults ++;
node.warning = true;
}
if (contextLength === 0) {
metrics.jQueryCallsOnEmptyObject ++;
node.warning = true;
}
if (node.data.type.indexOf('jQuery - ') === 0) {
metrics.jQueryCalls ++;
jQueryFunctionsCollection.push(node.data.type);
} }
// Mark errors with an error flag // Mark errors with an error flag
@@ -57,33 +93,60 @@ var jsExecutionTransformer = function() {
break; break;
} }
// Change the list of dom paths into a tree // Transform domPaths into objects
treeRecursiveParser(node, function(node) { changeListOfDomPaths(node);
if (node.data.callDetails && node.data.callDetails.context && node.data.callDetails.context.length > 0) {
node.data.callDetails.context.elements = node.data.callDetails.context.elements.map(offendersHelpers.domPathToDomElementObj, offendersHelpers);
}
if (node.data.type === 'appendChild' || node.data.type === 'insertBefore' || node.data.type === 'getComputedStyle') { // Count the number of DOM accesses, by counting the tree leafs
node.data.callDetails.arguments[0] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[0]); metrics.DOMaccesses += countTreeLeafs(node);
} });
if (node.data.type === 'insertBefore') { // Count the number of different jQuery functions called
node.data.callDetails.arguments[1] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[1]); if (hasjQuery) {
jQueryFunctionsCollection.sort().forEach(function(fnName, cnt) {
if (fnName === 'jQuery - find') {
fnName = 'jQuery - $';
} }
metrics.jQueryFunctionsUsed ++;
offenders.jQueryFunctionsUsed.push({
functionName: fnName.substring(9),
count: cnt
});
}); });
}
}
debug('JS execution transformation complete');
debug('Starting scroll execution transformation');
offenders.scrollExecutionTree = JSON.parse(data.toolsResults.phantomas.offenders.scrollExecutionTree[0]);
if (offenders.scrollExecutionTree.children) {
offenders.scrollExecutionTree.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);
}); });
} }
debug('Scroll execution transformation complete');
debug('JS execution transformation complete');
} catch(err) { } catch(err) {
throw err; throw err;
} }
data.javascriptExecutionTree = javascriptExecutionTree; data.javascriptExecutionTree = javascriptExecutionTree;
data.toolsResults.jsExecutionTransformer = { data.toolsResults.jsExecutionTransformer = {
metrics: metrics metrics: metrics,
offenders: offenders
}; };
return data; return data;
@@ -97,6 +160,63 @@ var jsExecutionTransformer = function() {
} }
fn(node); fn(node);
} }
function changeListOfDomPaths(rootNode) {
treeRecursiveParser(rootNode, function(node) {
if (node.data.callDetails && node.data.callDetails.context && node.data.callDetails.context.length > 0) {
node.data.callDetails.context.elements = node.data.callDetails.context.elements.map(offendersHelpers.domPathToDomElementObj, offendersHelpers);
}
if (node.data.type === 'appendChild' || node.data.type === 'insertBefore' || node.data.type === 'getComputedStyle') {
node.data.callDetails.arguments[0] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[0]);
}
if (node.data.type === 'insertBefore') {
node.data.callDetails.arguments[1] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[1]);
}
});
}
// Returns the number of leafs (nodes without children)
function countTreeLeafs(rootNode) {
var count = 0;
treeRecursiveParser(rootNode, function(node) {
if (!node.children &&
!node.error &&
!node.windowPerformance &&
node.data.type !== 'jQuery loaded') {
count ++;
}
});
return count;
}
function isPureString(str) {
return typeof str === 'string' && str[0] !== '{' && str !== '(function)' && str !== '[Object]' && str !== '[Array]' && str !== 'true' && str !== 'false' && str !== 'undefined' && str !== 'unknown' && str !== 'null';
}
function isABindWithoutEventDelegation(node, contextLength) {
// Count only on larger bindings
if (contextLength <= 3) {
return false;
}
if (node.data.type === 'jQuery - on' && node.data.callDetails.arguments[1] && !isPureString(node.data.callDetails.arguments[1])) {
return true;
}
if (node.data.type.indexOf('jQuery - ') === 0 && node.children && node.children.length === 1) {
var child = node.children[0];
if (child.data.type === 'jQuery - on' && child.data.callDetails.arguments[1] && !isPureString(child.data.callDetails.arguments[1])) {
return true;
}
}
return false;
}
}; };
module.exports = new jsExecutionTransformer(); module.exports = new jsExecutionTransformer();
@@ -104,10 +104,11 @@ exports.module = function(phantomas) {
var root = new ContextTreeNode(null, {type: 'main'}); var root = new ContextTreeNode(null, {type: 'main'});
var currentContext = root; var currentContext = root;
var depth = 0;
if (deepAnalysis) { if (deepAnalysis) {
phantomas.log('Entering deep Javascript analysis mode'); phantomas.log('Entering deep Javascript analysis mode');
} }
var depth = 0;
// Add a child but don't enter his context // Add a child but don't enter his context
function pushContext(data) { function pushContext(data) {
@@ -176,6 +177,13 @@ exports.module = function(phantomas) {
return root; return root;
} }
// Empty the tree
function resetTree() {
root = new ContextTreeNode(null, {type: 'main'});
currentContext = root;
depth = 0;
}
function ContextTreeNode(parent, data) { function ContextTreeNode(parent, data) {
@@ -196,6 +204,7 @@ exports.module = function(phantomas) {
phantomas.leaveContext = leaveContext; phantomas.leaveContext = leaveContext;
phantomas.getContextData = getContextData; phantomas.getContextData = getContextData;
phantomas.readFullTree = readFullTree; phantomas.readFullTree = readFullTree;
phantomas.resetTree = resetTree;
})(); })();
@@ -1,49 +0,0 @@
/**
* Analyzes events bound to DOM elements
*/
/* global Document: true, Element: true, window: true */
exports.version = '0.2.a';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('eventsBound'); // @desc number of EventTarget.addEventListener calls
// spy calls to EventTarget.addEventListener
// @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget.addEventListener
phantomas.once('init', function() {
phantomas.evaluate(function() {
(function(phantomas) {
function eventSpyBefore(eventType) {
/* jshint validthis: true */
var path = phantomas.getDOMPath(this);
//phantomas.log('DOM event: "' + eventType + '" bound to "' + path + '"');
phantomas.incrMetric('eventsBound');
phantomas.addOffender('eventsBound', '"%s" bound to "%s"', eventType, path);
phantomas.enterContext({
type: 'addEventListener',
callDetails: {
context: {
length: 1,
elements: [path]
},
arguments: [eventType]
},
backtrace: phantomas.getBacktrace()
});
}
function eventSpyAfter(result) {
phantomas.leaveContext();
}
phantomas.spy(Element.prototype, 'addEventListener', eventSpyBefore, eventSpyAfter);
phantomas.spy(Document.prototype, 'addEventListener', eventSpyBefore, eventSpyAfter);
phantomas.spy(window, 'addEventListener', eventSpyBefore, eventSpyAfter);
})(window.__phantomas);
});
});
};
@@ -0,0 +1,82 @@
/**
* Analyzes events bound to DOM elements
*/
/* global Document: true, Element: true, window: true */
exports.version = '0.4.a';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('eventsBound'); // @desc number of EventTarget.addEventListener calls
phantomas.setMetric('eventsDispatched'); // @desc number of EventTarget.dispatchEvent calls
phantomas.setMetric('eventsScrollBound'); // @desc number of scroll event bounds
phantomas.once('init', function() {
phantomas.evaluate(function() {
(function(phantomas) {
// spy calls to EventTarget.addEventListener
// @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget.addEventListener
function eventSpyBefore(eventType) {
/* jshint validthis: true */
var path = phantomas.getDOMPath(this);
//phantomas.log('DOM event: "' + eventType + '" bound to "' + path + '"');
phantomas.incrMetric('eventsBound');
phantomas.addOffender('eventsBound', '"%s" bound to "%s"', eventType, path);
phantomas.log('event ' + eventType + ' bound');
phantomas.enterContext({
type: 'addEventListener',
callDetails: {
context: {
length: 1,
elements: [path]
},
arguments: [eventType]
},
backtrace: phantomas.getBacktrace()
});
// count window.addEventListener('scroll', ...) - issue #508
if (eventType === 'scroll' && (path === 'window' || path === '#document')) {
phantomas.incrMetric('eventsScrollBound');
phantomas.addOffender('eventsScrollBound', 'bound by %s on %s', phantomas.getBacktrace(), path);
}
}
function eventSpyAfter(result) {
phantomas.leaveContext();
}
phantomas.spy(Element.prototype, 'addEventListener', eventSpyBefore, eventSpyAfter);
phantomas.spy(Document.prototype, 'addEventListener', eventSpyBefore, eventSpyAfter);
phantomas.spy(window, 'addEventListener', eventSpyBefore, eventSpyAfter);
// spy calls to EventTarget.dispatchEvent
// @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget.dispatchEvent
phantomas.spy(Element.prototype, 'dispatchEvent', function(ev) {
/* jshint validthis: true */
var path = phantomas.getDOMPath(this);
phantomas.log('Core JS event: triggered "%s" on "%s"', ev.type, path);
phantomas.incrMetric('eventsDispatched');
phantomas.addOffender('eventsDispatched', '"%s" on "%s"', ev.type, path);
});
})(window.__phantomas);
});
});
phantomas.on('report', function() {
phantomas.evaluate(function() {
(function(phantomas) {
// Check if a window.onscroll function is defined
if (typeof(window.onscroll) === "function") {
phantomas.incrMetric('eventsScrollBound');
phantomas.addOffender('eventsScrollBound', 'bound by %s on %s', '', 'window.onscroll');
}
}(window.__phantomas));
});
});
};
@@ -274,7 +274,6 @@ exports.module = function(phantomas) {
args = [].slice.call(arguments); args = [].slice.call(arguments);
args.forEach(function(arg, index) { args.forEach(function(arg, index) {
if (arg instanceof Array) { if (arg instanceof Array) {
arg = '[Array]'; arg = '[Array]';
} }
@@ -296,6 +295,13 @@ exports.module = function(phantomas) {
} else { } else {
try { try {
for (var key in arg) {
if (typeof arg[key] === 'function') {
arg[key] = '(function)';
}
}
arg = JSON.stringify(arg); arg = JSON.stringify(arg);
} catch(e) { } catch(e) {
arg = '[Object]'; arg = '[Object]';
@@ -9,11 +9,11 @@ exports.version = '0.1';
exports.module = function(phantomas) { exports.module = function(phantomas) {
'use strict'; 'use strict';
phantomas.setMetric('javascriptExecutionTree'); // @desc number of duplicated DOM queries phantomas.setMetric('javascriptExecutionTree');
// save data // save data
phantomas.on('report', function() { phantomas.on('report', function() {
phantomas.log('Reading execution tree JSON'); phantomas.log('JS execution tree: Reading execution tree JSON');
phantomas.evaluate(function() {(function(phantomas) { phantomas.evaluate(function() {(function(phantomas) {
var fullTree = phantomas.readFullTree(); var fullTree = phantomas.readFullTree();
@@ -0,0 +1,69 @@
exports.version = '0.1';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('scrollExecutionTree');
phantomas.on('report', function() {
phantomas.evaluate(function() {
(function(phantomas) {
var evt = document.createEvent('CustomEvent');
evt.initCustomEvent('scroll', false, false, null);
function triggerScrollEvent() {
phantomas.resetTree();
try {
// Chrome triggers them in this order:
// 1. document
phantomas.pushContext({
type: 'documentScroll'
});
document.dispatchEvent(evt);
// 2. window
phantomas.pushContext({
type: 'windowScroll'
});
window.dispatchEvent(evt);
// No need to call window.onscroll(), it's called by the scroll event on window
} catch(e) {
phantomas.log('ScrollListener error: %s', e);
}
}
var firstScrollTime = Date.now();
phantomas.log('ScrollListener: triggering a first scroll event...');
triggerScrollEvent();
// Ignore the first scroll event and only save the second one,
// because we want to detect un-throttled things, throttled ones are ok.
var secondScrollTime = Date.now();
phantomas.log('ScrollListener: triggering a second scroll event (%dms after the first)...', secondScrollTime - firstScrollTime);
triggerScrollEvent();
var fullTree = phantomas.readFullTree();
if (fullTree !== null) {
phantomas.setMetric('scrollExecutionTree', true, true);
phantomas.addOffender('scrollExecutionTree', JSON.stringify(fullTree));
phantomas.log('ScrollListener: scrollExecutionTree correctly extracted');
} else {
phantomas.log('Error: scrollExecutionTree could not be extracted');
}
phantomas.log('ScrollListener: end of scroll triggering');
})(window.__phantomas);
});
});
};
+6 -3
View File
@@ -20,12 +20,16 @@ var PhantomasWrapper = function() {
var options = { var options = {
// Cusomizable options // Cusomizable options
'timeout': task.options.timeout || 60, 'timeout': task.options.timeout || 45,
'js-deep-analysis': task.options.jsDeepAnalysis || false, '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) Chrome/27.0.1453.110 Safari/537.36' : null,
'tablet': (task.options.device === 'tablet'), 'tablet': (task.options.device === 'tablet'),
'phone': (task.options.device === 'phone'), 'phone': (task.options.device === 'phone'),
'screenshot': task.options.screenshot || false, 'screenshot': task.options.screenshot || false,
'wait-for-selector': task.options.waitForSelector,
'cookie': task.options.cookie,
'auth-user': task.options.authUser,
'auth-pass': task.options.authPass,
// Mandatory // Mandatory
'reporter': 'json:pretty', 'reporter': 'json:pretty',
@@ -35,14 +39,13 @@ var PhantomasWrapper = function() {
'domHiddenContent', // overriden 'domHiddenContent', // overriden
'domMutations', // not compatible with webkit 'domMutations', // not compatible with webkit
'domQueries', // overriden 'domQueries', // overriden
'eventListeners', // overridden 'events', // overridden
'filmStrip', // not needed 'filmStrip', // not needed
'har', // not needed for the moment 'har', // not needed for the moment
'javaScriptBottlenecks', // needs to be launched after custom module scopeYLT 'javaScriptBottlenecks', // needs to be launched after custom module scopeYLT
'jQuery', // overridden 'jQuery', // overridden
'jserrors', // overridden 'jserrors', // overridden
'pageSource', // not needed 'pageSource', // not needed
'waitForSelector', // not needed
'windowPerformance' // overriden 'windowPerformance' // overriden
].join(','), ].join(','),
'include-dirs': [ 'include-dirs': [
+10 -10
View File
@@ -1,6 +1,6 @@
{ {
"name": "yellowlabtools", "name": "yellowlabtools",
"version": "1.4.1", "version": "1.5.0",
"description": "Online tool to audit a webpage for performance and front-end quality issues", "description": "Online tool to audit a webpage for performance and front-end quality issues",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -12,17 +12,17 @@
"main": "./lib/index.js", "main": "./lib/index.js",
"dependencies": { "dependencies": {
"async": "~0.9.0", "async": "~0.9.0",
"body-parser": "~1.12.3", "body-parser": "~1.12.4",
"compression": "~1.4.3", "compression": "~1.4.3",
"cors": "^2.5.3", "cors": "^2.6.0",
"debug": "~2.1.3", "debug": "~2.2.0",
"express": "~4.12.3", "express": "~4.12.3",
"lwip": "0.0.6", "lwip": "0.0.6",
"meow": "^3.1.0", "meow": "^3.1.0",
"phantomas": "1.10.0", "phantomas": "1.10.2",
"ps-node": "0.0.4", "ps-node": "0.0.4",
"q": "~1.2.1", "q": "~1.4.0",
"rimraf": "~2.3.2", "rimraf": "~2.3.3",
"temporary": "0.0.8" "temporary": "0.0.8"
}, },
"devDependencies": { "devDependencies": {
@@ -32,7 +32,7 @@
"grunt-contrib-clean": "^0.6.0", "grunt-contrib-clean": "^0.6.0",
"grunt-contrib-concat": "^0.5.1", "grunt-contrib-concat": "^0.5.1",
"grunt-contrib-copy": "^0.8.0", "grunt-contrib-copy": "^0.8.0",
"grunt-contrib-cssmin": "^0.12.2", "grunt-contrib-cssmin": "^0.12.3",
"grunt-contrib-htmlmin": "^0.4.0", "grunt-contrib-htmlmin": "^0.4.0",
"grunt-contrib-jshint": "^0.11.2", "grunt-contrib-jshint": "^0.11.2",
"grunt-contrib-less": "^1.0.1", "grunt-contrib-less": "^1.0.1",
@@ -43,9 +43,9 @@
"grunt-inline-angular-templates": "^0.1.5", "grunt-inline-angular-templates": "^0.1.5",
"grunt-line-remover": "^0.0.2", "grunt-line-remover": "^0.0.2",
"grunt-mocha-test": "^0.12.7", "grunt-mocha-test": "^0.12.7",
"grunt-replace": "^0.8.0", "grunt-replace": "^0.9.2",
"grunt-usemin": "^3.0.0", "grunt-usemin": "^3.0.0",
"grunt-webfont": "^0.5.2", "grunt-webfont": "^0.5.3",
"matchdep": "^0.3.0", "matchdep": "^0.3.0",
"mocha": "^2.2.4", "mocha": "^2.2.4",
"request": "^2.55.0", "request": "^2.55.0",
+1 -1
View File
@@ -12,7 +12,7 @@ sudo apt-get install -y nodejs
source ~/.profile source ~/.profile
# Installation of some packages globally # Installation of some packages globally
npm install bower forever grunt-cli phantomjs -g npm install bower forever grunt-cli -g
source ~/.profile source ~/.profile
# Installation of YellowLabTools # Installation of YellowLabTools
-2
View File
@@ -18,8 +18,6 @@ bower install --config.interactive=false --allow-root
# Front-end compilation # Front-end compilation
rm -rf front/build rm -rf front/build
npm install grunt-cli -g
npm install phantomjs -g
grunt build grunt build
# Restart the server # Restart the server
+114 -2
View File
@@ -97,7 +97,11 @@ describe('api', function() {
url: wwwUrl + '/simple-page.html', url: wwwUrl + '/simple-page.html',
waitForResponse: true, waitForResponse: true,
screenshot: true, screenshot: true,
device: 'tablet' device: 'tablet',
//waitForSelector: '*',
cookie: 'foo=bar',
authUser: 'joe',
authPass: 'secret'
}, },
json: true, json: true,
headers: { headers: {
@@ -165,8 +169,12 @@ describe('api', function() {
body.should.have.a.property('javascriptExecutionTree').that.is.an('object'); body.should.have.a.property('javascriptExecutionTree').that.is.an('object');
body.javascriptExecutionTree.should.deep.equal({}); body.javascriptExecutionTree.should.deep.equal({});
// Check if the device is set to tablet // 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('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('authUser').that.equals('joe');
body.params.options.should.have.a.property('authPass').that.equals('secret');
// Check if the screenshot temporary file was correctly removed // Check if the screenshot temporary file was correctly removed
body.params.options.should.not.have.a.property('screenshot'); body.params.options.should.not.have.a.property('screenshot');
@@ -536,6 +544,110 @@ describe('api', function() {
}); });
it('should return the entire object and exclude toolsResults', function(done) {
this.timeout(5000);
request({
method: 'GET',
url: serverUrl + '/api/results/' + asyncRunId + '?exclude=toolsResults',
json: true,
}, function(error, response, body) {
if (!error && response.statusCode === 200) {
body.should.have.a.property('runId').that.equals(asyncRunId);
body.should.have.a.property('params').that.is.an('object');
body.should.have.a.property('scoreProfiles').that.is.an('object');
body.should.have.a.property('rules').that.is.an('object');
body.should.have.a.property('javascriptExecutionTree').that.is.an('object');
body.should.not.have.a.property('toolsResults').that.is.an('object');
done();
} else {
done(error || response.statusCode);
}
});
});
it('should return the entire object and exclude params and toolsResults', function(done) {
this.timeout(5000);
request({
method: 'GET',
url: serverUrl + '/api/results/' + asyncRunId + '?exclude=toolsResults,params',
json: true,
}, function(error, response, body) {
if (!error && response.statusCode === 200) {
body.should.have.a.property('runId').that.equals(asyncRunId);
body.should.have.a.property('scoreProfiles').that.is.an('object');
body.should.have.a.property('rules').that.is.an('object');
body.should.have.a.property('javascriptExecutionTree').that.is.an('object');
body.should.not.have.a.property('params').that.is.an('object');
body.should.not.have.a.property('toolsResults').that.is.an('object');
done();
} else {
done(error || response.statusCode);
}
});
});
it('should return the entire object and don\'t exclude anything', function(done) {
this.timeout(5000);
request({
method: 'GET',
url: serverUrl + '/api/results/' + asyncRunId + '?exclude=',
json: true,
}, function(error, response, body) {
if (!error && response.statusCode === 200) {
body.should.have.a.property('runId').that.equals(asyncRunId);
body.should.have.a.property('scoreProfiles').that.is.an('object');
body.should.have.a.property('rules').that.is.an('object');
body.should.have.a.property('javascriptExecutionTree').that.is.an('object');
body.should.have.a.property('params').that.is.an('object');
body.should.have.a.property('toolsResults').that.is.an('object');
done();
} else {
done(error || response.statusCode);
}
});
});
it('should return the entire object and don\'t exclude anything', function(done) {
this.timeout(5000);
request({
method: 'GET',
url: serverUrl + '/api/results/' + asyncRunId + '?exclude=null',
json: true,
}, function(error, response, body) {
if (!error && response.statusCode === 200) {
body.should.have.a.property('runId').that.equals(asyncRunId);
body.should.have.a.property('scoreProfiles').that.is.an('object');
body.should.have.a.property('rules').that.is.an('object');
body.should.have.a.property('javascriptExecutionTree').that.is.an('object');
body.should.have.a.property('params').that.is.an('object');
body.should.have.a.property('toolsResults').that.is.an('object');
done();
} else {
done(error || response.statusCode);
}
});
});
it('should retrieve the screenshot', function(done) { it('should retrieve the screenshot', function(done) {
this.timeout(5000); this.timeout(5000);
+34
View File
@@ -166,6 +166,40 @@ describe('offendersHelpers', function() {
]); ]);
}); });
it('should transform another backtrace syntax into an array', function() {
var result = offendersHelpers.backtraceToArray('phantomjs://webpage.evaluate():38 / e (http://s7.addthis.com/js/300/addthis_widget.js:1) / a (http://s7.addthis.com/js/300/addthis_widget.js:1) / http://s7.addthis.com/js/300/addthis_widget.js:3 / e (http://s7.addthis.com/js/300/addthis_widget.js:1) / http://s7.addthis.com/js/300/addthis_widget.js:8');
result.should.deep.equal([
{
file: 'phantomjs://webpage.evaluate()',
line: 38
},
{
functionName: 'e',
file: 'http://s7.addthis.com/js/300/addthis_widget.js',
line: 1
},
{
functionName: 'a',
file: 'http://s7.addthis.com/js/300/addthis_widget.js',
line: 1
},
{
file: 'http://s7.addthis.com/js/300/addthis_widget.js',
line: 3
},
{
functionName: 'e',
file: 'http://s7.addthis.com/js/300/addthis_widget.js',
line: 1
},
{
file: 'http://s7.addthis.com/js/300/addthis_widget.js',
line: 8
}
]);
});
it('should return null if it fails', function() { it('should return null if it fails', function() {
var result = offendersHelpers.backtraceToArray('http://pouet.com/js/jquery.footer-transverse-min-v1.0.20.js:1 /http://pouet.com/js/main.js:1'); var result = offendersHelpers.backtraceToArray('http://pouet.com/js/jquery.footer-transverse-min-v1.0.20.js:1 /http://pouet.com/js/main.js:1');
+2
View File
@@ -20,6 +20,7 @@
// Let's start the spaghetti code!!! // Let's start the spaghetti code!!!
$('li').bind('mouseover', function() {});
$('ul').find('li'); $('ul').find('li');
$('li', $('#foo')); $('li', $('#foo'));
var foo = document.getElementById('foo'); var foo = document.getElementById('foo');
@@ -102,6 +103,7 @@
$tbodytr.on('click', handler); $tbodytr.on('click', handler);
$tbody.on('click', 'tr', handler); $tbody.on('click', 'tr', handler);
$tbody.on('click', {someData: 'Joe'}, handler); $tbody.on('click', {someData: 'Joe'}, handler);
$tbody.on({event1: handler, event2: handler});
$tbodytr.off('click', handler); $tbodytr.off('click', handler);
$tbodytr.off('click', 'tr'); $tbodytr.off('click', 'tr');
$tbodytr.off('click'); $tbodytr.off('click');
+18
View File
@@ -0,0 +1,18 @@
<html>
<head>
<title>Scroll page</title>
</head>
<body>
<h1>Simple page</h1>
<script>
window.onscroll = function() {
document.getElementsByTagName('window.onscroll');
};
window.addEventListener('scroll', function() {
document.getElementsByTagName('scroll on window');
});
</script>
</body>
</html>