Compare commits

...
395 Commits
Author SHA1 Message Date
Gaël Métais 7b0f7d639a Fix current jQuery version 2018-03-03 02:33:23 -06:00
Gaël MétaisandGitHub 301766f4aa Merge pull request #264 from gmetais/develop
v1.13
2018-03-02 20:38:06 -06:00
Gaël Métais 6bdbfa2657 v1.13 2018-03-02 20:13:47 -06:00
Gaël MétaisandGitHub f5dafe6d43 Merge pull request #263 from gmetais/new-offsets
Score rules update
2018-03-02 20:11:12 -06:00
Gaël Métais 623f25e521 Update scores to match browsers perfs and new bandwidths 2018-03-02 20:00:31 -06:00
Gaël MétaisandGitHub a0e8a7f935 Update jQuery versions (#262) 2018-03-02 19:17:54 -06:00
Gaël Métais 61bd31e421 Fix google analytics trying to send events 2018-01-17 11:02:40 -06:00
Gaël Métais 84425e1319 Fix tests 2018-01-17 10:58:02 -06:00
Gaël MétaisandGitHub 09615f8ec4 Merge pull request #260 from gmetais/analytics
Don't load Google Analytics when not needed
2018-01-16 21:24:05 -06:00
Gaël Métais 3d368482bb Don't load analytics.js when not needed 2018-01-16 21:21:56 -06:00
Gaël MétaisandGitHub df2cec5a94 Merge pull request #258 from gmetais/remove-coverage
Remove Blanket coverage testing
2018-01-16 21:10:32 -06:00
Gaël Métais 369512c6c1 Remove Blanket coverage testing 2018-01-16 21:06:18 -06:00
Gaël Métais 10a4cc270a v1.12.4 2018-01-16 20:41:04 -06:00
Gaël Métais 1eda5e5860 Merge branch 'master' into develop 2018-01-16 20:37:56 -06:00
Paul HooijengaandGaël Métais 3fc0d9418f Use jimp instead of lwip for image processing
Jimp does not install on recent versions of NodeJS because of changes
in the bundled version of zlib. Jimp is a pure JS replacement.
2018-01-16 20:17:34 -06:00
Gaël MétaisandGitHub c4f243f4b0 Large dependencies update (#257)
* Massive dependecy update
2018-01-16 20:16:03 -06:00
Gaël Métais e057d740d8 Merge branch 'develop' of https://github.com/gmetais/YellowLabTools into develop 2018-01-16 15:07:44 -06:00
Catherine SnowandGaël Métais a20c5677b4 Fix typos 2017-12-04 17:19:32 -06:00
Gaël Métais 7f687051f8 Merge v1.12.3 into master 2017-05-28 10:48:46 +01:00
Gaël Métais 450c5c9829 v1.12.3 2017-05-28 09:35:45 +01:00
Gaël Métais 0ab019e5ab Remove special char from XML 2017-05-28 09:33:52 +01:00
Gaël MétaisandGaël Métais 40b3fa91ac v1.12.2 2017-05-27 23:25:59 +01:00
Gaël Métais f59c8da000 Ignore unused fonts (#234) 2017-05-27 23:25:59 +01:00
Gaël Métais 3555d01b68 Add file names to duplicated css selectors offenders (#233) 2017-05-27 23:25:59 +01:00
Gaël Métais b201b32b46 Hiddenchars (#221)
* Fix bug where an icons font is counted as two unused unicode ranges
* Display the number of hidden chars
2017-05-27 23:25:59 +01:00
Gaël Métais 89f79e0977 v1.12.2 2017-05-27 23:17:24 +01:00
Gaël MétaisandGitHub aa0233f49b Ignore unused fonts (#234) 2017-05-27 23:14:20 +01:00
Gaël MétaisandGitHub df4a45afb2 Add file names to duplicated css selectors offenders (#233) 2017-05-27 18:18:09 +01:00
Gaël Métais def0144dab Fix error when no url is provided 2017-01-18 12:16:26 +07:00
Gaël MétaisandGitHub ce16fa154c Hiddenchars (#221)
* Fix bug where an icons font is counted as two unused unicode ranges
* Display the number of hidden chars
2017-01-10 13:54:43 +08:00
Gaël MétaisandGitHub 063a21997b Merge pull request #220 from gmetais/develop
v1.12.1
2017-01-08 22:49:05 +08:00
Gaël Métais 92fce802c5 v1.12.1 2017-01-08 22:40:09 +08:00
Gaël MétaisandGitHub f76d597cbf Add a way to block some urls to avoid spam (#219)
Spam blocker
2017-01-08 22:38:34 +08:00
Gaël Métais ecbbbc2d6b Fix doughnut graph width in weight rule 2016-12-30 23:53:55 +08:00
Gaël MétaisandGitHub 2bb00a177e Merge pull request #218 from gmetais/develop
v1.12.0
2016-12-30 23:00:54 +08:00
Gaël Métais e13b2d4edc v1.12.0 2016-12-30 22:48:28 +08:00
Gaël Métais bb3a94eb84 Use a synced folder in Vagrant 2016-12-30 22:17:29 +08:00
Gaël Métais 49b460eee3 Update the vagrant box and Node version 2016-12-30 22:14:30 +08:00
Gaël Métais 28da1a0ec9 Update other dependencies 2016-12-30 11:36:28 +08:00
Gaël Métais 6176e6ff40 Update Angular to 1.6.1 2016-12-30 11:26:24 +08:00
Gaël Métais 4406287a5f Upgrade Chart.js to version 2 2016-12-29 23:40:55 +08:00
Gaël Métais 3f63f289ed Update to Grunt v1.0 2016-12-29 22:30:12 +08:00
Gaël Métais 5c07dd3023 Merge branch 'master' into develop 2016-12-29 22:23:16 +08:00
Gaël Métais fe71591200 Fix bug with blocked requests not being displayed 2016-12-29 19:45:40 +08:00
Gaël Métais 3f87e23bd7 Add domain blocking option to the CLI 2016-12-29 19:45:11 +08:00
Gaël Métais 9d2d99b5ba Fix still failing test 2016-12-29 19:18:41 +08:00
Gaël Métais 76df43fe66 Fix tests 2016-12-29 19:07:54 +08:00
Gaël Métais 0d72f6b987 Fix remaining issues with subfolder hosting 2016-12-29 18:57:03 +08:00
Gaël Métais 507b6b9133 Remove test files 2016-12-29 10:36:59 +08:00
Gaël MétaisandGitHub 4911d61d78 Fix invalid XML from the CLI xml reporter (#217)
* Remove javascriptExecutionTree from the xml output
* Change JSON structure for offenders classified by url
* Remove special chars from XML output
2016-12-28 23:51:18 +08:00
Gaël MétaisandGitHub bdf5f82959 Update Phantomas to 1.18 and add support for multiple cookies (#216) 2016-12-27 23:05:40 +08:00
Gaël Métais 86310083f2 Fix current settings list when closed 2016-12-27 21:14:32 +08:00
Gaël Métais 29faa4932d Add tested page url in server log 2016-12-22 14:38:10 +08:00
Gaël Métais f498bdabb3 Accept a URL parameter on the home page 2016-12-21 00:38:06 +08:00
Gaël Métais f4a1f06ba6 Increase test timeout because it sometimes fails in Travis 2016-12-20 22:39:32 +08:00
Gaël MétaisandGitHub 317b0fc4d7 Add proxy to the setting (#213) 2016-12-20 22:37:20 +08:00
Gaël Métais 5d081f27d1 Fix jQuery tests 2016-12-20 16:31:13 +08:00
Gaël Métais 9ca126f3cd Add an offset to Below the fold images 2016-12-20 16:28:49 +08:00
Gaël Métais 736d447d9e Update jQuery to 3.1 2016-12-20 16:17:01 +08:00
Gaël Métais f2f27ac639 Removing the jQuery Usage rule 2016-12-20 15:20:16 +08:00
Gaël Métais 90f6fd83ba Fix tests after waitForResponse default change 2016-12-20 15:03:23 +08:00
Gaël Métais d9e9465415 Merge branch 'develop' into develop 2016-12-20 14:39:10 +08:00
Gaël Métais f73fc49ee4 waitForResponse is now false by default in the Rest API 2016-12-20 14:36:54 +08:00
Gaël MétaisandGitHub afe448647c Update dependencies (#210)
Update dependencies
2016-12-20 14:01:10 +08:00
Gaël Métais 091242bedc Merge branch 'develop' into develop 2016-12-20 12:22:33 +08:00
Gaël Métais 8d4f76156b Webfonts analyzis (#209)
Add 3 new rules about webfonts
2016-12-20 12:21:21 +08:00
Gaël Métais 50eacba438 Fix Error 1001: JavaScript profiling failed (#197)
Fix for Error 1001: JavaScript profiling failed
2016-12-20 12:21:20 +08:00
Gaël MétaisandGitHub 5273dfbbf3 Webfonts analyzis (#209)
Add 3 new rules about webfonts
2016-12-20 12:16:28 +08:00
Gaël Métais db5632d3ba Fix a phantomas offender parsing problem with spaces 2016-11-13 21:05:46 +08:00
Gaël MétaisandGitHub 9898a8424a Fix Error 1001: JavaScript profiling failed (#197)
Fix for Error 1001: JavaScript profiling failed
2016-10-06 10:59:29 +08:00
Gaël Métais 0f354cfe9b Clarify cookies 2016-09-21 14:32:45 +08:00
Gaël MétaisandGitHub b433736241 Fix bug with EJS on grunt-built instances (#192)
Fix EJS syntax being incorrectly escaped by the build step.
2016-09-12 16:17:01 +08:00
Julien RamelandGaël Métais cee48c1f45 Adding subfolder support (#186)
Adding EJS Templating engine + a baseUrl variable injected in front-end.
2016-08-18 15:18:27 +08:00
Gaël Métais 6d1ea96e79 Merge branch 'develop' 2016-07-28 20:18:22 +08:00
Gaël Métais 5053f3f0f8 Authorship 2016-07-28 20:18:01 +08:00
Gaël Métais 7b16c4ebe1 Merge branch 'develop' 2016-07-27 12:59:02 +08:00
Gaël Métais b8636029c9 Personnal website 2016-07-27 12:58:10 +08:00
Gaël MétaisandGitHub 357158db3e Fix a bug when analyzing an SVG with a syntax error (#184) 2016-07-15 11:45:24 +08:00
Gaël Métais 796ee5516f Fix a bug when analyzing an SVG with a syntax error 2016-07-15 11:36:31 +08:00
Gaël MétaisandGitHub c82c1c192f Merge pull request #183 from gmetais/develop
v1.11.0
2016-07-11 23:55:09 +08:00
Gaël Métais c20cacac99 Redownload files with the keep-alive option 2016-07-11 23:24:14 +08:00
Gaël Métais decfc5823d Releasing v1.11.0 2016-07-11 18:18:32 +08:00
Gaël MétaisandGitHub 6da6377bb5 New rule "Identical content" (#181) 2016-07-11 17:01:24 +08:00
Gaël MétaisandGitHub eedb46e000 New rule called "Empty requests" (#180) 2016-07-11 15:46:00 +08:00
Gaël MétaisandGitHub fe8466b0e3 Improve content type detection (#179) 2016-07-11 00:39:21 +08:00
Gaël Métais f10fbc3fa3 Sometimes the lossy JPEG weights more than the lossless 2016-07-10 16:00:52 +08:00
Gaël Métais e186620a60 Now that Uglify-JS is faster with already minified files... 2016-07-10 15:15:45 +08:00
Gaël MétaisandGitHub e1316ea48b Add a new rule for Synchronous XMLHttpRequest (#178)
* Fix bug in spying function
* New rule Synchronous XHR
* Add the synchronousXHR rule to the dashboard
2016-07-10 15:11:34 +08:00
Gaël Métais a9adb3bb06 Disable Angular debugging, should improve perfs 2016-07-10 10:41:56 +08:00
Gaël MétaisandGitHub 5ccab1770b Fix JS backtrace parsing since PhantomJS 2.x (#177)
Fixes #177.
2016-07-10 10:24:51 +08:00
Gaël MétaisandGitHub 3d377b7b1f Reduce the weight of the breakpoints count rule (#176) 2016-07-10 08:50:48 +08:00
Gaël MétaisandGitHub 8bdb578b90 Merge pull request #175 from gmetais/universal
Fixes #155.
2016-07-10 08:14:59 +08:00
Gaël Métais 50a9ea628d Remove 2 rules related to CSS (#155) 2016-07-09 23:29:02 +08:00
Gaël MétaisandGitHub 637ce5c1bc Merge pull request #174 from gmetais/massive-update
Dependencies updated
2016-07-08 13:42:06 +08:00
Gaël Métais 7432f4de08 Check JPEG compression with progressive flag by default 2016-07-08 13:11:05 +08:00
Gaël Métais 594267b936 Update dependencies 2016-07-08 13:06:51 +08:00
Gaël MétaisandGitHub e210eca290 Merge pull request #173 from gmetais/update-jquery
Update scores for jQuery 3.0
2016-07-02 15:11:07 +08:00
Gaël Métais 189ce3db64 jQuery 3.0 2016-07-02 15:01:25 +08:00
Gaël Métais 07028ad3cd Redundant tags selectors description mistake 2016-06-15 23:27:35 +02:00
Gaël Métais 0d71781071 Should fix some 254 errors with self-signed certificates 2016-06-15 16:15:50 +02:00
Gaël Métais 8e72efdd96 v1.10.2 2016-05-21 09:23:13 +02:00
Gaël Métais e0a656e2ba Merge branch 'master' into develop 2016-05-21 09:21:38 +02:00
Gaël Métais 2afab43281 Merge pull request #160 from gmetais/fix-node6
Update lwip dependency
2016-05-19 16:34:45 +02:00
Gaël Métais d1a09260d5 Travis test Node v6.2 2016-05-19 16:21:43 +02:00
Gaël Métais 926db1c2b0 Update contributors list 2016-05-19 16:18:50 +02:00
Gaël Métais 12dd676e9d Merge pull request #158 from magikcypress/master
Thank you @magikcypress !
2016-05-19 16:17:33 +02:00
Gaël Métais dac72eec20 Fix an installation bug with Node v6 2016-05-19 16:03:33 +02:00
Vincent L 5e4d3c40f5 Fix pull request #158
https://github.com/gmetais/YellowLabTools/pull/158#issuecomment-220123344
2016-05-18 23:01:01 +02:00
Vincent L 990237f975 Update apiController.js 2016-05-18 22:54:56 +02:00
Vincent L 4e29d5cbea Update apiController.js
Fix bad request with curl

$ curl -X POST --data "url=perdu.com" http://localhost:8383/api/runs/
Bad request

$ curl -X POST --data "url=perdu.com" http://localhost:8383/api/runs/
Moved Temporarily. Redirecting to /api/results/eesudz6s7n
2016-05-18 20:20:09 +02:00
Gaël Métais 9d58dc20cb Merge pull request #152 from Webelys/stdin-warning
Improve Vagrant install
2016-05-06 10:51:58 +02:00
cam.lafit b6bf77956b Prevent dpkg-preconfigure: unable to re-open stdin warning
* Follow http://www.liviubalan.com/vagrant-repair-default-dpkg-preconfigure-unable-to-re-open-stdin-no-such-file-or-directory-message trick
* Move error message to /dev/null
2016-05-06 10:18:58 +02:00
cam.lafit ae9e520df1 Remove some stdin warning
* Ubuntu by default prevent messages being written to root's console
* Disallow this annoying error
2016-05-06 10:14:42 +02:00
cam.lafit ae65c1af3d 512Mo is not enough
* Add more memory to vbox
2016-05-06 09:59:10 +02:00
cam.lafit 6f892c7dbf Prefer official node repository
* Took from https://github.com/nodesource/distributions
** We need 0.12 version
** chris-lea PPA is an obsolete repository
2016-05-06 09:51:27 +02:00
cam.lafit 0c4fad6f54 Get official ubuntu box
* Is not needed to set url box, we can get directly box from https://atlas.hashicorp.com/ubuntu
2016-05-06 09:48:41 +02:00
Gaël Métais 1a9b749017 Merge pull request #149 from gmetais/css-offenders
Change the way offenders are displayed for some CSS related rules
2016-03-27 21:53:21 +03:00
Gaël Métais aee81cb824 Change the way offenders are displayed for some CSS related rules 2016-03-27 21:24:27 +03:00
Gaël Métais 6d6608754d Merge pull request #148 from gmetais/hidden-pixels
Merged
2016-03-27 17:47:53 +03:00
Gaël Métais 326bfc035f Update rule explaination 2016-03-27 17:35:01 +03:00
Gaël Métais 3ff23e1c47 Ignore 1x1 images in hiddenImages rule 2016-03-27 17:18:28 +03:00
Gaël Métais ada82fa7e1 Should fix uglify infinite loop on already uglyfied files that crashes YLT 2016-03-27 17:09:23 +03:00
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
132 changed files with 10154 additions and 1617 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/*
+16 -4
View File
@@ -1,7 +1,19 @@
language: node_js
sudo: false
node_js:
- "0.10.33"
- "8.9"
- "6.2"
- "4.7"
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
- "npm install -g npm"
- "npm install -g grunt-cli"
install:
- "npm install"
+13 -88
View File
@@ -80,27 +80,11 @@ module.exports = function(grunt) {
dev: {
src: ['front/src/css']
},
coverage: {
src: ['.tmp', 'coverage/']
},
build: {
src: ['front/build']
}
},
copy: {
beforeCoverage: {
files: [
{src: ['bin/server.js'], dest: '.tmp/'}
]
},
coverage: {
files: [
{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/img/favicon.png'], dest: './front/build/img/favicon.png'},
@@ -108,50 +92,18 @@ module.exports = function(grunt) {
]
}
},
lineremover: {
beforeCoverage: {
files: {
'.tmp/bin/cli.js': 'bin/cli.js'
},
options: {
exclusionPattern: /#!\/usr\/bin\/env node/
}
}
},
blanket: {
coverageApp: {
src: ['app/'],
dest: 'coverage/app/'
},
coverageLib: {
src: ['lib/'],
dest: 'coverage/lib/'
},
coverageBin: {
src: ['.tmp/bin/'],
dest: 'coverage/bin/'
}
},
mochaTest: {
test: {
options: {
reporter: 'spec',
},
src: ['coverage/test/core/*.js', 'coverage/test/api/*.js']
src: ['test/core/*.js', 'test/api/*.js']
},
'test-current-work': {
options: {
reporter: 'spec',
},
src: ['test/core/offendersHelpersTest.js']
},
coverage: {
options: {
reporter: 'html-cov',
quiet: true,
captureFile: 'coverage/coverage.html'
},
src: ['coverage/test/core/*.js', 'coverage/test/api/*.js']
src: ['test/api/apiTest.js']
}
},
env: {
@@ -182,7 +134,7 @@ module.exports = function(grunt) {
test: {
options: {
port: 8387,
server: './coverage/bin/server.js',
server: './bin/server.js',
showStack: true
}
},
@@ -243,7 +195,11 @@ module.exports = function(grunt) {
build: {
options: {
base: '.tmp',
method: 'append'
method: 'append',
unescape: {
'&lt;': '<',
'&gt;': '>'
}
},
files: {
'./front/build/main.html': ['.tmp/views/*.html']
@@ -261,35 +217,11 @@ module.exports = function(grunt) {
}
});
// Custom task: copies the test settings.json file to the coverage folder, and checks if there's no missing fields
grunt.registerTask('copy-test-server-settings', function() {
var mainSettingsFile = './server_config/settings.json';
var testSettingsFile = './test/fixtures/settings.json';
var mainSettings = grunt.file.readJSON(mainSettingsFile);
var testSettings = grunt.file.readJSON(testSettingsFile);
// Recursively compare keys of two objects (not the values)
function compareKeys(original, copy, context) {
for (var key in original) {
if (!copy[key] && copy[key] !== '' && copy[key] !== 0) {
grunt.fail.warn('Settings file ' + testSettingsFile + ' doesn\'t contain key ' + context + '.' + key);
}
if (original[key] !== null && typeof original[key] === 'object') {
compareKeys(original[key], copy[key], context + '.' + key);
}
}
}
compareKeys(mainSettings, testSettings, 'settings');
var outputFile = './coverage/server_config/settings.json';
grunt.file.write(outputFile, JSON.stringify(testSettings, null, 4));
grunt.log.ok('File ' + outputFile + ' created');
// Custom task that sets a variable for tests
grunt.registerTask('test-settings', function() {
process.env.IS_TEST = true;
});
grunt.registerTask('icons', [
'webfont:icons',
'less',
@@ -329,25 +261,18 @@ module.exports = function(grunt) {
]);
grunt.registerTask('test', [
'test-settings',
'build',
'express:testSuite',
'clean:coverage',
'copy-test-server-settings',
'lineremover:beforeCoverage',
'copy:beforeCoverage',
'blanket',
'copy:coverage',
'express:test',
'mochaTest:test',
'mochaTest:coverage',
'clean:tmp'
]);
grunt.registerTask('test-current-work', [
'test-settings',
'jshint',
'express:testSuite',
'clean:coverage',
'copy-test-server-settings',
'express:test-current-work',
'mochaTest:test-current-work',
'clean:tmp'
+62 -33
View File
@@ -1,39 +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 [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 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).
#### The Grunt task:
If you have a Continuous Integration platform, may I suggest you to use [grunt-yellowlabtools](https://github.com/gmetais/grunt-yellowlabtools)? You set it up once, then the task fails if the front-end quality conditions you defined are not met.
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
@@ -41,20 +75,15 @@ If you have a Continuous Integration platform, may I suggest you to use [grunt-y
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!
I can also help your company about Web Performances, visit [my website](https://www.gaelmetais.com).
## Contributors
- Achraf Ben Younes [achrafbenyounes](https://github.com/achrafbenyounes)
- [camlafit](https://github.com/camlafit)
- Vincent L. [magikcypress](https://github.com/magikcypress)
- Ousama Ben Younes [ousamabenyounes](https://github.com/ousamabenyounes)
Vendored
+11 -4
View File
@@ -1,17 +1,24 @@
Vagrant.configure("2") do |config|
config.vm.box = "precise64"
config.vm.box_url = "http://files.vagrantup.com/precise64.box"
config.vm.box = "ubuntu/trusty64"
config.vm.network :private_network, ip: "10.10.10.10"
config.ssh.forward_agent = true
# http://foo-o-rama.com/vagrant--stdin-is-not-a-tty--fix.html
config.vm.provision "fix-no-tty", type: "shell" do |s|
s.privileged = false
s.inline = "sudo sed -i '/tty/!s/mesg n/tty -s \\&\\& mesg n/' /root/.profile"
end
config.vm.provider :virtualbox do |vb|
vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
vb.customize ["modifyvm", :id, "--memory", 512]
vb.customize ["modifyvm", :id, "--memory", 1024]
vb.customize ["modifyvm", :id, "--cpus", 2]
end
config.vm.synced_folder "./", "/space/YellowLabTools"
config.vm.provision :shell, :path => "server_config/server_install.sh"
end
end
+67 -13
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: [
@@ -14,7 +15,15 @@ var cli = meow({
'Options:',
' --device Use "phone" or "tablet" to simulate a mobile device (by user-agent and viewport size).',
' --screenshot Will take a screenshot and use this value as the output path. It needs to end with ".png".',
' --js-deep-analysis When activated, the javascriptExecutionTree will contain sub-requests.',
//' --wait-for-selector Once the page is loaded, Phantomas will wait until the given CSS selector matches some elements.',
' --proxy Sets an HTTP proxy to pass through. Syntax is "host:port".',
' --cookie Adds a cookie on the main domain.',
' --auth-user Basic HTTP authentication username.',
' --auth-pass Basic HTTP authentication password.',
' --block-domain Disallow requests to given (comma-separated) domains - aka blacklist.',
' --allow-domain Only allow requests to given (comma-separated) domains - aka whitelist.',
' --no-externals Block all domains except the main one.',
' --reporter The output format: "json" or "xml". Default is "json".',
''
].join('\n'),
pkg: '../package.json'
@@ -24,8 +33,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];
@@ -46,14 +54,33 @@ if (screenshot) {
options.screenshot = cli.flags.screenshot;
}
// Deep JS analysis option
if (cli.flags.jsDeepAnalysis === true || cli.flags.jsDeepAnalysis === 'true') {
options.jsDeepAnalysis = true;
}
// Device simulation
options.device = cli.flags.device || 'desktop';
// Wait for CSS selector
options.waitForSelector = cli.flags.waitForSelector || null;
// Proxy
options.proxy = cli.flags.proxy || null;
// Cookie
options.cookie = cli.flags.cookie || null;
// HTTP basic auth
options.authUser = cli.flags.authUser || null;
options.authPass = cli.flags.authPass || null;
// Domain blocking
options.blockDomain = cli.flags.blockDomain || null;
options.allowDomain = cli.flags.allowDomain || null;
options.noExternals = cli.flags.noExternals || 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) {
'use strict';
@@ -63,7 +90,34 @@ options.device = cli.flags.device || 'desktop';
then(function(data) {
debug('Success');
console.log(JSON.stringify(data, null, 2));
switch(cli.flags.reporter) {
case 'xml':
var serializer = new EasyXml({
manifest: true
});
// Remove some heavy parts of the results object
delete data.toolsResults;
delete data.javascriptExecutionTree;
var xmlOutput = serializer.render(data);
// Remove special chars from XML tags: # [ ]
xmlOutput = xmlOutput.replace(/<([^>]*)#([^>]*)>/g, '<$1>');
xmlOutput = xmlOutput.replace(/<([^>]*)\[([^>]*)>/g, '<$1>');
xmlOutput = xmlOutput.replace(/<([^>]*)\]([^>]*)>/g, '<$1>');
// Remove special chars from text content: \n \0
xmlOutput = xmlOutput.replace(/(<[a-zA-Z]*>[^<]*)\n([^<]*<\/[a-zA-Z]*>)/g, '$1$2');
xmlOutput = xmlOutput.replace(/\0/g, '');
xmlOutput = xmlOutput.replace(/\uFFFF/g, '');
xmlOutput = xmlOutput.replace(/\u0002/g, '');
console.log(xmlOutput);
break;
default:
console.log(JSON.stringify(data, null, 2));
}
}).fail(function(err) {
+12 -1
View File
@@ -7,14 +7,22 @@ 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);
app.use(bodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded
// EJS HTML engine
app.engine('.html', require('ejs').__express);
app.set('view engine', 'ejs');
// Initialize the controllers
@@ -25,6 +33,9 @@ var frontController = require('../lib/server/controllers/frontController
// Let's start the server!
if (!process.env.GRUNTED) {
var settings = require('../server_config/settings.json');
app.locals.baseUrl = settings.baseUrl;
server.listen(settings.serverPort, function() {
console.log('Listening on port %d', server.address().port);
@@ -35,4 +46,4 @@ if (!process.env.GRUNTED) {
});
}
module.exports = app;
module.exports = app;
-14
View File
@@ -1,14 +0,0 @@
{
"name": "yellowlabtools",
"dependencies": {
"angular": "~1.3.15",
"angular-route": "~1.3.15",
"angular-resource": "~1.3.15",
"angular-sanitize": "~1.3.15",
"angular-animate": "~1.3.15",
"angular-local-storage": "~0.1.5"
},
"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

+7 -3
View File
@@ -109,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
+94 -3
View File
@@ -13,11 +13,11 @@
width: 50%;
}
.launchBtn {
background: #e74c3c;
background: #ffa319;
color: #fff;
}
.launchBtn:focus {
background: #ffa319;
background: #e74c3c;
}
.launchBtn.disabled {
background: #deaca6;
@@ -25,6 +25,20 @@
.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;
}
@@ -53,10 +67,87 @@
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;
}
+16 -9
View File
File diff suppressed because one or more lines are too long
+93 -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;
@@ -111,29 +126,41 @@
border-top: 1px solid #999;
z-index: 2;
}
.offenders .offenderButton .domTree {
.offendersTable .offenderButton .domTree,
.value .offenderButton .domTree {
text-align: left;
white-space: nowrap;
}
.offenders .offenderButton .domTree > div {
.offendersTable .offenderButton .domTree > div,
.value .offenderButton .domTree > div {
margin: 0.5em;
}
.offenders .offenderButton .domTree > div div {
.offendersTable .offenderButton .domTree > div div,
.value .offenderButton .domTree > div div {
margin-left: 1em;
}
.offenders .offenderButton .backtrace,
.offenders .offenderButton .cssFileAndLine {
.offendersTable .offenderButton .backtrace,
.value .offenderButton .backtrace,
.offendersTable .offenderButton .cssFileAndLine,
.value .offenderButton .cssFileAndLine {
white-space: nowrap;
padding: 0.5em;
}
.offenders .offenderButton.opens:hover {
.offendersTable .offenderButton.opens:hover,
.value .offenderButton.opens:hover {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
background: #ffe0cc;
z-index: 2;
}
.offenders .offenderButton.opens:hover > div {
.offendersTable .offenderButton.opens:hover > div,
.value .offenderButton.opens:hover > div {
display: block;
background: #ffe0cc;
}
.offendersTable .smallerOffenders,
.value .smallerOffenders {
font-size: 0.9em;
}
.offendersHtml {
display: inline-block;
@@ -149,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;
@@ -183,7 +212,6 @@
.colorPalette > div:hover:after {
content: " ";
position: absolute;
background-color: inherit;
left: -0.2em;
top: -0.2em;
width: 100%;
@@ -191,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: 20em;
margin: 2em auto 4em;
}
.totalWeightPie canvas {
max-width: inherit;
}
.offenderProblem {
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;
}
+43 -19
View File
@@ -62,34 +62,34 @@
background: #F04DA7;
}
.timeline .domComplete.interval {
background: #ede3ff;
background: #EDE3FF;
}
.timeline .domComplete .color {
background: #c2a3ff;
background: #C2A3FF;
}
.timeline .domContentLoadedEnd.interval {
background: #d8f0f0;
background: #D8F0F0;
}
.timeline .domContentLoadedEnd .color {
background: #7ecccc;
background: #7ECCCC;
}
.timeline .domContentLoaded.interval {
background: #e0ffd1;
background: #E0FFD1;
}
.timeline .domContentLoaded .color {
background: #a7e846;
background: #A7E846;
}
.timeline .domInteractive.interval {
background: #fffccc;
background: #FFFCCC;
}
.timeline .domInteractive .color {
background: #ffe433;
background: #FFE433;
}
.timeline .domCreation.interval {
background: #ffe0cc;
background: #FFE0CC;
}
.timeline .domCreation .color {
background: #ff6600;
background: #FF6600;
}
.timeline .tooltip.detailsOverlay {
position: absolute;
@@ -143,19 +143,25 @@
border: 1px dotted #aaa;
text-align: left;
}
.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;
@@ -288,27 +294,45 @@
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;
}
.warningsFilterOn > div {
.queryWithoutResultsFilterOn > div {
display: none;
}
.warningsFilterOn > div.warning {
.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;
}
+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></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.5 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></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.0 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></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.2 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></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.1 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></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 -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></g><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>
<?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>

Before

Width:  |  Height:  |  Size: 3.2 KiB

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></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.6 KiB

After

Width:  |  Height:  |  Size: 1.2 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></g><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>
<?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>

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 584 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></g><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>
<?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>

Before

Width:  |  Height:  |  Size: 3.2 KiB

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></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.3 KiB

After

Width:  |  Height:  |  Size: 887 B

+24 -1
View File
@@ -19,11 +19,30 @@ var yltApp = angular.module('YellowLabTools', [
]);
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()});
if (ga) {
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';
});
}]);
@@ -65,3 +84,7 @@ yltApp.config(['$routeProvider', '$locationProvider',
}
]);
// Disable debugging https://docs.angularjs.org/guide/production
yltApp.config(['$compileProvider', function ($compileProvider) {
$compileProvider.debugInfoEnabled(false);
}]);
+6 -9
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.showRulePage = function(ruleName) {
$location.path('/result/' + $scope.runId + '/rule/' + ruleName);
};
$scope.tweetText = 'I\'ve discovered this cool open-source tool that audits the front-end quality of a web page: ';
}
$scope.testAgain = function() {
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) {
+11 -2
View File
@@ -1,14 +1,23 @@
var indexCtrl = angular.module('indexCtrl', []);
indexCtrl.controller('IndexCtrl', ['$scope', 'Settings', 'API', function($scope, Settings, API) {
indexCtrl.controller('IndexCtrl', ['$scope', '$routeParams', '$location', 'Settings', 'API', function($scope, $routeParams, $location, Settings, API) {
$scope.settings = Settings.getMergedSettings();
console.log($scope.settings);
$scope.launchTest = function() {
if ($scope.url) {
$location.search('url', null);
$location.search('run', null);
Settings.saveSettings($scope.settings);
API.launchTest($scope.url, $scope.settings);
}
};
// Auto fill URL field and auto launch test when the good params are set in the URL
if ($routeParams.url) {
$scope.url = $routeParams.url;
if ($routeParams.run === 'true' || $routeParams.run === 1 || $routeParams.run === '1') {
$scope.launchTest();
}
}
}]);
+106 -2
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,101 @@ 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 = {
tooltips: {
callbacks: {
label: function(tooltipItem, data) {
var label = data.labels[tooltipItem.index];
var value = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index];
return label + ': ' + value + ' KB';
}
}
},
legend: {
display: true,
position: 'bottom',
labels: {
boxWidth: 12,
fontSize: 14
}
}
};
}
// 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 = {
scales: {
xAxes: [{
gridLines: {
display:false
}
}],
yAxes: [{
gridLines: {
display:false
}
}]
},
tooltips: {
enabled: false
},
elements: {
point: {
radius: 0
}
}
};
}
}
$scope.backToDashboard = function() {
+1 -1
View File
@@ -7,7 +7,7 @@ screenshotCtrl.controller('ScreenshotCtrl', ['$scope', '$rootScope', '$routePara
function loadResults() {
// Load result if needed
if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
Results.get({runId: $routeParams.runId}, function(result) {
Results.get({runId: $routeParams.runId, exclude: 'toolsResults'}, function(result) {
$rootScope.loadedResult = result;
$scope.result = result;
}, function(err) {
+25 -7
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,26 +19,44 @@ 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.jsCount.offendersObj.list;
var offenders = $scope.result.rules.totalRequests.offendersObj.list.byType.js;
$scope.scripts = [];
offenders.forEach(function(script) {
var filePath = script.file;
offenders.forEach(function(filePath) {
var shortPath = filePath;
if (filePath.length > 100) {
filePath = filePath.substr(0, 98) + '...';
shortPath = filePath.substr(0, 98) + '...';
}
var scriptObj = {
fullPath: script.file,
shortPath: filePath
fullPath: filePath,
shortPath: shortPath
};
$scope.scripts.push(scriptObj);
+126 -18
View File
@@ -109,7 +109,7 @@
} else if (context.length > 3) {
html += ' and ' + (context.length - 2) + ' more...';
}
return html + '}';
return html + ')';
}
function isJQuery(node) {
@@ -119,6 +119,22 @@
function getNonJQueryHTML(node, onASingleLine) {
var type = node.data.type;
if (node.windowPerformance) {
switch (type) {
case 'documentScroll':
return '(triggering the scroll event on <b>document</b>)';
case 'windowScroll':
return '(triggering the scroll event on <b>window</b>)';
case 'window.onscroll':
return '(calling the <b>window.onscroll</b> function)';
default:
return '';
}
}
if (!node.data.callDetails) {
return '';
}
@@ -153,6 +169,18 @@
case 'error':
return args[0];
case 'jQuery - onDOMReady':
return '(function)';
case 'documentScroll':
return 'The scroll event just triggered on document';
case 'windowScroll':
return 'The scroll event just triggered on window';
case 'window.onscroll':
return 'The window.onscroll function just got called';
default:
return '';
}
@@ -318,7 +346,9 @@
case 'jQuery - on':
case 'jQuery - one':
if (args[1]) {
if (isStringOfObject(args[0])) {
return '<b>' + args[0].replace(/&quot;\(function\)&quot;/g, '(function)') + '</b>';
} else if (args[1] && isPureString(args[1])) {
return 'bind <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[1] + '</b>';
} else {
return 'bind <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
@@ -518,9 +548,6 @@
}
break;
case 'jQuery - onDOMReady':
return '(function)';
default:
return '';
}
@@ -581,7 +608,12 @@
for (var i = 0 ; i < parsedBacktrace.length ; i++) {
html += '<div>';
html += '<div>' + (parsedBacktrace[i].fnName || '(anonymous function)') + '</div>';
html += '<div class="trace">' + getUrlLink(parsedBacktrace[i].filePath, 40) + ':' + parsedBacktrace[i].line + '</div>';
html += '<div class="trace">' + getUrlLink(parsedBacktrace[i].filePath, 40) + '</div>';
if (parsedBacktrace[i].column) {
html += '<div>' + parsedBacktrace[i].line + ':' + parsedBacktrace[i].column + '</div>';
} else {
html += '<div>line ' + parsedBacktrace[i].line + '</div>';
}
html += '</div>';
}
}
@@ -603,6 +635,22 @@
var fnName = null, fileAndLine;
var withFnResult = /^([^\s\(]+) \((.+:\d+)\)$/.exec(trace);
if (withFnResult === null) {
// Try the PhantomJS 2 format
withFnResult = /^([^\s\(]+) \((.+:\d+:\d+)\)$/.exec(trace);
}
if (withFnResult === null) {
// Yet another PhantomJS 2 format?
withFnResult = /^([^\s\(]+|global code)@(.+:\d+:\d+)$/.exec(trace);
}
if (withFnResult === null) {
// Try the PhantomJS 2 ERROR format
withFnResult = /^([^\s\(]+) (http.+:\d+)$/.exec(trace);
}
if (withFnResult === null) {
fileAndLine = trace;
} else {
@@ -610,15 +658,26 @@
fileAndLine = withFnResult[2];
}
var fileAndLineSplit = /^(.*):(\d+)$/.exec(fileAndLine);
// And now the second part
var fileAndLineSplit = /^(.*):(\d+):(\d+)$/.exec(fileAndLine);
if (fileAndLineSplit === null) {
fileAndLineSplit = /^(.*):(\d+)$/.exec(fileAndLine);
}
var filePath = fileAndLineSplit[1];
var line = fileAndLineSplit[2];
var column = fileAndLineSplit[3];
out.push({
fnName: fnName,
filePath: filePath,
line: line
});
// Filter phantomas code
if (filePath.indexOf('phantomjs://') === -1) {
out.push({
fnName: fnName,
filePath: filePath,
line: line,
column: column
});
}
});
} catch(e) {
@@ -643,8 +702,8 @@
if (node.data.callDetails.context && node.data.callDetails.context.length === 0) {
html += '<h4>Called on 0 jQuery element</h4><p class="advice">Useless function call, as the jQuery object is empty.</p>';
} else if (node.data.type === 'jQuery - bind' && node.data.callDetails.context.length > 5) {
html += '<p class="advice">The .bind() method attaches the event listener to each jQuery element one by one. Using the .on() method is preferable if available (from v1.7).</p>';
} else if (node.eventNotDelegated) {
html += '<p class="advice">This binding should use Event Delegation instead of binding each element one by one.</p>';
}
if (node.data.resultsNumber === 0) {
@@ -674,7 +733,7 @@
function getProfilerLineHTML(index, node) {
return '<div class="index">' + (index + 1) + '</div>' +
'<div class="type">' + node.data.type + (node.children ? '<div class="children">' + recursiveChildrenHTML(node) + '</div>' : '') + '</div>' +
'<div class="value offenders">' + getTimelineParamsHTML(node, false) + '</div>' +
'<div class="value">' + getTimelineParamsHTML(node, false) + '</div>' +
'<div class="details">' + getTimelineDetailsHTML(node) + '</div>' +
'<div class="startTime ' + node.data.loadingStep + '">' + numberWithCommas(node.data.timestamp, 0) + ' ms</div>';
}
@@ -692,7 +751,7 @@
}
function onDetailsClick(row) {
// Close if it's alreay open
// Close if it's already open
if (row.classList.contains('showDetails')) {
closeDetails(row);
return;
@@ -739,10 +798,23 @@
element.append(getProfilerLineHTML(scope.index, scope.node));
element[0].id = 'line_' + scope.index;
if (scope.node.warning || scope.node.error) {
if (scope.node.warning) {
element[0].classList.add('warning');
if (scope.node.queryWithoutResults) {
element[0].classList.add('queryWithoutResults');
}
if (scope.node.jQueryCallOnEmptyObject) {
element[0].classList.add('jQueryCallOnEmptyObject');
}
if (scope.node.eventNotDelegated) {
element[0].classList.add('eventNotDelegated');
}
}
// Bind click on the details icon
var detailsIcon = element[0].querySelector('.details div');
if (detailsIcon) {
@@ -816,4 +888,40 @@
};
});
})();
offendersDirectives.filter('bytes', function() {
return function(bytes) {
if (isNaN(parseFloat(bytes)) || !isFinite(bytes)) {
return '-';
}
var kilo = bytes / 1024;
if (kilo < 1) {
return bytes + ' bytes';
}
if (kilo < 100) {
return kilo.toFixed(1) + ' KB';
}
if (kilo < 1024) {
return kilo.toFixed(0) + ' KB';
}
var mega = kilo / 1024;
if (mega < 10) {
return mega.toFixed(2) + ' MB';
}
return mega.toFixed(1) + ' MB';
};
});
offendersDirectives.filter('addSpaces', function() {
return function(str) {
return str.split('').join(' ');
};
});
})();
+1 -1
View File
@@ -1,7 +1,7 @@
var resultsFactory = angular.module('resultsFactory', ['ngResource']);
resultsFactory.factory('Results', ['$resource', function($resource) {
return $resource('/api/results/:runId', {
return $resource('api/results/:runId', {
});
}]);
+1 -1
View File
@@ -1,7 +1,7 @@
var runsFactory = angular.module('runsFactory', ['ngResource']);
runsFactory.factory('Runs', ['$resource', function($resource) {
return $resource('/api/runs/:runId', {
return $resource('api/runs/:runId', {
});
}]);
+38 -4
View File
@@ -5,17 +5,40 @@ apiService.factory('API', ['$location', 'Runs', 'Results', function($location, R
return {
launchTest: function(url, settings) {
Runs.save({
var runObject = {
url: url,
waitForResponse: false,
screenshot: true,
jsTimeline: true,
device: settings.device
}, function(data) {
device: settings.device,
waitForSelector: settings.waitForSelector,
proxy: settings.proxy,
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) {
alert('Too many requests, you reached the max number of requests allowed in 24h');
} else if (response.status === 403) {
alert('This particular query was blocked due to spamming. If you think it\'s an error, please open an issue on GitHub.');
} else {
alert('An error occured...');
}
@@ -24,6 +47,17 @@ apiService.factory('API', ['$location', 'Runs', 'Results', function($location, R
relaunchTest: function(result) {
this.launchTest(result.params.url, result.params.options);
},
parseDomains: function(textareaContent) {
var lines = textareaContent.split('\n');
function removeEmptyLines (line) {
return line.trim() !== '';
}
// Remove empty lines
return lines.filter(removeEmptyLines).join(',');
}
};
+2 -1
View File
@@ -6,7 +6,8 @@ settingsService.factory('Settings', ['localStorageService', function(localStorag
getMergedSettings: function() {
var defaultSettings = {
device: 'desktop'
device: 'desktop',
showAdvanced: false
};
var savedValues = localStorageService.get('settings');
+5 -1
View File
@@ -108,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 {
File diff suppressed because one or more lines are too long
+107 -3
View File
@@ -16,10 +16,10 @@
}
.launchBtn {
background: #e74c3c;
background: #ffa319;
color: #fff;
&:focus {
background: #ffa319;
background: #e74c3c;
}
&.disabled {
background: #deaca6;
@@ -30,6 +30,20 @@
}
.settings {
width: 50%;
margin: 0 auto;
input, select {
font-size: 1em;
}
input[type=text], input[type=password], textarea {
width: 100%;
min-width: 4em;
}
}
.device {
margin-top: 3em;
.item {
@@ -62,10 +76,100 @@
}
}
.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;
+16 -9
View File
@@ -132,13 +132,21 @@ a.linkButton {
background: #000;
> div {
overflow: scroll;
position: relative;
overflow: hidden;
&:hover {
overflow: auto;
}
}
.screenshotImage {
width: 100%;
}
.screenshotError {
color: #fff;
}
}
.screenshotWrapper.desktop {
@@ -230,14 +238,6 @@ a.linkButton {
}
}
.star {
font-weight: bold;
span {
font-size: 1.2em;
}
}
.footer {
padding: 3em;
color: #413;
@@ -247,4 +247,11 @@ a.linkButton {
.version {
font-size: 0.7em;
}
.github {
margin: 1em 0 0 0.5em;
}
}
[class^="icon-"]:before, [class*=" icon-"]:before {
vertical-align: baseline;
}
+85 -8
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;
@@ -149,9 +163,14 @@
> div {
display: block;
background: #ffe0cc;
}
}
}
.smallerOffenders {
font-size: 0.9em;
}
}
.offendersHtml {
@@ -171,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;
@@ -208,7 +229,6 @@
&:hover:after {
content: " ";
position: absolute;
background-color: inherit;
left: -0.2em;
top: -0.2em;
width: 100%;
@@ -217,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: 20em;
margin: 2em auto 4em;
canvas {
max-width: inherit;
}
}
.offenderProblem {
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;
}
+35 -4
View File
@@ -162,13 +162,19 @@
text-align: left;
}
.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;
}
@@ -177,7 +183,8 @@
padding: 0.5em 1em;
}
.table > div > div {
.table > div > div,
.table > a > div {
padding: 0.1em 1em;
background: #f2f2f2;
display: table-cell;
@@ -346,10 +353,34 @@
color: #e74c3c;
cursor: pointer;
}
.warningsFilterOn {
.queryWithoutResultsFilterOn {
> div {
display: none;
&.warning {
&.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;
}
}
+48 -37
View File
@@ -2,57 +2,68 @@
<head>
<meta charset="utf-8">
<title>Yellow Lab Tools</title>
<base href="/">
<link rel="icon" type="image/png" href="/img/favicon.png">
<meta property="og:image" content="/img/logo-large.png" />
<base href="<%= baseUrl %>">
<link rel="icon" type="image/png" href="img/favicon.png">
<meta property="og:image" content="img/logo-large.png" />
<meta name="description" content="Free online web performance tool. Audit your webpage for performance and front-end quality issues. And it's open-source!" />
<!-- build:css /css/styles.css-->
<link rel="stylesheet" type="text/css" href="/css/main.css">
<link rel="stylesheet" type="text/css" href="/css/index.css">
<link rel="stylesheet" type="text/css" href="/css/dashboard.css">
<link rel="stylesheet" type="text/css" href="/css/queue.css">
<link rel="stylesheet" type="text/css" href="/css/rule.css">
<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">
<!-- build:css css/styles.css-->
<link rel="stylesheet" type="text/css" href="css/main.css">
<link rel="stylesheet" type="text/css" href="css/index.css">
<link rel="stylesheet" type="text/css" href="css/dashboard.css">
<link rel="stylesheet" type="text/css" href="css/queue.css">
<link rel="stylesheet" type="text/css" href="css/rule.css">
<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">
<!-- endbuild -->
<!-- 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="/bower_components/angular-animate/angular-animate.min.js"></script>
<script src="/bower_components/angular-local-storage/dist/angular-local-storage.min.js"></script>
<script src="/js/app.js"></script>
<script src="/js/controllers/indexCtrl.js"></script>
<script src="/js/controllers/dashboardCtrl.js"></script>
<script src="/js/controllers/queueCtrl.js"></script>
<script src="/js/controllers/ruleCtrl.js"></script>
<script src="/js/controllers/screenshotCtrl.js"></script>
<script src="/js/controllers/timelineCtrl.js"></script>
<script src="/js/models/resultsFactory.js"></script>
<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>
<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">
<script src="/js/directives/gradeDirective.js"></script>
<script src="/js/directives/offendersDirectives.js"></script>
<!-- endbuild -->
<head>
</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>
<br><a href="<%= baseUrl %>about">More about Yellow Lab Tools</a><br>
<div class="github"><iframe id="ghbtn" frameborder="0" scrolling="0" width="145px" height="30px"></iframe></div>
</div>
<!-- build:js js/all.js -->
<script src="node_modules/angular/angular.min.js"></script>
<script src="node_modules/chart.js/dist/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>
<script src="js/controllers/queueCtrl.js"></script>
<script src="js/controllers/ruleCtrl.js"></script>
<script src="js/controllers/screenshotCtrl.js"></script>
<script src="js/controllers/timelineCtrl.js"></script>
<script src="js/models/resultsFactory.js"></script>
<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 -->
<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');}
if('@@googleAnalyticsId'.indexOf('UA-')===0){(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','https://www.google-analytics.com/analytics.js','ga');ga('create','@@googleAnalyticsId','auto');}
</script>
</body>
</html>
+1 -1
View File
@@ -5,5 +5,5 @@
<p>By the way, <b>it's free</b> because we are geeks, not businessmen. All we want is <a href="https://github.com/gmetais/YellowLabTools" target="_blank" class="star">a <span>&#9733;</span> on GitHub</a>. It will boost our motivation to add more awesome features!!!</p>
<p><br><a href="/">Back to index</a></p>
<p><br><a href="<%= baseUrl %>">Back to index</a></p>
</div>
+18 -9
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 track by $index">
{{request}}
</div>
</div>
<div class="globalScore" ng-if="globalScore === 0 || globalScore > 0">
<div>
<h2>Global score</h2>
@@ -10,10 +17,11 @@
</div>
</div>
<div>
<a href="/result/{{result.runId}}/screenshot">
<div class="screenshotWrapper" ng-class="result.params.options.device">
<a href="result/{{result.runId}}/screenshot">
<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 has a brand new <a href="https://github.com/gmetais/grunt-yellowlabtools" target="_blank">Grunt plugin</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">
@@ -61,7 +70,7 @@
<div class="fromShare" ng-if="!error && fromSocialShare">
<p>Yellow Lab Tools is a free online tool that analyzes performance and front-end quality.</p>
<a class="linkButton" href="" ng-click="seeFullReport()">See the full report for this page</a>
<a class="linkButton" href="/">Test another webpage</a>
<a class="linkButton" href="<%= baseUrl %>">Test another webpage</a>
</div>
<div ng-if="error">
+83 -4
View File
@@ -7,9 +7,88 @@
<div class="settings">
<div class="device">
<div>Choose the simulated device:</div>
<div class="item" ng-class="{active: settings.device == 'desktop'}" ng-click="settings.device = 'desktop'"><div class="icon-screen"></div>Desktop</div>
<div class="item" ng-class="{active: settings.device == 'tablet'}" ng-click="settings.device = 'tablet'"><div class="icon-tablet"></div>Tablet</div>
<div class="item" ng-class="{active: settings.device == 'phone'}" ng-click="settings.device = 'phone'"><div class="icon-mobile"></div>Phone</div>
<a href="" class="item" ng-class="{active: settings.device == '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 || settings.proxy || settings.domains)">
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 ng-if="settings.proxy">proxy</span>
<span ng-if="settings.domains">domain blocking</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 cookies, separated by a pipe character.<br><br>Example: "bar1=foo1;domain=.domain1.com|bar2=foo2;domain=www.domain2.com"</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">
HTTP proxy
<span class="settingsTooltip">
<span class="icon-question"></span>
<div><b>HTTP proxy</b><br><br>Insert here your proxy settings with the format "host:port".<br><br>Example: "192.168.10.0:3333"</div>
</span>
</div>
<div><input type="text" name="proxy" ng-model="settings.proxy" /></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>
@@ -30,4 +109,4 @@
<h3>JS Profiling</h3>
<p>Untangles the JavaScript spaghetti code</p>
</div>
</div>
</div>
+2 -2
View File
@@ -6,7 +6,7 @@
<p class="statusSubMessage">{{status.error}}</p>
<a class="linkButton" href="https://github.com/gmetais/YellowLabTools/issues" target="_blank">Report the issue on GitHub</a>
<a class="linkButton" href="/">New test</a>
<a class="linkButton" href="<%= baseUrl %>">New test</a>
</div>
<div ng-if="status.statusCode == 'awaiting'">
<div class="status">
@@ -28,7 +28,7 @@
<div class="status">Error 404 (test not found)</div>
<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>
<a class="linkButton" href="<%= baseUrl %>">New test</a>
</div>
<div ng-if="connectionLost == true">
<div class="status">Connection lost with server</div>
+1 -1
View File
@@ -1,7 +1,7 @@
<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-arrow-left3"></div><span>New test<span></a>
<a class="menuItem back" href="<%= baseUrl %>"><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-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>
+368 -60
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>
@@ -18,7 +30,7 @@
<p>This rule reached the abnormality threshold, which means there is a real problem you should care about.</p>
</div>
<div class="offenders" ng-if="rule.policy.hasOffenders">
<h3><ng-pluralize count="rule.offendersObj.count" when="{'0': 'No offenders', 'one': '1 offender', 'other': '{} offenders'}"></ng-pluralize></h3>
<h3 ng-if="rule.offendersObj.count >= 0"><ng-pluralize count="rule.offendersObj.count" when="{'0': 'No offenders', 'one': '1 offender', 'other': '{} offenders'}"></ng-pluralize></h3>
<div ng-if="rule.offendersObj.list" class="offendersTable">
<div ng-repeat="offender in rule.offendersObj.list track by $index">
@@ -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,10 @@
</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">
@@ -75,61 +96,48 @@
</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 === 'cssParsingErrors'">
<b>{{offender.error}}</b>
<file-and-line file="offender.file" line="offender.line" column="offender.column"></file-and-line>
<span ng-if="offender.file">(<a href="http://jigsaw.w3.org/css-validator/validator?profile=css3&usermedium=all&warning=no&uri={{offender.file | encodeURIComponent}}" target="_blank">Check on the W3C validator</a>)</span>
</div>
<div ng-if="policyName === 'cssComplexSelectors' || policyName === 'cssComplexSelectorsByAttribute' || policyName === 'cssImports' || policyName === 'cssUniversalSelectors' || policyName === 'cssRedundantBodySelectors' || policyName === 'cssRedundantChildNodesSelectors'">
<span ng-if="offender.bolded" ng-bind-html="offender.bolded"></span>
<b ng-if="!offender.bolded">{{offender.css}}</b>
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
</div>
<div ng-if="policyName === 'cssDuplicatedSelectors'">
{{offender.rule}} (<b>x{{offender.occurrences}}</b>)
</div>
<div ng-if="policyName === 'cssDuplicatedProperties'">
Property <b>{{offender.property}}</b> duplicated in <b>{{offender.rule}} { }</b>
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
</div>
<div ng-if="policyName === 'cssEmptyRules'">
<b>{{offender.css}} { }</b>
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
</div>
<div ng-if="policyName === 'cssExpressions'">
{{offender.rule}} {{ '{' + offender.property}}: <b>expression(</b>{{offender.expression}}<b>)</b>}
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
</div>
<div ng-if="policyName === 'cssImportants'">
{{offender.rule}} {{ '{' + offender.property}}: {{offender.value}} <b>!important</b>}
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
</div>
<div ng-if="policyName === 'cssOldIEFixes'">
<span ng-if="offender.browser"><b>{{offender.browser}} fix:</b></span>
<span ng-bind-html="offender.bolded"></span>
<div ng-if="policyName === 'cssImports'">
{{offender.css}}
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
</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'">
<div ng-if="policyName === 'notFound' || policyName === 'emptyRequests' || policyName === 'closedConnections' || policyName === 'multipleRequests' || policyName === 'cachingDisabled' || policyName === 'cachingNotSpecified'">
<url-link url="offender" max-length="100"></url-link>
</div>
@@ -143,31 +151,331 @@
(<ng-pluralize count="offender.requests" when="{'one':'1 request','other':'{} requests'}"></ng-pluralize>)
</div>
<div ng-if="policyName === 'globalVariables' || policyName === 'jQueryVersionsLoaded'">
<div ng-if="policyName === 'globalVariables' || policyName === 'jQueryVersionsLoaded' || policyName === 'synchronousXHR'">
{{offender}}
</div>
<div ng-if="policyName === 'fontsCount'">
<url-link url="offender.url" max-length="70"></url-link>
({{offender.size | bytes}})
</div>
</div>
</div>
</div>
<div ng-repeat="fileDetails in rule.offendersObj.byFile track by $index">
<h3>
<ng-pluralize count="fileDetails.count" when="{'one': '1 offender', 'other': '{} offenders'}"></ng-pluralize>
in
<url-link ng-if="fileDetails.url !== 'Inline CSS'" url="fileDetails.url" max-length="80"></url-link>
<span ng-if="fileDetails.url === 'Inline CSS'">inline CSS</span>
</h3>
<div ng-if="!rule.offendersObj.list" class="offendersHtml">
<div class="offendersTable">
<div ng-repeat="offender in fileDetails.offenders track by $index">
<div ng-if="policyName === 'cssComplexSelectors' || policyName === 'cssComplexSelectorsByAttribute' || policyName === 'cssUniversalSelectors' || policyName === 'cssRedundantBodySelectors' || policyName === 'cssRedundantChildNodesSelectors'">
<span ng-if="offender.bolded" ng-bind-html="offender.bolded"></span>
<b ng-if="!offender.bolded">{{offender.css}}</b>
<span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
</div>
<div ng-if="policyName === 'cssMobileFirst'">
<b>{{offender.query}}</b> for <ng-pluralize count="offender.rules" when="{'one':'1 rule','other':'{} rules'}"></ng-pluralize>
<span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
</div>
<div ng-if="policyName === 'cssDuplicatedSelectors'">
{{offender.rule}} (<b>x{{offender.occurrences}}</b>)
</div>
<div ng-if="policyName === 'cssDuplicatedProperties'">
Property <b>{{offender.property}}</b> duplicated in <b>{{offender.rule}} { }</b>
<span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
</div>
<div ng-if="policyName === 'cssEmptyRules'">
<b>{{offender.css}} { }</b>
<span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
</div>
<div ng-if="policyName === 'cssExpressions'">
{{offender.rule}} {{ '{' + offender.property}}: <b>expression(</b>{{offender.expression}}<b>)</b>}
<span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
</div>
<div ng-if="policyName === 'cssImportants'">
{{offender.rule}} {{ '{' + offender.property}}: {{offender.value}} <b>!important</b>}
<span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
</div>
<div ng-if="policyName === 'cssOldIEFixes'">
<span ng-if="offender.browser"><b>{{offender.browser}} fix:</b></span>
<span ng-bind-html="offender.bolded"></span>
<span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
</div>
</div>
</div>
</div>
<div ng-if="!rule.offendersObj.list && !rule.offendersObj.byFile" 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" chart-data="weightData" chart-labels="weightLabels" chart-options="weightOptions" chart-colors="weightColours"></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="{offenderProblem: 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 === 'identicalFiles'">
<div ng-repeat="offender in rule.offendersObj.list track by $index">
<h4>A file of {{offender.weight | bytes}} is loaded {{offender.urls.length}} times:</h4>
<div class="offendersTable">
<div ng-repeat="url in offender.urls">
<div><url-link url="url" 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-colors="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 === 'heavyFonts'">
<div ng-repeat="font in rule.offendersObj.fonts | orderBy:'-weight' track by $index">
<h3><url-link url="font.url" max-length="80"></url-link></h3>
<div class="offendersTable">
<div>
<div>Weight</div>
<div ng-if="font.weight <= 40960">{{font.weight | bytes}}</div>
<div ng-if="font.weight > 40960" class="offenderProblem">{{font.weight | bytes}}</div>
</div>
<div>
<div>Number of glyphs</div>
<div ng-if="font.numGlyphs <= 500">{{font.numGlyphs}}</div>
<div ng-if="font.numGlyphs > 500" class="offenderProblem">{{font.numGlyphs}} (better &lt; 500)</div>
</div>
<div>
<div>Average glyph complexity</div>
<div ng-if="font.averageGlyphComplexity <= 35">{{font.averageGlyphComplexity}}</div>
<div ng-if="font.averageGlyphComplexity > 35" class="offenderProblem">{{font.averageGlyphComplexity}} (better &lt; 35)</div>
</div>
</div>
</div>
</div>
<div ng-if="policyName === 'unusedUnicodeRanges'">
<div ng-repeat="font in rule.offendersObj.fonts | orderBy:'-compressedWeigth' track by $index">
<h3><url-link url="font.url" max-length="60"></url-link> ({{font.weight | bytes}})</h3>
<div ng-if="font.isIconFont" class="offendersTable">
<div>
<div>
This font seems to be an icon font
<span ng-if="font.numGlyphsInCommonWithPageContent / font.glyphs <= 0.05" class="offenderProblem">but only {{font.numGlyphsInCommonWithPageContent}} of its {{font.glyphs}} glyphs <ng-pluralize count="font.numGlyphsInCommonWithPageContent" when="{'one': 'is', 'other': 'are'}"></ng-pluralize> possibly used!</span>
<span ng-if="font.numGlyphsInCommonWithPageContent / font.glyphs > 0.05">and {{font.numGlyphsInCommonWithPageContent}} of its {{font.glyphs}} glyphs <ng-pluralize count="font.numGlyphsInCommonWithPageContent" when="{'one': 'is', 'other': 'are'}"></ng-pluralize> possibly used.</span>
</div>
</div>
</div>
<div ng-if="!font.isIconFont" class="offendersTable">
<div ng-repeat="range in font.unicodeRanges track by $index">
<div><b>{{range.name}}</b></div>
<div ng-if="!range.underused">{{range.numGlyphsInCommonWithPageContent}} of its {{range.charset.length}} glyphs <ng-pluralize count="range.numGlyphsInCommonWithPageContent" when="{'one': 'is', 'other': 'are'}"></ng-pluralize> possibly used</div>
<div ng-if="range.underused" class="offenderProblem">{{range.numGlyphsInCommonWithPageContent}} of its {{range.charset.length}} glyphs are used</div>
<div>
<div class="offenderButton opens">
glyphes list
<div>{{range.charset | addSpaces}}</div>
</div>
</div>
</div>
<div ng-if="font.ligaturesOrHiddenChars > 0">
<div><b>Ligatures or hidden chars</b></div>
<div ng-class="{offenderProblem: (font.ligaturesOrHiddenChars > 25)}">{{font.ligaturesOrHiddenChars}} glyphs</div>
<div></div>
</div>
</div>
</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" ng-class="result.params.options.device">
<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>
+35 -16
View File
@@ -7,8 +7,8 @@
</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>
<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">
@@ -16,14 +16,14 @@
<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" ng-class="{clickable: duration > 0}" scroll-on-click="{{$index * timelineIntervalDuration}}"></div>
<div class="tooltip detailsOverlay">
@@ -45,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>
@@ -54,17 +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>
<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" ng-class="{warningsFilterOn: warningsFilterOn}">
<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>
+601 -601
View File
File diff suppressed because it is too large Load Diff
+62 -50
View File
@@ -1,9 +1,31 @@
{
"categories": {
"pageWeight": {
"label": "Page weight",
"policies": {
"totalWeight": 5,
"imageOptimization": 2,
"gzipCompression": 2,
"fileMinification": 1
}
},
"requests": {
"label": "Requests",
"policies": {
"totalRequests": 5,
"domains": 3,
"notFound": 3,
"identicalFiles": 2,
"emptyRequests": 3,
"smallRequests": 1,
"lazyLoadableImagesBelowTheFold": 2,
"hiddenImages": 1
}
},
"domComplexity": {
"label": "DOM complexity",
"policies": {
"DOMelementsCount": 1,
"DOMelementsCount": 3,
"DOMelementMaxDepth": 1,
"iframesCount": 1,
"DOMidDuplicated": 1
@@ -12,11 +34,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": {
@@ -24,21 +51,18 @@
"policies": {
"jsErrors": 1,
"documentWriteCalls": 2,
"synchronousXHR": 5,
"consoleMessages": 0.5,
"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,
"jQueryCallsOnEmptyObject": 1,
"jQueryNotDelegatedEvents": 1
}
},
"cssComplexity": {
@@ -46,13 +70,16 @@
"policies": {
"cssRules": 2,
"cssComplexSelectors": 2,
"cssComplexSelectorsByAttribute": 1.5,
"cssColors": 0.5
"cssColors": 0.5,
"similarColors": 0.5,
"cssBreakpoints": 0.5,
"cssMobileFirst": 1
}
},
"badCSS": {
"label": "Bad CSS",
"policies": {
"cssParsingErrors": 4,
"cssImports": 3,
"cssDuplicatedSelectors": 2,
"cssDuplicatedProperties": 1,
@@ -61,56 +88,41 @@
"cssImportants": 3,
"cssOldIEFixes": 1,
"cssOldPropertyPrefixes": 1,
"cssUniversalSelectors": 1,
"cssRedundantBodySelectors": 1,
"cssRedundantChildNodesSelectors": 1
}
},
"requests": {
"label": "Requests number",
"fonts": {
"label": "Web fonts",
"policies": {
"requests": 5,
"htmlCount": 0,
"jsCount": 1,
"cssCount": 1,
"imageCount": 0,
"webfontCount": 2,
"videoCount": 0,
"jsonCount": 0,
"otherCount": 0
"fontsCount": 1,
"heavyFonts": 1,
"unusedUnicodeRanges": 1
}
},
"smallRequests": {
"label": "Small requests",
"serverConfig": {
"label": "Server config",
"policies": {
"smallJsFiles": 1,
"smallCssFiles": 1,
"smallImages": 1
}
},
"network": {
"label": "Network",
"policies": {
"notFound": 2,
"http2": 2,
"closedConnections": 2,
"multipleRequests": 2,
"cachingNotSpecified": 1,
"cachingDisabled": 1,
"cachingTooShort": 1,
"domains": 1
"cachingTooShort": 1
}
}
},
"globalScore": {
"domComplexity": 1,
"pageWeight": 3,
"requests": 3,
"domComplexity": 2,
"domManipulations": 2,
"badJavascript": 1,
"jQueryVersion": 1,
"scroll": 1,
"badJavascript": 2,
"jQuery": 1,
"cssSyntaxError": 1,
"cssComplexity": 1,
"badCSS": 1,
"requests": 3,
"smallRequests": 1,
"network": 2
"fonts": 1,
"serverConfig": 1
}
}
+58 -6
View File
@@ -113,14 +113,17 @@ var OffendersHelpers = function() {
if (traceArray) {
var results = [];
var parts = null;
var obj;
for (var i=0 ; i<traceArray.length ; i++) {
parts = /^(([\w$]+) )?([^ ]+):(\d+)$/.exec(traceArray[i]);
// Handle the new PhantomJS 2.x syntax
parts = /^(([\w$]+)@)?([^ ]+):(\d+):(\d+)$/.exec(traceArray[i]);
if (parts) {
var obj = {
obj = {
file: parts[3],
line: parseInt(parts[4], 10)
line: parseInt(parts[4], 10),
column: parseInt(parts[5], 10)
};
if (parts[2]) {
@@ -128,8 +131,26 @@ var OffendersHelpers = function() {
}
results.push(obj);
} else {
return null;
// Old syntax
parts = /^(([\w$]+) )?\(?([^ ]+):(\d+)\)?$/.exec(traceArray[i]);
if (parts) {
obj = {
file: parts[3],
line: parseInt(parts[4], 10)
};
if (parts[2]) {
obj.functionName = parts[2];
}
results.push(obj);
} else {
return null;
}
}
}
return results;
@@ -151,7 +172,10 @@ var OffendersHelpers = function() {
};
this.cssOffenderPattern = function(offender) {
var parts = /^(.*) (?:<([^ \(]*)>|\[inline CSS\]) @ (\d+):(\d+)$/.exec(offender);
// Remove any line breaks
offender = offender.replace(/(\r\n|\n|\r)/gm, '');
var parts = /^(.*) (?:<([^ \(]*)>|\[inline CSS\]) ?@ ?(\d+):(\d+)$/.exec(offender);
if (!parts) {
return {
@@ -168,7 +192,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 {
@@ -182,6 +206,34 @@ var OffendersHelpers = function() {
}
};
this.orderByFile = function(offenders) {
var byFileObj = {};
offenders.forEach(function(offender) {
var file = offender.file || 'Inline CSS';
delete offender.file;
if (!byFileObj[file]) {
byFileObj[file] = {
url: file,
count: 0,
offenders: []
};
}
byFileObj[file].count ++;
byFileObj[file].offenders.push(offender);
});
// Transform object into array
var byFileArray = [];
for (var file in byFileObj) {
byFileArray.push(byFileObj[file]);
}
return {byFile: byFileArray};
};
};
module.exports = new OffendersHelpers();
+31 -5
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 redownload = require('./tools/redownload/redownload');
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 = jsExecutionTransformer.transform(data);
// Other tools go here
// Compare colors
data = colorDiff.compareAllColors(data);
// Check media queries
data = mediaQueriesChecker.analyzeMediaQueries(data);
// Redownload every file
return redownload.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);
+41 -6
View File
@@ -1,5 +1,5 @@
var debug = require('debug')('ylt:screenshotHandler');
var lwip = require('lwip');
var Jimp = require('jimp');
var tmp = require('temporary');
var Q = require('q');
var fs = require('fs');
@@ -48,6 +48,8 @@ var screenshotHandler = function() {
})
.then(this.addWhiteBackground)
.then(this.toBuffer);
};
@@ -55,7 +57,7 @@ var screenshotHandler = function() {
this.openImage = function(imagePath) {
var deferred = Q.defer();
lwip.open(imagePath, function(err, image){
Jimp.read(imagePath, function(err, image){
if (err) {
debug('Could not open imagePath %s', imagePath);
debug(err);
@@ -74,7 +76,7 @@ var screenshotHandler = function() {
this.resizeImage = function(image, newWidth) {
var deferred = Q.defer();
var currentWidth = image.width();
var currentWidth = image.bitmap.width;
var ratio = newWidth / currentWidth;
image.scale(ratio, function(err, image){
@@ -92,11 +94,44 @@ 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:
new Jimp(image.bitmap.width, image.bitmap.height, 0xFFFFFF, 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.composite(image, 0, 0, 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();
image.toBuffer('jpg', {quality: 90}, function(err, buffer){
image.quality(90).getBuffer(Jimp.MIME_JPEG, function(err, buffer){
if (err) {
debug('Could not save image to buffer');
debug(err);
@@ -108,7 +143,7 @@ var screenshotHandler = function() {
}
});
return deferred.promise;
return deferred.promise;
};
@@ -132,4 +167,4 @@ var screenshotHandler = function() {
};
};
module.exports = new screenshotHandler();
module.exports = new screenshotHandler();
+63 -20
View File
@@ -7,6 +7,7 @@ var RunsQueue = require('../datastores/runsQueue');
var RunsDatastore = require('../datastores/runsDatastore');
var ResultsDatastore = require('../datastores/resultsDatastore');
var serverSettings = (process.env.IS_TEST) ? require('../../../test/fixtures/settings.json') : require('../../../server_config/settings.json');
var ApiController = function(app) {
'use strict';
@@ -15,8 +16,6 @@ var ApiController = function(app) {
var runsDatastore = new RunsDatastore();
var resultsDatastore = new ResultsDatastore();
// Create a new run
app.post('/api/runs', function(req, res) {
@@ -25,16 +24,30 @@ var ApiController = function(app) {
req.body.url = 'http://' + req.body.url;
}
// Block requests to unwanted websites (=spam)
if (req.body.url && isBlocked(req.body.url)) {
console.error('Test blocked for URL: %s', req.body.url);
res.status(403).send('Forbidden');
return;
}
// Grab the test parameters and generate a random run ID
var run = {
runId: (Date.now()*1000 + Math.round(Math.random()*1000)).toString(36),
params: {
url: req.body.url,
waitForResponse: req.body.waitForResponse !== false && req.body.waitForResponse !== 'false' && req.body.waitForResponse !== 0,
waitForResponse: req.body.waitForResponse === true || req.body.waitForResponse === 'true' || req.body.waitForResponse === 1,
partialResult: req.body.partialResult || null,
screenshot: req.body.screenshot || false,
jsTimeline: req.body.jsTimeline || false,
device: req.body.device || 'desktop'
device: req.body.device || 'desktop',
proxy: req.body.proxy || null,
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
}
};
@@ -48,7 +61,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);
@@ -63,17 +75,26 @@ var ApiController = function(app) {
runsDatastore.updatePosition(run.runId, 0);
debug('Launching test %s on %s', run.runId, run.params.url);
console.log('Launching test ' + run.runId + ' on ' + run.params.url);
var runOptions = {
screenshot: run.params.screenshot ? screenshot.getTmpFilePath() : false,
jsDeepAnalysis: run.params.jsTimeline,
device: run.params.device
device: run.params.device,
proxy: run.params.proxy,
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);
})
// Phantomas completed, let's save the screenshot if any
.then(function(data) {
@@ -87,7 +108,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,12 +118,18 @@ var ApiController = function(app) {
data.screenshotBuffer = screenshotBuffer;
// Official path to get the image
data.screenshotUrl = '/api/results/' + data.runId + '/screenshot.jpg';
data.screenshotUrl = 'api/results/' + data.runId + '/screenshot.jpg';
}
})
// 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...');
});
}
@@ -114,12 +141,7 @@ var ApiController = function(app) {
// Remove uneeded temp screenshot path
delete data.params.options.screenshot;
// Empty javascriptExecutionTree if not needed
if (!run.params.jsTimeline) {
data.javascriptExecutionTree = {};
}
// Remove tools results if not needed
// Here we can remove tools results if not needed
return resultsDatastore.saveResult(data);
})
@@ -182,7 +204,7 @@ var ApiController = function(app) {
// The user doesn't want to wait for the response, sending the run ID only
if (!run.params.waitForResponse) {
console.log('Sending response without waiting.');
debug('Sending response without waiting.');
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify({runId: run.runId}));
}
@@ -234,6 +256,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;
});
});
@@ -302,6 +336,15 @@ var ApiController = function(app) {
});
});
function isBlocked(url) {
if (!serverSettings.blockedUrls) {
return false;
}
return serverSettings.blockedUrls.some(function(blockedUrl) {
return (url.indexOf(blockedUrl) === 0);
});
}
};
module.exports = ApiController;
module.exports = ApiController;
+16 -4
View File
@@ -6,21 +6,33 @@ var FrontController = function(app) {
var cacheDuration = 365 * 24 * 60 * 60 * 1000; // One year
var assetsPath = (app.get('env') === 'development') ? '../../../front/src' : '../../../front/build';
// Routes templating
var routes = ['/', '/about', '/result/:runId', '/result/:runId/timeline', '/result/:runId/screenshot', '/result/:runId/rule/:policy', '/queue/:runId'];
routes.forEach(function(route) {
app.get(route, function(req, res) {
res.setHeader('Cache-Control', 'public, max-age=20');
res.sendFile(path.join(__dirname, assetsPath, 'main.html'));
res.render(path.join(__dirname, assetsPath, 'main.html'), {
baseUrl: app.locals.baseUrl || '/'
});
});
});
// Views templating
app.get('/views/:viewName', function(req, res) {
res.setHeader('Cache-Control', 'public, max-age=' + cacheDuration);
res.render(path.join(__dirname, assetsPath, 'views/' + req.params.viewName), {
baseUrl: app.locals.baseUrl || '/'
});
});
// Static assets
app.use('/css', express.static(path.join(__dirname, assetsPath, 'css'), { maxAge: cacheDuration }));
app.use('/fonts', express.static(path.join(__dirname, assetsPath, 'fonts'), { maxAge: cacheDuration }));
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;
}
@@ -1,6 +1,6 @@
var config = require('../../../server_config/settings.json');
var config = (process.env.IS_TEST) ? require('../../../test/fixtures/settings.json') : require('../../../server_config/settings.json');
var debug = require('debug')('apiLimitsMiddleware');
var debug = require('debug')('apiLimitsMiddleware');
var apiLimitsMiddleware = function(req, res, next) {
+2 -2
View File
@@ -1,6 +1,6 @@
var config = require('../../../server_config/settings.json');
var config = (process.env.IS_TEST) ? require('../../../test/fixtures/settings.json') : require('../../../server_config/settings.json');
var debug = require('debug')('authMiddleware');
var debug = require('debug')('authMiddleware');
var authMiddleware = function(req, res, next) {
@@ -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();
+153 -45
View File
@@ -1,34 +1,65 @@
var debug = require('debug')('ylt:jsExecutionTransformer');
var offendersHelpers = require('../offendersHelpers');
var offendersHelpers = require('../offendersHelpers');
var Collection = require('./phantomas/custom_modules/util/collection');
var jsExecutionTransformer = function() {
this.transform = function(data) {
var javascriptExecutionTree = {};
var jQueryFunctionsCollection = new Collection();
var metrics = {
domManipulations: 0,
queriesWithoutResults: 0,
jQueryCalls: 0,
jQueryCallsOnEmptyObject: 0
domInteractive: 0,
domContentLoaded: 0,
domContentLoadedEnd: 0,
domComplete: 0,
DOMaccesses: 0,
DOMaccessesOnScroll: 0,
queriesWithoutResults: 0
};
debug('Starting JS execution transformation');
var offenders = {};
var hasjQuery = (data.toolsResults.phantomas.metrics.jQueryVersionsLoaded > 0);
if (hasjQuery) {
metrics.jQueryCalls = 0;
metrics.jQueryCallsOnEmptyObject = 0;
metrics.jQueryNotDelegatedEvents = 0;
}
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
@@ -39,51 +70,71 @@ 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);
// Change the list of dom paths into a tree
treeRecursiveParser(node, function(node) {
if (node.data.callDetails && node.data.callDetails.context && node.data.callDetails.context.length > 0) {
node.data.callDetails.context.elements = node.data.callDetails.context.elements.map(offendersHelpers.domPathToDomElementObj, offendersHelpers);
}
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]);
}
});
// Count the number of DOM accesses, by counting the tree leafs
metrics.DOMaccesses += countTreeLeafs(node);
});
}
}
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;
}
data.javascriptExecutionTree = javascriptExecutionTree;
data.toolsResults.jsExecutionTransformer = {
metrics: metrics
metrics: metrics,
offenders: offenders
};
return data;
@@ -97,6 +148,63 @@ var jsExecutionTransformer = function() {
}
fn(node);
}
function changeListOfDomPaths(rootNode) {
treeRecursiveParser(rootNode, function(node) {
if (node.data.callDetails && node.data.callDetails.context && node.data.callDetails.context.length > 0) {
node.data.callDetails.context.elements = node.data.callDetails.context.elements.map(offendersHelpers.domPathToDomElementObj, offendersHelpers);
}
if (node.data.type === 'appendChild' || node.data.type === 'insertBefore' || node.data.type === 'getComputedStyle') {
node.data.callDetails.arguments[0] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[0]);
}
if (node.data.type === 'insertBefore') {
node.data.callDetails.arguments[1] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[1]);
}
});
}
// Returns the number of leafs (nodes without children)
function countTreeLeafs(rootNode) {
var count = 0;
treeRecursiveParser(rootNode, function(node) {
if (!node.children &&
!node.error &&
!node.windowPerformance &&
node.data.type !== 'jQuery loaded') {
count ++;
}
});
return count;
}
function isPureString(str) {
return typeof str === 'string' && str[0] !== '{' && str !== '(function)' && str !== '[Object]' && str !== '[Array]' && str !== 'true' && str !== 'false' && str !== 'undefined' && str !== 'unknown' && str !== 'null';
}
function isABindWithoutEventDelegation(node, contextLength) {
// Count only on larger bindings
if (contextLength <= 3) {
return false;
}
if (node.data.type === 'jQuery - on' && node.data.callDetails.arguments[1] && !isPureString(node.data.callDetails.arguments[1])) {
return true;
}
if (node.data.type.indexOf('jQuery - ') === 0 && node.children && node.children.length === 1) {
var child = node.children[0];
if (child.data.type === 'jQuery - on' && child.data.callDetails.arguments[1] && !isPureString(child.data.callDetails.arguments[1])) {
return true;
}
}
return false;
}
};
module.exports = new jsExecutionTransformer();
+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
@@ -49,7 +49,7 @@ exports.module = function(phantomas) {
var err;
// Before
if (enabled) {
if (enabled && callbackBefore) {
callbackBefore.apply(this, arguments);
}
@@ -71,7 +71,8 @@ exports.module = function(phantomas) {
// After
if (enabled && callbackAfter) {
callbackAfter.call(this, result, arguments);
var args = Array.prototype.slice.call(arguments);
callbackAfter.apply(this, [result].concat(args));
}
if (err) {
@@ -104,49 +105,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 +199,6 @@ exports.module = function(phantomas) {
if (root !== currentContext) {
return null;
}
var current = currentContext;
function recusiveRead(node) {
if (node.children.length === 0) {
@@ -176,6 +214,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 +241,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);
});
};
@@ -0,0 +1,30 @@
/**
* Analyzes AJAX requests
*/
/* global window: true */
exports.version = '0.2.a';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('ajaxRequests'); // @desc number of AJAX requests
phantomas.setMetric('synchronousXHR'); // @desc number of synchronous
phantomas.on('init', function() {
phantomas.evaluate(function() {
(function(phantomas) {
phantomas.spy(window.XMLHttpRequest.prototype, 'open', null, function(result, method, url, async) {
phantomas.incrMetric('ajaxRequests');
phantomas.addOffender('ajaxRequests', '<%s> [%s]', url, method);
if (async === false) {
phantomas.incrMetric('synchronousXHR');
phantomas.addOffender('synchronousXHR', url);
phantomas.log('ajaxRequests: synchronous XMLHttpRequest call to <%s>', url);
}
}, true);
})(window.__phantomas);
});
});
};
@@ -0,0 +1,71 @@
/**
* Lists the number of different characters
*/
/* global document: true, Node: true, window: true */
exports.version = '1.0';
exports.module = function(phantomas) {
'use strict';
//phantomas.setMetric('differentCharacters'); // @desc the number of different characters in the body @offenders
phantomas.on('report', function() {
phantomas.log("charactersCount: starting to analyze characters on page...");
phantomas.evaluate(function() {
(function(phantomas) {
phantomas.spyEnabled(false, 'analyzing characters on page');
function getLetterCount(arr){
return arr.reduce(function(prev, next){
prev[next] = 1;
return prev;
}, {});
}
if (document.body && document.body.textContent) {
var allText = '';
// Traverse all DOM Tree to read text
var runner = new phantomas.nodeRunner();
runner.walk(document.body, function(node, depth) {
switch (node.nodeType) {
// Grabing text nodes
case Node.TEXT_NODE:
if (node.parentNode.tagName !== 'SCRIPT' && node.parentNode.tagName !== 'STYLE') {
allText += node.nodeValue;
}
break;
// Grabing CSS content properties
case Node.ELEMENT_NODE:
if (node.tagName !== 'SCRIPT' && node.tagName !== 'STYLE') {
allText += window.getComputedStyle(node).getPropertyValue('content');
allText += window.getComputedStyle(node, ':before').getPropertyValue('content');
allText += window.getComputedStyle(node, ':after').getPropertyValue('content');
}
break;
}
});
// Reduce all text found in page into a string of unique chars
var charactersList = getLetterCount(allText.split(''));
var charsetString = Object.keys(charactersList).sort().join('');
// Remove blank characters
charsetString = charsetString.replace(/\s/g, '');
phantomas.setMetric('differentCharacters', charsetString.length);
phantomas.addOffender('differentCharacters', charsetString);
}
phantomas.spyEnabled(true);
}(window.__phantomas));
});
phantomas.log("charactersCount: analyzing characters done.");
});
};
@@ -3,21 +3,23 @@
*/
/* global document: true, Node: true, window: true */
exports.version = '0.1.a';
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) {
phantomas.spyEnabled(false, 'checking the hiddenContentSize');
var runner = new phantomas.nodeRunner(),
lazyLoadableImages = {};
var runner = new phantomas.nodeRunner();
phantomas.spyEnabled(false, 'analyzing hidden content');
runner.walk(document.body, function(node, depth) {
switch (node.nodeType) {
@@ -36,6 +38,31 @@ exports.module = function(phantomas) {
}
}
// 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 (images[i].width === 1 && images[i].height === 1) 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;
}
@@ -43,6 +70,15 @@ exports.module = function(phantomas) {
}
});
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) {
@@ -39,9 +39,9 @@ exports.module = function(phantomas) {
});
}, function(result, args) {
var id = args[0];
var id = args ? '#' + args[0] : undefined;
querySpy('id', '#' + id, 'getElementById', '#document', (result === null));
querySpy('id', id, 'getElementById', '#document', (result === null));
var moreData = {
resultsNumber : (result === null) ? 0 : 1
@@ -74,10 +74,10 @@ exports.module = function(phantomas) {
function selectorClassNameAfter(result, args) {
/*jshint validthis: true */
var className = args[0];
var className = args ? '.' + args[0] : undefined;
var context = phantomas.getDOMPath(this);
querySpy('class', '.' + className, 'getElementsByClassName', context, (result.length === 0));
querySpy('class', className, 'getElementsByClassName', context, (result.length === 0));
var moreData = {
resultsNumber : (result && result.length > 0) ? result.length : 0
@@ -113,10 +113,10 @@ exports.module = function(phantomas) {
function selectorTagNameSpyAfter(result, args) {
/*jshint validthis: true */
var tagName = args[0];
var tagName = args ? args[0].toLowerCase() : undefined;
var context = phantomas.getDOMPath(this);
querySpy('tag name', tagName.toLowerCase(), 'getElementsByTagName', context, (result.length === 0));
querySpy('tag name', tagName, 'getElementsByTagName', context, (result.length === 0));
var moreData = {
resultsNumber : (result && result.length > 0) ? result.length : 0
@@ -156,7 +156,7 @@ exports.module = function(phantomas) {
function selectorQuerySpyAfter(result, args) {
/*jshint validthis: true */
var selector = args[0];
var selector = args ? args[0] : undefined;
var context = phantomas.getDOMPath(this);
querySpy('selector', selector, 'querySelectorAll', context, (!result || result.length === 0));
@@ -189,7 +189,7 @@ exports.module = function(phantomas) {
function selectorAllQuerySpryAfter(result, args) {
/*jshint validthis: true */
var selector = args[0];
var selector = args ? args[0] : undefined;
var context = phantomas.getDOMPath(this);
querySpy('selector', selector, 'querySelectorAll', context, (!result || result.length === 0));
@@ -365,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,49 +0,0 @@
/**
* Analyzes events bound to DOM elements
*/
/* global Document: true, Element: true, window: true */
exports.version = '0.2.a';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('eventsBound'); // @desc number of EventTarget.addEventListener calls
// spy calls to EventTarget.addEventListener
// @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget.addEventListener
phantomas.once('init', function() {
phantomas.evaluate(function() {
(function(phantomas) {
function eventSpyBefore(eventType) {
/* jshint validthis: true */
var path = phantomas.getDOMPath(this);
//phantomas.log('DOM event: "' + eventType + '" bound to "' + path + '"');
phantomas.incrMetric('eventsBound');
phantomas.addOffender('eventsBound', '"%s" bound to "%s"', eventType, path);
phantomas.enterContext({
type: 'addEventListener',
callDetails: {
context: {
length: 1,
elements: [path]
},
arguments: [eventType]
},
backtrace: phantomas.getBacktrace()
});
}
function eventSpyAfter(result) {
phantomas.leaveContext();
}
phantomas.spy(Element.prototype, 'addEventListener', eventSpyBefore, eventSpyAfter);
phantomas.spy(Document.prototype, 'addEventListener', eventSpyBefore, eventSpyAfter);
phantomas.spy(window, 'addEventListener', eventSpyBefore, eventSpyAfter);
})(window.__phantomas);
});
});
};
@@ -0,0 +1,82 @@
/**
* Analyzes events bound to DOM elements
*/
/* global Document: true, Element: true, window: true */
exports.version = '0.4.a';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('eventsBound'); // @desc number of EventTarget.addEventListener calls
phantomas.setMetric('eventsDispatched'); // @desc number of EventTarget.dispatchEvent calls
phantomas.setMetric('eventsScrollBound'); // @desc number of scroll event bounds
phantomas.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));
});
});
};
@@ -138,10 +138,9 @@ exports.module = function(phantomas) {
jQueryFunctions = jQueryFunctions.concat(jQueryTraversalFunctions);
// spy calls to jQuery functions
phantomas.once('init', function() {
phantomas.on('init', function() {
phantomas.evaluate(function(jQueryFunctions, jQueryTraversalFunctions) {
(function(phantomas) {
var jQuery;
var oldJQuery;
phantomas.spyGlobalVar('jQuery', function(jQuery) {
@@ -274,7 +273,6 @@ exports.module = function(phantomas) {
args = [].slice.call(arguments);
args.forEach(function(arg, index) {
if (arg instanceof Array) {
arg = '[Array]';
}
@@ -296,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]';
@@ -365,14 +370,6 @@ exports.module = function(phantomas) {
});
// store the last resource that was received
// try to report where given jQuery version was loaded from
phantomas.on('recv', function(entry) {
if (entry.isJS) {
lastUrl = entry.url;
}
});
phantomas.on('jQueryLoaded', function(version) {
phantomas.log('jQuery: loaded v' + version);
phantomas.setMetric('jQueryVersion', version);
@@ -380,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);
});
};
@@ -23,7 +23,7 @@ exports.module = function(phantomas) {
phantomas.log('javaScriptBottlenecks: to spy calls to eval() run phantomas with --spy-eval option');
}
phantomas.once('init', function() {
phantomas.on('init', function() {
phantomas.evaluate(function(spyEval) {
(function(phantomas) {
function report(msg, caller, backtrace, metric) {
@@ -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,81 @@
/**
* 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,
// Add an offset of 100px under the height of the screen
LAZYLOAD_OFFSET = 100;
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 + LAZYLOAD_OFFSET) {
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);
});
});
};
+71 -29
View File
@@ -17,33 +17,41 @@ var PhantomasWrapper = function() {
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': (task.options.device === 'desktop') ? 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.110 Safari/537.36' : null,
'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,
'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,
'ignore-ssl-errors': true,
'skip-modules': [
'blockDomains', // not needed
'domHiddenContent', // overriden
'ajaxRequests', // overridden
'domHiddenContent', // overridden
'domMutations', // not compatible with webkit
'domQueries', // overriden
'eventListeners', // overridden
'domQueries', // 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', //overridden
'pageSource', // not needed
'waitForSelector', // not needed
'windowPerformance' // overriden
'windowPerformance' // overridden
].join(','),
'include-dirs': [
path.join(__dirname, 'custom_modules/core'),
@@ -51,6 +59,12 @@ var PhantomasWrapper = function() {
].join(',')
};
// Proxy option can't be set to null or undefined...
// this is why it's set now and not in the object above
if (task.options.proxy) {
options.proxy = task.options.proxy;
}
// Output the command line for debugging purpose
debug('If you want to reproduce the phantomas task only, copy the following command line:');
var optionsString = '';
@@ -63,7 +77,7 @@ var PhantomasWrapper = function() {
if (value === true) {
optionsString += ' ' + '--' + opt;
} else if (value === false || value === null) {
} else if (value === false || value === null || value === undefined) {
// Nothing
} else {
optionsString += ' ' + '--' + opt + '=' + value;
@@ -72,56 +86,84 @@ var PhantomasWrapper = function() {
}
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;
+188
View File
@@ -0,0 +1,188 @@
var debug = require('debug')('ylt:contentTypeChecker');
var Q = require('q');
var isJpg = require('is-jpg');
var isPng = require('is-png');
var isSvg = require('is-svg');
var isGif = require('is-gif');
var isWoff = require('is-woff');
var isWoff2 = require('is-woff2');
var isOtf = require('is-otf');
var isTtf = require('is-ttf');
var isEot = require('is-eot');
var ContentTypeChecker = function() {
function checkContentType(entry) {
var deferred = Q.defer();
debug('Entering contentTypeChecker');
// Ignore very small files as they are generally tracking pixels
if (entry.weightCheck && entry.weightCheck.bodyBuffer && entry.weightCheck.bodySize > 100) {
var foundType;
try {
foundType = findContentType(entry.weightCheck.bodyBuffer);
if (!entry.contentType || entry.contentType === '') {
if (foundType === null) {
debug('ContentType is empty for file %s', entry.url);
} else {
debug('ContentType is empty for file %s. It should be %s.', entry.url, foundType.mimes[0]);
entry.oldContentType = null;
rewriteContentType(entry, foundType);
}
} else {
if (foundType !== null && foundType.mimes.indexOf(entry.contentType) === -1) {
debug('ContentType %s is wrong for %s. It should be %s.', entry.contentType, entry.url, foundType.mimes[0]);
entry.oldContentType = entry.contentType;
rewriteContentType(entry, foundType);
}
}
} catch(err) {
debug('Error while analyzing the contentType of %s', entry.url);
debug(err);
}
}
deferred.resolve(entry);
return deferred.promise;
}
function findContentType(bodyBuffer) {
var bodyStr = bodyBuffer.toString();
if (isJpg(bodyBuffer)) {
return contentTypes.jpeg;
}
if (isPng(bodyBuffer)) {
return contentTypes.png;
}
// https://github.com/sindresorhus/is-svg/issues/7
if (/<svg/.test(bodyStr) && isSvg(bodyStr)) {
return contentTypes.svg;
}
if (isGif(bodyBuffer)) {
return contentTypes.gif;
}
if (isWoff(bodyBuffer)) {
return contentTypes.woff;
}
if (isWoff2(bodyBuffer)) {
return contentTypes.woff2;
}
if (isOtf(bodyBuffer)) {
return contentTypes.otf;
}
if (isTtf(bodyBuffer)) {
return contentTypes.ttf;
}
if (isEot(bodyBuffer)) {
return contentTypes.eot;
}
return null;
}
function rewriteContentType(entry, contentTypeObj) {
delete(entry.isHTML);
delete(entry.isXML);
delete(entry.isCSS);
delete(entry.isJS);
delete(entry.isJSON);
delete(entry.isImage);
delete(entry.isSVG);
delete(entry.isVideo);
delete(entry.isWebFont);
delete(entry.isTTF);
delete(entry.isFavicon);
entry.contentType = contentTypeObj.mimes[0];
contentTypeObj.updateFn(entry);
}
var contentTypes = {
jpeg: {
mimes: ['image/jpeg'],
updateFn: function(entry) {
entry.type = 'image';
entry.isImage = true;
}
},
png: {
mimes: ['image/png'],
updateFn: function(entry) {
entry.type = 'image';
entry.isImage = true;
}
},
svg: {
mimes: ['image/svg+xml'],
updateFn: function(entry) {
entry.type = 'image';
entry.isImage = true;
entry.isSVG = true;
}
},
gif: {
mimes: ['image/gif'],
updateFn: function(entry) {
entry.type = 'image';
entry.isImage = true;
}
},
woff: {
mimes: ['application/x-font-woff', 'application/font-woff', 'font/woff'],
updateFn: function(entry) {
entry.type = 'webfont';
entry.isWebFont = true;
}
},
woff2: {
mimes: ['font/woff2', 'application/x-font-woff2', 'application/font-woff2'],
updateFn: function(entry) {
entry.type = 'webfont';
entry.isWebFont = true;
}
},
otf: {
mimes: ['application/x-font-otf', 'font/otf', 'font/opentype', 'application/x-font-opentype'],
updateFn: function(entry) {
entry.type = 'webfont';
entry.isWebFont = true;
}
},
ttf: {
mimes: ['application/x-font-ttf', 'font/ttf', 'application/x-font-truetype'],
updateFn: function(entry) {
entry.type = 'webfont';
entry.isWebFont = true;
}
},
eot: {
mimes: ['application/vnd.ms-fontobject', 'font/eot'],
updateFn: function(entry) {
entry.type = 'webfont';
entry.isWebFont = true;
}
}
};
return {
checkContentType: checkContentType,
findContentType: findContentType
};
};
module.exports = new ContentTypeChecker();
+354
View File
@@ -0,0 +1,354 @@
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.bodyBuffer) {
// No valid file available
deferred.resolve(entry);
return deferred.promise;
}
var fileSize = entry.weightCheck.uncompressedSize;
var bodyString = entry.weightCheck.bodyBuffer.toString();
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(bodyString)) {
debug('File is a JS');
return minifyJs(bodyString)
.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.bodyBuffer.toString())
.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.bodyBuffer.toString())
.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(function(ast) {
// Only do the compression step for smaller files
// otherwise it can take a very long time compared to the gain
if (body.length < 200*1024) {
return splittedUglifyStep3(ast);
} else {
debug('Skipping step 3 because the file is too big (%d bytes)!', body.length);
return ast;
}
})
.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 losing 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 losing some trying to compress JS files if they already look minified
// by counting the number of lines compared to the total size.
// Less than 2KB per line is suspicious
function looksAlreadyMinified(code) {
var linesCount = code.split(/\r\n|\r|\n/).length;
var linesRatio = code.length / linesCount;
var looksMinified = linesRatio > 2 * 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();
+388
View File
@@ -0,0 +1,388 @@
var debug = require('debug')('ylt:fontAnalyzer');
var Q = require('q');
var fontkit = require('fontkit');
var FontAnalyzer = function() {
function analyzeFont(entry, charsListOnPage) {
var deferred = Q.defer();
if (!entry.weightCheck || !entry.weightCheck.bodyBuffer) {
// No valid file available
deferred.resolve(entry);
return deferred.promise;
}
var fileSize = entry.weightCheck.uncompressedSize;
if (entry.isWebFont) {
debug('File %s is a font. Let\'s have a look inside!', entry.url);
getMetricsFromFont(entry, charsListOnPage)
.then(function(fontMetrics) {
entry.fontMetrics = fontMetrics;
deferred.resolve(entry);
})
.fail(function(error) {
debug('Could not open the font: %s', error);
deferred.resolve(entry);
});
} else {
deferred.resolve(entry);
}
return deferred.promise;
}
function getMetricsFromFont(entry, charsListOnPage) {
var deferred = Q.defer();
try {
var startTime = Date.now();
var font = fontkit.create(entry.weightCheck.bodyBuffer);
var result = {
name: font.fullName || font.postscriptName || font.familyName,
numGlyphs: font.numGlyphs,
averageGlyphComplexity: getAverageGlyphComplexity(font),
compressedWeight: entry.weightCheck.afterCompression || entry.weightCheck.bodySize,
unicodeRanges: readUnicodeRanges(font.characterSet, charsListOnPage),
numGlyphsInCommonWithPageContent: countPossiblyUsedGlyphs(getCharacterSetAsString(font.characterSet), charsListOnPage)
};
var endTime = Date.now();
debug('Font analysis took %dms', endTime - startTime);
// Mark fonts that are not used on the page (#224)
var fontIsUsed = false;
for (var range in result.unicodeRanges) {
if (result.unicodeRanges[range].numGlyphsInCommonWithPageContent > 0) {
fontIsUsed = true;
break;
}
}
result.isUsed = fontIsUsed;
deferred.resolve(result);
} catch(error) {
deferred.reject(error);
}
return deferred.promise;
}
// Reads the number of vector commands (complexity) needed to render glyphs and
// returns the average. Only first 100 glyphes are tested, otherwise it would take tool long;
function getAverageGlyphComplexity(font) {
var max = Math.min(font.numGlyphs, 100);
var totalPathsCommands = 0;
for (var i = 0; i < max; i++) {
totalPathsCommands += font.getGlyph(i).path.commands.length;
}
return Math.round(totalPathsCommands / max * 10) / 10;
}
function readUnicodeRanges(charsetInFont, charsListOnPage) {
var ranges = {};
// Assign a range to each char found in the font
charsetInFont.forEach(function(char) {
var currentRange = getUnicodeRangeFromChar(char);
var currentRangeName = currentRange.name;
if (!ranges[currentRangeName]) {
// Cloning the object
ranges[currentRangeName] = Object.assign({}, currentRange);
}
if (!ranges[currentRangeName].charset) {
ranges[currentRangeName].charset = '';
}
ranges[currentRangeName].charset += String.fromCharCode(char);
});
var range;
var expectedLength;
var actualLength;
for (var rangeName in ranges) {
/*jshint loopfunc: true */
range = ranges[rangeName];
// Estimate if range is used, based on the characters found in the page
range.numGlyphsInCommonWithPageContent = countPossiblyUsedGlyphs(range.charset, charsListOnPage);
// Calculate coverage
if (rangeName !== 'Others') {
expectedLength = range.rangeEnd - range.rangeStart + 1;
actualLength = range.charset.length;
range.coverage = Math.min(actualLength / expectedLength, 1);
}
}
return ranges;
}
function countPossiblyUsedGlyphs(charsetInFont, charsListOnPage) {
var count = 0;
charsListOnPage.split('').forEach(function(char) {
if (charsetInFont.indexOf(char) >= 0) {
count ++;
}
});
return count;
}
function getCharacterSetAsString(characterSet) {
var str = '';
characterSet.forEach(function(charCode) {
str += String.fromCharCode(charCode);
});
return str;
}
function getUnicodeRangeFromChar(char) {
return UNICODE_RANGES.find(function(range) {
return (char >= range.rangeStart && char <= range.rangeEnd);
}) || {name: 'Others'};
}
var UNICODE_RANGES = [
{
name: 'Basic Latin',
rangeStart: 0x0020,
rangeEnd: 0x007F
},
{
name: 'Latin-1 Supplement',
rangeStart: 0x00A0,
rangeEnd: 0x00FF
},
{
name: 'Latin Extended',
rangeStart: 0x0100,
rangeEnd: 0x024F
},
{
name: 'IPA Extensions',
rangeStart: 0x0250,
rangeEnd: 0x02AF
},
{
name: 'Greek and Coptic',
rangeStart: 0x0370,
rangeEnd: 0x03FF
},
{
name: 'Cyrillic',
rangeStart: 0x0400,
rangeEnd: 0x052F
},
{
name: 'Armenian',
rangeStart: 0x0530,
rangeEnd: 0x058F
},
{
name: 'Hebrew',
rangeStart: 0x0590,
rangeEnd: 0x05FF
},
{
name: 'Arabic',
rangeStart: 0x0600,
rangeEnd: 0x06FF
},
{
name: 'Syriac',
rangeStart: 0x0700,
rangeEnd: 0x074F
},
{
name: 'Thaana',
rangeStart: 0x0780,
rangeEnd: 0x07BF
},
{
name: 'Devanagari',
rangeStart: 0x0900,
rangeEnd: 0x097F
},
{
name: 'Bengali',
rangeStart: 0x0980,
rangeEnd: 0x09FF
},
{
name: 'Gurmukhi',
rangeStart: 0x0A00,
rangeEnd: 0x0A7F
},
{
name: 'Gujarati',
rangeStart: 0x0A80,
rangeEnd: 0x0AFF
},
{
name: 'Oriya',
rangeStart: 0x0B00,
rangeEnd: 0x0B7F
},
{
name: 'Tamil',
rangeStart: 0x0B80,
rangeEnd: 0x0BFF
},
{
name: 'Telugu',
rangeStart: 0x0C00,
rangeEnd: 0x0C7F
},
{
name: 'Kannada',
rangeStart: 0x0C80,
rangeEnd: 0x0CFF
},
{
name: 'Malayalam',
rangeStart: 0x0D00,
rangeEnd: 0x0D7F
},
{
name: 'Sinhala',
rangeStart: 0x0D80,
rangeEnd: 0x0DFF
},
{
name: 'Thai',
rangeStart: 0x0E00,
rangeEnd: 0x0E7F
},
{
name: 'Lao',
rangeStart: 0x0E80,
rangeEnd: 0x0EFF
},
{
name: 'Tibetan',
rangeStart: 0x0F00,
rangeEnd: 0x0FFF
},
{
name: 'Myanmar',
rangeStart: 0x1000,
rangeEnd: 0x109F
},
{
name: 'Georgian',
rangeStart: 0x10A0,
rangeEnd: 0x10FF
},
{
name: 'Hangul Jamo',
rangeStart: 0x1100,
rangeEnd: 0x11FF
},
{
name: 'Ethiopic',
rangeStart: 0x1200,
rangeEnd: 0x137F
},
{
name: 'Cherokee',
rangeStart: 0x13A0,
rangeEnd: 0x13FF
},
{
name: 'Unified Canadian Aboriginal Syllabics',
rangeStart: 0x1400,
rangeEnd: 0x167F
},
{
name: 'Ogham',
rangeStart: 0x1680,
rangeEnd: 0x169F
},
{
name: 'Runic',
rangeStart: 0x16A0,
rangeEnd: 0x16FF
},
{
name: 'Tagalog',
rangeStart: 0x1700,
rangeEnd: 0x171F
},
{
name: 'Hanunoo',
rangeStart: 0x1720,
rangeEnd: 0x173F
},
{
name: 'Buhid',
rangeStart: 0x1740,
rangeEnd: 0x175F
},
{
name: 'Tagbanwa',
rangeStart: 0x1760,
rangeEnd: 0x177F
},
{
name: 'Khmer',
rangeStart: 0x1780,
rangeEnd: 0x17FF
},
{
name: 'Mongolian',
rangeStart: 0x1800,
rangeEnd: 0x18AF
},
{
name: 'Limbu',
rangeStart: 0x1900,
rangeEnd: 0x194F
},
{
name: 'Tai Le',
rangeStart: 0x1950,
rangeEnd: 0x197F
},
{
name: 'Hiragana',
rangeStart: 0x3040,
rangeEnd: 0x309F
},
{
name: 'Katakana',
rangeStart: 0x30A0,
rangeEnd: 0x30FF
},
{
name: 'Bopomofo',
rangeStart: 0x3100,
rangeEnd: 0x312F
}
];
return {
analyzeFont: analyzeFont,
getMetricsFromFont: getMetricsFromFont,
readUnicodeRanges: readUnicodeRanges,
getAverageGlyphComplexity: getAverageGlyphComplexity,
countPossiblyUsedGlyphs: countPossiblyUsedGlyphs,
getCharacterSetAsString: getCharacterSetAsString,
getUnicodeRangeFromChar: getUnicodeRangeFromChar
};
};
module.exports = new FontAnalyzer();
+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.bodyBuffer) {
debug('Compression missing, trying to gzip file %s', entry.url);
var uncompressedSize = entry.weightCheck.uncompressedSize;
zlib.gzip(entry.weightCheck.bodyBuffer, 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 imageminJpegtran = require('imagemin-jpegtran');
var imageminJpegoptim = require('imagemin-jpegoptim');
var imageminOptipng = require('imagemin-optipng');
var imageminSvgo = require('imagemin-svgo');
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.bodyBuffer) {
// 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(entry.weightCheck.bodyBuffer)
.then(function(newFile) {
if (!newFile) {
debug('Optimization didn\'t work');
return entry;
}
var newFileSize = newFile.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(entry.weightCheck.bodyBuffer);
})
.then(function(newFile) {
if (!newFile) {
debug('Optimization didn\'t work');
return entry;
}
var newFileSize = newFile.length;
debug('JPEG lossy compression complete for %s', entry.url);
if (entry.weightCheck.lossless && entry.weightCheck.lossless < newFileSize) {
debug('Lossy compression is not as good as lossless compression. Skipping the lossy.');
} else 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(entry.weightCheck.bodyBuffer)
.then(function(newFile) {
if (!newFile) {
debug('Optimization didn\'t work');
return entry;
}
var newFileSize = newFile.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(entry.weightCheck.bodyBuffer)
.then(function(newFile) {
if (!newFile) {
debug('Optimization didn\'t work');
return entry;
}
var newFileSize = newFile.length;
debug('SVG lossless compression complete for %s', entry.url);
if (gainIsEnough(fileSize, newFileSize)) {
entry.weightCheck.bodyAfterOptimization = newFile.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 = imageminJpegtran({progressive: true});
} else if (type === 'jpeg' && lossy) {
engine = imageminJpegoptim({progressive: true, max: MAX_JPEG_QUALITY});
} else if (type === 'png' && !lossy) {
engine = imageminOptipng({optimizationLevel: OPTIPNG_COMPRESSION_LEVEL});
} else if (type === 'svg' && !lossy) {
engine = imageminSvgo({ plugins: [ { removeUselessDefs: false } ] });
} else {
deferred.reject('No optimization engine found for imagemin');
}
imagemin.buffer(imageBody, {use: engine})
.then(function(file) {
var endTime = Date.now();
debug('Optimization for %s took %d ms', type, endTime - startTime);
deferred.resolve(file);
})
.catch(function(err) {
debug('Optimisation failed:');
debug(err);
deferred.resolve();
});
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();
+802
View File
@@ -0,0 +1,802 @@
/*
* Redownloading every files after Phantomas has finished
* Checks weight and every kind of compression
*
*/
/*jshint -W069 */
var debug = require('debug')('ylt:redownload');
var Q = require('q');
var http = require('http');
var zlib = require('zlib');
var async = require('async');
var request = require('request');
var md5 = require('md5');
var imageOptimizer = require('./imageOptimizer');
var fileMinifier = require('./fileMinifier');
var gzipCompressor = require('./gzipCompressor');
var contentTypeChecker = require('./contentTypeChecker');
var fontAnalyzer = require('./fontAnalyzer');
var Redownload = 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
};
}
var proxy = null;
if (data.params && data.params.options && data.params.options.proxy) {
proxy = data.params.options.proxy;
if (proxy.indexOf('http:') === -1) {
proxy = 'http://' + proxy;
}
}
// Prevent a bug with the font analyzer on empty pages
var differentCharacters = '';
if (data.toolsResults.phantomas.offenders.differentCharacters && data.toolsResults.phantomas.offenders.differentCharacters.length > 0) {
differentCharacters = data.toolsResults.phantomas.offenders.differentCharacters[0];
}
// Transform every request into a download function with a callback when done
var redownloadList = requestsList.map(function(entry) {
return function(callback) {
redownloadEntry(entry, httpAuth, proxy)
.then(contentTypeChecker.checkContentType)
.then(imageOptimizer.optimizeImage)
.then(fileMinifier.minifyFile)
.then(gzipCompressor.compressFile)
.then(function(entry) {
return fontAnalyzer.analyzeFont(entry, differentCharacters);
})
.then(function(newEntry) {
debug('File %s - Redownloaded, optimized, minified, compressed, analyzed: 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 = {};
// Remove unused fonts that a normal browser would not download (fix #224)
results = results.filter(function(result) {
if (result && result.fontMetrics) {
return result.fontMetrics.isUsed !== false;
}
return true;
});
// Count requests
offenders.totalRequests = listRequestsByType(results);
metrics.totalRequests = offenders.totalRequests.total;
// Remove unwanted responses (redownload failed, about:blank)
results = results.filter(function(result) {
return (result !== null && result.weightCheck && result.url.indexOf('about:blank') !== 0);
});
// Wrong contentType
offenders.incorrectContentTypes = listIncorrectContentTypes(results);
metrics.incorrectContentTypes = offenders.incorrectContentTypes.length;
// Total weight
offenders.totalWeight = listRequestWeight(results);
metrics.totalWeight = offenders.totalWeight.totalWeight;
// Empty files
offenders.emptyRequests = listEmptyRequests(results);
metrics.emptyRequests = offenders.emptyRequests.length;
// Now remove unwanted responses (redirections and empty files)
results = results.filter(function(result) {
return ((result.status < 300 || result.status >= 400) && result.weightCheck.bodySize > 0);
});
// 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;
// Detect identical files
offenders.identicalFiles = listIdenticalFiles(results);
metrics.identicalFiles = offenders.identicalFiles.avoidableRequests;
// Fonts count
offenders.fontsCount = listFonts(results);
metrics.fontsCount = offenders.fontsCount.count;
// Heavy fonts
offenders.heavyFonts = listHeavyFonts(results);
metrics.heavyFonts = offenders.heavyFonts.totalGain;
// Unused Unicode ranges
offenders.unusedUnicodeRanges = listUnusedUnicodeRanges(results);
metrics.unusedUnicodeRanges = offenders.unusedUnicodeRanges.count;
data.toolsResults.redownload = {
metrics: metrics,
offenders: offenders
};
deferred.resolve(data);
}
});
return deferred.promise;
}
function listIncorrectContentTypes(requests) {
var results = [];
requests.forEach(function(req) {
if (req.oldContentType || req.oldContentType === null) {
results.push({
url: req.url,
current: req.oldContentType,
correct: req.contentType
});
}
});
return results;
}
function listEmptyRequests(requests) {
var results = [];
requests.forEach(function(req) {
var weight = req.weightCheck.bodySize || req.contentLength;
if (weight === 0 && req.method === 'GET' && req.status === 200) {
results.push(req.url);
}
});
return results;
}
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 listIdenticalFiles(requests) {
var hashes = {};
var list = [];
var avoidableRequestsCount = 0;
requests.forEach(function(req) {
var requestHash = md5(req.weightCheck.bodyBuffer);
// Try to exclude tracking pixels
if (req.weightCheck.bodySize < 80 && req.type === 'image') {
return;
}
if (!hashes[requestHash]) {
hashes[requestHash] = {
weight: req.weightCheck.bodySize,
urls: []
};
}
if (hashes[requestHash].urls.indexOf(req.url) === -1) {
hashes[requestHash].urls.push(req.url);
}
});
for (var hash in hashes) {
if (hashes[hash].urls.length > 1) {
list.push(hashes[hash]);
avoidableRequestsCount += hashes[hash].urls.length - 1;
}
}
return {
avoidableRequests: avoidableRequestsCount,
count: list.length,
list: list
};
}
function listFonts(requests) {
var list = [];
requests.forEach(function(req) {
if (req.isWebFont) {
list.push({
url: req.url,
size: req.weightCheck.bodySize
});
}
});
return {
count: list.length,
list: list
};
}
function listHeavyFonts(requests) {
var list = [];
var totalGain = 0;
var heavyFontsCount = 0;
var MAX_FONT_WEIGHT = 40 * 1024;
requests.forEach(function(req) {
if (req.isWebFont && req.fontMetrics) {
list.push({
url: req.url,
weight: req.weightCheck.bodySize,
numGlyphs: req.fontMetrics.numGlyphs,
averageGlyphComplexity: req.fontMetrics.averageGlyphComplexity
});
if (req.weightCheck.bodySize > MAX_FONT_WEIGHT) {
totalGain += req.weightCheck.bodySize - MAX_FONT_WEIGHT;
heavyFontsCount ++;
}
}
});
return {
count: heavyFontsCount,
fonts: list,
totalGain: totalGain
};
}
function listUnusedUnicodeRanges(requests) {
var list = [];
var unusedUnicodeRanges = 0;
requests.forEach(function(req) {
if (req.isWebFont && req.fontMetrics && req.fontMetrics.unicodeRanges) {
var ranges = [];
var others = null;
var rangeNames = Object.keys(req.fontMetrics.unicodeRanges);
var unicodePointsCount = 0;
var unusedRangesInFont = 0;
rangeNames.forEach(function(rangeName) {
var range = req.fontMetrics.unicodeRanges[rangeName];
// Exclude "Others"
if (rangeName === 'Others') {
if (range.numGlyphsInCommonWithPageContent === 0 && range.charset.length > 50) {
range.underused = true;
unusedRangesInFont ++;
}
unicodePointsCount += range.charset.length;
others = range;
} else if (range.charset.length > 0) {
// Now lets detect if the current Unicode range is unused.
// Reminder: range.coverage = glyphs declared in this range, divided by the range size
if (range.coverage > 0.25 && range.numGlyphsInCommonWithPageContent === 0) {
range.underused = true;
unusedRangesInFont ++;
}
unicodePointsCount += range.charset.length;
ranges.push(range);
}
});
// Detect if it's a icons font : if more than 90% of the icons are
// in the "Others", it looks like one.
if (others && others.charset.length / req.fontMetrics.numGlyphs > 0.9) {
list.push({
url: req.url,
weight: req.weightCheck.bodySize,
isIconFont: true,
glyphs: req.fontMetrics.numGlyphs,
numGlyphsInCommonWithPageContent: req.fontMetrics.numGlyphsInCommonWithPageContent
});
// And if less than 5% of the icons are used, let's report it as underused
if (others && others.numGlyphsInCommonWithPageContent / others.charset.length <= 0.05) {
unusedRangesInFont = 1;
}
// Not an icons font
} else {
if (others) {
// Insert back "Others" at the end of the list
ranges.push(others);
}
var ligaturesOrHiddenChars = req.fontMetrics.numGlyphs - unicodePointsCount;
if (ligaturesOrHiddenChars > 25) {
unusedUnicodeRanges ++;
}
list.push({
url: req.url,
weight: req.weightCheck.bodySize,
isIconFont: false,
unicodeRanges: ranges,
ligaturesOrHiddenChars: ligaturesOrHiddenChars
});
}
unusedUnicodeRanges += unusedRangesInFont;
}
});
return {
count: unusedUnicodeRanges,
fonts: list
};
}
function redownloadEntry(entry, httpAuth, proxy) {
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';
reqHeaders['Connection'] = 'keep-alive';
var requestOptions = {
method: entry.method,
url: entry.url,
headers: reqHeaders,
timeout: REQUEST_TIMEOUT,
proxy: proxy
};
// 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 bodyChunks = []; // an array of buffers
var isCompressed = false;
function tally() {
if (statusCode !== 200) {
callback({code: statusCode});
return;
}
var body = Buffer.concat(bodyChunks);
var result = {
bodyBuffer: 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) {
bodyChunks.push(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) {
bodyChunks.push(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:
res.on('data', function (data) {
bodyChunks.push(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 Redownload();
+91 -40
View File
@@ -1,7 +1,13 @@
{
"name": "yellowlabtools",
"version": "1.4.0",
"version": "1.13.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,95 @@
"bin": {
"yellowlabtools": "./bin/cli.js"
},
"engines": {
"node": ">= 4.0"
},
"main": "./lib/index.js",
"dependencies": {
"async": "~0.9.0",
"body-parser": "~1.12.3",
"compression": "~1.4.3",
"cors": "^2.5.3",
"debug": "~2.1.3",
"express": "~4.12.3",
"lwip": "0.0.6",
"meow": "^3.1.0",
"phantomas": "1.10.0",
"ps-node": "0.0.4",
"q": "~1.2.1",
"rimraf": "~2.3.2",
"temporary": "0.0.8"
"angular": "1.6.8",
"angular-animate": "1.6.8",
"angular-chart.js": "1.1.1",
"angular-local-storage": "0.7.1",
"angular-resource": "1.6.8",
"angular-route": "1.6.8",
"angular-sanitize": "1.6.8",
"async": "2.6.0",
"body-parser": "1.18.2",
"chart.js": "2.7.1",
"clean-css": "4.1.9",
"color-diff": "1.1.0",
"compression": "1.7.1",
"cors": "2.8.4",
"css-mq-parser": "0.0.3",
"debug": "3.1.0",
"easyxml": "2.0.1",
"ejs": "2.5.7",
"express": "4.16.2",
"fontkit": "1.7.7",
"imagemin": "5.3.1",
"imagemin-jpegoptim": "5.2.0",
"imagemin-jpegtran": "5.0.2",
"imagemin-optipng": "5.2.1",
"imagemin-svgo": "6.0.0",
"is-eot": "1.0.0",
"is-gif": "1.0.0",
"is-http2": "1.2.0",
"is-jpg": "1.0.0",
"is-otf": "0.1.2",
"is-png": "1.1.0",
"is-svg": "2.1.0",
"is-ttf": "0.2.2",
"is-woff": "1.0.3",
"is-woff2": "1.0.0",
"jimp": "0.2.28",
"md5": "2.2.1",
"meow": "3.7.0",
"minimize": "2.1.0",
"parse-color": "1.0.0",
"phantomas": "1.19.0",
"ps-node": "0.1.6",
"q": "1.5.1",
"request": "2.83.0",
"rimraf": "2.6.2",
"temporary": "0.0.8",
"try-thread-sleep": "1.0.2",
"uglify-js": "2.8.29"
},
"devDependencies": {
"chai": "^2.3.0",
"grunt": "^0.4.5",
"grunt-blanket": "^0.0.8",
"grunt-contrib-clean": "^0.6.0",
"grunt-contrib-concat": "^0.5.1",
"grunt-contrib-copy": "^0.8.0",
"grunt-contrib-cssmin": "^0.12.2",
"grunt-contrib-htmlmin": "^0.4.0",
"grunt-contrib-jshint": "^0.11.2",
"grunt-contrib-less": "^1.0.1",
"grunt-contrib-uglify": "^0.9.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.8.0",
"grunt-usemin": "^3.0.0",
"grunt-webfont": "^0.5.2",
"matchdep": "^0.3.0",
"mocha": "^2.2.4",
"request": "^2.55.0",
"sinon": "^1.14.1",
"sinon-chai": "^2.7.0"
"chai": "~4.1.2",
"grunt": "~1.0.1",
"grunt-contrib-clean": "~1.1.0",
"grunt-contrib-concat": "~1.0.1",
"grunt-contrib-copy": "~1.0.0",
"grunt-contrib-cssmin": "~2.2.1",
"grunt-contrib-htmlmin": "~2.4.0",
"grunt-contrib-jshint": "~1.1.0",
"grunt-contrib-less": "~1.4.1",
"grunt-contrib-uglify": "~3.3.0",
"grunt-contrib-watch": "~1.0.0",
"grunt-env": "~0.4.4",
"grunt-express": "~1.4.1",
"grunt-filerev": "~2.3.1",
"grunt-inline-angular-templates": "~0.1.5",
"grunt-mocha-test": "~0.13.3",
"grunt-parallel": "~0.5.1",
"grunt-replace": "~1.0.1",
"grunt-usemin": "~3.1.1",
"grunt-webfont": "~1.6.0",
"matchdep": "~2.0.0",
"mocha": "~4.1.0",
"sinon": "~4.1.6",
"sinon-chai": "~2.14.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);
});
+8 -13
View File
@@ -2,27 +2,22 @@
# APT-GET
sudo apt-get update
sudo apt-get install lsb-release libfontconfig1 libfreetype6 -y --force-yes
sudo apt-get install curl git python-software-properties build-essential make g++ -y --force-yes
sudo apt-get install lsb-release libfontconfig1 libfreetype6 libjpeg-dev -y --force-yes > /dev/null 2>&1
sudo apt-get install curl git software-properties-common build-essential make g++ -y --force-yes > /dev/null 2>&1
# Installation of NodeJS
sudo add-apt-repository -y ppa:chris-lea/node.js
sudo apt-get update
sudo apt-get install -y nodejs
curl -sL https://deb.nodesource.com/setup_4.x | sudo -E bash -
sudo apt-get install -y nodejs > /dev/null 2>&1
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
sudo mkdir /space
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
sudo chown -R $USER /space
cd /space/YellowLabTools
npm install || exit 1
# Front-end compilation
grunt build
+6 -8
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
# Stop the maintenance mode and restart the server
forever stopall
NODE_ENV=production forever start -c "node --stack-size=262000" bin/server.js
+5 -2
View File
@@ -1,10 +1,13 @@
{
"serverPort": 80,
"phantomasEngine": "webkit",
"googleAnalyticsId": "",
"screenshotWidth": 400,
"baseUrl": "/",
"authorizedKeys": {
},
"maxAnonymousRunsPerDay": 99999999,
"maxAnonymousCallsPerDay": 99999999
"maxAnonymousCallsPerDay": 99999999,
"blockedUrls": []
}
+5 -2
View File
@@ -1,10 +1,13 @@
{
"serverPort": 8383,
"phantomasEngine": "webkit",
"googleAnalyticsId": "",
"screenshotWidth": 400,
"baseUrl": "/",
"authorizedKeys": {
},
"maxAnonymousRunsPerDay": 99999999,
"maxAnonymousCallsPerDay": 99999999
"maxAnonymousCallsPerDay": 99999999,
"blockedUrls": []
}

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