Browse Source

Merge pull request #322 from YellowLabTools/ylt-v2

YellowLab Tools V2 - beta version
Gaël Métais 4 years ago
parent
commit
fc13053545
100 changed files with 1701 additions and 5078 deletions
  1. 1 0
      .gitignore
  2. 2 2
      .travis.yml
  3. 5 23
      Gruntfile.js
  4. 3 3
      README.md
  5. 5 5
      bin/cli.js
  6. BIN
      doc/img/YLT-animated.gif
  7. BIN
      doc/img/home.png
  8. BIN
      doc/img/logo.png
  9. BIN
      doc/img/screenshot.png
  10. 4 1
      front/src/css/about.css
  11. 93 38
      front/src/css/dashboard.css
  12. 0 2
      front/src/css/icons.css
  13. 14 8
      front/src/css/index.css
  14. 0 2
      front/src/css/main.css
  15. 13 0
      front/src/css/queue.css
  16. 24 15
      front/src/css/rule.css
  17. 11 1
      front/src/css/screenshot.css
  18. 0 338
      front/src/css/timeline.css
  19. 0 1
      front/src/fonts/svg-icons/arrow-left3.svg
  20. 0 1
      front/src/fonts/svg-icons/bars.svg
  21. 0 1
      front/src/fonts/svg-icons/lab.svg
  22. 0 1
      front/src/fonts/svg-icons/list.svg
  23. 0 1
      front/src/fonts/svg-icons/loop.svg
  24. 0 1
      front/src/fonts/svg-icons/mobile.svg
  25. 0 1
      front/src/fonts/svg-icons/question.svg
  26. 0 1
      front/src/fonts/svg-icons/screen.svg
  27. 0 1
      front/src/fonts/svg-icons/tablet.svg
  28. 0 1
      front/src/fonts/svg-icons/warning.svg
  29. BIN
      front/src/img/favicon-fail.png
  30. BIN
      front/src/img/favicon-success.png
  31. BIN
      front/src/img/favicon.png
  32. BIN
      front/src/img/logo-large.png
  33. 0 5
      front/src/js/app.js
  34. 61 3
      front/src/js/controllers/queueCtrl.js
  35. 0 62
      front/src/js/controllers/ruleCtrl.js
  36. 0 190
      front/src/js/controllers/timelineCtrl.js
  37. 11 642
      front/src/js/directives/offendersDirectives.js
  38. 2 2
      front/src/js/services/apiService.js
  39. 0 3
      front/src/js/services/menuService.js
  40. 1 1
      front/src/js/services/settingsService.js
  41. 5 1
      front/src/less/about.less
  42. 77 37
      front/src/less/dashboard.less
  43. 0 6
      front/src/less/icons.less
  44. 17 9
      front/src/less/index.less
  45. 61 19
      front/src/less/main.less
  46. 15 0
      front/src/less/queue.less
  47. 25 15
      front/src/less/rule.less
  48. 9 1
      front/src/less/screenshot.less
  49. 0 387
      front/src/less/timeline.less
  50. 2 6
      front/src/main.html
  51. 2 2
      front/src/views/about.html
  52. 10 6
      front/src/views/dashboard.html
  53. 13 12
      front/src/views/index.html
  54. 12 2
      front/src/views/queue.html
  55. 3 4
      front/src/views/resultSubHeader.html
  56. 114 94
      front/src/views/rule.html
  57. 1 1
      front/src/views/screenshot.html
  58. 0 100
      front/src/views/timeline.html
  59. 4 0
      lib/index.js
  60. 157 307
      lib/metadata/policies.js
  61. 23 35
      lib/metadata/scoreProfileGeneric.json
  62. 9 17
      lib/offendersHelpers.js
  63. 2 1
      lib/rulesChecker.js
  64. 60 15
      lib/runner.js
  65. 9 5
      lib/scoreCalculator.js
  66. 53 82
      lib/screenshotHandler.js
  67. 21 23
      lib/server/controllers/apiController.js
  68. 1 1
      lib/server/controllers/frontController.js
  69. 24 11
      lib/server/datastores/resultsDatastore.js
  70. 10 0
      lib/server/datastores/runsDatastore.js
  71. 1 1
      lib/tools/colorDiff.js
  72. 53 0
      lib/tools/domAccessAgregator.js
  73. 0 105
      lib/tools/isHttp2.js
  74. 0 210
      lib/tools/jsExecutionTransformer.js
  75. 0 251
      lib/tools/phantomas/custom_modules/core/scopeYLT/scopeYLT.js
  76. 0 30
      lib/tools/phantomas/custom_modules/modules/ajaxReqYLT/ajaxReqYLT.js
  77. 0 86
      lib/tools/phantomas/custom_modules/modules/domHiddenYLT/domHiddenYLT.js
  78. 0 387
      lib/tools/phantomas/custom_modules/modules/domQYLT/domQYLT.js
  79. 0 82
      lib/tools/phantomas/custom_modules/modules/eventYLT/eventYLT.js
  80. 0 381
      lib/tools/phantomas/custom_modules/modules/jQYLT/jQYLT.js
  81. 0 69
      lib/tools/phantomas/custom_modules/modules/javaScriptBottleYLT/javaScriptBottleYLT.js
  82. 0 49
      lib/tools/phantomas/custom_modules/modules/jsErrYLT/jsErrYLT.js
  83. 0 31
      lib/tools/phantomas/custom_modules/modules/jsTreeYLT/jsTreeYLT.js
  84. 0 81
      lib/tools/phantomas/custom_modules/modules/lazyLoadableYLT/lazyLoadableYLT.js
  85. 0 23
      lib/tools/phantomas/custom_modules/modules/requestsList/requestsList.js
  86. 0 69
      lib/tools/phantomas/custom_modules/modules/scrollListener/scrollListener.js
  87. 0 150
      lib/tools/phantomas/custom_modules/modules/windowPerfYLT/windowPerfYLT.js
  88. 38 136
      lib/tools/phantomas/phantomasWrapper.js
  89. 107 0
      lib/tools/redownload/brotliCompressor.js
  90. 64 16
      lib/tools/redownload/contentTypeChecker.js
  91. 27 125
      lib/tools/redownload/fileMinifier.js
  92. 83 7
      lib/tools/redownload/fontAnalyzer.js
  93. 9 8
      lib/tools/redownload/gzipCompressor.js
  94. 258 79
      lib/tools/redownload/redownload.js
  95. 47 47
      package.json
  96. 1 1
      server_config/server_install.sh
  97. 8 4
      server_config/settings-prod.json
  98. 6 2
      server_config/settings.json
  99. 0 33
      test/api/apiTest.js
  100. 10 60
      test/api/screenshotHandlerTest.js

+ 1 - 0
.gitignore

@@ -1,4 +1,5 @@
 node_modules
+package-lock.json
 .tmp
 .vagrant
 results/*

+ 2 - 2
.travis.yml

@@ -1,8 +1,8 @@
 language: node_js
 sudo: false
 node_js:
-  - "8.9"
-  - "6.2"
+  - "12.18"
+  - "14.7"
 env:
   - CXX=g++-4.8
 addons:

+ 5 - 23
Gruntfile.js

@@ -11,21 +11,6 @@ module.exports = function(grunt) {
         pkg: grunt.file.readJSON('package.json'),
         settings: grunt.file.readJSON('./server_config/settings.json'),
         
-        webfont: {
-            icons: {
-                src: 'front/src/fonts/svg-icons/*.svg',
-                dest: 'tmp',
-                destCss: 'front/src/less',
-                options: {
-                    engine: 'node',
-                    types: 'woff',
-                    stylesheet: 'less',
-                    embed: true,
-                    htmlDemo: false,
-                    syntax: 'bootstrap'
-                }
-            }
-        },
         less: {
             all: {
                 files: [
@@ -71,7 +56,10 @@ module.exports = function(grunt) {
                 'test/core/*.js',
                 'test/fixtures/*.js',
                 'front/src/js/**/*.js'
-            ]
+            ],
+            options: {
+                esversion: 6
+            }
         },
         clean: {
             tmp: {
@@ -103,7 +91,7 @@ module.exports = function(grunt) {
                 options: {
                     reporter: 'spec',
                 },
-                src: ['test/api/apiTest.js']
+                src: ['test/core/mediaQueriesCheckerTest.js']
             }
         },
         env: {
@@ -222,12 +210,6 @@ module.exports = function(grunt) {
         process.env.IS_TEST = true;
     });
 
-    grunt.registerTask('icons', [
-        'webfont:icons',
-        'less',
-        'clean:tmp'
-    ]);
-
     grunt.registerTask('build', [
         'jshint',
         'clean:build',

+ 3 - 3
README.md

@@ -14,7 +14,7 @@ Analyzes a webpage and detects **performance** or **front-end code quality** iss
             <a href="https://yellowlab.tools" target="_blank">https://yellowlab.tools</a>
         </td>
         <td width="30%">
-            <img src="./doc/img/YLT-animated.gif"></img>
+            <img src="./doc/img/home.png"></img>
         </td>
     </tr>
     <tr>
@@ -58,9 +58,9 @@ Analyzes a webpage and detects **performance** or **front-end code quality** iss
 
 ## How it works
 
-The tool loads the given URL via [PhantomJS](http://phantomjs.org/) (a headless browser) and collects various metrics and statistics with the help of [Phantomas](https://github.com/macbre/phantomas). These metrics are then categorized and transformed into scores. It also gives in-depth details so developpers can fix the detected issues.
+The tool loads the given URL and collects various metrics and statistics with the help of [Phantomas](https://github.com/macbre/phantomas). These metrics are then categorized and transformed into scores. It also gives in-depth details so developpers can fix the detected issues.
 
-By the way, it's free because we are geeks, not businessmen. All we want is a ★ on GitHub, it will boost our motivation to add more awesome features!!!
+By the way, it's free because I am a geek, not businessmen. In return, you can add a ★ on GitHub, it will boost my motivation to add more awesome features!!!
 
 ![example dashboard screenshot](./doc/img/screenshot.png)
 

+ 5 - 5
bin/cli.js

@@ -13,20 +13,20 @@ var cli = meow({
         '  yellowlabtools <url> <options>',
         '',
         'Options:',
-        '  --device             Use "phone" or "tablet" to simulate a mobile device (by user-agent and viewport size).',
+        '  --device             Simulates a device. Choose between phone (default), tablet, desktop and desktop-hd.',
         '  --screenshot         Will take a screenshot and use this value as the output path. It needs to end with ".png".',
         //'  --wait-for-selector  Once the page is loaded, Phantomas will wait until the given CSS selector matches some elements.',
         '  --proxy              Sets an HTTP proxy to pass through. Syntax is "host:port".',
         '  --cookie             Adds a cookie on the main domain.',
         '  --auth-user          Basic HTTP authentication username.',
         '  --auth-pass          Basic HTTP authentication password.',
-        '  --block-domain       Disallow requests to given (comma-separated) domains - aka blacklist.',
-        '  --allow-domain       Only allow requests to given (comma-separated) domains - aka whitelist.',
+        '  --block-domain       Disallow requests to given (comma-separated) domains.',
+        '  --allow-domain       Only allow requests to given (comma-separated) domains.',
         '  --no-externals       Block all domains except the main one.',
         '  --reporter           The output format: "json" or "xml". Default is "json".',
         ''
     ].join('\n'),
-    pkg: '../package.json'
+    pkg: require('../package.json')
 });
 
 
@@ -55,7 +55,7 @@ if (screenshot) {
 }
 
 // Device simulation
-options.device = cli.flags.device || 'desktop';
+options.device = cli.flags.device || 'mobile';
 
 // Wait for CSS selector
 options.waitForSelector = cli.flags.waitForSelector || null;

BIN
doc/img/YLT-animated.gif


BIN
doc/img/home.png


BIN
doc/img/logo.png


BIN
doc/img/screenshot.png


+ 4 - 1
front/src/css/about.css

@@ -6,5 +6,8 @@
   margin: 2em;
 }
 .about a {
-  color: #FFF;
+  color: #fff;
+}
+.sponsor {
+  color: #ffa319;
 }

+ 93 - 38
front/src/css/dashboard.css

@@ -5,15 +5,8 @@
   text-align: center;
 }
 .summary .globalScore {
-  display: table;
-  width: 60%;
   margin: 3em auto;
 }
-.summary .globalScore > div {
-  display: table-cell;
-  width: 50%;
-  vertical-align: middle;
-}
 .summary .globalScore .globalGrade {
   margin: 0.5 auto;
   width: 2.5em;
@@ -21,12 +14,10 @@
   line-height: 2.5em;
   border-radius: 0.5em;
   font-size: 3em;
-  font-weight: bold;
   vertical-align: middle;
 }
 .summary .globalScore .on100 {
   font-size: 1.2em;
-  font-weight: bold;
   margin: 0.5em 0 1em;
 }
 .summary .globalScore .screenshotWrapper:hover {
@@ -54,28 +45,63 @@
   top: 1.5em;
   left: 0.9em;
 }
+@media (min-width: 820px) {
+  .summary .globalScore {
+    width: 65%;
+    display: table;
+  }
+  .summary .globalScore > div {
+    display: table-cell;
+    width: 50%;
+    vertical-align: middle;
+  }
+}
 .summary .notations {
+  width: 100%;
   display: table;
-  width: 80%;
-  margin: 0 10% 1.5em;
-  border-spacing: 1em;
+  margin: 0 0 1.5em;
+  border-spacing: 0 1em;
+}
+@media (min-width: 820px) {
+  .summary .notations {
+    width: 80%;
+    margin: 0 10% 1.5em;
+    border-spacing: 1em;
+  }
 }
 .summary .notations > div {
   display: table-row;
 }
 .summary .notations > div > div {
-  display: table-cell;
-  height: 2.5em;
   vertical-align: middle;
 }
+@media (min-width: 820px) {
+  .summary .notations > div > div {
+    display: table-cell;
+    height: 2.5em;
+  }
+}
 .summary .notations .category {
-  font-weight: bold;
-  text-align: center;
-  width: 20%;
+  font-size: 1.2em;
+  width: 50%;
+  float: left;
+  text-align: left;
+  margin: 0.5em 0.25em;
+}
+@media (min-width: 820px) {
+  .summary .notations .category {
+    width: 20%;
+    text-align: center;
+    float: none;
+  }
 }
 .summary .notations .criteria {
   font-weight: normal;
-  width: 75%;
+}
+@media (min-width: 820px) {
+  .summary .notations .criteria {
+    width: 75%;
+  }
 }
 .summary .notations .A.categoryScore,
 .summary .notations .B.categoryScore,
@@ -87,10 +113,23 @@
   width: 2.5em;
   max-width: 2.5em;
   min-width: 2.5em;
-  font-size: 2em;
+  margin: 0.2em;
+  font-size: 1.5em;
   text-align: center;
   border-radius: 0.5em;
-  font-weight: bold;
+  float: right;
+}
+@media (min-width: 820px) {
+  .summary .notations .A.categoryScore,
+  .summary .notations .B.categoryScore,
+  .summary .notations .C.categoryScore,
+  .summary .notations .D.categoryScore,
+  .summary .notations .E.categoryScore,
+  .summary .notations .F.categoryScore,
+  .summary .notations .NA.categoryScore {
+    float: none;
+    font-size: 2em;
+  }
 }
 .summary .notations .grade .A,
 .summary .notations .grade .B,
@@ -114,14 +153,14 @@
   color: inherit;
 }
 .summary .notations .criteria .table > a:hover > div {
-  background: #EBD8E2;
+  background: #d8ebe0;
   cursor: pointer;
 }
 .summary .notations .criteria .table > a:hover > div.info {
   background: #FFF;
 }
