Compare commits

..
252 Commits
Author SHA1 Message Date
Gaël Métais 484d7ee3b2 Merge pull request #61 from gmetais/develop
V1.3.1
2015-03-18 08:22:41 +01:00
Gaël Métais 7a1bc282e9 v1.3.1 2015-03-18 08:16:48 +01:00
Gaël Métais f45480c0b9 Fix bugs with show warnings and some html escaping 2015-03-18 08:09:51 +01:00
Gaël Métais 23d980910d Increase stack size 2015-03-18 08:09:02 +01:00
Gaël Métais 9b939aa8b4 Remove console.log 2015-03-18 08:06:56 +01:00
Gaël Métais 838a9c3e7d Fix missing bower dependency 2015-03-17 09:44:56 +01:00
Gaël Métais f91de30717 Missing angular dependency 2015-03-17 09:44:11 +01:00
Gaël Métais 0c3299573f Merge pull request #60 from gmetais/develop
Merge V1.3.0
2015-03-17 09:28:26 +01:00
Gaël Métais 13e2d2d597 Fix travis running the tests twice 2015-03-17 08:50:27 +01:00
Gaël Métais a6bbc7119d Update version number 2015-03-17 08:46:31 +01:00
Gaël Métais 20bf58b84f Fix missing space 2015-03-17 08:14:01 +01:00
Gaël Métais fe176c92c0 Update dependencies 2015-03-16 20:00:42 +01:00
Gaël Métais 6009ed53b7 Fix cssParsingError offenders when no line & column are provided 2015-03-16 18:29:42 +01:00
Gaël Métais 79c387ba45 Merge pull request #59 from gmetais/phantomas1.10
Phantomas1.10
2015-03-16 17:43:30 +01:00
Gaël Métais dd767d6664 Fix tests 2015-03-16 17:20:34 +01:00
Gaël Métais 61202f1635 Fix syntax 2015-03-16 17:15:17 +01:00
Gaël Métais 457d6cc54b Add offenders for document.write calls 2015-03-16 16:54:17 +01:00
Gaël Métais bf8123b7c5 Revert eval quickfix 2015-03-16 14:07:54 +01:00
Gaël Métais c98ede9b81 Revert temporary fix for negative cache time 2015-03-16 13:58:42 +01:00
Gaël Métais 34671fca59 Revert fix for jQueryEventTriggers 2015-03-16 13:54:30 +01:00
Gaël Métais 7ad9381aad Revert viewport size 2015-03-16 13:33:23 +01:00
Gaël Métais 7387eefb10 Update phantomas 2015-03-16 13:32:56 +01:00
Gaël Métais ca3403a5b2 Merge branch 'develop' of https://github.com/gmetais/YellowLabTools into develop 2015-03-16 13:20:27 +01:00
Gaël Métais 8adc895c8c Fix missing spaces with htmlmin 2015-03-16 13:16:05 +01:00
Gaël Métais 6fe49d06ac Scuse my english 2015-03-16 13:06:56 +01:00
Gaël Métais c47e6d0219 Merge pull request #58 from gmetais/profilerParams
Profiler params
2015-03-16 13:04:23 +01:00
Gaël Métais 99c4b240ec Fix file url in backtrace 2015-03-16 12:54:31 +01:00
Gaël Métais 0947724245 Replace angular templating by homemade for speed purpose 2015-03-16 12:30:05 +01:00
Gaël Métais 15653ef961 Delete jsChildrenDirective not needed anymore 2015-03-16 12:00:56 +01:00
Gaël Métais 893c92266a Fix bug with script filter 2015-03-12 18:07:04 +01:00
Gaël Métais 50571995a4 Speed up timeline page, by removing some angular 2015-03-10 09:34:43 +01:00
Gaël Métais dc2f6e17cd Remove duplicated spying of window.onload 2015-03-10 09:26:07 +01:00
Gaël Métais 9a861b085a Remove touchscreen compatible hover (for perfs) 2015-03-09 12:51:38 +01:00
Gaël Métais 714cb318a9 One way binding 2015-03-09 10:37:00 +01:00
Gaël Métais 2827463b41 Remove unused search field 2015-03-09 07:52:04 +01:00
Gaël Métais 121a8b11b8 Improve arguments reading in timeline 2015-03-08 23:13:09 +01:00
Gaël Métais 3e0e31a5fe New test page 2015-03-08 10:32:57 +01:00
Gaël Métais a7c96f77fe Change the way context is saved in jsExecutionTree 2015-03-02 17:10:21 +01:00
Gaël Métais d3444e9aa0 Add a 'jQuery - find' wrapper on jQuery search by ID 2015-03-02 15:37:11 +01:00
Gaël Métais 71d2e61551 Add some spies on more jQuery functions 2015-03-02 11:23:10 +01:00
Gaël Métais 3756d1185f Better error message 2015-02-27 18:03:43 +01:00
Gaël Métais 5a22eb9542 Fix merge conflict 2015-02-27 17:52:51 +01:00
Gaël Métais cb535f1cee Scroll to the profiler on timeline click 2015-02-27 17:41:54 +01:00
Gaël Métais f304e5efb7 Merge pull request #54 from gmetais/scriptfilter
Introduce a script selector to filter the timeline and the profiler.
2015-02-27 10:04:00 +01:00
Gaël Métais 5732a8ffc8 Spy some more jQuery functions : animations 2015-02-26 19:49:18 +01:00
Gaël Métais 8b7ccb837a Adjust network category score, caching is more important 2015-02-26 19:48:35 +01:00
Gaël Métais f4bb3fc0bc Fix PhantomJS typo in README 2015-02-26 19:46:51 +01:00
Gaël Métais 62602a88bc Remove jQuery from timeline when it's filtered 2015-02-18 23:44:30 +01:00
Gaël Métais f63fa9faf2 Filter timeline by script 2015-02-13 16:32:58 +01:00
Gaël Métais 839bfd4756 Use shorten-url for backtraces 2015-02-13 11:53:10 +01:00
Gaël Métais 9392d21783 Merge branch 'develop' of https://github.com/gmetais/YellowLabTools into develop 2015-02-12 18:44:02 +01:00
Gaël Métais ca5594dfbb Forgotten rule cachingNotSpecified 2015-02-12 18:43:40 +01:00
Gaël Métais 1b938417e5 Add doc.createElement and createDocumentFragment to the profiling 2015-02-12 18:43:08 +01:00
Gaël Métais f95e94ac6d Merge pull request #50 from gmetais/subqueries
Show subqueries in timeline
2015-02-12 18:00:12 +01:00
Gaël Métais e9bb72a416 Add sub-queries to the JS profiler table 2015-02-12 17:28:41 +01:00
Gaël Métais 2d003febc1 Front needs the jsTimeline by default 2015-02-09 19:04:47 +01:00
Gaël Métais 42c27af604 No more javascriptExecutionTree bu default (option) 2015-02-09 19:03:04 +01:00
Gaël Métais ee490fc660 Add the --deep-js-analysis option to CLI 2015-02-09 19:01:59 +01:00
Gaël Métais 10500c2fba Fix wrong format 2015-02-09 18:53:32 +01:00
Gaël Métais c4011c0817 Restore deleted screenshot for README 2015-02-06 18:34:33 +01:00
Gaël Métais bc49bebb58 Fix install problems 2015-02-06 18:30:15 +01:00
Gaël Métais dcdd93bcaa Merge pull request #49 from gmetais/dashboard
Merging branch dashboard
2015-02-06 17:11:49 +01:00
Gaël Métais d5fdc3ce63 Fix wording 2015-02-06 16:57:15 +01:00
Gaël Métais c1b5a8625a Missing semicolon 2015-02-06 15:57:13 +01:00
Gaël Métais dd1fd34865 Add screenshot option to CLI 2015-02-06 15:50:15 +01:00
Gaël Métais 9b9e65abd4 Add test to NodeJS module screenshot feature 2015-02-06 15:12:48 +01:00
Gaël Métais e5f917a185 Remove unneeded about page controller 2015-02-06 14:34:45 +01:00
Gaël Métais 368bf4a6d9 Truncate jQuery version, can be long and break display 2015-02-06 14:32:21 +01:00
Gaël Métais a7df7a5436 Forgotten part in cssOldPropertyPrefixes offenders 2015-02-06 14:17:09 +01:00
Gaël Métais d3f5236073 Add an error message when server reboots 2015-02-06 12:32:54 +01:00
Gaël Métais db6fdbc121 Empty URL crashes the server 2015-02-06 12:32:19 +01:00
Gaël Métais dc9aa4d6b4 Fix url not sent 2015-02-06 12:15:20 +01:00
Gaël Métais 5f2553047d Add a warning when max runs per day is reached 2015-02-06 12:03:02 +01:00
Gaël Métais d556f84929 Add a meta description 2015-02-06 11:35:41 +01:00
Gaël Métais 4345ccf356 Fix recommendation 'as many fonts as possible' 2015-02-06 11:19:41 +01:00
Gaël Métais 4032468e10 Fix markdown formatting in npm module description 2015-02-06 11:18:01 +01:00
Gaël Métais 5330f0f8e2 Better links when an error occurs 2015-02-06 11:17:27 +01:00
Gaël Métais 39e6ecc920 Fix url in queue or dashboard pages without http:// 2015-02-06 10:51:27 +01:00
Gaël Métais 928e8f14ea Fix problem when http:// contains upper case 2015-02-06 10:50:36 +01:00
Gaël Métais 0e4ffcee62 Fix error message due to PhantomJS instead of Phantomas 2015-02-06 10:38:32 +01:00
Gaël Métais 955f353a31 Add some keywords on the index page for SEO 2015-02-06 10:19:28 +01:00
Gaël Métais 44df06fac7 v1.2.0 2015-02-06 09:49:39 +01:00
Gaël Métais f099f6f4a9 Change default viewport to 16:9 2015-02-06 09:45:03 +01:00
Gaël Métais e9259356bd Change default viewport to 16:9 2015-02-06 09:44:26 +01:00
Gaël Métais 2264445772 Merge pull request #48 from gmetais/thumbnail
Display a screenshot on dashboard
2015-02-05 23:42:15 +01:00
Gaël Métais 47f0e1f1bf Hover effect on the screenshot on dashboard 2015-02-05 23:17:15 +01:00
Gaël Métais 79d0898fb5 Add a screenshot page 2015-02-05 19:33:58 +01:00
Gaël Métais 85d1119ad6 Add a screenshot page 2015-02-05 19:31:19 +01:00
Gaël Métais 52e73ca7cd Display screenshot on the fashboard 2015-02-05 18:49:59 +01:00
Gaël Métais b5ac42792e Reduce screenshot width 2015-02-05 18:49:42 +01:00
Gaël Métais 6a844e163b Add the screenshot option in front-end 2015-02-05 18:49:05 +01:00
Gaël Métais efa4151850 New API route to retrieve a screenshot 2015-02-05 17:27:21 +01:00
Gaël Métais 55dc387d3c Fix random test fail 2015-02-05 17:19:39 +01:00
Gaël Métais ae47333eef New 'screenshot' option when launching a test 2015-02-05 15:25:17 +01:00
Gaël Métais 5bbe4cde77 Separate steps 2015-02-05 15:23:59 +01:00
Gaël Métais 0ff35d13e8 Fix require 2015-02-05 15:23:05 +01:00
Gaël Métais 3059ab835b Fix some policies 2015-02-03 21:31:04 +01:00
Gaël Métais 798f295912 Remove double jsHint check 2015-02-03 20:23:09 +01:00
Gaël Métais 71d115d161 Merge pull request #47 from softmonkeyjapan/master
Chore : Fix typo on policies
2015-02-03 10:11:31 +01:00
Loic Kartono d5368e5f90 Chore : Fix typo on policies 2015-02-03 10:51:48 +11:00
Gaël Métais d1f0b2e459 Merge pull request #43 from gmetais/phantomas1.9
Phantomas1.9
2015-01-26 20:03:28 +01:00
Gaël Métais ed4ddc0a43 Add CSS colors count and a palette as offender 2015-01-26 19:29:08 +01:00
Gaël Métais 61c09a5b49 Revert custom dev and implement jQueryVersionsLoaded 2015-01-26 15:10:26 +01:00
Gaël Métais a6194f2535 Merge pull request #42 from gmetais/offenders
Fixes #40.
2015-01-26 11:50:51 +01:00
Gaël Métais 901116fde6 Separate offender analyze, parsing in the core, display in the front 2015-01-26 11:34:38 +01:00
Gaël Métais d829525ab8 Update phantomas to v1.9 2015-01-26 11:32:37 +01:00
Gaël Métais ee666a3a6f Temporary fix for negative ttl in cachingTooShort 2015-01-22 15:15:38 +01:00
Gaël Métais 4a8a5829d7 End of the offender transform functions 2015-01-22 14:54:51 +01:00
Gaël Métais fd2271f6d6 Offenders layout for various CSS metrics 2015-01-19 20:31:06 +01:00
Gaël Métais cac36727e8 Handle function names in backtraces 2015-01-19 12:16:56 +01:00
Gaël Métais e6bd0d0006 Strange spaces disapearance in offenders 2015-01-19 12:16:19 +01:00
Gaël Métais 4ef83a844f Fix offendersTransformFn deleted after the first run 2015-01-19 12:15:05 +01:00
Gaël Métais 23dc46d697 Offenders layout for DOMqueriesAvoidable, jsErrors and globalVariables 2015-01-19 11:24:16 +01:00
Gaël Métais 6fc502b0b2 Offenders layout for DOM queries without results 2015-01-17 09:44:50 +01:00
Gaël Métais a4f22a8542 Offenders layout for DOM inserts 2015-01-17 09:21:50 +01:00
Gaël Métais bce7a1ad9a Add the possibility to change offenders' layout 2015-01-16 15:52:56 +01:00
Gaël Métais 49a6e4633a Include angular-sanitize module 2015-01-16 15:51:45 +01:00
Gaël Métais 41c02cb732 Remove coverage from 'grunt test-current-work' command 2015-01-16 15:50:56 +01:00
Gaël Métais 0f00111373 Fix #39 Error 1001 when using the Node module 2015-01-15 12:34:47 +01:00
Gaël Métais 6521d24925 Add quotes to user-agent in debug command line 2015-01-15 12:19:25 +01:00
Gaël Métais 34468198ec Hide ProTip on dashboard if user comes from social sharing 2015-01-12 16:39:54 +01:00
Gaël Métais ab30aae08e Re-introduce fake user-agent 2015-01-09 18:52:10 +01:00
Gaël Métais eb4d8cde7c Merge pull request #37 from gmetais/refactor
v1.1.0
2015-01-09 18:21:22 +01:00
Gaël Métais 95daa4b02e Fix tests 2015-01-09 18:18:35 +01:00
Gaël Métais 7e7331fb6f Merge v1.1.0 2015-01-09 18:13:16 +01:00
Gaël Métais 7485f00f2f Fix install & update scripts 2015-01-09 15:14:49 +01:00
Gaël Métais c3704fa4ff Merge pull request #36 from gmetais/build
Front-end build with Grunt for performances. Fixes #11.
2015-01-09 14:21:13 +01:00
Gaël Métais db39e79cf7 Front-end build for perfs - #11 2015-01-09 13:54:00 +01:00
Gaël Métais 7c8ffec254 Fix Angular problem after uglifying 2015-01-08 20:47:57 +01:00
Gaël Métais 7e3d5d17f8 Merge pull request #35 from gmetais/gatling
stress test scenario with gatlin
2015-01-08 16:15:25 +01:00
Achraf Ben Younes bbfe228e4c stress test scenario with gatlin 2015-01-08 16:07:05 +01:00
Gaël Métais a15e04c7d0 Add a screenshot to README 2015-01-08 12:38:14 +01:00
Gaël Métais 301b4de31d Update README for version 1.1 2015-01-08 12:23:41 +01:00
Gaël Métais 73f48b71a2 Error message if the result is not found 2015-01-08 11:02:36 +01:00
Gaël Métais d7aa24b467 API parameter resilience 2015-01-08 08:24:13 +01:00
Gaël Métais 377dee7d83 Rename yellowlabtools.js to index.js and change the way to call it 2015-01-06 22:50:32 +01:00
Gaël Métais 079ec94197 Fix API not returning anything if bad URL 2015-01-06 16:41:17 +01:00
Gaël Métais aee0905ea5 Tune DOM complexity score 2015-01-06 16:40:33 +01:00
Gaël Métais 5bc32ea89a Fix package.json bin for CLI 2015-01-05 23:21:11 +01:00
Gaël Métais a6162b8440 Fix package.json bin for CLI 2015-01-05 23:18:15 +01:00
Gaël Métais 3f389fcf79 Update vagrant script 2015-01-05 19:51:26 +01:00
Gaël Métais d78716c873 Copy offenders from jsCount etc. to the requests rule 2015-01-05 15:51:02 +01:00
Gaël Métais aabbf882df Reduce the weight of jsErrors in score 2015-01-05 15:05:23 +01:00
Gaël Métais 6b1b9252b3 Fix JS Errors duplicated in phantomas results 2015-01-05 11:54:51 +01:00
Gaël Métais e26fd5ed6f New async run launch option in API: return only a portion of the response 2015-01-05 11:45:52 +01:00
Gaël Métais 249cfa21e3 Change conversion algorithm from x/100 to A-F grade 2015-01-05 11:33:50 +01:00
Gaël Métais fb4d6afb07 Disabling eval count because it can cause JS errors 2014-12-30 15:29:11 +01:00
Gaël Métais 909ffc1aac Spyed functions will now re-throw errors. Watchup... 2014-12-30 15:24:30 +01:00
Gaël Métais 9ec5bd7183 Fix jsHint 2014-12-30 11:23:27 +01:00
Gaël Métais dbde3de8e8 Fix jsError on an exception inside an eval 2014-12-30 11:19:31 +01:00
Gaël Métais 5c80f3197a Add version number in the footer 2014-12-30 09:20:05 +01:00
Gaël Métais 17a644c5d8 Add Google Analytics 2014-12-30 08:11:47 +01:00
Gaël Métais 9bf420d317 Fix tests 2014-12-30 07:29:59 +01:00
Gaël Métais be2911707c Displaying error message in queue if fails 2014-12-29 22:55:52 +01:00
Gaël Métais f601044645 Add report and index links when a run fails 2014-12-29 22:06:33 +01:00
Gaël Métais c55a83cbe2 Add tested URL to the waiting page 2014-12-29 16:27:21 +01:00
Gaël Métais ef9326abc6 Add CORS headers to the API 2014-12-29 15:57:04 +01:00
Gaël Métais a219c1b570 Merge pull request #34 from gmetais/killphantomas
Kill the phantomas subprocess before killing YLT
2014-12-29 15:43:39 +01:00
Gaël Métais 1920a3e802 Kill the phantomas subprocess before killing YLT 2014-12-29 15:35:11 +01:00
Gaël Métais 67d0809e2f Merge pull request #32 from gmetais/phantomas1.8
Update phantomas to v1.8
2014-12-29 12:53:26 +01:00
Gaël Métais 7c6676c981 New 'Small requests' category with smallJsFils, smallCssFiles and smallImages rules 2014-12-29 12:46:56 +01:00
Gaël Métais 519f2ba617 Remove custom domComplexity module, maxDepth offenders added to phantomas 2014-12-29 12:17:52 +01:00
Gaël Métais 0b23fad909 Remove custom closedConnections module implemented in Phantomas 2014-12-29 10:35:18 +01:00
Gaël Métais 6fe61a041e Add DOMqueriesWithoutResults as a rule 2014-12-29 10:26:08 +01:00
Gaël Métais fb323c2086 Import DOMqueriesWithoutResults fonctionnality 2014-12-29 08:20:46 +01:00
Gaël Métais dccb9bb49c Move phantomas_custom to the phantomas tool directory 2014-12-28 18:05:10 +01:00
Gaël Métais e9505f31f7 Update phantomas and other dependencies 2014-12-28 16:57:45 +01:00
Gaël Métais c31e880f63 Add a tip for API advertising on the dashboard #20 2014-12-28 16:32:24 +01:00
Gaël Métais b064e73ef4 Improve UI on rule pages 2014-12-28 15:47:12 +01:00
Gaël Métais 606c2c6f95 DOMQueriesAvoidable taking offenders from DOMQueriesDuplicated 2014-12-23 18:06:33 +01:00
Gaël Métais c0e02726be Fix categories order on dashboard 2014-12-23 17:33:13 +01:00
Gaël Métais 2ddf2e8341 Fix Test again button on Rule page 2014-12-23 16:54:24 +01:00
Gaël Métais 5d2e647f10 Put the tweet block at the bottom of the dashboard 2014-12-23 16:27:18 +01:00
Gaël Métais a141122fbd Share score button + Restart test button 2014-12-23 16:08:39 +01:00
Gaël Métais b186677a53 Better scoring algorithm 2014-12-23 16:07:48 +01:00
Gaël Métais c8ba9ef99a Take in account abnormalityScore to calculate category score 2014-12-22 23:11:25 +01:00
Gaël Métais c994638168 Remove console.log 2014-12-22 22:40:59 +01:00
Gaël Métais b6df64337d Browser back button from result page goes to index ignoring queue 2014-12-22 22:22:58 +01:00
Gaël Métais 1c0936d4c1 Change hover color on dashboard 2014-12-22 22:16:04 +01:00
Gaël Métais 983671f5c3 'grunt dev' task with server autoreload 2014-12-22 22:13:17 +01:00
Gaël Métais d6f76de349 Repair grunt express task 2014-12-22 22:08:24 +01:00
Gaël Métais 7aede20819 Remove eye icon from font 2014-12-22 19:36:14 +01:00
Gaël Métais 0e5c7e1215 Progressively increment polling interval on queue page 2014-12-22 19:35:02 +01:00
Gaël Métais 3cd60e9a8d About page 2014-12-22 17:45:11 +01:00
Gaël Métais cf79e95c45 Add contributors to README 2014-12-22 17:09:29 +01:00
Gaël Métais 3c22cd216c Add the Policy page for the details of a policy score 2014-12-22 16:25:16 +01:00
Gaël Métais cb20075599 Re-construct the JS Timeline page 2014-12-22 14:07:16 +01:00
Gaël Métais c1257afc4c Add onclick property on the rules on dashboard 2014-12-21 14:19:56 +01:00
Gaël Métais 0862116a40 Cooler abnormality score for JS errors for the moment 2014-12-21 14:19:04 +01:00
Gaël Métais 6eefc0a623 jshint 2014-12-21 11:48:35 +01:00
Gaël Métais ed4f448370 Re-introduce menu on the result page 2014-12-21 11:42:09 +01:00
Gaël Métais 3dc4f4641c Remove console.log 2014-12-21 10:55:19 +01:00
Gaël Métais 95a5f5063e Repair test submission on Enter key 2014-12-20 18:43:11 +01:00
Gaël Métais 4cca167fc1 Change warning limit for not so important metrics 2014-12-20 18:38:48 +01:00
Gaël Métais 301ebd9b6f Re-enable warnings on dashboard 2014-12-20 18:37:31 +01:00
Gaël Métais 01fe0ca381 Optimized category score bubble position 2014-12-20 17:55:42 +01:00
Gaël Métais b6a9f5109c Improve colors 2014-12-20 17:43:21 +01:00
Gaël Métais 3320e993e7 Improve colors 2014-12-20 17:42:49 +01:00
Gaël Métais 239672116b Removing the old server 2014-12-20 09:25:26 +01:00
Gaël Métais 4d8e71fff4 Merge branch 'customRules' into refactor 2014-12-19 15:01:37 +01:00
Gaël Métais 4babf6d30e Merge branch 'refactor' of https://github.com/gmetais/YellowLabTools into refactor 2014-12-19 14:51:39 +01:00
Gaël Métais 9fe1808e1c Merge branch 'achrafbenyounes-refactor' into refactor 2014-12-19 14:51:14 +01:00
Gaël Métais ebb9dbc7f1 Fix mispelled 'requests' rule in API 2014-12-19 14:48:54 +01:00
Gaël Métais e2dc5e0705 Fix typo 2014-12-19 14:48:13 +01:00
Gaël Métais f66f464199 Fixes for jsHint 2014-12-19 14:44:03 +01:00
Gaël Métais 35d42b9ec5 Merge pull request #28 from gmetais/api-too-fast
Api responding too fast
2014-12-19 14:38:07 +01:00
Gaël Métais 0776551aa7 Fix #26 API redirects to the response before it's written to disk 2014-12-19 14:34:47 +01:00
Gaël Métais 482a890447 Add debugging logs to resultsDatastore 2014-12-19 14:27:19 +01:00
Gaël Métais bc8ebd8db3 Fix CERT_UNTRUSTED error on Travis 2014-12-19 14:26:46 +01:00
Gaël Métais 521a81ca1d Introducing custom score calculations (jQuery version) 2014-12-19 14:20:20 +01:00
achrafbenyounes c4a7b1f129 Enhancing index, view and dashboard ui 2014-12-18 18:02:39 +01:00
achrafbenyounes d712eb3a59 Enhancing index, view and dashboard ui 2014-12-18 18:02:04 +01:00
Gaël Métais 21cd0cb346 Merge pull request #25 from achrafbenyounes/refactor
Starting Angular FrontEnd
2014-12-15 22:00:04 +01:00
achrafbenyounes b1a0dfa7bd Update jshint config 2014-12-15 18:58:38 +01:00
achrafbenyounes ffbedc1e02 Starting Angular FrontEnd 2014-12-15 18:43:22 +01:00
Gaël Métais 0a647264d7 Merge pull request #23 from gmetais/restful
RESTful API
2014-12-14 22:57:55 +01:00
Gaël Métais 1385e28911 Fix tests 2014-12-14 22:50:56 +01:00
Gaël Métais 2ab843efdb Added some sub-requests to the API 2014-12-14 22:36:25 +01:00
Gaël Métais 90dac7dd19 Created the generic score profile 2014-12-14 21:35:46 +01:00
Gaël Métais 792f7d8e21 Implements a general score calculator 2014-12-14 15:47:58 +01:00
Gaël Métais 5b0eb07d0d Merge pull request #19 from gmetais/#4
Securize and add limits to API calls
2014-12-13 14:04:29 +01:00
Gaël Métais 8abfe93df7 The test itself starts the server in a before hook 2014-12-13 13:56:21 +01:00
Gaël Métais 3ad6cdef51 Launch the test server with coverage 2014-12-13 12:11:33 +01:00
Gaël Métais 0da2ad0c10 Add API limits configurable in settings.json 2014-12-12 20:26:19 +01:00
Gaël Métais 84faabca86 Fix Auth token 2014-12-10 08:16:11 +01:00
Gaël Métais 49015e3422 API authentification (tbc...) 2014-12-09 09:04:28 +01:00
Gaël Métais 906f0876a0 API is starting to run 2014-12-05 16:14:08 +01:00
Gaël Métais e18cd87a07 Writing the API (workin in progress) 2014-12-02 00:08:07 +01:00
Gaël Métais 35aed5fb55 Test that console.log is never called 2014-11-28 18:31:14 +01:00
Gaël Métais 6089e5fe20 Introduce debug instead of console.log 2014-11-28 17:53:39 +01:00
Gaël Métais 2a316ebb1e Fix tests 2014-11-28 17:22:32 +01:00
Gaël Métais 1f83fe1b48 CLI is working, API not ready, Front broken 2014-11-28 10:45:30 +01:00
Gaël Métais 7d1bb4af5b Merge branch 'wait4css' 2014-11-20 18:10:16 +01:00
Gaël Métais f57fea0a54 Release 1.0.1 2014-11-20 18:09:02 +01:00
Gaël Métais 13787708e9 Add version number to the home page 2014-11-20 18:07:50 +01:00
Gaël Métais 7af3bc519a Fix JSON cyclic problem 2014-11-20 17:54:41 +01:00
Gaël Métais e3d96de203 Add articles links to the home page 2014-11-20 16:54:11 +01:00
Gaël Métais ec08177607 jsHint complaining 2014-11-20 15:39:19 +01:00
Gaël Métais 362dc9bbcc Wait until all CSS files are analyzed 2014-11-20 15:23:19 +01:00
Gaël Métais a9fb0b3b08 Fix test, google added a redirection 2014-11-20 15:03:46 +01:00
Gaël Métais c6b417ca81 Add a warning message when CSS analyzing fails #2 2014-11-20 14:56:33 +01:00
Gaël Métais 4e64af0f10 Add quotes to User-Agent when console.logging the phantomas command line 2014-11-20 14:53:50 +01:00
Gaël Métais 2a2d99191d Another warning 2014-11-20 14:41:46 +01:00
Gaël Métais b0b6e71d2e Fix grade error 2014-11-20 13:02:49 +01:00
Gaël Métais 041904f4c6 Wording improved 2014-11-20 11:34:21 +01:00
Gaël Métais 5b2bf8956a #2 Give a A grade if no syntax error 2014-11-20 11:19:18 +01:00
Gaël Métais a1278fad49 #2 Better CSS parsing error message 2014-11-20 11:10:10 +01:00
Gaël Métais d84b831922 #1 Change user agent to simulate Chrome 2014-11-19 19:33:48 +01:00
Gaël Métais fe37d4e91c Meta og:image 2014-11-18 23:07:04 +01:00
Gaël Métais f3a08df532 Meta og:image 2014-11-18 23:01:53 +01:00
Gaël Métais f9b0f6d613 Remove Beta status on HP 2014-11-18 22:29:07 +01:00
Gaël Métais b72e74e0d3 Init 2014-11-18 22:27:10 +01:00
136 changed files with 9751 additions and 3026 deletions
+2
View File
@@ -1,7 +1,9 @@
node_modules
bower_components
.tmp
.vagrant
results/*
coverage
front/build
har.json
+2 -3
View File
@@ -1,8 +1,7 @@
language: node_js
node_js:
- "0.10"
- "0.10.33"
before_install:
- "npm install -g grunt-cli"
- "npm install -g phantomjs"
install: npm install
before_script: grunt build
install: npm install
+281 -27
View File
@@ -1,60 +1,135 @@
module.exports = function(grunt) {
// Load all grunt modules
require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks);
// Tell our Express server that Grunt launched it
process.env.GRUNTED = true;
// Project configuration.
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
settings: grunt.file.readJSON('./server_config/settings.json'),
font: {
icons: {
src: ['app/public/fonts/svg-icons/*.svg'],
destCss: 'app/public/styles/less/icons.less',
destFonts: 'app/public/fonts/icons.woff',
src: ['front/src/fonts/svg-icons/*.svg'],
destCss: 'front/src/less/icons.less',
destFonts: 'front/src/fonts/icons.woff',
// Optional: Custom routing of font filepaths for CSS
cssRouter: function (fontpath) {
var pathArray = fontpath.split('/');
var fileName = pathArray[pathArray.length - 1];
return '/public/fonts/' + fileName;
return '/fonts/' + fileName;
}
}
},
less: {
all: {
files: {
'app/public/styles/main.css': [ 'app/public/styles/less/main.less' ],
'app/public/styles/index.css': [ 'app/public/styles/less/index.less' ],
'app/public/styles/launchTest.css': [ 'app/public/styles/less/launchTest.less' ],
'app/public/styles/results.css': [ 'app/public/styles/less/results.less' ]
}
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/*',
'app/lib/*.js',
'bin/*.js',
'lib/**/*.js',
'app/nodeControllers/*.js',
'app/public/scripts/*.js',
'phantomas_custom/**/*.js'
'phantomas_custom/**/*.js',
'test/api/*.js',
'test/core/*.js',
'test/fixtures/*.js',
'front/src/js/**/*.js'
]
},
clean: {
icons: {
src: ['tmp']
tmp: {
src: ['.tmp']
},
dev: {
src: ['front/src/css']
},
coverage: {
src: ['coverage/']
src: ['.tmp', 'coverage/']
},
build: {
src: ['front/build']
}
},
copy: {
beforeCoverage: {
files: [
{src: ['bin/server.js'], dest: '.tmp/'}
]
},
coverage: {
src: ['test/**'],
dest: 'coverage/'
files: [
{cwd: 'test', src: '**/*', dest: 'coverage/test', expand: true},
{cwd: 'lib/metadata', src: '**/*', dest: 'coverage/lib/metadata', expand: true},
{cwd: 'node_modules/phantomas', src: '**/*', dest: 'coverage/node_modules/phantomas', expand: true},
{cwd: 'lib/tools/phantomas/custom_modules', src: '**/*', dest: 'coverage/lib/tools/phantomas/custom_modules', expand: true}
]
},
build: {
files: [
{src: ['./front/src/fonts/icons.woff'], dest: './front/build/fonts/icons.woff'},
{src: ['./front/src/img/favicon.png'], dest: './front/build/img/favicon.png'},
{src: ['./front/src/img/logo-large.png'], dest: './front/build/img/logo-large.png'},
]
}
},
lineremover: {
beforeCoverage: {
files: {
'.tmp/bin/cli.js': 'bin/cli.js'
},
options: {
exclusionPattern: /#!\/usr\/bin\/env node/
}
}
},
blanket: {
coverage: {
coverageApp: {
src: ['app/'],
dest: 'coverage/app/'
},
coverageLib: {
src: ['lib/'],
dest: 'coverage/lib/'
},
coverageBin: {
src: ['.tmp/bin/'],
dest: 'coverage/bin/'
}
},
mochaTest: {
@@ -62,44 +137,223 @@ module.exports = function(grunt) {
options: {
reporter: 'spec',
},
src: ['coverage/test/server/*.js']
src: ['coverage/test/core/*.js', 'coverage/test/api/*.js']
},
'test-current-work': {
options: {
reporter: 'spec',
},
src: ['test/core/offendersHelpersTest.js']
},
coverage: {
options: {
reporter: 'html-cov',
// use the quiet flag to suppress the mocha console output
quiet: true,
// specify a destination file to capture the mocha
// output (the quiet option does not suppress this)
captureFile: 'coverage/coverage.html'
},
src: ['coverage/test/server/*.js']
src: ['coverage/test/core/*.js', 'coverage/test/api/*.js']
}
},
env: {
dev: {
NODE_ENV: 'development'
},
built: {
NODE_ENV: 'production'
}
},
express: {
dev: {
options: {
port: 8383,
server: './bin/server.js',
serverreload: true,
showStack: true
}
},
built: {
options: {
port: 8383,
server: './bin/server.js',
serverreload: true,
showStack: true
}
},
test: {
options: {
port: 8387,
server: './coverage/bin/server.js',
showStack: true
}
},
'test-current-work': {
options: {
port: 8387,
server: './bin/server.js',
showStack: true
}
},
testSuite: {
options: {
port: 8388,
bases: 'test/www'
}
}
},
useminPrepare: {
html: './front/src/main.html',
options: {
dest: './front/build',
root: ['./', './front/src']
}
},
usemin: {
html: './front/build/main.html',
css: './front/build/css/*.css',
options: {
assetsDirs: ['front/build'],
patterns: {
css: [[/(\/fonts\/icons\.woff)/gm, 'Replacing reference to icons.woff']]
}
}
},
htmlmin: {
options: {
removeComments: true,
collapseWhitespace: true,
conservativeCollapse: true
},
main: {
files: [{
expand: true,
cwd: './front/build/',
src: 'main.html',
flatten: true,
dest: './front/build'
}]
},
views: {
files: [{
expand: true,
cwd: './front/src/views',
src: '*.html',
flatten: true,
dest: '.tmp/views/'
}]
}
},
inline_angular_templates: {
build: {
options: {
base: '.tmp',
method: 'append'
},
files: {
'./front/build/main.html': ['.tmp/views/*.html']
}
}
},
filerev: {
options: {
algorithm: 'md5',
length: 8
},
assets: {
src: './front/build/*/*.*'
}
}
});
require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks);
// Custom task: copies the test settings.json file to the coverage folder, and checks if there's no missing fields
grunt.registerTask('copy-test-server-settings', function() {
var mainSettingsFile = './server_config/settings.json';
var testSettingsFile = './test/fixtures/settings.json';
var mainSettings = grunt.file.readJSON(mainSettingsFile);
var testSettings = grunt.file.readJSON(testSettingsFile);
// Recursively compare keys of two objects (not the values)
function compareKeys(original, copy, context) {
for (var key in original) {
if (!copy[key] && copy[key] !== '' && copy[key] !== 0) {
grunt.fail.warn('Settings file ' + testSettingsFile + ' doesn\'t contain key ' + context + '.' + key);
}
if (original[key] !== null && typeof original[key] === 'object') {
compareKeys(original[key], copy[key], context + '.' + key);
}
}
}
compareKeys(mainSettings, testSettings, 'settings');
var outputFile = './coverage/server_config/settings.json';
grunt.file.write(outputFile, JSON.stringify(testSettings, null, 4));
grunt.log.ok('File ' + outputFile + ' created');
});
grunt.registerTask('icons', [
'font:icons',
'less',
'clean:icons'
'clean:tmp'
]);
grunt.registerTask('build', [
'jshint',
'less'
'clean:build',
'copy:build',
'less',
'useminPrepare',
'concat',
'uglify',
'cssmin',
'replace',
'htmlmin:views',
'inline_angular_templates',
'filerev',
'usemin',
'htmlmin:main',
'clean:tmp'
]);
grunt.registerTask('hint', [
'jshint'
]);
grunt.registerTask('dev', [
'env:dev',
'express:dev'
]);
grunt.registerTask('built', [
'env:built',
'express:built'
]);
grunt.registerTask('test', [
'build',
'express:testSuite',
'clean:coverage',
'copy-test-server-settings',
'lineremover:beforeCoverage',
'copy:beforeCoverage',
'blanket',
'copy:coverage',
'mochaTest'
'express:test',
'mochaTest:test',
'mochaTest:coverage',
'clean:tmp'
]);
grunt.registerTask('test-current-work', [
'jshint',
'express:testSuite',
'clean:coverage',
'copy-test-server-settings',
'express:test-current-work',
'mochaTest:test-current-work',
'clean:tmp'
]);
};
+34 -14
View File
@@ -1,37 +1,57 @@
# Yellow Lab Tools
Online tool designed to help Front-End developers optimize their website
Online tool that lets you test a webpage and detects **performance** and **front-end code quality** issues.
[![Dependency Status](https://gemnasium.com/gmetais/YellowLabTools.svg)](https://gemnasium.com/gmetais/YellowLabTools) [![Build Status](https://travis-ci.org/gmetais/YellowLabTools.svg?branch=master)](https://travis-ci.org/gmetais/YellowLabTools)
### Access the tool here: [http://yellowlab.tools](http://yellowlab.tools)
![example dashboard screenshot](screenshot.png)
## How it works
The tool is based on the fabulous [Phantomas](https://github.com/macbre/phantomas) by Maciej Brencz, that loads a page and collects many web performance metrics.
I rewrote some modules to deeper analyze Javascript interactions with the DOM (especially with jQuery).
And then it is wrapped inside a small NodeJS server to get an interface (inspired by my favorite tool: [WebPageTest](http://www.webpagetest.org/)).
The tool loads the given URL via [PhantomJS](http://phantomjs.org/) (a headless browser) and collects various metrics and statistics with the help of [Phantomas](https://github.com/macbre/phantomas). These metrics are categorized and transformed into scores. It also gives in-depth details so developpers can correct the detected issues.
**There are so many things left to do, your help would be greatly appreciated! Please report bugs, ask for evolutions and come code with me.**
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!!!
## Different ways to use YLT
#### The web interface:
This is the best way to discover the tool: [http://yellowlab.tools](http://yellowlab.tools).
Please note the server is hosted in Baltimore, USA.
#### The Command Line Client:
Might be useful if you want to build an automation tool. The documentation is [here](https://github.com/gmetais/YellowLabTools/wiki/Command-Line-Interface).
#### The NPM module:
Can be used to build automation tools in NodeJS. The documentation is [here](https://github.com/gmetais/YellowLabTools/wiki/NodeJS-module).
#### The Public API:
Hosted on our http://yellowlab.tools server, it is a RESTful API that allows you to launch distant tests. The documentation is [here](https://github.com/gmetais/YellowLabTools/wiki/Public-API).
## 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).
## Help needed
I'm currently looking for help in correcting the english wordings in the tool, as it is not my native language.
## Install your own 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-own-instance).
There are so many things left to do, **your help would be greatly appreciated**! Please report bugs, ask for evolutions and come code with me.
## License
Please read the [license](LICENSE).
Please read the [license](LICENSE).
Icons are the property of [IcoMoon.io](https://icomoon.io/)
## Author
Gaël Métais. I'm a webperf freelance based in Paris. If you understand french, you can visit [my website](http://www.gaelmetais.com).
Gaël Métais. I'm a webperf freelance based in Paris. If you understand french, you can visit [my website](http://www.gaelmetais.com).
## Contributors
- Achraf Ben Younes [achrafbenyounes](https://github.com/achrafbenyounes)
-104
View File
@@ -1,104 +0,0 @@
/**
* Yellow Lab Tools main file
*/
var async = require('async');
var phantomas = require('phantomas');
var PhantomasWrapper = function() {
'use strict';
/**
* This is the phantomas launcher. It merges user chosen options into the default options
* Available options :
*
* - timeout : in seconds (default 60)
* - jsDeepAnalysis : should we inspect subrequests in the javascript execution tree (reported durations of main tasks will be slower than usual)
*
*/
this.execute = function(task, callback) {
var options = {
// Cusomizable options
timeout: task.options.timeout || 60,
'js-deep-analysis': task.options.jsDeepAnalysis || false,
// Mandatory
reporter: 'json:pretty',
'analyze-css': true,
'skip-modules': [
'blockDomains', // not needed
'domComplexity', // overriden
'domMutations', // not compatible with webkit
'domQueries', // overriden
'eventListeners', // overridden
'filmStrip', // not needed
'har', // not needed for the moment
'pageSource', // not needed
'screenshot', // not needed for the moment
'waitForSelector', // not needed
'windowPerformance' // overriden
].join(','),
'include-dirs': [
'phantomas_custom/core',
'phantomas_custom/modules'
].join(',')
};
// Output the command line for debugging purpose
console.log('If you want to reproduce the phantomas task only, copy the following command line:');
var optionsString = '';
for (var opt in options) {
optionsString += ' ' + '--' + opt + '=' + options[opt];
}
console.log('node node_modules/phantomas/bin/phantomas.js --url=' + task.url + optionsString + ' --verbose');
// Kill the application if nothing happens for 10 minutes
var killer = setTimeout(function() {
console.log('Killing the server because the test ' + task.testId + ' on ' + task.url + ' was launched 10 minutes ago');
// Forever will restart the server
process.exit(1);
}, 600000);
// It's time to launch the test!!!
var triesNumber = 3;
async.retry(triesNumber, function(cb) {
phantomas(task.url, options, function(err, json, results) {
console.log('Returning from Phantomas');
// Adding some YellowLabTools errors here
if (json && json.metrics && !json.metrics.javascriptExecutionTree) {
err = 1001;
}
if (!err && (!json || !json.metrics)) {
err = 1002;
}
// Don't cancel test if it is a timeout and we've got some results
if (err === 252 && json) {
console.log('Timeout after ' + options.timeout + ' seconds. But it\'s not a problem, the test is valid.');
err = null;
}
if (err) {
console.log('Attempt failed for test id ' + task.testId + '. Error code ' + err);
}
cb(err, {json: json, results: results});
});
}, function(err, data) {
clearTimeout(killer);
if (err) {
console.log('All ' + triesNumber + ' attemps failed for test id ' + task.testId);
}
callback(err, data.json, data.results);
});
};
};
module.exports = new PhantomasWrapper();
-8
View File
@@ -1,8 +0,0 @@
/**
* Alternative to the standard String.prototype.replace function
* Avoids problems with $$, $1, $2, ...
*/
module.exports = function(str, searched, replacement) {
return str.split(searched).join(replacement);
};
-66
View File
@@ -1,66 +0,0 @@
/**
* Creation of a queue and it's worker function
*/
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var async = require('async');
var phantomasWrapper = require('./phantomasWrapper');
var testQueue = function() {
'use strict';
var currentTask = null;
var self = this;
var queue = async.queue(function(task, callback) {
currentTask = task;
console.log('Starting test ' + task.testId);
phantomasWrapper.execute(task, function(err, json, results) {
console.log('Test ' + task.testId + ' complete');
currentTask = null;
callback(err, json, results);
self.emit('queueMoving');
});
});
// Use this method to add a test to the queue
this.push = queue.push;
// Gives the position of a task in the queue
// Returns 0 if it is the current running task
// Returns -1 if not found
this.indexOf = function(testId) {
if (currentTask && currentTask.testId === testId) {
return 0;
}
var position = -1;
if (queue.length() > 0) {
queue.tasks.forEach(function(task, index) {
if (task.data.testId === testId) {
position = index + 1;
}
});
}
return position;
};
this.testComplete = function(testId) {
self.emit('testComplete', testId);
};
this.testFailed = function(testId) {
self.emit('testFailed', testId);
};
};
// extend the EventEmitter class
util.inherits(testQueue, EventEmitter);
module.exports = new testQueue();
-27
View File
@@ -1,27 +0,0 @@
/**
* Yellow Lab Tools home page controller
*/
var async = require('async');
var fs = require ('fs');
var strReplace = require('../lib/strReplace');
var indexController = function(req, res, googleAnalyticsId) {
'use strict';
async.parallel({
htmlTemplate: function(callback) {
fs.readFile('./app/node_views/index.html', {encoding: 'utf8'}, callback);
}
}, function(err, results) {
var html = results.htmlTemplate;
html = strReplace(html, '%%GA_ID%%', googleAnalyticsId);
res.setHeader('Content-Type', 'text/html');
res.send(html);
});
};
module.exports = indexController;
@@ -1,85 +0,0 @@
/**
* Controller for the test launching page (the waiting page, after the user submited a test on the index page)
*/
var async = require('async');
var fs = require ('fs');
var strReplace = require('../lib/strReplace');
var launchTestController = function(req, res, testQueue, googleAnalyticsId) {
'use strict';
// Generate test id
var testId = (Date.now()*1000 + Math.round(Math.random()*1000)).toString(36);
var resultsPath = 'results/' + testId;
var phantomasResultsPath = resultsPath + '/results.json';
var url = req.body.url;
if (url.indexOf('http://') !== 0 && url.indexOf('https://') !== 0) {
url = 'http://' + url;
}
var options = {};
if (req.body.timeout) {
options.timeout = req.body.timeout;
}
async.waterfall([
function htmlTemplate(callback) {
fs.readFile('./app/node_views/launchTest.html', {encoding: 'utf8'}, callback);
},
function sendResponse(html, callback) {
html = strReplace(html, '%%TEST_URL%%', url);
html = strReplace(html, '%%TEST_ID%%', testId);
html = strReplace(html, '%%GA_ID%%', googleAnalyticsId);
res.setHeader('Content-Type', 'text/html');
res.send(html);
callback();
},
function createFolder(callback) {
// Create results folder
fs.mkdir(resultsPath, callback);
},
function executePhantomas(callback) {
console.log('Adding test ' + testId + ' on ' + url + ' to the queue');
var task = {
testId: testId,
url: url,
options: options
};
testQueue.push(task, callback);
},
function writeResults(json, resultsObject, callback) {
console.log('Saving Phantomas results file to ' + phantomasResultsPath);
fs.writeFile(phantomasResultsPath, JSON.stringify(json, null, 4), callback);
}
], function(err) {
if (err) {
console.log('An error occured in the phantomas test: ', err);
fs.writeFile(phantomasResultsPath, JSON.stringify({url: url, error: err}, null, 4), function(err) {
if (err) {
console.log('Could not even write an error message on file ' + phantomasResultsPath);
console.log(err);
}
});
testQueue.testFailed(testId);
} else {
testQueue.testComplete(testId);
}
});
};
module.exports = launchTestController;
-47
View File
@@ -1,47 +0,0 @@
/**
* The page that dispays the results
*/
var async = require('async');
var fs = require('fs');
var strReplace = require('../lib/strReplace');
var resultsController = function(req, res, googleAnalyticsId) {
'use strict';
var testId = req.params.testId;
var resultsPath = 'results/' + testId;
var phantomasResultsPath = resultsPath + '/results.json';
console.log('Opening test ' + testId + ' results as HTML');
async.parallel({
htmlTemplate: function(callback) {
fs.readFile('./app/node_views/results.html', {encoding: 'utf8'}, callback);
},
phantomasResults: function(callback) {
fs.readFile(phantomasResultsPath, {encoding: 'utf8'}, callback);
}
}, function(err, results) {
if (err) {
console.log(err);
return res.status(404).send('Sorry, test not found...');
}
// Escape "</script>" because it can interfer with the HTML parser
var phantomasResults = results.phantomasResults;
phantomasResults = phantomasResults.replace(/<\/script>/g, '\\u003c/script>');
var html = results.htmlTemplate;
html = strReplace(html, '%%RESULTS%%', phantomasResults);
html = strReplace(html, '%%GA_ID%%', googleAnalyticsId);
res.setHeader('Content-Type', 'text/html');
res.send(html);
});
};
module.exports = resultsController;
@@ -1,60 +0,0 @@
/**
* Socket.io handler
*/
var fs = require('fs');
var waitingQueueSocket = function(socket, testQueue) {
socket.on('waiting', function(testId) {
console.log('User waiting for test id ' + testId);
sendTestStatus(testId);
testQueue.on('testComplete', function(id) {
if (testId === id) {
socket.emit('complete');
console.log('Sending complete event to test id ' + testId);
}
});
testQueue.on('testFailed', function(id) {
if (testId === id) {
socket.emit('failed');
console.log('Sending failed event to test id ' + testId);
}
});
testQueue.on('queueMoving', function() {
var positionInQueue = testQueue.indexOf(testId);
if (positionInQueue >= 0) {
socket.emit('position', positionInQueue);
console.log('Sending position to test id ' + testId);
}
});
});
// Finds the status of a test and send it to the client
function sendTestStatus(testId) {
// Check task position in queue
var positionInQueue = testQueue.indexOf(testId);
if (positionInQueue >= 0) {
socket.emit('position', positionInQueue);
} else {
// Find in results files
var exists = fs.exists('results/' + testId + '/results.json', function(exists) {
if (exists) {
// TODO : find a way to make sure the file is completely written
setTimeout(function() {
socket.emit('complete');
}, 4000);
} else {
socket.emit('404');
}
});
}
}
};
module.exports = waitingQueueSocket;
-35
View File
@@ -1,35 +0,0 @@
<html>
<head>
<meta charset="utf-8">
<title>Yellow Lab Tools</title>
<link rel="icon" type="image/png" href="/public/img/favicon.png">
<link rel="stylesheet" type="text/css" href="/public/styles/main.css">
<link rel="stylesheet" type="text/css" href="/public/styles/index.css">
</head>
<body>
<h1>Yellow Lab <span class="icon-lab"></span> Tools</h1>
<h2 class="promess">Free online test to help speeding up <b>heavy</b> web pages</h2>
<form method="post" action="/launchTest">
<input type="text" name="url" placeholder="http://www.mysite.com" class="url" />
<input type="submit" value="Launch test" class="launchBtn" onclick="this.className += ' clicked'" />
</form>
<p>(This is a BETA, your feedback is more than welcome)</p>
<div class="footer">
<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>, based on <a href="https://github.com/macbre/phantomas" target="_blank">Phantomas</a>.<br>If you like it, <a href="https://github.com/gmetais/YellowLabTools" target="_blank" class="star">give it a <span>&#9733;</span> on GitHub</a>!</p>
</div>
<script>
var GA_ID = '%%GA_ID%%';
if (GA_ID.length > 0 && window.location.host === 'yellowlab.tools') {
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', GA_ID, 'auto');
ga('send', 'pageview');
}
</script>
</body>
</html>
-97
View File
@@ -1,97 +0,0 @@
<html>
<head>
<meta charset="utf-8">
<title>Yellow Lab Tools - Awaiting</title>
<link rel="icon" type="image/png" href="/public/img/favicon.png">
<link rel="stylesheet" type="text/css" href="/public/styles/main.css">
<link rel="stylesheet" type="text/css" href="/public/styles/launchTest.css">
<script src="/socket.io/socket.io.js"></script>
</head>
<body>
<h1>Yellow Lab <span class="icon-lab"></span> Tools</h1>
<div id="status"></div>
<div>%%TEST_URL%%</div>
<div class="footer">
<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>, based on <a href="https://github.com/macbre/phantomas" target="_blank">Phantomas</a>.<br>If you like it, <a href="https://github.com/gmetais/YellowLabTools" target="_blank" class="star">give it a <span>&#9733;</span> on GitHub</a>!</p>
</div>
<script>
var testId = '%%TEST_ID%%';
var statusElement = document.getElementById('status');
var socket = io();
function askStatus() {
socket.emit('waiting', testId);
}
socket.on('position', function(position) {
if (position === 0) {
statusElement.innerHTML = 'Test is running...';
} else {
statusElement.innerHTML = 'Waiting behind ' + (position) + ' other tests';
}
sendPositionInQueueToGA(position);
// Rotate the spinner
document.body.className = 'waiting';
});
socket.on('complete', function() {
statusElement.innerHTML = 'Test complete';
window.location.replace('/results/' + testId);
// Stop the spinner
document.body.className = '';
});
socket.on('failed', function() {
statusElement.innerHTML = 'Test failed';
window.location.replace('/results/' + testId);
// Stop the spinner
document.body.className = '';
});
socket.on('404', function() {
statusElement.innerHTML = 'Test not found';
// Stop the spinner
document.body.className = '';
});
socket.on('reconnect_error', function() {
statusElement.innerHTML = "Server reboot. Please start a new test."
// Stop the spinner
document.body.className = '';
});
askStatus();
var GA_ID = '%%GA_ID%%';
if (GA_ID.length > 0 && window.location.host === 'yellowlab.tools') {
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', GA_ID, 'auto');
}
// Need stats on average queue position
var positionAlreadySent = false;
function sendPositionInQueueToGA(position) {
if (!positionAlreadySent && GA_ID.length > 0 && window.location.host === 'yellowlab.tools') {
ga('send', 'pageview', {
queuePosition: position
});
positionAlreadySent = true;
}
}
</script>
</body>
</html>
-933
View File
@@ -1,933 +0,0 @@
<html>
<head>
<meta charset="utf-8">
<title>Yellow Lab Tools - Results page</title>
<link rel="icon" type="image/png" href="/public/img/favicon.png">
<link rel="stylesheet" type="text/css" href="/public/styles/main.css">
<link rel="stylesheet" type="text/css" href="/public/styles/results.css">
<script src="/bower_components/angular/angular.min.js"></script>
<script src="/bower_components/ngModal/dist/ng-modal.js"></script>
<script src="/public/scripts/app.js"></script>
<script src="/public/scripts/resultsCtrl.js"></script>
<script src="/public/scripts/showOffenders.js"></script>
</head>
<body ng-app="YellowLabTools" ng-controller="ResultsCtrl">
<h1>Yellow Lab <span class="icon-lab"></span> Tools</h1>
<div ng-if="undefined">Untangling and counting the spaghettis...</div>
<div class="ng-cloak">
<div>Tested url: &nbsp; <a class="testedUrl" href="{{phantomasResults.url}}" target="_blank">{{phantomasResults.url}}</a></div>
<div class="resultsMenu">
<a class="menuItem back" href="/"><div class="icon-back"></div><span>New test<span></a>
<div class="menuItem" ng-if="!phantomasResults.error" ng-class="{active: view == 'execution'}" ng-click="setView('execution')"><div class="icon-spaghetti"></div><span>JS Timeline</span></div>
<div class="menuItem" ng-if="!phantomasResults.error" ng-class="{active: view == 'summary'}" ng-click="setView('summary')"><div class="icon-summary"></div><span>Grades</span></div>
</div>
<div ng-if="phantomasResults.error">
<h2>Error: {{phantomasResults.error}}</h2>
<div ng-if="phantomasResults.error == 252">Phantomas timed out</div>
<div ng-if="phantomasResults.error == 253">Phantomas config error</div>
<div ng-if="phantomasResults.error == 254">Phantomas failed to load page</div>
<div ng-if="phantomasResults.error == 255">Phantomas internal error</div>
<div ng-if="phantomasResults.error == 1001">Javascript execution tree error</div>
<div ng-if="phantomasResults.error == 1002">JSON undefined error</div>
</div>
<div ng-show="view == 'summary' && !phantomasResults.error" class="summary">
<h2>Grades</h2>
<div class="notations">
<div>
<div ng-class="notations.domComplexity">{{notations.domComplexity}}</div>
<div class="notation">DOM complexity</div>
<div class="criteria">
<div class="table">
<div ng-if="phantomasResults.metrics.DOMelementsCount" ng-class="{'warning': phantomasResults.metrics.DOMelementsCount > 5000}">
<div class="label">DOM elements count</div>
<div class="result">
{{phantomasResults.metrics.DOMelementsCount}}
</div>
<div class="info">
<div class="icon-question" ng-click="DOMelementsCountTooltip = true"></div>
<modal-dialog show="DOMelementsCountTooltip" dialog-title="Avoid too many DOM elements">
<p>A high number of DOM elements means a lot of work for the browser to render the page.</p>
<p>It also slows down Javascript DOM queries, as there are more elements to search through.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.DOMelementMaxDepth" ng-class="{'warning': phantomasResults.metrics.DOMelementMaxDepth > 30}">
<div class="label">DOM max depth</div>
<div class="result">
{{phantomasResults.metrics.DOMelementMaxDepth}}
<show-offenders modal-title="DOM max depth" metric-name="DOMelementMaxDepth" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="DOMelementMaxDepthTooltip = true"></div>
<modal-dialog show="DOMelementMaxDepthTooltip" dialog-title="Reduce DOM depth">
<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>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.iframesCount" ng-class="{'warning': phantomasResults.metrics.iframesCount > 30}">
<div class="label">Number of iframes</div>
<div class="result">
{{phantomasResults.metrics.iframesCount}}
</div>
<div class="info">
<div class="icon-question" ng-click="DOMelementMaxDepthTooltip = true"></div>
<modal-dialog show="DOMelementMaxDepthTooltip" dialog-title="iFrames are slow">
<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>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.DOMidDuplicated" ng-class="{'warning': phantomasResults.metrics.DOMidDuplicated > 20}">
<div class="label">IDs duplicated</div>
<div class="result">
{{phantomasResults.metrics.DOMidDuplicated}}
<show-offenders modal-title="DOM IDs duplicated" metric-name="DOMidDuplicated" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="DOMidDuplicatedTooltip = true"></div>
<modal-dialog show="DOMidDuplicatedTooltip" dialog-title="Warning: duplicated DOM IDs">
<p>IDs of HTML elements must be document-wide unique. This can cause problems with getElementById returning the wrong element.</p>
</modal-dialog>
</div>
</div>
</div>
</div>
</div>
<div>
<div ng-class="notations.jsDomManipulations">{{notations.jsDomManipulations}}</div>
<div class="notation">DOM manipulations</div>
<div class="criteria">
<div class="table">
<div ng-if="phantomasResults.metrics.DOMinserts" ng-class="{'warning': phantomasResults.metrics.DOMinserts > 1000}">
<div class="label">DOM inserts</div>
<div class="result">
{{phantomasResults.metrics.DOMinserts}}
<show-offenders modal-title="DOM inserts" metric-name="DOMinserts" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="DOMinsertsTooltip = true"></div>
<modal-dialog show="DOMinsertsTooltip" dialog-title="DOM insertions are slow">
<p>Working with the DOM in Javascript triggers layout calculations and slows down the page.</p>
<p>Try, as much as possible, to have an HTML page fully generated by the server instead of making changes with JS.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.DOMqueries" ng-class="{'warning': phantomasResults.metrics.DOMqueries > 2000}">
<div class="label">DOM queries</div>
<div class="result">
{{phantomasResults.metrics.DOMqueries}}
</div>
<div class="info">
<div class="icon-question" ng-click="DOMqueriesTooltip = true"></div>
<modal-dialog show="DOMqueriesTooltip" dialog-title="Avoid having too many DOM queries">
<p>DOM queries are like looking in a large catalog of items. Even if the browsers made progress on the performances of queries, websites often make hundreds of them.</p>
<p>Try to reduce the number of queries by refactoring your Javascript code.</p>
<p>Avoid also to have a read query between two write queries. To be able to reduce the number repaints and optimize performances, browsers buffer the DOM writing operations and treat them in bulk. But each time a DOM reading is asked, the browser needs to empty the buffer. This can be particularly slow inside a loop.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.DOMqueriesAvoidable" ng-class="{'warning': phantomasResults.metrics.DOMqueriesAvoidable > 500}">
<div class="label">Avoidable queries</div>
<div class="result">
{{phantomasResults.metrics.DOMqueriesAvoidable}}
<show-offenders modal-title="Duplicated DOM queries" metric-name="DOMqueriesDuplicated" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="duplicatedQueriesCountAllTooltip = true"></div>
<modal-dialog show="duplicatedQueriesCountAllTooltip" dialog-title="Cache duplicated DOM queries">
<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>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.eventsBound" ng-class="{'warning': phantomasResults.metrics.eventsBound > 2000}">
<div class="label">Events bound</div>
<div class="result">
{{phantomasResults.metrics.eventsBound}}
<show-offenders modal-title="Events bound" metric-name="eventsBound" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="eventsBoundTooltip = true"></div>
<modal-dialog show="eventsBoundTooltip" dialog-title="Reduce the number of events binding">
<p>Binding too many events has a cost.</p>
<p>It can be avoided by using "event delegation". Instead of binding events on each element one by one, events delegation binds them on the top level document element and uses the bubbling principle. It will imperceptibly slow down the event when it occurs, but the loading of the page will speed-up.</p>
</modal-dialog>
</div>
</div>
</div>
</div>
</div>
<div>
<div ng-class="notations.jsBadPractices">{{notations.jsBadPractices}}</div>
<div class="notation">Bad Javascript</div>
<div class="criteria">
<div class="table">
<div ng-if="phantomasResults.metrics.jsErrors" ng-class="{'warning': phantomasResults.metrics.jsErrors > 5}">
<div class="label">Javascript errors</div>
<div class="result">
{{phantomasResults.metrics.jsErrors}}
<show-offenders modal-title="Javascript errors" metric-name="jsErrors" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="jsErrorsTooltip = true"></div>
<modal-dialog show="jsErrorsTooltip" dialog-title="Javascript errors">
<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>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.evalCalls">
<div class="label">eval calls</div>
<div class="result">{{phantomasResults.metrics.evalCalls}}</div>
<div class="info">
<div class="icon-question" ng-click="evalCallsTooltip = true"></div>
<modal-dialog show="evalCallsTooltip" dialog-title="Eval is evil">
<p>The 'eval' function is slow and a bad coding practice. Try to get rid of it.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.documentWriteCalls">
<div class="label">document.write calls</div>
<div class="result">{{phantomasResults.metrics.documentWriteCalls}}</div>
<div class="info">
<div class="icon-question" ng-click="documentWriteCallsTooltip = true"></div>
<modal-dialog show="documentWriteCallsTooltip" dialog-title="document.write should have never existed">
<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>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.consoleMessages">
<div class="label">Console messages</div>
<div class="result">{{phantomasResults.metrics.consoleMessages}}</div>
<div class="info">
<div class="icon-question" ng-click="consoleMessagesTooltip = true"></div>
<modal-dialog show="consoleMessagesTooltip" dialog-title="Avoid using console.log()">
<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>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.globalVariables">
<div class="label">Global variables</div>
<div class="result">
{{phantomasResults.metrics.globalVariables}}
<show-offenders modal-title="Global variables" metric-name="globalVariables" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="globalVariablesTooltip = true"></div>
<modal-dialog show="globalVariablesTooltip" dialog-title="Minimize the use of global variables">
<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>
</modal-dialog>
</div>
</div>
<div ng-if="inBodyDomManipulations > 10" ng-class="{'warning': inBodyDomManipulations > 200}">
<div class="label">DOM manipulations in body</div>
<div class="result">
{{inBodyDomManipulations}}
</div>
<div class="info">
<div class="icon-question" ng-click="inBodyDomManipulationsTooltip = true"></div>
<modal-dialog show="inBodyDomManipulationsTooltip" dialog-title="Javascript executed inside the body">
<p>This metric counts the number of DOM queries, DOM inserts, binds, etc. made by the Javascript before the DOMContentLoaded event.</p>
<p>Wait for this event before manipulating the DOM. Do not execute Javascript in the middle of the BODY as it slows down the construction of the DOM and makes a poor maintainability. This is what i call spaghetti code.</p>
<p>The JS Timeline tab can help you identify what's happening.</p>
</modal-dialog>
</div>
</div>
</div>
</div>
</div>
<div>
<div ng-class="notations.jQueryLoading">{{notations.jQueryLoading}}</div>
<div class="notation">jQuery version</div>
<div class="criteria">
<div class="table">
<div ng-if="phantomasResults.metrics.jQueryDifferentVersions == 1">
<div class="label">jQuery version</div>
<div class="result">{{phantomasResults.metrics.jQueryVersion}}</div>
<div class="info">
<div class="icon-question" ng-click="jQueryVersionTooltip = true"></div>
<modal-dialog show="jQueryVersionTooltip" dialog-title="Use the latest version of jQuery">
<p>Current latest versions of jQuery are 1.11 (with support for old IE versions) and 2.1 (without).</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>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.jQueryDifferentVersions > 1" class="warning">
<div class="label">{{phantomasResults.metrics.jQueryDifferentVersions}} versions loaded</div>
<div class="result">
<span ng-repeat="version in phantomasResults.offenders.jQueryDifferentVersions">{{version}}<span ng-show="!$last"> & </span></span>
</div>
<div class="info">
<div class="icon-question" ng-click="jQueryDifferentVersionsTooltip = true"></div>
<modal-dialog show="jQueryDifferentVersionsTooltip" dialog-title="Several versions of jQuery loaded">
<p>jQuery is a heavy library. You should **never** load jQuery more than one on the same page.</p>
</modal-dialog>
</div>
</div>
</div>
</div>
</div>
<div>
<div ng-class="notations.cssComplexity">{{notations.cssComplexity}}</div>
<div class="notation">CSS complexity</div>
<div class="criteria">
<div class="table">
<div ng-if="phantomasResults.metrics.cssRules" ng-class="{'warning': phantomasResults.metrics.cssRules > 4000 || (phantomasResults.metrics.cssRules > 1000 && phantomasResults.metrics.cssRules > phantomasResults.metrics.DOMelementsCount)}">
<div class="label">Rules count</div>
<div class="result">
{{phantomasResults.metrics.cssRules}}
</div>
<div class="info">
<div class="icon-question" ng-click="cssRulesTooltip = true"></div>
<modal-dialog show="cssRulesTooltip" dialog-title="Clean up the CSS files">
<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 files per page.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.cssComplexSelectors" ng-class="{'warning': phantomasResults.metrics.cssComplexSelectors > 2000}">
<div class="label">Complex selectors</div>
<div class="result">
{{phantomasResults.metrics.cssComplexSelectors}}
<show-offenders modal-title="Complex selectors" metric-name="cssComplexSelectors" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="cssComplexSelectorsTooltip = true"></div>
<modal-dialog show="cssComplexSelectorsTooltip" dialog-title="Avoid complex selectors">
<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.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.cssComplexSelectorsByAttribute" ng-class="{'warning': phantomasResults.metrics.cssComplexSelectorsByAttribute > 100}">
<div class="label">Complex attributes selector</div>
<div class="result">
{{phantomasResults.metrics.cssComplexSelectorsByAttribute}}
<show-offenders modal-title="Complex attributes selector" metric-name="cssComplexSelectorsByAttribute" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="cssComplexSelectorsByAttributeTooltip = true"></div>
<modal-dialog show="cssComplexSelectorsByAttributeTooltip" dialog-title="Avoid complex attribute expressions">
<p>Complex attributes selectors are one of these:
<ul>
<li>.foo[type*=bar] (contains bar)</li>
<li>.foo[type^=bar] (starts with bar)</li>
<li>.foo[type|=bar] (starts with bar or bar-)</li>
<li>.foo[type$=bar] (ends with bar)</li>
<li>.foo[type~=bar baz] (bar or baz)</li>
</ul>
</p>
<p>Their matching process needs more CPU and it has a cost on performances.</p>
</modal-dialog>
</div>
</div>
</div>
</div>
</div>
<div>
<div ng-class="notations.badCss">{{notations.badCss}}</div>
<div class="notation">Bad CSS</div>
<div class="criteria">
<div class="table">
<div ng-if="phantomasResults.metrics.cssParsingErrors" class="warning">
<div class="label">CSS syntax error</div>
<div class="result">
{{phantomasResults.metrics.cssParsingErrors}}
<show-offenders modal-title="CSS syntax errors" metric-name="cssParsingErrors" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="cssRulesTooltip = true"></div>
<modal-dialog show="cssRulesTooltip" dialog-title="Parsing error while analyzing CSS">
<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>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.cssImports" class="warning">
<div class="label">Uses of @import</div>
<div class="result">
{{phantomasResults.metrics.cssImports}}
<show-offenders modal-title="Uses of @import" metric-name="cssImports" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="cssImportsTooltip = true"></div>
<modal-dialog show="cssImportsTooltip" dialog-title="Don't use @import">
<p>Its bad for performance to use @import because CSS files don't get downloaded in parallel.</p>
<p>You should use &lt;link rel='stylesheet' href='a.css'&gt; instead.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.cssDuplicatedSelectors" ng-class="{'warning': phantomasResults.metrics.cssDuplicatedSelectors > 80}">
<div class="label">Duplicated selectors</div>
<div class="result">
{{phantomasResults.metrics.cssDuplicatedSelectors}}
<show-offenders modal-title="Duplicated selectors" metric-name="cssDuplicatedSelectors" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="cssDuplicatedSelectorsTooltip = true"></div>
<modal-dialog show="cssDuplicatedSelectorsTooltip" dialog-title="Merge duplicated selectors">
<p>This is when two or more selectors are strictly identical and should be merged.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.cssDuplicatedProperties" ng-class="{'warning': phantomasResults.metrics.cssDuplicatedProperties > 100}">
<div class="label">Duplicated properties</div>
<div class="result">
{{phantomasResults.metrics.cssDuplicatedProperties}}
<show-offenders modal-title="Duplicated properties" metric-name="cssDuplicatedProperties" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="cssDuplicatedPropertiesTooltip = true"></div>
<modal-dialog show="cssDuplicatedPropertiesTooltip" dialog-title="Remove duplicated properties">
<p>This is the number of property definitions duplicated within a selector.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.cssEmptyRules" ng-class="{'warning': phantomasResults.metrics.cssEmptyRules > 100}">
<div class="label">Empty rules</div>
<div class="result">
{{phantomasResults.metrics.cssEmptyRules}}
<show-offenders modal-title="Empty rules" metric-name="cssEmptyRules" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="cssEmptyRulesTooltip = true"></div>
<modal-dialog show="cssEmptyRulesTooltip" dialog-title="Eliminate empty rules">
<p>Very easy to fix.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.cssExpressions">
<div class="label">CSS expressions</div>
<div class="result">
{{phantomasResults.metrics.cssExpressions}}
<show-offenders modal-title="CSS expressions" metric-name="cssExpressions" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="cssExpressionsTooltip = true"></div>
<modal-dialog show="cssExpressionsTooltip" dialog-title="Number of rules with CSS expressions">
<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>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.cssImportants">
<div class="label">Uses of !important</div>
<div class="result">
{{phantomasResults.metrics.cssImportants}}
<show-offenders modal-title="Uses of !important" metric-name="cssImportants" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="cssImportantsTooltip = true"></div>
<modal-dialog show="cssImportantsTooltip" dialog-title="Don't over-use !important">
<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>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.cssOldIEFixes">
<div class="label">Old IE fixes</div>
<div class="result">
{{phantomasResults.metrics.cssOldIEFixes}}
<show-offenders modal-title="Old IE fixes" metric-name="cssOldIEFixes" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="cssOldIEFixesTooltip = true"></div>
<modal-dialog show="cssOldIEFixesTooltip" dialog-title="Clean up old IE fixes">
<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>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.cssOldPropertyPrefixes">
<div class="label">Old prefixes</div>
<div class="result">
{{phantomasResults.metrics.cssOldPropertyPrefixes}}
<show-offenders modal-title="Old prefixes" metric-name="cssOldPropertyPrefixes" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="cssOldPropertyPrefixesTooltip = true"></div>
<modal-dialog show="cssOldPropertyPrefixesTooltip" dialog-title="Clean up old CSS3 prefixes">
<p>Many property prefixes such as -moz- or -webkit- are not needed anymore, or by very few people. You can remove them or replace them with the non-prefixed version. This will help reducing your stylesheets weight.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.cssUniversalSelectors">
<div class="label">Universal selectors</div>
<div class="result">
{{phantomasResults.metrics.cssUniversalSelectors}}
<show-offenders modal-title="Universal selectors" metric-name="cssUniversalSelectors" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="cssUniversalSelectorsTooltip = true"></div>
<modal-dialog show="cssUniversalSelectorsTooltip" dialog-title="Avoid universal selectors">
<p>Universal selectors are the most expensive CSS selectors.</p>
<p>More informations <a href="http://perfectionkills.com/profiling-css-for-fun-and-profit-optimization-notes/" target="_blank">here</a>.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.cssRedundantBodySelectors">
<div class="label">Redundant body selectors</div>
<div class="result">
{{phantomasResults.metrics.cssRedundantBodySelectors}}
<show-offenders modal-title="Redundant body selectors" metric-name="cssRedundantBodySelectors" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="cssRedundantBodySelectorsTooltip = true"></div>
<modal-dialog show="cssRedundantBodySelectorsTooltip" dialog-title="Useless redundant selectors">
<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>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.cssRedundantChildNodesSelectors">
<div class="label">Redundant tags selectors</div>
<div class="result">
{{phantomasResults.metrics.cssRedundantChildNodesSelectors}}
<show-offenders modal-title="Redundant tags selectors" metric-name="cssRedundantChildNodesSelectors" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="cssRedundantChildNodesSelectorsTooltip = true"></div>
<modal-dialog show="cssRedundantChildNodesSelectorsTooltip" dialog-title="Useless redundant selectors">
<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" element is <b>always</b> inside a "ul". Same thing for "tr td", "select option", ...</p>
<p>Lowering compexity in CSS selectors can make the page load a little faster.</p>
</modal-dialog>
</div>
</div>
</div>
</div>
</div>
<div>
<div ng-class="notations.requests">{{notations.requests}}</div>
<div class="notation">Requests number</div>
<div class="criteria">
<div class="table">
<div ng-class="{'warning': phantomasResults.metrics.requests > 200}">
<div class="label">Total requests</div>
<div class="result">
{{phantomasResults.metrics.requests}}
</div>
<div class="info">
<div class="icon-question" ng-click="requestsTooltip = true"></div>
<modal-dialog show="requestsTooltip" dialog-title="Minimize the number of requests">
<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 or icon fonts</li>
<li>Base64 encode small images in HTML or stylesheets</li>
<li>Use lazyloading for images</li>
</ul>
</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.htmlCount">
<div class="label">Documents</div>
<div class="result">
{{phantomasResults.metrics.htmlCount}}
<show-offenders modal-title="HTML count" metric-name="htmlCount" phantomas-results="phantomasResults"></show-offenders>
</div>
<div></div>
</div>
<div ng-if="phantomasResults.metrics.jsCount" ng-class="{'warning': phantomasResults.metrics.jsCount > 40}">
<div class="label">Scripts</div>
<div class="result">
{{phantomasResults.metrics.jsCount}}
<show-offenders modal-title="JS count" metric-name="jsCount" phantomas-results="phantomasResults"></show-offenders>
</div>
<div></div>
</div>
<div ng-if="phantomasResults.metrics.cssCount" ng-class="{'warning': phantomasResults.metrics.cssCount > 25}">
<div class="label">Stylesheets</div>
<div class="result">
{{phantomasResults.metrics.cssCount}}
<show-offenders modal-title="CSS count" metric-name="cssCount" phantomas-results="phantomasResults"></show-offenders>
</div>
<div></div>
</div>
<div ng-if="phantomasResults.metrics.imageCount" ng-class="{'warning': phantomasResults.metrics.imageCount > 80}">
<div class="label">Images</div>
<div class="result">
{{phantomasResults.metrics.imageCount}}
<show-offenders modal-title="Image count" metric-name="imageCount" phantomas-results="phantomasResults"></show-offenders>
</div>
<div></div>
</div>
<div ng-if="phantomasResults.metrics.webfontCount" ng-class="{'warning': phantomasResults.metrics.webfontCount > 4}">
<div class="label">Fonts</div>
<div class="result">
{{phantomasResults.metrics.webfontCount}}
<show-offenders modal-title="Webfont count" metric-name="webfontCount" phantomas-results="phantomasResults"></show-offenders>
</div>
<div></div>
</div>
<div ng-if="phantomasResults.metrics.videoCount">
<div class="label">Videos</div>
<div class="result">
{{phantomasResults.metrics.videoCount}}
<show-offenders modal-title="Video count" metric-name="videoCount" phantomas-results="phantomasResults"></show-offenders>
</div>
<div></div>
</div>
<div ng-if="phantomasResults.metrics.jsonCount">
<div class="label">JSON</div>
<div class="result">
{{phantomasResults.metrics.jsonCount}}
<show-offenders modal-title="JSON count" metric-name="jsonCount" phantomas-results="phantomasResults"></show-offenders>
</div>
<div></div>
</div>
<div ng-if="phantomasResults.metrics.otherCount">
<div class="label">Other</div>
<div class="result">
{{phantomasResults.metrics.otherCount}}
<show-offenders modal-title="Other count" metric-name="otherCount" phantomas-results="phantomasResults"></show-offenders>
</div>
<div></div>
</div>
</div>
</div>
</div>
<div>
<div ng-class="notations.network">{{notations.network}}</div>
<div class="notation">Network</div>
<div class="criteria">
<div class="table">
<div ng-if="phantomasResults.metrics.notFound" class="warning">
<div class="label">404 not found</div>
<div class="result">
{{phantomasResults.metrics.notFound}}
<show-offenders modal-title="404 not found" metric-name="notFound" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="notFoundTooltip = true"></div>
<modal-dialog show="notFoundTooltip" dialog-title="404 errors">
<p>404 errors are never cached, so each time a page ask for it, it hits se server. Even if it is behind a CDN or a reverse-proxy cache.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.closedConnections" ng-class="{'warning': phantomasResults.metrics.closedConnections > 20}">
<div class="label">Connections closed</div>
<div class="result">
{{phantomasResults.metrics.closedConnections}}
<show-offenders modal-title="Connections closed" metric-name="closedConnections" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="closedConnectionsTooltip = true"></div>
<modal-dialog show="closedConnectionsTooltip" dialog-title="Connection keep-alive is important">
<p>This counts the number of requests not keeping the connection alive (specifying "Connection: close" in the response headers). It is only counting a request if it is followed by another request on the same domain.</p>
<p>This is slowing down the next request, because the brower needs to open a new connection to the server, which means a additional round-trip.</p>
<p>Correct the problem by setting a Keep-Alive header on the guilty server.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.multipleRequests" ng-class="{'warning': phantomasResults.metrics.multipleRequests > 10}">
<div class="label">Duplicated requests</div>
<div class="result">
{{phantomasResults.metrics.multipleRequests}}
<show-offenders modal-title="Static assets requested more than once" metric-name="multipleRequests" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="multipleRequestsTooltip = true"></div>
<modal-dialog show="multipleRequestsTooltip" dialog-title="The same file is requested twice">
<p>This only happens when the asset has no cache and is requested more than once on the same page. Be very careful about it.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.cachingDisabled" ng-class="{'warning': phantomasResults.metrics.cachingDisabled > 25}">
<div class="label">Caching disabled</div>
<div class="result">
{{phantomasResults.metrics.cachingDisabled}}
<show-offenders modal-title="Caching disabled" metric-name="cachingDisabled" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="cachingDisabledTooltip = true"></div>
<modal-dialog show="cachingDisabledTooltip" dialog-title="Caching disabled">
<p>Counts responses with caching disabled (max-age=0)</p>
<p>Fix immediatly if on static assets.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.cachingNotSpecified" ng-class="{'warning': phantomasResults.metrics.cachingNotSpecified > 50}">
<div class="label">Caching not specified</div>
<div class="result">
{{phantomasResults.metrics.cachingNotSpecified}}
<show-offenders modal-title="Caching not specified" metric-name="cachingNotSpecified" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="cachingNotSpecifiedTooltip = true"></div>
<modal-dialog show="cachingNotSpecifiedTooltip" dialog-title="No caching header">
<p>Responses with no caching header sent (either Cache-Control or Expires).</p>
<p>Every request should have a cache time specified. If you really don't want cache, specify "max-age=0", otherwise some browsers will try to cache.</p>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.cachingTooShort">
<div class="label">Caching too short</div>
<div class="result">
{{phantomasResults.metrics.cachingTooShort}}
<show-offenders modal-title="Caching too short" metric-name="cachingTooShort" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="cachingTooShortTooltip = true"></div>
<modal-dialog show="cachingTooShortTooltip" dialog-title="Increase cache time">
<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>
</modal-dialog>
</div>
</div>
<div ng-if="phantomasResults.metrics.domains" ng-class="{'warning': phantomasResults.metrics.domains > 40}">
<div class="label">Different domains</div>
<div class="result">
{{phantomasResults.metrics.domains}}
<show-offenders modal-title="Different domains" metric-name="domains" phantomas-results="phantomasResults"></show-offenders>
</div>
<div class="info">
<div class="icon-question" ng-click="domainsTooltip = true"></div>
<modal-dialog show="domainsTooltip" dialog-title="Reduce the number of domains">
<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>
</modal-dialog>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div ng-show="view == 'execution' && !phantomasResults.error" class="execution">
<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 < phantomasResults.metrics.domInteractive,
domInteractive: $index * timelineIntervalDuration >= phantomasResults.metrics.domInteractive
&& $index * timelineIntervalDuration < phantomasResults.metrics.domContentLoaded,
domContentLoaded: $index * timelineIntervalDuration >= phantomasResults.metrics.domContentLoaded
&& $index * timelineIntervalDuration < phantomasResults.metrics.domContentLoadedEnd,
domContentLoadedEnd: $index * timelineIntervalDuration >= phantomasResults.metrics.domContentLoadedEnd
&& $index * timelineIntervalDuration < phantomasResults.metrics.domComplete,
domComplete: $index * timelineIntervalDuration >= phantomasResults.metrics.domComplete
}">
<div style="height: {{100 * duration / timelineMax | number: 0}}px" class="color"></div>
<div class="tooltip detailsOverlay">
<div>Timestamp: {{$index * timelineIntervalDuration | number: 0}} ms</div>
</div>
</div>
</div>
<div class="startTime">0 ms</div>
<div class="endTime">{{endTime | number: 0}} ms</div>
</div>
<div class="legend">
<div class="titles">
<div class="domCreation"><div class="color"></div>DOM creation</div>
<div class="domInteractive"><div class="color"></div>DOM interactive</div>
<div class="domContentLoaded"><div class="color"></div>DOM content loaded event</div>
<div class="domContentLoadedEnd"><div class="color"></div>Page completion</div>
<div class="domComplete"><div class="color"></div>Page is complete</div>
</div>
<div class="tips">
<div>Executing Javascript and DOM queries here is a <b>bad practice</b> and slows down the DOM construction.</div>
<div>Some frameworks do things here, but it's not reliable and should be avoided.</div>
<div>Also known as "document ready". This is where you should execute <b>top-priority</b> scripts, like binding action buttons or launch a video player.</div>
<div>Here you can execute <b>mid-priority</b> tasks. Loading a script with createElement('script') is one way to do so.</div>
<div>The page is considered loaded, it's time for low <b>priority things</b> : trackers, social plugins, easter egg...</div>
</div>
</div>
</div>
<h2>Javascript Profiler</h2>
<p>
The table below shows the interactions between Javascript and the DOM. It is useful to understand what happens while the page loads.
</p>
<div class="filters">
<div>
<input type="checkbox" ng-model="warningsFilterOn" id="warningsFilterOn" />
<label for="warningsFilterOn">Show warnings only</label>
</div>
<div>
<input type="checkbox" ng-model="textFilterOn" />
Filter by
<input type="text" ng-model="textFilter" placeholder="search..." class="textFilter" ng-change="textFilterOn = true" />
</div>
</div>
<div class="table">
<toto data-info="mavariable"></toto>
<div class="headers">
<div><!-- index --></div>
<div>Type</div>
<div>Params</div>
<div><!-- details --></div>
<div>Timestamp</div>
</div>
<div ng-repeat="node in javascript.children"
ng-if="(!warningsFilterOn
|| (node.data.type == 'jQuery - bind' && node.data.callDetails.context.length > 5)
|| node.data.resultsNumber === 0
|| node.data.callDetails.context.length === 0
|| node.data.type == 'error'
|| node.data.type == 'jQuery version change')
&& (!textFilterOn
|| !textFilter.length
|| node.data.type.indexOf(textFilter) >= 0
|| node.data.callDetails.arguments[0].indexOf(textFilter) >= 0
|| node.data.callDetails.arguments[1].indexOf(textFilter) >= 0
|| node.data.callDetails.arguments[2].indexOf(textFilter) >= 0
|| node.data.callDetails.arguments[3].indexOf(textFilter) >= 0)"
ng-class="{
showingDetails: node.data.showDetails,
jsError: node.data.type == 'error' || node.data.type == 'jQuery version change',
windowPerformance: node.data.type == 'domInteractive' || node.data.type == 'domContentLoaded' || node.data.type == 'domContentLoadedEnd' || node.data.type == 'domComplete'
}">
<div class="index">{{$index + 1}}</div>
<div class="type">{{node.data.type}}</div>
<div class="value">
{{node.data.callDetails.arguments[0]}}
<span ng-if="node.data.callDetails.arguments.length > 1"> : {{node.data.callDetails.arguments[1]}}</span>
<span ng-if="node.data.callDetails.arguments.length > 2"> : {{node.data.callDetails.arguments[2]}}</span>
<span ng-if="node.data.callDetails.arguments.length > 3"> : {{node.data.callDetails.arguments[3]}}</span>
</div>
<div class="details">
<div ng-class="{
'icon-question': !(node.data.type == 'jQuery - bind' && node.data.callDetails.context.length > 5) && node.data.resultsNumber !== 0 && node.data.callDetails.context.length !== 0,
'icon-warning': node.data.type == 'jQuery - bind' && node.data.callDetails.context.length > 5 || node.data.resultsNumber === 0 || node.data.callDetails.context.length === 0
}"
ng-click="onNodeDetailsClick(node)"
ng-if="node.data.type != 'jQuery loaded'
&& node.data.type != 'jQuery version change'
&& node.data.type != 'domInteractive'
&& node.data.type != 'domContentLoaded'
&& node.data.type != 'domContentLoadedEnd'
&& node.data.type != 'domComplete'"></div>
<div class="detailsOverlay" ng-show="node.data.showDetails">
<div class="closeBtn" ng-click="onNodeDetailsClick(node)"></div>
<div ng-if="node.data.callDetails.context.domElement">
<h4>Called on DOM element</h4>
<div>{{node.data.callDetails.context.domElement}}</div>
</div>
<div ng-if="node.data.callDetails.context.length === 0">
<h4>Called on 0 jQuery element</h4>
<p class="advice">Useless function call, as the jQuery object is empty.</p>
</div>
<div ng-if="node.data.callDetails.context.length == 1 && node.data.callDetails.context.firstElementPath">
<h4>Called on 1 jQuery element</h4>
<div>{{node.data.callDetails.context.firstElementPath}}</div>
</div>
<div ng-if="node.data.callDetails.context.length > 1">
<h4>Called on {{node.data.callDetails.context.length}} jQuery elements</h4>
<p class="advice" ng-if="node.data.type == 'jQuery - bind' && node.data.callDetails.context.length > 5">
The .bind() method attaches the event listener to each jQuery element one by one. Using the .on() method is preferable if available (from v1.7).
</p>
<p ng-if="node.data.callDetails.context.firstElementPath"><b>First one is:</b> {{node.data.callDetails.context.firstElementPath}}</p>
</div>
<p class="advice" ng-if="node.data.resultsNumber === 0">
The query returned 0 results. Could it be unused or dead code?
</p>
<div ng-if="node.data.parsedBacktrace">
<h4>Backtrace</h4>
<div class="table">
<div ng-repeat="trace in node.data.parsedBacktrace track by $index">
<div>{{trace.fnName || '(anonymous)'}}</div>
<div class="trace"><a href="{{trace.filePath}}" title="{{trace.filePath}}" target="_blank">{{trace.fileName || 'HTML'}}</a>:{{trace.line}}</div>
</div>
<div ng-if="node.data.parsedBacktrace.length == 0 && node.data.type != 'script loaded'">
<div>can't find any backtrace :/</div>
</div>
</div>
</div>
<div ng-if="node.children.length > 0">
<h4>Sub processes</h4>
<div class="table">
<div class="headers">
<div><!-- index --></div>
<div>Type</div>
<div>Params</div>
<div>Duration</div>
</div>
<div ng-repeat="node in node.children">
<div class="index">{{$index}}</div>
<div class="type">{{node.data.type}}</div>
<div class="value">
{{node.data.callDetails.arguments[0]}}
<span ng-if="node.data.callDetails.arguments.length > 1"> : {{node.data.callDetails.arguments[1]}}</span>
<span ng-if="node.data.callDetails.arguments.length > 2"> : {{node.data.callDetails.arguments[2]}}</span>
<span ng-if="node.data.callDetails.arguments.length > 3"> : {{node.data.callDetails.arguments[3]}}</span>
</div>
<div class="duration" ng-if="node.data.time != undefined">{{node.data.time}} ms</div>
<div class="duration" ng-if="node.data.time == undefined"></div>
</div>
</div>
</div>
</div>
</div>
<div class="startTime" ng-class="node.data.loadingStep">{{node.data.timestamp | number: 0}} ms</div>
</div>
</div>
</div>
</div>
<div class="footer">
<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>, based on <a href="https://github.com/macbre/phantomas" target="_blank">Phantomas</a>.<br>If you like it, <a href="https://github.com/gmetais/YellowLabTools" target="_blank" class="star">give it a <span>&#9733;</span> on GitHub</a>!</p>
</div>
<script>
var _phantomas_results = %%RESULTS%%;
var GA_ID = '%%GA_ID%%';
if (GA_ID.length > 0 && window.location.host === 'yellowlab.tools') {
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', GA_ID, 'auto');
ga('send', 'pageview');
}
</script>
</body>
</html>
Binary file not shown.
-5
View File
@@ -1,5 +0,0 @@
angular.module('YellowLabTools', [
'Results',
'ngModal',
'ShowOffendersDirective'
]);
-406
View File
@@ -1,406 +0,0 @@
var app = angular.module('Results', []);
app.controller('ResultsCtrl', function ($scope) {
// Grab results from nodeJS served page
$scope.phantomasResults = window._phantomas_results;
$scope.view = 'execution';
if ($scope.phantomasResults.metrics && $scope.phantomasResults.offenders && $scope.phantomasResults.offenders.javascriptExecutionTree) {
// Get the execution tree from the offenders
$scope.javascript = JSON.parse($scope.phantomasResults.offenders.javascriptExecutionTree);
// Sort globalVariables offenders alphabetically
if ($scope.phantomasResults.offenders.globalVariables) {
$scope.phantomasResults.offenders.globalVariables.sort();
}
initSummaryView();
initJSTimelineView();
}
$scope.setView = function(viewName) {
$scope.view = viewName;
};
$scope.onNodeDetailsClick = function(node) {
var isOpen = node.data.showDetails;
if (!isOpen) {
// Close all other nodes
$scope.javascript.children.forEach(function(currentNode) {
currentNode.data.showDetails = false;
});
// Parse the backtrace
if (!node.data.parsedBacktrace) {
node.data.parsedBacktrace = parseBacktrace(node.data.backtrace);
}
}
node.data.showDetails = !isOpen;
};
function initSummaryView() {
// Read the main elements of the tree and sum the total time
$scope.totalJSTime = 0;
$scope.inBodyDomManipulations = 0;
treeRunner($scope.javascript, function(node) {
if (node.data.time) {
$scope.totalJSTime += node.data.time;
}
if (node.data.timestamp < $scope.phantomasResults.metrics.domInteractive &&
node.data.type !== 'jQuery - onDOMReady') {
$scope.inBodyDomManipulations ++;
}
if (node.data.type !== 'main') {
// Don't check the children
return false;
}
});
// Grab the notes
$scope.notations = {
domComplexity: getDomComplexityScore(),
jsDomManipulations: getJsDomManipulationsScore(),
jsBadPractices: getJSBadPracticesScore(),
jQueryLoading: getJQueryLoadingScore(),
cssComplexity: getCSSComplexityScore(),
badCss: getBadCssScore(),
requests: requestsScore(),
network: networkScore()
};
}
function initJSTimelineView() {
if (!$scope.javascript.children) {
return;
}
// Read the execution tree and adjust the navigation timings (cause their not very well synchronised)
treeRunner($scope.javascript, function(node) {
switch(node.data.type) {
case 'domInteractive':
$scope.phantomasResults.metrics.domInteractive = node.data.timestamp;
break;
case 'domContentLoaded':
$scope.phantomasResults.metrics.domContentLoaded = node.data.timestamp;
break;
case 'domContentLoadedEnd':
$scope.phantomasResults.metrics.domContentLoadedEnd = node.data.timestamp;
break;
case 'domComplete':
$scope.phantomasResults.metrics.domComplete = node.data.timestamp;
break;
}
if (node.data.type !== 'main') {
// Don't check the children
return false;
}
});
// Now read the tree and display it on a timeline
// Split the timeline into 200 intervals
var numberOfIntervals = 199;
var lastEvent = $scope.javascript.children[$scope.javascript.children.length - 1];
$scope.endTime = lastEvent.data.timestamp + (lastEvent.data.time || 0);
$scope.timelineIntervalDuration = $scope.endTime / numberOfIntervals;
// Pre-fill array of as many elements as there are milleseconds
var millisecondsArray = Array.apply(null, new Array($scope.endTime + 1)).map(Number.prototype.valueOf,0);
// Create the milliseconds array from the execution tree
treeRunner($scope.javascript, 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;
}
}
if (node.data.type !== 'main') {
// Don't check the children
return false;
}
});
// 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 getDomComplexityScore() {
var note = 'A';
var score = $scope.phantomasResults.metrics.DOMelementsCount +
Math.pow($scope.phantomasResults.metrics.DOMelementMaxDepth, 2) +
$scope.phantomasResults.metrics.iframesCount * 50 +
$scope.phantomasResults.metrics.DOMidDuplicated * 25;
if (score > 1000) {
note = 'B';
}
if (score > 1500) {
note = 'C';
}
if (score > 2000) {
note = 'D';
}
if (score > 3000) {
note = 'E';
}
if (score > 4000) {
note = 'F';
}
return note;
}
function getJsDomManipulationsScore() {
var note = 'A';
var score = $scope.phantomasResults.metrics.DOMinserts * 2 +
$scope.phantomasResults.metrics.DOMqueries +
$scope.phantomasResults.metrics.DOMqueriesAvoidable * 2 +
$scope.phantomasResults.metrics.eventsBound;
if (score > 300) {
note = 'B';
}
if (score > 500) {
note = 'C';
}
if (score > 700) {
note = 'D';
}
if (score > 1000) {
note = 'E';
}
if (score > 1400) {
note = 'F';
}
return note;
}
function getJSBadPracticesScore() {
var note = 'A';
var score = $scope.phantomasResults.metrics.documentWriteCalls * 3 +
$scope.phantomasResults.metrics.evalCalls * 2 +
$scope.phantomasResults.metrics.jsErrors * 10 +
$scope.phantomasResults.metrics.consoleMessages / 2 +
$scope.phantomasResults.metrics.globalVariables / 20 +
Math.sqrt($scope.inBodyDomManipulations);
if (score > 10) {
note = 'B';
}
if (score > 15) {
note = 'C';
}
if (score > 20) {
note = 'D';
}
if (score > 30) {
note = 'E';
}
if (score > 45) {
note = 'F';
}
return note;
}
function getJQueryLoadingScore() {
var note = 'NA';
if ($scope.phantomasResults.metrics.jQueryDifferentVersions > 1) {
note = 'F';
} else if ($scope.phantomasResults.metrics.jQueryVersion) {
if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.11.') === 0 ||
$scope.phantomasResults.metrics.jQueryVersion.indexOf('1.12.') === 0 ||
$scope.phantomasResults.metrics.jQueryVersion.indexOf('2.1.') === 0 ||
$scope.phantomasResults.metrics.jQueryVersion.indexOf('2.2.') === 0) {
note = 'A';
} else if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.9.') === 0 ||
$scope.phantomasResults.metrics.jQueryVersion.indexOf('1.10.') === 0 ||
$scope.phantomasResults.metrics.jQueryVersion.indexOf('2.0.') === 0) {
note = 'B';
} else if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.7.') === 0 ||
$scope.phantomasResults.metrics.jQueryVersion.indexOf('1.8.') === 0) {
note = 'C';
} else if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.5.') === 0 ||
$scope.phantomasResults.metrics.jQueryVersion.indexOf('1.6.') === 0) {
note = 'D';
} else if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.2.') === 0 ||
$scope.phantomasResults.metrics.jQueryVersion.indexOf('1.3.') === 0 ||
$scope.phantomasResults.metrics.jQueryVersion.indexOf('1.4.') === 0) {
note = 'E';
}
}
return note;
}
function getCSSComplexityScore() {
if (!$scope.phantomasResults.metrics.cssRules) {
return 'NA';
}
var note = 'A';
var score = $scope.phantomasResults.metrics.cssRules +
$scope.phantomasResults.metrics.cssComplexSelectors * 5 +
$scope.phantomasResults.metrics.cssComplexSelectorsByAttribute * 10;
if (score > 800) {
note = 'B';
}
if (score > 1200) {
note = 'C';
}
if (score > 2500) {
note = 'D';
}
if (score > 4000) {
note = 'E';
}
if (score > 6000) {
note = 'F';
}
return note;
}
function getBadCssScore() {
if ($scope.phantomasResults.metrics.cssParsingErrors) {
return 'F';
} else if (!$scope.phantomasResults.metrics.cssRules) {
return 'NA';
}
var note = 'A';
var score = $scope.phantomasResults.metrics.cssDuplicatedSelectors +
$scope.phantomasResults.metrics.cssDuplicatedProperties +
$scope.phantomasResults.metrics.cssEmptyRules +
$scope.phantomasResults.metrics.cssExpressions * 10 +
$scope.phantomasResults.metrics.cssImportants * 2 +
$scope.phantomasResults.metrics.cssOldIEFixes * 10 +
$scope.phantomasResults.metrics.cssOldPropertyPrefixes +
$scope.phantomasResults.metrics.cssUniversalSelectors * 5 +
$scope.phantomasResults.metrics.cssRedundantBodySelectors * 0.5 +
$scope.phantomasResults.metrics.cssRedundantChildNodesSelectors * 0.5 +
$scope.phantomasResults.metrics.cssImports * 50;
if (score > 50) {
note = 'B';
}
if (score > 100) {
note = 'C';
}
if (score > 200) {
note = 'D';
}
if (score > 500) {
note = 'E';
}
if (score > 1000) {
note = 'F';
}
return note;
}
function requestsScore() {
var note = 'A';
var score = $scope.phantomasResults.metrics.requests;
if (score > 30) {
note = 'B';
}
if (score > 45) {
note = 'C';
}
if (score > 60) {
note = 'D';
}
if (score > 80) {
note = 'E';
}
if (score > 100) {
note = 'F';
}
return note;
}
function networkScore() {
var note = 'A';
var score = $scope.phantomasResults.metrics.notFound * 25 +
$scope.phantomasResults.metrics.closedConnections * 10 +
$scope.phantomasResults.metrics.multipleRequests * 10 +
$scope.phantomasResults.metrics.cachingDisabled * 2 +
$scope.phantomasResults.metrics.cachingNotSpecified +
$scope.phantomasResults.metrics.cachingTooShort / 2 +
$scope.phantomasResults.metrics.domains;
if (score > 20) {
note = 'B';
}
if (score > 40) {
note = 'C';
}
if (score > 60) {
note = 'D';
}
if (score > 80) {
note = 'E';
}
if (score > 100) {
note = 'F';
}
return note;
}
function parseBacktrace(str) {
if (!str) {
return null;
}
var out = [];
var splited = str.split(' / ');
splited.forEach(function(trace) {
var result = /^(\S*)\s?\(?(https?:\/\/\S+):(\d+)\)?$/g.exec(trace);
if (result && result[2].length > 0) {
var filePath = result[2];
var chunks = filePath.split('/');
var fileName = chunks[chunks.length - 1];
out.push({
fnName: result[1],
fileName: fileName,
filePath: filePath,
line: result[3]
});
}
});
return out;
}
// Goes on every node of the tree and calls the function fn. If fn returns false on a node, its children won't be checked.
function treeRunner(node, fn) {
if (fn(node) !== false && node.children) {
node.children.forEach(function(child) {
treeRunner(child, fn);
});
}
}
});
-17
View File
@@ -1,17 +0,0 @@
var app = angular.module("ShowOffendersDirective", []);
app.directive('showOffenders', function() {
return {
restrict: 'E',
transclude: true,
scope: {
modalTitle: "@",
metricName: "@",
phantomasResults: "="
},
controller: function($scope, $element, $attrs, $location) {
$scope.dialogShown = false;
},
template: '&nbsp;<span ng-click="dialogShown = true" class="icon-eye" title="See offenders"></span><modal-dialog show="dialogShown" dialog-title="{{modalTitle}}: {{phantomasResults.metrics[metricName]}}" width="70%"><ul><li ng-repeat="offender in phantomasResults.offenders[metricName] track by $index">{{offender}}</li></ul></modal-dialog>'
};
});
-25
View File
@@ -1,25 +0,0 @@
#status {
margin-top: 2em;
font-size: 2.5em;
}
@-webkit-keyframes rotating {
from {
-webkit-transform: rotate(0deg);
}
to {
-webkit-transform: rotate(360deg);
}
}
@keyframes rotating {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.waiting .icon-lab {
-webkit-animation: rotating 3s linear 0s infinite;
-webkit-transform: translateZ(0);
animation: rotating 3s linear 0s infinite;
}
-19
View File
@@ -1,19 +0,0 @@
#status {
margin-top: 2em;
font-size: 2.5em;
}
@-webkit-keyframes rotating {
from { -webkit-transform: rotate(0deg); }
to { -webkit-transform: rotate(360deg); }
}
@keyframes rotating {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.waiting .icon-lab {
-webkit-animation: rotating 3s linear 0s infinite;
-webkit-transform: translateZ(0);
animation: rotating 3s linear 0s infinite;
}
-66
View File
@@ -1,66 +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;
}
.footer {
padding: 3em;
color: #4e1836;
a {
color: inherit;
}
.star {
font-weight: bold;
span {
font-size: 1.2em;
}
}
}
/* Icons */
.icon-lab {
.icon(@lab);
}
.icon-question {
.icon(@question);
}
.icon-warning {
.icon(@warning);
}
.icon-back {
.icon(@arrow-left3);
}
.icon-summary {
.icon(@list);
}
.icon-spaghetti {
.icon(@bars);
}
.icon-eye {
.icon(@eye);
}
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env node
var debug = require('debug')('ylt:cli');
var meow = require('meow');
var path = require('path');
var ylt = require('../lib/index');
var cli = meow({
help: [
'Usage',
' yellowlabtools <url> <options>',
'',
'Options:',
' --screenshot Will take a screenshot and use this value as the output path. It needs to end with ".png".',
' --js-deep-analysis When activated, the javascriptExecutionTree will contain sub-requests.',
''
].join('\n'),
pkg: '../package.json'
});
// Check parameters
if (cli.input.length < 1) {
console.error('Incorrect parameters: url not provided');
process.exit(1);
}
var url = cli.input[0];
var options = {};
// Screenshot option
var screenshot = cli.flags.screenshot;
if (screenshot && (typeof screenshot !== 'string' || screenshot.toLowerCase().indexOf('.png', screenshot.length - 4) === -1)) {
console.error('Incorrect parameters: screenshot must be a path that ends with ".png"');
process.exit(1);
}
if (screenshot) {
if (path.resolve(screenshot) !== path.normalize(screenshot)) {
// It is not an absolute path, so it is relative to the current command-line directory
screenshot = path.join(process.cwd(), screenshot);
}
options.screenshot = cli.flags.screenshot;
}
// Deep JS analysis option
if (cli.flags.jsDeepAnalysis === true || cli.flags.jsDeepAnalysis === 'true') {
options.jsDeepAnalysis = true;
}
(function execute(url, options) {
'use strict';
ylt(url, options).
then(function(data) {
debug('Success');
console.log(JSON.stringify(data, null, 2));
}).fail(function(err) {
debug('Test failed for %s', url);
console.error(err);
});
debug('Test launched...');
})(url, options);
+38
View File
@@ -0,0 +1,38 @@
var express = require('express');
var app = express();
var server = require('http').createServer(app);
var bodyParser = require('body-parser');
var compress = require('compression');
var cors = require('cors');
var authMiddleware = require('../lib/server/middlewares/authMiddleware');
var apiLimitsMiddleware = require('../lib/server/middlewares/apiLimitsMiddleware');
// Middlewares
app.use(compress());
app.use(bodyParser.json());
app.use(cors());
app.use(authMiddleware);
app.use(apiLimitsMiddleware);
// Initialize the controllers
var apiController = require('../lib/server/controllers/apiController')(app);
var frontController = require('../lib/server/controllers/frontController')(app);
// Let's start the server!
if (!process.env.GRUNTED) {
var settings = require('../server_config/settings.json');
server.listen(settings.serverPort, function() {
console.log('Listening on port %d', server.address().port);
// For the tests
if (server.startTests) {
server.startTests();
}
});
}
module.exports = app;
+8 -2
View File
@@ -1,7 +1,13 @@
{
"name": "yellowlabtools",
"dependencies": {
"angular": "~1.3.1",
"ngModal": "git://github.com/gmetais/ngModal.git#1.2.3"
"angular": "~1.3.14",
"angular-route": "~1.3.14",
"angular-resource": "~1.3.14",
"angular-sanitize": "~1.3.14",
"angular-animate": "~1.3.14"
},
"resolutions": {
"angular": "~1.3.8"
}
}
+10
View File
@@ -0,0 +1,10 @@
.about {
margin: 3em auto;
width: 50%;
}
.about p {
margin: 2em;
}
.about a {
color: #FFF;
}
+179
View File
@@ -0,0 +1,179 @@
.testedUrl {
color: inherit;
}
.summary {
text-align: center;
}
.summary .globalScore {
display: table;
width: 60%;
margin: 3em auto;
}
.summary .globalScore > div {
display: table-cell;
width: 50%;
vertical-align: middle;
}
.summary .globalScore .globalGrade {
margin: 0.5 auto;
width: 2.5em;
height: 2.5em;
line-height: 2.5em;
border-radius: 0.5em;
font-size: 3em;
font-weight: bold;
vertical-align: middle;
}
.summary .globalScore .on100 {
font-size: 1.2em;
font-weight: bold;
margin: 0.5em 0 1em;
}
.summary .globalScore .screenshotWrapper:hover {
opacity: 0.75;
}
.summary .globalScore .screenshotWrapper:hover:after {
position: absolute;
width: 1.25em;
height: 1.25em;
top: 0.7em;
left: 1.55em;
font-size: 3em;
color: #FFF;
background: #000;
border-radius: 0.2em;
text-align: center;
content: "+";
opacity: 0.85;
}
.summary .notations {
display: table;
width: 80%;
margin: 0 10% 1.5em;
border-spacing: 1em;
}
.summary .notations > div {
display: table-row;
}
.summary .notations > div > div {
display: table-cell;
height: 2.5em;
vertical-align: middle;
}
.summary .notations .category {
font-weight: bold;
text-align: center;
width: 20%;
}
.summary .notations .criteria {
font-weight: normal;
width: 75%;
}
.summary .notations .A.categoryScore,
.summary .notations .B.categoryScore,
.summary .notations .C.categoryScore,
.summary .notations .D.categoryScore,
.summary .notations .E.categoryScore,
.summary .notations .F.categoryScore,
.summary .notations .NA.categoryScore {
width: 2.5em;
max-width: 2.5em;
min-width: 2.5em;
font-size: 2em;
text-align: center;
border-radius: 0.5em;
font-weight: bold;
}
.summary .notations .grade .A,
.summary .notations .grade .B,
.summary .notations .grade .C,
.summary .notations .grade .D,
.summary .notations .grade .E,
.summary .notations .grade .F,
.summary .notations .grade .NA {
width: 1em;
height: 1em;
font-size: 1em;
color: transparent;
margin: 0 auto;
border-radius: 0.5em;
}
.summary .notations .criteria .table {
width: 100%;
}
.summary .notations .criteria .table > div:hover > div {
background: #EBD8E2;
cursor: pointer;
}
.summary .notations .criteria .table > div:hover > div.info {
background: #FFF;
}
.summary .notations .criteria .table > div:hover > div.info .icon-question {
color: #EBD8E2;
}
.summary .notations .criteria .grade {
width: 10%;
padding-left: 0.5em;
padding-right: 0.5em;
vertical-align: middle;
}
.summary .notations .criteria .label {
width: 70%;
}
.summary .notations .criteria .result {
width: 18%;
font-weight: bold;
white-space: nowrap;
text-align: center;
vertical-align: middle;
}
.summary .notations .warning .label,
.summary .notations .warning .result,
.summary .notations .icon-warning {
color: #FF1919;
}
.summary .notations .criteria .info {
width: 2%;
text-align: center;
vertical-align: middle;
background: #FFF;
padding-left: 0.1em;
padding-right: 0.1em;
}
.summary .notations .criteria .icon-question {
color: transparent;
}
.summary .fromShare {
margin-bottom: 3em;
}
.summary .apiTip {
font-size: 0.8em;
margin-bottom: 4em;
color: #413;
}
.summary .apiTip a {
color: inherit;
}
.summary .tweet .tweetText {
color: #413;
background: #F2F2F2;
border: none;
width: 25em;
padding: 0.4em;
border-radius: 0.5em;
box-shadow: 0.05em 0.1em 0 0 #999;
}
.summary .tweet .tweetButton,
.summary .tweet .linkedinButton {
color: #413;
background: #F2F2F2;
margin-right: 0;
}
.summary .tweet .tweetButton:hover,
.summary .tweet .linkedinButton:hover {
color: #F2F2F2;
background: #e74c3c;
}
.summary .tweet input {
font-size: 0.9em;
}
+6
View File
@@ -0,0 +1,6 @@
@font-face {
font-family: "fontsmith-icons";
src: url("/fonts/icons.woff") format("woff");
font-weight: normal;
font-style: normal;
}
@@ -10,6 +10,21 @@
background: #e74c3c;
color: #fff;
}
.launchBtn.disabled {
background: #deaca6;
}
.features {
display: table;
width: 50%;
margin: 8em auto 0;
font-size: 0.9em;
color: #413;
}
.features > div {
width: 33.3%;
display: table-cell;
padding: 0 1.5em;
}
input[type=submit],
input.url {
padding: 0 0.5em;
@@ -1,6 +1,6 @@
@font-face {
font-family: "fontsmith-icons";
src: url("/public/fonts/icons.woff") format("woff");
src: url("/fonts/icons.woff") format("woff");
font-weight: normal;
font-style: normal;
}
@@ -32,18 +32,152 @@ h1 span {
width: 1em;
color: #ffa319;
}
.resultsMenu {
margin-top: 2em;
}
.resultsMenu .menuItem {
display: inline-block;
margin: 1em;
width: 8em;
height: 7em;
color: #fff;
border: 3px solid #fff;
border-radius: 0.5em;
cursor: pointer;
text-decoration: none;
}
.resultsMenu .menuItem.back,
.resultsMenu .menuItem.restart {
color: #413;
border-color: #413;
}
.resultsMenu .menuItem div {
padding-top: 0.5em;
font-size: 3em;
}
.resultsMenu .active,
.resultsMenu .menuItem.active:hover {
color: #ffa319;
border-color: #ffa319;
}
.resultsMenu .menuItem:hover {
color: #ffa319;
}
.resultsMenu span {
position: relative;
top: 0.5em;
}
/* Grade colors */
.A {
/* green */
background: #0C4;
}
.B {
/* green */
background: #CD0;
}
.C {
/* yellow */
background: #FD2;
}
.D {
/* orange */
background: #FA2;
}
.E {
/* red */
background: #F60;
}
.F {
/* red */
background: #F22;
}
.NA {
/* Non applicable */
background: #CCC;
}
.board {
margin-top: 2em;
padding: 1em;
background: #fff;
color: #000;
border-radius: 0.5em;
text-align: left;
}
.backToDashboard {
text-align: center;
}
.backToDashboard a {
font-size: 0.9em;
display: block;
margin-top: 4em;
color: black;
}
a.linkButton {
font-size: 1em;
padding: 0.3em 0.5em;
margin: 0.5em;
line-height: 2em;
border: 0 solid;
border-radius: 0.5em;
box-shadow: 0.1em 0.2em 0 0 #5e2846;
background: #e74c3c;
color: #fff;
text-decoration: none;
}
.screenshotWrapper.desktop {
display: inline-block;
position: relative;
border: 0.2em solid #AAA;
background: #000;
padding: 0.5em;
border-top-left-radius: 0.4em;
border-top-right-radius: 0.4em;
}
.screenshotWrapper.desktop:before {
position: absolute;
width: 15em;
height: 0.6em;
bottom: -0.75em;
left: -1em;
background: #CCC;
border-bottom-left-radius: 0.2em;
border-bottom-right-radius: 0.2em;
content: " ";
}
.screenshotWrapper.desktop:after {
position: absolute;
width: 0.4em;
height: 0.2em;
bottom: -0.55em;
left: 12.5em;
background: lime;
content: " ";
}
.screenshotWrapper.desktop > div {
width: 12em;
height: 6.75em;
overflow: scroll;
position: relative;
}
.screenshotWrapper.desktop .screenshotImage {
width: 100%;
}
.star {
font-weight: bold;
}
.star span {
font-size: 1.2em;
}
.footer {
padding: 3em;
color: #4e1836;
color: #413;
}
.footer a {
color: inherit;
}
.footer .star {
font-weight: bold;
}
.footer .star span {
font-size: 1.2em;
.footer .version {
font-size: 0.7em;
}
/* Icons */
.icon-lab {
@@ -57,7 +191,7 @@ h1 span {
-webkit-font-smoothing: antialiased;
}
.icon-lab:before {
content: "\e003";
content: "\e004";
}
.icon-question {
font-family: "fontsmith-icons";
@@ -109,7 +243,7 @@ h1 span {
-webkit-font-smoothing: antialiased;
}
.icon-summary:before {
content: "\e002";
content: "\e003";
}
.icon-spaghetti {
font-family: "fontsmith-icons";
@@ -124,7 +258,7 @@ h1 span {
.icon-spaghetti:before {
content: "\e005";
}
.icon-eye {
.icon-loop {
font-family: "fontsmith-icons";
speak: none;
font-style: normal;
@@ -134,6 +268,6 @@ h1 span {
line-height: 1;
-webkit-font-smoothing: antialiased;
}
.icon-eye:before {
content: "\e004";
.icon-loop:before {
content: "\e002";
}
+8
View File
@@ -0,0 +1,8 @@
.status {
margin-top: 2em;
font-size: 2.5em;
}
.statusSubMessage {
font-size: 0.8em;
margin-bottom: 6em;
}
+193
View File
@@ -0,0 +1,193 @@
.rule.board {
text-align: center;
}
.rule .ruleTable {
display: table;
border-spacing: 1em;
width: 90%;
margin: 2em auto;
background: #f2f2f2;
border: 1px dashed #666;
border-radius: 0.5em;
}
.rule .ruleTable > div {
display: table-cell;
vertical-align: middle;
}
.rule .ruleTable .left {
width: 33%;
font-weight: bold;
}
.rule .ruleTable .right {
width: 67%;
}
.rule .score {
font-size: 2.5em;
line-height: 2em;
height: 2em;
width: 2em;
border-radius: 0.5em;
margin: 0 auto 0.5em;
}
.rule .message {
width: 80%;
margin: 0 auto;
}
.rule .message p {
margin: 0.5em;
}
.rule .message ul {
list-style-type: none;
padding-left: 0;
}
.rule .message li:before {
content: '\25e6';
margin-right: 0.3em;
font-size: 1.2em;
position: relative;
top: 0.1em;
}
.rule .warning {
width: 90%;
margin: -1em auto 2em;
background: #FEE;
border: 1px dashed #e74c3c;
color: #e74c3c;
border-radius: 0.5em;
}
.rule .offendersTable {
display: table;
border-spacing: 0 0.25em;
margin: 0 auto;
min-width: 10%;
max-width: 90%;
}
.rule .offendersTable > div {
display: table-row;
}
.rule .offendersTable > div > div {
display: table-cell;
background: #f2f2f2;
padding: 0 1em;
word-wrap: break-word;
word-break: break-all;
}
.rule .offendersTable > div > div:hover {
background: #EBD8E2;
}
.rule .notFound {
font-size: 1em;
}
.rule .notFound h2 {
font-size: 3em;
margin-bottom: 1em;
}
.offenders .offenderButton {
display: inline-block;
position: relative;
background: #efe;
padding: 0 0.5em;
margin: 0.2em 0;
border-radius: 0.4em;
z-index: 1;
}
.offenders .offenderButton.opens {
padding-right: 0.75em;
}
.offenders .offenderButton.opens:after {
position: relative;
left: 0.5em;
content: '\25BC';
font-size: 0.8em;
}
.offenders .offenderButton > div {
display: none;
position: absolute;
right: 0;
min-width: 100%;
background: inherit;
border-bottom-left-radius: 0.4em;
border-bottom-right-radius: 0.4em;
border-top: 1px solid #999;
z-index: 2;
}
.offenders .offenderButton .domTree {
text-align: left;
white-space: nowrap;
}
.offenders .offenderButton .domTree > div {
margin: 0.5em;
}
.offenders .offenderButton .domTree > div div {
margin-left: 1em;
}
.offenders .offenderButton .backtrace,
.offenders .offenderButton .cssFileAndLine {
white-space: nowrap;
padding: 0.5em;
}
.offenders .offenderButton.opens:hover {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
background: #ffe0cc;
z-index: 2;
}
.offenders .offenderButton.opens:hover > div {
display: block;
}
.offendersHtml {
display: inline-block;
}
.domTree div {
text-align: left;
margin-left: 1em;
}
.domTree div span:only-child {
font-weight: bold;
}
.domTree div span:only-child span {
font-style: italic;
font-weight: normal;
}
.colorPalette {
width: 30em;
border: 2px solid #000;
text-align: left;
/* Checkerboard background */
background-color: #ddd;
background-image: linear-gradient(45deg, #aaaaaa 25%, transparent 25%, transparent 75%, #aaaaaa 75%, #aaaaaa), linear-gradient(45deg, #aaaaaa 25%, transparent 25%, transparent 75%, #aaaaaa 75%, #aaaaaa);
background-size: 1em 1em;
background-position: 0 0, 0.5em 0.5em;
}
.colorPalette > div {
display: inline-block;
height: 2em;
position: relative;
}
.colorPalette > div div {
display: none;
position: absolute;
left: 100%;
top: 100%;
background: #FFF;
padding: 0.5em;
border: 2px solid #f1c40f;
border-radius: 0.5em;
white-space: nowrap;
z-index: 3;
font-weight: bold;
}
.colorPalette > div:hover div {
display: block;
}
.colorPalette > div:hover:after {
content: " ";
position: absolute;
background-color: inherit;
left: -0.2em;
top: -0.2em;
width: 100%;
height: 100%;
z-index: 2;
border: 0.2em solid #f1c40f;
}
+7
View File
@@ -0,0 +1,7 @@
.screenshot.board {
text-align: center;
}
.screenshot .screenshotWrapper {
font-size: 2.08333333333333em;
margin-bottom: 0.5em;
}
@@ -1,143 +1,19 @@
/* Timeline colors, related to Window Performances */
.resultsMenu {
margin-top: 2em;
}
.resultsMenu .menuItem {
display: inline-block;
margin: 1em;
width: 8em;
height: 7em;
color: #fff;
border: 3px solid #fff;
border-radius: 0.5em;
cursor: pointer;
text-decoration: none;
}
.resultsMenu .back {
color: #5e2846;
border-color: #5e2846;
}
.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;
}
.testedUrl {
color: inherit;
}
h4 {
margin-bottom: 0.5em;
}
.summary,
.metrics,
.execution {
margin-top: 2em;
padding: 1em;
background: #fff;
color: #000;
border-radius: 0.5em;
text-align: left;
}
.notations {
display: table;
width: 90%;
margin: 0 10%;
border-spacing: 1em;
}
.notations > div {
display: table-row;
}
.notations > div > div {
display: table-cell;
height: 2.5em;
vertical-align: middle;
}
.notations .notation {
font-weight: bold;
text-align: center;
}
.notations .criteria {
font-weight: normal;
.selectScript {
padding-bottom: 2em;
font-size: 0.9em;
}
.notations .A,
.notations .B,
.notations .C,
.notations .D,
.notations .E,
.notations .F,
.notations .NA {
width: 2.5em;
font-size: 2em;
text-align: center;
border-radius: 0.5em;
font-weight: bold;
.selectScript select {
max-width: 30em;
}
.notations .A {
/* green */
background: #00DB61;
.selectScript.empty {
font-size: 0.8em;
}
.notations .B {
/* green */
background: #CAD63D;
}
.notations .C {
/* yellow */
background: #FFD119;
}
.notations .D {
/* orange */
background: #FFA319;
}
.notations .E {
/* red */
background: #FF6600;
}
.notations .F {
/* red */
background: #FF1919;
}
.notations .NA {
/* Non applicable */
background: #CCC;
}
.notations .icon-eye {
color: #9c4274;
cursor: pointer;
}
.notations .criteria .table {
width: 75%;
}
.notations .criteria .label {
width: 70%;
}
.notations .criteria .result {
width: 20%;
font-weight: bold;
white-space: nowrap;
text-align: center;
}
.notations .warning .label,
.notations .warning .result {
color: #FF1919;
}
.notations .criteria .info {
width: 10%;
text-align: center;
}
.notations .criteria .icon-question {
color: #f1c40f;
cursor: pointer;
.selectScript.empty select {
width: 10em;
}
.timeline {
margin: 2em 0 5em;
@@ -176,6 +52,9 @@ h4 {
bottom: 0;
width: 100%;
}
.timeline .interval .color.clickable {
cursor: pointer;
}
.timeline div.interval:hover {
background: #9C4274;
}
@@ -256,45 +135,13 @@ h4 {
width: 1.5em;
border-radius: 0.2em;
}
.metrics h4 {
padding-left: 2em;
}
.metrics .module {
padding-left: 4em;
padding-top: 0.5em;
}
.metrics .legend {
font-style: italic;
color: #aaa;
}
.metrics .offenders {
padding-left: 0em;
font-size: 0.8em;
}
.metrics .offenders div {
cursor: pointer;
}
.metrics .offenders ul {
margin-top: 0.5em;
}
.filters {
margin: 1em 0;
margin: 1em auto;
padding: 0.5em;
min-width: 30em;
width: 30%;
border: 1px dotted #aaa;
}
.slowRequestsLimit {
width: 3em;
font-size: 1em;
text-align: right;
border: 1px solid #aaa;
}
input.textFilter {
box-shadow: none;
font-size: 1em;
padding: 0 0.2em;
border: 1px solid #aaa;
border-radius: none;
width: 15em;
text-align: left;
}
.table {
display: table;
@@ -326,12 +173,65 @@ input.textFilter {
.table > div.showingDetails > div {
background: #f1c40f;
}
.table > div.highlight > div.startTime {
background-color: #C0F090;
}
.table > div.highlight-remove {
transition: 3s;
}
.table > div.highlight-remove > div.startTime {
transition: background-color 3s ease-in;
}
.table > div > .index {
color: #bbb;
word-break: normal;
}
.table > div > .type {
white-space: nowrap;
}
.table .children {
margin-top: 0.2em;
font-size: 0.8em;
line-height: 1.6em;
}
.table .child {
margin-left: 0.5em;
}
.table .child > .child {
margin-left: 1em;
}
.table .child:before {
content: "↳";
}
.table .child .childArgs {
display: none;
}
.table .child span {
position: relative;
}
.table .child span:hover {
background: #EBD8E2;
}
.table .child span:hover div {
display: inline-block;
}
.table .child span:hover .childArgs {
display: block;
position: absolute;
padding: 0 1em 0 2em;
left: 100%;
top: 0;
background: #EBD8E2;
line-height: 1.3em;
height: 1.3em;
z-index: 1;
}
.table .showingDetails .child span:hover {
background: inherit;
}
.table .showingDetails .child span:hover .childArgs {
display: none;
}
.table > div > .value {
width: 70%;
word-break: break-all;
@@ -343,10 +243,12 @@ input.textFilter {
color: #f1c40f;
cursor: pointer;
}
.table .details .icon-warning {
cursor: pointer;
.table .icon-warning {
display: inline-block;
width: 0.8em;
}
.detailsOverlay {
display: none;
position: absolute;
right: 3em;
top: -3em;
@@ -356,13 +258,16 @@ input.textFilter {
background: #fff;
border: 2px solid #f1c40f;
border-radius: 0.5em;
z-index: 1;
z-index: 2;
}
@media screen and (max-width: 1024px) {
.detailsOverlay {
width: 25em;
}
}
.showDetails .detailsOverlay {
display: block;
}
.detailsOverlay .closeBtn {
position: absolute;
top: 0.5em;
@@ -397,69 +302,13 @@ input.textFilter {
.table > div > .startTime.domCreation {
background: #ffe0cc;
}
.table .icon-warning {
.execution .icon-warning {
color: #e74c3c;
}
/**** NgModal popin (have a look inside bower_components) ****/
.ng-modal {
position: fixed;
z-index: 9999;
top: 0;
left: 0;
width: 100%;
height: 100%;
text-align: left;
}
.ng-modal-overlay {
position: absolute;
z-index: 9999;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #000;
opacity: 0.5;
}
.ng-modal-dialog {
z-index: 10000;
position: absolute;
top: 50%;
left: 50%;
width: 50%;
transform: translate(-50%, -50%);
-webkit-transform: translate(-50%, -50%);
background-color: #fff;
padding: 10px;
border: 3px solid #f1c40f;
border-radius: 0.5em;
color: #000;
}
.ng-modal-dialog-content {
overflow-x: hidden;
overflow-y: scroll;
word-wrap: break-word;
max-height: 20em;
font-weight: normal;
white-space: normal;
}
.ng-modal-close {
position: absolute;
top: 3px;
right: 5px;
cursor: pointer;
font-size: 120%;
padding: 5px;
display: inline-block;
}
.ng-modal-close-x {
font-weight: bold;
font-family: Arial, sans-serif;
.warningsFilterOn > div {
display: none;
}
.ng-modal-title {
font-weight: bold;
font-size: 1.5em;
display: block;
margin-bottom: 10px;
padding-bottom: 7px;
border-bottom: solid 1px #999;
.warningsFilterOn > div.warning {
display: table-row;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

@@ -1 +1 @@
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M 256.00,96.00C 144.341,96.00, 47.559,161.021,0.00,256.00c 47.559,94.979, 144.341,160.00, 256.00,160.00c 111.657,0.00, 208.439-65.021, 256.00-160.00 C 464.442,161.021, 367.657,96.00, 256.00,96.00z M 382.225,180.852c 30.081,19.187, 55.571,44.887, 74.717,75.148 c-19.146,30.261-44.637,55.961-74.718,75.149C 344.427,355.257, 300.779,368.00, 256.00,368.00c-44.78,0.00-88.428-12.743-126.225-36.852 C 99.695,311.962, 74.205,286.262, 55.058,256.00c 19.146-30.262, 44.637-55.962, 74.717-75.148c 1.959-1.25, 3.938-2.461, 5.929-3.65 C 130.725,190.866, 128.00,205.613, 128.00,221.00c0.00,70.692, 57.308,128.00, 128.00,128.00s 128.00-57.308, 128.00-128.00c0.00-15.387-2.725-30.134-7.704-43.799 C 378.286,178.39, 380.265,179.602, 382.225,180.852z M 256.00,205.00c0.00,26.51-21.49,48.00-48.00,48.00s-48.00-21.49-48.00-48.00s 21.49-48.00, 48.00-48.00 S 256.00,178.49, 256.00,205.00z" ></path></svg>
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M 437.011,74.99C 390.685,28.662, 326.693,0.00, 256.00,0.00C 146.256,0.00, 52.655,69.064, 16.251,166.094l 59.938,22.477 C 103.491,115.798, 173.692,64.00, 256.00,64.00c 53.02,0.00, 101.01,21.50, 135.753,56.247L 320.00,192.00l 192.00,0.00 L 512.00,0.00 L 437.011,74.99zM 256.00,448.00c-53.02,0.00-101.013-21.496-135.756-56.244L 192.00,320.00L0.00,320.00 l0.00,192.00 l 74.997-74.997C 121.32,483.334, 185.306,512.00, 256.00,512.00 c 109.745,0.00, 203.346-69.064, 239.75-166.094l-59.938-22.477C 408.51,396.202, 338.309,448.00, 256.00,448.00z" ></path></svg>

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Before

Width:  |  Height:  |  Size: 794 B

After

Width:  |  Height:  |  Size: 794 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

+65
View File
@@ -0,0 +1,65 @@
var yltApp = angular.module('YellowLabTools', [
'ngRoute',
'ngSanitize',
'ngAnimate',
'indexCtrl',
'dashboardCtrl',
'queueCtrl',
'ruleCtrl',
'screenshotCtrl',
'timelineCtrl',
'runsFactory',
'resultsFactory',
'apiService',
'menuService',
'gradeDirective',
'offendersDirectives'
]);
yltApp.run(['$rootScope', '$location', function($rootScope, $location) {
$rootScope.loadedRunId = null;
// Google Analytics
$rootScope.$on('$routeChangeSuccess', function(){
ga('send', 'pageview', {'page': $location.path()});
});
}]);
yltApp.config(['$routeProvider', '$locationProvider',
function($routeProvider, $locationProvider) {
$routeProvider.
when('/', {
templateUrl: 'views/index.html',
controller: 'IndexCtrl'
}).
when('/queue/:runId', {
templateUrl: 'views/queue.html',
controller: 'QueueCtrl'
}).
when('/about', {
templateUrl: 'views/about.html'
}).
when('/result/:runId', {
templateUrl: 'views/dashboard.html',
controller: 'DashboardCtrl'
}).
when('/result/:runId/timeline', {
templateUrl: 'views/timeline.html',
controller: 'TimelineCtrl'
}).
when('/result/:runId/screenshot', {
templateUrl: 'views/screenshot.html',
controller: 'ScreenshotCtrl'
}).
when('/result/:runId/rule/:policy', {
templateUrl: 'views/rule.html',
controller: 'RuleCtrl'
}).
otherwise({
redirectTo: '/'
});
$locationProvider.html5Mode(true);
}
]);
+68
View File
@@ -0,0 +1,68 @@
var dashboardCtrl = angular.module('dashboardCtrl', ['resultsFactory', 'menuService']);
dashboardCtrl.controller('DashboardCtrl', ['$scope', '$rootScope', '$routeParams', '$location', 'Results', 'API', 'Menu', function($scope, $rootScope, $routeParams, $location, Results, API, Menu) {
$scope.runId = $routeParams.runId;
$scope.Menu = Menu.setCurrentPage('dashboard', $scope.runId);
$scope.fromSocialShare = $location.search().share;
function loadResults() {
// Load result if needed
if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
Results.get({runId: $routeParams.runId}, function(result) {
$rootScope.loadedResult = result;
$scope.result = result;
init();
}, function(err) {
$scope.error = true;
});
} else {
$scope.result = $rootScope.loadedResult;
init();
}
}
function init() {
// By default, Angular sorts object's attributes alphabetically. Countering this problem by retrieving the keys order here.
$scope.categoriesOrder = Object.keys($scope.result.scoreProfiles.generic.categories);
$scope.globalScore = Math.max($scope.result.scoreProfiles.generic.globalScore, 0);
$scope.tweetText = 'My website\'s score is ' + $scope.globalScore + '/100 on #YellowLabTools!';
}
$scope.showRulePage = function(ruleName) {
$location.path('/result/' + $scope.runId + '/rule/' + ruleName);
};
$scope.testAgain = function() {
API.launchTest($scope.result.params.url);
};
/// When comming from a social shared link, the user needs to click on "See full report" button to display the full dashboard.
$scope.seeFullReport = function() {
$scope.fromSocialShare = false;
$location.search({});
};
$scope.shareOnTwitter = function(message) {
openSocialPopup('https://twitter.com/intent/tweet?url=' + document.URL + '%3Fshare&text=' + encodeURIComponent(message));
};
$scope.shareOnLinkedin = function(message) {
openSocialPopup('https://www.linkedin.com/shareArticle?mini=true&url=' + document.URL + '%3Fshare&title=' + encodeURIComponent(message) + '&summary=' + encodeURIComponent('YellowLabTools is a free online tool that analyzes performance and front-end quality of a webpage.'));
};
function openSocialPopup(url) {
var winHeight = 400;
var winWidth = 600;
var winTop = (screen.height / 2) - (winHeight / 2);
var winLeft = (screen.width / 2) - (winWidth / 2);
window.open(url, 'sharer', 'top=' + winTop + ',left=' + winLeft + ',toolbar=0,status=0,width=' + winWidth + ',height=' + winHeight);
}
// Returns the URL of the JSON result
$scope.getAPIUrl = function() {
return '/api/results/' + $scope.runId;
};
loadResults();
}]);
+9
View File
@@ -0,0 +1,9 @@
var indexCtrl = angular.module('indexCtrl', []);
indexCtrl.controller('IndexCtrl', ['$scope', '$location', 'API', function($scope, $location, API) {
$scope.launchTest = function() {
if ($scope.url) {
API.launchTest($scope.url);
}
};
}]);
+42
View File
@@ -0,0 +1,42 @@
var queueCtrl = angular.module('queueCtrl', ['runsFactory']);
queueCtrl.controller('QueueCtrl', ['$scope', '$routeParams', '$location', 'Runs', 'API', function($scope, $routeParams, $location, Runs, API) {
$scope.runId = $routeParams.runId;
var numberOfTries = 0;
function getRunStatus () {
Runs.get({runId: $scope.runId}, function(data) {
$scope.url = data.params.url;
$scope.status = data.status;
$scope.notFound = false;
$scope.connectionLost = false;
if (data.status.statusCode === 'running' || data.status.statusCode === 'awaiting') {
numberOfTries ++;
// Retrying in 2 seconds (and increasing the delay a bit more each time)
setTimeout(getRunStatus, 2000 + (numberOfTries * 100));
} else if (data.status.statusCode === 'complete') {
$location.path('/result/' + $scope.runId).replace();
} else {
// Handled by the view
}
}, function(response) {
if (response.status === 404) {
$scope.notFound = true;
$scope.connectionLost = false;
} else if (response.status === 0) {
// Connection lost, retry in 10 seconds
setTimeout(getRunStatus, 10000);
$scope.connectionLost = true;
$scope.notFound = false;
}
});
}
getRunStatus();
}]);
+36
View File
@@ -0,0 +1,36 @@
var ruleCtrl = angular.module('ruleCtrl', []);
ruleCtrl.controller('RuleCtrl', ['$scope', '$rootScope', '$routeParams', '$location', '$sce', 'Menu', 'Results', 'API', function($scope, $rootScope, $routeParams, $location, $sce, Menu, Results, API) {
$scope.runId = $routeParams.runId;
$scope.policyName = $routeParams.policy;
$scope.Menu = Menu.setCurrentPage(null, $scope.runId);
$scope.rule = null;
function loadResults() {
// Load result if needed
if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
Results.get({runId: $routeParams.runId}, function(result) {
$rootScope.loadedResult = result;
$scope.result = result;
init();
});
} else {
$scope.result = $rootScope.loadedResult;
init();
}
}
function init() {
$scope.rule = $scope.result.rules[$scope.policyName];
}
$scope.backToDashboard = function() {
$location.path('/result/' + $scope.runId);
};
$scope.testAgain = function() {
API.launchTest($scope.result.params.url);
};
loadResults();
}]);
@@ -0,0 +1,31 @@
var screenshotCtrl = angular.module('screenshotCtrl', ['resultsFactory', 'menuService']);
screenshotCtrl.controller('ScreenshotCtrl', ['$scope', '$rootScope', '$routeParams', '$location', 'Results', 'API', 'Menu', function($scope, $rootScope, $routeParams, $location, Results, API, Menu) {
$scope.runId = $routeParams.runId;
$scope.Menu = Menu.setCurrentPage(null, $scope.runId);
function loadResults() {
// Load result if needed
if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
Results.get({runId: $routeParams.runId}, function(result) {
$rootScope.loadedResult = result;
$scope.result = result;
init();
}, function(err) {
$scope.error = true;
});
} else {
$scope.result = $rootScope.loadedResult;
}
}
$scope.backToDashboard = function() {
$location.path('/result/' + $scope.runId);
};
$scope.testAgain = function() {
API.launchTest($scope.result.params.url);
};
loadResults();
}]);
+172
View File
@@ -0,0 +1,172 @@
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}, function(result) {
$rootScope.loadedResult = result;
$scope.result = result;
render();
});
} else {
$scope.result = $rootScope.loadedResult;
render();
}
}
function render() {
initScriptFiltering();
initExecutionTree();
initTimeline();
$timeout(initProfiler, 100);
}
function initScriptFiltering() {
var offenders = $scope.result.rules.jsCount.offendersObj.list;
$scope.scripts = [];
offenders.forEach(function(script) {
var filePath = script.file;
if (filePath.length > 100) {
filePath = filePath.substr(0, 98) + '...';
}
var scriptObj = {
fullPath: script.file,
shortPath: filePath
};
$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.launchTest($scope.result.params.url);
};
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
@@ -0,0 +1,33 @@
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';
};
}]
};
});
@@ -0,0 +1,800 @@
(function() {
"use strict";
var offendersDirectives = angular.module('offendersDirectives', []);
function getdomTreeHTML(tree) {
return '<div class="domTree">' + getdomTreeInnerHTML(tree) + '</div>';
}
function getdomTreeInnerHTML(tree) {
return recursiveHtmlBuilder(tree);
}
function recursiveHtmlBuilder(tree) {
var html = '';
var keys = Object.keys(tree);
keys.forEach(function(key) {
if (isNaN(tree[key])) {
html += '<div><span>' + key + '</span>' + recursiveHtmlBuilder(tree[key]) + '</div>';
} else if (tree[key] > 1) {
html += '<div><span>' + key + ' <span>(x' + tree[key] + ')</span></span></div>';
} else {
html += '<div><span>' + key + '</span></div>';
}
});
return html;
}
offendersDirectives.directive('domTree', function() {
return {
restrict: 'E',
scope: {
tree: '='
},
template: '<div class="domTree"></div>',
replace: true,
link: function(scope, element) {
element.append(getdomTreeInnerHTML(scope.tree));
}
};
});
function getDomElementButtonHTML(obj, onASingleLine) {
if (obj.tree && !onASingleLine) {
return '<div class="offenderButton opens">' + getDomElementButtonInnerHTML(obj, onASingleLine) + '</div>';
} else {
return '<div class="offenderButton">' + getDomElementButtonInnerHTML(obj, onASingleLine) + '</div>';
}
}
function getDomElementButtonInnerHTML(obj, onASingleLine) {
if (obj.type === 'html' ||
obj.type === 'body' ||
obj.type === 'head' ||
obj.type === 'window' ||
obj.type === 'document' ||
obj.type === 'fragment') {
return obj.type;
}
if (obj.type === 'notAnElement') {
return 'Incorrect element';
}
var html = '';
if (obj.type === 'domElement') {
html = 'DOM element <b>' + obj.element + '</b>';
} else if (obj.type === 'fragmentElement') {
html = 'Fragment element <b>' + obj.element + '</b>';
} else if (obj.type === 'createdElement') {
html = 'Created element <b>' + obj.element + '</b>';
}
if (obj.tree && !onASingleLine) {
html += getdomTreeHTML(obj.tree);
}
return html;
}
offendersDirectives.directive('domElementButton', function() {
return {
restrict: 'E',
scope: {
obj: '='
},
template: '<div class="offenderButton" ng-class="{opens: obj.tree}"></div>',
replace: true,
link: function(scope, element) {
element.append(getDomElementButtonInnerHTML(scope.obj));
}
};
});
function getJQueryContextButtonHTML(context, onASingleLine) {
if (context.length === 0) {
return '<span class="offenderButton">Empty jQuery object</span>';
}
if (context.length === 1) {
return getDomElementButtonHTML(context.elements[0], onASingleLine);
}
var html = context.length + ' elements (' + getDomElementButtonHTML(context.elements[0], onASingleLine) + ', ' + getDomElementButtonHTML(context.elements[1], onASingleLine);
if (context.length === 3) {
html += ', ' + getDomElementButtonHTML(context.elements[0], onASingleLine);
} else if (context.length > 3) {
html += ' and ' + (context.length - 2) + ' more...';
}
return html + '}';
}
function isJQuery(node) {
return node.data.type.indexOf('jQuery ') === 0;
}
function getNonJQueryHTML(node, onASingleLine) {
var type = node.data.type;
if (!node.data.callDetails) {
return '';
}
var args = node.data.callDetails.arguments;
var ctxt = node.data.callDetails.context;
switch (type) {
case 'getElementById':
case 'createElement':
return '<b>' + args[0] + '</b>';
case 'getElementsByClassName':
case 'getElementsByTagName':
case 'querySelector':
case 'querySelectorAll':
return '<b>' + args[0] + '</b> on ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine);
case 'appendChild':
return 'append ' + getDomElementButtonHTML(args[0], onASingleLine) + ' to ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine);
case 'insertBefore':
return 'insert ' + getDomElementButtonHTML(args[0], onASingleLine) + ' into ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine) + ' before ' + getDomElementButtonHTML(args[1], onASingleLine);
case 'addEventListener':
return 'bind <b>' + args[0] + '</b> to ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine);
case 'error':
return args[0];
default:
return '';
}
}
function getJQueryHTML(node, onASingleLine) {
var type = node.data.type;
var unescapedArgs = node.data.callDetails.arguments;
var args = [];
var ctxt = node.data.callDetails.context;
// escape HTML in args
for (var i = 0 ; i < 4 ; i ++) {
if (unescapedArgs[i]) {
args[i] = escapeHTML(unescapedArgs[i]);
}
}
if (type === 'jQuery loaded' || type === 'jQuery version change') {
return args[0];
}
switch (type) {
case 'jQuery - onDOMReady':
case 'jQuery - windowOnLoad':
return '(function)';
case 'jQuery - Sizzle call':
return '<b>' + args[0] + '</b> on ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine);
case 'jQuery - find':
if (ctxt && ctxt.length === 1 && ctxt.elements[0].type !== 'document') {
return '<b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else {
return '<b>' + args[0] + '</b>';
}
break;
case 'jQuery - html':
if (args[0] !== undefined) {
return 'set content "<b>' + args[0] + '</b>" to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else {
return 'get content from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - append':
return 'append ' + joinArgs(args) + ' to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - appendTo':
return 'append' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to <b>' + args[0] + '</b>';
case 'jQuery - prepend':
return 'prepend ' + joinArgs(args) + ' to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - prependTo':
return 'prepend ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to <b>' + args[0] + '</b>';
case 'jQuery - before':
return 'insert ' + joinArgs(args) + ' before ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - insertBefore':
return 'insert ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' before <b>' + args[0] + '</b>';
case 'jQuery - after':
return 'insert ' + joinArgs(args) + ' after ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - insertAfter':
return 'insert ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' after <b>' + args[0] + '</b>';
case 'jQuery - remove':
case 'jQuery - detach':
if (args[0]) {
return getJQueryContextButtonHTML(ctxt, onASingleLine) + ' filtered by <b>' + args[0] + '</b>';
} else {
return getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - empty':
case 'jQuery - clone':
case 'jQuery - unwrap':
case 'jQuery - show':
case 'jQuery - hide':
case 'jQuery - animate':
case 'jQuery - fadeIn':
case 'jQuery - fadeOut':
case 'jQuery - fadeTo':
case 'jQuery - fadeToggle':
case 'jQuery - slideDown':
case 'jQuery - slideUp':
case 'jQuery - slideToggle':
return getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - replaceWith':
return 'replace ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' with <b>' + args[0] + '</b>';
case 'jQuery - replaceAll':
return 'replace <b>' + args[0] + '</b> with ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - text':
if (args[0]) {
return 'set text "<b>' + args[0] + '</b>" to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else {
return 'get text from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - wrap':
case 'jQuery - wrapAll':
return 'wrap ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' within <b>' + args[0] + '</b>';
case 'jQuery - wrapInner':
return 'wrap the content of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' within <b>' + args[0] + '</b>';
case 'jQuery - css':
case 'jQuery - attr':
case 'jQuery - prop':
if (isStringOfObject(args[0])) {
return 'set <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else if (args[1]) {
return 'set <b>' + args[0] + '</b> : <b>' + args[1] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else {
return 'get <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - offset':
case 'jQuery - height':
case 'jQuery - innerHeight':
case 'jQuery - outerHeight':
case 'jQuery - width':
case 'jQuery - innerWidth':
case 'jQuery - outerWidth':
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 - toggle':
if (args[0] === 'true') {
return getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to visible';
} else if (args[0] === 'false') {
return getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to hidden';
} else {
return getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - on':
case 'jQuery - one':
if (args[1]) {
return 'bind <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[1] + '</b>';
} else {
return 'bind <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - off':
if (args[0]) {
if (args[1]) {
return 'unbind <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[1] + '</b>';
} else {
return 'unbind <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
} else {
return 'unbind all events';
}
break;
case 'jQuery - live':
case 'jQuery - bind':
return 'bind <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - die':
case 'jQuery - unbind':
if (args[0]) {
return 'unbind <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else {
return 'unbind all events';
}
break;
case 'jQuery - delegate':
return 'bind <b>' + args[1] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[0] + '</b>';
case 'jQuery - undelegate':
if (args[0]) {
if (args[1]) {
return 'unbind <b>' + args[1] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[0] + '</b>';
} else {
return 'unbind namespace <b>' + args[0] + '</b>';
}
} else {
return 'unbind all events';
}
break;
case 'jQuery - blur':
case 'jQuery - change':
case 'jQuery - click':
case 'jQuery - dblclick':
case 'jQuery - error':
case 'jQuery - focus':
case 'jQuery - focusin':
case 'jQuery - focusout':
case 'jQuery - hover':
case 'jQuery - keydown':
case 'jQuery - keypress':
case 'jQuery - keyup':
case 'jQuery - load':
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':
case 'jQuery - unload':
return 'bind on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - removeAttr':
case 'jQuery - removeProp':
return 'remove <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - val':
if (args[0]) {
return 'set value <b>' + args[0] + '</b> to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else {
return 'get value from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - hasClass':
case 'jQuery - addClass':
case 'jQuery - removeClass':
return '<b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - toggleClass':
if (args[0]) {
if (args[1]) {
return 'toggle <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to <b>' + args[1] + '</b>';
} else {
return 'toggle <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
} else {
return 'magic no-argument toggleClass';
}
break;
case 'jQuery - children':
if (args[0]) {
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' filtered by <b>' + args[0] + '</b>';
} else {
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - closest':
if (args[1]) {
return 'closest <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' in context <b>' + args[1] + '</b>';
} else {
return 'closest <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - next':
case 'jQuery - nextAll':
if (args[0]) {
return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching <b>' + args[0] + '</b>';
} else {
return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - nextUntil':
if (args[0]) {
if (args[1]) {
return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b> and matching <b>' + args[1] + '</b>';
} else {
return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b>';
}
} else {
return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - offsetParent':
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - prev':
case 'jQuery - prevAll':
if (args[0]) {
return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching <b>' + args[0] + '</b>';
} else {
return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - prevUntil':
if (args[0]) {
if (args[1]) {
return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b> and matching <b>' + args[1] + '</b>';
} else {
return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b>';
}
} else {
return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - parent':
case 'jQuery - parents':
if (args[0]) {
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching <b>' + args[0] + '</b>';
} else {
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - parentsUntil':
if (args[0]) {
if (args[1]) {
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b> and matching <b>' + args[1] + '</b>';
} else {
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b>';
}
} else {
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - siblings':
if (args[0]) {
return 'near ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching <b>' + args[0] + '</b>';
} else {
return 'near ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - onDOMReady':
return '(function)';
default:
return '';
}
}
function escapeHTML(html) {
var entityMap = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': '&quot;',
"'": '&#39;',
"/": '&#x2F;'
};
return String(html).replace(/[&<>"'\/]/g, function (s) {
return entityMap[s];
});
}
function joinArgs(args) {
var html = '<b>' + args[0] + '</b>';
if (args[1]) {
html += ', <b>' + args[1] + '</b>';
if (args[2]) {
html += ', <b>' + args[2] + '</b>';
if (args[3]) {
html += ', and more...';
}
}
}
return html;
}
function isStringOfObject(str) {
return typeof str === 'string' && str[0] === '{' && str[str.length - 1] === '}';
}
function isPureString(str) {
return typeof str === 'string' && str[0] !== '{' && str !== '(function)' && str !== '[Object]' && str !== '[Array]' && str !== 'true' && str !== 'false' && str !== 'undefined' && str !== 'unknown';
}
function getTimelineParamsHTML(node, onASingleLine) {
if (isJQuery(node)) {
return getJQueryHTML(node, onASingleLine);
} else {
return getNonJQueryHTML(node, onASingleLine);
}
}
function getBacktraceHTML(backtrace) {
var html = '';
var parsedBacktrace = parseBacktrace(backtrace);
if (!parsedBacktrace || parsedBacktrace.length === 0) {
html += '<div><div>can\'t find any backtrace :/</div></div>';
} else {
for (var i = 0 ; i < parsedBacktrace.length ; i++) {
html += '<div>';
html += '<div>' + (parsedBacktrace[i].fnName || '(anonymous function)') + '</div>';
html += '<div class="trace">' + getUrlLink(parsedBacktrace[i].filePath, 40) + ':' + parsedBacktrace[i].line + '</div>';
html += '</div>';
}
}
return html;
}
function parseBacktrace(str) {
if (!str) {
return null;
}
var out = [];
var splited = str.split(' / ');
try {
splited.forEach(function(trace) {
var fnName = null, fileAndLine;
var withFnResult = /^([^\s\(]+) \((.+:\d+)\)$/.exec(trace);
if (withFnResult === null) {
fileAndLine = trace;
} else {
fnName = withFnResult[1];
fileAndLine = withFnResult[2];
}
var fileAndLineSplit = /^(.*):(\d+)$/.exec(fileAndLine);
var filePath = fileAndLineSplit[1];
var line = fileAndLineSplit[2];
out.push({
fnName: fnName,
filePath: filePath,
line: line
});
});
} catch(e) {
return null;
}
return out;
}
function getTimelineDetailsHTML(node) {
var html = '';
if (node.data.type != 'jQuery loaded' && node.data.type != 'jQuery version change' && !node.windowPerformance) {
if (node.warning || node.error) {
html += '<div class="icon-warning"></div>';
} else {
html += '<div class="icon-question"></div>';
}
html += '<div class="detailsOverlay">';
html += '<div class="closeBtn">✖</div>';
if (node.data.callDetails.context && node.data.callDetails.context.length === 0) {
html += '<h4>Called on 0 jQuery element</h4><p class="advice">Useless function call, as the jQuery object is empty.</p>';
} else if (node.data.type === 'jQuery - bind' && node.data.callDetails.context.length > 5) {
html += '<p class="advice">The .bind() method attaches the event listener to each jQuery element one by one. Using the .on() method is preferable if available (from v1.7).</p>';
}
if (node.data.resultsNumber === 0) {
html += '<p class="advice">The query returned 0 results. Could it be unused or dead code?</p>';
} else if (node.data.resultsNumber > 0) {
html += '<p>The query returned ' + node.data.resultsNumber + ' ' + (node.data.resultsNumber > 1 ? 'results' : 'result') + '.</p>';
}
if (node.data.backtrace) {
html += '<h4>Backtrace</h4>';
html += '<div class="table">';
html += getBacktraceHTML(node.data.backtrace);
html += '</div>';
}
html += '</div>';
}
return html;
}
offendersDirectives.directive('profilerLine', ['$filter', function($filter) {
var numberWithCommas = $filter('number');
function getProfilerLineHTML(index, node) {
return '<div class="index">' + (index + 1) + '</div>' +
'<div class="type">' + node.data.type + (node.children ? '<div class="children">' + recursiveChildrenHTML(node) + '</div>' : '') + '</div>' +
'<div class="value offenders">' + getTimelineParamsHTML(node, false) + '</div>' +
'<div class="details">' + getTimelineDetailsHTML(node) + '</div>' +
'<div class="startTime ' + node.data.loadingStep + '">' + numberWithCommas(node.data.timestamp, 0) + ' ms</div>';
}
function recursiveChildrenHTML(node) {
var html = '';
if (node.children) {
node.children.forEach(function(child) {
html += '<div class="child"><span>' + child.data.type + '<div class="childArgs">' + getTimelineParamsHTML(child, true) + '</div></span>' + recursiveChildrenHTML(child) + '</div>';
});
}
return html;
}
function onDetailsClick(row) {
// Close if it's alreay open
if (row.classList.contains('showDetails')) {
closeDetails(row);
return;
}
// Close any other open details overlay
var openOnes = document.getElementsByClassName('showDetails');
if (openOnes.length > 0) {
openOnes[0].classList.remove('showDetails');
}
// Make it appear
row.classList.add('showDetails');
// Bind the close button
row.querySelector('.closeBtn').addEventListener('click', function() {
closeDetails(row);
});
}
function closeDetails(row) {
row.classList.remove('showDetails');
// Unbind the close button
row.querySelector('.closeBtn').removeEventListener('click', closeDetails);
}
return {
restrict: 'E',
scope: {
index: '=',
node: '='
},
template: '<div></div>',
replace: true,
link: function(scope, element) {
if (scope.node.error) {
element.addClass('jsError');
} else if (scope.node.windowPerformance) {
element.addClass('windowPerformance');
}
element.append(getProfilerLineHTML(scope.index, scope.node));
element[0].id = 'line_' + scope.index;
if (scope.node.warning || scope.node.error) {
element[0].classList.add('warning');
}
// Bind click on the details icon
var detailsIcon = element[0].querySelector('.details div');
if (detailsIcon) {
detailsIcon.addEventListener('click', function() {
onDetailsClick(this.parentNode.parentNode);
});
}
}
};
}]);
function shortenUrl(url, maxLength) {
if (!maxLength) {
maxLength = 110;
}
// Why dividing by 2.1? Because it adds a 5% margin.
var leftLength = Math.floor((maxLength - 5) / 2.1);
var rightLength = Math.ceil((maxLength - 5) / 2.1);
return (url.length > maxLength) ? url.substr(0, leftLength) + ' ... ' + url.substr(-rightLength) : url;
}
offendersDirectives.filter('shortenUrl', function() {
return shortenUrl;
});
function getUrlLink(url, maxLength) {
return '<a href="' + url + '" target="_blank" title="' + url + '">' + shortenUrl(url, maxLength) + '</a>';
}
offendersDirectives.directive('urlLink', function() {
return {
restrict: 'E',
scope: {
url: '=',
maxLength: '='
},
template: '<a href="{{url}}" target="_blank" title="{{url}}">{{url | shortenUrl:maxLength}}</a>',
replace: true
};
});
offendersDirectives.filter('encodeURIComponent', function() {
return window.encodeURIComponent;
});
offendersDirectives.directive('fileAndLine', function() {
return {
restrict: 'E',
scope: {
file: '=',
line: '=',
column: '='
},
template: '<span><span ng-if="file"><url-link url="file" max-length="60"></url-link></span><span ng-if="!file">&lt;inline CSS&gt;</span><span ng-if="line !== null && column !== null"> @ {{line}}:{{column}}</span></span>',
replace: true
};
});
offendersDirectives.directive('fileAndLineButton', function() {
return {
restrict: 'E',
scope: {
file: '=',
line: '=',
column: '='
},
template: '<div class="offenderButton opens">css file<div class="cssFileAndLine"><file-and-line file="file" line="line" column="column" button="true"></file-and-line></div></div>',
replace: true
};
});
})();
+7
View File
@@ -0,0 +1,7 @@
var resultsFactory = angular.module('resultsFactory', ['ngResource']);
resultsFactory.factory('Results', ['$resource', function($resource) {
return $resource('/api/results/:runId', {
});
}]);
+7
View File
@@ -0,0 +1,7 @@
var runsFactory = angular.module('runsFactory', ['ngResource']);
runsFactory.factory('Runs', ['$resource', function($resource) {
return $resource('/api/runs/:runId', {
});
}]);
+27
View File
@@ -0,0 +1,27 @@
var apiService = angular.module('apiService', []);
apiService.factory('API', ['$location', 'Runs', 'Results', function($location, Runs, Results) {
return {
launchTest: function(url) {
Runs.save({
url: url,
waitForResponse: false,
screenshot: true,
jsTimeline: true
}, function(data) {
$location.path('/queue/' + data.runId);
}, function(response) {
if (response.status === 429) {
alert('Too many requests, you reached the max number of requests allowed in 24h');
} else {
alert('An error occured...');
}
});
}
};
}]);
+34
View File
@@ -0,0 +1,34 @@
var menuService = angular.module('menuService', []);
menuService.factory('Menu', ['$location', function($location) {
var currentPage, currentRunId;
return {
getCurrentPage: function() {
return currentPage;
},
setCurrentPage: function(page, runId) {
currentPage = page;
currentRunId = runId;
return this;
},
changePage: function(page) {
switch (page) {
case 'index':
$location.path('/');
break;
case 'dashboard':
$location.path('/result/' + currentRunId);
break;
case 'timeline':
$location.path('/result/' + currentRunId + '/timeline');
break;
default:
console.err('Undefined Menu.changePage() destination');
}
}
};
}]);
+12
View File
@@ -0,0 +1,12 @@
.about {
margin: 3em auto;
width: 50%;
}
.about p {
margin: 2em;
}
.about a {
color: #FFF;
}
+178
View File
@@ -0,0 +1,178 @@
.testedUrl {
color: inherit;
}
.summary {
text-align: center;
}
.summary .globalScore {
display: table;
width: 60%;
margin: 3em auto;
> div {
display: table-cell;
width: 50%;
vertical-align: middle;
}
.globalGrade {
margin: 0.5 auto;
width: 2.5em;
height: 2.5em;
line-height: 2.5em;
border-radius: 0.5em;
font-size: 3em;
font-weight: bold;
vertical-align: middle;
}
.on100 {
font-size: 1.2em;
font-weight: bold;
margin: 0.5em 0 1em;
}
.screenshotWrapper:hover {
opacity: 0.75;
&:after {
position: absolute;
width: 1.25em;
height: 1.25em;
top: 0.7em;
left: 1.55em;
font-size: 3em;
color: #FFF;
background: #000;
border-radius: 0.2em;
text-align: center;
content: "+";
opacity: 0.85;
}
}
}
.summary .notations {
display: table;
width: 80%;
margin: 0 10% 1.5em;
border-spacing: 1em;
}
.summary .notations > div {
display: table-row;
}
.summary .notations > div > div {
display: table-cell;
height: 2.5em;
vertical-align: middle;
}
.summary .notations .category {
font-weight: bold;
text-align: center;
width: 20%;
}
.summary .notations .criteria {
font-weight: normal;
width: 75%;
}
.A, .B, .C, .D, .E, .F, .NA {
.summary .notations &.categoryScore {
width: 2.5em;
max-width: 2.5em;
min-width: 2.5em;
font-size: 2em;
text-align: center;
border-radius: 0.5em;
font-weight: bold;
}
.summary .notations .grade & {
width: 1em;
height: 1em;
font-size: 1em;
color: transparent;
margin: 0 auto;
border-radius: 0.5em;
}
}
.summary .notations .criteria .table {
width: 100%;
> div:hover > div {
background: #EBD8E2;
cursor: pointer;
&.info {
background: #FFF;
.icon-question {
color: #EBD8E2;
}
}
}
}
.summary .notations .criteria .grade {
width: 10%;
padding-left: 0.5em;
padding-right: 0.5em;
vertical-align: middle;
}
.summary .notations .criteria .label {
width: 70%;
}
.summary .notations .criteria .result {
width: 18%;
font-weight: bold;
white-space: nowrap;
text-align: center;
vertical-align: middle;
}
.summary .notations .warning .label, .summary .notations .warning .result, .summary .notations .icon-warning {
color: #FF1919;
}
.summary .notations .criteria .info {
width: 2%;
text-align: center;
vertical-align: middle;
background: #FFF;
padding-left: 0.1em;
padding-right: 0.1em;
}
.summary .notations .criteria .icon-question {
color: transparent;
}
.summary .fromShare {
margin-bottom: 3em;
}
.summary .apiTip {
font-size: 0.8em;
margin-bottom: 4em;
color: #413;
a {
color: inherit;
}
}
.summary .tweet {
.tweetText {
color: #413;
background: #F2F2F2;
border: none;
width: 25em;
padding: 0.4em;
border-radius: 0.5em;
box-shadow: 0.05em 0.1em 0 0 #999;
}
.tweetButton, .linkedinButton {
color: #413;
background: #F2F2F2;
margin-right: 0;
&:hover {
color: #F2F2F2;
background: #e74c3c;
}
}
input {
font-size: 0.9em;
}
}
@@ -1,24 +1,24 @@
@eye-font-family: "fontsmith-icons";
@eye-value: "\e004";
@eye: '"fontsmith-icons"' '"\\e004"';
@lab-font-family: "fontsmith-icons";
@lab-value: "\e003";
@lab: '"fontsmith-icons"' '"\\e003"';
@bars-font-family: "fontsmith-icons";
@bars-value: "\e005";
@bars: '"fontsmith-icons"' '"\\e005"';
@warning-font-family: "fontsmith-icons";
@warning-value: "\e000";
@warning: '"fontsmith-icons"' '"\\e000"';
@arrow-left3-font-family: "fontsmith-icons";
@arrow-left3-value: "\e006";
@arrow-left3: '"fontsmith-icons"' '"\\e006"';
@question-font-family: "fontsmith-icons";
@question-value: "\e001";
@question: '"fontsmith-icons"' '"\\e001"';
@lab-font-family: "fontsmith-icons";
@lab-value: "\e004";
@lab: '"fontsmith-icons"' '"\\e004"';
@list-font-family: "fontsmith-icons";
@list-value: "\e002";
@list: '"fontsmith-icons"' '"\\e002"';
@list-value: "\e003";
@list: '"fontsmith-icons"' '"\\e003"';
@bars-font-family: "fontsmith-icons";
@bars-value: "\e005";
@bars: '"fontsmith-icons"' '"\\e005"';
@arrow-left3-font-family: "fontsmith-icons";
@arrow-left3-value: "\e006";
@arrow-left3: '"fontsmith-icons"' '"\\e006"';
@loop-font-family: "fontsmith-icons";
@loop-value: "\e002";
@loop: '"fontsmith-icons"' '"\\e002"';
.icon-font-family(@char) {
font-family: ~`@{char}[0]`;
@@ -49,7 +49,7 @@
@font-face {
font-family: "fontsmith-icons";
src:url("/public/fonts/icons.woff") format("woff"),
src:url("/fonts/icons.woff") format("woff"),
;
font-weight: normal;
font-style: normal;
@@ -11,6 +11,23 @@
.launchBtn {
background: #e74c3c;
color: #fff;
&.disabled {
background: #deaca6;
}
}
.features {
display: table;
width: 50%;
margin: 8em auto 0;
font-size: 0.9em;
color: #413;
> div {
width: 33.3%;
display: table-cell;
padding: 0 1.5em;
}
}
input[type=submit], input.url {
+212
View File
@@ -0,0 +1,212 @@
@import "icons.less";
html {
margin: 100px 50px;
}
body {
margin: 0 auto;
max-width: 1280px;
background: #9c4274;
color: #fff;
font-size: 16px;
text-align: center;
}
body, input[type=submit], input[type=text], input[type=url], input[type=number], button {
font-family: 'Century Gothic', helvetica, arial, sans-serif;
}
input[type=submit] {
cursor: pointer;
}
h1 span {
display: inline-block;
height: 1em;
width: 1em;
color: #ffa319;
}
.resultsMenu {
margin-top: 2em;
}
.resultsMenu .menuItem {
display: inline-block;
margin: 1em;
width: 8em;
height: 7em;
color: #fff;
border: 3px solid #fff;
border-radius: 0.5em;
cursor: pointer;
text-decoration: none;
&.back, &.restart {
color: #413;
border-color: #413;
}
}
.resultsMenu .menuItem div {
padding-top: 0.5em;
font-size: 3em;
}
.resultsMenu .active, .resultsMenu .menuItem.active:hover {
color: #ffa319;
border-color: #ffa319;
}
.resultsMenu .menuItem:hover {
color: #ffa319;
}
.resultsMenu span {
position: relative;
top: 0.5em;
}
/* Grade colors */
.A {
/* green */
background: #0C4;
}
.B {
/* green */
background: #CD0;
}
.C {
/* yellow */
background: #FD2;
}
.D {
/* orange */
background: #FA2;
}
.E {
/* red */
background: #F60;
}
.F {
/* red */
background: #F22;
}
.NA {
/* Non applicable */
background: #CCC;
}
.board {
margin-top: 2em;
padding: 1em;
background: #fff;
color: #000;
border-radius: 0.5em;
text-align: left;
}
.backToDashboard {
text-align: center;
a {
font-size: 0.9em;
display: block;
margin-top: 4em;
color: black;
}
}
a.linkButton {
font-size: 1em;
padding: 0.3em 0.5em;
margin: 0.5em;
line-height: 2em;
border: 0 solid;
border-radius: 0.5em;
box-shadow: 0.1em 0.2em 0 0 #5e2846;
background: #e74c3c;
color: #fff;
text-decoration: none;
}
.screenshotWrapper.desktop {
display: inline-block;
position: relative;
border: 0.2em solid #AAA;
background: #000;
padding: 0.5em;
border-top-left-radius: 0.4em;
border-top-right-radius: 0.4em;
&:before {
position: absolute;
width: 15em;
height: 0.6em;
bottom: -0.75em;
left: -1em;
background: #CCC;
border-bottom-left-radius: 0.2em;
border-bottom-right-radius: 0.2em;
content: " ";
}
&:after {
position: absolute;
width: 0.4em;
height: 0.2em;
bottom: -0.55em;
left: 12.5em;
background: lime;
content: " ";
}
> div {
width: 12em;
height: 6.75em;
overflow: scroll;
position: relative;
}
.screenshotImage {
width: 100%;
}
}
.star {
font-weight: bold;
span {
font-size: 1.2em;
}
}
.footer {
padding: 3em;
color: #413;
a {
color: inherit;
}
.version {
font-size: 0.7em;
}
}
/* Icons */
.icon-lab {
.icon(@lab);
}
.icon-question {
.icon(@question);
}
.icon-warning {
.icon(@warning);
}
.icon-back {
.icon(@arrow-left3);
}
.icon-summary {
.icon(@list);
}
.icon-spaghetti {
.icon(@bars);
}
.icon-loop {
.icon(@loop);
}
+9
View File
@@ -0,0 +1,9 @@
.status {
margin-top: 2em;
font-size: 2.5em;
}
.statusSubMessage {
font-size: 0.8em;
margin-bottom: 6em;
}
+220
View File
@@ -0,0 +1,220 @@
.rule.board {
text-align: center;
}
.rule .ruleTable {
display: table;
border-spacing: 1em;
width: 90%;
margin: 2em auto;
background: #f2f2f2;
border: 1px dashed #666;
border-radius: 0.5em;
> div {
display: table-cell;
vertical-align: middle;
}
.left {
width: 33%;
font-weight: bold;
}
.right {
width: 67%;
}
}
.rule .score {
font-size: 2.5em;
line-height: 2em;
height: 2em;
width: 2em;
border-radius: 0.5em;
margin: 0 auto 0.5em;
}
.rule .message {
width: 80%;
margin: 0 auto;
p {
margin: 0.5em;
}
}
.rule .message ul {
list-style-type: none;
padding-left: 0;
}
.rule .message li:before {
content:'\25e6';
margin-right: 0.3em;
font-size: 1.2em;
position: relative;
top: 0.1em;
}
.rule .warning {
width: 90%;
margin: -1em auto 2em;
background: #FEE;
border: 1px dashed #e74c3c;
color: #e74c3c;
border-radius: 0.5em;
}
.rule .offendersTable {
display: table;
border-spacing: 0 0.25em;
margin: 0 auto;
min-width: 10%;
max-width: 90%;
> div {
display: table-row;
> div {
display: table-cell;
background: #f2f2f2;
padding: 0 1em;
word-wrap: break-word;
word-break: break-all;
&:hover {
background: #EBD8E2;
}
}
}
}
.rule .notFound {
font-size: 1em;
h2 {
font-size: 3em;
margin-bottom: 1em;
}
}
.offenders {
.offenderButton {
display: inline-block;
position: relative;
background: #efe;
padding: 0 0.5em;
margin: 0.2em 0;
border-radius: 0.4em;
z-index: 1;
&.opens {
padding-right: 0.75em;
&:after {
position: relative;
left: 0.5em;
content: '\25BC';
font-size: 0.8em;
}
}
> div {
display: none;
position: absolute;
right: 0;
min-width: 100%;
background: inherit;
border-bottom-left-radius: 0.4em;
border-bottom-right-radius: 0.4em;
border-top: 1px solid #999;
z-index: 2;
}
.domTree {
text-align: left;
white-space: nowrap;
> div {
margin: 0.5em;
div {
margin-left: 1em;
}
}
}
.backtrace, .cssFileAndLine {
white-space: nowrap;
padding: 0.5em;
}
&.opens:hover {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
background: #ffe0cc;
z-index: 2;
> div {
display: block;
}
}
}
}
.offendersHtml {
display: inline-block;
}
.domTree div {
text-align: left;
margin-left: 1em;
span:only-child {
font-weight: bold;
span {
font-style: italic;
font-weight: normal;
}
}
}
.colorPalette {
width: 30em;
border: 2px solid #000;
text-align: left;
/* Checkerboard background */
background-color: #ddd;
background-image: linear-gradient(45deg, #AAA 25%, transparent 25%, transparent 75%, #AAA 75%, #AAA), linear-gradient(45deg, #AAA 25%, transparent 25%, transparent 75%, #AAA 75%, #AAA);
background-size:1em 1em;
background-position:0 0, 0.5em 0.5em;
> div {
display: inline-block;
height: 2em;
position: relative;
div {
display: none;
position: absolute;
left: 100%;
top: 100%;
background: #FFF;
padding: 0.5em;
border: 2px solid #f1c40f;
border-radius: 0.5em;
white-space: nowrap;
z-index: 3;
font-weight: bold;
}
&:hover div {
display: block;
}
&:hover:after {
content: " ";
position: absolute;
background-color: inherit;
left: -0.2em;
top: -0.2em;
width: 100%;
height: 100%;
z-index: 2;
border: 0.2em solid #f1c40f;
}
}
}
+8
View File
@@ -0,0 +1,8 @@
.screenshot.board {
text-align: center;
}
.screenshot .screenshotWrapper {
font-size: 2.08333333333333em;
margin-bottom: 0.5em;
}
@@ -1,4 +1,3 @@
/* Timeline colors, related to Window Performances */
@domCreationColor: #FF6600;
@domCreationBg: #FFE0CC;
@@ -11,145 +10,26 @@
@domInteractiveColor: #FFE433;
@domInteractiveBg: #FFFCCC;
.resultsMenu {
margin-top: 2em;
}
.resultsMenu .menuItem {
display: inline-block;
margin: 1em;
width: 8em;
height: 7em;
color: #fff;
border: 3px solid #fff;
border-radius: 0.5em;
cursor: pointer;
text-decoration: none;
}
.resultsMenu .back {
color: #5e2846;
border-color: #5e2846;
}
.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;
}
.testedUrl {
color: inherit;
}
h4 {
margin-bottom: 0.5em;
}
.summary, .metrics, .execution {
margin-top: 2em;
padding: 1em;
background: #fff;
color: #000;
border-radius: 0.5em;
text-align: left;
}
.notations {
display: table;
width: 90%;
margin: 0 10%;
border-spacing: 1em;
}
.notations > div {
display: table-row;
}
.notations > div > div {
display: table-cell;
height: 2.5em;
vertical-align: middle;
}
.notations .notation {
font-weight: bold;
.execution {
text-align: center;
}
.notations .criteria {
font-weight: normal;
}
.notations .A, .notations .B, .notations .C, .notations .D, .notations .E, .notations .F, .notations .NA {
width: 2.5em;
font-size: 2em;
text-align: center;
border-radius: 0.5em;
font-weight: bold;
}
.notations .A {
/* green */
background: #00DB61;
}
.notations .B {
/* green */
background: #CAD63D;
}
.notations .C {
/* yellow */
background: #FFD119;
}
.notations .D {
/* orange */
background: #FFA319;
}
.notations .E {
/* red */
background: #FF6600;
}
.notations .F {
/* red */
background: #FF1919;
}
.notations .NA {
/* Non applicable */
background: #CCC;
}
.notations .icon-eye {
color: #9c4274;
cursor: pointer;
}
.notations .criteria .table {
width: 75%;
}
.notations .criteria .label {
width: 70%;
}
.notations .criteria .result {
width: 20%;
font-weight: bold;
white-space: nowrap;
text-align: center;
}
.notations .warning .label, .notations .warning .result {
color: #FF1919;
}
.notations .criteria .info {
width: 10%;
text-align: center;
}
.notations .criteria .icon-question {
color: #f1c40f;
cursor: pointer;
}
.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;
@@ -186,6 +66,10 @@ h4 {
position: absolute;
bottom: 0;
width: 100%;
&.clickable {
cursor: pointer;
}
}
.timeline div.interval:hover {
background: #9C4274;
@@ -269,54 +153,13 @@ h4 {
border-radius: 0.2em;
}
.metrics h4 {
padding-left: 2em;
}
.metrics .module {
padding-left: 4em;
padding-top: 0.5em;
}
.metrics .legend {
font-style: italic;
color: #aaa;
}
.metrics .offenders {
padding-left: 0em;
font-size: 0.8em;
}
.metrics .offenders div {
cursor: pointer;
}
.metrics .offenders ul {
margin-top: 0.5em;
}
.filters {
margin: 1em 0;
margin: 1em auto;
padding: 0.5em;
min-width: 30em;
width: 30%;
border: 1px dotted #aaa;
}
.slowRequestsLimit {
width: 3em;
font-size: 1em;
text-align: right;
border: 1px solid #aaa;
}
input.textFilter {
box-shadow: none;
font-size: 1em;
padding: 0 0.2em;
border: 1px solid #aaa;
border-radius: none;
width: 15em;
text-align: left;
}
.table {
@@ -351,14 +194,82 @@ input.textFilter {
background: #f1c40f;
}
.table > div.highlight {
> div.startTime {
background-color: #C0F090;
}
}
.table > div.highlight-remove {
transition: 3s;
> div.startTime {
transition: background-color 3s ease-in;
}
}
.table > div > .index {
color: #bbb;
word-break: normal;
}
.table > div > .type {
white-space:nowrap;
}
.table .children {
margin-top: 0.2em;
font-size: 0.8em;
line-height: 1.6em;
}
.table .child {
margin-left: 0.5em;
> .child {
margin-left: 1em;
}
&:before {
content: "↳";
}
.childArgs {
display: none;
}
span {
position: relative;
}
span:hover {
background: #EBD8E2;
div {
display: inline-block;
}
.childArgs {
display: block;
position: absolute;
padding: 0 1em 0 2em;
left: 100%;
top: 0;
background: #EBD8E2;
line-height: 1.3em;
height: 1.3em;
z-index: 1;
}
}
}
.table .showingDetails .child span:hover {
background: inherit;
.childArgs {
display: none;
}
}
.table > div > .value {
width: 70%;
word-break: break-all;
@@ -371,11 +282,13 @@ input.textFilter {
color: #f1c40f;
cursor: pointer;
}
.table .details .icon-warning {
cursor: pointer;
.table .icon-warning {
display: inline-block;
width: 0.8em;
}
.detailsOverlay {
display: none;
position: absolute;
right: 3em;
top: -3em;
@@ -385,13 +298,16 @@ input.textFilter {
background: #fff;
border: 2px solid #f1c40f;
border-radius: 0.5em;
z-index: 1;
z-index: 2;
}
@media screen and (max-width: 1024px) {
.detailsOverlay {
width: 25em;
}
}
.showDetails .detailsOverlay {
display: block;
}
.detailsOverlay .closeBtn {
position: absolute;
top: 0.5em;
@@ -426,72 +342,15 @@ input.textFilter {
.table > div > .startTime.domCreation {
background: @domCreationBg;
}
.table .icon-warning {
.execution .icon-warning {
color: #e74c3c;
}
/**** NgModal popin (have a look inside bower_components) ****/
.ng-modal {
position: fixed;
z-index: 9999;
top: 0;
left: 0;
width: 100%;
height: 100%;
text-align: left;
}
.ng-modal-overlay {
position: absolute;
z-index: 9999;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #000;
opacity: 0.5;
}
.ng-modal-dialog {
z-index: 10000;
position: absolute;
top: 50%;
left: 50%;
width: 50%;
transform: translate(-50%, -50%);
-webkit-transform: translate(-50%, -50%);
background-color: #fff;
padding: 10px;
border: 3px solid #f1c40f;
border-radius: 0.5em;
color: #000;
}
.ng-modal-dialog-content {
overflow-x: hidden;
overflow-y: scroll;
word-wrap: break-word;
max-height: 20em;
font-weight: normal;
white-space: normal;
}
.ng-modal-close {
position: absolute;
top: 3px;
right: 5px;
cursor: pointer;
font-size: 120%;
padding: 5px;
display: inline-block;
}
.ng-modal-close-x {
font-weight: bold;
font-family: Arial, sans-serif;
}
.ng-modal-title {
font-weight: bold;
font-size: 1.5em;
display: block;
margin-bottom: 10px;
padding-bottom: 7px;
border-bottom: solid 1px #999;
}
.warningsFilterOn {
> div {
display: none;
&.warning {
display: table-row;
}
}
}
+55
View File
@@ -0,0 +1,55 @@
<html>
<head>
<meta charset="utf-8">
<title>Yellow Lab Tools</title>
<base href="/">
<link rel="icon" type="image/png" href="/img/favicon.png">
<meta property="og:image" content="/img/logo-large.png" />
<meta name="description" content="Free online web performance tool. Audit your webpage for performance and front-end quality issues. And it's open-source!" />
<!-- build:css /css/styles.css-->
<link rel="stylesheet" type="text/css" href="/css/main.css">
<link rel="stylesheet" type="text/css" href="/css/index.css">
<link rel="stylesheet" type="text/css" href="/css/dashboard.css">
<link rel="stylesheet" type="text/css" href="/css/queue.css">
<link rel="stylesheet" type="text/css" href="/css/rule.css">
<link rel="stylesheet" type="text/css" href="/css/screenshot.css">
<link rel="stylesheet" type="text/css" href="/css/timeline.css">
<link rel="stylesheet" type="text/css" href="/css/about.css">
<!-- endbuild -->
<!-- build:js /js/all.js -->
<script src="/bower_components/angular/angular.min.js"></script>
<script src="/bower_components/angular-route/angular-route.min.js"></script>
<script src="/bower_components/angular-resource/angular-resource.min.js"></script>
<script src="/bower_components/angular-sanitize/angular-sanitize.min.js"></script>
<script src="/bower_components/angular-animate/angular-animate.min.js"></script>
<script src="/js/app.js"></script>
<script src="/js/controllers/indexCtrl.js"></script>
<script src="/js/controllers/dashboardCtrl.js"></script>
<script src="/js/controllers/queueCtrl.js"></script>
<script src="/js/controllers/ruleCtrl.js"></script>
<script src="/js/controllers/screenshotCtrl.js"></script>
<script src="/js/controllers/timelineCtrl.js"></script>
<script src="/js/models/resultsFactory.js"></script>
<script src="/js/models/runsFactory.js"></script>
<script src="/js/services/apiService.js"></script>
<script src="/js/services/menuService.js"></script>
<script src="/js/directives/gradeDirective.js"></script>
<script src="/js/directives/offendersDirectives.js"></script>
<!-- endbuild -->
<head>
<body ng-app="YellowLabTools">
<div id="header"><h1>Yellow Lab <span class="icon-lab"></span> Tools</h1></div>
<div id="body" ng-view autoscroll="true"></div>
<div class="footer">
<a href="/about">Learn more</a><br>If you like <b>Yellow Lab Tools</b>, <a href="https://github.com/gmetais/YellowLabTools" target="_blank" class="star">give it a <span>&#9733;</span> on GitHub</a>!<br>
<span class="version">@@version</span>
</div>
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','//www.google-analytics.com/analytics.js','ga');if('@@googleAnalyticsId'.indexOf('UA-')===0){ga('create','@@googleAnalyticsId','auto');}
</script>
</body>
</html>
+9
View File
@@ -0,0 +1,9 @@
<div class="about">
<p><b>Yellow Lab Tools</b> is an open source project by <a href="http://www.gaelmetais.com" target="_blank">Gaël Métais</a>. It lets you test a webpage (via an URL) and detects <b>performance</b> and <b>front-end code quality</b> issues.</p>
<p>This is done by loading the webpage via PhantomJS and collecting various metrics and statistics with the help of <a href="https://github.com/macbre/phantomas" target="_blank">Phantomas</a>. These metrics are categorized and transformed into scores. It also gives in-depth details so developpers can correct the detected issues.</p>
<p>By the way, <b>it's free</b> because we are geeks, not businessmen. All we want is <a href="https://github.com/gmetais/YellowLabTools" target="_blank" class="star">a <span>&#9733;</span> on GitHub</a>. It will boost our motivation to add more awesome features!!!</p>
<p><br><a href="/">Back to index</a></p>
</div>
+70
View File
@@ -0,0 +1,70 @@
<div ng-include="'views/resultSubHeader.html'"></div>
<div class="summary board">
<div class="globalScore" ng-if="globalScore === 0 || globalScore > 0">
<div>
<h2>Global score</h2>
<div class="globalScoreDisplay">
<grade score="result.scoreProfiles.generic.globalScore" class="globalGrade"></grade>
<div class="on100">{{globalScore}}/100</div>
</div>
</div>
<div>
<a href="/result/{{result.runId}}/screenshot">
<div class="screenshotWrapper desktop">
<div>
<img class="screenshotImage" ng-src="{{result.screenshotUrl}}"/>
</div>
</div>
</a>
</div>
</div>
<h2 ng-if="!error && !fromSocialShare">Score details</h2>
<div ng-if="!error && !fromSocialShare" class="notations">
<div ng-repeat="categoryKey in categoriesOrder" ng-init="category = result.scoreProfiles.generic.categories[categoryKey]">
<grade score="category.categoryScore" class="categoryScore"></grade>
<div class="category">{{category.label}}</div>
<div class="criteria">
<div class="table" title="Click to see details">
<div ng-repeat="ruleName in category.rules" ng-if="result.rules[ruleName]" ng-init="rule = result.rules[ruleName]"
ng-class="{'warning': rule.abnormal}" ng-click="showRulePage(ruleName)">
<div class="grade">
<grade score="rule.score"></grade>
</div>
<div class="label">{{rule.policy.label}}</div>
<div class="result">
{{rule.value}}
<span ng-if="rule.abnormal" class="icon-warning"></span>
<span ng-if="rule.abnormalityScore <= -100" class="icon-warning"></span>
<span ng-if="rule.abnormalityScore <= -300" class="icon-warning"></span>
</div>
<div class="info"><span class="icon-question"></span></div>
</div>
</div>
</div>
</div>
</div>
<div ng-if="!error && !fromSocialShare" class="apiTip">
<b>Did you know? Yellow Lab Tools now has an API</b>! <a href="{{getAPIUrl()}}" target="_blank">Here</a> is the JSON output for this run. Checkout <a href="https://github.com/gmetais/YellowLabTools/wiki/Public-API" target="_blank">the API doc</a>.
</div>
<div class="tweet" ng-if="!error && !fromSocialShare">
<form ng-submit="sendTweet()">
<input type="text" class="tweetText" ng-model="tweetText">
<input type="submit" class="tweetButton" value="Tweet this!" ng-click="shareOnTwitter(tweetText)">
<input type="submit" class="linkedinButton" value="LinkedIn this!" ng-click="shareOnLinkedin(tweetText)">
</form>
</div>
<div class="fromShare" ng-if="!error && fromSocialShare">
<p>Yellow Lab Tools is a free online tool that analyzes performance and front-end quality.</p>
<a class="linkButton" href="" ng-click="seeFullReport()">See the full report for this page</a>
<a class="linkButton" href="/">Test another webpage</a>
</div>
<div ng-if="error">
<h2>Run failed / Run not found</h2>
</div>
</div>
+24
View File
@@ -0,0 +1,24 @@
<h2 class="promess">Free online test to help speeding up <b>heavy</b> web pages</h2>
<form ng-submit="launchTest()" >
<input type="text" name="url" ng-model="url" placeholder="http://www.mysite.com" class="url" />
<input type="submit" value="Launch test" class="launchBtn" ng-class="{disabled: !url}" />
</form>
<div class="features">
<div>
<h3>Front-end quality</h3>
<p>Audits quality problems in the HTML, CSS and JS</p>
</div>
<div>
<h3>WebPerf</h3>
<p>Checks if performance good practices are applied</p>
</div>
<div>
<h3>JS Profiling</h3>
<p>Untangles the JavaScript spaghetti code</p>
</div>
</div>
+36
View File
@@ -0,0 +1,36 @@
<p ng-if="url">Tested url: &nbsp; <a href="{{url}}" target="_blank" class="testedUrl">{{url}}</a></p>
<div ng-if="!notFound && !connectionLost">
<div ng-if="status.statusCode == 'failed'">
<div class="status">Test failed</div>
<p class="statusSubMessage">{{status.error}}</p>
<a class="linkButton" href="https://github.com/gmetais/YellowLabTools/issues" target="_blank">Report the issue on GitHub</a>
<a class="linkButton" href="/">New test</a>
</div>
<div ng-if="status.statusCode == 'awaiting'">
<div class="status">
<ng-pluralize count="status.position" when="{'one': 'Waiting behind 1 other test', 'other': 'Waiting behind {} other tests'}">
</ng-pluralize>
</div>
<p class="statusSubMessage">(auto-refresh activated)</p>
</div>
<div ng-if="status.statusCode == 'running'">
<div class="status">Test is running...</div>
<p class="statusSubMessage">(auto-refresh activated)</p>
</div>
<div ng-if="status.statusCode == 'complete'">
<div class="status">Test complete</div>
<p class="statusSubMessage">Opening results...</p>
</div>
</div>
<div ng-if="notFound == true">
<div class="status">Error 404 (test not found)</div>
<p class="statusSubMessage">The server probably just rebooted. We are very sorry about that, please try to launch the test again.</p>
<a class="linkButton" href="/">New test</a>
</div>
<div ng-if="connectionLost == true">
<div class="status">Connection lost with server</div>
<p class="statusSubMessage">Check your wifi cable, or maybe YellowLab.tools is rebooting.</p>
</div>
+8
View File
@@ -0,0 +1,8 @@
<div>Tested url: &nbsp; <a href="{{result.params.url}}" target="_blank" class="testedUrl">{{result.params.url}}</a></div>
<div class="resultsMenu">
<a class="menuItem back" href="/"><div class="icon-back"></div><span>New test<span></a>
<a class="menuItem restart" href="" ng-click="testAgain()"><div class="icon-loop"></div><span>Test again<span></a>
<div class="menuItem" ng-class="{active: Menu.getCurrentPage() == 'dashboard'}" ng-click="Menu.changePage('dashboard')"><div class="icon-summary"></div><span>Dashboard</span></div>
<div class="menuItem" ng-class="{active: Menu.getCurrentPage() == 'timeline'}" ng-click="Menu.changePage('timeline')"><div class="icon-spaghetti"></div><span>JS Timeline</span></div>
</div>
+177
View File
@@ -0,0 +1,177 @@
<div ng-include="'views/resultSubHeader.html'"></div>
<div class="rule board">
<div class="backToDashboard"><a href="#" ng-click="backToDashboard()">Back to dashboard</a></div>
<div ng-if="rule" class="ruleTable">
<div class="left">
<h2>{{rule.policy.label}}</h2>
<grade score="rule.score" class="score"></grade>
<div>{{rule.score}}/100</div>
</div>
<div class="right">
<h3>Value: {{rule.value}}</h3>
<div ng-bind-html="rule.policy.message" class="message"></div>
</div>
</div>
<div ng-if="rule.abnormal" class="warning">
<h3>Warning</h3>
<p>This rule reached the abnormality threshold, which means there is a real problem you should care about.</p>
</div>
<div class="offenders" ng-if="rule.policy.hasOffenders">
<h3><ng-pluralize count="rule.offendersObj.count" when="{'0': 'No offenders', 'one': '1 offender', 'other': '{} offenders'}"></ng-pluralize></h3>
<div ng-if="rule.offendersObj.list" class="offendersTable">
<div ng-repeat="offender in rule.offendersObj.list track by $index">
<div ng-if="offender.parseError">
{{offender.parseError}}
</div>
<div ng-if="!offender.parseError">
<div ng-if="policyName === 'DOMidDuplicated'">
<b>{{offender.id}}</b>: {{offender.occurrences}} occurrences
</div>
<div ng-if="policyName === 'DOMinserts'">
<dom-element-button obj="offender.insertedElement"></dom-element-button> appended to <dom-element-button obj="offender.receiverElement"></dom-element-button>
</div>
<div ng-if="policyName === 'DOMqueriesWithoutResults'">
<b>{{offender.query}}</b> (in <dom-element-button obj="offender.context"></dom-element-button>) using {{offender.fn}}
</div>
<div ng-if="policyName === 'DOMqueriesAvoidable'">
<b>{{offender.query}}</b> (in <dom-element-button obj="offender.context"></dom-element-button>) using {{offender.fn}}: <b>{{offender.count}} queries</b>
</div>
<div ng-if="policyName === 'eventsBound'">
<b>{{offender.eventName}}</b> bound to <dom-element-button obj="offender.element"></dom-element-button>
</div>
<div ng-if="policyName === 'jsErrors'">
<b>{{offender.error}}</b>
<div class="offenderButton" ng-if="offender.backtrace.length == 0">no backtrace</div>
<div class="offenderButton opens" ng-if="offender.backtrace.length > 0">
backtrace
<div class="backtrace">
<div ng-repeat="obj in offender.backtrace track by $index">
<span ng-if="obj.functionName">{{obj.functionName}}()</span>
<url-link url="obj.file" max-length="60"></url-link>
line {{obj.line}}
</div>
</div>
</div>
</div>
<div ng-if="policyName === 'documentWriteCalls'">
<b>{{offender.writeFn}}</b>
<span ng-if="offender.from">
called from
<span ng-if="offender.from.functionName">{{offender.from.functionName}}()</span>
<url-link url="offender.from.file" max-length="50"></url-link>
line {{offender.from.line}}
</span>
<span ng-if="!offender.from">
called from (no backtrace available)
</span>
</div>
<div ng-if="policyName === 'cssParsingErrors'">
<b>{{offender.error}}</b>
<file-and-line file="offender.file" line="offender.line" column="offender.column"></file-and-line>
<span ng-if="offender.file">(<a href="http://jigsaw.w3.org/css-validator/validator?profile=css3&usermedium=all&warning=no&uri={{offender.file | encodeURIComponent}}" target="_blank">Check on the W3C validator</a>)</span>
</div>
<div ng-if="policyName === 'cssComplexSelectors' || policyName === 'cssComplexSelectorsByAttribute' || policyName === 'cssImports' || policyName === 'cssUniversalSelectors' || policyName === 'cssRedundantBodySelectors' || policyName === 'cssRedundantChildNodesSelectors'">
<span ng-if="offender.bolded" ng-bind-html="offender.bolded"></span>
<b ng-if="!offender.bolded">{{offender.css}}</b>
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
</div>
<div ng-if="policyName === 'cssDuplicatedSelectors'">
{{offender.rule}} (<b>x{{offender.occurrences}}</b>)
</div>
<div ng-if="policyName === 'cssDuplicatedProperties'">
Property <b>{{offender.property}}</b> duplicated in <b>{{offender.rule}} { }</b>
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
</div>
<div ng-if="policyName === 'cssEmptyRules'">
<b>{{offender.css}} { }</b>
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
</div>
<div ng-if="policyName === 'cssExpressions'">
{{offender.rule}} {{ '{' + offender.property}}: <b>expression(</b>{{offender.expression}}<b>)</b>}
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
</div>
<div ng-if="policyName === 'cssImportants'">
{{offender.rule}} {{ '{' + offender.property}}: {{offender.value}} <b>!important</b>}
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
</div>
<div ng-if="policyName === 'cssOldIEFixes'">
<span ng-if="offender.browser"><b>{{offender.browser}} fix:</b></span>
<span ng-bind-html="offender.bolded"></span>
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
</div>
<div ng-if="policyName === 'cssOldPropertyPrefixes'">
{{offender.rule}} {<b>{{offender.property}}</b>: {{offender.value + '}' }}
<br>
<b>{{offender.message}}</b>
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
</div>
<div ng-if="policyName === 'requests' || policyName === 'htmlCount' || policyName === 'jsCount' || policyName === 'cssCount' || policyName === 'imageCount' || policyName === 'webfontCount' || policyName === 'videoCount' || policyName === 'jsonCount' || policyName === 'otherCount' || policyName === 'smallJsFiles' || policyName === 'smallCssFiles' || policyName === 'smallImages'">
<url-link url="offender.file" max-length="100"></url-link>
<span ng-if="offender.size || offender.size === 0">({{offender.size}} kB)</span>
</div>
<div ng-if="policyName === 'notFound' || policyName === 'closedConnections' || policyName === 'multipleRequests' || policyName === 'cachingDisabled' || policyName === 'cachingNotSpecified'">
<url-link url="offender" max-length="100"></url-link>
</div>
<div ng-if="policyName === 'cachingTooShort'">
<url-link url="offender.file" max-length="100"></url-link>
cached for <b>{{offender.ttlWithUnit}} {{offender.unit}}</b>
</div>
<div ng-if="policyName === 'domains'">
<b>{{offender.domain}}</b>
(<ng-pluralize count="offender.requests" when="{'one':'1 request','other':'{} requests'}"></ng-pluralize>)
</div>
<div ng-if="policyName === 'globalVariables' || policyName === 'jQueryVersionsLoaded'">
{{offender}}
</div>
</div>
</div>
</div>
<div ng-if="!rule.offendersObj.list" class="offendersHtml">
<div ng-if="policyName === 'DOMelementMaxDepth'">
<dom-tree tree="rule.offendersObj.tree"></dom-tree>
</div>
<div ng-if="policyName === 'cssColors' && rule.offendersObj.count > 0">
<p>This is the colors palette, sized by total occurrences:</p>
<div class="colorPalette">
<div ng-repeat="offender in rule.offendersObj.palette" style="background-color: {{offender.color}}; width: {{offender.occurrences * 100 / rule.offendersObj.palette[0].occurrences}}%"><div>{{offender.color}} ({{offender.occurrences}} times)</div></div>
</div>
</div>
</div>
</div>
<div ng-if="!rule && rule !== null" class="notFound">
<h2>404</h2>
Rule "{{policyName}}"" not found
</div>
<div class="backToDashboard"><a href="#" ng-click="backToDashboard()">Back to dashboard</a></div>
</div>
+14
View File
@@ -0,0 +1,14 @@
<div ng-include="'views/resultSubHeader.html'"></div>
<div class="screenshot board">
<h2>Screenshot</h2>
<div class="screenshotWrapper desktop">
<div>
<img class="screenshotImage" ng-src="{{result.screenshotUrl}}"/>
</div>
</div>
<p>(scroll on the screenshot to see under the fold)</p>
<div class="backToDashboard"><a href="#" ng-click="backToDashboard()">Back to dashboard</a></div>
</div>
+81
View File
@@ -0,0 +1,81 @@
<div ng-include="'views/resultSubHeader.html'"></div>
<div class="execution board">
<div class="selectScript" ng-class="{empty:!selectedScript}">
Filter timeline and profiler by script:
<select ng-model="selectedScript" ng-options="script.shortPath for script in scripts" ng-change="changeScript()">
<option value="">All (no filter)</option>
</select>
</div>
<h2>Javascript Timeline</h2>
<p>This graph gives a quick view of when the Javascript interactions with the DOM occur during the loading of the page.</p>
<div class="timeline">
<div class="chart">
<div class="chartPoints">
<div ng-repeat="duration in timeline track by $index"
class="interval"
ng-class="{
domCreation: $index * timelineIntervalDuration < result.toolsResults.phantomas.metrics.domInteractive,
domInteractive: $index * timelineIntervalDuration >= result.toolsResults.phantomas.metrics.domInteractive
&& $index * timelineIntervalDuration < result.toolsResults.phantomas.metrics.domContentLoaded,
domContentLoaded: $index * timelineIntervalDuration >= result.toolsResults.phantomas.metrics.domContentLoaded
&& $index * timelineIntervalDuration < result.toolsResults.phantomas.metrics.domContentLoadedEnd,
domContentLoadedEnd: $index * timelineIntervalDuration >= result.toolsResults.phantomas.metrics.domContentLoadedEnd
&& $index * timelineIntervalDuration < result.toolsResults.phantomas.metrics.domComplete,
domComplete: $index * timelineIntervalDuration >= result.toolsResults.phantomas.metrics.domComplete
}">
<div style="height: {{100 * duration / timelineMax | number: 0}}px" class="color" ng-class="{clickable: duration > 0}" scroll-on-click="{{$index * timelineIntervalDuration}}"></div>
<div class="tooltip detailsOverlay">
<div>
Timestamp: {{$index * timelineIntervalDuration | number: 0}} ms
</div>
</div>
</div>
</div>
<div class="startTime">0 ms</div>
<div class="endTime">{{endTime | number: 0}} ms</div>
</div>
<div class="legend">
<div class="titles">
<div class="domCreation"><div class="color"></div>DOM creation</div>
<div class="domInteractive"><div class="color"></div>DOM interactive</div>
<div class="domContentLoaded"><div class="color"></div>DOM content loaded event</div>
<div class="domContentLoadedEnd"><div class="color"></div>Page completion</div>
<div class="domComplete"><div class="color"></div>Page is complete</div>
</div>
<div class="tips">
<div>Executing Javascript and DOM queries here is a <b>bad practice</b> and slows down the DOM construction.</div>
<div>Some frameworks do things here, but it's not reliable and should be avoided.</div>
<div>Also known as "document ready". This is where you should execute <b>top-priority</b> scripts, like binding action buttons or launch a video player.</div>
<div>Here you can execute <b>mid-priority</b> tasks. Loading a script with createElement('script') is one way to do so.</div>
<div>The page is considered loaded, it's time for low <b>priority things</b> : trackers, social plugins, easter egg...</div>
</div>
</div>
</div>
<h2>Javascript Profiler</h2>
<p>
The table below shows the interactions between Javascript and the DOM. It is useful to understand what happens while the page loads.
</p>
<div class="filters">
<div>
<input type="checkbox" ng-model="warningsFilterOn" id="warningsFilterOn" />
<label for="warningsFilterOn">Show warnings only</label>
</div>
</div>
<div class="table" ng-class="{warningsFilterOn: warningsFilterOn}">
<div class="headers">
<div><!-- index --></div>
<div>Type</div>
<div>Params</div>
<div><!-- details --></div>
<div>Timestamp</div>
</div>
<profiler-line ng-repeat="node in profilerData" data-index="$index" node="node"></profiler-line>
</div>
<div class="backToDashboard"><a href="#" ng-click="backToDashboard()">Back to dashboard</a></div>
</div>
+38
View File
@@ -0,0 +1,38 @@
var Q = require('q');
var Runner = require('./runner');
var yellowLabTools = function(url, options) {
'use strict';
var deferred = Q.defer();
if (!url) {
deferred.reject('URL missing');
} else {
if (url.toLowerCase().indexOf('http://') !== 0 && url.toLowerCase().indexOf('https://') !== 0) {
url = 'http://' + url;
}
var params = {
url: url,
options: options || {}
};
var runner = new Runner(params)
.then(function(data) {
deferred.resolve(data);
})
.fail(function(err) {
deferred.reject(err);
});
}
return deferred.promise;
};
module.exports = yellowLabTools;
File diff suppressed because it is too large Load Diff
+116
View File
@@ -0,0 +1,116 @@
{
"categories": {
"domComplexity": {
"label": "DOM complexity",
"policies": {
"DOMelementsCount": 1,
"DOMelementMaxDepth": 1,
"iframesCount": 1,
"DOMidDuplicated": 1
}
},
"domManipulations": {
"label": "DOM manipulations",
"policies": {
"DOMinserts": 2,
"DOMqueries": 1,
"DOMqueriesWithoutResults": 2,
"DOMqueriesAvoidable": 2,
"eventsBound": 1
}
},
"badJavascript": {
"label": "Bad JavaScript",
"policies": {
"jsErrors": 1,
"documentWriteCalls": 2,
"consoleMessages": 0.5,
"globalVariables": 0.5
}
},
"jQueryVersion": {
"label": "jQuery version",
"policies": {
"jQueryVersion": 5,
"jQueryVersionsLoaded": 0.1
}
},
"cssSyntaxError": {
"label": "CSS syntax errors",
"policies": {
"cssParsingErrors": 1
}
},
"cssComplexity": {
"label": "CSS complexity",
"policies": {
"cssRules": 2,
"cssComplexSelectors": 2,
"cssComplexSelectorsByAttribute": 1.5,
"cssColors": 0.5
}
},
"badCSS": {
"label": "Bad CSS",
"policies": {
"cssImports": 3,
"cssDuplicatedSelectors": 2,
"cssDuplicatedProperties": 1,
"cssEmptyRules": 2,
"cssExpressions": 1,
"cssImportants": 3,
"cssOldIEFixes": 1,
"cssOldPropertyPrefixes": 1,
"cssUniversalSelectors": 1,
"cssRedundantBodySelectors": 1,
"cssRedundantChildNodesSelectors": 1
}
},
"requests": {
"label": "Requests number",
"policies": {
"requests": 5,
"htmlCount": 0,
"jsCount": 1,
"cssCount": 1,
"imageCount": 0,
"webfontCount": 2,
"videoCount": 0,
"jsonCount": 0,
"otherCount": 0
}
},
"smallRequests": {
"label": "Small requests",
"policies": {
"smallJsFiles": 1,
"smallCssFiles": 1,
"smallImages": 1
}
},
"network": {
"label": "Network",
"policies": {
"notFound": 2,
"closedConnections": 2,
"multipleRequests": 2,
"cachingNotSpecified": 1,
"cachingDisabled": 1,
"cachingTooShort": 1,
"domains": 1
}
}
},
"globalScore": {
"domComplexity": 1,
"domManipulations": 2,
"badJavascript": 1,
"jQueryVersion": 1,
"cssSyntaxError": 1,
"cssComplexity": 1,
"badCSS": 1,
"requests": 3,
"smallRequests": 1,
"network": 2
}
}
+187
View File
@@ -0,0 +1,187 @@
var OffendersHelpers = function() {
this.domPathToArray = function(str) {
return str.split(/\s?>\s?/);
};
this.listOfDomArraysToTree = function(listOfDomArrays) {
var result = {};
function recursiveTreeBuilder(tree, domArray) {
if (domArray.length > 0) {
var currentDomElement = domArray.shift();
if (tree === null) {
tree = {};
}
tree[currentDomElement] = recursiveTreeBuilder(tree[currentDomElement] || null, domArray);
return tree;
} else if (tree === null) {
return 1;
} else {
return tree + 1;
}
}
listOfDomArrays.forEach(function(domArray) {
result = recursiveTreeBuilder(result, domArray);
});
return result;
};
this.domPathToDomElementObj = function(domPath) {
if (typeof domPath === 'boolean') {
return {
// Not a normal element path
type: 'notAnElement',
element: domPath
};
}
var domArray = this.domPathToArray(domPath);
var domTree = this.listOfDomArraysToTree([this.domPathToArray(domPath)]);
if (domArray[0] === 'html') {
return {
type: 'html'
};
}
if (domArray[0] === 'body') {
if (domArray.length === 1) {
return {
type: 'body'
};
} else {
return {
type: 'domElement',
element: domArray[domArray.length - 1],
tree: domTree
};
}
}
if (domArray[0] === 'head') {
return {
type: 'head'
};
}
if (domArray[0] === '#document') {
return {
type: 'document'
};
}
if (domArray[0] === 'window') {
return {
type: 'window'
};
}
if (domArray[0] === 'DocumentFragment') {
if (domArray.length === 1) {
return {
type: 'fragment'
};
} else {
return {
type: 'fragmentElement',
element: domArray[domArray.length - 1],
tree: domTree
};
}
}
// Not attached element, such as just created with document.createElement()
if (domArray.length === 1) {
return {
type: 'createdElement',
element: domPath
};
} else {
return {
type: 'createdElement',
element: domArray[domArray.length - 1],
tree: domTree
};
}
};
this.backtraceToArray = function(str) {
var traceArray = str.split(/ \/ /);
if (traceArray) {
var results = [];
var parts = null;
for (var i=0 ; i<traceArray.length ; i++) {
parts = /^(([\w$]+) )?([^ ]+):(\d+)$/.exec(traceArray[i]);
if (parts) {
var obj = {
file: parts[3],
line: parseInt(parts[4], 10)
};
if (parts[2]) {
obj.functionName = parts[2];
}
results.push(obj);
} else {
return null;
}
}
return results;
} else {
return null;
}
};
this.sortVarsLikeChromeDevTools = function(vars) {
return vars.sort(function(a, b) {
return (a < b) ? -1 : 1;
});
};
this.urlToLink = function(url) {
var shortUrl = (url.length > 110) ? url.substr(0, 47) + ' ... ' + url.substr(-48) : url;
return '<a href="' + url + '" target="_blank" title="' + url + '">' + shortUrl + '</a>';
};
this.cssOffenderPattern = function(offender) {
var parts = /^(.*) (?:<([^ \(]*)>|\[inline CSS\]) @ (\d+):(\d+)$/.exec(offender);
if (!parts) {
return {
offender: offender
};
} else {
return {
css: parts[1],
file: parts[2] || null,
line: parseInt(parts[3], 10),
column: parseInt(parts[4], 10)
};
}
};
this.fileWithSizePattern = function(fileWithSize) {
var parts = /^([^ ]*) \((\d+\.\d{2}) kB\)$/.exec(fileWithSize);
if (!parts) {
return {
file: fileWithSize
};
} else {
return {
file: parts[1],
size: parseFloat(parts[2])
};
}
};
};
module.exports = new OffendersHelpers();
+136
View File
@@ -0,0 +1,136 @@
var extend = require('util')._extend;
var debug = require('debug')('ylt:ruleschecker');
var RulesChecker = function() {
'use strict';
this.check = function(data, policies) {
/*jshint loopfunc:true */
var results = {};
debug('Starting checking rules');
for (var metricName in policies) {
var policy = policies[metricName];
var rule;
if (policy.tool &&
data.toolsResults[policy.tool] &&
data.toolsResults[policy.tool].metrics &&
(data.toolsResults[policy.tool].metrics[metricName] || data.toolsResults[policy.tool].metrics[metricName] === 0)) {
rule = {
value: data.toolsResults[policy.tool].metrics[metricName],
policy: extend({}, policy) // Clone object policy instead of reference
};
// Deal with offenders
if (policy.hasOffenders) {
var offenders = [];
// Take DOMqueriesAvoidable's offenders from DOMqueriesDuplicated, for example.
if (policy.takeOffendersFrom) {
var fromList = policy.takeOffendersFrom;
// takeOffendersFrom option can be a string or an array of strings.
if (typeof fromList === 'string') {
fromList = [fromList];
}
fromList.forEach(function(from) {
if (data.toolsResults[policy.tool] &&
data.toolsResults[policy.tool].offenders &&
data.toolsResults[policy.tool].offenders[from]) {
offenders = offenders.concat(data.toolsResults[policy.tool].offenders[from]);
}
});
data.toolsResults[policy.tool].offenders[metricName] = offenders;
} else if (data.toolsResults[policy.tool] &&
data.toolsResults[policy.tool].offenders &&
data.toolsResults[policy.tool].offenders[metricName]) {
offenders = data.toolsResults[policy.tool].offenders[metricName];
}
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) {
try {
offendersObj = policy.offendersTransformFn(offenders, rule);
} catch(err) {
debug('Error while transforming offenders for %s', metricName);
debug(err);
}
delete rule.policy.offendersTransformFn;
} else {
offendersObj = {
count: offenders.length,
list: offenders
};
}
rule.offendersObj = offendersObj;
}
rule.bad = rule.value > policy.isOkThreshold;
rule.abnormal = policy.isAbnormalThreshold && rule.value >= policy.isAbnormalThreshold;
// A value between 0 (bad) and 100 (very good).
var score = (policy.isBadThreshold - rule.value) * 100 / (policy.isBadThreshold - policy.isOkThreshold);
rule.score = Math.min(Math.max(Math.round(score), 0), 100);
// A value between 0 (abnormal) and negative-infinity (your website is a blackhole)
var abnormalityScore = (policy.isAbnormalThreshold - rule.value) * 100 / (policy.isAbnormalThreshold - policy.isOkThreshold);
rule.abnormalityScore = Math.min(Math.round(abnormalityScore), 0);
results[metricName] = rule;
debug('Metric %s calculated. Score: %d', metricName, rule.score);
} else if (policy.scoreFn) {
debug('Custom score function for %s', metricName);
// Custom score function
rule = policy.scoreFn(data);
// Check returned values (if the result is null, just don't save)
if (rule) {
rule.policy = {
label: policy.label,
message: policy.message
};
results[metricName] = rule;
debug('Metric %s calculated. Score: %d', metricName, rule.score);
} else {
debug('Metric %s is null. Ignored.', metricName);
}
} else {
debug('Metric %s not found for tool %s', metricName, policy.tool);
}
}
debug('Rules checking finished');
return results;
};
};
module.exports = new RulesChecker();
+64
View File
@@ -0,0 +1,64 @@
var Q = require('q');
var debug = require('debug')('ylt:runner');
var phantomasWrapper = require('./tools/phantomas/phantomasWrapper');
var jsExecutionTransformer = require('./tools/jsExecutionTransformer');
var rulesChecker = require('./rulesChecker');
var scoreCalculator = require('./scoreCalculator');
var Runner = function(params) {
'use strict';
var deferred = Q.defer();
// The pivot format
var data = {
params: params,
toolsResults: {}
};
// Execute Phantomas first
phantomasWrapper.execute(data).then(function(phantomasResults) {
data.toolsResults.phantomas = phantomasResults;
// Treat the JS Execution Tree from offenders
data.javascriptExecutionTree = jsExecutionTransformer.transform(data);
// Other tools go here
// Rules checker
var policies = require('./metadata/policies');
data.rules = rulesChecker.check(data, policies);
// Scores calculator
var scoreProfileGeneric = require('./metadata/scoreProfileGeneric.json');
data.scoreProfiles = {
generic : scoreCalculator.calculate(data, scoreProfileGeneric)
};
delete data.toolsResults.phantomas.metrics.javascriptExecutionTree;
delete data.toolsResults.phantomas.offenders.javascriptExecutionTree;
return data;
}).then(function(data) {
// Finished!
deferred.resolve(data);
}).fail(function(err) {
debug('Run failed');
debug(err);
deferred.reject(err);
});
return deferred.promise;
};
module.exports = Runner;
+87
View File
@@ -0,0 +1,87 @@
var Q = require('q');
var debug = require('debug')('ylt:scoreCalculator');
var ScoreCalculator = function() {
'use strict';
this.calculate = function(data, profile) {
var results = {
categories: {}
};
var categoryScore;
var categoryName;
var weight;
debug('Starting calculating scores');
// Calculate categories
for (categoryName in profile.categories) {
var categoryResult = {
label: profile.categories[categoryName].label
};
categoryScore = new ScoreMerger();
var rules = [];
var policyScore;
for (var policyName in profile.categories[categoryName].policies) {
weight = profile.categories[categoryName].policies[policyName];
if (data.rules[policyName]) {
policyScore = data.rules[policyName].score + (data.rules[policyName].abnormalityScore * 2);
categoryScore.push(policyScore, weight);
} else {
debug('Warning: could not find rule %s', policyName);
}
rules.push(policyName);
}
categoryResult.categoryScore = categoryScore.getScore();
categoryResult.rules = rules;
results.categories[categoryName] = categoryResult;
}
// Calculate general score
var globalScore = new ScoreMerger();
for (categoryName in profile.globalScore) {
weight = profile.globalScore[categoryName];
if (results.categories[categoryName]) {
globalScore.push(results.categories[categoryName].categoryScore, weight);
}
}
results.globalScore = Math.round(globalScore.getScore());
debug('Score calculation finished:');
debug(results);
return results;
};
var ScoreMerger = function() {
var sum = 0;
var totalWeight = 0;
this.push = function(score, weight) {
sum += (100 - score) * weight;
totalWeight += weight;
};
this.getScore = function() {
if (totalWeight === 0) {
return 100;
}
return Math.round(100 - (sum / totalWeight));
};
};
};
module.exports = new ScoreCalculator();
+135
View File
@@ -0,0 +1,135 @@
var debug = require('debug')('ylt:screenshotHandler');
var lwip = require('lwip');
var tmp = require('temporary');
var Q = require('q');
var fs = require('fs');
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) {
var that = this;
debug('Starting screenshot transformation');
return this.openImage(imagePath)
.then(function(image) {
return that.resizeImage(image, width);
})
.then(this.toBuffer);
};
this.openImage = function(imagePath) {
var deferred = Q.defer();
lwip.open(imagePath, function(err, image){
if (err) {
debug('Could not open imagePath %s', imagePath);
debug(err);
deferred.reject(err);
} else {
debug('Image correctly open');
deferred.resolve(image);
}
});
return deferred.promise;
};
this.resizeImage = function(image, newWidth) {
var deferred = Q.defer();
var currentWidth = image.width();
var ratio = newWidth / currentWidth;
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);
}
});
return deferred.promise;
};
this.toBuffer = function(image) {
var deferred = Q.defer();
image.toBuffer('jpg', {quality: 90}, function(err, buffer){
if (err) {
debug('Could not save image to buffer');
debug(err);
deferred.reject(err);
} else {
debug('Image correctly transformed to buffer');
deferred.resolve(buffer);
}
});
return deferred.promise;
};
this.deleteTmpFileAndFolder = function(tmpFilePath, screenshotTmpFolder) {
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.');
} else {
debug('Screenshot file deleted.');
}
screenshotTmpFolder.rmdir();
debug('Screenshot temp folder deleted');
deferred.resolve();
});
return deferred.promise;
};
};
module.exports = new screenshotHandler();
+305
View File
@@ -0,0 +1,305 @@
var debug = require('debug')('ylt:server');
var Q = require('q');
var ylt = require('../../index');
var ScreenshotHandler = require('../../screenshotHandler');
var RunsQueue = require('../datastores/runsQueue');
var RunsDatastore = require('../datastores/runsDatastore');
var ResultsDatastore = require('../datastores/resultsDatastore');
var ApiController = function(app) {
'use strict';
var queue = new RunsQueue();
var runsDatastore = new RunsDatastore();
var resultsDatastore = new ResultsDatastore();
// Create a new run
app.post('/api/runs', function(req, res) {
// Add http to the test URL
if (req.body.url && req.body.url.toLowerCase().indexOf('http://') !== 0 && req.body.url.toLowerCase().indexOf('https://') !== 0) {
req.body.url = 'http://' + req.body.url;
}
// Grab the test parameters and generate a random run ID
var run = {
runId: (Date.now()*1000 + Math.round(Math.random()*1000)).toString(36),
params: {
url: req.body.url,
waitForResponse: req.body.waitForResponse !== false && req.body.waitForResponse !== 'false' && req.body.waitForResponse !== 0,
partialResult: req.body.partialResult || null,
screenshot: req.body.screenshot || false,
jsTimeline: req.body.jsTimeline || false
}
};
// Create a temporary folder to save the screenshot
var screenshot;
if (run.params.screenshot) {
screenshot = ScreenshotHandler.getScreenshotTempFile();
}
// Add test to the testQueue
debug('Adding test %s to the queue', run.runId);
var queuePromise = queue.push(run.runId);
// Save the run to the datastore
runsDatastore.add(run, queuePromise.startingPosition);
// Listening for position updates
queuePromise.progress(function(position) {
runsDatastore.updatePosition(run.runId, position);
});
// Let's start the run
queuePromise.then(function() {
runsDatastore.updatePosition(run.runId, 0);
debug('Launching test %s on %s', run.runId, run.params.url);
var runOptions = {
screenshot: run.params.screenshot ? screenshot.getTmpFilePath() : false,
jsDeepAnalysis: run.params.jsTimeline
};
return ylt(run.params.url, runOptions);
})
// Phantomas completed, let's save the screenshot if any
.then(function(data) {
debug('Success');
data.runId = run.runId;
// Some conditional steps are made if there is a screenshot
var screenshotPromise = Q.resolve();
if (run.params.screenshot) {
// Replace the empty promise created earlier with Q.resolve()
screenshotPromise = screenshot.toThumbnail(400)
// Read screenshot
.then(function(screenshotBuffer) {
if (screenshotBuffer) {
debug('Image optimized');
data.screenshotBuffer = screenshotBuffer;
// Official path to get the image
data.screenshotUrl = '/api/results/' + data.runId + '/screenshot.jpg';
}
})
// Delete screenshot temporary file
.then(screenshot.deleteTmpFile);
}
// Let's continue
screenshotPromise
// Save results
.then(function() {
// Remove uneeded temp screenshot path
delete data.params.options.screenshot;
// Empty javascriptExecutionTree if not needed
if (!run.params.jsTimeline) {
data.javascriptExecutionTree = {};
}
// Remove tools results if not needed
return resultsDatastore.saveResult(data);
})
// Mark as the run as complete and send the response if the request is still waiting
.then(function() {
debug('Result saved in datastore');
runsDatastore.markAsComplete(run.runId);
if (run.params.waitForResponse) {
// If the user only wants a portion of the result (partialResult option)
switch(run.params.partialResult) {
case 'generalScores':
res.redirect(302, '/api/results/' + run.runId + '/generalScores');
break;
case 'rules':
res.redirect(302, '/api/results/' + run.runId + '/rules');
break;
case 'javascriptExecutionTree':
res.redirect(302, '/api/results/' + run.runId + '/javascriptExecutionTree');
break;
case 'phantomas':
res.redirect(302, '/api/results/' + run.runId + '/toolsResults/phantomas');
break;
default:
res.redirect(302, '/api/results/' + run.runId);
}
}
})
.fail(function(err) {
console.error('Test failed for URL: %s', run.params.url);
console.error(err.toString());
runsDatastore.markAsFailed(run.runId, err.toString());
res.status(500).send('An error occured');
});
})
.fail(function(err) {
console.error('Test failed for URL: %s', run.params.url);
console.error(err.toString());
runsDatastore.markAsFailed(run.runId, err.toString());
res.status(400).send('Bad request');
})
.finally(function() {
queue.remove(run.runId);
});
// The user doesn't want to wait for the response, sending the run ID only
if (!run.params.waitForResponse) {
console.log('Sending response without waiting.');
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify({runId: run.runId}));
}
});
// Retrive one run by id
app.get('/api/runs/:id', function(req, res) {
var runId = req.params.id;
var run = runsDatastore.get(runId);
if (run) {
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify(run, null, 2));
} else {
res.status(404).send('Not found');
}
});
// Retrieve the list of all runs
/*app.get('/api/runs', function(req, res) {
// NOT YET
});*/
// Delete one run by id
/*app.delete('/api/runs/:id', function(req, res) {
deleteRun()
});*/
// Delete all
/*app.delete('/api/runs', function(req, res) {
purgeRuns()
});
// List all
app.get('/api/runs', function(req, res) {
listRuns()
});
// Exists
app.head('/api/runs/:id', function(req, res) {
existsX();
// Returns 200 if the result exists or 404 if not
});
*/
// Retrive one result by id
app.get('/api/results/:id', function(req, res) {
getPartialResults(req.params.id, res, function(data) {
return data;
});
});
// Retrieve one result and return only the generalScores part of the response
app.get('/api/results/:id/generalScores', function(req, res) {
getPartialResults(req.params.id, res, function(data) {
return data.scoreProfiles.generic;
});
});
app.get('/api/results/:id/generalScores/:scoreProfile', function(req, res) {
getPartialResults(req.params.id, res, function(data) {
return data.scoreProfiles[req.params.scoreProfile];
});
});
app.get('/api/results/:id/rules', function(req, res) {
getPartialResults(req.params.id, res, function(data) {
return data.rules;
});
});
app.get('/api/results/:id/javascriptExecutionTree', function(req, res) {
getPartialResults(req.params.id, res, function(data) {
return data.javascriptExecutionTree;
});
});
app.get('/api/results/:id/toolsResults/phantomas', function(req, res) {
getPartialResults(req.params.id, res, function(data) {
return data.toolsResults.phantomas;
});
});
function getPartialResults(runId, res, partialGetterFn) {
resultsDatastore.getResult(runId)
.then(function(data) {
var results = partialGetterFn(data);
if (typeof results === 'undefined') {
res.status(404).send('Not found');
return;
}
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify(results, null, 2));
}).fail(function() {
res.status(404).send('Not found');
});
}
// Retrive one result by id
app.get('/api/results/:id/screenshot.jpg', function(req, res) {
var runId = req.params.id;
resultsDatastore.getScreenshot(runId)
.then(function(screenshotBuffer) {
res.setHeader('Content-Type', 'image/jpeg');
res.send(screenshotBuffer);
}).fail(function() {
res.status(404).send('Not found');
});
});
};
module.exports = ApiController;
+26
View File
@@ -0,0 +1,26 @@
var path = require('path');
var express = require('express');
var FrontController = function(app) {
'use strict';
var cacheDuration = 365 * 24 * 60 * 60 * 1000; // One year
var assetsPath = (app.get('env') === 'development') ? '../../../front/src' : '../../../front/build';
var routes = ['/', '/about', '/result/:runId', '/result/:runId/timeline', '/result/:runId/screenshot', '/result/:runId/rule/:policy', '/queue/:runId'];
routes.forEach(function(route) {
app.get(route, function(req, res) {
res.setHeader('Cache-Control', 'public, max-age=20');
res.sendFile(path.join(__dirname, assetsPath, 'main.html'));
});
});
app.use('/css', express.static(path.join(__dirname, assetsPath, 'css'), { maxAge: cacheDuration }));
app.use('/fonts', express.static(path.join(__dirname, assetsPath, 'fonts'), { maxAge: cacheDuration }));
app.use('/img', express.static(path.join(__dirname, assetsPath, 'img'), { maxAge: cacheDuration }));
app.use('/js', express.static(path.join(__dirname, assetsPath, 'js'), { maxAge: cacheDuration }));
app.use('/views', express.static(path.join(__dirname, assetsPath, 'views'), { maxAge: cacheDuration }));
app.use('/bower_components', express.static(path.join(__dirname, '../../../bower_components'), { maxAge: cacheDuration }));
};
module.exports = FrontController;
+120
View File
@@ -0,0 +1,120 @@
var fs = require('fs');
var rimraf = require('rimraf');
var path = require('path');
var Q = require('q');
var debug = require('debug')('ylt:resultsDatastore');
function ResultsDatastore() {
'use strict';
var resultFileName = 'results.json';
var resultScreenshotName = 'screenshot.jpg';
var resultsFolderName = 'results';
var resultsDir = path.join(__dirname, '..', '..', '..', resultsFolderName);
this.saveResult = function(testResults) {
return createResultFolder(testResults.runId)
.then(function() {
return saveScreenshotIfExists(testResults);
})
.then(function() {
debug('Saving results to disk...');
var resultFilePath = path.join(resultsDir, testResults.runId, resultFileName);
debug('Destination file is %s', resultFilePath);
return Q.nfcall(fs.writeFile, resultFilePath, JSON.stringify(testResults, null, 2));
});
};
this.getResult = function(runId) {
var resultFilePath = path.join(resultsDir, runId, resultFileName);
debug('Reading results (runID = %s) from disk...', runId);
return Q.nfcall(fs.readFile, resultFilePath, {encoding: 'utf8'}).then(function(data) {
return JSON.parse(data);
});
};
this.deleteResult = function(runId) {
var folder = path.join(resultsDir, runId);
debug('Deleting results (runID = %s) from disk...', runId);
return Q.nfcall(rimraf, folder);
};
// The folder /results/folderName/
function createResultFolder(folderName) {
var folder = path.join(resultsDir, folderName);
debug('Creating the folder %s', folderName);
return createGlobalFolder().then(function() {
return Q.nfcall(fs.mkdir, folder);
});
}
// The folder /results/
function createGlobalFolder() {
var deferred = Q.defer();
// Create the results folder if it doesn't exist
fs.exists(resultsDir, function(exists) {
if (exists) {
deferred.resolve();
} else {
debug('Creating the global results folder', resultsDir);
fs.mkdir(resultsDir, function(err) {
if (err) {
deferred.reject(err);
} else {
deferred.resolve();
}
});
}
});
return deferred.promise;
}
// If there is a screenshot, save it as screenshot.jpg in the same folder as the results
function saveScreenshotIfExists(testResults) {
var deferred = Q.defer();
if (testResults.screenshotBuffer) {
var screenshotFilePath = path.join(resultsDir, testResults.runId, resultScreenshotName);
fs.writeFile(screenshotFilePath, testResults.screenshotBuffer);
delete testResults.screenshotBuffer;
} else {
deferred.resolve();
}
return deferred;
}
this.getScreenshot = function(runId) {
var screenshotFilePath = path.join(resultsDir, runId, resultScreenshotName);
debug('Getting screenshot (runID = %s) from disk...', runId);
return Q.nfcall(fs.readFile, screenshotFilePath);
};
}
module.exports = ResultsDatastore;
+106
View File
@@ -0,0 +1,106 @@
function RunsDatastore() {
'use strict';
// NOT PERSISTING RUNS
// For the moment, maybe one day
var runs = {};
var STATUS_AWAITING = 'awaiting';
var STATUS_RUNNING = 'running';
var STATUS_COMPLETE = 'complete';
var STATUS_FAILED = 'failed';
this.add = function(run, position) {
runs[run.runId] = run;
this.updatePosition(run.runId, position);
};
this.get = function(runId) {
return runs[runId];
};
this.updatePosition = function(runId, position) {
var run = runs[runId];
if (position > 0) {
run.status = {
statusCode: STATUS_AWAITING,
position: position
};
} else {
run.status = {
statusCode: STATUS_RUNNING
};
}
runs[runId] = run;
};
this.markAsComplete = function(runId) {
var run = runs[runId];
run.status = {
statusCode: STATUS_COMPLETE
};
runs[runId] = run;
};
this.markAsFailed = function(runId, err) {
var run = runs[runId];
var errorMessage;
switch(err) {
case '252':
errorMessage = "Error 252: page timeout in Phantomas";
break;
case '253':
errorMessage = "Error 253: Phantomas config error";
break;
case '254':
errorMessage = "Error 254: page loading failed in PhantomJS";
break;
case '255':
errorMessage = "Error 255: Phantomas error";
break;
case '1001':
errorMessage = "Error 1001: JavaScript profiling failed";
break;
case '1002':
errorMessage = "Error 1002: missing Phantomas metrics";
break;
default:
errorMessage = err;
}
run.status = {
statusCode: STATUS_FAILED,
error: errorMessage
};
runs[runId] = run;
};
this.delete = function(runId) {
delete runs[runId];
};
this.list = function() {
var runsArray = [];
Object.keys(runs).forEach(function(key) {
runsArray.push(runs[key]);
});
return runsArray;
};
}
module.exports = RunsDatastore;
+83
View File
@@ -0,0 +1,83 @@
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;
@@ -0,0 +1,85 @@
var config = require('../../../server_config/settings.json');
var debug = require('debug')('apiLimitsMiddleware');
var apiLimitsMiddleware = function(req, res, next) {
'use strict';
debug('Entering API Limits Middleware with IP address %s', req.connection.remoteAddress);
if (req.path.indexOf('/api/') === 0 && !res.locals.hasApiKey) {
if (req.path === '/api/runs') {
if (!runsTable.accepts(req.connection.remoteAddress)) {
// Sorry :/
debug('Too many tests launched from IP address %s', req.connection.remoteAddress);
res.status(429).send('Too many requests');
return;
}
}
if (!callsTable.accepts(req.connection.remoteAddress)) {
// Sorry :/
debug('Too many API requests from IP address %s', req.connection.remoteAddress);
res.status(429).send('Too many requests');
return;
}
debug('Not blocked by the API limits');
// It's ok for the moment
}
next();
};
var RecordTable = function(maxPerDay) {
var table = {};
// Check if the user overpassed the limit and save its visit
this.accepts = function(ipAddress) {
if (table[ipAddress]) {
this.cleanEntry(ipAddress);
debug('%d visits in the last 24 hours', table[ipAddress].length);
if (table[ipAddress].length >= maxPerDay) {
return false;
} else {
table[ipAddress].push(Date.now());
}
} else {
table[ipAddress] = [];
table[ipAddress].push(Date.now());
}
return true;
};
// Clean the table for this guy
this.cleanEntry = function(ipAddress) {
table[ipAddress] = table[ipAddress].filter(function(date) {
return date > Date.now() - 1000*60*60*24;
});
};
// Clean the entire table once in a while
this.removeOld = function() {
for (var ipAddress in table) {
this.cleanEntry(ipAddress);
}
};
};
// Init the records tables
var runsTable = new RecordTable(config.maxAnonymousRunsPerDay);
var callsTable = new RecordTable(config.maxAnonymousCallsPerDay);
module.exports = apiLimitsMiddleware;
+42
View File
@@ -0,0 +1,42 @@
var config = require('../../../server_config/settings.json');
var debug = require('debug')('authMiddleware');
var authMiddleware = function(req, res, next) {
'use strict';
if (req.path.indexOf('/api/') === 0) {
if (req.headers && req.headers['x-api-key']) {
// Test if it's an authorized key
if (isApiKeyValid(req.headers['x-api-key'])) {
// Come in!
debug('Authorized key: %s', req.headers['x-api-key']);
res.locals.hasApiKey = true;
} else {
// Sorry :/
debug('Unauthorized key %s', req.headers['x-api-key']);
res.status(401).send('Unauthorized');
return;
}
} else {
debug('No authorization key');
// It's ok for the moment but you might be blocked by the apiLimitsMiddleware, dude
}
}
next();
};
function isApiKeyValid(apiKey) {
return (config.authorizedKeys[apiKey]) ? true : false;
}
module.exports = authMiddleware;
+89
View File
@@ -0,0 +1,89 @@
var debug = require('debug')('ylt:jsExecutionTransformer');
var offendersHelpers = require('../offendersHelpers');
var jsExecutionTransformer = function() {
this.transform = function(data) {
var javascriptExecutionTree = {};
debug('Starting JS execution transformation');
try {
javascriptExecutionTree = JSON.parse(data.toolsResults.phantomas.offenders.javascriptExecutionTree[0]);
if (javascriptExecutionTree.children) {
javascriptExecutionTree.children.forEach(function(node) {
// Mark abnormal things with a warning flag
var contextLenght = (node.data.callDetails && node.data.callDetails.context) ? node.data.callDetails.context.length : null;
if ((node.data.type === 'jQuery - bind' && contextLenght > 5) ||
node.data.resultsNumber === 0 ||
contextLenght === 0) {
node.warning = true;
}
// Mark errors with an error flag
if (node.data.type === 'error' || node.data.type === 'jQuery version change') {
node.error = true;
}
// Mark a performance flag
if (['domInteractive', 'domContentLoaded', 'domContentLoadedEnd', 'domComplete'].indexOf(node.data.type) >= 0) {
node.windowPerformance = true;
}
// Read the execution tree and adjust the navigation timings (cause their not very well synchronised)
switch(node.data.type) {
case 'domInteractive':
data.toolsResults.phantomas.metrics.domInteractive = node.data.timestamp;
break;
case 'domContentLoaded':
data.toolsResults.phantomas.metrics.domContentLoaded = node.data.timestamp;
break;
case 'domContentLoadedEnd':
data.toolsResults.phantomas.metrics.domContentLoadedEnd = node.data.timestamp;
break;
case 'domComplete':
data.toolsResults.phantomas.metrics.domComplete = node.data.timestamp;
break;
}
// Change the list of dom paths into a tree
treeRecursiveParser(node, function(node) {
if (node.data.callDetails && node.data.callDetails.context && node.data.callDetails.context.length > 0) {
node.data.callDetails.context.elements = node.data.callDetails.context.elements.map(offendersHelpers.domPathToDomElementObj, offendersHelpers);
}
if (node.data.type === 'appendChild' || node.data.type === 'insertBefore') {
node.data.callDetails.arguments[0] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[0]);
}
if (node.data.type === 'insertBefore') {
node.data.callDetails.arguments[1] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[1]);
}
});
});
}
debug('JS execution transformation complete');
} catch(err) {
throw err;
}
return javascriptExecutionTree;
};
function treeRecursiveParser(node, fn) {
if (node.children) {
node.children.forEach(function(child) {
treeRecursiveParser(child, fn);
});
}
fn(node);
}
};
module.exports = new jsExecutionTransformer();
@@ -7,7 +7,7 @@
*/
/* global document: true, window: true */
exports.version = '0.1';
exports.version = '0.2';
exports.module = function(phantomas) {
'use strict';
@@ -36,16 +36,17 @@ exports.module = function(phantomas) {
phantomas.log('Overwritting phantomas spy function');
function spy(obj, fn, callbackBefore, callbackAfter) {
var origFn = obj[fn];
var origFn = obj && obj[fn];
if (typeof origFn !== 'function') {
return false;
}
phantomas.log('Attaching a spy to "' + fn + '" function...');
phantomas.log('Attaching a YLT spy to "' + fn + '" function...');
obj[fn] = function() {
var result;
var err;
// Before
if (enabled) {
@@ -54,15 +55,28 @@ exports.module = function(phantomas) {
// Execute
try {
result = origFn.apply(this, arguments);
} catch(e) {
// Catching the err for the moment, because we need to make sure the callbackAfter function is called.
phantomas.log('Error catched on spyed function "' + fn + '": ' + e);
phantomas.log(arguments);
err = e;
} finally {
// After
if (enabled && callbackAfter) {
callbackAfter.call(this, result);
callbackAfter.call(this, result, arguments);
}
if (err) {
phantomas.log('Re-throwing the error');
throw err;
}
}
@@ -3,12 +3,13 @@
*/
/* global Element: true, Document: true, Node: true, window: true */
exports.version = '0.9.a';
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
@@ -21,29 +22,29 @@ exports.module = function(phantomas) {
phantomas.once('init', function() {
phantomas.evaluate(function() {
(function(phantomas) {
function querySpy(type, query, fnName, context) {
phantomas.emit('domQuery', type, query, fnName, context); // @desc DOM query has been made
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');
querySpy('id', '#' + id, 'getElementById', '#document');
phantomas.enterContext({
type: 'getElementById',
callDetails: {
context: {
domElement: '#document'
},
arguments: ['#' + id]
},
backtrace: phantomas.getBacktrace()
});
}, function(result) {
}, function(result, args) {
var id = args[0];
querySpy('id', '#' + id, 'getElementById', '#document', (result === null));
var moreData = {
resultsNumber : result ? 1 : 0
resultsNumber : (result === null) ? 0 : 1
};
phantomas.leaveContext(moreData);
});
@@ -56,13 +57,13 @@ exports.module = function(phantomas) {
phantomas.incrMetric('DOMqueriesByClassName');
phantomas.addOffender('DOMqueriesByClassName', '.%s (in %s)', className, context);
querySpy('class', '.' + className, 'getElementsByClassName', context);
phantomas.enterContext({
type: 'getElementsByClassName',
callDetails: {
context: {
domElement: context
length: 1,
elements: [context]
},
arguments: ['.' + className]
},
@@ -70,7 +71,14 @@ exports.module = function(phantomas) {
});
}
function selectorClassNameAfter(result) {
function selectorClassNameAfter(result, args) {
/*jshint validthis: true */
var className = args[0];
var context = phantomas.getDOMPath(this);
querySpy('class', '.' + className, 'getElementsByClassName', context, (result.length === 0));
var moreData = {
resultsNumber : (result && result.length > 0) ? result.length : 0
};
@@ -88,13 +96,13 @@ exports.module = function(phantomas) {
phantomas.incrMetric('DOMqueriesByTagName');
phantomas.addOffender('DOMqueriesByTagName', '%s (in %s)', tagName, context);
querySpy('tag name', tagName.toLowerCase(), 'getElementsByTagName', context);
phantomas.enterContext({
type: 'getElementsByTagName',
callDetails: {
context: {
domElement: context
length: 1,
elements: [context]
},
arguments: [tagName]
},
@@ -102,7 +110,14 @@ exports.module = function(phantomas) {
});
}
function selectorTagNameSpyAfter(result) {
function selectorTagNameSpyAfter(result, args) {
/*jshint validthis: true */
var tagName = args[0];
var context = phantomas.getDOMPath(this);
querySpy('tag name', tagName.toLowerCase(), 'getElementsByTagName', context, (result.length === 0));
var moreData = {
resultsNumber : (result && result.length > 0) ? result.length : 0
};
@@ -112,11 +127,11 @@ exports.module = function(phantomas) {
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);
querySpy('selector', selector, 'querySelectorAll', context);
}
function selectorQuerySpyBefore(selector) {
@@ -129,7 +144,8 @@ exports.module = function(phantomas) {
type: 'querySelector',
callDetails: {
context: {
domElement: context
length: 1,
elements: [context]
},
arguments: [selector]
},
@@ -137,7 +153,14 @@ exports.module = function(phantomas) {
});
}
function selectorQuerySpyAfter(result) {
function selectorQuerySpyAfter(result, args) {
/*jshint validthis: true */
var selector = args[0];
var context = phantomas.getDOMPath(this);
querySpy('selector', selector, 'querySelectorAll', context, (!result || result.length === 0));
var moreData = {
resultsNumber : result ? 1 : 0
};
@@ -154,7 +177,8 @@ exports.module = function(phantomas) {
type: 'querySelectorAll',
callDetails: {
context: {
domElement: context
length: 1,
elements: [context]
},
arguments: [selector]
},
@@ -162,7 +186,14 @@ exports.module = function(phantomas) {
});
}
function selectorAllQuerySpryAfter(result) {
function selectorAllQuerySpryAfter(result, args) {
/*jshint validthis: true */
var selector = args[0];
var context = phantomas.getDOMPath(this);
querySpy('selector', selector, 'querySelectorAll', context, (!result || result.length === 0));
var moreData = {
resultsNumber : (result && result.length > 0) ? result.length : 0
};
@@ -207,7 +238,8 @@ exports.module = function(phantomas) {
type: 'appendChild',
callDetails: {
context: {
domElement: context
length: 1,
elements: [context]
},
arguments: [appended]
},
@@ -215,20 +247,25 @@ exports.module = function(phantomas) {
});
}
function insertBeforeSpyBefore(child) {
function insertBeforeSpyBefore(child, refElement) {
/*jshint validthis: true */
var context = phantomas.getDOMPath(this);
var appended = phantomas.getDOMPath(child);
var referent = phantomas.getDOMPath(refElement);
appendChild(child, this, context, appended);
phantomas.enterContext({
type: 'insertBefore',
callDetails: {
context: {
domElement: context
length: 1,
elements: [context]
},
arguments: [appended]
arguments: [
appended,
referent
]
},
backtrace: phantomas.getBacktrace()
});
@@ -240,13 +277,72 @@ exports.module = function(phantomas) {
phantomas.spy(Node.prototype, 'insertBefore', insertBeforeSpyBefore, function(result) {
phantomas.leaveContext();
});
phantomas.spy(Document.prototype, 'createElement', function(tagName) {
phantomas.enterContext({
type: 'createElement',
callDetails: {
arguments: [tagName]
},
backtrace: phantomas.getBacktrace()
});
}, function(result, args) {
phantomas.leaveContext();
});
phantomas.spy(Document.prototype, 'createTextNode', function(text) {
phantomas.enterContext({
type: 'createTextNode',
callDetails: {
arguments: [text]
},
backtrace: phantomas.getBacktrace()
});
}, function(result, args) {
phantomas.leaveContext();
});
phantomas.spy(Document.prototype, 'createDocumentFragment', function() {
phantomas.enterContext({
type: 'createDocumentFragment',
callDetails: {
arguments: []
},
backtrace: phantomas.getBacktrace()
});
}, function(result, args) {
phantomas.leaveContext();
});
})(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('../../../node_modules/phantomas/lib/collection'),
var Collection = require('../../util/collection'),
DOMqueries = new Collection();
phantomas.on('domQuery', function(type, query, fnName, context) {
@@ -27,7 +27,8 @@ exports.module = function(phantomas) {
type: 'addEventListener',
callDetails: {
context: {
domElement: path
length: 1,
elements: [path]
},
arguments: [eventType]
},
@@ -7,16 +7,17 @@
/* global document: true, window: true */
/* jshint -W030 */
exports.version = '0.2.a';
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('jQuerySizzleCallsDuplicated'); // @desc number of calls on the same Sizzle request
phantomas.setMetric('jQueryDifferentVersions'); //@desc number of different jQuery versions loaded on the page (not counting iframes)
phantomas.setMetric('jQueryEventTriggers'); // @desc number of jQuery event triggers
var jQueryFunctions = [
// DOM manipulations
@@ -54,7 +55,20 @@ exports.module = function(phantomas) {
'scrollLeft',
'scrollTop',
// generic events
// Animations
'hide',
'show',
'toggle',
'animate',
'fadeIn',
'fadeOut',
'fadeTo',
'fadeToggle',
'slideDown',
'slideUp',
'slideToggle',
// Generic events
'on',
'off',
'live',
@@ -62,9 +76,10 @@ exports.module = function(phantomas) {
'delegate',
'undelegate',
'one',
'bind',
'unbind',
// more events
// More events
'blur',
'change',
'click',
@@ -89,10 +104,9 @@ exports.module = function(phantomas) {
'scroll',
'select',
'submit',
'toggle',
'unload',
// attributes
// Attributes
'attr',
'prop',
'removeAttr',
@@ -101,38 +115,55 @@ exports.module = function(phantomas) {
'hasClass',
'addClass',
'removeClass',
'toggleClass'
'toggleClass',
];
var jQueryTraversalFunctions = [
'children',
'closest',
'find',
'next',
'nextAll',
'nextUntil',
'offsetParent',
'parent',
'parents',
'parentsUntil',
'prev',
'prevAll',
'prevUntil',
'siblings'
];
jQueryFunctions = jQueryFunctions.concat(jQueryTraversalFunctions);
// spy calls to jQuery functions
phantomas.once('init', function() {
phantomas.evaluate(function(jQueryFunctions) {
phantomas.evaluate(function(jQueryFunctions, jQueryTraversalFunctions) {
(function(phantomas) {
var jQuery;
var oldJQuery;
// TODO: create a helper - phantomas.spyGlobalVar() ?
window.__defineSetter__('jQuery', function(val) {
phantomas.spyGlobalVar('jQuery', function(jQuery) {
var version;
var jQueryFn;
var oldJQuery = jQuery;
if (!val || !val.fn) {
if (!jQuery || !jQuery.fn) {
phantomas.log('jQuery: unable to detect version!');
return;
}
version = val.fn.jquery;
jQuery = val;
jQueryFn = val.fn;
// Older jQuery (v?.?) compatibility
if (!jQueryFn) {
jQueryFn = jQuery;
// 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;
phantomas.log('jQuery: loaded v' + version);
phantomas.setMetric('jQueryVersion', version);
// report the version of jQuery
version = jQuery.fn.jquery;
phantomas.emit('jQueryLoaded', version);
phantomas.pushContext({
type: (oldJQuery) ? 'jQuery version change' : 'jQuery loaded',
callDetails: {
@@ -140,11 +171,13 @@ exports.module = function(phantomas) {
},
backtrace: phantomas.getBacktrace()
});
oldJQuery = version;
// jQuery.ready.promise
// works for jQuery 1.8.0+ (released Aug 09 2012)
phantomas.spy(val.ready, 'promise', function(func) {
phantomas.spy(jQuery.ready, 'promise', function(func) {
phantomas.incrMetric('jQueryOnDOMReadyFunctions');
phantomas.addOffender('jQueryOnDOMReadyFunctions', phantomas.getCaller(3));
phantomas.pushContext({
type: 'jQuery - onDOMReady',
@@ -159,16 +192,16 @@ exports.module = function(phantomas) {
// Sizzle calls - jQuery.find
// works for jQuery 1.3+ (released Jan 13 2009)
phantomas.spy(val, 'find', function(selector, context) {
phantomas.spy(jQuery, 'find', function(selector, context) {
phantomas.incrMetric('jQuerySizzleCalls');
phantomas.emit('onSizzleCall', selector + ' (context: ' + (phantomas.getDOMPath(context) || 'unknown') + ')');
phantomas.addOffender('jQuerySizzleCalls', '%s (in %s)', selector, (phantomas.getDOMPath(context) || 'unknown'));
phantomas.enterContext({
type: 'jQuery - find',
type: 'jQuery - Sizzle call',
callDetails: {
context: {
length: this.length,
firstElementPath: phantomas.getDOMPath(context)
length: 1,
elements: [phantomas.getDOMPath(context)]
},
arguments: [selector]
},
@@ -182,40 +215,70 @@ exports.module = function(phantomas) {
phantomas.leaveContext(moreData);
}) || phantomas.log('jQuery: can not measure jQuerySizzleCalls (jQuery used on the page is too old)!');
// $().bind - jQuery.bind
// works for jQuery v?.?
phantomas.spy(jQueryFn, 'bind', function(eventTypes, func) {
phantomas.enterContext({
type: 'jQuery - bind',
callDetails: {
context: {
length: this.length,
firstElementPath: phantomas.getDOMPath(this[0]),
selector: this.selector
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]
},
arguments: [eventTypes, func]
},
backtrace: phantomas.getBacktrace()
});
backtrace: phantomas.getBacktrace()
});
}
}, function(result) {
phantomas.leaveContext();
}) || phantomas.log('jQuery: can not measure jQueryBindCalls (jQuery used on the page is too old)!');
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) {
var capitalizedName = functionName.substring(0,1).toUpperCase() + functionName.substring(1);
phantomas.spy(jQueryFn, functionName, function(args) {
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)) {
@@ -268,13 +331,17 @@ exports.module = function(phantomas) {
args[index] = arg;
});
var elements = [];
for (var i = 0 ; i < this.length ; i++) {
elements.push(phantomas.getDOMPath(this[i]));
}
phantomas.enterContext({
type: 'jQuery - ' + functionName,
callDetails: {
context: {
length: this.length,
firstElementPath: phantomas.getDOMPath(this[0])
elements: elements
},
arguments: args
},
@@ -282,45 +349,38 @@ exports.module = function(phantomas) {
});
}, function(result) {
phantomas.leaveContext();
}) || phantomas.log('jQuery: can not track jQuery - ' + capitalizedName + ' (this version of jQuery doesn\'t support it)');
if (jQueryTraversalFunctions.indexOf(functionName) >= 0) {
var moreData = {
resultsNumber : (result && result.length) ? result.length : 0
};
phantomas.leaveContext(moreData);
} else {
phantomas.leaveContext();
}
}) || phantomas.log('jQuery: can not track jQuery - ' + functionName + ' (this version of jQuery doesn\'t support it)');
});
});
window.__defineGetter__('jQuery', function() {
return jQuery;
});
})(window.__phantomas);
}, jQueryFunctions);
}, jQueryFunctions, jQueryTraversalFunctions);
});
// count Sizzle calls to detect duplicated queries
var Collection = require('../../../node_modules/phantomas/lib/collection'),
sizzleCalls = new Collection(),
jQueryLoading = new Collection();
phantomas.on('onSizzleCall', function(request) {
sizzleCalls.push(request);
// store the last resource that was received
// try to report where given jQuery version was loaded from
phantomas.on('recv', function(entry) {
if (entry.isJS) {
lastUrl = entry.url;
}
});
phantomas.on('jQueryLoaded', function(version) {
jQueryLoading.push(version);
});
phantomas.log('jQuery: loaded v' + version);
phantomas.setMetric('jQueryVersion', version);
phantomas.on('report', function() {
sizzleCalls.sort().forEach(function(id, cnt) {
if (cnt > 1) {
phantomas.incrMetric('jQuerySizzleCallsDuplicated');
phantomas.addOffender('jQuerySizzleCallsDuplicated', '%s: %d', id, cnt);
}
});
// report multiple jQuery "instances" (issue #435)
phantomas.incrMetric('jQueryVersionsLoaded');
phantomas.addOffender('jQueryVersionsLoaded', 'v%s', version);
jQueryLoading.forEach(function(version) {
phantomas.incrMetric('jQueryDifferentVersions');
phantomas.addOffender('jQueryDifferentVersions', '%s', version);
});
phantomas.log('jQuery: v%s (probably loaded from <%s>)', version, lastUrl);
});
};
@@ -0,0 +1,69 @@
/**
* Reports the use of functions known to be serious performance bottlenecks in JS
*
* @see http://www.nczonline.net/blog/2013/06/25/eval-isnt-evil-just-misunderstood/
* @see http://www.quirksmode.org/blog/archives/2005/06/three_javascrip_1.html
* @see http://www.stevesouders.com/blog/2012/04/10/dont-docwrite-scripts/
*
* Run phantomas with --spy-eval to count eval() calls (see issue #467)
*/
/* global document: true, window: true */
exports.version = '0.2';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('documentWriteCalls'); //@desc number of calls to either document.write or document.writeln @offenders
phantomas.setMetric('evalCalls'); // @desc number of calls to eval (either direct or via setTimeout / setInterval) @offenders
// spy calls to eval only when requested (issue #467)
var spyEval = phantomas.getParam('spy-eval') === true;
if (!spyEval) {
phantomas.log('javaScriptBottlenecks: to spy calls to eval() run phantomas with --spy-eval option');
}
phantomas.once('init', function() {
phantomas.evaluate(function(spyEval) {
(function(phantomas) {
function report(msg, caller, backtrace, metric) {
phantomas.log(msg + ': from ' + caller + '!');
phantomas.log('Backtrace: ' + backtrace);
phantomas.incrMetric(metric);
phantomas.addOffender(metric, "%s from %s", msg, caller);
}
// spy calls to eval()
if (spyEval) {
phantomas.spy(window, 'eval', function(code) {
report('eval() called directly', phantomas.getCaller(), phantomas.getBacktrace(), 'evalCalls');
phantomas.log('eval\'ed code: ' + (code || '').substring(0, 150) + '(...)');
});
}
// spy calls to setTimeout / setInterval with string passed instead of a function
phantomas.spy(window, 'setTimeout', function(fn, interval) {
if (typeof fn !== 'string') return;
report('eval() called via setTimeout("' + fn + '")', phantomas.getCaller(), phantomas.getBacktrace(), 'evalCalls');
});
phantomas.spy(window, 'setInterval', function(fn, interval) {
if (typeof fn !== 'string') return;
report('eval() called via setInterval("' + fn + '")', phantomas.getCaller(), phantomas.getBacktrace(), 'evalCalls');
});
// spy document.write(ln)
phantomas.spy(document, 'write', function(arg) {
report('document.write() used', phantomas.getCaller(), phantomas.getBacktrace(), 'documentWriteCalls');
});
phantomas.spy(document, 'writeln', function(arg) {
report('document.writeln() used', phantomas.getCaller(), phantomas.getBacktrace(), 'documentWriteCalls');
});
})(window.__phantomas);
}, spyEval);
});
};
@@ -0,0 +1,54 @@
/**
* Push items and count them
*/
function collection() {
/* jshint validthis: true */
this.items = {};
}
collection.prototype = {
push: function(item) {
if (typeof this.items[item] === 'undefined') {
this.items[item] = {
cnt: 1
};
} else {
this.items[item].cnt++;
}
},
sort: function() {
var newItems = {},
sortedKeys;
// sort in descending order (by cnt)
sortedKeys = Object.keys(this.items).sort((function(a, b) {
return this.items[b].cnt - this.items[a].cnt;
}).bind(this));
// build new items dictionary
sortedKeys.forEach(function(key) {
newItems[key] = this.items[key];
}, this);
this.items = newItems;
return this;
},
forEach: function(callback) {
Object.keys(this.items).forEach(function(key) {
callback(key, this.items[key].cnt);
}, this);
},
has: function(item) {
return (typeof this.items[item] !== 'undefined');
},
getLength: function() {
return Object.keys(this.items).length;
}
};
module.exports = collection;
+144
View File
@@ -0,0 +1,144 @@
var async = require('async');
var Q = require('q');
var ps = require('ps-node');
var path = require('path');
var debug = require('debug')('ylt:phantomaswrapper');
var phantomas = require('phantomas');
var PhantomasWrapper = function() {
'use strict';
/**
* This is the phantomas launcher. It merges user chosen options into the default options
* Available options :
*
* - timeout : in seconds (default 60)
* - jsDeepAnalysis : should we inspect subrequests in the javascript execution tree?
*
*/
this.execute = function(data) {
var deferred = Q.defer();
var task = data.params;
var options = {
// Cusomizable options
'timeout': task.options.timeout || 60,
'js-deep-analysis': task.options.jsDeepAnalysis || false,
'user-agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.110 Safari/537.36',
'screenshot': task.options.screenshot || false,
// Mandatory
'reporter': 'json:pretty',
'analyze-css': true,
'skip-modules': [
'blockDomains', // not needed
'domMutations', // not compatible with webkit
'domQueries', // overriden
'eventListeners', // overridden
'filmStrip', // not needed
'har', // not needed for the moment
'javaScriptBottlenecks', // needs to be launched after custom module scopeYLT
'jQuery', // overridden
'jserrors', // overridden
'pageSource', // not needed
'waitForSelector', // not needed
'windowPerformance' // overriden
].join(','),
'include-dirs': [
path.join(__dirname, 'custom_modules/core'),
path.join(__dirname, 'custom_modules/modules')
].join(',')
};
// Output the command line for debugging purpose
debug('If you want to reproduce the phantomas task only, copy the following command line:');
var optionsString = '';
for (var opt in options) {
var value = options[opt];
if ((typeof value === 'string' || value instanceof String) && value.indexOf(' ') >= 0) {
value = '"' + value + '"';
}
optionsString += ' ' + '--' + opt + '=' + value;
}
debug('node node_modules/phantomas/bin/phantomas.js --url=' + task.url + optionsString + ' --verbose');
// Kill the application if nothing happens
var phantomasPid;
var killer = setTimeout(function() {
debug('Killing the app because the test on %s was launched %d seconds ago', task.url, 5*options.timeout);
// If in server mode, forever will restart the server
// Kill the Phantomas process first
if (phantomasPid) {
ps.kill(phantomasPid, function(err) {
if (err) {
debug('Could not kill Phantomas process %s', phantomasPid);
}
else {
debug('Phantomas process %s was correctly killed', phantomasPid);
}
// Then suicide.
process.exit(1);
});
}
}, 5*options.timeout*1000);
// It's time to launch the test!!!
var triesNumber = 2;
async.retry(triesNumber, function(cb) {
var process = phantomas(task.url, options, function(err, json, results) {
debug('Returning from Phantomas');
// Adding some YellowLabTools errors here
if (json && json.metrics && (!json.metrics.javascriptExecutionTree || !json.offenders.javascriptExecutionTree)) {
err = 1001;
}
if (!err && (!json || !json.metrics)) {
err = 1002;
}
// Don't cancel test if it is a timeout and we've got some results
if (err === 252 && json) {
debug('Timeout after ' + options.timeout + ' seconds. But it\'s not a problem, the test is valid.');
err = null;
}
if (err) {
debug('Attempt failed. Error code ' + err);
}
cb(err, json);
});
phantomasPid = process.pid;
}, function(err, json) {
clearTimeout(killer);
if (err) {
debug('All ' + triesNumber + ' attemps failed for the test');
deferred.reject(err);
} else {
deferred.resolve(json);
}
});
return deferred.promise;
};
};
module.exports = new PhantomasWrapper();

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