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
145 changed files with 3854 additions and 12201 deletions
+6 -5
View File
@@ -1,8 +1,9 @@
node_modules
package-lock.json
.tmp
.vagrant
results/*
tmp
coverage
front/build
har.json
package-lock.json
yarn.lock
har.json
.idea/
-18
View File
@@ -1,18 +0,0 @@
language: node_js
sudo: false
node_js:
- "5.8"
- "0.12"
env:
- CXX=g++-4.8
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-4.8
before_install:
- "npm install -g npm"
- "npm install -g grunt-cli"
install:
- "npm install"
+1
View File
@@ -0,0 +1 @@
ko_fi: gaelmetais
-356
View File
@@ -1,356 +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'),
webfont: {
icons: {
src: 'front/src/fonts/svg-icons/*.svg',
dest: 'tmp',
destCss: 'front/src/less',
options: {
engine: 'node',
types: 'woff',
stylesheet: 'less',
embed: true,
htmlDemo: false,
syntax: 'bootstrap'
}
}
},
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/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/imageOptimizerTest.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']
}
},
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', [
'webfont: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'
]);
};
+29 -24
View File
@@ -11,15 +11,25 @@ Analyzes a webpage and detects **performance** or **front-end code quality** iss
<tr>
<td width="70%">
The <b>online tool</b> (recommended):
<a href="http://yellowlab.tools" target="_blank">http://yellowlab.tools</a>
<a href="https://yellowlab.tools" target="_blank">https://yellowlab.tools</a>
</td>
<td width="30%">
<img src="./doc/img/YLT-animated.gif"></img>
<img src="./doc/img/home.png"></img>
</td>
</tr>
<tr>
<td width="70%">
The <b>CLI</b> (Command Line Interface) - <a href="https://github.com/gmetais/YellowLabTools/wiki/Command-Line-Interface" target="_blank">Doc here</a>
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>
@@ -27,16 +37,7 @@ Analyzes a webpage and detects **performance** or **front-end code quality** iss
</tr>
<tr>
<td width="70%">
The <b>Grunt task</b>: <a href="https://github.com/gmetais/grunt-yellowlabtools" traget="_blank">gmetais/grunt-yellowlabtools</a>
<br>For developers or Continuous Integration
</td>
<td width="30%">
<img src="./doc/img/grunt-logo.png"></img>
</td>
</tr>
<tr>
<td width="70%">
The <b>NodeJS</b> package - <a href="https://github.com/gmetais/YellowLabTools/wiki/NodeJS-module" target="_blank">Doc here</a>
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>
@@ -44,7 +45,7 @@ Analyzes a webpage and detects **performance** or **front-end code quality** iss
</tr>
<tr>
<td width="70%">
The <b>public API</b> - <a href="https://github.com/gmetais/YellowLabTools/wiki/Public-API" target="_blank">Doc here</a>
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>
@@ -58,29 +59,33 @@ Analyzes a webpage and detects **performance** or **front-end code quality** iss
## 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 then categorized and transformed into scores. It also gives in-depth details so developpers can fix 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!!!
![example dashboard screenshot](./doc/img/screenshot.png)
## Test your localhost
You can use [ngrok](https://ngrok.com/), a tool that creates a secure tunnel between your localhost and the online tool (or the public API). You can also use the CLI or the Grunt tasks as they run on your machine.
## Install your own private instance
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).
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. Follow me on Twitter [@gaelmetais](https://twitter.com/gaelmetais), I tweet about Web Performances, Front-end and new versions of YellowLabTools!
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!
If you understand French, you can visit [my website](http://www.gaelmetais.com) (will be soon in English too).
<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
+42 -6
View File
@@ -13,16 +13,21 @@ var cli = meow({
' yellowlabtools <url> <options>',
'',
'Options:',
' --device Use "phone" or "tablet" to simulate a mobile device (by user-agent and viewport size).',
' --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".',
' --wait-for-selector Once the page is loaded, Phantomas will wait until the given CSS selector matches some elements.',
' --proxy Sets an HTTP proxy to pass through. Syntax is "host:port".',
' --cookie Adds a cookie on the main domain.',
' --auth-user Basic HTTP authentication username.',
' --auth-pass Basic HTTP authentication password.',
' --block-domain Disallow requests to given (comma-separated) domains.',
' --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')
});
@@ -51,11 +56,14 @@ if (screenshot) {
}
// Device simulation
options.device = cli.flags.device || 'desktop';
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;
@@ -63,6 +71,15 @@ options.cookie = cli.flags.cookie || null;
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"');
@@ -80,8 +97,27 @@ if (cli.flags.reporter && cli.flags.reporter !== 'json' && cli.flags.reporter !=
debug('Success');
switch(cli.flags.reporter) {
case 'xml':
var serializer = new EasyXml();
console.log(serializer.render(data));
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));
-40
View File
@@ -1,40 +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');
var wwwRedirectMiddleware = require('../lib/server/middlewares/wwwRedirectMiddleware');
// Middlewares
app.use(compress());
app.use(bodyParser.json());
app.use(cors());
app.use(wwwRedirectMiddleware);
app.use(authMiddleware);
app.use(apiLimitsMiddleware);
// 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;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

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;
}
-191
View File
@@ -1,191 +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 .globalScore .screenshotWrapper.phone:hover:after {
top: 1.7em;
left: 0.64em;
}
.summary .globalScore .screenshotWrapper.tablet:hover:after {
top: 1.5em;
left: 0.9em;
}
.summary .notations {
display: table;
width: 80%;
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 > a {
text-decoration: none;
color: inherit;
}
.summary .notations .criteria .table > a:hover > div {
background: #EBD8E2;
cursor: pointer;
}
.summary .notations .criteria .table > a:hover > div.info {
background: #FFF;
}
.summary .notations .criteria .table > a: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;
}
File diff suppressed because one or more lines are too long
-179
View File
@@ -1,179 +0,0 @@
.promess {
padding: 0em 2em;
margin-bottom: 0.5em;
font-weight: normal;
font-size: 1.2em;
}
.price {
padding: 0em 2em 3em;
margin-top: 0em;
font-size: 0.9em;
}
.url {
width: 50%;
}
.launchBtn {
background: #ffa319;
color: #fff;
}
.launchBtn:focus {
background: #e74c3c;
}
.launchBtn.disabled {
background: #deaca6;
}
.launchBtn.disabled:focus {
color: #ddd;
}
.settings {
width: 50%;
margin: 0 auto;
}
.settings input,
.settings select {
font-size: 1em;
}
.settings input[type=text],
.settings input[type=password],
.settings textarea {
width: 100%;
min-width: 4em;
}
.device {
margin-top: 3em;
}
.device .item {
display: inline-block;
margin: 1em 0.75em;
width: 5.5em;
height: 5.5em;
color: #FFF;
border: 1px solid #FFF;
padding: 1px;
border-radius: 0.5em;
cursor: pointer;
text-decoration: none;
font-size: 0.8em;
}
.device .item.active {
color: #ffa319;
border: 2px solid #ffa319;
padding: 0;
}
.device .item:hover {
color: #ffa319;
}
.device .item > div {
margin: 0.2em 0 0.1em;
font-size: 3em;
}
.settingsTooltip {
position: relative;
}
.settingsTooltip span {
font-size: 0.8em;
vertical-align: text-top;
}
.settingsTooltip div {
display: none;
position: absolute;
padding: 0.5em;
width: 25em;
background: #FFF;
color: #000;
font-size: 0.8em;
border-radius: 1em;
border: 2px solid #ffa319;
white-space: normal;
z-index: 2;
}
.settingsTooltip:hover div {
display: block;
}
.showAdvanced {
display: inline-block;
margin-top: 2em;
color: #FFF;
text-decoration: none;
font-size: 0.9em;
}
.showAdvanced:hover {
color: #ffa319;
}
.currentSettings {
font-size: 0.9em;
}
.currentSettings span {
color: #ffa319;
}
.currentSettings span:after {
color: #FFF;
content: ",";
}
.currentSettings span:last-child:after {
content: "";
}
.advanced {
margin: 1em 0 0;
display: table;
width: 100%;
text-align: left;
border-spacing: 0.75em;
}
.advanced > div {
display: table-row;
}
.advanced > div > div {
display: table-cell;
width: 75%;
}
.advanced > div > div.label {
width: 25%;
white-space: nowrap;
vertical-align: middle;
}
.advanced .subTable {
display: table;
border-spacing: 0;
width: 100%;
}
.advanced .subTable > div {
display: table-row;
}
.advanced .subTable > div > div {
display: table-cell;
padding: 0 0 0.75em;
}
.features {
display: table;
width: 50%;
margin: 6em 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;
}
File diff suppressed because one or more lines are too long
-8
View File
@@ -1,8 +0,0 @@
.status {
margin-top: 2em;
font-size: 2.5em;
}
.statusSubMessage {
font-size: 0.8em;
margin-bottom: 6em;
}
-268
View File
@@ -1,268 +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 h3 {
margin-bottom: 0em;
}
.rule .okThreshold {
font-style: italic;
font-size: 0.9em;
}
.rule .message {
width: 80%;
margin: 1.5em 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;
}
.rule .startTime {
display: none;
}
.offendersTable .offenderButton,
.value .offenderButton {
display: inline-block;
position: relative;
background: #efe;
padding: 0 0.5em;
margin: 0.2em 0;
border-radius: 0.4em;
z-index: 1;
cursor: pointer;
}
.offendersTable .offenderButton.opens,
.value .offenderButton.opens {
padding-right: 0.75em;
}
.offendersTable .offenderButton.opens:after,
.value .offenderButton.opens:after {
position: relative;
left: 0.5em;
content: '\25BC';
font-size: 0.8em;
}
.offendersTable .offenderButton > div,
.value .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;
}
.offendersTable .offenderButton .domTree,
.value .offenderButton .domTree {
text-align: left;
white-space: nowrap;
}
.offendersTable .offenderButton .domTree > div,
.value .offenderButton .domTree > div {
margin: 0.5em;
}
.offendersTable .offenderButton .domTree > div div,
.value .offenderButton .domTree > div div {
margin-left: 1em;
}
.offendersTable .offenderButton .backtrace,
.value .offenderButton .backtrace,
.offendersTable .offenderButton .cssFileAndLine,
.value .offenderButton .cssFileAndLine {
white-space: nowrap;
padding: 0.5em;
}
.offendersTable .offenderButton.opens:hover,
.value .offenderButton.opens:hover {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
background: #ffe0cc;
z-index: 2;
}
.offendersTable .offenderButton.opens:hover > div,
.value .offenderButton.opens:hover > div {
display: block;
background: #ffe0cc;
}
.offendersTable .smallerOffenders,
.value .smallerOffenders {
font-size: 0.9em;
}
.offendersHtml {
display: inline-block;
}
.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;
}
.checker {
/* Checkerboard background */
background-color: #ddd;
background-image: linear-gradient(45deg, #AAA 25%, transparent 25%, transparent 75%, #AAA 75%, #AAA), linear-gradient(45deg, #AAA 25%, transparent 25%, transparent 75%, #AAA 75%, #AAA);
background-size: 1em 1em;
background-position: 0 0, 0.5em 0.5em;
}
.colorPalette {
width: 30em;
border: 2px solid #000;
text-align: left;
}
.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;
left: -0.2em;
top: -0.2em;
width: 100%;
height: 100%;
z-index: 2;
border: 0.2em solid #f1c40f;
}
.similarColors {
margin: 1em;
width: 20em;
height: 6em;
}
.similarColors > div {
display: inline-block;
width: 10em;
height: 3.5em;
padding-top: 2.5em;
}
.totalWeightPie {
max-width: 39em;
margin: 2em auto 4em;
}
.totalWeightPie canvas {
max-width: inherit;
}
.hugeFile {
font-weight: bold;
color: #e74c3c;
}
.imageOffenders {
display: table;
border-spacing: 3em;
width: 90%;
}
.imageOffenders > div {
display: table-row;
}
.imageOffenders > div > div {
display: table-cell;
vertical-align: middle;
}
.imageOffenders img {
max-height: 10em;
max-width: 40em;
border: 1px solid #000;
margin-top: 0.5em;
}
.smallPreview {
display: block;
max-height: 4em;
max-width: 8em;
border: 1px solid #000;
margin: 1em auto 0.2em;
}
-7
View File
@@ -1,7 +0,0 @@
.screenshot.board {
text-align: center;
}
.screenshot .screenshotWrapper {
font-size: 2.08333333333333em;
margin-bottom: 0.5em;
}
-338
View File
@@ -1,338 +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;
}
.subFilters {
margin-left: 3em;
font-size: 0.9em;
}
.table {
display: table;
width: 100%;
border-spacing: 0.25em;
}
.table > div,
.table > a {
display: table-row;
}
.table > .headers > div {
font-weight: bold;
padding: 0.5em 1em;
}
.table > div > div,
.table > a > 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;
}
.queryWithoutResultsFilterOn > div {
display: none;
}
.queryWithoutResultsFilterOn > div.queryWithoutResults {
display: table-row;
}
.jQueryCallOnEmptyObjectFilterOn > div {
display: none;
}
.jQueryCallOnEmptyObjectFilterOn > div.jQueryCallOnEmptyObject {
display: table-row;
}
.eventNotDelegatedFilterOn > div {
display: none;
}
.eventNotDelegatedFilterOn > div.eventNotDelegated {
display: table-row;
}
.jsErrorFilterOn > div {
display: none;
}
.jsErrorFilterOn > div.jsError {
display: table-row;
}
@@ -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"><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: 1.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"><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: 634 B

