Compare commits

...
Author SHA1 Message Date
dependabot[bot]andGitHub c0bde3b3e6 Bump async from 2.6.1 to 2.6.4
Bumps [async](https://github.com/caolan/async) from 2.6.1 to 2.6.4.
- [Release notes](https://github.com/caolan/async/releases)
- [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md)
- [Commits](https://github.com/caolan/async/compare/v2.6.1...v2.6.4)

---
updated-dependencies:
- dependency-name: async
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-11 22:32:48 +00:00
Gaël MétaisandGitHub 73c5b3383e Merge pull request #391 for YLT v3.0
YLT v3.0 is coming soon!
2023-10-12 00:30:16 +02:00
Gaël MétaisandGitHub 5f09fb3bde Merge branch 'develop' into webp-avif 2023-10-06 16:43:32 +02:00
Gaël Métais 0610fd6fbb Update Phantomas and increase min NodeJS version 2023-10-06 16:18:23 +02:00
Gaël Métais 85c685a5ca Tag a phantomas version 2023-08-16 09:37:34 +02:00
Gaël Métais 2977506cb8 Adjustments to excessive image density 2023-08-16 07:57:01 +02:00
Gaël Métais 538d734a5b Switch imageExcessiveDensity to KB instead of simple count 2023-08-16 01:07:17 +02:00
Gaël Métais e680f429eb Re-introduce the Requests Number metric 2023-08-16 01:05:13 +02:00
Gaël Métais c308850eb3 WIP new image analizis module in Phantomas 2023-08-14 01:54:17 +02:00
Gaël Métais 8e941233a0 Revert the image reformater (moved to Phantomas) 2023-08-12 04:29:43 +02:00
Gaël MétaisandGitHub 22a8a206ab Merge pull request #388 from YellowLabTools/revert-387-webp-avif
Revert "Introduce a new "Old Image Formats" rule"
2023-08-12 04:08:02 +02:00
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
131 changed files with 1440 additions and 11480 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:
- "8.9"
- "6.2"
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
-281
View File
@@ -1,281 +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']
},
build: {
src: ['front/build']
}
},
copy: {
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'},
]
}
},
mochaTest: {
test: {
options: {
reporter: 'spec',
},
src: ['test/core/*.js', 'test/api/*.js']
},
'test-current-work': {
options: {
reporter: 'spec',
},
src: ['test/api/apiTest.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: './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',
unescape: {
'&lt;': '<',
'&gt;': '>'
}
},
files: {
'./front/build/main.html': ['.tmp/views/*.html']
}
}
},
filerev: {
options: {
algorithm: 'md5',
length: 8
},
assets: {
src: './front/build/*/*.*'
}
}
});
// Custom task that sets a variable for tests
grunt.registerTask('test-settings', function() {
process.env.IS_TEST = true;
});
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', [
'test-settings',
'build',
'express:testSuite',
'express:test',
'mochaTest:test',
'clean:tmp'
]);
grunt.registerTask('test-current-work', [
'test-settings',
'jshint',
'express:testSuite',
'express:test-current-work',
'mochaTest:test-current-work',
'clean:tmp'
]);
};
+25 -23
View File
@@ -14,12 +14,22 @@ Analyzes a webpage and detects **performance** or **front-end code quality** iss
<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,32 +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!
<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)
- [camlafit](https://github.com/camlafit)
- Km [camlafit](https://github.com/camlafit)
- Vincent L. [magikcypress](https://github.com/magikcypress)
- Ousama Ben Younes [ousamabenyounes](https://github.com/ousamabenyounes)
Vendored
-24
View File
@@ -1,24 +0,0 @@
Vagrant.configure("2") do |config|
config.vm.box = "ubuntu/trusty64"
config.vm.network :private_network, ip: "10.10.10.10"
config.ssh.forward_agent = true
# http://foo-o-rama.com/vagrant--stdin-is-not-a-tty--fix.html
config.vm.provision "fix-no-tty", type: "shell" do |s|
s.privileged = false
s.inline = "sudo sed -i '/tty/!s/mesg n/tty -s \\&\\& mesg n/' /root/.profile"
end
config.vm.provider :virtualbox do |vb|
vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
vb.customize ["modifyvm", :id, "--memory", 1024]
vb.customize ["modifyvm", :id, "--cpus", 2]
end
config.vm.synced_folder "./", "/space/YellowLabTools"
config.vm.provision :shell, :path => "server_config/server_install.sh"
end
Regular → Executable
+11 -7
View File
@@ -13,20 +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 - aka blacklist.',
' --allow-domain Only allow requests to given (comma-separated) domains - aka whitelist.',
' --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')
});
@@ -55,7 +56,7 @@ 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;
@@ -75,6 +76,10 @@ 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"');
@@ -98,7 +103,6 @@ if (cli.flags.reporter && cli.flags.reporter !== 'json' && cli.flags.reporter !=
// Remove some heavy parts of the results object
delete data.toolsResults;
delete data.javascriptExecutionTree;
var xmlOutput = serializer.render(data);
-49
View File
@@ -1,49 +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);
app.use(bodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded
// EJS HTML engine
app.engine('.html', require('ejs').__express);
app.set('view engine', 'ejs');
// Initialize the controllers
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');
app.locals.baseUrl = settings.baseUrl;
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 .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;
}
.summary .sponsor {
font-size: 0.9em;
margin-bottom: 4em;
color: #413;
}
.summary .sponsor a {
color: inherit;
}
File diff suppressed because one or more lines are too long
-182
View File
@@ -1,182 +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;
}
.homeSponsor {
margin-top: 3em;
}
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: 20em;
margin: 2em auto 4em;
}
.totalWeightPie canvas {
max-width: inherit;
}
.offenderProblem {
font-weight: bold;
color: #e74c3c;
}
.imageOffenders {
display: table;
border-spacing: 3em;
width: 90%;
}
.imageOffenders > div {
display: table-row;
}
.imageOffenders > div > div {
display: table-cell;
vertical-align: middle;
}
.imageOffenders img {
max-height: 10em;
max-width: 40em;
border: 1px solid #000;
margin-top: 0.5em;
}
.smallPreview {
display: block;
max-height: 6em;
max-width: 16em;
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

-90
View File
@@ -1,90 +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(){
if (typeof ga !== "undefined") {
ga('send', 'pageview', {'page': $location.path()});
}
});
// GitHub star button (asynchronously loaded iframe)
window.addEventListener('load', function() {
window.document.getElementById('ghbtn').src = 'https://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);
}
]);
// Disable debugging https://docs.angularjs.org/guide/production
yltApp.config(['$compileProvider', function ($compileProvider) {
$compileProvider.debugInfoEnabled(false);
}]);
-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 + 'https://yellowlab.tools'));
};
$scope.shareOnLinkedin = function(message) {
openSocialPopup('https://www.linkedin.com/shareArticle?mini=true&url=https://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();
}]);
-23
View File
@@ -1,23 +0,0 @@
var indexCtrl = angular.module('indexCtrl', []);
indexCtrl.controller('IndexCtrl', ['$scope', '$routeParams', '$location', 'Settings', 'API', function($scope, $routeParams, $location, Settings, API) {
$scope.settings = Settings.getMergedSettings();
$scope.launchTest = function() {
if ($scope.url) {
$location.search('url', null);
$location.search('run', null);
Settings.saveSettings($scope.settings);
API.launchTest($scope.url, $scope.settings);
}
};
// Auto fill URL field and auto launch test when the good params are set in the URL
if ($routeParams.url) {
$scope.url = $routeParams.url;
if ($routeParams.run === 'true' || $routeParams.run === 1 || $routeParams.run === '1') {
$scope.launchTest();
}
}
}]);
-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();
}]);
-140
View File
@@ -1,140 +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 = {
tooltips: {
callbacks: {
label: function(tooltipItem, data) {
var label = data.labels[tooltipItem.index];
var value = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index];
return label + ': ' + value + ' KB';
}
}
},
legend: {
display: true,
position: 'bottom',
labels: {
boxWidth: 12,
fontSize: 14
}
}
};
}
// Init "Breakpoints" chart
if ($scope.policyName === 'cssBreakpoints' && $scope.rule.value > 0) {
// Seek for the biggest breakpoint
var max = 0;
$scope.rule.offendersObj.forEach(function(offender) {
if (offender.pixels > max) {
max = offender.pixels;
}
});
max = Math.max(max + 100, 1400);
// We group offenders 10px by 10px
var GROUP_SIZE = 20;
// Generate an empty array of values
$scope.breakpointsLabels = [];
$scope.breakpointsData = [[]];
for (var i = 0; i <= max / GROUP_SIZE; i++) {
$scope.breakpointsLabels[i] = '';
$scope.breakpointsData[0][i] = 0;
}
// Fill it with results
$scope.rule.offendersObj.forEach(function(offender) {
var group = Math.floor((offender.pixels + 1) / GROUP_SIZE);
if ($scope.breakpointsLabels[group] !== '') {
$scope.breakpointsLabels[group] += '/';
}
$scope.breakpointsLabels[group] += offender.breakpoint;
$scope.breakpointsData[0][group] += offender.count;
});
$scope.breakpointsSeries = ['Number of CSS rules per breakpoint'];
$scope.breakpointsColours = ['#9c4274'];
$scope.breakpointsOptions = {
scales: {
xAxes: [{
gridLines: {
display:false
}
}],
yAxes: [{
gridLines: {
display:false
}
}]
},
tooltips: {
enabled: false
},
elements: {
point: {
radius: 0
}
}
};
}
}
$scope.backToDashboard = function() {
$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,938 +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) {
// Yet another PhantomJS 2 format?
withFnResult = /^([^\s\(]+|global code)@(.+:\d+:\d+)$/.exec(trace);
}
if (withFnResult === null) {
// Try the PhantomJS 2 ERROR format
withFnResult = /^([^\s\(]+) (http.+:\d+)$/.exec(trace);
}
if (withFnResult === null) {
fileAndLine = trace;
} else {
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];
// Filter phantomas code
if (filePath.indexOf('phantomjs://') === -1) {
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 already 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';
};
});
offendersDirectives.filter('addSpaces', function() {
return function(str) {
return str.split('').join(' ');
};
});
// Proxify an HTTP image to HTTPS if hosted on HTTPS
// Uses a great free open-source external service: https://images.weserv.nl
offendersDirectives.filter('https', function() {
return function(url) {
if (url && url.indexOf('http://') === 0 && window.location.protocol === 'https:') {
return 'https://images.weserv.nl/?url=' + encodeURIComponent(url.substr(7));
}
return url;
};
});
})();
-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', {
});
}]);
-64
View File
@@ -1,64 +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,
proxy: settings.proxy,
cookie: settings.cookie,
authUser: settings.authUser,
authPass: settings.authPass,
blockDomain: settings.blockDomain,
allowedDomains: settings.allowedDomains,
noExternals: settings.noExternals
};
if (settings.domainsBlackOrWhite === 'black') {
runObject.blockDomain = this.parseDomains(settings.domains);
} else if (settings.domainsBlackOrWhite === 'white') {
var allowedDomains = this.parseDomains(settings.domains);
if (allowedDomains.length > 0) {
runObject.allowDomain = allowedDomains;
} else {
runObject.noExternals = true;
}
}
Runs.save(runObject, function(data) {
$location.path('/queue/' + data.runId);
}, function(response) {
if (response.status === 429) {
alert('Too many requests, you reached the max number of requests allowed in 24h');
} else if (response.status === 403) {
alert('This particular query was blocked due to spamming. If you think it\'s an error, please open an issue on GitHub.');
} else {
alert('An error occured...');
}
});
},
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 .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;
}
}
.summary .sponsor {
font-size: 0.9em;
margin-bottom: 4em;
color: #413;
a {
color: inherit;
}
}
File diff suppressed because one or more lines are too long
-206
View File
@@ -1,206 +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;
}
.homeSponsor {
margin-top: 3em;
}
-260
View File
@@ -1,260 +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;
}
.sponsor {
font-size: 0.9em;
}
}
[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: 20em;
margin: 2em auto 4em;
canvas {
max-width: inherit;
}
}
.offenderProblem {
font-weight: bold;
color: #e74c3c;
}
.imageOffenders {
display: table;
border-spacing: 3em;
width: 90%;
> div {
display: table-row;
> div {
display: table-cell;
vertical-align: middle;
}
}
img {
max-height: 10em;
max-width: 40em;
border: 1px solid #000;
margin-top: 0.5em;
}
}
.smallPreview {
display: block;
max-height: 6em;
max-width: 16em;
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;
}
}
}
-69
View File
@@ -1,69 +0,0 @@
<html>
<head>
<meta charset="utf-8">
<title>Yellow Lab Tools</title>
<base href="<%= baseUrl %>">
<link rel="icon" type="image/png" href="img/favicon.png">
<meta property="og:image" content="img/logo-large.png" />
<meta name="description" content="Free online web performance tool. Audit your webpage for performance and front-end quality issues. And it's open-source!" />
<!-- build:css css/styles.css-->
<link rel="stylesheet" type="text/css" href="css/main.css">
<link rel="stylesheet" type="text/css" href="css/index.css">
<link rel="stylesheet" type="text/css" href="css/dashboard.css">
<link rel="stylesheet" type="text/css" href="css/queue.css">
<link rel="stylesheet" type="text/css" href="css/rule.css">
<link rel="stylesheet" type="text/css" href="css/screenshot.css">
<link rel="stylesheet" type="text/css" href="css/timeline.css">
<link rel="stylesheet" type="text/css" href="css/about.css">
<!-- endbuild -->
<link rel="preconnect" href="https://www.google-analytics.com">
<link rel="dns-prefetch" href="https://www.google-analytics.com">
<link rel="preconnect" href="https://ghbtns.com">
<link rel="dns-prefetch" href="https://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="<%= baseUrl %>about">More about Yellow Lab Tools</a><br>
<div class="github"><iframe id="ghbtn" frameborder="0" scrolling="0" width="145px" height="30px"></iframe></div>
</div>
<!-- build:js js/all.js -->
<script src="node_modules/angular/angular.min.js"></script>
<script src="node_modules/chart.js/dist/Chart.min.js"></script>
<script src="node_modules/angular-route/angular-route.min.js"></script>
<script src="node_modules/angular-resource/angular-resource.min.js"></script>
<script src="node_modules/angular-sanitize/angular-sanitize.min.js"></script>
<script src="node_modules/angular-animate/angular-animate.min.js"></script>
<script src="node_modules/angular-local-storage/dist/angular-local-storage.min.js"></script>
<script src="node_modules/angular-chart.js/dist/angular-chart.min.js"></script>
<script src="js/app.js"></script>
<script src="js/controllers/indexCtrl.js"></script>
<script src="js/controllers/dashboardCtrl.js"></script>
<script src="js/controllers/queueCtrl.js"></script>
<script src="js/controllers/ruleCtrl.js"></script>
<script src="js/controllers/screenshotCtrl.js"></script>
<script src="js/controllers/timelineCtrl.js"></script>
<script src="js/models/resultsFactory.js"></script>
<script src="js/models/runsFactory.js"></script>
<script src="js/services/apiService.js"></script>
<script src="js/services/menuService.js"></script>
<script src="js/services/settingsService.js"></script>
<script src="js/directives/gradeDirective.js"></script>
<script src="js/directives/offendersDirectives.js"></script>
<!-- endbuild -->
<script>
if('@@googleAnalyticsId'.indexOf('UA-')===0){(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');ga('create','@@googleAnalyticsId','auto');}
</script>
</body>
</html>
-13
View File
@@ -1,13 +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 allows you to 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 that developers 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>
<%if (sponsoring.about) { %>
<div class="sponsor"><%- sponsoring.about %></div>
<% } %>
<p><br><a href="<%= baseUrl %>">Back to index</a></p>
</div>
-80
View File
@@ -1,80 +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 track by $index">
{{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>
<%if (sponsoring.dashboard) { %>
<div ng-if="!error && !fromSocialShare" class="sponsor"><%- sponsoring.dashboard %></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="<%= baseUrl %>">Test another webpage</a>
</div>
<div ng-if="error">
<h2>Run failed / Run not found</h2>
</div>
</div>
-116
View File
@@ -1,116 +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 || settings.proxy || settings.domains)">
Currently set:
<span ng-if="settings.waitForSelector">wait for selector</span>
<span ng-if="settings.cookie">cookie</span>
<span ng-if="settings.authUser || settings.authPass">authentication</span>
<span ng-if="settings.proxy">proxy</span>
<span ng-if="settings.domains">domain blocking</span>
</span>
<div class="advanced" ng-show="settings.showAdvanced">
<!--<div>
<div class="label">
Wait selector
<span class="settingsTooltip">
<span class="icon-question"></span>
<div><b>Wait for a CSS selector</b><br><br>Once the page is considered loaded, PhantomJS will repeatedly try to match the given CSS selector until it is found in the page. A 60 seconds timeout still applies anyway.<br><br>Example: "body.loaded"</div>
</span>
</div>
<div><input type="text" name="waitForSelector" ng-model="settings.waitForSelector" /></div>
</div>-->
<div>
<div class="label">
Cookie
<span class="settingsTooltip">
<span class="icon-question"></span>
<div><b>Cookie</b><br><br>Adds cookies, separated by a pipe character.<br><br>Example: "bar1=foo1;domain=.domain1.com|bar2=foo2;domain=www.domain2.com"</div>
</span>
</div>
<div><input type="text" name="cookie" ng-model="settings.cookie" /></div>
</div>
<div>
<div class="label">
Authent
<span class="settingsTooltip">
<span class="icon-question"></span>
<div><b>Basic HTTP authentication</b><br><br>Enter your credentials here if you need to bypass a basic authentication.<br><br><i>PS: if your authentication is not basic, you might be able to copy the session cookie from your browser, paste it in the "Cookie" setting and launch a run before your cookie expires.</i></div>
</span>
</div>
<div class="subTable">
<div>
<div>username</div>
<div><input type="text" class="authField" name="authUser" ng-model="settings.authUser" /></div>
</div>
<div>
<div><span>password</div>
<div><input type="password" class="authField" name="authPass" ng-model="settings.authPass" /></div>
</div>
</div>
</div>
<div>
<div class="label">
HTTP proxy
<span class="settingsTooltip">
<span class="icon-question"></span>
<div><b>HTTP proxy</b><br><br>Insert here your proxy settings with the format "host:port".<br><br>Example: "192.168.10.0:3333"</div>
</span>
</div>
<div><input type="text" name="proxy" ng-model="settings.proxy" /></div>
</div>
<div>
<div class="label">
Block domains
<span class="settingsTooltip">
<span class="icon-question"></span>
<div><b>Block some domains</b><br><br>One line per domain or subdomain.<br><br><i><b>Example:</b><br>google-analytics.com<br>ads.yahoo.com<br>ajax.googleapis.com</i><br><br>An empty whitelist will block all domains except the main domain.</div>
</span>
</div>
<div>
<div>
<input type="radio" name="blackOrWhite" ng-model="settings.domainsBlackOrWhite" value="black" />blacklist
<input type="radio" name="blackOrWhite" ng-model="settings.domainsBlackOrWhite" value="white" />whitelist
</div>
<textarea name="domains" ng-model="settings.domains" rows="5"></textarea>
</div>
</div>
</div>
</div>
</form>
<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>
<%if (sponsoring.home) { %>
<div class="homeSponsor"><%- sponsoring.home %></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="<%= baseUrl %>">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="<%= baseUrl %>">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="<%= baseUrl %>"><div class="icon-arrow-left3"></div><span>New test<span></a>
<a class="menuItem restart" href="" ng-click="testAgain()"><div class="icon-loop"></div><span>Test again<span></a>
<div class="menuItem" ng-class="{active: Menu.getCurrentPage() == 'dashboard'}" ng-click="Menu.changePage('dashboard')"><div class="icon-list"></div><span>Dashboard</span></div>
<div class="menuItem" ng-class="{active: Menu.getCurrentPage() == 'timeline'}" ng-click="Menu.changePage('timeline')"><div class="icon-bars"></div><span>JS Timeline</span></div>
</div>
-491
View File
@@ -1,491 +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-if="rule.offendersObj.count >= 0"><ng-pluralize count="rule.offendersObj.count" when="{'0': 'No offenders', 'one': '1 offender', 'other': '{} offenders'}"></ng-pluralize></h3>
<div ng-if="rule.offendersObj.list" class="offendersTable">
<div ng-repeat="offender in rule.offendersObj.list track by $index">
<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 === '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 === 'cssImports'">
{{offender.css}}
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
</div>
<div ng-if="policyName === 'cssOldPropertyPrefixes'">
<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 | https}}" class="smallPreview checker"></img>
<url-link url="offender" max-length="100"></url-link>
</div>
<div ng-if="policyName === 'imagesTooLarge'">
<img ng-src="{{offender.url | https}}" class="smallPreview checker"></img>
<div>{{offender.width}}x{{offender.height}}</div>
<url-link url="offender.url" max-length="100"></url-link>
</div>
<div ng-if="policyName === 'notFound' || policyName === 'emptyRequests' || policyName === 'closedConnections' || policyName === 'multipleRequests' || policyName === 'cachingDisabled' || policyName === 'cachingNotSpecified'">
<url-link url="offender" max-length="100"></url-link>
</div>
<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' || policyName === 'synchronousXHR'">
{{offender}}
</div>
<div ng-if="policyName === 'fontsCount'">
<url-link url="offender.url" max-length="70"></url-link>
({{offender.size | bytes}})
</div>
</div>
</div>
</div>
<div ng-repeat="fileDetails in rule.offendersObj.byFile track by $index">
<h3>
<ng-pluralize count="fileDetails.count" when="{'one': '1 offender', 'other': '{} offenders'}"></ng-pluralize>
in
<url-link ng-if="fileDetails.url !== 'Inline CSS'" url="fileDetails.url" max-length="80"></url-link>
<span ng-if="fileDetails.url === 'Inline CSS'">inline CSS</span>
</h3>
<div class="offendersTable">
<div ng-repeat="offender in fileDetails.offenders track by $index">
<div ng-if="policyName === 'cssComplexSelectors' || policyName === 'cssComplexSelectorsByAttribute' || policyName === 'cssUniversalSelectors' || policyName === 'cssRedundantBodySelectors' || policyName === 'cssRedundantChildNodesSelectors'">
<span ng-if="offender.bolded" ng-bind-html="offender.bolded"></span>
<b ng-if="!offender.bolded">{{offender.css}}</b>
<span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
</div>
<div ng-if="policyName === 'cssMobileFirst'">
<b>{{offender.query}}</b> for <ng-pluralize count="offender.rules" when="{'one':'1 rule','other':'{} rules'}"></ng-pluralize>
<span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
</div>
<div ng-if="policyName === 'cssDuplicatedSelectors'">
{{offender.rule}} (<b>x{{offender.occurrences}}</b>)
</div>
<div ng-if="policyName === 'cssDuplicatedProperties'">
Property <b>{{offender.property}}</b> duplicated in <b>{{offender.rule}} { }</b>
<span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
</div>
<div ng-if="policyName === 'cssEmptyRules'">
<b>{{offender.css}} { }</b>
<span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
</div>
<div ng-if="policyName === 'cssExpressions'">
{{offender.rule}} {{ '{' + offender.property}}: <b>expression(</b>{{offender.expression}}<b>)</b>}
<span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
</div>
<div ng-if="policyName === 'cssImportants'">
{{offender.rule}} {{ '{' + offender.property}}: {{offender.value}} <b>!important</b>}
<span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
</div>
<div ng-if="policyName === 'cssOldIEFixes'">
<span ng-if="offender.browser"><b>{{offender.browser}} fix:</b></span>
<span ng-bind-html="offender.bolded"></span>
<span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
</div>
</div>
</div>
</div>
<div ng-if="!rule.offendersObj.list && !rule.offendersObj.byFile" class="offendersHtml">
<div ng-if="policyName === 'DOMelementMaxDepth'">
<dom-tree tree="rule.offendersObj.tree"></dom-tree>
</div>
<div ng-if="policyName === 'DOMaccessesOnScroll' && rule.offendersObj.children.length > 0">
<p>The table below shows the interactions between the JavaScript and the DOM on a scroll event.</p>
<div class="table" ng-class="{warningsFilterOn: warningsFilterOn}">
<div class="headers">
<div><!-- index --></div>
<div>Type</div>
<div>Params</div>
<div><!-- details --></div>
</div>
<profiler-line ng-repeat="node in rule.offendersObj.children" data-index="$index" node="node"></profiler-line>
</div>
</div>
<div ng-if="policyName === 'cssColors' && rule.offendersObj.count > 0">
<p>This is the colors palette, sized by total occurrences:</p>
<div class="colorPalette checker">
<div ng-repeat="offender in rule.offendersObj.palette" style="background-color: {{offender.color}}; width: {{offender.occurrences * 100 / rule.offendersObj.palette[0].occurrences}}%"><div>{{offender.color}} ({{offender.occurrences}} times)</div></div>
</div>
</div>
</div>
</div>
<div ng-if="policyName === 'totalWeight'">
<h3>Weight by MIME type</h3>
<div class="totalWeightPie">
<canvas class="chart chart-doughnut" chart-data="weightData" chart-labels="weightLabels" chart-options="weightOptions" chart-colors="weightColours"></canvas>
</div>
<div ng-repeat="type in weightLabels">
<h3>{{rule.offendersObj.list.byType[type].totalWeight | bytes}} of {{type}}</h3>
<div class="offendersTable">
<div ng-repeat="request in rule.offendersObj.list.byType[type].requests | orderBy:'-weight'" ng-if="request.weight > 0">
<div><url-link url="request.url" max-length="60"></url-link></div>
<div ng-class="{offenderProblem: request.weight > 102400}">{{request.weight | bytes}}</div>
</div>
</div>
</div>
</div>
<div ng-if="policyName === 'imageOptimization'">
<h3 ng-if="rule.value > 0">{{rule.value | bytes}} could be saved on <ng-pluralize count="rule.offendersObj.list.images.length" when="{'one': '1 image', 'other': '{} images'}"></ng-pluralize></h3>
<div class="imageOffenders">
<div ng-repeat="image in rule.offendersObj.list.images | orderBy:'-gain'">
<div>
Current file: <url-link url="image.url" max-length="50"></url-link>
<div><a href="{{image.url}}" target="_blank"><img ng-src="{{image.url | https}}" class="checker" /></a></div>
</div>
<div>
<p ng-if="!image.afterCompression">Current weight: {{image.original | bytes}}</p>
<p ng-if="image.afterCompression">Current weight: {{image.original | bytes}} ({{image.afterCompression | bytes}} gzipped)</p>
<p ng-if="image.lossless && image.afterOptimizationAndCompression">With a lossless optimization:<br/>{{image.afterOptimizationAndCompression | bytes}} gzipped (<b>-{{image.gain | bytes}}</b> gzipped)</p>
<p ng-if="image.lossless && !image.afterOptimizationAndCompression">With a lossless optimization:<br/>{{image.lossless | bytes}} <span ng-if="!image.lossy">(<b>-{{image.gain | bytes}}</b>)</span></p>
<p ng-if="image.lossy && image.afterOptimizationAndCompression">With a lossy optimization:<br/>{{image.afterOptimizationAndCompression | bytes}} gzipped (<b>-{{image.gain | bytes}} gzipped</b>)</p>
<p ng-if="image.lossy && !image.afterOptimizationAndCompression">With a lossy optimization:<br/>{{image.lossy | bytes}} (<b>-{{image.gain | bytes}}</b>)</p>
</div>
</div>
</div>
</div>
<div ng-if="policyName === 'gzipCompression'">
<h3 ng-if="rule.value > 0">{{rule.value | bytes}} could be saved on <ng-pluralize count="rule.offendersObj.list.images.length" when="{'one': '1 file', 'other': '{} files'}"></ng-pluralize></h3>
<div class="table">
<div class="headers">
<div>File</div>
<div>Current weight</div>
<div>Gzipped</div>
<div>Gain</div>
</div>
<div ng-repeat="file in rule.offendersObj.list.files | orderBy:'-gain'">
<div>
<url-link url="file.url" max-length="70"></url-link>
</div>
<div>{{file.original | bytes}}</div>
<div>{{file.gzipped | bytes}}</div>
<div><b>-{{file.gain | bytes}}</b></div>
</div>
</div>
</div>
<div ng-if="policyName === 'fileMinification'">
<h3 ng-if="rule.value > 0">{{rule.value | bytes}} could be saved on <ng-pluralize count="rule.offendersObj.list.files.length" when="{'one': '1 file', 'other': '{} files'}"></ng-pluralize></h3>
<div class="table">
<div class="headers">
<div>File</div>
<div>Current weight</div>
<div>Minified</div>
<div>Gain</div>
</div>
<div ng-repeat="file in rule.offendersObj.list.files | orderBy:'-gain'">
<div>
<url-link url="file.url" max-length="70"></url-link>
</div>
<div ng-if="!file.afterCompression">{{file.original | bytes}} (gzipped)</div>
<div ng-if="file.afterCompression">{{file.original | bytes}} ({{file.afterCompression | bytes}} gzipped)</div>
<div ng-if="!file.afterCompression">{{file.afterOptimizationAndCompression | bytes}} (gzipped)</div>
<div ng-if="file.afterCompression">{{file.optimized | bytes}} ({{file.afterOptimizationAndCompression | bytes}} gzipped)</div>
<div><b>-{{file.gain | bytes}}</b></div>
</div>
</div>
</div>
<div ng-if="policyName === 'totalRequests'">
<h3>Requests by MIME type</h3>
<div ng-repeat="(type, requests) in rule.offendersObj.list.byType">
<h3><ng-pluralize count="requests.length" when="{'0': 'No ' + type + ' request', 'one': '1 ' + type + ' request', 'other': '{} ' + type + ' requests'}"></ng-pluralize></h3>
<p ng-if="type == 'css' && requests.length > 2">Reduce the number of stylesheets by concatenating them.</p>
<p ng-if="type == 'js' && requests.length > 3">Reduce the number of scripts by concatenating them.</p>
<p ng-if="type == 'image' && requests.length > 5">Reduce the number of images by lazyloading them or by spriting them.</p>
<p ng-if="type == 'webfont' && requests.length > 1">Fonts are generally loaded on the critical path of the head. Load as few as possible.</p>
<p ng-if="type == 'other' && requests.length > 0">They can be Flash, XML, music or any undetected format.</p>
<div class="offendersTable">
<div ng-repeat="request in requests track by $index">
<div><url-link url="request" max-length="100"></url-link></div>
</div>
</div>
</div>
</div>
<div ng-if="policyName === 'identicalFiles'">
<div ng-repeat="offender in rule.offendersObj.list track by $index">
<h4>A file of {{offender.weight | bytes}} is loaded {{offender.urls.length}} times:</h4>
<div class="offendersTable">
<div ng-repeat="url in offender.urls">
<div><url-link url="url" max-length="100"></url-link></div>
</div>
</div>
</div>
</div>
<div ng-if="policyName === 'smallRequests'">
<div ng-repeat="(type, requests) in rule.offendersObj.list.byType">
<h3><ng-pluralize count="requests.length" when="{'0': 'small ' + type + ' file', 'one': '1 small ' + type + ' file', 'other': '{} small ' + type + ' files'}"></ng-pluralize></h3>
<p ng-if="type == 'css' && requests.length > 0">Try to inline these styles in the head of the HTML or to merge them with other files.</p>
<p ng-if="type == 'js' && requests.length > 0">Try to inline these scripts in the HTML or merge them with other files.</p>
<p ng-if="type == 'image' && requests.length > 0">Try to inline these images (with base64 encoding for most image types except SVG that don't need base64 encoding). You can also create sprites.</p>
<div class="table">
<div class="headers">
<div ng-if="type == 'image'">Preview</div>
<div>File</div>
<div>Weight (bytes)</div>
</div>
<div ng-repeat="request in requests track by $index">
<div ng-if="type == 'image'"><img ng-src="{{request.url | https}}" class="smallPreview checker" /></div>
<div><url-link url="request.url" max-length="100"></url-link></div>
<div>{{request.size}}</div>
</div>
</div>
</div>
</div>
<div ng-if="policyName === 'DOMaccesses'">
<h3>{{rule.value}} offenders</h3>
Please open the <a href="result/{{runId}}/timeline">JS timeline</a>
</div>
<div ng-if="policyName === 'queriesWithoutResults'">
<h3>{{rule.value}} offenders</h3>
Please open the <a href="result/{{runId}}/timeline#filter=queryWithoutResults">JS timeline, filtered by "Queries without results"</a>
</div>
<div ng-if="policyName === 'jQueryCallsOnEmptyObject'">
<h3>{{rule.value}} offenders</h3>
Please open the <a href="result/{{runId}}/timeline#filter=jQueryCallOnEmptyObject">JS timeline, filtered by "jQuery calls on empty object"</a>
</div>
<div ng-if="policyName === 'jQueryNotDelegatedEvents'">
<h3>{{rule.value}} offenders</h3>
Please open the <a href="result/{{runId}}/timeline#filter=eventNotDelegated">JS timeline, filtered by "Events not delegated"</a>
</div>
<div ng-if="policyName === 'cssBreakpoints'">
<div ng-if="rule.value > 0" class="cssBreakpointsGraph">
<h3>Breakpoints distribution graph</h3>
<canvas class="chart chart-line" chart-data="breakpointsData" chart-labels="breakpointsLabels" chart-options="breakpointsOptions" chart-colors="breakpointsColours" chart-series="breakpointsSeries" width="600" height="250"></canvas>
<h3>Breakpoints list</h3>
<div class="offendersTable">
<div ng-repeat="offender in rule.offendersObj | orderBy:'pixels'">
<div>Breakpoint <b>{{offender.breakpoint}}</b> involves <ng-pluralize count="offender.count" when="{'one': '1 rule', 'other': '{} rules'}"></ng-pluralize></div>
</div>
</div>
</div>
<div ng-if="rule.value === 0">
No breakpoint
</div>
</div>
<div ng-if="policyName === 'heavyFonts'">
<div ng-repeat="font in rule.offendersObj.fonts | orderBy:'-weight' track by $index">
<h3><url-link url="font.url" max-length="80"></url-link></h3>
<div class="offendersTable">
<div>
<div>Weight</div>
<div ng-if="font.weight <= 40960">{{font.weight | bytes}}</div>
<div ng-if="font.weight > 40960" class="offenderProblem">{{font.weight | bytes}}</div>
</div>
<div>
<div>Number of glyphs</div>
<div ng-if="font.numGlyphs <= 500">{{font.numGlyphs}}</div>
<div ng-if="font.numGlyphs > 500" class="offenderProblem">{{font.numGlyphs}} (better &lt; 500)</div>
</div>
<div>
<div>Average glyph complexity</div>
<div ng-if="font.averageGlyphComplexity <= 35">{{font.averageGlyphComplexity}}</div>
<div ng-if="font.averageGlyphComplexity > 35" class="offenderProblem">{{font.averageGlyphComplexity}} (better &lt; 35)</div>
</div>
</div>
</div>
</div>
<div ng-if="policyName === 'unusedUnicodeRanges'">
<div ng-repeat="font in rule.offendersObj.fonts | orderBy:'-compressedWeigth' track by $index">
<h3><url-link url="font.url" max-length="60"></url-link> ({{font.weight | bytes}})</h3>
<div ng-if="font.isIconFont" class="offendersTable">
<div>
<div>
This font seems to be an icon font
<span ng-if="font.numGlyphsInCommonWithPageContent / font.glyphs <= 0.05" class="offenderProblem">but only {{font.numGlyphsInCommonWithPageContent}} of its {{font.glyphs}} glyphs <ng-pluralize count="font.numGlyphsInCommonWithPageContent" when="{'one': 'is', 'other': 'are'}"></ng-pluralize> possibly used!</span>
<span ng-if="font.numGlyphsInCommonWithPageContent / font.glyphs > 0.05">and {{font.numGlyphsInCommonWithPageContent}} of its {{font.glyphs}} glyphs <ng-pluralize count="font.numGlyphsInCommonWithPageContent" when="{'one': 'is', 'other': 'are'}"></ng-pluralize> possibly used.</span>
</div>
</div>
</div>
<div ng-if="!font.isIconFont" class="offendersTable">
<div ng-repeat="range in font.unicodeRanges track by $index">
<div><b>{{range.name}}</b></div>
<div ng-if="!range.underused">{{range.numGlyphsInCommonWithPageContent}} of its {{range.charset.length}} glyphs <ng-pluralize count="range.numGlyphsInCommonWithPageContent" when="{'one': 'is', 'other': 'are'}"></ng-pluralize> possibly used</div>
<div ng-if="range.underused" class="offenderProblem">{{range.numGlyphsInCommonWithPageContent}} of its {{range.charset.length}} glyphs are used</div>
<div>
<div class="offenderButton opens">
glyphes list
<div>{{range.charset | addSpaces}}</div>
</div>
</div>
</div>
<div ng-if="font.ligaturesOrHiddenChars > 0">
<div><b>Ligatures or hidden chars</b></div>
<div ng-class="{offenderProblem: (font.ligaturesOrHiddenChars > 25)}">{{font.ligaturesOrHiddenChars}} glyphs</div>
<div></div>
</div>
</div>
</div>
</div>
<div ng-if="policyName === 'http2'">
<h3>Protocols advertised by the server</h3>
<div class="offendersTable">
<div ng-repeat="protocol in rule.offendersObj.list">
<div>{{protocol}}</div>
</div>
<div ng-if="!rule.offendersObj || rule.offendersObj.count == 0">
<div>none</div>
</div>
</div>
</div>
<div ng-if="!rule && rule !== null" class="notFound">
<h2>404</h2>
Rule "{{policyName}}"" not found
</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;
+246 -354
View File
@@ -15,9 +15,9 @@ var policies = {
"tool": "phantomas",
"label": "DOM max depth",
"message": "<p>A deep DOM makes the CSS matching with DOM elements difficult.</p><p>It also slows down JavaScript modifications to the DOM because changing the dimensions of an element makes the browser re-calculate the dimensions of it's parents. Same thing for JavaScript events, that bubble up to the document root.</p>",
"isOkThreshold": 12,
"isBadThreshold": 22,
"isAbnormalThreshold": 30,
"isOkThreshold": 15,
"isBadThreshold": 25,
"isAbnormalThreshold": 32,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
var domArrays = offenders.map(offendersHelpers.domPathToArray);
@@ -32,89 +32,38 @@ var policies = {
"tool": "phantomas",
"label": "Number of iframes",
"message": "<p>iFrames are the most complex HTML elements. They are pages, just like the main page, and the browser needs to create a new page context, which has a cost.</p>",
"isOkThreshold": 3,
"isOkThreshold": 4,
"isBadThreshold": 15,
"isAbnormalThreshold": 30,
"hasOffenders": false
"hasOffenders": true
},
"DOMidDuplicated": {
"tool": "phantomas",
"label": "IDs duplicated",
"message": "<p>IDs of HTML elements must be document-wide unique. This can cause problems with getElementById returning the wrong element.</p>",
"isOkThreshold": 0,
"isBadThreshold": 5,
"isBadThreshold": 10,
"isAbnormalThreshold": 50,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return {
count: offenders.length,
list: offenders.map(function(offender) {
var parts = /^(.*): ?(\d+) ?occurrences$/.exec(offender);
if (!parts) {
debug('DOMidDuplicated offenders transform function error with "%s"', offender);
return {
parseError: offender
};
}
return {
id: parts[1],
occurrences: parseInt(parts[2], 10)
};
})
};
}
"hasOffenders": true
},
"DOMaccesses": {
"tool": "jsExecutionTransformer",
"label": "DOM access",
"message": "<p>This metric counts the number of calls to DOM related functions (both native DOM functions and jQuery functions) on page load.</p><p>The more your JavaScript code accesses the DOM, the slower the page will load.</p><p>Try, as much as possible, to have an HTML page fully generated by the server instead of making changes with JS.</p><p>Try to reduce the number of queries by refactoring your JavaScript code.</p><p>Binding too many events also has a cost. Try to use <a href=\"https://learn.jquery.com/events/event-delegation/\" target=\"_blank\">event delegation</a> as much as possible.</p>",
"isOkThreshold": 200,
"scriptDuration": {
"tool": "phantomas",
"label": "Total JS execution time",
"message": "<p>This is the number of milliseconds spent by the browser on JavaScript execution during page load.</p><p>For more details, try using the performance tab in Chrome DevTools. It is a bit complicated at first sight, but you'll be able to analyze exactly where this execution time is spent.</p>",
"isOkThreshold": 500,
"isBadThreshold": 2000,
"isAbnormalThreshold": 4000,
"hasOffenders": false
"hasOffenders": false,
"unit": "ms"
},
"queriesWithoutResults": {
"tool": "jsExecutionTransformer",
"label": "Queries without result",
"message": "<p>Number of queries that return no result. Both native and jQuery DOM requests are counted.</p><p>It suggests the query is not used on the page, probably because it is some dead code.</p><p>Or maybe the code is trying to find an HTML block that is not always here. Look at the JS Timeline to see if the scripts correctly figures out the HTML block is not here and immediatly stops interacting further with the DOM.</p>",
"isOkThreshold": 0,
"isBadThreshold": 150,
"isAbnormalThreshold": 250,
"hasOffenders": false
},
"DOMqueriesAvoidable": {
"tool": "phantomas",
"label": "Duplicated DOM queries",
"message": "<p>This is the number of queries that could be avoided by removing all duplicated queries.</p><p>Simply save the result of a query in a variable. Ok it is not always simple, especially with third-party scripts, but at least do it with your own code.</p>",
"isOkThreshold": 0,
"isBadThreshold": 300,
"isAbnormalThreshold": 600,
"hasOffenders": true,
"takeOffendersFrom": "DOMqueriesDuplicated",
"offendersTransformFn": function(offenders) {
return {
count: offenders.length,
list: offenders.map(function(offender) {
var parts = /^[^"]* ?"(.*)" ?with ?(.*) ?\(in ?context ?(.*)\): ?(.*)\s?queries$/.exec(offender);
if (!parts) {
debug('DOMqueriesAvoidable offenders transform function error with "%s"', offender);
return {
parseError: offender
};
}
return {
query: parts[1],
context: offendersHelpers.domPathToDomElementObj(parts[3]),
fn: parts[2],
count: parseInt(parts[4], 10)
};
})
};
}
"DOMaccesses": {
"tool": "domAccessAgregator",
"label": "DOM access",
"message": "<p>This metric estimates the number of times the JavaScript reads, changes or binds the DOM.</p><p>The more your JavaScript code accesses the DOM, the slower the page will load.</p><p>Try, as much as possible, to have an HTML page fully generated by the server instead of making changes with JS.</p><p>Try to reduce the number of queries by refactoring your JavaScript code.</p><p>Binding too many events also has a cost.</p>",
"isOkThreshold": 500,
"isBadThreshold": 2500,
"isAbnormalThreshold": 5000,
"hasOffenders": true
},
"eventsScrollBound": {
"tool": "phantomas",
@@ -147,25 +96,13 @@ var policies = {
};
}
},
"DOMaccessesOnScroll": {
"tool": "jsExecutionTransformer",
"label": "DOM access on scroll",
"message": "<p>This rule counts the number of DOM-accessing functions calls on a scroll event, such as queries, readings, writings, bindings and jQuery functions.</p><p>Two scroll events are triggered quickly, one after the other, and only the second one is analyzed so throttled functions are ignored.</p><p>One of the main reasons of a poor scrolling experience is when too much JS is executed on each scroll event. Note that some devices such as smartphones and MacBooks send more scroll events than others.</p><p>Reduce the number of DOM accesses inside scroll listeners. Put DOM queries outside them when possible. Use <a href=\"http://blogorama.nerdworks.in/javascriptfunctionthrottlingan/\" target=\"_blank\">throttling or debouncing</a>.</p>",
"isOkThreshold": 1,
"isBadThreshold": 20,
"isAbnormalThreshold": 35,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return offenders;
}
},
"jsErrors": {
"tool": "phantomas",
"label": "JavaScript errors",
"message": "<p>Just to let you know there are some errors on the page.</p><p><b>Please note that some errors only occur in the PhantomJS browser, so you might need to double check on other browsers.</b></p>",
"message": "<p>Just to let you know there are some errors on the page.</p>",
"isOkThreshold": 0,
"isBadThreshold": 1,
"isAbnormalThreshold": 4,
"isAbnormalThreshold": 5,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return {
@@ -195,8 +132,8 @@ var policies = {
"label": "document.write calls",
"message": "<p>They slow down the page construction, especially if they are used to insert scripts in the page. Remove them ASAP.</p><p>If you cannot remove them because they come from a third-party script (such as ads), have a look at <a href=\"https://github.com/krux/postscribe\" target=\"_blank\">PostScribe</a>.</p>",
"isOkThreshold": 0,
"isBadThreshold": 3,
"isAbnormalThreshold": 8,
"isBadThreshold": 2,
"isAbnormalThreshold": 6,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return {
@@ -250,22 +187,13 @@ var policies = {
"isAbnormalThreshold": 1,
"hasOffenders": true
},
"consoleMessages": {
"tool": "phantomas",
"label": "Console messages",
"message": "<p>Try to keep your console clean when in production. Debugging is good for development only.</p><p>Writing in the console has a cost, especially when dumping large object variables.</p><p>There is also a problem with Internet Explorer 8, not knowing the console object.</p>",
"isOkThreshold": 3,
"isBadThreshold": 20,
"isAbnormalThreshold": 50,
"hasOffenders": false
},
"globalVariables": {
"tool": "phantomas",
"label": "Global variables",
"message": "<p>It is a bad practice because they clutter up the global namespace. If two scripts use the same variable name in the global scope, it can cause conflicts and it is generally hard to debug.</p><p>Global variables also take a (very) little bit longer to be accessed than variables in the local scope of a function.</p>",
"isOkThreshold": 40,
"isBadThreshold": 200,
"isAbnormalThreshold": 700,
"isOkThreshold": 20,
"isBadThreshold": 300,
"isAbnormalThreshold": 800,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return {
@@ -276,7 +204,7 @@ var policies = {
},
"jQueryVersion": {
"label": "jQuery version",
"message": "<p>The current latest version of jQuery is 3.3</p><p>Each new version of jQuery optimizes performances. Do not keep an old version of jQuery. Updating can sometimes break a few things, but it is generally quite easy to fix them up. So don't hesitate.</p>",
"message": "<p>The current latest version of jQuery is 3.5</p><p>Each new version of jQuery optimizes performances. Do not keep an old version of jQuery. Updating can sometimes break a few things, but it is generally quite easy to fix them up. So don't hesitate.</p>",
"hasOffenders": false,
"scoreFn": function(data) {
var differentVersions = data.toolsResults.phantomas.metrics.jQueryVersionsLoaded;
@@ -291,6 +219,8 @@ var policies = {
if (value.indexOf('3.3.') === 0 ||
value.indexOf('3.4.') === 0 ||
value.indexOf('3.5.') === 0 ||
value.indexOf('3.6.') === 0 ||
value.indexOf('3.7.') === 0 ||
value.indexOf('4.0.') === 0) {
score = 100;
} else if (value.indexOf('3.2.') === 0) {
@@ -345,76 +275,45 @@ var policies = {
"message": "<p>jQuery is a heavy library. You should <b>never</b> load jQuery more than once on the same page.</p>",
"isOkThreshold": 1,
"isBadThreshold": 2,
"isAbnormalThreshold": 2,
"isAbnormalThreshold": 3,
"hasOffenders": true
},
"jQueryCallsOnEmptyObject": {
"tool": "jsExecutionTransformer",
"label": "Calls on empty objects",
"message": "<p>This metric counts the number of jQuery functions called on an empty jQuery object. The call was useless.</p><p>This can be helpful to detect dead or unused code.</p>",
"isOkThreshold": 1,
"isBadThreshold": 100,
"isAbnormalThreshold": 180,
"hasOffenders": false
},
"jQueryNotDelegatedEvents": {
"tool": "jsExecutionTransformer",
"label": "Events not delegated",
"message": "<p>This is the number of events that are bound with the .bind() or the .on() function without using <a href=\"https://learn.jquery.com/events/event-delegation/\" target=\"_blank\">event delegation</a>.</p><p>This means jQuery binds each element contained in the object one by one. This is bad for performance.</p>",
"isOkThreshold": 1,
"isBadThreshold": 100,
"isAbnormalThreshold": 180,
"hasOffenders": false
},
"cssParsingErrors": {
"tool": "phantomas",
"label": "CSS syntax error",
"message": "<p>Yellow Lab Tools failed to parse a CSS file. I doubt the problem comes from the css parser.</p><p>Maybe a <a href=\"http://jigsaw.w3.org/css-validator\" target=\"_blank\">CSS validator</a> can help you.</p>",
"isOkThreshold": 0,
"isBadThreshold": 1,
"isBadThreshold": 2,
"isAbnormalThreshold": 20,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return {
count: offenders.length,
list: offenders.map(function(offender) {
if (offender === '[inline CSS] (Empty CSS was provided)') {
if (offender === '[inline CSS]') {
return {
error: 'Empty style tag',
error: 'Empty <style> tag',
file: null,
line: null,
column: null
};
}
var parts = /^(?:(?:<([^ \(]*)>|\[inline CSS\]) ?)?(?:\((((?! @ ).)*)(?: @ (\d+):(\d+))?\))?$/.exec(offender);
if (parts) {
if (offender.value) {
return {
error: parts[2] || 'Unknown parsing error' + (parts[1] ? '. The entire file was ignored. As a result, the other CSS metrics and scores are miscalculated.' : ''),
file: parts[1] || null,
line: (parts[4] && parts[5]) ? parseInt(parts[4], 10) : null,
column: (parts[4] && parts[5]) ? parseInt(parts[5], 10) : null
error: offender.value.message || 'Unknown parsing error. The entire file was ignored. As a result, the other CSS metrics and scores are miscalculated.',
file: offender.url || null,
line: offender.value.position ? offender.value.position.start.line : null,
column: offender.value.position ? offender.value.position.start.column : null
};
} else {
return {
error: 'Unknown parsing error. The entire file was ignored. As a result, the other CSS metrics and scores are miscalculated.',
file: offender,
line: null,
column: null
};
}
// Try another syntax
parts = /^(.*) <(.*)> @ (\d+):(\d+)$/.exec(offender);
if (parts) {
return {
error: parts[1] || 'Unknown parsing error',
file: parts[2] || null,
line: parseInt(parts[3], 10),
column: parseInt(parts[4], 10)
};
}
debug('cssParsingErrors offenders transform function error with "%s"', offender);
return {
parseError: offender
};
})
};
}
@@ -423,51 +322,18 @@ var policies = {
"tool": "phantomas",
"label": "Rules count",
"message": "<p>Having a huge number of CSS rules hurts performances. If the number of CSS rules is higher than the number of DOM elements, there is clearly a problem.</p><p>Huge stylesheets generally occur when the different pages of a website load all the CSS, concatenated in a single stylesheet, even if a large part of the rules are page-specific. Solution is to create one main CSS file with global rules and one custom file per page.</p>",
"isOkThreshold": 1000,
"isBadThreshold": 3000,
"isAbnormalThreshold": 4500,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
var hasInline = false;
var inlineCount = 0;
var files = [];
offenders.forEach(function(line) {
if (line.indexOf('[inline CSS]: ') === 0) {
hasInline = true;
inlineCount += parseInt(line.substr(14));
} else {
var parts = /^<(.*)>: (\d+)$/.exec(line);
if (parts) {
files.push({
file: parts[1],
rules: parseInt(parts[2], 10)
});
}
}
});
if (hasInline) {
files.push({
file: 'inline CSS',
rules: inlineCount
});
}
return {
count: files.length,
list: files
};
}
"isOkThreshold": 2000,
"isBadThreshold": 5000,
"isAbnormalThreshold": 8000,
"hasOffenders": true
},
"cssComplexSelectors": {
"tool": "phantomas",
"label": "Complex selectors",
"message": "<p>Complex selectors are CSS selectors with 4 or more expressions, like \"#header ul li .foo\".</p><p>They are adding more work for the browser, and this could be avoided by simplifying selectors. The <a href=\"http://getbem.com\" target=\"_blank\">B.E.M. methodology</a> is an useful way to simplify your CSS.</p>",
"message": "<p>Complex selectors are CSS selectors with 4 or more expressions, like \"#header ul li .foo\".</p><p>They are adding more work for the browser, and this could be avoided by simplifying selectors. The <a href=\"http://getbem.com\" target=\"_blank\">B.E.M. methodology</a> is a useful way to simplify your CSS.</p>",
"isOkThreshold": 0,
"isBadThreshold": 800,
"isAbnormalThreshold": 2000,
"isBadThreshold": 1000,
"isAbnormalThreshold": 24000,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
var parsedOffenders = offenders.map(function(offender) {
@@ -482,18 +348,18 @@ var policies = {
"tool": "phantomas",
"label": "Colors count",
"message": "<p>This is the number of different colors defined in CSS.</p><p>Your CSS will be easier to maintain if you keep a small color set.</p>",
"isOkThreshold": 50,
"isBadThreshold": 150,
"isAbnormalThreshold": 400,
"isOkThreshold": 100,
"isBadThreshold": 200,
"isAbnormalThreshold": 500,
"hasOffenders": true,
"offendersTransformFn": function(offenders, ruleObject) {
var deduplicatedObj = {};
offenders.map(function(offender) {
var parts = /^([^ ]*) \((\d+) times\)$/.exec(offender);
var parts = /^([^ ]*) \((\d+) times\)$/.exec(offender.value.message);
if (!parts) {
debug('cssColors offenders transform function error with "%s"', offender);
debug('cssColors offenders transform function error with "%s"', offender.value.message);
return;
}
@@ -529,8 +395,8 @@ var policies = {
"label": "Similar colors",
"message": "<p>This is the list of colors found in the stylesheets, that are very close to each other. The eye can barely see the difference.</p><p>Use this list to reduce the number of colors in your palette, it will be easier to maintain.</p>",
"isOkThreshold": 0,
"isBadThreshold": 40,
"isAbnormalThreshold": 80,
"isBadThreshold": 60,
"isAbnormalThreshold": 120,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return {
@@ -543,9 +409,9 @@ var policies = {
"tool": "mediaQueriesChecker",
"label": "Breakpoints count",
"message": "<p>This is the number of different breakpoints found in the stylesheets' media queries.</p><p>Please note this rule is based on <i>min-width</i>, <i>max-width</i>, <i>min-device-width</i> and <i>max-device-width</i> media queries only.</p><p>Your CSS will be easier to maintain if you keep a reasonable number of breakpoints. Try to make a fluid design - using percents - to avoid the creation of numerous breakpoints.</p>",
"isOkThreshold": 6,
"isBadThreshold": 40,
"isAbnormalThreshold": 60,
"isOkThreshold": 10,
"isBadThreshold": 50,
"isAbnormalThreshold": 80,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
var offendersTable = [];
@@ -565,9 +431,9 @@ var policies = {
"tool": "mediaQueriesChecker",
"label": "Not mobile-first media queries",
"message": "<p>This is the number of CSS rules inside media queries that address small screens.</p><p>The common good practice, when creating a responsive website, is to write it \"mobile-first\". More explanation in <a href=\"http://www.sitepoint.com/introduction-mobile-first-media-queries\" target=\"_blank\">this great article</a>.</p>",
"isOkThreshold": 50,
"isBadThreshold": 250,
"isAbnormalThreshold": 1000,
"isOkThreshold": 60,
"isBadThreshold": 400,
"isAbnormalThreshold": 1200,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return offendersHelpers.orderByFile(offenders);
@@ -596,12 +462,12 @@ var policies = {
"label": "Duplicated selectors",
"message": "<p>This is when two or more selectors are strictly identical and should be merged.</p>",
"isOkThreshold": 0,
"isBadThreshold": 50,
"isAbnormalThreshold": 100,
"isBadThreshold": 100,
"isAbnormalThreshold": 200,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
var parsedOffenders = offenders.map(function(offender) {
var parts = /^(.*) \((\d+) times\) ?<(.*)>$/.exec(offender);
var parts = /^(.*) \((\d+) times\)$/.exec(offender.value.message);
if (!parts) {
debug('cssDuplicatedSelectors offenders transform function error with "%s"', offender);
@@ -613,7 +479,7 @@ var policies = {
return {
rule: parts[1],
occurrences: parseInt(parts[2], 10),
file: parts[3]
file: offender.value.url
};
});
@@ -625,8 +491,8 @@ var policies = {
"label": "Duplicated properties",
"message": "<p>This is the number of property definitions duplicated within a selector.</p>",
"isOkThreshold": 0,
"isBadThreshold": 60,
"isAbnormalThreshold": 120,
"isBadThreshold": 100,
"isAbnormalThreshold": 200,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
var parsedOffenders = offenders.map(function(offender) {
@@ -659,54 +525,20 @@ var policies = {
"message": "<p>Very easy to fix: remove all empty rules.</p>",
"isOkThreshold": 0,
"isBadThreshold": 50,
"isAbnormalThreshold": 100,
"isAbnormalThreshold": 120,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
var parsedOffenders = offenders.map(offendersHelpers.cssOffenderPattern);
return offendersHelpers.orderByFile(parsedOffenders);
}
},
"cssExpressions": {
"tool": "phantomas",
"label": "CSS expressions",
"message": "<p>Such as: expression( document.body.clientWidth > 600 ? \"600px\" : \"auto\" )</p><p>This is a bad practice as it slows down browsers. There are some simpler CSS3 methods for doing this.</p>",
"isOkThreshold": 0,
"isBadThreshold": 1,
"isAbnormalThreshold": 20,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
var parsedOffenders = offenders.map(function(offender) {
var splittedOffender = offendersHelpers.cssOffenderPattern(offender);
var parts = /^(.*) {([^ ]+): expression\((.*)\)}$/.exec(splittedOffender.css);
if (!parts) {
debug('cssExpressions offenders transform function error with "%s"', offender);
return {
parseError: offender
};
}
return {
rule: parts[1],
property: parts[2],
expression: parts[3],
file: splittedOffender.file,
line: splittedOffender.line,
column: splittedOffender.column
};
});
return offendersHelpers.orderByFile(parsedOffenders);
}
},
"cssImportants": {
"tool": "phantomas",
"label": "Uses of !important",
"message": "<p>It can be useful, but only as a last resort. It is a bad practice because it overrides the normal cascading logic. The more you use !important, the more you need it again to over-override. This conducts to a poor maintainability.</p>",
"isOkThreshold": 0,
"isBadThreshold": 75,
"isAbnormalThreshold": 200,
"isBadThreshold": 200,
"isAbnormalThreshold": 1000,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
var parsedOffenders = offenders.map(function(offender) {
@@ -739,8 +571,8 @@ var policies = {
"label": "Old IE fixes",
"message": "<p>What browser do you need to support? Once you've got the answer, take a look at these old rules that pollute your CSS code and remove them.</p><p>IE6:<ul><li>* html</li><li>html > body (everything but IE6)</li></ul><p><p>IE7:<ul><li><b>*</b>height: 123px;</li><li>height: 123px <b>!ie</b>;</li></ul><p><p>IE9:<ul><li>-ms-filter</li><li>progid:DXImageTransform.Microsoft</li></ul></p>",
"isOkThreshold": 0,
"isBadThreshold": 50,
"isAbnormalThreshold": 150,
"isBadThreshold": 100,
"isAbnormalThreshold": 300,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
var parsedOffenders = offenders.map(function(offender) {
@@ -795,8 +627,8 @@ var policies = {
"label": "Old prefixes",
"message": "<p>Many property prefixes such as -moz- or -webkit- are not needed anymore, or by very few people. Sometimes, they have never even existed. You can remove them or replace them with the non-prefixed version. This will help reducing your stylesheets weight.</p><p>The prefixes database comes from <a href=\"http://caniuse.com/\" target=\"_blank\">Can I Use</a>.</p>",
"isOkThreshold": 0,
"isBadThreshold": 50,
"isAbnormalThreshold": 200,
"isBadThreshold": 100,
"isAbnormalThreshold": 400,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
var properties = {};
@@ -848,8 +680,8 @@ var policies = {
"label": "Redundant body selectors",
"message": "<p>This is one way to remove complexity from a CSS rule. Generally, when \"body\" is specified in a rule it can be removed, because an element is necessarily inside the body.</p>",
"isOkThreshold": 0,
"isBadThreshold": 60,
"isAbnormalThreshold": 200,
"isBadThreshold": 120,
"isAbnormalThreshold": 400,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
var parsedOffenders = offenders.map(function(offender) {
@@ -866,8 +698,8 @@ var policies = {
"label": "Redundant tags selectors",
"message": "<p>Some tags included inside other tags are obvious. For example, when \"ul li\" is specified in a rule, \"ul\" can be removed because the \"li\" tag is nearly always inside an \"ul\" container (the \"ol\" container is quite rare). Same thing for \"tr td\", \"select option\", ...</p><p>Lowering compexity in CSS selectors can make the page load a little faster.</p>",
"isOkThreshold": 0,
"isBadThreshold": 60,
"isAbnormalThreshold": 200,
"isBadThreshold": 120,
"isAbnormalThreshold": 400,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
var parsedOffenders = offenders.map(function(offender) {
@@ -897,41 +729,115 @@ var policies = {
"totalWeight": {
"tool": "redownload",
"label": "Total weight",
"message": "<p>The weight is of course very important if you want the page to load fast. Try to stay under 1MB, which is already very long to download over a slow connection.</p><p>Please note that Yellow Lab Tools' engine (PhantomJS) is not compatible with image srcset (unless you use a polyfill). This can lead to incorrect page weight.</p>",
"isOkThreshold": 1048576,
"isBadThreshold": 2621440,
"isAbnormalThreshold": 4194304,
"message": "<p>The weight is of course very important if you want the page to load fast. Try to stay under 1MB, which is already very long to download over a slow connection.</p>",
"isOkThreshold": 1572864,
"isBadThreshold": 3145728,
"isAbnormalThreshold": 5242880,
"hasOffenders": true,
"unit": 'bytes'
"unit": "bytes"
},
"imageOptimization": {
"tool": "redownload",
"imagesNotOptimized": {
"tool": "phantomas",
"label": "Image optimization",
"message": "<p>This metric measures the number of bytes that could be saved by optimizing images.</p><p>Image optimization is generally one of the easiest way to reduce a page weight, and as a result, the page load time. Don't use Photoshop or other image editing tools, they're not very good for optimization. Use specialized tools such as <a href=\"https://kraken.io/\" target=\"_blank\">Kraken.io</a> or the excellent <a href=\"https://imageoptim.com/\" target=\"_blank\">ImageOptim</a> on Mac. For SVG images, you can use <a href=\"https://jakearchibald.github.io/svgomg/\" target=\"_blank\">SVGOMG</a></p><p>The tools in use in YellowLabTools are not set to their maximum optimization power (JPEG quality 85), so you might be able to compress even more!</p><p>Please note that Yellow Lab Tools' engine (PhantomJS) is not compatible with image srcset (unless you use a polyfill). This can lead to incorrect page weight.</p>",
"isOkThreshold": 20480,
"message": "<p>This metric measures the number of bytes that could be saved by optimizing images.</p><p>Image optimization is generally one of the easiest way to reduce a page weight, and as a result, the page load time. Don't use Photoshop or other image editing tools, they're not very good for optimization. Use specialized tools such as <a href=\"https://kraken.io/\" target=\"_blank\">Kraken.io</a> or the excellent <a href=\"https://imageoptim.com/\" target=\"_blank\">ImageOptim</a> on Mac. For SVG images, you can use <a href=\"https://jakearchibald.github.io/svgomg/\" target=\"_blank\">SVGOMG</a>.</p><p>The tools in use in YellowLabTools are not set to their maximum optimization power, so you might be able to compress even more!</p>",
"isOkThreshold": 2048,
"isBadThreshold": 204800,
"isAbnormalThreshold": 307200,
"hasOffenders": true,
"unit": 'bytes'
"unit": "bytes",
"valueTransformFn": function(offenders) {
let totalGain = 0;
offenders.forEach((offender) => {
offender.gain = offender.fileSize - offender.newFileSize;
totalGain += offender.gain;
});
return totalGain;
},
"offendersTransformFn": function(offenders) {
return offenders;
}
},
"imagesTooLarge": {
"tool": "redownload",
"imagesOldFormat": {
"tool": "phantomas",
"label": "Old image formats",
"message": "<p>This metric goes further than \"Image optimization\". Measures the number of bytes that could be saved by converting images to newer and more efficient formats. The best image format is generally AVIF and the second best is WebP.</p><p>Be careful, you need to provide fallback images for old browsers and search engine bots.</p>",
"isOkThreshold": 2048,
"isBadThreshold": 512000,
"isAbnormalThreshold": 819200,
"hasOffenders": true,
"unit": "bytes",
"valueTransformFn": function(offenders) {
let totalGain = 0;
offenders.forEach((offender) => {
offender.gain = offender.fileSize - offender.newFileSize;
totalGain += offender.gain;
});
return totalGain;
},
"offendersTransformFn": function(offenders) {
return offenders;
}
},
"imagesScaledDown": {
"tool": "phantomas",
"label": "Oversized images",
"message": "<p>This is the number of images with a width >800px on mobile or >1500px on desktop. Try </p><p>Please ignore if the file is used as a sprite.</p><p>Please note that Yellow Lab Tools' engine (PhantomJS) is not compatible with image srcset (unless you use a polyfill). This can lead to incorrect detection.</p>",
"message": "<p>This rule compares the number of pixels in a loaded images to the number of physical pixels it is displayed on. Then it estimates the number of KB that could be saved by serving it with the correct dimensions.</p><p>Of course, it is hard to serve perfect images for all screens. For this reason, this rule is quite permissive.</p>",
"isOkThreshold": 2048,
"isBadThreshold": 307200,
"isAbnormalThreshold": 512000,
"hasOffenders": true,
"unit": "bytes",
"valueTransformFn": function(offenders) {
let totalGain = 0;
offenders.forEach((offender) => {
offender.gain = offender.fileSize - offender.newFileSize;
totalGain += offender.gain;
});
return totalGain;
},
"offendersTransformFn": function(offenders) {
return offenders;
}
},
"imagesExcessiveDensity": {
"tool": "phantomas",
"label": "Excessive image density",
"message": "<p>This metric measures the number of bytes that could be saved by going further than just resizing images.<p>Devices with very high pixel density screen (such as 3x or 4x) are programmed to load high density images. This is the normal behavior, however the human eye barely sees the difference over 2x. This metric alerts you if an image density is > 2.2x.</p><p>There is currently no browser functionnality to prevent the issue (for this reason its impact on global score is low). But you can build your own clever solution!</p>",
"isOkThreshold": 102400,
"isBadThreshold": 307200,
"isAbnormalThreshold": 512000,
"hasOffenders": true,
"unit": "bytes",
"valueTransformFn": function(offenders) {
let totalGain = 0;
offenders.forEach((offender) => {
offender.gain = offender.fileSize - offender.newFileSize;
totalGain += offender.gain;
});
return totalGain;
},
"offendersTransformFn": function(offenders) {
return offenders;
}
},
"imagesWithIncorrectSizesParam": {
"tool": "phantomas",
"label": "Incorrect sizes parameter",
"message": "<p>When using an adaptative image with a <i>srcset</i> attribute and <i>w</i> values, it is important to correctly set the <i>sizes</i> attribute. Otherwise, the browser might pick the wrong image in the <i>srcset</i>.</p><p>The <a href=\”https://chrome.google.com/webstore/detail/responsive-image-linter/mnddginionlghpblkimpdalcecpnbjln\" target=\"_blank\">Responsive Image Linter</a> extension for Chrome can help you further.</p>",
"isOkThreshold": 0,
"isBadThreshold": 5,
"isAbnormalThreshold": 10,
"hasOffenders": true
},
"gzipCompression": {
"compression": {
"tool": "redownload",
"label": "Gzip compression",
"message": "<p>Measures the number of bytes that could be saved by compressing file transfers.</p><p>Gzip is a powerfull weight reducer and should be enabled on text-based assets in your server's configuration. Note that gzipping small files (< 1 KB) is arguable, and that some assets such as images should not be gzipped as they are already compressed. <a href=\"https://gist.github.com/gmetais/971ce13a1fbeebd88445\" target=\"_blank\">Here</a> is a list of Content-Types that should be gzipped.</p>",
"isOkThreshold": 5125,
"isBadThreshold": 81920,
"isAbnormalThreshold": 153600,
"label": "Gzip/Brotli compression",
"message": "<p>Measures the number of bytes that could be saved by compressing textual files. Some files listed below might not be compressed at all, some might be already compressed with Gzip but would become even lighter with Brotli.</p><p>All major server systems are now compatible with Brotli.</p><p>Note that compressing small files (< 1 KB) is arguable, and that some assets such as images should not be compressed as it is already included in their format. <a href=\"https://letstalkaboutwebperf.com/en/gzip-brotli-server-config/\" target=\"_blank\">Here</a> is a list of MIME types that should be compressed.</p>",
"isOkThreshold": 20480,
"isBadThreshold": 204800,
"isAbnormalThreshold": 409600,
"hasOffenders": true,
"unit": 'bytes'
"unit": "bytes"
},
"fileMinification": {
"tool": "redownload",
@@ -941,45 +847,25 @@ var policies = {
"isBadThreshold": 61440,
"isAbnormalThreshold": 122880,
"hasOffenders": true,
"unit": 'bytes'
"unit": "bytes"
},
"totalRequests": {
"tool": "redownload",
"label": "Requests number",
"message": "<p>This is one of the most important performance rule. Every request is slowing down the page loading.</p><p>There are several technics to reduce their number:<ul><li>Concatenate JS files</li><li>Concatenate CSS files</li><li>Embed or inline small JS or CSS files in the HTML</li><li>Create sprites</li><li>Base64 encode small images in HTML or stylesheets</li><li>Use lazyloading for images</li></ul></p>",
"isOkThreshold": 20,
"isBadThreshold": 120,
"isAbnormalThreshold": 200,
"message": "<p>Each request slows down the page loading, especially on the protocol HTTP/1, but also a little on HTTP/2 or 3.</p><p>There are several technics to reduce their number:<ul><li>Concatenate JS files</li><li>Concatenate CSS files</li><li>Embed or inline small JS or CSS files in the HTML</li><li>Create sprites</li><li>Base64 encode small images in HTML or stylesheets</li><li>Use lazyloading for images</li></ul></p>",
"isOkThreshold": 80,
"isBadThreshold": 240,
"isAbnormalThreshold": 320,
"hasOffenders": true
},
"domains": {
"tool": "phantomas",
"label": "Different domains",
"message": "<p>For each domain met, the browser needs to make a DNS look-up, which is slow. Avoid having to many different domains and the page should render faster.</p><p>By the way, domain sharding is not a good practice anymore.</p>",
"isOkThreshold": 10,
"isBadThreshold": 25,
"isAbnormalThreshold": 50,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return {
count: offenders.length,
list: offenders.map(function(offender) {
var parts = /^([^ ]*): (\d+) request\(s\)$/.exec(offender);
if (!parts) {
debug('domains offenders transform function error with "%s"', offender);
return {
file: offender
};
}
return {
domain: parts[1],
requests: parseInt(parts[2])
};
})
};
}
"isOkThreshold": 12,
"isBadThreshold": 30,
"isAbnormalThreshold": 60,
"hasOffenders": true
},
"notFound": {
"tool": "phantomas",
@@ -1020,19 +906,10 @@ var policies = {
"isAbnormalThreshold": 5,
"hasOffenders": true
},
"smallRequests": {
"tool": "redownload",
"label": "Small requests",
"message": "<p>List of all requests that are less than 2 KB. Try to merge them with other files.</p>",
"isOkThreshold": 10,
"isBadThreshold": 50,
"isAbnormalThreshold": 80,
"hasOffenders": true
},
"lazyLoadableImagesBelowTheFold": {
"tool": "phantomas",
"label": "Below the fold images",
"message": "<p>This is the number of images displayed below the fold that could be lazy-loaded. This is an excellent way to accelerate the loading time of an heavy page.</p><p>I recommend using <a href=\"https://github.com/vvo/lazyload\" target=\"_blank\">this lazyloader</a>.</p>",
"message": "<p>This is the number of images displayed below the fold that could be lazy-loaded. This is an excellent way to accelerate the loading time of an heavy page.</p><p>I recommend using <a href=\"https://github.com/aFarkas/lazysizes\" target=\"_blank\">lazysizes</a> or <a href=\"https://github.com/ApoorvSaxena/lozad.js\" target=\"_blank\">Lozad</a>.</p>",
"isOkThreshold": 1,
"isBadThreshold": 12,
"isAbnormalThreshold": 30,
@@ -1053,7 +930,7 @@ var policies = {
"message": "<p>This is the number of custom web fonts loaded on the page.</p><p>Webfonts are beautiful, but heavy. You should keep their number as low as possible.</p>",
"isOkThreshold": 1,
"isBadThreshold": 5,
"isAbnormalThreshold": 7,
"isAbnormalThreshold": 8,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return offenders;
@@ -1062,11 +939,11 @@ var policies = {
"heavyFonts": {
"tool": "redownload",
"label": "Overweighted webfonts",
"message": "<p>This metric is the sum of all bytes above 40KB in loaded fonts. Over this size, the font is probably not optimized for the web.</p><p>It can be a compresson issue, a font that contains too many glyphs or a font with complex shapes.</p><p>Sorry, Yellow Lab Tools is not yet compatible with the WOFF2 font format that generates 20-30% smaller fonts. You can proceed to a manual verification on a modern browser.</p>",
"message": "<p>This metric is the sum of all bytes above 40KB in loaded fonts. Over this size, the font is probably not optimized for the web.</p><p>It can be a compresson issue, a font that contains too many glyphs or a font with complex shapes.</p>",
"isOkThreshold": 0,
"isBadThreshold": 102400,
"isAbnormalThreshold": 204800,
"unit": 'bytes',
"unit": "bytes",
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return offenders;
@@ -1084,42 +961,72 @@ var policies = {
return offenders;
}
},
"http2": {
"label": "HTTP/2 or SPDY",
"message": "<p>HTTP/2 is the latest version of the HTTP protocol and is designed to optimize load speed. SPDY is deprecated but still very well supported.</p><p>The latest versions of all major browsers are now compatible. The difficulty is on the server side, where technologies are not quite ready yet.</p>",
"nonWoff2Fonts": {
"tool": "redownload",
"label": "WOFF 2",
"message": "<p>The fonts listed here could be lighter if they were served with the latest WOFF 2 font file format. Some online tools can help you easily convert older formats to WOFF 2.</p>",
"isOkThreshold": 0,
"isBadThreshold": 51200,
"isAbnormalThreshold": 122880,
"hasOffenders": true,
"unit": "bytes"
},
"oldHttpProtocol": {
"label": "HTTP protocols",
"message": "<p>HTTP/2 is the latest version of the HTTP protocol. It is designed to optimize load speed. HTTP/3 will come soon and should be even faster!</p><p>When a domain sends more than 4 requests over HTTP/1, this metric counts one point for each new request. Below 5 requests, the benefits of HTTP/2 are generally less significant.</p>",
"hasOffenders": true,
"scoreFn": function(data) {
if (!data.toolsResults.http2) {
return null;
var count = 0;
var offenders = data.toolsResults.phantomas.offenders.oldHttpProtocol;
if (offenders) {
offenders.forEach(function(offender) {
if (offender.requests > 4) {
count += offender.requests - 4;
}
});
}
var isHttp2 = data.toolsResults.http2.metrics.http2;
var isOkThreshold = 0;
var isBadThreshold = 25;
var isAbnormalThreshold = 100;
var score = (isBadThreshold - count) * 100 / (isBadThreshold - isOkThreshold);
var abnormalityScore = (isAbnormalThreshold - count) * 100 / (isAbnormalThreshold - isOkThreshold);
var result = {
value: isHttp2 ? 'Yes' : 'No',
score: isHttp2 ? 100 : 0,
bad: !isHttp2,
abnormal: false,
abnormalityScore: 0
value: count,
score: Math.min(Math.max(Math.round(score), 0), 100),
bad: count > isBadThreshold,
abnormal: count > isAbnormalThreshold,
abnormalityScore: Math.min(Math.round(abnormalityScore), 0),
offendersObj: {
count: data.toolsResults.phantomas.metrics.oldHttpProtocol,
list: data.toolsResults.phantomas.offenders.oldHttpProtocol || []
}
};
if (data.toolsResults.http2.offenders) {
result.offendersObj = {
count: data.toolsResults.http2.offenders.http2.length,
list: data.toolsResults.http2.offenders.http2
};
}
return result;
}
},
"oldTlsProtocol": {
"tool": "phantomas",
"label": "TLS protocols",
"message": "<p>Counts the number of domains that use TLS versions < 1.3. This is the latest version and it includes a faster \"handshake\" technology.</p><p>The 1.0 and 1.1 versions are deprecated because they are unsafe, switch to 1.2 or above as soon as possible.</p><p>The version 1.3 includes an even faster option called 0-RTT, check your server compatibility on <a href='https://www.ssllabs.com/ssltest/' target=\"_blank\">SSL Labs</a>.</p>",
"isOkThreshold": 0,
"isBadThreshold": 5,
"isAbnormalThreshold": 15,
"hasOffenders": true
},
"cachingDisabled": {
"tool": "phantomas",
"label": "Caching disabled",
"message": "<p>Counts responses with caching disabled (max-age=0)</p><p>Fix immediatly if on static assets.</p>",
"isOkThreshold": 0,
"isBadThreshold": 12,
"isAbnormalThreshold": 25,
"isOkThreshold": 2,
"isBadThreshold": 15,
"isAbnormalThreshold": 30,
"hasOffenders": true
},
"cachingNotSpecified": {
@@ -1136,29 +1043,14 @@ var policies = {
"label": "Caching too short",
"message": "<p>Responses with too short caching time (less than a week).</p><p>The longer you cache, the better. Add versionning to your static assets, if it's not already done, and set their cache time to one year.</p>",
"isOkThreshold": 5,
"isBadThreshold": 20,
"isAbnormalThreshold": 40,
"isBadThreshold": 25,
"isAbnormalThreshold": 50,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return {
count: offenders.length,
list: offenders
.map(function(offender) {
var parts = /^([^ ]*) cached for (-?\d+(\.\d+)?) s$/.exec(offender);
if (!parts) {
debug('cachingTooShort offenders transform function error with "%s"', offender);
return {
file: offender
};
}
return {
file: parts[1],
ttl: Math.round(parseFloat(parts[2]))
};
}).sort(function(a, b) {
.sort(function(a, b) {
return a.ttl - b.ttl;
+42 -51
View File
@@ -1,24 +1,38 @@
{
"globalScore": {
"pageWeight": 2,
"images": 2,
"domComplexity": 1,
"javascriptComplexity": 2,
"badJavascript": 2,
"jQuery": 0.5,
"cssComplexity": 0.5,
"badCSS": 1,
"fonts": 1,
"serverConfig": 1
},
"categories": {
"pageWeight": {
"label": "Page weight",
"label": "Network",
"policies": {
"totalWeight": 5,
"imageOptimization": 2,
"imagesTooLarge": 2,
"gzipCompression": 2,
"fileMinification": 1
}
},
"requests": {
"label": "Requests",
"policies": {
"totalRequests": 5,
"totalRequests": 2,
"domains": 3,
"notFound": 3,
"compression": 2,
"fileMinification": 2,
"identicalFiles": 2,
"emptyRequests": 3,
"smallRequests": 1,
"notFound": 2
}
},
"images": {
"label": "Images",
"policies": {
"imagesNotOptimized": 2,
"imagesOldFormat": 1,
"imagesScaledDown": 2,
"imagesExcessiveDensity": 0.25,
"imagesWithIncorrectSizesParam": 1,
"lazyLoadableImagesBelowTheFold": 2,
"hiddenImages": 1
}
@@ -32,28 +46,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,
"synchronousXHR": 5,
"consoleMessages": 0.5,
"globalVariables": 0.5
}
},
@@ -61,9 +67,7 @@
"label": "jQuery",
"policies": {
"jQueryVersion": 2,
"jQueryVersionsLoaded": 2,
"jQueryCallsOnEmptyObject": 1,
"jQueryNotDelegatedEvents": 1
"jQueryVersionsLoaded": 2
}
},
"cssComplexity": {
@@ -80,13 +84,12 @@
"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,
"cssRedundantBodySelectors": 1,
@@ -97,33 +100,21 @@
"label": "Web fonts",
"policies": {
"fontsCount": 1,
"heavyFonts": 1,
"unusedUnicodeRanges": 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,
"cachingTooShort": 1
}
}
},
"globalScore": {
"pageWeight": 3,
"requests": 3,
"domComplexity": 2,
"domManipulations": 2,
"scroll": 1,
"badJavascript": 2,
"jQuery": 1,
"cssSyntaxError": 1,
"cssComplexity": 1,
"badCSS": 1,
"fonts": 1,
"serverConfig": 1
}
}
+10 -37
View File
@@ -117,7 +117,7 @@ var OffendersHelpers = function() {
for (var i=0 ; i<traceArray.length ; i++) {
// Handle the new PhantomJS 2.x syntax
parts = /^(([\w$]+)@)?([^ ]+):(\d+):(\d+)$/.exec(traceArray[i]);
parts = /^\s*at( (\w+))? \(?([^ ]+):(\d+):(\d+)\)?$$/.exec(traceArray[i]);
if (parts) {
obj = {
@@ -132,25 +132,6 @@ var OffendersHelpers = function() {
results.push(obj);
} else {
// Old syntax
parts = /^(([\w$]+) )?\(?([^ ]+):(\d+)\)?$/.exec(traceArray[i]);
if (parts) {
obj = {
file: parts[3],
line: parseInt(parts[4], 10)
};
if (parts[2]) {
obj.functionName = parts[2];
}
results.push(obj);
} else {
return null;
}
}
}
return results;
@@ -172,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) {
+10 -2
View File
@@ -57,9 +57,16 @@ var RulesChecker = function() {
data.toolsResults[policy.tool].offenders[metricName]) {
offenders = data.toolsResults[policy.tool].offenders[metricName];
}
// It is possible to declare a transformation function for the main metric value.
// The function should
if (policy.valueTransformFn) {
rule.value = policy.valueTransformFn(offenders);
}
var offendersObj = {};
// It is possible to declare a transformation function for the offenders.
// The function should take an array of strings as single parameter and return a string.
if (policy.offendersTransformFn) {
@@ -111,7 +118,8 @@ var RulesChecker = function() {
if (rule) {
rule.policy = {
label: policy.label,
message: policy.message
message: policy.message,
hasOffenders: policy.hasOffenders || false
};
results[metricName] = rule;
+66 -15
View File
@@ -2,10 +2,9 @@ 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 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 redownload.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;
};
+24 -86
View File
@@ -1,6 +1,5 @@
var debug = require('debug')('ylt:screenshotHandler');
var debug = require('debug')('ylt:screenshotHandlerAgent');
var Jimp = require('jimp');
var tmp = require('temporary');
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);
};
@@ -77,61 +46,33 @@ var screenshotHandler = function() {
var deferred = Q.defer();
var currentWidth = image.bitmap.width;
var ratio = newWidth / currentWidth;
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:
new Jimp(image.bitmap.width, image.bitmap.height, 0xFFFFFF, function(err, canvas){
if (err) {
debug('Could not create a white canvas');
debug(err);
deferred.reject(err);
} else {
// Paste original image on top of the canvas
canvas.composite(image, 0, 0, function(err, image){
if (err) {
debug('Could not paste image on the white canvas');
debug(err);
deferred.reject(err);
} else {
// Now image has a white background...
debug('White background correctly added');
deferred.resolve(image);
}
});
}
});
return deferred.promise;
};
this.toBuffer = function(image) {
var deferred = Q.defer();
image.quality(90).getBuffer(Jimp.MIME_JPEG, function(err, buffer){
image.quality(85).getBuffer(Jimp.MIME_JPEG, function(err, buffer){
if (err) {
debug('Could not save image to buffer');
debug(err);
@@ -147,19 +88,16 @@ var screenshotHandler = function() {
};
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();
});
-350
View File
@@ -1,350 +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 = (process.env.IS_TEST) ? require('../../../test/fixtures/settings.json') : 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;
}
// Block requests to unwanted websites (=spam)
if (req.body.url && isBlocked(req.body.url)) {
console.error('Test blocked for URL: %s', req.body.url);
res.status(403).send('Forbidden');
return;
}
// Grab the test parameters and generate a random run ID
var run = {
runId: (Date.now()*1000 + Math.round(Math.random()*1000)).toString(36),
params: {
url: req.body.url,
waitForResponse: req.body.waitForResponse === true || req.body.waitForResponse === 'true' || req.body.waitForResponse === 1,
partialResult: req.body.partialResult || null,
screenshot: req.body.screenshot || false,
device: req.body.device || 'desktop',
proxy: req.body.proxy || null,
waitForSelector: req.body.waitForSelector || null,
cookie: req.body.cookie || null,
authUser: req.body.authUser || null,
authPass: req.body.authPass || null,
blockDomain: req.body.blockDomain || null,
allowDomain: req.body.allowDomain || null,
noExternals: req.body.noExternals || false
}
};
// 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);
console.log('Launching test ' + run.runId + ' on ' + run.params.url);
var runOptions = {
screenshot: run.params.screenshot ? screenshot.getTmpFilePath() : false,
device: run.params.device,
proxy: run.params.proxy,
waitForSelector: run.params.waitForSelector,
cookie: run.params.cookie,
authUser: run.params.authUser,
authPass: run.params.authPass,
blockDomain: run.params.blockDomain,
allowDomain: run.params.allowDomain,
noExternals: run.params.noExternals,
phantomasEngine: serverSettings.phantomasEngine
};
return ylt(run.params.url, runOptions);
})
// Phantomas completed, let's save the screenshot if any
.then(function(data) {
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) {
debug('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');
});
});
function isBlocked(url) {
if (!serverSettings.blockedUrls) {
return false;
}
return serverSettings.blockedUrls.some(function(blockedUrl) {
return (url.indexOf(blockedUrl) === 0);
});
}
};
module.exports = ApiController;
-42
View File
@@ -1,42 +0,0 @@
var path = require('path');
var express = require('express');
var serverSettings = (process.env.IS_TEST) ? require('../../../test/fixtures/settings.json') : require('../../../server_config/settings.json');
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';
// Routes templating
var routes = ['/', '/about', '/result/:runId', '/result/:runId/timeline', '/result/:runId/screenshot', '/result/:runId/rule/:policy', '/queue/:runId'];
routes.forEach(function(route) {
app.get(route, function(req, res) {
res.setHeader('Cache-Control', 'public, max-age=20');
res.render(path.join(__dirname, assetsPath, 'main.html'), {
baseUrl: app.locals.baseUrl || '/',
sponsoring: serverSettings.sponsoring || {}
});
});
});
// Views templating
app.get('/views/:viewName', function(req, res) {
res.setHeader('Cache-Control', 'public, max-age=' + cacheDuration);
res.render(path.join(__dirname, assetsPath, 'views/' + req.params.viewName), {
baseUrl: app.locals.baseUrl || '/',
sponsoring: serverSettings.sponsoring || {}
});
});
// Static assets
app.use('/css', express.static(path.join(__dirname, assetsPath, 'css'), { maxAge: cacheDuration }));
app.use('/fonts', express.static(path.join(__dirname, assetsPath, 'fonts'), { maxAge: cacheDuration }));
app.use('/img', express.static(path.join(__dirname, assetsPath, 'img'), { maxAge: cacheDuration }));
app.use('/js', express.static(path.join(__dirname, assetsPath, 'js'), { maxAge: cacheDuration }));
app.use('/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,87 +0,0 @@
var config = (process.env.IS_TEST) ? require('../../../test/fixtures/settings.json') : require('../../../server_config/settings.json');
var debug = require('debug')('apiLimitsMiddleware');
var apiLimitsMiddleware = function(req, res, next) {
'use strict';
var ipAddress = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
debug('Entering API Limits Middleware with IP address %s', ipAddress);
if (req.path.indexOf('/api/') === 0 && !res.locals.hasApiKey) {
if (req.path === '/api/runs') {
if (!runsTable.accepts(ipAddress)) {
// Sorry :/
debug('Too many tests launched from IP address %s', ipAddress);
res.status(429).send('Too many requests');
return;
}
}
if (!callsTable.accepts(ipAddress)) {
// Sorry :/
debug('Too many API requests from IP address %s', ipAddress);
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 = (process.env.IS_TEST) ? require('../../../test/fixtures/settings.json') : 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();
-210
View File
@@ -1,210 +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.jQueryCallsOnEmptyObject = 0;
metrics.jQueryNotDelegatedEvents = 0;
}
try {
debug('Starting JS execution transformation');
javascriptExecutionTree = JSON.parse(data.toolsResults.phantomas.offenders.javascriptExecutionTree[0]);
if (javascriptExecutionTree.children) {
javascriptExecutionTree.children.forEach(function(node, 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);
});
}
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,251 +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) {
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) {
var args = Array.prototype.slice.call(arguments);
callbackAfter.apply(this, [result].concat(args));
}
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);
});
};
@@ -1,30 +0,0 @@
/**
* Analyzes AJAX requests
*/
/* global window: true */
exports.version = '0.2.a';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('ajaxRequests'); // @desc number of AJAX requests
phantomas.setMetric('synchronousXHR'); // @desc number of synchronous
phantomas.on('init', function() {
phantomas.evaluate(function() {
(function(phantomas) {
phantomas.spy(window.XMLHttpRequest.prototype, 'open', null, function(result, method, url, async) {
phantomas.incrMetric('ajaxRequests');
phantomas.addOffender('ajaxRequests', '<%s> [%s]', url, method);
if (async === false) {
phantomas.incrMetric('synchronousXHR');
phantomas.addOffender('synchronousXHR', url);
phantomas.log('ajaxRequests: synchronous XMLHttpRequest call to <%s>', url);
}
}, true);
})(window.__phantomas);
});
});
};
@@ -1,86 +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 (images[i].width === 1 && images[i].height === 1) continue;
if (!lazyLoadableImages[src]) {
path = phantomas.getDOMPath(images[i]);
lazyLoadableImages[src] = {
path: path
};
}
}
// don't run for child nodes as they're hidden as well
return false;
}
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 ? '#' + args[0] : undefined;
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 ? '.' + args[0] : undefined;
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 ? args[0].toLowerCase() : undefined;
var context = phantomas.getDOMPath(this);
querySpy('tag name', tagName, '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 ? args[0] : undefined;
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 ? args[0] : undefined;
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,81 +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,
// Add an offset of 100px under the height of the screen
LAZYLOAD_OFFSET = 100;
phantomas.log('lazyLoadableImages: %d image(s) found, assuming %dpx offset to be the fold', len, viewportHeight);
for (i = 0; i < len; i++) {
// @see https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect
offset = images[i].getBoundingClientRect().top;
src = images[i].src;
// ignore base64-encoded images
if (src === '' || /^data:/.test(src)) {
continue;
}
path = phantomas.getDOMPath(images[i]);
// get the most top position for a given image (deduplicate by src)
if (typeof processedImages[src] === 'undefined') {
processedImages[src] = {
offset: offset,
path: path
};
}
// maybe there's the same image loaded above the fold?
if (offset < processedImages[src].offset) {
processedImages[src] = {
offset: offset,
path: path
};
}
}
phantomas.log('lazyLoadableImages: checking %d unique image(s)', Object.keys(processedImages).length);
Object.keys(processedImages).forEach(function(src) {
var img = processedImages[src];
if (img.offset > viewportHeight + LAZYLOAD_OFFSET) {
phantomas.log('lazyLoadableImages: <%s> image (%s) is below the fold (at %dpx)', src, img.path, img.offset);
phantomas.incrMetric('lazyLoadableImagesBelowTheFold');
phantomas.addOffender('lazyLoadableImagesBelowTheFold', src);
}
});
phantomas.spyEnabled(true);
})(window.__phantomas);
});
});
};
@@ -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