Compare commits

...
280 Commits
Author SHA1 Message Date
Gaël Métais 6c17a709bf Merge pull request #85 from gmetais/develop
Releasing v1.7.0 very soon
2015-06-20 18:00:04 +02:00
Gaël Métais dd912ecc8f Merge pull request #84 from gmetais/reorder
New rules and rules reordering
2015-06-20 16:24:08 +02:00
Gaël Métais c9b12c4892 Remove bugged lossless image optim gain when there's a lossy one 2015-06-20 16:11:10 +02:00
Gaël Métais 414aea2637 Update dependencies and set version v1.7.0 2015-06-20 15:51:46 +02:00
Gaël Métais b7bc4dbadc Made Weight and Requests the first categories because they are the most important 2015-06-20 15:22:42 +02:00
Gaël Métais 8a8b871908 Changed some thresholds, mainly cooler 2015-06-20 15:19:50 +02:00
Gaël Métais 1225b27510 Fix timeline script filter 2015-06-20 14:34:13 +02:00
Gaël Métais f86bd86043 Add google analytics to already minified fies 2015-06-20 14:20:31 +02:00
Gaël Métais 8133164f44 Rename total requests number 2015-06-20 13:51:34 +02:00
Gaël Métais 2f447da34b Add Images not lazyloaded rule 2015-06-20 00:12:47 +02:00
Gaël Métais 41cdc8783e Remove uneeded policies 2015-06-19 23:47:25 +02:00
Gaël Métais ccbb324270 Replace TotalRequests and SmallRequests rules 2015-06-19 23:46:39 +02:00
Gaël Métais 8d4a0c34e7 Fix requests offenders 2015-06-19 18:45:34 +02:00
Gaël Métais 2620f75955 Fix SVG test 2015-06-19 18:01:02 +02:00
Gaël Métais 4ca525f701 Compare gzip size for svg optimizations 2015-06-19 17:16:30 +02:00
Gaël Métais 53a9aa377a Add Gzip offenders 2015-06-19 16:16:00 +02:00
Gaël Métais d166003dfe Update Phantomas, needed for svg optimization 2015-06-19 16:15:19 +02:00
Gaël Métais 31d80791a9 Compare minified files on gzipped size 2015-06-19 15:24:19 +02:00
Gaël Métais 02245aa5f1 Fix test 2015-06-19 14:55:13 +02:00
Gaël Métais 583b6b9a55 Optimizing JS minification step that takes too long 2015-06-19 14:05:21 +02:00
Gaël Métais d4815e855e Fix bug with SVG minification+compression 2015-06-18 23:06:57 +02:00
Gaël Métais 113665139a Rename isMinified into isOptimized 2015-06-18 22:51:02 +02:00
Gaël Métais 37e35b0ed6 Debugging test failing on travis 2015-06-18 16:42:24 +02:00
Gaël Métais eef8f00d8b Debugging test failing on travis 2015-06-18 15:51:00 +02:00
Gaël Métais bdb15a7780 Debugging test failing on travis 2015-06-18 15:09:10 +02:00
Gaël Métais 2b59912549 Debugging test failing on travis 2015-06-18 14:52:26 +02:00
Gaël Métais 8623085c2a Debugging test failing on travis 2015-06-18 13:50:34 +02:00
Gaël Métais 612627d66f Restore console.log correctly 2015-06-18 12:11:37 +02:00
Gaël Métais 04700beab3 Fixing tests on Travis 2015-06-18 11:55:36 +02:00
Gaël Métais 0d3d6c88e4 Trying to fix build on Travis 2015-06-18 11:10:47 +02:00
Gaël Métais db57f67e98 Change policy text 2015-06-18 11:08:16 +02:00
Gaël Métais baa145e15c Add a gzip compression task 2015-06-18 11:07:19 +02:00
Gaël Métais c1bae20f01 File minification 2015-06-16 20:33:19 +02:00
Gaël Métais 41bc676b12 Fix bug with about:blank 2015-06-01 17:44:34 +02:00
Gaël Métais cea74dd63d Cooler Image Optimization score 2015-06-01 17:44:18 +02:00
Gaël Métais 9ccca3b33b Add offenders to imageOptimization 2015-06-01 16:52:50 +02:00
Gaël Métais 1606cbab18 Add Image Optimization rule to the dashbard 2015-06-01 14:57:05 +02:00
Gaël Métais c428c8ae2d Optimize SVG files 2015-05-31 19:36:54 +02:00
Gaël Métais b52e84d1e7 Optimize PNG files 2015-05-31 19:25:34 +02:00
Gaël Métais 3fbcbbe8e0 Merge branch 'develop' into reorder 2015-05-31 19:02:40 +02:00
Gaël Métais 74eabd700c Add a JPEG optimizer 2015-05-31 18:39:52 +02:00
Gaël Métais d7ddae38e5 Total Weight offenders 2015-05-30 12:52:50 +02:00
Gaël Métais fe7de2db99 Fix rare bug with domComplete never triggered 2015-05-29 22:56:07 +02:00
Gaël Métais 9b9de42139 Fix test on headersSize 2015-05-29 17:01:51 +02:00
Gaël Métais 7b64288765 Fix typo in module name 2015-05-29 16:41:12 +02:00
Gaël Métais c6c967f89c Adding totalWeight metric 2015-05-29 16:13:32 +02:00
Gaël Métais 040dba961a v1.6.4 2015-05-28 11:26:39 +02:00
Gaël Métais 0146794cda Merge pull request #83 from gmetais/black
Fix black backgrounds in screenshots #82.
2015-05-28 11:21:00 +02:00
Gaël Métais b00eba77f9 Fix typo in comment 2015-05-28 11:12:02 +02:00
Gaël Métais 3f27ae249a Add a white background to screenshots 2015-05-28 11:09:45 +02:00
Gaël Métais 4384cffabe Merge branch 'develop' 2015-05-26 15:25:22 +02:00
Gaël Métais fde3234fc6 v1.6.3 2015-05-26 15:24:43 +02:00
Gaël Métais d998c7cd46 Merge branch 'develop' 2015-05-26 15:23:30 +02:00
Gaël Métais a8b62fc184 Fix score display on dashboard 2015-05-26 15:23:09 +02:00
Gaël Métais 65de8e08d3 Merge branch 'develop' 2015-05-26 11:19:54 +02:00
Gaël Métais 73cc1cd2e5 v1.6.2 2015-05-26 11:19:39 +02:00
Gaël Métais 26926071b0 Merge branch 'master' 2015-05-26 11:16:56 +02:00
Gaël Métais 5c433129ec Fix score display on dashboard 2015-05-26 11:16:24 +02:00
Gaël Métais 68bc1a6608 Wording 2015-05-25 19:06:45 +02:00
Gaël Métais e67a0e0bc0 Merge pull request #81 from gmetais/develop
v1.6.0
2015-05-25 16:38:14 +02:00
Gaël Métais f553d45396 v1.6.0 2015-05-25 16:01:24 +02:00
Gaël Métais 090a53b95a Killing a Phantomas process will not kill YLT anymore 2015-05-25 15:57:01 +02:00
Gaël Métais 22ab7515fb Redirect www. to non-www (for SEO) 2015-05-25 15:01:21 +02:00
Gaël Métais a97dcc5efb Change the tweet-this message 2015-05-25 14:35:39 +02:00
Gaël Métais 4b098aef69 Fix timeline bug, not being displayed 2015-05-25 14:04:27 +02:00
Gaël Métais cb46274288 New rule: jQuery calls on empty objects 2015-05-25 13:33:26 +02:00
Gaël Métais 1e60a43075 Don't count duplicated queries on elements not attached to DOM 2015-05-25 11:42:47 +02:00
Gaël Métais 1aa9cd032f Update dependencies 2015-05-25 09:22:45 +02:00
Gaël Métais 5e3748315c Remove jQueryNotDelegatedEvents offenders and send user to filtered timeline 2015-05-25 09:22:25 +02:00
Gaël Métais 7f67c032d1 fix tests 2015-05-25 08:30:02 +02:00
Gaël Métais 33621af3ad Replaced DOMqueriesWithoutResults by queriesWithoutResults to include jQuery queries 2015-05-22 18:54:32 +02:00
Gaël Métais 2fb4a7bd6a Replace rules DOMinserts, DOMqueries & eventsBound by DOMaccesses 2015-05-22 17:47:13 +02:00
Gaël Métais 1ff983a03b Add specific sub-filters on warnings in the timeline 2015-05-22 17:44:54 +02:00
Gaël Métais 701530ef04 Be cooler on global variables 2015-05-22 11:16:18 +02:00
Gaël Métais 719e624ddf Fix icons showing a background grid on IE 2015-05-22 11:09:29 +02:00
Gaël Métais 03a25df367 One webfont is ok 2015-05-22 10:57:39 +02:00
Gaël Métais c93ce684a8 Old jQuery major versions didn't have the trailing .0 in number 2015-05-22 10:52:12 +02:00
Gaël Métais 090708f38b Merge branch 'HipsterBrown-url-input' into develop 2015-05-22 10:06:27 +02:00
Gaël Métais 49f93e660e User agent sniffing 2015-05-22 10:05:41 +02:00
Nick Hehr 867680f4e9 Updates LaunchTest() form text input to be of type "url".
Making this change not only adds automatic url validation from modern
browsers but enabled special keyboards on mobile devices to make
inputing urls much easier.
2015-05-20 10:31:34 -04:00
Gaël Métais 36ac529cc8 Improve doc 2015-05-13 19:29:26 +02:00
Gaël Métais 0ae388f0f6 Improve doc 2015-05-13 19:23:12 +02:00
Gaël Métais b53ace765c Improve doc 2015-05-13 19:13:38 +02:00
Gaël Métais f256299a0c Improve doc 2015-05-13 18:48:33 +02:00
Gaël Métais 4fb05e406a New doc test 2015-05-13 18:21:31 +02:00
Gaël Métais 035639c68a Improve doc 2015-05-13 09:12:21 +02:00
Gaël Métais 09f53cac92 Add ngrok in README 2015-05-12 13:57:45 +02:00
Gaël Métais 05a27d9003 v1.5.1 2015-05-12 11:43:09 +02:00
Gaël Métais 5d4d1807ba v1.5.1 2015-05-12 11:42:50 +02:00
Gaël Métais 836e465414 Fix bug with DOMaccessesOnScroll offenders 2015-05-12 11:38:05 +02:00
Gaël Métais 94457b24cd Small wording adjustment 2015-05-12 10:30:28 +02:00
Gaël Métais 728aaec24d Merge pull request #77 from gmetais/develop
v1.5.0
2015-05-12 09:49:53 +02:00
Gaël Métais 4ccd2d7087 v1.5.0 2015-05-12 09:32:46 +02:00
Gaël Métais 9bc6d2928b New Events Not Delegated rule 2015-05-12 09:12:10 +02:00
Gaël Métais d7d4fb36ad Remove console log 2015-05-12 07:47:16 +02:00
Gaël Métais 4e6a88abec New jQuery usage rule 2015-05-11 19:43:15 +02:00
Gaël Métais a7051ace5d Add metrics queriesWithoutResults, jQueryCalls, jQueryCallsOnEmptyObject, jQueryNotDelegatedEvent 2015-05-11 18:28:11 +02:00
Gaël Métais cee47f2846 Remove temporarily domAccess from dashboard 2015-05-11 18:26:41 +02:00
Gaël Métais f9e60acdf6 Reduce timeout from 60s to 45 2015-05-11 17:16:05 +02:00
Gaël Métais 5f6ba048e6 Make the results API call smaller by removing toolsResults 2015-05-11 17:00:58 +02:00
Gaël Métais aba91de480 Add an exclude parameter to get result in API 2015-05-11 16:59:32 +02:00
Gaël Métais 118ec757a3 Update dependencies 2015-05-11 10:53:43 +02:00
Gaël Métais eaf712937b Merge pull request #76 from gmetais/screenshotError
Prevent fail when there's a screenshot error
2015-05-10 19:41:59 +02:00
Gaël Métais 6c47c406cb Prevent fail when there's a screenshot error 2015-05-10 14:51:36 +02:00
Gaël Métais eaeff3bf6a Display help if CLI is launched without an url 2015-05-07 23:16:13 +02:00
Gaël Métais 5b5fb3e32d Fix profiler details for jquery - on 2015-05-07 23:07:55 +02:00
Gaël Métais 07a8dd4912 Merge branch notGzipped 2015-05-07 19:56:31 +02:00
Gaël Métais a2f5127222 Merge pull request #75 from gmetais/notGzipped
New assetsNotGzipped rule
2015-05-07 19:56:14 +02:00
Gaël Métais eab48ea202 Fix screenshot bug when opeing an v1.3 result in 1.4 2015-05-07 19:51:43 +02:00
Gaël Métais 00e2717653 Rising scores on CSS Complexity and Bad CSS 2015-05-07 19:50:14 +02:00
Gaël Métais d83f150810 New assetsNotGzipped rule 2015-05-07 19:13:59 +02:00
Gaël Métais 2a7e6c2dd2 Merge pull request #74 from gmetais/scroll
New experimental "Scroll bottlenecks" rules
2015-05-07 17:53:55 +02:00
Gaël Métais f8f792645b Improve scroll bottlenecks rules 2015-05-07 17:10:01 +02:00
Gaël Métais 8c79f39389 Merge branch 'events' into scroll 2015-05-06 22:45:23 +02:00
Gaël Métais f7da653e87 New rules: eventsScrollBound & DOMaccessesOnScroll 2015-05-06 22:45:03 +02:00
Gaël Métais 1ed753eff1 Merge pull request #73 from gmetais/events
Fix events
2015-05-05 16:27:20 +02:00
Gaël Métais 385de967a4 Spy scrollBounds on document too 2015-05-05 13:29:20 +02:00
Gaël Métais ac84019777 Uncaught file name change 2015-05-04 19:09:10 +02:00
Gaël Métais b0c6fe9474 Merge pull request #72 from gmetais/cssParsing
Improve cssParsingError score and offenders
2015-05-04 18:56:26 +02:00
Gaël Métais 94e2b0233a Revert grunt-replace to v0.8.0 as 0.9.1 is broken 2015-05-04 18:36:25 +02:00
Gaël Métais c202e3426b Add a warning message when a entire stylesheet could not be parsed 2015-05-04 17:59:16 +02:00
Gaël Métais 7e86e83173 Change cssParsingError's weight and category 2015-05-04 17:20:01 +02:00
Gaël Métais 604857648b Easier to understand css offenders 2015-05-04 17:03:59 +02:00
Gaël Métais 76fb50473f Rename the rule jQuery versions loaded 2015-05-04 16:20:00 +02:00
Gaël Métais ffee3b225f Better parsing of cssParsingErrors' offenders 2015-05-04 16:16:33 +02:00
Gaël Métais 5f1dd42559 Update dependencies 2015-05-04 15:54:09 +02:00
Gaël Métais 57a7c78ce2 PhantomJS is not needed anymore for the build 2015-05-04 15:50:49 +02:00
Gaël Métais 33c6643388 Merge pull request #71 from gmetais/advancedSettings
Advanced settings
2015-05-04 15:49:15 +02:00
Gaël Métais 74deba61e6 Comment waitForSelector test while the feature is broken 2015-05-04 15:43:20 +02:00
Gaël Métais bb2f0e6c55 Add a cookie advanced option on the front page 2015-05-04 15:24:00 +02:00
Gaël Métais b2f6f68d1a Add new options to the front-end's home page 2015-04-30 22:18:27 +02:00
Gaël Métais fdde065a94 Merge pull request #70 from gmetais/develop
v1.4.1
2015-04-28 12:16:53 +02:00
Gaël Métais 9231b0914b Forgotten console.log 2015-04-28 08:03:45 +02:00
Gaël Métais 78f49ae7c9 v1.4.1 2015-04-27 23:36:31 +02:00
Gaël Métais 95ff81d81f Fix issues with icons after changing icon-font generator 2015-04-27 23:05:27 +02:00
Gaël Métais 92b73f62ad Fix problem with the latest LESS engine 2015-04-27 19:54:56 +02:00
Gaël Métais 1057836788 Merge pull request #69 from gmetais/develop
v1.4.0
2015-04-27 19:29:53 +02:00
Gaël Métais 93f8702962 v1.4.0 2015-04-27 19:17:13 +02:00
Gaël Métais 3d71454666 Merge pull request #68 from gmetais/mobile
Fixes the first part of #14.
2015-04-27 19:11:32 +02:00
Gaël Métais c0cc2744c6 Add a test 2015-04-27 18:53:18 +02:00
Gaël Métais 65b1c34d06 Add smartphone & tablet shapes arround screenshots 2015-04-27 18:45:08 +02:00
Gaël Métais 67615719ae Add device selection option to CLI 2015-04-27 18:43:26 +02:00
Gaël Métais 491c8ebe77 Add a device option to ylt's core 2015-04-27 18:43:04 +02:00
Gaël Métais cae23d61e2 Add a device selector on the launching page 2015-04-27 15:40:46 +02:00
Gaël Métais 9cad9ad2f9 Replace grunt-fontsmith by front-webfont + new icons 2015-04-27 14:05:36 +02:00
Gaël Métais ba977d6b49 Change submit button color on focus 2015-04-27 11:22:04 +02:00
Gaël Métais 88bb2f91a5 Change submit button color on focus 2015-04-27 11:19:25 +02:00
Gaël Métais dda11627f8 Update dependencies 2015-04-27 10:20:24 +02:00
Gaël Métais 2df713ca0c Make it clearer the project is open-source 2015-04-25 18:38:58 +02:00
Gaël Métais 486ede9396 Promote the Grunt plugin 2015-04-25 18:38:41 +02:00
Gaël Métais 72afcc3e5a Enabling spy when checking for hiddenContentSize 2015-04-25 18:08:28 +02:00
Gaël Métais ef2eaceb88 Remove evalCalls from policies (phantomas bug) 2015-04-25 18:03:56 +02:00
Gaël Métais c2db1c70e3 Fix .text considered reading when called with an empty string 2015-04-25 18:02:48 +02:00
Gaël Métais 13413899eb Merge pull request #62 from gmetais/develop
v1.3.2
2015-03-23 09:59:58 +01:00
Gaël Métais 98f074e8d5 jsHint 2015-03-23 09:49:55 +01:00
Gaël Métais 36a60bae4d v1.3.2 2015-03-20 12:01:10 +01:00
Gaël Métais 7c5a5daf2f Take in account trigger mode on some jQuery functions 2015-03-20 12:00:52 +01:00
Gaël Métais 7b74df7f04 Add spy on getComputedStyle() 2015-03-20 11:30:20 +01:00
Gaël Métais a6813a7ec1 Fix z-index issue in the profiler 2015-03-20 11:28:49 +01:00
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
122 changed files with 11580 additions and 1213 deletions
+4 -4
View File
@@ -1,8 +1,8 @@
language: node_js
node_js:
- "0.10.33"
- "0.12.4"
before_install:
- "npm install -g npm"
- "npm install -g grunt-cli"
- "npm install -g phantomjs"
install: npm install
before_script: grunt test
install:
- "npm install"
+36 -33
View File
@@ -11,17 +11,18 @@ module.exports = function(grunt) {
pkg: grunt.file.readJSON('package.json'),
settings: grunt.file.readJSON('./server_config/settings.json'),
font: {
webfont: {
icons: {
src: ['front/src/fonts/svg-icons/*.svg'],
destCss: 'front/src/less/icons.less',
destFonts: 'front/src/fonts/icons.woff',
// Optional: Custom routing of font filepaths for CSS
cssRouter: function (fontpath) {
var pathArray = fontpath.split('/');
var fileName = pathArray[pathArray.length - 1];
return '/fonts/' + fileName;
src: 'front/src/fonts/svg-icons/*.svg',
dest: 'tmp',
destCss: 'front/src/less',
options: {
engine: 'node',
types: 'woff',
stylesheet: 'less',
embed: true,
htmlDemo: false,
syntax: 'bootstrap'
}
}
},
@@ -66,7 +67,9 @@ module.exports = function(grunt) {
'app/nodeControllers/*.js',
'app/public/scripts/*.js',
'phantomas_custom/**/*.js',
'test/**/*.js',
'test/api/*.js',
'test/core/*.js',
'test/fixtures/*.js',
'front/src/js/**/*.js'
]
},
@@ -92,13 +95,14 @@ module.exports = function(grunt) {
},
coverage: {
files: [
{src: ['test/**'], dest: 'coverage/'},
{src: ['lib/metadata/**'], dest: 'coverage/'}
{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'},
]
@@ -139,7 +143,7 @@ module.exports = function(grunt) {
options: {
reporter: 'spec',
},
src: ['coverage/test/api/apiTest.js']
src: ['test/core/imageOptimizerTest.js']
},
coverage: {
options: {
@@ -154,7 +158,7 @@ module.exports = function(grunt) {
dev: {
NODE_ENV: 'development'
},
builded: {
built: {
NODE_ENV: 'production'
}
},
@@ -167,7 +171,7 @@ module.exports = function(grunt) {
showStack: true
}
},
builded: {
built: {
options: {
port: 8383,
server: './bin/server.js',
@@ -182,6 +186,13 @@ module.exports = function(grunt) {
showStack: true
}
},
'test-current-work': {
options: {
port: 8387,
server: './bin/server.js',
showStack: true
}
},
testSuite: {
options: {
port: 8388,
@@ -200,16 +211,14 @@ module.exports = function(grunt) {
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']]
}
assetsDirs: ['front/build']
}
},
htmlmin: {
options: {
removeComments: true,
collapseWhitespace: true
collapseWhitespace: true,
conservativeCollapse: true
},
main: {
files: [{
@@ -282,7 +291,7 @@ module.exports = function(grunt) {
grunt.registerTask('icons', [
'font:icons',
'webfont:icons',
'less',
'clean:tmp'
]);
@@ -314,14 +323,13 @@ module.exports = function(grunt) {
'express:dev'
]);
grunt.registerTask('builded', [
'env:builded',
'express:builded'
grunt.registerTask('built', [
'env:built',
'express:built'
]);
grunt.registerTask('test', [
'build',
'jshint',
'express:testSuite',
'clean:coverage',
'copy-test-server-settings',
@@ -336,16 +344,11 @@ module.exports = function(grunt) {
]);
grunt.registerTask('test-current-work', [
'build',
'jshint',
'express:testSuite',
'clean:coverage',
'copy-test-server-settings',
'lineremover:beforeCoverage',
'copy:beforeCoverage',
'blanket',
'copy:coverage',
'express:test',
'express:test-current-work',
'mochaTest:test-current-work',
'clean:tmp'
]);
+59 -24
View File
@@ -1,36 +1,73 @@
# Yellow Lab Tools
<img src="./doc/img/logo.png" alt="Yellow Lab Tools"></img>
Online tool that lets you test a webpage and detects **performance** and **front-end code quality** issues.
[![Dependency Status](https://gemnasium.com/gmetais/YellowLabTools.svg)](https://gemnasium.com/gmetais/YellowLabTools) [![Build Status](https://travis-ci.org/gmetais/YellowLabTools.svg?branch=master)](https://travis-ci.org/gmetais/YellowLabTools)
Analyzes a webpage and detects **performance** or **front-end code quality** issues. Easy to launch, easy to understand, in-depth details.
### Access the tool here: [http://yellowlab.tools](http://yellowlab.tools)
#
<img src="./doc/img/commandment.png" alt="No website should go live without being tested with Yellow Lab Tools"></img>
<table>
<tr>
<td width="70%">
The <b>online tool</b> (recommended):
<a href="http://yellowlab.tools" target="_blank">http://yellowlab.tools</a>
</td>
<td width="30%">
<img src="./doc/img/YLT-animated.gif"></img>
</td>
</tr>
<tr>
<td width="70%">
The <b>CLI</b> (Command Line Interface) - <a href="https://github.com/gmetais/YellowLabTools/wiki/Command-Line-Interface" target="_blank">Doc here</a>
</td>
<td width="30%">
<img src="./doc/img/YLT-cli-animated.gif"></img>
</td>
</tr>
<tr>
<td width="70%">
The <b>Grunt task</b>: <a href="https://github.com/gmetais/grunt-yellowlabtools" traget="_blank">gmetais/grunt-yellowlabtools</a>
<br>For developers or Continuous Integration
</td>
<td width="30%">
<img src="./doc/img/grunt-logo.png"></img>
</td>
</tr>
<tr>
<td width="70%">
The <b>NodeJS</b> package - <a href="https://github.com/gmetais/YellowLabTools/wiki/NodeJS-module" target="_blank">Doc here</a>
</td>
<td width="30%">
<img src="./doc/img/npm-logo.png"></img>
</td>
</tr>
<tr>
<td width="70%">
The <b>public API</b> - <a href="https://github.com/gmetais/YellowLabTools/wiki/Public-API" target="_blank">Doc here</a>
<br>
Allows you to launch runs from any language
</td>
<td width="30%">
<img src="./doc/img/api-logo.png"></img>
</td>
</tr>
</table>
![example dashboard screenshot](screenshot.png)
## How it works
The tool loads the given URL via [PhantomasJS](http://phantomjs.org/) (a headless browser) and collects various metrics and statistics with the help of [Phantomas](https://github.com/macbre/phantomas). These metrics are categorized and transformed into scores. It also gives in-depth details so developpers can correct the detected issues.
The tool loads the given URL via [PhantomJS](http://phantomjs.org/) (a headless browser) and collects various metrics and statistics with the help of [Phantomas](https://github.com/macbre/phantomas). These metrics are then categorized and transformed into scores. It also gives in-depth details so developpers can fix the detected issues.
By the way, it's free because we are geeks, not businessmen. All we want is a ★ on GitHub, it will boost our motivation to add more awesome features!!!
![example dashboard screenshot](./doc/img/screenshot.png)
## Different ways to use YLT
#### The web interface:
This is the best way to discover the tool: [http://yellowlab.tools](http://yellowlab.tools).
Please note the server is hosted in Baltimore, USA.
## Test your localhost
#### The Command Line Client:
Might be useful if you want to build an automation tool. The documentation is [here](https://github.com/gmetais/YellowLabTools/wiki/Command-Line-Interface).
#### The NPM module:
Can be used to build automation tools in NodeJS. The documentation is [here](https://github.com/gmetais/YellowLabTools/wiki/NodeJS-module).
#### The Public API:
Hosted on our http://yellowlab.tools server, it is a RESTful API that allows you to launch distant tests. The documentation is [here](https://github.com/gmetais/YellowLabTools/wiki/Public-API).
You can use [ngrok](https://ngrok.com/), a tool that creates a secure tunnel between your localhost and the online tool (or the public API). You can also use the CLI or the Grunt tasks as they run on your machine.
## Install your own private instance
@@ -38,18 +75,16 @@ Hosted on our http://yellowlab.tools server, it is a RESTful API that allows you
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
## Help needed!
There are so many things left to do, **your help would be greatly appreciated**! Please report bugs, ask for evolutions and come code with me.
## License
Please read the [license](LICENSE).
Icons are the property of [IcoMoon.io](https://icomoon.io/)
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
+63 -8
View File
@@ -1,22 +1,77 @@
#!/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:',
' --device Use "phone" or "tablet" to simulate a mobile device (by user-agent and viewport size).',
' --screenshot Will take a screenshot and use this value as the output path. It needs to end with ".png".',
' --js-deep-analysis When activated, the javascriptExecutionTree will contain sub-requests.',
' --wait-for-selector Once the page is loaded, Phantomas will wait until the given CSS selector matches some elements.',
' --cookie Adds a cookie on the main domain.',
' --auth-user Basic HTTP authentication username.',
' --auth-pass Basic HTTP authentication password.',
''
].join('\n'),
pkg: '../package.json'
});
// Check parameters
if (process.argv.length !== 3) {
console.error('Incorrect parameters');
console.error('\nUsage: ylt <pageUrl>\n');
process.exit(1);
if (cli.input.length < 1) {
cli.showHelp();
}
var url = process.argv[2];
var url = cli.input[0];
(function execute(url) {
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;
}
// Device simulation
options.device = cli.flags.device || 'desktop';
// Wait for CSS selector
options.waitForSelector = cli.flags.waitForSelector || null;
// Cookie
options.cookie = cli.flags.cookie || null;
// HTTP basic auth
options.authUser = cli.flags.authUser || null;
options.authPass = cli.flags.authPass || null;
(function execute(url, options) {
'use strict';
ylt(url).
ylt(url, options).
then(function(data) {
@@ -32,4 +87,4 @@ var url = process.argv[2];
debug('Test launched...');
})(url);
})(url, options);
+2
View File
@@ -7,12 +7,14 @@ var cors = require('cors');
var authMiddleware = require('../lib/server/middlewares/authMiddleware');
var apiLimitsMiddleware = require('../lib/server/middlewares/apiLimitsMiddleware');
var wwwRedirectMiddleware = require('../lib/server/middlewares/wwwRedirectMiddleware');
// Middlewares
app.use(compress());
app.use(bodyParser.json());
app.use(cors());
app.use(wwwRedirectMiddleware);
app.use(authMiddleware);
app.use(apiLimitsMiddleware);
+8 -3
View File
@@ -1,8 +1,13 @@
{
"name": "yellowlabtools",
"dependencies": {
"angular": "~1.3.8",
"angular-route": "~1.3.8",
"angular-resource": "~1.3.7"
"angular": "1.3.15",
"angular-route": "1.3.15",
"angular-resource": "1.3.15",
"angular-sanitize": "1.3.15",
"angular-animate": "1.3.15",
"angular-local-storage": "0.2.2",
"angular-chart.js": "0.7.1",
"Chart.js": "~1.0.2"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

+33 -13
View File
@@ -5,7 +5,14 @@
text-align: center;
}
.summary .globalScore {
margin-bottom: 3em;
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;
@@ -22,6 +29,31 @@
font-weight: bold;
margin: 0.5em 0 1em;
}
.summary .globalScore .screenshotWrapper:hover {
opacity: 0.75;
}
.summary .globalScore .screenshotWrapper:hover:after {
position: absolute;
width: 1.25em;
height: 1.25em;
top: 0.7em;
left: 1.55em;
font-size: 3em;
color: #FFF;
background: #000;
border-radius: 0.2em;
text-align: center;
content: "+";
opacity: 0.85;
}
.summary .globalScore .screenshotWrapper.phone:hover:after {
top: 1.7em;
left: 0.64em;
}
.summary .globalScore .screenshotWrapper.tablet:hover:after {
top: 1.5em;
left: 0.9em;
}
.summary .notations {
display: table;
width: 80%;
@@ -122,18 +154,6 @@
.summary .fromShare {
margin-bottom: 3em;
}
.summary .fromShare a {
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;
}
.summary .apiTip {
font-size: 0.8em;
margin-bottom: 4em;
File diff suppressed because one or more lines are too long
+142 -2
View File
@@ -1,18 +1,158 @@
.promess {
padding: 0em 2em 3em;
padding: 0em 2em;
margin-bottom: 0.5em;
font-weight: normal;
font-size: 1.2em;
}
.price {
padding: 0em 2em 3em;
margin-top: 0em;
font-size: 0.9em;
}
.url {
width: 50%;
}
.launchBtn {
background: #e74c3c;
background: #ffa319;
color: #fff;
}
.launchBtn:focus {
background: #e74c3c;
}
.launchBtn.disabled {
background: #deaca6;
}
.launchBtn.disabled:focus {
color: #ddd;
}
.settings {
width: 50%;
margin: 0 auto;
}
.settings input,
.settings select {
font-size: 1em;
}
.settings input[type=text] {
width: 100%;
min-width: 4em;
}
.device {
margin-top: 3em;
}
.device .item {
display: inline-block;
margin: 1em 0.75em;
width: 5.5em;
height: 5.5em;
color: #FFF;
border: 1px solid #FFF;
padding: 1px;
border-radius: 0.5em;
cursor: pointer;
text-decoration: none;
font-size: 0.8em;
}
.device .item.active {
color: #ffa319;
border: 2px solid #ffa319;
padding: 0;
}
.device .item:hover {
color: #ffa319;
}
.device .item > div {
margin: 0.2em 0 0.1em;
font-size: 3em;
}
.settingsTooltip {
position: relative;
}
.settingsTooltip span {
font-size: 0.8em;
vertical-align: text-top;
}
.settingsTooltip div {
display: none;
position: absolute;
padding: 0.5em;
width: 25em;
background: #FFF;
color: #000;
font-size: 0.8em;
border-radius: 1em;
border: 2px solid #ffa319;
white-space: normal;
z-index: 2;
}
.settingsTooltip:hover div {
display: block;
}
.showAdvanced {
display: inline-block;
margin-top: 2em;
color: #FFF;
text-decoration: none;
font-size: 0.9em;
}
.showAdvanced:hover {
color: #ffa319;
}
.currentSettings {
font-size: 0.9em;
}
.currentSettings span {
color: #ffa319;
}
.currentSettings span:after {
color: #FFF;
content: ",";
}
.currentSettings span:last-child:after {
content: "";
}
.advanced {
margin: 1em 0 0;
display: table;
width: 100%;
text-align: left;
border-spacing: 0.75em;
}
.advanced > div {
display: table-row;
}
.advanced > div > div {
display: table-cell;
width: 75%;
}
.advanced > div > div.label {
width: 25%;
white-space: nowrap;
}
.advanced .subTable {
display: table;
border-spacing: 0;
width: 100%;
}
.advanced .subTable > div {
display: table-row;
}
.advanced .subTable > div > div {
display: table-cell;
padding: 0 0 0.75em;
}
.features {
display: table;
width: 50%;
margin: 6em auto 0;
font-size: 0.9em;
color: #413;
}
.features > div {
width: 33.3%;
display: table-cell;
padding: 0 1.5em;
}
input[type=submit],
input.url {
padding: 0 0.5em;
+171 -93
View File
File diff suppressed because one or more lines are too long
-3
View File
@@ -6,6 +6,3 @@
font-size: 0.8em;
margin-bottom: 6em;
}
.queueLink {
color: #FFF;
}
+160
View File
@@ -82,3 +82,163 @@
font-size: 3em;
margin-bottom: 1em;
}
.rule .startTime {
display: none;
}
.offendersTable .offenderButton,
.value .offenderButton {
display: inline-block;
position: relative;
background: #efe;
padding: 0 0.5em;
margin: 0.2em 0;
border-radius: 0.4em;
z-index: 1;
}
.offendersTable .offenderButton.opens,
.value .offenderButton.opens {
padding-right: 0.75em;
}
.offendersTable .offenderButton.opens:after,
.value .offenderButton.opens:after {
position: relative;
left: 0.5em;
content: '\25BC';
font-size: 0.8em;
}
.offendersTable .offenderButton > div,
.value .offenderButton > div {
display: none;
position: absolute;
right: 0;
min-width: 100%;
background: inherit;
border-bottom-left-radius: 0.4em;
border-bottom-right-radius: 0.4em;
border-top: 1px solid #999;
z-index: 2;
}
.offendersTable .offenderButton .domTree,
.value .offenderButton .domTree {
text-align: left;
white-space: nowrap;
}
.offendersTable .offenderButton .domTree > div,
.value .offenderButton .domTree > div {
margin: 0.5em;
}
.offendersTable .offenderButton .domTree > div div,
.value .offenderButton .domTree > div div {
margin-left: 1em;
}
.offendersTable .offenderButton .backtrace,
.value .offenderButton .backtrace,
.offendersTable .offenderButton .cssFileAndLine,
.value .offenderButton .cssFileAndLine {
white-space: nowrap;
padding: 0.5em;
}
.offendersTable .offenderButton.opens:hover,
.value .offenderButton.opens:hover {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
background: #ffe0cc;
z-index: 2;
}
.offendersTable .offenderButton.opens:hover > div,
.value .offenderButton.opens:hover > div {
display: block;
background: #ffe0cc;
}
.offendersHtml {
display: inline-block;
}
.domTree div {
text-align: left;
margin-left: 1em;
}
.domTree div span:only-child {
font-weight: bold;
}
.domTree div span:only-child span {
font-style: italic;
font-weight: normal;
}
.checker {
/* Checkerboard background */
background-color: #ddd;
background-image: linear-gradient(45deg, #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 {
width: 30em;
border: 2px solid #000;
text-align: left;
}
.colorPalette > div {
display: inline-block;
height: 2em;
position: relative;
}
.colorPalette > div div {
display: none;
position: absolute;
left: 100%;
top: 100%;
background: #FFF;
padding: 0.5em;
border: 2px solid #f1c40f;
border-radius: 0.5em;
white-space: nowrap;
z-index: 3;
font-weight: bold;
}
.colorPalette > div:hover div {
display: block;
}
.colorPalette > div:hover:after {
content: " ";
position: absolute;
left: -0.2em;
top: -0.2em;
width: 100%;
height: 100%;
z-index: 2;
border: 0.2em solid #f1c40f;
}
.totalWeightPie {
max-width: 39em;
margin: 2em auto 4em;
}
.totalWeightPie canvas {
max-width: inherit;
}
.hugeFile {
font-weight: bold;
color: #e74c3c;
}
.imageOffenders {
display: table;
border-spacing: 3em;
width: 90%;
}
.imageOffenders > div {
display: table-row;
}
.imageOffenders > div > div {
display: table-cell;
vertical-align: middle;
}
.imageOffenders img {
max-height: 10em;
max-width: 40em;
border: 1px solid #000;
margin-top: 0.5em;
}
.smallPreview {
max-height: 1.6em;
max-width: 4em;
border: 1px solid #000;
margin-top: 0.2em;
}
+7
View File
@@ -0,0 +1,7 @@
.screenshot.board {
text-align: center;
}
.screenshot .screenshotWrapper {
font-size: 2.08333333333333em;
margin-bottom: 0.5em;
}
+101 -14
View File
@@ -2,6 +2,19 @@
.execution {
text-align: center;
}
.selectScript {
padding-bottom: 2em;
font-size: 0.9em;
}
.selectScript select {
max-width: 30em;
}
.selectScript.empty {
font-size: 0.8em;
}
.selectScript.empty select {
width: 10em;
}
.timeline {
margin: 2em 0 5em;
}
@@ -39,6 +52,9 @@
bottom: 0;
width: 100%;
}
.timeline .interval .color.clickable {
cursor: pointer;
}
.timeline div.interval:hover {
background: #9C4274;
}
@@ -127,19 +143,9 @@
border: 1px dotted #aaa;
text-align: left;
}
.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;
.subFilters {
margin-left: 3em;
font-size: 0.9em;
}
.table {
display: table;
@@ -171,12 +177,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: 2;
}
.table .showingDetails .child span:hover {
background: inherit;
}
.table .showingDetails .child span:hover .childArgs {
display: none;
}
.table > div > .value {
width: 70%;
word-break: break-all;
@@ -193,6 +252,7 @@ input.textFilter {
width: 0.8em;
}
.detailsOverlay {
display: none;
position: absolute;
right: 3em;
top: -3em;
@@ -202,13 +262,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;
@@ -247,3 +310,27 @@ input.textFilter {
color: #e74c3c;
cursor: pointer;
}
.queryWithoutResultsFilterOn > div {
display: none;
}
.queryWithoutResultsFilterOn > div.queryWithoutResults {
display: table-row;
}
.jQueryCallOnEmptyObjectFilterOn > div {
display: none;
}
.jQueryCallOnEmptyObjectFilterOn > div.jQueryCallOnEmptyObject {
display: table-row;
}
.eventNotDelegatedFilterOn > div {
display: none;
}
.eventNotDelegatedFilterOn > div.eventNotDelegated {
display: table-row;
}
.jsErrorFilterOn > div {
display: none;
}
.jsErrorFilterOn > div.jsError {
display: table-row;
}
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M 256.00,0.00C 397.385,0.00, 512.00,114.615, 512.00,256.00s -114.615,256.00, -256.00,256.00s -256.00-114.615, -256.00-256.00S 114.615,0.00, 256.00,0.00z M 256.00,464.00 c 114.875,0.00 208.00-93.125 208.00-208.00S 370.875,48.00, 256.00,48.00s -208.00,93.125, -208.00,208.00S 141.125,464.00, 256.00,464.00zM 105.372,233.373l 128.001-128.00c 12.496-12.497 32.757-12.497 45.254,0.00c 12.497,12.497 12.497,32.758,0.00,45.255L 205.255,224.00 L 384.00,224.00 c 17.673,0.00 32.00,14.327 32.00,32.00c0.00,17.673, -14.327,32.00, -32.00,32.00l-178.745,0.00 l 73.373,73.372c 12.497,12.497 12.497,32.759,0.00,45.256 C 272.379,412.876, 264.189,416.00, 256.00,416.00s -16.379-3.124, -22.627-9.372l -128.001-128.00C 92.876,266.131, 92.876,245.869, 105.372,233.373z" ></path></svg>
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M 256.00,0.00C 397.385,0.00, 512.00,114.615, 512.00,256.00s -114.615,256.00, -256.00,256.00s -256.00-114.615, -256.00-256.00S 114.615,0.00, 256.00,0.00z M 256.00,464.00 c 114.875,0.00 208.00-93.125 208.00-208.00S 370.875,48.00, 256.00,48.00s -208.00,93.125, -208.00,208.00S 141.125,464.00, 256.00,464.00zM 105.372,233.373l 128.001-128.00c 12.496-12.497 32.757-12.497 45.254,0.00c 12.497,12.497 12.497,32.758,0.00,45.255L 205.255,224.00 L 384.00,224.00 c 17.673,0.00 32.00,14.327 32.00,32.00c0.00,17.673, -14.327,32.00, -32.00,32.00l-178.745,0.00 l 73.373,73.372c 12.497,12.497 12.497,32.759,0.00,45.256 C 272.379,412.876, 264.189,416.00, 256.00,416.00s -16.379-3.124, -22.627-9.372l -128.001-128.00C 92.876,266.131, 92.876,245.869, 105.372,233.373z" ></path></svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

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

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 634 B

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

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 792 B

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

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 679 B

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

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 875 B

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

After

Width:  |  Height:  |  Size: 851 B

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

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

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

After

Width:  |  Height:  |  Size: 584 B

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

After

Width:  |  Height:  |  Size: 774 B

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

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 887 B

+24 -3
View File
@@ -1,20 +1,38 @@
var yltApp = angular.module('YellowLabTools', [
'ngRoute',
'ngSanitize',
'ngAnimate',
'indexCtrl',
'aboutCtrl',
'dashboardCtrl',
'queueCtrl',
'ruleCtrl',
'screenshotCtrl',
'timelineCtrl',
'runsFactory',
'resultsFactory',
'apiService',
'menuService',
'settingsService',
'gradeDirective',
'offendersDirectives',
'LocalStorageModule'
]);
yltApp.run(['$rootScope', '$location', function($rootScope, $location) {
$rootScope.isTouchDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(window.navigator.userAgent);
$rootScope.loadedRunId = null;
var oldHash;
// We don't want the hash to be kept between two pages
$rootScope.$on('$locationChangeStart', function(param1, param2, param3, param4){
var newHash = $location.hash();
if (newHash === oldHash) {
$location.hash(null);
}
oldHash = newHash;
});
// Google Analytics
$rootScope.$on('$routeChangeSuccess', function(){
ga('send', 'pageview', {'page': $location.path()});
@@ -33,8 +51,7 @@ yltApp.config(['$routeProvider', '$locationProvider',
controller: 'QueueCtrl'
}).
when('/about', {
templateUrl: 'views/about.html',
controller: 'AboutCtrl'
templateUrl: 'views/about.html'
}).
when('/result/:runId', {
templateUrl: 'views/dashboard.html',
@@ -44,6 +61,10 @@ yltApp.config(['$routeProvider', '$locationProvider',
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'
-5
View File
@@ -1,5 +0,0 @@
var aboutCtrl = angular.module('aboutCtrl', []);
aboutCtrl.controller('AboutCtrl', ['$scope', function($scope) {
$scope.about = "this is about YLT";
}]);
+8 -17
View File
@@ -1,6 +1,6 @@
var dashboardCtrl = angular.module('dashboardCtrl', ['resultsFactory', 'menuService']);
dashboardCtrl.controller('DashboardCtrl', ['$scope', '$rootScope', '$routeParams', '$location', 'Results', 'Runs', 'Menu', function($scope, $rootScope, $routeParams, $location, Results, Runs, Menu) {
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;
@@ -8,7 +8,7 @@ dashboardCtrl.controller('DashboardCtrl', ['$scope', '$rootScope', '$routeParams
function loadResults() {
// Load result if needed
if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
Results.get({runId: $routeParams.runId}, function(result) {
Results.get({runId: $routeParams.runId, exclude: 'toolsResults'}, function(result) {
$rootScope.loadedResult = result;
$scope.result = result;
init();
@@ -26,7 +26,8 @@ dashboardCtrl.controller('DashboardCtrl', ['$scope', '$rootScope', '$routeParams
$scope.categoriesOrder = Object.keys($scope.result.scoreProfiles.generic.categories);
$scope.globalScore = Math.max($scope.result.scoreProfiles.generic.globalScore, 0);
$scope.tweetText = 'My website\'s score is ' + $scope.globalScore + '/100 on #YellowLabTools!';
$scope.tweetText = 'I\'ve discovered this cool open-source tool that audits the front-end quality of a web page: ';
}
$scope.showRulePage = function(ruleName) {
@@ -34,26 +35,21 @@ dashboardCtrl.controller('DashboardCtrl', ['$scope', '$rootScope', '$routeParams
};
$scope.testAgain = function() {
Runs.save({
url: $scope.result.params.url,
waitForResponse: false
}, function(data) {
$location.path('/queue/' + data.runId);
});
API.relaunchTest($scope.result);
};
/// When comming from a social shared link, the user needs to click on "See full report" button to display the full dashboard.
// When comming from a social shared link, the user needs to click on "See full report" button to display the full dashboard.
$scope.seeFullReport = function() {
$scope.fromSocialShare = false;
$location.search({});
};
$scope.shareOnTwitter = function(message) {
openSocialPopup('https://twitter.com/intent/tweet?url=' + document.URL + '%3Fshare&text=' + encodeURIComponent(message));
openSocialPopup('https://twitter.com/intent/tweet?text=' + encodeURIComponent(message + 'http://yellowlab.tools'));
};
$scope.shareOnLinkedin = function(message) {
openSocialPopup('https://www.linkedin.com/shareArticle?mini=true&url=' + document.URL + '%3Fshare&title=' + encodeURIComponent(message) + '&summary=' + encodeURIComponent('YellowLabTools is a free online tool that analyzes performance and front-end quality of a webpage.'));
openSocialPopup('https://www.linkedin.com/shareArticle?mini=true&url=http://yellowlab.tools&title=' + encodeURIComponent(message) + '&summary=' + encodeURIComponent('YellowLabTools is a free online tool that analyzes performance and front-end quality of a webpage.'));
};
function openSocialPopup(url) {
@@ -64,10 +60,5 @@ dashboardCtrl.controller('DashboardCtrl', ['$scope', '$rootScope', '$routeParams
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();
}]);
+6 -8
View File
@@ -1,15 +1,13 @@
var indexCtrl = angular.module('indexCtrl', []);
indexCtrl.controller('IndexCtrl', ['$scope', '$location', 'Runs', function($scope, $location, Runs) {
indexCtrl.controller('IndexCtrl', ['$scope', 'Settings', 'API', function($scope, Settings, API) {
$scope.settings = Settings.getMergedSettings();
$scope.launchTest = function() {
if ($scope.url) {
Runs.save({
url: $scope.url,
waitForResponse: false
}, function(data) {
$location.path('/queue/' + data.runId);
});
Settings.saveSettings($scope.settings);
API.launchTest($scope.url, $scope.settings);
}
};
}]);
+13 -1
View File
@@ -1,6 +1,6 @@
var queueCtrl = angular.module('queueCtrl', ['runsFactory']);
queueCtrl.controller('QueueCtrl', ['$scope', '$routeParams', '$location', 'Runs', function($scope, $routeParams, $location, Runs) {
queueCtrl.controller('QueueCtrl', ['$scope', '$routeParams', '$location', 'Runs', 'API', function($scope, $routeParams, $location, Runs, API) {
$scope.runId = $routeParams.runId;
var numberOfTries = 0;
@@ -9,6 +9,8 @@ queueCtrl.controller('QueueCtrl', ['$scope', '$routeParams', '$location', 'Runs'
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 ++;
@@ -21,6 +23,16 @@ queueCtrl.controller('QueueCtrl', ['$scope', '$routeParams', '$location', 'Runs'
} 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;
}
});
}
+30 -10
View File
@@ -1,6 +1,15 @@
var ruleCtrl = angular.module('ruleCtrl', []);
var ruleCtrl = angular.module('ruleCtrl', ['chart.js']);
ruleCtrl.controller('RuleCtrl', ['$scope', '$rootScope', '$routeParams', '$location', '$sce', 'Menu', 'Results', 'Runs', function($scope, $rootScope, $routeParams, $location, $sce, Menu, Results, Runs) {
ruleCtrl.config(['ChartJsProvider', function (ChartJsProvider) {
// Configure all charts
ChartJsProvider.setOptions({
animation: false,
colours: ['#FF5252', '#FF8A80'],
responsive: true
});
}]);
ruleCtrl.controller('RuleCtrl', ['$scope', '$rootScope', '$routeParams', '$location', '$sce', 'Menu', 'Results', 'API', function($scope, $rootScope, $routeParams, $location, $sce, Menu, Results, API) {
$scope.runId = $routeParams.runId;
$scope.policyName = $routeParams.policy;
$scope.Menu = Menu.setCurrentPage(null, $scope.runId);
@@ -9,7 +18,7 @@ ruleCtrl.controller('RuleCtrl', ['$scope', '$rootScope', '$routeParams', '$locat
function loadResults() {
// Load result if needed
if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
Results.get({runId: $routeParams.runId}, function(result) {
Results.get({runId: $routeParams.runId, exclude: 'toolsResults'}, function(result) {
$rootScope.loadedResult = result;
$scope.result = result;
init();
@@ -22,7 +31,23 @@ ruleCtrl.controller('RuleCtrl', ['$scope', '$rootScope', '$routeParams', '$locat
function init() {
$scope.rule = $scope.result.rules[$scope.policyName];
$scope.message = $sce.trustAsHtml($scope.rule.policy.message);
// Init "Total Weight" chart
if ($scope.policyName === 'totalWeight') {
$scope.weightLabels = [];
$scope.weightColours = ['#7ECCCC', '#A7E846', '#FF944D', '#FFE74A', '#C2A3FF', '#5A9AED', '#FF6452', '#C1C1C1'];
$scope.weightData = [];
var types = ['html', 'css', 'js', 'json', 'image', 'video', 'webfont', 'other'];
types.forEach(function(type) {
$scope.weightLabels.push(type);
$scope.weightData.push(Math.round($scope.rule.offendersObj.list.byType[type].totalWeight / 1024));
});
$scope.weightOptions = {
tooltipTemplate: '<%=label%>: <%=value%> KB'
};
}
}
$scope.backToDashboard = function() {
@@ -30,12 +55,7 @@ ruleCtrl.controller('RuleCtrl', ['$scope', '$rootScope', '$routeParams', '$locat
};
$scope.testAgain = function() {
Runs.save({
url: $scope.result.params.url,
waitForResponse: false
}, function(data) {
$location.path('/queue/' + data.runId);
});
API.relaunchTest($scope.result);
};
loadResults();
@@ -0,0 +1,30 @@
var screenshotCtrl = angular.module('screenshotCtrl', ['resultsFactory', 'menuService']);
screenshotCtrl.controller('ScreenshotCtrl', ['$scope', '$rootScope', '$routeParams', '$location', 'Results', 'API', 'Menu', function($scope, $rootScope, $routeParams, $location, Results, API, Menu) {
$scope.runId = $routeParams.runId;
$scope.Menu = Menu.setCurrentPage(null, $scope.runId);
function loadResults() {
// Load result if needed
if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
Results.get({runId: $routeParams.runId, exclude: 'toolsResults'}, function(result) {
$rootScope.loadedResult = result;
$scope.result = result;
}, function(err) {
$scope.error = true;
});
} else {
$scope.result = $rootScope.loadedResult;
}
}
$scope.backToDashboard = function() {
$location.path('/result/' + $scope.runId);
};
$scope.testAgain = function() {
API.relaunchTest($scope.result);
};
loadResults();
}]);
+99 -54
View File
@@ -1,13 +1,13 @@
var timelineCtrl = angular.module('timelineCtrl', []);
timelineCtrl.controller('TimelineCtrl', ['$scope', '$rootScope', '$routeParams', '$location', '$timeout', 'Menu', 'Results', 'Runs', function($scope, $rootScope, $routeParams, $location, $timeout, Menu, Results, Runs) {
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) {
Results.get({runId: $routeParams.runId, exclude: 'toolsResults'}, function(result) {
$rootScope.loadedResult = result;
$scope.result = result;
render();
@@ -19,19 +19,70 @@ timelineCtrl.controller('TimelineCtrl', ['$scope', '$rootScope', '$routeParams',
}
function render() {
initFilters();
initScriptFiltering();
initExecutionTree();
initTimeline();
$timeout(initProfiler, 100);
}
function initFilters() {
var hash = $location.hash();
var filter = null;
if (hash.indexOf('filter=') === 0) {
filter = hash.substr(7);
}
$scope.warningsFilterOn = (filter !== null);
$scope.warningsFilters = {
queryWithoutResults: (filter === null || filter === 'queryWithoutResults'),
jQueryCallOnEmptyObject: (filter === null || filter === 'jQueryCallOnEmptyObject'),
eventNotDelegated: (filter === null || filter === 'eventNotDelegated'),
jsError: (filter === null || filter === 'jsError')
};
}
function initScriptFiltering() {
var offenders = $scope.result.rules.totalRequests.offendersObj.list.byType.js;
$scope.scripts = [];
offenders.forEach(function(filePath) {
var shortPath = filePath;
if (filePath.length > 100) {
shortPath = filePath.substr(0, 98) + '...';
}
var scriptObj = {
fullPath: filePath,
shortPath: shortPath
};
$scope.scripts.push(scriptObj);
});
}
function initExecutionTree() {
var originalExecutions = $scope.result.javascriptExecutionTree.children || [];
// Detect the last event of all (before filtering) and read time
var lastEvent = originalExecutions[originalExecutions.length - 1];
$scope.endTime = lastEvent.data.timestamp + (lastEvent.data.time || 0);
// Filter
$scope.executionTree = [];
originalExecutions.forEach(function(node) {
// Prepare a faster angular search by creating a kind of search index
node.searchIndex = (node.data.callDetails) ? [node.data.type].concat(node.data.callDetails.arguments).join('°°') : node.data.type;
// 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);
});
@@ -41,8 +92,6 @@ timelineCtrl.controller('TimelineCtrl', ['$scope', '$rootScope', '$routeParams',
// Split the timeline into 200 intervals
var numberOfIntervals = 199;
var lastEvent = $scope.executionTree[$scope.executionTree.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
@@ -80,66 +129,62 @@ timelineCtrl.controller('TimelineCtrl', ['$scope', '$rootScope', '$routeParams',
$scope.profilerData = $scope.executionTree;
}
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;
}
$scope.filter = function(textFilter, scriptName) {
$scope.changeScript = function() {
initExecutionTree();
initTimeline();
initProfiler();
};
$scope.onNodeDetailsClick = function(node) {
var isOpen = node.showDetails;
if (!isOpen) {
// Close all other nodes
$scope.executionTree.forEach(function(currentNode) {
currentNode.showDetails = false;
});
$scope.findLineIndexByTimestamp = function(timestamp) {
var lineIndex = 0;
// Parse the backtrace
if (!node.parsedBacktrace) {
node.parsedBacktrace = parseBacktrace(node.data.backtrace);
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;
}
}
node.showDetails = !isOpen;
return lineIndex;
};
$scope.backToDashboard = function() {
$location.path('/result/' + $scope.runId);
};
$scope.testAgain = function() {
Runs.save({
url: $scope.result.params.url,
waitForResponse: false
}, function(data) {
$location.path('/queue/' + data.runId);
});
API.relaunchTest($scope.result);
};
loadResults();
}]);
}]);
timelineCtrl.directive('scrollOnClick', ['$animate', '$timeout', function($animate, $timeout) {
return {
restrict: 'A',
link: function (scope, element, attributes) {
// When the user clicks on the timeline, find the right profiler line and scroll to it
element.on('click', function() {
var lineIndex = scope.findLineIndexByTimestamp(attributes.scrollOnClick);
var lineElement = angular.element(document.getElementById('line_' + lineIndex));
// Animate the background color to "flash" the row
lineElement.addClass('highlight');
$timeout(function() {
$animate.removeClass(lineElement, 'highlight');
scope.$digest();
}, 50);
window.scrollTo(0, lineElement[0].offsetTop);
});
}
};
}]);
@@ -0,0 +1,889 @@
(function() {
"use strict";
var offendersDirectives = angular.module('offendersDirectives', []);
function getdomTreeHTML(tree) {
return '<div class="domTree">' + getdomTreeInnerHTML(tree) + '</div>';
}
function getdomTreeInnerHTML(tree) {
return recursiveHtmlBuilder(tree);
}
function recursiveHtmlBuilder(tree) {
var html = '';
var keys = Object.keys(tree);
keys.forEach(function(key) {
if (isNaN(tree[key])) {
html += '<div><span>' + key + '</span>' + recursiveHtmlBuilder(tree[key]) + '</div>';
} else if (tree[key] > 1) {
html += '<div><span>' + key + ' <span>(x' + tree[key] + ')</span></span></div>';
} else {
html += '<div><span>' + key + '</span></div>';
}
});
return html;
}
offendersDirectives.directive('domTree', function() {
return {
restrict: 'E',
scope: {
tree: '='
},
template: '<div class="domTree"></div>',
replace: true,
link: function(scope, element) {
element.append(getdomTreeInnerHTML(scope.tree));
}
};
});
function getDomElementButtonHTML(obj, onASingleLine) {
if (obj.tree && !onASingleLine) {
return '<div class="offenderButton opens">' + getDomElementButtonInnerHTML(obj, onASingleLine) + '</div>';
} else {
return '<div class="offenderButton">' + getDomElementButtonInnerHTML(obj, onASingleLine) + '</div>';
}
}
function getDomElementButtonInnerHTML(obj, onASingleLine) {
if (obj.type === 'html' ||
obj.type === 'body' ||
obj.type === 'head' ||
obj.type === 'window' ||
obj.type === 'document' ||
obj.type === 'fragment') {
return obj.type;
}
if (obj.type === 'notAnElement') {
return 'Incorrect element';
}
var html = '';
if (obj.type === 'domElement') {
html = 'DOM element <b>' + obj.element + '</b>';
} else if (obj.type === 'fragmentElement') {
html = 'Fragment element <b>' + obj.element + '</b>';
} else if (obj.type === 'createdElement') {
html = 'Created element <b>' + obj.element + '</b>';
}
if (obj.tree && !onASingleLine) {
html += getdomTreeHTML(obj.tree);
}
return html;
}
offendersDirectives.directive('domElementButton', function() {
return {
restrict: 'E',
scope: {
obj: '='
},
template: '<div class="offenderButton" ng-class="{opens: obj.tree}"></div>',
replace: true,
link: function(scope, element) {
element.append(getDomElementButtonInnerHTML(scope.obj));
}
};
});
function getJQueryContextButtonHTML(context, onASingleLine) {
if (context.length === 0) {
return '<span class="offenderButton">Empty jQuery object</span>';
}
if (context.length === 1) {
return getDomElementButtonHTML(context.elements[0], onASingleLine);
}
var html = context.length + ' elements (' + getDomElementButtonHTML(context.elements[0], onASingleLine) + ', ' + getDomElementButtonHTML(context.elements[1], onASingleLine);
if (context.length === 3) {
html += ', ' + getDomElementButtonHTML(context.elements[0], onASingleLine);
} else if (context.length > 3) {
html += ' and ' + (context.length - 2) + ' more...';
}
return html + ')';
}
function isJQuery(node) {
return node.data.type.indexOf('jQuery ') === 0;
}
function getNonJQueryHTML(node, onASingleLine) {
var type = node.data.type;
if (node.windowPerformance) {
switch (type) {
case 'documentScroll':
return '(triggering the scroll event on <b>document</b>)';
case 'windowScroll':
return '(triggering the scroll event on <b>window</b>)';
case 'window.onscroll':
return '(calling the <b>window.onscroll</b> function)';
default:
return '';
}
}
if (!node.data.callDetails) {
return '';
}
var args = node.data.callDetails.arguments;
var ctxt = node.data.callDetails.context;
switch (type) {
case 'getElementById':
case 'createElement':
return '<b>' + args[0] + '</b>';
case 'getElementsByClassName':
case 'getElementsByTagName':
case 'querySelector':
case 'querySelectorAll':
return '<b>' + args[0] + '</b> on ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine);
case 'appendChild':
return 'append ' + getDomElementButtonHTML(args[0], onASingleLine) + ' to ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine);
case 'insertBefore':
return 'insert ' + getDomElementButtonHTML(args[0], onASingleLine) + ' into ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine) + ' before ' + getDomElementButtonHTML(args[1], onASingleLine);
case 'addEventListener':
return 'bind <b>' + args[0] + '</b> to ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine);
case 'getComputedStyle':
return getDomElementButtonHTML(args[0], onASingleLine) + (args[1] || '');
case 'error':
return args[0];
case 'jQuery - onDOMReady':
return '(function)';
case 'documentScroll':
return 'The scroll event just triggered on document';
case 'windowScroll':
return 'The scroll event just triggered on window';
case 'window.onscroll':
return 'The window.onscroll function just got called';
default:
return '';
}
}
function getJQueryHTML(node, onASingleLine) {
var type = node.data.type;
var unescapedArgs = node.data.callDetails.arguments;
var args = [];
var ctxt = node.data.callDetails.context;
// escape HTML in args
for (var i = 0 ; i < 4 ; i ++) {
if (unescapedArgs[i] !== undefined) {
args[i] = escapeHTML(unescapedArgs[i]);
}
}
if (type === 'jQuery loaded' || type === 'jQuery version change') {
return args[0];
}
switch (type) {
case 'jQuery - onDOMReady':
case 'jQuery - windowOnLoad':
return '(function)';
case 'jQuery - Sizzle call':
return '<b>' + args[0] + '</b> on ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine);
case 'jQuery - find':
if (ctxt && ctxt.length === 1 && ctxt.elements[0].type !== 'document') {
return '<b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else {
return '<b>' + args[0] + '</b>';
}
break;
case 'jQuery - html':
if (args[0] !== undefined) {
return 'set content "<b>' + args[0] + '</b>" to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else {
return 'get content from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - append':
return 'append ' + joinArgs(args) + ' to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - appendTo':
return 'append ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to <b>' + args[0] + '</b>';
case 'jQuery - prepend':
return 'prepend ' + joinArgs(args) + ' to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - prependTo':
return 'prepend ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to <b>' + args[0] + '</b>';
case 'jQuery - before':
return 'insert ' + joinArgs(args) + ' before ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - insertBefore':
return 'insert ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' before <b>' + args[0] + '</b>';
case 'jQuery - after':
return 'insert ' + joinArgs(args) + ' after ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - insertAfter':
return 'insert ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' after <b>' + args[0] + '</b>';
case 'jQuery - remove':
case 'jQuery - detach':
if (args[0]) {
return getJQueryContextButtonHTML(ctxt, onASingleLine) + ' filtered by <b>' + args[0] + '</b>';
} else {
return getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - empty':
case 'jQuery - clone':
case 'jQuery - unwrap':
case 'jQuery - show':
case 'jQuery - hide':
case 'jQuery - animate':
case 'jQuery - fadeIn':
case 'jQuery - fadeOut':
case 'jQuery - fadeTo':
case 'jQuery - fadeToggle':
case 'jQuery - slideDown':
case 'jQuery - slideUp':
case 'jQuery - slideToggle':
return getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - replaceWith':
return 'replace ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' with <b>' + args[0] + '</b>';
case 'jQuery - replaceAll':
return 'replace <b>' + args[0] + '</b> with ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - text':
if (args[0] !== undefined) {
return 'set text "<b>' + args[0] + '</b>" to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else {
return 'get text from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - wrap':
case 'jQuery - wrapAll':
return 'wrap ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' within <b>' + args[0] + '</b>';
case 'jQuery - wrapInner':
return 'wrap the content of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' within <b>' + args[0] + '</b>';
case 'jQuery - css':
case 'jQuery - attr':
case 'jQuery - prop':
if (isStringOfObject(args[0])) {
return 'set <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else if (args[1]) {
return 'set <b>' + args[0] + '</b> : <b>' + args[1] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else {
return 'get <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - offset':
case 'jQuery - height':
case 'jQuery - innerHeight':
case 'jQuery - width':
case 'jQuery - innerWidth':
case 'jQuery - scrollLeft':
case 'jQuery - scrollTop':
case 'jQuery - position':
if (args[0]) {
return 'set <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else {
return 'get from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - outerHeight':
case 'jQuery - outerWidth':
if (args[0] && args[0] !== 'true') {
return 'set <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else if (args[0] === 'true') {
return 'get from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' (with include margins option)';
} else {
return 'get from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - toggle':
if (args[0] === 'true') {
return getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to visible';
} else if (args[0] === 'false') {
return getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to hidden';
} else {
return getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - on':
case 'jQuery - one':
if (isStringOfObject(args[0])) {
return '<b>' + args[0].replace(/&quot;\(function\)&quot;/g, '(function)') + '</b>';
} else if (args[1] && isPureString(args[1])) {
return 'bind <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[1] + '</b>';
} else {
return 'bind <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - off':
if (args[0]) {
if (args[1]) {
return 'unbind <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[1] + '</b>';
} else {
return 'unbind <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
} else {
return 'unbind all events';
}
break;
case 'jQuery - live':
case 'jQuery - bind':
return 'bind <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - die':
case 'jQuery - unbind':
if (args[0]) {
return 'unbind <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else {
return 'unbind all events';
}
break;
case 'jQuery - delegate':
return 'bind <b>' + args[1] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[0] + '</b>';
case 'jQuery - undelegate':
if (args[0]) {
if (args[1]) {
return 'unbind <b>' + args[1] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[0] + '</b>';
} else {
return 'unbind namespace <b>' + args[0] + '</b>';
}
} else {
return 'unbind all events';
}
break;
case 'jQuery - blur':
case 'jQuery - change':
case 'jQuery - click':
case 'jQuery - dblclick':
case 'jQuery - focus':
case 'jQuery - keydown':
case 'jQuery - keypress':
case 'jQuery - keyup':
case 'jQuery - mousedown':
case 'jQuery - mouseenter':
case 'jQuery - mouseleave':
case 'jQuery - mousemove':
case 'jQuery - mouseout':
case 'jQuery - mouseover':
case 'jQuery - mouseup':
case 'jQuery - resize':
case 'jQuery - scroll':
case 'jQuery - select':
case 'jQuery - submit':
if (args[0]) {
return 'bind on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else {
return 'triggered on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - error':
case 'jQuery - focusin':
case 'jQuery - focusout':
case 'jQuery - hover':
case 'jQuery - load':
case 'jQuery - unload':
return 'bind on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - removeAttr':
case 'jQuery - removeProp':
return 'remove <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - val':
if (args[0]) {
return 'set value <b>' + args[0] + '</b> to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
} else {
return 'get value from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - hasClass':
case 'jQuery - addClass':
case 'jQuery - removeClass':
return '<b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - toggleClass':
if (args[0]) {
if (args[1]) {
return 'toggle <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to <b>' + args[1] + '</b>';
} else {
return 'toggle <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
} else {
return 'magic no-argument toggleClass';
}
break;
case 'jQuery - children':
if (args[0]) {
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' filtered by <b>' + args[0] + '</b>';
} else {
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - closest':
if (args[1]) {
return 'closest <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' in context <b>' + args[1] + '</b>';
} else {
return 'closest <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - next':
case 'jQuery - nextAll':
if (args[0]) {
return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching <b>' + args[0] + '</b>';
} else {
return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - nextUntil':
if (args[0]) {
if (args[1]) {
return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b> and matching <b>' + args[1] + '</b>';
} else {
return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b>';
}
} else {
return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - offsetParent':
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
case 'jQuery - prev':
case 'jQuery - prevAll':
if (args[0]) {
return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching <b>' + args[0] + '</b>';
} else {
return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - prevUntil':
if (args[0]) {
if (args[1]) {
return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b> and matching <b>' + args[1] + '</b>';
} else {
return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b>';
}
} else {
return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - parent':
case 'jQuery - parents':
if (args[0]) {
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching <b>' + args[0] + '</b>';
} else {
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - parentsUntil':
if (args[0]) {
if (args[1]) {
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b> and matching <b>' + args[1] + '</b>';
} else {
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b>';
}
} else {
return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
case 'jQuery - siblings':
if (args[0]) {
return 'near ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching <b>' + args[0] + '</b>';
} else {
return 'near ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
}
break;
default:
return '';
}
}
function escapeHTML(html) {
var entityMap = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': '&quot;',
"'": '&#39;',
"/": '&#x2F;'
};
return String(html).replace(/[&<>"'\/]/g, function (s) {
return entityMap[s];
});
}
function joinArgs(args) {
var html = '<b>' + args[0] + '</b>';
if (args[1]) {
html += ', <b>' + args[1] + '</b>';
if (args[2]) {
html += ', <b>' + args[2] + '</b>';
if (args[3]) {
html += ', and more...';
}
}
}
return html;
}
function isStringOfObject(str) {
return typeof str === 'string' && str[0] === '{' && str[str.length - 1] === '}';
}
function isPureString(str) {
return typeof str === 'string' && str[0] !== '{' && str !== '(function)' && str !== '[Object]' && str !== '[Array]' && str !== 'true' && str !== 'false' && str !== 'undefined' && str !== 'unknown';
}
function getTimelineParamsHTML(node, onASingleLine) {
if (isJQuery(node)) {
return getJQueryHTML(node, onASingleLine);
} else {
return getNonJQueryHTML(node, onASingleLine);
}
}
function getBacktraceHTML(backtrace) {
var html = '';
var parsedBacktrace = parseBacktrace(backtrace);
if (!parsedBacktrace || parsedBacktrace.length === 0) {
html += '<div><div>can\'t find any backtrace :/</div></div>';
} else {
for (var i = 0 ; i < parsedBacktrace.length ; i++) {
html += '<div>';
html += '<div>' + (parsedBacktrace[i].fnName || '(anonymous function)') + '</div>';
html += '<div class="trace">' + getUrlLink(parsedBacktrace[i].filePath, 40) + ':' + 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.eventNotDelegated) {
html += '<p class="advice">This binding should use Event Delegation instead of binding each element one by one.</p>';
}
if (node.data.resultsNumber === 0) {
html += '<p class="advice">The query returned 0 results. Could it be unused or dead code?</p>';
} else if (node.data.resultsNumber > 0) {
html += '<p>The query returned ' + node.data.resultsNumber + ' ' + (node.data.resultsNumber > 1 ? 'results' : 'result') + '.</p>';
}
if (node.data.backtrace) {
html += '<h4>Backtrace</h4>';
html += '<div class="table">';
html += getBacktraceHTML(node.data.backtrace);
html += '</div>';
}
html += '</div>';
}
return html;
}
offendersDirectives.directive('profilerLine', ['$filter', function($filter) {
var numberWithCommas = $filter('number');
function getProfilerLineHTML(index, node) {
return '<div class="index">' + (index + 1) + '</div>' +
'<div class="type">' + node.data.type + (node.children ? '<div class="children">' + recursiveChildrenHTML(node) + '</div>' : '') + '</div>' +
'<div class="value">' + getTimelineParamsHTML(node, false) + '</div>' +
'<div class="details">' + getTimelineDetailsHTML(node) + '</div>' +
'<div class="startTime ' + node.data.loadingStep + '">' + numberWithCommas(node.data.timestamp, 0) + ' ms</div>';
}
function recursiveChildrenHTML(node) {
var html = '';
if (node.children) {
node.children.forEach(function(child) {
html += '<div class="child"><span>' + child.data.type + '<div class="childArgs">' + getTimelineParamsHTML(child, true) + '</div></span>' + recursiveChildrenHTML(child) + '</div>';
});
}
return html;
}
function onDetailsClick(row) {
// Close if it's alreay open
if (row.classList.contains('showDetails')) {
closeDetails(row);
return;
}
// Close any other open details overlay
var openOnes = document.getElementsByClassName('showDetails');
if (openOnes.length > 0) {
openOnes[0].classList.remove('showDetails');
}
// Make it appear
row.classList.add('showDetails');
// Bind the close button
row.querySelector('.closeBtn').addEventListener('click', function() {
closeDetails(row);
});
}
function closeDetails(row) {
row.classList.remove('showDetails');
// Unbind the close button
row.querySelector('.closeBtn').removeEventListener('click', closeDetails);
}
return {
restrict: 'E',
scope: {
index: '=',
node: '='
},
template: '<div></div>',
replace: true,
link: function(scope, element) {
if (scope.node.error) {
element.addClass('jsError');
} else if (scope.node.windowPerformance) {
element.addClass('windowPerformance');
}
element.append(getProfilerLineHTML(scope.index, scope.node));
element[0].id = 'line_' + scope.index;
if (scope.node.warning) {
element[0].classList.add('warning');
if (scope.node.queryWithoutResults) {
element[0].classList.add('queryWithoutResults');
}
if (scope.node.jQueryCallOnEmptyObject) {
element[0].classList.add('jQueryCallOnEmptyObject');
}
if (scope.node.eventNotDelegated) {
element[0].classList.add('eventNotDelegated');
}
}
// Bind click on the details icon
var detailsIcon = element[0].querySelector('.details div');
if (detailsIcon) {
detailsIcon.addEventListener('click', function() {
onDetailsClick(this.parentNode.parentNode);
});
}
}
};
}]);
function shortenUrl(url, maxLength) {
if (!maxLength) {
maxLength = 110;
}
// Why dividing by 2.1? Because it adds a 5% margin.
var leftLength = Math.floor((maxLength - 5) / 2.1);
var rightLength = Math.ceil((maxLength - 5) / 2.1);
return (url.length > maxLength) ? url.substr(0, leftLength) + ' ... ' + url.substr(-rightLength) : url;
}
offendersDirectives.filter('shortenUrl', function() {
return shortenUrl;
});
function getUrlLink(url, maxLength) {
return '<a href="' + url + '" target="_blank" title="' + url + '">' + shortenUrl(url, maxLength) + '</a>';
}
offendersDirectives.directive('urlLink', function() {
return {
restrict: 'E',
scope: {
url: '=',
maxLength: '='
},
template: '<a href="{{url}}" target="_blank" title="{{url}}">{{url | shortenUrl:maxLength}}</a>',
replace: true
};
});
offendersDirectives.filter('encodeURIComponent', function() {
return window.encodeURIComponent;
});
offendersDirectives.directive('fileAndLine', function() {
return {
restrict: 'E',
scope: {
file: '=',
line: '=',
column: '='
},
template: '<span><span ng-if="file"><url-link url="file" max-length="60"></url-link></span><span ng-if="!file">&lt;inline CSS&gt;</span><span ng-if="line !== null && column !== null"> @ {{line}}:{{column}}</span></span>',
replace: true
};
});
offendersDirectives.directive('fileAndLineButton', function() {
return {
restrict: 'E',
scope: {
file: '=',
line: '=',
column: '='
},
template: '<div class="offenderButton opens">css file<div class="cssFileAndLine"><file-and-line file="file" line="line" column="column" button="true"></file-and-line></div></div>',
replace: true
};
});
offendersDirectives.filter('bytes', function() {
return function(bytes) {
if (isNaN(parseFloat(bytes)) || !isFinite(bytes)) {
return '-';
}
var kilo = bytes / 1024;
if (kilo < 1) {
return bytes + ' bytes';
}
if (kilo < 100) {
return kilo.toFixed(1) + ' KB';
}
if (kilo < 1024) {
return kilo.toFixed(0) + ' KB';
}
var mega = kilo / 1024;
if (mega < 10) {
return mega.toFixed(2) + ' MB';
}
return mega.toFixed(1) + ' MB';
};
});
})();
+49
View File
@@ -0,0 +1,49 @@
var apiService = angular.module('apiService', []);
apiService.factory('API', ['$location', 'Runs', 'Results', function($location, Runs, Results) {
return {
launchTest: function(url, settings) {
var runObject = {
url: url,
waitForResponse: false,
screenshot: true,
jsTimeline: true,
device: settings.device,
waitForSelector: settings.waitForSelector,
cookie: settings.cookie,
authUser: settings.authUser,
authPass: settings.authPass,
};
if (settings.waitForSelector && settings.waitForSelector !== '') {
runObject.waitForSelector = settings.waitForSelector;
}
if (settings.cookie && settings.cookie !== '') {
runObject.cookie = settings.cookie;
}
if (settings.authUser && settings.authUser !== '' && settings.authPass && settings.authPass !== '') {
runObject.authUser = settings.authUser;
runObject.authPass = settings.authPass;
}
Runs.save(runObject, function(data) {
$location.path('/queue/' + data.runId);
}, function(response) {
if (response.status === 429) {
alert('Too many requests, you reached the max number of requests allowed in 24h');
} else {
alert('An error occured...');
}
});
},
relaunchTest: function(result) {
this.launchTest(result.params.url, result.params.options);
}
};
}]);
+24
View File
@@ -0,0 +1,24 @@
var settingsService = angular.module('settingsService', []);
settingsService.factory('Settings', ['localStorageService', function(localStorageService) {
return {
getMergedSettings: function() {
var defaultSettings = {
device: 'desktop',
showAdvanced: false
};
var savedValues = localStorageService.get('settings');
return angular.extend(defaultSettings, savedValues);
},
saveSettings: function(settings) {
localStorageService.set('settings', settings);
}
};
}]);
+39 -13
View File
@@ -7,7 +7,16 @@
}
.summary .globalScore {
margin-bottom: 3em;
display: table;
width: 60%;
margin: 3em auto;
> div {
display: table-cell;
width: 50%;
vertical-align: middle;
}
.globalGrade {
margin: 0.5 auto;
width: 2.5em;
@@ -23,6 +32,35 @@
font-weight: bold;
margin: 0.5em 0 1em;
}
.screenshotWrapper:hover {
opacity: 0.75;
&:after {
position: absolute;
width: 1.25em;
height: 1.25em;
top: 0.7em;
left: 1.55em;
font-size: 3em;
color: #FFF;
background: #000;
border-radius: 0.2em;
text-align: center;
content: "+";
opacity: 0.85;
}
}
.screenshotWrapper.phone:hover:after {
top: 1.7em;
left: 0.64em;
}
.screenshotWrapper.tablet:hover:after {
top: 1.5em;
left: 0.9em;
}
}
.summary .notations {
@@ -114,18 +152,6 @@
.summary .fromShare {
margin-bottom: 3em;
a {
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;
}
}
.summary .apiTip {
+129 -53
View File
File diff suppressed because one or more lines are too long
+165 -2
View File
@@ -1,18 +1,181 @@
.promess {
padding: 0em 2em 3em;
padding: 0em 2em;
margin-bottom: 0.5em;
font-weight: normal;
font-size: 1.2em;
}
.price {
padding: 0em 2em 3em;
margin-top: 0em;
font-size: 0.9em;
}
.url {
width: 50%;
}
.launchBtn {
background: #e74c3c;
background: #ffa319;
color: #fff;
&:focus {
background: #e74c3c;
}
&.disabled {
background: #deaca6;
&:focus {
color: #ddd;
}
}
}
.settings {
width: 50%;
margin: 0 auto;
input, select {
font-size: 1em;
}
input[type=text] {
width: 100%;
min-width: 4em;
}
}
.device {
margin-top: 3em;
.item {
display: inline-block;
margin: 1em 0.75em;
width: 5.5em;
height: 5.5em;
color: #FFF;
border: 1px solid #FFF;
padding: 1px;
border-radius: 0.5em;
cursor: pointer;
text-decoration: none;
font-size: 0.8em;
&.active {
color: #ffa319;
border: 2px solid #ffa319;
padding: 0;
}
&:hover {
color: #ffa319;
}
> div {
margin: 0.2em 0 0.1em;
font-size: 3em;
}
}
}
.settingsTooltip {
position: relative;
span {
font-size: 0.8em;
vertical-align: text-top;
}
div {
display: none;
position: absolute;
padding: 0.5em;
width: 25em;
background: #FFF;
color: #000;
font-size: 0.8em;
border-radius: 1em;
border: 2px solid #ffa319;
white-space: normal;
z-index: 2;
}
&:hover div {
display: block;
}
}
.showAdvanced {
display: inline-block;
margin-top: 2em;
color: #FFF;
text-decoration: none;
font-size: 0.9em;
&:hover {
color: #ffa319;
}
}
.currentSettings {
font-size: 0.9em;
span {
color: #ffa319;
&:after {
color: #FFF;
content: ",";
}
&:last-child:after {
content: "";
}
}
}
.advanced {
margin: 1em 0 0;
display: table;
width: 100%;
text-align: left;
border-spacing: 0.75em;
> div {
display: table-row;
> div {
display: table-cell;
width: 75%;
&.label {
width: 25%;
white-space: nowrap;
}
}
}
.subTable {
display: table;
border-spacing: 0;
width: 100%;
> div {
display: table-row;
> div {
display: table-cell;
padding: 0 0 0.75em;
}
}
}
}
.features {
display: table;
width: 50%;
margin: 6em auto 0;
font-size: 0.9em;
color: #413;
> div {
width: 33.3%;
display: table-cell;
padding: 0 1.5em;
}
}
+124 -21
View File
@@ -112,6 +112,128 @@ h1 span {
}
}
a.linkButton {
font-size: 1em;
padding: 0.3em 0.5em;
margin: 0.5em;
line-height: 2em;
border: 0 solid;
border-radius: 0.5em;
box-shadow: 0.1em 0.2em 0 0 #5e2846;
background: #e74c3c;
color: #fff;
text-decoration: none;
}
.screenshotWrapper {
display: inline-block;
position: relative;
background: #000;
> div {
overflow: scroll;
position: relative;
}
.screenshotImage {
width: 100%;
}
.screenshotError {
color: #fff;
}
}
.screenshotWrapper.desktop {
border: 0.2em solid #AAA;
padding: 0.5em;
border-top-left-radius: 0.4em;
border-top-right-radius: 0.4em;
&:before {
position: absolute;
width: 15em;
height: 0.6em;
bottom: -0.75em;
left: -1em;
background: #CCC;
border-bottom-left-radius: 0.2em;
border-bottom-right-radius: 0.2em;
content: " ";
}
&:after {
position: absolute;
width: 0.4em;
height: 0.2em;
bottom: -0.55em;
left: 12.5em;
background: lime;
content: " ";
}
> div {
width: 12em;
height: 6.75em;
}
}
.screenshotWrapper.phone {
border: 0.07em solid #CCC;
padding: 1em 0.3em 1.5em;
border-radius: 0.6em;
&:before {
position: absolute;
width: 0.8em;
height: 0.8em;
bottom: 0.3em;
left: 3.3em;
border: 0.07em solid #CCC;
border-radius: 0.5em;
content: " ";
}
&:after {
position: absolute;
width: 1em;
height: 0.1em;
bottom: 13.9em;
left: 3.2em;
background: #555;
border-radius: 0.05em;
content: " ";
}
> div {
width: 6.75em;
height: 12em;
}
}
.screenshotWrapper.tablet {
border: 0.07em solid #CCC;
padding: 0.8em 0.5em 0.9em;
border-radius: 0.6em;
&:before {
position: absolute;
width: 0.5em;
height: 0.5em;
bottom: 0.15em;
left: 4.35em;
border: 0.07em solid #CCC;
border-radius: 0.4em;
content: " ";
}
> div {
width: 8.25em;
height: 11em;
}
}
.star {
font-weight: bold;
@@ -131,25 +253,6 @@ h1 span {
}
}
/* 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);
[class^="icon-"]:before, [class*=" icon-"]:before {
vertical-align: baseline;
}
-4
View File
@@ -6,8 +6,4 @@
.statusSubMessage {
font-size: 0.8em;
margin-bottom: 6em;
}
.queueLink {
color: #FFF;
}
+178
View File
@@ -88,4 +88,182 @@
font-size: 3em;
margin-bottom: 1em;
}
}
.rule .startTime {
display: none;
}
.offendersTable, .value {
.offenderButton {
display: inline-block;
position: relative;
background: #efe;
padding: 0 0.5em;
margin: 0.2em 0;
border-radius: 0.4em;
z-index: 1;
&.opens {
padding-right: 0.75em;
&:after {
position: relative;
left: 0.5em;
content: '\25BC';
font-size: 0.8em;
}
}
> div {
display: none;
position: absolute;
right: 0;
min-width: 100%;
background: inherit;
border-bottom-left-radius: 0.4em;
border-bottom-right-radius: 0.4em;
border-top: 1px solid #999;
z-index: 2;
}
.domTree {
text-align: left;
white-space: nowrap;
> div {
margin: 0.5em;
div {
margin-left: 1em;
}
}
}
.backtrace, .cssFileAndLine {
white-space: nowrap;
padding: 0.5em;
}
&.opens:hover {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
background: #ffe0cc;
z-index: 2;
> div {
display: block;
background: #ffe0cc;
}
}
}
}
.offendersHtml {
display: inline-block;
}
.domTree div {
text-align: left;
margin-left: 1em;
span:only-child {
font-weight: bold;
span {
font-style: italic;
font-weight: normal;
}
}
}
.checker {
/* Checkerboard background */
background-color: #ddd;
background-image: linear-gradient(45deg, #AAA 25%, transparent 25%, transparent 75%, #AAA 75%, #AAA), linear-gradient(45deg, #AAA 25%, transparent 25%, transparent 75%, #AAA 75%, #AAA);
background-size:1em 1em;
background-position:0 0, 0.5em 0.5em;
}
.colorPalette {
width: 30em;
border: 2px solid #000;
text-align: left;
> div {
display: inline-block;
height: 2em;
position: relative;
div {
display: none;
position: absolute;
left: 100%;
top: 100%;
background: #FFF;
padding: 0.5em;
border: 2px solid #f1c40f;
border-radius: 0.5em;
white-space: nowrap;
z-index: 3;
font-weight: bold;
}
&:hover div {
display: block;
}
&:hover:after {
content: " ";
position: absolute;
left: -0.2em;
top: -0.2em;
width: 100%;
height: 100%;
z-index: 2;
border: 0.2em solid #f1c40f;
}
}
}
.totalWeightPie {
max-width: 39em;
margin: 2em auto 4em;
canvas {
max-width: inherit;
}
}
.hugeFile {
font-weight: bold;
color: #e74c3c;
}
.imageOffenders {
display: table;
border-spacing: 3em;
width: 90%;
> div {
display: table-row;
> div {
display: table-cell;
vertical-align: middle;
}
}
img {
max-height: 10em;
max-width: 40em;
border: 1px solid #000;
margin-top: 0.5em;
}
}
.smallPreview {
max-height: 1.6em;
max-width: 4em;
border: 1px solid #000;
margin-top: 0.2em;
}
+8
View File
@@ -0,0 +1,8 @@
.screenshot.board {
text-align: center;
}
.screenshot .screenshotWrapper {
font-size: 2.08333333333333em;
margin-bottom: 0.5em;
}
+129 -15
View File
@@ -14,6 +14,23 @@
text-align: center;
}
.selectScript {
padding-bottom: 2em;
font-size: 0.9em;
select {
max-width: 30em;
}
&.empty {
font-size: 0.8em;
select {
width: 10em;
}
}
}
.timeline {
margin: 2em 0 5em;
}
@@ -49,6 +66,10 @@
position: absolute;
bottom: 0;
width: 100%;
&.clickable {
cursor: pointer;
}
}
.timeline div.interval:hover {
background: #9C4274;
@@ -141,20 +162,9 @@
text-align: left;
}
.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;
.subFilters {
margin-left: 3em;
font-size: 0.9em;
}
.table {
@@ -189,14 +199,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: 2;
}
}
}
.table .showingDetails .child span:hover {
background: inherit;
.childArgs {
display: none;
}
}
.table > div > .value {
width: 70%;
word-break: break-all;
@@ -215,6 +293,7 @@ input.textFilter {
}
.detailsOverlay {
display: none;
position: absolute;
right: 3em;
top: -3em;
@@ -224,13 +303,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;
@@ -268,4 +350,36 @@ input.textFilter {
.execution .icon-warning {
color: #e74c3c;
cursor: pointer;
}
.queryWithoutResultsFilterOn {
> div {
display: none;
&.queryWithoutResults {
display: table-row;
}
}
}
.jQueryCallOnEmptyObjectFilterOn {
> div {
display: none;
&.jQueryCallOnEmptyObject {
display: table-row;
}
}
}
.eventNotDelegatedFilterOn {
> div {
display: none;
&.eventNotDelegated {
display: table-row;
}
}
}
.jsErrorFilterOn {
> div {
display: none;
&.jsError {
display: table-row;
}
}
}
+13 -1
View File
@@ -5,6 +5,7 @@
<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">
@@ -12,25 +13,36 @@
<link rel="stylesheet" type="text/css" href="/css/dashboard.css">
<link rel="stylesheet" type="text/css" href="/css/queue.css">
<link rel="stylesheet" type="text/css" href="/css/rule.css">
<link rel="stylesheet" type="text/css" href="/css/screenshot.css">
<link rel="stylesheet" type="text/css" href="/css/timeline.css">
<link rel="stylesheet" type="text/css" href="/css/about.css">
<link rel="stylesheet" type="text/css" href="/bower_components/angular-chart.js/dist/angular-chart.css">
<!-- endbuild -->
<!-- build:js /js/all.js -->
<script src="/bower_components/angular/angular.min.js"></script>
<script src="/bower_components/Chart.js/Chart.min.js"></script>
<script src="/bower_components/angular-route/angular-route.min.js"></script>
<script src="/bower_components/angular-resource/angular-resource.min.js"></script>
<script src="/bower_components/angular-sanitize/angular-sanitize.min.js"></script>
<script src="/bower_components/angular-animate/angular-animate.min.js"></script>
<script src="/bower_components/angular-local-storage/dist/angular-local-storage.min.js"></script>
<script src="/bower_components/angular-chart.js/dist/angular-chart.min.js"></script>
<script src="/js/app.js"></script>
<script src="/js/controllers/indexCtrl.js"></script>
<script src="/js/controllers/aboutCtrl.js"></script>
<script src="/js/controllers/dashboardCtrl.js"></script>
<script src="/js/controllers/queueCtrl.js"></script>
<script src="/js/controllers/ruleCtrl.js"></script>
<script src="/js/controllers/screenshotCtrl.js"></script>
<script src="/js/controllers/timelineCtrl.js"></script>
<script src="/js/models/resultsFactory.js"></script>
<script src="/js/models/runsFactory.js"></script>
<script src="/js/services/apiService.js"></script>
<script src="/js/services/menuService.js"></script>
<script src="/js/services/settingsService.js"></script>
<script src="/js/directives/gradeDirective.js"></script>
<script src="/js/directives/offendersDirectives.js"></script>
<!-- endbuild -->
<head>
+22 -9
View File
@@ -2,10 +2,22 @@
<div class="summary board">
<div class="globalScore" ng-if="globalScore === 0 || globalScore > 0">
<h2> Global score</h2>
<div class="globalScoreDisplay">
<grade score="result.scoreProfiles.generic.globalScore" class="globalGrade"></grade>
<div class="on100">{{globalScore}}/100</div>
<div>
<h2>Global score</h2>
<div class="globalScoreDisplay">
<grade score="result.scoreProfiles.generic.globalScore" class="globalGrade"></grade>
<div class="on100">{{globalScore}}/100</div>
</div>
</div>
<div>
<a href="/result/{{result.runId}}/screenshot">
<div class="screenshotWrapper" ng-class="result.params.options.device || 'desktop'">
<div>
<img ng-if="result.screenshotUrl" class="screenshotImage" ng-src="{{result.screenshotUrl}}"/>
<span ng-if="!result.screenshotUrl" class="screenshotError">Screenshot not available</span>
</div>
</div>
</a>
</div>
</div>
@@ -23,7 +35,8 @@
</div>
<div class="label">{{rule.policy.label}}</div>
<div class="result">
{{rule.value}}
<span ng-if="rule.policy.unit == 'bytes'">{{rule.value | bytes}}</span>
<span ng-if="rule.policy.unit != 'bytes'">{{rule.value}}</span>
<span ng-if="rule.abnormal" class="icon-warning"></span>
<span ng-if="rule.abnormalityScore <= -100" class="icon-warning"></span>
<span ng-if="rule.abnormalityScore <= -300" class="icon-warning"></span>
@@ -35,8 +48,8 @@
</div>
</div>
<div ng-if="!error" 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 ng-if="!error && !fromSocialShare" class="apiTip">
<b>Did you know? Yellow Lab Tools has a brand new <a href="https://github.com/gmetais/grunt-yellowlabtools" target="_blank">Grunt plugin</a>!
</div>
<div class="tweet" ng-if="!error && !fromSocialShare">
@@ -49,8 +62,8 @@
<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 href="" ng-click="seeFullReport()">See the full report for this page</a>
<a href="/">Test another webpage</a>
<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">
+90 -3
View File
@@ -1,6 +1,93 @@
<h2 class="promess">Free online test to help speeding up <b>heavy</b> web pages</h2>
<h2 class="promess">Online test to help speeding up <b>heavy</b> web pages</h2>
<p class="price">Free and open source!</p>
<form ng-submit="launchTest()" >
<input type="text" name="url" ng-model="url" placeholder="http://www.mysite.com" class="url" />
<input type="{{$root.isTouchDevice? 'url' : '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="settings">
<div class="device">
<div>Choose the simulated device:</div>
<a href="" class="item" ng-class="{active: settings.device == 'desktop'}" ng-click="settings.device = 'desktop'"><div class="icon-screen"></div>Desktop</a>
<a href="" class="item" ng-class="{active: settings.device == 'tablet'}" ng-click="settings.device = 'tablet'"><div class="icon-tablet"></div>Tablet</a>
<a href="" class="item" ng-class="{active: settings.device == 'phone'}" ng-click="settings.device = 'phone'"><div class="icon-mobile"></div>Phone</a>
</div>
[ <a href="" class="showAdvanced" ng-click="settings.showAdvanced = !settings.showAdvanced">
<span ng-if="!settings.showAdvanced">Advanced settings &nbsp;</span>
<span ng-if="settings.showAdvanced">Hide advanced settings &nbsp;</span>
</a> ]
<span class="currentSettings" ng-if="!settings.showAdvanced && (settings.waitForSelector || settings.cookie || settings.authUser || settings.authPass)">
Currently set:
<span ng-if="settings.waitForSelector">wait for selector</span>
<span ng-if="settings.cookie">cookie</span>
<span ng-if="settings.authUser || settings.authPass">authentication</span>
</span>
<div class="advanced" ng-show="settings.showAdvanced">
<!--<div>
<div class="label">
Wait selector
<span class="settingsTooltip">
<span class="icon-question"></span>
<div><b>Wait for a CSS selector</b><br><br>Once the page is considered loaded, PhantomJS will repeatedly try to match the given CSS selector until it is found in the page. A 60 seconds timeout still applies anyway.<br><br>Example: "body.loaded"</div>
</span>
</div>
<div><input type="text" name="waitForSelector" ng-model="settings.waitForSelector" /></div>
</div>-->
<div>
<div class="label">
Cookie
<span class="settingsTooltip">
<span class="icon-question"></span>
<div><b>Cookie</b><br><br>Adds a cookie on the main domain.<br><br>Example: "bar=foo;domain=url"</div>
</span>
</div>
<div><input type="text" name="cookie" ng-model="settings.cookie" /></div>
</div>
<!--<div>
<div class="label">
Authent
<span class="settingsTooltip">
<span class="icon-question"></span>
<div><b>Basic HTTP authentication</b><br><br>Enter your credentials here if you need to bypass a basic authentication.<br><br><i>PS: if your authentication is not basic, you might be able to copy the session cookie from your browser, paste it in the "Cookie" setting and launch a run before your cookie expires.</i></div>
</span>
</div>
<div class="subTable">
<div>
<div>username</div>
<div><input type="text" class="authField" name="authUser" ng-model="settings.authUser" /></div>
</div>
<div>
<div><span>password</div>
<div><input type="text" class="authField" name="authPass" ng-model="settings.authPass" /></div>
</div>
</div>
</div>-->
<!--<div>
<div class="label">Blocked domains</div>
<div>
<div>
Blacklist / Whitelist
</div>
<textarea name=""></textarea>
</div>
</div>-->
</div>
</div>
</form>
<div class="features">
<div>
<h3>Front-end quality</h3>
<p>Audits quality problems in the HTML, CSS and JS</p>
</div>
<div>
<h3>WebPerf</h3>
<p>Checks if performance good practices are applied</p>
</div>
<div>
<h3>JS Profiling</h3>
<p>Untangles the JavaScript spaghetti code</p>
</div>
</div>
+31 -18
View File
@@ -1,23 +1,36 @@
<p>Tested url: &nbsp; <a href="{{url}}" target="_blank" class="testedUrl">{{url}}</a></p>
<p ng-if="url">Tested url: &nbsp; <a href="{{url}}" target="_blank" class="testedUrl">{{url}}</a></p>
<div ng-if="status.statusCode == 'failed'">
<div class="status">Test failed</div>
<p class="statusSubMessage">{{status.error}}</p>
<p><a class="queueLink" href="https://github.com/gmetais/YellowLabTools/issues" target="_blank">Report a bug on GitHub</a></p>
<p><a class="queueLink" href="/">Back to index</a></p>
</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 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>
<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 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="status.statusCode == 'complete'">
<div class="status">Test complete</div>
<p class="statusSubMessage">Opening results...</p>
<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>
+3 -3
View File
@@ -1,8 +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 back" href="/"><div class="icon-arrow-left3"></div><span>New test<span></a>
<a class="menuItem restart" href="" ng-click="testAgain()"><div class="icon-loop"></div><span>Test again<span></a>
<div class="menuItem" ng-class="{active: Menu.getCurrentPage() == 'dashboard'}" ng-click="Menu.changePage('dashboard')"><div class="icon-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 class="menuItem" ng-class="{active: Menu.getCurrentPage() == 'dashboard'}" ng-click="Menu.changePage('dashboard')"><div class="icon-list"></div><span>Dashboard</span></div>
<div class="menuItem" ng-class="{active: Menu.getCurrentPage() == 'timeline'}" ng-click="Menu.changePage('timeline')"><div class="icon-bars"></div><span>JS Timeline</span></div>
</div>
+310 -10
View File
@@ -9,25 +9,325 @@
<div>{{rule.score}}/100</div>
</div>
<div class="right">
<h3>Value: {{rule.value}}</h3>
<div ng-bind-html="message" class="message"></div>
<h3>
Value:
<span ng-if="rule.policy.unit == 'bytes'">{{rule.value | bytes}}</span>
<span ng-if="rule.policy.unit != 'bytes'">{{rule.value}}</span>
</h3>
<div 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">
<h3>
<ng-pluralize count="rule.offenders.length || 0" when="{'0': 'No offenders', 'one': '1 offender', 'other': '{} offenders'}">
</ng-pluralize>
</h3>
<div class="offendersTable">
<div ng-repeat="offender in rule.offenders track by $index">
<div>{{offender}}</div>
<div class="offenders" ng-if="rule.policy.hasOffenders">
<h3><ng-pluralize count="rule.offendersObj.count" when="{'0': 'No offenders', 'one': '1 offender', 'other': '{} offenders'}"></ng-pluralize></h3>
<div ng-if="rule.offendersObj.list" class="offendersTable">
<div ng-repeat="offender in rule.offendersObj.list track by $index">
<div ng-if="offender.parseError">
{{offender.parseError}}
</div>
<div ng-if="!offender.parseError">
<div ng-if="policyName === 'DOMidDuplicated'">
<b>{{offender.id}}</b>: {{offender.occurrences}} occurrences
</div>
<div ng-if="policyName === 'DOMqueriesAvoidable'">
<b>{{offender.query}}</b> (in <dom-element-button obj="offender.context"></dom-element-button>) using {{offender.fn}}: <b>{{offender.count}} queries</b>
</div>
<div ng-if="policyName === 'eventsScrollBound'">
<span ng-if="offender.target == 'window'">Scroll event bound on <b>window</b></span>
<span ng-if="offender.target == '#document'">Scroll event bound on <b>document</b></span>
<span ng-if="offender.target == 'window.onscroll'"><b>window.onscroll</b> function declared</span>
<div class="offenderButton" ng-if="offender.backtrace.length == 0">no backtrace</div>
<div class="offenderButton opens" ng-if="offender.backtrace.length > 0">
backtrace
<div class="backtrace">
<div ng-repeat="obj in offender.backtrace track by $index">
<span ng-if="obj.functionName">{{obj.functionName}}()</span>
<url-link url="obj.file" max-length="60"></url-link>
line {{obj.line}}
</div>
</div>
</div>
</div>
<div ng-if="policyName === 'jsErrors'">
<b>{{offender.error}}</b>
<div class="offenderButton" ng-if="offender.backtrace.length == 0">no backtrace</div>
<div class="offenderButton opens" ng-if="offender.backtrace.length > 0">
backtrace
<div class="backtrace">
<div ng-repeat="obj in offender.backtrace track by $index">
<span ng-if="obj.functionName">{{obj.functionName}}()</span>
<url-link url="obj.file" max-length="60"></url-link>
line {{obj.line}}
</div>
</div>
</div>
</div>
<div ng-if="policyName === 'jQueryFunctionsUsed'">
function <b>{{offender.functionName}}</b> used {{offender.count}} times
</div>
<div ng-if="policyName === 'documentWriteCalls'">
<b>{{offender.writeFn}}</b>
<span ng-if="offender.from">
called from
<span ng-if="offender.from.functionName">{{offender.from.functionName}}()</span>
<url-link url="offender.from.file" max-length="50"></url-link>
line {{offender.from.line}}
</span>
<span ng-if="!offender.from">
called from (no backtrace available)
</span>
</div>
<div ng-if="policyName === '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 === 'lazyLoadableImagesBelowTheFold'">
<url-link url="offender" max-length="100"></url-link>
<img ng-src="{{offender}}" class="smallPreview checker"></img>
</div>
<div ng-if="policyName === 'notFound' || policyName === 'closedConnections' || policyName === 'multipleRequests' || policyName === 'cachingDisabled' || policyName === 'cachingNotSpecified'">
<url-link url="offender" max-length="100"></url-link>
</div>
<div ng-if="policyName === 'cachingTooShort'">
<url-link url="offender.file" max-length="100"></url-link>
cached for <b>{{offender.ttlWithUnit}} {{offender.unit}}</b>
</div>
<div ng-if="policyName === 'domains'">
<b>{{offender.domain}}</b>
(<ng-pluralize count="offender.requests" when="{'one':'1 request','other':'{} requests'}"></ng-pluralize>)
</div>
<div ng-if="policyName === 'globalVariables' || policyName === 'jQueryVersionsLoaded'">
{{offender}}
</div>
</div>
</div>
</div>
<div ng-if="!rule.offendersObj.list" class="offendersHtml">
<div ng-if="policyName === 'DOMelementMaxDepth'">
<dom-tree tree="rule.offendersObj.tree"></dom-tree>
</div>
<div ng-if="policyName === 'DOMaccessesOnScroll' && rule.offendersObj.children.length > 0">
<p>The table below shows the interactions between the JavaScript and the DOM on a scroll event.</p>
<div class="table" ng-class="{warningsFilterOn: warningsFilterOn}">
<div class="headers">
<div><!-- index --></div>
<div>Type</div>
<div>Params</div>
<div><!-- details --></div>
</div>
<profiler-line ng-repeat="node in rule.offendersObj.children" data-index="$index" node="node"></profiler-line>
</div>
</div>
<div ng-if="policyName === 'cssColors' && rule.offendersObj.count > 0">
<p>This is the colors palette, sized by total occurrences:</p>
<div class="colorPalette checker">
<div ng-repeat="offender in rule.offendersObj.palette" style="background-color: {{offender.color}}; width: {{offender.occurrences * 100 / rule.offendersObj.palette[0].occurrences}}%"><div>{{offender.color}} ({{offender.occurrences}} times)</div></div>
</div>
</div>
</div>
</div>
<div ng-if="policyName === 'totalWeight'">
<h3>Weight by MIME type</h3>
<div class="totalWeightPie">
<canvas class="chart chart-doughnut" data="weightData" labels="weightLabels" options="weightOptions" colours="weightColours" legend="true"></canvas>
</div>
<div ng-repeat="type in weightLabels">
<h3>{{rule.offendersObj.list.byType[type].totalWeight | bytes}} of {{type}}</h3>
<div class="offendersTable">
<div ng-repeat="request in rule.offendersObj.list.byType[type].requests | orderBy:'-weight'" ng-if="request.weight > 0">
<div><url-link url="request.url" max-length="60"></url-link></div>
<div ng-class="{hugeFile: request.weight > 102400}">{{request.weight | bytes}}</div>
</div>
</div>
</div>
</div>
<div ng-if="policyName === 'imageOptimization'">
<h3 ng-if="rule.value > 0">{{rule.value | bytes}} could be saved on <ng-pluralize count="rule.offendersObj.list.images.length" when="{'one': '1 image', 'other': '{} images'}"></ng-pluralize></h3>
<div class="imageOffenders">
<div ng-repeat="image in rule.offendersObj.list.images | orderBy:'-gain'">
<div>
Current file: <url-link url="image.url" max-length="50"></url-link>
<div><a href="{{image.url}}" target="_blank"><img ng-src="{{image.url}}" class="checker" /></a></div>
</div>
<div>
<p ng-if="!image.afterCompression">Current weight: {{image.original | bytes}}</p>
<p ng-if="image.afterCompression">Current weight: {{image.original | bytes}} ({{image.afterCompression | bytes}} gzipped)</p>
<p ng-if="image.lossless && image.afterOptimizationAndCompression">With a lossless optimization:<br/>{{image.afterOptimizationAndCompression | bytes}} gzipped (<b>-{{image.gain | bytes}}</b> gzipped)</p>
<p ng-if="image.lossless && !image.afterOptimizationAndCompression">With a lossless optimization:<br/>{{image.lossless | bytes}} <span ng-if="!image.lossy">(<b>-{{image.gain | bytes}}</b>)</span></p>
<p ng-if="image.lossy && image.afterOptimizationAndCompression">With a lossy optimization:<br/>{{image.afterOptimizationAndCompression | bytes}} gzipped (<b>-{{image.gain | bytes}} gzipped</b>)</p>
<p ng-if="image.lossy && !image.afterOptimizationAndCompression">With a lossy optimization:<br/>{{image.lossy | bytes}} (<b>-{{image.gain | bytes}}</b>)</p>
</div>
</div>
</div>
</div>
<div ng-if="policyName === 'gzipCompression'">
<h3 ng-if="rule.value > 0">{{rule.value | bytes}} could be saved on <ng-pluralize count="rule.offendersObj.list.images.length" when="{'one': '1 file', 'other': '{} files'}"></ng-pluralize></h3>
<div class="table">
<div class="headers">
<div>File</div>
<div>Current weight</div>
<div>Gzipped</div>
<div>Gain</div>
</div>
<div ng-repeat="file in rule.offendersObj.list.files | orderBy:'-gain'">
<div>
<url-link url="file.url" max-length="70"></url-link>
</div>
<div>{{file.original | bytes}}</div>
<div>{{file.gzipped | bytes}}</div>
<div><b>-{{file.gain | bytes}}</b></div>
</div>
</div>
</div>
<div ng-if="policyName === 'fileMinification'">
<h3 ng-if="rule.value > 0">{{rule.value | bytes}} could be saved on <ng-pluralize count="rule.offendersObj.list.files.length" when="{'one': '1 file', 'other': '{} files'}"></ng-pluralize></h3>
<div class="table">
<div class="headers">
<div>File</div>
<div>Current weight</div>
<div>Minified</div>
<div>Gain</div>
</div>
<div ng-repeat="file in rule.offendersObj.list.files | orderBy:'-gain'">
<div>
<url-link url="file.url" max-length="70"></url-link>
</div>
<div ng-if="!file.afterCompression">{{file.original | bytes}} (gzipped)</div>
<div ng-if="file.afterCompression">{{file.original | bytes}} ({{file.afterCompression | bytes}} gzipped)</div>
<div ng-if="!file.afterCompression">{{file.afterOptimizationAndCompression | bytes}} (gzipped)</div>
<div ng-if="file.afterCompression">{{file.optimized | bytes}} ({{file.afterOptimizationAndCompression | bytes}} gzipped)</div>
<div><b>-{{file.gain | bytes}}</b></div>
</div>
</div>
</div>
<div ng-if="policyName === 'totalRequests'">
<h3>Requests by MIME type</h3>
<div ng-repeat="(type, requests) in rule.offendersObj.list.byType">
<h3><ng-pluralize count="requests.length" when="{'0': 'No ' + type + ' request', 'one': '1 ' + type + ' request', 'other': '{} ' + type + ' requests'}"></ng-pluralize></h3>
<p ng-if="type == 'css' && requests.length > 2">Reduce the number of stylesheets by concatenating them.</p>
<p ng-if="type == 'js' && requests.length > 3">Reduce the number of scripts by concatenating them.</p>
<p ng-if="type == 'image' && requests.length > 5">Reduce the number of images by lazyloading them or by spriting them.</p>
<p ng-if="type == 'webfont' && requests.length > 1">Fonts are generally loaded on the critical path of the head. Load as few as possible.</p>
<p ng-if="type == 'other' && requests.length > 0">They can be Flash, XML, music or any undetected format.</p>
<div class="offendersTable">
<div ng-repeat="request in requests track by $index">
<div><url-link url="request" max-length="100"></url-link></div>
</div>
</div>
</div>
</div>
<div ng-if="policyName === 'smallRequests'">
<div ng-repeat="(type, requests) in rule.offendersObj.list.byType">
<h3><ng-pluralize count="requests.length" when="{'0': 'small ' + type + ' file', 'one': '1 small ' + type + ' file', 'other': '{} small ' + type + ' files'}"></ng-pluralize></h3>
<p ng-if="type == 'css' && requests.length > 0">Try to inline these styles in the head of the HTML or to merge them with other files.</p>
<p ng-if="type == 'js' && requests.length > 0">Try to inline these scripts in the HTML or merge them with other files.</p>
<p ng-if="type == 'image' && requests.length > 0">Try to inline these images (with base64 encoding for most image types except SVG that don't need base64 encoding). You can also create sprites.</p>
<div class="table">
<div class="headers">
<div ng-if="type == 'image'">Preview</div>
<div>File</div>
<div>Weight (bytes)</div>
</div>
<div ng-repeat="request in requests track by $index">
<div ng-if="type == 'image'"><img ng-src="{{request.url}}" class="smallPreview checker" /></div>
<div><url-link url="request.url" max-length="100"></url-link></div>
<div>{{request.size}}</div>
</div>
</div>
</div>
</div>
<div ng-if="policyName === 'DOMaccesses'">
<h3>{{rule.value}} offenders</h3>
Please open the <a href="/result/{{runId}}/timeline">JS timeline</a>
</div>
<div ng-if="policyName === 'queriesWithoutResults'">
<h3>{{rule.value}} offenders</h3>
Please open the <a href="/result/{{runId}}/timeline#filter=queryWithoutResults">JS timeline, filtered by "Queries without results"</a>
</div>
<div ng-if="policyName === 'jQueryCallsOnEmptyObject'">
<h3>{{rule.value}} offenders</h3>
Please open the <a href="/result/{{runId}}/timeline#filter=jQueryCallOnEmptyObject">JS timeline, filtered by "jQuery calls on empty object"</a>
</div>
<div ng-if="policyName === 'jQueryNotDelegatedEvents'">
<h3>{{rule.value}} offenders</h3>
Please open the <a href="/result/{{runId}}/timeline#filter=eventNotDelegated">JS timeline, filtered by "Events not delegated"</a>
</div>
<div ng-if="!rule && rule !== null" class="notFound">
<h2>404</h2>
Rule "{{policyName}}"" not found
+15
View File
@@ -0,0 +1,15 @@
<div ng-include="'views/resultSubHeader.html'"></div>
<div class="screenshot board">
<h2>Screenshot</h2>
<div class="screenshotWrapper" ng-class="result.params.options.device || 'desktop'">
<div>
<img ng-if="result.screenshotUrl" class="screenshotImage" ng-src="{{result.screenshotUrl}}"/>
<span ng-if="!result.screenshotUrl" class="screenshotError">Screenshot not available</span>
</div>
</div>
<p>(scroll on the screenshot to see under the fold)</p>
<div class="backToDashboard"><a href="#" ng-click="backToDashboard()">Back to dashboard</a></div>
</div>
+47 -91
View File
@@ -1,7 +1,14 @@
<div ng-include="'views/resultSubHeader.html'"></div>
<div class="execution board">
<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="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">
@@ -9,18 +16,20 @@
<div ng-repeat="duration in timeline track by $index"
class="interval"
ng-class="{
domCreation: $index * timelineIntervalDuration < result.toolsResults.phantomas.metrics.domInteractive,
domInteractive: $index * timelineIntervalDuration >= result.toolsResults.phantomas.metrics.domInteractive
&& $index * timelineIntervalDuration < result.toolsResults.phantomas.metrics.domContentLoaded,
domContentLoaded: $index * timelineIntervalDuration >= result.toolsResults.phantomas.metrics.domContentLoaded
&& $index * timelineIntervalDuration < result.toolsResults.phantomas.metrics.domContentLoadedEnd,
domContentLoadedEnd: $index * timelineIntervalDuration >= result.toolsResults.phantomas.metrics.domContentLoadedEnd
&& $index * timelineIntervalDuration < result.toolsResults.phantomas.metrics.domComplete,
domComplete: $index * timelineIntervalDuration >= result.toolsResults.phantomas.metrics.domComplete
domCreation: $index * timelineIntervalDuration < result.javascriptExecutionTree.data.domInteractive,
domInteractive: $index * timelineIntervalDuration >= result.javascriptExecutionTree.data.domInteractive
&& $index * timelineIntervalDuration < result.javascriptExecutionTree.data.domContentLoaded,
domContentLoaded: $index * timelineIntervalDuration >= result.javascriptExecutionTree.data.domContentLoaded
&& $index * timelineIntervalDuration < result.javascriptExecutionTree.data.domContentLoadedEnd,
domContentLoadedEnd: $index * timelineIntervalDuration >= result.javascriptExecutionTree.data.domContentLoadedEnd
&& $index * timelineIntervalDuration < result.javascriptExecutionTree.data.domComplete,
domComplete: $index * timelineIntervalDuration >= result.javascriptExecutionTree.data.domComplete
}">
<div style="height: {{100 * duration / timelineMax | number: 0}}px" class="color"></div>
<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>
Timestamp: {{$index * timelineIntervalDuration | number: 0}} ms
</div>
</div>
</div>
</div>
@@ -36,7 +45,7 @@
<div class="domComplete"><div class="color"></div>Page is complete</div>
</div>
<div class="tips">
<div>Executing Javascript and DOM queries here is a <b>bad practice</b> and slows down the DOM construction.</div>
<div>Executing JavaScript and DOM queries here is a <b>bad practice</b> and slows down the DOM construction.</div>
<div>Some frameworks do things here, but it's not reliable and should be avoided.</div>
<div>Also known as "document ready". This is where you should execute <b>top-priority</b> scripts, like binding action buttons or launch a video player.</div>
<div>Here you can execute <b>mid-priority</b> tasks. Loading a script with createElement('script') is one way to do so.</div>
@@ -45,24 +54,36 @@
</div>
</div>
<h2>Javascript Profiler</h2>
<h2>JavaScript Profiler</h2>
<p>
The table below shows the interactions between Javascript and the DOM. It is useful to understand what happens while the page loads.
The table below shows the interactions between the JavaScript and the DOM. It is useful to understand what happens while the page loads.
</p>
<div class="filters">
<div>
<input type="checkbox" ng-model="warningsFilterOn" id="warningsFilterOn" />
<label for="warningsFilterOn">Show warnings only</label>
</div>
<div>
<input type="checkbox" ng-model="textFilterOn" />
Filter by
<input type="text" ng-model="textFilter" placeholder="search..." class="textFilter" ng-change="textFilterOn = true" />
<input type="checkbox" name="warningsFilter" ng-model="warningsFilterOn" id="warningsFilterOn" />
<label for="warningsFilterOn">Filter on warnings and errors</label>
<div class="subFilters" ng-if="warningsFilterOn">
<div>
<input type="checkbox" name="filters" ng-model="warningsFilters.queryWithoutResults" id="queryWithoutResultsFilterOn"/>
<label for="queryWithoutResultsFilterOn">Queries without results</label>
</div>
<div>
<input type="checkbox" name="filters" ng-model="warningsFilters.jQueryCallOnEmptyObject" id="jQueryCallOnEmptyObjectFilterOn" />
<label for="jQueryCallOnEmptyObjectFilterOn">jQuery calls on empty object</label>
</div>
<div>
<input type="checkbox" name="filters" ng-model="warningsFilters.eventNotDelegated" id="eventNotDelegatedFilterOn" />
<label for="eventNotDelegatedFilterOn">Events not delegated</label>
</div>
<div>
<input type="checkbox" name="filters" ng-model="warningsFilters.jsError" id="jsErrorFilterOn" />
<label for="jsErrorFilterOn">Errors</label>
</div>
</div>
</div>
{{queryWithoutResultsFilterOn}}
</div>
<div class="table">
<toto data-info="mavariable"></toto>
<div class="table" ng-class="{queryWithoutResultsFilterOn: warningsFilterOn && warningsFilters.queryWithoutResults, jQueryCallOnEmptyObjectFilterOn: warningsFilterOn && warningsFilters.jQueryCallOnEmptyObject, eventNotDelegatedFilterOn: warningsFilterOn && warningsFilters.eventNotDelegated, jsErrorFilterOn: warningsFilterOn && warningsFilters.jsError}">
<div class="headers">
<div><!-- index --></div>
<div>Type</div>
@@ -70,74 +91,9 @@
<div><!-- details --></div>
<div>Timestamp</div>
</div>
<div ng-if="(!warningsFilterOn || node.warning || node.error) && (!textFilterOn || !textFilter.length || node.searchIndex.indexOf(textFilter) >= 0)"
ng-repeat="node in profilerData" ng-class="{
showingDetails: node.showDetails,
jsError: node.error,
windowPerformance: node.windowPerformance
}">
<div class="index">{{$index + 1}}</div>
<div class="type">{{node.data.type}}</div>
<profiler-line ng-repeat="node in profilerData" data-index="$index" node="node"></profiler-line>
<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.warning && !node.error, 'icon-warning': node.warning || node.error}"
ng-click="onNodeDetailsClick(node)"
ng-if="node.data.type != 'jQuery loaded' && node.data.type != 'jQuery version change' && !node.windowPerformance"></div>
<div class="detailsOverlay" ng-if="node.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.parsedBacktrace">
<h4>Backtrace</h4>
<div class="table">
<div ng-repeat="trace in node.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.parsedBacktrace.length == 0 && node.data.type != 'script loaded'">
<div>can't find any backtrace :/</div>
</div>
</div>
</div>
</div>
</div>
<div class="startTime" ng-class="node.data.loadingStep">{{node.data.timestamp | number: 0}} ms</div>
</div>
</div>
<div class="backToDashboard"><a href="#" ng-click="backToDashboard()">Back to dashboard</a></div>
+1 -1
View File
@@ -14,7 +14,7 @@ var yellowLabTools = function(url, options) {
} else {
if (url.indexOf('http://') !== 0 && url.indexOf('https://') !== 0) {
if (url.toLowerCase().indexOf('http://') !== 0 && url.toLowerCase().indexOf('https://') !== 0) {
url = 'http://' + url;
}
+824 -229
View File
File diff suppressed because it is too large Load Diff
+52 -51
View File
@@ -1,9 +1,29 @@
{
"categories": {
"pageWeight": {
"label": "Page weight",
"policies": {
"totalWeight": 5,
"imageOptimization": 2,
"gzipCompression": 2,
"fileMinification": 1
}
},
"requests": {
"label": "Requests",
"policies": {
"totalRequests": 5,
"domains": 3,
"notFound": 3,
"multipleRequests": 2,
"smallRequests": 1,
"lazyLoadableImagesBelowTheFold": 2
}
},
"domComplexity": {
"label": "DOM complexity",
"policies": {
"DOMelementsCount": 1,
"DOMelementsCount": 3,
"DOMelementMaxDepth": 1,
"iframesCount": 1,
"DOMidDuplicated": 1
@@ -12,11 +32,16 @@
"domManipulations": {
"label": "DOM manipulations",
"policies": {
"DOMinserts": 2,
"DOMqueries": 1,
"DOMqueriesWithoutResults": 2,
"DOMqueriesAvoidable": 2,
"eventsBound": 1
"DOMaccesses": 3,
"queriesWithoutResults": 1,
"DOMqueriesAvoidable": 1
}
},
"scroll": {
"label": "Scroll bottlenecks",
"policies": {
"eventsScrollBound": 1,
"DOMaccessesOnScroll": 4
}
},
"badJavascript": {
@@ -28,17 +53,14 @@
"globalVariables": 0.5
}
},
"jQueryVersion": {
"label": "jQuery version",
"jQuery": {
"label": "jQuery",
"policies": {
"jQueryVersion": 5,
"jQueryDifferentVersions": 0.1
}
},
"cssSyntaxError": {
"label": "CSS syntax errors",
"policies": {
"cssParsingErrors": 1
"jQueryVersion": 2,
"jQueryVersionsLoaded": 2,
"jQueryFunctionsUsed": 1,
"jQueryCallsOnEmptyObject": 1,
"jQueryNotDelegatedEvents": 1
}
},
"cssComplexity": {
@@ -46,12 +68,14 @@
"policies": {
"cssRules": 2,
"cssComplexSelectors": 2,
"cssComplexSelectorsByAttribute": 1.5
"cssComplexSelectorsByAttribute": 1.5,
"cssColors": 0.5
}
},
"badCSS": {
"label": "Bad CSS",
"policies": {
"cssParsingErrors": 4,
"cssImports": 3,
"cssDuplicatedSelectors": 2,
"cssDuplicatedProperties": 1,
@@ -65,50 +89,27 @@
"cssRedundantChildNodesSelectors": 1
}
},
"requests": {
"label": "Requests number",
"serverConfig": {
"label": "Server config",
"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": 3,
"closedConnections": 3,
"multipleRequests": 3,
"closedConnections": 2,
"cachingNotSpecified": 1,
"cachingDisabled": 1,
"cachingTooShort": 1,
"domains": 1
"cachingTooShort": 1
}
}
},
"globalScore": {
"domComplexity": 1,
"pageWeight": 3,
"requests": 3,
"domComplexity": 2,
"domManipulations": 2,
"scroll": 1,
"badJavascript": 1,
"jQueryVersion": 1,
"jQuery": 1,
"cssSyntaxError": 1,
"cssComplexity": 1,
"badCSS": 1,
"requests": 3,
"smallRequests": 1,
"network": 2
"serverConfig": 1
}
}
+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}|NaN) kB\)$/.exec(fileWithSize);
if (!parts) {
return {
file: fileWithSize
};
} else {
return {
file: parts[1],
size: parseFloat(parts[2])
};
}
};
};
module.exports = new OffendersHelpers();
+55 -17
View File
@@ -1,3 +1,5 @@
var extend = require('util')._extend;
var debug = require('debug')('ylt:ruleschecker');
var RulesChecker = function() {
@@ -21,31 +23,67 @@ var RulesChecker = function() {
rule = {
value: data.toolsResults[policy.tool].metrics[metricName],
policy: policy
policy: extend({}, policy) // Clone object policy instead of reference
};
// Take DOMqueriesAvoidable's offenders from DOMqueriesDuplicated, for example.
if (policy.takeOffendersFrom) {
var fromList = policy.takeOffendersFrom;
// Deal with offenders
if (policy.hasOffenders) {
var offenders = [];
// takeOffendersFrom option can be a string or an array of strings.
if (typeof fromList === 'string') {
fromList = [fromList];
// 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 = {};
fromList.forEach(function(from) {
offenders = offenders.concat(data.toolsResults[policy.tool].offenders[from]);
});
// 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) {
data.toolsResults[policy.tool].offenders[metricName] = offenders;
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;
}
if (data.toolsResults[policy.tool].offenders &&
data.toolsResults[policy.tool].offenders[metricName] &&
data.toolsResults[policy.tool].offenders[metricName].length > 0) {
rule.offenders = data.toolsResults[policy.tool].offenders[metricName];
}
rule.bad = rule.value > policy.isOkThreshold;
rule.abnormal = policy.isAbnormalThreshold && rule.value >= policy.isAbnormalThreshold;
+22 -5
View File
@@ -3,6 +3,7 @@ var debug = require('debug')('ylt:runner');
var phantomasWrapper = require('./tools/phantomas/phantomasWrapper');
var jsExecutionTransformer = require('./tools/jsExecutionTransformer');
var weightChecker = require('./tools/weightChecker/weightChecker');
var rulesChecker = require('./rulesChecker');
var scoreCalculator = require('./scoreCalculator');
@@ -19,14 +20,20 @@ var Runner = function(params) {
};
// Execute Phantomas first
phantomasWrapper.execute(data).then(function(phantomasResults) {
phantomasWrapper.execute(data)
.then(function(phantomasResults) {
data.toolsResults.phantomas = phantomasResults;
// Treat the JS Execution Tree from offenders
data.javascriptExecutionTree = jsExecutionTransformer.transform(data);
data = jsExecutionTransformer.transform(data);
// Other tools go here
// Redownload every file
return weightChecker.recheckAllFiles(data);
})
.then(function(data) {
// Rules checker
var policies = require('./metadata/policies');
@@ -42,11 +49,21 @@ var Runner = function(params) {
delete data.toolsResults.phantomas.metrics.javascriptExecutionTree;
delete data.toolsResults.phantomas.offenders.javascriptExecutionTree;
delete data.toolsResults.phantomas.metrics.scrollExecutionTree;
delete data.toolsResults.phantomas.offenders.scrollExecutionTree;
//Finished!
return data;
})
.then(function(data) {
// Finished!
deferred.resolve(data);
}).fail(function(err) {
})
.fail(function(err) {
debug('Run failed');
debug(err);
+170
View File
@@ -0,0 +1,170 @@
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.addWhiteBackground)
.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;
};
// If the page doesn't set a bg color, the default PhantomJS one is transparent
// When transforming PNG to JPG, transparent pixels become black.
// This is why we need to add a transparent background.
this.addWhiteBackground = function(image) {
var deferred = Q.defer();
// Create a canvas with the same dimensions as your image:
lwip.create(image.width(), image.height(), 'white', function(err, canvas){
if (err) {
debug('Could not create a white canvas');
debug(err);
deferred.reject(err);
} else {
// Paste original image on top of the canvas
canvas.paste(0, 0, image, function(err, image){
if (err) {
debug('Could not paste image on the white canvas');
debug(err);
deferred.reject(err);
} else {
// Now image has a white background...
debug('White background correctly added');
deferred.resolve(image);
}
});
}
});
return deferred.promise;
};
this.toBuffer = function(image) {
var deferred = Q.defer();
image.toBuffer('jpg', {quality: 90}, function(err, buffer){
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();
+157 -49
View File
@@ -1,6 +1,8 @@
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');
@@ -18,16 +20,34 @@ var ApiController = function(app) {
// 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
partialResult: req.body.partialResult || null,
screenshot: req.body.screenshot || false,
jsTimeline: req.body.jsTimeline || false,
device: req.body.device || 'desktop',
waitForSelector: req.body.waitForSelector || null,
cookie: req.body.cookie || null,
authUser: req.body.authUser || null,
authPass: req.body.authPass || null
}
};
// 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);
@@ -49,81 +69,142 @@ var ApiController = function(app) {
debug('Launching test %s on %s', run.runId, run.params.url);
ylt(run.params.url)
var runOptions = {
screenshot: run.params.screenshot ? screenshot.getTmpFilePath() : false,
jsDeepAnalysis: run.params.jsTimeline,
device: run.params.device,
waitForSelector: run.params.waitForSelector,
cookie: run.params.cookie,
authUser: run.params.authUser,
authPass: run.params.authPass
};
.then(function(data) {
return ylt(run.params.url, runOptions);
debug('Success');
})
// 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)
// Save result in datastore
data.runId = run.runId;
resultsDatastore.saveResult(data)
.then(function() {
// Don't worry if there's an error
.fail(function(err) {
debug('An error occured while creating the screenshot\'s thumbnail. Ignoring and continuing...');
});
runsDatastore.markAsComplete(run.runId);
// Send result if the user was waiting
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) {
debug('Saving results to resultsDatastore failed:');
debug(err);
// Let's continue
screenshotPromise
res.status(500).send('Saving results failed');
});
// 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 = {};
data.scrollExecutionTree = {};
}
// 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(400).send('Bad request');
})
.finally(function() {
queue.remove(run.runId);
res.status(500).send('An error occured');
});
}).fail(function(err) {
console.error('Error or YLT\'s core instanciation');
console.error(err);
console.error(err.stack);
})
.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 not want to wait for the response, sending the run ID only
// 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;
@@ -168,6 +249,18 @@ var ApiController = function(app) {
// Retrive one result by id
app.get('/api/results/:id', function(req, res) {
getPartialResults(req.params.id, res, function(data) {
// Some fields can be excluded from the response, this way:
// /api/results/:id?exclude=field1,field2
if (req.query.exclude && typeof req.query.exclude === 'string') {
var excludedFields = req.query.exclude.split(',');
excludedFields.forEach(function(fieldName) {
if (data[fieldName]) {
delete data[fieldName];
}
});
}
return data;
});
});
@@ -221,6 +314,21 @@ var ApiController = function(app) {
});
}
// 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;
+1 -1
View File
@@ -7,7 +7,7 @@ var FrontController = function(app) {
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/rule/:policy', '/queue/:runId'];
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');
+40 -9
View File
@@ -9,24 +9,28 @@ 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) {
var promise = createResultFolder(testResults.runId);
return createResultFolder(testResults.runId)
debug('Saving results to disk...');
.then(function() {
return saveScreenshotIfExists(testResults);
})
promise.then(function() {
.then(function() {
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));
});
debug('Saving results to disk...');
return promise;
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));
});
};
@@ -84,6 +88,33 @@ function ResultsDatastore() {
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;
+7 -1
View File
@@ -58,6 +58,9 @@ function RunsDatastore() {
var errorMessage;
switch(err) {
case '1':
errorMessage = "Error 1: unknown error";
break;
case '252':
errorMessage = "Error 252: page timeout in Phantomas";
break;
@@ -65,7 +68,7 @@ function RunsDatastore() {
errorMessage = "Error 253: Phantomas config error";
break;
case '254':
errorMessage = "Error 254: page loading failed in Phantomas";
errorMessage = "Error 254: page loading failed in PhantomJS";
break;
case '255':
errorMessage = "Error 255: Phantomas error";
@@ -76,6 +79,9 @@ function RunsDatastore() {
case '1002':
errorMessage = "Error 1002: missing Phantomas metrics";
break;
case '1003':
errorMessage = "Error 1003: Phantomas not returning";
break;
default:
errorMessage = err;
}
+3
View File
@@ -1,4 +1,5 @@
var Q = require('q');
var debug = require('debug')('ylt:runsQueue');
function RunsQueue() {
@@ -11,6 +12,8 @@ function RunsQueue() {
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
@@ -0,0 +1,12 @@
var wwwRedirectMiddleware = function(req, res, next) {
'use strict';
// Redirect www.yellowlab.tools to yellowlab.tools without "www" (for SEO purpose)
if(/^www\.yellowlab\.tools/.test(req.headers.host)) {
res.redirect(301, req.protocol + '://' + req.headers.host.replace(/^www\./, '') + req.url);
} else {
next();
}
};
module.exports = wwwRedirectMiddleware;
+186 -24
View File
@@ -1,24 +1,68 @@
var debug = require('debug')('ylt:jsExecutionTransformer');
var offendersHelpers = require('../offendersHelpers');
var Collection = require('./phantomas/custom_modules/util/collection');
var jsExecutionTransformer = function() {
this.transform = function(data) {
var javascriptExecutionTree = {};
var jQueryFunctionsCollection = new Collection();
var metrics = {
domInteractive: 0,
domContentLoaded: 0,
domContentLoadedEnd: 0,
domComplete: 0,
debug('Starting JS execution transformation');
DOMaccesses: 0,
DOMaccessesOnScroll: 0,
queriesWithoutResults: 0
};
var offenders = {};
var hasjQuery = (data.toolsResults.phantomas.metrics.jQueryVersionsLoaded > 0);
if (hasjQuery) {
metrics.jQueryCalls = 0;
metrics.jQueryFunctionsUsed = 0;
metrics.jQueryCallsOnEmptyObject = 0;
metrics.jQueryNotDelegatedEvents = 0;
offenders.jQueryFunctionsUsed = [];
}
try {
debug('Starting JS execution transformation');
javascriptExecutionTree = JSON.parse(data.toolsResults.phantomas.offenders.javascriptExecutionTree[0]);
if (javascriptExecutionTree.children) {
javascriptExecutionTree.children.forEach(function(node) {
javascriptExecutionTree.children.forEach(function(node, index) {
// Mark abnormal things with a warning flag
var contextLenght = (node.data.callDetails && node.data.callDetails.context) ? node.data.callDetails.context.length : null;
if ((node.data.type === 'jQuery - bind' && contextLenght > 5) ||
node.data.resultsNumber === 0 ||
contextLenght === 0) {
var contextLength = (node.data.callDetails && node.data.callDetails.context) ? node.data.callDetails.context.length : null;
if (isABindWithoutEventDelegation(node, contextLength)) {
metrics.jQueryNotDelegatedEvents += contextLength;
node.warning = true;
node.eventNotDelegated = true;
}
if (node.data.resultsNumber === 0) {
metrics.queriesWithoutResults ++;
node.queryWithoutResults = true;
node.warning = true;
}
if (contextLength === 0) {
metrics.jQueryCallsOnEmptyObject ++;
node.jQueryCallOnEmptyObject = true;
node.warning = true;
}
if (node.data.type.indexOf('jQuery - ') === 0) {
metrics.jQueryCalls ++;
jQueryFunctionsCollection.push(node.data.type);
}
// Mark errors with an error flag
@@ -29,34 +73,152 @@ var jsExecutionTransformer = function() {
// Mark a performance flag
if (['domInteractive', 'domContentLoaded', 'domContentLoadedEnd', 'domComplete'].indexOf(node.data.type) >= 0) {
node.windowPerformance = true;
// Adjust the navigation timings (cause their not very well synchronised)
switch(node.data.type) {
case 'domInteractive':
javascriptExecutionTree.data.domInteractive = node.data.timestamp;
break;
case 'domContentLoaded':
javascriptExecutionTree.data.domContentLoaded = node.data.timestamp;
break;
case 'domContentLoadedEnd':
javascriptExecutionTree.data.domContentLoadedEnd = node.data.timestamp;
break;
case 'domComplete':
javascriptExecutionTree.data.domComplete = node.data.timestamp;
break;
}
}
// Fix rare bug when domComplete was never triggered
if (index === javascriptExecutionTree.children.length - 1 && !javascriptExecutionTree.data.domComplete) {
javascriptExecutionTree.data.domComplete = node.data.timestamp + 1000;
}
// Read the execution tree and adjust the navigation timings (cause their not very well synchronised)
switch(node.data.type) {
case 'domInteractive':
data.toolsResults.phantomas.metrics.domInteractive = node.data.timestamp;
break;
case 'domContentLoaded':
data.toolsResults.phantomas.metrics.domContentLoaded = node.data.timestamp;
break;
case 'domContentLoadedEnd':
data.toolsResults.phantomas.metrics.domContentLoadedEnd = node.data.timestamp;
break;
case 'domComplete':
data.toolsResults.phantomas.metrics.domComplete = node.data.timestamp;
break;
// Transform domPaths into objects
changeListOfDomPaths(node);
// Count the number of DOM accesses, by counting the tree leafs
metrics.DOMaccesses += countTreeLeafs(node);
});
// Count the number of different jQuery functions called
if (hasjQuery) {
jQueryFunctionsCollection.sort().forEach(function(fnName, cnt) {
if (fnName === 'jQuery - find') {
fnName = 'jQuery - $';
}
metrics.jQueryFunctionsUsed ++;
offenders.jQueryFunctionsUsed.push({
functionName: fnName.substring(9),
count: cnt
});
});
}
}
debug('JS execution transformation complete');
debug('Starting scroll execution transformation');
offenders.DOMaccessesOnScroll = JSON.parse(data.toolsResults.phantomas.offenders.scrollExecutionTree[0]);
if (offenders.DOMaccessesOnScroll.children) {
offenders.DOMaccessesOnScroll.children.forEach(function(node) {
// Mark a event flag
if (['documentScroll', 'windowScroll', 'window.onscroll'].indexOf(node.data.type) >= 0) {
node.windowPerformance = true;
}
// Transform domPaths into objects
changeListOfDomPaths(node);
// Count the number of DOM accesses, by counting the tree leafs
metrics.DOMaccessesOnScroll += countTreeLeafs(node);
});
}
debug('JS execution transformation complete');
debug('Scroll execution transformation complete');
} catch(err) {
throw err;
}
return javascriptExecutionTree;
data.javascriptExecutionTree = javascriptExecutionTree;
data.toolsResults.jsExecutionTransformer = {
metrics: metrics,
offenders: offenders
};
return data;
};
function treeRecursiveParser(node, fn) {
if (node.children) {
node.children.forEach(function(child) {
treeRecursiveParser(child, fn);
});
}
fn(node);
}
function changeListOfDomPaths(rootNode) {
treeRecursiveParser(rootNode, function(node) {
if (node.data.callDetails && node.data.callDetails.context && node.data.callDetails.context.length > 0) {
node.data.callDetails.context.elements = node.data.callDetails.context.elements.map(offendersHelpers.domPathToDomElementObj, offendersHelpers);
}
if (node.data.type === 'appendChild' || node.data.type === 'insertBefore' || node.data.type === 'getComputedStyle') {
node.data.callDetails.arguments[0] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[0]);
}
if (node.data.type === 'insertBefore') {
node.data.callDetails.arguments[1] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[1]);
}
});
}
// Returns the number of leafs (nodes without children)
function countTreeLeafs(rootNode) {
var count = 0;
treeRecursiveParser(rootNode, function(node) {
if (!node.children &&
!node.error &&
!node.windowPerformance &&
node.data.type !== 'jQuery loaded') {
count ++;
}
});
return count;
}
function isPureString(str) {
return typeof str === 'string' && str[0] !== '{' && str !== '(function)' && str !== '[Object]' && str !== '[Array]' && str !== 'true' && str !== 'false' && str !== 'undefined' && str !== 'unknown' && str !== 'null';
}
function isABindWithoutEventDelegation(node, contextLength) {
// Count only on larger bindings
if (contextLength <= 3) {
return false;
}
if (node.data.type === 'jQuery - on' && node.data.callDetails.arguments[1] && !isPureString(node.data.callDetails.arguments[1])) {
return true;
}
if (node.data.type.indexOf('jQuery - ') === 0 && node.children && node.children.length === 1) {
var child = node.children[0];
if (child.data.type === 'jQuery - on' && child.data.callDetails.arguments[1] && !isPureString(child.data.callDetails.arguments[1])) {
return true;
}
}
return false;
}
};
module.exports = new jsExecutionTransformer();
@@ -36,7 +36,7 @@ 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;
@@ -104,10 +104,11 @@ exports.module = function(phantomas) {
var root = new ContextTreeNode(null, {type: 'main'});
var currentContext = root;
var depth = 0;
if (deepAnalysis) {
phantomas.log('Entering deep Javascript analysis mode');
}
var depth = 0;
// Add a child but don't enter his context
function pushContext(data) {
@@ -176,6 +177,13 @@ exports.module = function(phantomas) {
return root;
}
// Empty the tree
function resetTree() {
root = new ContextTreeNode(null, {type: 'main'});
currentContext = root;
depth = 0;
}
function ContextTreeNode(parent, data) {
@@ -196,6 +204,7 @@ exports.module = function(phantomas) {
phantomas.leaveContext = leaveContext;
phantomas.getContextData = getContextData;
phantomas.readFullTree = readFullTree;
phantomas.resetTree = resetTree;
})();
@@ -0,0 +1,50 @@
/**
* Analyzes DOM hidden content
*/
/* global document: true, Node: true, window: true */
exports.version = '0.1.a';
exports.module = function(phantomas) {
'use strict';
// total length of HTML of hidden elements (i.e. display: none)
phantomas.setMetric('hiddenContentSize'); // @desc the size of content of hidden elements on the page (with CSS display: none) @offenders
// HTML size
phantomas.on('report', function() {
phantomas.evaluate(function() {
(function(phantomas) {
phantomas.spyEnabled(false, 'checking the hiddenContentSize');
var runner = new phantomas.nodeRunner();
runner.walk(document.body, function(node, depth) {
switch (node.nodeType) {
case Node.ELEMENT_NODE:
// @see https://developer.mozilla.org/en/DOM%3awindow.getComputedStyle
var styles = window.getComputedStyle(node);
if (styles && styles.getPropertyValue('display') === 'none') {
if (typeof node.innerHTML === 'string') {
var size = node.innerHTML.length;
phantomas.incrMetric('hiddenContentSize', size);
// log hidden containers bigger than 1 kB
if (size > 1024) {
phantomas.addOffender('hiddenContentSize', phantomas.getDOMPath(node) + ' (' + size + ' characters)');
}
}
// don't run for child nodes as they're hidden as well
return false;
}
break;
}
});
phantomas.spyEnabled(true);
}(window.__phantomas));
});
});
};
@@ -33,9 +33,6 @@ exports.module = function(phantomas) {
phantomas.enterContext({
type: 'getElementById',
callDetails: {
context: {
domElement: '#document'
},
arguments: ['#' + id]
},
backtrace: phantomas.getBacktrace()
@@ -65,7 +62,8 @@ exports.module = function(phantomas) {
type: 'getElementsByClassName',
callDetails: {
context: {
domElement: context
length: 1,
elements: [context]
},
arguments: ['.' + className]
},
@@ -103,7 +101,8 @@ exports.module = function(phantomas) {
type: 'getElementsByTagName',
callDetails: {
context: {
domElement: context
length: 1,
elements: [context]
},
arguments: [tagName]
},
@@ -145,7 +144,8 @@ exports.module = function(phantomas) {
type: 'querySelector',
callDetails: {
context: {
domElement: context
length: 1,
elements: [context]
},
arguments: [selector]
},
@@ -177,7 +177,8 @@ exports.module = function(phantomas) {
type: 'querySelectorAll',
callDetails: {
context: {
domElement: context
length: 1,
elements: [context]
},
arguments: [selector]
},
@@ -237,7 +238,8 @@ exports.module = function(phantomas) {
type: 'appendChild',
callDetails: {
context: {
domElement: context
length: 1,
elements: [context]
},
arguments: [appended]
},
@@ -245,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()
});
@@ -270,6 +277,68 @@ 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();
});
phantomas.spy(window, 'getComputedStyle', function(element, pseudoElement) {
var target = phantomas.getDOMPath(element);
phantomas.enterContext({
type: 'getComputedStyle',
callDetails: {
arguments: [target, pseudoElement]
},
backtrace: phantomas.getBacktrace()
});
}, function(result, args) {
phantomas.leaveContext();
});
})(window.__phantomas);
});
});
@@ -289,14 +358,19 @@ exports.module = function(phantomas) {
// 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) {
phantomas.log('DOM query: by %s - "%s" (using %s) in %s', type, query, fnName, context);
phantomas.incrMetric('DOMqueries');
if (context && context.indexOf('DocumentFragment') === -1) {
if (context && (
context.indexOf('html') === 0 ||
context.indexOf('body') === 0 ||
context.indexOf('head') === 0 ||
context.indexOf('#document') === 0
)) {
DOMqueries.push(type + ' "' + query + '" with ' + fnName + ' (in context ' + context + ')');
}
});
@@ -1,48 +0,0 @@
/**
* Analyzes events bound to DOM elements
*/
/* global Document: true, Element: true, window: true */
exports.version = '0.2.a';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('eventsBound'); // @desc number of EventTarget.addEventListener calls
// spy calls to EventTarget.addEventListener
// @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget.addEventListener
phantomas.once('init', function() {
phantomas.evaluate(function() {
(function(phantomas) {
function eventSpyBefore(eventType) {
/* jshint validthis: true */
var path = phantomas.getDOMPath(this);
//phantomas.log('DOM event: "' + eventType + '" bound to "' + path + '"');
phantomas.incrMetric('eventsBound');
phantomas.addOffender('eventsBound', '"%s" bound to "%s"', eventType, path);
phantomas.enterContext({
type: 'addEventListener',
callDetails: {
context: {
domElement: path
},
arguments: [eventType]
},
backtrace: phantomas.getBacktrace()
});
}
function eventSpyAfter(result) {
phantomas.leaveContext();
}
phantomas.spy(Element.prototype, 'addEventListener', eventSpyBefore, eventSpyAfter);
phantomas.spy(Document.prototype, 'addEventListener', eventSpyBefore, eventSpyAfter);
phantomas.spy(window, 'addEventListener', eventSpyBefore, eventSpyAfter);
})(window.__phantomas);
});
});
};
@@ -0,0 +1,82 @@
/**
* Analyzes events bound to DOM elements
*/
/* global Document: true, Element: true, window: true */
exports.version = '0.4.a';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('eventsBound'); // @desc number of EventTarget.addEventListener calls
phantomas.setMetric('eventsDispatched'); // @desc number of EventTarget.dispatchEvent calls
phantomas.setMetric('eventsScrollBound'); // @desc number of scroll event bounds
phantomas.once('init', function() {
phantomas.evaluate(function() {
(function(phantomas) {
// spy calls to EventTarget.addEventListener
// @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget.addEventListener
function eventSpyBefore(eventType) {
/* jshint validthis: true */
var path = phantomas.getDOMPath(this);
//phantomas.log('DOM event: "' + eventType + '" bound to "' + path + '"');
phantomas.incrMetric('eventsBound');
phantomas.addOffender('eventsBound', '"%s" bound to "%s"', eventType, path);
phantomas.log('event ' + eventType + ' bound');
phantomas.enterContext({
type: 'addEventListener',
callDetails: {
context: {
length: 1,
elements: [path]
},
arguments: [eventType]
},
backtrace: phantomas.getBacktrace()
});
// count window.addEventListener('scroll', ...) - issue #508
if (eventType === 'scroll' && (path === 'window' || path === '#document')) {
phantomas.incrMetric('eventsScrollBound');
phantomas.addOffender('eventsScrollBound', 'bound by %s on %s', phantomas.getBacktrace(), path);
}
}
function eventSpyAfter(result) {
phantomas.leaveContext();
}
phantomas.spy(Element.prototype, 'addEventListener', eventSpyBefore, eventSpyAfter);
phantomas.spy(Document.prototype, 'addEventListener', eventSpyBefore, eventSpyAfter);
phantomas.spy(window, 'addEventListener', eventSpyBefore, eventSpyAfter);
// spy calls to EventTarget.dispatchEvent
// @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget.dispatchEvent
phantomas.spy(Element.prototype, 'dispatchEvent', function(ev) {
/* jshint validthis: true */
var path = phantomas.getDOMPath(this);
phantomas.log('Core JS event: triggered "%s" on "%s"', ev.type, path);
phantomas.incrMetric('eventsDispatched');
phantomas.addOffender('eventsDispatched', '"%s" on "%s"', ev.type, path);
});
})(window.__phantomas);
});
});
phantomas.on('report', function() {
phantomas.evaluate(function() {
(function(phantomas) {
// Check if a window.onscroll function is defined
if (typeof(window.onscroll) === "function") {
phantomas.incrMetric('eventsScrollBound');
phantomas.addOffender('eventsScrollBound', 'bound by %s on %s', '', 'window.onscroll');
}
}(window.__phantomas));
});
});
};
@@ -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',
@@ -65,7 +79,7 @@ exports.module = function(phantomas) {
'bind',
'unbind',
// more events
// More events
'blur',
'change',
'click',
@@ -90,10 +104,9 @@ exports.module = function(phantomas) {
'scroll',
'select',
'submit',
'toggle',
'unload',
// attributes
// Attributes
'attr',
'prop',
'removeAttr',
@@ -102,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: {
@@ -141,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',
@@ -160,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]
},
@@ -184,16 +216,68 @@ exports.module = function(phantomas) {
}) || phantomas.log('jQuery: can not measure jQuerySizzleCalls (jQuery used on the page is too old)!');
phantomas.spy(jQuery.fn, 'init', function(selector, context) {
if (typeof selector === 'string' && /^#([\w\-]*)$/.exec(selector) !== null && !context) {
phantomas.enterContext({
type: 'jQuery - find',
callDetails: {
arguments: [selector]
},
backtrace: phantomas.getBacktrace()
});
}
}, function(result) {
var data = phantomas.getContextData();
if (data.type === 'jQuery - find' &&
!data.callDetails.context &&
data.callDetails.arguments.length === 1 &&
/^#([\w\-]*)$/.exec(data.callDetails.arguments[0]) !== null) {
var moreData = {
resultsNumber : (result && result.length) ? result.length : 0
};
phantomas.leaveContext(moreData);
}
});
if (!jQuery.event) {
phantomas.spy(jQuery.event, 'trigger', function(ev, data, elem) {
var path = phantomas.getDOMPath(elem),
type = ev.type || ev;
phantomas.log('Event: triggered "%s" on "%s"', type, path);
phantomas.incrMetric('jQueryEventTriggers');
phantomas.addOffender('jQueryEventTriggers', '"%s" on "%s"', type, path);
});
}
// jQuery events bound to window' onLoad event (#451)
phantomas.spy(jQuery.fn, 'on', function(eventName, func) {
if ((eventName === 'load') && (this[0] === window)) {
phantomas.incrMetric('jQueryWindowOnLoadFunctions');
phantomas.addOffender('jQueryWindowOnLoadFunctions', phantomas.getCaller(2));
}
});
// Add spys on many jQuery functions
jQueryFunctions.forEach(function(functionName) {
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)) {
@@ -211,6 +295,13 @@ exports.module = function(phantomas) {
} else {
try {
for (var key in arg) {
if (typeof arg[key] === 'function') {
arg[key] = '(function)';
}
}
arg = JSON.stringify(arg);
} catch(e) {
arg = '[Object]';
@@ -246,13 +337,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
},
@@ -260,45 +355,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);
});
};
@@ -4,44 +4,56 @@
* @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.1.a';
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
phantomas.setMetric('documentWriteCalls'); //@desc number of calls to either document.write or document.writeln
phantomas.setMetric('evalCalls'); // @desc number of calls to eval (either direct or via setTimeout / setInterval)
// 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() {
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()
/*phantomas.spy(window, 'eval', function(code) {
report('eval() called directly', phantomas.getCaller(), phantomas.getBacktrace(), 'evalCalls');
phantomas.log('eval\'ed code: ' + (code || '').substring(0, 150) + '(...)');
});*/
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) {
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) {
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) {
@@ -52,6 +64,6 @@ exports.module = function(phantomas) {
report('document.writeln() used', phantomas.getCaller(), phantomas.getBacktrace(), 'documentWriteCalls');
});
})(window.__phantomas);
});
}, spyEval);
});
};
@@ -1,29 +0,0 @@
/**
* Meters the number of page errors, and provides traces as offenders for "jsErrors" metric
*/
exports.version = '0.0';
exports.module = function(phantomas) {
'use strict';
/*phantomas.on('recv', function(entry, res) {
if (!entry.isJS) {
return;
}
// Yeah, this is weird, i'm sending the information back to the browser...
phantomas.evaluate(function(url) {
(function(phantomas) {
phantomas.pushContext({
type: 'script loaded',
callDetails: {
arguments: [url]
}
});
})(window.__phantomas);
}, entry.url);
});*/
};
@@ -9,11 +9,11 @@ exports.version = '0.1';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('javascriptExecutionTree'); // @desc number of duplicated DOM queries
phantomas.setMetric('javascriptExecutionTree');
// save data
phantomas.on('report', function() {
phantomas.log('Reading execution tree JSON');
phantomas.log('JS execution tree: Reading execution tree JSON');
phantomas.evaluate(function() {(function(phantomas) {
var fullTree = phantomas.readFullTree();
@@ -0,0 +1,23 @@
/**
* Retries download on every request to get the real file size
*
*/
exports.version = '0.1';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('requestsList');
var requests = [];
phantomas.on('recv', function(entry, res) {
requests.push(entry);
});
phantomas.on('report', function() {
phantomas.setMetric('requestsList', true, true);
phantomas.addOffender('requestsList', JSON.stringify(requests));
});
};
@@ -0,0 +1,69 @@
exports.version = '0.1';
exports.module = function(phantomas) {
'use strict';
phantomas.setMetric('scrollExecutionTree');
phantomas.on('report', function() {
phantomas.evaluate(function() {
(function(phantomas) {
var evt = document.createEvent('CustomEvent');
evt.initCustomEvent('scroll', false, false, null);
function triggerScrollEvent() {
phantomas.resetTree();
try {
// Chrome triggers them in this order:
// 1. document
phantomas.pushContext({
type: 'documentScroll'
});
document.dispatchEvent(evt);
// 2. window
phantomas.pushContext({
type: 'windowScroll'
});
window.dispatchEvent(evt);
// No need to call window.onscroll(), it's called by the scroll event on window
} catch(e) {
phantomas.log('ScrollListener error: %s', e);
}
}
var firstScrollTime = Date.now();
phantomas.log('ScrollListener: triggering a first scroll event...');
triggerScrollEvent();
// Ignore the first scroll event and only save the second one,
// because we want to detect un-throttled things, throttled ones are ok.
var secondScrollTime = Date.now();
phantomas.log('ScrollListener: triggering a second scroll event (%dms after the first)...', secondScrollTime - firstScrollTime);
triggerScrollEvent();
var fullTree = phantomas.readFullTree();
if (fullTree !== null) {
phantomas.setMetric('scrollExecutionTree', true, true);
phantomas.addOffender('scrollExecutionTree', JSON.stringify(fullTree));
phantomas.log('ScrollListener: scrollExecutionTree correctly extracted');
} else {
phantomas.log('Error: scrollExecutionTree could not be extracted');
}
phantomas.log('ScrollListener: end of scroll triggering');
})(window.__phantomas);
});
});
};
@@ -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;
+69 -33
View File
@@ -1,8 +1,9 @@
var async = require('async');
var Q = require('q');
var ps = require('ps-node');
var debug = require('debug')('ylt:phantomaswrapper');
var phantomas = require('phantomas');
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() {
@@ -10,44 +11,47 @@ var PhantomasWrapper = function() {
/**
* This is the phantomas launcher. It merges user chosen options into the default options
* Available options :
*
* - timeout : in seconds (default 60)
* - jsDeepAnalysis : should we inspect subrequests in the javascript execution tree?
*
*/
this.execute = function(data) {
var deferred = Q.defer();
var task = data.params;
var options = {
// Cusomizable options
timeout: task.options.timeout || 60,
'timeout': task.options.timeout || 45,
'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',
'user-agent': (task.options.device === 'desktop') ? 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.110 Safari/537.36' : null,
'tablet': (task.options.device === 'tablet'),
'phone': (task.options.device === 'phone'),
'screenshot': task.options.screenshot || false,
'wait-for-selector': task.options.waitForSelector,
'cookie': task.options.cookie,
'auth-user': task.options.authUser,
'auth-pass': task.options.authPass,
// Mandatory
reporter: 'json:pretty',
'reporter': 'json:pretty',
'analyze-css': true,
'skip-modules': [
'blockDomains', // not needed
'domHiddenContent', // overriden
'domMutations', // not compatible with webkit
'domQueries', // overriden
'eventListeners', // overridden
'events', // overridden
'filmStrip', // not needed
'har', // not needed for the moment
'javaScriptBottlenecks', // needs to be launched after custom module scopeYLT,
'javaScriptBottlenecks', // needs to be launched after custom module scopeYLT
'jQuery', // overridden
'jserrors', // overridden
'pageSource', // not needed
'screenshot', // not needed for the moment
'waitForSelector', // not needed
'windowPerformance' // overriden
].join(','),
'include-dirs': [
'lib/tools/phantomas/custom_modules/core',
'lib/tools/phantomas/custom_modules/modules'
path.join(__dirname, 'custom_modules/core'),
path.join(__dirname, 'custom_modules/modules')
].join(',')
};
@@ -55,42 +59,73 @@ var PhantomasWrapper = function() {
debug('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];
var value = options[opt];
if ((typeof value === 'string' || value instanceof String) && value.indexOf(' ') >= 0) {
value = '"' + value + '"';
}
if (value === true) {
optionsString += ' ' + '--' + opt;
} else if (value === false || value === null || value === undefined) {
// Nothing
} else {
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 isKilled = false;
// Kill phantomas if nothing happens
var killer = setTimeout(function() {
debug('Killing the app because the test on %s was launched %d seconds ago', task.url, 5*options.timeout);
// If in server mode, forever will restart the server
console.log('Killing phantomas because the test on ' + task.url + ' was launched ' + 5*options.timeout + ' seconds ago');
// Kill the Phantomas process first
if (phantomasPid) {
ps.kill(phantomasPid, function(err) {
if (err) {
debug('Could not kill Phantomas process %s', phantomasPid);
}
else {
debug('Phantomas process %s was correctly killed', phantomasPid);
// Suicide
process.exit(1);
// If in server mode, forever will restart the server
}
// Then suicide.
process.exit(1);
debug('Phantomas process %s was correctly killed', phantomasPid);
// Then mark the test as failed
// Error 1003 = Phantomas not answering
deferred.reject(1003);
isKilled = true;
});
} else {
// Suicide
process.exit(1);
}
}, 5*options.timeout*1000);
// It's time to launch the test!!!
var triesNumber = 2;
async.retry(triesNumber, function(cb) {
var process = phantomas(task.url, options, function(err, json, results) {
debug('Returning from Phantomas');
if (isKilled) {
debug('Process was killed, too late Phantomas, sorry...');
return;
}
debug('Returning from Phantomas with error %s', err);
// Adding some YellowLabTools errors here
if (json && json.metrics && !json.metrics.javascriptExecutionTree) {
if (json && json.metrics && (!json.metrics.javascriptExecutionTree || !json.offenders.javascriptExecutionTree)) {
err = 1001;
}
@@ -120,10 +155,11 @@ var PhantomasWrapper = function() {
if (err) {
debug('All ' + triesNumber + ' attemps failed for the test');
deferred.reject(err);
} else {
// Success!!!
deferred.resolve(json);
}
});
+343
View File
@@ -0,0 +1,343 @@
var debug = require('debug')('ylt:fileMinifier');
var Q = require('q');
var UglifyJS = require('uglify-js');
var CleanCSS = require('clean-css');
var Minimize = require('minimize');
var FileMinifier = function() {
function minifyFile(entry) {
var deferred = Q.defer();
if (!entry.weightCheck || !entry.weightCheck.body) {
// No valid file available
deferred.resolve(entry);
return deferred.promise;
}
var fileSize = entry.weightCheck.uncompressedSize;
debug('Let\'s try to optimize %s', entry.url);
debug('Current file size is %d', fileSize);
var startTime = Date.now();
if (entry.isJS && !isKnownAsMinified(entry.url) && !looksAlreadyMinified(entry.weightCheck.body)) {
debug('File is a JS');
return minifyJs(entry.weightCheck.body)
.then(function(newFile) {
if (!newFile) {
debug('Optimization didn\'t work');
return entry;
}
var endTime = Date.now();
var newFileSize = newFile.length;
debug('JS minification complete for %s', entry.url);
if (gainIsEnough(fileSize, newFileSize)) {
entry.weightCheck.bodyAfterOptimization = newFile;
entry.weightCheck.optimized = newFileSize;
entry.weightCheck.isOptimized = false;
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
}
return entry;
})
.fail(function(err) {
return entry;
});
} else if (entry.isCSS) {
debug('File is a CSS');
return minifyCss(entry.weightCheck.body)
.then(function(newFile) {
if (!newFile) {
debug('Optimization didn\'t work');
return entry;
}
var endTime = Date.now();
debug('CSS minification took %dms', endTime - startTime);
var newFileSize = newFile.length;
debug('CSS minification complete for %s', entry.url);
if (gainIsEnough(fileSize, newFileSize)) {
entry.weightCheck.bodyAfterOptimization = newFile;
entry.weightCheck.optimized = newFileSize;
entry.weightCheck.isOptimized = false;
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
}
return entry;
})
.fail(function(err) {
return entry;
});
} else if (entry.isHTML) {
debug('File is an HTML');
return minifyHtml(entry.weightCheck.body)
.then(function(newFile) {
if (!newFile) {
debug('Optimization didn\'t work');
return entry;
}
var endTime = Date.now();
debug('HTML minification took %dms', endTime - startTime);
var newFileSize = newFile.length;
debug('HTML minification complete for %s', entry.url);
if (gainIsEnough(fileSize, newFileSize)) {
entry.weightCheck.bodyAfterOptimization = newFile;
entry.weightCheck.optimized = newFileSize;
entry.weightCheck.isOptimized = false;
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
}
return entry;
})
.fail(function(err) {
return entry;
});
} else {
debug('Not minfiable type or already minified');
deferred.resolve(entry);
}
return deferred.promise;
}
// The gain is estimated of enough value if it's over 2KB or over 20%,
// but it's ignored if is below 400 bytes
function gainIsEnough(oldWeight, newWeight) {
var gain = oldWeight - newWeight;
var ratio = gain / oldWeight;
return (gain > 2096 || (ratio > 0.2 && gain > 400));
}
// Uglify
function minifyJs(body) {
// Splitting the Uglify function because it sometime takes too long (more than 10 seconds)
// I hope that, by splitting, it can be a little more asynchronous, so the application doesn't freeze.
return splittedUglifyStep1(body)
.delay(1)
.then(splittedUglifyStep2)
.delay(1)
.then(splittedUglifyStep3)
.delay(1)
.then(splittedUglifyStep4)
.delay(1)
.then(splittedUglifyStep5)
.delay(1)
.then(splittedUglifyStep6)
.delay(1)
.then(splittedUglifyStep7);
}
function splittedUglifyStep1(code) {
var deferred = Q.defer();
var startTime = Date.now();
try {
var toplevel_ast = UglifyJS.parse(code);
var endTime = Date.now();
debug('Uglify step 1 took %dms', endTime - startTime);
deferred.resolve(toplevel_ast);
} catch(err) {
debug('JS syntax error, Uglify\'s parser failed (step 1)');
deferred.reject(err);
}
return deferred.promise;
}
function splittedUglifyStep2(toplevel) {
var deferred = Q.defer();
var startTime = Date.now();
toplevel.figure_out_scope();
var endTime = Date.now();
debug('Uglify step 2 took %dms', endTime - startTime);
deferred.resolve(toplevel);
return deferred.promise;
}
function splittedUglifyStep3(toplevel) {
var deferred = Q.defer();
var startTime = Date.now();
var compressor = UglifyJS.Compressor({warnings: false});
var compressed_ast = toplevel.transform(compressor);
var endTime = Date.now();
debug('Uglify step 3 took %dms', endTime - startTime);
deferred.resolve(compressed_ast);
return deferred.promise;
}
function splittedUglifyStep4(compressed_ast) {
var deferred = Q.defer();
var startTime = Date.now();
compressed_ast.figure_out_scope();
var endTime = Date.now();
debug('Uglify step 4 took %dms', endTime - startTime);
deferred.resolve(compressed_ast);
return deferred.promise;
}
function splittedUglifyStep5(compressed_ast) {
var deferred = Q.defer();
var startTime = Date.now();
compressed_ast.compute_char_frequency();
var endTime = Date.now();
debug('Uglify step 5 took %dms', endTime - startTime);
deferred.resolve(compressed_ast);
return deferred.promise;
}
function splittedUglifyStep6(compressed_ast) {
var deferred = Q.defer();
var startTime = Date.now();
compressed_ast.mangle_names();
var endTime = Date.now();
debug('Uglify step 6 took %dms', endTime - startTime);
deferred.resolve(compressed_ast);
return deferred.promise;
}
function splittedUglifyStep7(compressed_ast) {
var deferred = Q.defer();
var startTime = Date.now();
var code = compressed_ast.print_to_string();
var endTime = Date.now();
debug('Uglify step 7 took %dms', endTime - startTime);
deferred.resolve(code);
return deferred.promise;
}
// Clear-css
function minifyCss(body) {
var deferred = Q.defer();
try {
var result = new CleanCSS({compatibility: 'ie8'}).minify(body);
deferred.resolve(result.styles);
} catch(err) {
deferred.reject(err);
}
return deferred.promise;
}
// HTMLMinifier
function minifyHtml(body) {
var deferred = Q.defer();
var minimize = new Minimize({
empty: true, // KEEP empty attributes
conditionals: true, // KEEP conditional internet explorer comments
spare: true // KEEP redundant attributes
});
minimize.parse(body, function (error, data) {
if (error) {
deferred.reject(error);
} else {
deferred.resolve(data);
}
});
return deferred.promise;
}
// Avoid loosing time trying to compress some JS libraries known as already compressed
function isKnownAsMinified(url) {
var result = false;
// Twitter
result = result || /^https?:\/\/platform\.twitter\.com\/widgets\.js/.test(url);
// Facebook
result = result || /^https:\/\/connect\.facebook\.net\/[^\/]*\/(sdk|all)\.js/.test(url);
// Google +1
result = result || /^https:\/\/apis\.google\.com\/js\/plusone\.js/.test(url);
// jQuery CDN
result = result || /^https?:\/\/code\.jquery\.com\/.*\.min.js/.test(url);
// Google Analytics
result = result || /^https?:\/\/(www|ssl)\.google-analytics\.com\/(.*)\.js/.test(url);
if (result === true) {
debug('This file is known as already minified. Skipping minification: %s', url);
}
return result;
}
// Avoid loosing tome trying to compress JS files if they alreay look minified
// by counting the number of lines compared to the total size.
// Less than 1000kb per line is suspicious
function looksAlreadyMinified(code) {
var linesCount = code.split(/\r\n|\r|\n/).length;
var linesRatio = code.length / linesCount;
var looksMinified = linesRatio > 1024;
debug('Lines ratio is %d bytes per line', Math.round(linesRatio));
debug(looksMinified ? 'It looks already minified' : 'It doesn\'t look minified');
return looksMinified;
}
function entryTypeCanBeMinified(entry) {
return entry.isJS || entry.isCSS || entry.isHTML;
}
return {
minifyFile: minifyFile,
minifyJs: minifyJs,
minifyCss: minifyCss,
minifyHtml: minifyHtml,
gainIsEnough: gainIsEnough,
entryTypeCanBeMinified: entryTypeCanBeMinified,
isKnownAsMinified: isKnownAsMinified
};
};
module.exports = new FileMinifier();
+100
View File
@@ -0,0 +1,100 @@
var debug = require('debug')('ylt:gzipCompressor');
var Q = require('q');
var zlib = require('zlib');
var GzipCompressor = function() {
function compressFile(entry) {
return gzipUncompressedFile(entry)
.then(gzipOptimizedFile);
}
// Gzip a file if it was not already gziped
function gzipUncompressedFile(entry) {
var deferred = Q.defer();
if (entryTypeCanBeGzipped(entry) && entry.weightCheck && !entry.weightCheck.isCompressed && entry.weightCheck.body) {
debug('Compression missing, trying to gzip file %s', entry.url);
var uncompressedSize = entry.weightCheck.uncompressedSize;
zlib.gzip(new Buffer(entry.weightCheck.body, 'utf8'), function(err, buffer) {
if (err) {
debug('Could not compress uncompressed file with gzip');
debug(err);
deferred.reject(err);
} else {
var compressedSize = buffer.length;
if (gainIsEnough(uncompressedSize, compressedSize)) {
debug('File correctly gziped, was %d and is now %d bytes', uncompressedSize, compressedSize);
entry.weightCheck.afterCompression = compressedSize;
} else {
debug('Gzip gain is not enough, was %d and is now %d bytes', uncompressedSize, compressedSize);
}
deferred.resolve(entry);
}
});
} else {
deferred.resolve(entry);
}
return deferred.promise;
}
// Gzip a file after minification or optimization if this step was successful
function gzipOptimizedFile(entry) {
var deferred = Q.defer();
if (entryTypeCanBeGzipped(entry) && entry.weightCheck && entry.weightCheck.isOptimized === false) {
debug('Trying to gzip file after minification: %s', entry.url);
var uncompressedSize = entry.weightCheck.optimized;
zlib.gzip(new Buffer(entry.weightCheck.bodyAfterOptimization, 'utf8'), function(err, buffer) {
if (err) {
debug('Could not compress minified file with gzip');
debug(err);
deferred.reject(err);
} else {
var compressedSize = buffer.length;
debug('Correctly gziped the minified file, was %d and is now %d bytes', uncompressedSize, compressedSize);
entry.weightCheck.afterOptimizationAndCompression = compressedSize;
deferred.resolve(entry);
}
});
} else {
deferred.resolve(entry);
}
return deferred.promise;
}
// The gain is estimated of enough value if it's over 1KB or over 20%,
// but it's ignored if is below 100 bytes
function gainIsEnough(oldWeight, newWeight) {
var gain = oldWeight - newWeight;
var ratio = gain / oldWeight;
return (gain > 2048 || (ratio > 0.2 && gain > 100));
}
function entryTypeCanBeGzipped(entry) {
return entry.isJS || entry.isCSS || entry.isHTML || entry.isJSON || entry.isSVG || entry.isTTF || entry.isXML || entry.isFavicon;
}
return {
compressFile: compressFile,
entryTypeCanBeGzipped: entryTypeCanBeGzipped
};
};
module.exports = new GzipCompressor();
+245
View File
@@ -0,0 +1,245 @@
var debug = require('debug')('ylt:imageOptimizer');
var Q = require('q');
var Imagemin = require('imagemin');
var jpegoptim = require('imagemin-jpegoptim');
var ImageOptimizer = function() {
var MAX_JPEG_QUALITY = 85;
var OPTIPNG_COMPRESSION_LEVEL = 1;
function optimizeImage(entry) {
var deferred = Q.defer();
if (!entry.weightCheck || !entry.weightCheck.body) {
// No valid file available
deferred.resolve(entry);
return deferred.promise;
}
var fileSize = entry.weightCheck.uncompressedSize;
debug('Let\'s try to optimize %s', entry.url);
debug('Current file size is %d', fileSize);
if (isJPEG(entry)) {
debug('File is a JPEG');
// Starting softly with a lossless compression
return compressJpegLosslessly(new Buffer(entry.weightCheck.body, 'binary'))
.then(function(newFile) {
if (!newFile) {
debug('Optimization didn\'t work');
return entry;
}
var newFileSize = newFile.contents.length;
debug('JPEG lossless compression complete for %s', entry.url);
if (gainIsEnough(fileSize, newFileSize)) {
entry.weightCheck.lossless = entry.weightCheck.optimized = newFileSize;
entry.weightCheck.isOptimized = false;
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
}
// Now let's compress lossy to MAX_JPEG_QUALITY
return compressJpegLossly(new Buffer(entry.weightCheck.body, 'binary'));
})
.then(function(newFile) {
if (!newFile) {
debug('Optimization didn\'t work');
return entry;
}
var newFileSize = newFile.contents.length;
debug('JPEG lossy compression complete for %s', entry.url);
if (gainIsEnough(fileSize, newFileSize)) {
if (entry.weightCheck.isOptimized !== false || newFileSize < entry.weightCheck.lossless) {
entry.weightCheck.optimized = newFileSize;
}
entry.weightCheck.lossy = newFileSize;
entry.weightCheck.isOptimized = false;
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
}
return entry;
})
.fail(function() {
return entry;
});
} else if (isPNG(entry)) {
debug('File is a PNG');
// Starting softly with a lossless compression
return compressPngLosslessly(new Buffer(entry.weightCheck.body, 'binary'))
.then(function(newFile) {
if (!newFile) {
debug('Optimization didn\'t work');
return entry;
}
var newFileSize = newFile.contents.length;
debug('PNG lossless compression complete for %s', entry.url);
debug('Old file size: %d', fileSize);
debug('New file size: %d', newFileSize);
debug('newgainIsEnough: %s', gainIsEnough(fileSize, newFileSize) ? 'true':'false');
if (gainIsEnough(fileSize, newFileSize)) {
entry.weightCheck.lossless = entry.weightCheck.optimized = newFileSize;
entry.weightCheck.isOptimized = false;
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
}
return entry;
})
.fail(function() {
return entry;
});
} else if (isSVG(entry)) {
debug('File is an SVG');
// Starting softly with a lossless compression
return compressSvgLosslessly(new Buffer(entry.weightCheck.body, 'utf8'))
.then(function(newFile) {
if (!newFile) {
debug('Optimization didn\'t work');
return entry;
}
var newFileSize = newFile.contents.length;
debug('SVG lossless compression complete for %s', entry.url);
if (gainIsEnough(fileSize, newFileSize)) {
entry.weightCheck.bodyAfterOptimization = newFile.contents.toString();
entry.weightCheck.lossless = entry.weightCheck.optimized = newFileSize;
entry.weightCheck.isOptimized = false;
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
}
return entry;
})
.fail(function() {
return entry;
});
} else {
debug('File type %s is not an optimizable image', entry.contentType);
deferred.resolve(entry);
}
return deferred.promise;
}
// The gain is estimated of enough value if it's over 2KB or over 20%,
// but it's ignored if is below 100 bytes
function gainIsEnough(oldWeight, newWeight) {
var gain = oldWeight - newWeight;
var ratio = gain / oldWeight;
return (gain > 2048 || (ratio > 0.2 && gain > 100));
}
function isJPEG(entry) {
return entry.isImage && entry.contentType === 'image/jpeg';
}
function isPNG(entry) {
return entry.isImage && entry.contentType === 'image/png';
}
function isSVG(entry) {
return entry.isImage && entry.isSVG;
}
function compressJpegLosslessly(imageBody) {
return imageminLauncher(imageBody, 'jpeg', false);
}
function compressJpegLossly(imageBody) {
return imageminLauncher(imageBody, 'jpeg', true);
}
function compressPngLosslessly(imageBody) {
return imageminLauncher(imageBody, 'png', false);
}
function compressSvgLosslessly(imageBody) {
return imageminLauncher(imageBody, 'svg', false);
}
function imageminLauncher(imageBody, type, lossy) {
var deferred = Q.defer();
var startTime = Date.now();
debug('Starting %s %s optimization', type, lossy ? 'lossy' : 'lossless');
var engine;
if (type === 'jpeg' && !lossy) {
engine = Imagemin.jpegtran();
} else if (type === 'jpeg' && lossy) {
engine = jpegoptim({max: MAX_JPEG_QUALITY});
} else if (type === 'png' && !lossy) {
engine = Imagemin.optipng({optimizationLevel: OPTIPNG_COMPRESSION_LEVEL});
} else if (type === 'svg' && !lossy) {
engine = Imagemin.svgo();
} else {
deferred.reject('No optimization engine found for imagemin');
}
try {
new Imagemin()
.src(imageBody)
.use(engine)
.run(function (err, files) {
if (err) {
deferred.reject(err);
} else {
deferred.resolve(files[0]);
var endTime = Date.now();
debug('Optimization for %s took %d ms', type, endTime - startTime);
}
});
} catch(err) {
deferred.reject(err);
}
return deferred.promise;
}
function entryTypeCanBeOptimized(entry) {
return isJPEG(entry) || isPNG(entry) || isSVG(entry);
}
return {
optimizeImage: optimizeImage,
compressJpegLosslessly: compressJpegLosslessly,
compressJpegLossly: compressJpegLossly,
compressPngLosslessly: compressPngLosslessly,
compressSvgLosslessly: compressSvgLosslessly,
gainIsEnough: gainIsEnough,
entryTypeCanBeOptimized: entryTypeCanBeOptimized
};
};
module.exports = new ImageOptimizer();
+501
View File
@@ -0,0 +1,501 @@
/*
* Redownloading every files after Phantomas has finished
* Checks weight and every kind of compression
*
*/
var debug = require('debug')('ylt:weightChecker');
var Q = require('q');
var http = require('http');
var zlib = require('zlib');
var async = require('async');
var request = require('request');
var imageOptimizer = require('./imageOptimizer');
var fileMinifier = require('./fileMinifier');
var gzipCompressor = require('./gzipCompressor');
var WeightChecker = function() {
var MAX_PARALLEL_DOWNLOADS = 10;
var REQUEST_TIMEOUT = 15000; // 15 seconds
// This function will re-download every asset and check if it could be optimized
function recheckAllFiles(data) {
var startTime = Date.now();
debug('Redownload started');
var deferred = Q.defer();
var requestsList = JSON.parse(data.toolsResults.phantomas.offenders.requestsList);
delete data.toolsResults.phantomas.metrics.requestsList;
delete data.toolsResults.phantomas.offenders.requestsList;
// Transform every request into a download function with a callback when done
var redownloadList = requestsList.map(function(entry) {
return function(callback) {
redownloadEntry(entry)
.then(imageOptimizer.optimizeImage)
.then(fileMinifier.minifyFile)
.then(gzipCompressor.compressFile)
.then(function(newEntry) {
callback(null, newEntry);
})
.fail(function(err) {
callback(err);
});
};
});
// Lanch all redownload functions and wait for completion
async.parallelLimit(redownloadList, MAX_PARALLEL_DOWNLOADS, function(err, results) {
if (err) {
debug(err);
deferred.reject(err);
} else {
debug('All files checked');
endTime = Date.now();
debug('Redownload took %d ms', endTime - startTime);
var metrics = {};
var offenders = {};
// Count requests
offenders.totalRequests = listRequestsByType(results);
metrics.totalRequests = offenders.totalRequests.total;
// Remove unwanted requests (redirections, about:blank)
results = results.filter(function(result) {
return (result !== null && result.weightCheck && result.weightCheck.bodySize > 0);
});
// Total weight
offenders.totalWeight = listRequestWeight(results);
metrics.totalWeight = offenders.totalWeight.totalWeight;
// Image compression
offenders.imageOptimization = listImageNotOptimized(results);
metrics.imageOptimization = offenders.imageOptimization.totalGain;
// File minification
offenders.fileMinification = listFilesNotMinified(results);
metrics.fileMinification = offenders.fileMinification.totalGain;
// Gzip compression
offenders.gzipCompression = listFilesNotGzipped(results);
metrics.gzipCompression = offenders.gzipCompression.totalGain;
// Small requests
offenders.smallRequests = listSmallRequests(results);
metrics.smallRequests = offenders.smallRequests.total;
data.toolsResults.weightChecker = {
metrics: metrics,
offenders: offenders
};
deferred.resolve(data);
}
});
return deferred.promise;
}
function listRequestWeight(requests) {
var results = {
totalWeight: 0,
byType: {
html: {
totalWeight: 0,
requests: []
},
css: {
totalWeight: 0,
requests: []
},
js: {
totalWeight: 0,
requests: []
},
json: {
totalWeight: 0,
requests: []
},
image: {
totalWeight: 0,
requests: []
},
video: {
totalWeight: 0,
requests: []
},
webfont: {
totalWeight: 0,
requests: []
},
other: {
totalWeight: 0,
requests: []
}
}
};
requests.forEach(function(req) {
var weight = ((typeof req.weightCheck.bodySize === 'number') ? req.weightCheck.bodySize + req.weightCheck.headersSize : req.contentLength) || 0;
var type = req.type || 'other';
results.totalWeight += weight;
results.byType[type].totalWeight += weight;
results.byType[type].requests.push({
url: req.url,
weight: weight
});
});
return results;
}
function listImageNotOptimized(requests) {
var results = {
totalGain: 0,
images: []
};
requests.forEach(function(req) {
if (req.weightCheck.bodySize > 0 && imageOptimizer.entryTypeCanBeOptimized(req) && req.weightCheck.isOptimized === false) {
var before = req.weightCheck.afterCompression || req.weightCheck.bodySize;
var after = req.weightCheck.afterOptimizationAndCompression || req.weightCheck.optimized;
var gain = before - after;
if (gain > 200) {
results.totalGain += gain;
results.images.push({
url: req.url,
original: req.weightCheck.bodySize,
isCompressed: req.weightCheck.isCompressed,
afterCompression: req.weightCheck.afterCompression,
afterOptimizationAndCompression: req.weightCheck.afterOptimizationAndCompression,
lossless: req.weightCheck.lossless,
lossy: req.weightCheck.lossy,
gain: gain
});
}
}
});
return results;
}
function listFilesNotMinified(requests) {
var results = {
totalGain: 0,
files: []
};
requests.forEach(function(req) {
if (req.weightCheck.bodySize > 0 && fileMinifier.entryTypeCanBeMinified(req) && req.weightCheck.isOptimized === false) {
var before = req.weightCheck.afterCompression || req.weightCheck.bodySize;
var after = req.weightCheck.afterOptimizationAndCompression || req.weightCheck.optimized;
var gain = before - after;
if (gain > 200) {
results.totalGain += gain;
results.files.push({
url: req.url,
original: req.weightCheck.bodySize,
isCompressed: req.weightCheck.isCompressed,
afterCompression: req.weightCheck.afterCompression,
afterOptimizationAndCompression: req.weightCheck.afterOptimizationAndCompression,
optimized: req.weightCheck.optimized,
gain: gain
});
}
}
});
return results;
}
function listFilesNotGzipped(requests) {
var results = {
totalGain: 0,
files: []
};
requests.forEach(function(req) {
if (req.weightCheck.uncompressedSize && req.weightCheck.isCompressed === false && req.weightCheck.afterCompression) {
var gain = req.weightCheck.uncompressedSize - req.weightCheck.afterCompression;
results.totalGain += gain;
results.files.push({
url: req.url,
original: req.weightCheck.uncompressedSize,
gzipped: req.weightCheck.afterCompression,
gain: gain
});
}
});
return results;
}
function listRequestsByType(requests) {
var results = {
total: 0,
byType: {
html: [],
css: [],
js: [],
json: [],
image: [],
video: [],
webfont: [],
other: []
}
};
requests.forEach(function(req) {
if (req.url !== 'about:blank') {
var type = req.type || 'other';
results.byType[type].push(req.url);
results.total ++;
}
});
return results;
}
function listSmallRequests(requests) {
var results = {
total: 0,
byType: {
css: [],
js: [],
image: []
}
};
requests.forEach(function(req) {
if (req.weightCheck.bodySize > 0 && req.weightCheck.bodySize < 2048) {
if (req.isCSS || req.isJS || req.isImage) {
results.byType[req.type].push({
url: req.url,
size: req.weightCheck.bodySize
});
results.total ++;
}
}
});
return results;
}
function redownloadEntry(entry) {
var deferred = Q.defer();
function downloadError(message) {
debug('Could not download %s Error: %s', entry.url, message);
entry.weightCheck = {
message: message
};
deferred.resolve(entry);
}
// Not downloaded again but will be counted in totalWeight
function notDownloadableFile(message) {
entry.weightCheck = {
message: message
};
deferred.resolve(entry);
}
// Not counted in totalWeight
function unwantedFile(message) {
debug(message);
deferred.resolve(entry);
}
if (entry.method !== 'GET') {
notDownloadableFile('only downloading GET');
return deferred.promise;
}
if (entry.status !== 200) {
unwantedFile('only downloading requests with status code 200');
return deferred.promise;
}
if (entry.url === 'about:blank') {
unwantedFile('not downloading about:blank');
return deferred.promise;
}
debug('Downloading %s', entry.url);
// Always add a gzip header before sending, in case the server listens to it
var reqHeaders = entry.requestHeaders;
reqHeaders['Accept-Encoding'] = 'gzip, deflate';
var requestOptions = {
method: entry.method,
url: entry.url,
headers: reqHeaders,
timeout: REQUEST_TIMEOUT
};
download(requestOptions, entry.contentType, function(error, result) {
if (error) {
if (error.code === 'ETIMEDOUT') {
downloadError('timeout after ' + REQUEST_TIMEOUT + 'ms');
} else {
downloadError('error while downloading: ' + error.code);
}
return;
}
debug('%s downloaded correctly', entry.url);
entry.weightCheck = result;
deferred.resolve(entry);
});
return deferred.promise;
}
// Inspired by https://github.com/cvan/fastHAR-api/blob/10cec585/app.js
function download(requestOptions, contentType, callback) {
var statusCode;
request(requestOptions)
.on('response', function(res) {
// Raw headers were added in NodeJS v0.12
// (https://github.com/joyent/node/issues/4844), but let's
// reconstruct them for backwards compatibility.
var rawHeaders = ('HTTP/' + res.httpVersion + ' ' + res.statusCode +
' ' + http.STATUS_CODES[res.statusCode] + '\r\n');
Object.keys(res.headers).forEach(function(headerKey) {
rawHeaders += headerKey + ': ' + res.headers[headerKey] + '\r\n';
});
rawHeaders += '\r\n';
var uncompressedSize = 0; // size after uncompression
var bodySize = 0; // bytes size over the wire
var body = ''; // plain text body (after uncompressing gzip/deflate)
var isCompressed = false;
function tally() {
if (statusCode !== 200) {
callback({code: statusCode});
return;
}
var result = {
body: body,
headersSize: Buffer.byteLength(rawHeaders, 'utf8'),
bodySize: bodySize,
isCompressed: isCompressed,
uncompressedSize: uncompressedSize
};
callback(null, result);
}
switch (res.headers['content-encoding']) {
case 'gzip':
var gzip = zlib.createGunzip();
gzip.on('data', function (data) {
body += data;
uncompressedSize += data.length;
}).on('end', function () {
isCompressed = true;
tally();
}).on('error', function(err) {
debug(err);
});
res.on('data', function (data) {
bodySize += data.length;
}).pipe(gzip);
break;
case 'deflate':
res.setEncoding('utf8');
var deflate = zlib.createInflate();
deflate.on('data', function (data) {
body += data;
uncompressedSize += data.length;
}).on('end', function () {
isCompressed = true;
tally();
}).on('error', function(err) {
debug(err);
});
res.on('data', function (data) {
bodySize += data.length;
}).pipe(deflate);
break;
default:
if (contentType === 'image/jpeg' || contentType === 'image/png') {
res.setEncoding('binary');
}
res.on('data', function (data) {
body += data;
uncompressedSize += data.length;
bodySize += data.length;
}).on('end', function () {
tally();
});
break;
}
})
.on('response', function(response) {
statusCode = response.statusCode;
})
.on('error', function(err) {
callback(err);
});
}
return {
recheckAllFiles: recheckAllFiles,
listRequestWeight: listRequestWeight,
redownloadEntry: redownloadEntry,
download: download
};
};
module.exports = new WeightChecker();
+37 -29
View File
@@ -1,6 +1,7 @@
{
"name": "yellowlabtools",
"version": "1.1.0",
"version": "1.7.0",
"description": "Online tool to audit a webpage for performance and front-end quality issues",
"repository": {
"type": "git",
"url": "git://github.com/gmetais/YellowLabTools.git"
@@ -10,44 +11,51 @@
},
"main": "./lib/index.js",
"dependencies": {
"async": "~0.9.0",
"body-parser": "~1.10.0",
"compression": "~1.2.2",
"cors": "^2.5.2",
"debug": "~2.1.0",
"express": "~4.10.6",
"phantomas": "1.8.0",
"ps-node": "0.0.3",
"q": "~1.1.2",
"rimraf": "~2.2.8"
"async": "1.2.1",
"body-parser": "1.13.1",
"clean-css": "3.3.0",
"compression": "1.5.0",
"cors": "2.7.1",
"debug": "2.2.0",
"express": "4.12.4",
"imagemin": "3.2.0",
"imagemin-jpegoptim": "4.0.0",
"lwip": "0.0.7",
"meow": "3.3.0",
"minimize": "1.4.1",
"phantomas": "1.11.0",
"ps-node": "0.0.4",
"q": "1.4.1",
"rimraf": "2.4.0",
"temporary": "0.0.8",
"uglify-js": "2.4.23"
},
"devDependencies": {
"chai": "^1.10.0",
"chai": "^3.0.0",
"grunt": "^0.4.5",
"grunt-blanket": "^0.0.8",
"grunt-contrib-clean": "^0.6.0",
"grunt-contrib-concat": "^0.5.0",
"grunt-contrib-copy": "^0.7.0",
"grunt-contrib-cssmin": "^0.11.0",
"grunt-contrib-htmlmin": "^0.3.0",
"grunt-contrib-jshint": "^0.10.0",
"grunt-contrib-less": "^0.12.0",
"grunt-contrib-uglify": "^0.7.0",
"grunt-env": "^0.4.2",
"grunt-contrib-concat": "^0.5.1",
"grunt-contrib-copy": "^0.8.0",
"grunt-contrib-cssmin": "^0.12.3",
"grunt-contrib-htmlmin": "^0.4.0",
"grunt-contrib-jshint": "^0.11.2",
"grunt-contrib-less": "^1.0.1",
"grunt-contrib-uglify": "^0.9.1",
"grunt-env": "^0.4.4",
"grunt-express": "^1.4.1",
"grunt-filerev": "^2.1.2",
"grunt-fontsmith": "^0.9.1",
"grunt-filerev": "^2.3.1",
"grunt-inline-angular-templates": "^0.1.5",
"grunt-line-remover": "^0.0.2",
"grunt-mocha-test": "^0.12.4",
"grunt-replace": "^0.8.0",
"grunt-mocha-test": "^0.12.7",
"grunt-replace": "^0.9.2",
"grunt-usemin": "^3.0.0",
"grunt-webfont": "^0.5.3",
"matchdep": "^0.3.0",
"mocha": "^2.1.0",
"phantomjs": "^1.9.13",
"request": "^2.51.0",
"sinon": "^1.12.1",
"sinon-chai": "^2.6.0"
"mocha": "^2.2.5",
"request": "^2.58.0",
"sinon": "^1.15.3",
"sinon-chai": "^2.8.0"
},
"scripts": {
"test": "grunt test"
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

+3 -3
View File
@@ -3,7 +3,7 @@
# APT-GET
sudo apt-get update
sudo apt-get install lsb-release libfontconfig1 libfreetype6 -y --force-yes
sudo apt-get install curl git python-software-properties -y --force-yes
sudo apt-get install curl git python-software-properties build-essential make g++ -y --force-yes
# Installation of NodeJS
sudo add-apt-repository -y ppa:chris-lea/node.js
@@ -12,7 +12,7 @@ sudo apt-get install -y nodejs
source ~/.profile
# Installation of some packages globally
npm install bower forever grunt-cli phantomjs -g
npm install bower forever grunt-cli -g
source ~/.profile
# Installation of YellowLabTools
@@ -30,4 +30,4 @@ grunt build
# Start the server
rm server_config/settings.json
cp server_config/settings-prod.json server_config/settings.json
NODE_ENV=production forever start -c "node --stack-size=65500" bin/server.js
NODE_ENV=production forever start -c "node --stack-size=262000" bin/server.js
+1 -3
View File
@@ -18,9 +18,7 @@ bower install --config.interactive=false --allow-root
# Front-end compilation
rm -rf front/build
npm install grunt-cli -g
npm install phantomjs -g
grunt build
# Restart the server
NODE_ENV=production forever start -c "node --stack-size=65500" bin/server.js
NODE_ENV=production forever start -c "node --stack-size=262000" bin/server.js
+167 -3
View File
@@ -16,6 +16,7 @@ describe('api', function() {
var syncRunResultUrl;
var asyncRunId;
var screenshotUrl;
it('should refuse a query with an invalid key', function(done) {
@@ -94,7 +95,13 @@ describe('api', function() {
url: serverUrl + '/api/runs',
body: {
url: wwwUrl + '/simple-page.html',
waitForResponse: true
waitForResponse: true,
screenshot: true,
device: 'tablet',
//waitForSelector: '*',
cookie: 'foo=bar',
authUser: 'joe',
authPass: 'secret'
},
json: true,
headers: {
@@ -131,7 +138,6 @@ describe('api', function() {
}, function(error, response, body) {
if (!error && response.statusCode === 302) {
console.log(response.headers.location);
response.headers.should.have.a.property('location').that.is.a('string');
response.headers.location.should.contain('/rules');
@@ -158,7 +164,27 @@ describe('api', function() {
body.should.have.a.property('scoreProfiles').that.is.an('object');
body.should.have.a.property('rules').that.is.an('object');
body.should.have.a.property('toolsResults').that.is.an('object');
// javascriptExecutionTree should only be filled if option jsTimeline is true
body.should.have.a.property('javascriptExecutionTree').that.is.an('object');
body.javascriptExecutionTree.should.deep.equal({});
// Check if settings are correctly sent and retrieved
body.params.options.should.have.a.property('device').that.equals('tablet');
//body.params.options.should.have.a.property('waitForSelector').that.equals('*');
body.params.options.should.have.a.property('cookie').that.equals('foo=bar');
body.params.options.should.have.a.property('authUser').that.equals('joe');
body.params.options.should.have.a.property('authPass').that.equals('secret');
// Check if the screenshot temporary file was correctly removed
body.params.options.should.not.have.a.property('screenshot');
// Check if the screenshot buffer was correctly removed
body.should.not.have.a.property('screenshotBuffer');
// Check if the screenshot url is here
body.should.have.a.property('screenshotUrl');
body.screenshotUrl.should.equal('/api/results/' + body.runId + '/screenshot.jpg');
screenshotUrl = body.screenshotUrl;
done();
@@ -177,7 +203,8 @@ describe('api', function() {
url: serverUrl + '/api/runs',
body: {
url: wwwUrl + '/simple-page.html',
waitForResponse: false
waitForResponse: false,
jsTimeline: true
},
json: true,
headers: {
@@ -516,4 +543,141 @@ describe('api', function() {
});
});
it('should return the entire object and exclude toolsResults', function(done) {
this.timeout(5000);
request({
method: 'GET',
url: serverUrl + '/api/results/' + asyncRunId + '?exclude=toolsResults',
json: true,
}, function(error, response, body) {
if (!error && response.statusCode === 200) {
body.should.have.a.property('runId').that.equals(asyncRunId);
body.should.have.a.property('params').that.is.an('object');
body.should.have.a.property('scoreProfiles').that.is.an('object');
body.should.have.a.property('rules').that.is.an('object');
body.should.have.a.property('javascriptExecutionTree').that.is.an('object');
body.should.not.have.a.property('toolsResults').that.is.an('object');
done();
} else {
done(error || response.statusCode);
}
});
});
it('should return the entire object and exclude params and toolsResults', function(done) {
this.timeout(5000);
request({
method: 'GET',
url: serverUrl + '/api/results/' + asyncRunId + '?exclude=toolsResults,params',
json: true,
}, function(error, response, body) {
if (!error && response.statusCode === 200) {
body.should.have.a.property('runId').that.equals(asyncRunId);
body.should.have.a.property('scoreProfiles').that.is.an('object');
body.should.have.a.property('rules').that.is.an('object');
body.should.have.a.property('javascriptExecutionTree').that.is.an('object');
body.should.not.have.a.property('params').that.is.an('object');
body.should.not.have.a.property('toolsResults').that.is.an('object');
done();
} else {
done(error || response.statusCode);
}
});
});
it('should return the entire object and don\'t exclude anything', function(done) {
this.timeout(5000);
request({
method: 'GET',
url: serverUrl + '/api/results/' + asyncRunId + '?exclude=',
json: true,
}, function(error, response, body) {
if (!error && response.statusCode === 200) {
body.should.have.a.property('runId').that.equals(asyncRunId);
body.should.have.a.property('scoreProfiles').that.is.an('object');
body.should.have.a.property('rules').that.is.an('object');
body.should.have.a.property('javascriptExecutionTree').that.is.an('object');
body.should.have.a.property('params').that.is.an('object');
body.should.have.a.property('toolsResults').that.is.an('object');
done();
} else {
done(error || response.statusCode);
}
});
});
it('should return the entire object and don\'t exclude anything', function(done) {
this.timeout(5000);
request({
method: 'GET',
url: serverUrl + '/api/results/' + asyncRunId + '?exclude=null',
json: true,
}, function(error, response, body) {
if (!error && response.statusCode === 200) {
body.should.have.a.property('runId').that.equals(asyncRunId);
body.should.have.a.property('scoreProfiles').that.is.an('object');
body.should.have.a.property('rules').that.is.an('object');
body.should.have.a.property('javascriptExecutionTree').that.is.an('object');
body.should.have.a.property('params').that.is.an('object');
body.should.have.a.property('toolsResults').that.is.an('object');
done();
} else {
done(error || response.statusCode);
}
});
});
it('should retrieve the screenshot', function(done) {
this.timeout(5000);
request({
method: 'GET',
url: serverUrl + screenshotUrl
}, function(error, response, body) {
if (!error && response.statusCode === 200) {
response.headers['content-type'].should.equal('image/jpeg');
done();
} else {
done(error || response.statusCode);
}
});
});
it('should fail on a unexistant screenshot', function(done) {
this.timeout(5000);
request({
method: 'GET',
url: serverUrl + '/api/results/000000/screenshot.jpg'
}, function(error, response, body) {
if (!error && response.statusCode === 404) {
done();
} else {
done(error || response.statusCode);
}
});
});
});
+47
View File
@@ -1,6 +1,9 @@
var should = require('chai').should();
var resultsDatastore = require('../../lib/server/datastores/resultsDatastore');
var fs = require('fs');
var path = require('path');
describe('resultsDatastore', function() {
var datastore = new resultsDatastore();
@@ -71,4 +74,48 @@ describe('resultsDatastore', function() {
done();
});
});
var testId3 = '555555';
var testData3 = {
runId: testId3,
other: {
foo: 'foo',
bar: 2
},
screenshotBuffer: fs.readFileSync(path.join(__dirname, '../fixtures/logo-large.png'))
};
it('should store a test with a screenshot', function(done) {
datastore.saveResult(testData3).then(function() {
done();
}).fail(function(err) {
done(err);
});
});
it('should have a normal result', function(done) {
datastore.getResult(testId3)
.then(function(results) {
results.should.not.have.a.property('screenshot');
done();
})
.fail(function(err) {
done(err);
});
});
it('should retrieve the saved image', function() {
datastore.getScreenshot(testId3)
.then(function(imageBuffer) {
imageBuffer.should.be.an.instanceof(Buffer);
done();
})
.fail(function(err) {
done(err);
});
});
});
+1 -1
View File
@@ -1,5 +1,5 @@
var should = require('chai').should();
var runsQueue = require('../../lib/server/datastores/runsQueue.js');
var runsQueue = require('../../lib/server/datastores/runsQueue');
describe('runsQueue', function() {
+128
View File
@@ -0,0 +1,128 @@
var should = require('chai').should();
var ScreenshotHandler = require('../../lib/screenshotHandler');
var fs = require('fs');
var path = require('path');
describe('screenshotHandler', function() {
var imagePath = path.join(__dirname, '../fixtures/logo-large.png');
var screenshot, lwipImage;
it('should open an image and return an lwip object', function(done) {
ScreenshotHandler.openImage(imagePath)
.then(function(image) {
lwipImage = image;
lwipImage.should.be.an('object');
lwipImage.width().should.equal(620);
lwipImage.height().should.equal(104);
done();
})
.fail(function(err) {
done(err);
});
});
it('should resize an lwip image', function(done) {
ScreenshotHandler.resizeImage(lwipImage, 310)
.then(function(image) {
lwipImage = image;
lwipImage.width().should.equal(310);
lwipImage.height().should.equal(52);
done();
})
.fail(function(err) {
done(err);
});
});
it('should transform a lwip image into a buffer', function(done) {
ScreenshotHandler.toBuffer(lwipImage)
.then(function(buffer) {
buffer.should.be.an.instanceof(Buffer);
done();
})
.fail(function(err) {
done(err);
});
});
it('should optimize an image and return a buffered version', function(done) {
ScreenshotHandler.optimize(imagePath, 200)
.then(function(buffer) {
buffer.should.be.an.instanceof(Buffer);
done();
})
.fail(function(err) {
done(err);
});
});
it('should provide a temporary file object', function() {
screenshot = ScreenshotHandler.getScreenshotTempFile();
screenshot.should.have.a.property('getTmpFolder').that.is.a('function');
screenshot.should.have.a.property('getTmpFilePath').that.is.a('function');
screenshot.should.have.a.property('toThumbnail').that.is.a('function');
screenshot.should.have.a.property('deleteTmpFile').that.is.a('function');
});
it('should have created the temporary folder', function() {
var folder = screenshot.getTmpFolder();
fs.existsSync(folder.path).should.equal(true);
});
it('should respond a temporary file', function() {
var file = screenshot.getTmpFilePath();
file.should.have.string('/screenshot.png');
});
it('should delete the temp folder when there is no file', function(done) {
var tmpFolderPath = screenshot;
screenshot.deleteTmpFile()
.delay(1000)
.then(function() {
fs.existsSync(screenshot.getTmpFolder().path).should.equal(false);
done();
})
.fail(function(err) {
done(err);
});
});
it('should delete the temp folder with the screenshot inside', function(done) {
screenshot = ScreenshotHandler.getScreenshotTempFile();
var tmpFolderPath = screenshot.getTmpFolder().path;
var tmpImagePath = path.join(tmpFolderPath, 'screenshot.png');
// Copy image
var testImage = fs.readFileSync(imagePath);
fs.writeFileSync(tmpImagePath, testImage);
fs.existsSync(tmpImagePath).should.equal(true);
screenshot.deleteTmpFile()
.delay(1000)
.then(function() {
fs.existsSync(tmpImagePath).should.equal(false);
fs.existsSync(tmpFolderPath).should.equal(false);
done();
})
.fail(function(err) {
done(err);
});
});
});
+224 -10
View File
@@ -1,13 +1,185 @@
var should = require('chai').should();
var rulesChecker = require('../../lib/rulesChecker');
describe('rulesChecker', function() {
describe('customPolicies', function() {
var policies = require('../../lib/metadata/policies.js');
var results;
it('should transform DOMelementMaxDepth offenders', function() {
results = rulesChecker.check({
"toolsResults": {
"phantomas": {
"metrics": {
"DOMelementMaxDepth": 3
},
"offenders": {
"DOMelementMaxDepth": [
"body > div#foo > span.bar"
]
}
}
}
}, policies);
results.should.have.a.property('DOMelementMaxDepth');
results.DOMelementMaxDepth.should.have.a.property('offendersObj').that.deep.equals({
"count": 1,
"tree": {
"body": {
"div#foo": {
"span.bar": 1
}
}
}
});
});
it('should transform DOMidDuplicated offenders', function() {
results = rulesChecker.check({
"toolsResults": {
"phantomas": {
"metrics": {
"DOMidDuplicated": 2
},
"offenders": {
"DOMidDuplicated": [
"colorswitch-30883-30865: 4 occurrences",
"foo: 1 occurrences"
]
}
}
}
}, policies);
results.should.have.a.property('DOMidDuplicated');
results.DOMidDuplicated.should.have.a.property('offendersObj').that.deep.equals({
"count": 2,
"list": [
{
"id": "colorswitch-30883-30865",
"occurrences": 4
},
{
"id": "foo",
"occurrences": 1
}
]
});
});
it('should transform DOMqueriesAvoidable offenders', function() {
results = rulesChecker.check({
"toolsResults": {
"phantomas": {
"metrics": {
"DOMqueriesAvoidable": 2
},
"offenders": {
"DOMqueriesDuplicated": [
"id \"#j2t-top-cart\" with getElementById (in context #document): 4 queries",
"class \".listingResult\" with getElementsByClassName (in context body > div#Global > div#Listing): 4 queries"
]
}
}
}
}, policies);
results.should.have.a.property('DOMqueriesAvoidable');
results.DOMqueriesAvoidable.should.have.a.property('offendersObj').that.deep.equals({
"count": 2,
"list": [
{
"query": "#j2t-top-cart",
"context": {
"type": "document"
},
"fn": "getElementById ",
"count": 4
},
{
"query": ".listingResult",
"context": {
"type": "domElement",
"element": "div#Listing",
"tree": {
"body": {
"div#Global": {
"div#Listing": 1
}
}
}
},
"fn": "getElementsByClassName ",
"count": 4
}
]
});
});
it('should transform jsErrors offenders', function() {
results = rulesChecker.check({
"toolsResults": {
"phantomas": {
"metrics": {
"jsErrors": 2
},
"offenders": {
"jsErrors": [
"TypeError: 'undefined' is not a function (evaluating 'this.successfullyCollected.bind(this)') - http://asset.easydmp.net/js/collect.js:1160 / callCollecte http://asset.easydmp.net/js/collect.js:1203 / callbackUpdateParams http://asset.easydmp.net/js/collect.js:1135 / http://asset.easydmp.net/js/collect.js:1191",
"TypeError: 'undefined' is not an object (evaluating 'd.readyState') - http://me.hunkal.com/p/:3"
]
}
}
}
}, policies);
results.should.have.a.property('jsErrors');
results.jsErrors.should.have.a.property('offendersObj').that.deep.equals({
"count": 2,
"list": [
{
"error": "TypeError: 'undefined' is not a function (evaluating 'this.successfullyCollected.bind(this)')",
"backtrace": [
{
"file": "http://asset.easydmp.net/js/collect.js",
"line": 1160
},
{
"file": "http://asset.easydmp.net/js/collect.js",
"line": 1203,
"functionName": "callCollecte"
},
{
"file": "http://asset.easydmp.net/js/collect.js",
"line": 1135,
"functionName": "callbackUpdateParams"
},
{
"file": "http://asset.easydmp.net/js/collect.js",
"line": 1191
}
]
},
{
"error": "TypeError: 'undefined' is not an object (evaluating 'd.readyState')",
"backtrace": [
{
"file": "http://me.hunkal.com/p/",
"line": 3
}
]
}
]
});
});
it('should grade correctly jQuery versions', function() {
var versions = {
'1.2.9': 0,
@@ -52,20 +224,20 @@ describe('rulesChecker', function() {
results.should.deep.equals({});
// If jQueryDifferentVersions is 0
// If jQueryVersionsLoaded is 0
results = rulesChecker.check({
"toolsResults": {
"phantomas": {
"metrics": {
"jQueryVersion": "1.6.0",
"jQueryDifferentVersions": 0
"jQueryVersionsLoaded": 0
}
}
}
}, policies);
results.should.not.have.a.property('jQueryVersion');
results.should.have.a.property('jQueryDifferentVersions');
results.jQueryDifferentVersions.should.have.a.property('score').that.equals(100);
results.should.have.a.property('jQueryVersionsLoaded');
results.jQueryVersionsLoaded.should.have.a.property('score').that.equals(100);
// If there are more than 1 jQuery version
@@ -74,14 +246,56 @@ describe('rulesChecker', function() {
"phantomas": {
"metrics": {
"jQueryVersion": "1.6.0",
"jQueryDifferentVersions": 2
"jQueryVersionsLoaded": 2
}
}
}
}, policies);
results.should.not.have.a.property('jQueryVersion');
results.should.have.a.property('jQueryDifferentVersions');
results.jQueryDifferentVersions.should.have.a.property('score').that.equals(0);
results.jQueryDifferentVersions.should.have.a.property('abnormal').that.equals(true);
results.should.have.a.property('jQueryVersionsLoaded');
results.jQueryVersionsLoaded.should.have.a.property('score').that.equals(0);
results.jQueryVersionsLoaded.should.have.a.property('abnormal').that.equals(true);
});
it('should transform cssParsingErrors offenders', function() {
results = rulesChecker.check({
"toolsResults": {
"phantomas": {
"metrics": {
"cssParsingErrors": 2
},
"offenders": {
"cssParsingErrors": [
"<http://www.sudexpress.com/skin/frontend/sudexpress/default/css/styles.css> (Error: CSS parsing failed: missing '}' @ 4:1)",
"<http://www.sudexpress.com/skin/frontend/sudexpress/default/css/reset.css> (Empty CSS was provided)"
]
}
}
}
}, policies);
results.should.have.a.property('cssParsingErrors');
results.cssParsingErrors.should.have.a.property('offendersObj').that.deep.equals({
"count": 2,
"list": [
{
"error": "Error: CSS parsing failed: missing '}'",
"file": "http://www.sudexpress.com/skin/frontend/sudexpress/default/css/styles.css",
"line": 4,
"column": 1
},
{
"error": "Empty CSS was provided",
"file": "http://www.sudexpress.com/skin/frontend/sudexpress/default/css/reset.css",
"line": null,
"column": null
}
]
});
});
// Enough for the moment, to be complete...
});

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