Compare commits

...
355 Commits
Author SHA1 Message Date
Gaël Métais 51e4265109 Merge pull request #147 from gmetais/develop
v1.10.0
2016-03-26 17:30:46 +02:00
Gaël Métais eaebb3adfa Update Phantomas to 1.15.1 2016-03-26 17:16:42 +02:00
Gaël Métais 76b4ca6ea5 Revert commented test due to cookies bug 2016-03-26 17:15:47 +02:00
Gaël Métais b8560c6e15 Merge pull request #146 from gmetais/phantomas-1.15
Update dependencies
2016-03-26 12:28:16 +02:00
Gaël Métais 8eec2549a4 Trying to fix Travis build for Node 5.* 2016-03-26 12:00:11 +02:00
Gaël Métais aaa1ad7eaa Update other dependecies 2016-03-26 11:41:51 +02:00
Gaël Métais 74c57fc11c Add offenders to CSSRules 2016-03-26 10:49:17 +02:00
Gaël Métais b7e25c4e1c Update Phantomas to v1.15 2016-03-26 10:25:28 +02:00
Gaël Métais 3f97333423 Test different versions of Node in Travis 2016-03-26 10:25:09 +02:00
Gaël Métais 71152f64a5 Merge pull request #145 from gmetais/fix-xml
Fix XML report error in cli
2016-03-24 16:22:09 +02:00
Gaël Métais 531d125dcc Fix error 252 console output when there is a timeout while using the CLI 2016-03-24 15:47:50 +02:00
Gaël Métais 299e79e890 Fix XML report error in cli 2016-03-24 14:16:06 +02:00
Gaël Métais 14f60d8dd3 Remove --js-deep-analylis & jsTimeline options, they are now on by default 2016-03-24 09:46:08 +02:00
Gaël Métais a00b1a96c7 Fix a bug when there is a JS redirection during the run 2016-03-23 17:21:26 +02:00
Gaël Métais 2847e02c5d Add an HTTPS check before checking HTTP2 2016-03-23 17:11:40 +02:00
Gaël Métais 57b30a1838 Fix typo 2016-03-23 13:34:23 +02:00
Gaël Métais f2a637ada7 Merge pull request #144 from gmetais/domains
New advanced option: possibility to block domains
2016-03-23 13:29:37 +02:00
Gaël Métais b3804586dc Regressing with PhantomJS 2.1, cookies don't work. Waiting for a fix. 2016-03-23 13:22:19 +02:00
Gaël Métais 113c1f0565 Fix jQuery version unit tests 2016-03-21 23:36:07 +02:00
Gaël Métais 3948eeaad4 New advanced settings: block domains 2016-03-21 21:28:17 +02:00
Gaël Métais f843184970 Add forgottent screenshotWidth parameter to tests 2016-03-20 16:20:57 +02:00
Gaël Métais 2aa9edeb8f Webkit2 engine does not exist anymore in phantomas 1.14 2016-03-20 14:55:19 +02:00
Gaël Métais 5e37cf8224 Update Phantomas to v1.14 2016-03-02 22:41:18 +01:00
Gaël Métais 8bd3a1edde Add screenshot width setting 2016-01-12 00:09:35 +01:00
Gaël Métais c68962e2f2 Merge pull request #136 from gmetais/jquery-2.2
Release of jQuery 2.2
2016-01-09 00:26:58 +01:00
Gaël Métais 6a9a247a87 Merge pull request #135 from gmetais/http2
Add Http2 detection
2016-01-09 00:26:07 +01:00
Gaël Métais e238370bc8 Release of jQuery 2.2 2016-01-09 00:22:51 +01:00
Gaël Métais 178f2fd135 Add Http2 detection 2016-01-09 00:10:01 +01:00
Gaël Métais 55cd1160db Merge pull request #133 from gmetais/develop
v1.9.3
2015-12-22 22:10:44 +01:00
Gaël Métais 06bdbd929c Add a maintenance-mode during production deploy 2015-12-22 22:06:19 +01:00
Gaël Métais 100eb01f27 v1.9.3 2015-12-22 19:36:47 +01:00
Gaël Métais 00ade4ad29 Update dependencies 2015-12-22 19:36:15 +01:00
Gaël Métais f3a5779102 Fix a bug when an incorrect protocol is called, for exemple htp instead of http 2015-12-22 19:27:57 +01:00
Gaël Métais 6f0b97e760 Removed some unused data from jsExecution in JSON 2015-12-22 19:19:39 +01:00
Gaël Métais 28f9ed64ff JS at the end of the body + preconnect 2015-12-22 19:13:32 +01:00
Gaël Métais b6924f70d4 Merge pull request #132 from gmetais/develop
v1.9.2
2015-12-21 18:25:10 +01:00
Gaël Métais 79ecae34c8 v1.9.2 2015-12-21 18:21:32 +01:00
Gaël Métais f4843baa0e Fixed a bug that prevented Phantomas to end the test and made it wait for the timeout 2015-12-21 16:08:34 +01:00
Gaël Métais cd97a275fe Missing legend on breakpoint graph 2015-11-27 11:17:50 +01:00
Gaël Métais f2e77a5c3a v1.9.1 2015-11-27 09:52:28 +01:00
Gaël Métais 23f4f9bd0b v1.9.1 2015-11-27 09:52:10 +01:00
Gaël Métais e8eb6964db Missing legend on breakpoint graph 2015-11-27 09:50:08 +01:00
Gaël Métais 9643c5bfcd Merge pull request #128 from gmetais/develop
Pushing to production soon
2015-11-26 18:50:33 +01:00
Gaël Métais 56ebe6bb8b Remove some chapters in doc 2015-11-26 18:44:48 +01:00
Gaël Métais 5f7c2f07b8 Adjust new cssMobileFirst rule 2015-11-26 18:44:12 +01:00
Gaël Métais bbe965e7a3 Merge pull request #127 from gmetais/dependencies
Update dependencies
2015-11-26 17:50:58 +01:00
Gaël Métais ebd03c7b1a Update dependencies 2015-11-26 17:41:22 +01:00
Gaël Métais 0d02a57600 Merge pull request #126 from gmetais/mediaQueries
Analyze media queries
2015-11-26 15:22:58 +01:00
Gaël Métais 394d3a5b25 Two new media queries related rules 2015-11-26 14:57:47 +01:00
Gaël Métais 4f9589fb6b Fix rare Cannot read property '1' of null error 2015-11-26 09:55:21 +01:00
Gaël Métais 048e3396de Merge pull request #122 from gmetais/develop
v1.8.2
2015-11-06 16:48:25 +01:00
Gaël Métais e3d378a4ba v1.8.2 2015-11-06 16:23:13 +01:00
Gaël Métais 91f8d9c2b0 Merge PR into develop: Change default user agent 2015-11-06 15:26:56 +01:00
Martin Smeeckaert 2285c896d2 change default user agent 2015-11-06 11:51:52 +01:00
Gaël Métais 9ff24918e2 Merge pull request #120 from gmetais/develop
v1.8.1
2015-11-02 19:18:53 +01:00
Gaël Métais e78e794240 Fix jsHint 2015-11-02 19:07:35 +01:00
Gaël Métais 0eb359542b Temporary fix for relative lazyloadable images src 2015-11-02 18:56:33 +01:00
Gaël Métais c54cf5a2ba Relaunch run on webkit1 if webkit2 fails 2015-10-28 22:55:52 +01:00
Gaël Métais 6f7f05b456 Small fix for old prefixes offenders 2015-10-28 22:09:26 +01:00
Gaël Métais da6a8834dd Merge pull request #119 from gmetais/old-prefixes
Merged
2015-10-28 21:28:13 +01:00
Gaël Métais 7ed16d63f3 Merge pull request #118 from gmetais/phantomas1.13
Merged
2015-10-28 21:26:02 +01:00
Gaël Métais d8730cc67b Fix tests after phantomas v1.13 2015-10-28 20:07:02 +01:00
Gaël Métais b67efe0cac Update dependencies 2015-10-28 20:05:43 +01:00
Gaël Métais 835cb2f90f Improve Old prefixes offenders readability 2015-10-28 12:04:00 +01:00
Gaël Métais f75e99f703 Fix bug where scrollExecutionTree is empty 2015-10-23 17:31:00 +02:00
Gaël Métais 7e1bf0fc92 Merge pull request #112 from gmetais/develop
v1.8.0
2015-09-20 01:34:58 +02:00
Gaël Métais 82693a8587 v1.8 + update dependencies 2015-09-20 01:29:45 +02:00
Gaël Métais 99eace0336 Merge pull request #111 from gmetais/color-diff
New rule: similar colors
2015-09-20 01:19:13 +02:00
Gaël Métais d0257fea8f New rule: similar colors 2015-09-20 01:06:42 +02:00
Gaël Métais 453a5cda2e Stricter hiddenImages rule 2015-09-19 11:55:38 +02:00
Gaël Métais bdbd1a0f08 Merge pull request #108 from gmetais/hiddenImages
New metric: hiddenImages
2015-09-18 18:59:47 +02:00
Gaël Métais d32b9f78a4 New metric: hiddenImages 2015-09-18 18:51:41 +02:00
Gaël Métais 46c2d8c49e Fix backtraces in the profiler when using PhantomJS 2 2015-09-18 13:49:24 +02:00
Gaël Métais 88b5567131 Merge pull request #106 from gmetais/develop
v1.7.8
2015-09-15 07:53:41 +02:00
Gaël Métais 0e81d16feb Reduce the timeout from 45 to 30s 2015-09-15 07:37:33 +02:00
Gaël Métais a900f0ff65 Cooler DOMaccessesOnScroll rule 2015-09-14 22:54:17 +02:00
Gaël Métais d4faa75b79 Update dependencies 2015-09-14 22:52:50 +02:00
Gaël Métais 47b0de1b49 v1.7.8 2015-09-14 22:50:43 +02:00
Gaël Métais fe6a10592b Merge pull request #105 from gmetais/basic-auth
Basic auth
2015-09-14 22:49:29 +02:00
Gaël Métais 8f02349ca5 Fix tests 2015-09-14 22:36:43 +02:00
Gaël Métais 1fb32de428 Change auth pwd field to a password type 2015-09-14 17:25:23 +02:00
Gaël Métais 31b66ee6fa Send auth headers with requests 2015-09-14 17:25:23 +02:00
Gaël Métais d22f9b81b2 Merge pull request #104 from gmetais/develop
v1.7.7
2015-09-11 10:47:02 +02:00
Gaël Métais 3051d61365 Show the goal for 100/100 on the rule page 2015-09-11 10:24:20 +02:00
Gaël Métais 53fd2e48e5 Cooler on global variables, not on document.write 2015-09-11 10:23:13 +02:00
Gaël Métais ef516c123d Update dependencies 2015-09-11 08:45:01 +02:00
Gaël Métais add8962390 Merge pull request #103 from gmetais/xml
Add xml output option to the CLI
2015-09-10 22:58:57 +02:00
Gaël Métais 1a444a481e Add xml output option to the CLI 2015-09-10 18:42:19 +02:00
Gaël Métais 96ca4a7933 Remove input type url for mobiles, causing a bug on iOS 2015-09-10 18:08:05 +02:00
Gaël Métais f07bc615b0 Merge pull request #101 from gmetais/develop
v1.7.6
2015-08-28 11:18:43 +02:00
Gaël Métais 2d0c88390a Fix tests 2015-08-28 10:57:46 +02:00
Gaël Métais 52574623fe Merge branch 'phantomjs2-quick' into develop 2015-08-28 10:49:19 +02:00
Gaël Métais b1d95ddf5f v1.7.6 2015-08-28 10:48:11 +02:00
Gaël Métais 092bb285ed Add a phantomasEngine setting for the server 2015-08-28 10:36:49 +02:00
Gaël Métais 52ce26796a Merge pull request #98 from gmetais/develop
v1.7.5
2015-08-16 14:38:35 +02:00
Gaël Métais 6bd55e4807 Avoid destroying SVG fonts when optimizing images 2015-08-16 14:23:37 +02:00
Gaël Métais 8325a9297e v1.7.5 2015-08-16 14:04:29 +02:00
Gaël Métais 9cb3c78def Hide scrollbars on screenshot (show then on hover) 2015-08-16 14:03:55 +02:00
Gaël Métais 143436ecd3 Change tooltip message 2015-08-16 13:47:41 +02:00
Gaël Métais 83cf87c8cf Merge pull request #97 from gmetais/browserify
Remove Bower and update dependencies
2015-08-16 13:36:43 +02:00
Gaël Métais 2f1003e8bc Remove bower from shell scripts 2015-08-16 13:30:57 +02:00
Gaël Métais e0649eb9bd Replace bower by npm (browserify) 2015-08-16 13:22:12 +02:00
Gaël Métais d5c607d370 Update dependencies 2015-08-16 13:06:18 +02:00
Gaël Métais 2c2c8bf4cb Add author to package.json 2015-08-11 21:24:43 +02:00
Gaël Métais 08459ca289 Add keywords to package.json 2015-08-11 21:23:46 +02:00
Gaël Métais 162f1fa1d1 Migrate Travis-CI to container-base infra 2015-08-01 19:42:33 +02:00
Gaël Métais d9113edbbe Merge pull request #95 from gmetais/develop
v1.7.4
2015-08-01 19:41:26 +02:00
Gaël Métais eb1c5c07e9 v1.7.4 2015-08-01 19:30:56 +02:00
Gaël Métais 0574429a93 Warn about srcset not being used 2015-08-01 19:27:48 +02:00
Gaël Métais 8ff565404b Warn about srcset not being used 2015-08-01 19:22:06 +02:00
Gaël Métais b08d4b4cae Merge pull request #94 from gmetais/links
Change ng-click to real <a> on dashboard rows
2015-08-01 18:53:02 +02:00
Gaël Métais f331662263 Merge branch 'develop' of https://github.com/gmetais/YellowLabTools into develop 2015-08-01 18:52:06 +02:00
Gaël Métais 10693283c5 Add license to package.json 2015-08-01 18:51:51 +02:00
Gaël Métais f2a933c51b Remove uneeded function 2015-08-01 18:44:49 +02:00
Gaël Métais 172b8cfb9b Change ng-click to real links on rule rows 2015-07-31 19:41:47 +02:00
Gaël Métais 47738ffa26 Merge pull request #93 from gmetais/fix-update
Exit server update script if npm install fails
2015-07-31 16:39:17 +02:00
Gaël Métais c8a29b4651 Fail the shell scripts if npm or bower fail 2015-07-31 16:16:26 +02:00
Gaël Métais c305cc2f7d Merge branch 'develop' of https://github.com/gmetais/YellowLabTools into develop 2015-07-31 16:14:59 +02:00
Gaël Métais 01676e1f99 Fix typo 2015-07-30 11:09:34 +02:00
Gaël Métais de1e15fb25 Merge pull request #90 from RaphArbuz/fix-typo-deboucing
fix typo: deboucing -> debouncing
2015-07-22 16:41:23 +02:00
RaphArbuz 0aa77c33ad fix typo: deboucing -> debouncing 2015-07-22 16:37:05 +02:00
Gaël Métais 22eeb1a47a Merge pull request #89 from gmetais/develop
v1.7.3
2015-07-22 16:02:03 +02:00
Gaël Métais 15b3c6617b v1.7.3 2015-07-22 16:01:08 +02:00
Gaël Métais e342a88f7d Fix YLT freezing the queue when an incorrect gzip header is sent on a file 2015-07-22 15:48:45 +02:00
Gaël Métais 7779efedf1 Add a GitHub star button in the footer 2015-07-20 11:48:35 +02:00
Gaël Métais 137b4d25b9 Fix css offenders parsing when containing line breaks 2015-07-08 23:25:11 +02:00
Gaël Métais 9be40adabf v1.7.2 2015-06-24 08:42:00 +02:00
Gaël Métais b6d4b7b40d v1.7.2 2015-06-24 08:41:43 +02:00
Gaël Métais 3256789082 Merge pull request #87 from gmetais/fix-xml
Fix server freeze issue
2015-06-24 08:32:30 +02:00
Gaël Métais 87adc575ac Fix type xml issue 2015-06-24 08:01:58 +02:00
Gaël Métais 30ae0f93f3 Remove unneeded promise step 2015-06-24 08:00:56 +02:00
Gaël Métais c339654112 Failing test case with xml file 2015-06-24 07:49:44 +02:00
Gaël Métais 25c9770fba v1.7.1 2015-06-22 15:29:45 +02:00
Gaël Métais 0bf7326604 v1.7.1 2015-06-22 15:29:36 +02:00
Gaël Métais 7fc5a9ba8f v1.7.1 2015-06-22 15:25:47 +02:00
Gaël Métais 230f805cae Add libjpeg to installation script 2015-06-22 15:19:00 +02:00
Gaël Métais bc9c828584 Fix dependency problem 2015-06-22 15:18:19 +02:00
Gaël Métais 6c17a709bf Merge pull request #85 from gmetais/develop
Releasing v1.7.0 very soon
2015-06-20 18:00:04 +02:00
Gaël Métais dd912ecc8f Merge pull request #84 from gmetais/reorder
New rules and rules reordering
2015-06-20 16:24:08 +02:00
Gaël Métais c9b12c4892 Remove bugged lossless image optim gain when there's a lossy one 2015-06-20 16:11:10 +02:00
Gaël Métais 414aea2637 Update dependencies and set version v1.7.0 2015-06-20 15:51:46 +02:00
Gaël Métais b7bc4dbadc Made Weight and Requests the first categories because they are the most important 2015-06-20 15:22:42 +02:00
Gaël Métais 8a8b871908 Changed some thresholds, mainly cooler 2015-06-20 15:19:50 +02:00
Gaël Métais 1225b27510 Fix timeline script filter 2015-06-20 14:34:13 +02:00
Gaël Métais f86bd86043 Add google analytics to already minified fies 2015-06-20 14:20:31 +02:00
Gaël Métais 8133164f44 Rename total requests number 2015-06-20 13:51:34 +02:00
Gaël Métais 2f447da34b Add Images not lazyloaded rule 2015-06-20 00:12:47 +02:00
Gaël Métais 41cdc8783e Remove uneeded policies 2015-06-19 23:47:25 +02:00
Gaël Métais ccbb324270 Replace TotalRequests and SmallRequests rules 2015-06-19 23:46:39 +02:00
Gaël Métais 8d4a0c34e7 Fix requests offenders 2015-06-19 18:45:34 +02:00
Gaël Métais 2620f75955 Fix SVG test 2015-06-19 18:01:02 +02:00
Gaël Métais 4ca525f701 Compare gzip size for svg optimizations 2015-06-19 17:16:30 +02:00
Gaël Métais 53a9aa377a Add Gzip offenders 2015-06-19 16:16:00 +02:00
Gaël Métais d166003dfe Update Phantomas, needed for svg optimization 2015-06-19 16:15:19 +02:00
Gaël Métais 31d80791a9 Compare minified files on gzipped size 2015-06-19 15:24:19 +02:00
Gaël Métais 02245aa5f1 Fix test 2015-06-19 14:55:13 +02:00
Gaël Métais 583b6b9a55 Optimizing JS minification step that takes too long 2015-06-19 14:05:21 +02:00
Gaël Métais d4815e855e Fix bug with SVG minification+compression 2015-06-18 23:06:57 +02:00
Gaël Métais 113665139a Rename isMinified into isOptimized 2015-06-18 22:51:02 +02:00
Gaël Métais 37e35b0ed6 Debugging test failing on travis 2015-06-18 16:42:24 +02:00
Gaël Métais eef8f00d8b Debugging test failing on travis 2015-06-18 15:51:00 +02:00
Gaël Métais bdb15a7780 Debugging test failing on travis 2015-06-18 15:09:10 +02:00
Gaël Métais 2b59912549 Debugging test failing on travis 2015-06-18 14:52:26 +02:00
Gaël Métais 8623085c2a Debugging test failing on travis 2015-06-18 13:50:34 +02:00
Gaël Métais 612627d66f Restore console.log correctly 2015-06-18 12:11:37 +02:00
Gaël Métais 04700beab3 Fixing tests on Travis 2015-06-18 11:55:36 +02:00
Gaël Métais 0d3d6c88e4 Trying to fix build on Travis 2015-06-18 11:10:47 +02:00
Gaël Métais db57f67e98 Change policy text 2015-06-18 11:08:16 +02:00
Gaël Métais baa145e15c Add a gzip compression task 2015-06-18 11:07:19 +02:00
Gaël Métais c1bae20f01 File minification 2015-06-16 20:33:19 +02:00
Gaël Métais 41bc676b12 Fix bug with about:blank 2015-06-01 17:44:34 +02:00
Gaël Métais cea74dd63d Cooler Image Optimization score 2015-06-01 17:44:18 +02:00
Gaël Métais 9ccca3b33b Add offenders to imageOptimization 2015-06-01 16:52:50 +02:00
Gaël Métais 1606cbab18 Add Image Optimization rule to the dashbard 2015-06-01 14:57:05 +02:00
Gaël Métais c428c8ae2d Optimize SVG files 2015-05-31 19:36:54 +02:00
Gaël Métais b52e84d1e7 Optimize PNG files 2015-05-31 19:25:34 +02:00
Gaël Métais 3fbcbbe8e0 Merge branch 'develop' into reorder 2015-05-31 19:02:40 +02:00
Gaël Métais 74eabd700c Add a JPEG optimizer 2015-05-31 18:39:52 +02:00
Gaël Métais d7ddae38e5 Total Weight offenders 2015-05-30 12:52:50 +02:00
Gaël Métais fe7de2db99 Fix rare bug with domComplete never triggered 2015-05-29 22:56:07 +02:00
Gaël Métais 9b9de42139 Fix test on headersSize 2015-05-29 17:01:51 +02:00
Gaël Métais 7b64288765 Fix typo in module name 2015-05-29 16:41:12 +02:00
Gaël Métais c6c967f89c Adding totalWeight metric 2015-05-29 16:13:32 +02:00
Gaël Métais 040dba961a v1.6.4 2015-05-28 11:26:39 +02:00
Gaël Métais 0146794cda Merge pull request #83 from gmetais/black
Fix black backgrounds in screenshots #82.
2015-05-28 11:21:00 +02:00
Gaël Métais b00eba77f9 Fix typo in comment 2015-05-28 11:12:02 +02:00
Gaël Métais 3f27ae249a Add a white background to screenshots 2015-05-28 11:09:45 +02:00
Gaël Métais 4384cffabe Merge branch 'develop' 2015-05-26 15:25:22 +02:00
Gaël Métais fde3234fc6 v1.6.3 2015-05-26 15:24:43 +02:00
Gaël Métais d998c7cd46 Merge branch 'develop' 2015-05-26 15:23:30 +02:00
Gaël Métais a8b62fc184 Fix score display on dashboard 2015-05-26 15:23:09 +02:00
Gaël Métais 65de8e08d3 Merge branch 'develop' 2015-05-26 11:19:54 +02:00
Gaël Métais 73cc1cd2e5 v1.6.2 2015-05-26 11:19:39 +02:00
Gaël Métais 26926071b0 Merge branch 'master' 2015-05-26 11:16:56 +02:00
Gaël Métais 5c433129ec Fix score display on dashboard 2015-05-26 11:16:24 +02:00
Gaël Métais 68bc1a6608 Wording 2015-05-25 19:06:45 +02:00
Gaël Métais e67a0e0bc0 Merge pull request #81 from gmetais/develop
v1.6.0
2015-05-25 16:38:14 +02:00
Gaël Métais f553d45396 v1.6.0 2015-05-25 16:01:24 +02:00
Gaël Métais 090a53b95a Killing a Phantomas process will not kill YLT anymore 2015-05-25 15:57:01 +02:00
Gaël Métais 22ab7515fb Redirect www. to non-www (for SEO) 2015-05-25 15:01:21 +02:00
Gaël Métais a97dcc5efb Change the tweet-this message 2015-05-25 14:35:39 +02:00
Gaël Métais 4b098aef69 Fix timeline bug, not being displayed 2015-05-25 14:04:27 +02:00
Gaël Métais cb46274288 New rule: jQuery calls on empty objects 2015-05-25 13:33:26 +02:00
Gaël Métais 1e60a43075 Don't count duplicated queries on elements not attached to DOM 2015-05-25 11:42:47 +02:00
Gaël Métais 1aa9cd032f Update dependencies 2015-05-25 09:22:45 +02:00
Gaël Métais 5e3748315c Remove jQueryNotDelegatedEvents offenders and send user to filtered timeline 2015-05-25 09:22:25 +02:00
Gaël Métais 7f67c032d1 fix tests 2015-05-25 08:30:02 +02:00
Gaël Métais 33621af3ad Replaced DOMqueriesWithoutResults by queriesWithoutResults to include jQuery queries 2015-05-22 18:54:32 +02:00
Gaël Métais 2fb4a7bd6a Replace rules DOMinserts, DOMqueries & eventsBound by DOMaccesses 2015-05-22 17:47:13 +02:00
Gaël Métais 1ff983a03b Add specific sub-filters on warnings in the timeline 2015-05-22 17:44:54 +02:00
Gaël Métais 701530ef04 Be cooler on global variables 2015-05-22 11:16:18 +02:00
Gaël Métais 719e624ddf Fix icons showing a background grid on IE 2015-05-22 11:09:29 +02:00
Gaël Métais 03a25df367 One webfont is ok 2015-05-22 10:57:39 +02:00
Gaël Métais c93ce684a8 Old jQuery major versions didn't have the trailing .0 in number 2015-05-22 10:52:12 +02:00
Gaël Métais 090708f38b Merge branch 'HipsterBrown-url-input' into develop 2015-05-22 10:06:27 +02:00
Gaël Métais 49f93e660e User agent sniffing 2015-05-22 10:05:41 +02:00
Nick Hehr 867680f4e9 Updates LaunchTest() form text input to be of type "url".
Making this change not only adds automatic url validation from modern
browsers but enabled special keyboards on mobile devices to make
inputing urls much easier.
2015-05-20 10:31:34 -04:00
Gaël Métais 36ac529cc8 Improve doc 2015-05-13 19:29:26 +02:00
Gaël Métais 0ae388f0f6 Improve doc 2015-05-13 19:23:12 +02:00
Gaël Métais b53ace765c Improve doc 2015-05-13 19:13:38 +02:00
Gaël Métais f256299a0c Improve doc 2015-05-13 18:48:33 +02:00
Gaël Métais 4fb05e406a New doc test 2015-05-13 18:21:31 +02:00
Gaël Métais 035639c68a Improve doc 2015-05-13 09:12:21 +02:00
Gaël Métais 09f53cac92 Add ngrok in README 2015-05-12 13:57:45 +02:00
Gaël Métais 05a27d9003 v1.5.1 2015-05-12 11:43:09 +02:00
Gaël Métais 5d4d1807ba v1.5.1 2015-05-12 11:42:50 +02:00
Gaël Métais 836e465414 Fix bug with DOMaccessesOnScroll offenders 2015-05-12 11:38:05 +02:00
Gaël Métais 94457b24cd Small wording adjustment 2015-05-12 10:30:28 +02:00
Gaël Métais 728aaec24d Merge pull request #77 from gmetais/develop
v1.5.0
2015-05-12 09:49:53 +02:00
Gaël Métais 4ccd2d7087 v1.5.0 2015-05-12 09:32:46 +02:00
Gaël Métais 9bc6d2928b New Events Not Delegated rule 2015-05-12 09:12:10 +02:00
Gaël Métais d7d4fb36ad Remove console log 2015-05-12 07:47:16 +02:00
Gaël Métais 4e6a88abec New jQuery usage rule 2015-05-11 19:43:15 +02:00
Gaël Métais a7051ace5d Add metrics queriesWithoutResults, jQueryCalls, jQueryCallsOnEmptyObject, jQueryNotDelegatedEvent 2015-05-11 18:28:11 +02:00
Gaël Métais cee47f2846 Remove temporarily domAccess from dashboard 2015-05-11 18:26:41 +02:00
Gaël Métais f9e60acdf6 Reduce timeout from 60s to 45 2015-05-11 17:16:05 +02:00
Gaël Métais 5f6ba048e6 Make the results API call smaller by removing toolsResults 2015-05-11 17:00:58 +02:00
Gaël Métais aba91de480 Add an exclude parameter to get result in API 2015-05-11 16:59:32 +02:00
Gaël Métais 118ec757a3 Update dependencies 2015-05-11 10:53:43 +02:00
Gaël Métais eaf712937b Merge pull request #76 from gmetais/screenshotError
Prevent fail when there's a screenshot error
2015-05-10 19:41:59 +02:00
Gaël Métais 6c47c406cb Prevent fail when there's a screenshot error 2015-05-10 14:51:36 +02:00
Gaël Métais eaeff3bf6a Display help if CLI is launched without an url 2015-05-07 23:16:13 +02:00
Gaël Métais 5b5fb3e32d Fix profiler details for jquery - on 2015-05-07 23:07:55 +02:00
Gaël Métais 07a8dd4912 Merge branch notGzipped 2015-05-07 19:56:31 +02:00
Gaël Métais a2f5127222 Merge pull request #75 from gmetais/notGzipped
New assetsNotGzipped rule
2015-05-07 19:56:14 +02:00
Gaël Métais eab48ea202 Fix screenshot bug when opeing an v1.3 result in 1.4 2015-05-07 19:51:43 +02:00
Gaël Métais 00e2717653 Rising scores on CSS Complexity and Bad CSS 2015-05-07 19:50:14 +02:00
Gaël Métais d83f150810 New assetsNotGzipped rule 2015-05-07 19:13:59 +02:00
Gaël Métais 2a7e6c2dd2 Merge pull request #74 from gmetais/scroll
New experimental "Scroll bottlenecks" rules
2015-05-07 17:53:55 +02:00
Gaël Métais f8f792645b Improve scroll bottlenecks rules 2015-05-07 17:10:01 +02:00
Gaël Métais 8c79f39389 Merge branch 'events' into scroll 2015-05-06 22:45:23 +02:00
Gaël Métais f7da653e87 New rules: eventsScrollBound & DOMaccessesOnScroll 2015-05-06 22:45:03 +02:00
Gaël Métais 1ed753eff1 Merge pull request #73 from gmetais/events
Fix events
2015-05-05 16:27:20 +02:00
Gaël Métais 385de967a4 Spy scrollBounds on document too 2015-05-05 13:29:20 +02:00
Gaël Métais ac84019777 Uncaught file name change 2015-05-04 19:09:10 +02:00
Gaël Métais b0c6fe9474 Merge pull request #72 from gmetais/cssParsing
Improve cssParsingError score and offenders
2015-05-04 18:56:26 +02:00
Gaël Métais 94e2b0233a Revert grunt-replace to v0.8.0 as 0.9.1 is broken 2015-05-04 18:36:25 +02:00
Gaël Métais c202e3426b Add a warning message when a entire stylesheet could not be parsed 2015-05-04 17:59:16 +02:00
Gaël Métais 7e86e83173 Change cssParsingError's weight and category 2015-05-04 17:20:01 +02:00
Gaël Métais 604857648b Easier to understand css offenders 2015-05-04 17:03:59 +02:00
Gaël Métais 76fb50473f Rename the rule jQuery versions loaded 2015-05-04 16:20:00 +02:00
Gaël Métais ffee3b225f Better parsing of cssParsingErrors' offenders 2015-05-04 16:16:33 +02:00
Gaël Métais 5f1dd42559 Update dependencies 2015-05-04 15:54:09 +02:00
Gaël Métais 57a7c78ce2 PhantomJS is not needed anymore for the build 2015-05-04 15:50:49 +02:00
Gaël Métais 33c6643388 Merge pull request #71 from gmetais/advancedSettings
Advanced settings
2015-05-04 15:49:15 +02:00
Gaël Métais 74deba61e6 Comment waitForSelector test while the feature is broken 2015-05-04 15:43:20 +02:00
Gaël Métais bb2f0e6c55 Add a cookie advanced option on the front page 2015-05-04 15:24:00 +02:00
Gaël Métais b2f6f68d1a Add new options to the front-end's home page 2015-04-30 22:18:27 +02:00
Gaël Métais fdde065a94 Merge pull request #70 from gmetais/develop
v1.4.1
2015-04-28 12:16:53 +02:00
Gaël Métais 9231b0914b Forgotten console.log 2015-04-28 08:03:45 +02:00
Gaël Métais 78f49ae7c9 v1.4.1 2015-04-27 23:36:31 +02:00
Gaël Métais 95ff81d81f Fix issues with icons after changing icon-font generator 2015-04-27 23:05:27 +02:00
Gaël Métais 92b73f62ad Fix problem with the latest LESS engine 2015-04-27 19:54:56 +02:00
Gaël Métais 1057836788 Merge pull request #69 from gmetais/develop
v1.4.0
2015-04-27 19:29:53 +02:00
Gaël Métais 93f8702962 v1.4.0 2015-04-27 19:17:13 +02:00
Gaël Métais 3d71454666 Merge pull request #68 from gmetais/mobile
Fixes the first part of #14.
2015-04-27 19:11:32 +02:00
Gaël Métais c0cc2744c6 Add a test 2015-04-27 18:53:18 +02:00
Gaël Métais 65b1c34d06 Add smartphone & tablet shapes arround screenshots 2015-04-27 18:45:08 +02:00
Gaël Métais 67615719ae Add device selection option to CLI 2015-04-27 18:43:26 +02:00
Gaël Métais 491c8ebe77 Add a device option to ylt's core 2015-04-27 18:43:04 +02:00
Gaël Métais cae23d61e2 Add a device selector on the launching page 2015-04-27 15:40:46 +02:00
Gaël Métais 9cad9ad2f9 Replace grunt-fontsmith by front-webfont + new icons 2015-04-27 14:05:36 +02:00
Gaël Métais ba977d6b49 Change submit button color on focus 2015-04-27 11:22:04 +02:00
Gaël Métais 88bb2f91a5 Change submit button color on focus 2015-04-27 11:19:25 +02:00
Gaël Métais dda11627f8 Update dependencies 2015-04-27 10:20:24 +02:00
Gaël Métais 2df713ca0c Make it clearer the project is open-source 2015-04-25 18:38:58 +02:00
Gaël Métais 486ede9396 Promote the Grunt plugin 2015-04-25 18:38:41 +02:00
Gaël Métais 72afcc3e5a Enabling spy when checking for hiddenContentSize 2015-04-25 18:08:28 +02:00
Gaël Métais ef2eaceb88 Remove evalCalls from policies (phantomas bug) 2015-04-25 18:03:56 +02:00
Gaël Métais c2db1c70e3 Fix .text considered reading when called with an empty string 2015-04-25 18:02:48 +02:00
Gaël Métais 13413899eb Merge pull request #62 from gmetais/develop
v1.3.2
2015-03-23 09:59:58 +01:00
Gaël Métais 98f074e8d5 jsHint 2015-03-23 09:49:55 +01:00
Gaël Métais 36a60bae4d v1.3.2 2015-03-20 12:01:10 +01:00
Gaël Métais 7c5a5daf2f Take in account trigger mode on some jQuery functions 2015-03-20 12:00:52 +01:00
Gaël Métais 7b74df7f04 Add spy on getComputedStyle() 2015-03-20 11:30:20 +01:00
Gaël Métais a6813a7ec1 Fix z-index issue in the profiler 2015-03-20 11:28:49 +01:00
Gaël Métais 484d7ee3b2 Merge pull request #61 from gmetais/develop
V1.3.1
2015-03-18 08:22:41 +01:00
Gaël Métais 7a1bc282e9 v1.3.1 2015-03-18 08:16:48 +01:00
Gaël Métais f45480c0b9 Fix bugs with show warnings and some html escaping 2015-03-18 08:09:51 +01:00
Gaël Métais 23d980910d Increase stack size 2015-03-18 08:09:02 +01:00
Gaël Métais 9b939aa8b4 Remove console.log 2015-03-18 08:06:56 +01:00
Gaël Métais 838a9c3e7d Fix missing bower dependency 2015-03-17 09:44:56 +01:00
Gaël Métais f91de30717 Missing angular dependency 2015-03-17 09:44:11 +01:00
Gaël Métais 0c3299573f Merge pull request #60 from gmetais/develop
Merge V1.3.0
2015-03-17 09:28:26 +01:00
Gaël Métais 13e2d2d597 Fix travis running the tests twice 2015-03-17 08:50:27 +01:00
Gaël Métais a6bbc7119d Update version number 2015-03-17 08:46:31 +01:00
Gaël Métais 20bf58b84f Fix missing space 2015-03-17 08:14:01 +01:00
Gaël Métais fe176c92c0 Update dependencies 2015-03-16 20:00:42 +01:00
Gaël Métais 6009ed53b7 Fix cssParsingError offenders when no line & column are provided 2015-03-16 18:29:42 +01:00
Gaël Métais 79c387ba45 Merge pull request #59 from gmetais/phantomas1.10
Phantomas1.10
2015-03-16 17:43:30 +01:00
Gaël Métais dd767d6664 Fix tests 2015-03-16 17:20:34 +01:00
Gaël Métais 61202f1635 Fix syntax 2015-03-16 17:15:17 +01:00
Gaël Métais 457d6cc54b Add offenders for document.write calls 2015-03-16 16:54:17 +01:00
Gaël Métais bf8123b7c5 Revert eval quickfix 2015-03-16 14:07:54 +01:00
Gaël Métais c98ede9b81 Revert temporary fix for negative cache time 2015-03-16 13:58:42 +01:00
Gaël Métais 34671fca59 Revert fix for jQueryEventTriggers 2015-03-16 13:54:30 +01:00
Gaël Métais 7ad9381aad Revert viewport size 2015-03-16 13:33:23 +01:00
Gaël Métais 7387eefb10 Update phantomas 2015-03-16 13:32:56 +01:00
Gaël Métais ca3403a5b2 Merge branch 'develop' of https://github.com/gmetais/YellowLabTools into develop 2015-03-16 13:20:27 +01:00
Gaël Métais 8adc895c8c Fix missing spaces with htmlmin 2015-03-16 13:16:05 +01:00
Gaël Métais 6fe49d06ac Scuse my english 2015-03-16 13:06:56 +01:00
Gaël Métais c47e6d0219 Merge pull request #58 from gmetais/profilerParams
Profiler params
2015-03-16 13:04:23 +01:00
Gaël Métais 99c4b240ec Fix file url in backtrace 2015-03-16 12:54:31 +01:00
Gaël Métais 0947724245 Replace angular templating by homemade for speed purpose 2015-03-16 12:30:05 +01:00
Gaël Métais 15653ef961 Delete jsChildrenDirective not needed anymore 2015-03-16 12:00:56 +01:00
Gaël Métais 893c92266a Fix bug with script filter 2015-03-12 18:07:04 +01:00
Gaël Métais 50571995a4 Speed up timeline page, by removing some angular 2015-03-10 09:34:43 +01:00
Gaël Métais dc2f6e17cd Remove duplicated spying of window.onload 2015-03-10 09:26:07 +01:00
Gaël Métais 9a861b085a Remove touchscreen compatible hover (for perfs) 2015-03-09 12:51:38 +01:00
Gaël Métais 714cb318a9 One way binding 2015-03-09 10:37:00 +01:00
Gaël Métais 2827463b41 Remove unused search field 2015-03-09 07:52:04 +01:00
Gaël Métais 121a8b11b8 Improve arguments reading in timeline 2015-03-08 23:13:09 +01:00
Gaël Métais 3e0e31a5fe New test page 2015-03-08 10:32:57 +01:00
Gaël Métais a7c96f77fe Change the way context is saved in jsExecutionTree 2015-03-02 17:10:21 +01:00
Gaël Métais d3444e9aa0 Add a 'jQuery - find' wrapper on jQuery search by ID 2015-03-02 15:37:11 +01:00
Gaël Métais 71d2e61551 Add some spies on more jQuery functions 2015-03-02 11:23:10 +01:00
Gaël Métais 3756d1185f Better error message 2015-02-27 18:03:43 +01:00
Gaël Métais 5a22eb9542 Fix merge conflict 2015-02-27 17:52:51 +01:00
Gaël Métais cb535f1cee Scroll to the profiler on timeline click 2015-02-27 17:41:54 +01:00
Gaël Métais f304e5efb7 Merge pull request #54 from gmetais/scriptfilter
Introduce a script selector to filter the timeline and the profiler.
2015-02-27 10:04:00 +01:00
Gaël Métais 5732a8ffc8 Spy some more jQuery functions : animations 2015-02-26 19:49:18 +01:00
Gaël Métais 8b7ccb837a Adjust network category score, caching is more important 2015-02-26 19:48:35 +01:00
Gaël Métais f4bb3fc0bc Fix PhantomJS typo in README 2015-02-26 19:46:51 +01:00
Gaël Métais 62602a88bc Remove jQuery from timeline when it's filtered 2015-02-18 23:44:30 +01:00
Gaël Métais f63fa9faf2 Filter timeline by script 2015-02-13 16:32:58 +01:00
Gaël Métais 839bfd4756 Use shorten-url for backtraces 2015-02-13 11:53:10 +01:00
Gaël Métais 9392d21783 Merge branch 'develop' of https://github.com/gmetais/YellowLabTools into develop 2015-02-12 18:44:02 +01:00
Gaël Métais ca5594dfbb Forgotten rule cachingNotSpecified 2015-02-12 18:43:40 +01:00
Gaël Métais 1b938417e5 Add doc.createElement and createDocumentFragment to the profiling 2015-02-12 18:43:08 +01:00
Gaël Métais f95e94ac6d Merge pull request #50 from gmetais/subqueries
Show subqueries in timeline
2015-02-12 18:00:12 +01:00
Gaël Métais e9bb72a416 Add sub-queries to the JS profiler table 2015-02-12 17:28:41 +01:00
Gaël Métais 2d003febc1 Front needs the jsTimeline by default 2015-02-09 19:04:47 +01:00
Gaël Métais 42c27af604 No more javascriptExecutionTree bu default (option) 2015-02-09 19:03:04 +01:00
Gaël Métais ee490fc660 Add the --deep-js-analysis option to CLI 2015-02-09 19:01:59 +01:00
Gaël Métais 10500c2fba Fix wrong format 2015-02-09 18:53:32 +01:00
119 changed files with 10309 additions and 1687 deletions
-3
View File
@@ -1,3 +0,0 @@
{
"directory" : "bower_components"
}
-1
View File
@@ -1,5 +1,4 @@
node_modules
bower_components
.tmp
.vagrant
results/*
+15 -5
View File
@@ -1,8 +1,18 @@
language: node_js
sudo: false
node_js:
- "0.10.33"
- "5.8"
- "0.12"
env:
- CXX=g++-4.8
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-4.8
before_install:
- "npm install -g grunt-cli"
- "npm install -g phantomjs"
install: npm install
before_script: grunt test
- "npm install -g npm"
- "npm install -g grunt-cli"
install:
- "npm install"
+28 -28
View File
@@ -11,17 +11,18 @@ module.exports = function(grunt) {
pkg: grunt.file.readJSON('package.json'),
settings: grunt.file.readJSON('./server_config/settings.json'),
font: {
webfont: {
icons: {
src: ['front/src/fonts/svg-icons/*.svg'],
destCss: 'front/src/less/icons.less',
destFonts: 'front/src/fonts/icons.woff',
// Optional: Custom routing of font filepaths for CSS
cssRouter: function (fontpath) {
var pathArray = fontpath.split('/');
var fileName = pathArray[pathArray.length - 1];
return '/fonts/' + fileName;
src: 'front/src/fonts/svg-icons/*.svg',
dest: 'tmp',
destCss: 'front/src/less',
options: {
engine: 'node',
types: 'woff',
stylesheet: 'less',
embed: true,
htmlDemo: false,
syntax: 'bootstrap'
}
}
},
@@ -66,7 +67,9 @@ module.exports = function(grunt) {
'app/nodeControllers/*.js',
'app/public/scripts/*.js',
'phantomas_custom/**/*.js',
'test/**/*.js',
'test/api/*.js',
'test/core/*.js',
'test/fixtures/*.js',
'front/src/js/**/*.js'
]
},
@@ -92,15 +95,14 @@ module.exports = function(grunt) {
},
coverage: {
files: [
{src: ['test/**'], dest: 'coverage/'},
{src: ['lib/metadata/**'], dest: 'coverage/'},
{src: ['node_modules/phantomas/**'], dest: 'coverage/'},
{src: ['lib/tools/phantomas/custom_modules/**'], dest: 'coverage/'}
{cwd: 'test', src: '**/*', dest: 'coverage/test', expand: true},
{cwd: 'lib/metadata', src: '**/*', dest: 'coverage/lib/metadata', expand: true},
{cwd: 'node_modules/phantomas', src: '**/*', dest: 'coverage/node_modules/phantomas', expand: true},
{cwd: 'lib/tools/phantomas/custom_modules', src: '**/*', dest: 'coverage/lib/tools/phantomas/custom_modules', expand: true}
]
},
build: {
files: [
{src: ['./front/src/fonts/icons.woff'], dest: './front/build/fonts/icons.woff'},
{src: ['./front/src/img/favicon.png'], dest: './front/build/img/favicon.png'},
{src: ['./front/src/img/logo-large.png'], dest: './front/build/img/logo-large.png'},
]
@@ -141,7 +143,7 @@ module.exports = function(grunt) {
options: {
reporter: 'spec',
},
src: ['test/core/offendersHelpersTest.js']
src: ['test/core/imageOptimizerTest.js']
},
coverage: {
options: {
@@ -156,7 +158,7 @@ module.exports = function(grunt) {
dev: {
NODE_ENV: 'development'
},
builded: {
built: {
NODE_ENV: 'production'
}
},
@@ -169,7 +171,7 @@ module.exports = function(grunt) {
showStack: true
}
},
builded: {
built: {
options: {
port: 8383,
server: './bin/server.js',
@@ -209,16 +211,14 @@ module.exports = function(grunt) {
html: './front/build/main.html',
css: './front/build/css/*.css',
options: {
assetsDirs: ['front/build'],
patterns: {
css: [[/(\/fonts\/icons\.woff)/gm, 'Replacing reference to icons.woff']]
}
assetsDirs: ['front/build']
}
},
htmlmin: {
options: {
removeComments: true,
collapseWhitespace: true
collapseWhitespace: true,
conservativeCollapse: true
},
main: {
files: [{
@@ -291,7 +291,7 @@ module.exports = function(grunt) {
grunt.registerTask('icons', [
'font:icons',
'webfont:icons',
'less',
'clean:tmp'
]);
@@ -323,9 +323,9 @@ module.exports = function(grunt) {
'express:dev'
]);
grunt.registerTask('builded', [
'env:builded',
'express:builded'
grunt.registerTask('built', [
'env:built',
'express:built'
]);
grunt.registerTask('test', [
+59 -30
View File
@@ -1,36 +1,73 @@
# Yellow Lab Tools
<img src="./doc/img/logo.png" alt="Yellow Lab Tools"></img>
Online tool that lets you test a webpage and detects **performance** and **front-end code quality** issues.
[![Dependency Status](https://gemnasium.com/gmetais/YellowLabTools.svg)](https://gemnasium.com/gmetais/YellowLabTools) [![Build Status](https://travis-ci.org/gmetais/YellowLabTools.svg?branch=master)](https://travis-ci.org/gmetais/YellowLabTools)
Analyzes a webpage and detects **performance** or **front-end code quality** issues. Free, easy to launch, easy to understand, in-depth details.
### Access the tool here: [http://yellowlab.tools](http://yellowlab.tools)
#
<img src="./doc/img/commandment.png" alt="No website should go live without being tested with Yellow Lab Tools"></img>
<table>
<tr>
<td width="70%">
The <b>online tool</b> (recommended):
<a href="http://yellowlab.tools" target="_blank">http://yellowlab.tools</a>
</td>
<td width="30%">
<img src="./doc/img/YLT-animated.gif"></img>
</td>
</tr>
<tr>
<td width="70%">
The <b>CLI</b> (Command Line Interface) - <a href="https://github.com/gmetais/YellowLabTools/wiki/Command-Line-Interface" target="_blank">Doc here</a>
</td>
<td width="30%">
<img src="./doc/img/YLT-cli-animated.gif"></img>
</td>
</tr>
<tr>
<td width="70%">
The <b>Grunt task</b>: <a href="https://github.com/gmetais/grunt-yellowlabtools" traget="_blank">gmetais/grunt-yellowlabtools</a>
<br>For developers or Continuous Integration
</td>
<td width="30%">
<img src="./doc/img/grunt-logo.png"></img>
</td>
</tr>
<tr>
<td width="70%">
The <b>NodeJS</b> package - <a href="https://github.com/gmetais/YellowLabTools/wiki/NodeJS-module" target="_blank">Doc here</a>
</td>
<td width="30%">
<img src="./doc/img/npm-logo.png"></img>
</td>
</tr>
<tr>
<td width="70%">
The <b>public API</b> - <a href="https://github.com/gmetais/YellowLabTools/wiki/Public-API" target="_blank">Doc here</a>
<br>
Allows you to launch runs from any language
</td>
<td width="30%">
<img src="./doc/img/api-logo.png"></img>
</td>
</tr>
</table>
![example dashboard screenshot](screenshot.png)
## How it works
The tool loads the given URL via [PhantomasJS](http://phantomjs.org/) (a headless browser) and collects various metrics and statistics with the help of [Phantomas](https://github.com/macbre/phantomas). These metrics are categorized and transformed into scores. It also gives in-depth details so developpers can correct the detected issues.
The tool loads the given URL via [PhantomJS](http://phantomjs.org/) (a headless browser) and collects various metrics and statistics with the help of [Phantomas](https://github.com/macbre/phantomas). These metrics are then categorized and transformed into scores. It also gives in-depth details so developpers can fix the detected issues.
By the way, it's free because we are geeks, not businessmen. All we want is a ★ on GitHub, it will boost our motivation to add more awesome features!!!
![example dashboard screenshot](./doc/img/screenshot.png)
## Different ways to use YLT
#### The web interface:
This is the best way to discover the tool: [http://yellowlab.tools](http://yellowlab.tools).
Please note the server is hosted in Baltimore, USA.
## Test your localhost
#### The Command Line Client:
Might be useful if you want to build an automation tool. The documentation is [here](https://github.com/gmetais/YellowLabTools/wiki/Command-Line-Interface).
#### The NPM module:
Can be used to build automation tools in NodeJS. The documentation is [here](https://github.com/gmetais/YellowLabTools/wiki/NodeJS-module).
#### The Public API:
Hosted on our http://yellowlab.tools server, it is a RESTful API that allows you to launch distant tests. The documentation is [here](https://github.com/gmetais/YellowLabTools/wiki/Public-API).
You can use [ngrok](https://ngrok.com/), a tool that creates a secure tunnel between your localhost and the online tool (or the public API). You can also use the CLI or the Grunt tasks as they run on your machine.
## Install your own private instance
@@ -38,18 +75,10 @@ Hosted on our http://yellowlab.tools server, it is a RESTful API that allows you
If your project is not accessible from outside, or if you want to fork and improve the tool, you can build your own instance. The documentation is [here](https://github.com/gmetais/YellowLabTools/wiki/Install-your-private-server).
## Help needed
There are so many things left to do, **your help would be greatly appreciated**! Please report bugs, ask for evolutions and come code with me.
## License
Please read the [license](LICENSE).
Icons are the property of [IcoMoon.io](https://icomoon.io/)
## Author
Gaël Métais. I'm a webperf freelance based in Paris. If you understand french, you can visit [my website](http://www.gaelmetais.com).
Gaël Métais. I'm a webperf freelance. Follow me on Twitter [@gaelmetais](https://twitter.com/gaelmetais), I tweet about Web Performances, Front-end and new versions of YellowLabTools!
If you understand French, you can visit [my website](http://www.gaelmetais.com) (will be soon in English too).
## Contributors
+39 -8
View File
@@ -1,10 +1,11 @@
#!/usr/bin/env node
var debug = require('debug')('ylt:cli');
var meow = require('meow');
var path = require('path');
var debug = require('debug')('ylt:cli');
var meow = require('meow');
var path = require('path');
var EasyXml = require('easyxml');
var ylt = require('../lib/index');
var ylt = require('../lib/index');
var cli = meow({
help: [
@@ -12,7 +13,13 @@ var cli = meow({
' yellowlabtools <url> <options>',
'',
'Options:',
' --screenshot Will take a screenshot and use this value as the output path. It needs to end with ".png".',
' --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".',
' --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.',
' --reporter The output format: "json" or "xml". Default is "json".',
''
].join('\n'),
pkg: '../package.json'
@@ -22,8 +29,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];
@@ -44,6 +50,24 @@ if (screenshot) {
options.screenshot = cli.flags.screenshot;
}
// 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;
// Output format
if (cli.flags.reporter && cli.flags.reporter !== 'json' && cli.flags.reporter !== 'xml') {
console.error('Incorrect parameters: reporter has to be "json" or "xml"');
process.exit(1);
}
(function execute(url, options) {
@@ -54,7 +78,14 @@ if (screenshot) {
then(function(data) {
debug('Success');
console.log(JSON.stringify(data, null, 2));
switch(cli.flags.reporter) {
case 'xml':
var serializer = new EasyXml();
console.log(serializer.render(data));
break;
default:
console.log(JSON.stringify(data, null, 2));
}
}).fail(function(err) {
+2
View File
@@ -7,12 +7,14 @@ var cors = require('cors');
var authMiddleware = require('../lib/server/middlewares/authMiddleware');
var apiLimitsMiddleware = require('../lib/server/middlewares/apiLimitsMiddleware');
var wwwRedirectMiddleware = require('../lib/server/middlewares/wwwRedirectMiddleware');
// Middlewares
app.use(compress());
app.use(bodyParser.json());
app.use(cors());
app.use(wwwRedirectMiddleware);
app.use(authMiddleware);
app.use(apiLimitsMiddleware);
-12
View File
@@ -1,12 +0,0 @@
{
"name": "yellowlabtools",
"dependencies": {
"angular": "~1.3.8",
"angular-route": "~1.3.8",
"angular-resource": "~1.3.7",
"angular-sanitize": "~1.4.0-beta.0"
},
"resolutions": {
"angular": "~1.3.8"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 B

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 96 KiB

+15 -3
View File
@@ -46,6 +46,14 @@
content: "+";
opacity: 0.85;
}
.summary .globalScore .screenshotWrapper.phone:hover:after {
top: 1.7em;
left: 0.64em;
}
.summary .globalScore .screenshotWrapper.tablet:hover:after {
top: 1.5em;
left: 0.9em;
}
.summary .notations {
display: table;
width: 80%;
@@ -101,14 +109,18 @@
.summary .notations .criteria .table {
width: 100%;
}
.summary .notations .criteria .table > div:hover > div {
.summary .notations .criteria .table > a {
text-decoration: none;
color: inherit;
}
.summary .notations .criteria .table > a:hover > div {
background: #EBD8E2;
cursor: pointer;
}
.summary .notations .criteria .table > div:hover > div.info {
.summary .notations .criteria .table > a:hover > div.info {
background: #FFF;
}
.summary .notations .criteria .table > div:hover > div.info .icon-question {
.summary .notations .criteria .table > a:hover > div.info .icon-question {
color: #EBD8E2;
}
.summary .notations .criteria .grade {
File diff suppressed because one or more lines are too long
+134 -3
View File
@@ -1,22 +1,153 @@
.promess {
padding: 0em 2em 3em;
padding: 0em 2em;
margin-bottom: 0.5em;
font-weight: normal;
font-size: 1.2em;
}
.price {
padding: 0em 2em 3em;
margin-top: 0em;
font-size: 0.9em;
}
.url {
width: 50%;
}
.launchBtn {
background: #e74c3c;
background: #ffa319;
color: #fff;
}
.launchBtn:focus {
background: #e74c3c;
}
.launchBtn.disabled {
background: #deaca6;
}
.launchBtn.disabled:focus {
color: #ddd;
}
.settings {
width: 50%;
margin: 0 auto;
}
.settings input,
.settings select {
font-size: 1em;
}
.settings input[type=text],
.settings input[type=password],
.settings textarea {
width: 100%;
min-width: 4em;
}
.device {
margin-top: 3em;
}
.device .item {
display: inline-block;
margin: 1em 0.75em;
width: 5.5em;
height: 5.5em;
color: #FFF;
border: 1px solid #FFF;
padding: 1px;
border-radius: 0.5em;
cursor: pointer;
text-decoration: none;
font-size: 0.8em;
}
.device .item.active {
color: #ffa319;
border: 2px solid #ffa319;
padding: 0;
}
.device .item:hover {
color: #ffa319;
}
.device .item > div {
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;
vertical-align: middle;
}
.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;
}
+130 -102
View File
File diff suppressed because one or more lines are too long
+94 -18
View File
@@ -29,9 +29,16 @@
border-radius: 0.5em;
margin: 0 auto 0.5em;
}
.rule h3 {
margin-bottom: 0em;
}
.rule .okThreshold {
font-style: italic;
font-size: 0.9em;
}
.rule .message {
width: 80%;
margin: 0 auto;
margin: 1.5em auto;
}
.rule .message p {
margin: 0.5em;
@@ -82,7 +89,11 @@
font-size: 3em;
margin-bottom: 1em;
}
.offenders .offenderButton {
.rule .startTime {
display: none;
}
.offendersTable .offenderButton,
.value .offenderButton {
display: inline-block;
position: relative;
background: #efe;
@@ -90,17 +101,21 @@
margin: 0.2em 0;
border-radius: 0.4em;
z-index: 1;
cursor: pointer;
}
.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;
@@ -109,30 +124,43 @@
border-bottom-left-radius: 0.4em;
border-bottom-right-radius: 0.4em;
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.mouseOver {
.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.mouseOver > div {
.offendersTable .offenderButton.opens:hover > div,
.value .offenderButton.opens:hover > div {
display: block;
background: #ffe0cc;
}
.offendersTable .smallerOffenders,
.value .smallerOffenders {
font-size: 0.9em;
}
.offendersHtml {
display: inline-block;
@@ -148,15 +176,17 @@
font-style: italic;
font-weight: normal;
}
.checker {
/* Checkerboard background */
background-color: #ddd;
background-image: linear-gradient(45deg, #AAA 25%, transparent 25%, transparent 75%, #AAA 75%, #AAA), linear-gradient(45deg, #AAA 25%, transparent 25%, transparent 75%, #AAA 75%, #AAA);
background-size: 1em 1em;
background-position: 0 0, 0.5em 0.5em;
}
.colorPalette {
width: 30em;
border: 2px solid #000;
text-align: left;
/* Checkerboard background */
background-color: #ddd;
background-image: linear-gradient(45deg, #aaaaaa 25%, transparent 25%, transparent 75%, #aaaaaa 75%, #aaaaaa), linear-gradient(45deg, #aaaaaa 25%, transparent 25%, transparent 75%, #aaaaaa 75%, #aaaaaa);
background-size: 1em 1em;
background-position: 0 0, 0.5em 0.5em;
}
.colorPalette > div {
display: inline-block;
@@ -182,7 +212,6 @@
.colorPalette > div:hover:after {
content: " ";
position: absolute;
background-color: inherit;
left: -0.2em;
top: -0.2em;
width: 100%;
@@ -190,3 +219,50 @@
z-index: 2;
border: 0.2em solid #f1c40f;
}
.similarColors {
margin: 1em;
width: 20em;
height: 6em;
}
.similarColors > div {
display: inline-block;
width: 10em;
height: 3.5em;
padding-top: 2.5em;
}
.totalWeightPie {
max-width: 39em;
margin: 2em auto 4em;
}
.totalWeightPie canvas {
max-width: inherit;
}
.hugeFile {
font-weight: bold;
color: #e74c3c;
}
.imageOffenders {
display: table;
border-spacing: 3em;
width: 90%;
}
.imageOffenders > div {
display: table-row;
}
.imageOffenders > div > div {
display: table-cell;
vertical-align: middle;
}
.imageOffenders img {
max-height: 10em;
max-width: 40em;
border: 1px solid #000;
margin-top: 0.5em;
}
.smallPreview {
display: block;
max-height: 4em;
max-width: 8em;
border: 1px solid #000;
margin: 1em auto 0.2em;
}
+120 -31
View File
@@ -2,6 +2,19 @@
.execution {
text-align: center;
}
.selectScript {
padding-bottom: 2em;
font-size: 0.9em;
}
.selectScript select {
max-width: 30em;
}
.selectScript.empty {
font-size: 0.8em;
}
.selectScript.empty select {
width: 10em;
}
.timeline {
margin: 2em 0 5em;
}
@@ -39,6 +52,9 @@
bottom: 0;
width: 100%;
}
.timeline .interval .color.clickable {
cursor: pointer;
}
.timeline div.interval:hover {
background: #9C4274;
}
@@ -46,34 +62,34 @@
background: #F04DA7;
}
.timeline .domComplete.interval {
background: #ede3ff;
background: #EDE3FF;
}
.timeline .domComplete .color {
background: #c2a3ff;
background: #C2A3FF;
}
.timeline .domContentLoadedEnd.interval {
background: #d8f0f0;
background: #D8F0F0;
}
.timeline .domContentLoadedEnd .color {
background: #7ecccc;
background: #7ECCCC;
}
.timeline .domContentLoaded.interval {
background: #e0ffd1;
background: #E0FFD1;
}
.timeline .domContentLoaded .color {
background: #a7e846;
background: #A7E846;
}
.timeline .domInteractive.interval {
background: #fffccc;
background: #FFFCCC;
}
.timeline .domInteractive .color {
background: #ffe433;
background: #FFE433;
}
.timeline .domCreation.interval {
background: #ffe0cc;
background: #FFE0CC;
}
.timeline .domCreation .color {
background: #ff6600;
background: #FF6600;
}
.timeline .tooltip.detailsOverlay {
position: absolute;
@@ -127,33 +143,25 @@
border: 1px dotted #aaa;
text-align: left;
}
.slowRequestsLimit {
width: 3em;
font-size: 1em;
text-align: right;
border: 1px solid #aaa;
}
input.textFilter {
box-shadow: none;
font-size: 1em;
padding: 0 0.2em;
border: 1px solid #aaa;
border-radius: none;
width: 15em;
.subFilters {
margin-left: 3em;
font-size: 0.9em;
}
.table {
display: table;
width: 100%;
border-spacing: 0.25em;
}
.table > div {
.table > div,
.table > a {
display: table-row;
}
.table > .headers > div {
font-weight: bold;
padding: 0.5em 1em;
}
.table > div > div {
.table > div > div,
.table > a > div {
padding: 0.1em 1em;
background: #f2f2f2;
display: table-cell;
@@ -171,12 +179,65 @@ input.textFilter {
.table > div.showingDetails > div {
background: #f1c40f;
}
.table > div.highlight > div.startTime {
background-color: #C0F090;
}
.table > div.highlight-remove {
transition: 3s;
}
.table > div.highlight-remove > div.startTime {
transition: background-color 3s ease-in;
}
.table > div > .index {
color: #bbb;
word-break: normal;
}
.table > div > .type {
white-space: nowrap;
}
.table .children {
margin-top: 0.2em;
font-size: 0.8em;
line-height: 1.6em;
}
.table .child {
margin-left: 0.5em;
}
.table .child > .child {
margin-left: 1em;
}
.table .child:before {
content: "↳";
}
.table .child .childArgs {
display: none;
}
.table .child span {
position: relative;
}
.table .child span:hover {
background: #EBD8E2;
}
.table .child span:hover div {
display: inline-block;
}
.table .child span:hover .childArgs {
display: block;
position: absolute;
padding: 0 1em 0 2em;
left: 100%;
top: 0;
background: #EBD8E2;
line-height: 1.3em;
height: 1.3em;
z-index: 2;
}
.table .showingDetails .child span:hover {
background: inherit;
}
.table .showingDetails .child span:hover .childArgs {
display: none;
}
.table > div > .value {
width: 70%;
word-break: break-all;
@@ -193,6 +254,7 @@ input.textFilter {
width: 0.8em;
}
.detailsOverlay {
display: none;
position: absolute;
right: 3em;
top: -3em;
@@ -202,13 +264,16 @@ input.textFilter {
background: #fff;
border: 2px solid #f1c40f;
border-radius: 0.5em;
z-index: 1;
z-index: 2;
}
@media screen and (max-width: 1024px) {
.detailsOverlay {
width: 25em;
}
}
.showDetails .detailsOverlay {
display: block;
}
.detailsOverlay .closeBtn {
position: absolute;
top: 0.5em;
@@ -229,21 +294,45 @@ input.textFilter {
white-space: nowrap;
}
.table > div > .startTime.domComplete {
background: #ede3ff;
background: #EDE3FF;
}
.table > div > .startTime.domContentLoadedEnd {
background: #d8f0f0;
background: #D8F0F0;
}
.table > div > .startTime.domContentLoaded {
background: #e0ffd1;
background: #E0FFD1;
}
.table > div > .startTime.domInteractive {
background: #fffccc;
background: #FFFCCC;
}
.table > div > .startTime.domCreation {
background: #ffe0cc;
background: #FFE0CC;
}
.execution .icon-warning {
color: #e74c3c;
cursor: pointer;
}
.queryWithoutResultsFilterOn > div {
display: none;
}
.queryWithoutResultsFilterOn > div.queryWithoutResults {
display: table-row;
}
.jQueryCallOnEmptyObjectFilterOn > div {
display: none;
}
.jQueryCallOnEmptyObjectFilterOn > div.jQueryCallOnEmptyObject {
display: table-row;
}
.eventNotDelegatedFilterOn > div {
display: none;
}
.eventNotDelegatedFilterOn > div.eventNotDelegated {
display: table-row;
}
.jsErrorFilterOn > div {
display: none;
}
.jsErrorFilterOn > div.jsError {
display: table-row;
}
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M 256.00,0.00C 397.385,0.00, 512.00,114.615, 512.00,256.00s -114.615,256.00, -256.00,256.00s -256.00-114.615, -256.00-256.00S 114.615,0.00, 256.00,0.00z M 256.00,464.00 c 114.875,0.00 208.00-93.125 208.00-208.00S 370.875,48.00, 256.00,48.00s -208.00,93.125, -208.00,208.00S 141.125,464.00, 256.00,464.00zM 105.372,233.373l 128.001-128.00c 12.496-12.497 32.757-12.497 45.254,0.00c 12.497,12.497 12.497,32.758,0.00,45.255L 205.255,224.00 L 384.00,224.00 c 17.673,0.00 32.00,14.327 32.00,32.00c0.00,17.673, -14.327,32.00, -32.00,32.00l-178.745,0.00 l 73.373,73.372c 12.497,12.497 12.497,32.759,0.00,45.256 C 272.379,412.876, 264.189,416.00, 256.00,416.00s -16.379-3.124, -22.627-9.372l -128.001-128.00C 92.876,266.131, 92.876,245.869, 105.372,233.373z" ></path></svg>
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M 256.00,0.00C 397.385,0.00, 512.00,114.615, 512.00,256.00s -114.615,256.00, -256.00,256.00s -256.00-114.615, -256.00-256.00S 114.615,0.00, 256.00,0.00z M 256.00,464.00 c 114.875,0.00 208.00-93.125 208.00-208.00S 370.875,48.00, 256.00,48.00s -208.00,93.125, -208.00,208.00S 141.125,464.00, 256.00,464.00zM 105.372,233.373l 128.001-128.00c 12.496-12.497 32.757-12.497 45.254,0.00c 12.497,12.497 12.497,32.758,0.00,45.255L 205.255,224.00 L 384.00,224.00 c 17.673,0.00 32.00,14.327 32.00,32.00c0.00,17.673, -14.327,32.00, -32.00,32.00l-178.745,0.00 l 73.373,73.372c 12.497,12.497 12.497,32.759,0.00,45.256 C 272.379,412.876, 264.189,416.00, 256.00,416.00s -16.379-3.124, -22.627-9.372l -128.001-128.00C 92.876,266.131, 92.876,245.869, 105.372,233.373z" ></path></svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

+1 -1
View File
@@ -1 +1 @@
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M0.00,416.00L 512.00,416.00L 512.00,480.00L0.00,480.00zM 64.00,288.00L 128.00,288.00L 128.00,384.00L 64.00,384.00zM 160.00,160.00L 224.00,160.00L 224.00,384.00L 160.00,384.00zM 256.00,256.00L 320.00,256.00L 320.00,384.00L 256.00,384.00zM 352.00,64.00L 416.00,64.00L 416.00,384.00L 352.00,384.00z" ></path></svg>
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M0.00,416.00L 512.00,416.00L 512.00,480.00L0.00,480.00zM 64.00,288.00L 128.00,288.00L 128.00,384.00L 64.00,384.00zM 160.00,160.00L 224.00,160.00L 224.00,384.00L 160.00,384.00zM 256.00,256.00L 320.00,256.00L 320.00,384.00L 256.00,384.00zM 352.00,64.00L 416.00,64.00L 416.00,384.00L 352.00,384.00z" ></path></svg>

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 634 B

+1 -1
View File
@@ -1 +1 @@
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M 478.145,402.241L 320.00,138.729L 320.00,32.00 l 16.00,0.00 c 8.80,0.00, 16.00-7.20, 16.00-16.00s-7.20-16.00-16.00-16.00L 176.00,0.00 c-8.80,0.00-16.00,7.20-16.00,16.00s 7.20,16.00, 16.00,16.00l 16.00,0.00 l0.00,106.729 L 33.856,402.241C-2.365,462.608, 25.60,512.00, 96.00,512.00l 320.00,0.00 C 486.40,512.00, 514.365,462.608, 478.145,402.241z M 120.519,320.00L 224.00,147.531L 224.00,32.00 l 64.00,0.00 l0.00,115.531 L 391.482,320.00L 120.519,320.00 z" ></path></svg>
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M 478.145,402.241L 320.00,138.729L 320.00,32.00 l 16.00,0.00 c 8.80,0.00, 16.00-7.20, 16.00-16.00s-7.20-16.00-16.00-16.00L 176.00,0.00 c-8.80,0.00-16.00,7.20-16.00,16.00s 7.20,16.00, 16.00,16.00l 16.00,0.00 l0.00,106.729 L 33.856,402.241C-2.365,462.608, 25.60,512.00, 96.00,512.00l 320.00,0.00 C 486.40,512.00, 514.365,462.608, 478.145,402.241z M 120.519,320.00L 224.00,147.531L 224.00,32.00 l 64.00,0.00 l0.00,115.531 L 391.482,320.00L 120.519,320.00 z" ></path></svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 792 B

+1 -1
View File
@@ -1 +1 @@
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M0.00,0.00L 128.00,0.00L 128.00,128.00L0.00,128.00zM 192.00,32.00L 512.00,32.00L 512.00,96.00L 192.00,96.00zM0.00,192.00L 128.00,192.00L 128.00,320.00L0.00,320.00zM 192.00,224.00L 512.00,224.00L 512.00,288.00L 192.00,288.00zM0.00,384.00L 128.00,384.00L 128.00,512.00L0.00,512.00zM 192.00,416.00L 512.00,416.00L 512.00,480.00L 192.00,480.00z" ></path></svg>
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M0.00,0.00L 128.00,0.00L 128.00,128.00L0.00,128.00zM 192.00,32.00L 512.00,32.00L 512.00,96.00L 192.00,96.00zM0.00,192.00L 128.00,192.00L 128.00,320.00L0.00,320.00zM 192.00,224.00L 512.00,224.00L 512.00,288.00L 192.00,288.00zM0.00,384.00L 128.00,384.00L 128.00,512.00L0.00,512.00zM 192.00,416.00L 512.00,416.00L 512.00,480.00L 192.00,480.00z" ></path></svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 679 B

+1 -1
View File
@@ -1 +1 @@
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M 437.011,74.99C 390.685,28.662, 326.693,0.00, 256.00,0.00C 146.256,0.00, 52.655,69.064, 16.251,166.094l 59.938,22.477 C 103.491,115.798, 173.692,64.00, 256.00,64.00c 53.02,0.00, 101.01,21.50, 135.753,56.247L 320.00,192.00l 192.00,0.00 L 512.00,0.00 L 437.011,74.99zM 256.00,448.00c-53.02,0.00-101.013-21.496-135.756-56.244L 192.00,320.00L0.00,320.00 l0.00,192.00 l 74.997-74.997C 121.32,483.334, 185.306,512.00, 256.00,512.00 c 109.745,0.00, 203.346-69.064, 239.75-166.094l-59.938-22.477C 408.51,396.202, 338.309,448.00, 256.00,448.00z" ></path></svg>
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M 437.011,74.99C 390.685,28.662, 326.693,0.00, 256.00,0.00C 146.256,0.00, 52.655,69.064, 16.251,166.094l 59.938,22.477 C 103.491,115.798, 173.692,64.00, 256.00,64.00c 53.02,0.00, 101.01,21.50, 135.753,56.247L 320.00,192.00l 192.00,0.00 L 512.00,0.00 L 437.011,74.99zM 256.00,448.00c-53.02,0.00-101.013-21.496-135.756-56.244L 192.00,320.00L0.00,320.00 l0.00,192.00 l 74.997-74.997C 121.32,483.334, 185.306,512.00, 256.00,512.00 c 109.745,0.00, 203.346-69.064, 239.75-166.094l-59.938-22.477C 408.51,396.202, 338.309,448.00, 256.00,448.00z" ></path></svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 875 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M 368.00,0.00L 144.00,0.00 c-26.40,0.00-48.00,21.60-48.00,48.00l0.00,416.00 c0.00,26.40, 21.60,48.00, 48.00,48.00l 224.00,0.00 c 26.40,0.00, 48.00-21.60, 48.00-48.00L 416.00,48.00 C 416.00,21.60, 394.40,0.00, 368.00,0.00z M 192.00,24.00 l 128.00,0.00 l0.00,16.00 L 192.00,40.00 L 192.00,24.00 z M 256.00,480.00c-17.673,0.00-32.00-14.327-32.00-32.00s 14.327-32.00, 32.00-32.00s 32.00,14.327, 32.00,32.00S 273.673,480.00, 256.00,480.00z M 384.00,384.00L 128.00,384.00 L 128.00,64.00 l 256.00,0.00 L 384.00,384.00 z" ></path></svg>

After

Width:  |  Height:  |  Size: 851 B

+1 -1
View File
@@ -1 +1 @@
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M 224.00,352.00L 288.00,352.00L 288.00,416.00L 224.00,416.00zM 352.00,128.00 C 369.673,128.00 384.00,142.327 384.00,160.00 L 384.00,256.00 L 288.00,320.00 L 224.00,320.00 L 224.00,288.00 L 320.00,224.00 L 320.00,192.00 L 160.00,192.00 L 160.00,128.00 L 352.00,128.00 ZM 256.00,48.00 C 200.441,48.00 148.208,69.636 108.922,108.922 C 69.636,148.208 48.00,200.441 48.00,256.00 C 48.00,311.559 69.636,363.792 108.922,403.078 C 148.208,442.364 200.441,464.00 256.00,464.00 C 311.559,464.00 363.792,442.364 403.078,403.078 C 442.364,363.792 464.00,311.559 464.00,256.00 C 464.00,200.441 442.364,148.208 403.078,108.922 C 363.792,69.636 311.559,48.00 256.00,48.00 Z M 256.00,0.00 L 256.00,0.00 C 397.385,0.00 512.00,114.615 512.00,256.00 C 512.00,397.385 397.385,512.00 256.00,512.00 C 114.615,512.00 0.00,397.385 0.00,256.00 C 0.00,114.615 114.615,0.00 256.00,0.00 Z" ></path></svg>
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M 224.00,352.00L 288.00,352.00L 288.00,416.00L 224.00,416.00zM 352.00,128.00 C 369.673,128.00 384.00,142.327 384.00,160.00 L 384.00,256.00 L 288.00,320.00 L 224.00,320.00 L 224.00,288.00 L 320.00,224.00 L 320.00,192.00 L 160.00,192.00 L 160.00,128.00 L 352.00,128.00 ZM 256.00,48.00 C 200.441,48.00 148.208,69.636 108.922,108.922 C 69.636,148.208 48.00,200.441 48.00,256.00 C 48.00,311.559 69.636,363.792 108.922,403.078 C 148.208,442.364 200.441,464.00 256.00,464.00 C 311.559,464.00 363.792,442.364 403.078,403.078 C 442.364,363.792 464.00,311.559 464.00,256.00 C 464.00,200.441 442.364,148.208 403.078,108.922 C 363.792,69.636 311.559,48.00 256.00,48.00 Z M 256.00,0.00 L 256.00,0.00 C 397.385,0.00 512.00,114.615 512.00,256.00 C 512.00,397.385 397.385,512.00 256.00,512.00 C 114.615,512.00 0.00,397.385 0.00,256.00 C 0.00,114.615 114.615,0.00 256.00,0.00 Z" ></path></svg>

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M 512.00,416.00L 512.00,32.00 L0.00,32.00 l0.00,384.00 l 224.00,0.00 l0.00,32.00 l-96.00,0.00 l0.00,32.00 l 256.00,0.00 l0.00-32.00 l-96.00,0.00 l0.00-32.00 L 512.00,416.00 z M 64.00,96.00l 384.00,0.00 l0.00,256.00 L 64.00,352.00 L 64.00,96.00 z" ></path></svg>

After

Width:  |  Height:  |  Size: 584 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M 400.00,0.00L 80.00,0.00 C 53.60,0.00, 32.00,21.60, 32.00,48.00l0.00,416.00 c0.00,26.40, 21.60,48.00, 48.00,48.00l 320.00,0.00 c 26.40,0.00, 48.00-21.60, 48.00-48.00L 448.00,48.00 C 448.00,21.60, 426.40,0.00, 400.00,0.00z M 240.00,496.00 c-8.836,0.00-16.00-7.163-16.00-16.00s 7.164-16.00, 16.00-16.00s 16.00,7.163, 16.00,16.00S 248.836,496.00, 240.00,496.00z M 384.00,448.00L 96.00,448.00 L 96.00,64.00 l 288.00,0.00 L 384.00,448.00 z" ></path></svg>

After

Width:  |  Height:  |  Size: 774 B

+1 -1
View File
@@ -1 +1 @@
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M 256.00,79.362 L 83.583,448.00 L 428.417,448.00 L 256.00,79.362 Z M 256.00,0.00 L 256.00,0.00 C 267.035,0.00 278.07,7.441 286.442,22.324 L 504.978,457.88 C 521.723,487.646 507.478,512.00 473.327,512.00 L 38.673,512.00 C 4.521,512.00 -9.722,487.646 7.021,457.88 L 225.558,22.324 C 233.93,7.441 244.965,0.00 256.00,0.00 ZM 224.00,384.00A32.00,32.00 2520.00 1,0 288.00,384A32.00,32.00 2520.00 1,0 224.00,384zM 256.00,192.00 C 273.673,192.00 288.00,206.327 288.00,224.00 L 278.00,320.00 L 234.00,320.00 L 224.00,224.00 C 224.00,206.327 238.327,192.00 256.00,192.00 Z" ></path></svg>
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M 256.00,79.362 L 83.583,448.00 L 428.417,448.00 L 256.00,79.362 Z M 256.00,0.00 L 256.00,0.00 C 267.035,0.00 278.07,7.441 286.442,22.324 L 504.978,457.88 C 521.723,487.646 507.478,512.00 473.327,512.00 L 38.673,512.00 C 4.521,512.00 -9.722,487.646 7.021,457.88 L 225.558,22.324 C 233.93,7.441 244.965,0.00 256.00,0.00 ZM 256.00,192.00 C 273.673,192.00 288.00,206.327 288.00,224.00 L 278.00,320.00 L 234.00,320.00 L 224.00,224.00 C 224.00,206.327 238.327,192.00 256.00,192.00 Z" ></path><circle cx="256" cy="384" r="31" style="stroke:#000000; fill:#000000"/></svg>

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 887 B

+21 -1
View File
@@ -1,6 +1,7 @@
var yltApp = angular.module('YellowLabTools', [
'ngRoute',
'ngSanitize',
'ngAnimate',
'indexCtrl',
'dashboardCtrl',
'queueCtrl',
@@ -11,17 +12,36 @@ var yltApp = angular.module('YellowLabTools', [
'resultsFactory',
'apiService',
'menuService',
'settingsService',
'gradeDirective',
'offendersDirectives'
'offendersDirectives',
'LocalStorageModule'
]);
yltApp.run(['$rootScope', '$location', function($rootScope, $location) {
$rootScope.isTouchDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(window.navigator.userAgent);
$rootScope.loadedRunId = null;
var oldHash;
// We don't want the hash to be kept between two pages
$rootScope.$on('$locationChangeStart', function(param1, param2, param3, param4){
var newHash = $location.hash();
if (newHash === oldHash) {
$location.hash(null);
}
oldHash = newHash;
});
// Google Analytics
$rootScope.$on('$routeChangeSuccess', function(){
ga('send', 'pageview', {'page': $location.path()});
});
// GitHub star button (asynchronously loaded iframe)
window.addEventListener('load', function() {
window.document.getElementById('ghbtn').src = 'http://ghbtns.com/github-btn.html?user=gmetais&repo=YellowLabTools&type=star&count=true&size=large';
});
}]);
yltApp.config(['$routeProvider', '$locationProvider',
+7 -15
View File
@@ -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();
@@ -26,29 +26,26 @@ dashboardCtrl.controller('DashboardCtrl', ['$scope', '$rootScope', '$routeParams
$scope.categoriesOrder = Object.keys($scope.result.scoreProfiles.generic.categories);
$scope.globalScore = Math.max($scope.result.scoreProfiles.generic.globalScore, 0);
$scope.tweetText = 'My website\'s score is ' + $scope.globalScore + '/100 on #YellowLabTools!';
$scope.tweetText = 'I\'ve discovered this cool open-source tool that audits the front-end quality of a web page: ';
}
$scope.showRulePage = function(ruleName) {
$location.path('/result/' + $scope.runId + '/rule/' + ruleName);
};
$scope.testAgain = function() {
API.launchTest($scope.result.params.url);
API.relaunchTest($scope.result);
};
/// When comming from a social shared link, the user needs to click on "See full report" button to display the full dashboard.
// When comming from a social shared link, the user needs to click on "See full report" button to display the full dashboard.
$scope.seeFullReport = function() {
$scope.fromSocialShare = false;
$location.search({});
};
$scope.shareOnTwitter = function(message) {
openSocialPopup('https://twitter.com/intent/tweet?url=' + document.URL + '%3Fshare&text=' + encodeURIComponent(message));
openSocialPopup('https://twitter.com/intent/tweet?text=' + encodeURIComponent(message + 'http://yellowlab.tools'));
};
$scope.shareOnLinkedin = function(message) {
openSocialPopup('https://www.linkedin.com/shareArticle?mini=true&url=' + document.URL + '%3Fshare&title=' + encodeURIComponent(message) + '&summary=' + encodeURIComponent('YellowLabTools is a free online tool that analyzes performance and front-end quality of a webpage.'));
openSocialPopup('https://www.linkedin.com/shareArticle?mini=true&url=http://yellowlab.tools&title=' + encodeURIComponent(message) + '&summary=' + encodeURIComponent('YellowLabTools is a free online tool that analyzes performance and front-end quality of a webpage.'));
};
function openSocialPopup(url) {
@@ -59,10 +56,5 @@ dashboardCtrl.controller('DashboardCtrl', ['$scope', '$rootScope', '$routeParams
window.open(url, 'sharer', 'top=' + winTop + ',left=' + winLeft + ',toolbar=0,status=0,width=' + winWidth + ',height=' + winHeight);
}
// Returns the URL of the JSON result
$scope.getAPIUrl = function() {
return '/api/results/' + $scope.runId;
};
loadResults();
}]);
+6 -2
View File
@@ -1,9 +1,13 @@
var indexCtrl = angular.module('indexCtrl', []);
indexCtrl.controller('IndexCtrl', ['$scope', '$location', 'API', function($scope, $location, API) {
indexCtrl.controller('IndexCtrl', ['$scope', 'Settings', 'API', function($scope, Settings, API) {
$scope.settings = Settings.getMergedSettings();
$scope.launchTest = function() {
if ($scope.url) {
API.launchTest($scope.url);
Settings.saveSettings($scope.settings);
API.launchTest($scope.url, $scope.settings);
}
};
}]);
+78 -3
View File
@@ -1,4 +1,13 @@
var ruleCtrl = angular.module('ruleCtrl', []);
var ruleCtrl = angular.module('ruleCtrl', ['chart.js']);
ruleCtrl.config(['ChartJsProvider', function (ChartJsProvider) {
// Configure all charts
ChartJsProvider.setOptions({
animation: false,
colours: ['#FF5252', '#FF8A80'],
responsive: true
});
}]);
ruleCtrl.controller('RuleCtrl', ['$scope', '$rootScope', '$routeParams', '$location', '$sce', 'Menu', 'Results', 'API', function($scope, $rootScope, $routeParams, $location, $sce, Menu, Results, API) {
$scope.runId = $routeParams.runId;
@@ -9,7 +18,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();
@@ -22,6 +31,72 @@ ruleCtrl.controller('RuleCtrl', ['$scope', '$rootScope', '$routeParams', '$locat
function init() {
$scope.rule = $scope.result.rules[$scope.policyName];
// Init "Total Weight" chart
if ($scope.policyName === 'totalWeight') {
$scope.weightLabels = [];
$scope.weightColours = ['#7ECCCC', '#A7E846', '#FF944D', '#FFE74A', '#C2A3FF', '#5A9AED', '#FF6452', '#C1C1C1'];
$scope.weightData = [];
var types = ['html', 'css', 'js', 'json', 'image', 'video', 'webfont', 'other'];
types.forEach(function(type) {
$scope.weightLabels.push(type);
$scope.weightData.push(Math.round($scope.rule.offendersObj.list.byType[type].totalWeight / 1024));
});
$scope.weightOptions = {
tooltipTemplate: '<%=label%>: <%=value%> KB'
};
}
// Init "Breakpoints" chart
if ($scope.policyName === 'cssBreakpoints' && $scope.rule.value > 0) {
// Seek for the biggest breakpoint
var max = 0;
$scope.rule.offendersObj.forEach(function(offender) {
if (offender.pixels > max) {
max = offender.pixels;
}
});
max = Math.max(max + 100, 1400);
// We group offenders 10px by 10px
var GROUP_SIZE = 20;
// Generate an empty array of values
$scope.breakpointsLabels = [];
$scope.breakpointsData = [[]];
for (var i = 0; i <= max / GROUP_SIZE; i++) {
$scope.breakpointsLabels[i] = '';
$scope.breakpointsData[0][i] = 0;
}
// Fill it with results
$scope.rule.offendersObj.forEach(function(offender) {
var group = Math.floor((offender.pixels + 1) / GROUP_SIZE);
if ($scope.breakpointsLabels[group] !== '') {
$scope.breakpointsLabels[group] += '/';
}
$scope.breakpointsLabels[group] += offender.breakpoint;
$scope.breakpointsData[0][group] += offender.count;
});
$scope.breakpointsSeries = ['Number of CSS rules per breakpoint'];
$scope.breakpointsColours = ['#9c4274'];
$scope.breakpointsOptions = {
scaleShowGridLines: false,
barShowStroke: false,
showTooltips: false,
pointDot: false,
responsive: true,
maintainAspectRatio: true,
strokeColor: 'rgba(20, 200, 20, 1)',
scaleFontSize: 9
};
}
}
$scope.backToDashboard = function() {
@@ -29,7 +104,7 @@ ruleCtrl.controller('RuleCtrl', ['$scope', '$rootScope', '$routeParams', '$locat
};
$scope.testAgain = function() {
API.launchTest($scope.result.params.url);
API.relaunchTest($scope.result);
};
loadResults();
+2 -3
View File
@@ -7,10 +7,9 @@ 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;
init();
}, function(err) {
$scope.error = true;
});
@@ -24,7 +23,7 @@ screenshotCtrl.controller('ScreenshotCtrl', ['$scope', '$rootScope', '$routePara
};
$scope.testAgain = function() {
API.launchTest($scope.result.params.url);
API.relaunchTest($scope.result);
};
loadResults();
+98 -48
View File
@@ -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();
@@ -19,19 +19,70 @@ timelineCtrl.controller('TimelineCtrl', ['$scope', '$rootScope', '$routeParams',
}
function render() {
initFilters();
initScriptFiltering();
initExecutionTree();
initTimeline();
$timeout(initProfiler, 100);
}
function initFilters() {
var hash = $location.hash();
var filter = null;
if (hash.indexOf('filter=') === 0) {
filter = hash.substr(7);
}
$scope.warningsFilterOn = (filter !== null);
$scope.warningsFilters = {
queryWithoutResults: (filter === null || filter === 'queryWithoutResults'),
jQueryCallOnEmptyObject: (filter === null || filter === 'jQueryCallOnEmptyObject'),
eventNotDelegated: (filter === null || filter === 'eventNotDelegated'),
jsError: (filter === null || filter === 'jsError')
};
}
function initScriptFiltering() {
var offenders = $scope.result.rules.totalRequests.offendersObj.list.byType.js;
$scope.scripts = [];
offenders.forEach(function(filePath) {
var shortPath = filePath;
if (filePath.length > 100) {
shortPath = filePath.substr(0, 98) + '...';
}
var scriptObj = {
fullPath: filePath,
shortPath: shortPath
};
$scope.scripts.push(scriptObj);
});
}
function initExecutionTree() {
var originalExecutions = $scope.result.javascriptExecutionTree.children || [];
// Detect the last event of all (before filtering) and read time
var lastEvent = originalExecutions[originalExecutions.length - 1];
$scope.endTime = lastEvent.data.timestamp + (lastEvent.data.time || 0);
// Filter
$scope.executionTree = [];
originalExecutions.forEach(function(node) {
// Prepare a faster angular search by creating a kind of search index
node.searchIndex = (node.data.callDetails) ? [node.data.type].concat(node.data.callDetails.arguments).join('°°') : node.data.type;
// Filter by script (if enabled)
if ($scope.selectedScript) {
if (node.data.backtrace && node.data.backtrace.indexOf($scope.selectedScript.fullPath + ':') === -1) {
return;
}
if (node.data.type === "jQuery loaded" || node.data.type === "jQuery version change") {
return;
}
}
$scope.executionTree.push(node);
});
@@ -41,8 +92,6 @@ timelineCtrl.controller('TimelineCtrl', ['$scope', '$rootScope', '$routeParams',
// Split the timeline into 200 intervals
var numberOfIntervals = 199;
var lastEvent = $scope.executionTree[$scope.executionTree.length - 1];
$scope.endTime = lastEvent.data.timestamp + (lastEvent.data.time || 0);
$scope.timelineIntervalDuration = $scope.endTime / numberOfIntervals;
// Pre-fill array of as many elements as there are milleseconds
@@ -80,61 +129,62 @@ timelineCtrl.controller('TimelineCtrl', ['$scope', '$rootScope', '$routeParams',
$scope.profilerData = $scope.executionTree;
}
function parseBacktrace(str) {
if (!str) {
return null;
}
var out = [];
var splited = str.split(' / ');
splited.forEach(function(trace) {
var result = /^(\S*)\s?\(?(https?:\/\/\S+):(\d+)\)?$/g.exec(trace);
if (result && result[2].length > 0) {
var filePath = result[2];
var chunks = filePath.split('/');
var fileName = chunks[chunks.length - 1];
out.push({
fnName: result[1],
fileName: fileName,
filePath: filePath,
line: result[3]
});
}
});
return out;
}
$scope.filter = function(textFilter, scriptName) {
$scope.changeScript = function() {
initExecutionTree();
initTimeline();
initProfiler();
};
$scope.onNodeDetailsClick = function(node) {
var isOpen = node.showDetails;
if (!isOpen) {
// Close all other nodes
$scope.executionTree.forEach(function(currentNode) {
currentNode.showDetails = false;
});
$scope.findLineIndexByTimestamp = function(timestamp) {
var lineIndex = 0;
// Parse the backtrace
if (!node.parsedBacktrace) {
node.parsedBacktrace = parseBacktrace(node.data.backtrace);
for (var i = 0; i < $scope.executionTree.length; i ++) {
var delta = $scope.executionTree[i].data.timestamp - timestamp;
if (delta < $scope.timelineIntervalDuration) {
lineIndex = i;
}
if (delta > 0) {
break;
}
}
node.showDetails = !isOpen;
return lineIndex;
};
$scope.backToDashboard = function() {
$location.path('/result/' + $scope.runId);
};
$scope.testAgain = function() {
API.launchTest($scope.result.params.url);
API.relaunchTest($scope.result);
};
loadResults();
}]);
}]);
timelineCtrl.directive('scrollOnClick', ['$animate', '$timeout', function($animate, $timeout) {
return {
restrict: 'A',
link: function (scope, element, attributes) {
// When the user clicks on the timeline, find the right profiler line and scroll to it
element.on('click', function() {
var lineIndex = scope.findLineIndexByTimestamp(attributes.scrollOnClick);
var lineElement = angular.element(document.getElementById('line_' + lineIndex));
// Animate the background color to "flash" the row
lineElement.addClass('highlight');
$timeout(function() {
$animate.removeClass(lineElement, 'highlight');
scope.$digest();
}, 50);
window.scrollTo(0, lineElement[0].offsetTop);
});
}
};
}]);
File diff suppressed because it is too large Load Diff
+41 -6
View File
@@ -4,12 +4,34 @@ apiService.factory('API', ['$location', 'Runs', 'Results', function($location, R
return {
launchTest: function(url) {
Runs.save({
launchTest: function(url, settings) {
var runObject = {
url: url,
waitForResponse: false,
screenshot: true
}, function(data) {
screenshot: true,
device: settings.device,
waitForSelector: settings.waitForSelector,
cookie: settings.cookie,
authUser: settings.authUser,
authPass: settings.authPass,
blockDomain: settings.blockDomain,
allowedDomains: settings.allowedDomains,
noExternals: settings.noExternals
};
if (settings.domainsBlackOrWhite === 'black') {
runObject.blockDomain = this.parseDomains(settings.domains);
} else if (settings.domainsBlackOrWhite === 'white') {
var allowedDomains = this.parseDomains(settings.domains);
if (allowedDomains.length > 0) {
runObject.allowDomain = allowedDomains;
} else {
runObject.noExternals = true;
}
}
Runs.save(runObject, function(data) {
$location.path('/queue/' + data.runId);
}, function(response) {
if (response.status === 429) {
@@ -18,9 +40,22 @@ apiService.factory('API', ['$location', 'Runs', 'Results', function($location, R
alert('An error occured...');
}
});
},
relaunchTest: function(result) {
this.launchTest(result.params.url, result.params.options);
},
parseDomains: function(textareaContent) {
var lines = textareaContent.split('\n');
function removeEmptyLines (line) {
return line.trim() !== '';
}
// Remove empty lines
return lines.filter(removeEmptyLines).join(',');
}
};
}]);
+24
View File
@@ -0,0 +1,24 @@
var settingsService = angular.module('settingsService', []);
settingsService.factory('Settings', ['localStorageService', function(localStorageService) {
return {
getMergedSettings: function() {
var defaultSettings = {
device: 'desktop',
showAdvanced: false
};
var savedValues = localStorageService.get('settings');
return angular.extend(defaultSettings, savedValues);
},
saveSettings: function(settings) {
localStorageService.set('settings', settings);
}
};
}]);
+15 -1
View File
@@ -51,6 +51,16 @@
opacity: 0.85;
}
}
.screenshotWrapper.phone:hover:after {
top: 1.7em;
left: 0.64em;
}
.screenshotWrapper.tablet:hover:after {
top: 1.5em;
left: 0.9em;
}
}
.summary .notations {
@@ -98,7 +108,11 @@
.summary .notations .criteria .table {
width: 100%;
> div:hover > div {
> a {
text-decoration: none;
color: inherit;
}
> a:hover > div {
background: #EBD8E2;
cursor: pointer;
&.info {
+129 -53
View File
File diff suppressed because one or more lines are too long
+153 -3
View File
@@ -1,25 +1,175 @@
.promess {
padding: 0em 2em 3em;
padding: 0em 2em;
margin-bottom: 0.5em;
font-weight: normal;
font-size: 1.2em;
}
.price {
padding: 0em 2em 3em;
margin-top: 0em;
font-size: 0.9em;
}
.url {
width: 50%;
}
.launchBtn {
background: #e74c3c;
background: #ffa319;
color: #fff;
&:focus {
background: #e74c3c;
}
&.disabled {
background: #deaca6;
&:focus {
color: #ddd;
}
}
}
.settings {
width: 50%;
margin: 0 auto;
input, select {
font-size: 1em;
}
input[type=text], input[type=password], textarea {
width: 100%;
min-width: 4em;
}
}
.device {
margin-top: 3em;
.item {
display: inline-block;
margin: 1em 0.75em;
width: 5.5em;
height: 5.5em;
color: #FFF;
border: 1px solid #FFF;
padding: 1px;
border-radius: 0.5em;
cursor: pointer;
text-decoration: none;
font-size: 0.8em;
&.active {
color: #ffa319;
border: 2px solid #ffa319;
padding: 0;
}
&:hover {
color: #ffa319;
}
> div {
margin: 0.2em 0 0.1em;
font-size: 3em;
}
}
}
.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;
vertical-align: middle;
}
}
}
.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;
+78 -33
View File
@@ -126,11 +126,31 @@ a.linkButton {
}
.screenshotWrapper.desktop {
.screenshotWrapper {
display: inline-block;
position: relative;
border: 0.2em solid #AAA;
background: #000;
> div {
position: relative;
overflow: hidden;
&:hover {
overflow: auto;
}
}
.screenshotImage {
width: 100%;
}
.screenshotError {
color: #fff;
}
}
.screenshotWrapper.desktop {
border: 0.2em solid #AAA;
padding: 0.5em;
border-top-left-radius: 0.4em;
border-top-right-radius: 0.4em;
@@ -160,20 +180,61 @@ a.linkButton {
> div {
width: 12em;
height: 6.75em;
overflow: scroll;
position: relative;
}
.screenshotImage {
width: 100%;
}
}
.screenshotWrapper.phone {
border: 0.07em solid #CCC;
padding: 1em 0.3em 1.5em;
border-radius: 0.6em;
.star {
font-weight: bold;
span {
font-size: 1.2em;
&:before {
position: absolute;
width: 0.8em;
height: 0.8em;
bottom: 0.3em;
left: 3.3em;
border: 0.07em solid #CCC;
border-radius: 0.5em;
content: " ";
}
&:after {
position: absolute;
width: 1em;
height: 0.1em;
bottom: 13.9em;
left: 3.2em;
background: #555;
border-radius: 0.05em;
content: " ";
}
> div {
width: 6.75em;
height: 12em;
}
}
.screenshotWrapper.tablet {
border: 0.07em solid #CCC;
padding: 0.8em 0.5em 0.9em;
border-radius: 0.6em;
&:before {
position: absolute;
width: 0.5em;
height: 0.5em;
bottom: 0.15em;
left: 4.35em;
border: 0.07em solid #CCC;
border-radius: 0.4em;
content: " ";
}
> div {
width: 8.25em;
height: 11em;
}
}
@@ -186,27 +247,11 @@ a.linkButton {
.version {
font-size: 0.7em;
}
.github {
margin: 1em 0 0 0.5em;
}
}
/* Icons */
.icon-lab {
.icon(@lab);
}
.icon-question {
.icon(@question);
}
.icon-warning {
.icon(@warning);
}
.icon-back {
.icon(@arrow-left3);
}
.icon-summary {
.icon(@list);
}
.icon-spaghetti {
.icon(@bars);
}
.icon-loop {
.icon(@loop);
[class^="icon-"]:before, [class*=" icon-"]:before {
vertical-align: baseline;
}
+87 -9
View File
@@ -32,9 +32,18 @@
margin: 0 auto 0.5em;
}
.rule h3 {
margin-bottom: 0em;
}
.rule .okThreshold {
font-style: italic;
font-size: 0.9em;
}
.rule .message {
width: 80%;
margin: 0 auto;
margin: 1.5em auto;
p {
margin: 0.5em;
}
@@ -90,7 +99,11 @@
}
}
.offenders {
.rule .startTime {
display: none;
}
.offendersTable, .value {
.offenderButton {
display: inline-block;
position: relative;
@@ -99,6 +112,7 @@
margin: 0.2em 0;
border-radius: 0.4em;
z-index: 1;
cursor: pointer;
&.opens {
padding-right: 0.75em;
@@ -120,6 +134,7 @@
border-bottom-left-radius: 0.4em;
border-bottom-right-radius: 0.4em;
border-top: 1px solid #999;
z-index: 2;
}
.domTree {
@@ -140,7 +155,7 @@
padding: 0.5em;
}
&.opens.mouseOver {
&.opens:hover {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
background: #ffe0cc;
@@ -148,9 +163,14 @@
> div {
display: block;
background: #ffe0cc;
}
}
}
.smallerOffenders {
font-size: 0.9em;
}
}
.offendersHtml {
@@ -170,16 +190,18 @@
}
}
.colorPalette {
width: 30em;
border: 2px solid #000;
text-align: left;
.checker {
/* Checkerboard background */
background-color: #ddd;
background-image: linear-gradient(45deg, #AAA 25%, transparent 25%, transparent 75%, #AAA 75%, #AAA), linear-gradient(45deg, #AAA 25%, transparent 25%, transparent 75%, #AAA 75%, #AAA);
background-size:1em 1em;
background-position:0 0, 0.5em 0.5em;
}
.colorPalette {
width: 30em;
border: 2px solid #000;
text-align: left;
> div {
display: inline-block;
@@ -207,7 +229,6 @@
&:hover:after {
content: " ";
position: absolute;
background-color: inherit;
left: -0.2em;
top: -0.2em;
width: 100%;
@@ -216,4 +237,61 @@
border: 0.2em solid #f1c40f;
}
}
}
.similarColors {
margin: 1em;
width: 20em;
height: 6em;
> div {
display: inline-block;
width: 10em;
height: 3.5em;
padding-top: 2.5em;
}
}
.totalWeightPie {
max-width: 39em;
margin: 2em auto 4em;
canvas {
max-width: inherit;
}
}
.hugeFile {
font-weight: bold;
color: #e74c3c;
}
.imageOffenders {
display: table;
border-spacing: 3em;
width: 90%;
> div {
display: table-row;
> div {
display: table-cell;
vertical-align: middle;
}
}
img {
max-height: 10em;
max-width: 40em;
border: 1px solid #000;
margin-top: 0.5em;
}
}
.smallPreview {
display: block;
max-height: 4em;
max-width: 8em;
border: 1px solid #000;
margin: 1em auto 0.2em;
}
+133 -17
View File
@@ -14,6 +14,23 @@
text-align: center;
}
.selectScript {
padding-bottom: 2em;
font-size: 0.9em;
select {
max-width: 30em;
}
&.empty {
font-size: 0.8em;
select {
width: 10em;
}
}
}
.timeline {
margin: 2em 0 5em;
}
@@ -49,6 +66,10 @@
position: absolute;
bottom: 0;
width: 100%;
&.clickable {
cursor: pointer;
}
}
.timeline div.interval:hover {
background: #9C4274;
@@ -141,20 +162,9 @@
text-align: left;
}
.slowRequestsLimit {
width: 3em;
font-size: 1em;
text-align: right;
border: 1px solid #aaa;
}
input.textFilter {
box-shadow: none;
font-size: 1em;
padding: 0 0.2em;
border: 1px solid #aaa;
border-radius: none;
width: 15em;
.subFilters {
margin-left: 3em;
font-size: 0.9em;
}
.table {
@@ -163,7 +173,8 @@ input.textFilter {
border-spacing: 0.25em;
}
.table > div {
.table > div,
.table > a {
display: table-row;
}
@@ -172,7 +183,8 @@ input.textFilter {
padding: 0.5em 1em;
}
.table > div > div {
.table > div > div,
.table > a > div {
padding: 0.1em 1em;
background: #f2f2f2;
display: table-cell;
@@ -189,14 +201,82 @@ input.textFilter {
background: #f1c40f;
}
.table > div.highlight {
> div.startTime {
background-color: #C0F090;
}
}
.table > div.highlight-remove {
transition: 3s;
> div.startTime {
transition: background-color 3s ease-in;
}
}
.table > div > .index {
color: #bbb;
word-break: normal;
}
.table > div > .type {
white-space:nowrap;
}
.table .children {
margin-top: 0.2em;
font-size: 0.8em;
line-height: 1.6em;
}
.table .child {
margin-left: 0.5em;
> .child {
margin-left: 1em;
}
&:before {
content: "↳";
}
.childArgs {
display: none;
}
span {
position: relative;
}
span:hover {
background: #EBD8E2;
div {
display: inline-block;
}
.childArgs {
display: block;
position: absolute;
padding: 0 1em 0 2em;
left: 100%;
top: 0;
background: #EBD8E2;
line-height: 1.3em;
height: 1.3em;
z-index: 2;
}
}
}
.table .showingDetails .child span:hover {
background: inherit;
.childArgs {
display: none;
}
}
.table > div > .value {
width: 70%;
word-break: break-all;
@@ -215,6 +295,7 @@ input.textFilter {
}
.detailsOverlay {
display: none;
position: absolute;
right: 3em;
top: -3em;
@@ -224,13 +305,16 @@ input.textFilter {
background: #fff;
border: 2px solid #f1c40f;
border-radius: 0.5em;
z-index: 1;
z-index: 2;
}
@media screen and (max-width: 1024px) {
.detailsOverlay {
width: 25em;
}
}
.showDetails .detailsOverlay {
display: block;
}
.detailsOverlay .closeBtn {
position: absolute;
top: 0.5em;
@@ -268,4 +352,36 @@ input.textFilter {
.execution .icon-warning {
color: #e74c3c;
cursor: pointer;
}
.queryWithoutResultsFilterOn {
> div {
display: none;
&.queryWithoutResults {
display: table-row;
}
}
}
.jQueryCallOnEmptyObjectFilterOn {
> div {
display: none;
&.jQueryCallOnEmptyObject {
display: table-row;
}
}
}
.eventNotDelegatedFilterOn {
> div {
display: none;
&.eventNotDelegated {
display: table-row;
}
}
}
.jsErrorFilterOn {
> div {
display: none;
&.jsError {
display: table-row;
}
}
}
+29 -13
View File
@@ -16,13 +16,36 @@
<link rel="stylesheet" type="text/css" href="/css/screenshot.css">
<link rel="stylesheet" type="text/css" href="/css/timeline.css">
<link rel="stylesheet" type="text/css" href="/css/about.css">
<link rel="stylesheet" type="text/css" href="/node_modules/angular-chart.js/dist/angular-chart.css">
<!-- endbuild -->
<link rel="preconnect" href="//www.google-analytics.com">
<link rel="dns-prefetch" href="//www.google-analytics.com">
<link rel="preconnect" href="//ghbtns.com">
<link rel="dns-prefetch" href="//ghbtns.com">
<link rel="preconnect" href="https://api.github.com">
<link rel="dns-prefetch" href="https://api.github.com">
<head>
<body ng-app="YellowLabTools">
<div id="header"><h1>Yellow Lab <span class="icon-lab"></span> Tools</h1></div>
<div id="body" ng-view autoscroll="true"></div>
<div class="footer">
<span class="version">@@version</span>
<br><a href="/about">More about Yellow Lab Tools</a><br>
<div class="github"><iframe id="ghbtn" frameborder="0" scrolling="0" width="145px" height="30px"></iframe></div>
</div>
<!-- build:js /js/all.js -->
<script src="/bower_components/angular/angular.min.js"></script>
<script src="/bower_components/angular-route/angular-route.min.js"></script>
<script src="/bower_components/angular-resource/angular-resource.min.js"></script>
<script src="/bower_components/angular-sanitize/angular-sanitize.min.js"></script>
<script src="/node_modules/angular/angular.min.js"></script>
<script src="/node_modules/chart.js/Chart.min.js"></script>
<script src="/node_modules/angular-route/angular-route.min.js"></script>
<script src="/node_modules/angular-resource/angular-resource.min.js"></script>
<script src="/node_modules/angular-sanitize/angular-sanitize.min.js"></script>
<script src="/node_modules/angular-animate/angular-animate.min.js"></script>
<script src="/node_modules/angular-local-storage/dist/angular-local-storage.min.js"></script>
<script src="/node_modules/angular-chart.js/dist/angular-chart.min.js"></script>
<script src="/js/app.js"></script>
<script src="/js/controllers/indexCtrl.js"></script>
<script src="/js/controllers/dashboardCtrl.js"></script>
@@ -34,18 +57,11 @@
<script src="/js/models/runsFactory.js"></script>
<script src="/js/services/apiService.js"></script>
<script src="/js/services/menuService.js"></script>
<script src="/js/services/settingsService.js"></script>
<script src="/js/directives/gradeDirective.js"></script>
<script src="/js/directives/offendersDirectives.js"></script>
<!-- endbuild -->
<head>
<body ng-app="YellowLabTools">
<div id="header"><h1>Yellow Lab <span class="icon-lab"></span> Tools</h1></div>
<div id="body" ng-view autoscroll="true"></div>
<div class="footer">
<a href="/about">Learn more</a><br>If you like <b>Yellow Lab Tools</b>, <a href="https://github.com/gmetais/YellowLabTools" target="_blank" class="star">give it a <span>&#9733;</span> on GitHub</a>!<br>
<span class="version">@@version</span>
</div>
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','//www.google-analytics.com/analytics.js','ga');if('@@googleAnalyticsId'.indexOf('UA-')===0){ga('create','@@googleAnalyticsId','auto');}
+16 -7
View File
@@ -1,6 +1,13 @@
<div ng-include="'views/resultSubHeader.html'"></div>
<div class="summary board">
<div ng-if="result.blockedRequests">
<b><ng-pluralize count="result.blockedRequests.length" when="{'0': 'No blocked request', 'one': '1 blocked request', 'other': '{} blocked requests'}"></ng-pluralize>:</b>
<div ng-repeat="request in result.blockedRequests">
{{request}}
</div>
</div>
<div class="globalScore" ng-if="globalScore === 0 || globalScore > 0">
<div>
<h2>Global score</h2>
@@ -11,9 +18,10 @@
</div>
<div>
<a href="/result/{{result.runId}}/screenshot">
<div class="screenshotWrapper desktop">
<div class="screenshotWrapper" ng-class="result.params.options.device || 'desktop'">
<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>
</a>
@@ -27,27 +35,28 @@
<div class="category">{{category.label}}</div>
<div class="criteria">
<div class="table" title="Click to see details">
<div ng-repeat="ruleName in category.rules" ng-if="result.rules[ruleName]" ng-init="rule = result.rules[ruleName]"
ng-class="{'warning': rule.abnormal}" ng-click="showRulePage(ruleName)">
<a ng-repeat="ruleName in category.rules" ng-if="result.rules[ruleName]" ng-init="rule = result.rules[ruleName]"
ng-class="{'warning': rule.abnormal}" href="/result/{{runId}}/rule/{{ruleName}}">
<div class="grade">
<grade score="rule.score"></grade>
</div>
<div class="label">{{rule.policy.label}}</div>
<div class="result">
{{rule.value}}
<span ng-if="rule.policy.unit == 'bytes'">{{rule.value | bytes}}</span>
<span ng-if="rule.policy.unit != 'bytes'">{{rule.value}}</span>
<span ng-if="rule.abnormal" class="icon-warning"></span>
<span ng-if="rule.abnormalityScore <= -100" class="icon-warning"></span>
<span ng-if="rule.abnormalityScore <= -300" class="icon-warning"></span>
</div>
<div class="info"><span class="icon-question"></span></div>
</div>
</a>
</div>
</div>
</div>
</div>
<div ng-if="!error && !fromSocialShare" class="apiTip">
<b>Did you know? Yellow Lab Tools now has an API</b>! <a href="{{getAPIUrl()}}" target="_blank">Here</a> is the JSON output for this run. Checkout <a href="https://github.com/gmetais/YellowLabTools/wiki/Public-API" target="_blank">the API doc</a>.
<b>Did you know?</b> You can <b>bookmark</b> this result page URL or <b>share it</b>, Yellow Lab Tools keeps test results for years!
</div>
<div class="tweet" ng-if="!error && !fromSocialShare">
-7
View File
@@ -1,7 +0,0 @@
<div class="offenderButton" ng-class="{opens: obj.tree}">
<span ng-if="obj.type == 'html' || obj.type == 'body' || obj.type == 'head' || obj.type == 'window' || obj.type == 'document' || obj.type == 'fragment'"><b>{{obj.type}}</b></span>
<span ng-if="obj.type == 'domElement'">DOM element <b>{{obj.element}}</b></span>
<span ng-if="obj.type == 'fragmentElement'">Fragment element <b>{{obj.element}}</b></span>
<span ng-if="obj.type == 'createdElement'">Created element <b>{{obj.element}}</b></span>
<dom-tree ng-if="obj.tree" tree="obj.tree"></dom-tree>
</div>
+78 -2
View File
@@ -1,8 +1,84 @@
<h2 class="promess">Free online test to help speeding up <b>heavy</b> web pages</h2>
<h2 class="promess">Online test to help speeding up <b>heavy</b> web pages</h2>
<p class="price">Free and open source!</p>
<form ng-submit="launchTest()" >
<input type="text" name="url" ng-model="url" placeholder="http://www.mysite.com" class="url" />
<input type="submit" value="Launch test" class="launchBtn" ng-class="{disabled: !url}" />
<div class="settings">
<div class="device">
<div>Choose the simulated device:</div>
<a href="" class="item" ng-class="{active: settings.device == 'desktop'}" ng-click="settings.device = 'desktop'"><div class="icon-screen"></div>Desktop</a>
<a href="" class="item" ng-class="{active: settings.device == 'tablet'}" ng-click="settings.device = 'tablet'"><div class="icon-tablet"></div>Tablet</a>
<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="password" class="authField" name="authPass" ng-model="settings.authPass" /></div>
</div>
</div>
</div>
<div>
<div class="label">
Block domains
<span class="settingsTooltip">
<span class="icon-question"></span>
<div><b>Block some domains</b><br><br>One line per domain or subdomain.<br><br><i><b>Example:</b><br>google-analytics.com<br>ads.yahoo.com<br>ajax.googleapis.com</i><br><br>An empty whitelist will block all domains except the main domain.</div>
</span>
</div>
<div>
<div>
<input type="radio" name="blackOrWhite" ng-model="settings.domainsBlackOrWhite" value="black" />blacklist
<input type="radio" name="blackOrWhite" ng-model="settings.domainsBlackOrWhite" value="white" />whitelist
</div>
<textarea name="domains" ng-model="settings.domains" rows="5"></textarea>
</div>
</div>
</div>
</div>
</form>
@@ -21,4 +97,4 @@
<h3>JS Profiling</h3>
<p>Untangles the JavaScript spaghetti code</p>
</div>
</div>
</div>
+1 -1
View File
@@ -26,7 +26,7 @@
</div>
<div ng-if="notFound == true">
<div class="status">Error 404 (test not found)</div>
<p class="statusSubMessage">The server probably rebooted. We are very sorry about that, please try to launch the test again.</p>
<p class="statusSubMessage">The server probably just rebooted. We are very sorry about that, please try to launch the test again.</p>
<a class="linkButton" href="/">New test</a>
</div>
+3 -3
View File
@@ -1,8 +1,8 @@
<div>Tested url: &nbsp; <a href="{{result.params.url}}" target="_blank" class="testedUrl">{{result.params.url}}</a></div>
<div class="resultsMenu">
<a class="menuItem back" href="/"><div class="icon-back"></div><span>New test<span></a>
<a class="menuItem back" href="/"><div class="icon-arrow-left3"></div><span>New test<span></a>
<a class="menuItem restart" href="" ng-click="testAgain()"><div class="icon-loop"></div><span>Test again<span></a>
<div class="menuItem" ng-class="{active: Menu.getCurrentPage() == 'dashboard'}" ng-click="Menu.changePage('dashboard')"><div class="icon-summary"></div><span>Dashboard</span></div>
<div class="menuItem" ng-class="{active: Menu.getCurrentPage() == 'timeline'}" ng-click="Menu.changePage('timeline')"><div class="icon-spaghetti"></div><span>JS Timeline</span></div>
<div class="menuItem" ng-class="{active: Menu.getCurrentPage() == 'dashboard'}" ng-click="Menu.changePage('dashboard')"><div class="icon-list"></div><span>Dashboard</span></div>
<div class="menuItem" ng-class="{active: Menu.getCurrentPage() == 'timeline'}" ng-click="Menu.changePage('timeline')"><div class="icon-bars"></div><span>JS Timeline</span></div>
</div>
+256 -24
View File
@@ -9,7 +9,19 @@
<div>{{rule.score}}/100</div>
</div>
<div class="right">
<h3>Value: {{rule.value}}</h3>
<h3>
Value:
<span ng-if="rule.policy.unit == 'bytes'">{{rule.value | bytes}}</span>
<span ng-if="rule.policy.unit != 'bytes'">{{rule.value}}</span>
</h3>
<div class="okThreshold" ng-if="rule.score < 100 && rule.policy.isOkThreshold !== undefined">
Have
<span ng-if="rule.policy.unit == 'bytes'">{{rule.policy.isOkThreshold | bytes}}</span>
<span ng-if="rule.policy.unit != 'bytes'">{{rule.policy.isOkThreshold}}</span>
<span ng-if="rule.policy.isOkThreshold > 0 && rule.policy.isOkThreshold < rule.policy.isBadThreshold">or less</span>
<span ng-if="rule.policy.isOkThreshold > rule.policy.isBadThreshold">or more</span>
to get the 100/100 score.
</div>
<div ng-bind-html="rule.policy.message" class="message"></div>
</div>
</div>
@@ -31,20 +43,25 @@
<b>{{offender.id}}</b>: {{offender.occurrences}} occurrences
</div>
<div ng-if="policyName === 'DOMinserts'">
<dom-element-button obj="offender.insertedElement"></dom-element-button> appended to <dom-element-button obj="offender.receiverElement"></dom-element-button>
</div>
<div ng-if="policyName === 'DOMqueriesWithoutResults'">
<b>{{offender.query}}</b> (in <dom-element-button obj="offender.context"></dom-element-button>) using {{offender.fn}}
</div>
<div ng-if="policyName === 'DOMqueriesAvoidable'">
<b>{{offender.query}}</b> (in <dom-element-button obj="offender.context"></dom-element-button>) using {{offender.fn}}: <b>{{offender.count}} queries</b>
</div>
<div ng-if="policyName === 'eventsBound'">
<b>{{offender.eventName}}</b> bound to <dom-element-button obj="offender.element"></dom-element-button>
<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'">
@@ -62,6 +79,38 @@
</div>
</div>
<div ng-if="policyName === 'jQueryFunctionsUsed'">
function <b>{{offender.functionName}}</b> used {{offender.count}} times
</div>
<div ng-if="policyName === 'documentWriteCalls'">
<b>{{offender.writeFn}}</b>
<span ng-if="offender.from">
called from
<span ng-if="offender.from.functionName">{{offender.from.functionName}}()</span>
<url-link url="offender.from.file" max-length="50"></url-link>
line {{offender.from.line}}
</span>
<span ng-if="!offender.from">
called from (no backtrace available)
</span>
</div>
<div ng-if="policyName === 'cssRules'">
<span ng-if="offender.file === 'inline CSS'">inline CSS</span>
<span ng-if="offender.file !== 'inline CSS'"><url-link url="offender.file" max-length="80"></url-link></span>
: <ng-pluralize count="offender.rules" when="{'0': '0 rule', 'one':'1 rule','other':'{} rules'}"></ng-pluralize>
</div>
<div ng-if="policyName === 'similarColors'">
<div class="similarColors checker"><div ng-style="{'background-color': offender.color1, 'color': offender.isDark ? '#FFF' : '#000'}">{{offender.color1}}</div><div ng-style="{'background-color': offender.color2, 'color': offender.isDark ? '#FFF' : '#000'}">{{offender.color2}}</div></div>
</div>
<div ng-if="policyName === 'cssMobileFirst'">
<b>{{offender.query}}</b> for <ng-pluralize count="offender.rules" when="{'one':'1 rule','other':'{} rules'}"></ng-pluralize>
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
</div>
<div ng-if="policyName === 'cssParsingErrors'">
<b>{{offender.error}}</b>
<file-and-line file="offender.file" line="offender.line" column="offender.column"></file-and-line>
@@ -105,15 +154,23 @@
</div>
<div ng-if="policyName === 'cssOldPropertyPrefixes'">
{{offender.rule}} {<b>{{offender.property}}</b>: {{offender.value + '}' }}
<br>
<b>{{offender.message}}</b>
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
<b>{{offender.property}} {{offender.message}}</b>
<div ng-if="offender.rules.length" ng-click="offender.showMore = !offender.showMore" class="offenderButton">
<span ng-if="!offender.showMore">show</span>
<span ng-if="offender.showMore">hide</span>
<ng-pluralize count="offender.rules.length" when="{'one':'1 offender','other':'{} offenders'}"></ng-pluralize>
</div>
<div ng-if="offender.showMore" class="smallerOffenders">
<div ng-repeat="cssRule in offender.rules">
{{cssRule.rule}} {{'{' + offender.property}}: {{cssRule.value + '}' }}
<file-and-line-button file="cssRule.file" line="cssRule.line" column="cssRule.column"></file-and-line-button>
</div>
</div>
</div>
<div ng-if="policyName === 'requests' || policyName === 'htmlCount' || policyName === 'jsCount' || policyName === 'cssCount' || policyName === 'imageCount' || policyName === 'webfontCount' || policyName === 'videoCount' || policyName === 'jsonCount' || policyName === 'otherCount' || policyName === 'smallJsFiles' || policyName === 'smallCssFiles' || policyName === 'smallImages'">
<url-link url="offender.file" max-length="100"></url-link>
<span ng-if="offender.size || offender.size === 0">({{offender.size}} kB)</span>
<div ng-if="policyName === 'lazyLoadableImagesBelowTheFold' || policyName === 'hiddenImages'">
<img ng-src="{{offender}}" class="smallPreview checker"></img>
<url-link url="offender" max-length="100"></url-link>
</div>
<div ng-if="policyName === 'notFound' || policyName === 'closedConnections' || policyName === 'multipleRequests' || policyName === 'cachingDisabled' || policyName === 'cachingNotSpecified'">
@@ -138,23 +195,198 @@
</div>
</div>
<div ng-if="!rule.offendersObj.list" class="offendersHtml">
<div ng-if="policyName === 'DOMelementMaxDepth'">
<dom-tree tree="rule.offendersObj.tree"></dom-tree>
</div>
<div ng-if="policyName === 'cssColors' && rule.offendersObj.count > 0">
<p>This is the colors palette, sized by total occurrences:</p>
<div class="colorPalette">
<div ng-repeat="offender in rule.offendersObj.palette" style="background-color: {{offender.color}}; width: {{offender.occurrences * 100 / rule.offendersObj.palette[0].occurrences}}%"><div>{{offender.color}} ({{offender.occurrences}} times)</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">
<p>This is the colors palette, sized by total occurrences:</p>
<div class="colorPalette checker">
<div ng-repeat="offender in rule.offendersObj.palette" style="background-color: {{offender.color}}; width: {{offender.occurrences * 100 / rule.offendersObj.palette[0].occurrences}}%"><div>{{offender.color}} ({{offender.occurrences}} times)</div></div>
</div>
</div>
</div>
</div>
<div ng-if="policyName === 'totalWeight'">
<h3>Weight by MIME type</h3>
<div class="totalWeightPie">
<canvas class="chart chart-doughnut" data="weightData" labels="weightLabels" options="weightOptions" colours="weightColours" legend="true"></canvas>
</div>
<div ng-repeat="type in weightLabels">
<h3>{{rule.offendersObj.list.byType[type].totalWeight | bytes}} of {{type}}</h3>
<div class="offendersTable">
<div ng-repeat="request in rule.offendersObj.list.byType[type].requests | orderBy:'-weight'" ng-if="request.weight > 0">
<div><url-link url="request.url" max-length="60"></url-link></div>
<div ng-class="{hugeFile: request.weight > 102400}">{{request.weight | bytes}}</div>
</div>
</div>
</div>
</div>
<div ng-if="policyName === 'imageOptimization'">
<h3 ng-if="rule.value > 0">{{rule.value | bytes}} could be saved on <ng-pluralize count="rule.offendersObj.list.images.length" when="{'one': '1 image', 'other': '{} images'}"></ng-pluralize></h3>
<div class="imageOffenders">
<div ng-repeat="image in rule.offendersObj.list.images | orderBy:'-gain'">
<div>
Current file: <url-link url="image.url" max-length="50"></url-link>
<div><a href="{{image.url}}" target="_blank"><img ng-src="{{image.url}}" class="checker" /></a></div>
</div>
<div>
<p ng-if="!image.afterCompression">Current weight: {{image.original | bytes}}</p>
<p ng-if="image.afterCompression">Current weight: {{image.original | bytes}} ({{image.afterCompression | bytes}} gzipped)</p>
<p ng-if="image.lossless && image.afterOptimizationAndCompression">With a lossless optimization:<br/>{{image.afterOptimizationAndCompression | bytes}} gzipped (<b>-{{image.gain | bytes}}</b> gzipped)</p>
<p ng-if="image.lossless && !image.afterOptimizationAndCompression">With a lossless optimization:<br/>{{image.lossless | bytes}} <span ng-if="!image.lossy">(<b>-{{image.gain | bytes}}</b>)</span></p>
<p ng-if="image.lossy && image.afterOptimizationAndCompression">With a lossy optimization:<br/>{{image.afterOptimizationAndCompression | bytes}} gzipped (<b>-{{image.gain | bytes}} gzipped</b>)</p>
<p ng-if="image.lossy && !image.afterOptimizationAndCompression">With a lossy optimization:<br/>{{image.lossy | bytes}} (<b>-{{image.gain | bytes}}</b>)</p>
</div>
</div>
</div>
</div>
<div ng-if="policyName === 'gzipCompression'">
<h3 ng-if="rule.value > 0">{{rule.value | bytes}} could be saved on <ng-pluralize count="rule.offendersObj.list.images.length" when="{'one': '1 file', 'other': '{} files'}"></ng-pluralize></h3>
<div class="table">
<div class="headers">
<div>File</div>
<div>Current weight</div>
<div>Gzipped</div>
<div>Gain</div>
</div>
<div ng-repeat="file in rule.offendersObj.list.files | orderBy:'-gain'">
<div>
<url-link url="file.url" max-length="70"></url-link>
</div>
<div>{{file.original | bytes}}</div>
<div>{{file.gzipped | bytes}}</div>
<div><b>-{{file.gain | bytes}}</b></div>
</div>
</div>
</div>
<div ng-if="policyName === 'fileMinification'">
<h3 ng-if="rule.value > 0">{{rule.value | bytes}} could be saved on <ng-pluralize count="rule.offendersObj.list.files.length" when="{'one': '1 file', 'other': '{} files'}"></ng-pluralize></h3>
<div class="table">
<div class="headers">
<div>File</div>
<div>Current weight</div>
<div>Minified</div>
<div>Gain</div>
</div>
<div ng-repeat="file in rule.offendersObj.list.files | orderBy:'-gain'">
<div>
<url-link url="file.url" max-length="70"></url-link>
</div>
<div ng-if="!file.afterCompression">{{file.original | bytes}} (gzipped)</div>
<div ng-if="file.afterCompression">{{file.original | bytes}} ({{file.afterCompression | bytes}} gzipped)</div>
<div ng-if="!file.afterCompression">{{file.afterOptimizationAndCompression | bytes}} (gzipped)</div>
<div ng-if="file.afterCompression">{{file.optimized | bytes}} ({{file.afterOptimizationAndCompression | bytes}} gzipped)</div>
<div><b>-{{file.gain | bytes}}</b></div>
</div>
</div>
</div>
<div ng-if="policyName === 'totalRequests'">
<h3>Requests by MIME type</h3>
<div ng-repeat="(type, requests) in rule.offendersObj.list.byType">
<h3><ng-pluralize count="requests.length" when="{'0': 'No ' + type + ' request', 'one': '1 ' + type + ' request', 'other': '{} ' + type + ' requests'}"></ng-pluralize></h3>
<p ng-if="type == 'css' && requests.length > 2">Reduce the number of stylesheets by concatenating them.</p>
<p ng-if="type == 'js' && requests.length > 3">Reduce the number of scripts by concatenating them.</p>
<p ng-if="type == 'image' && requests.length > 5">Reduce the number of images by lazyloading them or by spriting them.</p>
<p ng-if="type == 'webfont' && requests.length > 1">Fonts are generally loaded on the critical path of the head. Load as few as possible.</p>
<p ng-if="type == 'other' && requests.length > 0">They can be Flash, XML, music or any undetected format.</p>
<div class="offendersTable">
<div ng-repeat="request in requests track by $index">
<div><url-link url="request" max-length="100"></url-link></div>
</div>
</div>
</div>
</div>
<div ng-if="policyName === 'smallRequests'">
<div ng-repeat="(type, requests) in rule.offendersObj.list.byType">
<h3><ng-pluralize count="requests.length" when="{'0': 'small ' + type + ' file', 'one': '1 small ' + type + ' file', 'other': '{} small ' + type + ' files'}"></ng-pluralize></h3>
<p ng-if="type == 'css' && requests.length > 0">Try to inline these styles in the head of the HTML or to merge them with other files.</p>
<p ng-if="type == 'js' && requests.length > 0">Try to inline these scripts in the HTML or merge them with other files.</p>
<p ng-if="type == 'image' && requests.length > 0">Try to inline these images (with base64 encoding for most image types except SVG that don't need base64 encoding). You can also create sprites.</p>
<div class="table">
<div class="headers">
<div ng-if="type == 'image'">Preview</div>
<div>File</div>
<div>Weight (bytes)</div>
</div>
<div ng-repeat="request in requests track by $index">
<div ng-if="type == 'image'"><img ng-src="{{request.url}}" class="smallPreview checker" /></div>
<div><url-link url="request.url" max-length="100"></url-link></div>
<div>{{request.size}}</div>
</div>
</div>
</div>
</div>
<div ng-if="policyName === 'DOMaccesses'">
<h3>{{rule.value}} offenders</h3>
Please open the <a href="/result/{{runId}}/timeline">JS timeline</a>
</div>
<div ng-if="policyName === 'queriesWithoutResults'">
<h3>{{rule.value}} offenders</h3>
Please open the <a href="/result/{{runId}}/timeline#filter=queryWithoutResults">JS timeline, filtered by "Queries without results"</a>
</div>
<div ng-if="policyName === 'jQueryCallsOnEmptyObject'">
<h3>{{rule.value}} offenders</h3>
Please open the <a href="/result/{{runId}}/timeline#filter=jQueryCallOnEmptyObject">JS timeline, filtered by "jQuery calls on empty object"</a>
</div>
<div ng-if="policyName === 'jQueryNotDelegatedEvents'">
<h3>{{rule.value}} offenders</h3>
Please open the <a href="/result/{{runId}}/timeline#filter=eventNotDelegated">JS timeline, filtered by "Events not delegated"</a>
</div>
<div ng-if="policyName === 'cssBreakpoints'">
<div ng-if="rule.value > 0" class="cssBreakpointsGraph">
<h3>Breakpoints distribution graph</h3>
<canvas class="chart chart-line" chart-data="breakpointsData" chart-labels="breakpointsLabels" chart-options="breakpointsOptions" chart-legend="true" chart-colours="breakpointsColours" chart-series="breakpointsSeries" width="600" height="250"></canvas>
<h3>Breakpoints list</h3>
<div class="offendersTable">
<div ng-repeat="offender in rule.offendersObj | orderBy:'pixels'">
<div>Breakpoint <b>{{offender.breakpoint}}</b> involves <ng-pluralize count="offender.count" when="{'one': '1 rule', 'other': '{} rules'}"></ng-pluralize></div>
</div>
</div>
</div>
<div ng-if="rule.value === 0">
No breakpoint
</div>
</div>
<div ng-if="policyName === 'http2'">
<h3>Protocols advertised by the server</h3>
<div class="offendersTable">
<div ng-repeat="protocol in rule.offendersObj.list">
<div>{{protocol}}</div>
</div>
<div ng-if="!rule.offendersObj || rule.offendersObj.count == 0">
<div>none</div>
</div>
</div>
</div>
<div ng-if="!rule && rule !== null" class="notFound">
<h2>404</h2>
Rule "{{policyName}}"" not found
+3 -2
View File
@@ -2,9 +2,10 @@
<div class="screenshot board">
<h2>Screenshot</h2>
<div class="screenshotWrapper desktop">
<div class="screenshotWrapper" ng-class="result.params.options.device || 'desktop'">
<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>
+47 -91
View File
@@ -1,7 +1,14 @@
<div ng-include="'views/resultSubHeader.html'"></div>
<div class="execution board">
<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>
<div class="selectScript" ng-class="{empty:!selectedScript}">
Filter timeline and profiler by script:
<select ng-model="selectedScript" ng-options="script.shortPath for script in scripts" ng-change="changeScript()">
<option value="">All (no filter)</option>
</select>
</div>
<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>
<div class="timeline">
<div class="chart">
@@ -9,18 +16,20 @@
<div ng-repeat="duration in timeline track by $index"
class="interval"
ng-class="{
domCreation: $index * timelineIntervalDuration < result.toolsResults.phantomas.metrics.domInteractive,
domInteractive: $index * timelineIntervalDuration >= result.toolsResults.phantomas.metrics.domInteractive
&& $index * timelineIntervalDuration < result.toolsResults.phantomas.metrics.domContentLoaded,
domContentLoaded: $index * timelineIntervalDuration >= result.toolsResults.phantomas.metrics.domContentLoaded
&& $index * timelineIntervalDuration < result.toolsResults.phantomas.metrics.domContentLoadedEnd,
domContentLoadedEnd: $index * timelineIntervalDuration >= result.toolsResults.phantomas.metrics.domContentLoadedEnd
&& $index * timelineIntervalDuration < result.toolsResults.phantomas.metrics.domComplete,
domComplete: $index * timelineIntervalDuration >= result.toolsResults.phantomas.metrics.domComplete
domCreation: $index * timelineIntervalDuration < result.javascriptExecutionTree.data.domInteractive,
domInteractive: $index * timelineIntervalDuration >= result.javascriptExecutionTree.data.domInteractive
&& $index * timelineIntervalDuration < result.javascriptExecutionTree.data.domContentLoaded,
domContentLoaded: $index * timelineIntervalDuration >= result.javascriptExecutionTree.data.domContentLoaded
&& $index * timelineIntervalDuration < result.javascriptExecutionTree.data.domContentLoadedEnd,
domContentLoadedEnd: $index * timelineIntervalDuration >= result.javascriptExecutionTree.data.domContentLoadedEnd
&& $index * timelineIntervalDuration < result.javascriptExecutionTree.data.domComplete,
domComplete: $index * timelineIntervalDuration >= result.javascriptExecutionTree.data.domComplete
}">
<div style="height: {{100 * duration / timelineMax | number: 0}}px" class="color"></div>
<div style="height: {{100 * duration / timelineMax | number: 0}}px" class="color" ng-class="{clickable: duration > 0}" scroll-on-click="{{$index * timelineIntervalDuration}}"></div>
<div class="tooltip detailsOverlay">
<div>Timestamp: {{$index * timelineIntervalDuration | number: 0}} ms</div>
<div>
Timestamp: {{$index * timelineIntervalDuration | number: 0}} ms
</div>
</div>
</div>
</div>
@@ -36,7 +45,7 @@
<div class="domComplete"><div class="color"></div>Page is complete</div>
</div>
<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>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>
@@ -45,24 +54,36 @@
</div>
</div>
<h2>Javascript Profiler</h2>
<h2>JavaScript Profiler</h2>
<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>
<div class="filters">
<div>
<input type="checkbox" ng-model="warningsFilterOn" id="warningsFilterOn" />
<label for="warningsFilterOn">Show warnings only</label>
</div>
<div>
<input type="checkbox" ng-model="textFilterOn" />
Filter by
<input type="text" ng-model="textFilter" placeholder="search..." class="textFilter" ng-change="textFilterOn = true" />
<input type="checkbox" name="warningsFilter" ng-model="warningsFilterOn" id="warningsFilterOn" />
<label for="warningsFilterOn">Filter on warnings and errors</label>
<div class="subFilters" ng-if="warningsFilterOn">
<div>
<input type="checkbox" name="filters" ng-model="warningsFilters.queryWithoutResults" id="queryWithoutResultsFilterOn"/>
<label for="queryWithoutResultsFilterOn">Queries without results</label>
</div>
<div>
<input type="checkbox" name="filters" ng-model="warningsFilters.jQueryCallOnEmptyObject" id="jQueryCallOnEmptyObjectFilterOn" />
<label for="jQueryCallOnEmptyObjectFilterOn">jQuery calls on empty object</label>
</div>
<div>
<input type="checkbox" name="filters" ng-model="warningsFilters.eventNotDelegated" id="eventNotDelegatedFilterOn" />
<label for="eventNotDelegatedFilterOn">Events not delegated</label>
</div>
<div>
<input type="checkbox" name="filters" ng-model="warningsFilters.jsError" id="jsErrorFilterOn" />
<label for="jsErrorFilterOn">Errors</label>
</div>
</div>
</div>
{{queryWithoutResultsFilterOn}}
</div>
<div class="table">
<toto data-info="mavariable"></toto>
<div class="table" ng-class="{queryWithoutResultsFilterOn: warningsFilterOn && warningsFilters.queryWithoutResults, jQueryCallOnEmptyObjectFilterOn: warningsFilterOn && warningsFilters.jQueryCallOnEmptyObject, eventNotDelegatedFilterOn: warningsFilterOn && warningsFilters.eventNotDelegated, jsErrorFilterOn: warningsFilterOn && warningsFilters.jsError}">
<div class="headers">
<div><!-- index --></div>
<div>Type</div>
@@ -70,74 +91,9 @@
<div><!-- details --></div>
<div>Timestamp</div>
</div>
<div ng-if="(!warningsFilterOn || node.warning || node.error) && (!textFilterOn || !textFilter.length || node.searchIndex.indexOf(textFilter) >= 0)"
ng-repeat="node in profilerData" ng-class="{
showingDetails: node.showDetails,
jsError: node.error,
windowPerformance: node.windowPerformance
}">
<div class="index">{{$index + 1}}</div>
<div class="type">{{node.data.type}}</div>
<profiler-line ng-repeat="node in profilerData" data-index="$index" node="node"></profiler-line>
<div class="value">
{{node.data.callDetails.arguments[0]}}
<span ng-if="node.data.callDetails.arguments.length > 1"> : {{node.data.callDetails.arguments[1]}}</span>
<span ng-if="node.data.callDetails.arguments.length > 2"> : {{node.data.callDetails.arguments[2]}}</span>
<span ng-if="node.data.callDetails.arguments.length > 3"> : {{node.data.callDetails.arguments[3]}}</span>
</div>
<div class="details">
<div ng-class="{'icon-question': !node.warning && !node.error, 'icon-warning': node.warning || node.error}"
ng-click="onNodeDetailsClick(node)"
ng-if="node.data.type != 'jQuery loaded' && node.data.type != 'jQuery version change' && !node.windowPerformance"></div>
<div class="detailsOverlay" ng-if="node.showDetails">
<div class="closeBtn" ng-click="onNodeDetailsClick(node)"></div>
<div ng-if="node.data.callDetails.context.domElement">
<h4>Called on DOM element</h4>
<div>{{node.data.callDetails.context.domElement}}</div>
</div>
<div ng-if="node.data.callDetails.context.length === 0">
<h4>Called on 0 jQuery element</h4>
<p class="advice">Useless function call, as the jQuery object is empty.</p>
</div>
<div ng-if="node.data.callDetails.context.length == 1 && node.data.callDetails.context.firstElementPath">
<h4>Called on 1 jQuery element</h4>
<div>{{node.data.callDetails.context.firstElementPath}}</div>
</div>
<div ng-if="node.data.callDetails.context.length > 1">
<h4>Called on {{node.data.callDetails.context.length}} jQuery elements</h4>
<p class="advice" ng-if="node.data.type == 'jQuery - bind' && node.data.callDetails.context.length > 5">
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>
<p ng-if="node.data.callDetails.context.firstElementPath"><b>First one is:</b> {{node.data.callDetails.context.firstElementPath}}</p>
</div>
<p class="advice" ng-if="node.data.resultsNumber === 0">
The query returned 0 results. Could it be unused or dead code?
</p>
<div ng-if="node.parsedBacktrace">
<h4>Backtrace</h4>
<div class="table">
<div ng-repeat="trace in node.parsedBacktrace track by $index">
<div>{{trace.fnName || '(anonymous)'}}</div>
<div class="trace"><a href="{{trace.filePath}}" title="{{trace.filePath}}" target="_blank">{{trace.fileName || 'HTML'}}</a>:{{trace.line}}</div>
</div>
<div ng-if="node.parsedBacktrace.length == 0 && node.data.type != 'script loaded'">
<div>can't find any backtrace :/</div>
</div>
</div>
</div>
</div>
</div>
<div class="startTime" ng-class="node.data.loadingStep">{{node.data.timestamp | number: 0}} ms</div>
</div>
</div>
<div class="backToDashboard"><a href="#" ng-click="backToDashboard()">Back to dashboard</a></div>
+430 -382
View File
File diff suppressed because it is too large Load Diff
+56 -51
View File
@@ -1,9 +1,30 @@
{
"categories": {
"pageWeight": {
"label": "Page weight",
"policies": {
"totalWeight": 5,
"imageOptimization": 2,
"gzipCompression": 2,
"fileMinification": 1
}
},
"requests": {
"label": "Requests",
"policies": {
"totalRequests": 5,
"domains": 3,
"notFound": 3,
"multipleRequests": 2,
"smallRequests": 1,
"lazyLoadableImagesBelowTheFold": 2,
"hiddenImages": 1
}
},
"domComplexity": {
"label": "DOM complexity",
"policies": {
"DOMelementsCount": 1,
"DOMelementsCount": 3,
"DOMelementMaxDepth": 1,
"iframesCount": 1,
"DOMidDuplicated": 1
@@ -12,11 +33,16 @@
"domManipulations": {
"label": "DOM manipulations",
"policies": {
"DOMinserts": 2,
"DOMqueries": 1,
"DOMqueriesWithoutResults": 2,
"DOMqueriesAvoidable": 2,
"eventsBound": 1
"DOMaccesses": 3,
"queriesWithoutResults": 1,
"DOMqueriesAvoidable": 1
}
},
"scroll": {
"label": "Scroll bottlenecks",
"policies": {
"eventsScrollBound": 1,
"DOMaccessesOnScroll": 4
}
},
"badJavascript": {
@@ -28,17 +54,14 @@
"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": 2,
"jQueryVersionsLoaded": 2,
"jQueryFunctionsUsed": 1,
"jQueryCallsOnEmptyObject": 1,
"jQueryNotDelegatedEvents": 1
}
},
"cssComplexity": {
@@ -47,12 +70,16 @@
"cssRules": 2,
"cssComplexSelectors": 2,
"cssComplexSelectorsByAttribute": 1.5,
"cssColors": 0.5
"cssColors": 0.5,
"similarColors": 0.5,
"cssBreakpoints": 1,
"cssMobileFirst": 1
}
},
"badCSS": {
"label": "Bad CSS",
"policies": {
"cssParsingErrors": 4,
"cssImports": 3,
"cssDuplicatedSelectors": 2,
"cssDuplicatedProperties": 1,
@@ -66,50 +93,28 @@
"cssRedundantChildNodesSelectors": 1
}
},
"requests": {
"label": "Requests number",
"serverConfig": {
"label": "Server config",
"policies": {
"requests": 5,
"htmlCount": 0,
"jsCount": 1,
"cssCount": 1,
"imageCount": 0,
"webfontCount": 2,
"videoCount": 0,
"jsonCount": 0,
"otherCount": 0
}
},
"smallRequests": {
"label": "Small requests",
"policies": {
"smallJsFiles": 1,
"smallCssFiles": 1,
"smallImages": 1
}
},
"network": {
"label": "Network",
"policies": {
"notFound": 3,
"closedConnections": 3,
"multipleRequests": 3,
"http2": 2,
"closedConnections": 2,
"cachingNotSpecified": 1,
"cachingDisabled": 1,
"cachingTooShort": 1,
"domains": 1
"cachingTooShort": 1
}
}
},
"globalScore": {
"domComplexity": 1,
"pageWeight": 3,
"requests": 3,
"domComplexity": 2,
"domManipulations": 2,
"scroll": 1,
"badJavascript": 1,
"jQueryVersion": 1,
"jQuery": 1,
"cssSyntaxError": 1,
"cssComplexity": 1,
"badCSS": 1,
"requests": 3,
"smallRequests": 1,
"network": 2
"serverConfig": 1
}
}
+14 -2
View File
@@ -32,6 +32,15 @@ var OffendersHelpers = function() {
};
this.domPathToDomElementObj = function(domPath) {
if (typeof domPath === 'boolean') {
return {
// Not a normal element path
type: 'notAnElement',
element: domPath
};
}
var domArray = this.domPathToArray(domPath);
var domTree = this.listOfDomArraysToTree([this.domPathToArray(domPath)]);
@@ -106,7 +115,7 @@ var OffendersHelpers = function() {
var parts = null;
for (var i=0 ; i<traceArray.length ; i++) {
parts = /^(([\w$]+) )?([^ ]+):(\d+)$/.exec(traceArray[i]);
parts = /^(([\w$]+) )?\(?([^ ]+):(\d+)\)?$/.exec(traceArray[i]);
if (parts) {
var obj = {
@@ -142,6 +151,9 @@ var OffendersHelpers = function() {
};
this.cssOffenderPattern = function(offender) {
// Remove any line breaks
offender = offender.replace(/(\r\n|\n|\r)/gm, '');
var parts = /^(.*) (?:<([^ \(]*)>|\[inline CSS\]) @ (\d+):(\d+)$/.exec(offender);
if (!parts) {
@@ -159,7 +171,7 @@ var OffendersHelpers = function() {
};
this.fileWithSizePattern = function(fileWithSize) {
var parts = /^([^ ]*) \((\d+\.\d{2}) kB\)$/.exec(fileWithSize);
var parts = /^([^ ]*) \((\d+\.\d{2}|NaN) kB\)$/.exec(fileWithSize);
if (!parts) {
return {
+32 -6
View File
@@ -3,6 +3,10 @@ var debug = require('debug')('ylt:runner');
var phantomasWrapper = require('./tools/phantomas/phantomasWrapper');
var jsExecutionTransformer = require('./tools/jsExecutionTransformer');
var colorDiff = require('./tools/colorDiff');
var mediaQueriesChecker = require('./tools/mediaQueriesChecker');
var isHttp2 = require('./tools/isHttp2');
var weightChecker = require('./tools/weightChecker/weightChecker');
var rulesChecker = require('./rulesChecker');
var scoreCalculator = require('./scoreCalculator');
@@ -19,14 +23,31 @@ var Runner = function(params) {
};
// Execute Phantomas first
phantomasWrapper.execute(data).then(function(phantomasResults) {
phantomasWrapper.execute(data)
.then(function(phantomasResults) {
data.toolsResults.phantomas = phantomasResults;
// Treat the JS Execution Tree from offenders
data.javascriptExecutionTree = jsExecutionTransformer.transform(data);
data = jsExecutionTransformer.transform(data);
// Other tools go here
// Compare colors
data = colorDiff.compareAllColors(data);
// Check media queries
data = mediaQueriesChecker.analyzeMediaQueries(data);
// Redownload every file
return weightChecker.recheckAllFiles(data);
})
.then(function(data) {
// Check if HTTP2
return isHttp2.check(data);
})
.then(function(data) {
// Rules checker
var policies = require('./metadata/policies');
@@ -42,15 +63,20 @@ var Runner = function(params) {
delete data.toolsResults.phantomas.metrics.javascriptExecutionTree;
delete data.toolsResults.phantomas.offenders.javascriptExecutionTree;
delete data.toolsResults.phantomas.metrics.scrollExecutionTree;
delete data.toolsResults.phantomas.offenders.scrollExecutionTree;
return data;
}).then(function(data) {
if (data.toolsResults.phantomas.offenders.blockedRequests) {
data.blockedRequests = data.toolsResults.phantomas.offenders.blockedRequests;
}
// Finished!
deferred.resolve(data);
})
}).fail(function(err) {
.fail(function(err) {
debug('Run failed');
debug(err);
+36 -1
View File
@@ -11,7 +11,7 @@ var screenshotHandler = function() {
this.getScreenshotTempFile = function() {
var screenshotTmpFolder = new tmp.Dir();
var tmpFilePath = path.join(screenshotTmpFolder.path, 'screenshot.jpg');
var tmpFilePath = path.join(screenshotTmpFolder.path, 'screenshot.png');
var that = this;
return {
@@ -48,6 +48,8 @@ var screenshotHandler = function() {
})
.then(this.addWhiteBackground)
.then(this.toBuffer);
};
@@ -92,6 +94,39 @@ var screenshotHandler = function() {
return deferred.promise;
};
// If the page doesn't set a bg color, the default PhantomJS one is transparent
// When transforming PNG to JPG, transparent pixels become black.
// This is why we need to add a transparent background.
this.addWhiteBackground = function(image) {
var deferred = Q.defer();
// Create a canvas with the same dimensions as your image:
lwip.create(image.width(), image.height(), 'white', function(err, canvas){
if (err) {
debug('Could not create a white canvas');
debug(err);
deferred.reject(err);
} else {
// Paste original image on top of the canvas
canvas.paste(0, 0, image, function(err, image){
if (err) {
debug('Could not paste image on the white canvas');
debug(err);
deferred.reject(err);
} else {
// Now image has a white background...
debug('White background correctly added');
deferred.resolve(image);
}
});
}
});
return deferred.promise;
};
this.toBuffer = function(image) {
var deferred = Q.defer();
+44 -5
View File
@@ -6,6 +6,7 @@ var ScreenshotHandler = require('../../screenshotHandler');
var RunsQueue = require('../datastores/runsQueue');
var RunsDatastore = require('../datastores/runsDatastore');
var ResultsDatastore = require('../datastores/resultsDatastore');
var serverSettings = require('../../../server_config/settings.json');
var ApiController = function(app) {
@@ -32,7 +33,15 @@ var ApiController = function(app) {
url: req.body.url,
waitForResponse: req.body.waitForResponse !== false && req.body.waitForResponse !== 'false' && req.body.waitForResponse !== 0,
partialResult: req.body.partialResult || null,
screenshot: req.body.screenshot || false
screenshot: req.body.screenshot || false,
device: req.body.device || 'desktop',
waitForSelector: req.body.waitForSelector || null,
cookie: req.body.cookie || null,
authUser: req.body.authUser || null,
authPass: req.body.authPass || null,
blockDomain: req.body.blockDomain || null,
allowDomain: req.body.allowDomain || null,
noExternals: req.body.noExternals || false
}
};
@@ -46,7 +55,6 @@ var ApiController = function(app) {
debug('Adding test %s to the queue', run.runId);
var queuePromise = queue.push(run.runId);
// Save the run to the datastore
runsDatastore.add(run, queuePromise.startingPosition);
@@ -64,7 +72,16 @@ var ApiController = function(app) {
debug('Launching test %s on %s', run.runId, run.params.url);
var runOptions = {
screenshot: run.params.screenshot ? screenshot.getTmpFilePath() : false
screenshot: run.params.screenshot ? screenshot.getTmpFilePath() : false,
device: run.params.device,
waitForSelector: run.params.waitForSelector,
cookie: run.params.cookie,
authUser: run.params.authUser,
authPass: run.params.authPass,
blockDomain: run.params.blockDomain,
allowDomain: run.params.allowDomain,
noExternals: run.params.noExternals,
phantomasEngine: serverSettings.phantomasEngine
};
return ylt(run.params.url, runOptions);
@@ -83,7 +100,7 @@ var ApiController = function(app) {
if (run.params.screenshot) {
// Replace the empty promise created earlier with Q.resolve()
screenshotPromise = screenshot.toThumbnail(400)
screenshotPromise = screenshot.toThumbnail(serverSettings.screenshotWidth || 400)
// Read screenshot
.then(function(screenshotBuffer) {
@@ -97,8 +114,14 @@ var ApiController = function(app) {
}
})
// 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...');
});
}
@@ -107,7 +130,11 @@ var ApiController = function(app) {
// Save results
.then(function() {
// Remove uneeded temp screenshot path
delete data.params.options.screenshot;
// Here we can remove tools results if not needed
return resultsDatastore.saveResult(data);
})
@@ -221,6 +248,18 @@ var ApiController = function(app) {
// Retrive one result by id
app.get('/api/results/:id', function(req, res) {
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;
});
});
+1 -1
View File
@@ -20,7 +20,7 @@ var FrontController = function(app) {
app.use('/img', express.static(path.join(__dirname, assetsPath, 'img'), { maxAge: cacheDuration }));
app.use('/js', express.static(path.join(__dirname, assetsPath, 'js'), { maxAge: cacheDuration }));
app.use('/views', express.static(path.join(__dirname, assetsPath, 'views'), { maxAge: cacheDuration }));
app.use('/bower_components', express.static(path.join(__dirname, '../../../bower_components'), { maxAge: cacheDuration }));
app.use('/node_modules', express.static(path.join(__dirname, '../../../node_modules'), { maxAge: cacheDuration }));
};
module.exports = FrontController;
+6
View File
@@ -58,6 +58,9 @@ function RunsDatastore() {
var errorMessage;
switch(err) {
case '1':
errorMessage = "Error 1: unknown error";
break;
case '252':
errorMessage = "Error 252: page timeout in Phantomas";
break;
@@ -76,6 +79,9 @@ function RunsDatastore() {
case '1002':
errorMessage = "Error 1002: missing Phantomas metrics";
break;
case '1003':
errorMessage = "Error 1003: Phantomas not returning";
break;
default:
errorMessage = err;
}
@@ -0,0 +1,12 @@
var wwwRedirectMiddleware = function(req, res, next) {
'use strict';
// Redirect www.yellowlab.tools to yellowlab.tools without "www" (for SEO purpose)
if(/^www\.yellowlab\.tools/.test(req.headers.host)) {
res.redirect(301, req.protocol + '://' + req.headers.host.replace(/^www\./, '') + req.url);
} else {
next();
}
};
module.exports = wwwRedirectMiddleware;
+92
View File
@@ -0,0 +1,92 @@
var debug = require('debug')('ylt:colorDiff');
var diff = require('color-diff');
var parse = require('parse-color');
var colorDiff = function() {
var COLOR_DIFF_THRESHOLD = 0.9;
this.compareAllColors = function(data) {
debug('Starting to compare all colors...');
var offenders = data.toolsResults.phantomas.offenders.cssColors;
var colors = (offenders) ? this.parseAllColors(offenders) : [];
var result = [];
// Compare colors to each other
for (var i = 0; i < colors.length - 1 ; i++) {
for (var j = i + 1; j < colors.length; j++) {
debug('Comparing color %s with color %s', colors[i].original, colors[j].original);
if (this.compareTwoColors(colors[i], colors[j])) {
debug('Colors are similar!');
result.push({
color1: colors[i].original,
color2: colors[j].original,
isDark: (colors[i].Lab.L < 60)
});
}
}
}
data.toolsResults.colorDiff = {
metrics: {
similarColors: result.length
},
offenders: {
similarColors: result
}
};
return data;
};
this.parseAllColors = function(offenders) {
var parsedOffenders = offenders.map(this.parseOffender);
var deduplicatedColors = {};
parsedOffenders.forEach(function(color) {
if (color !== null) {
deduplicatedColors[color] = color;
}
});
return Object.keys(deduplicatedColors).map(this.parseColor);
};
this.parseOffender = function(offender) {
var regexResult = /^(.*) \(\d+ times\)$/.exec(offender);
return regexResult ? regexResult[1] : null;
};
this.parseColor = function(color) {
var colorArray = parse(color).rgba;
var obj = {
R: colorArray[0],
G: colorArray[1],
B: colorArray[2],
A: colorArray[3]
};
obj.Lab = diff.rgb_to_lab(obj);
obj.original = color;
return obj;
};
this.compareTwoColors = function(color1, color2) {
if (color1.A !== color2.A) {
debug('Comparison not possible, because the alpha channel is different');
return false;
}
var delta = diff.diff(color1.Lab, color2.Lab);
debug('Delta is %d', delta);
return delta <= COLOR_DIFF_THRESHOLD;
};
};
module.exports = new colorDiff();
+105
View File
@@ -0,0 +1,105 @@
var debug = require('debug')('ylt:isHttp2');
var url = require('url');
var Q = require('q');
var http2 = require('is-http2');
var isHttp2 = function() {
'use strict';
this.check = function(data) {
debug('Starting to check for HTTP2 support...');
return this.checkHttp2(data)
.then(function(result) {
if (result.isHttp) {
debug('The website is not even in HTTPS');
data.toolsResults.http2 = {
metrics: {
http2: false
}
};
} else if (result.isHttp2) {
debug('HTTP/2 (or SPDY) is supported');
data.toolsResults.http2 = {
metrics: {
http2: true
}
};
} else {
debug('HTTP/2 is not supported');
data.toolsResults.http2 = {
metrics: {
http2: false
}
};
}
// Add the supported protocols as offenders
if (result.supportedProtocols) {
debug('Supported protocols: ' + result.supportedProtocols.join(' '));
data.toolsResults.http2.offenders = {
http2: result.supportedProtocols
};
}
debug('End of HTTP2 support check');
return data;
})
.fail(function() {
return data;
});
};
this.getParsedUrl = function(data) {
return url.parse(data.toolsResults.phantomas.url);
};
this.getProtocol = function(data) {
return this.getParsedUrl(data).protocol;
};
this.getDomain = function(data) {
return this.getParsedUrl(data).hostname;
};
this.checkHttp2 = function(data) {
var deferred = Q.defer();
// Check if it's HTTPS first
if (this.getProtocol(data) === 'http:') {
deferred.resolve({
isHttp: true
});
} else {
// To make is-http2 work, you need to have openssl in a version greater than 1.0.0 installed and available in your $path.
http2(this.getDomain(data), {includeSpdy: true})
.then(function(result) {
deferred.resolve(result);
})
.catch(function(error) {
debug('Error while checking HTTP2 support:');
debug(error);
deferred.reject('Error while checking for HTTP2 support');
});
}
return deferred.promise;
};
};
module.exports = new isHttp2();
+190 -24
View File
@@ -1,24 +1,68 @@
var debug = require('debug')('ylt:jsExecutionTransformer');
var offendersHelpers = require('../offendersHelpers');
var Collection = require('./phantomas/custom_modules/util/collection');
var jsExecutionTransformer = function() {
this.transform = function(data) {
var javascriptExecutionTree = {};
var jQueryFunctionsCollection = new Collection();
var metrics = {
domInteractive: 0,
domContentLoaded: 0,
domContentLoadedEnd: 0,
domComplete: 0,
debug('Starting JS execution transformation');
DOMaccesses: 0,
DOMaccessesOnScroll: 0,
queriesWithoutResults: 0
};
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 = [];
}
try {
debug('Starting JS execution transformation');
javascriptExecutionTree = JSON.parse(data.toolsResults.phantomas.offenders.javascriptExecutionTree[0]);
if (javascriptExecutionTree.children) {
javascriptExecutionTree.children.forEach(function(node) {
javascriptExecutionTree.children.forEach(function(node, index) {
// 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;
node.warning = true;
node.eventNotDelegated = true;
}
if (node.data.resultsNumber === 0) {
metrics.queriesWithoutResults ++;
node.queryWithoutResults = true;
node.warning = true;
}
if (contextLength === 0) {
metrics.jQueryCallsOnEmptyObject ++;
node.jQueryCallOnEmptyObject = true;
node.warning = true;
}
if (node.data.type.indexOf('jQuery - ') === 0) {
metrics.jQueryCalls ++;
jQueryFunctionsCollection.push(node.data.type);
}
// Mark errors with an error flag
@@ -29,34 +73,156 @@ var jsExecutionTransformer = function() {
// Mark a performance flag
if (['domInteractive', 'domContentLoaded', 'domContentLoadedEnd', 'domComplete'].indexOf(node.data.type) >= 0) {
node.windowPerformance = true;
// Adjust the navigation timings (cause their not very well synchronised)
switch(node.data.type) {
case 'domInteractive':
javascriptExecutionTree.data.domInteractive = node.data.timestamp;
break;
case 'domContentLoaded':
javascriptExecutionTree.data.domContentLoaded = node.data.timestamp;
break;
case 'domContentLoadedEnd':
javascriptExecutionTree.data.domContentLoadedEnd = node.data.timestamp;
break;
case 'domComplete':
javascriptExecutionTree.data.domComplete = node.data.timestamp;
break;
}
}
// Fix rare bug when domComplete was never triggered
if (index === javascriptExecutionTree.children.length - 1 && !javascriptExecutionTree.data.domComplete) {
javascriptExecutionTree.data.domComplete = node.data.timestamp + 1000;
}
// Read the execution tree and adjust the navigation timings (cause their not very well synchronised)
switch(node.data.type) {
case 'domInteractive':
data.toolsResults.phantomas.metrics.domInteractive = node.data.timestamp;
break;
case 'domContentLoaded':
data.toolsResults.phantomas.metrics.domContentLoaded = node.data.timestamp;
break;
case 'domContentLoadedEnd':
data.toolsResults.phantomas.metrics.domContentLoadedEnd = node.data.timestamp;
break;
case 'domComplete':
data.toolsResults.phantomas.metrics.domComplete = node.data.timestamp;
break;
}
// Transform domPaths into objects
changeListOfDomPaths(node);
// Count the number of DOM accesses, by counting the tree leafs
metrics.DOMaccesses += countTreeLeafs(node);
});
}
// 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');
if (data.toolsResults.phantomas.offenders.scrollExecutionTree) {
debug('Starting scroll execution transformation');
offenders.DOMaccessesOnScroll = JSON.parse(data.toolsResults.phantomas.offenders.scrollExecutionTree[0]);
if (offenders.DOMaccessesOnScroll.children) {
offenders.DOMaccessesOnScroll.children.forEach(function(node) {
// Mark a event flag
if (['documentScroll', 'windowScroll', 'window.onscroll'].indexOf(node.data.type) >= 0) {
node.windowPerformance = true;
}
// Transform domPaths into objects
changeListOfDomPaths(node);
// Count the number of DOM accesses, by counting the tree leafs
metrics.DOMaccessesOnScroll += countTreeLeafs(node);
});
}
debug('Scroll execution transformation complete');
} else {
debug('Could not parse scrollExecutionTree');
}
} catch(err) {
throw err;
}
return javascriptExecutionTree;
data.javascriptExecutionTree = javascriptExecutionTree;
data.toolsResults.jsExecutionTransformer = {
metrics: metrics,
offenders: offenders
};
return data;
};
function treeRecursiveParser(node, fn) {
if (node.children) {
node.children.forEach(function(child) {
treeRecursiveParser(child, fn);
});
}
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();
+168
View File
@@ -0,0 +1,168 @@
var debug = require('debug')('ylt:mediaQueriesChecker');
var parseMediaQuery = require('css-mq-parser');
var offendersHelpers = require('../offendersHelpers');
var mediaQueriesChecker = function() {
'use strict';
var MOBILE_MIN_BREAKPOINT = 200;
var MOBILE_MAX_BREAKPOINT = 300;
this.analyzeMediaQueries = function(data) {
debug('Starting to check all media queries...');
var offenders = data.toolsResults.phantomas.offenders.cssMediaQueries;
var mediaQueries = (offenders) ? this.parseAllMediaQueries(offenders) : [];
var notMobileFirstCount = 0;
var notMobileFirstOffenders = [];
var breakpointsOffenders = {};
for (var i = 0; i < mediaQueries.length; i++) {
var item = mediaQueries[i];
if (!item) {
continue;
}
if (item.isForMobile) {
notMobileFirstCount += item.mediaQuery.rules;
notMobileFirstOffenders.push(item.mediaQuery);
}
for (var j = 0; j < item.breakpoints.length; j++) {
var breakpointString = item.breakpoints[j].string;
if (!breakpointsOffenders[breakpointString]) {
breakpointsOffenders[breakpointString] = {
count: 1,
pixels: item.breakpoints[j].pixels
};
} else {
breakpointsOffenders[breakpointString].count += 1;
}
}
}
data.toolsResults.mediaQueriesChecker = {
metrics: {
cssMobileFirst: notMobileFirstCount,
cssBreakpoints: Object.keys(breakpointsOffenders).length
},
offenders: {
cssMobileFirst: notMobileFirstOffenders,
cssBreakpoints: breakpointsOffenders
}
};
debug('End of media queries check');
return data;
};
this.parseAllMediaQueries = function(offenders) {
return offenders.map(this.parseOneMediaQuery);
};
this.parseOneMediaQuery = function(offender) {
var splittedOffender = offendersHelpers.cssOffenderPattern(offender);
var parts = /^@media (.*) \((\d+ rules)\)$/.exec(splittedOffender.css);
if (!parts) {
debug('Failed to parse media query ' + offender);
return false;
}
var rulesCount = parseInt(parts[2], 10);
var query = parts[1];
var isForMobile = false;
var breakpoints = [];
try {
var ast = parseMediaQuery(query);
var min = 0;
var max = Infinity;
var pixels;
ast.forEach(function(astItem) {
astItem.expressions.forEach(function(expression) {
if (expression.feature === 'width' || expression.feature === 'device-width') {
if (astItem.inverse === false) {
if (expression.modifier === 'max') {
pixels = toPixels(expression.value);
max = Math.min(max, pixels);
breakpoints.push({
string: expression.value,
pixels: pixels
});
} else if (expression.modifier === 'min') {
pixels = toPixels(expression.value);
min = Math.max(min, pixels);
breakpoints.push({
string: expression.value,
pixels: pixels
});
}
} else if (astItem.inverse === true) {
if (expression.modifier === 'max') {
pixels = toPixels(expression.value);
min = Math.max(min, pixels);
breakpoints.push({
string: expression.value,
pixels: pixels
});
} else if (expression.modifier === 'min') {
pixels = toPixels(expression.value);
max = Math.min(max, pixels);
breakpoints.push({
string: expression.value,
pixels: pixels
});
}
}
}
});
});
isForMobile = (min <= MOBILE_MIN_BREAKPOINT && max >= MOBILE_MAX_BREAKPOINT && max !== Infinity);
} catch(error) {
debug('Failed to parse media query ' + offender);
}
return {
mediaQuery: {
query: query,
rules: rulesCount,
file: splittedOffender.file,
line: splittedOffender.line,
column: splittedOffender.column
},
isForMobile: isForMobile,
breakpoints: breakpoints
};
};
// Parses a size in em, pt (or px) and returns it in px
function toPixels(size) {
var splittedSize = /^([\d\.]+)(.*)/.exec(size);
var value = parseFloat(splittedSize[1]);
var unit = splittedSize[2];
if (unit === 'em') {
return value * 16;
}
if (unit === 'pt') {
return value / 12 * 16;
}
return value;
}
};
module.exports = new mediaQueriesChecker();
@@ -18,8 +18,8 @@ exports.module = function(phantomas) {
responseEndTime = Date.now();
});
phantomas.once('init', function() {
phantomas.evaluate(function(responseEndTime, deepAnalysis) {
phantomas.on('init', function() {
phantomas.evaluate(function(responseEndTime) {
(function(phantomas) {
// Overwritting phantomas spy function
@@ -36,7 +36,7 @@ exports.module = function(phantomas) {
phantomas.log('Overwritting phantomas spy function');
function spy(obj, fn, callbackBefore, callbackAfter) {
var origFn = obj[fn];
var origFn = obj && obj[fn];
if (typeof origFn !== 'function') {
return false;
@@ -104,49 +104,87 @@ exports.module = function(phantomas) {
var root = new ContextTreeNode(null, {type: 'main'});
var currentContext = root;
if (deepAnalysis) {
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 its context
function pushContext(data) {
if (depth === 0 || deepAnalysis) {
// Some data is not needed on subchildren
if (depth === 0) {
data.timestamp = Date.now() - responseEndTime;
data.loadingStep = phantomas.currentStep || '';
currentContext.addChild(data);
}
// Some data is not needed on subchildren
if (depth > 0) {
if (data.backtrace) {
delete data.backtrace;
}
if (data.resultsNumber) {
delete data.resultsNumber;
}
}
currentContext.addChild(data);
}
// Add a child to the current context and enter his context
// Add a child to the current context and enter its context
function enterContext(data) {
if (depth === 0 || deepAnalysis) {
// Some data is not needed on subchildren
if (depth === 0) {
data.timestamp = Date.now() - responseEndTime;
data.loadingStep = phantomas.currentStep || '';
currentContext = currentContext.addChild(data);
}
// Some data is not needed on subchildren
if (depth > 0) {
if (data.backtrace) {
delete data.backtrace;
}
if (data.resultsNumber) {
delete data.resultsNumber;
}
}
currentContext = currentContext.addChild(data);
depth ++;
}
// Save given data in the current context and jump change current context to its parent
function leaveContext(moreData) {
if (depth === 1 || deepAnalysis) {
// Some data is not needed on subchildren
if (depth === 1) {
currentContext.data.time = Date.now() - currentContext.data.timestamp - responseEndTime;
}
// Merge previous data with moreData (ovewrites if exists)
if (moreData) {
for (var key in moreData) {
currentContext.data[key] = moreData[key];
}
// Some data is not needed on subchildren
if (depth > 1) {
if (moreData && moreData.backtrace) {
delete moreData.backtrace;
}
var parent = currentContext.parent;
if (parent === null) {
console.error('Error: trying to close root context in ContextTree');
} else {
currentContext = parent;
if (moreData && moreData.resultsNumber) {
delete moreData.resultsNumber;
}
}
// Merge previous data with moreData (ovewrites if exists)
if (moreData) {
for (var key in moreData) {
currentContext.data[key] = moreData[key];
}
}
var parent = currentContext.parent;
if (parent === null) {
console.error('Error: trying to close root context in ContextTree');
} else {
currentContext = parent;
}
depth --;
}
@@ -160,7 +198,6 @@ exports.module = function(phantomas) {
if (root !== currentContext) {
return null;
}
var current = currentContext;
function recusiveRead(node) {
if (node.children.length === 0) {
@@ -176,6 +213,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,10 +240,11 @@ exports.module = function(phantomas) {
phantomas.leaveContext = leaveContext;
phantomas.getContextData = getContextData;
phantomas.readFullTree = readFullTree;
phantomas.resetTree = resetTree;
})();
})(window.__phantomas);
}, responseEndTime, phantomas.getParam('js-deep-analysis'));
}, responseEndTime);
});
};
@@ -1,79 +0,0 @@
/**
* Analyzes HTTP caching headers
*
* @see https://developers.google.com/speed/docs/best-practices/caching
*/
exports.version = '0.2.a';
exports.module = function(phantomas) {
'use strict';
var cacheControlRegExp = /max-age=(\d+)/;
function getCachingTime(url, headers) {
// false means "no caching"
var ttl = false,
headerName,
now = new Date(),
headerDate;
for (headerName in headers) {
var value = headers[headerName];
switch (headerName.toLowerCase()) {
// parse max-age=...
//
// max-age=2592000
// public, max-age=300, must-revalidate
case 'cache-control':
var matches = value.match(cacheControlRegExp);
if (matches) {
ttl = parseInt(matches[1], 10);
}
break;
// catch Expires and Pragma headers
case 'expires':
case 'pragma':
// and Varnish specific headers
case 'x-pass-expires':
case 'x-pass-cache-control':
phantomas.incrMetric('oldCachingHeaders'); // @desc number of responses with old, HTTP 1.0 caching headers (Expires and Pragma)
phantomas.addOffender('oldCachingHeaders', url + ' - ' + headerName + ': ' + value);
headerDate = Date.parse(value);
if (headerDate) ttl = Math.round((headerDate - now) / 1000);
break;
}
}
//console.log(JSON.stringify(headers)); console.log("TTL: " + ttl + ' s');
return ttl;
}
phantomas.setMetric('cachingNotSpecified'); // @desc number of responses with no caching header sent (no Cache-Control header)
phantomas.setMetric('cachingTooShort'); // @desc number of responses with too short (less than a week) caching time
phantomas.setMetric('cachingDisabled'); // @desc number of responses with caching disabled (max-age=0)
phantomas.setMetric('oldCachingHeaders');
phantomas.on('recv', function(entry, res) {
var ttl = getCachingTime(entry.url, entry.headers);
// static assets
if (entry.isImage || entry.isJS || entry.isCSS) {
if (ttl === false) {
phantomas.incrMetric('cachingNotSpecified');
phantomas.addOffender('cachingNotSpecified', entry.url);
} else if (ttl <= 0) {
phantomas.incrMetric('cachingDisabled');
phantomas.addOffender('cachingDisabled', entry.url);
} else if (ttl < 7 * 86400) {
phantomas.incrMetric('cachingTooShort');
phantomas.addOffender('cachingTooShort', entry.url + ' cached for ' + ttl + ' s');
}
}
});
};
@@ -0,0 +1,84 @@
/**
* Analyzes DOM hidden content
*/
/* global document: true, Node: true, window: true */
exports.version = '1.0.a';
exports.module = function(phantomas) {
'use strict';
// total length of HTML of hidden elements (i.e. display: none)
phantomas.setMetric('hiddenContentSize'); // @desc the size of content of hidden elements on the page (with CSS display: none) @offenders
phantomas.setMetric('hiddenImages'); // @desc number of hidden images that can be lazy-loaded @offenders
// HTML size
phantomas.on('report', function() {
phantomas.evaluate(function() {
(function(phantomas) {
var runner = new phantomas.nodeRunner(),
lazyLoadableImages = {};
phantomas.spyEnabled(false, 'analyzing hidden content');
runner.walk(document.body, function(node, depth) {
switch (node.nodeType) {
case Node.ELEMENT_NODE:
// @see https://developer.mozilla.org/en/DOM%3awindow.getComputedStyle
var styles = window.getComputedStyle(node);
if (styles && styles.getPropertyValue('display') === 'none') {
if (typeof node.innerHTML === 'string') {
var size = node.innerHTML.length;
phantomas.incrMetric('hiddenContentSize', size);
// log hidden containers bigger than 1 kB
if (size > 1024) {
phantomas.addOffender('hiddenContentSize', phantomas.getDOMPath(node) + ' (' + size + ' characters)');
}
}
// count hidden images that can be lazy loaded (issue #524)
var images = [];
if (node.tagName === 'IMG') {
images = [node];
} else if (typeof node.querySelectorAll === 'function') {
images = node.querySelectorAll('img') || [];
}
for (var i = 0, len = images.length; i < len; i++) {
var src = images[i].src,
path;
if (src === '' || src.indexOf('data:image') === 0) continue;
if (!lazyLoadableImages[src]) {
path = phantomas.getDOMPath(images[i]);
lazyLoadableImages[src] = {
path: path
};
}
}
// don't run for child nodes as they're hidden as well
return false;
}
break;
}
});
Object.keys(lazyLoadableImages).forEach(function(img) {
var entry = lazyLoadableImages[img];
phantomas.incrMetric('hiddenImages');
phantomas.addOffender('hiddenImages', img);
phantomas.log('hiddenImages: <%s> image (%s) is hidden and can be lazy-loaded', img, entry.path);
});
phantomas.spyEnabled(true);
}(window.__phantomas));
});
});
};
@@ -19,7 +19,7 @@ exports.module = function(phantomas) {
phantomas.setMetric('DOMqueriesAvoidable'); // @desc number of repeated uses of a duplicated query
// fake native DOM functions
phantomas.once('init', function() {
phantomas.on('init', function() {
phantomas.evaluate(function() {
(function(phantomas) {
function querySpy(type, query, fnName, context, hasNoResults) {
@@ -33,9 +33,6 @@ exports.module = function(phantomas) {
phantomas.enterContext({
type: 'getElementById',
callDetails: {
context: {
domElement: '#document'
},
arguments: ['#' + id]
},
backtrace: phantomas.getBacktrace()
@@ -65,7 +62,8 @@ exports.module = function(phantomas) {
type: 'getElementsByClassName',
callDetails: {
context: {
domElement: context
length: 1,
elements: [context]
},
arguments: ['.' + className]
},
@@ -103,7 +101,8 @@ exports.module = function(phantomas) {
type: 'getElementsByTagName',
callDetails: {
context: {
domElement: context
length: 1,
elements: [context]
},
arguments: [tagName]
},
@@ -145,7 +144,8 @@ exports.module = function(phantomas) {
type: 'querySelector',
callDetails: {
context: {
domElement: context
length: 1,
elements: [context]
},
arguments: [selector]
},
@@ -177,7 +177,8 @@ exports.module = function(phantomas) {
type: 'querySelectorAll',
callDetails: {
context: {
domElement: context
length: 1,
elements: [context]
},
arguments: [selector]
},
@@ -237,7 +238,8 @@ exports.module = function(phantomas) {
type: 'appendChild',
callDetails: {
context: {
domElement: context
length: 1,
elements: [context]
},
arguments: [appended]
},
@@ -245,20 +247,25 @@ exports.module = function(phantomas) {
});
}
function insertBeforeSpyBefore(child) {
function insertBeforeSpyBefore(child, refElement) {
/*jshint validthis: true */
var context = phantomas.getDOMPath(this);
var appended = phantomas.getDOMPath(child);
var referent = phantomas.getDOMPath(refElement);
appendChild(child, this, context, appended);
phantomas.enterContext({
type: 'insertBefore',
callDetails: {
context: {
domElement: context
length: 1,
elements: [context]
},
arguments: [appended]
arguments: [
appended,
referent
]
},
backtrace: phantomas.getBacktrace()
});
@@ -270,6 +277,68 @@ exports.module = function(phantomas) {
phantomas.spy(Node.prototype, 'insertBefore', insertBeforeSpyBefore, function(result) {
phantomas.leaveContext();
});
phantomas.spy(Document.prototype, 'createElement', function(tagName) {
phantomas.enterContext({
type: 'createElement',
callDetails: {
arguments: [tagName]
},
backtrace: phantomas.getBacktrace()
});
}, function(result, args) {
phantomas.leaveContext();
});
phantomas.spy(Document.prototype, 'createTextNode', function(text) {
phantomas.enterContext({
type: 'createTextNode',
callDetails: {
arguments: [text]
},
backtrace: phantomas.getBacktrace()
});
}, function(result, args) {
phantomas.leaveContext();
});
phantomas.spy(Document.prototype, 'createDocumentFragment', function() {
phantomas.enterContext({
type: 'createDocumentFragment',
callDetails: {
arguments: []
},
backtrace: phantomas.getBacktrace()
});
}, function(result, args) {
phantomas.leaveContext();
});
phantomas.spy(window, 'getComputedStyle', function(element, pseudoElement) {
var target = phantomas.getDOMPath(element);
phantomas.enterContext({
type: 'getComputedStyle',
callDetails: {
arguments: [target, pseudoElement]
},
backtrace: phantomas.getBacktrace()
});
}, function(result, args) {
phantomas.leaveContext();
});
})(window.__phantomas);
});
});
@@ -296,7 +365,12 @@ exports.module = function(phantomas) {
phantomas.log('DOM query: by %s - "%s" (using %s) in %s', type, query, fnName, context);
phantomas.incrMetric('DOMqueries');
if (context && context.indexOf('DocumentFragment') === -1) {
if (context && (
context.indexOf('html') === 0 ||
context.indexOf('body') === 0 ||
context.indexOf('head') === 0 ||
context.indexOf('#document') === 0
)) {
DOMqueries.push(type + ' "' + query + '" with ' + fnName + ' (in context ' + context + ')');
}
});
@@ -1,48 +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: {
domElement: 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.on('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));
});
});
};
@@ -17,7 +17,7 @@ exports.module = function(phantomas) {
phantomas.setMetric('jQueryOnDOMReadyFunctions'); // @desc number of functions bound to onDOMReady event
phantomas.setMetric('jQueryWindowOnLoadFunctions'); // @desc number of functions bound to windowOnLoad event
phantomas.setMetric('jQuerySizzleCalls'); // @desc number of calls to Sizzle (including those that will be resolved using querySelectorAll)
//phantomas.setMetric('jQueryEventTriggers'); // @desc number of jQuery event triggers
phantomas.setMetric('jQueryEventTriggers'); // @desc number of jQuery event triggers
var jQueryFunctions = [
// DOM manipulations
@@ -55,7 +55,20 @@ exports.module = function(phantomas) {
'scrollLeft',
'scrollTop',
// generic events
// Animations
'hide',
'show',
'toggle',
'animate',
'fadeIn',
'fadeOut',
'fadeTo',
'fadeToggle',
'slideDown',
'slideUp',
'slideToggle',
// Generic events
'on',
'off',
'live',
@@ -66,7 +79,7 @@ exports.module = function(phantomas) {
'bind',
'unbind',
// more events
// More events
'blur',
'change',
'click',
@@ -91,10 +104,9 @@ exports.module = function(phantomas) {
'scroll',
'select',
'submit',
'toggle',
'unload',
// attributes
// Attributes
'attr',
'prop',
'removeAttr',
@@ -103,14 +115,32 @@ exports.module = function(phantomas) {
'hasClass',
'addClass',
'removeClass',
'toggleClass'
'toggleClass',
];
var jQueryTraversalFunctions = [
'children',
'closest',
'find',
'next',
'nextAll',
'nextUntil',
'offsetParent',
'parent',
'parents',
'parentsUntil',
'prev',
'prevAll',
'prevUntil',
'siblings'
];
jQueryFunctions = jQueryFunctions.concat(jQueryTraversalFunctions);
// spy calls to jQuery functions
phantomas.once('init', function() {
phantomas.evaluate(function(jQueryFunctions) {
phantomas.on('init', function() {
phantomas.evaluate(function(jQueryFunctions, jQueryTraversalFunctions) {
(function(phantomas) {
var jQuery;
var oldJQuery;
phantomas.spyGlobalVar('jQuery', function(jQuery) {
@@ -166,11 +196,11 @@ exports.module = function(phantomas) {
phantomas.addOffender('jQuerySizzleCalls', '%s (in %s)', selector, (phantomas.getDOMPath(context) || 'unknown'));
phantomas.enterContext({
type: 'jQuery - find',
type: 'jQuery - Sizzle call',
callDetails: {
context: {
length: this.length,
firstElementPath: phantomas.getDOMPath(context)
length: 1,
elements: [phantomas.getDOMPath(context)]
},
arguments: [selector]
},
@@ -184,7 +214,37 @@ exports.module = function(phantomas) {
phantomas.leaveContext(moreData);
}) || phantomas.log('jQuery: can not measure jQuerySizzleCalls (jQuery used on the page is too old)!');
/*if (!jQuery.event) {
phantomas.spy(jQuery.fn, 'init', function(selector, context) {
if (typeof selector === 'string' && /^#([\w\-]*)$/.exec(selector) !== null && !context) {
phantomas.enterContext({
type: 'jQuery - find',
callDetails: {
arguments: [selector]
},
backtrace: phantomas.getBacktrace()
});
}
}, function(result) {
var data = phantomas.getContextData();
if (data.type === 'jQuery - find' &&
!data.callDetails.context &&
data.callDetails.arguments.length === 1 &&
/^#([\w\-]*)$/.exec(data.callDetails.arguments[0]) !== null) {
var moreData = {
resultsNumber : (result && result.length) ? result.length : 0
};
phantomas.leaveContext(moreData);
}
});
if (!jQuery.event) {
phantomas.spy(jQuery.event, 'trigger', function(ev, data, elem) {
var path = phantomas.getDOMPath(elem),
type = ev.type || ev;
@@ -194,27 +254,18 @@ exports.module = function(phantomas) {
phantomas.incrMetric('jQueryEventTriggers');
phantomas.addOffender('jQueryEventTriggers', '"%s" on "%s"', type, path);
});
}*/
}
// jQuery events bound to window' onLoad event (#451)
phantomas.spy(jQuery.fn, 'on', function(eventName, func) {
if ((eventName === 'load') && (this[0] === window)) {
phantomas.incrMetric('jQueryWindowOnLoadFunctions');
phantomas.addOffender('jQueryWindowOnLoadFunctions', phantomas.getCaller(2));
phantomas.pushContext({
type: 'jQuery - windowOnLoad',
callDetails: {
arguments: [func]
},
backtrace: phantomas.getBacktrace()
});
}
});
// Add spys on many jQuery functions
jQueryFunctions.forEach(function(functionName) {
var capitalizedName = functionName.substring(0,1).toUpperCase() + functionName.substring(1);
phantomas.spy(jQuery.fn, functionName, function(args) {
@@ -222,6 +273,10 @@ exports.module = function(phantomas) {
args = [].slice.call(arguments);
args.forEach(function(arg, index) {
if (arg instanceof Array) {
arg = '[Array]';
}
if (arg instanceof Object) {
if (arg instanceof jQuery || (arg.jquery && arg.jquery.length > 0)) {
@@ -239,6 +294,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]';
@@ -274,13 +336,17 @@ exports.module = function(phantomas) {
args[index] = arg;
});
var elements = [];
for (var i = 0 ; i < this.length ; i++) {
elements.push(phantomas.getDOMPath(this[i]));
}
phantomas.enterContext({
type: 'jQuery - ' + functionName,
callDetails: {
context: {
length: this.length,
firstElementPath: phantomas.getDOMPath(this[0])
elements: elements
},
arguments: args
},
@@ -288,23 +354,22 @@ exports.module = function(phantomas) {
});
}, function(result) {
phantomas.leaveContext();
}) || phantomas.log('jQuery: can not track jQuery - ' + capitalizedName + ' (this version of jQuery doesn\'t support it)');
if (jQueryTraversalFunctions.indexOf(functionName) >= 0) {
var moreData = {
resultsNumber : (result && result.length) ? result.length : 0
};
phantomas.leaveContext(moreData);
} else {
phantomas.leaveContext();
}
}) || phantomas.log('jQuery: can not track jQuery - ' + functionName + ' (this version of jQuery doesn\'t support it)');
});
});
})(window.__phantomas);
}, jQueryFunctions);
}, jQueryFunctions, jQueryTraversalFunctions);
});
// store the last resource that was received
// try to report where given jQuery version was loaded from
phantomas.on('recv', function(entry) {
if (entry.isJS) {
lastUrl = entry.url;
}
});
phantomas.on('jQueryLoaded', function(version) {
phantomas.log('jQuery: loaded v' + version);
phantomas.setMetric('jQueryVersion', version);
@@ -312,7 +377,5 @@ exports.module = function(phantomas) {
// report multiple jQuery "instances" (issue #435)
phantomas.incrMetric('jQueryVersionsLoaded');
phantomas.addOffender('jQueryVersionsLoaded', 'v%s', version);
phantomas.log('jQuery: v%s (probably loaded from <%s>)', version, lastUrl);
});
};
@@ -4,44 +4,56 @@
* @see http://www.nczonline.net/blog/2013/06/25/eval-isnt-evil-just-misunderstood/
* @see http://www.quirksmode.org/blog/archives/2005/06/three_javascrip_1.html
* @see http://www.stevesouders.com/blog/2012/04/10/dont-docwrite-scripts/
*
* Run phantomas with --spy-eval to count eval() calls (see issue #467)
*/
/* global document: true, window: true */
exports.version = '0.1.a';
exports.version = '0.2';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('documentWriteCalls'); //@desc number of calls to either document.write or document.writeln @offenders
phantomas.setMetric('evalCalls'); // @desc number of calls to eval (either direct or via setTimeout / setInterval) @offenders
phantomas.setMetric('documentWriteCalls'); //@desc number of calls to either document.write or document.writeln
phantomas.setMetric('evalCalls'); // @desc number of calls to eval (either direct or via setTimeout / setInterval)
// spy calls to eval only when requested (issue #467)
var spyEval = phantomas.getParam('spy-eval') === true;
if (!spyEval) {
phantomas.log('javaScriptBottlenecks: to spy calls to eval() run phantomas with --spy-eval option');
}
phantomas.once('init', function() {
phantomas.evaluate(function() {
phantomas.on('init', function() {
phantomas.evaluate(function(spyEval) {
(function(phantomas) {
function report(msg, caller, backtrace, metric) {
phantomas.log(msg + ': from ' + caller + '!');
phantomas.log('Backtrace: ' + backtrace);
phantomas.incrMetric(metric);
phantomas.addOffender(metric, "%s from %s", msg, caller);
}
// spy calls to eval()
/*phantomas.spy(window, 'eval', function(code) {
report('eval() called directly', phantomas.getCaller(), phantomas.getBacktrace(), 'evalCalls');
phantomas.log('eval\'ed code: ' + (code || '').substring(0, 150) + '(...)');
});*/
if (spyEval) {
phantomas.spy(window, 'eval', function(code) {
report('eval() called directly', phantomas.getCaller(), phantomas.getBacktrace(), 'evalCalls');
phantomas.log('eval\'ed code: ' + (code || '').substring(0, 150) + '(...)');
});
}
// spy calls to setTimeout / setInterval with string passed instead of a function
/*phantomas.spy(window, 'setTimeout', function(fn, interval) {
phantomas.spy(window, 'setTimeout', function(fn, interval) {
if (typeof fn !== 'string') return;
report('eval() called via setTimeout("' + fn + '")', phantomas.getCaller(), phantomas.getBacktrace(), 'evalCalls');
});*/
});
/*phantomas.spy(window, 'setInterval', function(fn, interval) {
phantomas.spy(window, 'setInterval', function(fn, interval) {
if (typeof fn !== 'string') return;
report('eval() called via setInterval("' + fn + '")', phantomas.getCaller(), phantomas.getBacktrace(), 'evalCalls');
});*/
});
// spy document.write(ln)
phantomas.spy(document, 'write', function(arg) {
@@ -52,6 +64,6 @@ exports.module = function(phantomas) {
report('document.writeln() used', phantomas.getCaller(), phantomas.getBacktrace(), 'documentWriteCalls');
});
})(window.__phantomas);
});
}, spyEval);
});
};
@@ -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();
@@ -0,0 +1,79 @@
/**
* Analyzes images and detects which one can be lazy-loaded (are below the fold)
*
* @see https://github.com/macbre/phantomas/issues/494
*/
/* global document: true, window: true */
exports.version = '1.0.a';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('lazyLoadableImagesBelowTheFold'); // @desc number of images displayed below the fold that can be lazy-loaded
phantomas.on('report', function() {
phantomas.log('lazyLoadableImages: analyzing which images can be lazy-loaded...');
phantomas.evaluate(function() {
(function(phantomas) {
phantomas.spyEnabled(false, 'analyzing which images can be lazy-loaded');
var images = document.body.getElementsByTagName('img'),
i,
len = images.length,
offset,
path,
processedImages = {},
src,
viewportHeight = window.innerHeight;
phantomas.log('lazyLoadableImages: %d image(s) found, assuming %dpx offset to be the fold', len, viewportHeight);
for (i = 0; i < len; i++) {
// @see https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect
offset = images[i].getBoundingClientRect().top;
src = images[i].src;
// ignore base64-encoded images
if (src === '' || /^data:/.test(src)) {
continue;
}
path = phantomas.getDOMPath(images[i]);
// get the most top position for a given image (deduplicate by src)
if (typeof processedImages[src] === 'undefined') {
processedImages[src] = {
offset: offset,
path: path
};
}
// maybe there's the same image loaded above the fold?
if (offset < processedImages[src].offset) {
processedImages[src] = {
offset: offset,
path: path
};
}
}
phantomas.log('lazyLoadableImages: checking %d unique image(s)', Object.keys(processedImages).length);
Object.keys(processedImages).forEach(function(src) {
var img = processedImages[src];
if (img.offset > viewportHeight) {
phantomas.log('lazyLoadableImages: <%s> image (%s) is below the fold (at %dpx)', src, img.path, img.offset);
phantomas.incrMetric('lazyLoadableImagesBelowTheFold');
phantomas.addOffender('lazyLoadableImagesBelowTheFold', src);
}
});
phantomas.spyEnabled(true);
})(window.__phantomas);
});
});
};
@@ -0,0 +1,23 @@
/**
* Retries download on every request to get the real file size
*
*/
exports.version = '0.1';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('requestsList');
var requests = [];
phantomas.on('recv', function(entry, res) {
requests.push(entry);
});
phantomas.on('report', function() {
phantomas.setMetric('requestsList', true, true);
phantomas.addOffender('requestsList', JSON.stringify(requests));
});
};
@@ -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);
});
});
};
+69 -33
View File
@@ -11,42 +11,44 @@ var PhantomasWrapper = function() {
/**
* This is the phantomas launcher. It merges user chosen options into the default options
* Available options :
*
* - timeout : in seconds (default 60)
* - jsDeepAnalysis : should we inspect subrequests in the javascript execution tree?
*
*/
this.execute = function(data) {
var deferred = Q.defer();
var task = data.params;
var options = {
// Cusomizable options
'timeout': task.options.timeout || 60,
'js-deep-analysis': task.options.jsDeepAnalysis || false,
'user-agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.110 Safari/537.36',
'engine': task.options.phantomasEngine || 'webkit',
'timeout': task.options.timeout || 30,
'user-agent': (task.options.device === 'desktop') ? 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) YLT Chrome/27.0.1453.110 Safari/537.36' : null,
'tablet': (task.options.device === 'tablet'),
'phone': (task.options.device === 'phone'),
'screenshot': task.options.screenshot || false,
'viewport': '1366x768',
'wait-for-selector': task.options.waitForSelector,
'cookie': task.options.cookie,
'auth-user': task.options.authUser,
'auth-pass': task.options.authPass,
'block-domain': task.options.blockDomain,
'allow-domain': task.options.allowDomain,
'no-externals': task.options.noExternals,
// Mandatory
'reporter': 'json:pretty',
'analyze-css': true,
'skip-modules': [
'blockDomains', // not needed
'caching', // overriden
'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
'lazyLoadableImages', //overriden
'pageSource', // not needed
'waitForSelector', // not needed
'windowPerformance' // overriden
].join(','),
'include-dirs': [
@@ -65,61 +67,95 @@ var PhantomasWrapper = function() {
value = '"' + value + '"';
}
optionsString += ' ' + '--' + opt + '=' + value;
if (value === true) {
optionsString += ' ' + '--' + opt;
} else if (value === false || value === null || value === undefined) {
// Nothing
} else {
optionsString += ' ' + '--' + opt + '=' + value;
}
}
debug('node node_modules/phantomas/bin/phantomas.js --url=' + task.url + optionsString + ' --verbose');
// Kill the application if nothing happens
var phantomasPid;
var isKilled = false;
// Kill phantomas if nothing happens
var killer = setTimeout(function() {
debug('Killing the app because the test on %s was launched %d seconds ago', task.url, 5*options.timeout);
// If in server mode, forever will restart the server
console.log('Killing phantomas because the test on ' + task.url + ' was launched ' + 5*options.timeout + ' seconds ago');
// Kill the Phantomas process first
if (phantomasPid) {
ps.kill(phantomasPid, function(err) {
if (err) {
debug('Could not kill Phantomas process %s', phantomasPid);
}
else {
debug('Phantomas process %s was correctly killed', phantomasPid);
// Suicide
process.exit(1);
// If in server mode, forever will restart the server
}
// Then suicide.
process.exit(1);
debug('Phantomas process %s was correctly killed', phantomasPid);
// Then mark the test as failed
// Error 1003 = Phantomas not answering
deferred.reject(1003);
isKilled = true;
});
} else {
// Suicide
process.exit(1);
}
}, 5*options.timeout*1000);
// It's time to launch the test!!!
var triesNumber = 2;
var currentTry = 0;
async.retry(triesNumber, function(cb) {
currentTry ++;
var process = phantomas(task.url, options, function(err, json, results) {
debug('Returning from Phantomas');
var errorCode = err ? parseInt(err.message, 10) : null;
if (isKilled) {
debug('Process was killed, too late Phantomas, sorry...');
return;
}
debug('Returning from Phantomas with error %s', errorCode);
// Adding some YellowLabTools errors here
if (json && json.metrics && (!json.metrics.javascriptExecutionTree || !json.offenders.javascriptExecutionTree)) {
err = 1001;
errorCode = 1001;
}
if (!err && (!json || !json.metrics)) {
err = 1002;
if (!errorCode && (!json || !json.metrics)) {
errorCode = 1002;
}
// Don't cancel test if it is a timeout and we've got some results
if (err === 252 && json) {
if (errorCode === 252 && json) {
debug('Timeout after ' + options.timeout + ' seconds. But it\'s not a problem, the test is valid.');
err = null;
errorCode = null;
}
if (err) {
debug('Attempt failed. Error code ' + err);
if (errorCode) {
debug('Attempt failed. Error code ' + errorCode);
}
cb(err, json);
cb(errorCode, json);
}).fail(function() {
// This function is useless, but the failing promise needs to be handled,
// otherwise the module meow writes in the console in case of a timeout (error code 252).
debug('Failing promise handled');
});
phantomasPid = process.pid;
+343
View File
@@ -0,0 +1,343 @@
var debug = require('debug')('ylt:fileMinifier');
var Q = require('q');
var UglifyJS = require('uglify-js');
var CleanCSS = require('clean-css');
var Minimize = require('minimize');
var FileMinifier = function() {
function minifyFile(entry) {
var deferred = Q.defer();
if (!entry.weightCheck || !entry.weightCheck.body) {
// No valid file available
deferred.resolve(entry);
return deferred.promise;
}
var fileSize = entry.weightCheck.uncompressedSize;
debug('Let\'s try to optimize %s', entry.url);
debug('Current file size is %d', fileSize);
var startTime = Date.now();
if (entry.isJS && !isKnownAsMinified(entry.url) && !looksAlreadyMinified(entry.weightCheck.body)) {
debug('File is a JS');
return minifyJs(entry.weightCheck.body)
.then(function(newFile) {
if (!newFile) {
debug('Optimization didn\'t work');
return entry;
}
var endTime = Date.now();
var newFileSize = newFile.length;
debug('JS minification complete for %s', entry.url);
if (gainIsEnough(fileSize, newFileSize)) {
entry.weightCheck.bodyAfterOptimization = newFile;
entry.weightCheck.optimized = newFileSize;
entry.weightCheck.isOptimized = false;
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
}
return entry;
})
.fail(function(err) {
return entry;
});
} else if (entry.isCSS) {
debug('File is a CSS');
return minifyCss(entry.weightCheck.body)
.then(function(newFile) {
if (!newFile) {
debug('Optimization didn\'t work');
return entry;
}
var endTime = Date.now();
debug('CSS minification took %dms', endTime - startTime);
var newFileSize = newFile.length;
debug('CSS minification complete for %s', entry.url);
if (gainIsEnough(fileSize, newFileSize)) {
entry.weightCheck.bodyAfterOptimization = newFile;
entry.weightCheck.optimized = newFileSize;
entry.weightCheck.isOptimized = false;
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
}
return entry;
})
.fail(function(err) {
return entry;
});
} else if (entry.isHTML) {
debug('File is an HTML');
return minifyHtml(entry.weightCheck.body)
.then(function(newFile) {
if (!newFile) {
debug('Optimization didn\'t work');
return entry;
}
var endTime = Date.now();
debug('HTML minification took %dms', endTime - startTime);
var newFileSize = newFile.length;
debug('HTML minification complete for %s', entry.url);
if (gainIsEnough(fileSize, newFileSize)) {
entry.weightCheck.bodyAfterOptimization = newFile;
entry.weightCheck.optimized = newFileSize;
entry.weightCheck.isOptimized = false;
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
}
return entry;
})
.fail(function(err) {
return entry;
});
} else {
debug('Not minifiable type or already minified');
deferred.resolve(entry);
}
return deferred.promise;
}
// The gain is estimated of enough value if it's over 2KB or over 20%,
// but it's ignored if is below 400 bytes
function gainIsEnough(oldWeight, newWeight) {
var gain = oldWeight - newWeight;
var ratio = gain / oldWeight;
return (gain > 2096 || (ratio > 0.2 && gain > 400));
}
// Uglify
function minifyJs(body) {
// Splitting the Uglify function because it sometime takes too long (more than 10 seconds)
// I hope that, by splitting, it can be a little more asynchronous, so the application doesn't freeze.
return splittedUglifyStep1(body)
.delay(1)
.then(splittedUglifyStep2)
.delay(1)
.then(splittedUglifyStep3)
.delay(1)
.then(splittedUglifyStep4)
.delay(1)
.then(splittedUglifyStep5)
.delay(1)
.then(splittedUglifyStep6)
.delay(1)
.then(splittedUglifyStep7);
}
function splittedUglifyStep1(code) {
var deferred = Q.defer();
var startTime = Date.now();
try {
var toplevel_ast = UglifyJS.parse(code);
var endTime = Date.now();
debug('Uglify step 1 took %dms', endTime - startTime);
deferred.resolve(toplevel_ast);
} catch(err) {
debug('JS syntax error, Uglify\'s parser failed (step 1)');
deferred.reject(err);
}
return deferred.promise;
}
function splittedUglifyStep2(toplevel) {
var deferred = Q.defer();
var startTime = Date.now();
toplevel.figure_out_scope();
var endTime = Date.now();
debug('Uglify step 2 took %dms', endTime - startTime);
deferred.resolve(toplevel);
return deferred.promise;
}
function splittedUglifyStep3(toplevel) {
var deferred = Q.defer();
var startTime = Date.now();
var compressor = UglifyJS.Compressor({warnings: false});
var compressed_ast = toplevel.transform(compressor);
var endTime = Date.now();
debug('Uglify step 3 took %dms', endTime - startTime);
deferred.resolve(compressed_ast);
return deferred.promise;
}
function splittedUglifyStep4(compressed_ast) {
var deferred = Q.defer();
var startTime = Date.now();
compressed_ast.figure_out_scope();
var endTime = Date.now();
debug('Uglify step 4 took %dms', endTime - startTime);
deferred.resolve(compressed_ast);
return deferred.promise;
}
function splittedUglifyStep5(compressed_ast) {
var deferred = Q.defer();
var startTime = Date.now();
compressed_ast.compute_char_frequency();
var endTime = Date.now();
debug('Uglify step 5 took %dms', endTime - startTime);
deferred.resolve(compressed_ast);
return deferred.promise;
}
function splittedUglifyStep6(compressed_ast) {
var deferred = Q.defer();
var startTime = Date.now();
compressed_ast.mangle_names();
var endTime = Date.now();
debug('Uglify step 6 took %dms', endTime - startTime);
deferred.resolve(compressed_ast);
return deferred.promise;
}
function splittedUglifyStep7(compressed_ast) {
var deferred = Q.defer();
var startTime = Date.now();
var code = compressed_ast.print_to_string();
var endTime = Date.now();
debug('Uglify step 7 took %dms', endTime - startTime);
deferred.resolve(code);
return deferred.promise;
}
// Clear-css
function minifyCss(body) {
var deferred = Q.defer();
try {
var result = new CleanCSS({compatibility: 'ie8'}).minify(body);
deferred.resolve(result.styles);
} catch(err) {
deferred.reject(err);
}
return deferred.promise;
}
// HTMLMinifier
function minifyHtml(body) {
var deferred = Q.defer();
var minimize = new Minimize({
empty: true, // KEEP empty attributes
conditionals: true, // KEEP conditional internet explorer comments
spare: true // KEEP redundant attributes
});
minimize.parse(body, function (error, data) {
if (error) {
deferred.reject(error);
} else {
deferred.resolve(data);
}
});
return deferred.promise;
}
// Avoid loosing time trying to compress some JS libraries known as already compressed
function isKnownAsMinified(url) {
var result = false;
// Twitter
result = result || /^https?:\/\/platform\.twitter\.com\/widgets\.js/.test(url);
// Facebook
result = result || /^https:\/\/connect\.facebook\.net\/[^\/]*\/(sdk|all)\.js/.test(url);
// Google +1
result = result || /^https:\/\/apis\.google\.com\/js\/plusone\.js/.test(url);
// jQuery CDN
result = result || /^https?:\/\/code\.jquery\.com\/.*\.min.js/.test(url);
// Google Analytics
result = result || /^https?:\/\/(www|ssl)\.google-analytics\.com\/(.*)\.js/.test(url);
if (result === true) {
debug('This file is known as already minified. Skipping minification: %s', url);
}
return result;
}
// Avoid loosing tome trying to compress JS files if they alreay look minified
// by counting the number of lines compared to the total size.
// Less than 1000kb per line is suspicious
function looksAlreadyMinified(code) {
var linesCount = code.split(/\r\n|\r|\n/).length;
var linesRatio = code.length / linesCount;
var looksMinified = linesRatio > 1024;
debug('Lines ratio is %d bytes per line', Math.round(linesRatio));
debug(looksMinified ? 'It looks already minified' : 'It doesn\'t look minified');
return looksMinified;
}
function entryTypeCanBeMinified(entry) {
return entry.isJS || entry.isCSS || entry.isHTML;
}
return {
minifyFile: minifyFile,
minifyJs: minifyJs,
minifyCss: minifyCss,
minifyHtml: minifyHtml,
gainIsEnough: gainIsEnough,
entryTypeCanBeMinified: entryTypeCanBeMinified,
isKnownAsMinified: isKnownAsMinified
};
};
module.exports = new FileMinifier();
+102
View File
@@ -0,0 +1,102 @@
var debug = require('debug')('ylt:gzipCompressor');
var Q = require('q');
var zlib = require('zlib');
var GzipCompressor = function() {
function compressFile(entry) {
debug('Entering compressFile');
return gzipUncompressedFile(entry)
.then(gzipOptimizedFile);
}
// Gzip a file if it was not already gziped
function gzipUncompressedFile(entry) {
var deferred = Q.defer();
if (entryTypeCanBeGzipped(entry) && entry.weightCheck && !entry.weightCheck.isCompressed && entry.weightCheck.body) {
debug('Compression missing, trying to gzip file %s', entry.url);
var uncompressedSize = entry.weightCheck.uncompressedSize;
zlib.gzip(new Buffer(entry.weightCheck.body, 'utf8'), function(err, buffer) {
if (err) {
debug('Could not compress uncompressed file with gzip');
debug(err);
deferred.reject(err);
} else {
var compressedSize = buffer.length;
if (gainIsEnough(uncompressedSize, compressedSize)) {
debug('File correctly gziped, was %d and is now %d bytes', uncompressedSize, compressedSize);
entry.weightCheck.afterCompression = compressedSize;
} else {
debug('Gzip gain is not enough, was %d and is now %d bytes', uncompressedSize, compressedSize);
}
deferred.resolve(entry);
}
});
} else {
debug('Compression not needed');
deferred.resolve(entry);
}
return deferred.promise;
}
// Gzip a file after minification or optimization if this step was successful
function gzipOptimizedFile(entry) {
var deferred = Q.defer();
if (entryTypeCanBeGzipped(entry) && entry.weightCheck && entry.weightCheck.isOptimized === false) {
debug('Trying to gzip file after minification: %s', entry.url);
var uncompressedSize = entry.weightCheck.optimized;
zlib.gzip(new Buffer(entry.weightCheck.bodyAfterOptimization, 'utf8'), function(err, buffer) {
if (err) {
debug('Could not compress minified file with gzip');
debug(err);
deferred.reject(err);
} else {
var compressedSize = buffer.length;
debug('Correctly gziped the minified file, was %d and is now %d bytes', uncompressedSize, compressedSize);
entry.weightCheck.afterOptimizationAndCompression = compressedSize;
deferred.resolve(entry);
}
});
} else {
debug('Compressing optimized file not needed');
deferred.resolve(entry);
}
return deferred.promise;
}
// The gain is estimated of enough value if it's over 1KB or over 20%,
// but it's ignored if is below 100 bytes
function gainIsEnough(oldWeight, newWeight) {
var gain = oldWeight - newWeight;
var ratio = gain / oldWeight;
return (gain > 2048 || (ratio > 0.2 && gain > 100));
}
function entryTypeCanBeGzipped(entry) {
return entry.isJS || entry.isCSS || entry.isHTML || entry.isJSON || entry.isSVG || entry.isTTF || entry.isXML || entry.isFavicon;
}
return {
compressFile: compressFile,
entryTypeCanBeGzipped: entryTypeCanBeGzipped
};
};
module.exports = new GzipCompressor();
+245
View File
@@ -0,0 +1,245 @@
var debug = require('debug')('ylt:imageOptimizer');
var Q = require('q');
var Imagemin = require('imagemin');
var jpegoptim = require('imagemin-jpegoptim');
var ImageOptimizer = function() {
var MAX_JPEG_QUALITY = 85;
var OPTIPNG_COMPRESSION_LEVEL = 1;
function optimizeImage(entry) {
var deferred = Q.defer();
if (!entry.weightCheck || !entry.weightCheck.body) {
// No valid file available
deferred.resolve(entry);
return deferred.promise;
}
var fileSize = entry.weightCheck.uncompressedSize;
debug('Let\'s try to optimize %s', entry.url);
debug('Current file size is %d', fileSize);
if (isJPEG(entry)) {
debug('File is a JPEG');
// Starting softly with a lossless compression
return compressJpegLosslessly(new Buffer(entry.weightCheck.body, 'binary'))
.then(function(newFile) {
if (!newFile) {
debug('Optimization didn\'t work');
return entry;
}
var newFileSize = newFile.contents.length;
debug('JPEG lossless compression complete for %s', entry.url);
if (gainIsEnough(fileSize, newFileSize)) {
entry.weightCheck.lossless = entry.weightCheck.optimized = newFileSize;
entry.weightCheck.isOptimized = false;
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
}
// Now let's compress lossy to MAX_JPEG_QUALITY
return compressJpegLossly(new Buffer(entry.weightCheck.body, 'binary'));
})
.then(function(newFile) {
if (!newFile) {
debug('Optimization didn\'t work');
return entry;
}
var newFileSize = newFile.contents.length;
debug('JPEG lossy compression complete for %s', entry.url);
if (gainIsEnough(fileSize, newFileSize)) {
if (entry.weightCheck.isOptimized !== false || newFileSize < entry.weightCheck.lossless) {
entry.weightCheck.optimized = newFileSize;
}
entry.weightCheck.lossy = newFileSize;
entry.weightCheck.isOptimized = false;
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
}
return entry;
})
.fail(function() {
return entry;
});
} else if (isPNG(entry)) {
debug('File is a PNG');
// Starting softly with a lossless compression
return compressPngLosslessly(new Buffer(entry.weightCheck.body, 'binary'))
.then(function(newFile) {
if (!newFile) {
debug('Optimization didn\'t work');
return entry;
}
var newFileSize = newFile.contents.length;
debug('PNG lossless compression complete for %s', entry.url);
debug('Old file size: %d', fileSize);
debug('New file size: %d', newFileSize);
debug('newgainIsEnough: %s', gainIsEnough(fileSize, newFileSize) ? 'true':'false');
if (gainIsEnough(fileSize, newFileSize)) {
entry.weightCheck.lossless = entry.weightCheck.optimized = newFileSize;
entry.weightCheck.isOptimized = false;
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
}
return entry;
})
.fail(function() {
return entry;
});
} else if (isSVG(entry)) {
debug('File is an SVG');
// Starting softly with a lossless compression
return compressSvgLosslessly(new Buffer(entry.weightCheck.body, 'utf8'))
.then(function(newFile) {
if (!newFile) {
debug('Optimization didn\'t work');
return entry;
}
var newFileSize = newFile.contents.length;
debug('SVG lossless compression complete for %s', entry.url);
if (gainIsEnough(fileSize, newFileSize)) {
entry.weightCheck.bodyAfterOptimization = newFile.contents.toString();
entry.weightCheck.lossless = entry.weightCheck.optimized = newFileSize;
entry.weightCheck.isOptimized = false;
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
}
return entry;
})
.fail(function() {
return entry;
});
} else {
debug('File type %s is not an optimizable image', entry.contentType);
deferred.resolve(entry);
}
return deferred.promise;
}
// The gain is estimated of enough value if it's over 2KB or over 20%,
// but it's ignored if is below 100 bytes
function gainIsEnough(oldWeight, newWeight) {
var gain = oldWeight - newWeight;
var ratio = gain / oldWeight;
return (gain > 2048 || (ratio > 0.2 && gain > 100));
}
function isJPEG(entry) {
return entry.isImage && entry.contentType === 'image/jpeg';
}
function isPNG(entry) {
return entry.isImage && entry.contentType === 'image/png';
}
function isSVG(entry) {
return entry.isImage && entry.isSVG;
}
function compressJpegLosslessly(imageBody) {
return imageminLauncher(imageBody, 'jpeg', false);
}
function compressJpegLossly(imageBody) {
return imageminLauncher(imageBody, 'jpeg', true);
}
function compressPngLosslessly(imageBody) {
return imageminLauncher(imageBody, 'png', false);
}
function compressSvgLosslessly(imageBody) {
return imageminLauncher(imageBody, 'svg', false);
}
function imageminLauncher(imageBody, type, lossy) {
var deferred = Q.defer();
var startTime = Date.now();
debug('Starting %s %s optimization', type, lossy ? 'lossy' : 'lossless');
var engine;
if (type === 'jpeg' && !lossy) {
engine = Imagemin.jpegtran();
} else if (type === 'jpeg' && lossy) {
engine = jpegoptim({max: MAX_JPEG_QUALITY});
} else if (type === 'png' && !lossy) {
engine = Imagemin.optipng({optimizationLevel: OPTIPNG_COMPRESSION_LEVEL});
} else if (type === 'svg' && !lossy) {
engine = Imagemin.svgo({ plugins: [ { removeUselessDefs: false } ] });
} else {
deferred.reject('No optimization engine found for imagemin');
}
try {
new Imagemin()
.src(imageBody)
.use(engine)
.run(function (err, files) {
if (err) {
deferred.reject(err);
} else {
deferred.resolve(files[0]);
var endTime = Date.now();
debug('Optimization for %s took %d ms', type, endTime - startTime);
}
});
} catch(err) {
deferred.reject(err);
}
return deferred.promise;
}
function entryTypeCanBeOptimized(entry) {
return isJPEG(entry) || isPNG(entry) || isSVG(entry);
}
return {
optimizeImage: optimizeImage,
compressJpegLosslessly: compressJpegLosslessly,
compressJpegLossly: compressJpegLossly,
compressPngLosslessly: compressPngLosslessly,
compressSvgLosslessly: compressSvgLosslessly,
gainIsEnough: gainIsEnough,
entryTypeCanBeOptimized: entryTypeCanBeOptimized
};
};
module.exports = new ImageOptimizer();
+536
View File
@@ -0,0 +1,536 @@
/*
* Redownloading every files after Phantomas has finished
* Checks weight and every kind of compression
*
*/
var debug = require('debug')('ylt:weightChecker');
var Q = require('q');
var http = require('http');
var zlib = require('zlib');
var async = require('async');
var request = require('request');
var imageOptimizer = require('./imageOptimizer');
var fileMinifier = require('./fileMinifier');
var gzipCompressor = require('./gzipCompressor');
var WeightChecker = function() {
var MAX_PARALLEL_DOWNLOADS = 10;
var REQUEST_TIMEOUT = 15000; // 15 seconds
// This function will re-download every asset and check if it could be optimized
function recheckAllFiles(data) {
var startTime = Date.now();
debug('Redownload started');
var deferred = Q.defer();
var requestsList = JSON.parse(data.toolsResults.phantomas.offenders.requestsList);
delete data.toolsResults.phantomas.metrics.requestsList;
delete data.toolsResults.phantomas.offenders.requestsList;
var httpAuth = null;
if (data.params && data.params.options && data.params.options.authUser && data.params.options.authPass) {
httpAuth = {
username: data.params.options.authUser,
password: data.params.options.authPass
};
}
// Transform every request into a download function with a callback when done
var redownloadList = requestsList.map(function(entry) {
return function(callback) {
redownloadEntry(entry, httpAuth)
.then(imageOptimizer.optimizeImage)
.then(fileMinifier.minifyFile)
.then(gzipCompressor.compressFile)
.then(function(newEntry) {
debug('File %s - Redownloaded, optimized, minified, compressed: done', entry.url);
callback(null, newEntry);
})
.fail(function(err) {
callback(err);
});
};
});
// Lanch all redownload functions and wait for completion
async.parallelLimit(redownloadList, MAX_PARALLEL_DOWNLOADS, function(err, results) {
if (err) {
debug(err);
deferred.reject(err);
} else {
debug('All files checked');
endTime = Date.now();
debug('Redownload took %d ms', endTime - startTime);
var metrics = {};
var offenders = {};
// Count requests
offenders.totalRequests = listRequestsByType(results);
metrics.totalRequests = offenders.totalRequests.total;
// Remove unwanted requests (redirections, about:blank)
results = results.filter(function(result) {
return (result !== null && result.weightCheck && result.weightCheck.bodySize > 0);
});
// Total weight
offenders.totalWeight = listRequestWeight(results);
metrics.totalWeight = offenders.totalWeight.totalWeight;
// Image compression
offenders.imageOptimization = listImageNotOptimized(results);
metrics.imageOptimization = offenders.imageOptimization.totalGain;
// File minification
offenders.fileMinification = listFilesNotMinified(results);
metrics.fileMinification = offenders.fileMinification.totalGain;
// Gzip compression
offenders.gzipCompression = listFilesNotGzipped(results);
metrics.gzipCompression = offenders.gzipCompression.totalGain;
// Small requests
offenders.smallRequests = listSmallRequests(results);
metrics.smallRequests = offenders.smallRequests.total;
data.toolsResults.weightChecker = {
metrics: metrics,
offenders: offenders
};
deferred.resolve(data);
}
});
return deferred.promise;
}
function listRequestWeight(requests) {
var results = {
totalWeight: 0,
byType: {
html: {
totalWeight: 0,
requests: []
},
css: {
totalWeight: 0,
requests: []
},
js: {
totalWeight: 0,
requests: []
},
json: {
totalWeight: 0,
requests: []
},
image: {
totalWeight: 0,
requests: []
},
video: {
totalWeight: 0,
requests: []
},
webfont: {
totalWeight: 0,
requests: []
},
other: {
totalWeight: 0,
requests: []
}
}
};
requests.forEach(function(req) {
var weight = ((typeof req.weightCheck.bodySize === 'number') ? req.weightCheck.bodySize + req.weightCheck.headersSize : req.contentLength) || 0;
var type = req.type || 'other';
type = (results.byType[type]) ? type : 'other';
results.totalWeight += weight;
results.byType[type].totalWeight += weight;
results.byType[type].requests.push({
url: req.url,
weight: weight
});
});
return results;
}
function listImageNotOptimized(requests) {
var results = {
totalGain: 0,
images: []
};
requests.forEach(function(req) {
if (req.weightCheck.bodySize > 0 && imageOptimizer.entryTypeCanBeOptimized(req) && req.weightCheck.isOptimized === false) {
var before = req.weightCheck.afterCompression || req.weightCheck.bodySize;
var after = req.weightCheck.afterOptimizationAndCompression || req.weightCheck.optimized;
var gain = before - after;
if (gain > 200) {
results.totalGain += gain;
results.images.push({
url: req.url,
original: req.weightCheck.bodySize,
isCompressed: req.weightCheck.isCompressed,
afterCompression: req.weightCheck.afterCompression,
afterOptimizationAndCompression: req.weightCheck.afterOptimizationAndCompression,
lossless: req.weightCheck.lossless,
lossy: req.weightCheck.lossy,
gain: gain
});
}
}
});
return results;
}
function listFilesNotMinified(requests) {
var results = {
totalGain: 0,
files: []
};
requests.forEach(function(req) {
if (req.weightCheck.bodySize > 0 && fileMinifier.entryTypeCanBeMinified(req) && req.weightCheck.isOptimized === false) {
var before = req.weightCheck.afterCompression || req.weightCheck.bodySize;
var after = req.weightCheck.afterOptimizationAndCompression || req.weightCheck.optimized;
var gain = before - after;
if (gain > 200) {
results.totalGain += gain;
results.files.push({
url: req.url,
original: req.weightCheck.bodySize,
isCompressed: req.weightCheck.isCompressed,
afterCompression: req.weightCheck.afterCompression,
afterOptimizationAndCompression: req.weightCheck.afterOptimizationAndCompression,
optimized: req.weightCheck.optimized,
gain: gain
});
}
}
});
return results;
}
function listFilesNotGzipped(requests) {
var results = {
totalGain: 0,
files: []
};
requests.forEach(function(req) {
if (req.weightCheck.uncompressedSize && req.weightCheck.isCompressed === false && req.weightCheck.afterCompression) {
var gain = req.weightCheck.uncompressedSize - req.weightCheck.afterCompression;
results.totalGain += gain;
results.files.push({
url: req.url,
original: req.weightCheck.uncompressedSize,
gzipped: req.weightCheck.afterCompression,
gain: gain
});
}
});
return results;
}
function listRequestsByType(requests) {
var results = {
total: 0,
byType: {
html: [],
css: [],
js: [],
json: [],
image: [],
video: [],
webfont: [],
other: []
}
};
requests.forEach(function(req) {
if (req.url !== 'about:blank') {
var type = req.type || 'other';
type = (results.byType[type]) ? type : 'other';
results.byType[type].push(req.url);
results.total ++;
}
});
return results;
}
function listSmallRequests(requests) {
var results = {
total: 0,
byType: {
css: [],
js: [],
image: []
}
};
requests.forEach(function(req) {
if (req.weightCheck.bodySize > 0 && req.weightCheck.bodySize < 2048) {
if (req.isCSS || req.isJS || req.isImage) {
results.byType[req.type].push({
url: req.url,
size: req.weightCheck.bodySize
});
results.total ++;
}
}
});
return results;
}
function redownloadEntry(entry, httpAuth) {
var deferred = Q.defer();
function downloadError(message) {
debug('Could not download %s Error: %s', entry.url, message);
entry.weightCheck = {
message: message
};
deferred.resolve(entry);
}
// Not downloaded again but will be counted in totalWeight
function notDownloadableFile(message) {
entry.weightCheck = {
message: message
};
deferred.resolve(entry);
}
// Not counted in totalWeight
function unwantedFile(message) {
debug(message);
deferred.resolve(entry);
}
if (entry.method !== 'GET') {
notDownloadableFile('only downloading GET');
return deferred.promise;
}
if (entry.status !== 200) {
unwantedFile('only downloading requests with status code 200');
return deferred.promise;
}
if (entry.url === 'about:blank') {
unwantedFile('not downloading about:blank');
return deferred.promise;
}
debug('Downloading %s', entry.url);
// Always add a gzip header before sending, in case the server listens to it
var reqHeaders = entry.requestHeaders;
reqHeaders['Accept-Encoding'] = 'gzip, deflate';
var requestOptions = {
method: entry.method,
url: entry.url,
headers: reqHeaders,
timeout: REQUEST_TIMEOUT
};
// Basic auth
if (httpAuth) {
requestOptions.auth = {
user: httpAuth.username,
pass: httpAuth.password,
sendImmediately: false // Tries a first time without auth, wait for a 401 error before resending
};
}
download(requestOptions, entry.contentType, function(error, result) {
if (error) {
if (error.code === 'ETIMEDOUT') {
downloadError('timeout after ' + REQUEST_TIMEOUT + 'ms');
} else {
downloadError('error while downloading: ' + error.code);
}
return;
}
debug('%s downloaded correctly', entry.url);
entry.weightCheck = result;
deferred.resolve(entry);
});
return deferred.promise;
}
// Inspired by https://github.com/cvan/fastHAR-api/blob/10cec585/app.js
function download(requestOptions, contentType, callback) {
var statusCode;
try {
request(requestOptions)
.on('response', function(res) {
// Raw headers were added in NodeJS v0.12
// (https://github.com/joyent/node/issues/4844), but let's
// reconstruct them for backwards compatibility.
var rawHeaders = ('HTTP/' + res.httpVersion + ' ' + res.statusCode +
' ' + http.STATUS_CODES[res.statusCode] + '\r\n');
Object.keys(res.headers).forEach(function(headerKey) {
rawHeaders += headerKey + ': ' + res.headers[headerKey] + '\r\n';
});
rawHeaders += '\r\n';
var uncompressedSize = 0; // size after uncompression
var bodySize = 0; // bytes size over the wire
var body = ''; // plain text body (after uncompressing gzip/deflate)
var isCompressed = false;
function tally() {
if (statusCode !== 200) {
callback({code: statusCode});
return;
}
var result = {
body: body,
headersSize: Buffer.byteLength(rawHeaders, 'utf8'),
bodySize: bodySize,
isCompressed: isCompressed,
uncompressedSize: uncompressedSize
};
callback(null, result);
}
switch (res.headers['content-encoding']) {
case 'gzip':
var gzip = zlib.createGunzip();
gzip.on('data', function (data) {
body += data;
uncompressedSize += data.length;
}).on('end', function () {
isCompressed = true;
tally();
}).on('error', function(err) {
debug('Error while decoding %s', requestOptions.url);
debug(err);
callback(err);
});
res.on('data', function (data) {
bodySize += data.length;
}).pipe(gzip);
break;
case 'deflate':
res.setEncoding('utf8');
var deflate = zlib.createInflate();
deflate.on('data', function (data) {
body += data;
uncompressedSize += data.length;
}).on('end', function () {
isCompressed = true;
tally();
}).on('error', function(err) {
debug('Error while decoding %s', requestOptions.url);
debug(err);
callback(err);
});
res.on('data', function (data) {
bodySize += data.length;
}).pipe(deflate);
break;
default:
if (contentType === 'image/jpeg' || contentType === 'image/png') {
res.setEncoding('binary');
}
res.on('data', function (data) {
body += data;
uncompressedSize += data.length;
bodySize += data.length;
}).on('end', function () {
tally();
});
break;
}
})
.on('response', function(response) {
statusCode = response.statusCode;
})
.on('error', function(err) {
debug('Error while downloading %s', requestOptions.url);
debug(err);
callback(err);
});
} catch(err) {
debug('Error while downloading %s', requestOptions.url);
debug(err);
callback(err);
}
}
return {
recheckAllFiles: recheckAllFiles,
listRequestWeight: listRequestWeight,
redownloadEntry: redownloadEntry,
download: download
};
};
module.exports = new WeightChecker();
+76 -40
View File
@@ -1,7 +1,13 @@
{
"name": "yellowlabtools",
"version": "1.2.0",
"version": "1.10.0",
"description": "Online tool to audit a webpage for performance and front-end quality issues",
"license": "GPL-2.0",
"author": {
"name": "Gaël Métais",
"email": "gael@gaelmetais.com",
"url": "http://www.gaelmetais.com"
},
"repository": {
"type": "git",
"url": "git://github.com/gmetais/YellowLabTools.git"
@@ -9,50 +15,80 @@
"bin": {
"yellowlabtools": "./bin/cli.js"
},
"engines": {
"node": ">= 0.12.0"
},
"main": "./lib/index.js",
"dependencies": {
"async": "~0.9.0",
"body-parser": "~1.10.0",
"compression": "~1.2.2",
"cors": "^2.5.2",
"debug": "~2.1.0",
"express": "~4.10.6",
"lwip": "0.0.6",
"meow": "^3.0.0",
"phantomas": "1.9.0",
"ps-node": "0.0.3",
"q": "~1.1.2",
"rimraf": "~2.2.8",
"temporary": "0.0.8"
"angular": "1.5.2",
"angular-animate": "1.5.2",
"angular-chart.js": "0.9.0",
"angular-local-storage": "0.2.7",
"angular-resource": "1.5.2",
"angular-route": "1.5.2",
"angular-sanitize": "1.5.2",
"async": "1.5.2",
"body-parser": "1.15.0",
"chart.js": "1.0.2",
"clean-css": "3.4.10",
"color-diff": "1.0.0",
"compression": "1.6.1",
"cors": "2.7.1",
"css-mq-parser": "0.0.3",
"debug": "2.2.0",
"easyxml": "2.0.1",
"express": "4.13.4",
"imagemin": "4.0.0",
"imagemin-jpegoptim": "4.1.0",
"is-http2": "1.0.4",
"lwip": "0.0.8",
"meow": "3.7.0",
"minimize": "1.8.1",
"parse-color": "1.0.0",
"phantomas": "1.15.1",
"ps-node": "0.0.5",
"q": "1.4.1",
"request": "2.69.0",
"rimraf": "2.5.2",
"temporary": "0.0.8",
"try-thread-sleep": "1.0.0",
"uglify-js": "2.6.2"
},
"devDependencies": {
"chai": "^1.10.0",
"grunt": "^0.4.5",
"grunt-blanket": "^0.0.8",
"grunt-contrib-clean": "^0.6.0",
"grunt-contrib-concat": "^0.5.0",
"grunt-contrib-copy": "^0.7.0",
"grunt-contrib-cssmin": "^0.11.0",
"grunt-contrib-htmlmin": "^0.3.0",
"grunt-contrib-jshint": "^0.10.0",
"grunt-contrib-less": "^0.12.0",
"grunt-contrib-uglify": "^0.7.0",
"grunt-env": "^0.4.2",
"grunt-express": "^1.4.1",
"grunt-filerev": "^2.1.2",
"grunt-fontsmith": "^0.9.1",
"grunt-inline-angular-templates": "^0.1.5",
"grunt-line-remover": "^0.0.2",
"grunt-mocha-test": "^0.12.4",
"grunt-replace": "^0.8.0",
"grunt-usemin": "^3.0.0",
"matchdep": "^0.3.0",
"mocha": "^2.1.0",
"request": "^2.51.0",
"sinon": "^1.12.1",
"sinon-chai": "^2.6.0"
"chai": "~3.5.0",
"grunt": "~0.4.5",
"grunt-blanket": "~0.0.10",
"grunt-contrib-clean": "~1.0.0",
"grunt-contrib-concat": "~1.0.0",
"grunt-contrib-copy": "~1.0.0",
"grunt-contrib-cssmin": "~1.0.1",
"grunt-contrib-htmlmin": "~1.1.0",
"grunt-contrib-jshint": "~1.0.0",
"grunt-contrib-less": "~1.2.0",
"grunt-contrib-uglify": "~1.0.1",
"grunt-env": "~0.4.4",
"grunt-express": "~1.4.1",
"grunt-filerev": "~2.3.1",
"grunt-inline-angular-templates": "~0.1.5",
"grunt-line-remover": "~0.0.2",
"grunt-mocha-test": "~0.12.7",
"grunt-replace": "~0.11.0",
"grunt-usemin": "~3.1.1",
"grunt-webfont": "~1.2.0",
"matchdep": "~1.0.1",
"mocha": "~2.4.5",
"sinon": "~1.17.3",
"sinon-chai": "~2.8.0"
},
"scripts": {
"test": "grunt test"
}
},
"keywords": [
"performance",
"metrics",
"webperf",
"pagespeed",
"budget",
"phantomas"
]
}
+13
View File
@@ -0,0 +1,13 @@
var express = require('express');
var app = express();
var server = require('http').createServer(app);
var settings = require('./settings.json');
app.all('*', function(req, res) {
res.status(500).send('YellowLabTools is in maintenance. It should come back soon with a new version!');
});
server.listen(settings.serverPort, function() {
console.log('Maintenance mode started on port %d', server.address().port);
});
+4 -5
View File
@@ -2,7 +2,7 @@
# APT-GET
sudo apt-get update
sudo apt-get install lsb-release libfontconfig1 libfreetype6 -y --force-yes
sudo apt-get install lsb-release libfontconfig1 libfreetype6 libjpeg-dev -y --force-yes
sudo apt-get install curl git python-software-properties build-essential make g++ -y --force-yes
# Installation of NodeJS
@@ -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 forever grunt-cli -g
source ~/.profile
# Installation of YellowLabTools
@@ -21,8 +21,7 @@ sudo chown $USER /space
cd /space
git clone https://github.com/gmetais/YellowLabTools.git --branch master
cd YellowLabTools
npm install
bower install --config.interactive=false --allow-root
npm install || exit 1
# Front-end compilation
grunt build
@@ -30,4 +29,4 @@ grunt build
# Start the server
rm server_config/settings.json
cp server_config/settings-prod.json server_config/settings.json
NODE_ENV=production forever start -c "node --stack-size=65500" bin/server.js
NODE_ENV=production forever start -c "node --stack-size=262000" bin/server.js
+7 -9
View File
@@ -2,25 +2,23 @@
cd /space/YellowLabTools
# Stop the server
# Stop the server and start the maintenance mode
forever stopall
forever start server_config/maintenance.js
# Keep the settings.json file
git stash
git pull
git stash pop
# In case something was added in package.json or bower.json
# In case something was added in package.json
rm -rf node_modules
npm install
rm -rf bower_components
bower install --config.interactive=false --allow-root
npm install || exit 1
# Front-end compilation
rm -rf front/build
npm install grunt-cli -g
npm install phantomjs -g
grunt build
# Restart the server
NODE_ENV=production forever start -c "node --stack-size=65500" bin/server.js
# Stop the maintenance mode and restart the server
forever stopall
NODE_ENV=production forever start -c "node --stack-size=262000" bin/server.js
+2
View File
@@ -1,6 +1,8 @@
{
"serverPort": 80,
"phantomasEngine": "webkit",
"googleAnalyticsId": "",
"screenshotWidth": 400,
"authorizedKeys": {
+2
View File
@@ -1,6 +1,8 @@
{
"serverPort": 8383,
"phantomasEngine": "webkit",
"googleAnalyticsId": "",
"screenshotWidth": 400,
"authorizedKeys": {
+128 -2
View File
@@ -31,6 +31,7 @@ describe('api', function() {
},
json: true,
headers: {
'Content-Type': 'application/json',
'X-Api-Key': 'invalid'
}
}, function(error, response, body) {
@@ -53,6 +54,7 @@ describe('api', function() {
},
json: true,
headers: {
'Content-Type': 'application/json',
'X-Api-Key': Object.keys(config.authorizedKeys)[0]
}
}, function(error, response, body) {
@@ -76,6 +78,7 @@ describe('api', function() {
},
json: true,
headers: {
'Content-Type': 'application/json',
'X-Api-Key': Object.keys(config.authorizedKeys)[0]
}
}, function(error, response, body) {
@@ -96,10 +99,16 @@ describe('api', function() {
body: {
url: wwwUrl + '/simple-page.html',
waitForResponse: true,
screenshot: true
screenshot: true,
device: 'tablet',
//waitForSelector: '*',
cookie: 'foo=bar;domain=google.com',
authUser: 'joe',
authPass: 'secret'
},
json: true,
headers: {
'Content-Type': 'application/json',
'X-Api-Key': Object.keys(config.authorizedKeys)[0]
}
}, function(error, response, body) {
@@ -128,6 +137,7 @@ describe('api', function() {
},
json: true,
headers: {
'Content-Type': 'application/json',
'X-Api-Key': Object.keys(config.authorizedKeys)[0]
}
}, function(error, response, body) {
@@ -159,7 +169,16 @@ describe('api', function() {
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('toolsResults').that.is.an('object');
body.should.have.a.property('javascriptExecutionTree').that.is.an('object');
body.javascriptExecutionTree.should.not.deep.equal({});
// Check if settings are correctly sent and retrieved
body.params.options.should.have.a.property('device').that.equals('tablet');
//body.params.options.should.have.a.property('waitForSelector').that.equals('*');
body.params.options.should.have.a.property('cookie').that.equals('foo=bar;domain=google.com');
body.params.options.should.have.a.property('authUser').that.equals('joe');
body.params.options.should.have.a.property('authPass').that.equals('secret');
// Check if the screenshot temporary file was correctly removed
body.params.options.should.not.have.a.property('screenshot');
@@ -188,10 +207,12 @@ describe('api', function() {
url: serverUrl + '/api/runs',
body: {
url: wwwUrl + '/simple-page.html',
waitForResponse: false
waitForResponse: false,
jsTimeline: true
},
json: true,
headers: {
'Content-Type': 'application/json',
'X-Api-Key': Object.keys(config.authorizedKeys)[0]
}
}, function(error, response, body) {
@@ -216,6 +237,7 @@ describe('api', function() {
url: serverUrl + '/api/runs/' + asyncRunId,
json: true,
headers: {
'Content-Type': 'application/json',
'X-Api-Key': Object.keys(config.authorizedKeys)[0]
}
}, function(error, response, body) {
@@ -528,6 +550,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);
+2 -2
View File
@@ -85,7 +85,7 @@ describe('screenshotHandler', function() {
it('should respond a temporary file', function() {
var file = screenshot.getTmpFilePath();
file.should.have.string('/screenshot.jpg');
file.should.have.string('/screenshot.png');
});
@@ -106,7 +106,7 @@ describe('screenshotHandler', function() {
it('should delete the temp folder with the screenshot inside', function(done) {
screenshot = ScreenshotHandler.getScreenshotTempFile();
var tmpFolderPath = screenshot.getTmpFolder().path;
var tmpImagePath = path.join(tmpFolderPath, 'screenshot.jpg');
var tmpImagePath = path.join(tmpFolderPath, 'screenshot.png');
// Copy image
var testImage = fs.readFileSync(imagePath);
+92
View File
@@ -0,0 +1,92 @@
var should = require('chai').should();
var colorDiff = require('../../lib/tools/colorDiff');
describe('colorDiff', function() {
it('should parse offenders correctly', function() {
colorDiff.parseOffender('#000 (1 times)').should.equal('#000');
colorDiff.parseOffender('#5bc0de (2 times)').should.equal('#5bc0de');
colorDiff.parseOffender('rgba(0,0,0,0.075) (100 times)').should.equal('rgba(0,0,0,0.075)');
colorDiff.parseOffender('rgb(91,192,222) (1000 times)').should.equal('rgb(91,192,222)');
});
it('should parse colors correctly', function() {
colorDiff.parseColor('#000').should.deep.equal({R: 0, G: 0, B: 0, A: 1, Lab: {L: 0, a: 0, b: 0}, original: '#000'});
colorDiff.parseColor('rgba(255,255,255,0.5)').should.deep.equal({R: 255, G: 255, B: 255, A: 0.5, Lab: {L: 100, a: 0.00526049995830391, b: -0.010408184525267927}, original: 'rgba(255,255,255,0.5)'});
});
it('should not compare colors with different alpha', function() {
var color1 = colorDiff.parseColor("rgba(0, 0, 0, 1)");
var color2 = colorDiff.parseColor("rgba(0, 0, 0, 0.5)");
colorDiff.compareTwoColors(color1, color2).should.equal(false);
});
it('should find that two colors are similar', function() {
var color1 = colorDiff.parseColor("rgba(0, 0, 0, 1)");
var color2 = colorDiff.parseColor("rgba(0, 0, 1, 1)");
colorDiff.compareTwoColors(color1, color2).should.equal(true);
});
it('should find that two colors are similar even with alpha', function() {
var color1 = colorDiff.parseColor("rgba(0, 0, 0, 0.3)");
var color2 = colorDiff.parseColor("rgba(0, 0, 2, 0.3)");
colorDiff.compareTwoColors(color1, color2).should.equal(true);
});
it('should find that two colors are different', function() {
var color1 = colorDiff.parseColor("rgba(0, 0, 0, 1)");
var color2 = colorDiff.parseColor("rgba(99, 99, 99, 1)");
colorDiff.compareTwoColors(color1, color2).should.equal(false);
});
it('should compare all colors to each other', function() {
var colors = [
'#000 (1 times)',
'#5bc0de (2 times)',
'rgba(0,0,0,0.075) (100 times)',
'rgb(91,192,222) (1000 times)',
'rgba(0,0,2,1) (1 times)',
'rgba(99,99,99,1) (1 times)',
'rgba(100,100,100,1) (1 times)'
];
var data = {
toolsResults: {
phantomas: {
offenders: {
cssColors: colors
}
}
}
};
newData = colorDiff.compareAllColors(data);
newData.toolsResults.should.have.a.property('colorDiff');
newData.toolsResults.colorDiff.should.have.a.property('metrics');
newData.toolsResults.colorDiff.metrics.should.have.a.property('similarColors').that.equals(3);
newData.toolsResults.colorDiff.should.have.a.property('offenders');
newData.toolsResults.colorDiff.offenders.should.have.a.property('similarColors').that.deep.equals([
{
color1: '#000',
color2: 'rgba(0,0,2,1)',
isDark: true
},
{
color1: '#5bc0de',
color2: 'rgb(91,192,222)',
isDark: false
},
{
color1: 'rgba(99,99,99,1)',
color2: 'rgba(100,100,100,1)',
isDark: true
}
]);
});
});
+14 -193
View File
@@ -1,7 +1,7 @@
var should = require('chai').should();
var rulesChecker = require('../../lib/rulesChecker');
describe('rulesChecker', function() {
describe('customPolicies', function() {
var policies = require('../../lib/metadata/policies.js');
var results;
@@ -70,131 +70,6 @@ describe('rulesChecker', function() {
});
});
it('should transform DOMinserts offenders', function() {
results = rulesChecker.check({
"toolsResults": {
"phantomas": {
"metrics": {
"DOMinserts": 4
},
"offenders": {
"DOMinserts": [
"\"div\" appended to \"html\"",
"\"DocumentFragment > link[0]\" appended to \"head\"",
"\"div#Netaff-yh1XbS0vK3NaRGu\" appended to \"body > div#Global\"",
"\"img\" appended to \"body\""
]
}
}
}
}, policies);
results.should.have.a.property('DOMinserts');
results.DOMinserts.should.have.a.property('offendersObj').that.deep.equals({
"count": 4,
"list": [
{
"insertedElement": {
"type": "createdElement",
"element": "div"
},
"receiverElement": {
"type": "html"
}
},
{
"insertedElement": {
"type": "fragmentElement",
"element": "link[0]",
"tree": {
"DocumentFragment": {
"link[0]": 1
}
}
},
"receiverElement": {
"type": "head"
}
},
{
"insertedElement": {
"type": "createdElement",
"element": "div#Netaff-yh1XbS0vK3NaRGu"
},
"receiverElement": {
"type": "domElement",
"element": "div#Global",
"tree": {
"body": {
"div#Global": 1
}
}
}
},
{
"insertedElement": {
"type": "createdElement",
"element": "img"
},
"receiverElement": {
"type": "body"
}
}
]
});
});
it('should transform DOMqueriesWithoutResults offenders', function() {
results = rulesChecker.check({
"toolsResults": {
"phantomas": {
"metrics": {
"DOMqueriesWithoutResults": 2
},
"offenders": {
"DOMqueriesWithoutResults": [
"#SearchMenu (in #document) using getElementById",
".partnership-link (in body > div#Global > div#Header > ul#MainMenu) using getElementsByClassName"
]
}
}
}
}, policies);
results.should.have.a.property('DOMqueriesWithoutResults');
results.DOMqueriesWithoutResults.should.have.a.property('offendersObj').that.deep.equals({
"count": 2,
"list": [
{
"context": {
"type": "document"
},
"fn": "getElementById",
"query": "#SearchMenu "
},
{
"context": {
"element": "ul#MainMenu",
"tree": {
"body": {
"div#Global": {
"div#Header": {
"ul#MainMenu": 1
}
}
}
},
"type": "domElement"
},
"fn": "getElementsByClassName",
"query": ".partnership-link "
}
]
});
});
it('should transform DOMqueriesAvoidable offenders', function() {
results = rulesChecker.check({
@@ -246,61 +121,6 @@ describe('rulesChecker', function() {
});
it('should transform eventsBound offenders', function() {
results = rulesChecker.check({
"toolsResults": {
"phantomas": {
"metrics": {
"eventsBound": 2
},
"offenders": {
"eventsBound": [
"\"DOMContentLoaded\" bound to \"#document\"",
"\"unload\" bound to \"window\"",
"\"submit\" bound to \"body > div#Global > div#Header > form#search_mini_form\""
]
}
}
}
}, policies);
results.should.have.a.property('eventsBound');
results.eventsBound.should.have.a.property('offendersObj').that.deep.equals({
"count": 3,
"list": [
{
"element": {
"type": "document"
},
"eventName": "DOMContentLoaded"
},
{
"element": {
"type": "window"
},
"eventName": "unload"
},
{
"element": {
"element": "form#search_mini_form",
"tree": {
"body": {
"div#Global": {
"div#Header": {
"form#search_mini_form": 1
}
}
}
},
"type": "domElement"
},
"eventName": "submit"
}
]
});
});
it('should transform jsErrors offenders', function() {
results = rulesChecker.check({
"toolsResults": {
@@ -363,18 +183,19 @@ describe('rulesChecker', function() {
var versions = {
'1.2.9': 0,
'1.3.9': 0,
'1.4.4': 10,
'1.5.0': 20,
'1.6.3': 30,
'1.7.0': 40,
'1.8.3a': 50,
'1.9.2': 70,
'1.10.1': 90,
'2.0.0-rc1': 90,
'1.11.1': 100,
'2.1.1-beta1': 100,
'3.0.0': 100
'1.4.4': 0,
'1.5.0': 10,
'1.6.3': 20,
'1.7.0': 30,
'1.8.3a': 40,
'1.9.2': 50,
'1.10.1': 70,
'2.0.0-rc1': 70,
'1.11.1': 90,
'2.1.1-beta1': 90,
'1.12.1': 100,
'2.3.1': 100,
'3.1.0': 100
};
for (var version in versions) {
+244
View File
@@ -0,0 +1,244 @@
var should = require('chai').should();
var fileMinifier = require('../../lib/tools/weightChecker/fileMinifier');
var fs = require('fs');
var path = require('path');
describe('fileMinifier', function() {
it('should minify a JS file with minifyJs', function(done) {
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/unminified-script.js'));
var fileSize = fileContent.length;
fileMinifier.minifyJs(fileContent.toString()).then(function(newFile) {
var newFileSize = newFile.length;
newFileSize.should.be.below(fileSize);
done();
}).fail(function(err) {
done(err);
});
});
it('should minify a JS file with minifyFile', function(done) {
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/unminified-script.js'));
var fileSize = fileContent.length;
var entry = {
method: 'GET',
url: 'http://localhost:8388/unminified-script.js',
requestHeaders: {
'User-Agent': 'something',
Referer: 'http://www.google.fr/',
Accept: '*/*',
'Accept-Encoding': 'gzip, deflate'
},
status: 200,
isJS: true,
type: 'js',
contentLength: 999,
weightCheck: {
body: fileContent.toString('utf8'),
totalWeight: fileSize + 200,
headersSize: 200,
bodySize: fileSize,
isCompressed: false,
uncompressedSize: fileSize
}
};
fileMinifier.minifyFile(entry)
.then(function(newEntry) {
newEntry.weightCheck.should.have.a.property('isOptimized').that.equals(false);
newEntry.weightCheck.should.have.a.property('optimized').that.is.below(fileSize);
newEntry.weightCheck.should.have.a.property('bodyAfterOptimization').that.is.a('string');
done();
})
.fail(function(err) {
done(err);
});
});
it('should fail minifying an already minified JS', function(done) {
this.timeout(5000);
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jquery1.8.3.js'));
var fileSize = fileContent.length;
var entry = {
method: 'GET',
url: 'http://localhost:8388/jquery1.8.3.js',
requestHeaders: {
'User-Agent': 'something',
Referer: 'http://www.google.fr/',
Accept: '*/*',
'Accept-Encoding': 'gzip, deflate'
},
status: 200,
isJS: true,
type: 'js',
contentLength: 999,
weightCheck: {
body: fileContent.toString('utf8'),
totalWeight: fileSize + 200,
headersSize: 200,
bodySize: fileSize,
isCompressed: false,
uncompressedSize: fileSize
}
};
fileMinifier.minifyFile(entry)
.then(function(newEntry) {
newEntry.weightCheck.should.not.have.a.property('isOptimized');
newEntry.weightCheck.should.not.have.a.property('optimized');
newEntry.weightCheck.should.not.have.a.property('bodyAfterOptimization');
done();
})
.fail(function(err) {
done(err);
});
});
it('should fail minifying a JS with syntax errors', function(done) {
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/svg-image.svg'));
var fileSize = fileContent.length;
var entry = {
method: 'GET',
url: 'http://localhost:8388/svg-image.svg',
requestHeaders: {
'User-Agent': 'something',
Referer: 'http://www.google.fr/',
Accept: '*/*',
'Accept-Encoding': 'gzip, deflate'
},
status: 200,
isJS: true,
type: 'js',
contentLength: 999,
weightCheck: {
body: fileContent.toString('utf8'),
totalWeight: fileSize + 200,
headersSize: 200,
bodySize: fileSize,
isCompressed: false,
uncompressedSize: fileSize
}
};
fileMinifier.minifyFile(entry)
.then(function(newEntry) {
newEntry.weightCheck.should.not.have.a.property('isOptimized');
newEntry.weightCheck.should.not.have.a.property('optimized');
newEntry.weightCheck.should.not.have.a.property('bodyAfterOptimization');
done();
})
.fail(function(err) {
done(err);
});
});
it('should minify a CSS file with clean-css', function(done) {
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/unminified-stylesheet.css'));
var fileSize = fileContent.length;
fileMinifier.minifyCss(fileContent.toString()).then(function(newFile) {
var newFileSize = newFile.length;
newFileSize.should.be.below(fileSize);
done();
}).fail(function(err) {
done(err);
});
});
it('should minify a CSS file with minifyFile', function(done) {
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/unminified-stylesheet.css'));
var fileSize = fileContent.length;
var entry = {
method: 'GET',
url: 'http://localhost:8388/unminified-stylesheet.css',
requestHeaders: {
'User-Agent': 'something',
Referer: 'http://www.google.fr/',
Accept: '*/*',
'Accept-Encoding': 'gzip, deflate'
},
status: 200,
isCSS: true,
type: 'css',
contentLength: 999,
weightCheck: {
body: fileContent.toString('utf8'),
totalWeight: fileSize + 200,
headersSize: 200,
bodySize: fileSize,
isCompressed: false,
uncompressedSize: fileSize
}
};
fileMinifier.minifyFile(entry)
.then(function(newEntry) {
newEntry.weightCheck.should.have.a.property('isOptimized').that.equals(false);
newEntry.weightCheck.should.have.a.property('optimized').that.is.below(fileSize);
newEntry.weightCheck.should.have.a.property('bodyAfterOptimization').that.is.a('string');
done();
})
.fail(function(err) {
done(err);
});
});
it('should minify an HTML file with ', function(done) {
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jquery-page.html'));
var fileSize = fileContent.length;
fileMinifier.minifyHtml(fileContent.toString()).then(function(newFile) {
var newFileSize = newFile.length;
newFileSize.should.be.below(fileSize);
done();
}).fail(function(err) {
done(err);
});
});
it('should avoid minifying some JS files known as minified', function() {
fileMinifier.isKnownAsMinified('https://platform.twitter.com/widgets.js').should.equal(true);
fileMinifier.isKnownAsMinified('http://platform.twitter.com/widgets.js').should.equal(true);
fileMinifier.isKnownAsMinified('https://connect.facebook.net/fr_FR/sdk.js').should.equal(true);
fileMinifier.isKnownAsMinified('https://connect.facebook.net/en_EN/sdk.js').should.equal(true);
fileMinifier.isKnownAsMinified('https://connect.facebook.net/fr_FR/all.js').should.equal(true);
fileMinifier.isKnownAsMinified('https://connect.facebook.net/en_EN/all.js').should.equal(true);
fileMinifier.isKnownAsMinified('https://apis.google.com/js/plusone.js').should.equal(true);
fileMinifier.isKnownAsMinified('https://code.jquery.com/jquery-2.1.4.min.js').should.equal(true);
fileMinifier.isKnownAsMinified('http://code.jquery.com/jquery-2.1.4.min.js').should.equal(true);
fileMinifier.isKnownAsMinified('https://code.jquery.com/jquery-2.1.4.js').should.equal(false);
fileMinifier.isKnownAsMinified('http://code.jquery.com/jquery-2.1.4.js').should.equal(false);
fileMinifier.isKnownAsMinified('https://ssl.google-analytics.com/ga.js').should.equal(true);
fileMinifier.isKnownAsMinified('http://www.google-analytics.com/ga.js').should.equal(true);
fileMinifier.isKnownAsMinified('https://www.google-analytics.com/analytics.js').should.equal(true);
fileMinifier.isKnownAsMinified('http://www.google-analytics.com/analytics.js').should.equal(true);
fileMinifier.isKnownAsMinified('http://anydomain.com/anyurl').should.equal(false);
});
});
+583
View File
@@ -0,0 +1,583 @@
var should = require('chai').should();
var gzipCompressor = require('../../lib/tools/weightChecker/gzipCompressor');
var fileMinifier = require('../../lib/tools/weightChecker/fileMinifier');
var fs = require('fs');
var path = require('path');
describe('gzipCompressor', function() {
var minifiedJSContent = fs.readFileSync(path.resolve(__dirname, '../www/minified-script.js'));
var notMinifiedJSContent = fs.readFileSync(path.resolve(__dirname, '../www/unminified-script.js'));
var someTextFileContent = fs.readFileSync(path.resolve(__dirname, '../www/svg-image.svg'));
it('should gzip a JS file that was not gziped but was minified', function(done) {
var fileContent = minifiedJSContent;
var fileSize = fileContent.length;
var entry = {
method: 'GET',
url: 'http://localhost:8388/minified-script.js',
status: 200,
isJS: true,
type: 'js',
contentLength: 999,
weightCheck: {
body: fileContent.toString('utf8'),
totalWeight: fileSize + 200,
headersSize: 200,
bodySize: fileSize,
isCompressed: false,
uncompressedSize: fileSize,
isOptimized: true
}
};
gzipCompressor.compressFile(entry)
.then(function(newEntry) {
newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize);
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
done();
})
.fail(function(err) {
done(err);
});
});
it('should gzip a JS file that was not gziped and not minified', function(done) {
/*jshint expr: true*/
var fileContent = notMinifiedJSContent;
var minifiedContent = minifiedJSContent;
var fileSize = fileContent.length;
var minifiedSize = minifiedContent.length;
var entry = {
method: 'GET',
url: 'http://localhost:8388/unminified-script.js',
status: 200,
isJS: true,
type: 'js',
contentLength: 999,
weightCheck: {
body: fileContent.toString('utf8'),
bodyAfterOptimization: minifiedContent.toString('utf8'),
totalWeight: fileSize + 200,
headersSize: 200,
bodySize: fileSize,
isCompressed: false,
uncompressedSize: fileSize,
isOptimized: false,
optimized: minifiedSize
}
};
gzipCompressor.compressFile(entry)
.then(function(newEntry) {
newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize);
newEntry.weightCheck.should.have.a.property('afterOptimizationAndCompression').that.is.not.undefined;
newEntry.weightCheck.should.have.a.property('afterOptimizationAndCompression').that.is.below(newEntry.weightCheck.afterCompression);
done();
})
.fail(function(err) {
done(err);
});
});
it('should gzip a JS file that is gziped but not minified', function(done) {
/*jshint expr: true*/
var fileContent = notMinifiedJSContent;
var minifiedContent = minifiedJSContent;
var fileSize = 6436;
var gzipedSize = 2646;
var minifiedSize = 1954;
var entry = {
method: 'GET',
url: 'http://localhost:8388/unminified-script.js',
status: 200,
isJS: true,
type: 'js',
contentLength: 999,
weightCheck: {
body: fileContent.toString('utf8'),
bodyAfterOptimization: minifiedContent.toString('utf8'),
totalWeight: gzipedSize + 200,
headersSize: 200,
bodySize: gzipedSize,
isCompressed: true,
uncompressedSize: fileSize,
isOptimized: false,
optimized: minifiedSize
}
};
gzipCompressor.compressFile(entry)
.then(function(newEntry) {
newEntry.weightCheck.should.have.a.property('afterOptimizationAndCompression').that.is.not.undefined;
newEntry.weightCheck.should.have.a.property('afterOptimizationAndCompression').that.is.below(gzipedSize);
newEntry.weightCheck.should.have.a.property('afterOptimizationAndCompression').that.is.below(minifiedSize);
done();
})
.fail(function(err) {
done(err);
});
});
it('should not gzip a JS file that was gziped and minified', function(done) {
/*jshint expr: true*/
var fileContent = notMinifiedJSContent;
var fileSize = 6436;
var gzipedSize = 2646;
var entry = {
method: 'GET',
url: 'http://localhost:8388/unminified-script.js',
status: 200,
isJS: true,
type: 'js',
contentLength: 999,
weightCheck: {
body: fileContent.toString('utf8'),
totalWeight: gzipedSize + 200,
headersSize: 200,
bodySize: gzipedSize,
isCompressed: true,
uncompressedSize: fileSize,
isOptimized: true
}
};
gzipCompressor.compressFile(entry)
.then(function(newEntry) {
newEntry.weightCheck.should.not.have.a.property('minified');
newEntry.weightCheck.should.not.have.a.property('bodyAfterOptimization');
newEntry.weightCheck.should.not.have.a.property('afterCompression');
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
done();
})
.fail(function(err) {
done(err);
});
});
it('should gzip a CSS file', function(done) {
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/unminified-stylesheet.css'));
var fileSize = fileContent.length;
var entry = {
method: 'GET',
url: 'http://localhost:8388/unminified-stylesheet.css',
status: 200,
isCSS: true,
type: 'css',
contentLength: 999,
weightCheck: {
body: fileContent.toString('utf8'),
totalWeight: fileSize + 200,
headersSize: 200,
bodySize: fileSize,
isCompressed: false,
uncompressedSize: fileSize,
isOptimized: true
}
};
gzipCompressor.compressFile(entry)
.then(function(newEntry) {
newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize);
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
done();
})
.fail(function(err) {
done(err);
});
});
it('should gzip an HTML file', function(done) {
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jquery-page.html'));
var fileSize = fileContent.length;
var entry = {
method: 'GET',
url: 'http://localhost:8388/jquery-page.html',
status: 200,
isHTML: true,
type: 'html',
contentLength: 999,
weightCheck: {
body: fileContent.toString('utf8'),
totalWeight: fileSize + 200,
headersSize: 200,
bodySize: fileSize,
isCompressed: false,
uncompressedSize: fileSize,
isOptimized: true
}
};
gzipCompressor.compressFile(entry)
.then(function(newEntry) {
newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize);
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
done();
})
.fail(function(err) {
done(err);
});
});
it('should gzip an SVG file', function(done) {
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/svg-image.svg'));
var fileSize = fileContent.length;
var entry = {
method: 'GET',
url: 'http://localhost:8388/svg-image.svg',
status: 200,
isImage: true,
isSVG: true,
type: 'image',
contentLength: 999,
weightCheck: {
body: fileContent.toString('utf8'),
totalWeight: fileSize + 200,
headersSize: 200,
bodySize: fileSize,
isCompressed: false,
uncompressedSize: fileSize,
isOptimized: true
}
};
gzipCompressor.compressFile(entry)
.then(function(newEntry) {
newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize);
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
done();
})
.fail(function(err) {
done(err);
});
});
it('should gzip an XML file', function(done) {
var fileContent = someTextFileContent; // it dosn't matter if it's not the correct file type
var fileSize = fileContent.length;
var entry = {
method: 'GET',
url: 'http://localhost:8388/someTextFile.xml',
status: 200,
isXML: true,
type: 'xml',
contentLength: 999,
weightCheck: {
body: fileContent.toString('utf8'),
totalWeight: fileSize + 200,
headersSize: 200,
bodySize: fileSize,
isCompressed: false,
uncompressedSize: fileSize,
isOptimized: true
}
};
gzipCompressor.compressFile(entry)
.then(function(newEntry) {
newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize);
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
done();
})
.fail(function(err) {
done(err);
});
});
it('should gzip a JSON file', function(done) {
var fileContent = someTextFileContent; // it dosn't matter if it's not the correct file type
var fileSize = fileContent.length;
var entry = {
method: 'GET',
url: 'http://localhost:8388/someTextFile.json',
status: 200,
isJSON: true,
type: 'json',
contentLength: 999,
weightCheck: {
body: fileContent.toString('utf8'),
totalWeight: fileSize + 200,
headersSize: 200,
bodySize: fileSize,
isCompressed: false,
uncompressedSize: fileSize,
isOptimized: true
}
};
gzipCompressor.compressFile(entry)
.then(function(newEntry) {
newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize);
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
done();
})
.fail(function(err) {
done(err);
});
});
it('should gzip a TTF file', function(done) {
var fileContent = someTextFileContent; // it dosn't matter if it's not the correct file type
var fileSize = fileContent.length;
var entry = {
method: 'GET',
url: 'http://localhost:8388/someTextFile.ttf',
status: 200,
isWebFont: true,
isTTF: true,
type: 'webfont',
contentLength: 999,
weightCheck: {
body: fileContent.toString('utf8'),
totalWeight: fileSize + 200,
headersSize: 200,
bodySize: fileSize,
isCompressed: false,
uncompressedSize: fileSize,
isOptimized: true
}
};
gzipCompressor.compressFile(entry)
.then(function(newEntry) {
newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize);
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
done();
})
.fail(function(err) {
done(err);
});
});
it('should gzip a favicon file', function(done) {
var fileContent = someTextFileContent; // it dosn't matter if it's not the correct file type
var fileSize = fileContent.length;
var entry = {
method: 'GET',
url: 'http://localhost:8388/someTextFile.ico',
status: 200,
isFavicon: true,
type: 'favicon',
contentLength: 999,
weightCheck: {
body: fileContent.toString('utf8'),
totalWeight: fileSize + 200,
headersSize: 200,
bodySize: fileSize,
isCompressed: false,
uncompressedSize: fileSize,
isOptimized: true
}
};
gzipCompressor.compressFile(entry)
.then(function(newEntry) {
newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize);
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
done();
})
.fail(function(err) {
done(err);
});
});
it('should not gzip a JPEG file', function(done) {
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jpeg-image.jpg'));
var fileSize = fileContent.length;
var entry = {
method: 'GET',
url: 'http://localhost:8388/jpeg-image.jpg',
status: 200,
isImage: true,
type: 'image',
contentType: 'image/jpeg',
contentLength: 999,
weightCheck: {
body: fileContent.toString('utf8'),
totalWeight: fileSize + 200,
headersSize: 200,
bodySize: fileSize,
isCompressed: false,
uncompressedSize: fileSize,
isOptimized: true
}
};
gzipCompressor.compressFile(entry)
.then(function(newEntry) {
newEntry.weightCheck.should.not.have.a.property('afterCompression');
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
done();
})
.fail(function(err) {
done(err);
});
});
it('should not gzip a PNG file', function(done) {
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/png-image.png'));
var fileSize = fileContent.length;
var entry = {
method: 'GET',
url: 'http://localhost:8388/png-image.png',
status: 200,
isImage: true,
type: 'image',
contentType: 'image/png',
contentLength: 999,
weightCheck: {
body: fileContent.toString('utf8'),
totalWeight: fileSize + 200,
headersSize: 200,
bodySize: fileSize,
isCompressed: false,
uncompressedSize: fileSize,
isOptimized: true
}
};
gzipCompressor.compressFile(entry)
.then(function(newEntry) {
newEntry.weightCheck.should.not.have.a.property('afterCompression');
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
done();
})
.fail(function(err) {
done(err);
});
});
it('should not gzip a GIF file', function(done) {
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/png-image.png')); // Fake gif, don't tell anyone...
var fileSize = fileContent.length;
var entry = {
method: 'GET',
url: 'http://localhost:8388/gif-image.gif',
status: 200,
isImage: true,
type: 'image',
contentType: 'image/gif',
contentLength: 999,
weightCheck: {
body: fileContent.toString('utf8'),
totalWeight: fileSize + 200,
headersSize: 200,
bodySize: fileSize,
isCompressed: false,
uncompressedSize: fileSize,
isOptimized: true
}
};
gzipCompressor.compressFile(entry)
.then(function(newEntry) {
newEntry.weightCheck.should.not.have.a.property('afterCompression');
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
done();
})
.fail(function(err) {
done(err);
});
});
it('should not gzip a WEBP file', function(done) {
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/png-image.png')); // Fake webp, don't tell anyone...
var fileSize = fileContent.length;
var entry = {
method: 'GET',
url: 'http://localhost:8388/webp-image.webp',
status: 200,
isImage: true,
type: 'image',
contentType: 'image/webp',
contentLength: 999,
weightCheck: {
body: fileContent.toString('utf8'),
totalWeight: fileSize + 200,
headersSize: 200,
bodySize: fileSize,
isCompressed: false,
uncompressedSize: fileSize,
isOptimized: true
}
};
gzipCompressor.compressFile(entry)
.then(function(newEntry) {
newEntry.weightCheck.should.not.have.a.property('afterCompression');
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
done();
})
.fail(function(err) {
done(err);
});
});
});
+321
View File
@@ -0,0 +1,321 @@
var should = require('chai').should();
var imageOptimizer = require('../../lib/tools/weightChecker/imageOptimizer');
var fs = require('fs');
var path = require('path');
describe('imageOptimizer', function() {
it('should optimize a JPEG image losslessly', function(done) {
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jpeg-image.jpg'));
var fileSize = fileContent.length;
imageOptimizer.compressJpegLosslessly(fileContent).then(function(newFile) {
var newFileSize = newFile.contents.length;
newFileSize.should.be.below(fileSize);
done();
}).fail(function(err) {
done(err);
});
});
it('should optimize a JPEG image lossly', function(done) {
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jpeg-image.jpg'));
var fileSize = fileContent.length;
imageOptimizer.compressJpegLossly(fileContent).then(function(newFile) {
var newFileSize = newFile.contents.length;
newFileSize.should.be.below(fileSize);
done();
}).fail(function(err) {
done(err);
});
});
it('should find the best optimization for a jpeg', function(done) {
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jpeg-image.jpg'));
var fileSize = fileContent.length;
var entry = {
method: 'GET',
url: 'http://localhost:8388/an-image.jpg',
requestHeaders: {
'User-Agent': 'something',
Referer: 'http://www.google.fr/',
Accept: '*/*',
'Accept-Encoding': 'gzip, deflate'
},
status: 200,
isImage: true,
type: 'image',
contentType: 'image/jpeg',
contentLength: 999,
weightCheck: {
body: fileContent,
totalWeight: fileSize + 200,
headersSize: 200,
bodySize: fileSize,
isCompressed: false,
uncompressedSize: fileSize
}
};
imageOptimizer.optimizeImage(entry)
.then(function(newEntry) {
newEntry.weightCheck.should.have.a.property('isOptimized').that.equals(false);
newEntry.weightCheck.should.have.a.property('lossless').that.is.below(fileSize);
newEntry.weightCheck.should.have.a.property('lossy').that.is.below(newEntry.weightCheck.lossless);
done();
})
.fail(function(err) {
done(err);
});
});
it('should optimize a PNG image losslessly', function(done) {
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/png-image.png'));
var fileSize = fileContent.length;
imageOptimizer.compressPngLosslessly(fileContent).then(function(newFile) {
var newFileSize = newFile.contents.length;
newFileSize.should.be.below(fileSize);
done();
}).fail(function(err) {
done(err);
});
});
it('should fail to optimize an already optimized PNG', function(done) {
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/logo-large.png'));
var fileSize = fileContent.length;
imageOptimizer.compressPngLosslessly(fileContent).then(function(newFile) {
var newFileSize = newFile.contents.length;
newFileSize.should.equal(fileSize);
done();
}).fail(function(err) {
done(err);
});
});
it('should fail to optimize a non-PNG', function(done) {
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/svg-image.svg'));
var fileSize = fileContent.length;
imageOptimizer.compressPngLosslessly(fileContent).then(function(newFile) {
var newFileSize = newFile.contents.length;
newFileSize.should.equal(fileSize);
done();
}).fail(function(err) {
done(err);
});
});
it('should optimize a png', function(done) {
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/png-image.png'));
var fileSize = fileContent.length;
var entry = {
method: 'GET',
url: 'http://localhost:8388/an-image.png',
requestHeaders: {
'User-Agent': 'something',
Referer: 'http://www.google.fr/',
Accept: '*/*',
'Accept-Encoding': 'gzip, deflate'
},
status: 200,
isImage: true,
type: 'image',
contentType: 'image/png',
contentLength: 999,
weightCheck: {
body: fileContent,
totalWeight: fileSize + 200,
headersSize: 200,
bodySize: fileSize,
isCompressed: false,
uncompressedSize: fileSize
}
};
imageOptimizer.optimizeImage(entry)
.then(function(newEntry) {
newEntry.weightCheck.should.have.a.property('isOptimized').that.equals(false);
newEntry.weightCheck.should.have.a.property('lossless').that.is.below(fileSize);
done();
})
.fail(function(err) {
done(err);
});
});
it('should optimize an SVG image losslessly', function(done) {
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/svg-image.svg'));
var fileSize = fileContent.length;
imageOptimizer.compressSvgLosslessly(fileContent).then(function(newFile) {
var newFileSize = newFile.contents.length;
newFileSize.should.be.below(fileSize);
done();
}).fail(function(err) {
done(err);
});
});
it('should optimize an SVG', function(done) {
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/svg-image.svg'));
var fileSize = fileContent.length;
var entry = {
method: 'GET',
url: 'http://localhost:8388/an-image.svg',
requestHeaders: {
'User-Agent': 'something',
Referer: 'http://www.google.fr/',
Accept: '*/*',
'Accept-Encoding': 'gzip, deflate'
},
status: 200,
isImage: true,
isSVG: true,
type: 'image',
contentType: 'image/svg+xml',
contentLength: 999,
weightCheck: {
body: fileContent,
totalWeight: fileSize + 200,
headersSize: 200,
bodySize: fileSize,
isCompressed: false,
uncompressedSize: fileSize
}
};
imageOptimizer.optimizeImage(entry)
.then(function(newEntry) {
newEntry.weightCheck.should.have.a.property('isOptimized').that.equals(false);
newEntry.weightCheck.should.have.a.property('lossless').that.is.below(fileSize);
newEntry.weightCheck.should.have.a.property('optimized').that.equals(newEntry.weightCheck.lossless);
newEntry.weightCheck.should.have.a.property('bodyAfterOptimization').that.is.a('string');
done();
})
.fail(function(err) {
done(err);
});
});
it('shouldn\'t fail optimizing a corrupted jpeg', function(done) {
// In this test, we try to optimize a PNG but with a falsy "image/jpeg" content type
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/png-image.png'));
var fileSize = fileContent.length;
var entry = {
method: 'GET',
url: 'http://localhost:8388/an-image.png',
requestHeaders: {
'User-Agent': 'something',
Referer: 'http://www.google.fr/',
Accept: '*/*',
'Accept-Encoding': 'gzip, deflate'
},
status: 200,
isImage: true,
type: 'image',
contentType: 'image/jpeg',
contentLength: 999,
weightCheck: {
body: fileContent,
totalWeight: fileSize + 200,
headersSize: 200,
bodySize: fileSize,
isCompressed: false,
uncompressedSize: fileSize
}
};
imageOptimizer.optimizeImage(entry)
.then(function(newEntry) {
newEntry.weightCheck.should.not.have.a.property('isOptimized');
done();
})
.fail(function(err) {
done(err);
});
});
it('shouldn\'t fail optimizing a corrupted png', function(done) {
// In this test, we try to optimize a JPEG but with a falsy "image/png" content type
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jpeg-image.jpg'));
var fileSize = fileContent.length;
var entry = {
method: 'GET',
url: 'http://localhost:8388/an-image.jpg',
requestHeaders: {
'User-Agent': 'something',
Referer: 'http://www.google.fr/',
Accept: '*/*',
'Accept-Encoding': 'gzip, deflate'
},
status: 200,
isImage: true,
type: 'image',
contentType: 'image/png',
contentLength: 999,
weightCheck: {
body: fileContent,
totalWeight: fileSize + 200,
headersSize: 200,
bodySize: fileSize,
isCompressed: false,
uncompressedSize: fileSize
}
};
imageOptimizer.optimizeImage(entry)
.then(function(newEntry) {
newEntry.weightCheck.should.not.have.a.property('isOptimized');
done();
})
.fail(function(err) {
done(err);
});
});
it('should determine if gain is enough', function() {
imageOptimizer.gainIsEnough(20000, 10000).should.equal(true);
imageOptimizer.gainIsEnough(2000, 1000).should.equal(true);
imageOptimizer.gainIsEnough(20000, 21000).should.equal(false);
imageOptimizer.gainIsEnough(20000, 40000).should.equal(false);
imageOptimizer.gainIsEnough(20000, 19500).should.equal(false);
imageOptimizer.gainIsEnough(250, 120).should.equal(true);
imageOptimizer.gainIsEnough(200, 120).should.equal(false);
imageOptimizer.gainIsEnough(2000, 1900).should.equal(false);
imageOptimizer.gainIsEnough(200000, 197000).should.equal(true);
});
});

Some files were not shown because too many files have changed in this diff Show More