-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"><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: 792 B

-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"><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: 679 B

-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"><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: 875 B

-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"><path d="M 368.00,0.00L 144.00,0.00 c-26.40,0.00-48.00,21.60-48.00,48.00l0.00,416.00 c0.00,26.40, 21.60,48.00, 48.00,48.00l 224.00,0.00 c 26.40,0.00, 48.00-21.60, 48.00-48.00L 416.00,48.00 C 416.00,21.60, 394.40,0.00, 368.00,0.00z M 192.00,24.00 l 128.00,0.00 l0.00,16.00 L 192.00,40.00 L 192.00,24.00 z M 256.00,480.00c-17.673,0.00-32.00-14.327-32.00-32.00s 14.327-32.00, 32.00-32.00s 32.00,14.327, 32.00,32.00S 273.673,480.00, 256.00,480.00z M 384.00,384.00L 128.00,384.00 L 128.00,64.00 l 256.00,0.00 L 384.00,384.00 z" ></path></svg>

Before

Width:  |  Height:  |  Size: 851 B

-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"><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: 1.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"><path d="M 512.00,416.00L 512.00,32.00 L0.00,32.00 l0.00,384.00 l 224.00,0.00 l0.00,32.00 l-96.00,0.00 l0.00,32.00 l 256.00,0.00 l0.00-32.00 l-96.00,0.00 l0.00-32.00 L 512.00,416.00 z M 64.00,96.00l 384.00,0.00 l0.00,256.00 L 64.00,352.00 L 64.00,96.00 z" ></path></svg>

Before

Width:  |  Height:  |  Size: 584 B

-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"><path d="M 400.00,0.00L 80.00,0.00 C 53.60,0.00, 32.00,21.60, 32.00,48.00l0.00,416.00 c0.00,26.40, 21.60,48.00, 48.00,48.00l 320.00,0.00 c 26.40,0.00, 48.00-21.60, 48.00-48.00L 448.00,48.00 C 448.00,21.60, 426.40,0.00, 400.00,0.00z M 240.00,496.00 c-8.836,0.00-16.00-7.163-16.00-16.00s 7.164-16.00, 16.00-16.00s 16.00,7.163, 16.00,16.00S 248.836,496.00, 240.00,496.00z M 384.00,448.00L 96.00,448.00 L 96.00,64.00 l 288.00,0.00 L 384.00,448.00 z" ></path></svg>

Before

Width:  |  Height:  |  Size: 774 B

-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"><path d="M 256.00,79.362 L 83.583,448.00 L 428.417,448.00 L 256.00,79.362 Z M 256.00,0.00 L 256.00,0.00 C 267.035,0.00 278.07,7.441 286.442,22.324 L 504.978,457.88 C 521.723,487.646 507.478,512.00 473.327,512.00 L 38.673,512.00 C 4.521,512.00 -9.722,487.646 7.021,457.88 L 225.558,22.324 C 233.93,7.441 244.965,0.00 256.00,0.00 ZM 256.00,192.00 C 273.673,192.00 288.00,206.327 288.00,224.00 L 278.00,320.00 L 234.00,320.00 L 224.00,224.00 C 224.00,206.327 238.327,192.00 256.00,192.00 Z" ></path><circle cx="256" cy="384" r="31" style="stroke:#000000; fill:#000000"/></svg>

Before

Width:  |  Height:  |  Size: 887 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 794 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

