diff --git a/.travis.yml b/.travis.yml index 6a2e41f..b184fdb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,5 +3,4 @@ node_js: - "0.10.33" before_install: - "npm install -g grunt-cli" - - "npm install -g phantomjs" install: npm install \ No newline at end of file diff --git a/bin/cli.js b/bin/cli.js index b22da78..58a8fb4 100644 --- a/bin/cli.js +++ b/bin/cli.js @@ -15,6 +15,10 @@ var cli = meow({ ' --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.', + ' --auth-pass Basic HTTP authentication password.', '' ].join('\n'), pkg: '../package.json' @@ -24,8 +28,7 @@ var cli = meow({ // Check parameters if (cli.input.length < 1) { - console.error('Incorrect parameters: url not provided'); - process.exit(1); + cli.showHelp(); } var url = cli.input[0]; @@ -54,6 +57,16 @@ if (cli.flags.jsDeepAnalysis === true || cli.flags.jsDeepAnalysis === 'true') { // Device simulation 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) { 'use strict'; diff --git a/bower.json b/bower.json index 15139ff..acfc53c 100644 --- a/bower.json +++ b/bower.json @@ -6,7 +6,7 @@ "angular-resource": "~1.3.15", "angular-sanitize": "~1.3.15", "angular-animate": "~1.3.15", - "angular-local-storage": "~0.1.5" + "angular-local-storage": "~0.2.0" }, "resolutions": { "angular": "~1.3.8" diff --git a/front/src/css/icons.css b/front/src/css/icons.css index 1d55bd0..fab4f27 100644 --- a/front/src/css/icons.css +++ b/front/src/css/icons.css @@ -1,6 +1,6 @@ @font-face { font-family: "icons"; - src: url("data:application/x-font-woff;charset=utf-8;base64,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") format("woff"); + src: url("data:application/x-font-woff;charset=utf-8;base64,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") format("woff"); font-weight: normal; font-style: normal; } diff --git a/front/src/css/index.css b/front/src/css/index.css index 3548240..cd85516 100644 --- a/front/src/css/index.css +++ b/front/src/css/index.css @@ -13,11 +13,11 @@ width: 50%; } .launchBtn { - background: #e74c3c; + background: #ffa319; color: #fff; } .launchBtn:focus { - background: #ffa319; + background: #e74c3c; } .launchBtn.disabled { background: #deaca6; @@ -25,6 +25,18 @@ .launchBtn.disabled:focus { 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 { margin-top: 3em; } @@ -53,10 +65,86 @@ margin: 0.2em 0 0.1em; 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 { display: table; width: 50%; - margin: 8em auto 0; + margin: 6em auto 0; font-size: 0.9em; color: #413; } diff --git a/front/src/css/main.css b/front/src/css/main.css index fc20382..fed5912 100644 --- a/front/src/css/main.css +++ b/front/src/css/main.css @@ -1,6 +1,6 @@ @font-face { font-family: "icons"; - src: url("data:application/x-font-woff;charset=utf-8;base64,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") format("woff"); + src: url("data:application/x-font-woff;charset=utf-8;base64,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") format("woff"); font-weight: normal; font-style: normal; } @@ -198,6 +198,9 @@ a.linkButton { .screenshotWrapper .screenshotImage { width: 100%; } +.screenshotWrapper .screenshotError { + color: #fff; +} .screenshotWrapper.desktop { border: 0.2em solid #AAA; padding: 0.5em; diff --git a/front/src/css/rule.css b/front/src/css/rule.css index 26b009b..0903840 100644 --- a/front/src/css/rule.css +++ b/front/src/css/rule.css @@ -82,7 +82,11 @@ font-size: 3em; margin-bottom: 1em; } -.offenders .offenderButton { +.rule .startTime { + display: none; +} +.offendersTable .offenderButton, +.value .offenderButton { display: inline-block; position: relative; background: #efe; @@ -91,16 +95,19 @@ border-radius: 0.4em; z-index: 1; } -.offenders .offenderButton.opens { +.offendersTable .offenderButton.opens, +.value .offenderButton.opens { padding-right: 0.75em; } -.offenders .offenderButton.opens:after { +.offendersTable .offenderButton.opens:after, +.value .offenderButton.opens:after { position: relative; left: 0.5em; content: '\25BC'; font-size: 0.8em; } -.offenders .offenderButton > div { +.offendersTable .offenderButton > div, +.value .offenderButton > div { display: none; position: absolute; right: 0; @@ -111,28 +118,35 @@ border-top: 1px solid #999; z-index: 2; } -.offenders .offenderButton .domTree { +.offendersTable .offenderButton .domTree, +.value .offenderButton .domTree { text-align: left; white-space: nowrap; } -.offenders .offenderButton .domTree > div { +.offendersTable .offenderButton .domTree > div, +.value .offenderButton .domTree > div { margin: 0.5em; } -.offenders .offenderButton .domTree > div div { +.offendersTable .offenderButton .domTree > div div, +.value .offenderButton .domTree > div div { margin-left: 1em; } -.offenders .offenderButton .backtrace, -.offenders .offenderButton .cssFileAndLine { +.offendersTable .offenderButton .backtrace, +.value .offenderButton .backtrace, +.offendersTable .offenderButton .cssFileAndLine, +.value .offenderButton .cssFileAndLine { white-space: nowrap; padding: 0.5em; } -.offenders .offenderButton.opens:hover { +.offendersTable .offenderButton.opens:hover, +.value .offenderButton.opens:hover { border-bottom-left-radius: 0; border-bottom-right-radius: 0; background: #ffe0cc; z-index: 2; } -.offenders .offenderButton.opens:hover > div { +.offendersTable .offenderButton.opens:hover > div, +.value .offenderButton.opens:hover > div { display: block; background: #ffe0cc; } diff --git a/front/src/js/controllers/dashboardCtrl.js b/front/src/js/controllers/dashboardCtrl.js index a07d1e6..0b5732e 100644 --- a/front/src/js/controllers/dashboardCtrl.js +++ b/front/src/js/controllers/dashboardCtrl.js @@ -8,7 +8,7 @@ dashboardCtrl.controller('DashboardCtrl', ['$scope', '$rootScope', '$routeParams function loadResults() { // Load result if needed 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; $scope.result = result; init(); diff --git a/front/src/js/controllers/indexCtrl.js b/front/src/js/controllers/indexCtrl.js index 230bac7..51e4741 100644 --- a/front/src/js/controllers/indexCtrl.js +++ b/front/src/js/controllers/indexCtrl.js @@ -3,7 +3,6 @@ var indexCtrl = angular.module('indexCtrl', []); indexCtrl.controller('IndexCtrl', ['$scope', 'Settings', 'API', function($scope, Settings, API) { $scope.settings = Settings.getMergedSettings(); - console.log($scope.settings); $scope.launchTest = function() { if ($scope.url) { diff --git a/front/src/js/controllers/ruleCtrl.js b/front/src/js/controllers/ruleCtrl.js index 12d97bc..1d4c2c2 100644 --- a/front/src/js/controllers/ruleCtrl.js +++ b/front/src/js/controllers/ruleCtrl.js @@ -9,7 +9,7 @@ ruleCtrl.controller('RuleCtrl', ['$scope', '$rootScope', '$routeParams', '$locat function loadResults() { // Load result if needed 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; $scope.result = result; init(); diff --git a/front/src/js/controllers/screenshotCtrl.js b/front/src/js/controllers/screenshotCtrl.js index 0d2b60d..6bd5fbf 100644 --- a/front/src/js/controllers/screenshotCtrl.js +++ b/front/src/js/controllers/screenshotCtrl.js @@ -7,7 +7,7 @@ screenshotCtrl.controller('ScreenshotCtrl', ['$scope', '$rootScope', '$routePara function loadResults() { // Load result if needed 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; $scope.result = result; }, function(err) { diff --git a/front/src/js/controllers/timelineCtrl.js b/front/src/js/controllers/timelineCtrl.js index 799da5a..3887ff1 100644 --- a/front/src/js/controllers/timelineCtrl.js +++ b/front/src/js/controllers/timelineCtrl.js @@ -7,7 +7,7 @@ timelineCtrl.controller('TimelineCtrl', ['$scope', '$rootScope', '$routeParams', function loadResults() { // Load result if needed 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; $scope.result = result; render(); diff --git a/front/src/js/directives/offendersDirectives.js b/front/src/js/directives/offendersDirectives.js index f226bb7..df2b35c 100644 --- a/front/src/js/directives/offendersDirectives.js +++ b/front/src/js/directives/offendersDirectives.js @@ -109,7 +109,7 @@ } else if (context.length > 3) { html += ' and ' + (context.length - 2) + ' more...'; } - return html + '}'; + return html + ')'; } function isJQuery(node) { @@ -119,6 +119,22 @@ function getNonJQueryHTML(node, onASingleLine) { var type = node.data.type; + if (node.windowPerformance) { + switch (type) { + case 'documentScroll': + return '(triggering the scroll event on document)'; + + case 'windowScroll': + return '(triggering the scroll event on window)'; + + case 'window.onscroll': + return '(calling the window.onscroll function)'; + + default: + return ''; + } + } + if (!node.data.callDetails) { return ''; } @@ -153,6 +169,18 @@ case 'error': 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: return ''; } @@ -318,7 +346,9 @@ case 'jQuery - on': case 'jQuery - one': - if (args[1]) { + if (isStringOfObject(args[0])) { + return '' + args[0].replace(/"\(function\)"/g, '(function)') + ''; + } else if (args[1] && isPureString(args[1])) { return 'bind ' + args[0] + ' on ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by ' + args[1] + ''; } else { return 'bind ' + args[0] + ' on ' + getJQueryContextButtonHTML(ctxt, onASingleLine); @@ -518,9 +548,6 @@ } break; - case 'jQuery - onDOMReady': - return '(function)'; - default: return ''; } @@ -643,8 +670,8 @@ if (node.data.callDetails.context && node.data.callDetails.context.length === 0) { html += '

Called on 0 jQuery element

Useless function call, as the jQuery object is empty.

'; - } else if (node.data.type === 'jQuery - bind' && node.data.callDetails.context.length > 5) { - html += '

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).

'; + } else if (node.eventNotDelegated) { + html += '

This binding should use Event Delegation instead of binding each element one by one.

'; } if (node.data.resultsNumber === 0) { @@ -674,7 +701,7 @@ function getProfilerLineHTML(index, node) { return '
' + (index + 1) + '
' + '
' + node.data.type + (node.children ? '
' + recursiveChildrenHTML(node) + '
' : '') + '
' + - '
' + getTimelineParamsHTML(node, false) + '
' + + '
' + getTimelineParamsHTML(node, false) + '
' + '
' + getTimelineDetailsHTML(node) + '
' + '
' + numberWithCommas(node.data.timestamp, 0) + ' ms
'; } diff --git a/front/src/js/services/apiService.js b/front/src/js/services/apiService.js index 4aa145f..c0234bc 100644 --- a/front/src/js/services/apiService.js +++ b/front/src/js/services/apiService.js @@ -5,13 +5,32 @@ apiService.factory('API', ['$location', 'Runs', 'Results', function($location, R return { launchTest: function(url, settings) { - Runs.save({ + var runObject = { url: url, waitForResponse: false, screenshot: true, jsTimeline: true, - device: settings.device - }, function(data) { + device: settings.device, + 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); }, function(response) { if (response.status === 429) { diff --git a/front/src/js/services/settingsService.js b/front/src/js/services/settingsService.js index 6ecad61..81f9f5b 100644 --- a/front/src/js/services/settingsService.js +++ b/front/src/js/services/settingsService.js @@ -6,7 +6,8 @@ settingsService.factory('Settings', ['localStorageService', function(localStorag getMergedSettings: function() { var defaultSettings = { - device: 'desktop' + device: 'desktop', + showAdvanced: false }; var savedValues = localStorageService.get('settings'); diff --git a/front/src/less/icons.less b/front/src/less/icons.less index cd851ca..40b7108 100644 --- a/front/src/less/icons.less +++ b/front/src/less/icons.less @@ -4,7 +4,7 @@ @font-face { font-family:"icons"; - src:url("data:application/x-font-woff;charset=utf-8;base64,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") format("woff"); + src:url("data:application/x-font-woff;charset=utf-8;base64,d09GRgABAAAAABF0AA4AAAAAK9AAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABPUy8yAAABRAAAAEMAAABWQLtM2mNtYXAAAAGIAAAAOgAAAUriHBC2Y3Z0IAAAAcQAAAAUAAAAIv+YA5pmcGdtAAAB2AAABacAAAuX2BTb8Gdhc3AAAAeAAAAACAAAAAgAAAAQZ2x5ZgAAB4gAAAdJAAAZuvXTIk1oZWFkAAAO1AAAADIAAAA2A+vwjmhoZWEAAA8IAAAAIAAAACQD/QHAaG10eAAADygAAAAPAAAALBYAAABsb2NhAAAPOAAAABgAAAAYH8gkR21heHAAAA9QAAAAIAAAACACfQyHbmFtZQAAD3AAAAEdAAAB8hTbwvhwb3N0AAAQkAAAAGIAAAB/4g/aQ3ByZXAAABD0AAAAfQAAAIqMht7EeJxjYGRiYJzAwMrAwejCmMbAwOAOpb8ySDK0MDAwMbAyM2AFAWmuKQwOHxk/cjEe+H+AQY/xIIMDUJgRJAcAixQLGgB4nGNgYGBmgGAZBkYGEHAB8hjBfBYGDSDNBqQZGZgYGD5y/f8PUvCREUTzM0DVAwEjG8OIBwB5MwbBAAB4nGNgwAA8EMh48P8BEAYAFnAFM3icrVb5c9NGFJZ8JE5CjpKDFvVYsXGa2iuTUggGTAiSZRfcw7laCUorxU56H9Ayw9/gv+bJtDP0N/60fm9lm0CSdoZpJqP37e6nffeTyVCCjL3AD4VoPTNmtlo0tnMvoMsWrYbRoejtBZQpxn8XjILR6ch9y7bJCMnwZL1vmIYXuQ6ZikR06FBGia6g523Krdzrr5qTnt/xt+8HtrStXiCo3Q5s2gwtQVVG1TAUSUqKu7SKrcFK0BqfrzHzeTsQMKIXC5psBxF2BJ9NMlpntB5ZURiGFpnlMJRktIODMHQoqwTuyRVjGJT32gHlpUtj0oX5IZmRQzklYZfoJvl9V/BJqpyfOPc7lC3Z2PdET/Rwd7KWL8KtrSBqW/F2GMgQp5s7AY4sdmqg2aG8onGv3DcyaWjGsJSuRIilG1Nm/5DMDu6nfMmhcSXYyCmv8yxn7Au+gTajkClRXRtZUP3xKcPz3ZI9CvaEejn4k+ktZhkmePA4En5PxpwIHSnD4miSsGDk0ErKFmVcT1VMnfI6LeMtw3rh2tGXzijtUH9qMusHtiXtsGQ7NK2STManblx3aEaBKASd8e7y6wDSDWmaV9tYTWPl0CyumdMhEYhAB3ppxotELxI0g6A5NKdau0GS69bDZZo+kE8cekO1toLWTrpp2dif1/tnVWLMentBMjuL/MUuzZa5SFG6bnKGH9N4kLmETGSL7SDh4MFbt4f0stqSLfHaEFvpOb+C2uedEJ40YX8Tuy+n6pQEJoYxLxEtj4yNvmmaOlfzykiMjL8b0Kx0hU9TKMpJKI5cEUH9X3NzpjFjuG4vSs6Olelx2bqAMC3At/myQ4sqMVkuIc4sz6kky/JNleRYvqWSPMvzKhljaalknOXbKimwfEclEyw/UKJC5gOHSho8dKiswSOH3lUGTZdfw8b3YOO7uFvARpY2bGR5ATaylLCR5TJsZFmEjSxXYCPL92Ejy1XYyFIpUdOl5iionYuExyZ4Oh1oH8X1VlHklMlBJ11EETfFKZmQcVXyGPtXBkrJobVReswlulhK8uaiH2AMsYMfHo3M8eNLSlzR9n4EnukfV4IOO1E57xtLfxr8V9+Q1eSSuQiPLsN/GHyyvSjsuOrQFVU5V3No/b+oKMIO6FeREmOpKCqiyc2LUN7p9ZqyiW4PMNYxFtHR66a5uAD9VUyZJTQI/jWFJrzyQa8ihaj1cNe1F8eikt5BOdwJlqCI+31zK3iaEVlhPc2sZM+HLs/AAqap1GzZQPd5r7ZSxHMoHfYZL+pKynpxF8cZL7aAI55Br74TwyQMZtlADiU0NOAXhNaC+05QItNpl0ODI/Z5FFT+2K24kT0qaiPwbKdT7oUupPw6x0BgJ78yiIGsITQ39DYV0DxCNGSTlXG2ajpk7MAgosZuUBE1fBvZ4sGmYFuGIR8rYnXn6Nc3TdRJFTzIjOQyvjmwwBumJuLP86suDlO5oaSocNQaGMy1sJJUzAU04K3Rdvvo9ubL7BM5txVVyyde6iq6Vu5BMRcLrD3OQVoqVAHVG1XYMLpcXBKlXkGTpNfVMTQww1+jFJv/V/Wx+TxfahIj5Ei+7XBgo8/BGPrfYP9tOQjAwI+Ry024vJg2J77u6MP5Cl1GL358yv4dzFxzYZ6uAN9VdBWixVHzEVfRwKdsGKdPFJcjtQA/VX3MGYDPAEwGn6u+qXfaAHpnizk+wDZzGOwwh8EucxjsMec2wBfMYfAlcxgEzGEQMscDuMccBveZw+Ar5jB4wJwGwNfMYfANcxhEzGEQM8cF2GcOgw5zGHSZw+BA0fVRmA95QRtA32p0C+g7XU9YbGLxvaIbI/YPvNDsHzVi9k8aMfVnRbUR9RdeaOqvGjH1N42Y+lDRzRH1ES809XeNmPqHRkx9rJ5O5DLDH09umQoHlF1uPxl+U5x/ACtxTWMAAAEAAf//AA94nM1YS2/b2BU+h9TDki3ZoihST4oPvaz3W35FZuokflBOMrHsyJU9FZ0iMRBkYBSD2VZdFl1MBkUX3aRZtNNi4EWAme5VoP9iNt500V2Bbtou5F5KViczKJxmTBc2ee+1uNA5upffd77vgARwPqAAB4BAgxXs4IBpcMEseMALPuAhACGIQBQkUCAOSZiHDOSgACWoQA0asAjLsA478Bz/rr3JPOyof0giLVESfQzBkCgGjwFAokF6AlQKrRbKqstomUe7zWLXwZZGx5TNocNUBqedU9M6OLPomnG6dAVncjjrnpnVwR3LU545t+cJzBXQy8x5dWCK6GMZnw5sCXmO5XXg4mUq4OcCT8BfwVDQH9LrKFYxEhYjRwlEFLo1FASXBuFwtAvRqDvaCqlfXKRKnRi5hoLiyY1Ndl/95ebGrZWlpYWFer1aLZeLxXw+m02nU6lEIhaTZVEUhGdPj3qH3cd7D7Y3djZ31m6vrN9aX1peWl5eWlhcWFxcqDfqjUa9WqvWatVypVyplIulYqlUzBfyhUI+m8vmctl0Jp3JpFPzqfn5VCKZSCYTsXgsHo/JiqwosiiJkiQKUSEaFRo2NuP1XHahZL3kQgF5N9rzmGyit4kN8g/5KGCyUS9z7CxWyGzLGFtmkwuYqIrf/huKFAzh+41zePGSpl++GM+dTyjqk85oXu07g17H1pduhnF/ueXwBp19Bw6+b5gh4Nm3k8az/0R98XJYmYQl81/6RrRNJsxsGimQPMi7SDBnBj5XYY1gVIMHAFc5sLjkkxoVnzIZ3opHMfFQxkNV1d7kNnPnVXXQOzvr91EdDgBEU/Z1B/axrJ53dlu3LFN23IohAkUjpceRBouVtugJtILNbrXpSbTDlMM+pafQAc5ph1Ofx2mYcU3P6Gl0gXvW5dYzOAtzntk5PYseYLweRq8oOdoLrM/L6nn0Acf7OL2APPgDvF8vYoDQViColzAE4UgorMuiQAOMKMOllTESkbogSW6p1W5fThzt/fb+471HD7TNe2tq8z0pQ82/B2lUr0gaSbpWKderiQzKNpZbRh9t43ipifFGreLjTXwb5VdzdofD/tqY5l6FYjVUa7Fh/5n6zMy38j7anzvcDnI/H/6z0ERs/uz0449PIWYy7neuhnw0EE/Qb//Oair6+/2BAc7Bd1Zz6bffU3sXw+BXc3igBYf4D1XcRJt17QeU09H9IcV6H6Kfb2MwYN1qobCxg8q69oYl+ugVWGgrZTkmZEFRuBtFirCFhaL/O1eI+D+ThZyj3s0VFalEX7AF+flCFwydYVCF0gVFcStEFf3GSJC2WE9uaIb76i/ub1/OZ529Rx9sH94/1LbW7743n/0f2YzOo0I0D0/UT5nj7ZUayolkQ/E1BKyQZ0QPmYgwTTmstX8aWeF+tJq6G9oYnMNo+eaxqUj7lZLC7ha/cAeF4MZgqBqzNnkGsim4M5itAwdX4zbaOAEP0aLGjnvyaHCbV05UDRFqa0geM0nu5RkbQgyx4+XP/b5KhGaYYX5yTvbBXKIjUYa9cTR8zYb4ueGGESnM9JAwnzn7v0pO4ACOKac6vYGuOzi7jh56U3uTIjT3RQpphVJGNjAWM2xgAxUaFGKt5sfWCizpsbOqixTR+RfeSs5SF+YqNzZXMJOfeKsCGtZKhzmpSF14q9LYWwnIlifmKloZmSsd/NWxuQJSvCPhWOQoSbxVvLuA8fjIWyW6kEi4E4TyTi+SHRnBWCgYO7nB6e6rv/7wcLt1W72cA398dHj84fFuu3WwfbC1ee+uqt3WVpvEQl4bGy5ekQ3jBvLKXATtSq1RI0N2U6xAlZtUNU8l33KLZsqOM1XtuxmiCF730tpKNLqitcdL+i2LaCY0+yqKjLuniuR6+E04Yym+ZQ7N0iaG/rsSP9Y8koccjE8iJ0KbS4dDOOsh9M6wP+yb6vWMLxzRoHoOYE6PS4M2dPCvY3L7HYSliBQ+JrJEFCNPiVBS0FTjl8N3CyW5RIVACoekJyOdVBm1jKoEHNGDSYPr9+M8IydGolJE/OiGZrqvfrrdar6ju9V5vLf7wcNWe7t9b21Fa2rXR2S1q8o66UJSSGNJQV+TnDgZDi4qvDpav7IbTtLx8+EZiteqJoaesWktYp9oifN/EXzZ8E9XwldzhLBD/NsfHz2gXLg5Nku/LSLPcj7+GDhSJJ8CJJCmgNaBSk6qcmrSnp2f1OT0uD1bkyhnZlKUs5OinJsUZTk/Kco+H7MbK1A+YHkfq5cw4PcH9hT0l0f1+EjAYIUU5GCEYAYx2q2i0V4hBVnsgii6RYKzz0dpsjx3QvJkucBHNzPRffWz7Za6ejnMDrqdx2OYrd9Z1VSteev6gFa/aguZlugy7+VsdimPDW+dL3M+u5Ks1u0sZ3SJZJuJaDuHU+ycelxfzzDB4f0gM/O1y+NmnFkn42GCDLlNhdxnw09xwA1bvIAo8PgVNyCioedmBi4jFPNv0bcgRQAAAHicY2BkYGAA4tkvkvvi+W2+MnAzMYDAxbR4AQgdyPD/1/8DTGyMB4FcDgawNABIPQvdAAB4nGNgZGBgPPD/AIMeEwMDw/8vTDwMQBEUwA0Ae20Ev3icY2JgYGAiEgMAAjwAFwAAAAAAAdACfAPMBIAGPAb8CM4JcgsiDN0AAQAAAAsAbAAkAAAAAAACAHIAggB3AAABZAuXAAAAAHicXY69TsMwFIVP+odoEAIhMZulC1L6M/YB2pkO2dPESVslceS4lSoxM/MUzDwFz8WJeyUqbOn6O+ceXxvAA34QoFsBhr52q4cbqgv3SXfCA/Kj8BAhnoVHVC/CY7xiIhziCW+cEAxu6YyRCfdwj1q4T/9deED+EB5y+qfwiP6X8BgxvoVDTILRPjV1u9HFsUysZ19ibdu9qdU8mnm91rW2idOZ2p5VeyoWzuUqt6ZSK1M7XZZGNdYcdOqinXPNcjrNxY9SU2GPFIZ/brGBRoEjSiSwV/4fxUxY73RaYY4Is6v+mv3aZxI4nhkzW5xZW5w4e0HXIafOmTGoSCt/t0uX3IZO43sHOin9CDt/q8ESU+78Xz7yr1e/MPVTYgAAAHicbcFBDsIwDARAb5umLXkLJ17kIFNFMjE4Rv0+B67M0EQ/C/1XiDBhRsKCjBUbdlxQCrvbeVV5xC1V9jEr16RtRFKzV35abSrb+yMjmvU87i7Sc3BVifVk760fRF+VyBj+AAB4nGPw3sFwIihiIyNjX+QGxp0cDBwMyQUbGdicNjEwMmiBGJu5mRg5ICx+BjCL3WkXMwNQmhPI5nDaxeAAYTMzuGxUYewIjNjg0BGxkTnFZaMaiLeLo4GBkcWhIzkkAqQkEgg28zIx8mjtYPzfuoGldyMTUB9rigsAZa0kmwAAAA==") format("woff"); font-weight:normal; font-style:normal; } diff --git a/front/src/less/index.less b/front/src/less/index.less index 553b4e5..a7efe86 100644 --- a/front/src/less/index.less +++ b/front/src/less/index.less @@ -16,10 +16,10 @@ } .launchBtn { - background: #e74c3c; + background: #ffa319; color: #fff; &:focus { - background: #ffa319; + background: #e74c3c; } &.disabled { 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 { margin-top: 3em; .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 { display: table; width: 50%; - margin: 8em auto 0; + margin: 6em auto 0; font-size: 0.9em; color: #413; diff --git a/front/src/less/main.less b/front/src/less/main.less index bf52219..ff5d31c 100644 --- a/front/src/less/main.less +++ b/front/src/less/main.less @@ -139,6 +139,10 @@ a.linkButton { .screenshotImage { width: 100%; } + + .screenshotError { + color: #fff; + } } .screenshotWrapper.desktop { diff --git a/front/src/less/rule.less b/front/src/less/rule.less index 8506d1c..b0aa189 100644 --- a/front/src/less/rule.less +++ b/front/src/less/rule.less @@ -90,7 +90,11 @@ } } -.offenders { +.rule .startTime { + display: none; +} + +.offendersTable, .value { .offenderButton { display: inline-block; position: relative; diff --git a/front/src/views/dashboard.html b/front/src/views/dashboard.html index cdd8a7f..fa97376 100644 --- a/front/src/views/dashboard.html +++ b/front/src/views/dashboard.html @@ -11,9 +11,10 @@
-
+
- + + Screenshot not available
diff --git a/front/src/views/index.html b/front/src/views/index.html index 02caf53..aff58e1 100644 --- a/front/src/views/index.html +++ b/front/src/views/index.html @@ -7,9 +7,69 @@
Choose the simulated device:
-
Desktop
-
Tablet
-
Phone
+
Desktop
+
Tablet
+
Phone
+
+ [ + Advanced settings  ✚ + Hide advanced settings  ✖ + ] + + Currently set: + wait for selector + cookie + authentication + +
+ +
+
+ Cookie + + +
Cookie

Adds a cookie on the main domain.

Example: "bar=foo;domain=url"
+
+
+
+
+ +
diff --git a/front/src/views/rule.html b/front/src/views/rule.html index fcc6ae0..b54833a 100644 --- a/front/src/views/rule.html +++ b/front/src/views/rule.html @@ -47,6 +47,23 @@ {{offender.eventName}} bound to
+
+ Scroll event bound on window + Scroll event bound on document + window.onscroll function declared +
no backtrace
+
+ backtrace +
+
+ {{obj.functionName}}() + + line {{obj.line}} +
+
+
+
+
{{offender.error}}
no backtrace
@@ -62,6 +79,25 @@
+
+ Function {{offender.functionName}} used {{offender.count}} times +
+ +
+ function {{offender.functionName}} used on {{offender.contextLength}} DOM elements without event delegation +
no backtrace
+
+ backtrace +
+
+ {{obj.functionName}}() + + line {{obj.line}} +
+
+
+
+
{{offender.writeFn}} @@ -133,6 +169,11 @@
+
+ + ({{offender.type}}) +
+
cached for {{offender.ttlWithUnit}} {{offender.unit}} @@ -158,6 +199,19 @@
+
+

The table below shows the interactions between the JavaScript and the DOM on a scroll event.

+
+
+
+
Type
+
Params
+
+
+ +
+
+

This is the colors palette, sized by total occurrences:

diff --git a/front/src/views/screenshot.html b/front/src/views/screenshot.html index 31a4b67..7f4de0d 100644 --- a/front/src/views/screenshot.html +++ b/front/src/views/screenshot.html @@ -2,9 +2,10 @@

Screenshot

-
+
- + + Screenshot not available
diff --git a/front/src/views/timeline.html b/front/src/views/timeline.html index dc0398c..497340e 100644 --- a/front/src/views/timeline.html +++ b/front/src/views/timeline.html @@ -7,8 +7,8 @@
-

Javascript Timeline

-

This graph gives a quick view of when the Javascript interactions with the DOM occur during the loading of the page.

+

JavaScript Timeline

+

This graph gives a quick view of when the JavaScript interactions with the DOM occur during the loading of the page.

@@ -45,7 +45,7 @@
Page is complete
-
Executing Javascript and DOM queries here is a bad practice and slows down the DOM construction.
+
Executing JavaScript and DOM queries here is a bad practice and slows down the DOM construction.
Some frameworks do things here, but it's not reliable and should be avoided.
Also known as "document ready". This is where you should execute top-priority scripts, like binding action buttons or launch a video player.
Here you can execute mid-priority tasks. Loading a script with createElement('script') is one way to do so.
@@ -54,9 +54,9 @@
-

Javascript Profiler

+

JavaScript Profiler

- 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.

diff --git a/lib/metadata/policies.js b/lib/metadata/policies.js index 461f17f..a203b1c 100644 --- a/lib/metadata/policies.js +++ b/lib/metadata/policies.js @@ -66,6 +66,15 @@ var policies = { }; } }, + /*"DOMaccesses": { + "tool": "jsExecutionTransformer", + "label": "DOM access", + "message": "

TODO

TODO

", + "isOkThreshold": 50, + "isBadThreshold": 2000, + "isAbnormalThreshold": 3000, + "hasOffenders": false + },*/ "DOMinserts": { "tool": "phantomas", "label": "DOM inserts", @@ -195,6 +204,49 @@ var policies = { }; } }, + "eventsScrollBound": { + "tool": "phantomas", + "label": "Scroll events bound", + "message": "

Number of 'scroll' event listeners binded to 'window' or 'document'.

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.

", + "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": "

This rule counts the number of DOM-accessing functions calls, such as queries, readings, writings, bindings and jQuery functions.

Two scroll events are triggered quickly, one after the other, and only the second one is analyzed so throttled functions are ignored.

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.

Reduce the number of DOM accesses inside scroll listeners. Put DOM queries outside them when possible. Use throttling or deboucing.

", + "isOkThreshold": 1, + "isBadThreshold": 12, + "isAbnormalThreshold": 25, + "hasOffenders": true, + "offendersTransformFn": function(offenders) { + return offenders; + } + }, "jsErrors": { "tool": "phantomas", "label": "JavaScript errors", @@ -371,39 +423,79 @@ var policies = { }, "jQueryVersionsLoaded": { "tool": "phantomas", - "label": "Several versions loaded", + "label": "Several jQuery loaded", "message": "

jQuery is a heavy library. You should never load jQuery more than once on the same page.

", "isOkThreshold": 1, "isBadThreshold": 2, "isAbnormalThreshold": 2, "hasOffenders": true }, + "jQueryFunctionsUsed": { + "tool": "jsExecutionTransformer", + "label": "jQuery usage", + "message": "

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.

If only a few functions are used, why not trying to get rid of jQuery? Have a look at http://youmightnotneedjquery.com.

", + "isOkThreshold": 15, + "isBadThreshold": 6, + "isAbnormalThreshold": 0, + "hasOffenders": true + }, + "jQueryNotDelegatedEvents": { + "tool": "jsExecutionTransformer", + "label": "Events not delegated", + "message": "

This is the number of events that are bound with the .bind() or the .on() function without using event delegation.

This means jQuery binds each element contained in the object one by one. This is bad for performance.

", + "isOkThreshold": 1, + "isBadThreshold": 100, + "isAbnormalThreshold": 180, + "hasOffenders": true + }, "cssParsingErrors": { "tool": "phantomas", "label": "CSS syntax error", "message": "

Yellow Lab Tools failed to parse a CSS file. I doubt the problem comes from the css parser.

Maybe a CSS validator can help you.

", "isOkThreshold": 0, "isBadThreshold": 1, - "isAbnormalThreshold": 1, + "isAbnormalThreshold": 8, "hasOffenders": true, "offendersTransformFn": function(offenders) { return { count: offenders.length, list: offenders.map(function(offender) { - var parts = /^(?:(?:<([^ \(]*)>|\[inline CSS\]) ?)?(?:\((((?! @ ).)*)(?: @ (\d+):(\d+))?\))?$/.exec(offender); - - if (!parts) { - debug('cssParsingErrors offenders transform function error with "%s"', offender); + if (offender === '[inline CSS] (Empty CSS was provided)') { 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 { - error: parts[2], - file: parts[1] || null, - line: (parts[4] && parts[5]) ? parseInt(parts[4], 10) : null, - column: (parts[4] && parts[5]) ? parseInt(parts[5], 10) : null + parseError: offender }; }) }; @@ -413,9 +505,9 @@ var policies = { "tool": "phantomas", "label": "Rules count", "message": "

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.

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.

", - "isOkThreshold": 500, - "isBadThreshold": 2500, - "isAbnormalThreshold": 4000, + "isOkThreshold": 750, + "isBadThreshold": 3000, + "isAbnormalThreshold": 4500, "hasOffenders": false }, "cssComplexSelectors": { @@ -423,7 +515,7 @@ var policies = { "label": "Complex selectors", "message": "

Complex selectors are CSS selectors with 4 or more expressions, like \"#header ul li .foo\".

They are adding more work for the browser, and this could be avoided by simplifying selectors.

", "isOkThreshold": 0, - "isBadThreshold": 500, + "isBadThreshold": 600, "isAbnormalThreshold": 2000, "hasOffenders": true, "offendersTransformFn": function(offenders) { @@ -441,8 +533,8 @@ var policies = { "label": "Complex attributes selector", "message": "

Complex attributes selectors are one of these:

  • .foo[type*=bar] (contains bar)
  • .foo[type^=bar] (starts with bar)
  • .foo[type|=bar] (starts with bar or bar-)
  • .foo[type$=bar] (ends with bar)
  • .foo[type~=bar baz] (bar or baz)

Their matching process needs more CPU and it has a cost on performances.

", "isOkThreshold": 0, - "isBadThreshold": 50, - "isAbnormalThreshold": 100, + "isBadThreshold": 75, + "isAbnormalThreshold": 150, "hasOffenders": true, "offendersTransformFn": function(offenders) { return { @@ -526,8 +618,8 @@ var policies = { "label": "Duplicated selectors", "message": "

This is when two or more selectors are strictly identical and should be merged.

", "isOkThreshold": 0, - "isBadThreshold": 40, - "isAbnormalThreshold": 80, + "isBadThreshold": 50, + "isAbnormalThreshold": 100, "hasOffenders": true, "offendersTransformFn": function(offenders) { return { @@ -555,8 +647,8 @@ var policies = { "label": "Duplicated properties", "message": "

This is the number of property definitions duplicated within a selector.

", "isOkThreshold": 0, - "isBadThreshold": 50, - "isAbnormalThreshold": 100, + "isBadThreshold": 60, + "isAbnormalThreshold": 120, "hasOffenders": true, "offendersTransformFn": function(offenders) { return { @@ -589,7 +681,7 @@ var policies = { "label": "Empty rules", "message": "

Very easy to fix: remove all empty rules.

", "isOkThreshold": 0, - "isBadThreshold": 40, + "isBadThreshold": 50, "isAbnormalThreshold": 100, "hasOffenders": true, "offendersTransformFn": function(offenders) { @@ -643,8 +735,8 @@ var policies = { "label": "Uses of !important", "message": "

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.

", "isOkThreshold": 0, - "isBadThreshold": 50, - "isAbnormalThreshold": 150, + "isBadThreshold": 75, + "isAbnormalThreshold": 200, "hasOffenders": true, "offendersTransformFn": function(offenders) { return { @@ -678,7 +770,7 @@ var policies = { "label": "Old IE fixes", "message": "

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.

IE6:

  • * html
  • html > body (everything but IE6)

IE7:

  • *height: 123px;
  • height: 123px !ie;

IE9:

  • -ms-filter
  • progid:DXImageTransform.Microsoft

", "isOkThreshold": 0, - "isBadThreshold": 50, + "isBadThreshold": 75, "isAbnormalThreshold": 300, "hasOffenders": true, "offendersTransformFn": function(offenders) { @@ -735,7 +827,7 @@ var policies = { "label": "Old prefixes", "message": "

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.

", "isOkThreshold": 0, - "isBadThreshold": 50, + "isBadThreshold": 75, "isAbnormalThreshold": 300, "hasOffenders": true, "offendersTransformFn": function(offenders) { @@ -771,7 +863,7 @@ var policies = { "label": "Universal selectors", "message": "

Universal selectors are the most expensive CSS selectors.

More informations here.

", "isOkThreshold": 0, - "isBadThreshold": 40, + "isBadThreshold": 50, "isAbnormalThreshold": 150, "hasOffenders": true, "offendersTransformFn": function(offenders) { @@ -789,7 +881,7 @@ var policies = { "label": "Redundant body selectors", "message": "

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.

", "isOkThreshold": 0, - "isBadThreshold": 50, + "isBadThreshold": 60, "isAbnormalThreshold": 200, "hasOffenders": true, "offendersTransformFn": function(offenders) { @@ -810,7 +902,7 @@ var policies = { "label": "Redundant tags selectors", "message": "

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 always inside a \"ul\". Same thing for \"tr td\", \"select option\", ...

Lowering compexity in CSS selectors can make the page load a little faster.

", "isOkThreshold": 0, - "isBadThreshold": 50, + "isBadThreshold": 60, "isAbnormalThreshold": 200, "hasOffenders": true, "offendersTransformFn": function(offenders) { @@ -1056,6 +1148,35 @@ var policies = { "isAbnormalThreshold": 1, "hasOffenders": true }, + "assetsNotGzipped": { + "tool": "phantomas", + "label": "Not gzipped", + "message": "

This is the number of requests that should be compressed with gzip but aren't.

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. Here is a list of Content-Types that should be gzipped.

", + "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": { "tool": "phantomas", "label": "Connections closed", @@ -1176,7 +1297,6 @@ var policies = { domain: parts[1], requests: parseInt(parts[2]) }; - }) }; } diff --git a/lib/metadata/scoreProfileGeneric.json b/lib/metadata/scoreProfileGeneric.json index e1b4b57..aa2590c 100644 --- a/lib/metadata/scoreProfileGeneric.json +++ b/lib/metadata/scoreProfileGeneric.json @@ -19,6 +19,13 @@ "eventsBound": 1 } }, + "scroll": { + "label": "Scroll bottlenecks", + "policies": { + "eventsScrollBound": 1, + "DOMaccessesOnScroll": 4 + } + }, "badJavascript": { "label": "Bad JavaScript", "policies": { @@ -28,17 +35,13 @@ "globalVariables": 0.5 } }, - "jQueryVersion": { - "label": "jQuery version", + "jQuery": { + "label": "jQuery", "policies": { - "jQueryVersion": 5, - "jQueryVersionsLoaded": 0.1 - } - }, - "cssSyntaxError": { - "label": "CSS syntax errors", - "policies": { - "cssParsingErrors": 1 + "jQueryVersion": 1, + "jQueryVersionsLoaded": 1, + "jQueryFunctionsUsed": 1, + "jQueryNotDelegatedEvents": 1 } }, "cssComplexity": { @@ -53,6 +56,7 @@ "badCSS": { "label": "Bad CSS", "policies": { + "cssParsingErrors": 4, "cssImports": 3, "cssDuplicatedSelectors": 2, "cssDuplicatedProperties": 1, @@ -92,6 +96,7 @@ "label": "Network", "policies": { "notFound": 2, + "assetsNotGzipped": 1.5, "closedConnections": 2, "multipleRequests": 2, "cachingNotSpecified": 1, @@ -104,8 +109,9 @@ "globalScore": { "domComplexity": 1, "domManipulations": 2, + "scroll": 1, "badJavascript": 1, - "jQueryVersion": 1, + "jQuery": 1, "cssSyntaxError": 1, "cssComplexity": 1, "badCSS": 1, diff --git a/lib/offendersHelpers.js b/lib/offendersHelpers.js index 92405d9..a3630e0 100644 --- a/lib/offendersHelpers.js +++ b/lib/offendersHelpers.js @@ -115,7 +115,7 @@ var OffendersHelpers = function() { var parts = null; for (var i=0 ; i 0); + if (hasjQuery) { + metrics.jQueryCalls = 0; + metrics.jQueryFunctionsUsed = 0; + metrics.jQueryCallsOnEmptyObject = 0; + metrics.jQueryNotDelegatedEvents = 0; + + offenders.jQueryFunctionsUsed = []; + offenders.jQueryNotDelegatedEvents = []; + } try { + + debug('Starting JS execution transformation'); javascriptExecutionTree = JSON.parse(data.toolsResults.phantomas.offenders.javascriptExecutionTree[0]); if (javascriptExecutionTree.children) { javascriptExecutionTree.children.forEach(function(node) { - // Mark abnormal things with a warning flag - var contextLenght = (node.data.callDetails && node.data.callDetails.context) ? node.data.callDetails.context.length : null; - if ((node.data.type === 'jQuery - bind' && contextLenght > 5) || - node.data.resultsNumber === 0 || - contextLenght === 0) { + var contextLength = (node.data.callDetails && node.data.callDetails.context) ? node.data.callDetails.context.length : null; + + if (isABindWithoutEventDelegation(node, contextLength)) { + metrics.jQueryNotDelegatedEvents += contextLength; + offenders.jQueryNotDelegatedEvents.push({ + functionName: node.data.type.substring(9), + contextLength: contextLength, + backtrace: offendersHelpers.backtraceToArray(node.data.backtrace) + }); 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 @@ -57,33 +93,60 @@ var jsExecutionTransformer = function() { break; } - // Change the list of dom paths into a tree - treeRecursiveParser(node, 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); - } + // Transform domPaths into objects + changeListOfDomPaths(node); - 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]); - } + // Count the number of DOM accesses, by counting the tree leafs + metrics.DOMaccesses += countTreeLeafs(node); + }); - if (node.data.type === 'insertBefore') { - node.data.callDetails.arguments[1] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[1]); + // Count the number of different jQuery functions called + 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('JS execution transformation complete'); + debug('Scroll execution transformation complete'); } catch(err) { throw err; } data.javascriptExecutionTree = javascriptExecutionTree; + data.toolsResults.jsExecutionTransformer = { - metrics: metrics + metrics: metrics, + offenders: offenders }; return data; @@ -97,6 +160,63 @@ var jsExecutionTransformer = function() { } 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(); \ No newline at end of file diff --git a/lib/tools/phantomas/custom_modules/core/scopeYLT/scopeYLT.js b/lib/tools/phantomas/custom_modules/core/scopeYLT/scopeYLT.js index 4ec31ef..59e21a3 100644 --- a/lib/tools/phantomas/custom_modules/core/scopeYLT/scopeYLT.js +++ b/lib/tools/phantomas/custom_modules/core/scopeYLT/scopeYLT.js @@ -104,10 +104,11 @@ exports.module = function(phantomas) { var root = new ContextTreeNode(null, {type: 'main'}); var currentContext = root; + var depth = 0; + if (deepAnalysis) { phantomas.log('Entering deep Javascript analysis mode'); } - var depth = 0; // Add a child but don't enter his context function pushContext(data) { @@ -176,6 +177,13 @@ exports.module = function(phantomas) { return root; } + + // Empty the tree + function resetTree() { + root = new ContextTreeNode(null, {type: 'main'}); + currentContext = root; + depth = 0; + } function ContextTreeNode(parent, data) { @@ -196,6 +204,7 @@ exports.module = function(phantomas) { phantomas.leaveContext = leaveContext; phantomas.getContextData = getContextData; phantomas.readFullTree = readFullTree; + phantomas.resetTree = resetTree; })(); diff --git a/lib/tools/phantomas/custom_modules/modules/eventListYLT/eventListYLT.js b/lib/tools/phantomas/custom_modules/modules/eventListYLT/eventListYLT.js deleted file mode 100644 index 5b79785..0000000 --- a/lib/tools/phantomas/custom_modules/modules/eventListYLT/eventListYLT.js +++ /dev/null @@ -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); - }); - }); -}; diff --git a/lib/tools/phantomas/custom_modules/modules/eventYLT/eventYLT.js b/lib/tools/phantomas/custom_modules/modules/eventYLT/eventYLT.js new file mode 100644 index 0000000..82b678e --- /dev/null +++ b/lib/tools/phantomas/custom_modules/modules/eventYLT/eventYLT.js @@ -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)); + }); + }); +}; diff --git a/lib/tools/phantomas/custom_modules/modules/jQYLT/jQYLT.js b/lib/tools/phantomas/custom_modules/modules/jQYLT/jQYLT.js index f2403a8..c29b85a 100644 --- a/lib/tools/phantomas/custom_modules/modules/jQYLT/jQYLT.js +++ b/lib/tools/phantomas/custom_modules/modules/jQYLT/jQYLT.js @@ -274,7 +274,6 @@ exports.module = function(phantomas) { args = [].slice.call(arguments); args.forEach(function(arg, index) { - if (arg instanceof Array) { arg = '[Array]'; } @@ -296,6 +295,13 @@ exports.module = function(phantomas) { } else { try { + + for (var key in arg) { + if (typeof arg[key] === 'function') { + arg[key] = '(function)'; + } + } + arg = JSON.stringify(arg); } catch(e) { arg = '[Object]'; diff --git a/lib/tools/phantomas/custom_modules/modules/jsTreeYLT/jsTreeYLT.js b/lib/tools/phantomas/custom_modules/modules/jsTreeYLT/jsTreeYLT.js index a9772a5..697b1db 100644 --- a/lib/tools/phantomas/custom_modules/modules/jsTreeYLT/jsTreeYLT.js +++ b/lib/tools/phantomas/custom_modules/modules/jsTreeYLT/jsTreeYLT.js @@ -9,11 +9,11 @@ exports.version = '0.1'; exports.module = function(phantomas) { 'use strict'; - phantomas.setMetric('javascriptExecutionTree'); // @desc number of duplicated DOM queries + phantomas.setMetric('javascriptExecutionTree'); // save data phantomas.on('report', function() { - phantomas.log('Reading execution tree JSON'); + phantomas.log('JS execution tree: Reading execution tree JSON'); phantomas.evaluate(function() {(function(phantomas) { var fullTree = phantomas.readFullTree(); diff --git a/lib/tools/phantomas/custom_modules/modules/scrollListener/scrollListener.js b/lib/tools/phantomas/custom_modules/modules/scrollListener/scrollListener.js new file mode 100644 index 0000000..bc250b0 --- /dev/null +++ b/lib/tools/phantomas/custom_modules/modules/scrollListener/scrollListener.js @@ -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); + }); + }); +}; diff --git a/lib/tools/phantomas/phantomasWrapper.js b/lib/tools/phantomas/phantomasWrapper.js index f9b78c2..f4d85a0 100644 --- a/lib/tools/phantomas/phantomasWrapper.js +++ b/lib/tools/phantomas/phantomasWrapper.js @@ -20,12 +20,16 @@ var PhantomasWrapper = function() { var options = { // Cusomizable options - 'timeout': task.options.timeout || 60, + 'timeout': task.options.timeout || 45, '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, 'tablet': (task.options.device === 'tablet'), 'phone': (task.options.device === 'phone'), '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 'reporter': 'json:pretty', @@ -35,14 +39,13 @@ var PhantomasWrapper = function() { 'domHiddenContent', // overriden 'domMutations', // not compatible with webkit 'domQueries', // overriden - 'eventListeners', // overridden + 'events', // overridden 'filmStrip', // not needed 'har', // not needed for the moment 'javaScriptBottlenecks', // needs to be launched after custom module scopeYLT 'jQuery', // overridden 'jserrors', // overridden 'pageSource', // not needed - 'waitForSelector', // not needed 'windowPerformance' // overriden ].join(','), 'include-dirs': [ diff --git a/package.json b/package.json index 427b39f..13ed17f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "yellowlabtools", - "version": "1.4.1", + "version": "1.5.0", "description": "Online tool to audit a webpage for performance and front-end quality issues", "repository": { "type": "git", @@ -12,17 +12,17 @@ "main": "./lib/index.js", "dependencies": { "async": "~0.9.0", - "body-parser": "~1.12.3", + "body-parser": "~1.12.4", "compression": "~1.4.3", - "cors": "^2.5.3", - "debug": "~2.1.3", + "cors": "^2.6.0", + "debug": "~2.2.0", "express": "~4.12.3", "lwip": "0.0.6", "meow": "^3.1.0", - "phantomas": "1.10.0", + "phantomas": "1.10.2", "ps-node": "0.0.4", - "q": "~1.2.1", - "rimraf": "~2.3.2", + "q": "~1.4.0", + "rimraf": "~2.3.3", "temporary": "0.0.8" }, "devDependencies": { @@ -32,7 +32,7 @@ "grunt-contrib-clean": "^0.6.0", "grunt-contrib-concat": "^0.5.1", "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-jshint": "^0.11.2", "grunt-contrib-less": "^1.0.1", @@ -43,9 +43,9 @@ "grunt-inline-angular-templates": "^0.1.5", "grunt-line-remover": "^0.0.2", "grunt-mocha-test": "^0.12.7", - "grunt-replace": "^0.8.0", + "grunt-replace": "^0.9.2", "grunt-usemin": "^3.0.0", - "grunt-webfont": "^0.5.2", + "grunt-webfont": "^0.5.3", "matchdep": "^0.3.0", "mocha": "^2.2.4", "request": "^2.55.0", diff --git a/server_config/server_install.sh b/server_config/server_install.sh index 84a22f4..595142b 100644 --- a/server_config/server_install.sh +++ b/server_config/server_install.sh @@ -12,7 +12,7 @@ sudo apt-get install -y nodejs source ~/.profile # Installation of some packages globally -npm install bower forever grunt-cli phantomjs -g +npm install bower forever grunt-cli -g source ~/.profile # Installation of YellowLabTools diff --git a/server_config/server_update.sh b/server_config/server_update.sh index 7ee7e78..2519d61 100755 --- a/server_config/server_update.sh +++ b/server_config/server_update.sh @@ -18,8 +18,6 @@ bower install --config.interactive=false --allow-root # Front-end compilation rm -rf front/build -npm install grunt-cli -g -npm install phantomjs -g grunt build # Restart the server diff --git a/test/api/apiTest.js b/test/api/apiTest.js index 23b16a1..c13701f 100644 --- a/test/api/apiTest.js +++ b/test/api/apiTest.js @@ -97,7 +97,11 @@ describe('api', function() { url: wwwUrl + '/simple-page.html', waitForResponse: true, screenshot: true, - device: 'tablet' + device: 'tablet', + //waitForSelector: '*', + cookie: 'foo=bar', + authUser: 'joe', + authPass: 'secret' }, json: true, headers: { @@ -165,8 +169,12 @@ describe('api', function() { body.should.have.a.property('javascriptExecutionTree').that.is.an('object'); 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('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 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) { this.timeout(5000); diff --git a/test/core/offendersHelpersTest.js b/test/core/offendersHelpersTest.js index 6bf59c9..3cbefaf 100644 --- a/test/core/offendersHelpersTest.js +++ b/test/core/offendersHelpersTest.js @@ -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() { var result = offendersHelpers.backtraceToArray('http://pouet.com/js/jquery.footer-transverse-min-v1.0.20.js:1 /http://pouet.com/js/main.js:1'); diff --git a/test/www/jquery-page.html b/test/www/jquery-page.html index 2e29bbe..f2e3f16 100644 --- a/test/www/jquery-page.html +++ b/test/www/jquery-page.html @@ -20,6 +20,7 @@ // Let's start the spaghetti code!!! + $('li').bind('mouseover', function() {}); $('ul').find('li'); $('li', $('#foo')); var foo = document.getElementById('foo'); @@ -102,6 +103,7 @@ $tbodytr.on('click', handler); $tbody.on('click', 'tr', handler); $tbody.on('click', {someData: 'Joe'}, handler); + $tbody.on({event1: handler, event2: handler}); $tbodytr.off('click', handler); $tbodytr.off('click', 'tr'); $tbodytr.off('click'); diff --git a/test/www/scroll-page.html b/test/www/scroll-page.html new file mode 100644 index 0000000..2ff52a5 --- /dev/null +++ b/test/www/scroll-page.html @@ -0,0 +1,18 @@ + + + Scroll page + + +

Simple page

+ + + + \ No newline at end of file