Compare commits

...
Author SHA1 Message Date
Gaël MétaisandGitHub 773249ca4f Revert "Introduce a new "Old Image Formats" rule" 2023-08-12 04:07:03 +02:00
Gaël MétaisandGitHub 599d02ea0e Merge pull request #387 from YellowLabTools/webp-avif
Introduce a new "Old Image Formats" rule
2023-07-30 22:47:10 +02:00
Gaël Métais 675316c9b7 Wording 2023-07-26 18:02:21 +02:00
Gaël Métais c22fd8b132 Add more information to the Old Image Formats offenders 2023-07-26 17:28:53 +02:00
Gaël Métais c4b008f910 New rule that converts images to WebP and AVIF and checks the gain 2023-07-26 16:04:20 +02:00
Gaël Métais 0cde5a3607 Add Avif to content type detection 2023-07-21 10:17:18 +02:00
Gaël Métais 6180b9f86c Replace Jimp by Sharp for image processing (faster + supports webp/avif) 2023-07-20 17:38:14 +02:00
Gaël MétaisandGitHub daae7d8523 Merge pull request #386 from YellowLabTools/aws
Starting to prepare version 3.0.0
2023-07-20 16:33:05 +02:00
Gaël Métais 540e5b5ac0 Version 3.0.0 2023-07-20 16:10:46 +02:00
Gaël MétaisandGitHub 4b671001e0 Merge branch 'develop' into aws 2023-07-20 15:39:06 +02:00
Gaël Métais 1ae4bed339 Update more dependencies 2023-07-19 22:01:29 +02:00
Gaël Métais 04e734bd5a Updating some dependancies 2023-07-19 00:14:36 +02:00
Gaël Métais a0e71ef444 No support for IE10 when minifying css files 2023-07-19 00:13:44 +02:00
Gaël Métais 05d8da36d8 Fix tests 2023-07-19 00:12:51 +02:00
Gaël Métais 5cae61b896 Making ylt server work on both normal and AWS machines 2023-07-16 10:47:58 +02:00
Gaël Métais a319e84831 v2.2.0 2022-02-24 14:27:05 +01:00
Gaël MétaisandGitHub d38781400e Merge pull request #359 from EXXETA/add-storage-handling
Storage handling feature from phantomas 2.4.0
2022-02-24 14:21:32 +01:00
Kevin Kammleiter cbcc6647f2 feat(storage-handling): passthrough session and local storage 2022-01-17 13:13:07 +01:00
Gaël Métais d2a217319d Bring back the screenshot handler code 2021-05-11 00:12:25 +01:00
Gaël Métais 280b564ab3 Fix bug when the node module is called without any option 2021-05-11 00:11:47 +01:00
Gaël Métais 0e14b5bd04 Remove Lambda specific files, they will be commited into a new repository YellowLabTools/YellowLabTools-lambda-agent 2021-05-10 22:34:38 +01:00
Gaël Métais 90ded3e1cb Merge split into aws branch 2021-05-10 21:53:08 +01:00
Gaël MétaisandGitHub e795b3369d Merge pull request #344 from YellowLabTools/develop
Split YLT into two repositories
2021-05-07 16:10:53 +01:00
Gaël Métais 2f26c7db96 Fix documentation after split 2021-05-07 16:03:39 +01:00
Gaël Métais ed588ba43c Export version number 2021-04-25 00:03:31 +01:00
Gaël Métais c9f63138a0 Split YLT into 2 projects. The server is now in the project YellowLabTools/YellowLabTools-server 2021-04-24 02:40:30 +01:00
Gaël Métais 97f779dc83 Remove uneeded module temporary 2021-04-24 01:18:39 +01:00
Gaël Métais 2753446a57 Remove unneeded package ps-node 2021-04-24 01:15:45 +01:00
Gaël Métais 163eef58c2 Business errors differenciated from technical errors in the Lambda runner 2021-04-11 13:14:35 +01:00
Gaël Métais 201bf75bf7 Fix unused unicode ranges metric 2021-04-04 23:43:55 +01:00
Gaël MétaisandGitHub dd4d0d4607 Merge pull request #341 from YellowLabTools/docker
Removing unused .dockerignore file
2021-03-22 02:14:59 +00:00
Ben Younes Ousama 88bbaea4c9 Removing unused .dockerignore file 2021-03-22 02:41:31 +01:00
Gaël Métais ac37ebb4e2 Fix crash 2021-03-22 00:10:31 +00:00
Gaël Métais 16c3abf3f2 Remove JSHint from build steps 2021-03-22 00:06:18 +00:00
Gaël Métais d1681c3d88 Temporarily remove queue messages 2021-03-21 23:53:04 +00:00
Gaël Métais 97099e9f65 Add runId quickfix 2021-03-21 23:15:32 +00:00
Gaël Métais 4e8cb19f55 Add runId in the results 2021-03-21 23:13:56 +00:00
Gaël MétaisandGitHub f7280c1a90 Merge pull request #340 from YellowLabTools/docker
Promote the Docker image
2021-03-21 20:28:44 +00:00
Gaël Métais bfd6c5d2bd Promote the Docker image as a simple way to install YLT 2021-03-21 20:23:16 +00:00
Gaël Métais 47eab96a67 Two version of the API: normal/lambda. Will be merged later 2021-03-20 18:24:11 +00:00
Gaël Métais ec7cba8f49 Disable browser cache in Docker 2021-03-20 18:21:28 +00:00
Gaël Métais e0d497796a Save screenshot when called with a saveFile option 2021-03-16 07:45:49 +00:00
Gaël Métais c5bc02d45d Move settings invocation to init phase 2021-03-16 07:45:10 +00:00
Gaël Métais e2b713282c Fix error Service should be called with new 2021-03-16 07:44:18 +00:00
Gaël Métais 05c883fa3e Temporarily use a fork of Phantomas for Characters Count 2021-03-15 23:22:22 +00:00
Gaël Métais e5d9680b23 Merge branch 'aws' of https://github.com/YellowLabTools/YellowLabTools into aws 2021-03-15 23:20:54 +00:00
Gaël MétaisandGitHub 4a40483348 Merge pull request #337 from ohoareau/master
feat: add Dockerfile, Makefile and lambda handler to create image for AWS Lambda
2021-03-15 23:20:35 +00:00
Gaël MétaisandGitHub 5f12d1ce0e Merge branch 'aws' into master 2021-03-15 23:19:26 +00:00
Gaël Métais 050b2303f0 Merge branch 'master' into aws 2021-03-15 23:17:37 +00:00
Gaël MétaisandGitHub a579f5736b Merge pull request #338 from YellowLabTools/develop
Merge develop into master
2021-03-15 23:08:02 +00:00
Olivier Hoareau 82294983b0 feat: add Dockerfile to create image for Lambda 2021-03-15 11:25:08 +01:00
Gaël Métais 8558ea29df Run YLT on AWS Lambda. TODO: should work both ways 2021-03-15 01:49:28 +00:00
Gaël Métais 9211074b32 It is now possible to save the results to an AWS S3 bucket 2021-03-14 01:49:23 +00:00
Gaël Métais 2da4371847 The screenshot is now saved in the system /tmp folder 2021-03-14 01:35:26 +00:00
Gaël Métais 791bf9fc8d Remove Yarn.lock 2021-03-13 23:58:56 +00:00
Gaël Métais e4b324d209 Clean cli.js 2021-03-13 23:57:06 +00:00
Gaël Métais 3b0bd172ba Clean results from redownload details 2021-03-13 16:08:31 +00:00
Gaël Métais 91e1724540 Get ready for Phantomas 2.2 2021-03-13 11:17:58 +00:00
Gaël Métais fcdf3c9240 Fix monitoring being blocked 2021-03-11 09:56:41 +00:00
Gaël Métais 92d92b8021 Remove API limits for /api/runs in GET mode 2021-03-10 07:11:13 +00:00
Gaël Métais dc5bd2e71e Add last test launch time in the new /api/runs route 2021-03-10 00:54:50 +00:00
Gaël Métais 36fc0f0e10 Working on implementing unicode ranges again 2021-03-07 11:41:00 +00:00
Gaël Métais 52a781dee8 Start working on CDN detection 2021-03-07 09:48:29 +00:00
Gaël Métais 15d60a3a7b Working on implementing unicode ranges again 2021-03-07 09:45:56 +00:00
Gaël Métais 0a1b9ce27a Fix JS error offenders 2021-03-02 00:19:56 +00:00
Gaël Métais bb9429e907 Update Imagemin and dependencies 2021-03-01 02:50:23 +00:00
Gaël Métais 1213854644 Update Imagemin and dependencies 2021-03-01 01:31:35 +00:00
Gaël Métais e3d4100055 Update ttf2woff2 library 2021-03-01 01:18:48 +00:00
Gaël Métais 418426419d Improve compression detection 2021-02-28 13:59:16 +00:00
Gaël Métais 707048a1df Change Brotli wording 2021-02-26 16:22:29 +00:00
Gaël Métais e9ab1fd16e Sorry, I should have kept is-json package 2021-02-25 15:21:45 +00:00
Gaël Métais cc74359e83 Fix SSL errors not being correctly ignored 2021-02-20 13:02:14 +00:00
Gaël Métais 9be862e37b Remove is-json package 2021-02-19 20:08:13 +00:00
Gaël Métais 2133c1e16a Merge branch 'develop' of https://github.com/YellowLabTools/YellowLabTools into develop 2021-02-19 20:04:08 +00:00
Gaël Métais e46d4cff27 Fix offenders not being displayed for globalVariables and synchronousXHR 2021-02-19 20:03:02 +00:00
Gaël Métais 38b362e5fe Comment typo 2021-02-15 22:46:42 +00:00
Gaël Métais 6d01461e53 Improve JSON detection when the mime-type is not application/json 2021-02-14 18:32:05 +00:00
Gaël MétaisandGitHub 942dd89ea3 Merge pull request #327 from YellowLabTools/develop
The v2.0 beta version is almost finished
2021-02-03 23:21:53 +00:00
Gaël MétaisandGitHub d40d4ec7fb Merge branch 'master' into develop 2021-02-03 23:21:30 +00:00
Gaël Métais 96f720c195 Update latest jQuery version 2021-02-03 21:35:33 +00:00
Gaël Métais 3f5c54d5de Remove the Tweet this button 2021-02-02 00:24:26 +00:00
Gaël Métais c62805ae57 Slightly increase DOM max depth threshold 2021-02-01 23:44:52 +00:00
Gaël Métais acb1b72036 Remove the CSS Expressions metric, too rare 2021-02-01 16:22:17 +00:00
Gaël Métais 443f452689 Fix a bug with the favicon spinner 2021-02-01 15:10:30 +00:00
Gaël Métais 72a347aa9f Fix bug with EJS on production env 2021-01-31 01:42:03 +00:00
Gaël Métais ab7ffa0d94 Replace grunt-replace with EJS 2021-01-31 00:29:37 +00:00
Gaël Métais 1259439603 Fix device icons on Safari 2021-01-31 00:09:52 +00:00
Gaël Métais 208ed63c96 Fix TTF files not being converted to WOFF2 2021-01-30 23:56:03 +00:00
Gaël Métais dd439d5ec3 Don't try to convert huge fonts to woff2, otherwise the server freezes for too long 2021-01-30 23:55:28 +00:00
Gaël Métais 44a14f326b Add tmp/ to .gitignore 2021-01-30 23:05:27 +00:00
Gaël Métais 4d4a9523f8 Wording 2021-01-25 12:24:38 +00:00
Gaël Métais b24a676f22 Change about page's display on mobile 2021-01-24 14:34:06 +00:00
Gaël Métais 4e692d608c Various wordings changed on home and about pages 2021-01-24 14:10:02 +00:00
Gaël Métais b54eb0cca9 New API entry that counts the number of pending tests 2021-01-24 00:30:28 +00:00
Gaël Métais 24ad2f45cc Replace the recommended lazyloading library 2021-01-24 00:29:45 +00:00
Gaël Métais 27b5ce8d60 Removing the ability to scroll on the screenshot as they will only show above the fold 2021-01-22 15:57:03 +00:00
Gaël Métais 1ce350b12e Chromium requires more package dependencies than before 2021-01-22 15:00:15 +00:00
Gaël Métais 405d53220b Fix server stability issue 2021-01-14 00:24:22 +00:00
Gaël Métais 3123e90d5a Make the Similar colors detector more relax 2021-01-12 09:57:50 +00:00
Gaël Métais ea7a9811e4 Improve WP detection 2021-01-10 23:58:05 +00:00
Gaël Métais 74b04ff922 Improve WP detection 2021-01-10 23:50:44 +00:00
Gaël Métais 4b24cd68ce Debugging 2021-01-10 23:39:54 +00:00
Gaël Métais c94089842a Improve wording 2021-01-09 22:12:13 +00:00
Gaël Métais 9c0743d668 Fix UI on the Brotli rule 2021-01-09 22:05:27 +00:00
Gaël Métais d023af20a0 Add a Buy me a coffee button 2021-01-08 02:52:47 +00:00
Gaël Métais b81ddd8dd9 Add a Buy me a coffee button 2021-01-08 02:51:59 +00:00
Gaël Métais c1e7f58567 Add a Buy me a coffee button 2021-01-08 02:50:58 +00:00
Gaël Métais 8c055d0b72 Adding a bunch of debian packages required by Chrome Headless 2021-01-08 02:44:53 +00:00
Gaël Métais 44a30f7a0d Sponsorship 2021-01-08 02:43:10 +00:00
Gaël Métais f837ad2270 Probably fixes a bug that stucks the server 2021-01-08 02:42:01 +00:00
Gaël Métais 6b909bd0e3 Clean up error 500 page 2021-01-07 21:31:37 +00:00
Gaël Métais 23d7dea0bb Improve WordPress detection 2021-01-07 21:14:35 +00:00
Gaël MétaisandGitHub fc13053545 Merge pull request #322 from YellowLabTools/ylt-v2
YellowLab Tools V2 - beta version
2021-01-06 23:28:49 +00:00
Gaël MétaisandGitHub e874ccb5b1 Merge branch 'develop' into ylt-v2 2021-01-06 23:14:34 +00:00
Gaël Métais fd11e8c1fe In some rare cases, the HTML minifier can be very slow, timeouting other simultaneous requests 2021-01-05 22:19:02 +00:00
Gaël Métais 294c5ab06d Adding favicon animation and color codes 2021-01-03 21:44:47 +00:00
Gaël Métais 576dac3aff Adding favicon animation and color codes 2021-01-03 19:06:55 +00:00
Gaël Métais 4b80310c0d Fix settings files 2021-01-03 01:49:11 +00:00
Gaël Métais bd9cb2e257 Add a progress bar on the UI 2021-01-03 01:43:46 +00:00
Gaël Métais 64733d55f8 Wording 2021-01-02 22:24:42 +00:00
Gaël Métais 88483af607 Detect WordPress for optional sponsoring message 2021-01-02 17:14:51 +00:00
Gaël Métais 1726e023ea Rewording 2021-01-02 15:57:00 +00:00
Gaël Métais 7e9b12f818 Adjust scoring 2020-12-30 00:52:18 +01:00
Gaël Métais 834a9c9ac5 Remove the Small Requests rule 2020-12-30 00:39:56 +01:00
Gaël Métais 3f098cd33a Give less score weight to requests number 2020-12-30 00:37:33 +01:00
Gaël Métais 4193d61e0b Remove uselss code 2020-12-30 00:26:36 +01:00
Gaël Métais e8d464a244 If you fix this rule, your global score increases by X points 2020-12-30 00:21:29 +01:00
Gaël Métais 4c1c375eef Fix tests starting before the end of the previous one 2020-12-29 22:45:29 +01:00
Gaël Métais 65a2bbae0a Adjust scores to nowadays browsers and network 2020-12-29 11:56:54 +01:00
Gaël Métais 57616aba62 Fix wording 2020-12-29 11:55:57 +01:00
Gaël Métais 4396bd4625 White background not needed & optimize images more 2020-12-29 11:55:22 +01:00
Gaël Métais df4f5685be New metric Total JS execution time 2020-12-23 15:24:48 +01:00
Gaël Métais 1e5f797195 Fix http2 offenders page 2020-12-22 12:27:35 +01:00
Gaël Métais 8f06ceaedd Compare minification with brotli instead of gzip 2020-12-22 00:00:44 +01:00
Gaël Métais 6b58b72028 Replace brotli module by zlib 2020-12-21 23:59:39 +01:00
Gaël Métais 51dc1b1618 Add Brotli compression as the default compresion 2020-12-21 10:21:20 +01:00
Gaël Métais be5549bf81 Replace the HTML minifier so that it minifies inline JS and CSS 2020-12-21 02:14:30 +01:00
Gaël Métais c4b7bd4d71 Fix error 2020-12-21 02:09:24 +01:00
Gaël Métais f25eb2901e Remove old phantomas modules 2020-12-21 01:32:43 +01:00
Gaël Métais 65025b57c5 Fix syntax error 2020-12-21 01:28:01 +01:00
Gaël Métais 8e3b212df0 Linting 2020-12-21 01:27:34 +01:00
Gaël Métais d93af3d6e4 Detect non WOFF2 fonts and estimate gain 2020-12-21 01:18:56 +01:00
Gaël Métais dd0c17a9d4 Fix content type checking after switching to phantomas v2 2020-12-19 23:44:36 +01:00
Gaël Métais d334726991 Fix offendersdisplay on jquery versions page 2020-11-26 19:58:46 +01:00
Gaël Métais 9aea4e5677 New metrics: old HTTP and TLS protocols 2020-11-26 01:37:58 +01:00
Gaël Métais ffd9efeb98 New DOMaccess agregated form + new score category JS complexity 2020-11-24 00:18:55 +01:00
Gaël Métais 5bf3bd7ce5 Fix a potential divided by O error 2020-11-23 13:49:04 +01:00
Gaël Métais 693fce1bb1 Flag as alpha version 2020-11-10 15:00:02 +01:00
Gaël Métais 48fbb1076f Adjust design 2020-10-06 08:30:48 +02:00
Gaël Métais 7bc994b99f Documentation 2020-10-06 00:59:14 +02:00
Gaël Métais 8586d1cc12 Documentation 2020-10-06 00:58:36 +02:00
Gaël Métais de280c23ea Increase timeout to 2 minutes 2020-10-06 00:55:29 +02:00
Gaël Métais 06887003cf lib/tools/phantomas/phantomasWrapper.js 2020-10-06 00:54:54 +02:00
Gaël Métais e2eacbc051 Responsive design for mobile (finally) 2020-10-05 12:49:26 +02:00
Gaël Métais 93b81493c0 Graphical changes, essentially colors 2020-10-04 18:50:28 +02:00
Gaël Métais 4287f60ab3 Adapt Oversized Images detection to our new screen resolutions 2020-10-04 13:23:43 +02:00
Gaël Métais f62e9baa4a Add a new Desktop HP device (large DPR2 screen) 2020-10-04 10:09:06 +02:00
Gaël Métais 2d2da6e939 Fix icons 2020-10-04 02:57:53 +02:00
Gaël Métais 2bf3d5372b Fix screenshots and continue implementing phantomas v2 2020-10-04 02:54:00 +02:00
Gaël Métais a4f8e20293 Fix screenshots and continue implementing phantomas v2 2020-10-04 02:44:16 +02:00
Gaël Métais ac915e8973 Fix screenshots and continue implementing phantomas v2 2020-10-04 02:43:10 +02:00
Gaël Métais e19a521c4e Various adaptations for Phantomas v2 2020-08-11 03:01:43 +02:00
Gaël Métais bafea756ee Add brotli decompression to redownload.js 2020-08-11 02:04:59 +02:00
Gaël Métais 1bdfc8a5d2 Rename blacklist and whitelist into block list and allowlist 2020-08-11 01:37:32 +02:00
Gaël Métais 6e6285c5ae Various adaptations for Phantomas v2 2020-08-10 13:56:37 +02:00
Gaël Métais f49411b3a9 Various adaptations for Phantomas v2 2020-08-10 01:03:42 +02:00
Gaël Métais 1d3dee1950 Fix image optimization 2020-08-09 02:06:20 +02:00
Gaël Métais caf3ddd68a Font related stuff not needed anymore 2020-08-09 01:52:23 +02:00
Gaël Métais 17b5168bde Node compatibility set to >=12.0 2020-08-09 01:28:25 +02:00
Gaël Métais 9bfc16a4d5 Get rid of the icons font 2020-08-09 01:13:09 +02:00
Gaël Métais 98f5245317 Create an error page in case YLT does not respond (allong with nginx instructions) 2020-08-08 09:55:07 +02:00
Gaël Métais 4286c528f5 Fix GitHub button size 2020-08-08 09:52:51 +02:00
Gaël MétaisandGitHub 8f5f9d6238 1.13.4 (#305)
* Update GitHub repository URL
* Update jQuery version
2020-02-04 12:40:57 -03:00
Gaël Métais 26d3a53d44 Merge branch 'master' into develop 2020-02-04 12:37:04 -03:00
Gaël Métais de8e444aba Update jQuery version 2020-02-04 12:36:45 -03:00
Gaël Métais 35da556697 Update GitHub repository URL 2020-02-04 12:36:10 -03:00
PurpleBabarandGaël Métais 52ac5e5424 Dockerize app (#303)
Add a Dockerfile to the project
2020-01-17 11:10:53 -03:00
Gaël Métais 517206364a Work in progress: switch to Phantomas v2 2019-03-14 03:27:37 +01:00
Gaël Métais 5e82eea9f1 Remove JS Timeline and Scroll Bottlenecks 2019-03-13 19:27:57 +01:00
Gaël MétaisandGitHub b6e8768767 Update dependencies (#280)
* Remove an always failing test
* Update dependencies
* Revert 'temporary' npm package to v0.0.8
* Update Node version in Vagrantfile
2019-03-13 15:51:12 +00:00
Gaël Métais 5df3d4f63b Add package-lock.json to .gitignore 2019-02-18 12:57:19 +01:00
Gaël Métais c59e9fc3f5 Merge branch 'develop' 2019-01-13 17:39:27 +05:30
Gaël Métais febe9871b9 Fix missing text 2019-01-13 17:39:11 +05:30
Gaël Métais 4d5d881af6 Merge branch 'develop' 2019-01-13 17:21:48 +05:30
Gaël Métais 2735deff73 v1.13.3 2019-01-13 17:21:29 +05:30
Gaël Métais ee93add6fc Remove failing 4.7 tests on Travis 2019-01-13 17:19:54 +05:30
Gaël MétaisandGitHub 8ffab2ae70 New rule: oversized images (#276)
New rule: oversized images
2019-01-13 17:18:03 +05:30
Gaël Métais fa216dddd5 Fix typo 2019-01-07 12:59:57 +05:30
Gaël Métais 34ec39a858 Merge branch 'develop' 2018-04-05 20:31:24 -05:00
Gaël Métais 7dd611b61a Fix production env 2018-04-05 20:31:09 -05:00
Gaël Métais 5d017258b7 Add sponsoring slots on home, dashboard and about pages 2018-04-05 19:16:59 -05:00
Gaël Métais e58c00ecdf v1.13.2 2018-04-05 19:16:59 -05:00
Gaël Métais b88848ba8a Fix test timeout 2018-04-05 19:16:59 -05:00
Gaël Métais 6b6e7c647a Fix IP address filtering when behind a proxy 2018-04-05 19:16:59 -05:00
Gaël Métais b972422cc6 Getting ready for HTTPS & HTTP/2 2018-04-05 19:16:59 -05:00
Gaël Métais 52ded23315 Fix about text 2018-04-05 19:16:59 -05:00
Gaël Métais b3399f419e Add sponsoring slots on home, dashboard and about pages 2018-04-05 19:07:38 -05:00
Gaël Métais e2d60dde43 v1.13.2 2018-04-04 16:10:15 -05:00
Gaël Métais 214b3c75d0 Fix test timeout 2018-04-01 00:31:52 -05:00
Gaël Métais 95659839b2 Fix IP address filtering when behind a proxy 2018-04-01 00:31:52 -05:00
Gaël Métais cab3f57737 Getting ready for HTTPS & HTTP/2 2018-04-01 00:31:52 -05:00
Gaël Métais dbbd01a83d Fix about text 2018-03-28 20:10:56 -05:00
Gaël Métais 90e66841df v1.13.1 2018-03-17 21:14:25 -05:00
Gaël MétaisandGitHub 5ba41c6ea0 Merge pull request #266 from hackedd/github-button-ssl
Load Github buttons over HTTPS
2018-03-05 08:53:14 -06:00
Gaël MétaisandGitHub 63c0de42db Merge pull request #265 from hackedd/ga-undefined
Fix "ReferenceError: ga is not defined"
2018-03-05 08:52:21 -06:00
Paul Hooijenga 5646a22245 Fix "ReferenceError: ga is not defined" 2018-03-05 09:43:13 +01:00
Paul Hooijenga 0e13801a7a Load Github buttons over HTTPS 2018-03-05 09:39:18 +01:00
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
Gaël Métais 1057836788 Merge pull request #69 from gmetais/develop
v1.4.0
2015-04-27 19:29:53 +02:00
Gaël Métais 93f8702962 v1.4.0 2015-04-27 19:17:13 +02:00
Gaël Métais 3d71454666 Merge pull request #68 from gmetais/mobile
Fixes the first part of #14.
2015-04-27 19:11:32 +02:00
Gaël Métais c0cc2744c6 Add a test 2015-04-27 18:53:18 +02:00
Gaël Métais 65b1c34d06 Add smartphone & tablet shapes arround screenshots 2015-04-27 18:45:08 +02:00
Gaël Métais 67615719ae Add device selection option to CLI 2015-04-27 18:43:26 +02:00
Gaël Métais 491c8ebe77 Add a device option to ylt's core 2015-04-27 18:43:04 +02:00
Gaël Métais cae23d61e2 Add a device selector on the launching page 2015-04-27 15:40:46 +02:00
Gaël Métais 9cad9ad2f9 Replace grunt-fontsmith by front-webfont + new icons 2015-04-27 14:05:36 +02:00
Gaël Métais ba977d6b49 Change submit button color on focus 2015-04-27 11:22:04 +02:00
Gaël Métais 88bb2f91a5 Change submit button color on focus 2015-04-27 11:19:25 +02:00
Gaël Métais dda11627f8 Update dependencies 2015-04-27 10:20:24 +02:00
Gaël Métais 2df713ca0c Make it clearer the project is open-source 2015-04-25 18:38:58 +02:00
Gaël Métais 486ede9396 Promote the Grunt plugin 2015-04-25 18:38:41 +02:00
Gaël Métais 72afcc3e5a Enabling spy when checking for hiddenContentSize 2015-04-25 18:08:28 +02:00
Gaël Métais ef2eaceb88 Remove evalCalls from policies (phantomas bug) 2015-04-25 18:03:56 +02:00
Gaël Métais c2db1c70e3 Fix .text considered reading when called with an empty string 2015-04-25 18:02:48 +02:00
146 changed files with 8068 additions and 9003 deletions
-3
View File
@@ -1,3 +0,0 @@
{
"directory" : "bower_components"
}
+6 -6
View File
@@ -1,9 +1,9 @@
node_modules
bower_components
package-lock.json
.tmp
.vagrant
results/*
tmp
coverage
front/build
har.json
package-lock.json
yarn.lock
har.json
.idea/
-7
View File
@@ -1,7 +0,0 @@
language: node_js
node_js:
- "0.10.33"
before_install:
- "npm install -g grunt-cli"
- "npm install -g phantomjs"
install: npm install
+1
View File
@@ -0,0 +1 @@
ko_fi: gaelmetais
-359
View File
@@ -1,359 +0,0 @@
module.exports = function(grunt) {
// Load all grunt modules
require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks);
// Tell our Express server that Grunt launched it
process.env.GRUNTED = true;
// Project configuration.
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
settings: grunt.file.readJSON('./server_config/settings.json'),
font: {
icons: {
src: ['front/src/fonts/svg-icons/*.svg'],
destCss: 'front/src/less/icons.less',
destFonts: 'front/src/fonts/icons.woff',
// Optional: Custom routing of font filepaths for CSS
cssRouter: function (fontpath) {
var pathArray = fontpath.split('/');
var fileName = pathArray[pathArray.length - 1];
return '/fonts/' + fileName;
}
}
},
less: {
all: {
files: [
{
expand: true,
cwd: 'front/src/less/',
src: ['**/*.less'],
dest: 'front/src/css/',
ext: '.css'
}
]
}
},
replace: {
dist: {
options: {
patterns: [
{
match: 'googleAnalyticsId',
replacement: '<%= settings.googleAnalyticsId %>'
},
{
match: 'version',
replacement: 'v<%= pkg.version %>'
}
]
},
files: [
{expand: true, flatten: true, src: ['front/src/main.html'], dest: 'front/build/'}
]
}
},
jshint: {
all: [
'*.js',
'app/lib/*.js',
'bin/*.js',
'lib/**/*.js',
'app/nodeControllers/*.js',
'app/public/scripts/*.js',
'phantomas_custom/**/*.js',
'test/api/*.js',
'test/core/*.js',
'test/fixtures/*.js',
'front/src/js/**/*.js'
]
},
clean: {
tmp: {
src: ['.tmp']
},
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/fonts/icons.woff'], dest: './front/build/fonts/icons.woff'},
{src: ['./front/src/img/favicon.png'], dest: './front/build/img/favicon.png'},
{src: ['./front/src/img/logo-large.png'], dest: './front/build/img/logo-large.png'},
]
}
},
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']
},
'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']
}
},
env: {
dev: {
NODE_ENV: 'development'
},
built: {
NODE_ENV: 'production'
}
},
express: {
dev: {
options: {
port: 8383,
server: './bin/server.js',
serverreload: true,
showStack: true
}
},
built: {
options: {
port: 8383,
server: './bin/server.js',
serverreload: true,
showStack: true
}
},
test: {
options: {
port: 8387,
server: './coverage/bin/server.js',
showStack: true
}
},
'test-current-work': {
options: {
port: 8387,
server: './bin/server.js',
showStack: true
}
},
testSuite: {
options: {
port: 8388,
bases: 'test/www'
}
}
},
useminPrepare: {
html: './front/src/main.html',
options: {
dest: './front/build',
root: ['./', './front/src']
}
},
usemin: {
html: './front/build/main.html',
css: './front/build/css/*.css',
options: {
assetsDirs: ['front/build'],
patterns: {
css: [[/(\/fonts\/icons\.woff)/gm, 'Replacing reference to icons.woff']]
}
}
},
htmlmin: {
options: {
removeComments: true,
collapseWhitespace: true,
conservativeCollapse: true
},
main: {
files: [{
expand: true,
cwd: './front/build/',
src: 'main.html',
flatten: true,
dest: './front/build'
}]
},
views: {
files: [{
expand: true,
cwd: './front/src/views',
src: '*.html',
flatten: true,
dest: '.tmp/views/'
}]
}
},
inline_angular_templates: {
build: {
options: {
base: '.tmp',
method: 'append'
},
files: {
'./front/build/main.html': ['.tmp/views/*.html']
}
}
},
filerev: {
options: {
algorithm: 'md5',
length: 8
},
assets: {
src: './front/build/*/*.*'
}
}
});
// 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');
});
grunt.registerTask('icons', [
'font:icons',
'less',
'clean:tmp'
]);
grunt.registerTask('build', [
'jshint',
'clean:build',
'copy:build',
'less',
'useminPrepare',
'concat',
'uglify',
'cssmin',
'replace',
'htmlmin:views',
'inline_angular_templates',
'filerev',
'usemin',
'htmlmin:main',
'clean:tmp'
]);
grunt.registerTask('hint', [
'jshint'
]);
grunt.registerTask('dev', [
'env:dev',
'express:dev'
]);
grunt.registerTask('built', [
'env:built',
'express:built'
]);
grunt.registerTask('test', [
'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', [
'jshint',
'express:testSuite',
'clean:coverage',
'copy-test-server-settings',
'express:test-current-work',
'mochaTest:test-current-work',
'clean:tmp'
]);
};
+67 -33
View File
@@ -1,57 +1,91 @@
# 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="https://yellowlab.tools" target="_blank">https://yellowlab.tools</a>
</td>
<td width="30%">
<img src="./doc/img/home.png"></img>
</td>
</tr>
<tr>
<td width="70%">
The <b>Docker image</b> - <a href="https://github.com/ousamabenyounes/docker-yellowlabtools" target="_blank">ousamabenyounes/docker-yellowlabtools</a>
<br>
Your own private instance of Yellow Lab Tools, on your computer.
</td>
<td width="30%">
<img src="./doc/img/docker-logo.png"></img>
</td>
</tr>
<tr>
<td width="70%">
The <b>CLI</b> (Command Line Interface) - <a href="https://github.com/YellowLabTools/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>NodeJS</b> package - <a href="https://github.com/YellowLabTools/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/YellowLabTools/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 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!!!
By the way, it's free because I am a geek, not businessmen. In return, you can add a ★ on GitHub, it will boost my motivation to add more awesome features!!!
## 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.
#### 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).
![example dashboard screenshot](./doc/img/screenshot.png)
## Install your own private instance
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).
If your project is not accessible from outside or if you want to test your localhost, you might want to run your own instance of Yellow Lab Tools.
The classical way is to clone the YLT server's GitHub repository and run it on Linux or MacOS. The documentation is [here](https://github.com/YellowLabTools/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/)
The new recommended solution is to run Yellow Lab Tools inside a Docker virtual machine. My friend Ousama Ben Younes maintains [this ready-to-use Docker image based on Alpine](https://github.com/ousamabenyounes/docker-yellowlabtools)).
## 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 Yellow Lab Tools!
<a href='https://ko-fi.com/gaelmetais' target='_blank'><img height='35' style='border:0px;height:46px;' src='https://az743702.vo.msecnd.net/cdn/kofi3.png?v=0' border='0' alt='Buy me a coffee' /><a>
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)
- Km [camlafit](https://github.com/camlafit)
- Vincent L. [magikcypress](https://github.com/magikcypress)
- Ousama Ben Younes [ousamabenyounes](https://github.com/ousamabenyounes)
Vendored
-17
View File
@@ -1,17 +0,0 @@
Vagrant.configure("2") do |config|
config.vm.box = "precise64"
config.vm.box_url = "http://files.vagrantup.com/precise64.box"
config.vm.network :private_network, ip: "10.10.10.10"
config.ssh.forward_agent = true
config.vm.provider :virtualbox do |vb|
vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
vb.customize ["modifyvm", :id, "--memory", 512]
vb.customize ["modifyvm", :id, "--cpus", 2]
end
config.vm.provision :shell, :path => "server_config/server_install.sh"
end
Regular → Executable
+74 -12
View File
@@ -1,10 +1,11 @@
#!/usr/bin/env node
var debug = require('debug')('ylt:cli');
var meow = require('meow');
var path = require('path');
var debug = require('debug')('ylt:cli');
var meow = require('meow');
var path = require('path');
var EasyXml = require('easyxml');
var ylt = require('../lib/index');
var ylt = require('../lib/index');
var cli = meow({
help: [
@@ -12,19 +13,28 @@ var cli = meow({
' yellowlabtools <url> <options>',
'',
'Options:',
' --device Simulates a device. Choose between phone (default), tablet, desktop and desktop-hd.',
' --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.',
' --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.',
' --allow-domain Only allow requests to given (comma-separated) domains.',
' --no-externals Block all domains except the main one.',
' --reporter The output format: "json" or "xml". Default is "json".',
' --local-storage Ability to set a local storage, key-value pairs (e.g. "bar=foo;domain=url")',
' --session-storage Ability to set a session storage, key-value pairs (e.g. "bar=foo;domain=url")',
''
].join('\n'),
pkg: '../package.json'
pkg: require('../package.json')
});
// Check parameters
if (cli.input.length < 1) {
console.error('Incorrect parameters: url not provided');
process.exit(1);
cli.showHelp();
}
var url = cli.input[0];
@@ -45,9 +55,35 @@ 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 || 'mobile';
// 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;
// Storage injection
options.localStorage = cli.flags.localStorage;
options.sessionStorage = cli.flags.sessionStorage;
// 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);
}
@@ -59,7 +95,33 @@ if (cli.flags.jsDeepAnalysis === true || cli.flags.jsDeepAnalysis === 'true') {
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;
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) {
-38
View File
@@ -1,38 +0,0 @@
var express = require('express');
var app = express();
var server = require('http').createServer(app);
var bodyParser = require('body-parser');
var compress = require('compression');
var cors = require('cors');
var authMiddleware = require('../lib/server/middlewares/authMiddleware');
var apiLimitsMiddleware = require('../lib/server/middlewares/apiLimitsMiddleware');
// Middlewares
app.use(compress());
app.use(bodyParser.json());
app.use(cors());
app.use(authMiddleware);
app.use(apiLimitsMiddleware);
// Initialize the controllers
var apiController = require('../lib/server/controllers/apiController')(app);
var frontController = require('../lib/server/controllers/frontController')(app);
// Let's start the server!
if (!process.env.GRUNTED) {
var settings = require('../server_config/settings.json');
server.listen(settings.serverPort, function() {
console.log('Listening on port %d', server.address().port);
// For the tests
if (server.startTests) {
server.startTests();
}
});
}
module.exports = app;
-13
View File
@@ -1,13 +0,0 @@
{
"name": "yellowlabtools",
"dependencies": {
"angular": "~1.3.14",
"angular-route": "~1.3.14",
"angular-resource": "~1.3.14",
"angular-sanitize": "~1.3.14",
"angular-animate": "~1.3.14"
},
"resolutions": {
"angular": "~1.3.8"
}
}
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: 22 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

-10
View File
@@ -1,10 +0,0 @@
.about {
margin: 3em auto;
width: 50%;
}
.about p {
margin: 2em;
}
.about a {
color: #FFF;
}
-179
View File
@@ -1,179 +0,0 @@
.testedUrl {
color: inherit;
}
.summary {
text-align: center;
}
.summary .globalScore {
display: table;
width: 60%;
margin: 3em auto;
}
.summary .globalScore > div {
display: table-cell;
width: 50%;
vertical-align: middle;
}
.summary .globalScore .globalGrade {
margin: 0.5 auto;
width: 2.5em;
height: 2.5em;
line-height: 2.5em;
border-radius: 0.5em;
font-size: 3em;
font-weight: bold;
vertical-align: middle;
}
.summary .globalScore .on100 {
font-size: 1.2em;
font-weight: bold;
margin: 0.5em 0 1em;
}
.summary .globalScore .screenshotWrapper:hover {
opacity: 0.75;
}
.summary .globalScore .screenshotWrapper:hover:after {
position: absolute;
width: 1.25em;
height: 1.25em;
top: 0.7em;
left: 1.55em;
font-size: 3em;
color: #FFF;
background: #000;
border-radius: 0.2em;
text-align: center;
content: "+";
opacity: 0.85;
}
.summary .notations {
display: table;
width: 80%;
margin: 0 10% 1.5em;
border-spacing: 1em;
}
.summary .notations > div {
display: table-row;
}
.summary .notations > div > div {
display: table-cell;
height: 2.5em;
vertical-align: middle;
}
.summary .notations .category {
font-weight: bold;
text-align: center;
width: 20%;
}
.summary .notations .criteria {
font-weight: normal;
width: 75%;
}
.summary .notations .A.categoryScore,
.summary .notations .B.categoryScore,
.summary .notations .C.categoryScore,
.summary .notations .D.categoryScore,
.summary .notations .E.categoryScore,
.summary .notations .F.categoryScore,
.summary .notations .NA.categoryScore {
width: 2.5em;
max-width: 2.5em;
min-width: 2.5em;
font-size: 2em;
text-align: center;
border-radius: 0.5em;
font-weight: bold;
}
.summary .notations .grade .A,
.summary .notations .grade .B,
.summary .notations .grade .C,
.summary .notations .grade .D,
.summary .notations .grade .E,
.summary .notations .grade .F,
.summary .notations .grade .NA {
width: 1em;
height: 1em;
font-size: 1em;
color: transparent;
margin: 0 auto;
border-radius: 0.5em;
}
.summary .notations .criteria .table {
width: 100%;
}
.summary .notations .criteria .table > div:hover > div {
background: #EBD8E2;
cursor: pointer;
}
.summary .notations .criteria .table > div:hover > div.info {
background: #FFF;
}
.summary .notations .criteria .table > div:hover > div.info .icon-question {
color: #EBD8E2;
}
.summary .notations .criteria .grade {
width: 10%;
padding-left: 0.5em;
padding-right: 0.5em;
vertical-align: middle;
}
.summary .notations .criteria .label {
width: 70%;
}
.summary .notations .criteria .result {
width: 18%;
font-weight: bold;
white-space: nowrap;
text-align: center;
vertical-align: middle;
}
.summary .notations .warning .label,
.summary .notations .warning .result,
.summary .notations .icon-warning {
color: #FF1919;
}
.summary .notations .criteria .info {
width: 2%;
text-align: center;
vertical-align: middle;
background: #FFF;
padding-left: 0.1em;
padding-right: 0.1em;
}
.summary .notations .criteria .icon-question {
color: transparent;
}
.summary .fromShare {
margin-bottom: 3em;
}
.summary .apiTip {
font-size: 0.8em;
margin-bottom: 4em;
color: #413;
}
.summary .apiTip a {
color: inherit;
}
.summary .tweet .tweetText {
color: #413;
background: #F2F2F2;
border: none;
width: 25em;
padding: 0.4em;
border-radius: 0.5em;
box-shadow: 0.05em 0.1em 0 0 #999;
}
.summary .tweet .tweetButton,
.summary .tweet .linkedinButton {
color: #413;
background: #F2F2F2;
margin-right: 0;
}
.summary .tweet .tweetButton:hover,
.summary .tweet .linkedinButton:hover {
color: #F2F2F2;
background: #e74c3c;
}
.summary .tweet input {
font-size: 0.9em;
}
-6
View File
@@ -1,6 +0,0 @@
@font-face {
font-family: "fontsmith-icons";
src: url("/fonts/icons.woff") format("woff");
font-weight: normal;
font-style: normal;
}
-48
View File
@@ -1,48 +0,0 @@
.promess {
padding: 0em 2em 3em;
font-weight: normal;
font-size: 1.2em;
}
.url {
width: 50%;
}
.launchBtn {
background: #e74c3c;
color: #fff;
}
.launchBtn.disabled {
background: #deaca6;
}
.features {
display: table;
width: 50%;
margin: 8em auto 0;
font-size: 0.9em;
color: #413;
}
.features > div {
width: 33.3%;
display: table-cell;
padding: 0 1.5em;
}
input[type=submit],
input.url {
padding: 0 0.5em;
margin: 0.5em;
font-size: 1.2em;
height: 2em;
border: 0 solid;
border-radius: 0.5em;
box-shadow: 0.1em 0.2em 0 0 #5e2846;
outline: none;
}
input[type=submit]:hover {
color: #ddd;
}
input[type=submit].clicked {
color: #ddd;
position: relative;
left: 0.1em;
top: 0.2em;
box-shadow: none;
}
-273
View File
@@ -1,273 +0,0 @@
@font-face {
font-family: "fontsmith-icons";
src: url("/fonts/icons.woff") format("woff");
font-weight: normal;
font-style: normal;
}
html {
margin: 100px 50px;
}
body {
margin: 0 auto;
max-width: 1280px;
background: #9c4274;
color: #fff;
font-size: 16px;
text-align: center;
}
body,
input[type=submit],
input[type=text],
input[type=url],
input[type=number],
button {
font-family: 'Century Gothic', helvetica, arial, sans-serif;
}
input[type=submit] {
cursor: pointer;
}
h1 span {
display: inline-block;
height: 1em;
width: 1em;
color: #ffa319;
}
.resultsMenu {
margin-top: 2em;
}
.resultsMenu .menuItem {
display: inline-block;
margin: 1em;
width: 8em;
height: 7em;
color: #fff;
border: 3px solid #fff;
border-radius: 0.5em;
cursor: pointer;
text-decoration: none;
}
.resultsMenu .menuItem.back,
.resultsMenu .menuItem.restart {
color: #413;
border-color: #413;
}
.resultsMenu .menuItem div {
padding-top: 0.5em;
font-size: 3em;
}
.resultsMenu .active,
.resultsMenu .menuItem.active:hover {
color: #ffa319;
border-color: #ffa319;
}
.resultsMenu .menuItem:hover {
color: #ffa319;
}
.resultsMenu span {
position: relative;
top: 0.5em;
}
/* Grade colors */
.A {
/* green */
background: #0C4;
}
.B {
/* green */
background: #CD0;
}
.C {
/* yellow */
background: #FD2;
}
.D {
/* orange */
background: #FA2;
}
.E {
/* red */
background: #F60;
}
.F {
/* red */
background: #F22;
}
.NA {
/* Non applicable */
background: #CCC;
}
.board {
margin-top: 2em;
padding: 1em;
background: #fff;
color: #000;
border-radius: 0.5em;
text-align: left;
}
.backToDashboard {
text-align: center;
}
.backToDashboard a {
font-size: 0.9em;
display: block;
margin-top: 4em;
color: black;
}
a.linkButton {
font-size: 1em;
padding: 0.3em 0.5em;
margin: 0.5em;
line-height: 2em;
border: 0 solid;
border-radius: 0.5em;
box-shadow: 0.1em 0.2em 0 0 #5e2846;
background: #e74c3c;
color: #fff;
text-decoration: none;
}
.screenshotWrapper.desktop {
display: inline-block;
position: relative;
border: 0.2em solid #AAA;
background: #000;
padding: 0.5em;
border-top-left-radius: 0.4em;
border-top-right-radius: 0.4em;
}
.screenshotWrapper.desktop:before {
position: absolute;
width: 15em;
height: 0.6em;
bottom: -0.75em;
left: -1em;
background: #CCC;
border-bottom-left-radius: 0.2em;
border-bottom-right-radius: 0.2em;
content: " ";
}
.screenshotWrapper.desktop:after {
position: absolute;
width: 0.4em;
height: 0.2em;
bottom: -0.55em;
left: 12.5em;
background: lime;
content: " ";
}
.screenshotWrapper.desktop > div {
width: 12em;
height: 6.75em;
overflow: scroll;
position: relative;
}
.screenshotWrapper.desktop .screenshotImage {
width: 100%;
}
.star {
font-weight: bold;
}
.star span {
font-size: 1.2em;
}
.footer {
padding: 3em;
color: #413;
}
.footer a {
color: inherit;
}
.footer .version {
font-size: 0.7em;
}
/* Icons */
.icon-lab {
font-family: "fontsmith-icons";
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
}
.icon-lab:before {
content: "\e004";
}
.icon-question {
font-family: "fontsmith-icons";
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
}
.icon-question:before {
content: "\e001";
}
.icon-warning {
font-family: "fontsmith-icons";
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
}
.icon-warning:before {
content: "\e000";
}
.icon-back {
font-family: "fontsmith-icons";
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
}
.icon-back:before {
content: "\e006";
}
.icon-summary {
font-family: "fontsmith-icons";
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
}
.icon-summary:before {
content: "\e003";
}
.icon-spaghetti {
font-family: "fontsmith-icons";
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
}
.icon-spaghetti:before {
content: "\e005";
}
.icon-loop {
font-family: "fontsmith-icons";
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
}
.icon-loop:before {
content: "\e002";
}
-8
View File
@@ -1,8 +0,0 @@
.status {
margin-top: 2em;
font-size: 2.5em;
}
.statusSubMessage {
font-size: 0.8em;
margin-bottom: 6em;
}
-193
View File
@@ -1,193 +0,0 @@
.rule.board {
text-align: center;
}
.rule .ruleTable {
display: table;
border-spacing: 1em;
width: 90%;
margin: 2em auto;
background: #f2f2f2;
border: 1px dashed #666;
border-radius: 0.5em;
}
.rule .ruleTable > div {
display: table-cell;
vertical-align: middle;
}
.rule .ruleTable .left {
width: 33%;
font-weight: bold;
}
.rule .ruleTable .right {
width: 67%;
}
.rule .score {
font-size: 2.5em;
line-height: 2em;
height: 2em;
width: 2em;
border-radius: 0.5em;
margin: 0 auto 0.5em;
}
.rule .message {
width: 80%;
margin: 0 auto;
}
.rule .message p {
margin: 0.5em;
}
.rule .message ul {
list-style-type: none;
padding-left: 0;
}
.rule .message li:before {
content: '\25e6';
margin-right: 0.3em;
font-size: 1.2em;
position: relative;
top: 0.1em;
}
.rule .warning {
width: 90%;
margin: -1em auto 2em;
background: #FEE;
border: 1px dashed #e74c3c;
color: #e74c3c;
border-radius: 0.5em;
}
.rule .offendersTable {
display: table;
border-spacing: 0 0.25em;
margin: 0 auto;
min-width: 10%;
max-width: 90%;
}
.rule .offendersTable > div {
display: table-row;
}
.rule .offendersTable > div > div {
display: table-cell;
background: #f2f2f2;
padding: 0 1em;
word-wrap: break-word;
word-break: break-all;
}
.rule .offendersTable > div > div:hover {
background: #EBD8E2;
}
.rule .notFound {
font-size: 1em;
}
.rule .notFound h2 {
font-size: 3em;
margin-bottom: 1em;
}
.offenders .offenderButton {
display: inline-block;
position: relative;
background: #efe;
padding: 0 0.5em;
margin: 0.2em 0;
border-radius: 0.4em;
z-index: 1;
}
.offenders .offenderButton.opens {
padding-right: 0.75em;
}
.offenders .offenderButton.opens:after {
position: relative;
left: 0.5em;
content: '\25BC';
font-size: 0.8em;
}
.offenders .offenderButton > div {
display: none;
position: absolute;
right: 0;
min-width: 100%;
background: inherit;
border-bottom-left-radius: 0.4em;
border-bottom-right-radius: 0.4em;
border-top: 1px solid #999;
z-index: 2;
}
.offenders .offenderButton .domTree {
text-align: left;
white-space: nowrap;
}
.offenders .offenderButton .domTree > div {
margin: 0.5em;
}
.offenders .offenderButton .domTree > div div {
margin-left: 1em;
}
.offenders .offenderButton .backtrace,
.offenders .offenderButton .cssFileAndLine {
white-space: nowrap;
padding: 0.5em;
}
.offenders .offenderButton.opens:hover {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
background: #ffe0cc;
z-index: 2;
}
.offenders .offenderButton.opens:hover > div {
display: block;
}
.offendersHtml {
display: inline-block;
}
.domTree div {
text-align: left;
margin-left: 1em;
}
.domTree div span:only-child {
font-weight: bold;
}
.domTree div span:only-child span {
font-style: italic;
font-weight: normal;
}
.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;
height: 2em;
position: relative;
}
.colorPalette > div div {
display: none;
position: absolute;
left: 100%;
top: 100%;
background: #FFF;
padding: 0.5em;
border: 2px solid #f1c40f;
border-radius: 0.5em;
white-space: nowrap;
z-index: 3;
font-weight: bold;
}
.colorPalette > div:hover div {
display: block;
}
.colorPalette > div:hover:after {
content: " ";
position: absolute;
background-color: inherit;
left: -0.2em;
top: -0.2em;
width: 100%;
height: 100%;
z-index: 2;
border: 0.2em solid #f1c40f;
}
-7
View File
@@ -1,7 +0,0 @@
.screenshot.board {
text-align: center;
}
.screenshot .screenshotWrapper {
font-size: 2.08333333333333em;
margin-bottom: 0.5em;
}
-314
View File
@@ -1,314 +0,0 @@
/* Timeline colors, related to Window Performances */
.execution {
text-align: center;
}
.selectScript {
padding-bottom: 2em;
font-size: 0.9em;
}
.selectScript select {
max-width: 30em;
}
.selectScript.empty {
font-size: 0.8em;
}
.selectScript.empty select {
width: 10em;
}
.timeline {
margin: 2em 0 5em;
}
.timeline .chart {
position: relative;
width: 100%;
border-bottom: 1px solid #000;
}
.timeline .startTime,
.timeline .endTime {
position: absolute;
bottom: 0.5em;
font-size: 0.8em;
}
.timeline .startTime {
left: 0em;
}
.timeline .endTime {
right: 0em;
}
.timeline .chartPoints {
display: table;
height: 100px;
width: 99%;
margin: 0 auto;
}
.timeline .interval {
display: table-cell;
position: relative;
height: 100px;
width: 0.5%;
}
.timeline .interval .color {
position: absolute;
bottom: 0;
width: 100%;
}
.timeline .interval .color.clickable {
cursor: pointer;
}
.timeline div.interval:hover {
background: #9C4274;
}
.timeline .interval:hover .color {
background: #F04DA7;
}
.timeline .domComplete.interval {
background: #ede3ff;
}
.timeline .domComplete .color {
background: #c2a3ff;
}
.timeline .domContentLoadedEnd.interval {
background: #d8f0f0;
}
.timeline .domContentLoadedEnd .color {
background: #7ecccc;
}
.timeline .domContentLoaded.interval {
background: #e0ffd1;
}
.timeline .domContentLoaded .color {
background: #a7e846;
}
.timeline .domInteractive.interval {
background: #fffccc;
}
.timeline .domInteractive .color {
background: #ffe433;
}
.timeline .domCreation.interval {
background: #ffe0cc;
}
.timeline .domCreation .color {
background: #ff6600;
}
.timeline .tooltip.detailsOverlay {
position: absolute;
display: none;
width: auto;
padding: 0.5em 1em;
top: -1.5em;
right: 1em;
}
.timeline .interval:hover .tooltip {
display: block;
}
.timeline .legend {
display: table;
width: 100%;
margin-top: 1em;
}
.timeline .legend > div {
display: table-row;
}
.timeline .legend > div > div {
position: relative;
display: table-cell;
margin-top: 1em;
}
.timeline .titles {
font-weight: bold;
}
.timeline .titles > div {
padding: 0 1em 0 2em;
}
.timeline .tips {
font-size: 0.7em;
}
.timeline .tips > div {
padding: 1em 1em 0 0;
}
.timeline .legend .color {
display: block;
position: absolute;
left: 0;
height: 1.5em;
width: 1.5em;
border-radius: 0.2em;
}
.filters {
margin: 1em auto;
padding: 0.5em;
min-width: 30em;
width: 30%;
border: 1px dotted #aaa;
text-align: left;
}
.table {
display: table;
width: 100%;
border-spacing: 0.25em;
}
.table > div {
display: table-row;
}
.table > .headers > div {
font-weight: bold;
padding: 0.5em 1em;
}
.table > div > div {
padding: 0.1em 1em;
background: #f2f2f2;
display: table-cell;
text-align: left;
}
.table > div.jsError > .type,
.table > div.jsError > .value {
color: #e74c3c;
font-weight: bold;
}
.table > div.windowPerformance > div,
.table > div.windowPerformance > div.startTime {
background: #EBD8E2;
}
.table > div.showingDetails > div {
background: #f1c40f;
}
.table > div.highlight > div.startTime {
background-color: #C0F090;
}
.table > div.highlight-remove {
transition: 3s;
}
.table > div.highlight-remove > div.startTime {
transition: background-color 3s ease-in;
}
.table > div > .index {
color: #bbb;
word-break: normal;
}
.table > div > .type {
white-space: nowrap;
}
.table .children {
margin-top: 0.2em;
font-size: 0.8em;
line-height: 1.6em;
}
.table .child {
margin-left: 0.5em;
}
.table .child > .child {
margin-left: 1em;
}
.table .child:before {
content: "↳";
}
.table .child .childArgs {
display: none;
}
.table .child span {
position: relative;
}
.table .child span:hover {
background: #EBD8E2;
}
.table .child span:hover div {
display: inline-block;
}
.table .child span:hover .childArgs {
display: block;
position: absolute;
padding: 0 1em 0 2em;
left: 100%;
top: 0;
background: #EBD8E2;
line-height: 1.3em;
height: 1.3em;
z-index: 2;
}
.table .showingDetails .child span:hover {
background: inherit;
}
.table .showingDetails .child span:hover .childArgs {
display: none;
}
.table > div > .value {
width: 70%;
word-break: break-all;
}
.table > div > .details {
position: relative;
}
.table .details .icon-question {
color: #f1c40f;
cursor: pointer;
}
.table .icon-warning {
display: inline-block;
width: 0.8em;
}
.detailsOverlay {
display: none;
position: absolute;
right: 3em;
top: -3em;
width: 45em;
min-height: 1em;
padding: 0 1em 1em;
background: #fff;
border: 2px solid #f1c40f;
border-radius: 0.5em;
z-index: 2;
}
@media screen and (max-width: 1024px) {
.detailsOverlay {
width: 25em;
}
}
.showDetails .detailsOverlay {
display: block;
}
.detailsOverlay .closeBtn {
position: absolute;
top: 0.5em;
right: 0.5em;
color: #f1c40f;
cursor: pointer;
}
.detailsOverlay .advice {
color: #e74c3c;
font-weight: bold;
}
.detailsOverlay .trace {
word-break: break-all;
}
.table > div > .duration,
.table > div > .startTime {
text-align: center;
white-space: nowrap;
}
.table > div > .startTime.domComplete {
background: #ede3ff;
}
.table > div > .startTime.domContentLoadedEnd {
background: #d8f0f0;
}
.table > div > .startTime.domContentLoaded {
background: #e0ffd1;
}
.table > div > .startTime.domInteractive {
background: #fffccc;
}
.table > div > .startTime.domCreation {
background: #ffe0cc;
}
.execution .icon-warning {
color: #e74c3c;
cursor: pointer;
}
.warningsFilterOn > div {
display: none;
}
.warningsFilterOn > div.warning {
display: table-row;
}
Binary file not shown.
@@ -1 +0,0 @@
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M 256.00,0.00C 397.385,0.00, 512.00,114.615, 512.00,256.00s -114.615,256.00, -256.00,256.00s -256.00-114.615, -256.00-256.00S 114.615,0.00, 256.00,0.00z M 256.00,464.00 c 114.875,0.00 208.00-93.125 208.00-208.00S 370.875,48.00, 256.00,48.00s -208.00,93.125, -208.00,208.00S 141.125,464.00, 256.00,464.00zM 105.372,233.373l 128.001-128.00c 12.496-12.497 32.757-12.497 45.254,0.00c 12.497,12.497 12.497,32.758,0.00,45.255L 205.255,224.00 L 384.00,224.00 c 17.673,0.00 32.00,14.327 32.00,32.00c0.00,17.673, -14.327,32.00, -32.00,32.00l-178.745,0.00 l 73.373,73.372c 12.497,12.497 12.497,32.759,0.00,45.256 C 272.379,412.876, 264.189,416.00, 256.00,416.00s -16.379-3.124, -22.627-9.372l -128.001-128.00C 92.876,266.131, 92.876,245.869, 105.372,233.373z" ></path></svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

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

Before

Width:  |  Height:  |  Size: 3.1 KiB

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

Before

Width:  |  Height:  |  Size: 3.3 KiB

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

Before

Width:  |  Height:  |  Size: 3.2 KiB

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

Before

Width:  |  Height:  |  Size: 3.3 KiB

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

Before

Width:  |  Height:  |  Size: 3.7 KiB

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

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 794 B

-65
View File
@@ -1,65 +0,0 @@
var yltApp = angular.module('YellowLabTools', [
'ngRoute',
'ngSanitize',
'ngAnimate',
'indexCtrl',
'dashboardCtrl',
'queueCtrl',
'ruleCtrl',
'screenshotCtrl',
'timelineCtrl',
'runsFactory',
'resultsFactory',
'apiService',
'menuService',
'gradeDirective',
'offendersDirectives'
]);
yltApp.run(['$rootScope', '$location', function($rootScope, $location) {
$rootScope.loadedRunId = null;
// Google Analytics
$rootScope.$on('$routeChangeSuccess', function(){
ga('send', 'pageview', {'page': $location.path()});
});
}]);
yltApp.config(['$routeProvider', '$locationProvider',
function($routeProvider, $locationProvider) {
$routeProvider.
when('/', {
templateUrl: 'views/index.html',
controller: 'IndexCtrl'
}).
when('/queue/:runId', {
templateUrl: 'views/queue.html',
controller: 'QueueCtrl'
}).
when('/about', {
templateUrl: 'views/about.html'
}).
when('/result/:runId', {
templateUrl: 'views/dashboard.html',
controller: 'DashboardCtrl'
}).
when('/result/:runId/timeline', {
templateUrl: 'views/timeline.html',
controller: 'TimelineCtrl'
}).
when('/result/:runId/screenshot', {
templateUrl: 'views/screenshot.html',
controller: 'ScreenshotCtrl'
}).
when('/result/:runId/rule/:policy', {
templateUrl: 'views/rule.html',
controller: 'RuleCtrl'
}).
otherwise({
redirectTo: '/'
});
$locationProvider.html5Mode(true);
}
]);
-68
View File
@@ -1,68 +0,0 @@
var dashboardCtrl = angular.module('dashboardCtrl', ['resultsFactory', 'menuService']);
dashboardCtrl.controller('DashboardCtrl', ['$scope', '$rootScope', '$routeParams', '$location', 'Results', 'API', 'Menu', function($scope, $rootScope, $routeParams, $location, Results, API, Menu) {
$scope.runId = $routeParams.runId;
$scope.Menu = Menu.setCurrentPage('dashboard', $scope.runId);
$scope.fromSocialShare = $location.search().share;
function loadResults() {
// Load result if needed
if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
Results.get({runId: $routeParams.runId}, function(result) {
$rootScope.loadedResult = result;
$scope.result = result;
init();
}, function(err) {
$scope.error = true;
});
} else {
$scope.result = $rootScope.loadedResult;
init();
}
}
function init() {
// By default, Angular sorts object's attributes alphabetically. Countering this problem by retrieving the keys order here.
$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.testAgain = function() {
API.launchTest($scope.result.params.url);
};
/// 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));
};
$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.'));
};
function openSocialPopup(url) {
var winHeight = 400;
var winWidth = 600;
var winTop = (screen.height / 2) - (winHeight / 2);
var winLeft = (screen.width / 2) - (winWidth / 2);
window.open(url, 'sharer', 'top=' + winTop + ',left=' + winLeft + ',toolbar=0,status=0,width=' + winWidth + ',height=' + winHeight);
}
// Returns the URL of the JSON result
$scope.getAPIUrl = function() {
return '/api/results/' + $scope.runId;
};
loadResults();
}]);
-9
View File
@@ -1,9 +0,0 @@
var indexCtrl = angular.module('indexCtrl', []);
indexCtrl.controller('IndexCtrl', ['$scope', '$location', 'API', function($scope, $location, API) {
$scope.launchTest = function() {
if ($scope.url) {
API.launchTest($scope.url);
}
};
}]);
-42
View File
@@ -1,42 +0,0 @@
var queueCtrl = angular.module('queueCtrl', ['runsFactory']);
queueCtrl.controller('QueueCtrl', ['$scope', '$routeParams', '$location', 'Runs', 'API', function($scope, $routeParams, $location, Runs, API) {
$scope.runId = $routeParams.runId;
var numberOfTries = 0;
function getRunStatus () {
Runs.get({runId: $scope.runId}, function(data) {
$scope.url = data.params.url;
$scope.status = data.status;
$scope.notFound = false;
$scope.connectionLost = false;
if (data.status.statusCode === 'running' || data.status.statusCode === 'awaiting') {
numberOfTries ++;
// Retrying in 2 seconds (and increasing the delay a bit more each time)
setTimeout(getRunStatus, 2000 + (numberOfTries * 100));
} else if (data.status.statusCode === 'complete') {
$location.path('/result/' + $scope.runId).replace();
} else {
// Handled by the view
}
}, function(response) {
if (response.status === 404) {
$scope.notFound = true;
$scope.connectionLost = false;
} else if (response.status === 0) {
// Connection lost, retry in 10 seconds
setTimeout(getRunStatus, 10000);
$scope.connectionLost = true;
$scope.notFound = false;
}
});
}
getRunStatus();
}]);
-36
View File
@@ -1,36 +0,0 @@
var ruleCtrl = angular.module('ruleCtrl', []);
ruleCtrl.controller('RuleCtrl', ['$scope', '$rootScope', '$routeParams', '$location', '$sce', 'Menu', 'Results', 'API', function($scope, $rootScope, $routeParams, $location, $sce, Menu, Results, API) {
$scope.runId = $routeParams.runId;
$scope.policyName = $routeParams.policy;
$scope.Menu = Menu.setCurrentPage(null, $scope.runId);
$scope.rule = null;
function loadResults() {
// Load result if needed
if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
Results.get({runId: $routeParams.runId}, function(result) {
$rootScope.loadedResult = result;
$scope.result = result;
init();
});
} else {
$scope.result = $rootScope.loadedResult;
init();
}
}
function init() {
$scope.rule = $scope.result.rules[$scope.policyName];
}
$scope.backToDashboard = function() {
$location.path('/result/' + $scope.runId);
};
$scope.testAgain = function() {
API.launchTest($scope.result.params.url);
};
loadResults();
}]);
@@ -1,31 +0,0 @@
var screenshotCtrl = angular.module('screenshotCtrl', ['resultsFactory', 'menuService']);
screenshotCtrl.controller('ScreenshotCtrl', ['$scope', '$rootScope', '$routeParams', '$location', 'Results', 'API', 'Menu', function($scope, $rootScope, $routeParams, $location, Results, API, Menu) {
$scope.runId = $routeParams.runId;
$scope.Menu = Menu.setCurrentPage(null, $scope.runId);
function loadResults() {
// Load result if needed
if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
Results.get({runId: $routeParams.runId}, function(result) {
$rootScope.loadedResult = result;
$scope.result = result;
init();
}, function(err) {
$scope.error = true;
});
} else {
$scope.result = $rootScope.loadedResult;
}
}
$scope.backToDashboard = function() {
$location.path('/result/' + $scope.runId);
};
$scope.testAgain = function() {
API.launchTest($scope.result.params.url);
};
loadResults();
}]);
-33
View File
@@ -1,33 +0,0 @@
var gradeDirective = angular.module('gradeDirective', []);
gradeDirective.directive('grade', function() {
return {
restrict: 'E',
scope: {
score: '=score'
},
template: '<div ng-class="getGrade(score)">{{getGrade(score)}}</div>',
replace: true,
controller : ['$scope', function($scope) {
$scope.getGrade = function(score) {
if (score > 80) {
return 'A';
}
if (score > 60) {
return 'B';
}
if (score > 40) {
return 'C';
}
if (score > 20) {
return 'D';
}
if (score > 0) {
return 'E';
}
return 'F';
};
}]
};
});
@@ -1,819 +0,0 @@
(function() {
"use strict";
var offendersDirectives = angular.module('offendersDirectives', []);
function getdomTreeHTML(tree) {
return '<div class="domTree">' + getdomTreeInnerHTML(tree) + '</div>';
}
function getdomTreeInnerHTML(tree) {
return recursiveHtmlBuilder(tree);
}
function recursiveHtmlBuilder(tree) {
var html = '';
var keys = Object.keys(tree);
keys.forEach(function(key) {
if (isNaN(tree[key])) {
html += '<div><span>' + key + '</span>' + recursiveHtmlBuilder(tree[key]) + '</div>';
} else if (tree[key] > 1) {
html += '<div><span>' + key + ' <span>(x' + tree[key] + ')</span></span></div>';
} else {
html += '<div><span>' + key + '</span></div>';
}
});
return html;
}
offendersDirectives.directive('domTree', function() {
return {
restrict: 'E',
scope: {
tree: '='
},
template: '<div class="domTree"></div>',
replace: true,
link: function(scope, element) {
element.append(getdomTreeInnerHTML(scope.tree));
}
};
});
function getDomElementButtonHTML(obj, onASingleLine) {
if (obj.tree && !onASingleLine) {
return '<div class="offenderButton opens">' + getDomElementButtonInnerHTML(obj, onASingleLine) + '</div>';
} else {
return '<div class="offenderButton">' + getDomElementButtonInnerHTML(obj, onASingleLine) + '</div>';
}
}
function getDomElementButtonInnerHTML(obj, onASingleLine) {
if (obj.type === 'html' ||
obj.type === 'body' ||
obj.type === 'head' ||
obj.type === 'window' ||
obj.type === 'document' ||
obj.type === 'fragment') {
return obj.type;
}
if (obj.type === 'notAnElement') {
return 'Incorrect element';
}
var html = '';
if (obj.type === 'domElement') {
html = 'DOM element <b>' + obj.element + '</b>';
} else if (obj.type === 'fragmentElement') {
html = 'Fragment element <b>' + obj.element + '</b>';
} else if (obj.type === 'createdElement') {
html = 'Created element <b>' + obj.element + '</b>';
}
if (obj.tree && !onASingleLine) {
html += getdomTreeHTML(obj.tree);
}
return html;
}
offendersDirectives.directive('domElementButton', function() {
return {
restrict: 'E',
scope: {
obj: '='
},
template: '<div class="offenderButton" ng-class="{opens: obj.tree}"></div>',
replace: true,
link: function(scope, element) {
element.append(getDomElementButtonInnerHTML(scope.obj));
}
};
});
function getJQueryContextButtonHTML(context, onASingleLine) {
if (context.length === 0) {
return '<span class="offenderButton">Empty jQuery object</span>';
}
if (context.length === 1) {
return getDomElementButtonHTML(context.elements[0], onASingleLine);
}
var html = context.length + ' elements (' + getDomElementButtonHTML(context.elements[0], onASingleLine) + ', ' + getDomElementButtonHTML(context.elements[1], onASingleLine);
if (context.length === 3) {
html += ', ' + getDomElementButtonHTML(context.elements[0], onASingleLine);
} else if (context.length > 3) {
html += ' and ' + (context.length - 2) + ' more...';
}
return html + '}';
}
function isJQuery(node) {
return node.data.type.indexOf('jQuery ') === 0;
}
function getNonJQueryHTML(node, onASingleLine) {
var type = node.data.type;
if (!node.data.callDetails) {
return '';
}
var args = node.data.callDetails.arguments;
var ctxt = node.data.callDetails.context;
switch (type) {
case 'getElementById':
case 'createElement':
return '<b>' + args[0] + '</b>';
case 'getElementsByClassName':
case 'getElementsByTagName':
case 'querySelector':
case 'querySelectorAll':
return '<b>' + args[0] + '</b> on ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine);
case 'appendChild':
return 'append ' + getDomElementButtonHTML(args[0], onASingleLine) + ' to ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine);
case 'insertBefore':
return 'insert ' + getDomElementButtonHTML(args[0], onASingleLine) + ' into ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine) + ' before ' + getDomElementButtonHTML(args[1], onASingleLine);
case 'addEventListener':
return 'bind <b>' + args[0] + '</b> to ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine);
case 'getComputedStyle':
return getDomElementButtonHTML(args[0], onASingleLine) + (args[1] || '');
case 'error':
return args[0];
default:
return '';
}
}
function getJQueryHTML(node, onASingleLine) {
var type = node.data.type;
var unescapedArgs = node.data.callDetails.arguments;
var args = [];
var ctxt = node.data.callDetails.context;
// escape HTML in args
for (var i = 0 ; i < 4 ; i ++) {
if (unescapedArgs[i]) {
args[i] = escapeHTML(unescapedArgs[i]);
}
}
if (type === 'jQuery loaded' || type === 'jQuery version change') {
return args[0];
}
switch (type) {
case 'jQuery - onDOMReady':
case 'jQuery - windowOnLoad':
return '(function)';
case 'jQuery - Sizzle call':
return '<b>' + args[0] + '</b> on ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine);
case 'jQuery - find':
if (ctxt && ctxt.length === 1 && ctxt.elements[0].type !== 'document') {
return '<b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else {
return '<b>' + args[0] + '</b>';
}
break;
case 'jQuery - html':
if (args[0] !== undefined) {
return 'set content "<b>' + args[0] + '</b>" to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else {
return 'get content from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - append':
return 'append ' + joinArgs(args) + ' to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - appendTo':
return 'append ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to <b>' + args[0] + '</b>';
case 'jQuery - prepend':
return 'prepend ' + joinArgs(args) + ' to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - prependTo':
return 'prepend ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to <b>' + args[0] + '</b>';
case 'jQuery - before':
return 'insert ' + joinArgs(args) + ' before ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - insertBefore':
return 'insert ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' before <b>' + args[0] + '</b>';
case 'jQuery - after':
return 'insert ' + joinArgs(args) + ' after ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - insertAfter':
return 'insert ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' after <b>' + args[0] + '</b>';
case 'jQuery - remove':
case 'jQuery - detach':
if (args[0]) {
return getJQueryContextButtonHTML(ctxt, onASingleLine) + ' filtered by <b>' + args[0] + '</b>';
} else {
return getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - empty':
case 'jQuery - clone':
case 'jQuery - unwrap':
case 'jQuery - show':
case 'jQuery - hide':
case 'jQuery - animate':
case 'jQuery - fadeIn':
case 'jQuery - fadeOut':
case 'jQuery - fadeTo':
case 'jQuery - fadeToggle':
case 'jQuery - slideDown':
case 'jQuery - slideUp':
case 'jQuery - slideToggle':
return getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - replaceWith':
return 'replace ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' with <b>' + args[0] + '</b>';
case 'jQuery - replaceAll':
return 'replace <b>' + args[0] + '</b> with ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - text':
if (args[0]) {
return 'set text "<b>' + args[0] + '</b>" to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else {
return 'get text from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - wrap':
case 'jQuery - wrapAll':
return 'wrap ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' within <b>' + args[0] + '</b>';
case 'jQuery - wrapInner':
return 'wrap the content of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' within <b>' + args[0] + '</b>';
case 'jQuery - css':
case 'jQuery - attr':
case 'jQuery - prop':
if (isStringOfObject(args[0])) {
return 'set <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else if (args[1]) {
return 'set <b>' + args[0] + '</b> : <b>' + args[1] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else {
return 'get <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - offset':
case 'jQuery - height':
case 'jQuery - innerHeight':
case 'jQuery - width':
case 'jQuery - innerWidth':
case 'jQuery - scrollLeft':
case 'jQuery - scrollTop':
case 'jQuery - position':
if (args[0]) {
return 'set <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else {
return 'get from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - outerHeight':
case 'jQuery - outerWidth':
if (args[0] && args[0] !== 'true') {
return 'set <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else if (args[0] === 'true') {
return 'get from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' (with include margins option)';
} else {
return 'get from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - toggle':
if (args[0] === 'true') {
return getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to visible';
} else if (args[0] === 'false') {
return getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to hidden';
} else {
return getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - on':
case 'jQuery - one':
if (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);
}
break;
case 'jQuery - off':
if (args[0]) {
if (args[1]) {
return 'unbind <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[1] + '</b>';
} else {
return 'unbind <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
} else {
return 'unbind all events';
}
break;
case 'jQuery - live':
case 'jQuery - bind':
return 'bind <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - die':
case 'jQuery - unbind':
if (args[0]) {
return 'unbind <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else {
return 'unbind all events';
}
break;
case 'jQuery - delegate':
return 'bind <b>' + args[1] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[0] + '</b>';
case 'jQuery - undelegate':
if (args[0]) {
if (args[1]) {
return 'unbind <b>' + args[1] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[0] + '</b>';
} else {
return 'unbind namespace <b>' + args[0] + '</b>';
}
} else {
return 'unbind all events';
}
break;
case 'jQuery - blur':
case 'jQuery - change':
case 'jQuery - click':
case 'jQuery - dblclick':
case 'jQuery - focus':
case 'jQuery - keydown':
case 'jQuery - keypress':
case 'jQuery - keyup':
case 'jQuery - mousedown':
case 'jQuery - mouseenter':
case 'jQuery - mouseleave':
case 'jQuery - mousemove':
case 'jQuery - mouseout':
case 'jQuery - mouseover':
case 'jQuery - mouseup':
case 'jQuery - resize':
case 'jQuery - scroll':
case 'jQuery - select':
case 'jQuery - submit':
if (args[0]) {
return 'bind on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else {
return 'triggered on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - error':
case 'jQuery - focusin':
case 'jQuery - focusout':
case 'jQuery - hover':
case 'jQuery - load':
case 'jQuery - unload':
return 'bind on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - removeAttr':
case 'jQuery - removeProp':
return 'remove <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - val':
if (args[0]) {
return 'set value <b>' + args[0] + '</b> to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else {
return 'get value from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - hasClass':
case 'jQuery - addClass':
case 'jQuery - removeClass':
return '<b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - toggleClass':
if (args[0]) {
if (args[1]) {
return 'toggle <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to <b>' + args[1] + '</b>';
} else {
return 'toggle <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
} else {
return 'magic no-argument toggleClass';
}
break;
case 'jQuery - children':
if (args[0]) {
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' filtered by <b>' + args[0] + '</b>';
} else {
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - closest':
if (args[1]) {
return 'closest <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' in context <b>' + args[1] + '</b>';
} else {
return 'closest <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - next':
case 'jQuery - nextAll':
if (args[0]) {
return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching <b>' + args[0] + '</b>';
} else {
return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - nextUntil':
if (args[0]) {
if (args[1]) {
return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b> and matching <b>' + args[1] + '</b>';
} else {
return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b>';
}
} else {
return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - offsetParent':
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - prev':
case 'jQuery - prevAll':
if (args[0]) {
return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching <b>' + args[0] + '</b>';
} else {
return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - prevUntil':
if (args[0]) {
if (args[1]) {
return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b> and matching <b>' + args[1] + '</b>';
} else {
return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b>';
}
} else {
return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - parent':
case 'jQuery - parents':
if (args[0]) {
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching <b>' + args[0] + '</b>';
} else {
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - parentsUntil':
if (args[0]) {
if (args[1]) {
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b> and matching <b>' + args[1] + '</b>';
} else {
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b>';
}
} else {
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - siblings':
if (args[0]) {
return 'near ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching <b>' + args[0] + '</b>';
} else {
return 'near ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - onDOMReady':
return '(function)';
default:
return '';
}
}
function escapeHTML(html) {
var entityMap = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': '&quot;',
"'": '&#39;',
"/": '&#x2F;'
};
return String(html).replace(/[&<>"'\/]/g, function (s) {
return entityMap[s];
});
}
function joinArgs(args) {
var html = '<b>' + args[0] + '</b>';
if (args[1]) {
html += ', <b>' + args[1] + '</b>';
if (args[2]) {
html += ', <b>' + args[2] + '</b>';
if (args[3]) {
html += ', and more...';
}
}
}
return html;
}
function isStringOfObject(str) {
return typeof str === 'string' && str[0] === '{' && str[str.length - 1] === '}';
}
function isPureString(str) {
return typeof str === 'string' && str[0] !== '{' && str !== '(function)' && str !== '[Object]' && str !== '[Array]' && str !== 'true' && str !== 'false' && str !== 'undefined' && str !== 'unknown';
}
function getTimelineParamsHTML(node, onASingleLine) {
if (isJQuery(node)) {
return getJQueryHTML(node, onASingleLine);
} else {
return getNonJQueryHTML(node, onASingleLine);
}
}
function getBacktraceHTML(backtrace) {
var html = '';
var parsedBacktrace = parseBacktrace(backtrace);
if (!parsedBacktrace || parsedBacktrace.length === 0) {
html += '<div><div>can\'t find any backtrace :/</div></div>';
} else {
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>';
}
}
return html;
}
function parseBacktrace(str) {
if (!str) {
return null;
}
var out = [];
var splited = str.split(' / ');
try {
splited.forEach(function(trace) {
var fnName = null, fileAndLine;
var withFnResult = /^([^\s\(]+) \((.+:\d+)\)$/.exec(trace);
if (withFnResult === null) {
fileAndLine = trace;
} else {
fnName = withFnResult[1];
fileAndLine = withFnResult[2];
}
var fileAndLineSplit = /^(.*):(\d+)$/.exec(fileAndLine);
var filePath = fileAndLineSplit[1];
var line = fileAndLineSplit[2];
out.push({
fnName: fnName,
filePath: filePath,
line: line
});
});
} catch(e) {
return null;
}
return out;
}
function getTimelineDetailsHTML(node) {
var html = '';
if (node.data.type != 'jQuery loaded' && node.data.type != 'jQuery version change' && !node.windowPerformance) {
if (node.warning || node.error) {
html += '<div class="icon-warning"></div>';
} else {
html += '<div class="icon-question"></div>';
}
html += '<div class="detailsOverlay">';
html += '<div class="closeBtn">✖</div>';
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>';
}
if (node.data.resultsNumber === 0) {
html += '<p class="advice">The query returned 0 results. Could it be unused or dead code?</p>';
} else if (node.data.resultsNumber > 0) {
html += '<p>The query returned ' + node.data.resultsNumber + ' ' + (node.data.resultsNumber > 1 ? 'results' : 'result') + '.</p>';
}
if (node.data.backtrace) {
html += '<h4>Backtrace</h4>';
html += '<div class="table">';
html += getBacktraceHTML(node.data.backtrace);
html += '</div>';
}
html += '</div>';
}
return html;
}
offendersDirectives.directive('profilerLine', ['$filter', function($filter) {
var numberWithCommas = $filter('number');
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="details">' + getTimelineDetailsHTML(node) + '</div>' +
'<div class="startTime ' + node.data.loadingStep + '">' + numberWithCommas(node.data.timestamp, 0) + ' ms</div>';
}
function recursiveChildrenHTML(node) {
var html = '';
if (node.children) {
node.children.forEach(function(child) {
html += '<div class="child"><span>' + child.data.type + '<div class="childArgs">' + getTimelineParamsHTML(child, true) + '</div></span>' + recursiveChildrenHTML(child) + '</div>';
});
}
return html;
}
function onDetailsClick(row) {
// Close if it's alreay open
if (row.classList.contains('showDetails')) {
closeDetails(row);
return;
}
// Close any other open details overlay
var openOnes = document.getElementsByClassName('showDetails');
if (openOnes.length > 0) {
openOnes[0].classList.remove('showDetails');
}
// Make it appear
row.classList.add('showDetails');
// Bind the close button
row.querySelector('.closeBtn').addEventListener('click', function() {
closeDetails(row);
});
}
function closeDetails(row) {
row.classList.remove('showDetails');
// Unbind the close button
row.querySelector('.closeBtn').removeEventListener('click', closeDetails);
}
return {
restrict: 'E',
scope: {
index: '=',
node: '='
},
template: '<div></div>',
replace: true,
link: function(scope, element) {
if (scope.node.error) {
element.addClass('jsError');
} else if (scope.node.windowPerformance) {
element.addClass('windowPerformance');
}
element.append(getProfilerLineHTML(scope.index, scope.node));
element[0].id = 'line_' + scope.index;
if (scope.node.warning || scope.node.error) {
element[0].classList.add('warning');
}
// Bind click on the details icon
var detailsIcon = element[0].querySelector('.details div');
if (detailsIcon) {
detailsIcon.addEventListener('click', function() {
onDetailsClick(this.parentNode.parentNode);
});
}
}
};
}]);
function shortenUrl(url, maxLength) {
if (!maxLength) {
maxLength = 110;
}
// Why dividing by 2.1? Because it adds a 5% margin.
var leftLength = Math.floor((maxLength - 5) / 2.1);
var rightLength = Math.ceil((maxLength - 5) / 2.1);
return (url.length > maxLength) ? url.substr(0, leftLength) + ' ... ' + url.substr(-rightLength) : url;
}
offendersDirectives.filter('shortenUrl', function() {
return shortenUrl;
});
function getUrlLink(url, maxLength) {
return '<a href="' + url + '" target="_blank" title="' + url + '">' + shortenUrl(url, maxLength) + '</a>';
}
offendersDirectives.directive('urlLink', function() {
return {
restrict: 'E',
scope: {
url: '=',
maxLength: '='
},
template: '<a href="{{url}}" target="_blank" title="{{url}}">{{url | shortenUrl:maxLength}}</a>',
replace: true
};
});
offendersDirectives.filter('encodeURIComponent', function() {
return window.encodeURIComponent;
});
offendersDirectives.directive('fileAndLine', function() {
return {
restrict: 'E',
scope: {
file: '=',
line: '=',
column: '='
},
template: '<span><span ng-if="file"><url-link url="file" max-length="60"></url-link></span><span ng-if="!file">&lt;inline CSS&gt;</span><span ng-if="line !== null && column !== null"> @ {{line}}:{{column}}</span></span>',
replace: true
};
});
offendersDirectives.directive('fileAndLineButton', function() {
return {
restrict: 'E',
scope: {
file: '=',
line: '=',
column: '='
},
template: '<div class="offenderButton opens">css file<div class="cssFileAndLine"><file-and-line file="file" line="line" column="column" button="true"></file-and-line></div></div>',
replace: true
};
});
})();
-7
View File
@@ -1,7 +0,0 @@
var resultsFactory = angular.module('resultsFactory', ['ngResource']);
resultsFactory.factory('Results', ['$resource', function($resource) {
return $resource('/api/results/:runId', {
});
}]);
-7
View File
@@ -1,7 +0,0 @@
var runsFactory = angular.module('runsFactory', ['ngResource']);
runsFactory.factory('Runs', ['$resource', function($resource) {
return $resource('/api/runs/:runId', {
});
}]);
-27
View File
@@ -1,27 +0,0 @@
var apiService = angular.module('apiService', []);
apiService.factory('API', ['$location', 'Runs', 'Results', function($location, Runs, Results) {
return {
launchTest: function(url) {
Runs.save({
url: url,
waitForResponse: false,
screenshot: true,
jsTimeline: true
}, 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 {
alert('An error occured...');
}
});
}
};
}]);
-34
View File
@@ -1,34 +0,0 @@
var menuService = angular.module('menuService', []);
menuService.factory('Menu', ['$location', function($location) {
var currentPage, currentRunId;
return {
getCurrentPage: function() {
return currentPage;
},
setCurrentPage: function(page, runId) {
currentPage = page;
currentRunId = runId;
return this;
},
changePage: function(page) {
switch (page) {
case 'index':
$location.path('/');
break;
case 'dashboard':
$location.path('/result/' + currentRunId);
break;
case 'timeline':
$location.path('/result/' + currentRunId + '/timeline');
break;
default:
console.err('Undefined Menu.changePage() destination');
}
}
};
}]);
-12
View File
@@ -1,12 +0,0 @@
.about {
margin: 3em auto;
width: 50%;
}
.about p {
margin: 2em;
}
.about a {
color: #FFF;
}
-178
View File
@@ -1,178 +0,0 @@
.testedUrl {
color: inherit;
}
.summary {
text-align: center;
}
.summary .globalScore {
display: table;
width: 60%;
margin: 3em auto;
> div {
display: table-cell;
width: 50%;
vertical-align: middle;
}
.globalGrade {
margin: 0.5 auto;
width: 2.5em;
height: 2.5em;
line-height: 2.5em;
border-radius: 0.5em;
font-size: 3em;
font-weight: bold;
vertical-align: middle;
}
.on100 {
font-size: 1.2em;
font-weight: bold;
margin: 0.5em 0 1em;
}
.screenshotWrapper:hover {
opacity: 0.75;
&:after {
position: absolute;
width: 1.25em;
height: 1.25em;
top: 0.7em;
left: 1.55em;
font-size: 3em;
color: #FFF;
background: #000;
border-radius: 0.2em;
text-align: center;
content: "+";
opacity: 0.85;
}
}
}
.summary .notations {
display: table;
width: 80%;
margin: 0 10% 1.5em;
border-spacing: 1em;
}
.summary .notations > div {
display: table-row;
}
.summary .notations > div > div {
display: table-cell;
height: 2.5em;
vertical-align: middle;
}
.summary .notations .category {
font-weight: bold;
text-align: center;
width: 20%;
}
.summary .notations .criteria {
font-weight: normal;
width: 75%;
}
.A, .B, .C, .D, .E, .F, .NA {
.summary .notations &.categoryScore {
width: 2.5em;
max-width: 2.5em;
min-width: 2.5em;
font-size: 2em;
text-align: center;
border-radius: 0.5em;
font-weight: bold;
}
.summary .notations .grade & {
width: 1em;
height: 1em;
font-size: 1em;
color: transparent;
margin: 0 auto;
border-radius: 0.5em;
}
}
.summary .notations .criteria .table {
width: 100%;
> div:hover > div {
background: #EBD8E2;
cursor: pointer;
&.info {
background: #FFF;
.icon-question {
color: #EBD8E2;
}
}
}
}
.summary .notations .criteria .grade {
width: 10%;
padding-left: 0.5em;
padding-right: 0.5em;
vertical-align: middle;
}
.summary .notations .criteria .label {
width: 70%;
}
.summary .notations .criteria .result {
width: 18%;
font-weight: bold;
white-space: nowrap;
text-align: center;
vertical-align: middle;
}
.summary .notations .warning .label, .summary .notations .warning .result, .summary .notations .icon-warning {
color: #FF1919;
}
.summary .notations .criteria .info {
width: 2%;
text-align: center;
vertical-align: middle;
background: #FFF;
padding-left: 0.1em;
padding-right: 0.1em;
}
.summary .notations .criteria .icon-question {
color: transparent;
}
.summary .fromShare {
margin-bottom: 3em;
}
.summary .apiTip {
font-size: 0.8em;
margin-bottom: 4em;
color: #413;
a {
color: inherit;
}
}
.summary .tweet {
.tweetText {
color: #413;
background: #F2F2F2;
border: none;
width: 25em;
padding: 0.4em;
border-radius: 0.5em;
box-shadow: 0.05em 0.1em 0 0 #999;
}
.tweetButton, .linkedinButton {
color: #413;
background: #F2F2F2;
margin-right: 0;
&:hover {
color: #F2F2F2;
background: #e74c3c;
}
}
input {
font-size: 0.9em;
}
}
-56
View File
@@ -1,56 +0,0 @@
@warning-font-family: "fontsmith-icons";
@warning-value: "\e000";
@warning: '"fontsmith-icons"' '"\\e000"';
@question-font-family: "fontsmith-icons";
@question-value: "\e001";
@question: '"fontsmith-icons"' '"\\e001"';
@lab-font-family: "fontsmith-icons";
@lab-value: "\e004";
@lab: '"fontsmith-icons"' '"\\e004"';
@list-font-family: "fontsmith-icons";
@list-value: "\e003";
@list: '"fontsmith-icons"' '"\\e003"';
@bars-font-family: "fontsmith-icons";
@bars-value: "\e005";
@bars: '"fontsmith-icons"' '"\\e005"';
@arrow-left3-font-family: "fontsmith-icons";
@arrow-left3-value: "\e006";
@arrow-left3: '"fontsmith-icons"' '"\\e006"';
@loop-font-family: "fontsmith-icons";
@loop-value: "\e002";
@loop: '"fontsmith-icons"' '"\\e002"';
.icon-font-family(@char) {
font-family: ~`@{char}[0]`;
}
.icon-font() {
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
}
.icon-content(@char) {
content: ~`@{char}[1]`;
}
.icon(@char) {
.icon-font-family(@char);
.icon-font();
&:before {
.icon-content(@char);
}
}
@font-face {
font-family: "fontsmith-icons";
src:url("/fonts/icons.woff") format("woff"),
;
font-weight: normal;
font-style: normal;
}
-52
View File
@@ -1,52 +0,0 @@
.promess {
padding: 0em 2em 3em;
font-weight: normal;
font-size: 1.2em;
}
.url {
width: 50%;
}
.launchBtn {
background: #e74c3c;
color: #fff;
&.disabled {
background: #deaca6;
}
}
.features {
display: table;
width: 50%;
margin: 8em auto 0;
font-size: 0.9em;
color: #413;
> div {
width: 33.3%;
display: table-cell;
padding: 0 1.5em;
}
}
input[type=submit], input.url {
padding: 0 0.5em;
margin: 0.5em;
font-size: 1.2em;
height: 2em;
border: 0 solid;
border-radius: 0.5em;
box-shadow: 0.1em 0.2em 0 0 #5e2846;
outline: none;
}
input[type=submit]:hover {
color: #ddd;
}
input[type=submit].clicked {
color: #ddd;
position: relative;
left: 0.1em;
top: 0.2em;
box-shadow: none;
}
-212
View File
@@ -1,212 +0,0 @@
@import "icons.less";
html {
margin: 100px 50px;
}
body {
margin: 0 auto;
max-width: 1280px;
background: #9c4274;
color: #fff;
font-size: 16px;
text-align: center;
}
body, input[type=submit], input[type=text], input[type=url], input[type=number], button {
font-family: 'Century Gothic', helvetica, arial, sans-serif;
}
input[type=submit] {
cursor: pointer;
}
h1 span {
display: inline-block;
height: 1em;
width: 1em;
color: #ffa319;
}
.resultsMenu {
margin-top: 2em;
}
.resultsMenu .menuItem {
display: inline-block;
margin: 1em;
width: 8em;
height: 7em;
color: #fff;
border: 3px solid #fff;
border-radius: 0.5em;
cursor: pointer;
text-decoration: none;
&.back, &.restart {
color: #413;
border-color: #413;
}
}
.resultsMenu .menuItem div {
padding-top: 0.5em;
font-size: 3em;
}
.resultsMenu .active, .resultsMenu .menuItem.active:hover {
color: #ffa319;
border-color: #ffa319;
}
.resultsMenu .menuItem:hover {
color: #ffa319;
}
.resultsMenu span {
position: relative;
top: 0.5em;
}
/* Grade colors */
.A {
/* green */
background: #0C4;
}
.B {
/* green */
background: #CD0;
}
.C {
/* yellow */
background: #FD2;
}
.D {
/* orange */
background: #FA2;
}
.E {
/* red */
background: #F60;
}
.F {
/* red */
background: #F22;
}
.NA {
/* Non applicable */
background: #CCC;
}
.board {
margin-top: 2em;
padding: 1em;
background: #fff;
color: #000;
border-radius: 0.5em;
text-align: left;
}
.backToDashboard {
text-align: center;
a {
font-size: 0.9em;
display: block;
margin-top: 4em;
color: black;
}
}
a.linkButton {
font-size: 1em;
padding: 0.3em 0.5em;
margin: 0.5em;
line-height: 2em;
border: 0 solid;
border-radius: 0.5em;
box-shadow: 0.1em 0.2em 0 0 #5e2846;
background: #e74c3c;
color: #fff;
text-decoration: none;
}
.screenshotWrapper.desktop {
display: inline-block;
position: relative;
border: 0.2em solid #AAA;
background: #000;
padding: 0.5em;
border-top-left-radius: 0.4em;
border-top-right-radius: 0.4em;
&:before {
position: absolute;
width: 15em;
height: 0.6em;
bottom: -0.75em;
left: -1em;
background: #CCC;
border-bottom-left-radius: 0.2em;
border-bottom-right-radius: 0.2em;
content: " ";
}
&:after {
position: absolute;
width: 0.4em;
height: 0.2em;
bottom: -0.55em;
left: 12.5em;
background: lime;
content: " ";
}
> div {
width: 12em;
height: 6.75em;
overflow: scroll;
position: relative;
}
.screenshotImage {
width: 100%;
}
}
.star {
font-weight: bold;
span {
font-size: 1.2em;
}
}
.footer {
padding: 3em;
color: #413;
a {
color: inherit;
}
.version {
font-size: 0.7em;
}
}
/* Icons */
.icon-lab {
.icon(@lab);
}
.icon-question {
.icon(@question);
}
.icon-warning {
.icon(@warning);
}
.icon-back {
.icon(@arrow-left3);
}
.icon-summary {
.icon(@list);
}
.icon-spaghetti {
.icon(@bars);
}
.icon-loop {
.icon(@loop);
}
-9
View File
@@ -1,9 +0,0 @@
.status {
margin-top: 2em;
font-size: 2.5em;
}
.statusSubMessage {
font-size: 0.8em;
margin-bottom: 6em;
}
-220
View File
@@ -1,220 +0,0 @@
.rule.board {
text-align: center;
}
.rule .ruleTable {
display: table;
border-spacing: 1em;
width: 90%;
margin: 2em auto;
background: #f2f2f2;
border: 1px dashed #666;
border-radius: 0.5em;
> div {
display: table-cell;
vertical-align: middle;
}
.left {
width: 33%;
font-weight: bold;
}
.right {
width: 67%;
}
}
.rule .score {
font-size: 2.5em;
line-height: 2em;
height: 2em;
width: 2em;
border-radius: 0.5em;
margin: 0 auto 0.5em;
}
.rule .message {
width: 80%;
margin: 0 auto;
p {
margin: 0.5em;
}
}
.rule .message ul {
list-style-type: none;
padding-left: 0;
}
.rule .message li:before {
content:'\25e6';
margin-right: 0.3em;
font-size: 1.2em;
position: relative;
top: 0.1em;
}
.rule .warning {
width: 90%;
margin: -1em auto 2em;
background: #FEE;
border: 1px dashed #e74c3c;
color: #e74c3c;
border-radius: 0.5em;
}
.rule .offendersTable {
display: table;
border-spacing: 0 0.25em;
margin: 0 auto;
min-width: 10%;
max-width: 90%;
> div {
display: table-row;
> div {
display: table-cell;
background: #f2f2f2;
padding: 0 1em;
word-wrap: break-word;
word-break: break-all;
&:hover {
background: #EBD8E2;
}
}
}
}
.rule .notFound {
font-size: 1em;
h2 {
font-size: 3em;
margin-bottom: 1em;
}
}
.offenders {
.offenderButton {
display: inline-block;
position: relative;
background: #efe;
padding: 0 0.5em;
margin: 0.2em 0;
border-radius: 0.4em;
z-index: 1;
&.opens {
padding-right: 0.75em;
&:after {
position: relative;
left: 0.5em;
content: '\25BC';
font-size: 0.8em;
}
}
> div {
display: none;
position: absolute;
right: 0;
min-width: 100%;
background: inherit;
border-bottom-left-radius: 0.4em;
border-bottom-right-radius: 0.4em;
border-top: 1px solid #999;
z-index: 2;
}
.domTree {
text-align: left;
white-space: nowrap;
> div {
margin: 0.5em;
div {
margin-left: 1em;
}
}
}
.backtrace, .cssFileAndLine {
white-space: nowrap;
padding: 0.5em;
}
&.opens:hover {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
background: #ffe0cc;
z-index: 2;
> div {
display: block;
}
}
}
}
.offendersHtml {
display: inline-block;
}
.domTree div {
text-align: left;
margin-left: 1em;
span:only-child {
font-weight: bold;
span {
font-style: italic;
font-weight: normal;
}
}
}
.colorPalette {
width: 30em;
border: 2px solid #000;
text-align: left;
/* 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;
> div {
display: inline-block;
height: 2em;
position: relative;
div {
display: none;
position: absolute;
left: 100%;
top: 100%;
background: #FFF;
padding: 0.5em;
border: 2px solid #f1c40f;
border-radius: 0.5em;
white-space: nowrap;
z-index: 3;
font-weight: bold;
}
&:hover div {
display: block;
}
&:hover:after {
content: " ";
position: absolute;
background-color: inherit;
left: -0.2em;
top: -0.2em;
width: 100%;
height: 100%;
z-index: 2;
border: 0.2em solid #f1c40f;
}
}
}
-8
View File
@@ -1,8 +0,0 @@
.screenshot.board {
text-align: center;
}
.screenshot .screenshotWrapper {
font-size: 2.08333333333333em;
margin-bottom: 0.5em;
}
-356
View File
@@ -1,356 +0,0 @@
/* Timeline colors, related to Window Performances */
@domCreationColor: #FF6600;
@domCreationBg: #FFE0CC;
@domContentLoadedColor: #A7E846;
@domContentLoadedBg: #E0FFD1;
@domContentLoadedEndColor: #7ECCCC;
@domContentLoadedEndBg: #D8F0F0;
@domCompleteColor: #C2A3FF;
@domCompleteBg: #EDE3FF;
@domInteractiveColor: #FFE433;
@domInteractiveBg: #FFFCCC;
.execution {
text-align: center;
}
.selectScript {
padding-bottom: 2em;
font-size: 0.9em;
select {
max-width: 30em;
}
&.empty {
font-size: 0.8em;
select {
width: 10em;
}
}
}
.timeline {
margin: 2em 0 5em;
}
.timeline .chart {
position: relative;
width: 100%;
border-bottom: 1px solid #000;
}
.timeline .startTime, .timeline .endTime {
position: absolute;
bottom: 0.5em;
font-size: 0.8em;
}
.timeline .startTime {
left: 0em;
}
.timeline .endTime {
right: 0em;
}
.timeline .chartPoints {
display: table;
height: 100px;
width: 99%;
margin: 0 auto;
}
.timeline .interval {
display: table-cell;
position: relative;
height: 100px;
width: 0.5%;
}
.timeline .interval .color {
position: absolute;
bottom: 0;
width: 100%;
&.clickable {
cursor: pointer;
}
}
.timeline div.interval:hover {
background: #9C4274;
}
.timeline .interval:hover .color {
background: #F04DA7;
}
.timeline .domComplete.interval {
background: @domCompleteBg;
}
.timeline .domComplete .color {
background: @domCompleteColor;
}
.timeline .domContentLoadedEnd.interval {
background: @domContentLoadedEndBg;
}
.timeline .domContentLoadedEnd .color {
background: @domContentLoadedEndColor;
}
.timeline .domContentLoaded.interval {
background: @domContentLoadedBg;
}
.timeline .domContentLoaded .color {
background: @domContentLoadedColor;
}
.timeline .domInteractive.interval {
background: @domInteractiveBg;
}
.timeline .domInteractive .color {
background: @domInteractiveColor;
}
.timeline .domCreation.interval {
background: @domCreationBg;
}
.timeline .domCreation .color {
background: @domCreationColor;
}
.timeline .tooltip.detailsOverlay {
position: absolute;
display: none;
width: auto;
padding: 0.5em 1em;
top: -1.5em;
right: 1em;
}
.timeline .interval:hover .tooltip {
display: block;
}
.timeline .legend {
display: table;
width: 100%;
margin-top: 1em;
}
.timeline .legend > div {
display: table-row;
}
.timeline .legend > div > div {
position: relative;
display: table-cell;
margin-top: 1em;
}
.timeline .titles {
font-weight: bold;
}
.timeline .titles > div {
padding: 0 1em 0 2em;
}
.timeline .tips {
font-size: 0.7em;
}
.timeline .tips > div {
padding: 1em 1em 0 0;
}
.timeline .legend .color {
display: block;
position: absolute;
left: 0;
height: 1.5em;
width: 1.5em;
border-radius: 0.2em;
}
.filters {
margin: 1em auto;
padding: 0.5em;
min-width: 30em;
width: 30%;
border: 1px dotted #aaa;
text-align: left;
}
.table {
display: table;
width: 100%;
border-spacing: 0.25em;
}
.table > div {
display: table-row;
}
.table > .headers > div {
font-weight: bold;
padding: 0.5em 1em;
}
.table > div > div {
padding: 0.1em 1em;
background: #f2f2f2;
display: table-cell;
text-align: left;
}
.table > div.jsError > .type, .table > div.jsError > .value {
color: #e74c3c;
font-weight: bold;
}
.table > div.windowPerformance > div, .table > div.windowPerformance > div.startTime {
background: #EBD8E2;
}
.table > div.showingDetails > div {
background: #f1c40f;
}
.table > div.highlight {
> div.startTime {
background-color: #C0F090;
}
}
.table > div.highlight-remove {
transition: 3s;
> div.startTime {
transition: background-color 3s ease-in;
}
}
.table > div > .index {
color: #bbb;
word-break: normal;
}
.table > div > .type {
white-space:nowrap;
}
.table .children {
margin-top: 0.2em;
font-size: 0.8em;
line-height: 1.6em;
}
.table .child {
margin-left: 0.5em;
> .child {
margin-left: 1em;
}
&:before {
content: "↳";
}
.childArgs {
display: none;
}
span {
position: relative;
}
span:hover {
background: #EBD8E2;
div {
display: inline-block;
}
.childArgs {
display: block;
position: absolute;
padding: 0 1em 0 2em;
left: 100%;
top: 0;
background: #EBD8E2;
line-height: 1.3em;
height: 1.3em;
z-index: 2;
}
}
}
.table .showingDetails .child span:hover {
background: inherit;
.childArgs {
display: none;
}
}
.table > div > .value {
width: 70%;
word-break: break-all;
}
.table > div > .details {
position: relative;
}
.table .details .icon-question {
color: #f1c40f;
cursor: pointer;
}
.table .icon-warning {
display: inline-block;
width: 0.8em;
}
.detailsOverlay {
display: none;
position: absolute;
right: 3em;
top: -3em;
width: 45em;
min-height: 1em;
padding: 0 1em 1em;
background: #fff;
border: 2px solid #f1c40f;
border-radius: 0.5em;
z-index: 2;
}
@media screen and (max-width: 1024px) {
.detailsOverlay {
width: 25em;
}
}
.showDetails .detailsOverlay {
display: block;
}
.detailsOverlay .closeBtn {
position: absolute;
top: 0.5em;
right: 0.5em;
color: #f1c40f;
cursor: pointer;
}
.detailsOverlay .advice {
color: #e74c3c;
font-weight: bold;
}
.detailsOverlay .trace {
word-break: break-all;
}
.table > div > .duration, .table > div > .startTime {
text-align: center;
white-space:nowrap;
}
.table > div > .startTime.domComplete {
background: @domCompleteBg;
}
.table > div > .startTime.domContentLoadedEnd {
background: @domContentLoadedEndBg;
}
.table > div > .startTime.domContentLoaded {
background: @domContentLoadedBg;
}
.table > div > .startTime.domInteractive {
background: @domInteractiveBg;
}
.table > div > .startTime.domCreation {
background: @domCreationBg;
}
.execution .icon-warning {
color: #e74c3c;
cursor: pointer;
}
.warningsFilterOn {
> div {
display: none;
&.warning {
display: table-row;
}
}
}
-55
View File
@@ -1,55 +0,0 @@
<html>
<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" />
<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">
<!-- 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="/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/directives/gradeDirective.js"></script>
<script src="/js/directives/offendersDirectives.js"></script>
<!-- endbuild -->
<head>
<body ng-app="YellowLabTools">
<div id="header"><h1>Yellow Lab <span class="icon-lab"></span> Tools</h1></div>
<div id="body" ng-view autoscroll="true"></div>
<div class="footer">
<a href="/about">Learn more</a><br>If you like <b>Yellow Lab Tools</b>, <a href="https://github.com/gmetais/YellowLabTools" target="_blank" class="star">give it a <span>&#9733;</span> on GitHub</a>!<br>
<span class="version">@@version</span>
</div>
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','//www.google-analytics.com/analytics.js','ga');if('@@googleAnalyticsId'.indexOf('UA-')===0){ga('create','@@googleAnalyticsId','auto');}
</script>
</body>
</html>
-9
View File
@@ -1,9 +0,0 @@
<div class="about">
<p><b>Yellow Lab Tools</b> is an open source project by <a href="http://www.gaelmetais.com" target="_blank">Gaël Métais</a>. It lets you test a webpage (via an URL) and detects <b>performance</b> and <b>front-end code quality</b> issues.</p>
<p>This is done by loading the webpage via PhantomJS and collecting various metrics and statistics with the help of <a href="https://github.com/macbre/phantomas" target="_blank">Phantomas</a>. These metrics are categorized and transformed into scores. It also gives in-depth details so developpers can correct the detected issues.</p>
<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>
</div>
-70
View File
@@ -1,70 +0,0 @@
<div ng-include="'views/resultSubHeader.html'"></div>
<div class="summary board">
<div class="globalScore" ng-if="globalScore === 0 || globalScore > 0">
<div>
<h2>Global score</h2>
<div class="globalScoreDisplay">
<grade score="result.scoreProfiles.generic.globalScore" class="globalGrade"></grade>
<div class="on100">{{globalScore}}/100</div>
</div>
</div>
<div>
<a href="/result/{{result.runId}}/screenshot">
<div class="screenshotWrapper desktop">
<div>
<img class="screenshotImage" ng-src="{{result.screenshotUrl}}"/>
</div>
</div>
</a>
</div>
</div>
<h2 ng-if="!error && !fromSocialShare">Score details</h2>
<div ng-if="!error && !fromSocialShare" class="notations">
<div ng-repeat="categoryKey in categoriesOrder" ng-init="category = result.scoreProfiles.generic.categories[categoryKey]">
<grade score="category.categoryScore" class="categoryScore"></grade>
<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)">
<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.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>
</div>
</div>
</div>
</div>
<div ng-if="!error && !fromSocialShare" class="apiTip">
<b>Did you know? Yellow Lab Tools now has an API</b>! <a href="{{getAPIUrl()}}" target="_blank">Here</a> is the JSON output for this run. Checkout <a href="https://github.com/gmetais/YellowLabTools/wiki/Public-API" target="_blank">the API doc</a>.
</div>
<div class="tweet" ng-if="!error && !fromSocialShare">
<form ng-submit="sendTweet()">
<input type="text" class="tweetText" ng-model="tweetText">
<input type="submit" class="tweetButton" value="Tweet this!" ng-click="shareOnTwitter(tweetText)">
<input type="submit" class="linkedinButton" value="LinkedIn this!" ng-click="shareOnLinkedin(tweetText)">
</form>
</div>
<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>
</div>
<div ng-if="error">
<h2>Run failed / Run not found</h2>
</div>
</div>
-24
View File
@@ -1,24 +0,0 @@
<h2 class="promess">Free online test to help speeding up <b>heavy</b> web pages</h2>
<form ng-submit="launchTest()" >
<input type="text" name="url" ng-model="url" placeholder="http://www.mysite.com" class="url" />
<input type="submit" value="Launch test" class="launchBtn" ng-class="{disabled: !url}" />
</form>
<div class="features">
<div>
<h3>Front-end quality</h3>
<p>Audits quality problems in the HTML, CSS and JS</p>
</div>
<div>
<h3>WebPerf</h3>
<p>Checks if performance good practices are applied</p>
</div>
<div>
<h3>JS Profiling</h3>
<p>Untangles the JavaScript spaghetti code</p>
</div>
</div>
-36
View File
@@ -1,36 +0,0 @@
<p ng-if="url">Tested url: &nbsp; <a href="{{url}}" target="_blank" class="testedUrl">{{url}}</a></p>
<div ng-if="!notFound && !connectionLost">
<div ng-if="status.statusCode == 'failed'">
<div class="status">Test failed</div>
<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>
</div>
<div ng-if="status.statusCode == 'awaiting'">
<div class="status">
<ng-pluralize count="status.position" when="{'one': 'Waiting behind 1 other test', 'other': 'Waiting behind {} other tests'}">
</ng-pluralize>
</div>
<p class="statusSubMessage">(auto-refresh activated)</p>
</div>
<div ng-if="status.statusCode == 'running'">
<div class="status">Test is running...</div>
<p class="statusSubMessage">(auto-refresh activated)</p>
</div>
<div ng-if="status.statusCode == 'complete'">
<div class="status">Test complete</div>
<p class="statusSubMessage">Opening results...</p>
</div>
</div>
<div ng-if="notFound == true">
<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>
</div>
<div ng-if="connectionLost == true">
<div class="status">Connection lost with server</div>
<p class="statusSubMessage">Check your wifi cable, or maybe YellowLab.tools is rebooting.</p>
</div>
-8
View File
@@ -1,8 +0,0 @@
<div>Tested url: &nbsp; <a href="{{result.params.url}}" target="_blank" class="testedUrl">{{result.params.url}}</a></div>
<div class="resultsMenu">
<a class="menuItem back" href="/"><div class="icon-back"></div><span>New test<span></a>
<a class="menuItem restart" href="" ng-click="testAgain()"><div class="icon-loop"></div><span>Test again<span></a>
<div class="menuItem" ng-class="{active: Menu.getCurrentPage() == 'dashboard'}" ng-click="Menu.changePage('dashboard')"><div class="icon-summary"></div><span>Dashboard</span></div>
<div class="menuItem" ng-class="{active: Menu.getCurrentPage() == 'timeline'}" ng-click="Menu.changePage('timeline')"><div class="icon-spaghetti"></div><span>JS Timeline</span></div>
</div>
-177
View File
@@ -1,177 +0,0 @@
<div ng-include="'views/resultSubHeader.html'"></div>
<div class="rule board">
<div class="backToDashboard"><a href="#" ng-click="backToDashboard()">Back to dashboard</a></div>
<div ng-if="rule" class="ruleTable">
<div class="left">
<h2>{{rule.policy.label}}</h2>
<grade score="rule.score" class="score"></grade>
<div>{{rule.score}}/100</div>
</div>
<div class="right">
<h3>Value: {{rule.value}}</h3>
<div ng-bind-html="rule.policy.message" class="message"></div>
</div>
</div>
<div ng-if="rule.abnormal" class="warning">
<h3>Warning</h3>
<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>
<div ng-if="rule.offendersObj.list" class="offendersTable">
<div ng-repeat="offender in rule.offendersObj.list track by $index">
<div ng-if="offender.parseError">
{{offender.parseError}}
</div>
<div ng-if="!offender.parseError">
<div ng-if="policyName === 'DOMidDuplicated'">
<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>
<div ng-if="policyName === 'jsErrors'">
<b>{{offender.error}}</b>
<div class="offenderButton" ng-if="offender.backtrace.length == 0">no backtrace</div>
<div class="offenderButton opens" ng-if="offender.backtrace.length > 0">
backtrace
<div class="backtrace">
<div ng-repeat="obj in offender.backtrace track by $index">
<span ng-if="obj.functionName">{{obj.functionName}}()</span>
<url-link url="obj.file" max-length="60"></url-link>
line {{obj.line}}
</div>
</div>
</div>
</div>
<div ng-if="policyName === 'documentWriteCalls'">
<b>{{offender.writeFn}}</b>
<span ng-if="offender.from">
called from
<span ng-if="offender.from.functionName">{{offender.from.functionName}}()</span>
<url-link url="offender.from.file" max-length="50"></url-link>
line {{offender.from.line}}
</span>
<span ng-if="!offender.from">
called from (no backtrace available)
</span>
</div>
<div ng-if="policyName === '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>
<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>
</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>
<div ng-if="policyName === 'notFound' || policyName === 'closedConnections' || policyName === 'multipleRequests' || policyName === 'cachingDisabled' || policyName === 'cachingNotSpecified'">
<url-link url="offender" max-length="100"></url-link>
</div>
<div ng-if="policyName === 'cachingTooShort'">
<url-link url="offender.file" max-length="100"></url-link>
cached for <b>{{offender.ttlWithUnit}} {{offender.unit}}</b>
</div>
<div ng-if="policyName === 'domains'">
<b>{{offender.domain}}</b>
(<ng-pluralize count="offender.requests" when="{'one':'1 request','other':'{} requests'}"></ng-pluralize>)
</div>
<div ng-if="policyName === 'globalVariables' || policyName === 'jQueryVersionsLoaded'">
{{offender}}
</div>
</div>
</div>
</div>
<div ng-if="!rule.offendersObj.list" class="offendersHtml">
<div ng-if="policyName === 'DOMelementMaxDepth'">
<dom-tree tree="rule.offendersObj.tree"></dom-tree>
</div>
<div ng-if="policyName === 'cssColors' && rule.offendersObj.count > 0">
<p>This is the colors palette, sized by total occurrences:</p>
<div class="colorPalette">
<div ng-repeat="offender in rule.offendersObj.palette" style="background-color: {{offender.color}}; width: {{offender.occurrences * 100 / rule.offendersObj.palette[0].occurrences}}%"><div>{{offender.color}} ({{offender.occurrences}} times)</div></div>
</div>
</div>
</div>
</div>
<div ng-if="!rule && rule !== null" class="notFound">
<h2>404</h2>
Rule "{{policyName}}"" not found
</div>
<div class="backToDashboard"><a href="#" ng-click="backToDashboard()">Back to dashboard</a></div>
</div>
-14
View File
@@ -1,14 +0,0 @@
<div ng-include="'views/resultSubHeader.html'"></div>
<div class="screenshot board">
<h2>Screenshot</h2>
<div class="screenshotWrapper desktop">
<div>
<img class="screenshotImage" ng-src="{{result.screenshotUrl}}"/>
</div>
</div>
<p>(scroll on the screenshot to see under the fold)</p>
<div class="backToDashboard"><a href="#" ng-click="backToDashboard()">Back to dashboard</a></div>
</div>
-81
View File
@@ -1,81 +0,0 @@
<div ng-include="'views/resultSubHeader.html'"></div>
<div class="execution board">
<div class="selectScript" ng-class="{empty:!selectedScript}">
Filter timeline and profiler by script:
<select ng-model="selectedScript" ng-options="script.shortPath for script in scripts" ng-change="changeScript()">
<option value="">All (no filter)</option>
</select>
</div>
<h2>Javascript Timeline</h2>
<p>This graph gives a quick view of when the Javascript interactions with the DOM occur during the loading of the page.</p>
<div class="timeline">
<div class="chart">
<div class="chartPoints">
<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
}">
<div style="height: {{100 * duration / timelineMax | number: 0}}px" class="color" ng-class="{clickable: duration > 0}" scroll-on-click="{{$index * timelineIntervalDuration}}"></div>
<div class="tooltip detailsOverlay">
<div>
Timestamp: {{$index * timelineIntervalDuration | number: 0}} ms
</div>
</div>
</div>
</div>
<div class="startTime">0 ms</div>
<div class="endTime">{{endTime | number: 0}} ms</div>
</div>
<div class="legend">
<div class="titles">
<div class="domCreation"><div class="color"></div>DOM creation</div>
<div class="domInteractive"><div class="color"></div>DOM interactive</div>
<div class="domContentLoaded"><div class="color"></div>DOM content loaded event</div>
<div class="domContentLoadedEnd"><div class="color"></div>Page completion</div>
<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>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>
<div>The page is considered loaded, it's time for low <b>priority things</b> : trackers, social plugins, easter egg...</div>
</div>
</div>
</div>
<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.
</p>
<div class="filters">
<div>
<input type="checkbox" ng-model="warningsFilterOn" id="warningsFilterOn" />
<label for="warningsFilterOn">Show warnings only</label>
</div>
</div>
<div class="table" ng-class="{warningsFilterOn: warningsFilterOn}">
<div class="headers">
<div><!-- index --></div>
<div>Type</div>
<div>Params</div>
<div><!-- details --></div>
<div>Timestamp</div>
</div>
<profiler-line ng-repeat="node in profilerData" data-index="$index" node="node"></profiler-line>
</div>
<div class="backToDashboard"><a href="#" ng-click="backToDashboard()">Back to dashboard</a></div>
</div>
+51 -10
View File
@@ -1,11 +1,13 @@
var Q = require('q');
var debug = require('debug')('ylt:index');
var Q = require('q');
var Runner = require('./runner');
var Runner = require('./runner');
var ScreenshotHandler = require('./screenshotHandler');
var packageJson = require('../package.json');
var yellowLabTools = function(url, options) {
'use strict';
var deferred = Q.defer();
if (!url) {
@@ -24,15 +26,54 @@ var yellowLabTools = function(url, options) {
};
var runner = new Runner(params)
.then(function(data) {
.progress(deferred.notify)
.then(function(data) {
// If a screenshot saveFunction was provided in the options
if (options && typeof options.saveScreenshotFn === 'function') {
debug('Now optimizing screenshot...');
// TODO: temporarily set all screenshot sizes to 600px, until we find a solution
ScreenshotHandler.findAndOptimizeScreenshot(data.params.options.screenshot, 600)
.then(function(screenshotBuffer) {
debug('Screenshot optimized, now saving...');
return options.saveScreenshotFn('screenshot.jpg', screenshotBuffer);
})
.then(function(response) {
debug('Screenshot saved');
debug(response);
// Remove uneeded temp screenshot path
delete data.params.options.screenshot;
})
.catch(function(err) {
// It's ok if we can't save the screenshot
debug('Screenshot could not be saved');
debug(err);
})
.finally(function() {
deferred.resolve(data);
});
} else {
deferred.resolve(data);
})
.fail(function(err) {
deferred.reject(err);
});
}
})
.catch(function(err) {
deferred.reject(err);
});
}
return deferred.promise;
};
module.exports = yellowLabTools;
module.exports = yellowLabTools;
module.exports.version = packageJson.version;
+539 -712
View File
File diff suppressed because it is too large Load Diff
+61 -60
View File
@@ -1,44 +1,58 @@
{
"categories": {
"pageWeight": {
"label": "Page weight",
"policies": {
"totalWeight": 5,
"imageOptimization": 2,
"imagesTooLarge": 1,
"compression": 2,
"fileMinification": 2
}
},
"requests": {
"label": "Requests",
"policies": {
"totalRequests": 2,
"domains": 3,
"notFound": 2,
"identicalFiles": 2,
"emptyRequests": 3,
"lazyLoadableImagesBelowTheFold": 2,
"hiddenImages": 1
}
},
"domComplexity": {
"label": "DOM complexity",
"policies": {
"DOMelementsCount": 1,
"DOMelementsCount": 3,
"DOMelementMaxDepth": 1,
"iframesCount": 1,
"DOMidDuplicated": 1
}
},
"domManipulations": {
"label": "DOM manipulations",
"javascriptComplexity": {
"label": "JS complexity",
"policies": {
"DOMinserts": 2,
"DOMqueries": 1,
"DOMqueriesWithoutResults": 2,
"DOMqueriesAvoidable": 2,
"eventsBound": 1
"scriptDuration": 4,
"DOMaccesses": 2,
"eventsScrollBound": 1
}
},
"badJavascript": {
"label": "Bad JavaScript",
"label": "Bad JS",
"policies": {
"jsErrors": 1,
"documentWriteCalls": 2,
"consoleMessages": 0.5,
"synchronousXHR": 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
}
},
"cssComplexity": {
@@ -46,71 +60,58 @@
"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": {
"cssImports": 3,
"cssParsingErrors": 3,
"cssImports": 4,
"cssDuplicatedSelectors": 2,
"cssDuplicatedProperties": 1,
"cssEmptyRules": 2,
"cssExpressions": 1,
"cssImportants": 3,
"cssImportants": 2,
"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": 0.5,
"unusedUnicodeRanges": 0.5,
"nonWoff2Fonts": 0.5
}
},
"smallRequests": {
"label": "Small requests",
"serverConfig": {
"label": "Server config",
"policies": {
"smallJsFiles": 1,
"smallCssFiles": 1,
"smallImages": 1
}
},
"network": {
"label": "Network",
"policies": {
"notFound": 2,
"oldHttpProtocol": 2,
"oldTlsProtocol": 2,
"closedConnections": 2,
"multipleRequests": 2,
"cachingNotSpecified": 1,
"cachingDisabled": 1,
"cachingTooShort": 1,
"domains": 1
"cachingTooShort": 1
}
}
},
"globalScore": {
"domComplexity": 1,
"domManipulations": 2,
"badJavascript": 1,
"jQueryVersion": 1,
"cssSyntaxError": 1,
"cssComplexity": 1,
"pageWeight": 3,
"requests": 2,
"domComplexity": 2,
"javascriptComplexity": 2,
"badJavascript": 2,
"jQuery": 0.5,
"cssComplexity": 0.5,
"badCSS": 1,
"requests": 3,
"smallRequests": 1,
"network": 2
"fonts": 1,
"serverConfig": 1
}
}
+45 -20
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 = /^\s*at( (\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,7 @@ var OffendersHelpers = function() {
}
results.push(obj);
} else {
return null;
}
}
return results;
@@ -151,24 +153,19 @@ var OffendersHelpers = function() {
};
this.cssOffenderPattern = function(offender) {
var parts = /^(.*) (?:<([^ \(]*)>|\[inline CSS\]) @ (\d+):(\d+)$/.exec(offender);
if (!parts) {
return {
offender: offender
};
} else {
return {
css: parts[1],
file: parts[2] || null,
line: parseInt(parts[3], 10),
column: parseInt(parts[4], 10)
};
}
// Used to work with strings
// As of Phantomas v2, offender is now in JSON format.
// Let's just adapt this for now and we'll see later if we remove completely this function
return {
css: offender.value.message,
file: offender.url,
line: offender.value.position.start.line,
column: offender.value.position.start.column
};
};
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 +179,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();
+2 -1
View File
@@ -111,7 +111,8 @@ var RulesChecker = function() {
if (rule) {
rule.policy = {
label: policy.label,
message: policy.message
message: policy.message,
hasOffenders: policy.hasOffenders || false
};
results[metricName] = rule;
+89 -12
View File
@@ -2,7 +2,10 @@ var Q = require('q');
var debug = require('debug')('ylt:runner');
var phantomasWrapper = require('./tools/phantomas/phantomasWrapper');
var jsExecutionTransformer = require('./tools/jsExecutionTransformer');
var colorDiff = require('./tools/colorDiff');
var domAccessAgregator = require('./tools/domAccessAgregator');
var mediaQueriesChecker = require('./tools/mediaQueriesChecker');
var redownload = require('./tools/redownload/redownload');
var rulesChecker = require('./rulesChecker');
var scoreCalculator = require('./scoreCalculator');
@@ -19,14 +22,67 @@ var Runner = function(params) {
};
// Execute Phantomas first
phantomasWrapper.execute(data).then(function(phantomasResults) {
phantomasWrapper.execute(data)
// For the progress bar
.progress(function(event) {
if (event === 'domReady') {
deferred.notify({
estimatedProgress: 0.15,
milestone: 'domReady'
});
}
if (event === 'domComplete') {
deferred.notify({
estimatedProgress: 0.25,
milestone: 'domComplete'
});
}
})
.then(function(phantomasResults) {
// For the progress bar
deferred.notify({
estimatedProgress: 0.4,
milestone: 'phantomas'
});
data.toolsResults.phantomas = phantomasResults;
// Treat the JS Execution Tree from offenders
data = jsExecutionTransformer.transform(data);
// Mix all DOM Access metrics together
data = domAccessAgregator.agregate(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)
.progress(function(redownloadedProgress) {
deferred.notify({
estimatedProgress: 0.4 + redownloadedProgress * 0.55,
milestone: 'phantomas'
});
});
})
.then(function(data) {
// For the progress bar
deferred.notify({
estimatedProgress: 0.99,
milestone: 'redownload'
});
// Fix: don't display Unicode ranges if the module is not present in Phantomas
if (!data.toolsResults.phantomas.metrics.differentCharacters) {
delete data.toolsResults.redownload.metrics.unusedUnicodeRanges;
delete data.toolsResults.redownload.offenders.unusedUnicodeRanges;
}
// Rules checker
var policies = require('./metadata/policies');
@@ -36,21 +92,42 @@ var Runner = function(params) {
// Scores calculator
var scoreProfileGeneric = require('./metadata/scoreProfileGeneric.json');
data.scoreProfiles = {
generic : scoreCalculator.calculate(data, scoreProfileGeneric)
generic : scoreCalculator.calculate(data, scoreProfileGeneric, true)
};
delete data.toolsResults.phantomas.metrics.javascriptExecutionTree;
delete data.toolsResults.phantomas.offenders.javascriptExecutionTree;
return data;
// Calculate "If you fix this issue, your new score is..." on each rule
debug('Calculate "If you fix this issue..." scores');
Object.keys(data.rules).forEach(function(ruleName) {
// Save current values
var oldScore = data.rules[ruleName].score;
var oldAbnormalityScore = data.rules[ruleName].abnormalityScore;
// Simulate a 100/100 score on a specific rule
data.rules[ruleName].score = 100;
data.rules[ruleName].abnormalityScore = 0;
// Calculate new score
data.rules[ruleName].globalScoreIfFixed = scoreCalculator.calculate(data, scoreProfileGeneric, false).globalScore;
// Revert values
data.rules[ruleName].score = oldScore;
data.rules[ruleName].abnormalityScore = oldAbnormalityScore;
});
debug('Calculating is finished');
}).then(function(data) {
if (data.toolsResults.phantomas.offenders.blockedRequests) {
data.blockedRequests = data.toolsResults.phantomas.offenders.blockedRequests;
}
// Report WordPress detection
if (data.toolsResults.redownload.metrics.isWordPress === true) {
data.frameworks = {isWordPress: true};
}
// Finished!
deferred.resolve(data);
})
}).fail(function(err) {
.fail(function(err) {
debug('Run failed');
debug(err);
+9 -5
View File
@@ -4,7 +4,7 @@ var debug = require('debug')('ylt:scoreCalculator');
var ScoreCalculator = function() {
'use strict';
this.calculate = function(data, profile) {
this.calculate = function(data, profile, debugFlag) {
var results = {
categories: {}
@@ -13,7 +13,9 @@ var ScoreCalculator = function() {
var categoryName;
var weight;
debug('Starting calculating scores');
if (debugFlag) {
debug('Starting calculating scores');
}
// Calculate categories
for (categoryName in profile.categories) {
@@ -31,7 +33,7 @@ var ScoreCalculator = function() {
if (data.rules[policyName]) {
policyScore = data.rules[policyName].score + (data.rules[policyName].abnormalityScore * 2);
categoryScore.push(policyScore, weight);
} else {
} else if (debugFlag) {
debug('Warning: could not find rule %s', policyName);
}
@@ -59,8 +61,10 @@ var ScoreCalculator = function() {
results.globalScore = Math.round(globalScore.getScore());
debug('Score calculation finished:');
debug(results);
if (debugFlag) {
debug('Score calculation finished:');
debug(results);
}
return results;
};
+29 -56
View File
@@ -1,6 +1,5 @@
var debug = require('debug')('ylt:screenshotHandler');
var lwip = require('lwip');
var tmp = require('temporary');
var debug = require('debug')('ylt:screenshotHandlerAgent');
var Jimp = require('jimp');
var Q = require('q');
var fs = require('fs');
var path = require('path');
@@ -8,44 +7,16 @@ var path = require('path');
var screenshotHandler = function() {
this.getScreenshotTempFile = function() {
var screenshotTmpFolder = new tmp.Dir();
var tmpFilePath = path.join(screenshotTmpFolder.path, 'screenshot.png');
var that = this;
return {
getTmpFolder: function() {
return screenshotTmpFolder;
},
getTmpFilePath: function() {
return tmpFilePath;
},
toThumbnail: function(width) {
return that.optimize(tmpFilePath, width);
},
deleteTmpFile: function() {
return that.deleteTmpFileAndFolder(tmpFilePath, screenshotTmpFolder);
}
};
};
this.optimize = function(imagePath, width) {
this.findAndOptimizeScreenshot = function(tmpScreenshotPath, width) {
var that = this;
debug('Starting screenshot transformation');
return this.openImage(imagePath)
return this.openImage(tmpScreenshotPath)
.then(function(image) {
that.deleteTmpFile(tmpScreenshotPath);
return that.resizeImage(image, width);
})
.then(this.toBuffer);
@@ -55,7 +26,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,20 +45,25 @@ var screenshotHandler = function() {
this.resizeImage = function(image, newWidth) {
var deferred = Q.defer();
var currentWidth = image.width();
var ratio = newWidth / currentWidth;
var currentWidth = image.bitmap.width;
image.scale(ratio, function(err, image){
if (err) {
debug('Could not resize image');
debug(err);
if (currentWidth > 0) {
var ratio = newWidth / currentWidth;
deferred.reject(err);
} else {
debug('Image correctly resized');
deferred.resolve(image);
}
});
image.scale(ratio, function(err, image){
if (err) {
debug('Could not resize image');
debug(err);
deferred.reject(err);
} else {
debug('Image correctly resized');
deferred.resolve(image);
}
});
} else {
deferred.reject('Could not resize an empty image');
}
return deferred.promise;
};
@@ -96,7 +72,7 @@ var screenshotHandler = function() {
this.toBuffer = function(image) {
var deferred = Q.defer();
image.toBuffer('jpg', {quality: 90}, function(err, buffer){
image.quality(85).getBuffer(Jimp.MIME_JPEG, function(err, buffer){
if (err) {
debug('Could not save image to buffer');
debug(err);
@@ -108,23 +84,20 @@ var screenshotHandler = function() {
}
});
return deferred.promise;
return deferred.promise;
};
this.deleteTmpFileAndFolder = function(tmpFilePath, screenshotTmpFolder) {
this.deleteTmpFile = function(tmpFilePath) {
var deferred = Q.defer();
fs.unlink(tmpFilePath, function (err) {
if (err) {
debug('Screenshot file not found, could not be deleted. But it is not a problem.');
debug('Screenshot temporary file not found, could not be deleted. But it is not a problem.');
} else {
debug('Screenshot file deleted.');
debug('Screenshot temporary file deleted.');
}
screenshotTmpFolder.rmdir();
debug('Screenshot temp folder deleted');
deferred.resolve();
});
@@ -132,4 +105,4 @@ var screenshotHandler = function() {
};
};
module.exports = new screenshotHandler();
module.exports = new screenshotHandler();
-305
View File
@@ -1,305 +0,0 @@
var debug = require('debug')('ylt:server');
var Q = require('q');
var ylt = require('../../index');
var ScreenshotHandler = require('../../screenshotHandler');
var RunsQueue = require('../datastores/runsQueue');
var RunsDatastore = require('../datastores/runsDatastore');
var ResultsDatastore = require('../datastores/resultsDatastore');
var ApiController = function(app) {
'use strict';
var queue = new RunsQueue();
var runsDatastore = new RunsDatastore();
var resultsDatastore = new ResultsDatastore();
// Create a new run
app.post('/api/runs', function(req, res) {
// Add http to the test URL
if (req.body.url && req.body.url.toLowerCase().indexOf('http://') !== 0 && req.body.url.toLowerCase().indexOf('https://') !== 0) {
req.body.url = 'http://' + req.body.url;
}
// 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,
partialResult: req.body.partialResult || null,
screenshot: req.body.screenshot || false,
jsTimeline: req.body.jsTimeline || false
}
};
// Create a temporary folder to save the screenshot
var screenshot;
if (run.params.screenshot) {
screenshot = ScreenshotHandler.getScreenshotTempFile();
}
// Add test to the testQueue
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);
// Listening for position updates
queuePromise.progress(function(position) {
runsDatastore.updatePosition(run.runId, position);
});
// Let's start the run
queuePromise.then(function() {
runsDatastore.updatePosition(run.runId, 0);
debug('Launching test %s on %s', run.runId, run.params.url);
var runOptions = {
screenshot: run.params.screenshot ? screenshot.getTmpFilePath() : false,
jsDeepAnalysis: run.params.jsTimeline
};
return ylt(run.params.url, runOptions);
})
// Phantomas completed, let's save the screenshot if any
.then(function(data) {
debug('Success');
data.runId = run.runId;
// Some conditional steps are made if there is a screenshot
var screenshotPromise = Q.resolve();
if (run.params.screenshot) {
// Replace the empty promise created earlier with Q.resolve()
screenshotPromise = screenshot.toThumbnail(400)
// Read screenshot
.then(function(screenshotBuffer) {
if (screenshotBuffer) {
debug('Image optimized');
data.screenshotBuffer = screenshotBuffer;
// Official path to get the image
data.screenshotUrl = '/api/results/' + data.runId + '/screenshot.jpg';
}
})
// Delete screenshot temporary file
.then(screenshot.deleteTmpFile);
}
// Let's continue
screenshotPromise
// Save results
.then(function() {
// 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
return resultsDatastore.saveResult(data);
})
// Mark as the run as complete and send the response if the request is still waiting
.then(function() {
debug('Result saved in datastore');
runsDatastore.markAsComplete(run.runId);
if (run.params.waitForResponse) {
// If the user only wants a portion of the result (partialResult option)
switch(run.params.partialResult) {
case 'generalScores':
res.redirect(302, '/api/results/' + run.runId + '/generalScores');
break;
case 'rules':
res.redirect(302, '/api/results/' + run.runId + '/rules');
break;
case 'javascriptExecutionTree':
res.redirect(302, '/api/results/' + run.runId + '/javascriptExecutionTree');
break;
case 'phantomas':
res.redirect(302, '/api/results/' + run.runId + '/toolsResults/phantomas');
break;
default:
res.redirect(302, '/api/results/' + run.runId);
}
}
})
.fail(function(err) {
console.error('Test failed for URL: %s', run.params.url);
console.error(err.toString());
runsDatastore.markAsFailed(run.runId, err.toString());
res.status(500).send('An error occured');
});
})
.fail(function(err) {
console.error('Test failed for URL: %s', run.params.url);
console.error(err.toString());
runsDatastore.markAsFailed(run.runId, err.toString());
res.status(400).send('Bad request');
})
.finally(function() {
queue.remove(run.runId);
});
// 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.');
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify({runId: run.runId}));
}
});
// Retrive one run by id
app.get('/api/runs/:id', function(req, res) {
var runId = req.params.id;
var run = runsDatastore.get(runId);
if (run) {
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify(run, null, 2));
} else {
res.status(404).send('Not found');
}
});
// Retrieve the list of all runs
/*app.get('/api/runs', function(req, res) {
// NOT YET
});*/
// Delete one run by id
/*app.delete('/api/runs/:id', function(req, res) {
deleteRun()
});*/
// Delete all
/*app.delete('/api/runs', function(req, res) {
purgeRuns()
});
// List all
app.get('/api/runs', function(req, res) {
listRuns()
});
// Exists
app.head('/api/runs/:id', function(req, res) {
existsX();
// Returns 200 if the result exists or 404 if not
});
*/
// Retrive one result by id
app.get('/api/results/:id', function(req, res) {
getPartialResults(req.params.id, res, function(data) {
return data;
});
});
// Retrieve one result and return only the generalScores part of the response
app.get('/api/results/:id/generalScores', function(req, res) {
getPartialResults(req.params.id, res, function(data) {
return data.scoreProfiles.generic;
});
});
app.get('/api/results/:id/generalScores/:scoreProfile', function(req, res) {
getPartialResults(req.params.id, res, function(data) {
return data.scoreProfiles[req.params.scoreProfile];
});
});
app.get('/api/results/:id/rules', function(req, res) {
getPartialResults(req.params.id, res, function(data) {
return data.rules;
});
});
app.get('/api/results/:id/javascriptExecutionTree', function(req, res) {
getPartialResults(req.params.id, res, function(data) {
return data.javascriptExecutionTree;
});
});
app.get('/api/results/:id/toolsResults/phantomas', function(req, res) {
getPartialResults(req.params.id, res, function(data) {
return data.toolsResults.phantomas;
});
});
function getPartialResults(runId, res, partialGetterFn) {
resultsDatastore.getResult(runId)
.then(function(data) {
var results = partialGetterFn(data);
if (typeof results === 'undefined') {
res.status(404).send('Not found');
return;
}
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify(results, null, 2));
}).fail(function() {
res.status(404).send('Not found');
});
}
// Retrive one result by id
app.get('/api/results/:id/screenshot.jpg', function(req, res) {
var runId = req.params.id;
resultsDatastore.getScreenshot(runId)
.then(function(screenshotBuffer) {
res.setHeader('Content-Type', 'image/jpeg');
res.send(screenshotBuffer);
}).fail(function() {
res.status(404).send('Not found');
});
});
};
module.exports = ApiController;
-26
View File
@@ -1,26 +0,0 @@
var path = require('path');
var express = require('express');
var FrontController = function(app) {
'use strict';
var cacheDuration = 365 * 24 * 60 * 60 * 1000; // One year
var assetsPath = (app.get('env') === 'development') ? '../../../front/src' : '../../../front/build';
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'));
});
});
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 }));
};
module.exports = FrontController;
-120
View File
@@ -1,120 +0,0 @@
var fs = require('fs');
var rimraf = require('rimraf');
var path = require('path');
var Q = require('q');
var debug = require('debug')('ylt:resultsDatastore');
function ResultsDatastore() {
'use strict';
var resultFileName = 'results.json';
var resultScreenshotName = 'screenshot.jpg';
var resultsFolderName = 'results';
var resultsDir = path.join(__dirname, '..', '..', '..', resultsFolderName);
this.saveResult = function(testResults) {
return createResultFolder(testResults.runId)
.then(function() {
return saveScreenshotIfExists(testResults);
})
.then(function() {
debug('Saving results to disk...');
var resultFilePath = path.join(resultsDir, testResults.runId, resultFileName);
debug('Destination file is %s', resultFilePath);
return Q.nfcall(fs.writeFile, resultFilePath, JSON.stringify(testResults, null, 2));
});
};
this.getResult = function(runId) {
var resultFilePath = path.join(resultsDir, runId, resultFileName);
debug('Reading results (runID = %s) from disk...', runId);
return Q.nfcall(fs.readFile, resultFilePath, {encoding: 'utf8'}).then(function(data) {
return JSON.parse(data);
});
};
this.deleteResult = function(runId) {
var folder = path.join(resultsDir, runId);
debug('Deleting results (runID = %s) from disk...', runId);
return Q.nfcall(rimraf, folder);
};
// The folder /results/folderName/
function createResultFolder(folderName) {
var folder = path.join(resultsDir, folderName);
debug('Creating the folder %s', folderName);
return createGlobalFolder().then(function() {
return Q.nfcall(fs.mkdir, folder);
});
}
// The folder /results/
function createGlobalFolder() {
var deferred = Q.defer();
// Create the results folder if it doesn't exist
fs.exists(resultsDir, function(exists) {
if (exists) {
deferred.resolve();
} else {
debug('Creating the global results folder', resultsDir);
fs.mkdir(resultsDir, function(err) {
if (err) {
deferred.reject(err);
} else {
deferred.resolve();
}
});
}
});
return deferred.promise;
}
// If there is a screenshot, save it as screenshot.jpg in the same folder as the results
function saveScreenshotIfExists(testResults) {
var deferred = Q.defer();
if (testResults.screenshotBuffer) {
var screenshotFilePath = path.join(resultsDir, testResults.runId, resultScreenshotName);
fs.writeFile(screenshotFilePath, testResults.screenshotBuffer);
delete testResults.screenshotBuffer;
} else {
deferred.resolve();
}
return deferred;
}
this.getScreenshot = function(runId) {
var screenshotFilePath = path.join(resultsDir, runId, resultScreenshotName);
debug('Getting screenshot (runID = %s) from disk...', runId);
return Q.nfcall(fs.readFile, screenshotFilePath);
};
}
module.exports = ResultsDatastore;
-106
View File
@@ -1,106 +0,0 @@
function RunsDatastore() {
'use strict';
// NOT PERSISTING RUNS
// For the moment, maybe one day
var runs = {};
var STATUS_AWAITING = 'awaiting';
var STATUS_RUNNING = 'running';
var STATUS_COMPLETE = 'complete';
var STATUS_FAILED = 'failed';
this.add = function(run, position) {
runs[run.runId] = run;
this.updatePosition(run.runId, position);
};
this.get = function(runId) {
return runs[runId];
};
this.updatePosition = function(runId, position) {
var run = runs[runId];
if (position > 0) {
run.status = {
statusCode: STATUS_AWAITING,
position: position
};
} else {
run.status = {
statusCode: STATUS_RUNNING
};
}
runs[runId] = run;
};
this.markAsComplete = function(runId) {
var run = runs[runId];
run.status = {
statusCode: STATUS_COMPLETE
};
runs[runId] = run;
};
this.markAsFailed = function(runId, err) {
var run = runs[runId];
var errorMessage;
switch(err) {
case '252':
errorMessage = "Error 252: page timeout in Phantomas";
break;
case '253':
errorMessage = "Error 253: Phantomas config error";
break;
case '254':
errorMessage = "Error 254: page loading failed in PhantomJS";
break;
case '255':
errorMessage = "Error 255: Phantomas error";
break;
case '1001':
errorMessage = "Error 1001: JavaScript profiling failed";
break;
case '1002':
errorMessage = "Error 1002: missing Phantomas metrics";
break;
default:
errorMessage = err;
}
run.status = {
statusCode: STATUS_FAILED,
error: errorMessage
};
runs[runId] = run;
};
this.delete = function(runId) {
delete runs[runId];
};
this.list = function() {
var runsArray = [];
Object.keys(runs).forEach(function(key) {
runsArray.push(runs[key]);
});
return runsArray;
};
}
module.exports = RunsDatastore;
-83
View File
@@ -1,83 +0,0 @@
var Q = require('q');
var debug = require('debug')('ylt:runsQueue');
function RunsQueue() {
'use strict';
var queue = [];
this.push = function(runId) {
var deferred = Q.defer();
var startingPosition = queue.length;
debug('Adding run %s to the queue, position is %d', runId, startingPosition);
if (startingPosition === 0) {
// The queue is empty, let's run immediatly
queue.push({
runId: runId
});
deferred.resolve();
} else {
queue.push({
runId: runId,
positionChangedCallback: function(position) {
deferred.notify(position);
},
itIsTimeCallback: function() {
deferred.resolve();
}
});
}
var promise = deferred.promise;
promise.startingPosition = startingPosition;
return promise;
};
this.getPosition = function(runId) {
// Position 0 means it's a work in progress (a run is removed AFTER it is finished, not before)
var position = -1;
queue.some(function(run, index) {
if (run.runId === runId) {
position = index;
return true;
}
return false;
});
return position;
};
this.remove = function(runId) {
var position = this.getPosition(runId);
if (position >= 0) {
queue.splice(position, 1);
}
// Update other runs' positions
queue.forEach(function(run, index) {
if (index === 0 && run.itIsTimeCallback) {
run.itIsTimeCallback();
} else if (index > 0 && run.positionChangedCallback) {
run.positionChangedCallback(index);
}
});
};
this.length = function() {
return queue.length;
};
}
module.exports = RunsQueue;
@@ -1,85 +0,0 @@
var config = require('../../../server_config/settings.json');
var debug = require('debug')('apiLimitsMiddleware');
var apiLimitsMiddleware = function(req, res, next) {
'use strict';
debug('Entering API Limits Middleware with IP address %s', req.connection.remoteAddress);
if (req.path.indexOf('/api/') === 0 && !res.locals.hasApiKey) {
if (req.path === '/api/runs') {
if (!runsTable.accepts(req.connection.remoteAddress)) {
// Sorry :/
debug('Too many tests launched from IP address %s', req.connection.remoteAddress);
res.status(429).send('Too many requests');
return;
}
}
if (!callsTable.accepts(req.connection.remoteAddress)) {
// Sorry :/
debug('Too many API requests from IP address %s', req.connection.remoteAddress);
res.status(429).send('Too many requests');
return;
}
debug('Not blocked by the API limits');
// It's ok for the moment
}
next();
};
var RecordTable = function(maxPerDay) {
var table = {};
// Check if the user overpassed the limit and save its visit
this.accepts = function(ipAddress) {
if (table[ipAddress]) {
this.cleanEntry(ipAddress);
debug('%d visits in the last 24 hours', table[ipAddress].length);
if (table[ipAddress].length >= maxPerDay) {
return false;
} else {
table[ipAddress].push(Date.now());
}
} else {
table[ipAddress] = [];
table[ipAddress].push(Date.now());
}
return true;
};
// Clean the table for this guy
this.cleanEntry = function(ipAddress) {
table[ipAddress] = table[ipAddress].filter(function(date) {
return date > Date.now() - 1000*60*60*24;
});
};
// Clean the entire table once in a while
this.removeOld = function() {
for (var ipAddress in table) {
this.cleanEntry(ipAddress);
}
};
};
// Init the records tables
var runsTable = new RecordTable(config.maxAnonymousRunsPerDay);
var callsTable = new RecordTable(config.maxAnonymousCallsPerDay);
module.exports = apiLimitsMiddleware;
-42
View File
@@ -1,42 +0,0 @@
var config = require('../../../server_config/settings.json');
var debug = require('debug')('authMiddleware');
var authMiddleware = function(req, res, next) {
'use strict';
if (req.path.indexOf('/api/') === 0) {
if (req.headers && req.headers['x-api-key']) {
// Test if it's an authorized key
if (isApiKeyValid(req.headers['x-api-key'])) {
// Come in!
debug('Authorized key: %s', req.headers['x-api-key']);
res.locals.hasApiKey = true;
} else {
// Sorry :/
debug('Unauthorized key %s', req.headers['x-api-key']);
res.status(401).send('Unauthorized');
return;
}
} else {
debug('No authorization key');
// It's ok for the moment but you might be blocked by the apiLimitsMiddleware, dude
}
}
next();
};
function isApiKeyValid(apiKey) {
return (config.authorizedKeys[apiKey]) ? true : false;
}
module.exports = authMiddleware;
+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.7;
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.value.message);
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();
+53
View File
@@ -0,0 +1,53 @@
var debug = require('debug')('ylt:domAccessAgregator');
var domAccessAgregator = function() {
'use strict';
this.agregate = function(data) {
debug('Starting to agregate DOM Accesses...');
let count = 0;
let offenders = {
byType: {}
};
const metricsToGather = [
'DOMqueriesById',
'DOMqueriesByTagName',
'DOMqueriesByClassName',
'DOMqueriesByQuerySelectorAll',
'DOMinserts',
'DOMmutationsInserts',
'DOMmutationsRemoves',
'DOMmutationsAttributes',
'eventsBound'
];
metricsToGather.forEach(key => {
if (data.toolsResults.phantomas.metrics[key]) {
count += data.toolsResults.phantomas.metrics[key];
}
offenders.byType[key] = [];
if (data.toolsResults.phantomas.offenders[key]) {
offenders.byType[key] = data.toolsResults.phantomas.offenders[key];
}
});
data.toolsResults.domAccessAgregator = {
metrics: {
DOMaccesses: count
},
offenders: {
DOMaccesses: offenders
}
};
debug('Done agregating DOM Accesses.');
return data;
};
};
module.exports = new domAccessAgregator();
-102
View File
@@ -1,102 +0,0 @@
var debug = require('debug')('ylt:jsExecutionTransformer');
var offendersHelpers = require('../offendersHelpers');
var jsExecutionTransformer = function() {
this.transform = function(data) {
var javascriptExecutionTree = {};
var metrics = {
domManipulations: 0,
queriesWithoutResults: 0,
jQueryCalls: 0,
jQueryCallsOnEmptyObject: 0
};
debug('Starting JS execution transformation');
try {
javascriptExecutionTree = JSON.parse(data.toolsResults.phantomas.offenders.javascriptExecutionTree[0]);
if (javascriptExecutionTree.children) {
javascriptExecutionTree.children.forEach(function(node) {
// Mark abnormal things with a warning flag
var contextLenght = (node.data.callDetails && node.data.callDetails.context) ? node.data.callDetails.context.length : null;
if ((node.data.type === 'jQuery - bind' && contextLenght > 5) ||
node.data.resultsNumber === 0 ||
contextLenght === 0) {
node.warning = true;
}
// Mark errors with an error flag
if (node.data.type === 'error' || node.data.type === 'jQuery version change') {
node.error = true;
}
// Mark a performance flag
if (['domInteractive', 'domContentLoaded', 'domContentLoadedEnd', 'domComplete'].indexOf(node.data.type) >= 0) {
node.windowPerformance = true;
}
// 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;
}
// 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]);
}
});
});
}
debug('JS execution transformation complete');
} catch(err) {
throw err;
}
data.javascriptExecutionTree = javascriptExecutionTree;
data.toolsResults.jsExecutionTransformer = {
metrics: metrics
};
return data;
};
function treeRecursiveParser(node, fn) {
if (node.children) {
node.children.forEach(function(child) {
treeRecursiveParser(child, fn);
});
}
fn(node);
}
};
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();
@@ -1,205 +0,0 @@
/**
* Overwritting the original spying functions in scope.js
* This is done so we now have a before AND an after callback on the spy
*
* @see http://code.jquery.com/jquery-1.10.2.js
* @see http://code.jquery.com/jquery-2.0.3.js
*/
/* global document: true, window: true */
exports.version = '0.2';
exports.module = function(phantomas) {
'use strict';
var responseEndTime = Date.now();
phantomas.on('responseEnd', function() {
responseEndTime = Date.now();
});
phantomas.once('init', function() {
phantomas.evaluate(function(responseEndTime, deepAnalysis) {
(function(phantomas) {
// Overwritting phantomas spy function
(function() {
var enabled = true;
// turn off spying to not include internal phantomas actions
function spyEnabled(state, reason) {
enabled = (state === true);
phantomas.log('Spying ' + (enabled ? 'enabled' : 'disabled') + (reason ? ' - ' + reason : ''));
}
phantomas.log('Overwritting phantomas spy function');
function spy(obj, fn, callbackBefore, callbackAfter) {
var origFn = obj && obj[fn];
if (typeof origFn !== 'function') {
return false;
}
phantomas.log('Attaching a YLT spy to "' + fn + '" function...');
obj[fn] = function() {
var result;
var err;
// Before
if (enabled) {
callbackBefore.apply(this, arguments);
}
// Execute
try {
result = origFn.apply(this, arguments);
} catch(e) {
// Catching the err for the moment, because we need to make sure the callbackAfter function is called.
phantomas.log('Error catched on spyed function "' + fn + '": ' + e);
phantomas.log(arguments);
err = e;
} finally {
// After
if (enabled && callbackAfter) {
callbackAfter.call(this, result, arguments);
}
if (err) {
phantomas.log('Re-throwing the error');
throw err;
}
}
return result;
};
// copy custom properties of original function to the mocked one
Object.keys(origFn).forEach(function(key) {
obj[fn][key] = origFn[key];
});
obj[fn].prototype = origFn.prototype;
return true;
}
phantomas.spyEnabled = spyEnabled;
phantomas.spy = spy;
})();
// Adding some code for the Javascript execution tree construction
(function() {
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
function pushContext(data) {
if (depth === 0 || deepAnalysis) {
data.timestamp = Date.now() - responseEndTime;
data.loadingStep = phantomas.currentStep || '';
currentContext.addChild(data);
}
}
// Add a child to the current context and enter his context
function enterContext(data) {
if (depth === 0 || deepAnalysis) {
data.timestamp = Date.now() - responseEndTime;
data.loadingStep = phantomas.currentStep || '';
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) {
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];
}
}
var parent = currentContext.parent;
if (parent === null) {
console.error('Error: trying to close root context in ContextTree');
} else {
currentContext = parent;
}
}
depth --;
}
function getContextData() {
return currentContext.data;
}
// Returns a clean object, without the parent which causes recursive loops
function readFullTree() {
// Return null if the contextTree is not correctly closed
if (root !== currentContext) {
return null;
}
var current = currentContext;
function recusiveRead(node) {
if (node.children.length === 0) {
delete node.children;
} else {
for (var i=0, max=node.children.length ; i<max ; i++) {
recusiveRead(node.children[i]);
}
}
delete node.parent;
}
recusiveRead(root);
return root;
}
function ContextTreeNode(parent, data) {
this.data = data;
this.parent = parent;
this.children = [];
this.addChild = function(data) {
var child = new ContextTreeNode(this, data);
this.children.push(child);
return child;
};
}
phantomas.log('Adding some contextTree functions to phantomas');
phantomas.pushContext = pushContext;
phantomas.enterContext = enterContext;
phantomas.leaveContext = leaveContext;
phantomas.getContextData = getContextData;
phantomas.readFullTree = readFullTree;
})();
})(window.__phantomas);
}, responseEndTime, phantomas.getParam('js-deep-analysis'));
});
};
@@ -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.");
});
};
@@ -1,382 +0,0 @@
/**
* Analyzes DOM queries done via native DOM methods
*/
/* global Element: true, Document: true, Node: true, window: true */
exports.version = '0.10.a';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('DOMqueries'); // @desc number of all DOM queries @offenders
phantomas.setMetric('DOMqueriesWithoutResults'); // @desc number of DOM queries that retutned nothing @offenders
phantomas.setMetric('DOMqueriesById'); // @desc number of document.getElementById calls
phantomas.setMetric('DOMqueriesByClassName'); // @desc number of document.getElementsByClassName calls
phantomas.setMetric('DOMqueriesByTagName'); // @desc number of document.getElementsByTagName calls
phantomas.setMetric('DOMqueriesByQuerySelectorAll'); // @desc number of document.querySelector(All) calls
phantomas.setMetric('DOMinserts'); // @desc number of DOM nodes inserts
phantomas.setMetric('DOMqueriesDuplicated'); // @desc number of DOM queries called more than once
phantomas.setMetric('DOMqueriesAvoidable'); // @desc number of repeated uses of a duplicated query
// fake native DOM functions
phantomas.once('init', function() {
phantomas.evaluate(function() {
(function(phantomas) {
function querySpy(type, query, fnName, context, hasNoResults) {
phantomas.emit('domQuery', type, query, fnName, context, hasNoResults); // @desc DOM query has been made
}
phantomas.spy(Document.prototype, 'getElementById', function(id) {
phantomas.incrMetric('DOMqueriesById');
phantomas.addOffender('DOMqueriesById', '#%s (in %s)', id, '#document');
phantomas.enterContext({
type: 'getElementById',
callDetails: {
arguments: ['#' + id]
},
backtrace: phantomas.getBacktrace()
});
}, function(result, args) {
var id = args[0];
querySpy('id', '#' + id, 'getElementById', '#document', (result === null));
var moreData = {
resultsNumber : (result === null) ? 0 : 1
};
phantomas.leaveContext(moreData);
});
// selectors by class name
function selectorClassNameSpyBefore(className) {
/*jshint validthis: true */
var context = phantomas.getDOMPath(this);
phantomas.incrMetric('DOMqueriesByClassName');
phantomas.addOffender('DOMqueriesByClassName', '.%s (in %s)', className, context);
phantomas.enterContext({
type: 'getElementsByClassName',
callDetails: {
context: {
length: 1,
elements: [context]
},
arguments: ['.' + className]
},
backtrace: phantomas.getBacktrace()
});
}
function selectorClassNameAfter(result, args) {
/*jshint validthis: true */
var className = args[0];
var context = phantomas.getDOMPath(this);
querySpy('class', '.' + className, 'getElementsByClassName', context, (result.length === 0));
var moreData = {
resultsNumber : (result && result.length > 0) ? result.length : 0
};
phantomas.leaveContext(moreData);
}
phantomas.spy(Document.prototype, 'getElementsByClassName', selectorClassNameSpyBefore, selectorClassNameAfter);
phantomas.spy(Element.prototype, 'getElementsByClassName', selectorClassNameSpyBefore, selectorClassNameAfter);
// selectors by tag name
function selectorTagNameSpyBefore(tagName) {
/*jshint validthis: true */
var context = phantomas.getDOMPath(this);
phantomas.incrMetric('DOMqueriesByTagName');
phantomas.addOffender('DOMqueriesByTagName', '%s (in %s)', tagName, context);
phantomas.enterContext({
type: 'getElementsByTagName',
callDetails: {
context: {
length: 1,
elements: [context]
},
arguments: [tagName]
},
backtrace: phantomas.getBacktrace()
});
}
function selectorTagNameSpyAfter(result, args) {
/*jshint validthis: true */
var tagName = args[0];
var context = phantomas.getDOMPath(this);
querySpy('tag name', tagName.toLowerCase(), 'getElementsByTagName', context, (result.length === 0));
var moreData = {
resultsNumber : (result && result.length > 0) ? result.length : 0
};
phantomas.leaveContext(moreData);
}
phantomas.spy(Document.prototype, 'getElementsByTagName', selectorTagNameSpyBefore, selectorTagNameSpyAfter);
phantomas.spy(Element.prototype, 'getElementsByTagName', selectorTagNameSpyBefore, selectorTagNameSpyAfter);
// selector queries
function selectorQuerySpy(selector, context) {
phantomas.incrMetric('DOMqueriesByQuerySelectorAll');
phantomas.addOffender('DOMqueriesByQuerySelectorAll', '%s (in %s)', selector, context);
}
function selectorQuerySpyBefore(selector) {
/*jshint validthis: true */
var context = phantomas.getDOMPath(this);
selectorQuerySpy(selector, context);
phantomas.enterContext({
type: 'querySelector',
callDetails: {
context: {
length: 1,
elements: [context]
},
arguments: [selector]
},
backtrace: phantomas.getBacktrace()
});
}
function selectorQuerySpyAfter(result, args) {
/*jshint validthis: true */
var selector = args[0];
var context = phantomas.getDOMPath(this);
querySpy('selector', selector, 'querySelectorAll', context, (!result || result.length === 0));
var moreData = {
resultsNumber : result ? 1 : 0
};
phantomas.leaveContext(moreData);
}
function selectorAllQuerySpyBefore(selector) {
/*jshint validthis: true */
var context = phantomas.getDOMPath(this);
selectorQuerySpy(selector, context);
phantomas.enterContext({
type: 'querySelectorAll',
callDetails: {
context: {
length: 1,
elements: [context]
},
arguments: [selector]
},
backtrace: phantomas.getBacktrace()
});
}
function selectorAllQuerySpryAfter(result, args) {
/*jshint validthis: true */
var selector = args[0];
var context = phantomas.getDOMPath(this);
querySpy('selector', selector, 'querySelectorAll', context, (!result || result.length === 0));
var moreData = {
resultsNumber : (result && result.length > 0) ? result.length : 0
};
phantomas.leaveContext(moreData);
}
phantomas.spy(Document.prototype, 'querySelector', selectorQuerySpyBefore, selectorQuerySpyAfter);
phantomas.spy(Document.prototype, 'querySelectorAll', selectorAllQuerySpyBefore, selectorAllQuerySpryAfter);
phantomas.spy(Element.prototype, 'querySelector', selectorQuerySpyBefore, selectorQuerySpyAfter);
phantomas.spy(Element.prototype, 'querySelectorAll', selectorAllQuerySpyBefore, selectorAllQuerySpryAfter);
// count DOM inserts
function appendChild(child, element, context, appended) {
/*jshint validthis: true */
// ignore appending to the node that's not yet added to DOM tree
if (!element.parentNode) {
return;
}
// don't count elements added to fragments as a DOM inserts (issue #350)
// DocumentFragment > div[0]
if (context.indexOf('DocumentFragment') === 0) {
return;
}
phantomas.incrMetric('DOMinserts');
phantomas.addOffender('DOMinserts', '"%s" appended to "%s"', appended, context);
//phantomas.log('DOM insert: node "%s" appended to "%s"', appended, context);
}
function appendChildSpyBefore(child) {
/*jshint validthis: true */
var context = phantomas.getDOMPath(this);
var appended = phantomas.getDOMPath(child);
appendChild(child, this, context, appended);
phantomas.enterContext({
type: 'appendChild',
callDetails: {
context: {
length: 1,
elements: [context]
},
arguments: [appended]
},
backtrace: phantomas.getBacktrace()
});
}
function insertBeforeSpyBefore(child, refElement) {
/*jshint validthis: true */
var context = phantomas.getDOMPath(this);
var appended = phantomas.getDOMPath(child);
var referent = phantomas.getDOMPath(refElement);
appendChild(child, this, context, appended);
phantomas.enterContext({
type: 'insertBefore',
callDetails: {
context: {
length: 1,
elements: [context]
},
arguments: [
appended,
referent
]
},
backtrace: phantomas.getBacktrace()
});
}
phantomas.spy(Node.prototype, 'appendChild', appendChildSpyBefore, function(result) {
phantomas.leaveContext();
});
phantomas.spy(Node.prototype, 'insertBefore', insertBeforeSpyBefore, function(result) {
phantomas.leaveContext();
});
phantomas.spy(Document.prototype, 'createElement', function(tagName) {
phantomas.enterContext({
type: 'createElement',
callDetails: {
arguments: [tagName]
},
backtrace: phantomas.getBacktrace()
});
}, function(result, args) {
phantomas.leaveContext();
});
phantomas.spy(Document.prototype, 'createTextNode', function(text) {
phantomas.enterContext({
type: 'createTextNode',
callDetails: {
arguments: [text]
},
backtrace: phantomas.getBacktrace()
});
}, function(result, args) {
phantomas.leaveContext();
});
phantomas.spy(Document.prototype, 'createDocumentFragment', function() {
phantomas.enterContext({
type: 'createDocumentFragment',
callDetails: {
arguments: []
},
backtrace: phantomas.getBacktrace()
});
}, function(result, args) {
phantomas.leaveContext();
});
phantomas.spy(window, 'getComputedStyle', function(element, pseudoElement) {
var target = phantomas.getDOMPath(element);
phantomas.enterContext({
type: 'getComputedStyle',
callDetails: {
arguments: [target, pseudoElement]
},
backtrace: phantomas.getBacktrace()
});
}, function(result, args) {
phantomas.leaveContext();
});
})(window.__phantomas);
});
});
// report DOM queries that return no results (issue #420)
phantomas.on('domQuery', function(type, query, fnName, context, hasNoResults) {
// ignore DOM queries within DOM fragments (used internally by jQuery)
if (context.indexOf('body') !== 0 && context.indexOf('#document') !== 0) {
return;
}
if (hasNoResults === true) {
phantomas.incrMetric('DOMqueriesWithoutResults');
phantomas.addOffender('DOMqueriesWithoutResults', '%s (in %s) using %s', query, context, fnName);
}
});
// count DOM queries by either ID, tag name, class name and selector query
// @see https://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-document-doctype
var Collection = require('../../util/collection'),
DOMqueries = new Collection();
phantomas.on('domQuery', function(type, query, fnName, context) {
phantomas.log('DOM query: by %s - "%s" (using %s) in %s', type, query, fnName, context);
phantomas.incrMetric('DOMqueries');
if (context && context.indexOf('DocumentFragment') === -1) {
DOMqueries.push(type + ' "' + query + '" with ' + fnName + ' (in context ' + context + ')');
}
});
phantomas.on('report', function() {
DOMqueries.sort().forEach(function(query, cnt) {
if (cnt > 1) {
phantomas.incrMetric('DOMqueriesDuplicated');
phantomas.incrMetric('DOMqueriesAvoidable', cnt - 1);
phantomas.addOffender('DOMqueriesDuplicated', '%s: %d queries', query, cnt);
}
});
});
};
@@ -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);
});
});
};
@@ -1,386 +0,0 @@
/**
* Analyzes jQuery activity
*
* @see http://code.jquery.com/jquery-1.10.2.js
* @see http://code.jquery.com/jquery-2.0.3.js
*/
/* global document: true, window: true */
/* jshint -W030 */
exports.version = '1.0.a';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('jQueryVersion', ''); // @desc version of jQuery framework (if loaded) [string]
phantomas.setMetric('jQueryVersionsLoaded'); // @desc number of loaded jQuery "instances" (even in the same version)
phantomas.setMetric('jQueryOnDOMReadyFunctions'); // @desc number of functions bound to onDOMReady event
phantomas.setMetric('jQueryWindowOnLoadFunctions'); // @desc number of functions bound to windowOnLoad event
phantomas.setMetric('jQuerySizzleCalls'); // @desc number of calls to Sizzle (including those that will be resolved using querySelectorAll)
phantomas.setMetric('jQueryEventTriggers'); // @desc number of jQuery event triggers
var jQueryFunctions = [
// DOM manipulations
'html',
'append',
'appendTo',
'prepend',
'prependTo',
'before',
'insertBefore',
'after',
'insertAfter',
'remove',
'detach',
'empty',
'clone',
'replaceWith',
'replaceAll',
'text',
'wrap',
'wrapAll',
'wrapInner',
'unwrap',
// Style manipulations
'css',
'offset',
'position',
'height',
'innerHeight',
'outerHeight',
'width',
'innerWidth',
'outerWidth',
'scrollLeft',
'scrollTop',
// Animations
'hide',
'show',
'toggle',
'animate',
'fadeIn',
'fadeOut',
'fadeTo',
'fadeToggle',
'slideDown',
'slideUp',
'slideToggle',
// Generic events
'on',
'off',
'live',
'die',
'delegate',
'undelegate',
'one',
'bind',
'unbind',
// More events
'blur',
'change',
'click',
'dblclick',
'error',
'focus',
'focusin',
'focusout',
'hover',
'keydown',
'keypress',
'keyup',
'load',
'mousedown',
'mouseenter',
'mouseleave',
'mousemove',
'mouseout',
'mouseover',
'mouseup',
'resize',
'scroll',
'select',
'submit',
'unload',
// Attributes
'attr',
'prop',
'removeAttr',
'removeProp',
'val',
'hasClass',
'addClass',
'removeClass',
'toggleClass',
];
var jQueryTraversalFunctions = [
'children',
'closest',
'find',
'next',
'nextAll',
'nextUntil',
'offsetParent',
'parent',
'parents',
'parentsUntil',
'prev',
'prevAll',
'prevUntil',
'siblings'
];
jQueryFunctions = jQueryFunctions.concat(jQueryTraversalFunctions);
// spy calls to jQuery functions
phantomas.once('init', function() {
phantomas.evaluate(function(jQueryFunctions, jQueryTraversalFunctions) {
(function(phantomas) {
var jQuery;
var oldJQuery;
phantomas.spyGlobalVar('jQuery', function(jQuery) {
var version;
if (!jQuery || !jQuery.fn) {
phantomas.log('jQuery: unable to detect version!');
return;
}
// Tag the current version of jQuery to avoid multiple reports of jQuery being loaded
// when it's actually only restored via $.noConflict(true) - see comments in #435
if (jQuery.__phantomas === true) {
phantomas.log('jQuery: this instance has already been seen by phantomas');
return;
}
jQuery.__phantomas = true;
// report the version of jQuery
version = jQuery.fn.jquery;
phantomas.emit('jQueryLoaded', version);
phantomas.pushContext({
type: (oldJQuery) ? 'jQuery version change' : 'jQuery loaded',
callDetails: {
arguments: ['version ' + version]
},
backtrace: phantomas.getBacktrace()
});
oldJQuery = version;
// jQuery.ready.promise
// works for jQuery 1.8.0+ (released Aug 09 2012)
phantomas.spy(jQuery.ready, 'promise', function(func) {
phantomas.incrMetric('jQueryOnDOMReadyFunctions');
phantomas.addOffender('jQueryOnDOMReadyFunctions', phantomas.getCaller(3));
phantomas.pushContext({
type: 'jQuery - onDOMReady',
callDetails: {
arguments: [func]
},
backtrace: phantomas.getBacktrace()
});
}) || phantomas.log('jQuery: can not measure jQueryOnDOMReadyFunctions (jQuery used on the page is too old)!');
// Sizzle calls - jQuery.find
// works for jQuery 1.3+ (released Jan 13 2009)
phantomas.spy(jQuery, 'find', function(selector, context) {
phantomas.incrMetric('jQuerySizzleCalls');
phantomas.addOffender('jQuerySizzleCalls', '%s (in %s)', selector, (phantomas.getDOMPath(context) || 'unknown'));
phantomas.enterContext({
type: 'jQuery - Sizzle call',
callDetails: {
context: {
length: 1,
elements: [phantomas.getDOMPath(context)]
},
arguments: [selector]
},
backtrace: phantomas.getBacktrace()
});
}, function(result) {
var moreData = {
resultsNumber : (result && result.length) ? result.length : 0
};
phantomas.leaveContext(moreData);
}) || phantomas.log('jQuery: can not measure jQuerySizzleCalls (jQuery used on the page is too old)!');
phantomas.spy(jQuery.fn, 'init', function(selector, context) {
if (typeof selector === 'string' && /^#([\w\-]*)$/.exec(selector) !== null && !context) {
phantomas.enterContext({
type: 'jQuery - find',
callDetails: {
arguments: [selector]
},
backtrace: phantomas.getBacktrace()
});
}
}, function(result) {
var data = phantomas.getContextData();
if (data.type === 'jQuery - find' &&
!data.callDetails.context &&
data.callDetails.arguments.length === 1 &&
/^#([\w\-]*)$/.exec(data.callDetails.arguments[0]) !== null) {
var moreData = {
resultsNumber : (result && result.length) ? result.length : 0
};
phantomas.leaveContext(moreData);
}
});
if (!jQuery.event) {
phantomas.spy(jQuery.event, 'trigger', function(ev, data, elem) {
var path = phantomas.getDOMPath(elem),
type = ev.type || ev;
phantomas.log('Event: triggered "%s" on "%s"', type, path);
phantomas.incrMetric('jQueryEventTriggers');
phantomas.addOffender('jQueryEventTriggers', '"%s" on "%s"', type, path);
});
}
// jQuery events bound to window' onLoad event (#451)
phantomas.spy(jQuery.fn, 'on', function(eventName, func) {
if ((eventName === 'load') && (this[0] === window)) {
phantomas.incrMetric('jQueryWindowOnLoadFunctions');
phantomas.addOffender('jQueryWindowOnLoadFunctions', phantomas.getCaller(2));
}
});
// Add spys on many jQuery functions
jQueryFunctions.forEach(function(functionName) {
phantomas.spy(jQuery.fn, functionName, function(args) {
// Clean args
args = [].slice.call(arguments);
args.forEach(function(arg, index) {
if (arg instanceof Array) {
arg = '[Array]';
}
if (arg instanceof Object) {
if (arg instanceof jQuery || (arg.jquery && arg.jquery.length > 0)) {
arg = phantomas.getDOMPath(arg[0]) || 'unknown';
} else if (arg instanceof HTMLElement) {
arg = phantomas.getDOMPath(arg) || 'unknown';
} else if (typeof arg === 'function') {
arg = '(function)';
} else {
try {
arg = JSON.stringify(arg);
} catch(e) {
arg = '[Object]';
}
}
}
if ((typeof arg === 'string' || arg instanceof String) && arg.length > 200) {
arg = arg.substring(0, 200) + '...';
}
if (typeof arg === 'function') {
arg = '(function)';
}
if (arg === true) {
arg = 'true';
}
if (arg === false) {
arg = 'false';
}
if (arg === null) {
arg = 'null';
}
if (typeof arg !== 'number' && typeof arg !== 'string' && !(arg instanceof String)) {
arg = 'undefined';
}
args[index] = arg;
});
var elements = [];
for (var i = 0 ; i < this.length ; i++) {
elements.push(phantomas.getDOMPath(this[i]));
}
phantomas.enterContext({
type: 'jQuery - ' + functionName,
callDetails: {
context: {
length: this.length,
elements: elements
},
arguments: args
},
backtrace: phantomas.getBacktrace()
});
}, function(result) {
if (jQueryTraversalFunctions.indexOf(functionName) >= 0) {
var moreData = {
resultsNumber : (result && result.length) ? result.length : 0
};
phantomas.leaveContext(moreData);
} else {
phantomas.leaveContext();
}
}) || phantomas.log('jQuery: can not track jQuery - ' + functionName + ' (this version of jQuery doesn\'t support it)');
});
});
})(window.__phantomas);
}, jQueryFunctions, jQueryTraversalFunctions);
});
// store the last resource that was received
// try to report where given jQuery version was loaded from
phantomas.on('recv', function(entry) {
if (entry.isJS) {
lastUrl = entry.url;
}
});
phantomas.on('jQueryLoaded', function(version) {
phantomas.log('jQuery: loaded v' + version);
phantomas.setMetric('jQueryVersion', version);
// report multiple jQuery "instances" (issue #435)
phantomas.incrMetric('jQueryVersionsLoaded');
phantomas.addOffender('jQueryVersionsLoaded', 'v%s', version);
phantomas.log('jQuery: v%s (probably loaded from <%s>)', version, lastUrl);
});
};
@@ -1,69 +0,0 @@
/**
* Reports the use of functions known to be serious performance bottlenecks in JS
*
* @see http://www.nczonline.net/blog/2013/06/25/eval-isnt-evil-just-misunderstood/
* @see http://www.quirksmode.org/blog/archives/2005/06/three_javascrip_1.html
* @see http://www.stevesouders.com/blog/2012/04/10/dont-docwrite-scripts/
*
* Run phantomas with --spy-eval to count eval() calls (see issue #467)
*/
/* global document: true, window: true */
exports.version = '0.2';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('documentWriteCalls'); //@desc number of calls to either document.write or document.writeln @offenders
phantomas.setMetric('evalCalls'); // @desc number of calls to eval (either direct or via setTimeout / setInterval) @offenders
// spy calls to eval only when requested (issue #467)
var spyEval = phantomas.getParam('spy-eval') === true;
if (!spyEval) {
phantomas.log('javaScriptBottlenecks: to spy calls to eval() run phantomas with --spy-eval option');
}
phantomas.once('init', function() {
phantomas.evaluate(function(spyEval) {
(function(phantomas) {
function report(msg, caller, backtrace, metric) {
phantomas.log(msg + ': from ' + caller + '!');
phantomas.log('Backtrace: ' + backtrace);
phantomas.incrMetric(metric);
phantomas.addOffender(metric, "%s from %s", msg, caller);
}
// spy calls to eval()
if (spyEval) {
phantomas.spy(window, 'eval', function(code) {
report('eval() called directly', phantomas.getCaller(), phantomas.getBacktrace(), 'evalCalls');
phantomas.log('eval\'ed code: ' + (code || '').substring(0, 150) + '(...)');
});
}
// spy calls to setTimeout / setInterval with string passed instead of a function
phantomas.spy(window, 'setTimeout', function(fn, interval) {
if (typeof fn !== 'string') return;
report('eval() called via setTimeout("' + fn + '")', phantomas.getCaller(), phantomas.getBacktrace(), 'evalCalls');
});
phantomas.spy(window, 'setInterval', function(fn, interval) {
if (typeof fn !== 'string') return;
report('eval() called via setInterval("' + fn + '")', phantomas.getCaller(), phantomas.getBacktrace(), 'evalCalls');
});
// spy document.write(ln)
phantomas.spy(document, 'write', function(arg) {
report('document.write() used', phantomas.getCaller(), phantomas.getBacktrace(), 'documentWriteCalls');
});
phantomas.spy(document, 'writeln', function(arg) {
report('document.writeln() used', phantomas.getCaller(), phantomas.getBacktrace(), 'documentWriteCalls');
});
})(window.__phantomas);
}, spyEval);
});
};
@@ -1,49 +0,0 @@
/**
* Meters the number of page errors, and provides traces as offenders for "jsErrors" metric
*/
exports.version = '0.3.a';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('jsErrors'); // @desc number of JavaScript errors
function formatTrace(trace) {
var ret = [];
if(Array.isArray(trace)) {
trace.forEach(function(entry) {
ret.push((entry.function ? entry.function + ' ' : '') + (entry.sourceURL || entry.file) + ':' + entry.line);
});
}
return ret;
}
phantomas.on('jserror', function(msg, trace) {
trace = formatTrace(trace);
phantomas.log(msg);
phantomas.log('Backtrace: ' + trace.join(' / '));
phantomas.incrMetric('jsErrors');
phantomas.addOffender('jsErrors', msg + ' - ' + trace.join(' / '));
// Yeah, this is weird, i'm sending the error back to the browser...
phantomas.evaluate(function(msg, caller, trace) {
(function(phantomas) {
phantomas.pushContext({
type: 'error',
callDetails: {
arguments: [msg]
},
caller: caller,
backtrace: trace
});
})(window.__phantomas);
}, msg, trace[0], trace.join(' / '));
});
};
@@ -1,31 +0,0 @@
/**
* Saves the javascript interractions with the DOM
*
* Run phantomas with --js-execution-tree option to use this module
*/
exports.version = '0.1';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('javascriptExecutionTree'); // @desc number of duplicated DOM queries
// save data
phantomas.on('report', function() {
phantomas.log('Reading execution tree JSON');
phantomas.evaluate(function() {(function(phantomas) {
var fullTree = phantomas.readFullTree();
if (fullTree === null) {
phantomas.log('JS execution tree: error, the execution tree is not correctly closed');
return;
}
phantomas.setMetric('javascriptExecutionTree', true, true);
phantomas.addOffender('javascriptExecutionTree', JSON.stringify(fullTree));
})(window.__phantomas);});
});
};
@@ -1,150 +0,0 @@
/**
* Measure when the page reaches certain states
*
* @see http://w3c-test.org/webperf/specs/NavigationTiming/#dom-performancetiming-domloading
* @see https://developers.google.com/web/fundamentals/performance/critical-rendering-path/measure-crp
*/
/* global document: true, window: true */
exports.version = '1.0.a';
exports.module = function(phantomas) {
'use strict';
// times below are calculated relative to performance.timing.responseEnd (#117)
phantomas.setMetric('domInteractive'); // @desc time it took to parse the HTML and construct the DOM
phantomas.setMetric('domContentLoaded'); // @desc time it took to construct both DOM and CSSOM, no stylesheets that are blocking JavaScript execution (i.e. onDOMReady)
phantomas.setMetric('domContentLoadedEnd'); // @desc time it took to finish handling of onDOMReady event @unreliable
phantomas.setMetric('domComplete'); // @desc time it took to load all page resources, the loading spinner has stopped spinning
// backend vs frontend time
phantomas.setMetric('timeBackend'); // @desc time to the first byte compared to the total loading time [%]
phantomas.setMetric('timeFrontend'); // @desc time to window.load compared to the total loading time [%]
// measure dom... metrics from the moment HTML response was fully received
var responseEndTime = Date.now();
phantomas.on('responseEnd', function() {
responseEndTime = Date.now();
phantomas.log('Performance timing: responseEnd = %d', responseEndTime);
});
phantomas.on('init', function() {
phantomas.evaluate(function(responseEndTime) {
(function(phantomas) {
phantomas.spyEnabled(false, 'installing window.performance metrics');
phantomas.currentStep = 'domCreation';
// extend window.performance
// "init" event is sometimes fired twice, pass a value set by "responseEnd" event handler (fixes #192)
if (typeof window.performance === 'undefined') {
window.performance = {
timing: {
responseEnd: responseEndTime
}
};
phantomas.log('Performance timing: emulating window.performance');
}
else {
phantomas.log('Performance timing: using native window.performance');
}
// onDOMReady
document.addEventListener("DOMContentLoaded", function() {
setTimeout(function() {
// use NavigationTiming if possible
var time = window.performance.timing.domContentLoadedEventEnd ?
(window.performance.timing.domContentLoadedEventEnd - window.performance.timing.responseEnd)
:
(Date.now() - responseEndTime);
phantomas.currentStep = 'domContentLoadedEnd';
phantomas.setMetric('domContentLoadedEnd', time, true);
phantomas.log('Performance timing: document reached "DOMContentLoadedEnd" state after %d ms', time);
phantomas.pushContext({
type: 'domContentLoadedEnd'
});
}, 0);
var time = Date.now() - responseEndTime;
phantomas.currentStep = 'domContentLoaded';
phantomas.setMetric('domContentLoaded', time, true);
phantomas.log('Performance timing: document reached "DOMContentLoaded" state after %d ms', time);
phantomas.pushContext({
type: 'domContentLoaded'
});
});
// emulate Navigation Timing
document.addEventListener('readystatechange', function() {
var readyState = document.readyState,
responseEndTime = window.performance.timing.responseEnd,
time = Date.now() - responseEndTime,
metricName;
// @see http://www.w3.org/TR/html5/dom.html#documentreadystate
switch(readyState) {
// the browser has finished parsing all of the HTML and DOM construction is complete
case 'interactive':
metricName = 'domInteractive';
break;
// the processing is complete and all of the resources on the page have finished downloading
case 'complete':
metricName = 'domComplete';
phantomas.log('Performance timing: %j', window.performance.timing);
break;
default:
phantomas.log('Performance timing: unhandled "%s" state!', readyState);
return;
}
phantomas.currentStep = metricName;
phantomas.setMetric(metricName, time, true);
phantomas.log('Performance timing: document reached "%s" state after %d ms', readyState, time);
phantomas.pushContext({
type: metricName
});
});
phantomas.spyEnabled(true);
})(window.__phantomas);
}, responseEndTime);
});
/**
* Emit metrics with backend vs frontend time
*
* Performance Golden Rule:
* "80-90% of the end-user response time is spent on the frontend. Start there."
*
* @see http://www.stevesouders.com/blog/2012/02/10/the-performance-golden-rule/
*/
phantomas.on('report', function() {
// The “backend” time is the time it takes the server to get the first byte back to the client.
// The “frontend” time is measured from the last byte of the response (responseEnd) until all resources are fetched (domComplete)
var backendTime = parseInt(phantomas.getMetric('timeToFirstByte'), 10),
frontendTime = parseInt(phantomas.getMetric('domComplete'), 10),
totalTime = backendTime + frontendTime,
backendTimePercentage;
if (totalTime === 0) {
return;
}
backendTimePercentage = Math.round(backendTime / totalTime * 100);
phantomas.setMetric('timeBackend', backendTimePercentage);
phantomas.setMetric('timeFrontend', 100 - backendTimePercentage);
phantomas.log('Performance timing: backend vs frontend time - %d% / %d%', backendTimePercentage, 100 - backendTimePercentage);
});
};
+56 -100
View File
@@ -1,6 +1,4 @@
var async = require('async');
var Q = require('q');
var ps = require('ps-node');
var path = require('path');
var debug = require('debug')('ylt:phantomaswrapper');
var phantomas = require('phantomas');
@@ -11,129 +9,87 @@ var PhantomasWrapper = function() {
/**
* This is the phantomas launcher. It merges user chosen options into the default options
* Available options :
*
* - timeout : in seconds (default 60)
* - jsDeepAnalysis : should we inspect subrequests in the javascript execution tree?
*
*/
this.execute = function(data) {
var deferred = Q.defer();
var task = data.params;
var viewportOption = null;
// Setting screen dimensions for desktop devices only.
// Phone and tablet dimensions are dealt by Phantomas.
if (task.options.device === 'desktop') {
// Similar to an old non-retina Macbook Air 13"
viewportOption = '1280x800x1';
} else if (task.options.device === 'desktop-hd') {
// Similar to a retina Macbook Pro 16"
viewportOption = '1536x960x2';
}
var options = {
// Cusomizable options
'timeout': task.options.timeout || 60,
'js-deep-analysis': task.options.jsDeepAnalysis || false,
'user-agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.110 Safari/537.36',
'timeout': task.options.timeout || 120,
'user-agent': (task.options.device === 'desktop') ? 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) YLT Chrome/85.0.4183.121 Safari/537.36' : null,
'tablet': (task.options.device === 'tablet'),
'phone': (task.options.device === 'phone'),
'screenshot': task.options.screenshot || false,
'viewport': viewportOption,
'wait-for-network-idle': true,
'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,
'local-storage': task.options.localStorage,
'session-storage': task.options.sessionStorage,
// Mandatory
'reporter': 'json:pretty',
'analyze-css': true,
'skip-modules': [
'blockDomains', // not needed
'domMutations', // not compatible with webkit
'domQueries', // overriden
'eventListeners', // overridden
'filmStrip', // not needed
'har', // not needed for the moment
'javaScriptBottlenecks', // needs to be launched after custom module scopeYLT
'jQuery', // overridden
'jserrors', // overridden
'pageSource', // not needed
'waitForSelector', // not needed
'windowPerformance' // overriden
].join(','),
'include-dirs': [
path.join(__dirname, 'custom_modules/core'),
path.join(__dirname, 'custom_modules/modules')
].join(',')
'ignoreSslErrors': true, // until Phantomas 2.1
'ignore-ssl-errors': true // for Phantomas >= 2.2
};
// Output the command line for debugging purpose
debug('If you want to reproduce the phantomas task only, copy the following command line:');
var optionsString = '';
for (var opt in options) {
var value = options[opt];
if ((typeof value === 'string' || value instanceof String) && value.indexOf(' ') >= 0) {
value = '"' + value + '"';
}
optionsString += ' ' + '--' + opt + '=' + value;
// 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;
}
debug('node node_modules/phantomas/bin/phantomas.js --url=' + task.url + optionsString + ' --verbose');
// Kill the application if nothing happens
var phantomasPid;
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
// 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);
}
// Then suicide.
process.exit(1);
});
var debugCmd = 'DEBUG=* node node_modules/phantomas/bin/phantomas.js --url ' + task.url;
Object.keys(options).forEach(function(key) {
if (key !== 'wait-for-network-idle' && options[key] !== null) {
debugCmd += ' --' + key + ' ' + options[key];
}
}, 5*options.timeout*1000);
});
debug('If you want to run Phantomas alone for debugging purpose, this is the command: %s', debugCmd);
// It's time to launch the test!!!
var triesNumber = 2;
const promise = phantomas(task.url, options);
async.retry(triesNumber, function(cb) {
var process = phantomas(task.url, options, function(err, json, results) {
debug('Returning from Phantomas');
// Adding some YellowLabTools errors here
if (json && json.metrics && (!json.metrics.javascriptExecutionTree || !json.offenders.javascriptExecutionTree)) {
err = 1001;
}
if (!err && (!json || !json.metrics)) {
err = 1002;
}
// Don't cancel test if it is a timeout and we've got some results
if (err === 252 && json) {
debug('Timeout after ' + options.timeout + ' seconds. But it\'s not a problem, the test is valid.');
err = null;
}
if (err) {
debug('Attempt failed. Error code ' + err);
}
cb(err, json);
});
phantomasPid = process.pid;
}, function(err, json) {
clearTimeout(killer);
if (err) {
debug('All ' + triesNumber + ' attemps failed for the test');
deferred.reject(err);
} else {
// handle the promise
promise.
then(results => {
var json = {
generator: results.getGenerator(),
url: results.getUrl(),
metrics: results.getMetrics(),
offenders: results.getAllOffenders()
};
deferred.resolve(json);
}).
catch(res => {
console.error(res);
deferred.reject('Phantomas failed: ' + res.message);
});
promise.on('milestone', function(event) {
if (event === 'domReady' || event === 'domComplete') {
deferred.notify(event);
}
});
+107
View File
@@ -0,0 +1,107 @@
var debug = require('debug')('ylt:brotliCompressor');
var Q = require('q');
var zlib = require('zlib');
var gzipCompressor = require('./gzipCompressor');
var GzipCompressor = function() {
var BROTLI_COMPRESSION_LEVEL = 9;
function compressFile(entry) {
debug('Entering brotli compressor');
return brotlifyFile(entry)
.then(brotliOptimizedFile);
}
// Compress with Brotli files that were not compressed or not with brotli (with gzip or deflate)
function brotlifyFile(entry) {
var deferred = Q.defer();
if (gzipCompressor.entryTypeCanBeCompressed(entry) && entry.weightCheck && entry.weightCheck.bodyBuffer) {
if (!entry.weightCheck.isCompressed) {
debug('File %s was not compressed at all, trying Brotli over it.', entry.url);
} else if (entry.weightCheck.compressionTool !== 'brotli') {
debug('File %s was compressed with %s. Trying with Brotli.', entry.url, entry.weightCheck.compressionTool);
}
zlib.brotliCompress(entry.weightCheck.bodyBuffer, {
params: {
[zlib.constants.BROTLI_PARAM_QUALITY]: 9
}
}, function(err, buffer) {
if (err) {
debug('Could not compress uncompressed file with brotli');
debug(err);
deferred.reject(err);
} else {
var compressedSize = buffer.length;
if (!entry.weightCheck.isCompressed) {
debug('Brotli size is %d, was %d, this is %d% better.', compressedSize, entry.weightCheck.bodySize, Math.round((entry.weightCheck.bodySize - compressedSize) * 100 / entry.weightCheck.bodySize));
} else if (entry.weightCheck.compressionTool !== 'brotli') {
debug('Brotli size is %d, was %d with %s, this is %d% better.', compressedSize, entry.weightCheck.bodySize, entry.weightCheck.compressionTool, Math.round((entry.weightCheck.bodySize - compressedSize) * 100 / entry.weightCheck.bodySize));
}
entry.weightCheck.afterBrotliCompression = 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 brotliOptimizedFile(entry) {
var deferred = Q.defer();
if (gzipCompressor.entryTypeCanBeCompressed(entry) && entry.weightCheck && entry.weightCheck.isOptimized === false) {
debug('Trying to brotlify file after minification: %s', entry.url);
var uncompressedSize = entry.weightCheck.optimized;
zlib.brotliCompress(Buffer.from(entry.weightCheck.bodyAfterOptimization, 'utf8'), {
params: {
[zlib.constants.BROTLI_PARAM_QUALITY]: 9
}
}, function(err, buffer) {
if (err) {
debug('Could not compress minified file with brotli');
debug(err);
deferred.reject(err);
} else {
var compressedSize = buffer.length;
debug('Correctly brotlified the minified file, was %d and is now %d bytes', uncompressedSize, compressedSize);
entry.weightCheck.afterOptimizationAndBrotliCompression = compressedSize;
deferred.resolve(entry);
}
}
);
} else {
debug('Compressing optimized file not needed');
deferred.resolve(entry);
}
return deferred.promise;
}
return {
compressFile: compressFile
};
};
module.exports = new GzipCompressor();
+250
View File
@@ -0,0 +1,250 @@
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 isWebp = require('is-webp');
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 isJson = require('is-json');
var ContentTypeChecker = function() {
function checkContentType(entry) {
var deferred = Q.defer();
// Setting isSomething values:
switch(entry.type) {
case 'html':
entry.isHTML = true;
break;
case 'xml':
entry.isXML = true;
break;
case 'css':
entry.isCSS = true;
break;
case 'js':
entry.isJS = true;
break;
case 'json':
entry.isJSON = true;
break;
case 'image':
entry.isImage = true;
break;
case 'webfont':
entry.isWebFont = true;
break;
case 'video':
entry.isVideo = true;
break;
case 'favicon':
entry.isFavicon = true;
break;
}
// Now let's check for mistakes by analysing body content. It happens more often then we think!
// 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 it's an image or a font, then rewrite.
if (foundType !== null && (foundType.type === 'image' || foundType.type === 'webfont' || foundType.type === 'json')) {
if (foundType.type !== entry.type) {
debug('Content type %s is wrong for %s. It should be %s.', entry.type, entry.ulr, foundType.type);
}
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 (isWebp(bodyBuffer)) {
return contentTypes.webp;
}
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;
}
if (isJson(bodyStr)) {
return contentTypes.json;
}
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: {
type: 'image',
mimes: ['image/jpeg'],
updateFn: function(entry) {
entry.type = 'image';
entry.isImage = true;
}
},
png: {
type: 'image',
mimes: ['image/png'],
updateFn: function(entry) {
entry.type = 'image';
entry.isImage = true;
}
},
svg: {
type: 'image',
mimes: ['image/svg+xml'],
updateFn: function(entry) {
entry.type = 'image';
entry.isImage = true;
entry.isSVG = true;
}
},
gif: {
type: 'image',
mimes: ['image/gif'],
updateFn: function(entry) {
entry.type = 'image';
entry.isImage = true;
}
},
webp: {
type: 'image',
mimes: ['image/webp'],
updateFn: function(entry) {
entry.type = 'image';
entry.isImage = true;
}
},
woff: {
type: 'webfont',
mimes: ['application/x-font-woff', 'application/font-woff', 'font/woff'],
updateFn: function(entry) {
entry.type = 'webfont';
entry.isWebFont = true;
entry.isWoff = true;
}
},
woff2: {
type: 'webfont',
mimes: ['font/woff2', 'application/x-font-woff2', 'application/font-woff2'],
updateFn: function(entry) {
entry.type = 'webfont';
entry.isWebFont = true;
entry.isWoff2 = true;
}
},
otf: {
type: 'webfont',
mimes: ['application/x-font-otf', 'font/otf', 'font/opentype', 'application/x-font-opentype'],
updateFn: function(entry) {
entry.type = 'webfont';
entry.isWebFont = true;
}
},
ttf: {
type: 'webfont',
mimes: ['application/x-font-ttf', 'font/ttf', 'application/x-font-truetype'],
updateFn: function(entry) {
entry.type = 'webfont';
entry.isWebFont = true;
entry.isTTF = true;
}
},
eot: {
type: 'webfont',
mimes: ['application/vnd.ms-fontobject', 'font/eot'],
updateFn: function(entry) {
entry.type = 'webfont';
entry.isWebFont = true;
}
},
json: {
type: 'json',
mimes: ['application/json'],
updateFn: function(entry) {
entry.type = 'json';
entry.isJSON = true;
}
},
};
return {
checkContentType: checkContentType,
findContentType: findContentType
};
};
module.exports = new ContentTypeChecker();
+256
View File
@@ -0,0 +1,256 @@
var debug = require('debug')('ylt:fileMinifier');
var Q = require('q');
var UglifyJS = require('uglify-js');
var CleanCSS = require('clean-css');
var htmlMinifier = require('html-minifier');
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) {
var deferred = Q.defer();
var startTime = Date.now();
var result = UglifyJS.minify(body, {
// Only do the compression step for smaller files
// otherwise it can take a very long time compared to the gain
compress: (body.length < 200*1024)
});
var endTime = Date.now();
debug('Uglify took %dms', endTime - startTime);
deferred.resolve(result.code);
return deferred.promise;
}
// Clear-css
function minifyCss(body) {
var deferred = Q.defer();
try {
var result = new CleanCSS().minify(body);
deferred.resolve(result.styles);
} catch(err) {
deferred.reject(err);
}
return deferred.promise;
}
// HTMLMinifier
function minifyHtml(body) {
var deferred = Q.defer();
try {
var result = htmlMinifier.minify(body, {
collapseWhitespace: true,
conservativeCollapse: true,
continueOnParseError: true,
decodeEntities: true,
minifyCSS: true,
minifyJS: true,
preserveLineBreaks: true,
removeAttributeQuotes: true,
removeComments: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true
});
deferred.resolve(result);
} catch(err) {
deferred.reject(err);
}
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 time 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();
+470
View File
@@ -0,0 +1,470 @@
var debug = require('debug')('ylt:fontAnalyzer');
var Q = require('q');
var fontkit = require('fontkit');
var woffTools = require('woff-tools');
var ttf2woff2 = require('ttf2woff2');
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;
}
if (entry.isWebFont) {
debug('File %s is a font. Let\'s have a look inside!', entry.url);
convertToWoff2(entry)
.then(function(entry) {
return getMetricsFromFont(entry, charsListOnPage);
})
.then(function(fontMetrics) {
entry.fontMetrics = fontMetrics;
})
.fail(function(error) {
debug('Could not open the font: %s', error);
});
}
deferred.resolve(entry);
return deferred.promise;
}
function convertToWoff2(entry) {
var deferred = Q.defer();
debug('Entering font format converter...');
var fileSize = entry.weightCheck.bodySize;
var ttf;
var woff2;
var newFileSize;
if (entry.isWoff2) {
debug('File is already a woff2.');
deferred.resolve(entry);
} else if (fileSize > 1024 * 1024) {
// Don't try to convert huge font files, it would block the server for several minutes
debug('Font is too big, skipping conversion.');
deferred.resolve(entry);
} else if (entry.isWoff) {
debug('File is a woff. Let\'s convert to woff2');
try {
debug('Current file size is %d', fileSize);
ttf = woffTools.toSfnt(entry.weightCheck.bodyBuffer);
woff2 = ttf2woff2(ttf);
newFileSize = woff2.length;
debug('New font size is %d', newFileSize);
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
entry.weightCheck.sizeAsWoff2 = newFileSize;
deferred.resolve(entry);
} catch(error) {
deferred.reject(error);
}
} else if (entry.isTTF) {
debug('File is a TTF. Let\'s convert to woff2');
try {
debug('Current file size is %d', fileSize);
woff2 = ttf2woff2(entry.weightCheck.bodyBuffer);
newFileSize = woff2.length;
debug('New image size is %d', newFileSize);
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
entry.weightCheck.sizeAsWoff2 = newFileSize;
deferred.resolve(entry);
} catch(error) {
deferred.reject(error);
}
} else {
// Other font formats are not handled
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 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.afterGzipCompression || 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();
+103
View File
@@ -0,0 +1,103 @@
var debug = require('debug')('ylt:gzipCompressor');
var Q = require('q');
var zlib = require('zlib');
var GzipCompressor = function() {
function compressFile(entry) {
debug('Entering gzip compressor');
return gzipUncompressedFile(entry)
.then(gzipOptimizedFile);
}
// Gzip a file if it was not already gziped
function gzipUncompressedFile(entry) {
var deferred = Q.defer();
if (entryTypeCanBeCompressed(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.afterGzipCompression = 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 (entryTypeCanBeCompressed(entry) && entry.weightCheck && entry.weightCheck.isOptimized === false) {
debug('Trying to gzip file after minification: %s', entry.url);
var uncompressedSize = entry.weightCheck.optimized;
zlib.gzip(Buffer.from(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.afterOptimizationAndGzipCompression = 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 entryTypeCanBeCompressed(entry) {
return entry.isJS || entry.isCSS || entry.isHTML || entry.isJSON || entry.isSVG || entry.isTTF || entry.isXML || entry.isFavicon;
}
return {
compressFile: compressFile,
gainIsEnough: gainIsEnough,
entryTypeCanBeCompressed: entryTypeCanBeCompressed
};
};
module.exports = new GzipCompressor();

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