-84
View File
@@ -1,84 +0,0 @@
var yltApp = angular.module('YellowLabTools', [
'ngRoute',
'ngSanitize',
'ngAnimate',
'indexCtrl',
'dashboardCtrl',
'queueCtrl',
'ruleCtrl',
'screenshotCtrl',
'timelineCtrl',
'runsFactory',
'resultsFactory',
'apiService',
'menuService',
'settingsService',
'gradeDirective',
'offendersDirectives',
'LocalStorageModule'
]);
yltApp.run(['$rootScope', '$location', function($rootScope, $location) {
$rootScope.isTouchDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(window.navigator.userAgent);
$rootScope.loadedRunId = null;
var oldHash;
// We don't want the hash to be kept between two pages
$rootScope.$on('$locationChangeStart', function(param1, param2, param3, param4){
var newHash = $location.hash();
if (newHash === oldHash) {
$location.hash(null);
}
oldHash = newHash;
});
// Google Analytics
$rootScope.$on('$routeChangeSuccess', function(){
ga('send', 'pageview', {'page': $location.path()});
});
// GitHub star button (asynchronously loaded iframe)
window.addEventListener('load', function() {
window.document.getElementById('ghbtn').src = 'http://ghbtns.com/github-btn.html?user=gmetais&repo=YellowLabTools&type=star&count=true&size=large';
});
}]);
yltApp.config(['$routeProvider', '$locationProvider',
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);
}
]);
-60
View File
@@ -1,60 +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, exclude: 'toolsResults'}, 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 = 'I\'ve discovered this cool open-source tool that audits the front-end quality of a web page: ';
}
$scope.testAgain = function() {
API.relaunchTest($scope.result);
};
// When comming from a social shared link, the user needs to click on "See full report" button to display the full dashboard.
$scope.seeFullReport = function() {
$scope.fromSocialShare = false;
$location.search({});
};
$scope.shareOnTwitter = function(message) {
openSocialPopup('https://twitter.com/intent/tweet?text=' + encodeURIComponent(message + 'http://yellowlab.tools'));
};
$scope.shareOnLinkedin = function(message) {
openSocialPopup('https://www.linkedin.com/shareArticle?mini=true&url=http://yellowlab.tools&title=' + encodeURIComponent(message) + '&summary=' + encodeURIComponent('YellowLabTools is a free online tool that analyzes performance and front-end quality of a webpage.'));
};
function openSocialPopup(url) {
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);
}
loadResults();
}]);
-13
View File
@@ -1,13 +0,0 @@
var indexCtrl = angular.module('indexCtrl', []);
indexCtrl.controller('IndexCtrl', ['$scope', 'Settings', 'API', function($scope, Settings, API) {
$scope.settings = Settings.getMergedSettings();
$scope.launchTest = function() {
if ($scope.url) {
Settings.saveSettings($scope.settings);
API.launchTest($scope.url, $scope.settings);
}
};
}]);
-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();
}]);
-111
View File
@@ -1,111 +0,0 @@
var ruleCtrl = angular.module('ruleCtrl', ['chart.js']);
ruleCtrl.config(['ChartJsProvider', function (ChartJsProvider) {
// Configure all charts
ChartJsProvider.setOptions({
animation: false,
colours: ['#FF5252', '#FF8A80'],
responsive: true
});
}]);
ruleCtrl.controller('RuleCtrl', ['$scope', '$rootScope', '$routeParams', '$location', '$sce', 'Menu', 'Results', 'API', function($scope, $rootScope, $routeParams, $location, $sce, Menu, Results, API) {
$scope.runId = $routeParams.runId;
$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, exclude: 'toolsResults'}, function(result) {
$rootScope.loadedResult = result;
$scope.result = result;
init();
});
} else {
$scope.result = $rootScope.loadedResult;
init();
}
}
function init() {
$scope.rule = $scope.result.rules[$scope.policyName];
// Init "Total Weight" chart
if ($scope.policyName === 'totalWeight') {
$scope.weightLabels = [];
$scope.weightColours = ['#7ECCCC', '#A7E846', '#FF944D', '#FFE74A', '#C2A3FF', '#5A9AED', '#FF6452', '#C1C1C1'];
$scope.weightData = [];
var types = ['html', 'css', 'js', 'json', 'image', 'video', 'webfont', 'other'];
types.forEach(function(type) {
$scope.weightLabels.push(type);
$scope.weightData.push(Math.round($scope.rule.offendersObj.list.byType[type].totalWeight / 1024));
});
$scope.weightOptions = {
tooltipTemplate: '<%=label%>: <%=value%> KB'
};
}
// Init "Breakpoints" chart
if ($scope.policyName === 'cssBreakpoints' && $scope.rule.value > 0) {
// Seek for the biggest breakpoint
var max = 0;
$scope.rule.offendersObj.forEach(function(offender) {
if (offender.pixels > max) {
max = offender.pixels;
}
});
max = Math.max(max + 100, 1400);
// We group offenders 10px by 10px
var GROUP_SIZE = 20;
// Generate an empty array of values
$scope.breakpointsLabels = [];
$scope.breakpointsData = [[]];
for (var i = 0; i <= max / GROUP_SIZE; i++) {
$scope.breakpointsLabels[i] = '';
$scope.breakpointsData[0][i] = 0;
}
// Fill it with results
$scope.rule.offendersObj.forEach(function(offender) {
var group = Math.floor((offender.pixels + 1) / GROUP_SIZE);
if ($scope.breakpointsLabels[group] !== '') {
$scope.breakpointsLabels[group] += '/';
}
$scope.breakpointsLabels[group] += offender.breakpoint;
$scope.breakpointsData[0][group] += offender.count;
});
$scope.breakpointsSeries = ['Number of CSS rules per breakpoint'];
$scope.breakpointsColours = ['#9c4274'];
$scope.breakpointsOptions = {
scaleShowGridLines: false,
barShowStroke: false,
showTooltips: false,
pointDot: false,
responsive: true,
maintainAspectRatio: true,
strokeColor: 'rgba(20, 200, 20, 1)',
scaleFontSize: 9
};
}
}
$scope.backToDashboard = function() {
$location.path('/result/' + $scope.runId);
};
$scope.testAgain = function() {
API.relaunchTest($scope.result);
};
loadResults();
}]);
@@ -1,30 +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, exclude: 'toolsResults'}, function(result) {
$rootScope.loadedResult = result;
$scope.result = result;
}, function(err) {
$scope.error = true;
});
} else {
$scope.result = $rootScope.loadedResult;
}
}
$scope.backToDashboard = function() {
$location.path('/result/' + $scope.runId);
};
$scope.testAgain = function() {
API.relaunchTest($scope.result);
};
loadResults();
}]);
-190
View File
@@ -1,190 +0,0 @@
var timelineCtrl = angular.module('timelineCtrl', []);
timelineCtrl.controller('TimelineCtrl', ['$scope', '$rootScope', '$routeParams', '$location', '$timeout', 'Menu', 'Results', 'API', function($scope, $rootScope, $routeParams, $location, $timeout, Menu, Results, API) {
$scope.runId = $routeParams.runId;
$scope.Menu = Menu.setCurrentPage('timeline', $scope.runId);
function loadResults() {
// Load result if needed
if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
Results.get({runId: $routeParams.runId, exclude: 'toolsResults'}, function(result) {
$rootScope.loadedResult = result;
$scope.result = result;
render();
});
} else {
$scope.result = $rootScope.loadedResult;
render();
}
}
function render() {
initFilters();
initScriptFiltering();
initExecutionTree();
initTimeline();
$timeout(initProfiler, 100);
}
function initFilters() {
var hash = $location.hash();
var filter = null;
if (hash.indexOf('filter=') === 0) {
filter = hash.substr(7);
}
$scope.warningsFilterOn = (filter !== null);
$scope.warningsFilters = {
queryWithoutResults: (filter === null || filter === 'queryWithoutResults'),
jQueryCallOnEmptyObject: (filter === null || filter === 'jQueryCallOnEmptyObject'),
eventNotDelegated: (filter === null || filter === 'eventNotDelegated'),
jsError: (filter === null || filter === 'jsError')
};
}
function initScriptFiltering() {
var offenders = $scope.result.rules.totalRequests.offendersObj.list.byType.js;
$scope.scripts = [];
offenders.forEach(function(filePath) {
var shortPath = filePath;
if (filePath.length > 100) {
shortPath = filePath.substr(0, 98) + '...';
}
var scriptObj = {
fullPath: filePath,
shortPath: shortPath
};
$scope.scripts.push(scriptObj);
});
}
function initExecutionTree() {
var originalExecutions = $scope.result.javascriptExecutionTree.children || [];
// Detect the last event of all (before filtering) and read time
var lastEvent = originalExecutions[originalExecutions.length - 1];
$scope.endTime = lastEvent.data.timestamp + (lastEvent.data.time || 0);
// Filter
$scope.executionTree = [];
originalExecutions.forEach(function(node) {
// Filter by script (if enabled)
if ($scope.selectedScript) {
if (node.data.backtrace && node.data.backtrace.indexOf($scope.selectedScript.fullPath + ':') === -1) {
return;
}
if (node.data.type === "jQuery loaded" || node.data.type === "jQuery version change") {
return;
}
}
$scope.executionTree.push(node);
});
}
function initTimeline() {
// Split the timeline into 200 intervals
var numberOfIntervals = 199;
$scope.timelineIntervalDuration = $scope.endTime / numberOfIntervals;
// Pre-fill array of as many elements as there are milleseconds
var millisecondsArray = Array.apply(null, new Array($scope.endTime + 1)).map(Number.prototype.valueOf,0);
// Create the milliseconds array from the execution tree
$scope.executionTree.forEach(function(node) {
if (node.data.time !== undefined) {
// Ignore artefacts (durations > 100ms)
var time = Math.min(node.data.time, 100) || 1;
for (var i=node.data.timestamp, max=node.data.timestamp + time ; i<max ; i++) {
millisecondsArray[i] |= 1;
}
}
});
// Pre-fill array of 200 elements
$scope.timeline = Array.apply(null, new Array(numberOfIntervals + 1)).map(Number.prototype.valueOf,0);
// Create the timeline from the milliseconds array
millisecondsArray.forEach(function(value, timestamp) {
if (value === 1) {
$scope.timeline[Math.floor(timestamp / $scope.timelineIntervalDuration)] += 1;
}
});
// Get the maximum value of the array (needed for display)
$scope.timelineMax = Math.max.apply(Math, $scope.timeline);
}
function initProfiler() {
$scope.profilerData = $scope.executionTree;
}
$scope.changeScript = function() {
initExecutionTree();
initTimeline();
initProfiler();
};
$scope.findLineIndexByTimestamp = function(timestamp) {
var lineIndex = 0;
for (var i = 0; i < $scope.executionTree.length; i ++) {
var delta = $scope.executionTree[i].data.timestamp - timestamp;
if (delta < $scope.timelineIntervalDuration) {
lineIndex = i;
}
if (delta > 0) {
break;
}
}
return lineIndex;
};
$scope.backToDashboard = function() {
$location.path('/result/' + $scope.runId);
};
$scope.testAgain = function() {
API.relaunchTest($scope.result);
};
loadResults();
}]);
timelineCtrl.directive('scrollOnClick', ['$animate', '$timeout', function($animate, $timeout) {
return {
restrict: 'A',
link: function (scope, element, attributes) {
// When the user clicks on the timeline, find the right profiler line and scroll to it
element.on('click', function() {
var lineIndex = scope.findLineIndexByTimestamp(attributes.scrollOnClick);
var lineElement = angular.element(document.getElementById('line_' + lineIndex));
// Animate the background color to "flash" the row
lineElement.addClass('highlight');
$timeout(function() {
$animate.removeClass(lineElement, 'highlight');
scope.$digest();
}, 50);
window.scrollTo(0, lineElement[0].offsetTop);
});
}
};
}]);
-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,913 +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.windowPerformance) {
switch (type) {
case 'documentScroll':
return '(triggering the scroll event on <b>document</b>)';
case 'windowScroll':
return '(triggering the scroll event on <b>window</b>)';
case 'window.onscroll':
return '(calling the <b>window.onscroll</b> function)';
default:
return '';
}
}
if (!node.data.callDetails) {
return '';
}
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];
case 'jQuery - onDOMReady':
return '(function)';
case 'documentScroll':
return 'The scroll event just triggered on document';
case 'windowScroll':
return 'The scroll event just triggered on window';
case 'window.onscroll':
return 'The window.onscroll function just got called';
default:
return '';
}
}
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] !== undefined) {
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] !== undefined) {
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 (isStringOfObject(args[0])) {
return '<b>' + args[0].replace(/&quot;\(function\)&quot;/g, '(function)') + '</b>';
} else if (args[1] && isPureString(args[1])) {
return 'bind <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[1] + '</b>';
} else {
return 'bind <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
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;
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) + '</div>';
if (parsedBacktrace[i].column) {
html += '<div>' + parsedBacktrace[i].line + ':' + parsedBacktrace[i].column + '</div>';
} else {
html += '<div>line ' + 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) {
// Try the PhantomJS 2 format
withFnResult = /^([^\s\(]+) \((.+:\d+:\d+)\)$/.exec(trace);
}
if (withFnResult === null) {
// Try the PhantomJS 2 ERROR format
withFnResult = /^([^\s\(]+) (http.+:\d+)$/.exec(trace);
}
if (withFnResult === null) {
fileAndLine = trace;
} else {
fnName = withFnResult[1];
fileAndLine = withFnResult[2];
}
// And now the second part
var fileAndLineSplit = /^(.*):(\d+):(\d+)$/.exec(fileAndLine);
if (fileAndLineSplit === null) {
fileAndLineSplit = /^(.*):(\d+)$/.exec(fileAndLine);
}
var filePath = fileAndLineSplit[1];
var line = fileAndLineSplit[2];
var column = fileAndLineSplit[3];
out.push({
fnName: fnName,
filePath: filePath,
line: line,
column: column
});
});
} 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.eventNotDelegated) {
html += '<p class="advice">This binding should use Event Delegation instead of binding each element one by one.</p>';
}
if (node.data.resultsNumber === 0) {
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">' + 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) {
element[0].classList.add('warning');
if (scope.node.queryWithoutResults) {
element[0].classList.add('queryWithoutResults');
}
if (scope.node.jQueryCallOnEmptyObject) {
element[0].classList.add('jQueryCallOnEmptyObject');
}
if (scope.node.eventNotDelegated) {
element[0].classList.add('eventNotDelegated');
}
}
// Bind click on the details icon
var detailsIcon = element[0].querySelector('.details div');
if (detailsIcon) {
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
};
});
offendersDirectives.filter('bytes', function() {
return function(bytes) {
if (isNaN(parseFloat(bytes)) || !isFinite(bytes)) {
return '-';
}
var kilo = bytes / 1024;
if (kilo < 1) {
return bytes + ' bytes';
}
if (kilo < 100) {
return kilo.toFixed(1) + ' KB';
}
if (kilo < 1024) {
return kilo.toFixed(0) + ' KB';
}
var mega = kilo / 1024;
if (mega < 10) {
return mega.toFixed(2) + ' MB';
}
return mega.toFixed(1) + ' MB';
};
});
})();
-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', {
});
}]);
-61
View File
@@ -1,61 +0,0 @@
var apiService = angular.module('apiService', []);
apiService.factory('API', ['$location', 'Runs', 'Results', function($location, Runs, Results) {
return {
launchTest: function(url, settings) {
var runObject = {
url: url,
waitForResponse: false,
screenshot: true,
device: settings.device,
waitForSelector: settings.waitForSelector,
cookie: settings.cookie,
authUser: settings.authUser,
authPass: settings.authPass,
blockDomain: settings.blockDomain,
allowedDomains: settings.allowedDomains,
noExternals: settings.noExternals
};
if (settings.domainsBlackOrWhite === 'black') {
runObject.blockDomain = this.parseDomains(settings.domains);
} else if (settings.domainsBlackOrWhite === 'white') {
var allowedDomains = this.parseDomains(settings.domains);
if (allowedDomains.length > 0) {
runObject.allowDomain = allowedDomains;
} else {
runObject.noExternals = true;
}
}
Runs.save(runObject, function(data) {
$location.path('/queue/' + data.runId);
}, function(response) {
if (response.status === 429) {
alert('Too many requests, you reached the max number of requests allowed in 24h');
} else {
alert('An error occured...');
}
});
},
relaunchTest: function(result) {
this.launchTest(result.params.url, result.params.options);
},
parseDomains: function(textareaContent) {
var lines = textareaContent.split('\n');
function removeEmptyLines (line) {
return line.trim() !== '';
}
// Remove empty lines
return lines.filter(removeEmptyLines).join(',');
}
};
}]);
-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');
}
}
};
}]);
-24
View File
@@ -1,24 +0,0 @@
var settingsService = angular.module('settingsService', []);
settingsService.factory('Settings', ['localStorageService', function(localStorageService) {
return {
getMergedSettings: function() {
var defaultSettings = {
device: 'desktop',
showAdvanced: false
};
var savedValues = localStorageService.get('settings');
return angular.extend(defaultSettings, savedValues);
},
saveSettings: function(settings) {
localStorageService.set('settings', settings);
}
};
}]);
-12
View File
@@ -1,12 +0,0 @@
.about {
margin: 3em auto;
width: 50%;
}
.about p {
margin: 2em;
}
.about a {
color: #FFF;
}
-192
View File
@@ -1,192 +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;
}
}
.screenshotWrapper.phone:hover:after {
top: 1.7em;
left: 0.64em;
}
.screenshotWrapper.tablet:hover:after {
top: 1.5em;
left: 0.9em;
}
}
.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%;
> a {
text-decoration: none;
color: inherit;
}
> a: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;
}
}
File diff suppressed because one or more lines are too long
-202
View File
@@ -1,202 +0,0 @@
.promess {
padding: 0em 2em;
margin-bottom: 0.5em;
font-weight: normal;
font-size: 1.2em;
}
.price {
padding: 0em 2em 3em;
margin-top: 0em;
font-size: 0.9em;
}
.url {
width: 50%;
}
.launchBtn {
background: #ffa319;
color: #fff;
&:focus {
background: #e74c3c;
}
&.disabled {
background: #deaca6;
&:focus {
color: #ddd;
}
}
}
.settings {
width: 50%;
margin: 0 auto;
input, select {
font-size: 1em;
}
input[type=text], input[type=password], textarea {
width: 100%;
min-width: 4em;
}
}
.device {
margin-top: 3em;
.item {
display: inline-block;
margin: 1em 0.75em;
width: 5.5em;
height: 5.5em;
color: #FFF;
border: 1px solid #FFF;
padding: 1px;
border-radius: 0.5em;
cursor: pointer;
text-decoration: none;
font-size: 0.8em;
&.active {
color: #ffa319;
border: 2px solid #ffa319;
padding: 0;
}
&:hover {
color: #ffa319;
}
> div {
margin: 0.2em 0 0.1em;
font-size: 3em;
}
}
}
.settingsTooltip {
position: relative;
span {
font-size: 0.8em;
vertical-align: text-top;
}
div {
display: none;
position: absolute;
padding: 0.5em;
width: 25em;
background: #FFF;
color: #000;
font-size: 0.8em;
border-radius: 1em;
border: 2px solid #ffa319;
white-space: normal;
z-index: 2;
}
&:hover div {
display: block;
}
}
.showAdvanced {
display: inline-block;
margin-top: 2em;
color: #FFF;
text-decoration: none;
font-size: 0.9em;
&:hover {
color: #ffa319;
}
}
.currentSettings {
font-size: 0.9em;
span {
color: #ffa319;
&:after {
color: #FFF;
content: ",";
}
&:last-child:after {
content: "";
}
}
}
.advanced {
margin: 1em 0 0;
display: table;
width: 100%;
text-align: left;
border-spacing: 0.75em;
> div {
display: table-row;
> div {
display: table-cell;
width: 75%;
&.label {
width: 25%;
white-space: nowrap;
vertical-align: middle;
}
}
}
.subTable {
display: table;
border-spacing: 0;
width: 100%;
> div {
display: table-row;
> div {
display: table-cell;
padding: 0 0 0.75em;
}
}
}
}
.features {
display: table;
width: 50%;
margin: 6em 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;
}
-257
View File
@@ -1,257 +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 {
display: inline-block;
position: relative;
background: #000;
> div {
position: relative;
overflow: hidden;
&:hover {
overflow: auto;
}
}
.screenshotImage {
width: 100%;
}
.screenshotError {
color: #fff;
}
}
.screenshotWrapper.desktop {
border: 0.2em solid #AAA;
padding: 0.5em;
border-top-left-radius: 0.4em;
border-top-right-radius: 0.4em;
&: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;
}
}
.screenshotWrapper.phone {
border: 0.07em solid #CCC;
padding: 1em 0.3em 1.5em;
border-radius: 0.6em;
&:before {
position: absolute;
width: 0.8em;
height: 0.8em;
bottom: 0.3em;
left: 3.3em;
border: 0.07em solid #CCC;
border-radius: 0.5em;
content: " ";
}
&:after {
position: absolute;
width: 1em;
height: 0.1em;
bottom: 13.9em;
left: 3.2em;
background: #555;
border-radius: 0.05em;
content: " ";
}
> div {
width: 6.75em;
height: 12em;
}
}
.screenshotWrapper.tablet {
border: 0.07em solid #CCC;
padding: 0.8em 0.5em 0.9em;
border-radius: 0.6em;
&:before {
position: absolute;
width: 0.5em;
height: 0.5em;
bottom: 0.15em;
left: 4.35em;
border: 0.07em solid #CCC;
border-radius: 0.4em;
content: " ";
}
> div {
width: 8.25em;
height: 11em;
}
}
.footer {
padding: 3em;
color: #413;
a {
color: inherit;
}
.version {
font-size: 0.7em;
}
.github {
margin: 1em 0 0 0.5em;
}
}
[class^="icon-"]:before, [class*=" icon-"]:before {
vertical-align: baseline;
}
-9
View File
@@ -1,9 +0,0 @@
.status {
margin-top: 2em;
font-size: 2.5em;
}
.statusSubMessage {
font-size: 0.8em;
margin-bottom: 6em;
}
-297
View File
@@ -1,297 +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 h3 {
margin-bottom: 0em;
}
.rule .okThreshold {
font-style: italic;
font-size: 0.9em;
}
.rule .message {
width: 80%;
margin: 1.5em 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;
}
}
.rule .startTime {
display: none;
}
.offendersTable, .value {
.offenderButton {
display: inline-block;
position: relative;
background: #efe;
padding: 0 0.5em;
margin: 0.2em 0;
border-radius: 0.4em;
z-index: 1;
cursor: pointer;
&.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;
background: #ffe0cc;
}
}
}
.smallerOffenders {
font-size: 0.9em;
}
}
.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;
}
}
}
.checker {
/* Checkerboard background */
background-color: #ddd;
background-image: linear-gradient(45deg, #AAA 25%, transparent 25%, transparent 75%, #AAA 75%, #AAA), linear-gradient(45deg, #AAA 25%, transparent 25%, transparent 75%, #AAA 75%, #AAA);
background-size:1em 1em;
background-position:0 0, 0.5em 0.5em;
}
.colorPalette {
width: 30em;
border: 2px solid #000;
text-align: left;
> div {
display: inline-block;
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;
left: -0.2em;
top: -0.2em;
width: 100%;
height: 100%;
z-index: 2;
border: 0.2em solid #f1c40f;
}
}
}
.similarColors {
margin: 1em;
width: 20em;
height: 6em;
> div {
display: inline-block;
width: 10em;
height: 3.5em;
padding-top: 2.5em;
}
}
.totalWeightPie {
max-width: 39em;
margin: 2em auto 4em;
canvas {
max-width: inherit;
}
}
.hugeFile {
font-weight: bold;
color: #e74c3c;
}
.imageOffenders {
display: table;
border-spacing: 3em;
width: 90%;
> div {
display: table-row;
> div {
display: table-cell;
vertical-align: middle;
}
}
img {
max-height: 10em;
max-width: 40em;
border: 1px solid #000;
margin-top: 0.5em;
}
}
.smallPreview {
display: block;
max-height: 4em;
max-width: 8em;
border: 1px solid #000;
margin: 1em auto 0.2em;
}
-8
View File
@@ -1,8 +0,0 @@
.screenshot.board {
text-align: center;
}
.screenshot .screenshotWrapper {
font-size: 2.08333333333333em;
margin-bottom: 0.5em;
}
-387
View File
@@ -1,387 +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;
}
.subFilters {
margin-left: 3em;
font-size: 0.9em;
}
.table {
display: table;
width: 100%;
border-spacing: 0.25em;
}
.table > div,
.table > a {
display: table-row;
}
.table > .headers > div {
font-weight: bold;
padding: 0.5em 1em;
}
.table > div > div,
.table > a > 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;
}
.queryWithoutResultsFilterOn {
> div {
display: none;
&.queryWithoutResults {
display: table-row;
}
}
}
.jQueryCallOnEmptyObjectFilterOn {
> div {
display: none;
&.jQueryCallOnEmptyObject {
display: table-row;
}
}
}
.eventNotDelegatedFilterOn {
> div {
display: none;
&.eventNotDelegated {
display: table-row;
}
}
}
.jsErrorFilterOn {
> div {
display: none;
&.jsError {
display: table-row;
}
}
}
-70
View File
@@ -1,70 +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">
<link rel="stylesheet" type="text/css" href="/node_modules/angular-chart.js/dist/angular-chart.css">
<!-- endbuild -->
<link rel="preconnect" href="//www.google-analytics.com">
<link rel="dns-prefetch" href="//www.google-analytics.com">
<link rel="preconnect" href="//ghbtns.com">
<link rel="dns-prefetch" href="//ghbtns.com">
<link rel="preconnect" href="https://api.github.com">
<link rel="dns-prefetch" href="https://api.github.com">
<head>
<body ng-app="YellowLabTools">
<div id="header"><h1>Yellow Lab <span class="icon-lab"></span> Tools</h1></div>
<div id="body" ng-view autoscroll="true"></div>
<div class="footer">
<span class="version">@@version</span>
<br><a href="/about">More about Yellow Lab Tools</a><br>
<div class="github"><iframe id="ghbtn" frameborder="0" scrolling="0" width="145px" height="30px"></iframe></div>
</div>
<!-- build:js /js/all.js -->
<script src="/node_modules/angular/angular.min.js"></script>
<script src="/node_modules/chart.js/Chart.min.js"></script>
<script src="/node_modules/angular-route/angular-route.min.js"></script>
<script src="/node_modules/angular-resource/angular-resource.min.js"></script>
<script src="/node_modules/angular-sanitize/angular-sanitize.min.js"></script>
<script src="/node_modules/angular-animate/angular-animate.min.js"></script>
<script src="/node_modules/angular-local-storage/dist/angular-local-storage.min.js"></script>
<script src="/node_modules/angular-chart.js/dist/angular-chart.min.js"></script>
<script src="/js/app.js"></script>
<script src="/js/controllers/indexCtrl.js"></script>
<script src="/js/controllers/dashboardCtrl.js"></script>
<script src="/js/controllers/queueCtrl.js"></script>
<script src="/js/controllers/ruleCtrl.js"></script>
<script src="/js/controllers/screenshotCtrl.js"></script>
<script src="/js/controllers/timelineCtrl.js"></script>
<script src="/js/models/resultsFactory.js"></script>
<script src="/js/models/runsFactory.js"></script>
<script src="/js/services/apiService.js"></script>
<script src="/js/services/menuService.js"></script>
<script src="/js/services/settingsService.js"></script>
<script src="/js/directives/gradeDirective.js"></script>
<script src="/js/directives/offendersDirectives.js"></script>
<!-- endbuild -->
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','//www.google-analytics.com/analytics.js','ga');if('@@googleAnalyticsId'.indexOf('UA-')===0){ga('create','@@googleAnalyticsId','auto');}
</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>
-79
View File
@@ -1,79 +0,0 @@
<div ng-include="'views/resultSubHeader.html'"></div>
<div class="summary board">
<div ng-if="result.blockedRequests">
<b><ng-pluralize count="result.blockedRequests.length" when="{'0': 'No blocked request', 'one': '1 blocked request', 'other': '{} blocked requests'}"></ng-pluralize>:</b>
<div ng-repeat="request in result.blockedRequests">
{{request}}
</div>
</div>
<div class="globalScore" ng-if="globalScore === 0 || globalScore > 0">
<div>
<h2>Global score</h2>
<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" ng-class="result.params.options.device || 'desktop'">
<div>
<img ng-if="result.screenshotUrl" class="screenshotImage" ng-src="{{result.screenshotUrl}}"/>
<span ng-if="!result.screenshotUrl" class="screenshotError">Screenshot not available</span>
</div>
</div>
</a>
</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">
<a ng-repeat="ruleName in category.rules" ng-if="result.rules[ruleName]" ng-init="rule = result.rules[ruleName]"
ng-class="{'warning': rule.abnormal}" href="/result/{{runId}}/rule/{{ruleName}}">
<div class="grade">
<grade score="rule.score"></grade>
</div>
<div class="label">{{rule.policy.label}}</div>
<div class="result">
<span ng-if="rule.policy.unit == 'bytes'">{{rule.value | bytes}}</span>
<span ng-if="rule.policy.unit != 'bytes'">{{rule.value}}</span>
<span ng-if="rule.abnormal" class="icon-warning"></span>
<span ng-if="rule.abnormalityScore <= -100" class="icon-warning"></span>
<span ng-if="rule.abnormalityScore <= -300" class="icon-warning"></span>
</div>
<div class="info"><span class="icon-question"></span></div>
</a>
</div>
</div>
</div>
</div>
<div ng-if="!error && !fromSocialShare" class="apiTip">
<b>Did you know?</b> You can <b>bookmark</b> this result page URL or <b>share it</b>, Yellow Lab Tools keeps test results for years!
</div>
<div class="tweet" ng-if="!error && !fromSocialShare">
<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>
-100
View File
@@ -1,100 +0,0 @@
<h2 class="promess">Online test to help speeding up <b>heavy</b> web pages</h2>
<p class="price">Free and open source!</p>
<form ng-submit="launchTest()" >
<input type="text" name="url" ng-model="url" placeholder="http://www.mysite.com" class="url" />
<input type="submit" value="Launch test" class="launchBtn" ng-class="{disabled: !url}" />
<div class="settings">
<div class="device">
<div>Choose the simulated device:</div>
<a href="" class="item" ng-class="{active: settings.device == 'desktop'}" ng-click="settings.device = 'desktop'"><div class="icon-screen"></div>Desktop</a>
<a href="" class="item" ng-class="{active: settings.device == 'tablet'}" ng-click="settings.device = 'tablet'"><div class="icon-tablet"></div>Tablet</a>
<a href="" class="item" ng-class="{active: settings.device == 'phone'}" ng-click="settings.device = 'phone'"><div class="icon-mobile"></div>Phone</a>
</div>
[ <a href="" class="showAdvanced" ng-click="settings.showAdvanced = !settings.showAdvanced">
<span ng-if="!settings.showAdvanced">Advanced settings &nbsp;</span>
<span ng-if="settings.showAdvanced">Hide advanced settings &nbsp;</span>
</a> ]
<span class="currentSettings" ng-if="!settings.showAdvanced && (settings.waitForSelector || settings.cookie || settings.authUser || settings.authPass)">
Currently set:
<span ng-if="settings.waitForSelector">wait for selector</span>
<span ng-if="settings.cookie">cookie</span>
<span ng-if="settings.authUser || settings.authPass">authentication</span>
</span>
<div class="advanced" ng-show="settings.showAdvanced">
<!--<div>
<div class="label">
Wait selector
<span class="settingsTooltip">
<span class="icon-question"></span>
<div><b>Wait for a CSS selector</b><br><br>Once the page is considered loaded, PhantomJS will repeatedly try to match the given CSS selector until it is found in the page. A 60 seconds timeout still applies anyway.<br><br>Example: "body.loaded"</div>
</span>
</div>
<div><input type="text" name="waitForSelector" ng-model="settings.waitForSelector" /></div>
</div>-->
<div>
<div class="label">
Cookie
<span class="settingsTooltip">
<span class="icon-question"></span>
<div><b>Cookie</b><br><br>Adds a cookie on the main domain.<br><br>Example: "bar=foo;domain=url"</div>
</span>
</div>
<div><input type="text" name="cookie" ng-model="settings.cookie" /></div>
</div>
<div>
<div class="label">
Authent
<span class="settingsTooltip">
<span class="icon-question"></span>
<div><b>Basic HTTP authentication</b><br><br>Enter your credentials here if you need to bypass a basic authentication.<br><br><i>PS: if your authentication is not basic, you might be able to copy the session cookie from your browser, paste it in the "Cookie" setting and launch a run before your cookie expires.</i></div>
</span>
</div>
<div class="subTable">
<div>
<div>username</div>
<div><input type="text" class="authField" name="authUser" ng-model="settings.authUser" /></div>
</div>
<div>
<div><span>password</div>
<div><input type="password" class="authField" name="authPass" ng-model="settings.authPass" /></div>
</div>
</div>
</div>
<div>
<div class="label">
Block domains
<span class="settingsTooltip">
<span class="icon-question"></span>
<div><b>Block some domains</b><br><br>One line per domain or subdomain.<br><br><i><b>Example:</b><br>google-analytics.com<br>ads.yahoo.com<br>ajax.googleapis.com</i><br><br>An empty whitelist will block all domains except the main domain.</div>
</span>
</div>
<div>
<div>
<input type="radio" name="blackOrWhite" ng-model="settings.domainsBlackOrWhite" value="black" />blacklist
<input type="radio" name="blackOrWhite" ng-model="settings.domainsBlackOrWhite" value="white" />whitelist
</div>
<textarea name="domains" ng-model="settings.domains" rows="5"></textarea>
</div>
</div>
</div>
</div>
</form>
<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-arrow-left3"></div><span>New test<span></a>
<a class="menuItem restart" href="" ng-click="testAgain()"><div class="icon-loop"></div><span>Test again<span></a>
<div class="menuItem" ng-class="{active: Menu.getCurrentPage() == 'dashboard'}" ng-click="Menu.changePage('dashboard')"><div class="icon-list"></div><span>Dashboard</span></div>
<div class="menuItem" ng-class="{active: Menu.getCurrentPage() == 'timeline'}" ng-click="Menu.changePage('timeline')"><div class="icon-bars"></div><span>JS Timeline</span></div>
</div>
-396
View File
@@ -1,396 +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:
<span ng-if="rule.policy.unit == 'bytes'">{{rule.value | bytes}}</span>
<span ng-if="rule.policy.unit != 'bytes'">{{rule.value}}</span>
</h3>
<div class="okThreshold" ng-if="rule.score < 100 && rule.policy.isOkThreshold !== undefined">
Have
<span ng-if="rule.policy.unit == 'bytes'">{{rule.policy.isOkThreshold | bytes}}</span>
<span ng-if="rule.policy.unit != 'bytes'">{{rule.policy.isOkThreshold}}</span>
<span ng-if="rule.policy.isOkThreshold > 0 && rule.policy.isOkThreshold < rule.policy.isBadThreshold">or less</span>
<span ng-if="rule.policy.isOkThreshold > rule.policy.isBadThreshold">or more</span>
to get the 100/100 score.
</div>
<div ng-bind-html="rule.policy.message" class="message"></div>
</div>
</div>
<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 === '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 === 'eventsScrollBound'">
<span ng-if="offender.target == 'window'">Scroll event bound on <b>window</b></span>
<span ng-if="offender.target == '#document'">Scroll event bound on <b>document</b></span>
<span ng-if="offender.target == 'window.onscroll'"><b>window.onscroll</b> function declared</span>
<div class="offenderButton" ng-if="offender.backtrace.length == 0">no backtrace</div>
<div class="offenderButton opens" ng-if="offender.backtrace.length > 0">
backtrace
<div class="backtrace">
<div ng-repeat="obj in offender.backtrace track by $index">
<span ng-if="obj.functionName">{{obj.functionName}}()</span>
<url-link url="obj.file" max-length="60"></url-link>
line {{obj.line}}
</div>
</div>
</div>
</div>
<div ng-if="policyName === 'jsErrors'">
<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 === 'jQueryFunctionsUsed'">
function <b>{{offender.functionName}}</b> used {{offender.count}} times
</div>
<div ng-if="policyName === 'documentWriteCalls'">
<b>{{offender.writeFn}}</b>
<span ng-if="offender.from">
called from
<span ng-if="offender.from.functionName">{{offender.from.functionName}}()</span>
<url-link url="offender.from.file" max-length="50"></url-link>
line {{offender.from.line}}
</span>
<span ng-if="!offender.from">
called from (no backtrace available)
</span>
</div>
<div ng-if="policyName === 'cssRules'">
<span ng-if="offender.file === 'inline CSS'">inline CSS</span>
<span ng-if="offender.file !== 'inline CSS'"><url-link url="offender.file" max-length="80"></url-link></span>
: <ng-pluralize count="offender.rules" when="{'0': '0 rule', 'one':'1 rule','other':'{} rules'}"></ng-pluralize>
</div>
<div ng-if="policyName === 'similarColors'">
<div class="similarColors checker"><div ng-style="{'background-color': offender.color1, 'color': offender.isDark ? '#FFF' : '#000'}">{{offender.color1}}</div><div ng-style="{'background-color': offender.color2, 'color': offender.isDark ? '#FFF' : '#000'}">{{offender.color2}}</div></div>
</div>
<div ng-if="policyName === 'cssMobileFirst'">
<b>{{offender.query}}</b> for <ng-pluralize count="offender.rules" when="{'one':'1 rule','other':'{} rules'}"></ng-pluralize>
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
</div>
<div ng-if="policyName === 'cssParsingErrors'">
<b>{{offender.error}}</b>
<file-and-line file="offender.file" line="offender.line" column="offender.column"></file-and-line>
<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'">
<b>{{offender.property}} {{offender.message}}</b>
<div ng-if="offender.rules.length" ng-click="offender.showMore = !offender.showMore" class="offenderButton">
<span ng-if="!offender.showMore">show</span>
<span ng-if="offender.showMore">hide</span>
<ng-pluralize count="offender.rules.length" when="{'one':'1 offender','other':'{} offenders'}"></ng-pluralize>
</div>
<div ng-if="offender.showMore" class="smallerOffenders">
<div ng-repeat="cssRule in offender.rules">
{{cssRule.rule}} {{'{' + offender.property}}: {{cssRule.value + '}' }}
<file-and-line-button file="cssRule.file" line="cssRule.line" column="cssRule.column"></file-and-line-button>
</div>
</div>
</div>
<div ng-if="policyName === 'lazyLoadableImagesBelowTheFold' || policyName === 'hiddenImages'">
<img ng-src="{{offender}}" class="smallPreview checker"></img>
<url-link url="offender" max-length="100"></url-link>
</div>
<div ng-if="policyName === 'notFound' || policyName === 'closedConnections' || policyName === 'multipleRequests' || policyName === 'cachingDisabled' || policyName === 'cachingNotSpecified'">
<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 === 'DOMaccessesOnScroll' && rule.offendersObj.children.length > 0">
<p>The table below shows the interactions between the JavaScript and the DOM on a scroll event.</p>
<div class="table" ng-class="{warningsFilterOn: warningsFilterOn}">
<div class="headers">
<div><!-- index --></div>
<div>Type</div>
<div>Params</div>
<div><!-- details --></div>
</div>
<profiler-line ng-repeat="node in rule.offendersObj.children" data-index="$index" node="node"></profiler-line>
</div>
</div>
<div ng-if="policyName === 'cssColors' && rule.offendersObj.count > 0">
<p>This is the colors palette, sized by total occurrences:</p>
<div class="colorPalette checker">
<div ng-repeat="offender in rule.offendersObj.palette" style="background-color: {{offender.color}}; width: {{offender.occurrences * 100 / rule.offendersObj.palette[0].occurrences}}%"><div>{{offender.color}} ({{offender.occurrences}} times)</div></div>
</div>
</div>
</div>
</div>
<div ng-if="policyName === 'totalWeight'">
<h3>Weight by MIME type</h3>
<div class="totalWeightPie">
<canvas class="chart chart-doughnut" data="weightData" labels="weightLabels" options="weightOptions" colours="weightColours" legend="true"></canvas>
</div>
<div ng-repeat="type in weightLabels">
<h3>{{rule.offendersObj.list.byType[type].totalWeight | bytes}} of {{type}}</h3>
<div class="offendersTable">
<div ng-repeat="request in rule.offendersObj.list.byType[type].requests | orderBy:'-weight'" ng-if="request.weight > 0">
<div><url-link url="request.url" max-length="60"></url-link></div>
<div ng-class="{hugeFile: request.weight > 102400}">{{request.weight | bytes}}</div>
</div>
</div>
</div>
</div>
<div ng-if="policyName === 'imageOptimization'">
<h3 ng-if="rule.value > 0">{{rule.value | bytes}} could be saved on <ng-pluralize count="rule.offendersObj.list.images.length" when="{'one': '1 image', 'other': '{} images'}"></ng-pluralize></h3>
<div class="imageOffenders">
<div ng-repeat="image in rule.offendersObj.list.images | orderBy:'-gain'">
<div>
Current file: <url-link url="image.url" max-length="50"></url-link>
<div><a href="{{image.url}}" target="_blank"><img ng-src="{{image.url}}" class="checker" /></a></div>
</div>
<div>
<p ng-if="!image.afterCompression">Current weight: {{image.original | bytes}}</p>
<p ng-if="image.afterCompression">Current weight: {{image.original | bytes}} ({{image.afterCompression | bytes}} gzipped)</p>
<p ng-if="image.lossless && image.afterOptimizationAndCompression">With a lossless optimization:<br/>{{image.afterOptimizationAndCompression | bytes}} gzipped (<b>-{{image.gain | bytes}}</b> gzipped)</p>
<p ng-if="image.lossless && !image.afterOptimizationAndCompression">With a lossless optimization:<br/>{{image.lossless | bytes}} <span ng-if="!image.lossy">(<b>-{{image.gain | bytes}}</b>)</span></p>
<p ng-if="image.lossy && image.afterOptimizationAndCompression">With a lossy optimization:<br/>{{image.afterOptimizationAndCompression | bytes}} gzipped (<b>-{{image.gain | bytes}} gzipped</b>)</p>
<p ng-if="image.lossy && !image.afterOptimizationAndCompression">With a lossy optimization:<br/>{{image.lossy | bytes}} (<b>-{{image.gain | bytes}}</b>)</p>
</div>
</div>
</div>
</div>
<div ng-if="policyName === 'gzipCompression'">
<h3 ng-if="rule.value > 0">{{rule.value | bytes}} could be saved on <ng-pluralize count="rule.offendersObj.list.images.length" when="{'one': '1 file', 'other': '{} files'}"></ng-pluralize></h3>
<div class="table">
<div class="headers">
<div>File</div>
<div>Current weight</div>
<div>Gzipped</div>
<div>Gain</div>
</div>
<div ng-repeat="file in rule.offendersObj.list.files | orderBy:'-gain'">
<div>
<url-link url="file.url" max-length="70"></url-link>
</div>
<div>{{file.original | bytes}}</div>
<div>{{file.gzipped | bytes}}</div>
<div><b>-{{file.gain | bytes}}</b></div>
</div>
</div>
</div>
<div ng-if="policyName === 'fileMinification'">
<h3 ng-if="rule.value > 0">{{rule.value | bytes}} could be saved on <ng-pluralize count="rule.offendersObj.list.files.length" when="{'one': '1 file', 'other': '{} files'}"></ng-pluralize></h3>
<div class="table">
<div class="headers">
<div>File</div>
<div>Current weight</div>
<div>Minified</div>
<div>Gain</div>
</div>
<div ng-repeat="file in rule.offendersObj.list.files | orderBy:'-gain'">
<div>
<url-link url="file.url" max-length="70"></url-link>
</div>
<div ng-if="!file.afterCompression">{{file.original | bytes}} (gzipped)</div>
<div ng-if="file.afterCompression">{{file.original | bytes}} ({{file.afterCompression | bytes}} gzipped)</div>
<div ng-if="!file.afterCompression">{{file.afterOptimizationAndCompression | bytes}} (gzipped)</div>
<div ng-if="file.afterCompression">{{file.optimized | bytes}} ({{file.afterOptimizationAndCompression | bytes}} gzipped)</div>
<div><b>-{{file.gain | bytes}}</b></div>
</div>
</div>
</div>
<div ng-if="policyName === 'totalRequests'">
<h3>Requests by MIME type</h3>
<div ng-repeat="(type, requests) in rule.offendersObj.list.byType">
<h3><ng-pluralize count="requests.length" when="{'0': 'No ' + type + ' request', 'one': '1 ' + type + ' request', 'other': '{} ' + type + ' requests'}"></ng-pluralize></h3>
<p ng-if="type == 'css' && requests.length > 2">Reduce the number of stylesheets by concatenating them.</p>
<p ng-if="type == 'js' && requests.length > 3">Reduce the number of scripts by concatenating them.</p>
<p ng-if="type == 'image' && requests.length > 5">Reduce the number of images by lazyloading them or by spriting them.</p>
<p ng-if="type == 'webfont' && requests.length > 1">Fonts are generally loaded on the critical path of the head. Load as few as possible.</p>
<p ng-if="type == 'other' && requests.length > 0">They can be Flash, XML, music or any undetected format.</p>
<div class="offendersTable">
<div ng-repeat="request in requests track by $index">
<div><url-link url="request" max-length="100"></url-link></div>
</div>
</div>
</div>
</div>
<div ng-if="policyName === 'smallRequests'">
<div ng-repeat="(type, requests) in rule.offendersObj.list.byType">
<h3><ng-pluralize count="requests.length" when="{'0': 'small ' + type + ' file', 'one': '1 small ' + type + ' file', 'other': '{} small ' + type + ' files'}"></ng-pluralize></h3>
<p ng-if="type == 'css' && requests.length > 0">Try to inline these styles in the head of the HTML or to merge them with other files.</p>
<p ng-if="type == 'js' && requests.length > 0">Try to inline these scripts in the HTML or merge them with other files.</p>
<p ng-if="type == 'image' && requests.length > 0">Try to inline these images (with base64 encoding for most image types except SVG that don't need base64 encoding). You can also create sprites.</p>
<div class="table">
<div class="headers">
<div ng-if="type == 'image'">Preview</div>
<div>File</div>
<div>Weight (bytes)</div>
</div>
<div ng-repeat="request in requests track by $index">
<div ng-if="type == 'image'"><img ng-src="{{request.url}}" class="smallPreview checker" /></div>
<div><url-link url="request.url" max-length="100"></url-link></div>
<div>{{request.size}}</div>
</div>
</div>
</div>
</div>
<div ng-if="policyName === 'DOMaccesses'">
<h3>{{rule.value}} offenders</h3>
Please open the <a href="/result/{{runId}}/timeline">JS timeline</a>
</div>
<div ng-if="policyName === 'queriesWithoutResults'">
<h3>{{rule.value}} offenders</h3>
Please open the <a href="/result/{{runId}}/timeline#filter=queryWithoutResults">JS timeline, filtered by "Queries without results"</a>
</div>
<div ng-if="policyName === 'jQueryCallsOnEmptyObject'">
<h3>{{rule.value}} offenders</h3>
Please open the <a href="/result/{{runId}}/timeline#filter=jQueryCallOnEmptyObject">JS timeline, filtered by "jQuery calls on empty object"</a>
</div>
<div ng-if="policyName === 'jQueryNotDelegatedEvents'">
<h3>{{rule.value}} offenders</h3>
Please open the <a href="/result/{{runId}}/timeline#filter=eventNotDelegated">JS timeline, filtered by "Events not delegated"</a>
</div>
<div ng-if="policyName === 'cssBreakpoints'">
<div ng-if="rule.value > 0" class="cssBreakpointsGraph">
<h3>Breakpoints distribution graph</h3>
<canvas class="chart chart-line" chart-data="breakpointsData" chart-labels="breakpointsLabels" chart-options="breakpointsOptions" chart-legend="true" chart-colours="breakpointsColours" chart-series="breakpointsSeries" width="600" height="250"></canvas>
<h3>Breakpoints list</h3>
<div class="offendersTable">
<div ng-repeat="offender in rule.offendersObj | orderBy:'pixels'">
<div>Breakpoint <b>{{offender.breakpoint}}</b> involves <ng-pluralize count="offender.count" when="{'one': '1 rule', 'other': '{} rules'}"></ng-pluralize></div>
</div>
</div>
</div>
<div ng-if="rule.value === 0">
No breakpoint
</div>
</div>
<div ng-if="policyName === 'http2'">
<h3>Protocols advertised by the server</h3>
<div class="offendersTable">
<div ng-repeat="protocol in rule.offendersObj.list">
<div>{{protocol}}</div>
</div>
<div ng-if="!rule.offendersObj || rule.offendersObj.count == 0">
<div>none</div>
</div>
</div>
</div>
<div ng-if="!rule && rule !== null" class="notFound">
<h2>404</h2>
Rule "{{policyName}}"" not found
</div>
<div class="backToDashboard"><a href="#" ng-click="backToDashboard()">Back to dashboard</a></div>
</div>
-15
View File
@@ -1,15 +0,0 @@
<div ng-include="'views/resultSubHeader.html'"></div>
<div class="screenshot board">
<h2>Screenshot</h2>
<div class="screenshotWrapper" ng-class="result.params.options.device || 'desktop'">
<div>
<img ng-if="result.screenshotUrl" class="screenshotImage" ng-src="{{result.screenshotUrl}}"/>
<span ng-if="!result.screenshotUrl" class="screenshotError">Screenshot not available</span>
</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>
-100
View File
@@ -1,100 +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.javascriptExecutionTree.data.domInteractive,
domInteractive: $index * timelineIntervalDuration >= result.javascriptExecutionTree.data.domInteractive
&& $index * timelineIntervalDuration < result.javascriptExecutionTree.data.domContentLoaded,
domContentLoaded: $index * timelineIntervalDuration >= result.javascriptExecutionTree.data.domContentLoaded
&& $index * timelineIntervalDuration < result.javascriptExecutionTree.data.domContentLoadedEnd,
domContentLoadedEnd: $index * timelineIntervalDuration >= result.javascriptExecutionTree.data.domContentLoadedEnd
&& $index * timelineIntervalDuration < result.javascriptExecutionTree.data.domComplete,
domComplete: $index * timelineIntervalDuration >= result.javascriptExecutionTree.data.domComplete
}">
<div style="height: {{100 * duration / timelineMax | number: 0}}px" class="color" ng-class="{clickable: duration > 0}" scroll-on-click="{{$index * timelineIntervalDuration}}"></div>
<div class="tooltip detailsOverlay">
<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 the JavaScript and the DOM. It is useful to understand what happens while the page loads.
</p>
<div class="filters">
<div>
<input type="checkbox" name="warningsFilter" ng-model="warningsFilterOn" id="warningsFilterOn" />
<label for="warningsFilterOn">Filter on warnings and errors</label>
<div class="subFilters" ng-if="warningsFilterOn">
<div>
<input type="checkbox" name="filters" ng-model="warningsFilters.queryWithoutResults" id="queryWithoutResultsFilterOn"/>
<label for="queryWithoutResultsFilterOn">Queries without results</label>
</div>
<div>
<input type="checkbox" name="filters" ng-model="warningsFilters.jQueryCallOnEmptyObject" id="jQueryCallOnEmptyObjectFilterOn" />
<label for="jQueryCallOnEmptyObjectFilterOn">jQuery calls on empty object</label>
</div>
<div>
<input type="checkbox" name="filters" ng-model="warningsFilters.eventNotDelegated" id="eventNotDelegatedFilterOn" />
<label for="eventNotDelegatedFilterOn">Events not delegated</label>
</div>
<div>
<input type="checkbox" name="filters" ng-model="warningsFilters.jsError" id="jsErrorFilterOn" />
<label for="jsErrorFilterOn">Errors</label>
</div>
</div>
</div>
{{queryWithoutResultsFilterOn}}
</div>
<div class="table" ng-class="{queryWithoutResultsFilterOn: warningsFilterOn && warningsFilters.queryWithoutResults, jQueryCallOnEmptyObjectFilterOn: warningsFilterOn && warningsFilters.jQueryCallOnEmptyObject, eventNotDelegatedFilterOn: warningsFilterOn && warningsFilters.eventNotDelegated, jsErrorFilterOn: warningsFilterOn && warningsFilters.jsError}">
<div class="headers">
<div><!-- index --></div>
<div>Type</div>
<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;
+388 -567
View File
File diff suppressed because it is too large Load Diff
+36 -39
View File
@@ -5,18 +5,19 @@
"policies": {
"totalWeight": 5,
"imageOptimization": 2,
"gzipCompression": 2,
"fileMinification": 1
"imagesTooLarge": 1,
"compression": 2,
"fileMinification": 2
}
},
"requests": {
"label": "Requests",
"policies": {
"totalRequests": 5,
"totalRequests": 2,
"domains": 3,
"notFound": 3,
"multipleRequests": 2,
"smallRequests": 1,
"notFound": 2,
"identicalFiles": 2,
"emptyRequests": 3,
"lazyLoadableImagesBelowTheFold": 2,
"hiddenImages": 1
}
@@ -30,27 +31,20 @@
"DOMidDuplicated": 1
}
},
"domManipulations": {
"label": "DOM manipulations",
"javascriptComplexity": {
"label": "JS complexity",
"policies": {
"DOMaccesses": 3,
"queriesWithoutResults": 1,
"DOMqueriesAvoidable": 1
}
},
"scroll": {
"label": "Scroll bottlenecks",
"policies": {
"eventsScrollBound": 1,
"DOMaccessesOnScroll": 4
"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
}
},
@@ -58,10 +52,7 @@
"label": "jQuery",
"policies": {
"jQueryVersion": 2,
"jQueryVersionsLoaded": 2,
"jQueryFunctionsUsed": 1,
"jQueryCallsOnEmptyObject": 1,
"jQueryNotDelegatedEvents": 1
"jQueryVersionsLoaded": 2
}
},
"cssComplexity": {
@@ -69,34 +60,41 @@
"policies": {
"cssRules": 2,
"cssComplexSelectors": 2,
"cssComplexSelectorsByAttribute": 1.5,
"cssColors": 0.5,
"similarColors": 0.5,
"cssBreakpoints": 1,
"cssBreakpoints": 0.5,
"cssMobileFirst": 1
}
},
"badCSS": {
"label": "Bad CSS",
"policies": {
"cssParsingErrors": 4,
"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
}
},
"fonts": {
"label": "Web fonts",
"policies": {
"fontsCount": 1,
"heavyFonts": 0.5,
"unusedUnicodeRanges": 0.5,
"nonWoff2Fonts": 0.5
}
},
"serverConfig": {
"label": "Server config",
"policies": {
"http2": 2,
"oldHttpProtocol": 2,
"oldTlsProtocol": 2,
"closedConnections": 2,
"cachingNotSpecified": 1,
"cachingDisabled": 1,
@@ -106,15 +104,14 @@
},
"globalScore": {
"pageWeight": 3,
"requests": 3,
"requests": 2,
"domComplexity": 2,
"domManipulations": 2,
"scroll": 1,
"badJavascript": 1,
"jQuery": 1,
"cssSyntaxError": 1,
"cssComplexity": 1,
"javascriptComplexity": 2,
"badJavascript": 2,
"jQuery": 0.5,
"cssComplexity": 0.5,
"badCSS": 1,
"fonts": 1,
"serverConfig": 1
}
}
+44 -22
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,23 +153,15 @@ var OffendersHelpers = function() {
};
this.cssOffenderPattern = function(offender) {
// Remove any line breaks
offender = offender.replace(/(\r\n|\n|\r)/gm, '');
var parts = /^(.*) (?:<([^ \(]*)>|\[inline CSS\]) @ (\d+):(\d+)$/.exec(offender);
if (!parts) {
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) {
@@ -185,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;
+67 -16
View File
@@ -2,11 +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 isHttp2 = require('./tools/isHttp2');
var weightChecker = require('./tools/weightChecker/weightChecker');
var redownload = require('./tools/redownload/redownload');
var rulesChecker = require('./rulesChecker');
var scoreCalculator = require('./scoreCalculator');
@@ -25,11 +24,33 @@ var Runner = function(params) {
// Execute Phantomas first
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);
// Compare colors
data = colorDiff.compareAllColors(data);
@@ -38,16 +59,30 @@ var Runner = function(params) {
data = mediaQueriesChecker.analyzeMediaQueries(data);
// Redownload every file
return weightChecker.recheckAllFiles(data);
return redownload.recheckAllFiles(data)
.progress(function(redownloadedProgress) {
deferred.notify({
estimatedProgress: 0.4 + redownloadedProgress * 0.55,
milestone: 'phantomas'
});
});
})
.then(function(data) {
// Check if HTTP2
return isHttp2.check(data);
})
.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');
@@ -57,20 +92,36 @@ 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;
delete data.toolsResults.phantomas.metrics.scrollExecutionTree;
delete data.toolsResults.phantomas.offenders.scrollExecutionTree;
// 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');
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);
+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 -91
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,48 +7,18 @@ 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.addWhiteBackground)
.then(this.toBuffer);
};
@@ -57,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);
@@ -76,62 +45,34 @@ 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;
};
// If the page doesn't set a bg color, the default PhantomJS one is transparent
// When transforming PNG to JPG, transparent pixels become black.
// This is why we need to add a transparent background.
this.addWhiteBackground = function(image) {
var deferred = Q.defer();
// Create a canvas with the same dimensions as your image:
lwip.create(image.width(), image.height(), 'white', function(err, canvas){
if (err) {
debug('Could not create a white canvas');
debug(err);
deferred.reject(err);
} else {
// Paste original image on top of the canvas
canvas.paste(0, 0, image, function(err, image){
if (err) {
debug('Could not paste image on the white canvas');
debug(err);
deferred.reject(err);
} else {
// Now image has a white background...
debug('White background correctly added');
deferred.resolve(image);
}
});
}
});
return deferred.promise;
};
this.toBuffer = function(image) {
var deferred = Q.defer();
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);
@@ -143,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();
});
@@ -167,4 +105,4 @@ var screenshotHandler = function() {
};
};
module.exports = new screenshotHandler();
module.exports = new screenshotHandler();
-333
View File
@@ -1,333 +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 serverSettings = require('../../../server_config/settings.json');
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,
device: req.body.device || 'desktop',
waitForSelector: req.body.waitForSelector || null,
cookie: req.body.cookie || null,
authUser: req.body.authUser || null,
authPass: req.body.authPass || null,
blockDomain: req.body.blockDomain || null,
allowDomain: req.body.allowDomain || null,
noExternals: req.body.noExternals || false
}
};
// 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,
device: run.params.device,
waitForSelector: run.params.waitForSelector,
cookie: run.params.cookie,
authUser: run.params.authUser,
authPass: run.params.authPass,
blockDomain: run.params.blockDomain,
allowDomain: run.params.allowDomain,
noExternals: run.params.noExternals,
phantomasEngine: serverSettings.phantomasEngine
};
return ylt(run.params.url, runOptions);
})
// 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(serverSettings.screenshotWidth || 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)
// Don't worry if there's an error
.fail(function(err) {
debug('An error occured while creating the screenshot\'s thumbnail. Ignoring and continuing...');
});
}
// Let's continue
screenshotPromise
// Save results
.then(function() {
// Remove uneeded temp screenshot path
delete data.params.options.screenshot;
// Here we can remove tools results if not needed
return resultsDatastore.saveResult(data);
})
// 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) {
// Some fields can be excluded from the response, this way:
// /api/results/:id?exclude=field1,field2
if (req.query.exclude && typeof req.query.exclude === 'string') {
var excludedFields = req.query.exclude.split(',');
excludedFields.forEach(function(fieldName) {
if (data[fieldName]) {
delete data[fieldName];
}
});
}
return data;
});
});
// 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('/node_modules', express.static(path.join(__dirname, '../../../node_modules'), { 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;
-112
View File
@@ -1,112 +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 '1':
errorMessage = "Error 1: unknown error";
break;
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;
case '1003':
errorMessage = "Error 1003: Phantomas not returning";
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;
@@ -1,12 +0,0 @@
var wwwRedirectMiddleware = function(req, res, next) {
'use strict';
// Redirect www.yellowlab.tools to yellowlab.tools without "www" (for SEO purpose)
if(/^www\.yellowlab\.tools/.test(req.headers.host)) {
res.redirect(301, req.protocol + '://' + req.headers.host.replace(/^www\./, '') + req.url);
} else {
next();
}
};
module.exports = wwwRedirectMiddleware;
+2 -2
View File
@@ -5,7 +5,7 @@ var parse = require('parse-color');
var colorDiff = function() {
var COLOR_DIFF_THRESHOLD = 0.9;
var COLOR_DIFF_THRESHOLD = 0.7;
this.compareAllColors = function(data) {
debug('Starting to compare all colors...');
@@ -55,7 +55,7 @@ var colorDiff = function() {
};
this.parseOffender = function(offender) {
var regexResult = /^(.*) \(\d+ times\)$/.exec(offender);
var regexResult = /^(.*) \(\d+ times\)$/.exec(offender.value.message);
return regexResult ? regexResult[1] : null;
};
+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();
-105
View File
@@ -1,105 +0,0 @@
var debug = require('debug')('ylt:isHttp2');
var url = require('url');
var Q = require('q');
var http2 = require('is-http2');
var isHttp2 = function() {
'use strict';
this.check = function(data) {
debug('Starting to check for HTTP2 support...');
return this.checkHttp2(data)
.then(function(result) {
if (result.isHttp) {
debug('The website is not even in HTTPS');
data.toolsResults.http2 = {
metrics: {
http2: false
}
};
} else if (result.isHttp2) {
debug('HTTP/2 (or SPDY) is supported');
data.toolsResults.http2 = {
metrics: {
http2: true
}
};
} else {
debug('HTTP/2 is not supported');
data.toolsResults.http2 = {
metrics: {
http2: false
}
};
}
// Add the supported protocols as offenders
if (result.supportedProtocols) {
debug('Supported protocols: ' + result.supportedProtocols.join(' '));
data.toolsResults.http2.offenders = {
http2: result.supportedProtocols
};
}
debug('End of HTTP2 support check');
return data;
})
.fail(function() {
return data;
});
};
this.getParsedUrl = function(data) {
return url.parse(data.toolsResults.phantomas.url);
};
this.getProtocol = function(data) {
return this.getParsedUrl(data).protocol;
};
this.getDomain = function(data) {
return this.getParsedUrl(data).hostname;
};
this.checkHttp2 = function(data) {
var deferred = Q.defer();
// Check if it's HTTPS first
if (this.getProtocol(data) === 'http:') {
deferred.resolve({
isHttp: true
});
} else {
// To make is-http2 work, you need to have openssl in a version greater than 1.0.0 installed and available in your $path.
http2(this.getDomain(data), {includeSpdy: true})
.then(function(result) {
deferred.resolve(result);
})
.catch(function(error) {
debug('Error while checking HTTP2 support:');
debug(error);
deferred.reject('Error while checking for HTTP2 support');
});
}
return deferred.promise;
};
};
module.exports = new isHttp2();
-228
View File
@@ -1,228 +0,0 @@
var debug = require('debug')('ylt:jsExecutionTransformer');
var offendersHelpers = require('../offendersHelpers');
var Collection = require('./phantomas/custom_modules/util/collection');
var jsExecutionTransformer = function() {
this.transform = function(data) {
var javascriptExecutionTree = {};
var jQueryFunctionsCollection = new Collection();
var metrics = {
domInteractive: 0,
domContentLoaded: 0,
domContentLoadedEnd: 0,
domComplete: 0,
DOMaccesses: 0,
DOMaccessesOnScroll: 0,
queriesWithoutResults: 0
};
var offenders = {};
var hasjQuery = (data.toolsResults.phantomas.metrics.jQueryVersionsLoaded > 0);
if (hasjQuery) {
metrics.jQueryCalls = 0;
metrics.jQueryFunctionsUsed = 0;
metrics.jQueryCallsOnEmptyObject = 0;
metrics.jQueryNotDelegatedEvents = 0;
offenders.jQueryFunctionsUsed = [];
}
try {
debug('Starting JS execution transformation');
javascriptExecutionTree = JSON.parse(data.toolsResults.phantomas.offenders.javascriptExecutionTree[0]);
if (javascriptExecutionTree.children) {
javascriptExecutionTree.children.forEach(function(node, index) {
var contextLength = (node.data.callDetails && node.data.callDetails.context) ? node.data.callDetails.context.length : null;
if (isABindWithoutEventDelegation(node, contextLength)) {
metrics.jQueryNotDelegatedEvents += contextLength;
node.warning = true;
node.eventNotDelegated = true;
}
if (node.data.resultsNumber === 0) {
metrics.queriesWithoutResults ++;
node.queryWithoutResults = true;
node.warning = true;
}
if (contextLength === 0) {
metrics.jQueryCallsOnEmptyObject ++;
node.jQueryCallOnEmptyObject = true;
node.warning = true;
}
if (node.data.type.indexOf('jQuery - ') === 0) {
metrics.jQueryCalls ++;
jQueryFunctionsCollection.push(node.data.type);
}
// Mark errors with an error flag
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;
// Adjust the navigation timings (cause their not very well synchronised)
switch(node.data.type) {
case 'domInteractive':
javascriptExecutionTree.data.domInteractive = node.data.timestamp;
break;
case 'domContentLoaded':
javascriptExecutionTree.data.domContentLoaded = node.data.timestamp;
break;
case 'domContentLoadedEnd':
javascriptExecutionTree.data.domContentLoadedEnd = node.data.timestamp;
break;
case 'domComplete':
javascriptExecutionTree.data.domComplete = node.data.timestamp;
break;
}
}
// Fix rare bug when domComplete was never triggered
if (index === javascriptExecutionTree.children.length - 1 && !javascriptExecutionTree.data.domComplete) {
javascriptExecutionTree.data.domComplete = node.data.timestamp + 1000;
}
// Transform domPaths into objects
changeListOfDomPaths(node);
// Count the number of DOM accesses, by counting the tree leafs
metrics.DOMaccesses += countTreeLeafs(node);
});
// Count the number of different jQuery functions called
if (hasjQuery) {
jQueryFunctionsCollection.sort().forEach(function(fnName, cnt) {
if (fnName === 'jQuery - find') {
fnName = 'jQuery - $';
}
metrics.jQueryFunctionsUsed ++;
offenders.jQueryFunctionsUsed.push({
functionName: fnName.substring(9),
count: cnt
});
});
}
}
debug('JS execution transformation complete');
if (data.toolsResults.phantomas.offenders.scrollExecutionTree) {
debug('Starting scroll execution transformation');
offenders.DOMaccessesOnScroll = JSON.parse(data.toolsResults.phantomas.offenders.scrollExecutionTree[0]);
if (offenders.DOMaccessesOnScroll.children) {
offenders.DOMaccessesOnScroll.children.forEach(function(node) {
// Mark a event flag
if (['documentScroll', 'windowScroll', 'window.onscroll'].indexOf(node.data.type) >= 0) {
node.windowPerformance = true;
}
// Transform domPaths into objects
changeListOfDomPaths(node);
// Count the number of DOM accesses, by counting the tree leafs
metrics.DOMaccessesOnScroll += countTreeLeafs(node);
});
}
debug('Scroll execution transformation complete');
} else {
debug('Could not parse scrollExecutionTree');
}
} catch(err) {
throw err;
}
data.javascriptExecutionTree = javascriptExecutionTree;
data.toolsResults.jsExecutionTransformer = {
metrics: metrics,
offenders: offenders
};
return data;
};
function treeRecursiveParser(node, fn) {
if (node.children) {
node.children.forEach(function(child) {
treeRecursiveParser(child, fn);
});
}
fn(node);
}
function changeListOfDomPaths(rootNode) {
treeRecursiveParser(rootNode, function(node) {
if (node.data.callDetails && node.data.callDetails.context && node.data.callDetails.context.length > 0) {
node.data.callDetails.context.elements = node.data.callDetails.context.elements.map(offendersHelpers.domPathToDomElementObj, offendersHelpers);
}
if (node.data.type === 'appendChild' || node.data.type === 'insertBefore' || node.data.type === 'getComputedStyle') {
node.data.callDetails.arguments[0] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[0]);
}
if (node.data.type === 'insertBefore') {
node.data.callDetails.arguments[1] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[1]);
}
});
}
// Returns the number of leafs (nodes without children)
function countTreeLeafs(rootNode) {
var count = 0;
treeRecursiveParser(rootNode, function(node) {
if (!node.children &&
!node.error &&
!node.windowPerformance &&
node.data.type !== 'jQuery loaded') {
count ++;
}
});
return count;
}
function isPureString(str) {
return typeof str === 'string' && str[0] !== '{' && str !== '(function)' && str !== '[Object]' && str !== '[Array]' && str !== 'true' && str !== 'false' && str !== 'undefined' && str !== 'unknown' && str !== 'null';
}
function isABindWithoutEventDelegation(node, contextLength) {
// Count only on larger bindings
if (contextLength <= 3) {
return false;
}
if (node.data.type === 'jQuery - on' && node.data.callDetails.arguments[1] && !isPureString(node.data.callDetails.arguments[1])) {
return true;
}
if (node.data.type.indexOf('jQuery - ') === 0 && node.children && node.children.length === 1) {
var child = node.children[0];
if (child.data.type === 'jQuery - on' && child.data.callDetails.arguments[1] && !isPureString(child.data.callDetails.arguments[1])) {
return true;
}
}
return false;
}
};
module.exports = new jsExecutionTransformer();
@@ -1,250 +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.on('init', function() {
phantomas.evaluate(function(responseEndTime) {
(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;
var depth = 0;
// Add a child but don't enter its context
function pushContext(data) {
// Some data is not needed on subchildren
if (depth === 0) {
data.timestamp = Date.now() - responseEndTime;
data.loadingStep = phantomas.currentStep || '';
}
// Some data is not needed on subchildren
if (depth > 0) {
if (data.backtrace) {
delete data.backtrace;
}
if (data.resultsNumber) {
delete data.resultsNumber;
}
}
currentContext.addChild(data);
}
// Add a child to the current context and enter its context
function enterContext(data) {
// Some data is not needed on subchildren
if (depth === 0) {
data.timestamp = Date.now() - responseEndTime;
data.loadingStep = phantomas.currentStep || '';
}
// Some data is not needed on subchildren
if (depth > 0) {
if (data.backtrace) {
delete data.backtrace;
}
if (data.resultsNumber) {
delete data.resultsNumber;
}
}
currentContext = currentContext.addChild(data);
depth ++;
}
// Save given data in the current context and jump change current context to its parent
function leaveContext(moreData) {
// Some data is not needed on subchildren
if (depth === 1) {
currentContext.data.time = Date.now() - currentContext.data.timestamp - responseEndTime;
}
// Some data is not needed on subchildren
if (depth > 1) {
if (moreData && moreData.backtrace) {
delete moreData.backtrace;
}
if (moreData && moreData.resultsNumber) {
delete moreData.resultsNumber;
}
}
// Merge previous data with moreData (ovewrites if exists)
if (moreData) {
for (var key in moreData) {
currentContext.data[key] = moreData[key];
}
}
var parent = currentContext.parent;
if (parent === null) {
console.error('Error: trying to close root context in ContextTree');
} else {
currentContext = parent;
}
depth --;
}
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;
}
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;
}
// Empty the tree
function resetTree() {
root = new ContextTreeNode(null, {type: 'main'});
currentContext = root;
depth = 0;
}
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;
phantomas.resetTree = resetTree;
})();
})(window.__phantomas);
}, responseEndTime);
});
};
@@ -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,84 +0,0 @@
/**
* Analyzes DOM hidden content
*/
/* global document: true, Node: true, window: true */
exports.version = '1.0.a';
exports.module = function(phantomas) {
'use strict';
// total length of HTML of hidden elements (i.e. display: none)
phantomas.setMetric('hiddenContentSize'); // @desc the size of content of hidden elements on the page (with CSS display: none) @offenders
phantomas.setMetric('hiddenImages'); // @desc number of hidden images that can be lazy-loaded @offenders
// HTML size
phantomas.on('report', function() {
phantomas.evaluate(function() {
(function(phantomas) {
var runner = new phantomas.nodeRunner(),
lazyLoadableImages = {};
phantomas.spyEnabled(false, 'analyzing hidden content');
runner.walk(document.body, function(node, depth) {
switch (node.nodeType) {
case Node.ELEMENT_NODE:
// @see https://developer.mozilla.org/en/DOM%3awindow.getComputedStyle
var styles = window.getComputedStyle(node);
if (styles && styles.getPropertyValue('display') === 'none') {
if (typeof node.innerHTML === 'string') {
var size = node.innerHTML.length;
phantomas.incrMetric('hiddenContentSize', size);
// log hidden containers bigger than 1 kB
if (size > 1024) {
phantomas.addOffender('hiddenContentSize', phantomas.getDOMPath(node) + ' (' + size + ' characters)');
}
}
// count hidden images that can be lazy loaded (issue #524)
var images = [];
if (node.tagName === 'IMG') {
images = [node];
} else if (typeof node.querySelectorAll === 'function') {
images = node.querySelectorAll('img') || [];
}
for (var i = 0, len = images.length; i < len; i++) {
var src = images[i].src,
path;
if (src === '' || src.indexOf('data:image') === 0) continue;
if (!lazyLoadableImages[src]) {
path = phantomas.getDOMPath(images[i]);
lazyLoadableImages[src] = {
path: path
};
}
}
// don't run for child nodes as they're hidden as well
return false;
}
break;
}
});
Object.keys(lazyLoadableImages).forEach(function(img) {
var entry = lazyLoadableImages[img];
phantomas.incrMetric('hiddenImages');
phantomas.addOffender('hiddenImages', img);
phantomas.log('hiddenImages: <%s> image (%s) is hidden and can be lazy-loaded', img, entry.path);
});
phantomas.spyEnabled(true);
}(window.__phantomas));
});
});
};
@@ -1,387 +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.on('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('html') === 0 ||
context.indexOf('body') === 0 ||
context.indexOf('head') === 0 ||
context.indexOf('#document') === 0
)) {
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,82 +0,0 @@
/**
* Analyzes events bound to DOM elements
*/
/* global Document: true, Element: true, window: true */
exports.version = '0.4.a';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('eventsBound'); // @desc number of EventTarget.addEventListener calls
phantomas.setMetric('eventsDispatched'); // @desc number of EventTarget.dispatchEvent calls
phantomas.setMetric('eventsScrollBound'); // @desc number of scroll event bounds
phantomas.on('init', function() {
phantomas.evaluate(function() {
(function(phantomas) {
// spy calls to EventTarget.addEventListener
// @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget.addEventListener
function eventSpyBefore(eventType) {
/* jshint validthis: true */
var path = phantomas.getDOMPath(this);
//phantomas.log('DOM event: "' + eventType + '" bound to "' + path + '"');
phantomas.incrMetric('eventsBound');
phantomas.addOffender('eventsBound', '"%s" bound to "%s"', eventType, path);
phantomas.log('event ' + eventType + ' bound');
phantomas.enterContext({
type: 'addEventListener',
callDetails: {
context: {
length: 1,
elements: [path]
},
arguments: [eventType]
},
backtrace: phantomas.getBacktrace()
});
// count window.addEventListener('scroll', ...) - issue #508
if (eventType === 'scroll' && (path === 'window' || path === '#document')) {
phantomas.incrMetric('eventsScrollBound');
phantomas.addOffender('eventsScrollBound', 'bound by %s on %s', phantomas.getBacktrace(), path);
}
}
function eventSpyAfter(result) {
phantomas.leaveContext();
}
phantomas.spy(Element.prototype, 'addEventListener', eventSpyBefore, eventSpyAfter);
phantomas.spy(Document.prototype, 'addEventListener', eventSpyBefore, eventSpyAfter);
phantomas.spy(window, 'addEventListener', eventSpyBefore, eventSpyAfter);
// spy calls to EventTarget.dispatchEvent
// @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget.dispatchEvent
phantomas.spy(Element.prototype, 'dispatchEvent', function(ev) {
/* jshint validthis: true */
var path = phantomas.getDOMPath(this);
phantomas.log('Core JS event: triggered "%s" on "%s"', ev.type, path);
phantomas.incrMetric('eventsDispatched');
phantomas.addOffender('eventsDispatched', '"%s" on "%s"', ev.type, path);
});
})(window.__phantomas);
});
});
phantomas.on('report', function() {
phantomas.evaluate(function() {
(function(phantomas) {
// Check if a window.onscroll function is defined
if (typeof(window.onscroll) === "function") {
phantomas.incrMetric('eventsScrollBound');
phantomas.addOffender('eventsScrollBound', 'bound by %s on %s', '', 'window.onscroll');
}
}(window.__phantomas));
});
});
};
@@ -1,381 +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.on('init', function() {
phantomas.evaluate(function(jQueryFunctions, jQueryTraversalFunctions) {
(function(phantomas) {
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 {
for (var key in arg) {
if (typeof arg[key] === 'function') {
arg[key] = '(function)';
}
}
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);
});
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);
});
};
@@ -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.on('init', function() {
phantomas.evaluate(function(spyEval) {
(function(phantomas) {
function report(msg, caller, backtrace, metric) {
phantomas.log(msg + ': from ' + caller + '!');
phantomas.log('Backtrace: ' + backtrace);
phantomas.incrMetric(metric);
phantomas.addOffender(metric, "%s from %s", msg, caller);
}
// spy calls to eval()
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');
// save data
phantomas.on('report', function() {
phantomas.log('JS execution tree: 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,79 +0,0 @@
/**
* Analyzes images and detects which one can be lazy-loaded (are below the fold)
*
* @see https://github.com/macbre/phantomas/issues/494
*/
/* global document: true, window: true */
exports.version = '1.0.a';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('lazyLoadableImagesBelowTheFold'); // @desc number of images displayed below the fold that can be lazy-loaded
phantomas.on('report', function() {
phantomas.log('lazyLoadableImages: analyzing which images can be lazy-loaded...');
phantomas.evaluate(function() {
(function(phantomas) {
phantomas.spyEnabled(false, 'analyzing which images can be lazy-loaded');
var images = document.body.getElementsByTagName('img'),
i,
len = images.length,
offset,
path,
processedImages = {},
src,
viewportHeight = window.innerHeight;
phantomas.log('lazyLoadableImages: %d image(s) found, assuming %dpx offset to be the fold', len, viewportHeight);
for (i = 0; i < len; i++) {
// @see https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect
offset = images[i].getBoundingClientRect().top;
src = images[i].src;
// ignore base64-encoded images
if (src === '' || /^data:/.test(src)) {
continue;
}
path = phantomas.getDOMPath(images[i]);
// get the most top position for a given image (deduplicate by src)
if (typeof processedImages[src] === 'undefined') {
processedImages[src] = {
offset: offset,
path: path
};
}
// maybe there's the same image loaded above the fold?
if (offset < processedImages[src].offset) {
processedImages[src] = {
offset: offset,
path: path
};
}
}
phantomas.log('lazyLoadableImages: checking %d unique image(s)', Object.keys(processedImages).length);
Object.keys(processedImages).forEach(function(src) {
var img = processedImages[src];
if (img.offset > viewportHeight) {
phantomas.log('lazyLoadableImages: <%s> image (%s) is below the fold (at %dpx)', src, img.path, img.offset);
phantomas.incrMetric('lazyLoadableImagesBelowTheFold');
phantomas.addOffender('lazyLoadableImagesBelowTheFold', src);
}
});
phantomas.spyEnabled(true);
})(window.__phantomas);
});
});
};
@@ -1,23 +0,0 @@
/**
* Retries download on every request to get the real file size
*
*/
exports.version = '0.1';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('requestsList');
var requests = [];
phantomas.on('recv', function(entry, res) {
requests.push(entry);
});
phantomas.on('report', function() {
phantomas.setMetric('requestsList', true, true);
phantomas.addOffender('requestsList', JSON.stringify(requests));
});
};
@@ -1,69 +0,0 @@
exports.version = '0.1';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('scrollExecutionTree');
phantomas.on('report', function() {
phantomas.evaluate(function() {
(function(phantomas) {
var evt = document.createEvent('CustomEvent');
evt.initCustomEvent('scroll', false, false, null);
function triggerScrollEvent() {
phantomas.resetTree();
try {
// Chrome triggers them in this order:
// 1. document
phantomas.pushContext({
type: 'documentScroll'
});
document.dispatchEvent(evt);
// 2. window
phantomas.pushContext({
type: 'windowScroll'
});
window.dispatchEvent(evt);
// No need to call window.onscroll(), it's called by the scroll event on window
} catch(e) {
phantomas.log('ScrollListener error: %s', e);
}
}
var firstScrollTime = Date.now();
phantomas.log('ScrollListener: triggering a first scroll event...');
triggerScrollEvent();
// Ignore the first scroll event and only save the second one,
// because we want to detect un-throttled things, throttled ones are ok.
var secondScrollTime = Date.now();
phantomas.log('ScrollListener: triggering a second scroll event (%dms after the first)...', secondScrollTime - firstScrollTime);
triggerScrollEvent();
var fullTree = phantomas.readFullTree();
if (fullTree !== null) {
phantomas.setMetric('scrollExecutionTree', true, true);
phantomas.addOffender('scrollExecutionTree', JSON.stringify(fullTree));
phantomas.log('ScrollListener: scrollExecutionTree correctly extracted');
} else {
phantomas.log('Error: scrollExecutionTree could not be extracted');
}
phantomas.log('ScrollListener: end of scroll triggering');
})(window.__phantomas);
});
});
};
@@ -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);
});
};

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