-.summary .notations .criteria .table > a:hover > div.info .icon-question {
-  color: #EBD8E2;
+.summary .notations .criteria .table > a:hover > div.info svg {
+  fill: #d8ebe0;
 }
 .summary .notations .criteria .grade {
   width: 10%;
@@ -134,32 +173,48 @@
 }
 .summary .notations .criteria .result {
   width: 18%;
-  font-weight: bold;
   white-space: nowrap;
   text-align: center;
   vertical-align: middle;
 }
 .summary .notations .warning .label,
-.summary .notations .warning .result,
-.summary .notations .icon-warning {
+.summary .notations .warning .result {
   color: #FF1919;
 }
-.summary .notations .criteria .info {
-  width: 2%;
-  text-align: center;
-  vertical-align: middle;
-  background: #FFF;
-  padding-left: 0.1em;
-  padding-right: 0.1em;
+.summary .notations .icon-warning svg {
+  fill: #FF1919;
+  margin: -2px 0;
 }
-.summary .notations .criteria .icon-question {
-  color: transparent;
+.summary .notations .criteria .info {
+  display: none;
+}
+@media (min-width: 820px) {
+  .summary .notations .criteria .info {
+    display: table-cell;
+    width: 2%;
+    text-align: center;
+    vertical-align: middle;
+    background: #FFF;
+    padding-left: 0.1em;
+    padding-right: 0.1em;
+  }
+}
+.summary .notations .criteria .info svg {
+  fill: transparent;
 }
 .summary .fromShare {
   margin-bottom: 3em;
 }
+.summary .tweet {
+  display: none;
+}
+@media (min-width: 820px) {
+  .summary .tweet {
+    display: block;
+  }
+}
 .summary .tweet .tweetText {
-  color: #413;
+  color: #212240;
   background: #F2F2F2;
   border: none;
   width: 25em;
@@ -169,7 +224,7 @@
 }
 .summary .tweet .tweetButton,
 .summary .tweet .linkedinButton {
-  color: #413;
+  color: #212240;
   background: #F2F2F2;
   margin-right: 0;
 }
@@ -184,7 +239,7 @@
 .summary .sponsor {
   font-size: 0.9em;
   margin-bottom: 4em;
-  color: #413;
+  color: #ffa319;
 }
 .summary .sponsor a {
   color: inherit;

File diff suppressed because it is too large
+ 0 - 2
front/src/css/icons.css


+ 14 - 8
front/src/css/index.css

@@ -20,7 +20,7 @@
   background: #e74c3c;
 }
 .launchBtn.disabled {
-  background: #deaca6;
+  background: #f1bd70;
 }
 .launchBtn.disabled:focus {
   color: #ddd;
@@ -55,23 +55,28 @@
   text-decoration: none;
   font-size: 0.8em;
 }
+.device .item > svg {
+  margin: 0.6em 0 0.3em;
+  fill: #fff;
+}
 .device .item.active {
   color: #ffa319;
   border: 2px solid #ffa319;
   padding: 0;
 }
+.device .item.active > svg {
+  fill: #ffa319;
+}
 .device .item:hover {
   color: #ffa319;
 }
-.device .item > div {
-  margin: 0.2em 0 0.1em;
-  font-size: 3em;
+.device .item:hover > svg {
+  fill: #ffa319;
 }
 .settingsTooltip {
   position: relative;
 }
-.settingsTooltip span {
-  font-size: 0.8em;
+.settingsTooltip svg {
   vertical-align: text-top;
 }
 .settingsTooltip div {
@@ -85,6 +90,8 @@
   border-radius: 1em;
   border: 2px solid #ffa319;
   white-space: normal;
+  word-break: break-all;
+  word-break: break-word;
   z-index: 2;
 }
 .settingsTooltip:hover div {
@@ -149,7 +156,7 @@
   width: 50%;
   margin: 6em auto 0;
   font-size: 0.9em;
-  color: #413;
+  color: #8abfaf;
 }
 .features > div {
   width: 33.3%;
@@ -164,7 +171,6 @@ input.url {
   height: 2em;
   border: 0 solid;
   border-radius: 0.5em;
-  box-shadow: 0.1em 0.2em 0 0 #5e2846;
   outline: none;
 }
 input[type=submit]:hover {

File diff suppressed because it is too large
+ 0 - 2
front/src/css/main.css


+ 13 - 0
front/src/css/queue.css

@@ -6,3 +6,16 @@
   font-size: 0.8em;
   margin-bottom: 6em;
 }
+.progressBarEmpty {
+  width: 90%;
+  max-width: 300px;
+  margin: 1em auto;
+  padding: 0.05em;
+  border: 1px solid #ffa319;
+}
+.progressBarFilled {
+  width: 5%;
+  height: 0.5em;
+  background: #ffa319;
+  transition: width 3s ease-out;
+}

+ 24 - 15
front/src/css/rule.css

@@ -2,7 +2,6 @@
   text-align: center;
 }
 .rule .ruleTable {
-  display: table;
   border-spacing: 1em;
   width: 90%;
   margin: 2em auto;
@@ -10,16 +9,20 @@
   border: 1px dashed #666;
   border-radius: 0.5em;
 }
-.rule .ruleTable > div {
-  display: table-cell;
-  vertical-align: middle;
-}
-.rule .ruleTable .left {
-  width: 33%;
-  font-weight: bold;
-}
-.rule .ruleTable .right {
-  width: 67%;
+@media (min-width: 820px) {
+  .rule .ruleTable {
+    display: table;
+  }
+  .rule .ruleTable > div {
+    display: table-cell;
+    vertical-align: middle;
+  }
+  .rule .ruleTable .left {
+    width: 33%;
+  }
+  .rule .ruleTable .right {
+    width: 67%;
+  }
 }
 .rule .score {
   font-size: 2.5em;
@@ -27,7 +30,7 @@
   height: 2em;
   width: 2em;
   border-radius: 0.5em;
-  margin: 0 auto 0.5em;
+  margin: 0 auto 0.25em;
 }
 .rule h3 {
   margin-bottom: 0em;
@@ -67,7 +70,13 @@
   border-spacing: 0 0.25em;
   margin: 0 auto;
   min-width: 10%;
-  max-width: 90%;
+  font-size: 0.875em;
+}
+@media (min-width: 820px) {
+  .rule .offendersTable {
+    max-width: 90%;
+    font-size: 1em;
+  }
 }
 .rule .offendersTable > div {
   display: table-row;
@@ -75,12 +84,12 @@
 .rule .offendersTable > div > div {
   display: table-cell;
   background: #f2f2f2;
-  padding: 0 1em;
+  padding: 0 0.25em;
   word-wrap: break-word;
   word-break: break-all;
 }
 .rule .offendersTable > div > div:hover {
-  background: #EBD8E2;
+  background: #d8ebe0;
 }
 .rule .notFound {
   font-size: 1em;

+ 11 - 1
front/src/css/screenshot.css

@@ -2,6 +2,16 @@
   text-align: center;
 }
 .screenshot .screenshotWrapper {
-  font-size: 2.08333333333333em;
+  font-size: 1.2em;
   margin-bottom: 0.5em;
 }
+@media (min-width: 420px) {
+  .screenshot .screenshotWrapper {
+    font-size: 1.6em;
+  }
+}
+@media (min-width: 640px) {
+  .screenshot .screenshotWrapper {
+    font-size: 2.08333333em;
+  }
+}

+ 0 - 338
front/src/css/timeline.css

@@ -1,338 +0,0 @@
-/* Timeline colors, related to Window Performances */
-.execution {
-  text-align: center;
-}
-.selectScript {
-  padding-bottom: 2em;
-  font-size: 0.9em;
-}
-.selectScript select {
-  max-width: 30em;
-}
-.selectScript.empty {
-  font-size: 0.8em;
-}
-.selectScript.empty select {
-  width: 10em;
-}
-.timeline {
-  margin: 2em 0 5em;
-}
-.timeline .chart {
-  position: relative;
-  width: 100%;
-  border-bottom: 1px solid #000;
-}
-.timeline .startTime,
-.timeline .endTime {
-  position: absolute;
-  bottom: 0.5em;
-  font-size: 0.8em;
-}
-.timeline .startTime {
-  left: 0em;
-}
-.timeline .endTime {
-  right: 0em;
-}
-.timeline .chartPoints {
-  display: table;
-  height: 100px;
-  width: 99%;
-  margin: 0 auto;
-}
-.timeline .interval {
-  display: table-cell;
-  position: relative;
-  height: 100px;
-  width: 0.5%;
-}
-.timeline .interval .color {
-  position: absolute;
-  bottom: 0;
-  width: 100%;
-}
-.timeline .interval .color.clickable {
-  cursor: pointer;
-}
-.timeline div.interval:hover {
-  background: #9C4274;
-}
-.timeline .interval:hover .color {
-  background: #F04DA7;
-}
-.timeline .domComplete.interval {
-  background: #EDE3FF;
-}
-.timeline .domComplete .color {
-  background: #C2A3FF;
-}
-.timeline .domContentLoadedEnd.interval {
-  background: #D8F0F0;
-}
-.timeline .domContentLoadedEnd .color {
-  background: #7ECCCC;
-}
-.timeline .domContentLoaded.interval {
-  background: #E0FFD1;
-}
-.timeline .domContentLoaded .color {
-  background: #A7E846;
-}
-.timeline .domInteractive.interval {
-  background: #FFFCCC;
-}
-.timeline .domInteractive .color {
-  background: #FFE433;
-}
-.timeline .domCreation.interval {
-  background: #FFE0CC;
-}
-.timeline .domCreation .color {
-  background: #FF6600;
-}
-.timeline .tooltip.detailsOverlay {
-  position: absolute;
-  display: none;
-  width: auto;
-  padding: 0.5em 1em;
-  top: -1.5em;
-  right: 1em;
-}
-.timeline .interval:hover .tooltip {
-  display: block;
-}
-.timeline .legend {
-  display: table;
-  width: 100%;
-  margin-top: 1em;
-}
-.timeline .legend > div {
-  display: table-row;
-}
-.timeline .legend > div > div {
-  position: relative;
-  display: table-cell;
-  margin-top: 1em;
-}
-.timeline .titles {
-  font-weight: bold;
-}
-.timeline .titles > div {
-  padding: 0 1em 0 2em;
-}
-.timeline .tips {
-  font-size: 0.7em;
-}
-.timeline .tips > div {
-  padding: 1em 1em 0 0;
-}
-.timeline .legend .color {
-  display: block;
-  position: absolute;
-  left: 0;
-  height: 1.5em;
-  width: 1.5em;
-  border-radius: 0.2em;
-}
-.filters {
-  margin: 1em auto;
-  padding: 0.5em;
-  min-width: 30em;
-  width: 30%;
-  border: 1px dotted #aaa;
-  text-align: left;
-}
-.subFilters {
-  margin-left: 3em;
-  font-size: 0.9em;
-}
-.table {
-  display: table;
-  width: 100%;
-  border-spacing: 0.25em;
-}
-.table > div,
-.table > a {
-  display: table-row;
-}
-.table > .headers > div {
-  font-weight: bold;
-  padding: 0.5em 1em;
-}
-.table > div > div,
-.table > a > div {
-  padding: 0.1em 1em;
-  background: #f2f2f2;
-  display: table-cell;
-  text-align: left;
-}
-.table > div.jsError > .type,
-.table > div.jsError > .value {
-  color: #e74c3c;
-  font-weight: bold;
-}
-.table > div.windowPerformance > div,
-.table > div.windowPerformance > div.startTime {
-  background: #EBD8E2;
-}
-.table > div.showingDetails > div {
-  background: #f1c40f;
-}
-.table > div.highlight > div.startTime {
-  background-color: #C0F090;
-}
-.table > div.highlight-remove {
-  transition: 3s;
-}
-.table > div.highlight-remove > div.startTime {
-  transition: background-color 3s ease-in;
-}
-.table > div > .index {
-  color: #bbb;
-  word-break: normal;
-}
-.table > div > .type {
-  white-space: nowrap;
-}
-.table .children {
-  margin-top: 0.2em;
-  font-size: 0.8em;
-  line-height: 1.6em;
-}
-.table .child {
-  margin-left: 0.5em;
-}
-.table .child > .child {
-  margin-left: 1em;
-}
-.table .child:before {
-  content: "↳";
-}
-.table .child .childArgs {
-  display: none;
-}
-.table .child span {
-  position: relative;
-}
-.table .child span:hover {
-  background: #EBD8E2;
-}
-.table .child span:hover div {
-  display: inline-block;
-}
-.table .child span:hover .childArgs {
-  display: block;
-  position: absolute;
-  padding: 0 1em 0 2em;
-  left: 100%;
-  top: 0;
-  background: #EBD8E2;
-  line-height: 1.3em;
-  height: 1.3em;
-  z-index: 2;
-}
-.table .showingDetails .child span:hover {
-  background: inherit;
-}
-.table .showingDetails .child span:hover .childArgs {
-  display: none;
-}
-.table > div > .value {
-  width: 70%;
-  word-break: break-all;
-}
-.table > div > .details {
-  position: relative;
-}
-.table .details .icon-question {
-  color: #f1c40f;
-  cursor: pointer;
-}
-.table .icon-warning {
-  display: inline-block;
-  width: 0.8em;
-}
-.detailsOverlay {
-  display: none;
-  position: absolute;
-  right: 3em;
-  top: -3em;
-  width: 45em;
-  min-height: 1em;
-  padding: 0 1em 1em;
-  background: #fff;
-  border: 2px solid #f1c40f;
-  border-radius: 0.5em;
-  z-index: 2;
-}
-@media screen and (max-width: 1024px) {
-  .detailsOverlay {
-    width: 25em;
-  }
-}
-.showDetails .detailsOverlay {
-  display: block;
-}
-.detailsOverlay .closeBtn {
-  position: absolute;
-  top: 0.5em;
-  right: 0.5em;
-  color: #f1c40f;
-  cursor: pointer;
-}
-.detailsOverlay .advice {
-  color: #e74c3c;
-  font-weight: bold;
-}
-.detailsOverlay .trace {
-  word-break: break-all;
-}
-.table > div > .duration,
-.table > div > .startTime {
-  text-align: center;
-  white-space: nowrap;
-}
-.table > div > .startTime.domComplete {
-  background: #EDE3FF;
-}
-.table > div > .startTime.domContentLoadedEnd {
-  background: #D8F0F0;
-}
-.table > div > .startTime.domContentLoaded {
-  background: #E0FFD1;
-}
-.table > div > .startTime.domInteractive {
-  background: #FFFCCC;
-}
-.table > div > .startTime.domCreation {
-  background: #FFE0CC;
-}
-.execution .icon-warning {
-  color: #e74c3c;
-  cursor: pointer;
-}
-.queryWithoutResultsFilterOn > div {
-  display: none;
-}
-.queryWithoutResultsFilterOn > div.queryWithoutResults {
-  display: table-row;
-}
-.jQueryCallOnEmptyObjectFilterOn > div {
-  display: none;
-}
-.jQueryCallOnEmptyObjectFilterOn > div.jQueryCallOnEmptyObject {
-  display: table-row;
-}
-.eventNotDelegatedFilterOn > div {
-  display: none;
-}
-.eventNotDelegatedFilterOn > div.eventNotDelegated {
-  display: table-row;
-}
-.jsErrorFilterOn > div {
-  display: none;
-}
-.jsErrorFilterOn > div.jsError {
-  display: table-row;
-}

+ 0 - 1
front/src/fonts/svg-icons/arrow-left3.svg

@@ -1 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M 256.00,0.00C 397.385,0.00, 512.00,114.615, 512.00,256.00s -114.615,256.00, -256.00,256.00s -256.00-114.615, -256.00-256.00S 114.615,0.00, 256.00,0.00z M 256.00,464.00 c 114.875,0.00 208.00-93.125 208.00-208.00S 370.875,48.00, 256.00,48.00s -208.00,93.125, -208.00,208.00S 141.125,464.00, 256.00,464.00zM 105.372,233.373l 128.001-128.00c 12.496-12.497 32.757-12.497 45.254,0.00c 12.497,12.497 12.497,32.758,0.00,45.255L 205.255,224.00 L 384.00,224.00 c 17.673,0.00 32.00,14.327 32.00,32.00c0.00,17.673, -14.327,32.00, -32.00,32.00l-178.745,0.00 l 73.373,73.372c 12.497,12.497 12.497,32.759,0.00,45.256 C 272.379,412.876, 264.189,416.00, 256.00,416.00s -16.379-3.124, -22.627-9.372l -128.001-128.00C 92.876,266.131, 92.876,245.869, 105.372,233.373z" ></path></svg>

+ 0 - 1
front/src/fonts/svg-icons/bars.svg

@@ -1 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M0.00,416.00L 512.00,416.00L 512.00,480.00L0.00,480.00zM 64.00,288.00L 128.00,288.00L 128.00,384.00L 64.00,384.00zM 160.00,160.00L 224.00,160.00L 224.00,384.00L 160.00,384.00zM 256.00,256.00L 320.00,256.00L 320.00,384.00L 256.00,384.00zM 352.00,64.00L 416.00,64.00L 416.00,384.00L 352.00,384.00z" ></path></svg>

+ 0 - 1
front/src/fonts/svg-icons/lab.svg

@@ -1 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M 478.145,402.241L 320.00,138.729L 320.00,32.00 l 16.00,0.00 c 8.80,0.00, 16.00-7.20, 16.00-16.00s-7.20-16.00-16.00-16.00L 176.00,0.00 c-8.80,0.00-16.00,7.20-16.00,16.00s 7.20,16.00, 16.00,16.00l 16.00,0.00 l0.00,106.729 L 33.856,402.241C-2.365,462.608, 25.60,512.00, 96.00,512.00l 320.00,0.00 C 486.40,512.00, 514.365,462.608, 478.145,402.241z M 120.519,320.00L 224.00,147.531L 224.00,32.00 l 64.00,0.00 l0.00,115.531 L 391.482,320.00L 120.519,320.00 z" ></path></svg>

+ 0 - 1
front/src/fonts/svg-icons/list.svg

@@ -1 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M0.00,0.00L 128.00,0.00L 128.00,128.00L0.00,128.00zM 192.00,32.00L 512.00,32.00L 512.00,96.00L 192.00,96.00zM0.00,192.00L 128.00,192.00L 128.00,320.00L0.00,320.00zM 192.00,224.00L 512.00,224.00L 512.00,288.00L 192.00,288.00zM0.00,384.00L 128.00,384.00L 128.00,512.00L0.00,512.00zM 192.00,416.00L 512.00,416.00L 512.00,480.00L 192.00,480.00z" ></path></svg>

+ 0 - 1
front/src/fonts/svg-icons/loop.svg

@@ -1 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M 437.011,74.99C 390.685,28.662, 326.693,0.00, 256.00,0.00C 146.256,0.00, 52.655,69.064, 16.251,166.094l 59.938,22.477 C 103.491,115.798, 173.692,64.00, 256.00,64.00c 53.02,0.00, 101.01,21.50, 135.753,56.247L 320.00,192.00l 192.00,0.00 L 512.00,0.00 L 437.011,74.99zM 256.00,448.00c-53.02,0.00-101.013-21.496-135.756-56.244L 192.00,320.00L0.00,320.00 l0.00,192.00 l 74.997-74.997C 121.32,483.334, 185.306,512.00, 256.00,512.00 c 109.745,0.00, 203.346-69.064, 239.75-166.094l-59.938-22.477C 408.51,396.202, 338.309,448.00, 256.00,448.00z" ></path></svg>

+ 0 - 1
front/src/fonts/svg-icons/mobile.svg

@@ -1 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M 368.00,0.00L 144.00,0.00 c-26.40,0.00-48.00,21.60-48.00,48.00l0.00,416.00 c0.00,26.40, 21.60,48.00, 48.00,48.00l 224.00,0.00 c 26.40,0.00, 48.00-21.60, 48.00-48.00L 416.00,48.00 C 416.00,21.60, 394.40,0.00, 368.00,0.00z M 192.00,24.00 l 128.00,0.00 l0.00,16.00 L 192.00,40.00 L 192.00,24.00 z M 256.00,480.00c-17.673,0.00-32.00-14.327-32.00-32.00s 14.327-32.00, 32.00-32.00s 32.00,14.327, 32.00,32.00S 273.673,480.00, 256.00,480.00z M 384.00,384.00L 128.00,384.00 L 128.00,64.00 l 256.00,0.00 L 384.00,384.00 z" ></path></svg>

+ 0 - 1
front/src/fonts/svg-icons/question.svg

@@ -1 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M 224.00,352.00L 288.00,352.00L 288.00,416.00L 224.00,416.00zM 352.00,128.00 C 369.673,128.00 384.00,142.327 384.00,160.00 L 384.00,256.00 L 288.00,320.00 L 224.00,320.00 L 224.00,288.00 L 320.00,224.00 L 320.00,192.00 L 160.00,192.00 L 160.00,128.00 L 352.00,128.00 ZM 256.00,48.00 C 200.441,48.00 148.208,69.636 108.922,108.922 C 69.636,148.208 48.00,200.441 48.00,256.00 C 48.00,311.559 69.636,363.792 108.922,403.078 C 148.208,442.364 200.441,464.00 256.00,464.00 C 311.559,464.00 363.792,442.364 403.078,403.078 C 442.364,363.792 464.00,311.559 464.00,256.00 C 464.00,200.441 442.364,148.208 403.078,108.922 C 363.792,69.636 311.559,48.00 256.00,48.00 Z M 256.00,0.00 L 256.00,0.00 C 397.385,0.00 512.00,114.615 512.00,256.00 C 512.00,397.385 397.385,512.00 256.00,512.00 C 114.615,512.00 0.00,397.385 0.00,256.00 C 0.00,114.615 114.615,0.00 256.00,0.00 Z" ></path></svg>

+ 0 - 1
front/src/fonts/svg-icons/screen.svg

@@ -1 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M 512.00,416.00L 512.00,32.00 L0.00,32.00 l0.00,384.00 l 224.00,0.00 l0.00,32.00 l-96.00,0.00 l0.00,32.00 l 256.00,0.00 l0.00-32.00 l-96.00,0.00 l0.00-32.00 L 512.00,416.00 z M 64.00,96.00l 384.00,0.00 l0.00,256.00 L 64.00,352.00 L 64.00,96.00 z" ></path></svg>

+ 0 - 1
front/src/fonts/svg-icons/tablet.svg

@@ -1 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M 400.00,0.00L 80.00,0.00 C 53.60,0.00, 32.00,21.60, 32.00,48.00l0.00,416.00 c0.00,26.40, 21.60,48.00, 48.00,48.00l 320.00,0.00 c 26.40,0.00, 48.00-21.60, 48.00-48.00L 448.00,48.00 C 448.00,21.60, 426.40,0.00, 400.00,0.00z M 240.00,496.00 c-8.836,0.00-16.00-7.163-16.00-16.00s 7.164-16.00, 16.00-16.00s 16.00,7.163, 16.00,16.00S 248.836,496.00, 240.00,496.00z M 384.00,448.00L 96.00,448.00 L 96.00,64.00 l 288.00,0.00 L 384.00,448.00 z" ></path></svg>

+ 0 - 1
front/src/fonts/svg-icons/warning.svg

@@ -1 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M 256.00,79.362 L 83.583,448.00 L 428.417,448.00 L 256.00,79.362 Z M 256.00,0.00 L 256.00,0.00 C 267.035,0.00 278.07,7.441 286.442,22.324 L 504.978,457.88 C 521.723,487.646 507.478,512.00 473.327,512.00 L 38.673,512.00 C 4.521,512.00 -9.722,487.646 7.021,457.88 L 225.558,22.324 C 233.93,7.441 244.965,0.00 256.00,0.00 ZM 256.00,192.00 C 273.673,192.00 288.00,206.327 288.00,224.00 L 278.00,320.00 L 234.00,320.00 L 224.00,224.00 C 224.00,206.327 238.327,192.00 256.00,192.00 Z" ></path><circle cx="256" cy="384" r="31" style="stroke:#000000; fill:#000000"/></svg>

BIN
front/src/img/favicon-fail.png


BIN
front/src/img/favicon-success.png


BIN
front/src/img/favicon.png


BIN
front/src/img/logo-large.png


+ 0 - 5
front/src/js/app.js

@@ -7,7 +7,6 @@ var yltApp = angular.module('YellowLabTools', [
     'queueCtrl',
     'ruleCtrl',
     'screenshotCtrl',
-    'timelineCtrl',
     'runsFactory',
     'resultsFactory',
     'apiService',
@@ -64,10 +63,6 @@ yltApp.config(['$routeProvider', '$locationProvider',
                 templateUrl: 'views/dashboard.html',
                 controller: 'DashboardCtrl'
             }).
-            when('/result/:runId/timeline', {
-                templateUrl: 'views/timeline.html',
-                controller: 'TimelineCtrl'
-            }).
             when('/result/:runId/screenshot', {
                 templateUrl: 'views/screenshot.html',
                 controller: 'ScreenshotCtrl'

+ 61 - 3
front/src/js/controllers/queueCtrl.js

@@ -5,26 +5,50 @@ queueCtrl.controller('QueueCtrl', ['$scope', '$routeParams', '$location', 'Runs'
 
     var numberOfTries = 0;
     
+    var favicon = document.querySelector('link[rel=icon]');
+    var faviconUrl = 'img/favicon.png';
+    var faviconSuccessUrl = 'img/favicon-success.png';
+    var faviconFailUrl = 'img/favicon-fail.png';
+    var faviconInterval = null;
+    var faviconCounter = 0;
+    var faviconCanvas = null;
+    var faviconCanvasContext = null;
+    var faviconImage = null;
+    
     function getRunStatus () {
         Runs.get({runId: $scope.runId}, function(data) {
             $scope.url = data.params.url;
             $scope.status = data.status;
+            $scope.progress = data.progress;
             $scope.notFound = false;
             $scope.connectionLost = false;
 
-            if (data.status.statusCode === 'running' || data.status.statusCode === 'awaiting') {
+            if (data.status.statusCode === 'awaiting') {
                 numberOfTries ++;
+                rotateFavicon();
 
-                // Retrying in 2 seconds (and increasing the delay a bit more each time)
+                // Retrying every 2 seconds (and increasing the delay a bit more each time)
                 setTimeout(getRunStatus, 2000 + (numberOfTries * 100));
 
+            } else if (data.status.statusCode === 'running') {
+                numberOfTries ++;
+                rotateFavicon();
+
+                // Retrying every second or so
+                setTimeout(getRunStatus, 1000 + (numberOfTries * 10));
+
             } else if (data.status.statusCode === 'complete') {
+                stopFavicon(true);
+
                 $location.path('/result/' + $scope.runId).replace();
             } else {
-                // Handled by the view
+                stopFavicon(false);
+
+                // The rest is handled by the view
             }
         }, function(response) {
             if (response.status === 404) {
+                stopFavicon(false);
                 $scope.notFound = true;
                 $scope.connectionLost = false;
             } else if (response.status === 0) {
@@ -35,6 +59,40 @@ queueCtrl.controller('QueueCtrl', ['$scope', '$routeParams', '$location', 'Runs'
             }
         });
     }
+
+    function rotateFavicon() {
+        if (!faviconInterval) {
+            faviconImage = new Image();
+            faviconImage.onload = function() {
+                faviconCanvas = document.getElementById('faviconRotator');
+                faviconCanvasContext = faviconCanvas.getContext('2d');
+                faviconCanvasContext.fillStyle = '#212240';
+                
+                if (!!faviconCanvasContext) {
+                    faviconInterval = window.setInterval(faviconTick, 300);
+                }
+            };
+            faviconImage.src = faviconUrl;
+        }
+    }
+
+    function faviconTick() {
+        faviconCounter ++;
+        faviconCanvasContext.save();
+        faviconCanvasContext.fillRect(0, 0, 32, 32);
+        faviconCanvasContext.translate(16, 16);
+        faviconCanvasContext.rotate(22.5 * faviconCounter * Math.PI / 180);
+        faviconCanvasContext.translate(-16, -16);
+        faviconCanvasContext.drawImage(faviconImage, 0, 0, 32, 32);
+        faviconCanvasContext.restore();
+        favicon.href = faviconCanvas.toDataURL('image/png');
+    }
+
+    function stopFavicon(isSuccess) {
+        window.clearInterval(faviconInterval);
+        faviconInterval = null;
+        favicon.href = isSuccess ? faviconSuccessUrl : faviconFailUrl;
+    }
     
     getRunStatus();
 }]);

+ 0 - 62
front/src/js/controllers/ruleCtrl.js

@@ -64,68 +64,6 @@ ruleCtrl.controller('RuleCtrl', ['$scope', '$rootScope', '$routeParams', '$locat
                 }
             };
         }
-
-        // Init "Breakpoints" chart
-        if ($scope.policyName === 'cssBreakpoints' && $scope.rule.value > 0) {
-
-            // Seek for the biggest breakpoint
-            var max = 0;
-            $scope.rule.offendersObj.forEach(function(offender) {
-                if (offender.pixels > max) {
-                    max = offender.pixels;
-                }
-            });
-            max = Math.max(max + 100, 1400);
-
-            // We group offenders 10px by 10px
-            var GROUP_SIZE = 20;
-
-            // Generate an empty array of values
-            $scope.breakpointsLabels = [];
-            $scope.breakpointsData = [[]];
-            for (var i = 0; i <= max / GROUP_SIZE; i++) {
-                $scope.breakpointsLabels[i] = '';
-                $scope.breakpointsData[0][i] = 0;
-            }
-
-            // Fill it with results
-            $scope.rule.offendersObj.forEach(function(offender) {
-                var group = Math.floor((offender.pixels + 1) / GROUP_SIZE);
-
-                if ($scope.breakpointsLabels[group] !== '') {
-                    $scope.breakpointsLabels[group] += '/';
-                }
-                $scope.breakpointsLabels[group] += offender.breakpoint;
-
-                $scope.breakpointsData[0][group] += offender.count;
-            });
-
-            $scope.breakpointsSeries = ['Number of CSS rules per breakpoint'];
-            $scope.breakpointsColours = ['#9c4274'];
-            $scope.breakpointsOptions = {
-                scales: {
-                    xAxes: [{
-                        gridLines: {
-                            display:false
-                        }
-                    }],
-                    yAxes: [{
-                        gridLines: {
-                            display:false
-                        }
-                    }]
-                },
-                tooltips: {
-                    enabled: false
-                },
-
-                elements: {
-                    point: {
-                        radius: 0
-                    }
-                }
-            };
-        }
     }
 
     $scope.backToDashboard = function() {

+ 0 - 190
front/src/js/controllers/timelineCtrl.js

@@ -1,190 +0,0 @@
-var timelineCtrl = angular.module('timelineCtrl', []);
-
-timelineCtrl.controller('TimelineCtrl', ['$scope', '$rootScope', '$routeParams', '$location', '$timeout', 'Menu', 'Results', 'API', function($scope, $rootScope, $routeParams, $location, $timeout, Menu, Results, API) {
-    $scope.runId = $routeParams.runId;
-    $scope.Menu = Menu.setCurrentPage('timeline', $scope.runId);
-
-    function loadResults() {
-        // Load result if needed
-        if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
-            Results.get({runId: $routeParams.runId, exclude: 'toolsResults'}, function(result) {
-                $rootScope.loadedResult = result;
-                $scope.result = result;
-                render();
-            });
-        } else {
-            $scope.result = $rootScope.loadedResult;
-            render();
-        }
-    }
-
-    function render() {
-        initFilters();
-        initScriptFiltering();
-        initExecutionTree();
-        initTimeline();
-        $timeout(initProfiler, 100);
-    }
-
-    function initFilters() {
-        var hash = $location.hash();
-        var filter = null;
-        
-        if (hash.indexOf('filter=') === 0) {
-            filter = hash.substr(7);
-        }
-
-        $scope.warningsFilterOn = (filter !== null);
-        $scope.warningsFilters = {
-            queryWithoutResults: (filter === null || filter === 'queryWithoutResults'),
-            jQueryCallOnEmptyObject: (filter === null || filter === 'jQueryCallOnEmptyObject'),
-            eventNotDelegated: (filter === null || filter === 'eventNotDelegated'),
-            jsError: (filter === null || filter === 'jsError')
-        };
-    }
-
-    function initScriptFiltering() {
-        var offenders = $scope.result.rules.totalRequests.offendersObj.list.byType.js;
-        $scope.scripts = [];
-
-        offenders.forEach(function(filePath) {
-            var shortPath = filePath;
-
-            if (filePath.length > 100) {
-                shortPath = filePath.substr(0, 98) + '...';
-            }
-
-            var scriptObj = {
-                fullPath: filePath,
-                shortPath: shortPath
-            };
-
-            $scope.scripts.push(scriptObj);
-        });
-    }
-
-    function initExecutionTree() {
-        var originalExecutions = $scope.result.javascriptExecutionTree.children || [];
-        
-        // Detect the last event of all (before filtering) and read time
-        var lastEvent = originalExecutions[originalExecutions.length - 1];
-        $scope.endTime =  lastEvent.data.timestamp + (lastEvent.data.time || 0);
-
-        // Filter
-        $scope.executionTree = [];
-        originalExecutions.forEach(function(node) {
-            
-            // Filter by script (if enabled)
-            if ($scope.selectedScript) {
-                if (node.data.backtrace && node.data.backtrace.indexOf($scope.selectedScript.fullPath + ':') === -1) {
-                    return;
-                }
-                if (node.data.type === "jQuery loaded" || node.data.type === "jQuery version change") {
-                    return;
-                }
-            }
-
-            $scope.executionTree.push(node);
-        });
-    }
-
-    function initTimeline() {
-
-        // Split the timeline into 200 intervals
-        var numberOfIntervals = 199;
-        $scope.timelineIntervalDuration = $scope.endTime / numberOfIntervals;
-        
-        // Pre-fill array of as many elements as there are milleseconds
-        var millisecondsArray = Array.apply(null, new Array($scope.endTime + 1)).map(Number.prototype.valueOf,0);
-        
-        // Create the milliseconds array from the execution tree
-        $scope.executionTree.forEach(function(node) {
-            if (node.data.time !== undefined) {
-
-                // Ignore artefacts (durations > 100ms)
-                var time = Math.min(node.data.time, 100) || 1;
-
-                for (var i=node.data.timestamp, max=node.data.timestamp + time ; i<max ; i++) {
-                    millisecondsArray[i] |= 1;
-                }
-            }
-        });
-
-        // Pre-fill array of 200 elements
-        $scope.timeline = Array.apply(null, new Array(numberOfIntervals + 1)).map(Number.prototype.valueOf,0);
-
-        // Create the timeline from the milliseconds array
-        millisecondsArray.forEach(function(value, timestamp) {
-            if (value === 1) {
-                $scope.timeline[Math.floor(timestamp / $scope.timelineIntervalDuration)] += 1;
-            }
-        });
-        
-        // Get the maximum value of the array (needed for display)
-        $scope.timelineMax = Math.max.apply(Math, $scope.timeline);
-    }
-
-
-    function initProfiler() {
-        $scope.profilerData = $scope.executionTree;
-    }
-
-    $scope.changeScript = function() {
-        initExecutionTree();
-        initTimeline();
-        initProfiler();
-    };
-
-    $scope.findLineIndexByTimestamp = function(timestamp) {
-        var lineIndex = 0;
-
-        for (var i = 0; i < $scope.executionTree.length; i ++) {
-            var delta = $scope.executionTree[i].data.timestamp - timestamp;
-            
-            if (delta < $scope.timelineIntervalDuration) {
-                lineIndex = i;
-            }
-
-            if (delta > 0) {
-                break;
-            }
-        }
-
-        return lineIndex;
-    };
-
-
-    $scope.backToDashboard = function() {
-        $location.path('/result/' + $scope.runId);
-    };
-
-    $scope.testAgain = function() {
-        API.relaunchTest($scope.result);
-    };
-
-    loadResults();
-
-}]);
-
-timelineCtrl.directive('scrollOnClick', ['$animate', '$timeout', function($animate, $timeout) {
-    return {
-        restrict: 'A',
-        link: function (scope, element, attributes) {            
-            // When the user clicks on the timeline, find the right profiler line and scroll to it
-            element.on('click', function() {
-                var lineIndex = scope.findLineIndexByTimestamp(attributes.scrollOnClick);
-                var lineElement = angular.element(document.getElementById('line_' + lineIndex));
-                
-                // Animate the background color to "flash" the row
-                lineElement.addClass('highlight');
-                $timeout(function() {
-                    $animate.removeClass(lineElement, 'highlight');
-                    scope.$digest();
-                }, 50);
-
-
-                window.scrollTo(0, lineElement[0].offsetTop);
-            });
-        }
-    };
-}]);

+ 11 - 642
front/src/js/directives/offendersDirectives.js

@@ -93,510 +93,12 @@
         };
     });
 
-
-    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;'
+    offendersDirectives.filter('lastDOMNode', function() {
+        return function(str) {
+            var splited = str.split(' > ');
+            return splited[splited.length - 1];
         };
-
-        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 = '';
@@ -687,145 +189,6 @@
         return out;
     }
 
-    function getTimelineDetailsHTML(node) {
-        var html = '';
-
-        if (node.data.type != 'jQuery loaded' && node.data.type != 'jQuery version change' && !node.windowPerformance) {
-            if (node.warning || node.error) {
-                html += '<div class="icon-warning"></div>';
-            } else {
-                html += '<div class="icon-question"></div>';
-            }
-
-            html += '<div class="detailsOverlay">';
-            html += '<div class="closeBtn">✖</div>';
-
-            if (node.data.callDetails.context && node.data.callDetails.context.length === 0) {
-                html += '<h4>Called on 0 jQuery element</h4><p class="advice">Useless function call, as the jQuery object is empty.</p>';
-            } else if (node.eventNotDelegated) {
-                html += '<p class="advice">This binding should use Event Delegation instead of binding each element one by one.</p>';
-            }
-
-            if (node.data.resultsNumber === 0) {
-                html += '<p class="advice">The query returned 0 results. Could it be unused or dead code?</p>';
-            } else if (node.data.resultsNumber > 0) {
-                html += '<p>The query returned ' + node.data.resultsNumber + ' ' + (node.data.resultsNumber > 1 ? 'results' : 'result') + '.</p>';
-            }
-
-            if (node.data.backtrace) {
-                html += '<h4>Backtrace</h4>';
-                html += '<div class="table">';
-                html += getBacktraceHTML(node.data.backtrace);
-                html += '</div>';
-            }
-
-            html += '</div>';
-        }
-        
-        return html;
-    }
-
-    
-    offendersDirectives.directive('profilerLine', ['$filter', function($filter) {
-        
-        var numberWithCommas = $filter('number');
-
-        function getProfilerLineHTML(index, node) {
-            return  '<div class="index">' + (index + 1) + '</div>' +
-                    '<div class="type">' + node.data.type + (node.children ? '<div class="children">' + recursiveChildrenHTML(node) + '</div>' : '') + '</div>' +
-                    '<div class="value">' + getTimelineParamsHTML(node, false) + '</div>' +
-                    '<div class="details">' + getTimelineDetailsHTML(node) + '</div>' +
-                    '<div class="startTime ' + node.data.loadingStep + '">' + numberWithCommas(node.data.timestamp, 0) + ' ms</div>';
-        }
-
-        function recursiveChildrenHTML(node) {
-            var html = '';
-            
-            if (node.children) {
-                node.children.forEach(function(child) {
-                    html += '<div class="child"><span>' + child.data.type + '<div class="childArgs">' + getTimelineParamsHTML(child, true) + '</div></span>' + recursiveChildrenHTML(child) + '</div>';
-                });
-            }
-
-            return html;
-        }
-
-        function onDetailsClick(row) {
-            // Close if it's already open
-            if (row.classList.contains('showDetails')) {
-                closeDetails(row);
-                return;
-            }
-
-            // Close any other open details overlay
-            var openOnes = document.getElementsByClassName('showDetails');
-            if (openOnes.length > 0) {
-                openOnes[0].classList.remove('showDetails');
-            }
-
-            // Make it appear
-            row.classList.add('showDetails');
-
-            // Bind the close button
-            row.querySelector('.closeBtn').addEventListener('click', function() {
-                closeDetails(row);
-            });
-        }
-
-        function closeDetails(row) {
-            row.classList.remove('showDetails');
-
-            // Unbind the close button
-            row.querySelector('.closeBtn').removeEventListener('click', closeDetails);   
-        }
-
-        return {
-            restrict: 'E',
-            scope: {
-                index: '=',
-                node: '='
-            },
-            template: '<div></div>',
-            replace: true,
-            link: function(scope, element) {
-                
-                if (scope.node.error) {
-                    element.addClass('jsError');
-                } else if (scope.node.windowPerformance) {
-                    element.addClass('windowPerformance');
-                }
-
-                element.append(getProfilerLineHTML(scope.index, scope.node));
-                element[0].id = 'line_' + scope.index;
-
-                if (scope.node.warning) {
-                    element[0].classList.add('warning');
-
-                    if (scope.node.queryWithoutResults) {
-                        element[0].classList.add('queryWithoutResults');
-                    }
-
-                    if (scope.node.jQueryCallOnEmptyObject) {
-                        element[0].classList.add('jQueryCallOnEmptyObject');
-                    }
-
-                    if (scope.node.eventNotDelegated) {
-                        element[0].classList.add('eventNotDelegated');
-                    }
-                }
-
-
-                // Bind click on the details icon
-                var detailsIcon = element[0].querySelector('.details div');
-                if (detailsIcon) {
-                    detailsIcon.addEventListener('click', function() {
-                        onDetailsClick(this.parentNode.parentNode);
-                    });
-                }
-            }
-        };
-    }]);
-
     function shortenUrl(url, maxLength) {
         if (!maxLength) {
             maxLength = 110;
@@ -935,4 +298,10 @@
         };
     });
 
+    offendersDirectives.filter('roundNbr', function() {
+        return function(nbr) {
+            return Math.round(nbr);
+        };
+    });
+
 })();

+ 2 - 2
front/src/js/services/apiService.js

@@ -21,9 +21,9 @@ apiService.factory('API', ['$location', 'Runs', 'Results', function($location, R
             };
 
             
-            if (settings.domainsBlackOrWhite === 'black') {
+            if (settings.domainsBlockOrAllow === 'block') {
                 runObject.blockDomain = this.parseDomains(settings.domains);
-            } else if (settings.domainsBlackOrWhite === 'white') {
+            } else if (settings.domainsBlockOrAllow === 'allow') {
                 var allowedDomains = this.parseDomains(settings.domains);
                 if (allowedDomains.length > 0) {
                     runObject.allowDomain = allowedDomains;

+ 0 - 3
front/src/js/services/menuService.js

@@ -22,9 +22,6 @@ menuService.factory('Menu', ['$location', function($location) {
                 case 'dashboard':
                     $location.path('/result/' + currentRunId);
                     break;
-                case 'timeline':
-                    $location.path('/result/' + currentRunId + '/timeline');
-                    break;
                 default:
                     console.err('Undefined Menu.changePage() destination');
             }

+ 1 - 1
front/src/js/services/settingsService.js

@@ -6,7 +6,7 @@ settingsService.factory('Settings', ['localStorageService', function(localStorag
 
         getMergedSettings: function() {
             var defaultSettings = {
-                device: 'desktop',
+                device: 'phone',
                 showAdvanced: false
             };
             

+ 5 - 1
front/src/less/about.less

@@ -8,5 +8,9 @@
 }
 
 .about a {
-    color: #FFF;
+    color: #fff;
+}
+
+.sponsor {
+    color: #ffa319;
 }

+ 77 - 37
front/src/less/dashboard.less

@@ -7,16 +7,8 @@
 }
 
 .summary .globalScore {
-    display: table;
-    width: 60%;
     margin: 3em auto;
 
-    > div {
-        display: table-cell;
-        width: 50%;
-        vertical-align: middle;
-    }
-
     .globalGrade {
         margin: 0.5 auto;
         width: 2.5em;
@@ -24,12 +16,10 @@
         line-height: 2.5em;
         border-radius: 0.5em;
         font-size: 3em;
-        font-weight: bold;
         vertical-align: middle;
     }
     .on100 {
         font-size: 1.2em;
-        font-weight: bold;
         margin: 0.5em 0 1em;
     }
 
@@ -61,40 +51,77 @@
         top: 1.5em;
         left: 0.9em;
     }
+
+    @media (min-width: 820px) {
+        width: 65%;
+        display: table;
+
+        > div {
+            display: table-cell;
+            width: 50%;
+            vertical-align: middle;
+        }
+    }
 }
 
 .summary .notations {
+    width: 100%;
     display: table;
-    width: 80%;
-    margin: 0 10% 1.5em;
-    border-spacing: 1em;
+    margin: 0 0 1.5em;
+    border-spacing: 0 1em;
+
+    @media (min-width: 820px) {
+        width: 80%;
+        margin: 0 10% 1.5em;
+        border-spacing: 1em;
+    }
 }
 .summary .notations > div {
     display: table-row;
 }
 .summary .notations > div > div {
-    display: table-cell;
-    height: 2.5em;
     vertical-align: middle;
+
+    @media (min-width: 820px) {
+        display: table-cell;
+        height: 2.5em;
+    }
 }
 .summary .notations .category {
-    font-weight: bold;
-    text-align: center;
-    width: 20%;
+    font-size: 1.2em;
+    width: 50%;
+    float: left;
+    text-align: left;
+    margin: 0.5em 0.25em;
+
+    @media (min-width: 820px) {
+        width: 20%;
+        text-align: center;
+        float: none;
+    }
 }
 .summary .notations .criteria {
     font-weight: normal;
-    width: 75%;
+
+    @media (min-width: 820px) {
+        width: 75%;
+    }
 }
 .A, .B, .C, .D, .E, .F, .NA {
     .summary .notations &.categoryScore {
         width: 2.5em;
         max-width: 2.5em;
         min-width: 2.5em;
-        font-size: 2em;
+        margin: 0.2em;
+        font-size: 1.5em;
         text-align: center;
         border-radius: 0.5em;
-        font-weight: bold;
+        float: right;
+
+        @media (min-width: 820px) {
+            float: none;
+            font-size: 2em;
+        }
     }
     .summary .notations .grade & {
         width: 1em;
@@ -113,12 +140,12 @@
         color: inherit;
     }
     > a:hover > div {
-        background: #EBD8E2;
+        background: #d8ebe0;
         cursor: pointer;
         &.info {
             background: #FFF;
-            .icon-question {
-                color: #EBD8E2;
+            svg {
+                fill: #d8ebe0;
             }
         }
     }
@@ -134,24 +161,32 @@
 }
 .summary .notations .criteria .result {
     width: 18%;
-    font-weight: bold;
     white-space: nowrap;
     text-align: center;
     vertical-align: middle;
 }
-.summary .notations .warning .label, .summary .notations .warning .result, .summary .notations .icon-warning {
+.summary .notations .warning .label, .summary .notations .warning .result {
     color: #FF1919;
 }
+.summary .notations .icon-warning svg {
+    fill: #FF1919;
+    margin: -2px 0;
+}
 .summary .notations .criteria .info {
-    width: 2%;
-    text-align: center;
-    vertical-align: middle;
-    background: #FFF;
-    padding-left: 0.1em;
-    padding-right: 0.1em;
+    display: none;
+
+    @media (min-width: 820px) {
+        display: table-cell;
+        width: 2%;
+        text-align: center;
+        vertical-align: middle;
+        background: #FFF;
+        padding-left: 0.1em;
+        padding-right: 0.1em;
+    }
 }
-.summary .notations .criteria .icon-question {
-    color: transparent;
+.summary .notations .criteria .info svg {
+    fill: transparent;
 }
 
 .summary .fromShare {
@@ -159,8 +194,13 @@
 }
 
 .summary .tweet {
+    display: none;
+    @media (min-width: 820px) {
+        display: block;
+    }
+
     .tweetText {
-        color: #413;
+        color: #212240;
         background: #F2F2F2;
         border: none;
         width: 25em;
@@ -169,7 +209,7 @@
         box-shadow: 0.05em 0.1em 0 0 #999;
     }
     .tweetButton, .linkedinButton {
-        color: #413;
+        color: #212240;
         background: #F2F2F2;
         margin-right: 0;
         &:hover {
@@ -185,7 +225,7 @@
 .summary .sponsor {
     font-size: 0.9em;
     margin-bottom: 4em;
-    color: #413;
+    color: #ffa319;
     a {
         color: inherit;
     }

File diff suppressed because it is too large
+ 0 - 6
front/src/less/icons.less


+ 17 - 9
front/src/less/index.less

@@ -22,7 +22,7 @@
         background: #e74c3c;
     }
     &.disabled {
-        background: #deaca6;
+        background: #f1bd70;
         &:focus {
             color: #ddd;
         }
@@ -59,27 +59,34 @@
         text-decoration: none;
         font-size: 0.8em;
 
+        > svg {
+            margin: 0.6em 0 0.3em;
+            fill: #fff;
+        }
+
         &.active {
             color: #ffa319;
             border: 2px solid #ffa319;
             padding: 0;
+
+            > svg {
+                fill: #ffa319;
+            }
         }
 
         &:hover {
             color: #ffa319;
-        }
 
-        > div {
-            margin: 0.2em 0 0.1em;
-            font-size: 3em;
+            > svg {
+                fill: #ffa319;
+            }
         }
     }
 }
 
 .settingsTooltip {
     position: relative;
-    span {
-        font-size: 0.8em;
+    svg {
         vertical-align: text-top;
     }
     
@@ -94,6 +101,8 @@
         border-radius: 1em;
         border: 2px solid #ffa319;
         white-space: normal;
+        word-break: break-all;
+        word-break: break-word;
         z-index: 2;
     }
 
@@ -171,7 +180,7 @@
     width: 50%;
     margin: 6em auto 0;
     font-size: 0.9em;
-    color: #413;
+    color: #8abfaf;
 
     > div {
         width: 33.3%;
@@ -187,7 +196,6 @@ input[type=submit], input.url {
     height: 2em;
     border: 0 solid;
     border-radius: 0.5em;
-    box-shadow: 0.1em 0.2em 0 0 #5e2846;
     outline: none;
 }
 input[type=submit]:hover {

+ 61 - 19
front/src/less/main.less

@@ -1,13 +1,14 @@
-@import "icons.less";
-
 html {
-    margin: 100px 50px;
+    margin: 35px 5px;
+    @media (min-width: 640px) {
+        margin: 100px 50px;
+    }
 }
 
 body {
     margin: 0 auto;
     max-width: 1280px;
-    background: #9c4274;
+    background: #212240;
     color: #fff;
     font-size: 16px;
     text-align: center;
@@ -21,41 +22,61 @@ input[type=submit] {
     cursor: pointer;
 }
 
-h1 span {
-    display: inline-block;
-    height: 1em;
-    width: 1em;
-    color: #ffa319;
+h1 {
+    font-weight: 200;
 }
 
 .resultsMenu {
     margin-top: 2em;
 }
 .resultsMenu .menuItem {
+    font-size: 0.8em;
     display: inline-block;
-    margin: 1em;
-    width: 8em;
+    width: 7em;
     height: 7em;
     color: #fff;
-    border: 3px solid #fff;
-    border-radius: 0.5em;
     cursor: pointer;
     text-decoration: none;
+
+    @media (min-width: 640px) {
+        font-size: 1em;
+        margin: 1em;
+        width: 8em;
+        border: 2px solid #fff;
+        border-radius: 0.5em;
+    }
+
+    svg {
+        fill: #fff;
+    }
+
     &.back, &.restart {
-        color: #413;
-        border-color: #413;
+        color: #fff;
+        border-color: #fff;
     }
 }
 .resultsMenu .menuItem div {
     padding-top: 0.5em;
     font-size: 3em;
 }
+.resultsMenu svg {
+    display: block;
+    margin: 1.2em auto 0.2em;
+}
 .resultsMenu .active, .resultsMenu .menuItem.active:hover {
     color: #ffa319;
     border-color: #ffa319;
+
+    svg {
+        fill: #ffa319;
+    }
 }
 .resultsMenu .menuItem:hover {
     color: #ffa319;
+
+    svg {
+        fill: #ffa319;
+    }
 }
 .resultsMenu span {
     position: relative;
@@ -149,7 +170,7 @@ a.linkButton {
     }
 }
 
-.screenshotWrapper.desktop {
+.screenshotWrapper.desktop, .screenshotWrapper.desktop-hd {
     border: 0.2em solid #AAA;
     padding: 0.5em;
     border-top-left-radius: 0.4em;
@@ -238,9 +259,30 @@ a.linkButton {
     }
 }
 
+.table {
+    display: table;
+    width: 100%;
+    border-spacing: 0.25em;
+}
+.table > div,
+.table > a {
+    display: table-row;
+}
+.table > .headers > div {
+    font-weight: bold;
+    padding: 0.5em 1em;
+}
+.table > div > div,
+.table > a > div {
+    padding: 0.1em 1em;
+    background: #f2f2f2;
+    display: table-cell;
+    text-align: left;
+}
+
 .footer {
     padding: 3em;
-    color: #413;
+    color: #fff;
     a {
         color: inherit;
     }
@@ -255,6 +297,6 @@ a.linkButton {
     }
 }
 
-[class^="icon-"]:before, [class*=" icon-"]:before {
-    vertical-align: baseline;
+.homeSponsor {
+    color: #ffa319;
 }

+ 15 - 0
front/src/less/queue.less

@@ -6,4 +6,19 @@
 .statusSubMessage {
     font-size: 0.8em;
     margin-bottom: 6em;
+}
+
+.progressBarEmpty {
+    width: 90%;
+    max-width: 300px;
+    margin: 1em auto;
+    padding: 0.05em;
+    border: 1px solid #ffa319;
+}
+
+.progressBarFilled {
+    width: 5%;
+    height: 0.5em;
+    background: #ffa319;
+    transition: width 3s ease-out;
 }

+ 25 - 15
front/src/less/rule.less

@@ -3,23 +3,27 @@
 }
 
 .rule .ruleTable {
-    display: table;
     border-spacing: 1em;
     width: 90%;
     margin: 2em auto;
     background: #f2f2f2;
     border: 1px dashed #666;
     border-radius: 0.5em;
-    > div {
-        display: table-cell;
-        vertical-align: middle;
-    }
-    .left {
-        width: 33%;
-        font-weight: bold;
-    }
-    .right {
-        width: 67%;
+
+    @media (min-width: 820px) {
+        display: table;
+
+        > div {
+            display: table-cell;
+            vertical-align: middle;
+        }
+
+        .left {
+            width: 33%;
+        }
+        .right {
+            width: 67%;
+        }
     }
 }
 
@@ -29,7 +33,7 @@
     height: 2em;
     width: 2em;
     border-radius: 0.5em;
-    margin: 0 auto 0.5em;
+    margin: 0 auto 0.25em;
 }
 
 .rule h3 {
@@ -75,17 +79,23 @@
     border-spacing: 0 0.25em;
     margin: 0 auto;
     min-width: 10%;
-    max-width: 90%;
+    font-size: 0.875em;
+
+    @media (min-width: 820px) {
+        max-width: 90%;
+        font-size: 1em;
+    }
+
     > div {
         display: table-row;
         > div {
             display: table-cell;
             background: #f2f2f2;
-            padding: 0 1em;
+            padding: 0 0.25em;
             word-wrap: break-word;
             word-break: break-all;
             &:hover {
-                background: #EBD8E2;
+                background: #d8ebe0;
             }
         }
     }

+ 9 - 1
front/src/less/screenshot.less

@@ -3,6 +3,14 @@
 }
 
 .screenshot .screenshotWrapper {
-    font-size: 2.08333333333333em;
+    font-size: 1.2em;
     margin-bottom: 0.5em;
+
+    @media (min-width: 420px) {
+        font-size: 1.6em;
+    }
+
+    @media (min-width: 640px) {
+        font-size: 2.08333333333333em;
+    }
 }

+ 0 - 387
front/src/less/timeline.less

@@ -1,387 +0,0 @@
-/* Timeline colors, related to Window Performances */
-@domCreationColor: #FF6600;
-@domCreationBg: #FFE0CC;
-@domContentLoadedColor: #A7E846;
-@domContentLoadedBg: #E0FFD1;
-@domContentLoadedEndColor: #7ECCCC;
-@domContentLoadedEndBg: #D8F0F0;
-@domCompleteColor: #C2A3FF;
-@domCompleteBg: #EDE3FF;
-@domInteractiveColor: #FFE433;
-@domInteractiveBg: #FFFCCC;
-
-.execution {
-    text-align: center;
-}
-
-.selectScript {
-    padding-bottom: 2em;
-    font-size: 0.9em;
-
-    select {
-        max-width: 30em;
-    }
-
-    &.empty {
-        font-size: 0.8em;
-
-        select {
-            width: 10em;
-        }
-    }
-}
-
-.timeline {
-    margin: 2em 0 5em;
-}
-.timeline .chart {
-    position: relative;
-    width: 100%;
-    border-bottom: 1px solid #000;
-}
-.timeline .startTime, .timeline .endTime {
-    position: absolute;
-    bottom: 0.5em;
-    font-size: 0.8em;
-}
-.timeline .startTime {
-    left: 0em;
-}
-.timeline .endTime {
-    right: 0em;
-}
-.timeline .chartPoints {
-    display: table;
-    height: 100px;
-    width: 99%;
-    margin: 0 auto;
-}
-.timeline .interval {
-    display: table-cell;
-    position: relative;
-    height: 100px;
-    width: 0.5%;
-}
-.timeline .interval .color {
-    position: absolute;
-    bottom: 0;
-    width: 100%;
-
-    &.clickable {
-        cursor: pointer;
-    }
-}
-.timeline div.interval:hover {
-    background: #9C4274;
-}
-.timeline .interval:hover .color {
-    background: #F04DA7;
-}
-.timeline .domComplete.interval {
-    background: @domCompleteBg;
-}
-.timeline .domComplete .color {
-    background: @domCompleteColor;
-}
-.timeline .domContentLoadedEnd.interval {
-    background: @domContentLoadedEndBg;
-}
-.timeline .domContentLoadedEnd .color {
-    background: @domContentLoadedEndColor;
-}
-.timeline .domContentLoaded.interval {
-    background: @domContentLoadedBg;
-}
-.timeline .domContentLoaded .color {
-    background: @domContentLoadedColor;
-}
-.timeline .domInteractive.interval {
-    background: @domInteractiveBg;
-}
-.timeline .domInteractive .color {
-    background: @domInteractiveColor;
-}
-.timeline .domCreation.interval {
-    background: @domCreationBg;
-}
-.timeline .domCreation .color {
-    background: @domCreationColor;
-}
-.timeline .tooltip.detailsOverlay {
-    position: absolute;
-    display: none;
-    width: auto;
-    padding: 0.5em 1em;
-    top: -1.5em;
-    right: 1em;
-}
-.timeline .interval:hover .tooltip {
-    display: block;
-}
-
-.timeline .legend {
-    display: table;
-    width: 100%;
-    margin-top: 1em;
-}
-.timeline .legend > div {
-    display: table-row;
-}
-.timeline .legend > div > div {
-    position: relative;
-    display: table-cell;
-    margin-top: 1em;
-}
-.timeline .titles {
-    font-weight: bold;
-}
-.timeline .titles > div {
-    padding: 0 1em 0 2em;
-}
-.timeline .tips {
-    font-size: 0.7em;
-}
-.timeline .tips > div {
-    padding: 1em 1em 0 0;
-}
-.timeline .legend .color {
-    display: block;
-    position: absolute;
-    left: 0;
-    height: 1.5em;
-    width: 1.5em;
-    border-radius: 0.2em;
-}
-
-.filters {
-    margin: 1em auto;
-    padding: 0.5em;
-    min-width: 30em;
-    width: 30%;
-    border: 1px dotted #aaa;
-    text-align: left;
-}
-
-.subFilters {
-    margin-left: 3em;
-    font-size: 0.9em;
-}
-
-.table {
-    display: table;
-    width: 100%;
-    border-spacing: 0.25em;
-}
-
-.table > div,
-.table > a {
-    display: table-row;
-}
-
-.table > .headers > div {
-    font-weight: bold;
-    padding: 0.5em 1em;
-}
-
-.table > div > div,
-.table > a > div {
-    padding: 0.1em 1em;
-    background: #f2f2f2;
-    display: table-cell;
-    text-align: left;
-}
-.table > div.jsError > .type, .table > div.jsError > .value {
-    color: #e74c3c;
-    font-weight: bold;
-}
-.table > div.windowPerformance > div, .table > div.windowPerformance > div.startTime {
-    background: #EBD8E2;
-}
-.table > div.showingDetails > div {
-    background: #f1c40f;
-}
-
-.table > div.highlight {
-    > div.startTime {
-        background-color: #C0F090;
-    }
-}
-.table > div.highlight-remove {
-    transition: 3s;
-
-    > div.startTime {
-        transition: background-color 3s ease-in;
-    }
-}
-
-.table > div > .index {
-    color: #bbb;
-    word-break: normal;
-}
-
-.table > div > .type {
-    white-space:nowrap;
-}
-
-.table .children {
-    margin-top: 0.2em;
-    font-size: 0.8em;
-    line-height: 1.6em;
-}
-
-.table .child {
-    margin-left: 0.5em;
-
-    > .child {
-        margin-left: 1em;
-    }
-
-    &:before {
-        content: "↳";
-    }
-
-    .childArgs {
-        display: none;
-    }
-
-    span {
-        position: relative;
-    }
-
-    span:hover {
-        background: #EBD8E2;
-
-        div {
-            display: inline-block;
-        }
-
-        .childArgs {
-            display: block;
-            position: absolute;
-            padding: 0 1em 0 2em;
-            left: 100%;
-            top: 0;
-            background: #EBD8E2;
-            line-height: 1.3em;
-            height: 1.3em;
-            z-index: 2;
-        }
-    }
-}
-
-.table .showingDetails .child span:hover {
-    background: inherit;
-
-    .childArgs {
-        display: none;
-    }
-}
-
-.table > div > .value {
-    width: 70%;
-    word-break: break-all;
-}
-
-.table > div > .details {
-    position: relative;
-}
-.table .details .icon-question {
-    color: #f1c40f;
-    cursor: pointer;
-}
-.table .icon-warning {
-    display: inline-block;
-    width: 0.8em;
-}
-
-.detailsOverlay {
-    display: none;
-    position: absolute;
-    right: 3em;
-    top: -3em;
-    width: 45em;
-    min-height: 1em;
-    padding: 0 1em 1em;
-    background: #fff;
-    border: 2px solid #f1c40f;
-    border-radius: 0.5em;
-    z-index: 2;
-}
-@media screen and (max-width: 1024px) {
-    .detailsOverlay {
-        width: 25em;
-    }
-}
-.showDetails .detailsOverlay {
-    display: block;
-}
-.detailsOverlay .closeBtn {
-    position: absolute;
-    top: 0.5em;
-    right: 0.5em;
-    color: #f1c40f;
-    cursor: pointer;
-}
-.detailsOverlay .advice {
-    color: #e74c3c;
-    font-weight: bold;
-}
-.detailsOverlay .trace {
-    word-break: break-all;
-}
-
-.table > div > .duration, .table > div > .startTime {
-    text-align: center;
-    white-space:nowrap;
-}
-.table > div > .startTime.domComplete {
-    background: @domCompleteBg;
-}
-.table > div > .startTime.domContentLoadedEnd {
-    background: @domContentLoadedEndBg;
-}
-.table > div > .startTime.domContentLoaded {
-    background: @domContentLoadedBg;
-}
-.table > div > .startTime.domInteractive {
-    background: @domInteractiveBg;
-}
-.table > div > .startTime.domCreation {
-    background: @domCreationBg;
-}
-.execution .icon-warning {
-    color: #e74c3c;
-    cursor: pointer;
-}
-.queryWithoutResultsFilterOn {
-    > div {
-        display: none;
-        &.queryWithoutResults {
-            display: table-row;
-        }
-    }
-}
-.jQueryCallOnEmptyObjectFilterOn {
-    > div {
-        display: none;
-        &.jQueryCallOnEmptyObject {
-            display: table-row;
-        }
-    }
-}
-.eventNotDelegatedFilterOn {
-    > div {
-        display: none;
-        &.eventNotDelegated {
-            display: table-row;
-        }
-    }
-}
-.jsErrorFilterOn {
-    > div {
-        display: none;
-        &.jsError {
-            display: table-row;
-        }
-    }
-}

+ 2 - 6
front/src/main.html

@@ -4,6 +4,7 @@
     <title>Yellow Lab Tools</title>
     <base href="<%= baseUrl %>">
     <link rel="icon" type="image/png" href="img/favicon.png">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
     <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!" />
 
@@ -14,21 +15,17 @@
     <link rel="stylesheet" type="text/css" href="css/queue.css">
     <link rel="stylesheet" type="text/css" href="css/rule.css">
     <link rel="stylesheet" type="text/css" href="css/screenshot.css">
-    <link rel="stylesheet" type="text/css" href="css/timeline.css">
     <link rel="stylesheet" type="text/css" href="css/about.css">
     <!-- endbuild -->
 
     <link rel="preconnect" href="https://www.google-analytics.com">
-    <link rel="dns-prefetch" href="https://www.google-analytics.com">
     <link rel="preconnect" href="https://ghbtns.com">
-    <link rel="dns-prefetch" href="https://ghbtns.com">
     <link rel="preconnect" href="https://api.github.com">
-    <link rel="dns-prefetch" href="https://api.github.com">
 
 </head>
 
 <body ng-app="YellowLabTools">
-    <div id="header"><h1>Yellow Lab <span class="icon-lab"></span> Tools</h1></div>
+    <div id="header"><h1>Yellow Lab <svg width="32" height="32" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" fill="#ffa319"><path d="M478 402L320 139V32h16c9 0 16-7 16-16s-7-16-16-16H176c-9 0-16 7-16 16s7 16 16 16h16v107L34 402c-36 61-8 110 62 110h320c70 0 98-49 62-110zm-357-82l103-172V32h64v116l103 172H121z"/></svg> Tools</h1></div>
     <div id="body" ng-view autoscroll="true"></div>
     <div class="footer">
         <span class="version">@@version</span>
@@ -51,7 +48,6 @@
     <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>

+ 2 - 2
front/src/views/about.html

@@ -1,9 +1,9 @@
 <div class="about">
-    <p><b>Yellow Lab Tools</b> is an open source project by <a href="http://www.gaelmetais.com" target="_blank">Gaël Métais</a>. It allows you to test a webpage (via an URL) and detects <b>performance</b> and <b>front-end code quality</b> issues.</p>
+    <p><b>Yellow Lab Tools</b> is an open source project by <a href="https://letstalkaboutwebperf.com/en/" target="_blank">Gaël Métais</a>. It allows you to test a webpage (via an URL) and detects <b>performance</b> and <b>front-end code quality</b> issues.</p>
 
     <p>This is done by loading the webpage via PhantomJS and collecting various metrics and statistics with the help of <a href="https://github.com/macbre/phantomas" target="_blank">Phantomas</a>. These metrics are categorized and transformed into scores. It also gives in-depth details so that developers can correct the detected issues.</p>
 
-    <p>By the way, <b>it's free</b> because we are geeks, not businessmen. All we want is <a href="https://github.com/YellowLabTools/YellowLabTools" target="_blank" class="star">a <span>&#9733;</span> on GitHub</a>. It will boost our motivation to add more awesome features!!!</p>
+    <p>By the way, <b>it's free</b> because I'm a geek, not a businessman. In return, you can add <a href="https://github.com/YellowLabTools/YellowLabTools" target="_blank" class="star">a <span>&#9733;</span> on GitHub</a>. It will boost my motivation to add more awesome features!!!</p>
 
     <%if (sponsoring.about) { %>
         <div class="sponsor"><%- sponsoring.about %></div>

+ 10 - 6
front/src/views/dashboard.html

@@ -18,7 +18,7 @@
         </div>
         <div>
             <a href="result/{{result.runId}}/screenshot">
-                <div class="screenshotWrapper" ng-class="result.params.options.device || 'desktop'">
+                <div class="screenshotWrapper" ng-class="result.params.options.device || 'phone'">
                     <div>
                         <img ng-if="result.screenshotUrl" class="screenshotImage" ng-src="{{result.screenshotUrl}}"/>
                         <span ng-if="!result.screenshotUrl" class="screenshotError">Screenshot not available</span>
@@ -43,18 +43,22 @@
                         <div class="label">{{rule.policy.label}}</div>
                         <div class="result">
                             <span ng-if="rule.policy.unit == 'bytes'">{{rule.value | bytes}}</span>
-                            <span ng-if="rule.policy.unit != 'bytes'">{{rule.value}}</span>
-                            <span ng-if="rule.abnormal" class="icon-warning"></span>
-                            <span ng-if="rule.abnormalityScore <= -100" class="icon-warning"></span>
-                            <span ng-if="rule.abnormalityScore <= -300" class="icon-warning"></span>
+                            <span ng-if="rule.policy.unit != 'bytes'">{{rule.value}} <span ng-if="rule.policy.unit"> {{rule.policy.unit}}</span></span>
+                            <span ng-if="rule.abnormal" class="icon-warning"><svg width="16" height="16" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M256 79L84 448h344L256 79zm0-79c11 0 22 7 30 22l219 436c17 30 2 54-32 54H39c-34 0-49-24-32-54L226 22c8-15 19-22 30-22zm0 192c18 0 32 14 32 32l-10 96h-44l-10-96c0-18 14-32 32-32z"/><circle cx="256" cy="384" r="31" stroke="#000"/></svg></span>
+                            <span ng-if="rule.abnormalityScore <= -100" class="icon-warning"><svg width="16" height="16" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M256 79L84 448h344L256 79zm0-79c11 0 22 7 30 22l219 436c17 30 2 54-32 54H39c-34 0-49-24-32-54L226 22c8-15 19-22 30-22zm0 192c18 0 32 14 32 32l-10 96h-44l-10-96c0-18 14-32 32-32z"/><circle cx="256" cy="384" r="31" stroke="#000"/></svg></span>
+                            <span ng-if="rule.abnormalityScore <= -300" class="icon-warning"><svg width="16" height="16" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M256 79L84 448h344L256 79zm0-79c11 0 22 7 30 22l219 436c17 30 2 54-32 54H39c-34 0-49-24-32-54L226 22c8-15 19-22 30-22zm0 192c18 0 32 14 32 32l-10 96h-44l-10-96c0-18 14-32 32-32z"/><circle cx="256" cy="384" r="31" stroke="#000"/></svg></span>
                         </div>
-                        <div class="info"><span class="icon-question"></span></div>
+                        <div class="info"><svg width="16" height="16" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M224 352h64v64h-64zm128-224c18 0 32 14 32 32v96l-96 64h-64v-32l96-64v-32H160v-64h192zm-96-80A207 207 0 0048 256a207 207 0 00208 208 207 207 0 00208-208A207 207 0 00256 48zm0-48a256 256 0 110 512 256 256 0 010-512z"/></svg></div>
                     </a>
                 </div>
             </div>
         </div>
     </div>
 
+    <%if (sponsoring.wordpress) { %>
+        <div ng-if="result.frameworks.isWordPress && !error" class="sponsor"><%- sponsoring.wordpress %></div>
+    <% } %>
+
     <%if (sponsoring.dashboard) { %>
         <div ng-if="!error && !fromSocialShare" class="sponsor"><%- sponsoring.dashboard %></div>
     <% } %>

+ 13 - 12
front/src/views/index.html

@@ -2,14 +2,15 @@
 <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="text" name="url" ng-model="url" placeholder="https://www.mysite.com" class="url" />
     <input type="submit" value="Launch test" class="launchBtn" ng-class="{disabled: !url}" />
     <div class="settings">
         <div class="device">
             <div>Choose the simulated device:</div>
-            <a href="" class="item" ng-class="{active: settings.device == 'desktop'}" ng-click="settings.device = 'desktop'"><div class="icon-screen"></div>Desktop</a>
-            <a href="" class="item" ng-class="{active: settings.device == 'tablet'}" ng-click="settings.device = 'tablet'"><div class="icon-tablet"></div>Tablet</a>
-            <a href="" class="item" ng-class="{active: settings.device == 'phone'}" ng-click="settings.device = 'phone'"><div class="icon-mobile"></div>Phone</a>
+            <a href="" class="item" ng-class="{active: settings.device == 'phone'}" ng-click="settings.device = 'phone'"><svg width="38" height="38" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M368 0H144c-26 0-48 22-48 48v416c0 26 22 48 48 48h224c26 0 48-22 48-48V48c0-26-22-48-48-48zM192 24h128v16H192V24zm64 456a32 32 0 110-64 32 32 0 010 64zm128-96H128V64h256v320z"/></svg>Phone</a>
+            <a href="" class="item" ng-class="{active: settings.device == 'tablet'}" ng-click="settings.device = 'tablet'"><svg width="38" height="38" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M400 0H80C54 0 32 22 32 48v416c0 26 22 48 48 48h320c26 0 48-22 48-48V48c0-26-22-48-48-48zM240 496a16 16 0 110-32 16 16 0 010 32zm144-48H96V64h288v384z"/></svg>Tablet</a>
+            <a href="" class="item" ng-class="{active: settings.device == 'desktop'}" ng-click="settings.device = 'desktop'"><svg width="38" height="38" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M512 416V32H0v384h224v32h-96v32h256v-32h-96v-32h224zM64 96h384v256H64V96z"/></svg>Desktop</a>
+            <a href="" class="item" ng-class="{active: settings.device == 'desktop-hd'}" ng-click="settings.device = 'desktop-hd'"><svg width="38" height="38" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M512 416V32H0v384h224v32h-96v32h256v-32h-96v-32zM64 96h384v256H64z"/><path d="M270 297V161h28c14 0 25 2 33 4 8 3 16 7 23 14 14 13 21 29 21 50s-7 38-22 51c-7 6-15 11-23 13-8 3-18 4-32 4zm20-19h10c9 0 16-1 23-3a47 47 0 0031-46c0-15-5-27-15-36-9-8-22-12-39-12h-10zm-123-64h59v-53h20v136h-20v-63h-59v63h-20V161h20z"/></svg>Desktop</a>
         </div>
         [ <a href="" class="showAdvanced" ng-click="settings.showAdvanced = !settings.showAdvanced">
             <span ng-if="!settings.showAdvanced">Advanced settings &nbsp;✚</span>
@@ -28,7 +29,7 @@
                 <div class="label">
                     Wait selector
                     <span class="settingsTooltip">
-                        <span class="icon-question"></span>
+                        <svg width="14" height="14" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#FFF" d="M224 352h64v64h-64zm128-224c18 0 32 14 32 32v96l-96 64h-64v-32l96-64v-32H160v-64h192zm-96-80A207 207 0 0048 256a207 207 0 00208 208 207 207 0 00208-208A207 207 0 00256 48zm0-48a256 256 0 110 512 256 256 0 010-512z"/></svg>
                         <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>
@@ -38,7 +39,7 @@
                 <div class="label">
                     Cookie
                     <span class="settingsTooltip">
-                        <span class="icon-question"></span>
+                        <svg width="14" height="14" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#FFF" d="M224 352h64v64h-64zm128-224c18 0 32 14 32 32v96l-96 64h-64v-32l96-64v-32H160v-64h192zm-96-80A207 207 0 0048 256a207 207 0 00208 208 207 207 0 00208-208A207 207 0 00256 48zm0-48a256 256 0 110 512 256 256 0 010-512z"/></svg>
                         <div><b>Cookie</b><br><br>Adds cookies, separated by a pipe character.<br><br>Example: "bar1=foo1;domain=.domain1.com|bar2=foo2;domain=www.domain2.com"</div>
                     </span>
                 </div>
@@ -48,7 +49,7 @@
                 <div class="label">
                     Authent
                     <span class="settingsTooltip">
-                        <span class="icon-question"></span>
+                        <svg width="14" height="14" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#FFF" d="M224 352h64v64h-64zm128-224c18 0 32 14 32 32v96l-96 64h-64v-32l96-64v-32H160v-64h192zm-96-80A207 207 0 0048 256a207 207 0 00208 208 207 207 0 00208-208A207 207 0 00256 48zm0-48a256 256 0 110 512 256 256 0 010-512z"/></svg>
                         <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>
@@ -67,7 +68,7 @@
                 <div class="label">
                     HTTP proxy
                     <span class="settingsTooltip">
-                        <span class="icon-question"></span>
+                        <svg width="14" height="14" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#FFF" d="M224 352h64v64h-64zm128-224c18 0 32 14 32 32v96l-96 64h-64v-32l96-64v-32H160v-64h192zm-96-80A207 207 0 0048 256a207 207 0 00208 208 207 207 0 00208-208A207 207 0 00256 48zm0-48a256 256 0 110 512 256 256 0 010-512z"/></svg>
                         <div><b>HTTP proxy</b><br><br>Insert here your proxy settings with the format "host:port".<br><br>Example: "192.168.10.0:3333"</div>
                     </span>
                 </div>
@@ -77,14 +78,14 @@
                 <div class="label">
                     Block domains
                     <span class="settingsTooltip">
-                        <span class="icon-question"></span>
-                        <div><b>Block some domains</b><br><br>One line per domain or subdomain.<br><br><i><b>Example:</b><br>google-analytics.com<br>ads.yahoo.com<br>ajax.googleapis.com</i><br><br>An empty whitelist will block all domains except the main domain.</div>
+                        <svg width="14" height="14" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#FFF" d="M224 352h64v64h-64zm128-224c18 0 32 14 32 32v96l-96 64h-64v-32l96-64v-32H160v-64h192zm-96-80A207 207 0 0048 256a207 207 0 00208 208 207 207 0 00208-208A207 207 0 00256 48zm0-48a256 256 0 110 512 256 256 0 010-512z"/></svg>
+                        <div><b>Block some domains</b><br><br>One line per domain or subdomain.<br><br><i><b>Example:</b><br>google-analytics.com<br>ads.yahoo.com<br>ajax.googleapis.com</i><br><br>An empty allow list will block all domains except the main domain.</div>
                     </span>
                 </div>
                 <div>
                     <div>
-                        <input type="radio" name="blackOrWhite" ng-model="settings.domainsBlackOrWhite" value="black" />blacklist
-                        <input type="radio" name="blackOrWhite" ng-model="settings.domainsBlackOrWhite" value="white" />whitelist
+                        <input type="radio" name="blockOrAllow" ng-model="settings.domainsBlockOrAllow" value="block" />block list
+                        <input type="radio" name="blockOrAllow" ng-model="settings.domainsBlockOrAllow" value="allow" />allow list
                     </div>
                     <textarea name="domains" ng-model="settings.domains" rows="5"></textarea>
                 </div>

+ 12 - 2
front/src/views/queue.html

@@ -17,7 +17,16 @@
     </div>
     <div ng-if="status.statusCode == 'running'">
         <div class="status">Test is running...</div>
-        <p class="statusSubMessage">(auto-refresh activated)</p>
+        <div class="progress">
+            <div class="progressBarEmpty">
+                <div class="progressBarFilled" ng-style="{'width': (progress.estimatedProgress*100) + '%'}"></div>
+            </div>
+        </div>
+        <p class="statusSubMessage" ng-if="!progress">(Phantomas launched)</p>
+        <p class="statusSubMessage" ng-if="progress.milestone == 'domReady'">(DOM Ready fired)</p>
+        <p class="statusSubMessage" ng-if="progress.milestone == 'domComplete'">(page loaded, waiting for late requests)</p>
+        <p class="statusSubMessage" ng-if="progress.milestone == 'phantomas'">(now simulating compression, optimization and minification)</p>
+        <p class="statusSubMessage" ng-if="progress.milestone == 'redownload'">(calculating score and retrieving screenshot)</p>
     </div>
     <div ng-if="status.statusCode == 'complete'">
         <div class="status">Test complete</div>
@@ -33,4 +42,5 @@
 <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>
+</div>
+<canvas id="faviconRotator" hidden width=32 height=32></canvas>

+ 3 - 4
front/src/views/resultSubHeader.html

@@ -1,8 +1,7 @@
 <div>Tested url: &nbsp; <a href="{{result.params.url}}" target="_blank" class="testedUrl">{{result.params.url}}</a></div>
 
 <div class="resultsMenu">
-    <a class="menuItem back" href="<%= baseUrl %>"><div class="icon-arrow-left3"></div><span>New test<span></a>
-    <a class="menuItem restart" href="" ng-click="testAgain()"><div class="icon-loop"></div><span>Test again<span></a>
-    <div class="menuItem" ng-class="{active: Menu.getCurrentPage() == 'dashboard'}" ng-click="Menu.changePage('dashboard')"><div class="icon-list"></div><span>Dashboard</span></div>
-    <div class="menuItem" ng-class="{active: Menu.getCurrentPage() == 'timeline'}" ng-click="Menu.changePage('timeline')"><div class="icon-bars"></div><span>JS Timeline</span></div>
+    <a class="menuItem back" href="<%= baseUrl %>"><svg width="48" height="48" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M256 0a256 256 0 110 512 256 256 0 010-512zm0 464a208 208 0 100-416 208 208 0 000 416zM105 233l128-128a32 32 0 1146 46l-74 73h179a32 32 0 010 64H205l74 73a32 32 0 01-46 46L105 279a32 32 0 010-46z"/></svg><span>New test<span></a>
+    <a class="menuItem restart" href="" ng-click="testAgain()"><svg width="48" height="48" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M437 75a255 255 0 00-421 91l60 23a192 192 0 01316-69l-72 72h192V0l-75 75zM256 448c-53 0-101-21-136-56l72-72H0v192l75-75a255 255 0 00421-91l-60-23c-27 73-98 125-180 125z"/></svg><span>Test again<span></a>
+    <div class="menuItem" ng-class="{active: Menu.getCurrentPage() == 'dashboard'}" ng-click="Menu.changePage('dashboard')"><svg width="48" height="48" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h128v128H0zm192 32h320v64H192zM0 192h128v128H0zm192 32h320v64H192zM0 384h128v128H0zm192 32h320v64H192z"/></svg><span>Dashboard</span></div>
 </div>

+ 114 - 94
front/src/views/rule.html

@@ -12,15 +12,27 @@
             <h3>
                 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.policy.unit != 'bytes'">{{rule.value}}<span ng-if="rule.policy.unit"> {{rule.policy.unit}}</span></span>
             </h3>
             <div class="okThreshold" ng-if="rule.score < 100 && rule.policy.isOkThreshold !== undefined">
                 Have
                 <span ng-if="rule.policy.unit == 'bytes'">{{rule.policy.isOkThreshold | bytes}}</span>
-                <span ng-if="rule.policy.unit != 'bytes'">{{rule.policy.isOkThreshold}}</span>
+                <span ng-if="rule.policy.unit != 'bytes'">{{rule.policy.isOkThreshold}}<span ng-if="rule.policy.unit"> {{rule.policy.unit}}</span></span>
                 <span ng-if="rule.policy.isOkThreshold > 0 && rule.policy.isOkThreshold < rule.policy.isBadThreshold">or less</span>
                 <span ng-if="rule.policy.isOkThreshold > rule.policy.isBadThreshold">or more</span>
-                to get the 100/100 score.
+                to get the 100/100 score on this issue.
+            </div>
+            <div class="okThreshold" ng-if="rule.globalScoreIfFixed > result.scoreProfiles.generic.globalScore && rule.globalScoreIfFixed > 0 && result.scoreProfiles.generic.globalScore >= 0">
+                Your new global would increase by {{rule.globalScoreIfFixed - result.scoreProfiles.generic.globalScore}} points ({{rule.globalScoreIfFixed}}/100).
+            </div>
+            <div class="okThreshold" ng-if="rule.globalScoreIfFixed > result.scoreProfiles.generic.globalScore && rule.globalScoreIfFixed > 0 && result.scoreProfiles.generic.globalScore < 0">
+                Your new global would increase by {{rule.globalScoreIfFixed}} points ({{rule.globalScoreIfFixed}}/100).
+            </div>
+            <div class="okThreshold" ng-if="rule.globalScoreIfFixed > result.scoreProfiles.generic.globalScore && rule.globalScoreIfFixed <= 0">
+                Your new global score would increase, but still not enough to reach 0/100. That's embarassing...
+            </div>
+            <div class="okThreshold" ng-if="rule.globalScoreIfFixed == result.scoreProfiles.generic.globalScore && rule.score < 100">
+                Your new global score would slightly increase, but not enough to gain a single point.
             </div>
             <div ng-bind-html="rule.policy.message" class="message"></div>
         </div>
@@ -30,7 +42,7 @@
         <p>This rule reached the abnormality threshold, which means there is a real problem you should care about.</p>
     </div>
     <div class="offenders" ng-if="rule.policy.hasOffenders">
-        <h3 ng-if="rule.offendersObj.count >= 0"><ng-pluralize count="rule.offendersObj.count" when="{'0': 'No offenders', 'one': '1 offender', 'other': '{} offenders'}"></ng-pluralize></h3>
+        <h3 ng-if="rule.offendersObj.count >= 0"><ng-pluralize count="rule.offendersObj.count" when="{'0': 'No offender', '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">
@@ -39,8 +51,13 @@
                 </div>
                 <div ng-if="!offender.parseError">
 
+                    <div ng-if="policyName === 'iframesCount'">
+                        <span ng-if="offender.url">{{offender.url}}</span>
+                        <span ng-if="!offender.url">an iframe without URL</span>
+                    </div>
+
                     <div ng-if="policyName === 'DOMidDuplicated'">
-                        <b>{{offender.id}}</b>: {{offender.occurrences}} occurrences
+                        <b>{{offender.id}}</b>: {{offender.count}} occurrences
                     </div>
 
                     <div ng-if="policyName === 'DOMqueriesAvoidable'">
@@ -97,9 +114,9 @@
                     </div>
 
                     <div ng-if="policyName === 'cssRules'">
-                        <span ng-if="offender.file === 'inline CSS'">inline CSS</span>
-                        <span ng-if="offender.file !== 'inline CSS'"><url-link url="offender.file" max-length="80"></url-link></span>
-                        : <ng-pluralize count="offender.rules" when="{'0': '0 rule', 'one':'1 rule','other':'{} rules'}"></ng-pluralize>
+                        <span ng-if="offender.url === '[inline CSS]'">inline CSS</span>
+                        <span ng-if="offender.url !== '[inline CSS]'"><url-link url="offender.url" max-length="80"></url-link></span>
+                        : <ng-pluralize count="offender.value" when="{'0': '0 rule', 'one':'1 rule','other':'{} rules'}"></ng-pluralize>
                     </div>
 
                     <div ng-if="policyName === 'similarColors'">
@@ -118,7 +135,7 @@
                     </div>
 
                     <div ng-if="policyName === 'cssOldPropertyPrefixes'">
-                        <b>{{offender.property}} {{offender.message}}</b>
+                        <b>{{offender.property}}</b> {{offender.message}}
                         <div ng-if="offender.rules.length" ng-click="offender.showMore = !offender.showMore" class="offenderButton">
                             <span ng-if="!offender.showMore">show</span>
                             <span ng-if="offender.showMore">hide</span>
@@ -132,7 +149,12 @@
                         </div>
                     </div>
 
-                    <div ng-if="policyName === 'lazyLoadableImagesBelowTheFold' || policyName === 'hiddenImages'">
+                    <div ng-if="policyName === 'lazyLoadableImagesBelowTheFold'">
+                        <img ng-src="{{offender.url | https}}" class="smallPreview checker"></img>
+                        <url-link url="offender.url" max-length="70"></url-link> (offset: {{offender.offset | roundNbr}}px)
+                    </div>
+
+                    <div ng-if="policyName === 'hiddenImages'">
                         <img ng-src="{{offender | https}}" class="smallPreview checker"></img>
                         <url-link url="offender" max-length="100"></url-link>
                     </div>
@@ -148,7 +170,7 @@
                     </div>
 
                     <div ng-if="policyName === 'cachingTooShort'">
-                        <url-link url="offender.file" max-length="100"></url-link>
+                        <url-link url="offender.url" max-length="100"></url-link>
                         cached for <b>{{offender.ttlWithUnit}} {{offender.unit}}</b>
                     </div>
 
@@ -158,13 +180,21 @@
                     </div>
 
                     <div ng-if="policyName === 'globalVariables' || policyName === 'jQueryVersionsLoaded' || policyName === 'synchronousXHR'">
-                        {{offender}}
+                        {{offender.version}}
                     </div>
 
                     <div ng-if="policyName === 'fontsCount'">
                         <url-link url="offender.url" max-length="70"></url-link>
                         ({{offender.size | bytes}})
                     </div>
+
+                    <div ng-if="policyName === 'oldHttpProtocol'">
+                        <b>{{offender.domain}}</b> sends <span ng-class="offender.requests > 4 ? 'offenderProblem' : ''"><b><ng-pluralize count="offender.requests" when="{'one':'1 request','other':'{} requests'}"></ng-pluralize></b></span> over {{offender.httpVersion}}
+                    </div>
+
+                    <div ng-if="policyName === 'oldTlsProtocol'">
+                        <b>{{offender.domain}}</b> uses {{offender.tlsVersion}} <span ng-if="offender.beforeDomReady === true" class="offenderProblem">and seems to be on the critical path</span>
+                    </div>
                 </div>
             </div>
         </div>
@@ -173,8 +203,8 @@
             <h3>
                 <ng-pluralize count="fileDetails.count" when="{'one': '1 offender', 'other': '{} offenders'}"></ng-pluralize>
                 in
-                <url-link ng-if="fileDetails.url !== 'Inline CSS'" url="fileDetails.url" max-length="80"></url-link>
-                <span ng-if="fileDetails.url === 'Inline CSS'">inline CSS</span>
+                <url-link ng-if="fileDetails.url !== 'Inline CSS' && fileDetails.url !== '[inline CSS]'" url="fileDetails.url" max-length="80"></url-link>
+                <span ng-if="fileDetails.url === 'Inline CSS' || fileDetails.url === '[inline CSS]'">inline CSS</span>
             </h3>
 
             <div class="offendersTable">
@@ -229,19 +259,6 @@
                 <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">
@@ -267,6 +284,36 @@
         </div>
     </div>
 
+    <div ng-if="policyName === 'DOMaccesses'">
+        <div ng-repeat="(type, list) in rule.offendersObj.list.byType">
+            <h3>
+                <ng-pluralize count="list.length" when="{'0': 'No offender', 'one': '1 offender', 'other': '{} offenders'}"></ng-pluralize> from
+                <span ng-if="type === 'DOMqueriesById'">getElementById()</span>
+                <span ng-if="type === 'DOMqueriesByTagName'">getElementsByTagName()</span>
+                <span ng-if="type === 'DOMqueriesByClassName'">getElementsByClassName()</span>
+                <span ng-if="type === 'DOMqueriesByQuerySelectorAll'">querySelector() or querySelectorAll()</span>
+                <span ng-if="type === 'DOMinserts'">appendChild() or insertBefore()</span>
+                <span ng-if="type === 'DOMmutationsInserts'">added nodes</span>
+                <span ng-if="type === 'DOMmutationsRemoves'">removed nodes</span>
+                <span ng-if="type === 'DOMmutationsAttributes'">attribute changes</span>
+                <span ng-if="type === 'eventsBound'">addEventListener()</span>
+            </h3>
+            <div class="offendersTable">
+                <div ng-repeat="access in list">
+                    <div ng-if="type === 'DOMqueriesById'">#{{access.id}}</div>
+                    <div ng-if="type === 'DOMqueriesByTagName'">{{access.tag}} <b>on</b> <span title="{{access.node}}">{{access.node | lastDOMNode}}</span></div>
+                    <div ng-if="type === 'DOMqueriesByClassName'">.{{access.class}} <b>on</b> <span title="{{access.node}}">{{access.node | lastDOMNode}}</span></div>
+                    <div ng-if="type === 'DOMqueriesByQuerySelectorAll'">{{access.selector}} <b>on</b> <span title="{{access.node}}">{{access.node | lastDOMNode}}</span></div>
+                    <div ng-if="type === 'DOMinserts'"><span title="{{access.append}}">{{access.append | lastDOMNode}}</span> <b>added to</b> <span title="{{access.node}}">{{access.node | lastDOMNode}}</span></div>
+                    <div ng-if="type === 'DOMmutationsInserts'">{{access.node}} <b>added to</b> {{access.target}}</div>
+                    <div ng-if="type === 'DOMmutationsRemoves'">{{access.node}} <b>removed from</b> {{access.target}}</div>
+                    <div ng-if="type === 'DOMmutationsAttributes'">{{access.attribute}} <b>changed on</b> {{access.node}}</div>
+                    <div ng-if="type === 'eventsBound'">{{access.eventType}} <b>on</b> <span title="{{access.path}}">{{access.path | lastDOMNode}}</span></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">
@@ -276,32 +323,40 @@
                     <div><a href="{{image.url}}" target="_blank"><img ng-src="{{image.url | https}}" class="checker" /></a></div>
                 </div>
                 <div>
-                    <p ng-if="!image.afterCompression">Current weight: {{image.original | bytes}}</p>
-                    <p ng-if="image.afterCompression">Current weight: {{image.original | bytes}} ({{image.afterCompression | bytes}} gzipped)</p>
-                    <p ng-if="image.lossless && image.afterOptimizationAndCompression">With a lossless optimization:<br/>{{image.afterOptimizationAndCompression | bytes}} gzipped (<b>-{{image.gain | bytes}}</b> gzipped)</p>
-                    <p ng-if="image.lossless && !image.afterOptimizationAndCompression">With a lossless optimization:<br/>{{image.lossless | bytes}} <span ng-if="!image.lossy">(<b>-{{image.gain | bytes}}</b>)</span></p>
-                    <p ng-if="image.lossy && image.afterOptimizationAndCompression">With a lossy optimization:<br/>{{image.afterOptimizationAndCompression | bytes}} gzipped (<b>-{{image.gain | bytes}} gzipped</b>)</p>
-                    <p ng-if="image.lossy && !image.afterOptimizationAndCompression">With a lossy optimization:<br/>{{image.lossy | bytes}} (<b>-{{image.gain | bytes}}</b>)</p>
+                    <p ng-if="!image.isCompressible || image.isCompressed">Current weight: {{image.originalWeigth | bytes}}</p>
+                    <p ng-if="image.isCompressible && !image.isCompressed">Current weight: {{image.originalWeigth | bytes}} ({{image.originalCompressedWeight | bytes}} compressed)</p>
+
+                    <p ng-if="image.lossless && image.isCompressible">With a lossless optimization:<br/>{{image.afterOptimizationAndCompression | bytes}} compressed (<b>-{{image.gain | bytes}}</b> compressed)</p>
+                    <p ng-if="image.lossless && !image.isCompressible">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.isCompressible">With a lossy optimization:<br/>{{image.afterOptimizationAndCompression | bytes}} compressed (<b>-{{image.gain | bytes}} compressed</b>)</p>
+                    <p ng-if="image.lossy && !image.isCompressible">With a lossy optimization:<br/>{{image.lossy | bytes}} (<b>-{{image.gain | bytes}}</b>)</p>
                 </div>
             </div>
         </div>
     </div>
 
-    <div ng-if="policyName === 'gzipCompression'">
+    <div ng-if="policyName === 'compression'">
         <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>Gzip</div>
+                <div>Brotli</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>
+                    <url-link url="file.url" max-length="60"></url-link>
                 </div>
-                <div>{{file.original | bytes}}</div>
-                <div>{{file.gzipped | bytes}}</div>
+                <div>{{file.originalSize | bytes}}</div>
+                
+                <div ng-if="file.wasCompressed"><i>already gzipped</i></div>
+                <div ng-if="!file.wasCompressed">{{file.gzipped | bytes}}</div>
+
+                <div>{{file.brotlified | bytes}}</div>
+                
                 <div><b>-{{file.gain | bytes}}</b></div>
             </div>
         </div>
@@ -318,12 +373,12 @@
             </div>
             <div ng-repeat="file in rule.offendersObj.list.files | orderBy:'-gain'">
                 <div>
-                    <url-link url="file.url" max-length="70"></url-link>
+                    <url-link url="file.url" max-length="60"></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 ng-if="file.isCompressed">{{file.originalWeigth | bytes}} (compressed)</div>
+                <div ng-if="!file.isCompressed">{{file.originalWeigth | bytes}} ({{file.originalCompressedWeight | bytes}} compressed)</div>
+                <div ng-if="file.isCompressed">{{file.afterOptimizationAndCompression | bytes}} (compressed)</div>
+                <div ng-if="!file.isCompressed">{{file.optimized | bytes}} ({{file.afterOptimizationAndCompression | bytes}} compressed)</div>
                 <div><b>-{{file.gain | bytes}}</b></div>
             </div>
         </div>
@@ -357,51 +412,8 @@
         </div>
     </div>
 
-    <div ng-if="policyName === 'smallRequests'">
-        <div ng-repeat="(type, requests) in rule.offendersObj.list.byType">
-            <h3><ng-pluralize count="requests.length" when="{'0': 'small ' + type + ' file', 'one': '1 small ' + type + ' file', 'other': '{} small ' + type + ' files'}"></ng-pluralize></h3>
-            <p ng-if="type == 'css' && requests.length > 0">Try to inline these styles in the head of the HTML or to merge them with other files.</p>
-            <p ng-if="type == 'js' && requests.length > 0">Try to inline these scripts in the HTML or merge them with other files.</p>
-            <p ng-if="type == 'image' && requests.length > 0">Try to inline these images (with base64 encoding for most image types except SVG that don't need base64 encoding). You can also create sprites.</p>
-            <div class="table">
-                <div class="headers">
-                    <div ng-if="type == 'image'">Preview</div>
-                    <div>File</div>
-                    <div>Weight (bytes)</div>
-                </div>
-                <div ng-repeat="request in requests track by $index">
-                    <div ng-if="type == 'image'"><img ng-src="{{request.url | https}}" class="smallPreview checker" /></div>
-                    <div><url-link url="request.url" max-length="100"></url-link></div>
-                    <div>{{request.size}}</div>
-                </div>
-            </div>
-        </div>
-    </div>
-
-    <div ng-if="policyName === 'DOMaccesses'">
-        <h3>{{rule.value}} offenders</h3>
-        Please open the <a href="result/{{runId}}/timeline">JS timeline</a>
-    </div>
-
-    <div ng-if="policyName === 'queriesWithoutResults'">
-        <h3>{{rule.value}} offenders</h3>
-        Please open the <a href="result/{{runId}}/timeline#filter=queryWithoutResults">JS timeline, filtered by "Queries without results"</a>
-    </div>
-
-    <div ng-if="policyName === 'jQueryCallsOnEmptyObject'">
-        <h3>{{rule.value}} offenders</h3>
-        Please open the <a href="result/{{runId}}/timeline#filter=jQueryCallOnEmptyObject">JS timeline, filtered by "jQuery calls on empty object"</a>
-    </div>
-
-    <div ng-if="policyName === 'jQueryNotDelegatedEvents'">
-        <h3>{{rule.value}} offenders</h3>
-        Please open the <a href="result/{{runId}}/timeline#filter=eventNotDelegated">JS timeline, filtered by "Events not delegated"</a>
-    </div>
-
     <div ng-if="policyName === 'cssBreakpoints'">
-        <div ng-if="rule.value > 0" class="cssBreakpointsGraph">
-            <h3>Breakpoints distribution graph</h3>
-            <canvas class="chart chart-line" chart-data="breakpointsData" chart-labels="breakpointsLabels" chart-options="breakpointsOptions" chart-colors="breakpointsColours" chart-series="breakpointsSeries" width="600" height="250"></canvas>
+        <div ng-if="rule.value > 0">
             <h3>Breakpoints list</h3>
             <div class="offendersTable">
                 <div ng-repeat="offender in rule.offendersObj | orderBy:'pixels'">
@@ -470,14 +482,22 @@
         </div>
     </div>
 
-    <div ng-if="policyName === 'http2'">
-        <h3>Protocols advertised by the server</h3>
-        <div class="offendersTable">
-            <div ng-repeat="protocol in rule.offendersObj.list">
-                <div>{{protocol}}</div>
+    <div ng-if="policyName === 'nonWoff2Fonts'">
+        <h3 ng-if="rule.value > 0">{{rule.value | bytes}} could be saved on <ng-pluralize count="rule.offendersObj.list.fonts.length" when="{'one': '1 file', 'other': '{} files'}"></ng-pluralize></h3>
+        <div class="table">
+            <div class="headers">
+                <div>File</div>
+                <div>Current weight</div>
+                <div>WOFF 2 weight</div>
+                <div>Gain</div>
             </div>
-            <div ng-if="!rule.offendersObj || rule.offendersObj.count == 0">
-                <div>none</div>
+            <div ng-repeat="file in rule.offendersObj.list.fonts | orderBy:'-gain'">
+                <div>
+                    <url-link url="file.url" max-length="70"></url-link>
+                </div>
+                <div>{{file.originalSize | bytes}}</div>
+                <div>{{file.woff2Size | bytes}}</div>
+                <div><b>-{{file.gain | bytes}}</b></div>
             </div>
         </div>
     </div>

+ 1 - 1
front/src/views/screenshot.html

@@ -2,7 +2,7 @@
 <div class="screenshot board">
     <h2>Screenshot</h2>
 
-    <div class="screenshotWrapper" ng-class="result.params.options.device || 'desktop'">
+    <div class="screenshotWrapper" ng-class="result.params.options.device || 'phone'">
         <div>
             <img ng-if="result.screenshotUrl" class="screenshotImage" ng-src="{{result.screenshotUrl}}"/>
             <span ng-if="!result.screenshotUrl" class="screenshotError">Screenshot not available</span>

+ 0 - 100
front/src/views/timeline.html

@@ -1,100 +0,0 @@
-<div ng-include="'views/resultSubHeader.html'"></div>
-<div class="execution board">
-    <div class="selectScript" ng-class="{empty:!selectedScript}">
-        Filter timeline and profiler by script:
-        <select ng-model="selectedScript" ng-options="script.shortPath for script in scripts" ng-change="changeScript()">
-            <option value="">All (no filter)</option>
-        </select>
-    </div>
-
-    <h2>JavaScript Timeline</h2>
-    <p>This graph gives a quick view of when the JavaScript interactions with the DOM occur during the loading of the page.</p>
-
-    <div class="timeline">
-        <div class="chart">
-            <div class="chartPoints">
-                <div ng-repeat="duration in timeline track by $index"
-                     class="interval"
-                     ng-class="{
-                        domCreation: $index * timelineIntervalDuration < result.javascriptExecutionTree.data.domInteractive,
-                        domInteractive: $index * timelineIntervalDuration >= result.javascriptExecutionTree.data.domInteractive
-                            && $index * timelineIntervalDuration < result.javascriptExecutionTree.data.domContentLoaded,
-                        domContentLoaded: $index * timelineIntervalDuration >= result.javascriptExecutionTree.data.domContentLoaded
-                            && $index * timelineIntervalDuration < result.javascriptExecutionTree.data.domContentLoadedEnd,
-                        domContentLoadedEnd: $index * timelineIntervalDuration >= result.javascriptExecutionTree.data.domContentLoadedEnd
-                            && $index * timelineIntervalDuration < result.javascriptExecutionTree.data.domComplete,
-                        domComplete: $index * timelineIntervalDuration >= result.javascriptExecutionTree.data.domComplete
-                     }">
-                    <div style="height: {{100 * duration / timelineMax | number: 0}}px" class="color" ng-class="{clickable: duration > 0}" scroll-on-click="{{$index * timelineIntervalDuration}}"></div>
-                    <div class="tooltip detailsOverlay">
-                        <div>
-                            Timestamp: {{$index * timelineIntervalDuration | number: 0}} ms
-                        </div>
-                    </div>
-                </div>
-            </div>
-            <div class="startTime">0 ms</div>
-            <div class="endTime">{{endTime | number: 0}} ms</div>
-        </div>
-        <div class="legend">
-            <div class="titles">
-                <div class="domCreation"><div class="color"></div>DOM creation</div>
-                <div class="domInteractive"><div class="color"></div>DOM interactive</div>
-                <div class="domContentLoaded"><div class="color"></div>DOM content loaded event</div>
-                <div class="domContentLoadedEnd"><div class="color"></div>Page completion</div>
-                <div class="domComplete"><div class="color"></div>Page is complete</div>
-            </div>
-            <div class="tips">
-                <div>Executing JavaScript and DOM queries here is a <b>bad practice</b> and slows down the DOM construction.</div>
-                <div>Some frameworks do things here, but it's not reliable and should be avoided.</div>
-                <div>Also known as "document ready". This is where you should execute <b>top-priority</b> scripts, like binding action buttons or launch a video player.</div>
-                <div>Here you can execute <b>mid-priority</b> tasks. Loading a script with createElement('script') is one way to do so.</div>
-                <div>The page is considered loaded, it's time for low <b>priority things</b> : trackers, social plugins, easter egg...</div>
-            </div>
-        </div>
-    </div>
-
-    <h2>JavaScript Profiler</h2>
-    <p>
-        The table below shows the interactions between the JavaScript and the DOM. It is useful to understand what happens while the page loads.
-    </p>
-    <div class="filters">
-        <div>
-            <input type="checkbox" name="warningsFilter" ng-model="warningsFilterOn" id="warningsFilterOn" />
-            <label for="warningsFilterOn">Filter on warnings and errors</label>
-            <div class="subFilters" ng-if="warningsFilterOn">
-                <div>
-                    <input type="checkbox" name="filters" ng-model="warningsFilters.queryWithoutResults" id="queryWithoutResultsFilterOn"/>
-                    <label for="queryWithoutResultsFilterOn">Queries without results</label>
-                </div>
-                <div>
-                    <input type="checkbox" name="filters" ng-model="warningsFilters.jQueryCallOnEmptyObject" id="jQueryCallOnEmptyObjectFilterOn" />
-                    <label for="jQueryCallOnEmptyObjectFilterOn">jQuery calls on empty object</label>
-                </div>
-                <div>
-                    <input type="checkbox" name="filters" ng-model="warningsFilters.eventNotDelegated" id="eventNotDelegatedFilterOn" />
-                    <label for="eventNotDelegatedFilterOn">Events not delegated</label>
-                </div>
-                <div>
-                    <input type="checkbox" name="filters" ng-model="warningsFilters.jsError" id="jsErrorFilterOn" />
-                    <label for="jsErrorFilterOn">Errors</label>
-                </div>
-            </div>
-        </div>
-        {{queryWithoutResultsFilterOn}}
-    </div>
-    <div class="table" ng-class="{queryWithoutResultsFilterOn: warningsFilterOn && warningsFilters.queryWithoutResults, jQueryCallOnEmptyObjectFilterOn: warningsFilterOn && warningsFilters.jQueryCallOnEmptyObject, eventNotDelegatedFilterOn: warningsFilterOn && warningsFilters.eventNotDelegated, jsErrorFilterOn: warningsFilterOn && warningsFilters.jsError}">
-        <div class="headers">
-            <div><!-- index --></div>
-            <div>Type</div>
-            <div>Params</div>
-            <div><!-- details --></div>
-            <div>Timestamp</div>
-        </div>
-
-        <profiler-line ng-repeat="node in profilerData" data-index="$index" node="node"></profiler-line>
-
-    </div>
-
-    <div class="backToDashboard"><a href="#" ng-click="backToDashboard()">Back to dashboard</a></div>
-</div>

+ 4 - 0
lib/index.js

@@ -24,9 +24,13 @@ var yellowLabTools = function(url, options) {
         };
 
         var runner = new Runner(params)
+        
+            .progress(deferred.notify)
+
             .then(function(data) {
                 deferred.resolve(data);
             })
+
             .fail(function(err) {
                 deferred.reject(err);
             });

+ 157 - 307
lib/metadata/policies.js

@@ -15,9 +15,9 @@ var policies = {
         "tool": "phantomas",
         "label": "DOM max depth",
         "message": "<p>A deep DOM makes the CSS matching with DOM elements difficult.</p><p>It also slows down JavaScript modifications to the DOM because changing the dimensions of an element makes the browser re-calculate the dimensions of it's parents. Same thing for JavaScript events, that bubble up to the document root.</p>",
-        "isOkThreshold": 12,
-        "isBadThreshold": 22,
-        "isAbnormalThreshold": 30,
+        "isOkThreshold": 14,
+        "isBadThreshold": 24,
+        "isAbnormalThreshold": 32,
         "hasOffenders": true,
         "offendersTransformFn": function(offenders) {
             var domArrays = offenders.map(offendersHelpers.domPathToArray);
@@ -32,89 +32,38 @@ var policies = {
         "tool": "phantomas",
         "label": "Number of iframes",
         "message": "<p>iFrames are the most complex HTML elements. They are pages, just like the main page, and the browser needs to create a new page context, which has a cost.</p>",
-        "isOkThreshold": 3,
+        "isOkThreshold": 4,
         "isBadThreshold": 15,
         "isAbnormalThreshold": 30,
-        "hasOffenders": false
+        "hasOffenders": true
     },
     "DOMidDuplicated": {
         "tool": "phantomas",
         "label": "IDs duplicated",
         "message": "<p>IDs of HTML elements must be document-wide unique. This can cause problems with getElementById returning the wrong element.</p>",
         "isOkThreshold": 0,
-        "isBadThreshold": 5,
+        "isBadThreshold": 10,
         "isAbnormalThreshold": 50,
-        "hasOffenders": true,
-        "offendersTransformFn": function(offenders) {
-            return {
-                count: offenders.length,
-                list: offenders.map(function(offender) {
-                    var parts = /^(.*): ?(\d+) ?occurrences$/.exec(offender);
-
-                    if (!parts) {
-                        debug('DOMidDuplicated offenders transform function error with "%s"', offender);
-                        return {
-                            parseError: offender
-                        };
-                    }
-
-                    return {
-                        id: parts[1],
-                        occurrences: parseInt(parts[2], 10)
-                    };
-                })
-            };
-        }
+        "hasOffenders": true
     },
-    "DOMaccesses": {
-        "tool": "jsExecutionTransformer",
-        "label": "DOM access",
-        "message": "<p>This metric counts the number of calls to DOM related functions (both native DOM functions and jQuery functions) on page load.</p><p>The more your JavaScript code accesses the DOM, the slower the page will load.</p><p>Try, as much as possible, to have an HTML page fully generated by the server instead of making changes with JS.</p><p>Try to reduce the number of queries by refactoring your JavaScript code.</p><p>Binding too many events also has a cost. Try to use <a href=\"https://learn.jquery.com/events/event-delegation/\" target=\"_blank\">event delegation</a> as much as possible.</p>",
-        "isOkThreshold": 200,
+    "scriptDuration": {
+        "tool": "phantomas",
+        "label": "Total JS execution time",
+        "message": "<p>This is the number of milliseconds spent by the browser on JavaScript execution during page load.</p><p>For more details, try using the performance tab in Chrome DevTools. It is a bit complicated at first sight, but you'll be able to analyze exactly where this execution time is spent.</p>",
+        "isOkThreshold": 500,
         "isBadThreshold": 2000,
         "isAbnormalThreshold": 4000,
-        "hasOffenders": false
-    },
-    "queriesWithoutResults": {
-        "tool": "jsExecutionTransformer",
-        "label": "Queries without result",
-        "message": "<p>Number of queries that return no result. Both native and jQuery DOM requests are counted.</p><p>It suggests the query is not used on the page, probably because it is some dead code.</p><p>Or maybe the code is trying to find an HTML block that is not always here. Look at the JS Timeline to see if the scripts correctly figures out the HTML block is not here and immediatly stops interacting further with the DOM.</p>",
-        "isOkThreshold": 0,
-        "isBadThreshold": 150,
-        "isAbnormalThreshold": 250,
-        "hasOffenders": false
+        "hasOffenders": false,
+        "unit": 'ms'
     },
-    "DOMqueriesAvoidable": {
-        "tool": "phantomas",
-        "label": "Duplicated DOM queries",
-        "message": "<p>This is the number of queries that could be avoided by removing all duplicated queries.</p><p>Simply save the result of a query in a variable. Ok it is not always simple, especially with third-party scripts, but at least do it with your own code.</p>",
-        "isOkThreshold": 0,
-        "isBadThreshold": 300,
-        "isAbnormalThreshold": 600,
-        "hasOffenders": true,
-        "takeOffendersFrom": "DOMqueriesDuplicated",
-        "offendersTransformFn": function(offenders) {
-            return {
-                count: offenders.length,
-                list: offenders.map(function(offender) {
-                    var parts = /^[^"]* ?"(.*)" ?with ?(.*) ?\(in ?context ?(.*)\): ?(.*)\s?queries$/.exec(offender);
-
-                    if (!parts) {
-                        debug('DOMqueriesAvoidable offenders transform function error with "%s"', offender);
-                        return {
-                            parseError: offender
-                        };
-                    }
-
-                    return {
-                        query: parts[1],
-                        context: offendersHelpers.domPathToDomElementObj(parts[3]),
-                        fn: parts[2],
-                        count: parseInt(parts[4], 10)
-                    };
-                })
-            };
-        }
+    "DOMaccesses": {
+        "tool": "domAccessAgregator",
+        "label": "DOM access",
+        "message": "<p>This metric estimates the number of times the JavaScript reads, changes or binds the DOM.</p><p>The more your JavaScript code accesses the DOM, the slower the page will load.</p><p>Try, as much as possible, to have an HTML page fully generated by the server instead of making changes with JS.</p><p>Try to reduce the number of queries by refactoring your JavaScript code.</p><p>Binding too many events also has a cost.</p>",
+        "isOkThreshold": 500,
+        "isBadThreshold": 2500,
+        "isAbnormalThreshold": 5000,
+        "hasOffenders": true
     },
     "eventsScrollBound": {
         "tool": "phantomas",
@@ -147,25 +96,13 @@ var policies = {
             };
         }
     },
-    "DOMaccessesOnScroll": {
-        "tool": "jsExecutionTransformer",
-        "label": "DOM access on scroll",
-        "message": "<p>This rule counts the number of DOM-accessing functions calls on a scroll event, such as queries, readings, writings, bindings and jQuery functions.</p><p>Two scroll events are triggered quickly, one after the other, and only the second one is analyzed so throttled functions are ignored.</p><p>One of the main reasons of a poor scrolling experience is when too much JS is executed on each scroll event. Note that some devices such as smartphones and MacBooks send more scroll events than others.</p><p>Reduce the number of DOM accesses inside scroll listeners. Put DOM queries outside them when possible. Use <a href=\"http://blogorama.nerdworks.in/javascriptfunctionthrottlingan/\" target=\"_blank\">throttling or debouncing</a>.</p>",
-        "isOkThreshold": 1,
-        "isBadThreshold": 20,
-        "isAbnormalThreshold": 35,
-        "hasOffenders": true,
-        "offendersTransformFn": function(offenders) {
-            return offenders;
-        }
-    },
     "jsErrors": {
         "tool": "phantomas",
         "label": "JavaScript errors",
-        "message": "<p>Just to let you know there are some errors on the page.</p><p><b>Please note that some errors only occur in the PhantomJS browser, so you might need to double check on other browsers.</b></p>",
+        "message": "<p>Just to let you know there are some errors on the page.</p>",
         "isOkThreshold": 0,
         "isBadThreshold": 1,
-        "isAbnormalThreshold": 4,
+        "isAbnormalThreshold": 5,
         "hasOffenders": true,
         "offendersTransformFn": function(offenders) {
             return {
@@ -195,8 +132,8 @@ var policies = {
         "label": "document.write calls",
         "message": "<p>They slow down the page construction, especially if they are used to insert scripts in the page. Remove them ASAP.</p><p>If you cannot remove them because they come from a third-party script (such as ads), have a look at <a href=\"https://github.com/krux/postscribe\" target=\"_blank\">PostScribe</a>.</p>",
         "isOkThreshold": 0,
-        "isBadThreshold": 3,
-        "isAbnormalThreshold": 8,
+        "isBadThreshold": 2,
+        "isAbnormalThreshold": 6,
         "hasOffenders": true,
         "offendersTransformFn": function(offenders) {
             return {
@@ -250,22 +187,13 @@ var policies = {
         "isAbnormalThreshold": 1,
         "hasOffenders": true
     },
-    "consoleMessages": {
-        "tool": "phantomas",
-        "label": "Console messages",
-        "message": "<p>Try to keep your console clean when in production. Debugging is good for development only.</p><p>Writing in the console has a cost, especially when dumping large object variables.</p><p>There is also a problem with Internet Explorer 8, not knowing the console object.</p>",
-        "isOkThreshold": 3,
-        "isBadThreshold": 20,
-        "isAbnormalThreshold": 50,
-        "hasOffenders": false
-    },
     "globalVariables": {
         "tool": "phantomas",
         "label": "Global variables",
         "message": "<p>It is a bad practice because they clutter up the global namespace. If two scripts use the same variable name in the global scope, it can cause conflicts and it is generally hard to debug.</p><p>Global variables also take a (very) little bit longer to be accessed than variables in the local scope of a function.</p>",
-        "isOkThreshold": 40,
-        "isBadThreshold": 200,
-        "isAbnormalThreshold": 700,
+        "isOkThreshold": 20,
+        "isBadThreshold": 300,
+        "isAbnormalThreshold": 800,
         "hasOffenders": true,
         "offendersTransformFn": function(offenders) {
             return {
@@ -347,76 +275,45 @@ var policies = {
         "message": "<p>jQuery is a heavy library. You should <b>never</b> load jQuery more than once on the same page.</p>",
         "isOkThreshold": 1,
         "isBadThreshold": 2,
-        "isAbnormalThreshold": 2,
+        "isAbnormalThreshold": 3,
         "hasOffenders": true
     },
-    "jQueryCallsOnEmptyObject": {
-        "tool": "jsExecutionTransformer",
-        "label": "Calls on empty objects",
-        "message": "<p>This metric counts the number of jQuery functions called on an empty jQuery object. The call was useless.</p><p>This can be helpful to detect dead or unused code.</p>",
-        "isOkThreshold": 1,
-        "isBadThreshold": 100,
-        "isAbnormalThreshold": 180,
-        "hasOffenders": false
-    },
-    "jQueryNotDelegatedEvents": {
-        "tool": "jsExecutionTransformer",
-        "label": "Events not delegated",
-        "message": "<p>This is the number of events that are bound with the .bind() or the .on() function without using <a href=\"https://learn.jquery.com/events/event-delegation/\" target=\"_blank\">event delegation</a>.</p><p>This means jQuery binds each element contained in the object one by one. This is bad for performance.</p>",
-        "isOkThreshold": 1,
-        "isBadThreshold": 100,
-        "isAbnormalThreshold": 180,
-        "hasOffenders": false
-    },
     "cssParsingErrors": {
         "tool": "phantomas",
         "label": "CSS syntax error",
         "message": "<p>Yellow Lab Tools failed to parse a CSS file. I doubt the problem comes from the css parser.</p><p>Maybe a <a href=\"http://jigsaw.w3.org/css-validator\" target=\"_blank\">CSS validator</a> can help you.</p>",
         "isOkThreshold": 0,
-        "isBadThreshold": 1,
+        "isBadThreshold": 2,
         "isAbnormalThreshold": 20,
         "hasOffenders": true,
         "offendersTransformFn": function(offenders) {
             return {
                 count: offenders.length,
                 list: offenders.map(function(offender) {
-                    if (offender === '[inline CSS] (Empty CSS was provided)') {
+                    if (offender === '[inline CSS]') {
                         return {
-                            error: 'Empty style tag',
+                            error: 'Empty <style> tag',
                             file: null,
                             line: null,
                             column: null
                         };
                     }
 
-                    var parts = /^(?:(?:<([^ \(]*)>|\[inline CSS\]) ?)?(?:\((((?! @ ).)*)(?: @ (\d+):(\d+))?\))?$/.exec(offender);
-
-                    if (parts) {
+                    if (offender.value) {
                         return {
-                            error: parts[2] || 'Unknown parsing error' + (parts[1] ? '. The entire file was ignored. As a result, the other CSS metrics and scores are miscalculated.' : ''),
-                            file: parts[1] || null,
-                            line: (parts[4] && parts[5]) ? parseInt(parts[4], 10) : null,
-                            column: (parts[4] && parts[5]) ? parseInt(parts[5], 10) : null
+                            error: offender.value.message || 'Unknown parsing error. The entire file was ignored. As a result, the other CSS metrics and scores are miscalculated.',
+                            file: offender.url || null,
+                            line: offender.value.position ? offender.value.position.start.line : null,
+                            column: offender.value.position ? offender.value.position.start.column : null
                         };
-                    }
-
-                    // Try another syntax
-                    parts = /^(.*) <(.*)> @ (\d+):(\d+)$/.exec(offender);
-
-                    if (parts) {
+                    } else {
                         return {
-                            error: parts[1] || 'Unknown parsing error',
-                            file: parts[2] || null,
-                            line: parseInt(parts[3], 10),
-                            column: parseInt(parts[4], 10)
+                            error: 'Unknown parsing error. The entire file was ignored. As a result, the other CSS metrics and scores are miscalculated.',
+                            file: offender,
+                            line: null,
+                            column: null
                         };
                     }
-
-
-                    debug('cssParsingErrors offenders transform function error with "%s"', offender);
-                    return {
-                        parseError: offender
-                    };
                 })
             };
         }
@@ -425,51 +322,18 @@ var policies = {
         "tool": "phantomas",
         "label": "Rules count",
         "message": "<p>Having a huge number of CSS rules hurts performances. If the number of CSS rules is higher than the number of DOM elements, there is clearly a problem.</p><p>Huge stylesheets generally occur when the different pages of a website load all the CSS, concatenated in a single stylesheet, even if a large part of the rules are page-specific. Solution is to create one main CSS file with global rules and one custom file per page.</p>",
-        "isOkThreshold": 1000,
-        "isBadThreshold": 3000,
-        "isAbnormalThreshold": 4500,
-        "hasOffenders": true,
-        "offendersTransformFn": function(offenders) {
-            var hasInline = false;
-            var inlineCount = 0;
-            var files = [];
-
-            offenders.forEach(function(line) {
-                if (line.indexOf('[inline CSS]: ') === 0) {
-                    hasInline = true;
-                    inlineCount += parseInt(line.substr(14));
-                } else {
-                    var parts = /^<(.*)>: (\d+)$/.exec(line);
-
-                    if (parts) {
-                        files.push({
-                            file: parts[1],
-                            rules: parseInt(parts[2], 10)
-                        });
-                    }
-                }
-            });
-
-            if (hasInline) {
-                files.push({
-                    file: 'inline CSS',
-                    rules: inlineCount
-                });
-            }
-
-            return {
-                count: files.length,
-                list: files
-            };
-        }
+        "isOkThreshold": 2000,
+        "isBadThreshold": 5000,
+        "isAbnormalThreshold": 8000,
+        "hasOffenders": true
     },
     "cssComplexSelectors": {
         "tool": "phantomas",
         "label": "Complex selectors",
-        "message": "<p>Complex selectors are CSS selectors with 4 or more expressions, like \"#header ul li .foo\".</p><p>They are adding more work for the browser, and this could be avoided by simplifying selectors. The <a href=\"http://getbem.com\" target=\"_blank\">B.E.M. methodology</a> is an useful way to simplify your CSS.</p>",
+        "message": "<p>Complex selectors are CSS selectors with 4 or more expressions, like \"#header ul li .foo\".</p><p>They are adding more work for the browser, and this could be avoided by simplifying selectors. The <a href=\"http://getbem.com\" target=\"_blank\">B.E.M. methodology</a> is a useful way to simplify your CSS.</p>",
         "isOkThreshold": 0,
-        "isBadThreshold": 800,
-        "isAbnormalThreshold": 2000,
+        "isBadThreshold": 1000,
+        "isAbnormalThreshold": 24000,
         "hasOffenders": true,
         "offendersTransformFn": function(offenders) {
             var parsedOffenders = offenders.map(function(offender) {
@@ -484,18 +348,18 @@ var policies = {
         "tool": "phantomas",
         "label": "Colors count",
         "message": "<p>This is the number of different colors defined in CSS.</p><p>Your CSS will be easier to maintain if you keep a small color set.</p>",
-        "isOkThreshold": 50,
-        "isBadThreshold": 150,
-        "isAbnormalThreshold": 400,
+        "isOkThreshold": 100,
+        "isBadThreshold": 200,
+        "isAbnormalThreshold": 500,
         "hasOffenders": true,
         "offendersTransformFn": function(offenders, ruleObject) {
             var deduplicatedObj = {};
 
             offenders.map(function(offender) {
-                var parts = /^([^ ]*) \((\d+) times\)$/.exec(offender);
+                var parts = /^([^ ]*) \((\d+) times\)$/.exec(offender.value.message);
 
                 if (!parts) {
-                    debug('cssColors offenders transform function error with "%s"', offender);
+                    debug('cssColors offenders transform function error with "%s"', offender.value.message);
                     return;
                 }
 
@@ -531,8 +395,8 @@ var policies = {
         "label": "Similar colors",
         "message": "<p>This is the list of colors found in the stylesheets, that are very close to each other. The eye can barely see the difference.</p><p>Use this list to reduce the number of colors in your palette, it will be easier to maintain.</p>",
         "isOkThreshold": 0,
-        "isBadThreshold": 40,
-        "isAbnormalThreshold": 80,
+        "isBadThreshold": 60,
+        "isAbnormalThreshold": 120,
         "hasOffenders": true,
         "offendersTransformFn": function(offenders) {
             return {
@@ -545,9 +409,9 @@ var policies = {
         "tool": "mediaQueriesChecker",
         "label": "Breakpoints count",
         "message": "<p>This is the number of different breakpoints found in the stylesheets' media queries.</p><p>Please note this rule is based on <i>min-width</i>, <i>max-width</i>, <i>min-device-width</i> and <i>max-device-width</i> media queries only.</p><p>Your CSS will be easier to maintain if you keep a reasonable number of breakpoints. Try to make a fluid design - using percents - to avoid the creation of numerous breakpoints.</p>",
-        "isOkThreshold": 6,
-        "isBadThreshold": 40,
-        "isAbnormalThreshold": 60,
+        "isOkThreshold": 10,
+        "isBadThreshold": 50,
+        "isAbnormalThreshold": 80,
         "hasOffenders": true,
         "offendersTransformFn": function(offenders) {
             var offendersTable = [];
@@ -567,9 +431,9 @@ var policies = {
         "tool": "mediaQueriesChecker",
         "label": "Not mobile-first media queries",
         "message": "<p>This is the number of CSS rules inside media queries that address small screens.</p><p>The common good practice, when creating a responsive website, is to write it \"mobile-first\". More explanation in <a href=\"http://www.sitepoint.com/introduction-mobile-first-media-queries\" target=\"_blank\">this great article</a>.</p>",
-        "isOkThreshold": 50,
-        "isBadThreshold": 250,
-        "isAbnormalThreshold": 1000,
+        "isOkThreshold": 60,
+        "isBadThreshold": 400,
+        "isAbnormalThreshold": 1200,
         "hasOffenders": true,
         "offendersTransformFn": function(offenders) {
             return offendersHelpers.orderByFile(offenders);
@@ -598,12 +462,12 @@ var policies = {
         "label": "Duplicated selectors",
         "message": "<p>This is when two or more selectors are strictly identical and should be merged.</p>",
         "isOkThreshold": 0,
-        "isBadThreshold": 50,
-        "isAbnormalThreshold": 100,
+        "isBadThreshold": 100,
+        "isAbnormalThreshold": 200,
         "hasOffenders": true,
         "offendersTransformFn": function(offenders) {
             var parsedOffenders = offenders.map(function(offender) {
-                var parts = /^(.*) \((\d+) times\) ?<(.*)>$/.exec(offender);
+                var parts = /^(.*) \((\d+) times\)$/.exec(offender.value.message);
 
                 if (!parts) {
                     debug('cssDuplicatedSelectors offenders transform function error with "%s"', offender);
@@ -615,7 +479,7 @@ var policies = {
                 return {
                     rule: parts[1],
                     occurrences: parseInt(parts[2], 10),
-                    file: parts[3]
+                    file: offender.value.url
                 };
             });
 
@@ -627,8 +491,8 @@ var policies = {
         "label": "Duplicated properties",
         "message": "<p>This is the number of property definitions duplicated within a selector.</p>",
         "isOkThreshold": 0,
-        "isBadThreshold": 60,
-        "isAbnormalThreshold": 120,
+        "isBadThreshold": 100,
+        "isAbnormalThreshold": 200,
         "hasOffenders": true,
         "offendersTransformFn": function(offenders) {
             var parsedOffenders = offenders.map(function(offender) {
@@ -661,7 +525,7 @@ var policies = {
         "message": "<p>Very easy to fix: remove all empty rules.</p>",
         "isOkThreshold": 0,
         "isBadThreshold": 50,
-        "isAbnormalThreshold": 100,
+        "isAbnormalThreshold": 120,
         "hasOffenders": true,
         "offendersTransformFn": function(offenders) {
             var parsedOffenders = offenders.map(offendersHelpers.cssOffenderPattern);
@@ -707,8 +571,8 @@ var policies = {
         "label": "Uses of !important",
         "message": "<p>It can be useful, but only as a last resort. It is a bad practice because it overrides the normal cascading logic. The more you use !important, the more you need it again to over-override. This conducts to a poor maintainability.</p>",
         "isOkThreshold": 0,
-        "isBadThreshold": 75,
-        "isAbnormalThreshold": 200,
+        "isBadThreshold": 200,
+        "isAbnormalThreshold": 500,
         "hasOffenders": true,
         "offendersTransformFn": function(offenders) {
             var parsedOffenders = offenders.map(function(offender) {
@@ -741,8 +605,8 @@ var policies = {
         "label": "Old IE fixes",
         "message": "<p>What browser do you need to support? Once you've got the answer, take a look at these old rules that pollute your CSS code and remove them.</p><p>IE6:<ul><li>* html</li><li>html > body (everything but IE6)</li></ul><p><p>IE7:<ul><li><b>*</b>height: 123px;</li><li>height: 123px <b>!ie</b>;</li></ul><p><p>IE9:<ul><li>-ms-filter</li><li>progid:DXImageTransform.Microsoft</li></ul></p>",
         "isOkThreshold": 0,
-        "isBadThreshold": 50,
-        "isAbnormalThreshold": 150,
+        "isBadThreshold": 100,
+        "isAbnormalThreshold": 300,
         "hasOffenders": true,
         "offendersTransformFn": function(offenders) {
             var parsedOffenders = offenders.map(function(offender) {
@@ -797,8 +661,8 @@ var policies = {
         "label": "Old prefixes",
         "message": "<p>Many property prefixes such as -moz- or -webkit- are not needed anymore, or by very few people. Sometimes, they have never even existed. You can remove them or replace them with the non-prefixed version. This will help reducing your stylesheets weight.</p><p>The prefixes database comes from <a href=\"http://caniuse.com/\" target=\"_blank\">Can I Use</a>.</p>",
         "isOkThreshold": 0,
-        "isBadThreshold": 50,
-        "isAbnormalThreshold": 200,
+        "isBadThreshold": 100,
+        "isAbnormalThreshold": 400,
         "hasOffenders": true,
         "offendersTransformFn": function(offenders) {
             var properties = {};
@@ -850,8 +714,8 @@ var policies = {
         "label": "Redundant body selectors",
         "message": "<p>This is one way to remove complexity from a CSS rule. Generally, when \"body\" is specified in a rule it can be removed, because an element is necessarily inside the body.</p>",
         "isOkThreshold": 0,
-        "isBadThreshold": 60,
-        "isAbnormalThreshold": 200,
+        "isBadThreshold": 120,
+        "isAbnormalThreshold": 400,
         "hasOffenders": true,
         "offendersTransformFn": function(offenders) {
             var parsedOffenders = offenders.map(function(offender) {
@@ -868,8 +732,8 @@ var policies = {
         "label": "Redundant tags selectors",
         "message": "<p>Some tags included inside other tags are obvious. For example, when \"ul li\" is specified in a rule, \"ul\" can be removed because the \"li\" tag is nearly always inside an \"ul\" container (the \"ol\" container is quite rare). Same thing for \"tr td\", \"select option\", ...</p><p>Lowering compexity in CSS selectors can make the page load a little faster.</p>",
         "isOkThreshold": 0,
-        "isBadThreshold": 60,
-        "isAbnormalThreshold": 200,
+        "isBadThreshold": 120,
+        "isAbnormalThreshold": 400,
         "hasOffenders": true,
         "offendersTransformFn": function(offenders) {
             var parsedOffenders = offenders.map(function(offender) {
@@ -899,17 +763,17 @@ var policies = {
     "totalWeight": {
         "tool": "redownload",
         "label": "Total weight",
-        "message": "<p>The weight is of course very important if you want the page to load fast. Try to stay under 1MB, which is already very long to download over a slow connection.</p><p>Please note that Yellow Lab Tools' engine (PhantomJS) is not compatible with image srcset (unless you use a polyfill). This can lead to incorrect page weight.</p>",
-        "isOkThreshold": 1048576,
-        "isBadThreshold": 2621440,
-        "isAbnormalThreshold": 4194304,
+        "message": "<p>The weight is of course very important if you want the page to load fast. Try to stay under 1MB, which is already very long to download over a slow connection.</p>",
+        "isOkThreshold": 1572864,
+        "isBadThreshold": 3145728,
+        "isAbnormalThreshold": 5242880,
         "hasOffenders": true,
         "unit": 'bytes'
     },
     "imageOptimization": {
         "tool": "redownload",
         "label": "Image optimization",
-        "message": "<p>This metric measures the number of bytes that could be saved by optimizing images.</p><p>Image optimization is generally one of the easiest way to reduce a page weight, and as a result, the page load time. Don't use Photoshop or other image editing tools, they're not very good for optimization. Use specialized tools such as <a href=\"https://kraken.io/\" target=\"_blank\">Kraken.io</a> or the excellent <a href=\"https://imageoptim.com/\" target=\"_blank\">ImageOptim</a> on Mac. For SVG images, you can use <a href=\"https://jakearchibald.github.io/svgomg/\" target=\"_blank\">SVGOMG</a></p><p>The tools in use in YellowLabTools are not set to their maximum optimization power (JPEG quality 85), so you might be able to compress even more!</p><p>Please note that Yellow Lab Tools' engine (PhantomJS) is not compatible with image srcset (unless you use a polyfill). This can lead to incorrect page weight.</p>",
+        "message": "<p>This metric measures the number of bytes that could be saved by optimizing images.</p><p>Image optimization is generally one of the easiest way to reduce a page weight, and as a result, the page load time. Don't use Photoshop or other image editing tools, they're not very good for optimization. Use specialized tools such as <a href=\"https://kraken.io/\" target=\"_blank\">Kraken.io</a> or the excellent <a href=\"https://imageoptim.com/\" target=\"_blank\">ImageOptim</a> on Mac. For SVG images, you can use <a href=\"https://jakearchibald.github.io/svgomg/\" target=\"_blank\">SVGOMG</a>.</p><p>The tools in use in YellowLabTools are not set to their maximum optimization power (JPEG quality 85), so you might be able to compress even more!</p>",
         "isOkThreshold": 20480,
         "isBadThreshold": 204800,
         "isAbnormalThreshold": 307200,
@@ -919,19 +783,19 @@ var policies = {
     "imagesTooLarge": {
         "tool": "redownload",
         "label": "Oversized images",
-        "message": "<p>This is the number of images with a width >800px on mobile or >1500px on desktop. Try reducing their size.</p><p>Please ignore if the file is used as a sprite.</p><p>Please note that Yellow Lab Tools' engine (PhantomJS) is not compatible with image srcset (unless you use a polyfill). This can lead to incorrect detection.</p>",
+        "message": "<p>This is the number of images with a width >1200px on mobile, >1800px on tablet, >2400 on desktop, >3200px on HD desktop. Try reducing their size.</p><p>Please ignore if the file is used as a sprite.</p>",
         "isOkThreshold": 0,
         "isBadThreshold": 5,
         "isAbnormalThreshold": 10,
         "hasOffenders": true
     },
-    "gzipCompression": {
+    "compression": {
         "tool": "redownload",
-        "label": "Gzip compression",
-        "message": "<p>Measures the number of bytes that could be saved by compressing file transfers.</p><p>Gzip is a powerfull weight reducer and should be enabled on text-based assets in your server's configuration. Note that gzipping small files (< 1 KB) is arguable, and that some assets such as images should not be gzipped as they are already compressed. <a href=\"https://gist.github.com/gmetais/971ce13a1fbeebd88445\" target=\"_blank\">Here</a> is a list of Content-Types that should be gzipped.</p>",
-        "isOkThreshold": 5125,
-        "isBadThreshold": 81920,
-        "isAbnormalThreshold": 153600,
+        "label": "Brotli compression",
+        "message": "<p>Measures the number of bytes that could be saved by compressing textual files.</p><p>Gzip is a good old algorithm that offers great improvements. But Brotli is a new-generation algorithm and provides even better results. All major server systems are now compatible with Brotli.</p><p>Note that compressing small files (< 1 KB) is arguable, and that some assets such as images should not be gzipped as they are already compressed. <a href=\"https://gist.github.com/gmetais/971ce13a1fbeebd88445\" target=\"_blank\">Here</a> is a list of Content-Types that should be compressed.</p>",
+        "isOkThreshold": 20480,
+        "isBadThreshold": 204800,
+        "isAbnormalThreshold": 409600,
         "hasOffenders": true,
         "unit": 'bytes'
     },
@@ -948,40 +812,20 @@ var policies = {
     "totalRequests": {
         "tool": "redownload",
         "label": "Requests number",
-        "message": "<p>This is one of the most important performance rule. Every request is slowing down the page loading.</p><p>There are several technics to reduce their number:<ul><li>Concatenate JS files</li><li>Concatenate CSS files</li><li>Embed or inline small JS or CSS files in the HTML</li><li>Create sprites</li><li>Base64 encode small images in HTML or stylesheets</li><li>Use lazyloading for images</li></ul></p>",
-        "isOkThreshold": 20,
-        "isBadThreshold": 120,
-        "isAbnormalThreshold": 200,
+        "message": "<p>Each request slows down the page loading, especially on the protocol HTTP/1, but also a little on HTTP/2.</p><p>There are several technics to reduce their number:<ul><li>Concatenate JS files</li><li>Concatenate CSS files</li><li>Embed or inline small JS or CSS files in the HTML</li><li>Create sprites</li><li>Base64 encode small images in HTML or stylesheets</li><li>Use lazyloading for images</li></ul></p>",
+        "isOkThreshold": 80,
+        "isBadThreshold": 240,
+        "isAbnormalThreshold": 320,
         "hasOffenders": true
     },
     "domains": {
         "tool": "phantomas",
         "label": "Different domains",
         "message": "<p>For each domain met, the browser needs to make a DNS look-up, which is slow. Avoid having to many different domains and the page should render faster.</p><p>By the way, domain sharding is not a good practice anymore.</p>",
-        "isOkThreshold": 10,
-        "isBadThreshold": 25,
-        "isAbnormalThreshold": 50,
-        "hasOffenders": true,
-        "offendersTransformFn": function(offenders) {
-            return {
-                count: offenders.length,
-                list: offenders.map(function(offender) {
-                    var parts = /^([^ ]*): (\d+) request\(s\)$/.exec(offender);
-
-                    if (!parts) {
-                        debug('domains offenders transform function error with "%s"', offender);
-                        return {
-                            file: offender
-                        };
-                    }
-
-                    return {
-                        domain: parts[1],
-                        requests: parseInt(parts[2])
-                    };
-                })
-            };
-        }
+        "isOkThreshold": 12,
+        "isBadThreshold": 30,
+        "isAbnormalThreshold": 60,
+        "hasOffenders": true
     },
     "notFound": {
         "tool": "phantomas",
@@ -1022,15 +866,6 @@ var policies = {
         "isAbnormalThreshold": 5,
         "hasOffenders": true
     },
-    "smallRequests": {
-        "tool": "redownload",
-        "label": "Small requests",
-        "message": "<p>List of all requests that are less than 2 KB. Try to merge them with other files.</p>",
-        "isOkThreshold": 10,
-        "isBadThreshold": 50,
-        "isAbnormalThreshold": 80,
-        "hasOffenders": true
-    },
     "lazyLoadableImagesBelowTheFold": {
         "tool": "phantomas",
         "label": "Below the fold images",
@@ -1055,7 +890,7 @@ var policies = {
         "message": "<p>This is the number of custom web fonts loaded on the page.</p><p>Webfonts are beautiful, but heavy. You should keep their number as low as possible.</p>",
         "isOkThreshold": 1,
         "isBadThreshold": 5,
-        "isAbnormalThreshold": 7,
+        "isAbnormalThreshold": 8,
         "hasOffenders": true,
         "offendersTransformFn": function(offenders) {
             return offenders;
@@ -1064,7 +899,7 @@ var policies = {
     "heavyFonts": {
         "tool": "redownload",
         "label": "Overweighted webfonts",
-        "message": "<p>This metric is the sum of all bytes above 40KB in loaded fonts. Over this size, the font is probably not optimized for the web.</p><p>It can be a compresson issue, a font that contains too many glyphs or a font with complex shapes.</p><p>Sorry, Yellow Lab Tools is not yet compatible with the WOFF2 font format that generates 20-30% smaller fonts. You can proceed to a manual verification on a modern browser.</p>",
+        "message": "<p>This metric is the sum of all bytes above 40KB in loaded fonts. Over this size, the font is probably not optimized for the web.</p><p>It can be a compresson issue, a font that contains too many glyphs or a font with complex shapes.</p>",
         "isOkThreshold": 0,
         "isBadThreshold": 102400,
         "isAbnormalThreshold": 204800,
@@ -1074,7 +909,7 @@ var policies = {
             return offenders;
         }
     },
-    "unusedUnicodeRanges": {
+    /*"unusedUnicodeRanges": {
         "tool": "redownload",
         "label": "Unused Unicode ranges",
         "message": "<p>This metric counts the number of unused Unicode ranges inside each font. For example, one font could include Cyrillic glyphs but none of them are used on the page.</p><p>It also reveals the number of ligatures (letters that are represented differently when close to each other) and hidden chars (glyphs not linked to the unicode system that can't be displayed on the web).</p><p>Because of technical limitations, Yellow Lab Tools checks each font against the glyphs of the entire page. As a result, estimated use is >= to reality. For example, if you read that 10 glyphs are \"possibly used\", it means that these 10 glyphs are used on the page but nothing guaranties that they are displayed using this font.</p><p>Tools such as <a href=\"https://www.fontsquirrel.com/tools/webfont-generator\" target=\"_blank\">Font Squirrel</a> can remove some unicode ranges from a font.</p><p>In the case of an icon font, make sure you only keep the icons that are used on the website and to remove the others. Several tools are able to extract SVG images from a font, then some other tools can generate a font from the SVGs you want to keep.</p>",
@@ -1085,43 +920,73 @@ var policies = {
         "offendersTransformFn": function(offenders) {
             return offenders;
         }
+    },*/
+    "nonWoff2Fonts": {
+        "tool": "redownload",
+        "label": "WOFF 2",
+        "message": "<p>The fonts listed here could be lighter if they were served with the latest WOFF 2 font file format. Some online tools can help you easily convert older formats to WOFF 2.</p>",
+        "isOkThreshold": 0,
+        "isBadThreshold": 51200,
+        "isAbnormalThreshold": 122880,
+        "hasOffenders": true,
+        "unit": 'bytes'
     },
-    "http2": {
-        "label": "HTTP/2 or SPDY",
-        "message": "<p>HTTP/2 is the latest version of the HTTP protocol and is designed to optimize load speed. SPDY is deprecated but still very well supported.</p><p>The latest versions of all major browsers are now compatible. The difficulty is on the server side, where technologies are not quite ready yet.</p>",
+    "oldHttpProtocol": {
+        "label": "HTTP protocols",
+        "message": "<p>HTTP/2 is the latest version of the HTTP protocol. It is designed to optimize load speed. HTTP/3 will come soon and should be even faster!</p><p>When a domain sends more than 4 requests over HTTP/1, this metric counts one point for each new request. Below 5 requests, the benefits of HTTP/2 are generally less significant.</p>",
         "hasOffenders": true,
         "scoreFn": function(data) {
-            if (!data.toolsResults.http2) {
-                return null;
+            var count = 0;
+
+            var offenders = data.toolsResults.phantomas.offenders.oldHttpProtocol;
+
+            if (offenders) {
+                offenders.forEach(function(offender) {
+                    if (offender.requests > 4) {
+                        count += offender.requests - 4;
+                    }
+                });    
             }
 
-            var isHttp2 = data.toolsResults.http2.metrics.http2;
+            var isOkThreshold = 0;
+            var isBadThreshold = 25;
+            var isAbnormalThreshold = 100;
 
+            var score = (isBadThreshold - count) * 100 / (isBadThreshold - isOkThreshold);
+            var abnormalityScore = (isAbnormalThreshold - count) * 100 / (isAbnormalThreshold - isOkThreshold);
+            
+            
             var result = {
-                value: isHttp2 ? 'Yes' : 'No',
-                score: isHttp2 ? 100 : 0,
-                bad: !isHttp2,
-                abnormal: false,
-                abnormalityScore: 0
+                value: count,
+                score: Math.min(Math.max(Math.round(score), 0), 100),
+                bad: count > isBadThreshold,
+                abnormal: count > isAbnormalThreshold,
+                abnormalityScore: Math.min(Math.round(abnormalityScore), 0),
+                offendersObj: {
+                    count: data.toolsResults.phantomas.metrics.oldHttpProtocol,
+                    list: data.toolsResults.phantomas.offenders.oldHttpProtocol || []
+                }
             };
 
-            if (data.toolsResults.http2.offenders) {
-                result.offendersObj = {
-                    count: data.toolsResults.http2.offenders.http2.length,
-                    list: data.toolsResults.http2.offenders.http2
-                };
-            }
-
             return result;
         }
     },
+    "oldTlsProtocol": {
+        "tool": "phantomas",
+        "label": "TLS protocols",
+        "message": "<p>Counts the number of domains that use TLS versions < 1.3. This is the latest version and it includes a faster \"handshake\" technology.</p><p>The 1.0 and 1.1 versions are deprecated because they are unsafe, switch to 1.2 or above as soon as possible.</p><p>The version 1.3 includes an even faster option called 0-RTT, check your server compatibility on <a href='https://www.ssllabs.com/ssltest/' target=\"_blank\">SSL Labs</a>.</p>",
+        "isOkThreshold": 0,
+        "isBadThreshold": 5,
+        "isAbnormalThreshold": 15,
+        "hasOffenders": true
+    },
     "cachingDisabled": {
         "tool": "phantomas",
         "label": "Caching disabled",
         "message": "<p>Counts responses with caching disabled (max-age=0)</p><p>Fix immediatly if on static assets.</p>",
-        "isOkThreshold": 0,
-        "isBadThreshold": 12,
-        "isAbnormalThreshold": 25,
+        "isOkThreshold": 2,
+        "isBadThreshold": 15,
+        "isAbnormalThreshold": 30,
         "hasOffenders": true
     },
     "cachingNotSpecified": {
@@ -1138,29 +1003,14 @@ var policies = {
         "label": "Caching too short",
         "message": "<p>Responses with too short caching time (less than a week).</p><p>The longer you cache, the better. Add versionning to your static assets, if it's not already done, and set their cache time to one year.</p>",
         "isOkThreshold": 5,
-        "isBadThreshold": 20,
-        "isAbnormalThreshold": 40,
+        "isBadThreshold": 25,
+        "isAbnormalThreshold": 50,
         "hasOffenders": true,
         "offendersTransformFn": function(offenders) {
             return {
                 count: offenders.length,
                 list: offenders
-                    .map(function(offender) {
-                        var parts = /^([^ ]*) cached for (-?\d+(\.\d+)?) s$/.exec(offender);
-
-                        if (!parts) {
-                            debug('cachingTooShort offenders transform function error with "%s"', offender);
-                            return {
-                                file: offender
-                            };
-                        }
-
-                        return {
-                            file: parts[1],
-                            ttl: Math.round(parseFloat(parts[2]))
-                        };
-
-                    }).sort(function(a, b) {
+                    .sort(function(a, b) {
 
                         return a.ttl - b.ttl;
 

+ 23 - 35
lib/metadata/scoreProfileGeneric.json

@@ -5,20 +5,19 @@
             "policies": {
                 "totalWeight": 5,
                 "imageOptimization": 2,
-                "imagesTooLarge": 2,
-                "gzipCompression": 2,
-                "fileMinification": 1
+                "imagesTooLarge": 1,
+                "compression": 2,
+                "fileMinification": 2
             }
         },
         "requests": {
             "label": "Requests",
             "policies": {
-                "totalRequests": 5,
+                "totalRequests": 2,
                 "domains": 3,
-                "notFound": 3,
+                "notFound": 2,
                 "identicalFiles": 2,
                 "emptyRequests": 3,
-                "smallRequests": 1,
                 "lazyLoadableImagesBelowTheFold": 2,
                 "hiddenImages": 1
             }
@@ -32,28 +31,20 @@
                 "DOMidDuplicated": 1
             }
         },
-        "domManipulations": {
-            "label": "DOM manipulations",
+        "javascriptComplexity": {
+            "label": "JS complexity",
             "policies": {
-                "DOMaccesses": 3,
-                "queriesWithoutResults": 1,
-                "DOMqueriesAvoidable": 1
-            }
-        },
-        "scroll": {
-            "label": "Scroll bottlenecks",
-            "policies": {
-                "eventsScrollBound": 1,
-                "DOMaccessesOnScroll": 4
+                "scriptDuration": 4,
+                "DOMaccesses": 2,
+                "eventsScrollBound": 1
             }
         },
         "badJavascript": {
-            "label": "Bad JavaScript",
+            "label": "Bad JS",
             "policies": {
                 "jsErrors": 1,
                 "documentWriteCalls": 2,
                 "synchronousXHR": 5,
-                "consoleMessages": 0.5,
                 "globalVariables": 0.5
             }
         },
@@ -61,9 +52,7 @@
             "label": "jQuery",
             "policies": {
                 "jQueryVersion": 2,
-                "jQueryVersionsLoaded": 2,
-                "jQueryCallsOnEmptyObject": 1,
-                "jQueryNotDelegatedEvents": 1
+                "jQueryVersionsLoaded": 2
             }
         },
         "cssComplexity": {
@@ -80,13 +69,13 @@
         "badCSS": {
             "label": "Bad CSS",
             "policies": {
-                "cssParsingErrors": 4,
-                "cssImports": 3,
+                "cssParsingErrors": 3,
+                "cssImports": 4,
                 "cssDuplicatedSelectors": 2,
                 "cssDuplicatedProperties": 1,
                 "cssEmptyRules": 2,
                 "cssExpressions": 1,
-                "cssImportants": 3,
+                "cssImportants": 2,
                 "cssOldIEFixes": 1,
                 "cssOldPropertyPrefixes": 1,
                 "cssRedundantBodySelectors": 1,
@@ -97,14 +86,15 @@
             "label": "Web fonts",
             "policies": {
                 "fontsCount": 1,
-                "heavyFonts": 1,
-                "unusedUnicodeRanges": 1
+                "heavyFonts": 0.5,
+                "nonWoff2Fonts": 0.5
             }
         },
         "serverConfig": {
             "label": "Server config",
             "policies": {
-                "http2": 2,
+                "oldHttpProtocol": 2,
+                "oldTlsProtocol": 2,
                 "closedConnections": 2,
                 "cachingNotSpecified": 1,
                 "cachingDisabled": 1,
@@ -114,14 +104,12 @@
     },
     "globalScore": {
         "pageWeight": 3,
-        "requests": 3,
+        "requests": 2,
         "domComplexity": 2,
-        "domManipulations": 2,
-        "scroll": 1,
+        "javascriptComplexity": 2,
         "badJavascript": 2,
-        "jQuery": 1,
-        "cssSyntaxError": 1,
-        "cssComplexity": 1,
+        "jQuery": 0.5,
+        "cssComplexity": 0.5,
         "badCSS": 1,
         "fonts": 1,
         "serverConfig": 1

+ 9 - 17
lib/offendersHelpers.js

@@ -172,23 +172,15 @@ var OffendersHelpers = function() {
     };
 
     this.cssOffenderPattern = function(offender) {
-        // Remove any line breaks
-        offender = offender.replace(/(\r\n|\n|\r)/gm, '');
-
-        var parts = /^(.*) (?:<([^ \(]*)>|\[inline CSS\]) ?@ ?(\d+):(\d+)$/.exec(offender);
-        
-        if (!parts) {
-            return {
-                offender: offender
-            };
-        } else {
-            return {
-                css: parts[1],
-                file: parts[2] || null,
-                line: parseInt(parts[3], 10),
-                column: parseInt(parts[4], 10)
-            };
-        }
+        // Used to work with strings
+        // As of Phantomas v2, offender is now in JSON format.
+        // Let's just adapt this for now and we'll see later if we remove completely this function
+        return {
+            css: offender.value.message,
+            file: offender.url,
+            line: offender.value.position.start.line,
+            column: offender.value.position.start.column
+        };
     };
 
     this.fileWithSizePattern = function(fileWithSize) {

+ 2 - 1
lib/rulesChecker.js

@@ -111,7 +111,8 @@ var RulesChecker = function() {
                 if (rule) {
                     rule.policy = {
                         label: policy.label,
-                        message: policy.message
+                        message: policy.message,
+                        hasOffenders: policy.hasOffenders || false
                     };
 
                     results[metricName] = rule;

+ 60 - 15
lib/runner.js

@@ -2,10 +2,9 @@ var Q                       = require('q');
 var debug                   = require('debug')('ylt:runner');
 
 var phantomasWrapper        = require('./tools/phantomas/phantomasWrapper');
-var jsExecutionTransformer  = require('./tools/jsExecutionTransformer');
 var colorDiff               = require('./tools/colorDiff');
+var domAccessAgregator      = require('./tools/domAccessAgregator');
 var mediaQueriesChecker     = require('./tools/mediaQueriesChecker');
-var isHttp2                 = require('./tools/isHttp2');
 var redownload              = require('./tools/redownload/redownload');
 var rulesChecker            = require('./rulesChecker');
 var scoreCalculator         = require('./scoreCalculator');
@@ -25,11 +24,33 @@ var Runner = function(params) {
     // Execute Phantomas first
     phantomasWrapper.execute(data)
 
+    // For the progress bar
+    .progress(function(event) {
+        if (event === 'domReady') {
+            deferred.notify({
+                estimatedProgress: 0.15,
+                milestone: 'domReady'
+            });
+        }
+        if (event === 'domComplete') {
+            deferred.notify({
+                estimatedProgress: 0.25,
+                milestone: 'domComplete'
+            });
+        }
+    })
+
     .then(function(phantomasResults) {
+        // For the progress bar
+        deferred.notify({
+            estimatedProgress: 0.4,
+            milestone: 'phantomas'
+        });
+
         data.toolsResults.phantomas = phantomasResults;
 
-        // Treat the JS Execution Tree from offenders
-        data = jsExecutionTransformer.transform(data);
+        // Mix all DOM Access metrics together
+        data = domAccessAgregator.agregate(data);
 
         // Compare colors
         data = colorDiff.compareAllColors(data);
@@ -38,17 +59,25 @@ var Runner = function(params) {
         data = mediaQueriesChecker.analyzeMediaQueries(data);
 
         // Redownload every file
-        return redownload.recheckAllFiles(data);
+        return redownload.recheckAllFiles(data)
 
-    })
+        .progress(function(redownloadedProgress) {
+            deferred.notify({
+                estimatedProgress: 0.4 + redownloadedProgress * 0.55,
+                milestone: 'phantomas'
+            });
+        });
 
-    .then(function(data) {
-        // Check if HTTP2
-        return isHttp2.check(data);
     })
 
     .then(function(data) {
 
+        // For the progress bar
+        deferred.notify({
+            estimatedProgress: 0.99,
+            milestone: 'redownload'
+        });
+
         // Rules checker
         var policies = require('./metadata/policies');
         data.rules = rulesChecker.check(data, policies);
@@ -57,20 +86,36 @@ var Runner = function(params) {
         // Scores calculator
         var scoreProfileGeneric = require('./metadata/scoreProfileGeneric.json');
         data.scoreProfiles = {
-            generic : scoreCalculator.calculate(data, scoreProfileGeneric)
+            generic : scoreCalculator.calculate(data, scoreProfileGeneric, true)
         };
 
-        
-        delete data.toolsResults.phantomas.metrics.javascriptExecutionTree;
-        delete data.toolsResults.phantomas.offenders.javascriptExecutionTree;
-        delete data.toolsResults.phantomas.metrics.scrollExecutionTree;
-        delete data.toolsResults.phantomas.offenders.scrollExecutionTree;
+
+        // Calculate "If you fix this issue, your new score is..." on each rule
+        debug('Calculate "If you fix this issue..." scores');
+        Object.keys(data.rules).forEach(function(ruleName) {
+            // Save current values
+            var oldScore = data.rules[ruleName].score;
+            var oldAbnormalityScore = data.rules[ruleName].abnormalityScore;
+            // Simulate a 100/100 score on a specific rule
+            data.rules[ruleName].score = 100;
+            data.rules[ruleName].abnormalityScore = 0;
+            // Calculate new score
+            data.rules[ruleName].globalScoreIfFixed = scoreCalculator.calculate(data, scoreProfileGeneric, false).globalScore;
+            // Revert values
+            data.rules[ruleName].score = oldScore;
+            data.rules[ruleName].abnormalityScore = oldAbnormalityScore;
+        });
+        debug('Calculating is finished');
 
 
         if (data.toolsResults.phantomas.offenders.blockedRequests) {
             data.blockedRequests = data.toolsResults.phantomas.offenders.blockedRequests;
         }
 
+        // Report WordPress detection
+        if (data.toolsResults.redownload.metrics.isWordPress === true) {
+            data.frameworks = {isWordPress: true};
+        }
 
         // Finished!
         deferred.resolve(data);

+ 9 - 5
lib/scoreCalculator.js

@@ -4,7 +4,7 @@ var debug = require('debug')('ylt:scoreCalculator');
 var ScoreCalculator = function() {
     'use strict';
 
-    this.calculate = function(data, profile) {
+    this.calculate = function(data, profile, debugFlag) {
 
         var results = {
             categories: {}
@@ -13,7 +13,9 @@ var ScoreCalculator = function() {
         var categoryName;
         var weight;
 
-        debug('Starting calculating scores');
+        if (debugFlag) {
+            debug('Starting calculating scores');
+        }
 
         // Calculate categories
         for (categoryName in profile.categories) {
@@ -31,7 +33,7 @@ var ScoreCalculator = function() {
                 if (data.rules[policyName]) {
                     policyScore = data.rules[policyName].score + (data.rules[policyName].abnormalityScore * 2);
                     categoryScore.push(policyScore, weight);
-                } else {
+                } else if (debugFlag) {
                     debug('Warning: could not find rule %s', policyName);
                 }
 
@@ -59,8 +61,10 @@ var ScoreCalculator = function() {
         results.globalScore = Math.round(globalScore.getScore());
 
 
-        debug('Score calculation finished:');
-        debug(results);
+        if (debugFlag) {
+            debug('Score calculation finished:');
+            debug(results);
+        }
 
         return results;
     };

+ 53 - 82
lib/screenshotHandler.js

@@ -1,6 +1,5 @@
 var debug       = require('debug')('ylt:screenshotHandler');
 var Jimp        = require('jimp');
-var tmp         = require('temporary');
 var Q           = require('q');
 var fs          = require('fs');
 var path        = require('path');
@@ -8,48 +7,24 @@ 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);
-            }
-        };
-    };
+    var tmpFolderPath = 'tmp';
+    var tmpFolderFullPath = path.join(__dirname, '..', tmpFolderPath);
+    var tmpFileName = 'temp-screenshot.png';
+    var tmpFileFullPath = path.join(tmpFolderFullPath, tmpFileName);
 
 
-    this.optimize = function(imagePath, width) {
+    this.findAndOptimizeScreenshot = function(width) {
         var that = this;
 
         debug('Starting screenshot transformation');
 
-        return this.openImage(imagePath)
+        return this.openImage(tmpFileFullPath)
 
             .then(function(image) {
-
+                that.deleteTmpFile(tmpFileFullPath);
                 return that.resizeImage(image, width);
-
             })
 
-            .then(this.addWhiteBackground)
-
             .then(this.toBuffer);
     };
 
@@ -77,50 +52,41 @@ var screenshotHandler = function() {
         var deferred = Q.defer();
 
         var currentWidth = image.bitmap.width;
-        var ratio = newWidth / currentWidth;
 
-        image.scale(ratio, function(err, image){
-            if (err) {
-                debug('Could not resize image');
-                debug(err);
+        if (currentWidth > 0) {
+            var ratio = newWidth / currentWidth;
 
-                deferred.reject(err);
-            } else {
-                debug('Image correctly resized');
-                deferred.resolve(image);
-            }
-        });
+            image.scale(ratio, function(err, image){
+                if (err) {
+                    debug('Could not resize image');
+                    debug(err);
+
+                    deferred.reject(err);
+                } else {
+                    debug('Image correctly resized');
+                    deferred.resolve(image);
+                }
+            });
+        } else {
+            deferred.reject('Could not resize an empty image');
+        }
 
         return deferred.promise;        
     };
 
-    // If the page doesn't set a bg color, the default PhantomJS one is transparent
-    // When transforming PNG to JPG, transparent pixels become black.
-    // This is why we need to add a transparent background.
-    this.addWhiteBackground = function(image) {
+
+    this.toBuffer = function(image) {
         var deferred = Q.defer();
 
-        // Create a canvas with the same dimensions as your image:
-        new Jimp(image.bitmap.width, image.bitmap.height, 0xFFFFFF, function(err, canvas){
+        image.quality(85).getBuffer(Jimp.MIME_JPEG, function(err, buffer){
             if (err) {
-                debug('Could not create a white canvas');
+                debug('Could not save image to buffer');
                 debug(err);
 
                 deferred.reject(err);
             } else {
-                // Paste original image on top of the canvas
-                canvas.composite(image, 0, 0, function(err, image){
-                    if (err) {
-                        debug('Could not paste image on the white canvas');
-                        debug(err);
-
-                        deferred.reject(err);
-                    } else {
-                        // Now image has a white background...
-                        debug('White background correctly added');
-                        deferred.resolve(image);
-                    }
-                });
+                debug('Image correctly transformed to buffer');
+                deferred.resolve(buffer);
             }
         });
 
@@ -128,43 +94,48 @@ var screenshotHandler = function() {
     };
 
 
-    this.toBuffer = function(image) {
+    this.deleteTmpFile = function(tmpFilePath) {
         var deferred = Q.defer();
 
-        image.quality(90).getBuffer(Jimp.MIME_JPEG, function(err, buffer){
+        fs.unlink(tmpFilePath, function (err) {
             if (err) {
-                debug('Could not save image to buffer');
-                debug(err);
-
-                deferred.reject(err);
+                debug('Screenshot temporary file not found, could not be deleted. But it is not a problem.');
             } else {
-                debug('Image correctly transformed to buffer');
-                deferred.resolve(buffer);
+                debug('Screenshot temporary file deleted.');
             }
+
+            deferred.resolve();
         });
 
         return deferred.promise;
     };
 
-
-    this.deleteTmpFileAndFolder = function(tmpFilePath, screenshotTmpFolder) {
+    // Create a /tmp folder on the project's root directory
+    this.createTmpScreenshotFolder = function() {
         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.');
+        // Create the folder if it doesn't exist
+        fs.exists(tmpFolderFullPath, function(exists) {
+            if (exists) {
+                deferred.resolve();
             } else {
-                debug('Screenshot file deleted.');
+                debug('Creating the tmp image folder', tmpFolderFullPath);
+                fs.mkdir(tmpFolderFullPath, function(err) {
+                    if (err) {
+                        deferred.reject(err);
+                    } else {
+                        deferred.resolve();
+                    }
+                });
             }
-
-            screenshotTmpFolder.rmdir();
-            debug('Screenshot temp folder deleted');
-
-            deferred.resolve();
         });
 
         return deferred.promise;
     };
+
+    this.getTmpFileRelativePath = function() {
+        return tmpFolderPath + '/' + tmpFileName;
+    };
 };
 
 module.exports = new screenshotHandler();

+ 21 - 23
lib/server/controllers/apiController.js

@@ -51,11 +51,8 @@ var ApiController = function(app) {
             }
         };
 
-        // Create a temporary folder to save the screenshot
-        var screenshot;
-        if (run.params.screenshot) {
-            screenshot = ScreenshotHandler.getScreenshotTempFile();
-        }
+        // Create the tmp folder if it doesn't exist
+        ScreenshotHandler.createTmpScreenshotFolder(run.runId);
 
         // Add test to the testQueue
         debug('Adding test %s to the queue', run.runId);
@@ -78,7 +75,7 @@ var ApiController = function(app) {
             console.log('Launching test ' + run.runId + ' on ' + run.params.url);
 
             var runOptions = {
-                screenshot: run.params.screenshot ? screenshot.getTmpFilePath() : false,
+                screenshot: run.params.screenshot ? ScreenshotHandler.getTmpFileRelativePath() : false,
                 device: run.params.device,
                 proxy: run.params.proxy,
                 waitForSelector: run.params.waitForSelector,
@@ -87,54 +84,54 @@ var ApiController = function(app) {
                 authPass: run.params.authPass,
                 blockDomain: run.params.blockDomain,
                 allowDomain: run.params.allowDomain,
-                noExternals: run.params.noExternals,
-                phantomasEngine: serverSettings.phantomasEngine
+                noExternals: run.params.noExternals
             };
 
-            return ylt(run.params.url, runOptions);
+            return ylt(run.params.url, runOptions)
+
+            // Update the progress bar on each progress
+            .progress(function(progress) {
+                runsDatastore.updateRunProgress(run.runId, progress);
+            });
 
         })
 
-        // Phantomas completed, let's save the screenshot if any
+        // Phantomas completed
         .then(function(data) {
 
             debug('Success');
             data.runId = run.runId;
 
             
-            // Some conditional steps are made if there is a screenshot
+            // Some conditional steps exist if there is a screenshot
             var screenshotPromise = Q.resolve();
 
             if (run.params.screenshot) {
-                
+
+                var screenshotSize = serverSettings.screenshotWidth ? serverSettings.screenshotWidth[run.params.device] : 400;
+
                 // Replace the empty promise created earlier with Q.resolve()
-                screenshotPromise = screenshot.toThumbnail(serverSettings.screenshotWidth || 400)
+                screenshotPromise = ScreenshotHandler.findAndOptimizeScreenshot(screenshotSize)
                 
                     // 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';
+                            data.screenshotUrl = '/api/results/' + data.runId + '/screenshot.jpg';
                         }
-
                     })
-                    
-                    // Delete screenshot temporary file
-                    .then(screenshot.deleteTmpFile)
 
                     // Don't worry if there's an error
                     .fail(function(err) {
                         debug('An error occured while creating the screenshot\'s thumbnail. Ignoring and continuing...');
+                        debug(err);
                     });
 
             }
 
             // Let's continue
-            screenshotPromise
+            return screenshotPromise
 
                 // Save results
                 .then(function() {
@@ -142,7 +139,8 @@ var ApiController = function(app) {
                     delete data.params.options.screenshot;
 
                     // Here we can remove tools results if not needed
-
+                    delete data.toolsResults.phantomas.offenders.requests;
+                    
                     return resultsDatastore.saveResult(data);
                 })
 

+ 1 - 1
lib/server/controllers/frontController.js

@@ -10,7 +10,7 @@ var FrontController = function(app) {
     var assetsPath = (app.get('env') === 'development') ? '../../../front/src' : '../../../front/build';
 
     // Routes templating    
-    var routes = ['/', '/about', '/result/:runId', '/result/:runId/timeline', '/result/:runId/screenshot', '/result/:runId/rule/:policy', '/queue/:runId'];
+    var routes = ['/', '/about', '/result/:runId', '/result/:runId/screenshot', '/result/:runId/rule/:policy', '/queue/:runId'];
 
     routes.forEach(function(route) {
         app.get(route, function(req, res) {

+ 24 - 11
lib/server/datastores/resultsDatastore.js

@@ -16,10 +16,13 @@ function ResultsDatastore() {
 
     this.saveResult = function(testResults) {
         
+        var screenshotFilePath = path.join(resultsDir, testResults.runId, resultScreenshotName);
+        var screenshotAPIPath = '/';
+
         return createResultFolder(testResults.runId)
 
             .then(function() {
-                return saveScreenshotIfExists(testResults);
+                return saveScreenshotIfExists(testResults, screenshotFilePath);
             })
 
             .then(function() {
@@ -56,10 +59,10 @@ function ResultsDatastore() {
 
 
     // The folder /results/folderName/
-    function createResultFolder(folderName) {
-        var folder = path.join(resultsDir, folderName);
+    function createResultFolder(runId) {
+        var folder = path.join(resultsDir, runId);
 
-        debug('Creating the folder %s', folderName);
+        debug('Creating the folder %s', runId);
 
         return createGlobalFolder().then(function() {
             return Q.nfcall(fs.mkdir, folder);
@@ -89,22 +92,32 @@ function ResultsDatastore() {
         return deferred.promise;
     }
 
+    this.getResultFolder = function(runId) {
+        return path.join(resultsDir, runId);
+    };
+
     // If there is a screenshot, save it as screenshot.jpg in the same folder as the results
-    function saveScreenshotIfExists(testResults) {
+    function saveScreenshotIfExists(testResults, path) {
         var deferred = Q.defer();
 
         if (testResults.screenshotBuffer) {
 
-            var screenshotFilePath = path.join(resultsDir, testResults.runId, resultScreenshotName);
-            fs.writeFile(screenshotFilePath, testResults.screenshotBuffer);
-
+            fs.writeFile(path, testResults.screenshotBuffer, function(err) {
+                if (err) {
+                    debug('Could not save final screenshot');
+                    debug(err);
+                    // But it is OK, we don't need to fail the run
+                    deferred.resolve();
+                } else {
+                    debug('Final screenshot saved: ' + path);
+                    deferred.resolve();
+                }
+            });
             delete testResults.screenshotBuffer;
 
-        } else {
-            deferred.resolve();
         }
 
-        return deferred;
+        return deferred.promise;
     }
 
     this.getScreenshot = function(runId) {

+ 10 - 0
lib/server/datastores/runsDatastore.js

@@ -42,6 +42,16 @@ function RunsDatastore() {
     };
 
 
+    // When the test is launched, set the progress bar
+    this.updateRunProgress = function(runId, progress) {
+        var run = runs[runId];
+
+        run.progress = progress;
+
+        runs[runId] = run;
+    };
+
+
     this.markAsComplete = function(runId) {
         var run = runs[runId];
 

+ 1 - 1
lib/tools/colorDiff.js

@@ -55,7 +55,7 @@ var colorDiff = function() {
     };
 
     this.parseOffender = function(offender) {
-        var regexResult = /^(.*) \(\d+ times\)$/.exec(offender);
+        var regexResult = /^(.*) \(\d+ times\)$/.exec(offender.value.message);
         return regexResult ? regexResult[1] : null;
     };
 

+ 53 - 0
lib/tools/domAccessAgregator.js

@@ -0,0 +1,53 @@
+var debug   = require('debug')('ylt:domAccessAgregator');
+
+var domAccessAgregator = function() {
+    'use strict';
+
+    this.agregate = function(data) {
+        debug('Starting to agregate DOM Accesses...');
+
+        let count = 0;
+        let offenders = {
+            byType: {}
+        };
+
+        const metricsToGather = [
+            'DOMqueriesById',
+            'DOMqueriesByTagName',
+            'DOMqueriesByClassName',
+            'DOMqueriesByQuerySelectorAll',
+            'DOMinserts',
+            'DOMmutationsInserts',
+            'DOMmutationsRemoves',
+            'DOMmutationsAttributes',
+            'eventsBound'
+        ];
+
+        metricsToGather.forEach(key => {
+            
+            if (data.toolsResults.phantomas.metrics[key]) {
+                count += data.toolsResults.phantomas.metrics[key];
+            }
+            
+            offenders.byType[key] = [];
+            if (data.toolsResults.phantomas.offenders[key]) {
+                offenders.byType[key] = data.toolsResults.phantomas.offenders[key];
+            }
+        });
+
+        data.toolsResults.domAccessAgregator = {
+            metrics: {
+                DOMaccesses: count
+            },
+            offenders: {
+                DOMaccesses: offenders
+            }
+        };
+
+        debug('Done agregating DOM Accesses.');
+
+        return data;
+    };
+};
+
+module.exports = new domAccessAgregator();

+ 0 - 105
lib/tools/isHttp2.js

@@ -1,105 +0,0 @@
-var debug   = require('debug')('ylt:isHttp2');
-var url     = require('url');
-var Q       = require('q');
-var http2   = require('is-http2');
-
-var isHttp2 = function() {
-    'use strict';
-
-    this.check = function(data) {
-        debug('Starting to check for HTTP2 support...');
-
-        return this.checkHttp2(data)
-
-            .then(function(result) {
-
-                if (result.isHttp) {
-                    debug('The website is not even in HTTPS');
-
-                    data.toolsResults.http2 = {
-                        metrics: {
-                            http2: false
-                        }
-                    };
-
-                } else if (result.isHttp2) {
-                    debug('HTTP/2 (or SPDY) is supported');
-
-                    data.toolsResults.http2 = {
-                        metrics: {
-                            http2: true
-                        }
-                    };
-                
-                } else {
-                    debug('HTTP/2 is not supported');
-                    
-                    data.toolsResults.http2 = {
-                        metrics: {
-                            http2: false
-                        }
-                    };
-                }
-
-                // Add the supported protocols as offenders
-                if (result.supportedProtocols) {
-                    debug('Supported protocols: ' + result.supportedProtocols.join(' '));
-                    data.toolsResults.http2.offenders = {
-                        http2: result.supportedProtocols
-                    };
-                }
-
-                debug('End of HTTP2 support check');
-
-                return data;
-            })
-
-            .fail(function() {
-                return data;
-            });
-    };
-
-    this.getParsedUrl = function(data) {
-        return url.parse(data.toolsResults.phantomas.url);
-    };
-
-    this.getProtocol = function(data) {
-        return this.getParsedUrl(data).protocol;
-    };
-
-    this.getDomain = function(data) {
-        return this.getParsedUrl(data).hostname;
-    };
-
-    this.checkHttp2 = function(data) {
-        var deferred = Q.defer();
-
-        // Check if it's HTTPS first
-        if (this.getProtocol(data) === 'http:') {
-            
-            deferred.resolve({
-                isHttp: true
-            });
-
-        } else {
-
-            // To make is-http2 work, you need to have openssl in a version greater than 1.0.0 installed and available in your $path.
-            http2(this.getDomain(data), {includeSpdy: true})
-
-                .then(function(result) {
-                    deferred.resolve(result);
-                })
-                
-                .catch(function(error) {
-                    debug('Error while checking HTTP2 support:');
-                    debug(error);
-                    deferred.reject('Error while checking for HTTP2 support');
-                });
-
-        }
-
-        return deferred.promise;
-    };
-};
-
-module.exports = new isHttp2();

+ 0 - 210
lib/tools/jsExecutionTransformer.js

@@ -1,210 +0,0 @@
-var debug = require('debug')('ylt:jsExecutionTransformer');
-
-var offendersHelpers    = require('../offendersHelpers');
-var Collection          = require('./phantomas/custom_modules/util/collection');
-
-var jsExecutionTransformer = function() {
-
-    this.transform = function(data) {
-        var javascriptExecutionTree = {};
-        var jQueryFunctionsCollection = new Collection();
-        
-        var metrics = {
-            domInteractive: 0,
-            domContentLoaded: 0,
-            domContentLoadedEnd: 0,
-            domComplete: 0,
-
-            DOMaccesses: 0,
-            DOMaccessesOnScroll: 0,
-            queriesWithoutResults: 0
-        };
-
-        var offenders = {};
-
-        var hasjQuery = (data.toolsResults.phantomas.metrics.jQueryVersionsLoaded > 0);
-        if (hasjQuery) {
-            metrics.jQueryCalls = 0;
-            metrics.jQueryCallsOnEmptyObject = 0;
-            metrics.jQueryNotDelegatedEvents = 0;
-        }
-
-        try {
-
-            debug('Starting JS execution transformation');
-            javascriptExecutionTree = JSON.parse(data.toolsResults.phantomas.offenders.javascriptExecutionTree[0]);
-        
-            if (javascriptExecutionTree.children) {
-                javascriptExecutionTree.children.forEach(function(node, index) {
-                    
-                    var contextLength = (node.data.callDetails && node.data.callDetails.context) ? node.data.callDetails.context.length : null;
-
-                    if (isABindWithoutEventDelegation(node, contextLength)) {
-                        metrics.jQueryNotDelegatedEvents += contextLength;
-                        node.warning = true;
-                        node.eventNotDelegated = true;
-                    }
-
-                    if (node.data.resultsNumber === 0) {
-                        metrics.queriesWithoutResults ++;
-                        node.queryWithoutResults = true;
-                        node.warning = true;
-                    }
-
-                    if (contextLength === 0) {
-                        metrics.jQueryCallsOnEmptyObject ++;
-                        node.jQueryCallOnEmptyObject = true;
-                        node.warning = true;
-                    }
-
-                    if (node.data.type.indexOf('jQuery - ') === 0) {
-                        metrics.jQueryCalls ++;
-                        jQueryFunctionsCollection.push(node.data.type);
-                    }
-
-                    // Mark errors with an error flag
-                    if (node.data.type === 'error' || node.data.type === 'jQuery version change') {
-                        node.error = true;
-                    }
-
-                    // Mark a performance flag
-                    if (['domInteractive', 'domContentLoaded', 'domContentLoadedEnd', 'domComplete'].indexOf(node.data.type) >= 0) {
-                        node.windowPerformance = true;
-
-                        // Adjust the navigation timings (cause their not very well synchronised)
-                        switch(node.data.type) {
-                            case 'domInteractive':
-                                javascriptExecutionTree.data.domInteractive = node.data.timestamp;
-                                break;
-                            case 'domContentLoaded':
-                                javascriptExecutionTree.data.domContentLoaded = node.data.timestamp;
-                                break;
-                            case 'domContentLoadedEnd':
-                                javascriptExecutionTree.data.domContentLoadedEnd = node.data.timestamp;
-                                break;
-                            case 'domComplete':
-                                javascriptExecutionTree.data.domComplete = node.data.timestamp;
-                                break;
-                        }
-                    }
-                    // Fix rare bug when domComplete was never triggered
-                    if (index === javascriptExecutionTree.children.length - 1 && !javascriptExecutionTree.data.domComplete) {
-                        javascriptExecutionTree.data.domComplete = node.data.timestamp + 1000;
-                    }
-
-                    // Transform domPaths into objects
-                    changeListOfDomPaths(node);
-
-                    // Count the number of DOM accesses, by counting the tree leafs
-                    metrics.DOMaccesses += countTreeLeafs(node);
-                });
-
-            }
-            debug('JS execution transformation complete');
-
-
-            if (data.toolsResults.phantomas.offenders.scrollExecutionTree) {
-                debug('Starting scroll execution transformation');
-                offenders.DOMaccessesOnScroll = JSON.parse(data.toolsResults.phantomas.offenders.scrollExecutionTree[0]);
-                if (offenders.DOMaccessesOnScroll.children) {
-                    offenders.DOMaccessesOnScroll.children.forEach(function(node) {
-                        
-                        // Mark a event flag
-                        if (['documentScroll', 'windowScroll', 'window.onscroll'].indexOf(node.data.type) >= 0) {
-                            node.windowPerformance = true;
-                        }
-
-                        // Transform domPaths into objects
-                        changeListOfDomPaths(node);
-                        
-                        // Count the number of DOM accesses, by counting the tree leafs
-                        metrics.DOMaccessesOnScroll += countTreeLeafs(node);
-                    });
-                }
-                debug('Scroll execution transformation complete');
-            } else {
-                debug('Could not parse scrollExecutionTree');
-            }
-
-        } catch(err) {
-            throw err;
-        }
-
-        data.javascriptExecutionTree = javascriptExecutionTree;
-        
-        data.toolsResults.jsExecutionTransformer = {
-            metrics: metrics,
-            offenders: offenders
-        };
-
-        return data;
-    };
-
-    function treeRecursiveParser(node, fn) {
-        if (node.children) {
-            node.children.forEach(function(child) {
-                treeRecursiveParser(child, fn);
-            });
-        }
-        fn(node);
-    }
-
-    function changeListOfDomPaths(rootNode) {
-        treeRecursiveParser(rootNode, function(node) {
-            
-            if (node.data.callDetails && node.data.callDetails.context && node.data.callDetails.context.length > 0) {
-                node.data.callDetails.context.elements = node.data.callDetails.context.elements.map(offendersHelpers.domPathToDomElementObj, offendersHelpers);
-            }
-
-            if (node.data.type === 'appendChild' || node.data.type === 'insertBefore' || node.data.type === 'getComputedStyle') {
-                node.data.callDetails.arguments[0] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[0]);
-            }
-
-            if (node.data.type === 'insertBefore') {
-                node.data.callDetails.arguments[1] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[1]);
-            }
-        });
-    }
-
-    // Returns the number of leafs (nodes without children)
-    function countTreeLeafs(rootNode) {
-        var count = 0;
-
-        treeRecursiveParser(rootNode, function(node) {
-            if (!node.children &&
-                !node.error &&
-                !node.windowPerformance &&
-                node.data.type !== 'jQuery loaded') {
-                count ++;
-            }
-        });
-
-        return count;
-    }
-
-    function isPureString(str) {
-        return typeof str === 'string' && str[0] !== '{' && str !== '(function)' && str !== '[Object]' && str !== '[Array]' && str !== 'true' && str !== 'false' && str !== 'undefined' && str !== 'unknown' && str !== 'null';
-    }
-
-    function isABindWithoutEventDelegation(node, contextLength) {
-        // Count only on larger bindings
-        if (contextLength <= 3) {
-            return false;
-        }
-
-        if (node.data.type === 'jQuery - on' && node.data.callDetails.arguments[1] && !isPureString(node.data.callDetails.arguments[1])) {
-            return true;
-        }
-
-        if (node.data.type.indexOf('jQuery - ') === 0 && node.children && node.children.length === 1) {
-            var child = node.children[0];
-            if (child.data.type === 'jQuery - on' && child.data.callDetails.arguments[1] && !isPureString(child.data.callDetails.arguments[1])) {
-                return true;
-            }
-        }
-
-        return false;
-    }
-};
-
-module.exports = new jsExecutionTransformer();

+ 0 - 251
lib/tools/phantomas/custom_modules/core/scopeYLT/scopeYLT.js

@@ -1,251 +0,0 @@
-/**
- * Overwritting the original spying functions in scope.js
- * This is done so we now have a before AND an after callback on the spy
- *
- * @see http://code.jquery.com/jquery-1.10.2.js
- * @see http://code.jquery.com/jquery-2.0.3.js
- */
-/* global document: true, window: true */
-
-exports.version = '0.2';
-
-exports.module = function(phantomas) {
-    'use strict';
-
-    var responseEndTime = Date.now();
-
-    phantomas.on('responseEnd', function() {
-        responseEndTime = Date.now();
-    });
-
-    phantomas.on('init', function() {
-        phantomas.evaluate(function(responseEndTime) {
-            (function(phantomas) {
-    
-                // Overwritting phantomas spy function
-                (function() {
-                    var enabled = true;
-
-                    // turn off spying to not include internal phantomas actions
-                    function spyEnabled(state, reason) {
-                        enabled = (state === true);
-
-                        phantomas.log('Spying ' + (enabled ? 'enabled' : 'disabled') + (reason ? ' - ' + reason : ''));
-                    }
-
-                    phantomas.log('Overwritting phantomas spy function');
-
-                    function spy(obj, fn, callbackBefore, callbackAfter) {
-                        var origFn = obj && obj[fn];
-
-                        if (typeof origFn !== 'function') {
-                            return false;
-                        }
-
-                        phantomas.log('Attaching a YLT spy to "' + fn + '" function...');
-
-                        obj[fn] = function() {
-                            var result;
-                            var err;
-                            
-                            // Before
-                            if (enabled && callbackBefore) {
-                                callbackBefore.apply(this, arguments);
-                            }
-
-                            // Execute
-                            try {
-
-                                result = origFn.apply(this, arguments);
-
-                            } catch(e) {
-
-                                // Catching the err for the moment, because we need to make sure the callbackAfter function is called.
-
-                                phantomas.log('Error catched on spyed function "' + fn + '": ' + e);
-                                phantomas.log(arguments);
-
-                                err = e;
-
-                            } finally {
-
-                                // After
-                                if (enabled && callbackAfter) {
-                                    var args = Array.prototype.slice.call(arguments);
-                                    callbackAfter.apply(this, [result].concat(args));
-                                }
-
-                                if (err) {
-                                    phantomas.log('Re-throwing the error');
-                                    throw err;
-                                }
-                            }
-
-                            return result;
-                        };
-
-                        // copy custom properties of original function to the mocked one
-                        Object.keys(origFn).forEach(function(key) {
-                            obj[fn][key] = origFn[key];
-                        });
-
-                        obj[fn].prototype = origFn.prototype;
-
-                        return true;
-                    }
-
-                    phantomas.spyEnabled = spyEnabled;
-                    phantomas.spy = spy;
-                })();
-
-
-
-                // Adding some code for the Javascript execution tree construction
-                (function() {
-
-                    var root = new ContextTreeNode(null, {type: 'main'});
-                    var currentContext = root;
-                    var depth = 0;
-
-
-                    // Add a child but don't enter its context
-                    function pushContext(data) {
-                        
-                        // Some data is not needed on subchildren
-                        if (depth === 0) {
-                            data.timestamp = Date.now() - responseEndTime;
-                            data.loadingStep = phantomas.currentStep || '';
-                        }
-
-                        // Some data is not needed on subchildren
-                        if (depth > 0) {
-                            if (data.backtrace) {
-                                delete data.backtrace;
-                            }
-                            if (data.resultsNumber) {
-                                delete data.resultsNumber;
-                            }
-                        }
-
-                        currentContext.addChild(data);
-                    }
-                    
-                    // Add a child to the current context and enter its context
-                    function enterContext(data) {
-                        
-                        // Some data is not needed on subchildren
-                        if (depth === 0) {
-                            data.timestamp = Date.now() - responseEndTime;
-                            data.loadingStep = phantomas.currentStep || '';
-                        }
-
-                        // Some data is not needed on subchildren
-                        if (depth > 0) {
-                            if (data.backtrace) {
-                                delete data.backtrace;
-                            }
-                            if (data.resultsNumber) {
-                                delete data.resultsNumber;
-                            }
-                        }
-
-                        currentContext = currentContext.addChild(data);
-
-                        depth ++;
-                    }
-                    
-                    // Save given data in the current context and jump change current context to its parent
-                    function leaveContext(moreData) {
-                        
-                        // Some data is not needed on subchildren
-                        if (depth === 1) {
-                            currentContext.data.time = Date.now() - currentContext.data.timestamp - responseEndTime;
-                        }
-
-                        // Some data is not needed on subchildren
-                        if (depth > 1) {
-                            if (moreData && moreData.backtrace) {
-                                delete moreData.backtrace;
-                            }
-                            if (moreData && moreData.resultsNumber) {
-                                delete moreData.resultsNumber;
-                            }
-                        }
-
-                        // Merge previous data with moreData (ovewrites if exists)
-                        if (moreData) {
-                            for (var key in moreData) {
-                                currentContext.data[key] = moreData[key];
-                            }
-                        }
-
-                        var parent = currentContext.parent;
-                        if (parent === null) {
-                            console.error('Error: trying to close root context in ContextTree');
-                        } else {
-                            currentContext = parent;
-                        }
-
-                        depth --;
-                    }
-                    
-                    function getContextData() {
-                        return currentContext.data;
-                    }
-                    
-                    // Returns a clean object, without the parent which causes recursive loops
-                    function readFullTree() {
-                        // Return null if the contextTree is not correctly closed
-                        if (root !== currentContext) {
-                            return null;
-                        }
-
-                        function recusiveRead(node) {
-                            if (node.children.length === 0) {
-                                delete node.children;
-                            } else {
-                                for (var i=0, max=node.children.length ; i<max ; i++) {
-                                    recusiveRead(node.children[i]);
-                                }
-                            }
-                            delete node.parent;
-                        }
-                        recusiveRead(root);
-
-                        return root;
-                    }
-
-                    // Empty the tree
-                    function resetTree() {
-                        root = new ContextTreeNode(null, {type: 'main'});
-                        currentContext = root;
-                        depth = 0;
-                    }
-                    
-
-                    function ContextTreeNode(parent, data) {
-                        this.data = data;
-                        this.parent = parent;
-                        this.children = [];
-
-                        this.addChild = function(data) {
-                            var child = new ContextTreeNode(this, data);
-                            this.children.push(child);
-                            return child;
-                        };
-                    }
-
-                    phantomas.log('Adding some contextTree functions to phantomas');
-                    phantomas.pushContext = pushContext;
-                    phantomas.enterContext = enterContext;
-                    phantomas.leaveContext = leaveContext;
-                    phantomas.getContextData = getContextData;
-                    phantomas.readFullTree = readFullTree;
-                    phantomas.resetTree = resetTree;
-
-                })();
-
-            })(window.__phantomas);
-        }, responseEndTime);
-    });
-};

+ 0 - 30
lib/tools/phantomas/custom_modules/modules/ajaxReqYLT/ajaxReqYLT.js

@@ -1,30 +0,0 @@
-/**
- * Analyzes AJAX requests
- */
-/* global window: true */
-
-exports.version = '0.2.a';
-
-exports.module = function(phantomas) {
-    'use strict';
-
-    phantomas.setMetric('ajaxRequests'); // @desc number of AJAX requests
-    phantomas.setMetric('synchronousXHR'); // @desc number of synchronous 
-
-    phantomas.on('init', function() {
-        phantomas.evaluate(function() {
-            (function(phantomas) {
-                phantomas.spy(window.XMLHttpRequest.prototype, 'open', null, function(result, method, url, async) {
-                    phantomas.incrMetric('ajaxRequests');
-                    phantomas.addOffender('ajaxRequests', '<%s> [%s]', url, method);
-
-                    if (async === false) {
-                        phantomas.incrMetric('synchronousXHR');
-                        phantomas.addOffender('synchronousXHR', url);
-                        phantomas.log('ajaxRequests: synchronous XMLHttpRequest call to <%s>', url);
-                    }
-                }, true);
-            })(window.__phantomas);
-        });
-    });
-};

+ 0 - 86
lib/tools/phantomas/custom_modules/modules/domHiddenYLT/domHiddenYLT.js

@@ -1,86 +0,0 @@
-/**
- * Analyzes DOM hidden content
- */
-/* global document: true, Node: true, window: true */
-
-exports.version = '1.0.a';
-
-exports.module = function(phantomas) {
-    'use strict';
-
-    // total length of HTML of hidden elements (i.e. display: none)
-    phantomas.setMetric('hiddenContentSize'); // @desc the size of content of hidden elements on the page (with CSS display: none) @offenders
-    phantomas.setMetric('hiddenImages'); // @desc number of hidden images that can be lazy-loaded @offenders
-
-    // HTML size
-    phantomas.on('report', function() {
-        phantomas.evaluate(function() {
-            (function(phantomas) {
-                var runner = new phantomas.nodeRunner(),
-                    lazyLoadableImages = {};
-
-                phantomas.spyEnabled(false, 'analyzing hidden content');
-
-                runner.walk(document.body, function(node, depth) {
-                    switch (node.nodeType) {
-                        case Node.ELEMENT_NODE:
-                            // @see https://developer.mozilla.org/en/DOM%3awindow.getComputedStyle
-                            var styles = window.getComputedStyle(node);
-
-                            if (styles && styles.getPropertyValue('display') === 'none') {
-                                if (typeof node.innerHTML === 'string') {
-                                    var size = node.innerHTML.length;
-                                    phantomas.incrMetric('hiddenContentSize', size);
-
-                                    // log hidden containers bigger than 1 kB
-                                    if (size > 1024) {
-                                        phantomas.addOffender('hiddenContentSize', phantomas.getDOMPath(node) + ' (' + size + ' characters)');
-                                    }
-                                }
-
-                                // count hidden images that can be lazy loaded (issue #524)
-                                var images = [];
-                                if (node.tagName === 'IMG') {
-                                    images = [node];
-                                } else if (typeof node.querySelectorAll === 'function') {
-                                    images = node.querySelectorAll('img') || [];
-                                }
-
-                                for (var i = 0, len = images.length; i < len; i++) {
-                                    var src = images[i].src,
-                                        path;
-
-                                    if (src === '' || src.indexOf('data:image') === 0) continue;
-
-                                    if (images[i].width === 1 && images[i].height === 1) continue;
-
-                                    if (!lazyLoadableImages[src]) {
-                                        path = phantomas.getDOMPath(images[i]);
-
-                                        lazyLoadableImages[src] = {
-                                            path: path
-                                        };
-                                    }
-                                }
-
-                                // don't run for child nodes as they're hidden as well
-                                return false;
-                            }
-                            break;
-                    }
-                });
-
-                Object.keys(lazyLoadableImages).forEach(function(img) {
-                    var entry = lazyLoadableImages[img];
-
-                    phantomas.incrMetric('hiddenImages');
-                    phantomas.addOffender('hiddenImages', img);
-
-                    phantomas.log('hiddenImages: <%s> image (%s) is hidden and can be lazy-loaded', img, entry.path);
-                });
-
-                phantomas.spyEnabled(true);
-            }(window.__phantomas));
-        });
-    });
-};

+ 0 - 387
lib/tools/phantomas/custom_modules/modules/domQYLT/domQYLT.js

@@ -1,387 +0,0 @@
-/**
- * Analyzes DOM queries done via native DOM methods
- */
-/* global Element: true, Document: true, Node: true, window: true */
-
-exports.version = '0.10.a';
-
-exports.module = function(phantomas) {
-    'use strict';
-
-    phantomas.setMetric('DOMqueries'); // @desc number of all DOM queries @offenders
-    phantomas.setMetric('DOMqueriesWithoutResults'); // @desc number of DOM queries that retutned nothing @offenders
-    phantomas.setMetric('DOMqueriesById'); // @desc number of document.getElementById calls
-    phantomas.setMetric('DOMqueriesByClassName'); // @desc number of document.getElementsByClassName calls
-    phantomas.setMetric('DOMqueriesByTagName'); // @desc number of document.getElementsByTagName calls
-    phantomas.setMetric('DOMqueriesByQuerySelectorAll'); // @desc number of document.querySelector(All) calls
-    phantomas.setMetric('DOMinserts'); // @desc number of DOM nodes inserts
-    phantomas.setMetric('DOMqueriesDuplicated'); // @desc number of DOM queries called more than once
-    phantomas.setMetric('DOMqueriesAvoidable'); // @desc number of repeated uses of a duplicated query
-
-    // fake native DOM functions
-    phantomas.on('init', function() {
-        phantomas.evaluate(function() {
-            (function(phantomas) {
-                function querySpy(type, query, fnName, context, hasNoResults) {
-                    phantomas.emit('domQuery', type, query, fnName, context, hasNoResults); // @desc DOM query has been made
-                }
-
-                phantomas.spy(Document.prototype, 'getElementById', function(id) {
-                    phantomas.incrMetric('DOMqueriesById');
-                    phantomas.addOffender('DOMqueriesById', '#%s (in %s)', id, '#document');
-
-                    phantomas.enterContext({
-                        type: 'getElementById',
-                        callDetails: {
-                            arguments: ['#' + id]
-                        },
-                        backtrace: phantomas.getBacktrace()
-                    });
-
-                }, function(result, args) {
-                    var id = args ? '#' + args[0] : undefined;
-
-                    querySpy('id', id, 'getElementById', '#document', (result === null));
-
-                    var moreData = {
-                        resultsNumber : (result === null) ? 0 : 1
-                    };
-                    phantomas.leaveContext(moreData);
-                });
-
-                // selectors by class name
-                function selectorClassNameSpyBefore(className) {
-                    /*jshint validthis: true */
-
-                    var context = phantomas.getDOMPath(this);
-
-                    phantomas.incrMetric('DOMqueriesByClassName');
-                    phantomas.addOffender('DOMqueriesByClassName', '.%s (in %s)', className, context);
-
-                    phantomas.enterContext({
-                        type: 'getElementsByClassName',
-                        callDetails: {
-                            context: {
-                                length: 1,
-                                elements: [context]
-                            },
-                            arguments: ['.' + className]
-                        },
-                        backtrace: phantomas.getBacktrace()
-                    });
-                }
-
-                function selectorClassNameAfter(result, args) {
-                    /*jshint validthis: true */
-
-                    var className = args ? '.' + args[0] : undefined;
-                    var context = phantomas.getDOMPath(this);
-
-                    querySpy('class', className, 'getElementsByClassName', context, (result.length === 0));
-                    
-                    var moreData = {
-                        resultsNumber : (result && result.length > 0) ? result.length : 0
-                    };
-                    phantomas.leaveContext(moreData);
-                }
-
-                phantomas.spy(Document.prototype, 'getElementsByClassName', selectorClassNameSpyBefore, selectorClassNameAfter);
-                phantomas.spy(Element.prototype, 'getElementsByClassName', selectorClassNameSpyBefore, selectorClassNameAfter);
-
-                // selectors by tag name
-                function selectorTagNameSpyBefore(tagName) {
-                    /*jshint validthis: true */
-
-                    var context = phantomas.getDOMPath(this);
-
-                    phantomas.incrMetric('DOMqueriesByTagName');
-                    phantomas.addOffender('DOMqueriesByTagName', '%s (in %s)', tagName, context);
-
-                    phantomas.enterContext({
-                        type: 'getElementsByTagName',
-                        callDetails: {
-                            context: {
-                                length: 1,
-                                elements: [context]
-                            },
-                            arguments: [tagName]
-                        },
-                        backtrace: phantomas.getBacktrace()
-                    });
-                }
-
-                function selectorTagNameSpyAfter(result, args) {
-                    /*jshint validthis: true */
-                    
-                    var tagName = args ? args[0].toLowerCase() : undefined;
-                    var context = phantomas.getDOMPath(this);
-
-                    querySpy('tag name', tagName, 'getElementsByTagName', context, (result.length === 0));
-                    
-                    var moreData = {
-                        resultsNumber : (result && result.length > 0) ? result.length : 0
-                    };
-                    phantomas.leaveContext(moreData);
-                }
-
-                phantomas.spy(Document.prototype, 'getElementsByTagName', selectorTagNameSpyBefore, selectorTagNameSpyAfter);
-                phantomas.spy(Element.prototype, 'getElementsByTagName', selectorTagNameSpyBefore, selectorTagNameSpyAfter);
-
-
-                // selector queries
-                function selectorQuerySpy(selector, context) {
-                    phantomas.incrMetric('DOMqueriesByQuerySelectorAll');
-                    phantomas.addOffender('DOMqueriesByQuerySelectorAll', '%s (in %s)', selector, context);
-                }
-
-                function selectorQuerySpyBefore(selector) {
-                    /*jshint validthis: true */
-
-                    var context = phantomas.getDOMPath(this);
-                    selectorQuerySpy(selector, context);
-
-                    phantomas.enterContext({
-                        type: 'querySelector',
-                        callDetails: {
-                            context: {
-                                length: 1,
-                                elements: [context]
-                            },
-                            arguments: [selector]
-                        },
-                        backtrace: phantomas.getBacktrace()
-                    });
-                }
-
-                function selectorQuerySpyAfter(result, args) {
-                    /*jshint validthis: true */
-
-                    var selector = args ? args[0] : undefined;
-                    var context = phantomas.getDOMPath(this);
-
-                    querySpy('selector', selector, 'querySelectorAll', context, (!result || result.length === 0));
-                    
-                    var moreData = {
-                        resultsNumber : result ? 1 : 0
-                    };
-                    phantomas.leaveContext(moreData);
-                }
-
-                function selectorAllQuerySpyBefore(selector) {
-                    /*jshint validthis: true */
-
-                    var context = phantomas.getDOMPath(this);
-                    selectorQuerySpy(selector, context);
-
-                    phantomas.enterContext({
-                        type: 'querySelectorAll',
-                        callDetails: {
-                            context: {
-                                length: 1,
-                                elements: [context]
-                            },
-                            arguments: [selector]
-                        },
-                        backtrace: phantomas.getBacktrace()
-                    });
-                }
-
-                function selectorAllQuerySpryAfter(result, args) {
-                    /*jshint validthis: true */
-
-                    var selector = args ? args[0] : undefined;
-                    var context = phantomas.getDOMPath(this);
-
-                    querySpy('selector', selector, 'querySelectorAll', context, (!result || result.length === 0));
-
-                    var moreData = {
-                        resultsNumber : (result && result.length > 0) ? result.length : 0
-                    };
-                    phantomas.leaveContext(moreData);
-                }
-
-                phantomas.spy(Document.prototype, 'querySelector', selectorQuerySpyBefore, selectorQuerySpyAfter);
-                phantomas.spy(Document.prototype, 'querySelectorAll', selectorAllQuerySpyBefore, selectorAllQuerySpryAfter);
-                phantomas.spy(Element.prototype, 'querySelector', selectorQuerySpyBefore, selectorQuerySpyAfter);
-                phantomas.spy(Element.prototype, 'querySelectorAll', selectorAllQuerySpyBefore, selectorAllQuerySpryAfter);
-
-
-                // count DOM inserts
-                function appendChild(child, element, context, appended) {
-                    /*jshint validthis: true */
-
-                    // ignore appending to the node that's not yet added to DOM tree
-                    if (!element.parentNode) {
-                        return;
-                    }
-
-                    // don't count elements added to fragments as a DOM inserts (issue #350)
-                    // DocumentFragment > div[0]
-                    if (context.indexOf('DocumentFragment') === 0) {
-                        return;
-                    }
-
-                    phantomas.incrMetric('DOMinserts');
-                    phantomas.addOffender('DOMinserts', '"%s" appended to "%s"', appended, context);
-
-                    //phantomas.log('DOM insert: node "%s" appended to "%s"', appended, context);
-                }
-
-                function appendChildSpyBefore(child) {
-                    /*jshint validthis: true */
-
-                    var context = phantomas.getDOMPath(this);
-                    var appended = phantomas.getDOMPath(child);
-                    appendChild(child, this, context, appended);
-
-                    phantomas.enterContext({
-                        type: 'appendChild',
-                        callDetails: {
-                            context: {
-                                length: 1,
-                                elements: [context]
-                            },
-                            arguments: [appended]
-                        },
-                        backtrace: phantomas.getBacktrace()
-                    });
-                }
-
-                function insertBeforeSpyBefore(child, refElement) {
-                    /*jshint validthis: true */
-                    
-                    var context = phantomas.getDOMPath(this);
-                    var appended = phantomas.getDOMPath(child);
-                    var referent = phantomas.getDOMPath(refElement);
-                    appendChild(child, this, context, appended);
-
-                    phantomas.enterContext({
-                        type: 'insertBefore',
-                        callDetails: {
-                            context: {
-                                length: 1,
-                                elements: [context]
-                            },
-                            arguments: [
-                                appended,
-                                referent
-                            ]
-                        },
-                        backtrace: phantomas.getBacktrace()
-                    });
-                }
-
-                phantomas.spy(Node.prototype, 'appendChild', appendChildSpyBefore, function(result) {
-                    phantomas.leaveContext();
-                });
-                phantomas.spy(Node.prototype, 'insertBefore', insertBeforeSpyBefore, function(result) {
-                    phantomas.leaveContext();
-                });
-
-
-                phantomas.spy(Document.prototype, 'createElement', function(tagName) {
-                    
-                    phantomas.enterContext({
-                        type: 'createElement',
-                        callDetails: {
-                            arguments: [tagName]
-                        },
-                        backtrace: phantomas.getBacktrace()
-                    });
-
-                }, function(result, args) {
-                    phantomas.leaveContext();
-                });
-
-
-                phantomas.spy(Document.prototype, 'createTextNode', function(text) {
-                    
-                    phantomas.enterContext({
-                        type: 'createTextNode',
-                        callDetails: {
-                            arguments: [text]
-                        },
-                        backtrace: phantomas.getBacktrace()
-                    });
-
-                }, function(result, args) {
-                    phantomas.leaveContext();
-                });
-
-
-                phantomas.spy(Document.prototype, 'createDocumentFragment', function() {
-                    
-                    phantomas.enterContext({
-                        type: 'createDocumentFragment',
-                        callDetails: {
-                            arguments: []
-                        },
-                        backtrace: phantomas.getBacktrace()
-                    });
-
-                }, function(result, args) {
-                    phantomas.leaveContext();
-                });
-
-
-                phantomas.spy(window, 'getComputedStyle', function(element, pseudoElement) {
-                    var target = phantomas.getDOMPath(element);
-                    
-                    phantomas.enterContext({
-                        type: 'getComputedStyle',
-                        callDetails: {
-                            arguments: [target, pseudoElement]
-                        },
-                        backtrace: phantomas.getBacktrace()
-                    });
-
-                }, function(result, args) {
-                    phantomas.leaveContext();
-                });
-
-            })(window.__phantomas);
-        });
-    });
-
-    // report DOM queries that return no results (issue #420)
-    phantomas.on('domQuery', function(type, query, fnName, context, hasNoResults) {
-        // ignore DOM queries within DOM fragments (used internally by jQuery)
-        if (context.indexOf('body') !== 0 && context.indexOf('#document') !== 0) {
-            return;
-        }
-
-        if (hasNoResults === true) {
-            phantomas.incrMetric('DOMqueriesWithoutResults');
-            phantomas.addOffender('DOMqueriesWithoutResults', '%s (in %s) using %s', query, context, fnName);
-        }
-    });
-
-    // count DOM queries by either ID, tag name, class name and selector query
-    // @see https://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-document-doctype
-    var Collection = require('../../util/collection'),
-        DOMqueries = new Collection();
-
-    phantomas.on('domQuery', function(type, query, fnName, context) {
-        phantomas.log('DOM query: by %s - "%s" (using %s) in %s', type, query, fnName, context);
-        phantomas.incrMetric('DOMqueries');
-
-        if (context && (
-                context.indexOf('html') === 0 ||
-                context.indexOf('body') === 0 ||
-                context.indexOf('head') === 0 ||
-                context.indexOf('#document') === 0
-            )) {
-            DOMqueries.push(type + ' "' + query + '" with ' + fnName + ' (in context ' + context + ')');
-        }
-    });
-
-    phantomas.on('report', function() {
-        DOMqueries.sort().forEach(function(query, cnt) {
-            if (cnt > 1) {
-                phantomas.incrMetric('DOMqueriesDuplicated');
-                phantomas.incrMetric('DOMqueriesAvoidable', cnt - 1);
-                phantomas.addOffender('DOMqueriesDuplicated', '%s: %d queries', query, cnt);
-            }
-        });
-    });
-};

+ 0 - 82
lib/tools/phantomas/custom_modules/modules/eventYLT/eventYLT.js

@@ -1,82 +0,0 @@
-/**
- * Analyzes events bound to DOM elements
- */
-/* global Document: true, Element: true, window: true */
-
-exports.version = '0.4.a';
-
-exports.module = function(phantomas) {
-    'use strict';
-    
-    phantomas.setMetric('eventsBound'); // @desc number of EventTarget.addEventListener calls
-    phantomas.setMetric('eventsDispatched'); // @desc number of EventTarget.dispatchEvent calls
-    phantomas.setMetric('eventsScrollBound'); // @desc number of scroll event bounds
-
-    phantomas.on('init', function() {
-        phantomas.evaluate(function() {
-            (function(phantomas) {
-                // spy calls to EventTarget.addEventListener
-                // @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget.addEventListener
-                function eventSpyBefore(eventType) {
-                    /* jshint validthis: true */
-                    var path = phantomas.getDOMPath(this);
-                    //phantomas.log('DOM event: "' + eventType + '" bound to "' + path + '"');
-
-                    phantomas.incrMetric('eventsBound');
-                    phantomas.addOffender('eventsBound', '"%s" bound to "%s"', eventType, path);
-                    phantomas.log('event ' + eventType + ' bound');
-
-                    phantomas.enterContext({
-                        type: 'addEventListener',
-                        callDetails: {
-                            context: {
-                                length: 1,
-                                elements: [path]
-                            },
-                            arguments: [eventType]
-                        },
-                        backtrace: phantomas.getBacktrace()
-                    });
-
-                    // count window.addEventListener('scroll', ...) - issue #508
-                    if (eventType === 'scroll' && (path === 'window' || path === '#document')) {
-                        phantomas.incrMetric('eventsScrollBound');
-                        phantomas.addOffender('eventsScrollBound', 'bound by %s on %s', phantomas.getBacktrace(), path);
-                    }
-                }
-
-                function eventSpyAfter(result) {
-                    phantomas.leaveContext();
-                }
-
-                phantomas.spy(Element.prototype, 'addEventListener', eventSpyBefore, eventSpyAfter);
-                phantomas.spy(Document.prototype, 'addEventListener', eventSpyBefore, eventSpyAfter);
-                phantomas.spy(window, 'addEventListener', eventSpyBefore, eventSpyAfter);
-
-                // spy calls to EventTarget.dispatchEvent
-                // @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget.dispatchEvent
-                phantomas.spy(Element.prototype, 'dispatchEvent', function(ev) {
-                    /* jshint validthis: true */
-                    var path = phantomas.getDOMPath(this);
-
-                    phantomas.log('Core JS event: triggered "%s" on "%s"', ev.type, path);
-
-                    phantomas.incrMetric('eventsDispatched');
-                    phantomas.addOffender('eventsDispatched', '"%s" on "%s"', ev.type, path);
-                });
-            })(window.__phantomas);
-        });
-    });
-
-    phantomas.on('report', function() {
-        phantomas.evaluate(function() {
-            (function(phantomas) {
-                // Check if a window.onscroll function is defined
-                if (typeof(window.onscroll) === "function") {
-                    phantomas.incrMetric('eventsScrollBound');
-                    phantomas.addOffender('eventsScrollBound', 'bound by %s on %s', '', 'window.onscroll');
-                }
-            }(window.__phantomas));
-        });
-    });
-};

+ 0 - 381
lib/tools/phantomas/custom_modules/modules/jQYLT/jQYLT.js

@@ -1,381 +0,0 @@
-/**
- * Analyzes jQuery activity
- *
- * @see http://code.jquery.com/jquery-1.10.2.js
- * @see http://code.jquery.com/jquery-2.0.3.js
- */
-/* global document: true, window: true */
-/* jshint -W030 */
-
-exports.version = '1.0.a';
-
-exports.module = function(phantomas) {
-    'use strict';
-
-    phantomas.setMetric('jQueryVersion', ''); // @desc version of jQuery framework (if loaded) [string]
-    phantomas.setMetric('jQueryVersionsLoaded'); // @desc number of loaded jQuery "instances" (even in the same version)
-    phantomas.setMetric('jQueryOnDOMReadyFunctions'); // @desc number of functions bound to onDOMReady event
-    phantomas.setMetric('jQueryWindowOnLoadFunctions'); // @desc number of functions bound to windowOnLoad event
-    phantomas.setMetric('jQuerySizzleCalls'); // @desc number of calls to Sizzle (including those that will be resolved using querySelectorAll)
-    phantomas.setMetric('jQueryEventTriggers'); // @desc number of jQuery event triggers
-
-    var jQueryFunctions = [
-        // DOM manipulations
-        'html',
-        'append',
-        'appendTo',
-        'prepend',
-        'prependTo',
-        'before',
-        'insertBefore',
-        'after',
-        'insertAfter',
-        'remove',
-        'detach',
-        'empty',
-        'clone',
-        'replaceWith',
-        'replaceAll',
-        'text',
-        'wrap',
-        'wrapAll',
-        'wrapInner',
-        'unwrap',
-
-        // Style manipulations
-        'css',
-        'offset',
-        'position',
-        'height',
-        'innerHeight',
-        'outerHeight',
-        'width',
-        'innerWidth',
-        'outerWidth',
-        'scrollLeft',
-        'scrollTop',
-
-        // Animations
-        'hide',
-        'show',
-        'toggle',
-        'animate',
-        'fadeIn',
-        'fadeOut',
-        'fadeTo',
-        'fadeToggle',
-        'slideDown',
-        'slideUp',
-        'slideToggle',
-
-        // Generic events
-        'on',
-        'off',
-        'live',
-        'die',
-        'delegate',
-        'undelegate',
-        'one',
-        'bind',
-        'unbind',
-
-        // More events
-        'blur',
-        'change',
-        'click',
-        'dblclick',
-        'error',
-        'focus',
-        'focusin',
-        'focusout',
-        'hover',
-        'keydown',
-        'keypress',
-        'keyup',
-        'load',
-        'mousedown',
-        'mouseenter',
-        'mouseleave',
-        'mousemove',
-        'mouseout',
-        'mouseover',
-        'mouseup',
-        'resize',
-        'scroll',
-        'select',
-        'submit',
-        'unload',
-
-        // Attributes
-        'attr',
-        'prop',
-        'removeAttr',
-        'removeProp',
-        'val',
-        'hasClass',
-        'addClass',
-        'removeClass',
-        'toggleClass',
-    ];
-
-    var jQueryTraversalFunctions = [
-        'children',
-        'closest',
-        'find',
-        'next',
-        'nextAll',
-        'nextUntil',
-        'offsetParent',
-        'parent',
-        'parents',
-        'parentsUntil',
-        'prev',
-        'prevAll',
-        'prevUntil',
-        'siblings'
-    ];
-
-    jQueryFunctions = jQueryFunctions.concat(jQueryTraversalFunctions);
-
-    // spy calls to jQuery functions
-    phantomas.on('init', function() {
-        phantomas.evaluate(function(jQueryFunctions, jQueryTraversalFunctions) {
-            (function(phantomas) {
-                var oldJQuery;
-
-                phantomas.spyGlobalVar('jQuery', function(jQuery) {
-                    var version;
-
-                    if (!jQuery || !jQuery.fn) {
-                        phantomas.log('jQuery: unable to detect version!');
-                        return;
-                    }
-
-                    // Tag the current version of jQuery to avoid multiple reports of jQuery being loaded
-                    // when it's actually only restored via $.noConflict(true) - see comments in #435
-                    if (jQuery.__phantomas === true) {
-                        phantomas.log('jQuery: this instance has already been seen by phantomas');
-                        return;
-                    }
-                    jQuery.__phantomas = true;
-
-                    // report the version of jQuery
-                    version = jQuery.fn.jquery;
-                    phantomas.emit('jQueryLoaded', version);
-
-                    phantomas.pushContext({
-                        type: (oldJQuery) ? 'jQuery version change' : 'jQuery loaded',
-                        callDetails: {
-                            arguments: ['version ' + version]
-                        },
-                        backtrace: phantomas.getBacktrace()
-                    });
-                    oldJQuery = version;
-
-                    // jQuery.ready.promise
-                    // works for jQuery 1.8.0+ (released Aug 09 2012)
-                    phantomas.spy(jQuery.ready, 'promise', function(func) {
-                        phantomas.incrMetric('jQueryOnDOMReadyFunctions');
-                        phantomas.addOffender('jQueryOnDOMReadyFunctions', phantomas.getCaller(3));
-
-                        phantomas.pushContext({
-                            type: 'jQuery - onDOMReady',
-                            callDetails: {
-                                arguments: [func]
-                            },
-                            backtrace: phantomas.getBacktrace()
-                        });
-
-                    }) || phantomas.log('jQuery: can not measure jQueryOnDOMReadyFunctions (jQuery used on the page is too old)!');
-
-
-                    // Sizzle calls - jQuery.find
-                    // works for jQuery 1.3+ (released Jan 13 2009)
-                    phantomas.spy(jQuery, 'find', function(selector, context) {
-                        phantomas.incrMetric('jQuerySizzleCalls');
-                        phantomas.addOffender('jQuerySizzleCalls', '%s (in %s)', selector, (phantomas.getDOMPath(context) || 'unknown'));
-                        
-                        phantomas.enterContext({
-                            type: 'jQuery - Sizzle call',
-                            callDetails: {
-                                context: {
-                                    length: 1,
-                                    elements: [phantomas.getDOMPath(context)]
-                                },
-                                arguments: [selector]
-                            },
-                            backtrace: phantomas.getBacktrace()
-                        });
-
-                    }, function(result) {
-                        var moreData = {
-                            resultsNumber : (result && result.length) ? result.length : 0
-                        };
-                        phantomas.leaveContext(moreData);
-                    }) || phantomas.log('jQuery: can not measure jQuerySizzleCalls (jQuery used on the page is too old)!');
-
-
-                    phantomas.spy(jQuery.fn, 'init', function(selector, context) {
-                        if (typeof selector === 'string' && /^#([\w\-]*)$/.exec(selector) !== null && !context) {
-
-                            phantomas.enterContext({
-                                type: 'jQuery - find',
-                                callDetails: {
-                                    arguments: [selector]
-                                },
-                                backtrace: phantomas.getBacktrace()
-                            });
-
-                        }
-
-                    }, function(result) {
-                        var data = phantomas.getContextData();
-
-                        if (data.type === 'jQuery - find' &&
-                                !data.callDetails.context &&
-                                data.callDetails.arguments.length === 1 &&
-                                /^#([\w\-]*)$/.exec(data.callDetails.arguments[0]) !== null) {
-
-                            var moreData = {
-                                resultsNumber : (result && result.length) ? result.length : 0
-                            };
-                            phantomas.leaveContext(moreData);
-                        }
-                    });
-
-
-                    if (!jQuery.event) {
-                        phantomas.spy(jQuery.event, 'trigger', function(ev, data, elem) {
-                            var path = phantomas.getDOMPath(elem),
-                                type = ev.type || ev;
-
-                            phantomas.log('Event: triggered "%s" on "%s"', type, path);
-
-                            phantomas.incrMetric('jQueryEventTriggers');
-                            phantomas.addOffender('jQueryEventTriggers', '"%s" on "%s"', type, path);
-                        });
-                    }
-
-                    // jQuery events bound to window' onLoad event (#451)
-                    phantomas.spy(jQuery.fn, 'on', function(eventName, func) {
-                        if ((eventName === 'load') && (this[0] === window)) {
-                            phantomas.incrMetric('jQueryWindowOnLoadFunctions');
-                            phantomas.addOffender('jQueryWindowOnLoadFunctions', phantomas.getCaller(2));
-                        }
-                    });
-
-                    // Add spys on many jQuery functions
-                    jQueryFunctions.forEach(function(functionName) {
-                        
-                        phantomas.spy(jQuery.fn, functionName, function(args) {
-
-                            // Clean args
-                            args = [].slice.call(arguments);
-                            args.forEach(function(arg, index) {
-                                
-                                if (arg instanceof Array) {
-                                    arg = '[Array]';
-                                }
-
-                                if (arg instanceof Object) {
-                                    
-                                    if (arg instanceof jQuery || (arg.jquery && arg.jquery.length > 0)) {
-                                        
-                                        arg = phantomas.getDOMPath(arg[0]) || 'unknown';
-
-                                    } else if (arg instanceof HTMLElement) {
-                                        
-                                        arg = phantomas.getDOMPath(arg) || 'unknown';
-
-                                    } else if (typeof arg === 'function') {
-
-                                        arg = '(function)';
-
-                                    } else {
-                                        
-                                        try {
-
-                                            for (var key in arg) {
-                                                if (typeof arg[key] === 'function') {
-                                                    arg[key] = '(function)';
-                                                }
-                                            }
-
-                                            arg = JSON.stringify(arg);
-                                        } catch(e) {
-                                            arg = '[Object]';
-                                        }
-                                        
-                                    }
-                                }
-
-                                if ((typeof arg === 'string' || arg instanceof String) && arg.length > 200) {
-                                    arg = arg.substring(0, 200) + '...';
-                                }
-
-                                if (typeof arg === 'function') {
-                                    arg = '(function)';
-                                }
-
-                                if (arg === true) {
-                                    arg = 'true';
-                                }
-
-                                if (arg === false) {
-                                    arg = 'false';
-                                }
-
-                                if (arg === null) {
-                                    arg = 'null';
-                                }
-
-                                if (typeof arg !== 'number' && typeof arg !== 'string' && !(arg instanceof String)) {
-                                    arg = 'undefined';
-                                }
-
-                                args[index] = arg;
-                            });
-
-                            var elements = [];
-                            for (var i = 0 ; i < this.length ; i++) {
-                                elements.push(phantomas.getDOMPath(this[i]));
-                            }
-
-                            phantomas.enterContext({
-                                type: 'jQuery - ' + functionName,
-                                callDetails: {
-                                    context: {
-                                        length: this.length,
-                                        elements: elements
-                                    },
-                                    arguments: args
-                                },
-                                backtrace: phantomas.getBacktrace()
-                            });
-
-                        }, function(result) {
-                            if (jQueryTraversalFunctions.indexOf(functionName) >= 0) {
-                                var moreData = {
-                                    resultsNumber : (result && result.length) ? result.length : 0
-                                };
-                                phantomas.leaveContext(moreData);
-                            } else {
-                                phantomas.leaveContext();
-                            }
-                        }) || phantomas.log('jQuery: can not track jQuery - ' + functionName + ' (this version of jQuery doesn\'t support it)');
-                    });
-                });
-            })(window.__phantomas);
-        }, jQueryFunctions, jQueryTraversalFunctions);
-    });
-
-
-    phantomas.on('jQueryLoaded', function(version) {
-        phantomas.log('jQuery: loaded v' + version);
-        phantomas.setMetric('jQueryVersion', version);
-
-        // report multiple jQuery "instances" (issue #435)
-        phantomas.incrMetric('jQueryVersionsLoaded');
-        phantomas.addOffender('jQueryVersionsLoaded', 'v%s', version);
-    });
-};

+ 0 - 69
lib/tools/phantomas/custom_modules/modules/javaScriptBottleYLT/javaScriptBottleYLT.js

@@ -1,69 +0,0 @@
-/**
- * Reports the use of functions known to be serious performance bottlenecks in JS
- *
- * @see http://www.nczonline.net/blog/2013/06/25/eval-isnt-evil-just-misunderstood/
- * @see http://www.quirksmode.org/blog/archives/2005/06/three_javascrip_1.html
- * @see http://www.stevesouders.com/blog/2012/04/10/dont-docwrite-scripts/
- *
- * Run phantomas with --spy-eval to count eval() calls (see issue #467)
- */
-/* global document: true, window: true */
-
-exports.version = '0.2';
-
-exports.module = function(phantomas) {
-    'use strict';
-    
-    phantomas.setMetric('documentWriteCalls'); //@desc number of calls to either document.write or document.writeln @offenders
-    phantomas.setMetric('evalCalls'); // @desc number of calls to eval (either direct or via setTimeout / setInterval) @offenders
-
-    // spy calls to eval only when requested (issue #467)
-    var spyEval = phantomas.getParam('spy-eval') === true;
-    if (!spyEval) {
-        phantomas.log('javaScriptBottlenecks: to spy calls to eval() run phantomas with --spy-eval option');
-    }
-
-    phantomas.on('init', function() {
-        phantomas.evaluate(function(spyEval) {
-            (function(phantomas) {
-                function report(msg, caller, backtrace, metric) {
-                    phantomas.log(msg + ': from ' + caller + '!');
-                    phantomas.log('Backtrace: ' + backtrace);
-
-                    phantomas.incrMetric(metric);
-                    phantomas.addOffender(metric, "%s from %s", msg, caller);
-                }
-
-                // spy calls to eval()
-                if (spyEval) {
-                    phantomas.spy(window, 'eval', function(code) {
-                        report('eval() called directly', phantomas.getCaller(), phantomas.getBacktrace(), 'evalCalls');
-                        phantomas.log('eval\'ed code: ' + (code || '').substring(0, 150) + '(...)');
-                    });
-                }
-
-                // spy calls to setTimeout / setInterval with string passed instead of a function
-                phantomas.spy(window, 'setTimeout', function(fn, interval) {
-                    if (typeof fn !== 'string') return;
-
-                    report('eval() called via setTimeout("' + fn + '")', phantomas.getCaller(), phantomas.getBacktrace(), 'evalCalls');
-                });
-
-                phantomas.spy(window, 'setInterval', function(fn, interval) {
-                    if (typeof fn !== 'string') return;
-
-                    report('eval() called via setInterval("' + fn + '")', phantomas.getCaller(), phantomas.getBacktrace(), 'evalCalls');
-                });
-
-                // spy document.write(ln)
-                phantomas.spy(document, 'write', function(arg) {
-                    report('document.write() used', phantomas.getCaller(), phantomas.getBacktrace(), 'documentWriteCalls');
-                });
-
-                phantomas.spy(document, 'writeln', function(arg) {
-                    report('document.writeln() used', phantomas.getCaller(), phantomas.getBacktrace(), 'documentWriteCalls');
-                });
-            })(window.__phantomas);
-        }, spyEval);
-    });
-};

+ 0 - 49
lib/tools/phantomas/custom_modules/modules/jsErrYLT/jsErrYLT.js

@@ -1,49 +0,0 @@
-/**
- * Meters the number of page errors, and provides traces as offenders for "jsErrors" metric
- */
-
-exports.version = '0.3.a';
-
-exports.module = function(phantomas) {
-    'use strict';
-    
-    phantomas.setMetric('jsErrors'); // @desc number of JavaScript errors
-    
-    function formatTrace(trace) {
-        var ret = [];
-
-        if(Array.isArray(trace)) {
-            trace.forEach(function(entry) {
-                ret.push((entry.function ? entry.function + ' ' : '') + (entry.sourceURL || entry.file) + ':' + entry.line);
-            });
-        }
-
-        return ret;
-    }
-
-    phantomas.on('jserror', function(msg, trace) {
-        trace = formatTrace(trace);
-
-        phantomas.log(msg);
-        phantomas.log('Backtrace: ' + trace.join(' / '));
-
-        phantomas.incrMetric('jsErrors');
-        phantomas.addOffender('jsErrors', msg + ' - ' + trace.join(' / '));
-
-        // Yeah, this is weird, i'm sending the error back to the browser...
-        phantomas.evaluate(function(msg, caller, trace) {
-            (function(phantomas) {
-
-                phantomas.pushContext({
-                    type: 'error',
-                    callDetails: {
-                        arguments: [msg]
-                    },
-                    caller: caller,
-                    backtrace: trace
-                });
-
-            })(window.__phantomas);
-        }, msg, trace[0], trace.join(' / '));
-    });
-};

+ 0 - 31
lib/tools/phantomas/custom_modules/modules/jsTreeYLT/jsTreeYLT.js

@@ -1,31 +0,0 @@
-/**
- * Saves the javascript interractions with the DOM
- *
- * Run phantomas with --js-execution-tree option to use this module
- */
-
-exports.version = '0.1';
-
-exports.module = function(phantomas) {
-    'use strict';
-
-    phantomas.setMetric('javascriptExecutionTree');
-
-    // save data
-    phantomas.on('report', function() {
-        phantomas.log('JS execution tree: Reading execution tree JSON');
-
-        phantomas.evaluate(function() {(function(phantomas) {
-            var fullTree = phantomas.readFullTree();
-
-            if (fullTree === null) {
-                phantomas.log('JS execution tree: error, the execution tree is not correctly closed');
-                return;
-            }
-
-            phantomas.setMetric('javascriptExecutionTree', true, true);
-            phantomas.addOffender('javascriptExecutionTree', JSON.stringify(fullTree));
-
-        })(window.__phantomas);});
-    });
-};

+ 0 - 81
lib/tools/phantomas/custom_modules/modules/lazyLoadableYLT/lazyLoadableYLT.js

@@ -1,81 +0,0 @@
-/**
- * Analyzes images and detects which one can be lazy-loaded (are below the fold)
- *
- * @see https://github.com/macbre/phantomas/issues/494
- */
-/* global document: true, window: true */
-
-exports.version = '1.0.a';
-
-exports.module = function(phantomas) {
-    'use strict';
-    
-    phantomas.setMetric('lazyLoadableImagesBelowTheFold'); // @desc number of images displayed below the fold that can be lazy-loaded
-
-    phantomas.on('report', function() {
-        phantomas.log('lazyLoadableImages: analyzing which images can be lazy-loaded...');
-
-        phantomas.evaluate(function() {
-            (function(phantomas) {
-                phantomas.spyEnabled(false, 'analyzing which images can be lazy-loaded');
-
-                var images = document.body.getElementsByTagName('img'),
-                    i,
-                    len = images.length,
-                    offset,
-                    path,
-                    processedImages = {},
-                    src,
-                    viewportHeight = window.innerHeight,
-                    // Add an offset of 100px under the height of the screen
-                    LAZYLOAD_OFFSET = 100;
-
-                phantomas.log('lazyLoadableImages: %d image(s) found, assuming %dpx offset to be the fold', len, viewportHeight);
-
-                for (i = 0; i < len; i++) {
-                    // @see https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect
-                    offset = images[i].getBoundingClientRect().top;
-                    src = images[i].src;
-
-                    // ignore base64-encoded images
-                    if (src === '' || /^data:/.test(src)) {
-                        continue;
-                    }
-
-                    path = phantomas.getDOMPath(images[i]);
-
-                    // get the most top position for a given image (deduplicate by src)
-                    if (typeof processedImages[src] === 'undefined') {
-                        processedImages[src] = {
-                            offset: offset,
-                            path: path
-                        };
-                    }
-
-                    // maybe there's the same image loaded above the fold?
-                    if (offset < processedImages[src].offset) {
-                        processedImages[src] = {
-                            offset: offset,
-                            path: path
-                        };
-                    }
-                }
-
-                phantomas.log('lazyLoadableImages: checking %d unique image(s)', Object.keys(processedImages).length);
-
-                Object.keys(processedImages).forEach(function(src) {
-                    var img = processedImages[src];
-
-                    if (img.offset > viewportHeight + LAZYLOAD_OFFSET) {
-                        phantomas.log('lazyLoadableImages: <%s> image (%s) is below the fold (at %dpx)', src, img.path, img.offset);
-
-                        phantomas.incrMetric('lazyLoadableImagesBelowTheFold');
-                        phantomas.addOffender('lazyLoadableImagesBelowTheFold', src);
-                    }
-                });
-
-                phantomas.spyEnabled(true);
-            })(window.__phantomas);
-        });
-    });
-};

+ 0 - 23
lib/tools/phantomas/custom_modules/modules/requestsList/requestsList.js

@@ -1,23 +0,0 @@
-/**
- * Retries download on every request to get the real file size
- *
- */
-
-exports.version = '0.1';
-
-exports.module = function(phantomas) {
-    'use strict';
-
-    phantomas.setMetric('requestsList');
-
-    var requests = [];
-
-    phantomas.on('recv', function(entry, res) {
-        requests.push(entry);
-    });
-
-    phantomas.on('report', function() {
-        phantomas.setMetric('requestsList', true, true);
-        phantomas.addOffender('requestsList', JSON.stringify(requests));
-    });
-};

+ 0 - 69
lib/tools/phantomas/custom_modules/modules/scrollListener/scrollListener.js

@@ -1,69 +0,0 @@
-exports.version = '0.1';
-
-exports.module = function(phantomas) {
-    'use strict';
-
-    phantomas.setMetric('scrollExecutionTree');
-
-    phantomas.on('report', function() {
-
-        phantomas.evaluate(function() {
-            (function(phantomas) {
-
-                var evt = document.createEvent('CustomEvent');
-                evt.initCustomEvent('scroll', false, false, null);
-
-                function triggerScrollEvent() {
-                    phantomas.resetTree();
-
-                    try {
-
-                        // Chrome triggers them in this order:
-
-                        // 1. document
-                        phantomas.pushContext({
-                            type: 'documentScroll'
-                        });
-                        document.dispatchEvent(evt);
-
-                        // 2. window
-                        phantomas.pushContext({
-                            type: 'windowScroll'
-                        });
-                        window.dispatchEvent(evt);
-
-                        // No need to call window.onscroll(), it's called by the scroll event on window
-
-                    } catch(e) {
-                        phantomas.log('ScrollListener error: %s', e);
-                    }
-                }
-
-                var firstScrollTime = Date.now();
-                phantomas.log('ScrollListener: triggering a first scroll event...');
-                triggerScrollEvent();
-
-
-                // Ignore the first scroll event and only save the second one,
-                // because we want to detect un-throttled things, throttled ones are ok.
-                var secondScrollTime = Date.now();
-                phantomas.log('ScrollListener: triggering a second scroll event (%dms after the first)...', secondScrollTime - firstScrollTime);
-                triggerScrollEvent();
-
-
-                var fullTree = phantomas.readFullTree();
-                if (fullTree !== null) {
-                    phantomas.setMetric('scrollExecutionTree', true, true);
-                    phantomas.addOffender('scrollExecutionTree', JSON.stringify(fullTree));
-                    phantomas.log('ScrollListener: scrollExecutionTree correctly extracted');
-                } else {
-                    phantomas.log('Error: scrollExecutionTree could not be extracted');
-                }
-
-
-                phantomas.log('ScrollListener: end of scroll triggering');
-
-            })(window.__phantomas);
-        });
-    });
-};

+ 0 - 150
lib/tools/phantomas/custom_modules/modules/windowPerfYLT/windowPerfYLT.js

@@ -1,150 +0,0 @@
-/**
- * Measure when the page reaches certain states
- *
- * @see http://w3c-test.org/webperf/specs/NavigationTiming/#dom-performancetiming-domloading
- * @see https://developers.google.com/web/fundamentals/performance/critical-rendering-path/measure-crp
- */
-/* global document: true, window: true */
-
-exports.version = '1.0.a';
-
-exports.module = function(phantomas) {
-    'use strict';
-    
-    // times below are calculated relative to performance.timing.responseEnd (#117)
-    phantomas.setMetric('domInteractive');      // @desc time it took to parse the HTML and construct the DOM
-    phantomas.setMetric('domContentLoaded');    // @desc time it took to construct both DOM and CSSOM, no stylesheets that are blocking JavaScript execution (i.e. onDOMReady)
-    phantomas.setMetric('domContentLoadedEnd'); // @desc time it took to finish handling of onDOMReady event @unreliable
-    phantomas.setMetric('domComplete');         // @desc time it took to load all page resources, the loading spinner has stopped spinning
-
-    // backend vs frontend time
-    phantomas.setMetric('timeBackend');  // @desc time to the first byte compared to the total loading time [%]
-    phantomas.setMetric('timeFrontend'); // @desc time to window.load compared to the total loading time [%]
-
-    // measure dom... metrics from the moment HTML response was fully received
-    var responseEndTime = Date.now();
-
-    phantomas.on('responseEnd', function() {
-        responseEndTime = Date.now();
-        phantomas.log('Performance timing: responseEnd = %d', responseEndTime);
-    });
-
-    phantomas.on('init', function() {
-        phantomas.evaluate(function(responseEndTime) {
-            (function(phantomas) {
-                phantomas.spyEnabled(false, 'installing window.performance metrics');
-
-                phantomas.currentStep = 'domCreation';
-
-                // extend window.performance
-                // "init" event is sometimes fired twice, pass a value set by "responseEnd" event handler (fixes #192)
-                if (typeof window.performance === 'undefined') {
-                    window.performance = {
-                        timing: {
-                            responseEnd: responseEndTime
-                        }
-                    };
-
-                    phantomas.log('Performance timing: emulating window.performance');
-                }
-                else {
-                    phantomas.log('Performance timing: using native window.performance');
-                }
-
-                // onDOMReady
-                document.addEventListener("DOMContentLoaded", function() {
-                    
-                    setTimeout(function() {
-                        // use NavigationTiming if possible
-                        var time = window.performance.timing.domContentLoadedEventEnd ?
-                            (window.performance.timing.domContentLoadedEventEnd - window.performance.timing.responseEnd)
-                            :
-                            (Date.now() - responseEndTime);
-
-                        phantomas.currentStep = 'domContentLoadedEnd';
-                        phantomas.setMetric('domContentLoadedEnd', time, true);
-                        phantomas.log('Performance timing: document reached "DOMContentLoadedEnd" state after %d ms', time);
-
-                        phantomas.pushContext({
-                            type: 'domContentLoadedEnd'
-                        });
-                    }, 0);
-
-                    var time = Date.now() - responseEndTime;
-
-                    phantomas.currentStep = 'domContentLoaded';
-                    phantomas.setMetric('domContentLoaded', time, true);
-                    phantomas.log('Performance timing: document reached "DOMContentLoaded" state after %d ms', time);
-
-                    phantomas.pushContext({
-                        type: 'domContentLoaded'
-                    });
-                });
-
-                // emulate Navigation Timing
-                document.addEventListener('readystatechange', function() {
-                    var readyState = document.readyState,
-                        responseEndTime = window.performance.timing.responseEnd,
-                        time = Date.now() - responseEndTime,
-                        metricName;
-
-                    // @see http://www.w3.org/TR/html5/dom.html#documentreadystate
-                    switch(readyState) {
-                        // the browser has finished parsing all of the HTML and DOM construction is complete
-                        case 'interactive':
-                            metricName = 'domInteractive';
-                            break;
-
-                        // the processing is complete and all of the resources on the page have finished downloading
-                        case 'complete':
-                            metricName = 'domComplete';
-                            phantomas.log('Performance timing: %j', window.performance.timing);
-                            break;
-
-                        default:
-                            phantomas.log('Performance timing: unhandled "%s" state!', readyState);
-                            return;
-                    }
-
-                    phantomas.currentStep = metricName;
-                    phantomas.setMetric(metricName, time, true);
-                    phantomas.log('Performance timing: document reached "%s" state after %d ms', readyState, time);
-
-                    phantomas.pushContext({
-                        type: metricName
-                    });
-                });
-
-                phantomas.spyEnabled(true);
-            })(window.__phantomas);
-        }, responseEndTime);
-    });
-
-    /**
-     * Emit metrics with backend vs frontend time
-     *
-     * Performance Golden Rule:
-     * "80-90% of the end-user response time is spent on the frontend. Start there."
-     *
-     * @see http://www.stevesouders.com/blog/2012/02/10/the-performance-golden-rule/
-     */
-    phantomas.on('report', function() {
-        //  The “backend” time is the time it takes the server to get the first byte back to the client.
-        //  The “frontend” time is measured from the last byte of the response (responseEnd) until all resources are fetched (domComplete)
-        var backendTime = parseInt(phantomas.getMetric('timeToFirstByte'), 10),
-            frontendTime = parseInt(phantomas.getMetric('domComplete'), 10),
-            totalTime = backendTime + frontendTime,
-            backendTimePercentage;
-
-        if (totalTime === 0) {
-            return;
-        }
-
-        backendTimePercentage = Math.round(backendTime / totalTime * 100);
-
-        phantomas.setMetric('timeBackend', backendTimePercentage);
-        phantomas.setMetric('timeFrontend', 100 - backendTimePercentage);
-
-        phantomas.log('Performance timing: backend vs frontend time - %d% / %d%', backendTimePercentage, 100 - backendTimePercentage);
-    });
-};

+ 38 - 136
lib/tools/phantomas/phantomasWrapper.js

@@ -17,16 +17,28 @@ var PhantomasWrapper = function() {
         var deferred = Q.defer();
         var task = data.params;
 
+        var viewportOption = null;
+        // Setting screen dimensions for desktop devices only.
+        // Phone and tablet dimensions are dealt by Phantomas.
+        if (task.options.device === 'desktop') {
+            // Similar to an old non-retina Macbook Air 13"
+            viewportOption = '1280x800x1';
+        } else if (task.options.device === 'desktop-hd') {
+            // Similar to a retina Macbook Pro 16"
+            viewportOption = '1536x960x2';
+        }
+
         var options = {
             
             // Cusomizable options
-            'engine': task.options.phantomasEngine || 'webkit',
-            'timeout': task.options.timeout || 30,
-            'user-agent': (task.options.device === 'desktop') ? 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) YLT Chrome/27.0.1453.110 Safari/537.36' : null,
+            'timeout': task.options.timeout || 120,
+            'user-agent': (task.options.device === 'desktop') ? 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) YLT Chrome/85.0.4183.121 Safari/537.36' : null,
             'tablet': (task.options.device === 'tablet'),
             'phone': (task.options.device === 'phone'),
             'screenshot': task.options.screenshot || false,
-            'wait-for-selector': task.options.waitForSelector,
+            'viewport': viewportOption,
+            'wait-for-network-idle': true,
+            //'wait-for-selector': task.options.waitForSelector,
             'cookie': task.options.cookie,
             'auth-user': task.options.authUser,
             'auth-pass': task.options.authPass,
@@ -35,151 +47,41 @@ var PhantomasWrapper = function() {
             'no-externals': task.options.noExternals,
 
             // Mandatory
-            'reporter': 'json:pretty',
             'analyze-css': true,
-            'ignore-ssl-errors': true,
-            'skip-modules': [
-                'ajaxRequests', // overridden
-                'domHiddenContent', // overridden
-                'domMutations', // not compatible with webkit
-                'domQueries', // overridden
-                'events', // overridden
-                'filmStrip', // not needed
-                'har', // not needed for the moment
-                'javaScriptBottlenecks', // needs to be launched after custom module scopeYLT
-                'jQuery', // overridden
-                'jserrors', // overridden
-                'lazyLoadableImages', //overridden
-                'pageSource', // not needed
-                'windowPerformance' // overridden
-            ].join(','),
-            'include-dirs': [
-                path.join(__dirname, 'custom_modules/core'),
-                path.join(__dirname, 'custom_modules/modules')
-            ].join(',')
+            'ignore-ssl-errors': true
         };
 
+
         // Proxy option can't be set to null or undefined...
         // this is why it's set now and not in the object above
         if (task.options.proxy) {
             options.proxy = task.options.proxy;
         }
 
-        // Output the command line for debugging purpose
-        debug('If you want to reproduce the phantomas task only, copy the following command line:');
-        var optionsString = '';
-        for (var opt in options) {
-            var value = options[opt];
-            
-            if ((typeof value === 'string' || value instanceof String) && value.indexOf(' ') >= 0) {
-                value = '"' + value + '"';
-            }
-
-            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');
-
-
-        var phantomasPid;
-        var isKilled = false;
-
-        // Kill phantomas if nothing happens
-        var killer = setTimeout(function() {
-            console.log('Killing phantomas because the test on ' + task.url + ' was launched ' + 5*options.timeout + ' seconds ago');
-            
-            if (phantomasPid) {
-                ps.kill(phantomasPid, function(err) {
-                    
-                    if (err) {
-                        debug('Could not kill Phantomas process %s', phantomasPid);
-
-                        // Suicide
-                        process.exit(1);
-                        // If in server mode, forever will restart the server
-                    }
-
-                    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;
-        var currentTry = 0;
-
-        async.retry(triesNumber, function(cb) {
-
-            currentTry ++;
-
-            var process = phantomas(task.url, options, function(err, json, results) {
-                var errorCode = err ? parseInt(err.message, 10) : null;
-                
-                if (isKilled) {
-                    debug('Process was killed, too late Phantomas, sorry...');
-                    return;
-                }
-
-
-                debug('Returning from Phantomas with error %s', errorCode);
-
-                // Adding some YellowLabTools errors here
-                if (json && json.metrics && (!json.metrics.javascriptExecutionTree || !json.offenders.javascriptExecutionTree)) {
-                    errorCode = 1001;
-                }
-
-                if (!errorCode && (!json || !json.metrics)) {
-                    errorCode = 1002;
-                }
-
-                // Don't cancel test if it is a timeout and we've got some results
-                if (errorCode === 252 && json) {
-                    debug('Timeout after ' + options.timeout + ' seconds. But it\'s not a problem, the test is valid.');
-                    errorCode = null;
-                }
-
-                if (errorCode) {
-                    debug('Attempt failed. Error code ' + errorCode);
-                }
-
-                cb(errorCode, json);
-            
-            }).fail(function() {
-                // This function is useless, but the failing promise needs to be handled,
-                // otherwise the module meow writes in the console in case of a timeout (error code 252).
-                debug('Failing promise handled');
-            });
-            
-            phantomasPid = process.pid;
-
-        }, function(err, json) {
-
-            clearTimeout(killer);
-
-            if (err) {
-                debug('All ' + triesNumber + ' attemps failed for the test');
-                deferred.reject(err);
-
-            } else {
+    
+        const promise = phantomas(task.url, options);
+
+        // handle the promise
+        promise.
+            then(results => {
+                var json = {
+                    generator: results.getGenerator(),
+                    url: results.getUrl(),
+                    metrics: results.getMetrics(),
+                    offenders: results.getAllOffenders()
+                };
 
                 deferred.resolve(json);
+            }).
+            catch(res => {
+                console.error(res);
+                deferred.reject('Phantomas failed: ' + res.message);
+            });
 
+        promise.on('milestone', function(event) {
+            if (event === 'domReady' || event === 'domComplete') {
+                deferred.notify(event);
             }
         });
 

+ 107 - 0
lib/tools/redownload/brotliCompressor.js

@@ -0,0 +1,107 @@
+var debug   = require('debug')('ylt:brotliCompressor');
+
+var Q       = require('q');
+var zlib    = require('zlib');
+
+var gzipCompressor  = require('./gzipCompressor');
+
+
+var GzipCompressor = function() {
+
+    var BROTLI_COMPRESSION_LEVEL = 9;
+
+    function compressFile(entry) {
+        debug('Entering brotli compressor');
+        return brotlifyFile(entry)
+
+        .then(brotliOptimizedFile);
+    }
+
+    // Compress with Brotli files that were not compressed or not with brotli (with gzip or deflate)
+    function brotlifyFile(entry) {
+        var deferred = Q.defer();
+
+        if (gzipCompressor.entryTypeCanBeCompressed(entry) && entry.weightCheck && entry.weightCheck.bodyBuffer) {
+
+            if (!entry.weightCheck.isCompressed) {
+                debug('File %s was not compressed at all, trying Brotli over it.', entry.url);
+            } else if (entry.weightCheck.compressionTool !== 'brotli') {
+                debug('File %s was compressed with %s. Trying with Brotli.', entry.url, entry.weightCheck.compressionTool);
+            }
+
+            zlib.brotliCompress(entry.weightCheck.bodyBuffer, {
+                    params: {
+                        [zlib.constants.BROTLI_PARAM_QUALITY]: 9
+                    }
+                }, function(err, buffer) {
+                    if (err) {
+                        debug('Could not compress uncompressed file with brotli');
+                        debug(err);
+
+                        deferred.reject(err);
+                    } else {
+                        var compressedSize = buffer.length;
+
+                        if (!entry.weightCheck.isCompressed) {
+                            debug('Brotli size is %d, was %d, this is %d% better.', compressedSize, entry.weightCheck.bodySize, Math.round((entry.weightCheck.bodySize - compressedSize) * 100 / entry.weightCheck.bodySize));
+                        } else if (entry.weightCheck.compressionTool !== 'brotli') {
+                            debug('Brotli size is %d, was %d with %s, this is %d% better.', compressedSize, entry.weightCheck.bodySize, entry.weightCheck.compressionTool, Math.round((entry.weightCheck.bodySize - compressedSize) * 100 / entry.weightCheck.bodySize));
+                        }
+
+                        entry.weightCheck.afterBrotliCompression = compressedSize;
+
+                        deferred.resolve(entry);
+                    }
+                }
+            );
+        } else {
+            debug('Compression not needed');
+            deferred.resolve(entry);
+        }
+
+        return deferred.promise;
+    }
+
+    // Gzip a file after minification or optimization if this step was successful
+    function brotliOptimizedFile(entry) {
+        var deferred = Q.defer();
+
+        if (gzipCompressor.entryTypeCanBeCompressed(entry) && entry.weightCheck && entry.weightCheck.isOptimized === false) {
+            debug('Trying to brotlify file after minification: %s', entry.url);
+
+            var uncompressedSize = entry.weightCheck.optimized;
+
+            zlib.brotliCompress(Buffer.from(entry.weightCheck.bodyAfterOptimization, 'utf8'), {
+                    params: {
+                        [zlib.constants.BROTLI_PARAM_QUALITY]: 9
+                    }
+                }, function(err, buffer) {
+                    if (err) {
+                        debug('Could not compress minified file with brotli');
+                        debug(err);
+
+                        deferred.reject(err);
+                    } else {
+                        var compressedSize = buffer.length;
+
+                        debug('Correctly brotlified the minified file, was %d and is now %d bytes', uncompressedSize, compressedSize);
+                        entry.weightCheck.afterOptimizationAndBrotliCompression = compressedSize;
+
+                        deferred.resolve(entry);
+                    }
+                }
+            );
+        } else {
+            debug('Compressing optimized file not needed');
+            deferred.resolve(entry);
+        }
+
+        return deferred.promise;
+    }
+
+    return {
+        compressFile: compressFile
+    };
+};
+
+module.exports = new GzipCompressor();

+ 64 - 16
lib/tools/redownload/contentTypeChecker.js

@@ -4,6 +4,7 @@ var isJpg   = require('is-jpg');
 var isPng   = require('is-png');
 var isSvg   = require('is-svg');
 var isGif   = require('is-gif');
+var isWebp  = require('is-webp');
 var isWoff  = require('is-woff');
 var isWoff2 = require('is-woff2');
 var isOtf   = require('is-otf');
@@ -14,30 +15,53 @@ var ContentTypeChecker = function() {
 
     function checkContentType(entry) {
         var deferred = Q.defer();
-
-        debug('Entering contentTypeChecker');
         
+        // Setting isSomething values:
+        switch(entry.type) {
+            case 'html':
+                entry.isHTML = true;
+                break;
+            case 'xml':
+                entry.isXML = true;
+                break;
+            case 'css':
+                entry.isCSS = true;
+                break;
+            case 'js':
+                entry.isJS = true;
+                break;
+            case 'json':
+                entry.isJSON = true;
+                break;
+            case 'image':
+                entry.isImage = true;
+                break;
+            case 'webfont':
+                entry.isWebFont = true;
+                break;
+            case 'video':
+                entry.isVideo = true;
+                break;
+            case 'favicon':
+                entry.isFavicon = true;
+                break;
+        }
+
+        // Now let's check for mistakes by analysing body content. It happens more often then we think!
+
         // Ignore very small files as they are generally tracking pixels
         if (entry.weightCheck && entry.weightCheck.bodyBuffer && entry.weightCheck.bodySize > 100) {
             var foundType;
 
             try {
                 foundType = findContentType(entry.weightCheck.bodyBuffer);
-            
-                if (!entry.contentType || entry.contentType === '') {
-                    if (foundType === null) {
-                        debug('ContentType is empty for file %s', entry.url);
-                    } else {
-                        debug('ContentType is empty for file %s. It should be %s.', entry.url, foundType.mimes[0]);
-                        entry.oldContentType = null;
-                        rewriteContentType(entry, foundType);
-                    }
-                } else {
-                    if (foundType !== null && foundType.mimes.indexOf(entry.contentType) === -1) {
-                        debug('ContentType %s is wrong for %s. It should be %s.', entry.contentType, entry.url, foundType.mimes[0]);
-                        entry.oldContentType = entry.contentType;
-                        rewriteContentType(entry, foundType);
+
+                // If it's an image or a font, then rewrite.
+                if (foundType !== null && (foundType.type === 'image' || foundType.type === 'webfont')) {
+                    if (foundType.type !== entry.type) {
+                        debug('Content type %s is wrong for %s. It should be %s.', entry.type, entry.ulr, foundType.type);
                     }
+                    rewriteContentType(entry, foundType);
                 }
 
             } catch(err) {
@@ -71,6 +95,10 @@ var ContentTypeChecker = function() {
             return contentTypes.gif;
         }
 
+        if (isWebp(bodyBuffer)) {
+            return contentTypes.webp;
+        }
+
         if (isWoff(bodyBuffer)) {
             return contentTypes.woff;
         }
@@ -114,6 +142,7 @@ var ContentTypeChecker = function() {
 
     var contentTypes = {
         jpeg: {
+            type: 'image',
             mimes: ['image/jpeg'],
             updateFn: function(entry) {
                 entry.type = 'image';
@@ -121,6 +150,7 @@ var ContentTypeChecker = function() {
             }
         },
         png: {
+            type: 'image',
             mimes: ['image/png'],
             updateFn: function(entry) {
                 entry.type = 'image';
@@ -128,6 +158,7 @@ var ContentTypeChecker = function() {
             }
         },
         svg: {
+            type: 'image',
             mimes: ['image/svg+xml'],
             updateFn: function(entry) {
                 entry.type = 'image';
@@ -136,27 +167,41 @@ var ContentTypeChecker = function() {
             }
         },
         gif: {
+            type: 'image',
             mimes: ['image/gif'],
             updateFn: function(entry) {
                 entry.type = 'image';
                 entry.isImage = true;
             }
         },
+        webp: {
+            type: 'image',
+            mimes: ['image/webp'],
+            updateFn: function(entry) {
+                entry.type = 'image';
+                entry.isImage = true;
+            }
+        },
         woff: {
+            type: 'webfont',
             mimes: ['application/x-font-woff', 'application/font-woff', 'font/woff'],
             updateFn: function(entry) {
                 entry.type = 'webfont';
                 entry.isWebFont = true;
+                entry.isWoff = true;
             }
         },
         woff2: {
+            type: 'webfont',
             mimes: ['font/woff2', 'application/x-font-woff2', 'application/font-woff2'],
             updateFn: function(entry) {
                 entry.type = 'webfont';
                 entry.isWebFont = true;
+                entry.isWoff2 = true;
             }
         },
         otf: {
+            type: 'webfont',
             mimes: ['application/x-font-otf', 'font/otf', 'font/opentype', 'application/x-font-opentype'],
             updateFn: function(entry) {
                 entry.type = 'webfont';
@@ -164,13 +209,16 @@ var ContentTypeChecker = function() {
             }
         },
         ttf: {
+            type: 'webfont',
             mimes: ['application/x-font-ttf', 'font/ttf', 'application/x-font-truetype'],
             updateFn: function(entry) {
                 entry.type = 'webfont';
                 entry.isWebFont = true;
+                entry.isTTF = true;
             }
         },
         eot: {
+            type: 'webfont',
             mimes: ['application/vnd.ms-fontobject', 'font/eot'],
             updateFn: function(entry) {
                 entry.type = 'webfont';

+ 27 - 125
lib/tools/redownload/fileMinifier.js

@@ -3,7 +3,7 @@ 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 htmlMinifier    = require('html-minifier');
 
 
 var FileMinifier = function() {
@@ -141,123 +141,19 @@ var FileMinifier = function() {
     // 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(function(ast) {
-            // Only do the compression step for smaller files
-            // otherwise it can take a very long time compared to the gain
-            if (body.length < 200*1024) {
-                return splittedUglifyStep3(ast);
-            } else {
-                debug('Skipping step 3 because the file is too big (%d bytes)!', body.length);
-                return ast;
-            }
-        })
-        .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 result = UglifyJS.minify(body, {
+            // Only do the compression step for smaller files
+            // otherwise it can take a very long time compared to the gain
+            compress: (body.length < 200*1024)
+        });
 
         var endTime = Date.now();
-        debug('Uglify step 6 took %dms', endTime - startTime);
-        deferred.resolve(compressed_ast);
-        return deferred.promise;
-    }
+        debug('Uglify took %dms', endTime - startTime);
+        deferred.resolve(result.code);
 
-    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;
     }
 
@@ -279,19 +175,25 @@ var FileMinifier = function() {
     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);
-            }
-        });
+        try {
+            var result = htmlMinifier.minify(body, {
+                collapseWhitespace: true,
+                conservativeCollapse: true,
+                continueOnParseError: true,
+                decodeEntities: true,
+                minifyCSS: true,
+                minifyJS: true,
+                preserveLineBreaks: true,
+                removeAttributeQuotes: true,
+                removeComments: true,
+                removeScriptTypeAttributes: true,
+                removeStyleLinkTypeAttributes: true
+            });
+            deferred.resolve(result);
+        } catch(err) {
+            deferred.reject(err);
+        }
+        
 
         return deferred.promise;
     }

+ 83 - 7
lib/tools/redownload/fontAnalyzer.js

@@ -2,6 +2,8 @@ var debug = require('debug')('ylt:fontAnalyzer');
 
 var Q           = require('q');
 var fontkit     = require('fontkit');
+var woffTools   = require('woff-tools');
+var ttf2woff2   = require('ttf2woff2');
 
 var FontAnalyzer = function() {
 
@@ -14,30 +16,104 @@ var FontAnalyzer = function() {
             return deferred.promise;
         }
 
-        var fileSize = entry.weightCheck.uncompressedSize;
-
         if (entry.isWebFont) {
             debug('File %s is a font. Let\'s have a look inside!', entry.url);
             
-            getMetricsFromFont(entry, charsListOnPage)
+            convertToWoff2(entry)
+
+            .then(function(entry) {
+                return getMetricsFromFont(entry, charsListOnPage);
+            })
 
             .then(function(fontMetrics) {
                 entry.fontMetrics = fontMetrics;
-                deferred.resolve(entry);
             })
 
             .fail(function(error) {
                 debug('Could not open the font: %s', error);
-                deferred.resolve(entry);
             });
 
+        }
+
+        deferred.resolve(entry);
+        
+        return deferred.promise;
+    }
+
+    function convertToWoff2(entry) {
+        var deferred = Q.defer();
+
+        debug('Entering font format converter...');
+
+        var fileSize = entry.weightCheck.bodySize;
+        var ttf;
+        var woff2;
+        var newFileSize;
+
+        if (entry.isWoff2) {
+
+            debug('File is already a woff2.');
+            deferred.resolve(entry);
+
+        } else if (entry.isWoff) {
+
+            debug('File is a woff. Let\'s convert to woff2');
+
+            try {
+
+                debug('Current file size is %d', fileSize);
+
+                ttf = woffTools.toSfnt(entry.weightCheck.bodyBuffer);
+                woff2 = ttf2woff2(ttf);
+                newFileSize = woff2.length;
+
+                debug('New image size is %d', newFileSize);
+                debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
+                entry.weightCheck.sizeAsWoff2 = newFileSize;
+
+                deferred.resolve(entry);
+
+            } catch(error) {
+                deferred.reject(error);
+            }
+
+        } else if (entry.isTtf) {
+
+            debug('File is a TTF. Let\'s convert to woff2');
+
+            try {
+
+                debug('Current file size is %d', fileSize);
+
+                woff2 = ttf2woff2(entry.weightCheck.bodyBuffer);
+                newFileSize = woff2.length;
+
+                debug('New image size is %d', newFileSize);
+                debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
+                entry.weightCheck.sizeAsWoff2 = newFileSize;
+
+                deferred.resolve(entry);
+
+            } catch(error) {
+                deferred.reject(error);
+            }
+
         } else {
+            // Other font formats are not handled
             deferred.resolve(entry);
         }
-        
+
         return deferred.promise;
     }
 
+    // The gain is estimated of enough value if it's over 1KB or over 20%,
+    // but it's ignored if is below 100 bytes
+    function gainIsEnough(oldWeight, newWeight) {
+        var gain = oldWeight - newWeight;
+        var ratio = gain / oldWeight;
+        return (gain > 2048 || (ratio > 0.2 && gain > 100));
+    }
+
     function getMetricsFromFont(entry, charsListOnPage) {
         var deferred = Q.defer();
         
@@ -49,7 +125,7 @@ var FontAnalyzer = function() {
                 name: font.fullName || font.postscriptName || font.familyName,
                 numGlyphs: font.numGlyphs,
                 averageGlyphComplexity: getAverageGlyphComplexity(font),
-                compressedWeight: entry.weightCheck.afterCompression || entry.weightCheck.bodySize,
+                compressedWeight: entry.weightCheck.afterGzipCompression || entry.weightCheck.bodySize,
                 unicodeRanges: readUnicodeRanges(font.characterSet, charsListOnPage),
                 numGlyphsInCommonWithPageContent: countPossiblyUsedGlyphs(getCharacterSetAsString(font.characterSet), charsListOnPage)
             };

+ 9 - 8
lib/tools/redownload/gzipCompressor.js

@@ -6,7 +6,7 @@ var zlib    = require('zlib');
 var GzipCompressor = function() {
 
     function compressFile(entry) {
-        debug('Entering compressFile');
+        debug('Entering gzip compressor');
         return gzipUncompressedFile(entry)
 
         .then(gzipOptimizedFile);
@@ -16,7 +16,7 @@ var GzipCompressor = function() {
     function gzipUncompressedFile(entry) {
         var deferred = Q.defer();
 
-        if (entryTypeCanBeGzipped(entry) && entry.weightCheck && !entry.weightCheck.isCompressed && entry.weightCheck.bodyBuffer) {
+        if (entryTypeCanBeCompressed(entry) && entry.weightCheck && !entry.weightCheck.isCompressed && entry.weightCheck.bodyBuffer) {
             debug('Compression missing, trying to gzip file %s', entry.url);
 
             var uncompressedSize = entry.weightCheck.uncompressedSize;
@@ -33,7 +33,7 @@ var GzipCompressor = function() {
                     if (gainIsEnough(uncompressedSize, compressedSize)) {
                         debug('File correctly gziped, was %d and is now %d bytes', uncompressedSize, compressedSize);
 
-                        entry.weightCheck.afterCompression = compressedSize;
+                        entry.weightCheck.afterGzipCompression = compressedSize;
                     } else {
                         debug('Gzip gain is not enough, was %d and is now %d bytes', uncompressedSize, compressedSize);
                     }
@@ -53,12 +53,12 @@ var GzipCompressor = function() {
     function gzipOptimizedFile(entry) {
         var deferred = Q.defer();
 
-        if (entryTypeCanBeGzipped(entry) && entry.weightCheck && entry.weightCheck.isOptimized === false) {
+        if (entryTypeCanBeCompressed(entry) && entry.weightCheck && entry.weightCheck.isOptimized === false) {
             debug('Trying to gzip file after minification: %s', entry.url);
 
             var uncompressedSize = entry.weightCheck.optimized;
 
-            zlib.gzip(new Buffer(entry.weightCheck.bodyAfterOptimization, 'utf8'), function(err, buffer) {
+            zlib.gzip(Buffer.from(entry.weightCheck.bodyAfterOptimization, 'utf8'), function(err, buffer) {
                 if (err) {
                     debug('Could not compress minified file with gzip');
                     debug(err);
@@ -68,7 +68,7 @@ var GzipCompressor = function() {
                     var compressedSize = buffer.length;
 
                     debug('Correctly gziped the minified file, was %d and is now %d bytes', uncompressedSize, compressedSize);
-                    entry.weightCheck.afterOptimizationAndCompression = compressedSize;
+                    entry.weightCheck.afterOptimizationAndGzipCompression = compressedSize;
 
                     deferred.resolve(entry);
                 }
@@ -89,13 +89,14 @@ var GzipCompressor = function() {
         return (gain > 2048 || (ratio > 0.2 && gain > 100));
     }
 
-    function entryTypeCanBeGzipped(entry) {
+    function entryTypeCanBeCompressed(entry) {
         return entry.isJS || entry.isCSS || entry.isHTML || entry.isJSON || entry.isSVG || entry.isTTF || entry.isXML || entry.isFavicon;
     }
 
     return {
         compressFile: compressFile,
-        entryTypeCanBeGzipped: entryTypeCanBeGzipped
+        gainIsEnough: gainIsEnough,
+        entryTypeCanBeCompressed: entryTypeCanBeCompressed
     };
 };
 

+ 258 - 79
lib/tools/redownload/redownload.js

@@ -17,6 +17,7 @@ var md5                 = require('md5');
 var imageOptimizer      = require('./imageOptimizer');
 var fileMinifier        = require('./fileMinifier');
 var gzipCompressor      = require('./gzipCompressor');
+var brotliCompressor    = require('./brotliCompressor');
 var contentTypeChecker  = require('./contentTypeChecker');
 var fontAnalyzer        = require('./fontAnalyzer');
 var imageDimensions     = require('./imageDimensions');
@@ -24,19 +25,27 @@ var imageDimensions     = require('./imageDimensions');
 
 var Redownload = function() {
 
-    var MAX_PARALLEL_DOWNLOADS = 10;
-    var REQUEST_TIMEOUT = 15000; // 15 seconds
+    var MAX_PARALLEL_DOWNLOADS = 5;
+    var REQUEST_TIMEOUT = 30000; // 30 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;
+        var requestsOffenders = data.toolsResults.phantomas.offenders.requests;
+        var gzipOffenders = data.toolsResults.phantomas.offenders.gzipRequests;
+        var postOffenders = data.toolsResults.phantomas.offenders.postRequests;
+        var notFoundOffenders = data.toolsResults.phantomas.offenders.notFound;
+        var redirectOffenders = data.toolsResults.phantomas.offenders.redirects;
+
+        var requestsList = mergeOffenders(requestsOffenders, gzipOffenders, postOffenders, notFoundOffenders, redirectOffenders);
+
+        var totalCount = requestsList.length;
+        var doneCount = 0;
 
         var httpAuth = null;
         if (data.params && data.params.options && data.params.options.authUser && data.params.options.authPass) {
@@ -76,12 +85,19 @@ var Redownload = function() {
 
                 .then(gzipCompressor.compressFile)
 
+                .then(brotliCompressor.compressFile)
+
                 .then(function(entry) {
                     return fontAnalyzer.analyzeFont(entry, differentCharacters);
                 })
 
                 .then(function(newEntry) {
                     debug('File %s - Redownloaded, optimized, minified, compressed, analyzed: done', entry.url);
+
+                    // For the progress bar
+                    doneCount ++;
+                    deferred.notify(doneCount/totalCount);
+
                     callback(null, newEntry);
                 })
 
@@ -106,13 +122,6 @@ var Redownload = function() {
                 var metrics = {};
                 var offenders = {};
 
-                // Remove unused fonts that a normal browser would not download (fix #224)
-                results = results.filter(function(result) {
-                    if (result && result.fontMetrics) {
-                        return result.fontMetrics.isUsed !== false;
-                    }
-                    return true;
-                });
 
                 // Count requests
                 offenders.totalRequests = listRequestsByType(results);
@@ -137,33 +146,26 @@ var Redownload = function() {
                 offenders.emptyRequests = listEmptyRequests(results);
                 metrics.emptyRequests = offenders.emptyRequests.length;
 
-
-                // Now remove unwanted responses (redirections and empty files)
+                // Remove some more unwanted responses (redirections and empty files)
                 results = results.filter(function(result) {
-                    return ((result.status < 300 || result.status >= 400) && result.weightCheck.bodySize > 0);
+                    return (/* (result.status < 300 || result.status >= 400) && */ result.weightCheck.bodySize > 0);
                 });
 
-
                 // Image compression
                 offenders.imageOptimization = listImagesNotOptimized(results);
                 metrics.imageOptimization = offenders.imageOptimization.totalGain;
 
                 // Image width
-                var isMobile = data.params.options.device === 'phone';
-                offenders.imagesTooLarge = listImagesTooLarge(results, isMobile);
+                offenders.imagesTooLarge = listImagesTooLarge(results, data.params.options.device);
                 metrics.imagesTooLarge = offenders.imagesTooLarge.length;
 
                 // 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;
+                // Gzip/Brotli compression
+                offenders.compression = listFilesNotBrotlified(results);
+                metrics.compression = offenders.compression.totalGain;
 
                 // Detect identical files
                 offenders.identicalFiles = listIdenticalFiles(results);
@@ -173,6 +175,10 @@ var Redownload = function() {
                 offenders.fontsCount = listFonts(results);
                 metrics.fontsCount = offenders.fontsCount.count;
 
+                // Conversion to woff2
+                offenders.nonWoff2Fonts = listNonWoff2Fonts(results);
+                metrics.nonWoff2Fonts = offenders.nonWoff2Fonts.totalGain;
+
                 // Heavy fonts
                 offenders.heavyFonts = listHeavyFonts(results);
                 metrics.heavyFonts = offenders.heavyFonts.totalGain;
@@ -181,6 +187,8 @@ var Redownload = function() {
                 offenders.unusedUnicodeRanges = listUnusedUnicodeRanges(results);
                 metrics.unusedUnicodeRanges = offenders.unusedUnicodeRanges.count;
 
+                // Detect WordPress
+                metrics.isWordPress = detectWordPress(results);
 
                 data.toolsResults.redownload = {
                     metrics: metrics,
@@ -194,6 +202,57 @@ var Redownload = function() {
         return deferred.promise;
     }
 
+    function mergeOffenders(requests, compressedOffenders, postOffenders, notFoundOffenders, redirectOffenders) {
+        
+        // Parse each request and check if it can be found in other offenders
+        requests.forEach(function(request) {
+
+            // Is it compressed?
+            if (compressedOffenders) {
+                compressedOffenders.some(function(entry) {
+                    if (entry.url === request.url) {
+                        request.compressed = true;
+                        request.bodySize = entry.bodySize;
+                        request.transferedSize = entry.transferedSize;
+                        return true;
+                    }
+                });
+            }
+
+            // Is it a POST request?
+            if (postOffenders) {
+                postOffenders.some(function(url) {
+                    if (url === request.url) {
+                        request.post = true;
+                        return true;
+                    }
+                });
+            }
+
+            // Is it a 404?
+            if (notFoundOffenders) {
+                notFoundOffenders.some(function(url) {
+                    if (url === request.url) {
+                        request.notFound = true;
+                        return true;
+                    }
+                });
+            }
+
+            // Is it a redirection?
+            if (redirectOffenders) {
+                redirectOffenders.some(function(message) {
+                    if (message.split(' ')[0] === request.url) {
+                        request.redirect = true;
+                        return true;
+                    }
+                });
+            }
+        });
+
+        return requests;
+    }
+
     function listIncorrectContentTypes(requests) {
         var results = [];
         
@@ -288,9 +347,34 @@ var Redownload = function() {
         };
 
         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 before, after;
+
+                if (req.weightCheck.isCompressed === true) {
+                    // The resource is compressed
+
+                    before = req.weightCheck.bodySize;
+                    
+                    if (req.weightCheck.compressionTool === 'brotli') {
+                        after = req.weightCheck.afterOptimizationAndBrotliCompression;
+                    } else {
+                        after = req.weightCheck.afterOptimizationAndGzipCompression;
+                    }
+
+                } else if (req.weightCheck.afterBrotliCompression) {
+                    // The resource is not compressed but should be
+
+                    before = req.weightCheck.afterBrotliCompression;
+                    after = req.weightCheck.afterOptimizationAndBrotliCompression;
+                } else {
+                    // The resource is not compressed but is not subject to compression
+
+                    before = req.weightCheck.bodySize;
+                    after = req.weightCheck.optimized;
+                }
+
                 var gain = before - after;
 
                 if (gain > 200) {
@@ -298,10 +382,11 @@ var Redownload = function() {
 
                     results.images.push({
                         url: req.url,
-                        original: req.weightCheck.bodySize,
+                        originalWeigth: req.weightCheck.bodySize,
+                        isCompressible: (req.weightCheck.afterBrotliCompression > 0),
                         isCompressed: req.weightCheck.isCompressed,
-                        afterCompression: req.weightCheck.afterCompression,
-                        afterOptimizationAndCompression: req.weightCheck.afterOptimizationAndCompression,
+                        originalCompressedWeight: before,
+                        afterOptimizationAndCompression: after,
                         lossless: req.weightCheck.lossless,
                         lossy: req.weightCheck.lossy,
                         gain: gain
@@ -309,17 +394,23 @@ var Redownload = function() {
                 }
             }
         });
-
         return results;
     }
 
-    function listImagesTooLarge(requests, isMobile) {
+    function listImagesTooLarge(requests, device) {
         var results = [];
 
         requests.forEach(function(req) {
+            const thresholds = {
+                'phone': 1200,
+                'tablet': 1800,
+                'desktop': 2400,
+                'desktop-hd': 3200
+            };
+
             if (req.weightCheck.bodySize > 0 && 
                 req.imageDimensions &&
-                ((isMobile && req.imageDimensions.width > 800) || req.imageDimensions.width > 1500)) {
+                req.imageDimensions.width > thresholds[device]) {
 
                 results.push({
                     url: req.url,
@@ -342,8 +433,31 @@ var Redownload = function() {
 
         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 before, after;
+
+                if (req.weightCheck.isCompressed === true) {
+                    // The resource is compressed
+
+                    before = req.weightCheck.bodySize;
+                    
+                    if (req.weightCheck.compressionTool === 'brotli') {
+                        after = req.weightCheck.afterOptimizationAndBrotliCompression;
+                    } else {
+                        after = req.weightCheck.afterOptimizationAndGzipCompression;
+                    }
+
+                } else if (req.weightCheck.afterBrotliCompression) {
+                    // The resource is not compressed but should be
+
+                    before = req.weightCheck.afterBrotliCompression;
+                    after = req.weightCheck.afterOptimizationAndBrotliCompression;
+                } else {
+                    // The resource is not compressed but is not subject to compression
+
+                    before = req.weightCheck.bodySize;
+                    after = req.weightCheck.optimized;
+                }
+
                 var gain = before - after;
 
                 if (gain > 200) {
@@ -351,10 +465,11 @@ var Redownload = function() {
 
                     results.files.push({
                         url: req.url,
-                        original: req.weightCheck.bodySize,
+                        originalWeigth: req.weightCheck.bodySize,
+                        isCompressible: (req.weightCheck.afterBrotliCompression > 0),
                         isCompressed: req.weightCheck.isCompressed,
-                        afterCompression: req.weightCheck.afterCompression,
-                        afterOptimizationAndCompression: req.weightCheck.afterOptimizationAndCompression,
+                        originalCompressedWeight: before,
+                        afterOptimizationAndCompression: after,
                         optimized: req.weightCheck.optimized,
                         gain: gain
                     });
@@ -365,24 +480,37 @@ var Redownload = function() {
         return results;
     }
 
-    function listFilesNotGzipped(requests) {
+    function listFilesNotBrotlified(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;
+            if (req.weightCheck.compressionTool !== 'brotli') {
+                
+                var file = {
+                    url: req.url,
+                    wasCompressed: req.weightCheck.isCompressed,
+                    brotlified: req.weightCheck.afterBrotliCompression
+                };
 
-                results.totalGain += gain;
+                if (req.weightCheck.isCompressed) {
+                    // The file was already gzipped (or deflated)
+                    file.originalSize = req.weightCheck.bodySize;
+                    file.gain = req.weightCheck.bodySize - req.weightCheck.afterBrotliCompression;
+                } else {
+                    // The file was not compressed at all
+                    file.originalSize = req.weightCheck.uncompressedSize;
+                    file.gzipped = req.weightCheck.afterGzipCompression;
+                    file.gain = req.weightCheck.uncompressedSize - req.weightCheck.afterBrotliCompression;
+                }
 
-                results.files.push({
-                    url: req.url,
-                    original: req.weightCheck.uncompressedSize,
-                    gzipped: req.weightCheck.afterCompression,
-                    gain: gain
-                });
+                // Just checking a last time if the gain is positive
+                if (file.gain > 200) {
+                    results.totalGain += file.gain;
+                    results.files.push(file);
+                }
             }
         });
 
@@ -417,31 +545,6 @@ var Redownload = function() {
         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 listIdenticalFiles(requests) {
         var hashes = {};
         var list = [];
@@ -499,6 +602,42 @@ var Redownload = function() {
         };
     }
 
+    function listNonWoff2Fonts(requests) {
+        var results = {
+            totalGain: 0,
+            fonts: []
+        };
+
+        requests.forEach(function(req) {
+            if (!req.isWoff2 && req.weightCheck.sizeAsWoff2) {
+                var before = req.weightCheck.bodySize;
+                var after = req.weightCheck.sizeAsWoff2;
+                var gain = before - after;
+                
+                var type = null;
+                if (req.isWoff) {
+                    type = 'woff';
+                } else if (req.isTtf) {
+                    type = 'ttf';
+                }
+
+                if (gain > 200) {
+                    results.totalGain += gain;
+
+                    results.fonts.push({
+                        url: req.url,
+                        originalSize: before,
+                        type: type,
+                        woff2Size: after,
+                        gain: gain
+                    });
+                }
+            }
+        });
+
+        return results;
+    }
+
     function listHeavyFonts(requests) {
         var list = [];
         var totalGain = 0;
@@ -613,6 +752,17 @@ var Redownload = function() {
         };
     }
 
+    function detectWordPress(requests) {
+        // Check the first file only
+        if (requests[0].isHTML && 
+            requests[0].weightCheck.bodyBuffer &&
+            requests[0].weightCheck.bodyBuffer.indexOf('/wp-content/') > 0) {
+            
+            return true;
+        }
+        return false;
+    }
+
 
     function redownloadEntry(entry, httpAuth, proxy) {
         var deferred = Q.defer();
@@ -639,13 +789,15 @@ var Redownload = function() {
             deferred.resolve(entry);
         }
 
-        if (entry.method !== 'GET') {
+        if (entry.post) {
             notDownloadableFile('only downloading GET');
+            // ... at least trying to
             return deferred.promise;
         }
 
-        if (entry.status !== 200) {
+        if (entry.notFound || entry.redirect) {
             unwantedFile('only downloading requests with status code 200');
+            // ...at least trying to
             return deferred.promise;
         }
 
@@ -654,12 +806,12 @@ var Redownload = function() {
             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';
+        // Always add compression and webp headers before sending, in case the server listens to them
+        var reqHeaders = [];
+        reqHeaders['Accept'] = '*/*,image/webp';
+        reqHeaders['Accept-Encoding'] = 'gzip, deflate, br';
         reqHeaders['Connection'] = 'keep-alive';
 
         var requestOptions = {
@@ -723,6 +875,7 @@ var Redownload = function() {
                 var bodySize = 0;  // bytes size over the wire
                 var bodyChunks = [];  // an array of buffers
                 var isCompressed = false;
+                var compressionTool = '';
 
                 function tally() {
 
@@ -738,6 +891,7 @@ var Redownload = function() {
                         headersSize: Buffer.byteLength(rawHeaders, 'utf8'),
                         bodySize: bodySize,
                         isCompressed: isCompressed,
+                        compressionTool: compressionTool,
                         uncompressedSize: uncompressedSize
                     };
 
@@ -755,6 +909,7 @@ var Redownload = function() {
                             uncompressedSize += data.length;
                         }).on('end', function () {
                             isCompressed = true;
+                            compressionTool = 'gzip';
                             tally();
                         }).on('error', function(err) {
                             debug('Error while decoding %s', requestOptions.url);
@@ -777,6 +932,7 @@ var Redownload = function() {
                             uncompressedSize += data.length;
                         }).on('end', function () {
                             isCompressed = true;
+                            compressionTool = 'deflate';
                             tally();
                         }).on('error', function(err) {
                             debug('Error while decoding %s', requestOptions.url);
@@ -788,6 +944,29 @@ var Redownload = function() {
                             bodySize += data.length;
                         }).pipe(deflate);
 
+                        break;
+                    case 'br':
+
+                        var brotli = zlib.createBrotliDecompress();
+
+                        brotli.on('data', function (data) {
+
+                            bodyChunks.push(data);
+                            uncompressedSize += data.length;
+                        }).on('end', function () {
+                            isCompressed = true;
+                            compressionTool = 'brotli';
+                            tally();
+                        }).on('error', function(err) {
+                            debug('Error while decoding %s', requestOptions.url);
+                            debug(err);
+                            callback(err);
+                        });
+
+                        res.on('data', function (data) {
+                            bodySize += data.length;
+                        }).pipe(brotli);
+
                         break;
                     default:
                         res.on('data', function (data) {

+ 47 - 47
package.json

@@ -1,6 +1,6 @@
 {
   "name": "yellowlabtools",
-  "version": "1.13.4",
+  "version": "2.0.0-beta",
   "description": "Online tool to audit a webpage for performance and front-end quality issues",
   "license": "GPL-2.0",
   "author": {
@@ -16,72 +16,73 @@
     "yellowlabtools": "./bin/cli.js"
   },
   "engines": {
-    "node": ">= 4.0"
+    "node": ">= 12.0"
   },
   "main": "./lib/index.js",
   "dependencies": {
-    "angular": "1.6.8",
-    "angular-animate": "1.6.8",
+    "angular": "1.7.7",
+    "angular-animate": "1.7.7",
     "angular-chart.js": "1.1.1",
     "angular-local-storage": "0.7.1",
-    "angular-resource": "1.6.8",
-    "angular-route": "1.6.8",
-    "angular-sanitize": "1.6.8",
-    "async": "2.6.0",
-    "body-parser": "1.18.2",
-    "chart.js": "2.7.1",
-    "clean-css": "4.1.9",
+    "angular-resource": "1.7.7",
+    "angular-route": "1.7.7",
+    "angular-sanitize": "1.7.7",
+    "async": "2.6.1",
+    "body-parser": "1.18.3",
+    "chart.js": "2.7.3",
+    "clean-css": "4.2.1",
     "color-diff": "1.1.0",
-    "compression": "1.7.1",
-    "cors": "2.8.4",
+    "compression": "1.7.3",
+    "cors": "2.8.5",
     "css-mq-parser": "0.0.3",
-    "debug": "3.1.0",
+    "debug": "4.1.1",
     "easyxml": "2.0.1",
-    "ejs": "2.5.7",
-    "express": "4.16.2",
-    "fontkit": "1.7.7",
+    "ejs": "2.6.1",
+    "express": "4.16.4",
+    "fontkit": "1.7.8",
+    "html-minifier": "4.0.0",
     "image-size": "0.7.1",
-    "imagemin": "5.3.1",
-    "imagemin-jpegoptim": "5.2.0",
-    "imagemin-jpegtran": "5.0.2",
-    "imagemin-optipng": "5.2.1",
-    "imagemin-svgo": "6.0.0",
+    "imagemin": "6.1.0",
+    "imagemin-jpegoptim": "6.0.0",
+    "imagemin-jpegtran": "6.0.0",
+    "imagemin-optipng": "6.0.0",
+    "imagemin-svgo": "7.0.0",
     "is-eot": "1.0.0",
-    "is-gif": "1.0.0",
-    "is-http2": "1.2.0",
-    "is-jpg": "1.0.0",
+    "is-gif": "3.0.0",
+    "is-jpg": "2.0.0",
     "is-otf": "0.1.2",
     "is-png": "1.1.0",
-    "is-svg": "2.1.0",
+    "is-svg": "3.0.0",
     "is-ttf": "0.2.2",
+    "is-webp": "1.0.1",
     "is-woff": "1.0.3",
     "is-woff2": "1.0.0",
-    "jimp": "0.2.28",
+    "jimp": "0.6.0",
     "md5": "2.2.1",
-    "meow": "3.7.0",
-    "minimize": "2.1.0",
+    "meow": "5.0.0",
     "parse-color": "1.0.0",
-    "phantomas": "1.19.0",
+    "phantomas": "github:macbre/phantomas#devel",
     "ps-node": "0.1.6",
     "q": "1.5.1",
-    "request": "2.83.0",
-    "rimraf": "2.6.2",
+    "request": "2.88.0",
+    "rimraf": "2.6.3",
     "temporary": "0.0.8",
-    "try-thread-sleep": "1.0.2",
-    "uglify-js": "2.8.29"
+    "ttf2woff2": "3.0.0",
+    "uglify-js": "3.4.9",
+    "woff-tools": "0.1.0"
   },
   "devDependencies": {
-    "chai": "~4.1.2",
-    "grunt": "~1.0.1",
-    "grunt-contrib-clean": "~1.1.0",
+    "chai": "~4.2.0",
+    "grunt": "~1.0.3",
+    "grunt-contrib-clean": "~2.0.0",
     "grunt-contrib-concat": "~1.0.1",
     "grunt-contrib-copy": "~1.0.0",
-    "grunt-contrib-cssmin": "~2.2.1",
-    "grunt-contrib-htmlmin": "~2.4.0",
-    "grunt-contrib-jshint": "~1.1.0",
-    "grunt-contrib-less": "~1.4.1",
-    "grunt-contrib-uglify": "~3.3.0",
-    "grunt-contrib-watch": "~1.0.0",
+    "grunt-contrib-cssmin": "~3.0.0",
+    "grunt-contrib-htmlmin": "~3.0.0",
+    "grunt-contrib-jshint": "~2.0.0",
+    "grunt-contrib-less": "~2.0.0",
+    "grunt-contrib-uglify": "~4.0.0",
+    "grunt-contrib-watch": "~1.1.0",
     "grunt-env": "~0.4.4",
     "grunt-express": "~1.4.1",
     "grunt-filerev": "~2.3.1",
@@ -90,11 +91,10 @@
     "grunt-parallel": "~0.5.1",
     "grunt-replace": "~1.0.1",
     "grunt-usemin": "~3.1.1",
-    "grunt-webfont": "~1.6.0",
     "matchdep": "~2.0.0",
-    "mocha": "~4.1.0",
-    "sinon": "~4.1.6",
-    "sinon-chai": "~2.14.0"
+    "mocha": "~5.2.0",
+    "sinon": "~7.2.3",
+    "sinon-chai": "~3.3.0"
   },
   "scripts": {
     "test": "grunt test",

+ 1 - 1
server_config/server_install.sh

@@ -6,7 +6,7 @@ sudo apt-get install lsb-release libfontconfig1 libfreetype6 libjpeg-dev -y --fo
 sudo apt-get install curl git software-properties-common build-essential make g++ -y --force-yes > /dev/null 2>&1
 
 # Installation of NodeJS
-curl -sL https://deb.nodesource.com/setup_4.x | sudo -E bash -
+curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -
 sudo apt-get install -y nodejs > /dev/null 2>&1
 source ~/.profile
 

+ 8 - 4
server_config/settings-prod.json

@@ -1,14 +1,18 @@
 {
     "serverPort": 80,
-    "phantomasEngine": "webkit",
     "googleAnalyticsId": "",
-    "screenshotWidth": 400,
+    "screenshotWidth": {
+        "phone": 360,
+        "tablet": 420,
+        "desktop": 600,
+        "desktop-hd": 600
+    },
     "baseUrl": "/",
     "authorizedKeys": {
         
     },
-    "maxAnonymousRunsPerDay": 99999999,
-    "maxAnonymousCallsPerDay": 99999999,
+    "maxAnonymousRunsPerDay": 1000,
+    "maxAnonymousCallsPerDay": 100000,
     "blockedUrls": [],
 
     "sponsoring" : {

+ 6 - 2
server_config/settings.json

@@ -1,8 +1,12 @@
 {
     "serverPort": 8383,
-    "phantomasEngine": "webkit",
     "googleAnalyticsId": "",
-    "screenshotWidth": 400,
+    "screenshotWidth": {
+        "phone": 360,
+        "tablet": 420,
+        "desktop": 600,
+        "desktop-hd": 600
+    },
     "baseUrl": "/",
     "authorizedKeys": {
         

+ 0 - 33
test/api/apiTest.js

@@ -171,9 +171,6 @@ describe('api', function() {
                 body.should.have.a.property('rules').that.is.an('object');
                 body.should.have.a.property('toolsResults').that.is.an('object');
 
-                body.should.have.a.property('javascriptExecutionTree').that.is.an('object');
-                body.javascriptExecutionTree.should.not.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('*');
@@ -410,10 +407,6 @@ describe('api', function() {
                 body.should.have.a.property('toolsResults').that.is.an('object');
                 body.toolsResults.should.have.a.property('phantomas').that.is.an('object');
 
-                body.should.have.a.property('javascriptExecutionTree').that.is.an('object');
-                body.javascriptExecutionTree.should.have.a.property('data').that.is.an('object');
-                body.javascriptExecutionTree.data.should.have.a.property('type').that.equals('main');
-
                 done();
 
             } else {
@@ -507,28 +500,6 @@ describe('api', function() {
     });
 
 
-    it('should return the javascript execution tree', function(done) {
-        this.timeout(5000);
-
-        request({
-            method: 'GET',
-            url: serverUrl + '/api/results/' + asyncRunId + '/javascriptExecutionTree',
-            json: true,
-        }, function(error, response, body) {
-            if (!error && response.statusCode === 200) {
-                
-                body.should.have.a.property('data').that.is.an('object');
-                body.data.should.have.a.property('type').that.equals('main');
-                
-                done();
-
-            } else {
-                done(error || response.statusCode);
-            }
-        });
-    });
-
-
     it('should return the phantomas results', function(done) {
         this.timeout(5000);
 
@@ -565,7 +536,6 @@ describe('api', function() {
                 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');
 
@@ -591,7 +561,6 @@ describe('api', function() {
                 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');
@@ -617,7 +586,6 @@ describe('api', function() {
                 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');
 
@@ -642,7 +610,6 @@ describe('api', function() {
                 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');
 

+ 10 - 60
test/api/screenshotHandlerTest.js

@@ -3,6 +3,7 @@ var ScreenshotHandler = require('../../lib/screenshotHandler');
 
 var fs = require('fs');
 var path = require('path');
+var rimraf = require('rimraf');
 
 describe('screenshotHandler', function() {
 
@@ -55,10 +56,14 @@ describe('screenshotHandler', function() {
     });
 
 
-    it('should optimize an image and return a buffered version', function(done) {
-        ScreenshotHandler.optimize(imagePath, 200)
+    it('should create the tmp folder if it doesn\'t exist', function(done) {
+        // Delete tmp folder if it exists
+        rimraf.sync("/some/directory");
+        
+        // The function we want to test
+        ScreenshotHandler.createTmpScreenshotFolder()
             .then(function(buffer) {
-                buffer.should.be.an.instanceof(Buffer);
+                fs.existsSync(path.join(__dirname, '../../tmp')).should.equal(true);
                 done();
             })
             .fail(function(err) {
@@ -66,63 +71,8 @@ describe('screenshotHandler', function() {
             });
     });
 
-
-    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 return the tmp folder path', function() {
+        ScreenshotHandler.getTmpFileRelativePath().should.equal('tmp/temp-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);
-            });
-    });
 });

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