فهرست منبع

Merge pull request #37 from gmetais/refactor

v1.1.0
Gaël Métais 10 سال پیش
والد
کامیت
eb4d8cde7c
100فایلهای تغییر یافته به همراه3889 افزوده شده و 3012 حذف شده
  1. 2 0
      .gitignore
  2. 2 2
      .travis.yml
  3. 275 27
      Gruntfile.js
  4. 32 12
      README.md
  5. 0 112
      app/lib/phantomasWrapper.js
  6. 0 8
      app/lib/strReplace.js
  7. 0 66
      app/lib/testQueue.js
  8. 0 28
      app/node_controllers/indexController.js
  9. 0 85
      app/node_controllers/launchTestController.js
  10. 0 47
      app/node_controllers/resultsController.js
  11. 0 60
      app/node_controllers/waitingQueueSocket.js
  12. 0 43
      app/node_views/index.html
  13. 0 97
      app/node_views/launchTest.html
  14. 0 959
      app/node_views/results.html
  15. BIN
      app/public/fonts/icons.woff
  16. 0 5
      app/public/scripts/app.js
  17. 0 417
      app/public/scripts/resultsCtrl.js
  18. 0 17
      app/public/scripts/showOffenders.js
  19. 0 25
      app/public/styles/launchTest.css
  20. 0 19
      app/public/styles/less/launchTest.less
  21. 0 66
      app/public/styles/less/main.less
  22. 35 0
      bin/cli.js
  23. 38 0
      bin/server.js
  24. 3 2
      bower.json
  25. 10 0
      front/src/css/about.css
  26. 167 0
      front/src/css/dashboard.css
  27. 6 0
      front/src/css/icons.css
  28. 2 10
      front/src/css/index.css
  29. 96 12
      front/src/css/main.css
  30. 11 0
      front/src/css/queue.css
  31. 84 0
      front/src/css/rule.css
  32. 8 224
      front/src/css/timeline.css
  33. BIN
      front/src/fonts/icons.woff
  34. 0 0
      front/src/fonts/svg-icons/arrow-left3.svg
  35. 0 0
      front/src/fonts/svg-icons/bars.svg
  36. 0 0
      front/src/fonts/svg-icons/lab.svg
  37. 0 0
      front/src/fonts/svg-icons/list.svg
  38. 0 0
      front/src/fonts/svg-icons/loop.svg
  39. 0 0
      front/src/fonts/svg-icons/question.svg
  40. 0 0
      front/src/fonts/svg-icons/warning.svg
  41. 0 0
      front/src/img/favicon.png
  42. 0 0
      front/src/img/logo-large.png
  43. 58 0
      front/src/js/app.js
  44. 5 0
      front/src/js/controllers/aboutCtrl.js
  45. 73 0
      front/src/js/controllers/dashboardCtrl.js
  46. 15 0
      front/src/js/controllers/indexCtrl.js
  47. 30 0
      front/src/js/controllers/queueCtrl.js
  48. 42 0
      front/src/js/controllers/ruleCtrl.js
  49. 145 0
      front/src/js/controllers/timelineCtrl.js
  50. 33 0
      front/src/js/directives/gradeDirective.js
  51. 7 0
      front/src/js/models/resultsFactory.js
  52. 7 0
      front/src/js/models/runsFactory.js
  53. 34 0
      front/src/js/services/menuService.js
  54. 12 0
      front/src/less/about.less
  55. 162 0
      front/src/less/dashboard.less
  56. 15 15
      front/src/less/icons.less
  57. 2 12
      front/src/less/index.less
  58. 155 0
      front/src/less/main.less
  59. 13 0
      front/src/less/queue.less
  60. 91 0
      front/src/less/rule.less
  61. 10 236
      front/src/less/timeline.less
  62. 49 0
      front/src/main.html
  63. 9 0
      front/src/views/about.html
  64. 59 0
      front/src/views/dashboard.html
  65. 6 0
      front/src/views/index.html
  66. 23 0
      front/src/views/queue.html
  67. 8 0
      front/src/views/resultSubHeader.html
  68. 37 0
      front/src/views/rule.html
  69. 144 0
      front/src/views/timeline.html
  70. 38 0
      lib/index.js
  71. 453 0
      lib/metadata/policies.js
  72. 114 0
      lib/metadata/scoreProfileGeneric.json
  73. 98 0
      lib/rulesChecker.js
  74. 60 0
      lib/runner.js
  75. 87 0
      lib/scoreCalculator.js
  76. 226 0
      lib/server/controllers/apiController.js
  77. 26 0
      lib/server/controllers/frontController.js
  78. 89 0
      lib/server/datastores/resultsDatastore.js
  79. 106 0
      lib/server/datastores/runsDatastore.js
  80. 80 0
      lib/server/datastores/runsQueue.js
  81. 85 0
      lib/server/middlewares/apiLimitsMiddleware.js
  82. 42 0
      lib/server/middlewares/authMiddleware.js
  83. 62 0
      lib/tools/jsExecutionTransformer.js
  84. 17 3
      lib/tools/phantomas/custom_modules/core/scopeYLT/scopeYLT.js
  85. 57 14
      lib/tools/phantomas/custom_modules/modules/domQYLT/domQYLT.js
  86. 0 0
      lib/tools/phantomas/custom_modules/modules/eventListYLT/eventListYLT.js
  87. 1 1
      lib/tools/phantomas/custom_modules/modules/jQYLT/jQYLT.js
  88. 57 0
      lib/tools/phantomas/custom_modules/modules/javaScriptBottleYLT/javaScriptBottleYLT.js
  89. 0 0
      lib/tools/phantomas/custom_modules/modules/jsErrYLT/jsErrYLT.js
  90. 0 0
      lib/tools/phantomas/custom_modules/modules/jsFileLoadYLT/jsFileLoadYLT.js
  91. 0 0
      lib/tools/phantomas/custom_modules/modules/jsTreeYLT/jsTreeYLT.js
  92. 0 0
      lib/tools/phantomas/custom_modules/modules/windowPerfYLT/windowPerfYLT.js
  93. 133 0
      lib/tools/phantomas/phantomasWrapper.js
  94. 36 14
      package.json
  95. 0 138
      phantomas_custom/modules/analyzeStyleYLT/analyzeStyleYLT.js
  96. 0 135
      phantomas_custom/modules/domComplexYLT/domComplexYLT.js
  97. 0 37
      phantomas_custom/modules/keepAlive/keepAlive.js
  98. BIN
      screenshot.png
  99. 0 60
      server.js
  100. 7 4
      server_config/server_install.sh

+ 2 - 0
.gitignore

@@ -1,7 +1,9 @@
 node_modules
 bower_components
+.tmp
 .vagrant
 results/*
 coverage
+front/build
 
 har.json

+ 2 - 2
.travis.yml

@@ -1,8 +1,8 @@
 language: node_js
 node_js:
-    - "0.10"
+    - "0.10.33"
 before_install:
     - "npm install -g grunt-cli"
     - "npm install -g phantomjs"
 install: npm install
-before_script: grunt build
+before_script: grunt test

+ 275 - 27
Gruntfile.js

@@ -1,60 +1,131 @@
 module.exports = function(grunt) {
 
+    // Load all grunt modules
+    require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks);
+
+    // Tell our Express server that Grunt launched it
+    process.env.GRUNTED = true;
+
     // Project configuration.
     grunt.initConfig({
         pkg: grunt.file.readJSON('package.json'),
+        settings: grunt.file.readJSON('./server_config/settings.json'),
         
         font: {
             icons: {
-                src: ['app/public/fonts/svg-icons/*.svg'],
-                destCss: 'app/public/styles/less/icons.less',
-                destFonts: 'app/public/fonts/icons.woff',
+                src: ['front/src/fonts/svg-icons/*.svg'],
+                destCss: 'front/src/less/icons.less',
+                destFonts: 'front/src/fonts/icons.woff',
 
                 // Optional: Custom routing of font filepaths for CSS
                 cssRouter: function (fontpath) {
                     var pathArray = fontpath.split('/');
                     var fileName = pathArray[pathArray.length - 1];
-                    return '/public/fonts/' + fileName;
+                    return '/fonts/' + fileName;
                 }
             }
         },
         less: {
             all: {
-                files: {
-                    'app/public/styles/main.css': [ 'app/public/styles/less/main.less' ],
-                    'app/public/styles/index.css': [ 'app/public/styles/less/index.less' ],
-                    'app/public/styles/launchTest.css': [ 'app/public/styles/less/launchTest.less' ],
-                    'app/public/styles/results.css': [ 'app/public/styles/less/results.less' ]
-                }
+                files: [
+                    {
+                        expand: true,
+                        cwd: 'front/src/less/',
+                        src: ['**/*.less'],
+                        dest: 'front/src/css/',
+                        ext: '.css'
+                    }
+                ]
+            }
+        },
+        replace: {
+            dist: {
+                options: {
+                    patterns: [
+                        {
+                            match: 'googleAnalyticsId',
+                            replacement: '<%= settings.googleAnalyticsId %>'
+                        },
+                        {
+                            match: 'version',
+                            replacement: 'v<%= pkg.version %>'
+                        }
+                    ]
+                },
+                files: [
+                    {expand: true, flatten: true, src: ['front/src/main.html'], dest: 'front/build/'}
+                ]
             }
         },
         jshint: {
             all: [
                 '*.js',
-                'app/lib/*',
+                'app/lib/*.js',
+                'bin/*.js',
+                'lib/**/*.js',
                 'app/nodeControllers/*.js',
                 'app/public/scripts/*.js',
-                'phantomas_custom/**/*.js'
+                'phantomas_custom/**/*.js',
+                'test/**/*.js',
+                'front/src/js/**/*.js'
             ]
         },
         clean: {
-            icons: {
-                src: ['tmp']
+            tmp: {
+                src: ['.tmp']
+            },
+            dev: {
+                src: ['front/src/css']
             },
             coverage: {
-                src: ['coverage/']
+                src: ['.tmp', 'coverage/']
+            },
+            build: {
+                src: ['front/build']
             }
         },
         copy: {
+            beforeCoverage: {
+                files: [
+                    {src: ['bin/server.js'], dest: '.tmp/'}
+                ]
+            },
             coverage: {
-                src: ['test/**'],
-                dest: 'coverage/'
+                files: [
+                    {src: ['test/**'], dest: 'coverage/'},
+                    {src: ['lib/metadata/**'], dest: 'coverage/'}
+                ]
+            },
+            build: {
+                files: [
+                    {src: ['./front/src/fonts/icons.woff'], dest: './front/build/fonts/icons.woff'},
+                    {src: ['./front/src/img/favicon.png'], dest: './front/build/img/favicon.png'},
+                    {src: ['./front/src/img/logo-large.png'], dest: './front/build/img/logo-large.png'},
+                ]
+            }
+        },
+        lineremover: {
+            beforeCoverage: {
+                files: {
+                    '.tmp/bin/cli.js': 'bin/cli.js'
+                },
+                options: {
+                    exclusionPattern: /#!\/usr\/bin\/env node/
+                }
             }
         },
         blanket: {
-            coverage: {
+            coverageApp: {
                 src: ['app/'],
                 dest: 'coverage/app/'
+            },
+            coverageLib: {
+                src: ['lib/'],
+                dest: 'coverage/lib/'
+            },
+            coverageBin: {
+                src: ['.tmp/bin/'],
+                dest: 'coverage/bin/'
             }
         },
         mochaTest: {
@@ -62,44 +133,221 @@ module.exports = function(grunt) {
                 options: {
                     reporter: 'spec',
                 },
-                src: ['coverage/test/server/*.js']
+                src: ['coverage/test/core/*.js', 'coverage/test/api/*.js']
+            },
+            'test-current-work': {
+                options: {
+                    reporter: 'spec',
+                },
+                src: ['coverage/test/api/apiTest.js']
             },
             coverage: {
                 options: {
                     reporter: 'html-cov',
-                    // use the quiet flag to suppress the mocha console output
                     quiet: true,
-                    // specify a destination file to capture the mocha
-                    // output (the quiet option does not suppress this)
                     captureFile: 'coverage/coverage.html'
                 },
-                src: ['coverage/test/server/*.js']
+                src: ['coverage/test/core/*.js', 'coverage/test/api/*.js']
+            }
+        },
+        env: {
+            dev: {
+                NODE_ENV: 'development'
+            },
+            builded: {
+                NODE_ENV: 'production'
+            }
+        },
+        express: {
+            dev: {
+                options: {
+                    port: 8383,
+                    server: './bin/server.js',
+                    serverreload: true,
+                    showStack: true
+                }
+            },
+            builded: {
+                options: {
+                    port: 8383,
+                    server: './bin/server.js',
+                    serverreload: true,
+                    showStack: true
+                }
+            },
+            test: {
+                options: {
+                    port: 8387,
+                    server: './coverage/bin/server.js',
+                    showStack: true
+                }
+            },
+            testSuite: {
+                options: {
+                    port: 8388,
+                    bases: 'test/www'
+                }
+            }
+        },
+        useminPrepare: {
+            html: './front/src/main.html',
+            options: {
+                dest: './front/build',
+                root: ['./', './front/src']
+            }
+        },
+        usemin: {
+            html: './front/build/main.html',
+            css: './front/build/css/*.css',
+            options: {
+                assetsDirs: ['front/build'],
+                patterns: {
+                    css: [[/(\/fonts\/icons\.woff)/gm, 'Replacing reference to icons.woff']]
+                }
+            }
+        },
+        htmlmin: {
+            options: {
+                removeComments: true,
+                collapseWhitespace: true
+            },
+            main: {
+                files: [{
+                    expand: true,
+                    cwd: './front/build/',
+                    src: 'main.html',
+                    flatten: true,
+                    dest: './front/build'
+                }]
+            },
+            views: {
+                files: [{
+                    expand: true,
+                    cwd: './front/src/views',
+                    src: '*.html',
+                    flatten: true,
+                    dest: '.tmp/views/'
+                }]
+            }
+        },
+        inline_angular_templates: {
+            build: {
+                options: {
+                    base: '.tmp',
+                    method: 'append'
+                },
+                files: {
+                    './front/build/main.html': ['.tmp/views/*.html']
+                }
+            }
+        },
+        filerev: {
+            options: {
+                algorithm: 'md5',
+                length: 8
+            },
+            assets: {
+                src: './front/build/*/*.*'
             }
         }
     });
 
-    require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks);
+
+    // Custom task: copies the test settings.json file to the coverage folder, and checks if there's no missing fields
+    grunt.registerTask('copy-test-server-settings', function() {
+        var mainSettingsFile = './server_config/settings.json';
+        var testSettingsFile = './test/fixtures/settings.json';
+
+        var mainSettings = grunt.file.readJSON(mainSettingsFile);
+        var testSettings = grunt.file.readJSON(testSettingsFile);
+
+        // Recursively compare keys of two objects (not the values)
+        function compareKeys(original, copy, context) {
+            for (var key in original) {
+                if (!copy[key] && copy[key] !== '' && copy[key] !== 0) {
+                    grunt.fail.warn('Settings file ' + testSettingsFile + ' doesn\'t contain key ' + context + '.' + key);
+                }
+                if (original[key] !== null && typeof original[key] === 'object') {
+                    compareKeys(original[key], copy[key], context + '.' + key);
+                }
+            }
+        }
+
+        compareKeys(mainSettings, testSettings, 'settings');
+
+        var outputFile = './coverage/server_config/settings.json';
+        grunt.file.write(outputFile, JSON.stringify(testSettings, null, 4));
+        grunt.log.ok('File ' + outputFile + ' created');
+    });
+
 
     grunt.registerTask('icons', [
         'font:icons',
         'less',
-        'clean:icons'
+        'clean:tmp'
     ]);
 
     grunt.registerTask('build', [
         'jshint',
-        'less'
+        'clean:build',
+        'copy:build',
+        'less',
+        'useminPrepare',
+        'concat',
+        'uglify',
+        'cssmin',
+        'replace',
+        'htmlmin:views',
+        'inline_angular_templates',
+        'filerev',
+        'usemin',
+        'htmlmin:main',
+        'clean:tmp'
     ]);
 
     grunt.registerTask('hint', [
         'jshint'
     ]);
 
+    grunt.registerTask('dev', [
+        'env:dev',
+        'express:dev'
+    ]);
+
+    grunt.registerTask('builded', [
+        'env:builded',
+        'express:builded'
+    ]);
+
     grunt.registerTask('test', [
+        'build',
+        'jshint',
+        'express:testSuite',
+        'clean:coverage',
+        'copy-test-server-settings',
+        'lineremover:beforeCoverage',
+        'copy:beforeCoverage',
+        'blanket',
+        'copy:coverage',
+        'express:test',
+        'mochaTest:test',
+        'mochaTest:coverage',
+        'clean:tmp'
+    ]);
+
+    grunt.registerTask('test-current-work', [
+        'build',
+        'jshint',
+        'express:testSuite',
         'clean:coverage',
+        'copy-test-server-settings',
+        'lineremover:beforeCoverage',
+        'copy:beforeCoverage',
         'blanket',
         'copy:coverage',
-        'mochaTest'
+        'express:test',
+        'mochaTest:test-current-work',
+        'clean:tmp'
     ]);
 
 };

+ 32 - 12
README.md

@@ -1,37 +1,57 @@
 # Yellow Lab Tools
 
-Online tool designed to help Front-End developers optimize their website
+Online tool that lets you test a webpage and detects **performance** and **front-end code quality** issues.
 
 [![Dependency Status](https://gemnasium.com/gmetais/YellowLabTools.svg)](https://gemnasium.com/gmetais/YellowLabTools) [![Build Status](https://travis-ci.org/gmetais/YellowLabTools.svg?branch=master)](https://travis-ci.org/gmetais/YellowLabTools)
 
 
 ### Access the tool here: [http://yellowlab.tools](http://yellowlab.tools)
 
+![example dashboard screenshot](screenshot.png)
+
 
 ## How it works
 
-The tool is based on the fabulous [Phantomas](https://github.com/macbre/phantomas) by Maciej Brencz, that loads a page and collects many web performance metrics.
-I rewrote some modules to deeper analyze Javascript interactions with the DOM (especially with jQuery).
-And then it is wrapped inside a small NodeJS server to get an interface (inspired by my favorite tool: [WebPageTest](http://www.webpagetest.org/)).
+The tool loads the given URL via [PhantomasJS](http://phantomjs.org/) (a headless browser) and collects various metrics and statistics with the help of [Phantomas](https://github.com/macbre/phantomas). These metrics are categorized and transformed into scores. It also gives in-depth details so developpers can correct the detected issues.
 
-**There are so many things left to do, your help would be greatly appreciated! Please report bugs, ask for evolutions and come code with me.**
+By the way, it's free because we are geeks, not businessmen. All we want is a ★ on GitHub, it will boost our motivation to add more awesome features!!!
 
 
-## Help needed
+## Different ways to use YLT
 
-I'm currently looking for help in correcting the english wordings in the tool, as it is not my native language.
+#### The web interface:
+This is the best way to discover the tool: [http://yellowlab.tools](http://yellowlab.tools).  
+Please note the server is hosted in Baltimore, USA.
 
+#### The Command Line Client:
+Might be useful if you want to build an automation tool. The documentation is [here](https://github.com/gmetais/YellowLabTools/wiki/Command-Line-Interface).
 
-## Install your own instance
+#### The NPM module:
+Can be used to build automation tools in NodeJS. The documentation is [here](https://github.com/gmetais/YellowLabTools/wiki/NodeJS-module).
 
-If your project is not accessible from outside, or if you want to fork and improve the tool, you can build your own instance. The documentation is [here](https://github.com/gmetais/YellowLabTools/wiki/Install-your-own-instance).
+#### The Public API:
+Hosted on our http://yellowlab.tools server, it is a RESTful API that allows you to launch distant tests. The documentation is [here](https://github.com/gmetais/YellowLabTools/wiki/Public-API).
 
 
-## License
-Please read the [license](LICENSE).
+## Install your own private instance
+
+If your project is not accessible from outside, or if you want to fork and improve the tool, you can build your own instance. The documentation is [here](https://github.com/gmetais/YellowLabTools/wiki/Install-your-private-server).
+
+
+## Help needed
 
+There are so many things left to do, **your help would be greatly appreciated**! Please report bugs, ask for evolutions and come code with me.
+
+
+## License
+Please read the [license](LICENSE).  
 Icons are the property of [IcoMoon.io](https://icomoon.io/)
 
 
 ## Author
-Gaël Métais. I'm a webperf freelance based in Paris. If you understand french, you can visit [my website](http://www.gaelmetais.com).
+Gaël Métais. I'm a webperf freelance based in Paris. If you understand french, you can visit [my website](http://www.gaelmetais.com).
+
+
+## Contributors
+- Achraf Ben Younes [achrafbenyounes](https://github.com/achrafbenyounes)
+

+ 0 - 112
app/lib/phantomasWrapper.js

@@ -1,112 +0,0 @@
-/**
- * Yellow Lab Tools main file
- */
-
-var async           = require('async');
-var phantomas       = require('phantomas');
-
-var PhantomasWrapper = function() {
-    'use strict';
-
-    /**
-     * This is the phantomas launcher. It merges user chosen options into the default options
-     * Available options :
-     *
-     * - timeout : in seconds (default 60)
-     * - jsDeepAnalysis : should we inspect subrequests in the javascript execution tree (reported durations of main tasks will be slower than usual)
-     *
-     */
-     this.execute = function(task, callback) {
-
-        var options = {
-            // Cusomizable options
-            timeout: task.options.timeout || 60,
-            'js-deep-analysis': task.options.jsDeepAnalysis || false,
-            'user-agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.110 Safari/537.36',
-
-            // Mandatory
-            reporter: 'json:pretty',
-            'analyze-css': true,
-            'skip-modules': [
-                'analyzeCss', // overriden
-                'blockDomains', // not needed
-                'domComplexity', // overriden
-                'domMutations', // not compatible with webkit
-                'domQueries', // overriden
-                'eventListeners', // overridden
-                'filmStrip', // not needed
-                'har', // not needed for the moment
-                'pageSource', // not needed
-                'screenshot', // not needed for the moment
-                'waitForSelector', // not needed
-                'windowPerformance' // overriden
-            ].join(','),
-            'include-dirs': [
-                'phantomas_custom/core',
-                'phantomas_custom/modules'
-            ].join(',')
-        };
-
-        // Output the command line for debugging purpose
-        console.log('If you want to reproduce the phantomas task only, copy the following command line:');
-        var optionsString = '';
-        for (var opt in options) {
-
-            var value = options[opt];
-            if ((typeof value === 'string' || value instanceof String) && value.indexOf(' ') >= 0) {
-                value = '"' + value + '"';
-            }
-
-            optionsString += ' ' + '--' + opt + '=' + value;
-        }
-        console.log('node node_modules/phantomas/bin/phantomas.js --url=' + task.url + optionsString + ' --verbose');
-
-        // Kill the application if nothing happens for 10 minutes
-        var killer = setTimeout(function() {
-            console.log('Killing the server because the test ' + task.testId + ' on ' + task.url + ' was launched 10 minutes ago');
-            // Forever will restart the server
-            process.exit(1);
-        }, 600000);
-
-        // It's time to launch the test!!!
-        var triesNumber = 3;
-
-        async.retry(triesNumber, function(cb) {
-            phantomas(task.url, options, function(err, json, results) {
-                console.log('Returning from Phantomas');
-
-                // Adding some YellowLabTools errors here
-                if (json && json.metrics && !json.metrics.javascriptExecutionTree) {
-                    err = 1001;
-                }
-
-                if (!err && (!json || !json.metrics)) {
-                    err = 1002;
-                }
-
-                // Don't cancel test if it is a timeout and we've got some results
-                if (err === 252 && json) {
-                    console.log('Timeout after ' + options.timeout + ' seconds. But it\'s not a problem, the test is valid.');
-                    err = null;
-                }
-
-                if (err) {
-                    console.log('Attempt failed for test id ' + task.testId + '. Error code ' + err);
-                }
-
-                cb(err, {json: json, results: results});
-            });
-        }, function(err, data) {
-
-            clearTimeout(killer);
-
-            if (err) {
-                console.log('All ' + triesNumber + ' attemps failed for test id ' + task.testId);
-            }
-            callback(err, data.json, data.results);
-        });
-
-    };
-};
-
-module.exports = new PhantomasWrapper();

+ 0 - 8
app/lib/strReplace.js

@@ -1,8 +0,0 @@
-/**
- * Alternative to the standard String.prototype.replace function
- * Avoids problems with $$, $1, $2, ...
- */
-
-module.exports = function(str, searched, replacement) {
-    return str.split(searched).join(replacement);
-};

+ 0 - 66
app/lib/testQueue.js

@@ -1,66 +0,0 @@
-/**
- * Creation of a queue and it's worker function
- */
-
-var util                = require('util');
-var EventEmitter        = require('events').EventEmitter;
-var async               = require('async');
-var phantomasWrapper    = require('./phantomasWrapper');
-
-
-var testQueue = function() {
-    'use strict';
-
-    var currentTask = null;
-    var self = this;
-
-    var queue = async.queue(function(task, callback) {
-        currentTask = task;
-
-        console.log('Starting test ' + task.testId);
-        
-        phantomasWrapper.execute(task, function(err, json, results) {
-            console.log('Test ' + task.testId + ' complete');
-            currentTask = null;
-            callback(err, json, results);
-            self.emit('queueMoving');
-        });
-    });
-
-    
-    // Use this method to add a test to the queue
-    this.push = queue.push;
-
-    
-    // Gives the position of a task in the queue
-    // Returns 0 if it is the current running task
-    // Returns -1 if not found
-    this.indexOf = function(testId) {
-        if (currentTask && currentTask.testId === testId) {
-            return 0;
-        }
-
-        var position = -1;
-        if (queue.length() > 0) {
-            queue.tasks.forEach(function(task, index) {
-                if (task.data.testId === testId) {
-                    position = index + 1;
-                }
-            });
-        }
-        return position;
-    };
-
-    this.testComplete = function(testId) {
-        self.emit('testComplete', testId);
-    };
-
-    this.testFailed = function(testId) {
-        self.emit('testFailed', testId);
-    };
-};
-
-// extend the EventEmitter class
-util.inherits(testQueue, EventEmitter);
-
-module.exports = new testQueue();

+ 0 - 28
app/node_controllers/indexController.js

@@ -1,28 +0,0 @@
-/**
- * Yellow Lab Tools home page controller
- */
-
-var async           = require('async');
-var fs              = require ('fs');
-var strReplace      = require('../lib/strReplace');
-
-var indexController = function(req, res, googleAnalyticsId, version) {
-    'use strict';
-
-    async.parallel({
-        
-        htmlTemplate: function(callback) {
-            fs.readFile('./app/node_views/index.html', {encoding: 'utf8'}, callback);
-        }
-
-    }, function(err, results) {
-        var html = results.htmlTemplate;
-        html = strReplace(html, '%%GA_ID%%', googleAnalyticsId);
-        html = strReplace(html, '%%VERSION%%', version);
-
-        res.setHeader('Content-Type', 'text/html');
-        res.send(html);
-    });
- };
-
- module.exports = indexController;

+ 0 - 85
app/node_controllers/launchTestController.js

@@ -1,85 +0,0 @@
-/**
- * Controller for the test launching page (the waiting page, after the user submited a test on the index page)
- */
-
-var async           = require('async');
-var fs              = require ('fs');
-var strReplace      = require('../lib/strReplace');
-
-var launchTestController = function(req, res, testQueue, googleAnalyticsId) {
-    'use strict';
-
-    // Generate test id
-    var testId = (Date.now()*1000 + Math.round(Math.random()*1000)).toString(36);
-
-    var resultsPath = 'results/' + testId;
-    var phantomasResultsPath = resultsPath + '/results.json';
-    
-    var url = req.body.url;
-    if (url.indexOf('http://') !== 0 && url.indexOf('https://') !== 0) {
-        url = 'http://' + url;
-    }
-
-    var options = {};
-    if (req.body.timeout) {
-        options.timeout = req.body.timeout;
-    }
-
-    async.waterfall([
-        
-        function htmlTemplate(callback) {
-            fs.readFile('./app/node_views/launchTest.html', {encoding: 'utf8'}, callback);
-        },
-
-        function sendResponse(html, callback) {
-
-            html = strReplace(html, '%%TEST_URL%%', url);
-            html = strReplace(html, '%%TEST_ID%%', testId);
-            html = strReplace(html, '%%GA_ID%%', googleAnalyticsId);
-
-            res.setHeader('Content-Type', 'text/html');
-            res.send(html);
-
-            callback();
-        },
-
-        function createFolder(callback) {
-            // Create results folder
-            fs.mkdir(resultsPath, callback);
-        },
-
-        function executePhantomas(callback) {
-            console.log('Adding test ' + testId + ' on ' + url + ' to the queue');
-            
-            var task = {
-                testId: testId,
-                url: url,
-                options: options
-            };
-
-            testQueue.push(task, callback);
-        },
-
-        function writeResults(json, resultsObject, callback) {
-            console.log('Saving Phantomas results file to ' + phantomasResultsPath);
-            fs.writeFile(phantomasResultsPath, JSON.stringify(json, null, 4), callback);
-        }
-
-    ], function(err) {
-        if (err) {
-            console.log('An error occured in the phantomas test: ', err);
-
-            fs.writeFile(phantomasResultsPath, JSON.stringify({url: url, error: err}, null, 4), function(err) {
-                if (err) {
-                    console.log('Could not even write an error message on file ' + phantomasResultsPath);
-                    console.log(err);
-                }
-            });
-            testQueue.testFailed(testId);
-        } else {
-            testQueue.testComplete(testId);
-        }
-    });
- };
-
- module.exports = launchTestController;

+ 0 - 47
app/node_controllers/resultsController.js

@@ -1,47 +0,0 @@
-/**
- * The page that dispays the results
- */
-
-var async           = require('async');
-var fs              = require('fs');
-var strReplace      = require('../lib/strReplace');
-
-var resultsController = function(req, res, googleAnalyticsId) {
-    'use strict';
-
-    var testId = req.params.testId;
-    var resultsPath = 'results/' + testId;
-    var phantomasResultsPath = resultsPath + '/results.json';
-
-    console.log('Opening test ' + testId + ' results as HTML');
-
-    async.parallel({
-        
-        htmlTemplate: function(callback) {
-            fs.readFile('./app/node_views/results.html', {encoding: 'utf8'}, callback);
-        },
-
-        phantomasResults: function(callback) {
-            fs.readFile(phantomasResultsPath, {encoding: 'utf8'}, callback);
-        }
-
-    }, function(err, results) {
-        if (err) {
-            console.log(err);
-            return res.status(404).send('Sorry, test not found...');
-        }
-
-        // Escape "</script>" because it can interfer with the HTML parser
-        var phantomasResults = results.phantomasResults;
-        phantomasResults = phantomasResults.replace(/<\/script>/g, '\\u003c/script>');
-
-        var html = results.htmlTemplate;
-        html = strReplace(html, '%%RESULTS%%', phantomasResults);
-        html = strReplace(html, '%%GA_ID%%', googleAnalyticsId);
-
-        res.setHeader('Content-Type', 'text/html');
-        res.send(html);
-    });
-};
-
-module.exports = resultsController;

+ 0 - 60
app/node_controllers/waitingQueueSocket.js

@@ -1,60 +0,0 @@
-/**
- * Socket.io handler
- */
-
-var fs = require('fs');
-
-var waitingQueueSocket = function(socket, testQueue) {
-    
-    socket.on('waiting', function(testId) {
-        console.log('User waiting for test id ' + testId);
-
-        sendTestStatus(testId);
-
-        testQueue.on('testComplete', function(id) {
-            if (testId === id) {
-                socket.emit('complete');
-                console.log('Sending complete event to test id ' + testId);
-            }
-        });
-
-        testQueue.on('testFailed', function(id) {
-            if (testId === id) {
-                socket.emit('failed');
-                console.log('Sending failed event to test id ' + testId);
-            }
-        });
-
-        testQueue.on('queueMoving', function() {
-            var positionInQueue = testQueue.indexOf(testId);
-            if (positionInQueue >= 0) {
-                socket.emit('position', positionInQueue);
-                console.log('Sending position to test id ' + testId);
-            }
-        });
-    });
-
-    // Finds the status of a test and send it to the client
-    function sendTestStatus(testId) {
-        // Check task position in queue
-        var positionInQueue = testQueue.indexOf(testId);
-
-        if (positionInQueue >= 0) {
-            socket.emit('position', positionInQueue);
-        } else {
-            // Find in results files
-            var exists = fs.exists('results/' + testId + '/results.json', function(exists) {
-                if (exists) {
-                    // TODO : find a way to make sure the file is completely written
-                    setTimeout(function() {
-                        socket.emit('complete');
-                    }, 4000);
-                } else {
-                    socket.emit('404');
-                }
-            });
-        }
-    }
-};
-
-module.exports = waitingQueueSocket;

+ 0 - 43
app/node_views/index.html

@@ -1,43 +0,0 @@
-<html>
-<head>
-    <meta charset="utf-8">
-    <meta property="og:image" content="http://yellowlab.tools/public/img/logo-large.png" />
-    <title>Yellow Lab Tools</title>
-    <link rel="icon" type="image/png" href="/public/img/favicon.png">
-    <link rel="stylesheet" type="text/css" href="/public/styles/main.css">
-    <link rel="stylesheet" type="text/css" href="/public/styles/index.css">
-</head>
-<body>
-    <h1>Yellow Lab <span class="icon-lab"></span> Tools</h1>
-    <h2 class="promess">Free online test to help speeding up <b>heavy</b> web pages</h2>
-    <form method="post" action="/launchTest">
-        <input type="text" name="url" placeholder="http://www.mysite.com" class="url" />
-        <input type="submit" value="Launch test" class="launchBtn" onclick="this.className += ' clicked'" />
-    </form>
-
-    <div class="readings">
-        <h3>If you want to learn more...</h3>
-        <a target="_blank" href="http://gmetais.github.io/yellowlabtools/2014/11/20/what_is_yellow_lab_tools.html">What is Yellow Lab Tools?</a>
-        <br>
-        <a target="_blank" href="http://gmetais.github.io/yellowlabtools/2014/11/18/untangle_the_js_spaghetti_code.html">Learn how to deeply analyze your JavaScript</a>
-    </div>
-
-    <div class="footer">
-        <p><b>Yellow Lab Tools</b> is an open source project by <a href="http://www.gaelmetais.com" target="_blank">Gaël Métais</a>, based on <a href="https://github.com/macbre/phantomas" target="_blank">Phantomas</a>.<br>If you like it, <a href="https://github.com/gmetais/YellowLabTools" target="_blank" class="star">give it a <span>&#9733;</span> on GitHub</a>!</p>
-        <p class="version">v%%VERSION%%</p>
-    </div>
-
-    <script>
-        var GA_ID = '%%GA_ID%%';
-        if (GA_ID.length > 0 && window.location.host === 'yellowlab.tools') {
-            (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
-            (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
-            m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
-            })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
-
-            ga('create', GA_ID, 'auto');
-            ga('send', 'pageview');
-        }
-    </script>
-</body>
-</html>

+ 0 - 97
app/node_views/launchTest.html

@@ -1,97 +0,0 @@
-<html>
-<head>
-    <meta charset="utf-8">
-    <title>Yellow Lab Tools - Awaiting</title>
-    <link rel="icon" type="image/png" href="/public/img/favicon.png">
-    <link rel="stylesheet" type="text/css" href="/public/styles/main.css">
-    <link rel="stylesheet" type="text/css" href="/public/styles/launchTest.css">
-    <script src="/socket.io/socket.io.js"></script>
-</head>
-<body>
-    <h1>Yellow Lab <span class="icon-lab"></span> Tools</h1>
-
-    <div id="status"></div>
-
-    <div>%%TEST_URL%%</div>
-
-    <div class="footer">
-        <p><b>Yellow Lab Tools</b> is an open source project by <a href="http://www.gaelmetais.com" target="_blank">Gaël Métais</a>, based on <a href="https://github.com/macbre/phantomas" target="_blank">Phantomas</a>.<br>If you like it, <a href="https://github.com/gmetais/YellowLabTools" target="_blank" class="star">give it a <span>&#9733;</span> on GitHub</a>!</p>
-    </div>
-
-    <script>
-        var testId = '%%TEST_ID%%';
-        var statusElement = document.getElementById('status');
-        var socket = io();
-        
-        function askStatus() {
-            socket.emit('waiting', testId);
-        }
-        
-        socket.on('position', function(position) {
-            if (position === 0) {
-                statusElement.innerHTML = 'Test is running...';
-            } else {
-                statusElement.innerHTML = 'Waiting behind ' + (position) + ' other tests';
-            }
-            sendPositionInQueueToGA(position);
-
-            // Rotate the spinner
-            document.body.className = 'waiting';
-        });
-
-        socket.on('complete', function() {
-            statusElement.innerHTML = 'Test complete';
-            window.location.replace('/results/' + testId);
-
-            // Stop the spinner
-            document.body.className = '';
-        });
-
-        socket.on('failed', function() {
-            statusElement.innerHTML = 'Test failed';
-            window.location.replace('/results/' + testId);
-
-            // Stop the spinner
-            document.body.className = '';
-        });
-
-        socket.on('404', function() {
-            statusElement.innerHTML = 'Test not found';
-
-            // Stop the spinner
-            document.body.className = '';
-        });
-
-        socket.on('reconnect_error', function() {
-            statusElement.innerHTML = "Server reboot. Please start a new test."
-
-            // Stop the spinner
-            document.body.className = '';
-        });
-
-        askStatus();
-
-
-        var GA_ID = '%%GA_ID%%';
-        if (GA_ID.length > 0 && window.location.host === 'yellowlab.tools') {
-            (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
-            (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
-            m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
-            })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
-
-            ga('create', GA_ID, 'auto');
-        }
-
-        // Need stats on average queue position
-        var positionAlreadySent = false;
-        function sendPositionInQueueToGA(position) {
-            if (!positionAlreadySent && GA_ID.length > 0 && window.location.host === 'yellowlab.tools') {
-                ga('send', 'pageview', {
-                    queuePosition: position
-                });
-                positionAlreadySent = true;
-            }
-        }
-    </script>
-</body>
-</html>

+ 0 - 959
app/node_views/results.html

@@ -1,959 +0,0 @@
-<html>
-<head>
-    <meta charset="utf-8">
-    <title>Yellow Lab Tools - Results page</title>
-    <link rel="icon" type="image/png" href="/public/img/favicon.png">
-    <link rel="stylesheet" type="text/css" href="/public/styles/main.css">
-    <link rel="stylesheet" type="text/css" href="/public/styles/results.css">
-    <script src="/bower_components/angular/angular.min.js"></script>
-    <script src="/bower_components/ngModal/dist/ng-modal.js"></script>
-    <script src="/public/scripts/app.js"></script>
-    <script src="/public/scripts/resultsCtrl.js"></script>
-    <script src="/public/scripts/showOffenders.js"></script>
-</head>
-<body ng-app="YellowLabTools" ng-controller="ResultsCtrl">
-    <h1>Yellow Lab <span class="icon-lab"></span> Tools</h1>
-
-    <div ng-if="undefined">Untangling and counting the spaghettis...</div>
-
-    <div class="ng-cloak">
-        <div>Tested url: &nbsp; <a class="testedUrl" href="{{phantomasResults.url}}" target="_blank">{{phantomasResults.url}}</a></div>
-
-        <div class="resultsMenu">
-            <a class="menuItem back" href="/"><div class="icon-back"></div><span>New test<span></a>
-            <div class="menuItem" ng-if="!phantomasResults.error" ng-class="{active: view == 'execution'}" ng-click="setView('execution')"><div class="icon-spaghetti"></div><span>JS Timeline</span></div>
-            <div class="menuItem" ng-if="!phantomasResults.error" ng-class="{active: view == 'summary'}" ng-click="setView('summary')"><div class="icon-summary"></div><span>Grades</span></div>
-        </div>
-
-        <div ng-if="phantomasResults.error">
-            <h2>Error: {{phantomasResults.error}}</h2>
-            <div ng-if="phantomasResults.error == 252">Phantomas timed out</div>
-            <div ng-if="phantomasResults.error == 253">Phantomas config error</div>
-            <div ng-if="phantomasResults.error == 254">Phantomas failed to load page</div>
-            <div ng-if="phantomasResults.error == 255">Phantomas internal error</div>
-            <div ng-if="phantomasResults.error == 1001">Javascript execution tree error</div>
-            <div ng-if="phantomasResults.error == 1002">JSON undefined error</div>
-        </div>
-
-        <div ng-show="view == 'summary' && !phantomasResults.error" class="summary">
-            <h2>Grades</h2>
-
-            <div class="notations">
-                <div>
-                    <div ng-class="notations.domComplexity">{{notations.domComplexity}}</div>
-                    <div class="notation">DOM complexity</div>
-                    <div class="criteria">
-                        <div class="table">
-                            <div ng-if="phantomasResults.metrics.DOMelementsCount" ng-class="{'warning': phantomasResults.metrics.DOMelementsCount > 5000}">
-                                <div class="label">DOM elements count</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.DOMelementsCount}}
-                                </div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="DOMelementsCountTooltip = true"></div>
-                                    <modal-dialog show="DOMelementsCountTooltip" dialog-title="Avoid too many DOM elements">
-                                        <p>A high number of DOM elements means a lot of work for the browser to render the page.</p>
-                                        <p>It also slows down Javascript DOM queries, as there are more elements to search through.</p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.DOMelementMaxDepth" ng-class="{'warning': phantomasResults.metrics.DOMelementMaxDepth > 30}">
-                                <div class="label">DOM max depth</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.DOMelementMaxDepth}}
-                                    <show-offenders modal-title="DOM max depth" metric-name="DOMelementMaxDepth" phantomas-results="phantomasResults"></show-offenders>
-                                </div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="DOMelementMaxDepthTooltip = true"></div>
-                                    <modal-dialog show="DOMelementMaxDepthTooltip" dialog-title="Reduce DOM depth">
-                                        <p>A deep DOM makes the CSS matching with DOM elements difficult.</p>
-                                        <p>It also slows down Javascript modifications to the DOM because changing the dimensions of an element makes the browser re-calculate the dimensions of it's parents. Same thing for Javascript events, that bubble up to the document root.</p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.iframesCount" ng-class="{'warning': phantomasResults.metrics.iframesCount > 30}">
-                                <div class="label">Number of iframes</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.iframesCount}}
-                                </div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="DOMelementMaxDepthTooltip = true"></div>
-                                    <modal-dialog show="DOMelementMaxDepthTooltip" dialog-title="iFrames are slow">
-                                        <p>iFrames are the most complex HTML elements. They are pages, just like the main page, and the browser needs to create a new page context, which has a cost.</p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.DOMidDuplicated" ng-class="{'warning': phantomasResults.metrics.DOMidDuplicated > 20}">
-                                <div class="label">IDs duplicated</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.DOMidDuplicated}}
-                                    <show-offenders modal-title="DOM IDs duplicated" metric-name="DOMidDuplicated" phantomas-results="phantomasResults"></show-offenders>
-                                </div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="DOMidDuplicatedTooltip = true"></div>
-                                    <modal-dialog show="DOMidDuplicatedTooltip" dialog-title="Warning: duplicated DOM IDs">
-                                        <p>IDs of HTML elements must be document-wide unique. This can cause problems with getElementById returning the wrong element.</p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                        </div>
-                    </div>
-                </div>
-                <div>
-                    <div ng-class="notations.jsDomManipulations">{{notations.jsDomManipulations}}</div>
-                    <div class="notation">DOM manipulations</div>
-                    <div class="criteria">
-                        <div class="table">
-                            <div ng-if="phantomasResults.metrics.DOMinserts" ng-class="{'warning': phantomasResults.metrics.DOMinserts > 1000}">
-                                <div class="label">DOM inserts</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.DOMinserts}}
-                                    <show-offenders modal-title="DOM inserts" metric-name="DOMinserts" phantomas-results="phantomasResults"></show-offenders>
-                                </div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="DOMinsertsTooltip = true"></div>
-                                    <modal-dialog show="DOMinsertsTooltip" dialog-title="DOM insertions are slow">
-                                        <p>Working with the DOM in Javascript triggers layout calculations and slows down the page.</p>
-                                        <p>Try, as much as possible, to have an HTML page fully generated by the server instead of making changes with JS.</p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.DOMqueries" ng-class="{'warning': phantomasResults.metrics.DOMqueries > 2000}">
-                                <div class="label">DOM queries</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.DOMqueries}}
-                                </div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="DOMqueriesTooltip = true"></div>
-                                    <modal-dialog show="DOMqueriesTooltip" dialog-title="Avoid having too many DOM queries">
-                                        <p>DOM queries are like looking in a large catalog of items. Even if the browsers made progress on the performances of queries, websites often make hundreds of them.</p>
-                                        <p>Try to reduce the number of queries by refactoring your Javascript code.</p>
-                                        <p>Avoid also to have a read query between two write queries. To be able to reduce the number repaints and optimize performances, browsers buffer the DOM writing operations and treat them in bulk. But each time a DOM reading is asked, the browser needs to empty the buffer. This can be particularly slow inside a loop.</p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.DOMqueriesAvoidable" ng-class="{'warning': phantomasResults.metrics.DOMqueriesAvoidable > 500}">
-                                <div class="label">Avoidable queries</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.DOMqueriesAvoidable}}
-                                    <show-offenders modal-title="Duplicated DOM queries" metric-name="DOMqueriesDuplicated" phantomas-results="phantomasResults"></show-offenders>
-                                </div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="duplicatedQueriesCountAllTooltip = true"></div>
-                                    <modal-dialog show="duplicatedQueriesCountAllTooltip" dialog-title="Cache duplicated DOM queries">
-                                        <p>This is the number of queries that could be avoided by removing all duplicated queries.</p>
-                                        <p>Simply save the result of a query in a variable. Ok it is not always simple, especially with third-party scripts, but at least do it with your own code.</p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.eventsBound" ng-class="{'warning': phantomasResults.metrics.eventsBound > 2000}">
-                                <div class="label">Events bound</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.eventsBound}}
-                                    <show-offenders modal-title="Events bound" metric-name="eventsBound" phantomas-results="phantomasResults"></show-offenders>
-                                </div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="eventsBoundTooltip = true"></div>
-                                    <modal-dialog show="eventsBoundTooltip" dialog-title="Reduce the number of events binding">
-                                        <p>Binding too many events has a cost.</p>
-                                        <p>It can be avoided by using "event delegation". Instead of binding events on each element one by one, events delegation binds them on the top level document element and uses the bubbling principle. It will imperceptibly slow down the event when it occurs, but the loading of the page will speed-up.</p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                        </div>
-                    </div>
-                </div>
-                <div>
-                    <div ng-class="notations.jsBadPractices">{{notations.jsBadPractices}}</div>
-                    <div class="notation">Bad Javascript</div>
-                    <div class="criteria">
-                        <div class="table">
-                            <div ng-if="phantomasResults.metrics.jsErrors" ng-class="{'warning': phantomasResults.metrics.jsErrors > 5}">
-                                <div class="label">Javascript errors</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.jsErrors}}
-                                    <show-offenders modal-title="Javascript errors" metric-name="jsErrors" phantomas-results="phantomasResults"></show-offenders>
-                                </div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="jsErrorsTooltip = true"></div>
-                                    <modal-dialog show="jsErrorsTooltip" dialog-title="Javascript errors">
-                                        <p>Just to let you know there are some errors on the page.</p>
-                                        <p><b>Please note that some errors only occur in the PhantomJS browser, so you might need to double check on other browsers.</b></p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.evalCalls">
-                                <div class="label">eval calls</div>
-                                <div class="result">{{phantomasResults.metrics.evalCalls}}</div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="evalCallsTooltip = true"></div>
-                                    <modal-dialog show="evalCallsTooltip" dialog-title="Eval is evil">
-                                        <p>The 'eval' function is slow and a bad coding practice. Try to get rid of it.</p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.documentWriteCalls">
-                                <div class="label">document.write calls</div>
-                                <div class="result">{{phantomasResults.metrics.documentWriteCalls}}</div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="documentWriteCallsTooltip = true"></div>
-                                    <modal-dialog show="documentWriteCallsTooltip" dialog-title="document.write should have never existed">
-                                        <p>They slow down the page construction, especially if they are used to insert scripts in the page. Remove them ASAP.</p>
-                                        <p>If you cannot remove them because they come from a third-party script (such as ads), have a look at <a href="https://github.com/krux/postscribe" target="_blank">PostScribe</a>.</p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.consoleMessages">
-                                <div class="label">Console messages</div>
-                                <div class="result">{{phantomasResults.metrics.consoleMessages}}</div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="consoleMessagesTooltip = true"></div>
-                                    <modal-dialog show="consoleMessagesTooltip" dialog-title="Avoid using console.log()">
-                                        <p>Try to keep your console clean when in production. Debugging is good for development only.</p>
-                                        <p>Writing in the console has a cost, especially when dumping large object variables.</p>
-                                        <p>There is also a problem with Internet Explorer 8, not knowing the console object.</p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.globalVariables">
-                                <div class="label">Global variables</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.globalVariables}}
-                                    <show-offenders modal-title="Global variables" metric-name="globalVariables" phantomas-results="phantomasResults"></show-offenders>
-                                </div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="globalVariablesTooltip = true"></div>
-                                    <modal-dialog show="globalVariablesTooltip" dialog-title="Minimize the use of global variables">
-                                        <p>It is a bad practice because they clutter up the global namespace. If two scripts use the same variable name in the global scope, it can cause conflicts and it is generally hard to debug.</p>
-                                        <p>Global variables also take a (very) little bit longer to be accessed than variables in the local scope of a function.</p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                            <div ng-if="inBodyDomManipulations > 10" ng-class="{'warning': inBodyDomManipulations > 200}">
-                                <div class="label">DOM manipulations in body</div>
-                                <div class="result">
-                                    {{inBodyDomManipulations}}
-                                </div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="inBodyDomManipulationsTooltip = true"></div>
-                                    <modal-dialog show="inBodyDomManipulationsTooltip" dialog-title="Javascript executed inside the body">
-                                        <p>This metric counts the number of DOM queries, DOM inserts, binds, etc. made by the Javascript before the DOMContentLoaded event.</p>
-                                        <p>Wait for this event before manipulating the DOM. Do not execute Javascript in the middle of the BODY as it slows down the construction of the DOM and makes a poor maintainability. This is what i call spaghetti code.</p>
-                                        <p>The JS Timeline tab can help you identify what's happening.</p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                        </div>
-                    </div>
-                </div>
-                <div>
-                    <div ng-class="notations.jQueryLoading">{{notations.jQueryLoading}}</div>
-                    <div class="notation">jQuery version</div>
-                    <div class="criteria">
-                        <div class="table">
-                            <div ng-if="phantomasResults.metrics.jQueryDifferentVersions == 1">
-                                <div class="label">jQuery version</div>
-                                <div class="result">{{phantomasResults.metrics.jQueryVersion}}</div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="jQueryVersionTooltip = true"></div>
-                                    <modal-dialog show="jQueryVersionTooltip" dialog-title="Use the latest version of jQuery">
-                                        <p>Current latest versions of jQuery are 1.11 (with support for old IE versions) and 2.1 (without).</p>
-                                        <p>Each new version of jQuery optimizes performances. Do not keep an old version of jQuery. Updating can sometimes break a few things, but it is generally quite easy to fix them up. So don't hesitate.</p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.jQueryDifferentVersions > 1" class="warning">
-                                <div class="label">{{phantomasResults.metrics.jQueryDifferentVersions}} versions loaded</div>
-                                <div class="result">
-                                    <span ng-repeat="version in phantomasResults.offenders.jQueryDifferentVersions">{{version}}<span ng-show="!$last"> & </span></span>
-                                </div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="jQueryDifferentVersionsTooltip = true"></div>
-                                    <modal-dialog show="jQueryDifferentVersionsTooltip" dialog-title="Several versions of jQuery loaded">
-                                        <p>jQuery is a heavy library. You should **never** load jQuery more than one on the same page.</p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                        </div>
-                    </div>
-                </div>
-                <div>
-                    <div ng-if="!phantomasResults.metrics.cssParsingErrors" class="A">A</div>
-                    <div ng-if="phantomasResults.metrics.cssParsingErrors" class="F">F</div>
-                    <div class="notation">CSS syntax errors</div>
-                    <div ng-if="!phantomasResults.metrics.cssParsingErrors" class="criteria"></div>
-                    <div ng-if="phantomasResults.metrics.cssParsingErrors" class="criteria">
-                        <div class="table">
-                            <div class="warning">
-                                <div class="label">CSS files with syntax errors</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.cssParsingErrors}}
-                                    <show-offenders modal-title="CSS syntax errors" metric-name="cssParsingErrors" phantomas-results="phantomasResults"></show-offenders>
-                                </div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="cssParsingErrorsTooltip = true"></div>
-                                    <modal-dialog show="cssParsingErrorsTooltip" dialog-title="Parsing error while analyzing CSS">
-                                        <p>Yellow Lab Tools failed to parse a CSS file. I doubt the problem comes from the css parser.</p>
-                                        <p>
-                                            Direct links to the <b>W3C CSS Validator</b> for the following stylesheet(s):
-                                            <ul>
-                                                <li ng-repeat="stylesheet in cssW3cDirectUrls">
-                                                    <a href="{{stylesheet.w3c}}" target="_blank">{{stylesheet.url}}</a>
-                                                </li>
-                                            </ul>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                        </div>
-                    </div>
-                </div>
-                <div>
-                    <div ng-class="notations.cssComplexity">{{notations.cssComplexity}}</div>
-                    <div class="notation">CSS complexity</div>
-                    <div class="criteria">
-                        <div class="table">
-                            <div ng-if="phantomasResults.metrics.cssRules" ng-class="{'warning': phantomasResults.metrics.cssRules > 4000 || (phantomasResults.metrics.cssRules > 1000 && phantomasResults.metrics.cssRules > phantomasResults.metrics.DOMelementsCount)}">
-                                <div class="label">Rules count</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.cssRules}}
-                                </div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="cssRulesTooltip = true"></div>
-                                    <modal-dialog show="cssRulesTooltip" dialog-title="Clean up the CSS files">
-                                        <p>Having a huge number of CSS rules hurts performances. If the number of CSS rules is higher than the number of DOM elements, there is clearly a problem.</p>
-                                        <p>Huge stylesheets generally occur when the different pages of a website load all the CSS, concatenated in a single stylesheet, even if a large part of the rules are page-specific. Solution is to create one main CSS file with global rules and one custom files per page.</p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.cssComplexSelectors" ng-class="{'warning': phantomasResults.metrics.cssComplexSelectors > 2000}">
-                                <div class="label">Complex selectors</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.cssComplexSelectors}}
-                                    <show-offenders modal-title="Complex selectors" metric-name="cssComplexSelectors" phantomas-results="phantomasResults"></show-offenders>
-                                </div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="cssComplexSelectorsTooltip = true"></div>
-                                    <modal-dialog show="cssComplexSelectorsTooltip" dialog-title="Avoid complex selectors">
-                                        <p>Complex selectors are CSS selectors with 4 or more expressions, like "#header ul li .foo".</p>
-                                        <p>They are adding more work for the browser, and this could be avoided by simplifying selectors.</p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.cssComplexSelectorsByAttribute" ng-class="{'warning': phantomasResults.metrics.cssComplexSelectorsByAttribute > 100}">
-                                <div class="label">Complex attributes selector</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.cssComplexSelectorsByAttribute}}
-                                    <show-offenders modal-title="Complex attributes selector" metric-name="cssComplexSelectorsByAttribute" phantomas-results="phantomasResults"></show-offenders>
-                                </div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="cssComplexSelectorsByAttributeTooltip = true"></div>
-                                    <modal-dialog show="cssComplexSelectorsByAttributeTooltip" dialog-title="Avoid complex attribute expressions">
-                                        <p>Complex attributes selectors are one of these:
-                                            <ul>
-                                                <li>.foo[type*=bar] (contains bar)</li>
-                                                <li>.foo[type^=bar] (starts with bar)</li>
-                                                <li>.foo[type|=bar] (starts with bar or bar-)</li>
-                                                <li>.foo[type$=bar] (ends with bar)</li>
-                                                <li>.foo[type~=bar baz] (bar or baz)</li>
-                                            </ul>
-                                        </p>
-                                        <p>Their matching process needs more CPU and it has a cost on performances.</p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.cssParsingErrors">
-                                <div class="label">(<ng-pluralize count="phantomasResults.metrics.cssParsingErrors" when="{'one': '1 file', 'other': '{} files'}"></ng-pluralize> couldn't be parsed)</div>
-                                <div class="result"></div>
-                                <div class="info"></div>
-                            </div>
-                        </div>
-                    </div>
-                </div>
-                <div>
-                    <div ng-class="notations.badCss">{{notations.badCss}}</div>
-                    <div class="notation">Bad CSS</div>
-                    <div class="criteria">
-                        <div class="table">
-                            <div ng-if="phantomasResults.metrics.cssImports" class="warning">
-                                <div class="label">Uses of @import</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.cssImports}}
-                                    <show-offenders modal-title="Uses of @import" metric-name="cssImports" phantomas-results="phantomasResults"></show-offenders>
-                                </div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="cssImportsTooltip = true"></div>
-                                    <modal-dialog show="cssImportsTooltip" dialog-title="Don't use @import">
-                                        <p>It’s bad for performance to use @import because CSS files don't get downloaded in parallel.</p>
-                                        <p>You should use &lt;link rel='stylesheet' href='a.css'&gt; instead.</p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.cssDuplicatedSelectors" ng-class="{'warning': phantomasResults.metrics.cssDuplicatedSelectors > 80}">
-                                <div class="label">Duplicated selectors</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.cssDuplicatedSelectors}}
-                                    <show-offenders modal-title="Duplicated selectors" metric-name="cssDuplicatedSelectors" phantomas-results="phantomasResults"></show-offenders>
-                                </div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="cssDuplicatedSelectorsTooltip = true"></div>
-                                    <modal-dialog show="cssDuplicatedSelectorsTooltip" dialog-title="Merge duplicated selectors">
-                                        <p>This is when two or more selectors are strictly identical and should be merged.</p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.cssDuplicatedProperties" ng-class="{'warning': phantomasResults.metrics.cssDuplicatedProperties > 100}">
-                                <div class="label">Duplicated properties</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.cssDuplicatedProperties}}
-                                    <show-offenders modal-title="Duplicated properties" metric-name="cssDuplicatedProperties" phantomas-results="phantomasResults"></show-offenders>
-                                </div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="cssDuplicatedPropertiesTooltip = true"></div>
-                                    <modal-dialog show="cssDuplicatedPropertiesTooltip" dialog-title="Remove duplicated properties">
-                                        <p>This is the number of property definitions duplicated within a selector.</p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.cssEmptyRules" ng-class="{'warning': phantomasResults.metrics.cssEmptyRules > 100}">
-                                <div class="label">Empty rules</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.cssEmptyRules}}
-                                    <show-offenders modal-title="Empty rules" metric-name="cssEmptyRules" phantomas-results="phantomasResults"></show-offenders>
-                                </div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="cssEmptyRulesTooltip = true"></div>
-                                    <modal-dialog show="cssEmptyRulesTooltip" dialog-title="Eliminate empty rules">
-                                        <p>Very easy to fix.</p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.cssExpressions">
-                                <div class="label">CSS expressions</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.cssExpressions}}
-                                    <show-offenders modal-title="CSS expressions" metric-name="cssExpressions" phantomas-results="phantomasResults"></show-offenders>
-                                </div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="cssExpressionsTooltip = true"></div>
-                                    <modal-dialog show="cssExpressionsTooltip" dialog-title="Number of rules with CSS expressions">
-                                        <p>Such as: expression( document.body.clientWidth > 600 ? "600px" : "auto" )</p>
-                                        <p>This is a bad practice as it slows down browsers. There are some simpler CSS3 methods for doing this.</p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.cssImportants">
-                                <div class="label">Uses of !important</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.cssImportants}}
-                                    <show-offenders modal-title="Uses of !important" metric-name="cssImportants" phantomas-results="phantomasResults"></show-offenders>
-                                </div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="cssImportantsTooltip = true"></div>
-                                    <modal-dialog show="cssImportantsTooltip" dialog-title="Don't over-use !important">
-                                        <p>It can be useful, but only as a last resort. It is a bad practice because it overrides the normal cascading logic. The more you use !important, the more you need it again to over-override. This conducts to a poor maintainability.</p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.cssOldIEFixes">
-                                <div class="label">Old IE fixes</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.cssOldIEFixes}}
-                                    <show-offenders modal-title="Old IE fixes" metric-name="cssOldIEFixes" phantomas-results="phantomasResults"></show-offenders>
-                                </div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="cssOldIEFixesTooltip = true"></div>
-                                    <modal-dialog show="cssOldIEFixesTooltip" dialog-title="Clean up old IE fixes">
-                                        <p>What browser do you need to support? Once you've got the answer, take a look at these old rules that pollute your CSS code and remove them.</p>
-                                        <p>IE6:
-                                            <ul>
-                                                <li>* html</li>
-                                                <li>html > body (everything but IE6)</li>
-                                            </ul>
-                                        <p>
-                                        <p>IE7:
-                                            <ul>
-                                                <li><b>*</b>height: 123px;</li>
-                                                <li>height: 123px <b>!ie</b>;</li>
-                                            </ul>
-                                        <p>
-                                        <p>IE9:
-                                            <ul>
-                                                <li>-ms-filter</li>
-                                                <li>progid:DXImageTransform.Microsoft</li>
-                                            </ul>
-                                        </p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.cssOldPropertyPrefixes">
-                                <div class="label">Old prefixes</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.cssOldPropertyPrefixes}}
-                                    <show-offenders modal-title="Old prefixes" metric-name="cssOldPropertyPrefixes" phantomas-results="phantomasResults"></show-offenders>
-                                </div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="cssOldPropertyPrefixesTooltip = true"></div>
-                                    <modal-dialog show="cssOldPropertyPrefixesTooltip" dialog-title="Clean up old CSS3 prefixes">
-                                        <p>Many property prefixes such as -moz- or -webkit- are not needed anymore, or by very few people. You can remove them or replace them with the non-prefixed version. This will help reducing your stylesheets weight.</p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.cssUniversalSelectors">
-                                <div class="label">Universal selectors</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.cssUniversalSelectors}}
-                                    <show-offenders modal-title="Universal selectors" metric-name="cssUniversalSelectors" phantomas-results="phantomasResults"></show-offenders>
-                                </div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="cssUniversalSelectorsTooltip = true"></div>
-                                    <modal-dialog show="cssUniversalSelectorsTooltip" dialog-title="Avoid universal selectors">
-                                        <p>Universal selectors are the most expensive CSS selectors.</p>
-                                        <p>More informations <a href="http://perfectionkills.com/profiling-css-for-fun-and-profit-optimization-notes/" target="_blank">here</a>.</p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.cssRedundantBodySelectors">
-                                <div class="label">Redundant body selectors</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.cssRedundantBodySelectors}}
-                                    <show-offenders modal-title="Redundant body selectors" metric-name="cssRedundantBodySelectors" phantomas-results="phantomasResults"></show-offenders>
-                                </div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="cssRedundantBodySelectorsTooltip = true"></div>
-                                    <modal-dialog show="cssRedundantBodySelectorsTooltip" dialog-title="Useless redundant selectors">
-                                        <p>This is one way to remove complexity from a CSS rule. Generally, when "body" is specified in a rule it can be removed, because an element is necessarily inside the body.</p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.cssRedundantChildNodesSelectors">
-                                <div class="label">Redundant tags selectors</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.cssRedundantChildNodesSelectors}}
-                                    <show-offenders modal-title="Redundant tags selectors" metric-name="cssRedundantChildNodesSelectors" phantomas-results="phantomasResults"></show-offenders>
-                                </div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="cssRedundantChildNodesSelectorsTooltip = true"></div>
-                                    <modal-dialog show="cssRedundantChildNodesSelectorsTooltip" dialog-title="Useless redundant selectors">
-                                        <p>Some tags included inside other tags are obvious. For example, when "ul li" is specified in a rule, "ul" can be removed because the "li" element is <b>always</b> inside a "ul". Same thing for "tr td", "select option", ...</p>
-                                        <p>Lowering compexity in CSS selectors can make the page load a little faster.</p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.cssParsingErrors">
-                                <div class="label">(<ng-pluralize count="phantomasResults.metrics.cssParsingErrors" when="{'one': '1 file', 'other': '{} files'}"></ng-pluralize> couldn't be parsed)</div>
-                                <div class="result"></div>
-                                <div class="info"></div>
-                            </div>
-                        </div>
-                    </div>
-                </div>
-                <div>
-                    <div ng-class="notations.requests">{{notations.requests}}</div>
-                    <div class="notation">Requests number</div>
-                    <div class="criteria">
-                        <div class="table">
-                            <div ng-class="{'warning': phantomasResults.metrics.requests > 200}">
-                                <div class="label">Total requests</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.requests}}
-                                </div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="requestsTooltip = true"></div>
-                                    <modal-dialog show="requestsTooltip" dialog-title="Minimize the number of requests">
-                                        <p>This is one of the most important performance rule. Every request is slowing down the page loading.</p>
-                                        <p>There are several technics to reduce their number:
-                                            <ul>
-                                                <li>Concatenate JS files</li>
-                                                <li>Concatenate CSS files</li>
-                                                <li>Embed or inline small JS or CSS files in the HTML</li>
-                                                <li>Create sprites or icon fonts</li>
-                                                <li>Base64 encode small images in HTML or stylesheets</li>
-                                                <li>Use lazyloading for images</li>
-                                            </ul>
-                                        </p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.htmlCount">
-                                <div class="label">Documents</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.htmlCount}}
-                                    <show-offenders modal-title="HTML count" metric-name="htmlCount" phantomas-results="phantomasResults"></show-offenders>
-                                </div>
-                                <div></div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.jsCount" ng-class="{'warning': phantomasResults.metrics.jsCount > 40}">
-                                <div class="label">Scripts</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.jsCount}}
-                                    <show-offenders modal-title="JS count" metric-name="jsCount" phantomas-results="phantomasResults"></show-offenders>
-                                </div>
-                                <div></div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.cssCount" ng-class="{'warning': phantomasResults.metrics.cssCount > 25}">
-                                <div class="label">Stylesheets</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.cssCount}}
-                                    <show-offenders modal-title="CSS count" metric-name="cssCount" phantomas-results="phantomasResults"></show-offenders>
-                                </div>
-                                <div></div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.imageCount" ng-class="{'warning': phantomasResults.metrics.imageCount > 80}">
-                                <div class="label">Images</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.imageCount}}
-                                    <show-offenders modal-title="Image count" metric-name="imageCount" phantomas-results="phantomasResults"></show-offenders>
-                                </div>
-                                <div></div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.webfontCount" ng-class="{'warning': phantomasResults.metrics.webfontCount > 4}">
-                                <div class="label">Fonts</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.webfontCount}}
-                                    <show-offenders modal-title="Webfont count" metric-name="webfontCount" phantomas-results="phantomasResults"></show-offenders>
-                                </div>
-                                <div></div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.videoCount">
-                                <div class="label">Videos</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.videoCount}}
-                                    <show-offenders modal-title="Video count" metric-name="videoCount" phantomas-results="phantomasResults"></show-offenders>
-                                </div>
-                                <div></div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.jsonCount">
-                                <div class="label">JSON</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.jsonCount}}
-                                    <show-offenders modal-title="JSON count" metric-name="jsonCount" phantomas-results="phantomasResults"></show-offenders>
-                                </div>
-                                <div></div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.otherCount">
-                                <div class="label">Other</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.otherCount}}
-                                    <show-offenders modal-title="Other count" metric-name="otherCount" phantomas-results="phantomasResults"></show-offenders>
-                                </div>
-                                <div></div>
-                            </div>
-                        </div>
-                    </div>
-                </div>
-                <div>
-                    <div ng-class="notations.network">{{notations.network}}</div>
-                    <div class="notation">Network</div>
-                    <div class="criteria">
-                        <div class="table">
-                            <div ng-if="phantomasResults.metrics.notFound" class="warning">
-                                <div class="label">404 not found</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.notFound}}
-                                    <show-offenders modal-title="404 not found" metric-name="notFound" phantomas-results="phantomasResults"></show-offenders>
-                                </div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="notFoundTooltip = true"></div>
-                                    <modal-dialog show="notFoundTooltip" dialog-title="404 errors">
-                                        <p>404 errors are never cached, so each time a page ask for it, it hits se server. Even if it is behind a CDN or a reverse-proxy cache.</p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.closedConnections" ng-class="{'warning': phantomasResults.metrics.closedConnections > 20}">
-                                <div class="label">Connections closed</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.closedConnections}}
-                                    <show-offenders modal-title="Connections closed" metric-name="closedConnections" phantomas-results="phantomasResults"></show-offenders>
-                                </div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="closedConnectionsTooltip = true"></div>
-                                    <modal-dialog show="closedConnectionsTooltip" dialog-title="Connection keep-alive is important">
-                                        <p>This counts the number of requests not keeping the connection alive (specifying "Connection: close" in the response headers). It is only counting a request if it is followed by another request on the same domain.</p>
-                                        <p>This is slowing down the next request, because the brower needs to open a new connection to the server, which means a additional round-trip.</p>
-                                        <p>Correct the problem by setting a Keep-Alive header on the guilty server.</p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.multipleRequests" ng-class="{'warning': phantomasResults.metrics.multipleRequests > 10}">
-                                <div class="label">Duplicated requests</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.multipleRequests}}
-                                    <show-offenders modal-title="Static assets requested more than once" metric-name="multipleRequests" phantomas-results="phantomasResults"></show-offenders>
-                                </div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="multipleRequestsTooltip = true"></div>
-                                    <modal-dialog show="multipleRequestsTooltip" dialog-title="The same file is requested twice">
-                                        <p>This only happens when the asset has no cache and is requested more than once on the same page. Be very careful about it.</p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.cachingDisabled" ng-class="{'warning': phantomasResults.metrics.cachingDisabled > 25}">
-                                <div class="label">Caching disabled</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.cachingDisabled}}
-                                    <show-offenders modal-title="Caching disabled" metric-name="cachingDisabled" phantomas-results="phantomasResults"></show-offenders>
-                                </div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="cachingDisabledTooltip = true"></div>
-                                    <modal-dialog show="cachingDisabledTooltip" dialog-title="Caching disabled">
-                                        <p>Counts responses with caching disabled (max-age=0)</p>
-                                        <p>Fix immediatly if on static assets.</p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.cachingNotSpecified" ng-class="{'warning': phantomasResults.metrics.cachingNotSpecified > 50}">
-                                <div class="label">Caching not specified</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.cachingNotSpecified}}
-                                    <show-offenders modal-title="Caching not specified" metric-name="cachingNotSpecified" phantomas-results="phantomasResults"></show-offenders>
-                                </div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="cachingNotSpecifiedTooltip = true"></div>
-                                    <modal-dialog show="cachingNotSpecifiedTooltip" dialog-title="No caching header">
-                                        <p>Responses with no caching header sent (either Cache-Control or Expires).</p>
-                                        <p>Every request should have a cache time specified. If you really don't want cache, specify "max-age=0", otherwise some browsers will try to cache.</p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.cachingTooShort">
-                                <div class="label">Caching too short</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.cachingTooShort}}
-                                    <show-offenders modal-title="Caching too short" metric-name="cachingTooShort" phantomas-results="phantomasResults"></show-offenders>
-                                </div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="cachingTooShortTooltip = true"></div>
-                                    <modal-dialog show="cachingTooShortTooltip" dialog-title="Increase cache time">
-                                        <p>Responses with too short caching time (less than a week).</p>
-                                        <p>The longer you cache, the better. Add versionning to your static assets, if it's not already done, and set their cache time to one year.</p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                            <div ng-if="phantomasResults.metrics.domains" ng-class="{'warning': phantomasResults.metrics.domains > 40}">
-                                <div class="label">Different domains</div>
-                                <div class="result">
-                                    {{phantomasResults.metrics.domains}}
-                                    <show-offenders modal-title="Different domains" metric-name="domains" phantomas-results="phantomasResults"></show-offenders>
-                                </div>
-                                <div class="info">
-                                    <div class="icon-question" ng-click="domainsTooltip = true"></div>
-                                    <modal-dialog show="domainsTooltip" dialog-title="Reduce the number of domains">
-                                        <p>For each domain met, the browser needs to make a DNS look-up, which is slow. Avoid having to many different domains and the page should render faster.</p>
-                                        <p>By the way, domain sharding is not a good practice anymore.</p>
-                                    </modal-dialog>
-                                </div>
-                            </div>
-                        </div>
-                    </div>
-                </div>
-            </div>
-        </div>
-
-        <div ng-show="view == 'execution' && !phantomasResults.error" class="execution">
-            <h2>Javascript Timeline</h2>
-            <p>
-                This graph gives a quick view of when the Javascript interactions with the DOM occur during the loading of the page.
-            </p>
-            <div class="timeline">
-                <div class="chart">
-                    <div class="chartPoints">
-                        <div ng-repeat="duration in timeline track by $index"
-                             class="interval"
-                             ng-class="{
-                                        domCreation: $index * timelineIntervalDuration < phantomasResults.metrics.domInteractive,
-                                        domInteractive: $index * timelineIntervalDuration >= phantomasResults.metrics.domInteractive
-                                                        && $index * timelineIntervalDuration < phantomasResults.metrics.domContentLoaded,
-                                        domContentLoaded: $index * timelineIntervalDuration >= phantomasResults.metrics.domContentLoaded
-                                                        && $index * timelineIntervalDuration < phantomasResults.metrics.domContentLoadedEnd,
-                                        domContentLoadedEnd: $index * timelineIntervalDuration >= phantomasResults.metrics.domContentLoadedEnd
-                                                        && $index * timelineIntervalDuration < phantomasResults.metrics.domComplete,
-                                        domComplete: $index * timelineIntervalDuration >= phantomasResults.metrics.domComplete
-                             }">
-                            <div style="height: {{100 * duration / timelineMax | number: 0}}px" class="color"></div>
-                            <div class="tooltip detailsOverlay">
-                                <div>Timestamp: {{$index * timelineIntervalDuration | number: 0}} ms</div>
-                            </div>
-                        </div>
-                    </div>
-                    <div class="startTime">0 ms</div>
-                    <div class="endTime">{{endTime | number: 0}} ms</div>
-                </div>
-                <div class="legend">
-                    <div class="titles">
-                        <div class="domCreation"><div class="color"></div>DOM creation</div>
-                        <div class="domInteractive"><div class="color"></div>DOM interactive</div>
-                        <div class="domContentLoaded"><div class="color"></div>DOM content loaded event</div>
-                        <div class="domContentLoadedEnd"><div class="color"></div>Page completion</div>
-                        <div class="domComplete"><div class="color"></div>Page is complete</div>
-                    </div>
-                    <div class="tips">
-                        <div>Executing Javascript and DOM queries here is a <b>bad practice</b> and slows down the DOM construction.</div>
-                        <div>Some frameworks do things here, but it's not reliable and should be avoided.</div>
-                        <div>Also known as "document ready". This is where you should execute <b>top-priority</b> scripts, like binding action buttons or launch a video player.</div>
-                        <div>Here you can execute <b>mid-priority</b> tasks. Loading a script with createElement('script') is one way to do so.</div>
-                        <div>The page is considered loaded, it's time for low <b>priority things</b> : trackers, social plugins, easter egg...</div>
-                    </div>
-                </div>
-            </div>
-
-            <h2>Javascript Profiler</h2>
-            <p>
-                The table below shows the interactions between Javascript and the DOM. It is useful to understand what happens while the page loads.
-            </p>
-            <div class="filters">
-                <div>
-                    <input type="checkbox" ng-model="warningsFilterOn" id="warningsFilterOn" />
-                    <label for="warningsFilterOn">Show warnings only</label>
-                </div>
-                <div>
-                    <input type="checkbox" ng-model="textFilterOn" />
-                    Filter by
-                    <input type="text" ng-model="textFilter" placeholder="search..." class="textFilter" ng-change="textFilterOn = true" />
-                </div>
-            </div>
-            <div class="table">
-
-                <toto data-info="mavariable"></toto>
-                <div class="headers">
-                    <div><!-- index --></div>
-                    <div>Type</div>
-                    <div>Params</div>
-                    <div><!-- details --></div>
-                    <div>Timestamp</div>
-                </div>
-                <div ng-repeat="node in javascript.children"
-                     ng-if="(!warningsFilterOn 
-                                || (node.data.type == 'jQuery - bind' && node.data.callDetails.context.length > 5)
-                                || node.data.resultsNumber === 0
-                                || node.data.callDetails.context.length === 0
-                                || node.data.type == 'error'
-                                || node.data.type == 'jQuery version change')
-                             && (!textFilterOn 
-                                || !textFilter.length 
-                                || node.data.type.indexOf(textFilter) >= 0 
-                                || node.data.callDetails.arguments[0].indexOf(textFilter) >= 0
-                                || node.data.callDetails.arguments[1].indexOf(textFilter) >= 0
-                                || node.data.callDetails.arguments[2].indexOf(textFilter) >= 0
-                                || node.data.callDetails.arguments[3].indexOf(textFilter) >= 0)"
-                     ng-class="{
-                                    showingDetails: node.data.showDetails,
-                                    jsError: node.data.type == 'error' || node.data.type == 'jQuery version change',
-                                    windowPerformance: node.data.type == 'domInteractive' || node.data.type == 'domContentLoaded' || node.data.type == 'domContentLoadedEnd' || node.data.type == 'domComplete'
-                                }">
-                    <div class="index">{{$index + 1}}</div>
-                    <div class="type">{{node.data.type}}</div>
-
-                    <div class="value">
-                        {{node.data.callDetails.arguments[0]}}
-                        <span ng-if="node.data.callDetails.arguments.length > 1"> : {{node.data.callDetails.arguments[1]}}</span>
-                        <span ng-if="node.data.callDetails.arguments.length > 2"> : {{node.data.callDetails.arguments[2]}}</span>
-                        <span ng-if="node.data.callDetails.arguments.length > 3"> : {{node.data.callDetails.arguments[3]}}</span>
-                    </div>
-                    
-                    <div class="details">
-                        <div ng-class="{
-                                    'icon-question': !(node.data.type == 'jQuery - bind' && node.data.callDetails.context.length > 5) && node.data.resultsNumber !== 0 && node.data.callDetails.context.length !== 0,
-                                    'icon-warning': node.data.type == 'jQuery - bind' && node.data.callDetails.context.length > 5 || node.data.resultsNumber === 0 || node.data.callDetails.context.length === 0
-                                }"
-                             ng-click="onNodeDetailsClick(node)"
-                             ng-if="node.data.type != 'jQuery loaded'
-                                    && node.data.type != 'jQuery version change'
-                                    && node.data.type != 'domInteractive'
-                                    && node.data.type != 'domContentLoaded'
-                                    && node.data.type != 'domContentLoadedEnd'
-                                    && node.data.type != 'domComplete'"></div>
-                        <div class="detailsOverlay" ng-show="node.data.showDetails">
-                            <div class="closeBtn" ng-click="onNodeDetailsClick(node)">✖</div>
-
-                            <div ng-if="node.data.callDetails.context.domElement">
-                                <h4>Called on DOM element</h4>
-                                <div>{{node.data.callDetails.context.domElement}}</div>
-                            </div>
-
-                            <div ng-if="node.data.callDetails.context.length === 0">
-                                <h4>Called on 0 jQuery element</h4>
-                                <p class="advice">Useless function call, as the jQuery object is empty.</p>
-                            </div>
-
-                            <div ng-if="node.data.callDetails.context.length == 1 && node.data.callDetails.context.firstElementPath">
-                                <h4>Called on 1 jQuery element</h4>
-                                <div>{{node.data.callDetails.context.firstElementPath}}</div>
-                            </div>
-
-                            <div ng-if="node.data.callDetails.context.length > 1">
-                                <h4>Called on {{node.data.callDetails.context.length}} jQuery elements</h4>
-                                <p class="advice" ng-if="node.data.type == 'jQuery - bind' && node.data.callDetails.context.length > 5">
-                                    The .bind() method attaches the event listener to each jQuery element one by one. Using the .on() method is preferable if available (from v1.7).
-                                </p>
-                                <p ng-if="node.data.callDetails.context.firstElementPath"><b>First one is:</b> {{node.data.callDetails.context.firstElementPath}}</p>
-                            </div>
-
-                            <p class="advice" ng-if="node.data.resultsNumber === 0">
-                                The query returned 0 results. Could it be unused or dead code?
-                            </p>
-
-                            <div ng-if="node.data.parsedBacktrace">
-                                <h4>Backtrace</h4>
-                                <div class="table">
-                                    <div ng-repeat="trace in node.data.parsedBacktrace track by $index">
-                                        <div>{{trace.fnName || '(anonymous)'}}</div>
-                                        <div class="trace"><a href="{{trace.filePath}}" title="{{trace.filePath}}" target="_blank">{{trace.fileName || 'HTML'}}</a>:{{trace.line}}</div>
-                                    </div>
-                                    <div ng-if="node.data.parsedBacktrace.length == 0 && node.data.type != 'script loaded'">
-                                        <div>can't find any backtrace :/</div>
-                                    </div>
-                                </div>
-                            </div>
-
-                            <div ng-if="node.children.length > 0">
-                                <h4>Sub processes</h4>
-                                <div class="table">
-                                    <div class="headers">
-                                        <div><!-- index --></div>
-                                        <div>Type</div>
-                                        <div>Params</div>
-                                        <div>Duration</div>
-                                    </div>
-                                    <div ng-repeat="node in node.children">
-                                        <div class="index">{{$index}}</div>
-                                        <div class="type">{{node.data.type}}</div>
-                                        <div class="value">
-                                            {{node.data.callDetails.arguments[0]}}
-                                            <span ng-if="node.data.callDetails.arguments.length > 1"> : {{node.data.callDetails.arguments[1]}}</span>
-                                            <span ng-if="node.data.callDetails.arguments.length > 2"> : {{node.data.callDetails.arguments[2]}}</span>
-                                            <span ng-if="node.data.callDetails.arguments.length > 3"> : {{node.data.callDetails.arguments[3]}}</span>
-                                        </div>
-                                        <div class="duration" ng-if="node.data.time != undefined">{{node.data.time}} ms</div>
-                                        <div class="duration" ng-if="node.data.time == undefined"></div>
-                                    </div>
-                                </div>
-                            </div>
-                        </div>
-                    </div>
-                    <div class="startTime" ng-class="node.data.loadingStep">{{node.data.timestamp | number: 0}} ms</div>
-                </div>
-            </div>
-        </div>
-    </div>
-
-    <div class="footer">
-        <p><b>Yellow Lab Tools</b> is an open source project by <a href="http://www.gaelmetais.com" target="_blank">Gaël Métais</a>, based on <a href="https://github.com/macbre/phantomas" target="_blank">Phantomas</a>.<br>If you like it, <a href="https://github.com/gmetais/YellowLabTools" target="_blank" class="star">give it a <span>&#9733;</span> on GitHub</a>!</p>
-    </div>
-
-    <script>
-
-        var _phantomas_results = %%RESULTS%%;
-
-        var GA_ID = '%%GA_ID%%';
-        if (GA_ID.length > 0 && window.location.host === 'yellowlab.tools') {
-            (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
-            (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
-            m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
-            })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
-
-            ga('create', GA_ID, 'auto');
-            ga('send', 'pageview');
-        }
-    </script>
-</body>
-</html>

BIN
app/public/fonts/icons.woff


+ 0 - 5
app/public/scripts/app.js

@@ -1,5 +0,0 @@
-angular.module('YellowLabTools', [
-  'Results',
-  'ngModal',
-  'ShowOffendersDirective'
-]);

+ 0 - 417
app/public/scripts/resultsCtrl.js

@@ -1,417 +0,0 @@
-var app = angular.module('Results', []);
-
-app.controller('ResultsCtrl', function ($scope) {
-    // Grab results from nodeJS served page
-    $scope.phantomasResults = window._phantomas_results;
-
-    $scope.view = 'execution';
-
-    if ($scope.phantomasResults.metrics && $scope.phantomasResults.offenders && $scope.phantomasResults.offenders.javascriptExecutionTree) {
-
-        // Get the execution tree from the offenders
-        $scope.javascript = JSON.parse($scope.phantomasResults.offenders.javascriptExecutionTree);
-
-        // Sort globalVariables offenders alphabetically
-        if ($scope.phantomasResults.offenders.globalVariables) {
-            $scope.phantomasResults.offenders.globalVariables.sort();
-        }
-
-
-        initSummaryView();
-        initJSTimelineView();
-
-    }
-
-    $scope.setView = function(viewName) {
-        $scope.view = viewName;
-    };
-
-    $scope.onNodeDetailsClick = function(node) {
-        var isOpen = node.data.showDetails;
-        if (!isOpen) {
-            // Close all other nodes
-            $scope.javascript.children.forEach(function(currentNode) {
-                currentNode.data.showDetails = false;
-            });
-
-            // Parse the backtrace
-            if (!node.data.parsedBacktrace) {
-                node.data.parsedBacktrace = parseBacktrace(node.data.backtrace);
-            }
-
-        }
-        node.data.showDetails = !isOpen;
-    };
-
-    function initSummaryView() {
-
-        // Read the main elements of the tree and sum the total time
-        $scope.totalJSTime = 0;
-        $scope.inBodyDomManipulations = 0;
-        treeRunner($scope.javascript, function(node) {
-            if (node.data.time) {
-                $scope.totalJSTime += node.data.time;
-            }
-
-            if (node.data.timestamp < $scope.phantomasResults.metrics.domInteractive &&
-                    node.data.type !== 'jQuery - onDOMReady') {
-                $scope.inBodyDomManipulations ++;
-            }
-            
-            if (node.data.type !== 'main') {
-                // Don't check the children
-                return false;
-            }
-        });
-
-        // If there are some CSS parsing errors, prepare the W3C CSS Validator direct URLs
-        if ($scope.phantomasResults.offenders.cssParsingErrors) {
-            $scope.cssW3cDirectUrls = [];
-            $scope.phantomasResults.offenders.cssParsingErrors.forEach(function(errorString, index) {
-                var stylesheet = errorString.split(' ')[0];
-                var w3cUrl = 'http://jigsaw.w3.org/css-validator/validator?profile=css3&usermedium=all&warning=no&vextwarning=true&lang=en&uri=' + encodeURIComponent(stylesheet);
-                $scope.cssW3cDirectUrls.push({
-                    url: stylesheet,
-                    w3c: w3cUrl
-                });
-            });
-        }
-
-        // Grab the notes
-        $scope.notations = {
-            domComplexity: getDomComplexityScore(),
-            jsDomManipulations: getJsDomManipulationsScore(),
-            jsBadPractices: getJSBadPracticesScore(),
-            jQueryLoading: getJQueryLoadingScore(),
-            cssComplexity: getCSSComplexityScore(),
-            badCss: getBadCssScore(),
-            requests: requestsScore(),
-            network: networkScore()
-        };
-    }
-
-    function initJSTimelineView() {
-
-        if (!$scope.javascript.children) {
-            return;
-        }
-
-        // Read the execution tree and adjust the navigation timings (cause their not very well synchronised)
-        treeRunner($scope.javascript, function(node) {
-            switch(node.data.type) {
-                case 'domInteractive':
-                    $scope.phantomasResults.metrics.domInteractive = node.data.timestamp;
-                    break;
-                case 'domContentLoaded':
-                    $scope.phantomasResults.metrics.domContentLoaded = node.data.timestamp;
-                    break;
-                case 'domContentLoadedEnd':
-                    $scope.phantomasResults.metrics.domContentLoadedEnd = node.data.timestamp;
-                    break;
-                case 'domComplete':
-                    $scope.phantomasResults.metrics.domComplete = node.data.timestamp;
-                    break;
-            }
-
-            if (node.data.type !== 'main') {
-                // Don't check the children
-                return false;
-            }
-        });
-
-
-        // Now read the tree and display it on a timeline
-        
-        // Split the timeline into 200 intervals
-        var numberOfIntervals = 199;
-        var lastEvent = $scope.javascript.children[$scope.javascript.children.length - 1];
-        $scope.endTime =  lastEvent.data.timestamp + (lastEvent.data.time || 0);
-        $scope.timelineIntervalDuration = $scope.endTime / numberOfIntervals;
-        
-        // Pre-fill array of as many elements as there are milleseconds
-        var millisecondsArray = Array.apply(null, new Array($scope.endTime + 1)).map(Number.prototype.valueOf,0);
-        
-        // Create the milliseconds array from the execution tree
-        treeRunner($scope.javascript, function(node) {
-            
-            if (node.data.time !== undefined) {
-
-                // Ignore artefacts (durations > 100ms)
-                var time = Math.min(node.data.time, 100) || 1;
-
-                for (var i=node.data.timestamp, max=node.data.timestamp + time ; i<max ; i++) {
-                    millisecondsArray[i] |= 1;
-                }
-            }
-
-            if (node.data.type !== 'main') {
-                // Don't check the children
-                return false;
-            }
-        });
-
-        // Pre-fill array of 200 elements
-        $scope.timeline = Array.apply(null, new Array(numberOfIntervals + 1)).map(Number.prototype.valueOf,0);
-
-        // Create the timeline from the milliseconds array
-        millisecondsArray.forEach(function(value, timestamp) {
-            if (value === 1) {
-                $scope.timeline[Math.floor(timestamp / $scope.timelineIntervalDuration)] += 1;
-            }
-        });
-        
-        // Get the maximum value of the array (needed for display)
-        $scope.timelineMax = Math.max.apply(Math, $scope.timeline);
-    }
-
-
-    function getDomComplexityScore() {
-        var note = 'A';
-        var score = $scope.phantomasResults.metrics.DOMelementsCount +
-                    Math.pow($scope.phantomasResults.metrics.DOMelementMaxDepth, 2) +
-                    $scope.phantomasResults.metrics.iframesCount * 50 +
-                    $scope.phantomasResults.metrics.DOMidDuplicated * 25;
-        if (score > 1000) {
-            note = 'B';
-        }
-        if (score > 1500) {
-            note = 'C';
-        }
-        if (score > 2000) {
-            note = 'D';
-        }
-        if (score > 3000) {
-            note = 'E';
-        }
-        if (score > 4000) {
-            note = 'F';
-        }
-        return note;
-    }
-
-    function getJsDomManipulationsScore() {
-        var note = 'A';
-        var score = $scope.phantomasResults.metrics.DOMinserts * 2 +
-                    $scope.phantomasResults.metrics.DOMqueries +
-                    $scope.phantomasResults.metrics.DOMqueriesAvoidable * 2 +
-                    $scope.phantomasResults.metrics.eventsBound;
-        if (score > 300) {
-            note = 'B';
-        }
-        if (score > 500) {
-            note = 'C';
-        }
-        if (score > 700) {
-            note = 'D';
-        }
-        if (score > 1000) {
-            note = 'E';
-        }
-        if (score > 1400) {
-            note = 'F';
-        }
-        return note;
-    }
-
-    function getJSBadPracticesScore() {
-        var note = 'A';
-        var score = $scope.phantomasResults.metrics.documentWriteCalls * 3 +
-                    $scope.phantomasResults.metrics.evalCalls * 2 +
-                    $scope.phantomasResults.metrics.jsErrors * 10 +
-                    $scope.phantomasResults.metrics.consoleMessages / 2 +
-                    $scope.phantomasResults.metrics.globalVariables / 20 +
-                    Math.sqrt($scope.inBodyDomManipulations);
-        if (score > 10) {
-            note = 'B';
-        }
-        if (score > 15) {
-            note = 'C';
-        }
-        if (score > 20) {
-            note = 'D';
-        }
-        if (score > 30) {
-            note = 'E';
-        }
-        if (score > 45) {
-            note = 'F';
-        }
-        return note;
-    }
-
-    function getJQueryLoadingScore() {
-        var note = 'NA';
-        if ($scope.phantomasResults.metrics.jQueryDifferentVersions > 1) {
-            note = 'F';
-        } else if ($scope.phantomasResults.metrics.jQueryVersion) {
-            if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.11.') === 0 ||
-                $scope.phantomasResults.metrics.jQueryVersion.indexOf('1.12.') === 0 ||
-                $scope.phantomasResults.metrics.jQueryVersion.indexOf('2.1.') === 0 ||
-                $scope.phantomasResults.metrics.jQueryVersion.indexOf('2.2.') === 0) {
-                note = 'A';
-            } else if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.9.') === 0 ||
-                       $scope.phantomasResults.metrics.jQueryVersion.indexOf('1.10.') === 0 ||
-                       $scope.phantomasResults.metrics.jQueryVersion.indexOf('2.0.') === 0) {
-                note = 'B';
-            } else if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.7.') === 0 ||
-                       $scope.phantomasResults.metrics.jQueryVersion.indexOf('1.8.') === 0) {
-                note = 'C';
-            } else if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.5.') === 0 ||
-                       $scope.phantomasResults.metrics.jQueryVersion.indexOf('1.6.') === 0) {
-                note = 'D';
-            } else if ($scope.phantomasResults.metrics.jQueryVersion.indexOf('1.2.') === 0 ||
-                       $scope.phantomasResults.metrics.jQueryVersion.indexOf('1.3.') === 0 ||
-                       $scope.phantomasResults.metrics.jQueryVersion.indexOf('1.4.') === 0) {
-                note = 'E';
-            }
-        }
-        return note;
-    }
-
-    function getCSSComplexityScore() {
-        if (!$scope.phantomasResults.metrics.cssRules) {
-            return 'NA';
-        }
-
-        var note = 'A';
-        var score = $scope.phantomasResults.metrics.cssRules +
-                    $scope.phantomasResults.metrics.cssComplexSelectors * 5 +
-                    $scope.phantomasResults.metrics.cssComplexSelectorsByAttribute * 10;
-        if (score > 800) {
-            note = 'B';
-        }
-        if (score > 1200) {
-            note = 'C';
-        }
-        if (score > 2500) {
-            note = 'D';
-        }
-        if (score > 4000) {
-            note = 'E';
-        }
-        if (score > 6000) {
-            note = 'F';
-        }
-        return note;
-    }
-
-    function getBadCssScore() {
-        if (!$scope.phantomasResults.metrics.cssRules) {
-            return 'NA';
-        }
-
-        var note = 'A';
-        var score = $scope.phantomasResults.metrics.cssDuplicatedSelectors +
-                    $scope.phantomasResults.metrics.cssDuplicatedProperties +
-                    $scope.phantomasResults.metrics.cssEmptyRules +
-                    $scope.phantomasResults.metrics.cssExpressions * 10 +
-                    $scope.phantomasResults.metrics.cssImportants * 2 +
-                    $scope.phantomasResults.metrics.cssOldIEFixes * 10 +
-                    $scope.phantomasResults.metrics.cssOldPropertyPrefixes +
-                    $scope.phantomasResults.metrics.cssUniversalSelectors * 5 +
-                    $scope.phantomasResults.metrics.cssRedundantBodySelectors * 0.5 +
-                    $scope.phantomasResults.metrics.cssRedundantChildNodesSelectors * 0.5 +
-                    $scope.phantomasResults.metrics.cssImports * 50;
-        if (score > 50) {
-            note = 'B';
-        }
-        if (score > 100) {
-            note = 'C';
-        }
-        if (score > 200) {
-            note = 'D';
-        }
-        if (score > 500) {
-            note = 'E';
-        }
-        if (score > 1000) {
-            note = 'F';
-        }
-        return note;
-    }
-
-    function requestsScore() {
-        var note = 'A';
-        var score = $scope.phantomasResults.metrics.requests;
-        if (score > 30) {
-            note = 'B';
-        }
-        if (score > 45) {
-            note = 'C';
-        }
-        if (score > 60) {
-            note = 'D';
-        }
-        if (score > 80) {
-            note = 'E';
-        }
-        if (score > 100) {
-            note = 'F';
-        }
-        return note;
-    }
-
-    function networkScore() {
-        var note = 'A';
-        var score = $scope.phantomasResults.metrics.notFound * 25 +
-                    $scope.phantomasResults.metrics.closedConnections * 10 +
-                    $scope.phantomasResults.metrics.multipleRequests * 10 +
-                    $scope.phantomasResults.metrics.cachingDisabled * 2 +
-                    $scope.phantomasResults.metrics.cachingNotSpecified +
-                    $scope.phantomasResults.metrics.cachingTooShort / 2 +
-                    $scope.phantomasResults.metrics.domains;
-        if (score > 20) {
-            note = 'B';
-        }
-        if (score > 40) {
-            note = 'C';
-        }
-        if (score > 60) {
-            note = 'D';
-        }
-        if (score > 80) {
-            note = 'E';
-        }
-        if (score > 100) {
-            note = 'F';
-        }
-        return note;
-    }
-
-
-
-    function parseBacktrace(str) {
-        if (!str) {
-            return null;
-        }
-
-        var out = [];
-        var splited = str.split(' / ');
-        splited.forEach(function(trace) {
-            var result = /^(\S*)\s?\(?(https?:\/\/\S+):(\d+)\)?$/g.exec(trace);
-            if (result && result[2].length > 0) {
-                var filePath = result[2];
-                var chunks = filePath.split('/');
-                var fileName = chunks[chunks.length - 1];
-
-                out.push({
-                    fnName: result[1],
-                    fileName: fileName,
-                    filePath: filePath,
-                    line: result[3]
-                });
-            }
-        });
-        return out;
-    }
-
-    // Goes on every node of the tree and calls the function fn. If fn returns false on a node, its children won't be checked.
-    function treeRunner(node, fn) {
-        if (fn(node) !== false && node.children) {
-            node.children.forEach(function(child) {
-                treeRunner(child, fn);
-            });
-        }
-    }
-
-});

+ 0 - 17
app/public/scripts/showOffenders.js

@@ -1,17 +0,0 @@
-var app = angular.module("ShowOffendersDirective", []);
-
-app.directive('showOffenders', function() {
-    return {
-        restrict: 'E',
-        transclude: true,
-        scope: {
-            modalTitle: "@",
-            metricName: "@",
-            phantomasResults: "="
-        },
-        controller: function($scope, $element, $attrs, $location) {
-            $scope.dialogShown = false;
-        },
-        template: '&nbsp;<span ng-click="dialogShown = true" class="icon-eye" title="See offenders"></span><modal-dialog show="dialogShown" dialog-title="{{modalTitle}}: {{phantomasResults.metrics[metricName]}}" width="70%"><ul><li ng-repeat="offender in phantomasResults.offenders[metricName] track by $index">{{offender}}</li></ul></modal-dialog>'
-    };
-});

+ 0 - 25
app/public/styles/launchTest.css

@@ -1,25 +0,0 @@
-#status {
-  margin-top: 2em;
-  font-size: 2.5em;
-}
-@-webkit-keyframes rotating {
-  from {
-    -webkit-transform: rotate(0deg);
-  }
-  to {
-    -webkit-transform: rotate(360deg);
-  }
-}
-@keyframes rotating {
-  from {
-    transform: rotate(0deg);
-  }
-  to {
-    transform: rotate(360deg);
-  }
-}
-.waiting .icon-lab {
-  -webkit-animation: rotating 3s linear 0s infinite;
-  -webkit-transform: translateZ(0);
-  animation: rotating 3s linear 0s infinite;
-}

+ 0 - 19
app/public/styles/less/launchTest.less

@@ -1,19 +0,0 @@
-#status {
-    margin-top: 2em;
-    font-size: 2.5em;
-}
-
-@-webkit-keyframes rotating {
-    from { -webkit-transform: rotate(0deg); }
-    to { -webkit-transform: rotate(360deg); }
-}
-@keyframes rotating {
-    from { transform: rotate(0deg); }
-    to { transform: rotate(360deg); }
-}
-
-.waiting .icon-lab {
-    -webkit-animation: rotating 3s linear 0s infinite;
-    -webkit-transform: translateZ(0);
-    animation: rotating 3s linear 0s infinite;
-}

+ 0 - 66
app/public/styles/less/main.less

@@ -1,66 +0,0 @@
-@import "icons.less";
-
-html {
-    margin: 100px 50px;
-}
-
-body {
-    margin: 0 auto;
-    max-width: 1280px;
-    background: #9c4274;
-    color: #fff;
-    font-size: 16px;
-    text-align: center;
-}
-
-body, input[type=submit], input[type=text], input[type=url], input[type=number], button {
-    font-family: 'Century Gothic', helvetica, arial, sans-serif;
-}
-
-input[type=submit] {
-    cursor: pointer;
-}
-
-h1 span {
-    display: inline-block;
-    height: 1em;
-    width: 1em;
-    color: #ffa319;
-}
-
-.footer {
-    padding: 3em;
-    color: #4e1836;
-    a {
-        color: inherit;
-    }
-    .star {
-        font-weight: bold;
-        span {
-            font-size: 1.2em;
-        }
-    }
-}
-
-/* Icons */
-.icon-lab {
-    .icon(@lab);
-}
-.icon-question {
-    .icon(@question);
-}
-.icon-warning {
-    .icon(@warning);
-}
-.icon-back {
-    .icon(@arrow-left3);
-}
-.icon-summary {
-    .icon(@list);
-}
-.icon-spaghetti {
-    .icon(@bars);
-}
-.icon-eye {
-    .icon(@eye);
-}

+ 35 - 0
bin/cli.js

@@ -0,0 +1,35 @@
+#!/usr/bin/env node
+
+var debug = require('debug')('ylt:cli');
+
+var ylt = require('../lib/index');
+
+// Check parameters
+if (process.argv.length !== 3) {
+    console.error('Incorrect parameters');
+    console.error('\nUsage: ylt <pageUrl>\n');
+    process.exit(1);
+}
+
+var url = process.argv[2];
+
+(function execute(url) {
+    'use strict';
+
+    ylt(url).
+
+        then(function(data) {
+
+            debug('Success');
+            console.log(JSON.stringify(data, null, 2));
+
+        }).fail(function(err) {
+            
+            debug('Test failed for %s', url);
+            console.error(err);
+            
+        });
+
+    debug('Test launched...');
+
+})(url);

+ 38 - 0
bin/server.js

@@ -0,0 +1,38 @@
+var express                 = require('express');
+var app                     = express();
+var server                  = require('http').createServer(app);
+var bodyParser              = require('body-parser');
+var compress                = require('compression');
+var cors                    = require('cors');
+
+var authMiddleware          = require('../lib/server/middlewares/authMiddleware');
+var apiLimitsMiddleware     = require('../lib/server/middlewares/apiLimitsMiddleware');
+
+
+// Middlewares
+app.use(compress());
+app.use(bodyParser.json());
+app.use(cors());
+app.use(authMiddleware);
+app.use(apiLimitsMiddleware);
+
+
+// Initialize the controllers
+var apiController           = require('../lib/server/controllers/apiController')(app);
+var frontController         = require('../lib/server/controllers/frontController')(app);
+
+
+// Let's start the server!
+if (!process.env.GRUNTED) {
+    var settings = require('../server_config/settings.json');
+    server.listen(settings.serverPort, function() {
+        console.log('Listening on port %d', server.address().port);
+
+        // For the tests
+        if (server.startTests) {
+            server.startTests();
+        }
+    });
+}
+
+module.exports = app;

+ 3 - 2
bower.json

@@ -1,7 +1,8 @@
 {
   "name": "yellowlabtools",
   "dependencies": {
-    "angular": "~1.3.1",
-    "ngModal": "git://github.com/gmetais/ngModal.git#1.2.3"
+    "angular": "~1.3.8",
+    "angular-route": "~1.3.8",
+    "angular-resource": "~1.3.7"
   }
 }

+ 10 - 0
front/src/css/about.css

@@ -0,0 +1,10 @@
+.about {
+  margin: 3em auto;
+  width: 50%;
+}
+.about p {
+  margin: 2em;
+}
+.about a {
+  color: #FFF;
+}

+ 167 - 0
front/src/css/dashboard.css

@@ -0,0 +1,167 @@
+.testedUrl {
+  color: inherit;
+}
+.summary {
+  text-align: center;
+}
+.summary .globalScore {
+  margin-bottom: 3em;
+}
+.summary .globalScore .globalGrade {
+  margin: 0.5 auto;
+  width: 2.5em;
+  height: 2.5em;
+  line-height: 2.5em;
+  border-radius: 0.5em;
+  font-size: 3em;
+  font-weight: bold;
+  vertical-align: middle;
+}
+.summary .globalScore .on100 {
+  font-size: 1.2em;
+  font-weight: bold;
+  margin: 0.5em 0 1em;
+}
+.summary .notations {
+  display: table;
+  width: 80%;
+  margin: 0 10% 1.5em;
+  border-spacing: 1em;
+}
+.summary .notations > div {
+  display: table-row;
+}
+.summary .notations > div > div {
+  display: table-cell;
+  height: 2.5em;
+  vertical-align: middle;
+}
+.summary .notations .category {
+  font-weight: bold;
+  text-align: center;
+  width: 20%;
+}
+.summary .notations .criteria {
+  font-weight: normal;
+  width: 75%;
+}
+.summary .notations .A.categoryScore,
+.summary .notations .B.categoryScore,
+.summary .notations .C.categoryScore,
+.summary .notations .D.categoryScore,
+.summary .notations .E.categoryScore,
+.summary .notations .F.categoryScore,
+.summary .notations .NA.categoryScore {
+  width: 2.5em;
+  max-width: 2.5em;
+  min-width: 2.5em;
+  font-size: 2em;
+  text-align: center;
+  border-radius: 0.5em;
+  font-weight: bold;
+}
+.summary .notations .grade .A,
+.summary .notations .grade .B,
+.summary .notations .grade .C,
+.summary .notations .grade .D,
+.summary .notations .grade .E,
+.summary .notations .grade .F,
+.summary .notations .grade .NA {
+  width: 1em;
+  height: 1em;
+  font-size: 1em;
+  color: transparent;
+  margin: 0 auto;
+  border-radius: 0.5em;
+}
+.summary .notations .criteria .table {
+  width: 100%;
+}
+.summary .notations .criteria .table > div:hover > div {
+  background: #EBD8E2;
+  cursor: pointer;
+}
+.summary .notations .criteria .table > div:hover > div.info {
+  background: #FFF;
+}
+.summary .notations .criteria .table > div:hover > div.info .icon-question {
+  color: #EBD8E2;
+}
+.summary .notations .criteria .grade {
+  width: 10%;
+  padding-left: 0.5em;
+  padding-right: 0.5em;
+  vertical-align: middle;
+}
+.summary .notations .criteria .label {
+  width: 70%;
+}
+.summary .notations .criteria .result {
+  width: 18%;
+  font-weight: bold;
+  white-space: nowrap;
+  text-align: center;
+  vertical-align: middle;
+}
+.summary .notations .warning .label,
+.summary .notations .warning .result,
+.summary .notations .icon-warning {
+  color: #FF1919;
+}
+.summary .notations .criteria .info {
+  width: 2%;
+  text-align: center;
+  vertical-align: middle;
+  background: #FFF;
+  padding-left: 0.1em;
+  padding-right: 0.1em;
+}
+.summary .notations .criteria .icon-question {
+  color: transparent;
+}
+.summary .fromShare {
+  margin-bottom: 3em;
+}
+.summary .fromShare a {
+  font-size: 1em;
+  padding: 0.3em 0.5em;
+  margin: 0.5em;
+  line-height: 2em;
+  border: 0 solid;
+  border-radius: 0.5em;
+  box-shadow: 0.1em 0.2em 0 0 #5e2846;
+  background: #e74c3c;
+  color: #fff;
+  text-decoration: none;
+}
+.summary .apiTip {
+  font-size: 0.8em;
+  margin-bottom: 4em;
+  color: #413;
+}
+.summary .apiTip a {
+  color: inherit;
+}
+.summary .tweet .tweetText {
+  color: #413;
+  background: #F2F2F2;
+  border: none;
+  width: 25em;
+  padding: 0.4em;
+  border-radius: 0.5em;
+  box-shadow: 0.05em 0.1em 0 0 #999;
+}
+.summary .tweet .tweetButton,
+.summary .tweet .linkedinButton {
+  color: #413;
+  background: #F2F2F2;
+  margin-right: 0;
+}
+.summary .tweet .tweetButton:hover,
+.summary .tweet .linkedinButton:hover {
+  color: #F2F2F2;
+  background: #e74c3c;
+}
+.summary .tweet input {
+  font-size: 0.9em;
+}

+ 6 - 0
front/src/css/icons.css

@@ -0,0 +1,6 @@
+@font-face {
+  font-family: "fontsmith-icons";
+  src: url("/fonts/icons.woff") format("woff");
+  font-weight: normal;
+  font-style: normal;
+}

+ 2 - 10
app/public/styles/index.css → front/src/css/index.css

@@ -10,16 +10,8 @@
   background: #e74c3c;
   color: #fff;
 }
-.readings {
-  margin-top: 5em;
-  font-size: 0.8em;
-}
-.readings a {
-  color: inherit;
-  line-height: 1.7em;
-}
-.version {
-  font-size: 0.7em;
+.launchBtn.disabled {
+  background: #deaca6;
 }
 input[type=submit],
 input.url {

+ 96 - 12
app/public/styles/main.css → front/src/css/main.css

@@ -1,6 +1,6 @@
 @font-face {
   font-family: "fontsmith-icons";
-  src: url("/public/fonts/icons.woff") format("woff");
+  src: url("/fonts/icons.woff") format("woff");
   font-weight: normal;
   font-style: normal;
 }
@@ -32,18 +32,102 @@ h1 span {
   width: 1em;
   color: #ffa319;
 }
+.resultsMenu {
+  margin-top: 2em;
+}
+.resultsMenu .menuItem {
+  display: inline-block;
+  margin: 1em;
+  width: 8em;
+  height: 7em;
+  color: #fff;
+  border: 3px solid #fff;
+  border-radius: 0.5em;
+  cursor: pointer;
+  text-decoration: none;
+}
+.resultsMenu .menuItem.back,
+.resultsMenu .menuItem.restart {
+  color: #413;
+  border-color: #413;
+}
+.resultsMenu .menuItem div {
+  padding-top: 0.5em;
+  font-size: 3em;
+}
+.resultsMenu .active,
+.resultsMenu .menuItem.active:hover {
+  color: #ffa319;
+  border-color: #ffa319;
+}
+.resultsMenu .menuItem:hover {
+  color: #ffa319;
+}
+.resultsMenu span {
+  position: relative;
+  top: 0.5em;
+}
+/* Grade colors */
+.A {
+  /* green */
+  background: #0C4;
+}
+.B {
+  /* green */
+  background: #CD0;
+}
+.C {
+  /* yellow */
+  background: #FD2;
+}
+.D {
+  /* orange */
+  background: #FA2;
+}
+.E {
+  /* red */
+  background: #F60;
+}
+.F {
+  /* red */
+  background: #F22;
+}
+.NA {
+  /* Non applicable */
+  background: #CCC;
+}
+.board {
+  margin-top: 2em;
+  padding: 1em;
+  background: #fff;
+  color: #000;
+  border-radius: 0.5em;
+  text-align: left;
+}
+.backToDashboard {
+  text-align: center;
+}
+.backToDashboard a {
+  font-size: 0.9em;
+  display: block;
+  margin-top: 4em;
+  color: black;
+}
+.star {
+  font-weight: bold;
+}
+.star span {
+  font-size: 1.2em;
+}
 .footer {
   padding: 3em;
-  color: #4e1836;
+  color: #413;
 }
 .footer a {
   color: inherit;
 }
-.footer .star {
-  font-weight: bold;
-}
-.footer .star span {
-  font-size: 1.2em;
+.footer .version {
+  font-size: 0.7em;
 }
 /* Icons */
 .icon-lab {
@@ -57,7 +141,7 @@ h1 span {
   -webkit-font-smoothing: antialiased;
 }
 .icon-lab:before {
-  content: "\e003";
+  content: "\e004";
 }
 .icon-question {
   font-family: "fontsmith-icons";
@@ -109,7 +193,7 @@ h1 span {
   -webkit-font-smoothing: antialiased;
 }
 .icon-summary:before {
-  content: "\e002";
+  content: "\e003";
 }
 .icon-spaghetti {
   font-family: "fontsmith-icons";
@@ -124,7 +208,7 @@ h1 span {
 .icon-spaghetti:before {
   content: "\e005";
 }
-.icon-eye {
+.icon-loop {
   font-family: "fontsmith-icons";
   speak: none;
   font-style: normal;
@@ -134,6 +218,6 @@ h1 span {
   line-height: 1;
   -webkit-font-smoothing: antialiased;
 }
-.icon-eye:before {
-  content: "\e004";
+.icon-loop:before {
+  content: "\e002";
 }

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

@@ -0,0 +1,11 @@
+.status {
+  margin-top: 2em;
+  font-size: 2.5em;
+}
+.statusSubMessage {
+  font-size: 0.8em;
+  margin-bottom: 6em;
+}
+.queueLink {
+  color: #FFF;
+}

+ 84 - 0
front/src/css/rule.css

@@ -0,0 +1,84 @@
+.rule.board {
+  text-align: center;
+}
+.rule .ruleTable {
+  display: table;
+  border-spacing: 1em;
+  width: 90%;
+  margin: 2em auto;
+  background: #f2f2f2;
+  border: 1px dashed #666;
+  border-radius: 0.5em;
+}
+.rule .ruleTable > div {
+  display: table-cell;
+  vertical-align: middle;
+}
+.rule .ruleTable .left {
+  width: 33%;
+  font-weight: bold;
+}
+.rule .ruleTable .right {
+  width: 67%;
+}
+.rule .score {
+  font-size: 2.5em;
+  line-height: 2em;
+  height: 2em;
+  width: 2em;
+  border-radius: 0.5em;
+  margin: 0 auto 0.5em;
+}
+.rule .message {
+  width: 80%;
+  margin: 0 auto;
+}
+.rule .message p {
+  margin: 0.5em;
+}
+.rule .message ul {
+  list-style-type: none;
+  padding-left: 0;
+}
+.rule .message li:before {
+  content: '\25e6';
+  margin-right: 0.3em;
+  font-size: 1.2em;
+  position: relative;
+  top: 0.1em;
+}
+.rule .warning {
+  width: 90%;
+  margin: -1em auto 2em;
+  background: #FEE;
+  border: 1px dashed #e74c3c;
+  color: #e74c3c;
+  border-radius: 0.5em;
+}
+.rule .offendersTable {
+  display: table;
+  border-spacing: 0 0.25em;
+  margin: 0 auto;
+  min-width: 10%;
+  max-width: 90%;
+}
+.rule .offendersTable > div {
+  display: table-row;
+}
+.rule .offendersTable > div > div {
+  display: table-cell;
+  background: #f2f2f2;
+  padding: 0 1em;
+  word-wrap: break-word;
+  word-break: break-all;
+}
+.rule .offendersTable > div > div:hover {
+  background: #EBD8E2;
+}
+.rule .notFound {
+  font-size: 1em;
+}
+.rule .notFound h2 {
+  font-size: 3em;
+  margin-bottom: 1em;
+}

+ 8 - 224
app/public/styles/results.css → front/src/css/timeline.css

@@ -1,144 +1,7 @@
 /* Timeline colors, related to Window Performances */
-.resultsMenu {
-  margin-top: 2em;
-}
-.resultsMenu .menuItem {
-  display: inline-block;
-  margin: 1em;
-  width: 8em;
-  height: 7em;
-  color: #fff;
-  border: 3px solid #fff;
-  border-radius: 0.5em;
-  cursor: pointer;
-  text-decoration: none;
-}
-.resultsMenu .back {
-  color: #5e2846;
-  border-color: #5e2846;
-}
-.resultsMenu .menuItem div {
-  padding-top: 0.5em;
-  font-size: 3em;
-}
-.resultsMenu .active,
-.resultsMenu .menuItem.active:hover {
-  color: #ffa319;
-  border-color: #ffa319;
-}
-.resultsMenu .menuItem:hover {
-  color: #ffa319;
-}
-.resultsMenu span {
-  position: relative;
-  top: 0.5em;
-}
-.testedUrl {
-  color: inherit;
-}
-h4 {
-  margin-bottom: 0.5em;
-}
-.summary,
-.metrics,
 .execution {
-  margin-top: 2em;
-  padding: 1em;
-  background: #fff;
-  color: #000;
-  border-radius: 0.5em;
-  text-align: left;
-}
-.notations {
-  display: table;
-  width: 90%;
-  margin: 0 10%;
-  border-spacing: 1em;
-}
-.notations > div {
-  display: table-row;
-}
-.notations > div > div {
-  display: table-cell;
-  height: 2.5em;
-  vertical-align: middle;
-}
-.notations .notation {
-  font-weight: bold;
-  text-align: center;
-}
-.notations .criteria {
-  font-weight: normal;
-}
-.notations .A,
-.notations .B,
-.notations .C,
-.notations .D,
-.notations .E,
-.notations .F,
-.notations .NA {
-  width: 2.5em;
-  font-size: 2em;
-  text-align: center;
-  border-radius: 0.5em;
-  font-weight: bold;
-}
-.notations .A {
-  /* green */
-  background: #00DB61;
-}
-.notations .B {
-  /* green */
-  background: #CAD63D;
-}
-.notations .C {
-  /* yellow */
-  background: #FFD119;
-}
-.notations .D {
-  /* orange */
-  background: #FFA319;
-}
-.notations .E {
-  /* red */
-  background: #FF6600;
-}
-.notations .F {
-  /* red */
-  background: #FF1919;
-}
-.notations .NA {
-  /* Non applicable */
-  background: #CCC;
-}
-.notations .icon-eye {
-  color: #9c4274;
-  cursor: pointer;
-}
-.notations .criteria .table {
-  width: 75%;
-}
-.notations .criteria .label {
-  width: 70%;
-}
-.notations .criteria .result {
-  width: 20%;
-  font-weight: bold;
-  white-space: nowrap;
-  text-align: center;
-}
-.notations .warning .label,
-.notations .warning .result {
-  color: #FF1919;
-}
-.notations .criteria .info {
-  width: 10%;
   text-align: center;
 }
-.notations .criteria .icon-question {
-  color: #f1c40f;
-  cursor: pointer;
-}
 .timeline {
   margin: 2em 0 5em;
 }
@@ -256,31 +119,13 @@ h4 {
   width: 1.5em;
   border-radius: 0.2em;
 }
-.metrics h4 {
-  padding-left: 2em;
-}
-.metrics .module {
-  padding-left: 4em;
-  padding-top: 0.5em;
-}
-.metrics .legend {
-  font-style: italic;
-  color: #aaa;
-}
-.metrics .offenders {
-  padding-left: 0em;
-  font-size: 0.8em;
-}
-.metrics .offenders div {
-  cursor: pointer;
-}
-.metrics .offenders ul {
-  margin-top: 0.5em;
-}
 .filters {
-  margin: 1em 0;
+  margin: 1em auto;
   padding: 0.5em;
+  min-width: 30em;
+  width: 30%;
   border: 1px dotted #aaa;
+  text-align: left;
 }
 .slowRequestsLimit {
   width: 3em;
@@ -343,8 +188,9 @@ input.textFilter {
   color: #f1c40f;
   cursor: pointer;
 }
-.table .details .icon-warning {
-  cursor: pointer;
+.table .icon-warning {
+  display: inline-block;
+  width: 0.8em;
 }
 .detailsOverlay {
   position: absolute;
@@ -397,69 +243,7 @@ input.textFilter {
 .table > div > .startTime.domCreation {
   background: #ffe0cc;
 }
-.table .icon-warning {
+.execution .icon-warning {
   color: #e74c3c;
-}
-/**** NgModal popin (have a look inside bower_components) ****/
-.ng-modal {
-  position: fixed;
-  z-index: 9999;
-  top: 0;
-  left: 0;
-  width: 100%;
-  height: 100%;
-  text-align: left;
-}
-.ng-modal-overlay {
-  position: absolute;
-  z-index: 9999;
-  top: 0;
-  left: 0;
-  width: 100%;
-  height: 100%;
-  background-color: #000;
-  opacity: 0.5;
-}
-.ng-modal-dialog {
-  z-index: 10000;
-  position: absolute;
-  top: 50%;
-  left: 50%;
-  width: 50%;
-  transform: translate(-50%, -50%);
-  -webkit-transform: translate(-50%, -50%);
-  background-color: #fff;
-  padding: 10px;
-  border: 3px solid #f1c40f;
-  border-radius: 0.5em;
-  color: #000;
-}
-.ng-modal-dialog-content {
-  overflow-x: hidden;
-  overflow-y: scroll;
-  word-wrap: break-word;
-  max-height: 20em;
-  font-weight: normal;
-  white-space: normal;
-}
-.ng-modal-close {
-  position: absolute;
-  top: 3px;
-  right: 5px;
   cursor: pointer;
-  font-size: 120%;
-  padding: 5px;
-  display: inline-block;
-}
-.ng-modal-close-x {
-  font-weight: bold;
-  font-family: Arial, sans-serif;
-}
-.ng-modal-title {
-  font-weight: bold;
-  font-size: 1.5em;
-  display: block;
-  margin-bottom: 10px;
-  padding-bottom: 7px;
-  border-bottom: solid 1px #999;
 }

BIN
front/src/fonts/icons.woff


+ 0 - 0
app/public/fonts/svg-icons/arrow-left3.svg → front/src/fonts/svg-icons/arrow-left3.svg


+ 0 - 0
app/public/fonts/svg-icons/bars.svg → front/src/fonts/svg-icons/bars.svg


+ 0 - 0
app/public/fonts/svg-icons/lab.svg → front/src/fonts/svg-icons/lab.svg


+ 0 - 0
app/public/fonts/svg-icons/list.svg → front/src/fonts/svg-icons/list.svg


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
front/src/fonts/svg-icons/loop.svg


+ 0 - 0
app/public/fonts/svg-icons/question.svg → front/src/fonts/svg-icons/question.svg


+ 0 - 0
app/public/fonts/svg-icons/warning.svg → front/src/fonts/svg-icons/warning.svg


+ 0 - 0
app/public/img/favicon.png → front/src/img/favicon.png


+ 0 - 0
app/public/img/logo-large.png → front/src/img/logo-large.png


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

@@ -0,0 +1,58 @@
+var yltApp = angular.module('YellowLabTools', [
+    'ngRoute',
+    'indexCtrl',
+    'aboutCtrl',
+    'dashboardCtrl',
+    'queueCtrl',
+    'ruleCtrl',
+    'timelineCtrl',
+    'runsFactory',
+    'resultsFactory',
+    'menuService',
+    'gradeDirective',
+]);
+
+yltApp.run(['$rootScope', '$location', function($rootScope, $location) {
+    $rootScope.loadedRunId = null;
+
+    // Google Analytics
+    $rootScope.$on('$routeChangeSuccess', function(){
+        ga('send', 'pageview', {'page': $location.path()});
+    });
+}]);
+
+yltApp.config(['$routeProvider', '$locationProvider',
+    function($routeProvider, $locationProvider) {
+        $routeProvider.
+            when('/', {
+                templateUrl: 'views/index.html',
+                controller: 'IndexCtrl'
+            }).
+            when('/queue/:runId', {
+                templateUrl: 'views/queue.html',
+                controller: 'QueueCtrl'
+            }).
+            when('/about', {
+                templateUrl: 'views/about.html',
+                controller: 'AboutCtrl'
+            }).
+            when('/result/:runId', {
+                templateUrl: 'views/dashboard.html',
+                controller: 'DashboardCtrl'
+            }).
+            when('/result/:runId/timeline', {
+                templateUrl: 'views/timeline.html',
+                controller: 'TimelineCtrl'
+            }).
+            when('/result/:runId/rule/:policy', {
+                templateUrl: 'views/rule.html',
+                controller: 'RuleCtrl'
+            }).
+            otherwise({
+                redirectTo: '/'
+            });
+            
+            $locationProvider.html5Mode(true);
+    }
+]);
+

+ 5 - 0
front/src/js/controllers/aboutCtrl.js

@@ -0,0 +1,5 @@
+var aboutCtrl = angular.module('aboutCtrl', []);
+
+aboutCtrl.controller('AboutCtrl', ['$scope', function($scope) {
+    $scope.about = "this is about YLT";
+}]);

+ 73 - 0
front/src/js/controllers/dashboardCtrl.js

@@ -0,0 +1,73 @@
+var dashboardCtrl = angular.module('dashboardCtrl', ['resultsFactory', 'menuService']);
+
+dashboardCtrl.controller('DashboardCtrl', ['$scope', '$rootScope', '$routeParams', '$location', 'Results', 'Runs', 'Menu', function($scope, $rootScope, $routeParams, $location, Results, Runs, Menu) {
+    $scope.runId = $routeParams.runId;
+    $scope.Menu = Menu.setCurrentPage('dashboard', $scope.runId);
+    $scope.fromSocialShare = $location.search().share;
+    
+    function loadResults() {
+        // Load result if needed
+        if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
+            Results.get({runId: $routeParams.runId}, function(result) {
+                $rootScope.loadedResult = result;
+                $scope.result = result;
+                init();
+            }, function(err) {
+                $scope.error = true;
+            });
+        } else {
+            $scope.result = $rootScope.loadedResult;
+            init();
+        }
+    }
+
+    function init() {
+        // By default, Angular sorts object's attributes alphabetically. Countering this problem by retrieving the keys order here.
+        $scope.categoriesOrder = Object.keys($scope.result.scoreProfiles.generic.categories);
+        
+        $scope.globalScore = Math.max($scope.result.scoreProfiles.generic.globalScore, 0);
+        $scope.tweetText = 'My website\'s score is ' + $scope.globalScore + '/100 on #YellowLabTools!';
+    }
+
+    $scope.showRulePage = function(ruleName) {
+        $location.path('/result/' + $scope.runId + '/rule/' + ruleName);
+    };
+
+    $scope.testAgain = function() {
+        Runs.save({
+                url: $scope.result.params.url,
+                waitForResponse: false
+            }, function(data) {
+                $location.path('/queue/' + data.runId);
+            });
+    };
+
+    /// When comming from a social shared link, the user needs to click on "See full report" button to display the full dashboard.
+    $scope.seeFullReport = function() {
+        $scope.fromSocialShare = false;
+        $location.search({});
+    };
+
+    $scope.shareOnTwitter = function(message) {
+        openSocialPopup('https://twitter.com/intent/tweet?url=' + document.URL + '%3Fshare&text=' + encodeURIComponent(message));
+    };
+
+    $scope.shareOnLinkedin = function(message) {
+        openSocialPopup('https://www.linkedin.com/shareArticle?mini=true&url=' + document.URL + '%3Fshare&title=' + encodeURIComponent(message) + '&summary=' + encodeURIComponent('YellowLabTools is a free online tool that analyzes performance and front-end quality of a webpage.'));
+    };
+
+    function openSocialPopup(url) {
+        var winHeight = 400;
+        var winWidth = 600;
+        var winTop = (screen.height / 2) - (winHeight / 2);
+        var winLeft = (screen.width / 2) - (winWidth / 2);
+        window.open(url, 'sharer', 'top=' + winTop + ',left=' + winLeft + ',toolbar=0,status=0,width=' + winWidth + ',height=' + winHeight);
+    }
+
+    // Returns the URL of the JSON result
+    $scope.getAPIUrl = function() {
+        return '/api/results/' + $scope.runId;
+    };
+
+    loadResults();
+}]);

+ 15 - 0
front/src/js/controllers/indexCtrl.js

@@ -0,0 +1,15 @@
+var indexCtrl = angular.module('indexCtrl', []);
+
+indexCtrl.controller('IndexCtrl', ['$scope', '$location', 'Runs', function($scope, $location, Runs) {
+    $scope.launchTest = function() {
+        if ($scope.url) {
+            Runs.save({
+                url: $scope.url,
+                waitForResponse: false
+            }, function(data) {
+                $location.path('/queue/' + data.runId);
+            });
+            
+        }
+    };
+}]);

+ 30 - 0
front/src/js/controllers/queueCtrl.js

@@ -0,0 +1,30 @@
+var queueCtrl = angular.module('queueCtrl', ['runsFactory']);
+
+queueCtrl.controller('QueueCtrl', ['$scope', '$routeParams', '$location', 'Runs', function($scope, $routeParams, $location, Runs) {
+    $scope.runId = $routeParams.runId;
+
+    var numberOfTries = 0;
+    
+    function getRunStatus () {
+        Runs.get({runId: $scope.runId}, function(data) {
+            $scope.url = data.params.url;
+            $scope.status = data.status;
+
+            if (data.status.statusCode === 'running' || data.status.statusCode === 'awaiting') {
+                numberOfTries ++;
+
+                // Retrying in 2 seconds (and increasing the delay a bit more each time)
+                setTimeout(getRunStatus, 2000 + (numberOfTries * 100));
+
+            } else if (data.status.statusCode === 'complete') {
+                $location.path('/result/' + $scope.runId).replace();
+            } else {
+                // Handled by the view
+            }
+        });
+    }
+    
+    getRunStatus();
+}]);
+
+    

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

@@ -0,0 +1,42 @@
+var ruleCtrl = angular.module('ruleCtrl', []);
+
+ruleCtrl.controller('RuleCtrl', ['$scope', '$rootScope', '$routeParams', '$location', '$sce', 'Menu', 'Results', 'Runs', function($scope, $rootScope, $routeParams, $location, $sce, Menu, Results, Runs) {
+    $scope.runId = $routeParams.runId;
+    $scope.policyName = $routeParams.policy;
+    $scope.Menu = Menu.setCurrentPage(null, $scope.runId);
+    $scope.rule = null;
+
+    function loadResults() {
+        // Load result if needed
+        if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
+            Results.get({runId: $routeParams.runId}, function(result) {
+                $rootScope.loadedResult = result;
+                $scope.result = result;
+                init();
+            });
+        } else {
+            $scope.result = $rootScope.loadedResult;
+            init();
+        }
+    }
+
+    function init() {
+        $scope.rule = $scope.result.rules[$scope.policyName];
+        $scope.message = $sce.trustAsHtml($scope.rule.policy.message);
+    }
+
+    $scope.backToDashboard = function() {
+        $location.path('/result/' + $scope.runId);
+    };
+
+    $scope.testAgain = function() {
+        Runs.save({
+                url: $scope.result.params.url,
+                waitForResponse: false
+            }, function(data) {
+                $location.path('/queue/' + data.runId);
+            });
+    };
+
+    loadResults();
+}]);

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

@@ -0,0 +1,145 @@
+var timelineCtrl = angular.module('timelineCtrl', []);
+
+timelineCtrl.controller('TimelineCtrl', ['$scope', '$rootScope', '$routeParams', '$location', '$timeout', 'Menu', 'Results', 'Runs', function($scope, $rootScope, $routeParams, $location, $timeout, Menu, Results, Runs) {
+    $scope.runId = $routeParams.runId;
+    $scope.Menu = Menu.setCurrentPage('timeline', $scope.runId);
+
+    function loadResults() {
+        // Load result if needed
+        if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
+            Results.get({runId: $routeParams.runId}, function(result) {
+                $rootScope.loadedResult = result;
+                $scope.result = result;
+                render();
+            });
+        } else {
+            $scope.result = $rootScope.loadedResult;
+            render();
+        }
+    }
+
+    function render() {
+        initExecutionTree();
+        initTimeline();
+        $timeout(initProfiler, 100);
+    }
+
+    function initExecutionTree() {
+        var originalExecutions = $scope.result.javascriptExecutionTree.children || [];
+        $scope.executionTree = [];
+
+        originalExecutions.forEach(function(node) {
+
+            // Prepare a faster angular search by creating a kind of search index
+            node.searchIndex = (node.data.callDetails) ? [node.data.type].concat(node.data.callDetails.arguments).join('°°') : node.data.type;
+
+            $scope.executionTree.push(node);
+        });
+    }
+
+    function initTimeline() {
+
+        // Split the timeline into 200 intervals
+        var numberOfIntervals = 199;
+        var lastEvent = $scope.executionTree[$scope.executionTree.length - 1];
+        $scope.endTime =  lastEvent.data.timestamp + (lastEvent.data.time || 0);
+        $scope.timelineIntervalDuration = $scope.endTime / numberOfIntervals;
+        
+        // Pre-fill array of as many elements as there are milleseconds
+        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;
+    }
+
+
+    function parseBacktrace(str) {
+        if (!str) {
+            return null;
+        }
+
+        var out = [];
+        var splited = str.split(' / ');
+        splited.forEach(function(trace) {
+            var result = /^(\S*)\s?\(?(https?:\/\/\S+):(\d+)\)?$/g.exec(trace);
+            if (result && result[2].length > 0) {
+                var filePath = result[2];
+                var chunks = filePath.split('/');
+                var fileName = chunks[chunks.length - 1];
+
+                out.push({
+                    fnName: result[1],
+                    fileName: fileName,
+                    filePath: filePath,
+                    line: result[3]
+                });
+            }
+        });
+        return out;
+    }
+
+    $scope.filter = function(textFilter, scriptName) {
+
+    };
+
+    $scope.onNodeDetailsClick = function(node) {
+        var isOpen = node.showDetails;
+        if (!isOpen) {
+            // Close all other nodes
+            $scope.executionTree.forEach(function(currentNode) {
+                currentNode.showDetails = false;
+            });
+
+            // Parse the backtrace
+            if (!node.parsedBacktrace) {
+                node.parsedBacktrace = parseBacktrace(node.data.backtrace);
+            }
+
+        }
+        node.showDetails = !isOpen;
+    };
+
+    $scope.backToDashboard = function() {
+        $location.path('/result/' + $scope.runId);
+    };
+
+    $scope.testAgain = function() {
+        Runs.save({
+                url: $scope.result.params.url,
+                waitForResponse: false
+            }, function(data) {
+                $location.path('/queue/' + data.runId);
+            });
+    };
+
+    loadResults();
+
+}]);

+ 33 - 0
front/src/js/directives/gradeDirective.js

@@ -0,0 +1,33 @@
+var gradeDirective = angular.module('gradeDirective', []);
+
+gradeDirective.directive('grade', function() {
+ 
+    return {
+        restrict: 'E',
+        scope: {
+            score: '=score'
+        },
+        template: '<div ng-class="getGrade(score)">{{getGrade(score)}}</div>',
+        replace: true,
+        controller : ['$scope', function($scope) {
+            $scope.getGrade = function(score) {
+                if (score > 80) {
+                    return 'A';
+                }
+                if (score > 60) {
+                    return 'B';
+                }
+                if (score > 40) {
+                    return 'C';
+                }
+                if (score > 20) {
+                    return 'D';
+                }
+                if (score > 0) {
+                    return 'E';
+                }
+                return 'F';
+            };
+        }]
+    };
+});

+ 7 - 0
front/src/js/models/resultsFactory.js

@@ -0,0 +1,7 @@
+var resultsFactory = angular.module('resultsFactory', ['ngResource']);
+
+resultsFactory.factory('Results', ['$resource', function($resource) {
+    return $resource('/api/results/:runId', {
+        
+    });
+}]);

+ 7 - 0
front/src/js/models/runsFactory.js

@@ -0,0 +1,7 @@
+var runsFactory = angular.module('runsFactory', ['ngResource']);
+
+runsFactory.factory('Runs', ['$resource', function($resource) {
+    return $resource('/api/runs/:runId', {
+    
+    });
+}]);

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

@@ -0,0 +1,34 @@
+var menuService = angular.module('menuService', []);
+
+menuService.factory('Menu', ['$location', function($location) {
+
+    var currentPage, currentRunId;
+
+    return {
+        getCurrentPage: function() {
+            return currentPage;
+        },
+        setCurrentPage: function(page, runId) {
+            currentPage = page;
+            currentRunId = runId;
+
+            return this;
+        },
+        changePage: function(page) {
+            switch (page) {
+                case 'index':
+                    $location.path('/');
+                    break;
+                case 'dashboard':
+                    $location.path('/result/' + currentRunId);
+                    break;
+                case 'timeline':
+                    $location.path('/result/' + currentRunId + '/timeline');
+                    break;
+                default:
+                    console.err('Undefined Menu.changePage() destination');
+            }
+        }
+    };
+
+}]);

+ 12 - 0
front/src/less/about.less

@@ -0,0 +1,12 @@
+.about {
+    margin: 3em auto;
+    width: 50%;
+}
+
+.about p {
+    margin: 2em;
+}
+
+.about a {
+    color: #FFF;
+}

+ 162 - 0
front/src/less/dashboard.less

@@ -0,0 +1,162 @@
+.testedUrl {
+    color: inherit;
+}
+
+.summary {
+    text-align: center;
+}
+
+.summary .globalScore {
+    margin-bottom: 3em;
+    .globalGrade {
+        margin: 0.5 auto;
+        width: 2.5em;
+        height: 2.5em;
+        line-height: 2.5em;
+        border-radius: 0.5em;
+        font-size: 3em;
+        font-weight: bold;
+        vertical-align: middle;
+    }
+    .on100 {
+        font-size: 1.2em;
+        font-weight: bold;
+        margin: 0.5em 0 1em;
+    }
+}
+
+.summary .notations {
+    display: table;
+    width: 80%;
+    margin: 0 10% 1.5em;
+    border-spacing: 1em;
+}
+.summary .notations > div {
+    display: table-row;
+}
+.summary .notations > div > div {
+    display: table-cell;
+    height: 2.5em;
+    vertical-align: middle;
+}
+.summary .notations .category {
+    font-weight: bold;
+    text-align: center;
+    width: 20%;
+}
+.summary .notations .criteria {
+    font-weight: normal;
+    width: 75%;
+}
+.A, .B, .C, .D, .E, .F, .NA {
+    .summary .notations &.categoryScore {
+        width: 2.5em;
+        max-width: 2.5em;
+        min-width: 2.5em;
+        font-size: 2em;
+        text-align: center;
+        border-radius: 0.5em;
+        font-weight: bold;
+    }
+    .summary .notations .grade & {
+        width: 1em;
+        height: 1em;
+        font-size: 1em;
+        color: transparent;
+        margin: 0 auto;
+        border-radius: 0.5em;
+    }
+}
+
+.summary .notations .criteria .table {
+    width: 100%;
+    > div:hover > div {
+        background: #EBD8E2;
+        cursor: pointer;
+        &.info {
+            background: #FFF;
+            .icon-question {
+                color: #EBD8E2;
+            }
+        }
+    }
+}
+.summary .notations .criteria .grade {
+    width: 10%;
+    padding-left: 0.5em;
+    padding-right: 0.5em;
+    vertical-align: middle;
+}
+.summary .notations .criteria .label {
+    width: 70%;
+}
+.summary .notations .criteria .result {
+    width: 18%;
+    font-weight: bold;
+    white-space: nowrap;
+    text-align: center;
+    vertical-align: middle;
+}
+.summary .notations .warning .label, .summary .notations .warning .result, .summary .notations .icon-warning {
+    color: #FF1919;
+}
+.summary .notations .criteria .info {
+    width: 2%;
+    text-align: center;
+    vertical-align: middle;
+    background: #FFF;
+    padding-left: 0.1em;
+    padding-right: 0.1em;
+}
+.summary .notations .criteria .icon-question {
+    color: transparent;
+}
+
+.summary .fromShare {
+    margin-bottom: 3em;
+    a {
+        font-size: 1em;
+        padding: 0.3em 0.5em;
+        margin: 0.5em;
+        line-height: 2em;
+        border: 0 solid;
+        border-radius: 0.5em;
+        box-shadow: 0.1em 0.2em 0 0 #5e2846;
+        background: #e74c3c;
+        color: #fff;
+        text-decoration: none;
+    }
+}
+
+.summary .apiTip {
+    font-size: 0.8em;
+    margin-bottom: 4em;
+    color: #413;
+    a {
+        color: inherit;
+    }
+}
+
+.summary .tweet {
+    .tweetText {
+        color: #413;
+        background: #F2F2F2;
+        border: none;
+        width: 25em;
+        padding: 0.4em;
+        border-radius: 0.5em;
+        box-shadow: 0.05em 0.1em 0 0 #999;
+    }
+    .tweetButton, .linkedinButton {
+        color: #413;
+        background: #F2F2F2;
+        margin-right: 0;
+        &:hover {
+            color: #F2F2F2;
+            background: #e74c3c;
+        }
+    }
+    input {
+        font-size: 0.9em;
+    }
+}

+ 15 - 15
app/public/styles/less/icons.less → front/src/less/icons.less

@@ -1,24 +1,24 @@
-@eye-font-family: "fontsmith-icons";
-@eye-value: "\e004";
-@eye: '"fontsmith-icons"' '"\\e004"';
-@lab-font-family: "fontsmith-icons";
-@lab-value: "\e003";
-@lab: '"fontsmith-icons"' '"\\e003"';
-@bars-font-family: "fontsmith-icons";
-@bars-value: "\e005";
-@bars: '"fontsmith-icons"' '"\\e005"';
 @warning-font-family: "fontsmith-icons";
 @warning-value: "\e000";
 @warning: '"fontsmith-icons"' '"\\e000"';
-@arrow-left3-font-family: "fontsmith-icons";
-@arrow-left3-value: "\e006";
-@arrow-left3: '"fontsmith-icons"' '"\\e006"';
 @question-font-family: "fontsmith-icons";
 @question-value: "\e001";
 @question: '"fontsmith-icons"' '"\\e001"';
+@lab-font-family: "fontsmith-icons";
+@lab-value: "\e004";
+@lab: '"fontsmith-icons"' '"\\e004"';
 @list-font-family: "fontsmith-icons";
-@list-value: "\e002";
-@list: '"fontsmith-icons"' '"\\e002"';
+@list-value: "\e003";
+@list: '"fontsmith-icons"' '"\\e003"';
+@bars-font-family: "fontsmith-icons";
+@bars-value: "\e005";
+@bars: '"fontsmith-icons"' '"\\e005"';
+@arrow-left3-font-family: "fontsmith-icons";
+@arrow-left3-value: "\e006";
+@arrow-left3: '"fontsmith-icons"' '"\\e006"';
+@loop-font-family: "fontsmith-icons";
+@loop-value: "\e002";
+@loop: '"fontsmith-icons"' '"\\e002"';
 
 .icon-font-family(@char) {
   font-family: ~`@{char}[0]`;
@@ -49,7 +49,7 @@
 
 @font-face {
   font-family: "fontsmith-icons";
-  src:url("/public/fonts/icons.woff") format("woff"),
+  src:url("/fonts/icons.woff") format("woff"),
     ;
   font-weight: normal;
   font-style: normal;

+ 2 - 12
app/public/styles/less/index.less → front/src/less/index.less

@@ -11,21 +11,11 @@
 .launchBtn {
     background: #e74c3c;
     color: #fff;
-}
-
-.readings {
-    margin-top: 5em;
-    font-size: 0.8em;
-    & a {
-        color: inherit;
-        line-height: 1.7em;
+    &.disabled {
+        background: #deaca6;
     }
 }
 
-.version {
-    font-size: 0.7em;
-}
-
 input[type=submit], input.url {
     padding: 0 0.5em;
     margin: 0.5em;

+ 155 - 0
front/src/less/main.less

@@ -0,0 +1,155 @@
+@import "icons.less";
+
+html {
+    margin: 100px 50px;
+}
+
+body {
+    margin: 0 auto;
+    max-width: 1280px;
+    background: #9c4274;
+    color: #fff;
+    font-size: 16px;
+    text-align: center;
+}
+
+body, input[type=submit], input[type=text], input[type=url], input[type=number], button {
+    font-family: 'Century Gothic', helvetica, arial, sans-serif;
+}
+
+input[type=submit] {
+    cursor: pointer;
+}
+
+h1 span {
+    display: inline-block;
+    height: 1em;
+    width: 1em;
+    color: #ffa319;
+}
+
+.resultsMenu {
+    margin-top: 2em;
+}
+.resultsMenu .menuItem {
+    display: inline-block;
+    margin: 1em;
+    width: 8em;
+    height: 7em;
+    color: #fff;
+    border: 3px solid #fff;
+    border-radius: 0.5em;
+    cursor: pointer;
+    text-decoration: none;
+    &.back, &.restart {
+        color: #413;
+        border-color: #413;
+    }
+}
+.resultsMenu .menuItem div {
+    padding-top: 0.5em;
+    font-size: 3em;
+}
+.resultsMenu .active, .resultsMenu .menuItem.active:hover {
+    color: #ffa319;
+    border-color: #ffa319;
+}
+.resultsMenu .menuItem:hover {
+    color: #ffa319;
+}
+.resultsMenu span {
+    position: relative;
+    top: 0.5em;
+}
+
+/* Grade colors */
+.A {
+    /* green */
+    background: #0C4;
+}
+.B {
+    /* green */
+    background: #CD0;
+}
+.C {
+    /* yellow */
+    background: #FD2;
+}
+.D {
+    /* orange */
+    background: #FA2;
+}
+.E {
+    /* red */
+    background: #F60;
+}
+.F {
+    /* red */
+    background: #F22;
+}
+.NA {
+    /* Non applicable */
+    background: #CCC;
+}
+
+.board {
+    margin-top: 2em;
+    padding: 1em;
+    background: #fff;
+    color: #000;
+    border-radius: 0.5em;
+    text-align: left;
+}
+
+.backToDashboard {
+    text-align: center;
+
+    a {
+        font-size: 0.9em;
+        display: block;
+        margin-top: 4em;
+        color: black;
+    }
+}
+
+
+.star {
+    font-weight: bold;
+    span {
+        font-size: 1.2em;
+    }
+}
+
+.footer {
+    padding: 3em;
+    color: #413;
+    a {
+        color: inherit;
+    }
+    .version {
+        font-size: 0.7em;
+    }
+}
+
+/* Icons */
+.icon-lab {
+    .icon(@lab);
+}
+.icon-question {
+    .icon(@question);
+}
+.icon-warning {
+    .icon(@warning);
+}
+.icon-back {
+    .icon(@arrow-left3);
+}
+.icon-summary {
+    .icon(@list);
+}
+.icon-spaghetti {
+    .icon(@bars);
+}
+.icon-loop {
+    .icon(@loop);
+}

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

@@ -0,0 +1,13 @@
+.status {
+    margin-top: 2em;
+    font-size: 2.5em;
+}
+
+.statusSubMessage {
+    font-size: 0.8em;
+    margin-bottom: 6em;
+}
+
+.queueLink {
+    color: #FFF;
+}

+ 91 - 0
front/src/less/rule.less

@@ -0,0 +1,91 @@
+.rule.board {
+    text-align: center;
+}
+
+.rule .ruleTable {
+    display: table;
+    border-spacing: 1em;
+    width: 90%;
+    margin: 2em auto;
+    background: #f2f2f2;
+    border: 1px dashed #666;
+    border-radius: 0.5em;
+    > div {
+        display: table-cell;
+        vertical-align: middle;
+    }
+    .left {
+        width: 33%;
+        font-weight: bold;
+    }
+    .right {
+        width: 67%;
+    }
+}
+
+.rule .score {
+    font-size: 2.5em;
+    line-height: 2em;
+    height: 2em;
+    width: 2em;
+    border-radius: 0.5em;
+    margin: 0 auto 0.5em;
+}
+
+.rule .message {
+    width: 80%;
+    margin: 0 auto;
+    p {
+        margin: 0.5em;
+    }
+}
+
+.rule .message ul {
+    list-style-type: none;
+    padding-left: 0;
+}
+.rule .message li:before {
+    content:'\25e6';
+    margin-right: 0.3em;
+    font-size: 1.2em;
+    position: relative;
+    top: 0.1em;
+}
+
+.rule .warning {
+    width: 90%;
+    margin: -1em auto 2em;
+    background: #FEE;
+    border: 1px dashed #e74c3c;
+    color: #e74c3c;
+    border-radius: 0.5em;
+}
+
+.rule .offendersTable {
+    display: table;
+    border-spacing: 0 0.25em;
+    margin: 0 auto;
+    min-width: 10%;
+    max-width: 90%;
+    > div {
+        display: table-row;
+        > div {
+            display: table-cell;
+            background: #f2f2f2;
+            padding: 0 1em;
+            word-wrap: break-word;
+            word-break: break-all;
+            &:hover {
+                background: #EBD8E2;
+            }
+        }
+    }
+}
+
+.rule .notFound {
+    font-size: 1em;
+    h2 {
+        font-size: 3em;
+        margin-bottom: 1em;
+    }
+}

+ 10 - 236
app/public/styles/less/results.less → front/src/less/timeline.less

@@ -1,4 +1,3 @@
-
 /* Timeline colors, related to Window Performances */
 @domCreationColor: #FF6600;
 @domCreationBg: #FFE0CC;
@@ -11,145 +10,9 @@
 @domInteractiveColor: #FFE433;
 @domInteractiveBg: #FFFCCC;
 
-
-.resultsMenu {
-    margin-top: 2em;
-}
-
-.resultsMenu .menuItem {
-    display: inline-block;
-    margin: 1em;
-    width: 8em;
-    height: 7em;
-    color: #fff;
-    border: 3px solid #fff;
-    border-radius: 0.5em;
-    cursor: pointer;
-    text-decoration: none;
-}
-.resultsMenu .back {
-    color: #5e2846;
-    border-color: #5e2846;
-}
-.resultsMenu .menuItem div {
-    padding-top: 0.5em;
-    font-size: 3em;
-}
-.resultsMenu .active, .resultsMenu .menuItem.active:hover {
-    color: #ffa319;
-    border-color: #ffa319;
-}
-.resultsMenu .menuItem:hover {
-    color: #ffa319;
-}
-
-.resultsMenu span {
-    position: relative;
-    top: 0.5em;
-}
-
-.testedUrl {
-    color: inherit;
-}
-
-h4 {
-    margin-bottom: 0.5em;
-}
-
-.summary, .metrics, .execution {
-    margin-top: 2em;
-    padding: 1em;
-    background: #fff;
-    color: #000;
-    border-radius: 0.5em;
-    text-align: left;
-}
-
-.notations {
-    display: table;
-    width: 90%;
-    margin: 0 10%;
-    border-spacing: 1em;
-}
-.notations > div {
-    display: table-row;
-}
-.notations > div > div {
-    display: table-cell;
-    height: 2.5em;
-    vertical-align: middle;
-}
-.notations .notation {
-    font-weight: bold;
+.execution {
     text-align: center;
 }
-.notations .criteria {
-    font-weight: normal;
-}
-.notations .A, .notations .B, .notations .C, .notations .D, .notations .E, .notations .F, .notations .NA {
-    width: 2.5em;
-    font-size: 2em;
-    text-align: center;
-    border-radius: 0.5em;
-    font-weight: bold;
-}
-.notations .A {
-    /* green */
-    background: #00DB61;
-}
-.notations .B {
-    /* green */
-    background: #CAD63D;
-}
-.notations .C {
-    /* yellow */
-    background: #FFD119;
-}
-.notations .D {
-    /* orange */
-    background: #FFA319;
-}
-.notations .E {
-    /* red */
-    background: #FF6600;
-}
-.notations .F {
-    /* red */
-    background: #FF1919;
-}
-.notations .NA {
-    /* Non applicable */
-    background: #CCC;
-}
-.notations .icon-eye {
-    color: #9c4274;
-    cursor: pointer;
-}
-
-.notations .criteria .table {
-    width: 75%;
-}
-.notations .criteria .label {
-    width: 70%;
-}
-.notations .criteria .result {
-    width: 20%;
-    font-weight: bold;
-    white-space: nowrap;
-    text-align: center;
-}
-.notations .warning .label, .notations .warning .result {
-    color: #FF1919;
-}
-.notations .criteria .info {
-    width: 10%;
-    text-align: center;
-}
-.notations .criteria .icon-question {
-    color: #f1c40f;
-    cursor: pointer;
-}
-
 
 .timeline {
     margin: 2em 0 5em;
@@ -269,38 +132,13 @@ h4 {
     border-radius: 0.2em;
 }
 
-
-.metrics h4 {
-    padding-left: 2em;
-}
-
-.metrics .module {
-    padding-left: 4em;
-    padding-top: 0.5em;
-}
-
-.metrics .legend {
-    font-style: italic;
-    color: #aaa;
-}
-
-.metrics .offenders {
-    padding-left: 0em;
-    font-size: 0.8em;
-}
-
-.metrics .offenders div {
-    cursor: pointer;
-}
-
-.metrics .offenders ul {
-    margin-top: 0.5em;
-}
-
 .filters {
-    margin: 1em 0;
+    margin: 1em auto;
     padding: 0.5em;
+    min-width: 30em;
+    width: 30%;
     border: 1px dotted #aaa;
+    text-align: left;
 }
 
 .slowRequestsLimit {
@@ -371,8 +209,9 @@ input.textFilter {
     color: #f1c40f;
     cursor: pointer;
 }
-.table .details .icon-warning {
-    cursor: pointer;
+.table .icon-warning {
+    display: inline-block;
+    width: 0.8em;
 }
 
 .detailsOverlay {
@@ -426,72 +265,7 @@ input.textFilter {
 .table > div > .startTime.domCreation {
     background: @domCreationBg;
 }
-
-.table .icon-warning {
+.execution .icon-warning {
     color: #e74c3c;
-}
-
-
-/**** NgModal popin (have a look inside bower_components) ****/
-.ng-modal {
-    position: fixed;
-    z-index: 9999;
-    top: 0;
-    left: 0;
-    width: 100%;
-    height: 100%;
-    text-align: left;
-}
-.ng-modal-overlay {
-    position: absolute;
-    z-index: 9999;
-    top: 0;
-    left: 0;
-    width: 100%;
-    height: 100%;
-    background-color: #000;
-    opacity: 0.5;
-}
-.ng-modal-dialog {
-    z-index: 10000;
-    position: absolute;
-    top: 50%;
-    left: 50%;
-    width: 50%;
-    transform: translate(-50%, -50%);
-    -webkit-transform: translate(-50%, -50%);
-    background-color: #fff;
-    padding: 10px;
-    border: 3px solid #f1c40f;
-    border-radius: 0.5em;
-    color: #000;
-}
-.ng-modal-dialog-content {
-    overflow-x: hidden;
-    overflow-y: scroll;
-    word-wrap: break-word;
-    max-height: 20em;
-    font-weight: normal;
-    white-space: normal;
-}
-.ng-modal-close {
-    position: absolute;
-    top: 3px;
-    right: 5px;
     cursor: pointer;
-    font-size: 120%;
-    padding: 5px;
-    display: inline-block;
-}
-.ng-modal-close-x {
-    font-weight: bold;
-    font-family: Arial, sans-serif;
-}
-.ng-modal-title {
-    font-weight: bold;
-    font-size: 1.5em;
-    display: block;
-    margin-bottom: 10px;
-    padding-bottom: 7px;
-    border-bottom: solid 1px #999;
-}
+}

+ 49 - 0
front/src/main.html

@@ -0,0 +1,49 @@
+<html>
+<head>
+	<meta charset="utf-8">
+    <title>Yellow Lab Tools</title>
+    <base href="/">
+    <link rel="icon" type="image/png" href="/img/favicon.png">
+    <meta property="og:image" content="/img/logo-large.png" />
+
+    <!-- build:css /css/styles.css-->
+    <link rel="stylesheet" type="text/css" href="/css/main.css">
+    <link rel="stylesheet" type="text/css" href="/css/index.css">
+    <link rel="stylesheet" type="text/css" href="/css/dashboard.css">
+    <link rel="stylesheet" type="text/css" href="/css/queue.css">
+    <link rel="stylesheet" type="text/css" href="/css/rule.css">
+    <link rel="stylesheet" type="text/css" href="/css/timeline.css">
+    <link rel="stylesheet" type="text/css" href="/css/about.css">
+    <!-- endbuild -->
+
+    <!-- build:js /js/all.js -->
+    <script src="/bower_components/angular/angular.min.js"></script>
+    <script src="/bower_components/angular-route/angular-route.min.js"></script>
+    <script src="/bower_components/angular-resource/angular-resource.min.js"></script>
+    <script src="/js/app.js"></script>
+    <script src="/js/controllers/indexCtrl.js"></script>
+    <script src="/js/controllers/aboutCtrl.js"></script>
+    <script src="/js/controllers/dashboardCtrl.js"></script>
+    <script src="/js/controllers/queueCtrl.js"></script>
+    <script src="/js/controllers/ruleCtrl.js"></script>
+    <script src="/js/controllers/timelineCtrl.js"></script>
+    <script src="/js/models/resultsFactory.js"></script>
+    <script src="/js/models/runsFactory.js"></script>
+    <script src="/js/services/menuService.js"></script>
+    <script src="/js/directives/gradeDirective.js"></script>
+    <!-- endbuild -->
+<head>
+
+<body ng-app="YellowLabTools">
+    <div id="header"><h1>Yellow Lab <span class="icon-lab"></span> Tools</h1></div>
+    <div id="body" ng-view autoscroll="true"></div>
+    <div class="footer">
+        <a href="/about">Learn more</a><br>If you like <b>Yellow Lab Tools</b>, <a href="https://github.com/gmetais/YellowLabTools" target="_blank" class="star">give it a <span>&#9733;</span> on GitHub</a>!<br>
+        <span class="version">@@version</span>
+    </div>
+
+    <script>
+        (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','//www.google-analytics.com/analytics.js','ga');if('@@googleAnalyticsId'.indexOf('UA-')===0){ga('create','@@googleAnalyticsId','auto');}
+    </script>
+</body>
+</html>

+ 9 - 0
front/src/views/about.html

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

+ 59 - 0
front/src/views/dashboard.html

@@ -0,0 +1,59 @@
+<div ng-include="'views/resultSubHeader.html'"></div>
+<div class="summary board">
+    
+    <div class="globalScore" ng-if="globalScore === 0 || globalScore > 0">
+        <h2> Global score</h2>
+        <div class="globalScoreDisplay">
+            <grade score="result.scoreProfiles.generic.globalScore" class="globalGrade"></grade>
+            <div class="on100">{{globalScore}}/100</div>
+        </div>
+    </div>
+
+    <h2 ng-if="!error && !fromSocialShare">Score details</h2>
+    <div ng-if="!error && !fromSocialShare" class="notations">
+        <div ng-repeat="categoryKey in categoriesOrder" ng-init="category = result.scoreProfiles.generic.categories[categoryKey]">
+            <grade score="category.categoryScore" class="categoryScore"></grade>
+            <div class="category">{{category.label}}</div>
+            <div class="criteria">
+                <div class="table" title="Click to see details">
+                    <div ng-repeat="ruleName in category.rules" ng-if="result.rules[ruleName]" ng-init="rule = result.rules[ruleName]"
+                         ng-class="{'warning': rule.abnormal}" ng-click="showRulePage(ruleName)">
+                        <div class="grade">
+                            <grade score="rule.score"></grade>
+                        </div>
+                        <div class="label">{{rule.policy.label}}</div>
+                        <div class="result">
+                            {{rule.value}}
+                            <span ng-if="rule.abnormal" class="icon-warning"></span>
+                            <span ng-if="rule.abnormalityScore <= -100" class="icon-warning"></span>
+                            <span ng-if="rule.abnormalityScore <= -300" class="icon-warning"></span>
+                        </div>
+                        <div class="info"><span class="icon-question"></span></div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <div ng-if="!error" class="apiTip">
+        <b>Did you know? Yellow Lab Tools now has an API</b>! <a href="{{getAPIUrl()}}" target="_blank">Here</a> is the JSON output for this run. Checkout <a href="https://github.com/gmetais/YellowLabTools/wiki/Public-API" target="_blank">the API doc</a>.
+    </div>
+
+    <div class="tweet" ng-if="!error && !fromSocialShare">
+        <form ng-submit="sendTweet()">
+            <input type="text" class="tweetText" ng-model="tweetText">
+            <input type="submit" class="tweetButton" value="Tweet this!" ng-click="shareOnTwitter(tweetText)">
+            <input type="submit" class="linkedinButton" value="LinkedIn this!" ng-click="shareOnLinkedin(tweetText)">
+        </form>
+    </div>
+
+    <div class="fromShare" ng-if="!error && fromSocialShare">
+        <p>Yellow Lab Tools is a free online tool that analyzes performance and front-end quality.</p>
+        <a href="" ng-click="seeFullReport()">See the full report for this page</a>
+        <a href="/">Test another webpage</a>
+    </div>
+
+    <div ng-if="error">
+        <h2>Run failed / Run not found</h2>
+    </div>
+</div>

+ 6 - 0
front/src/views/index.html

@@ -0,0 +1,6 @@
+<h2 class="promess">Free online test to help speeding up <b>heavy</b> web pages</h2>
+
+<form ng-submit="launchTest()" >
+    <input type="text" name="url" ng-model="url" placeholder="http://www.mysite.com" class="url" />
+    <input type="submit" value="Launch test" class="launchBtn" ng-class="{disabled: !url}" />
+</form>

+ 23 - 0
front/src/views/queue.html

@@ -0,0 +1,23 @@
+<p>Tested url: &nbsp; <a href="{{url}}" target="_blank" class="testedUrl">{{url}}</a></p>
+
+<div ng-if="status.statusCode == 'failed'">
+    <div class="status">Test failed</div>
+    <p class="statusSubMessage">{{status.error}}</p>
+    <p><a class="queueLink" href="https://github.com/gmetais/YellowLabTools/issues" target="_blank">Report a bug on GitHub</a></p>
+    <p><a class="queueLink" href="/">Back to index</a></p>
+</div>
+<div ng-if="status.statusCode == 'awaiting'">
+    <div class="status">
+        <ng-pluralize count="status.position" when="{'one': 'Waiting behind 1 other test', 'other': 'Waiting behind {} other tests'}">
+        </ng-pluralize>
+    </div>
+    <p class="statusSubMessage">(auto-refresh activated)</p>
+</div>
+<div ng-if="status.statusCode == 'running'">
+    <div class="status">Test is running...</div>
+    <p class="statusSubMessage">(auto-refresh activated)</p>
+</div>
+<div ng-if="status.statusCode == 'complete'">
+    <div class="status">Test complete</div>
+    <p class="statusSubMessage">Opening results...</p>
+</div>

+ 8 - 0
front/src/views/resultSubHeader.html

@@ -0,0 +1,8 @@
+<div>Tested url: &nbsp; <a href="{{result.params.url}}" target="_blank" class="testedUrl">{{result.params.url}}</a></div>
+
+<div class="resultsMenu">
+    <a class="menuItem back" href="/"><div class="icon-back"></div><span>New test<span></a>
+    <a class="menuItem restart" href="" ng-click="testAgain()"><div class="icon-loop"></div><span>Test again<span></a>
+    <div class="menuItem" ng-class="{active: Menu.getCurrentPage() == 'dashboard'}" ng-click="Menu.changePage('dashboard')"><div class="icon-summary"></div><span>Dashboard</span></div>
+    <div class="menuItem" ng-class="{active: Menu.getCurrentPage() == 'timeline'}" ng-click="Menu.changePage('timeline')"><div class="icon-spaghetti"></div><span>JS Timeline</span></div>
+</div>

+ 37 - 0
front/src/views/rule.html

@@ -0,0 +1,37 @@
+<div ng-include="'views/resultSubHeader.html'"></div>
+<div class="rule board">
+    <div class="backToDashboard"><a href="#" ng-click="backToDashboard()">Back to dashboard</a></div>
+
+    <div ng-if="rule" class="ruleTable">
+        <div class="left">
+            <h2>{{rule.policy.label}}</h2>
+            <grade score="rule.score" class="score"></grade>
+            <div>{{rule.score}}/100</div>
+        </div>
+        <div class="right">
+            <h3>Value: {{rule.value}}</h3>
+            <div ng-bind-html="message" class="message"></div>
+        </div>
+    </div>
+    <div ng-if="rule.abnormal" class="warning">
+        <h3>Warning</h3>
+        <p>This rule reached the abnormality threshold, which means there is a real problem you should care about.</p>
+    </div>
+    <div class="offenders">
+        <h3>
+            <ng-pluralize count="rule.offenders.length || 0" when="{'0': 'No offenders', 'one': '1 offender', 'other': '{} offenders'}">
+            </ng-pluralize>
+        </h3>
+        <div class="offendersTable">
+            <div ng-repeat="offender in rule.offenders track by $index">
+                <div>{{offender}}</div>
+            </div>
+        </div>
+    </div>
+    <div ng-if="!rule && rule !== null" class="notFound">
+        <h2>404</h2>
+        Rule "{{policyName}}"" not found
+    </div>
+
+    <div class="backToDashboard"><a href="#" ng-click="backToDashboard()">Back to dashboard</a></div>
+</div>

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

@@ -0,0 +1,144 @@
+<div ng-include="'views/resultSubHeader.html'"></div>
+<div class="execution board">
+    <h2>Javascript Timeline</h2>
+    <p>This graph gives a quick view of when the Javascript interactions with the DOM occur during the loading of the page.</p>
+
+    <div class="timeline">
+        <div class="chart">
+            <div class="chartPoints">
+                <div ng-repeat="duration in timeline track by $index"
+                     class="interval"
+                     ng-class="{
+                        domCreation: $index * timelineIntervalDuration < result.toolsResults.phantomas.metrics.domInteractive,
+                        domInteractive: $index * timelineIntervalDuration >= result.toolsResults.phantomas.metrics.domInteractive
+                            && $index * timelineIntervalDuration < result.toolsResults.phantomas.metrics.domContentLoaded,
+                        domContentLoaded: $index * timelineIntervalDuration >= result.toolsResults.phantomas.metrics.domContentLoaded
+                            && $index * timelineIntervalDuration < result.toolsResults.phantomas.metrics.domContentLoadedEnd,
+                        domContentLoadedEnd: $index * timelineIntervalDuration >= result.toolsResults.phantomas.metrics.domContentLoadedEnd
+                            && $index * timelineIntervalDuration < result.toolsResults.phantomas.metrics.domComplete,
+                        domComplete: $index * timelineIntervalDuration >= result.toolsResults.phantomas.metrics.domComplete
+                     }">
+                    <div style="height: {{100 * duration / timelineMax | number: 0}}px" class="color"></div>
+                    <div class="tooltip detailsOverlay">
+                        <div>Timestamp: {{$index * timelineIntervalDuration | number: 0}} ms</div>
+                    </div>
+                </div>
+            </div>
+            <div class="startTime">0 ms</div>
+            <div class="endTime">{{endTime | number: 0}} ms</div>
+        </div>
+        <div class="legend">
+            <div class="titles">
+                <div class="domCreation"><div class="color"></div>DOM creation</div>
+                <div class="domInteractive"><div class="color"></div>DOM interactive</div>
+                <div class="domContentLoaded"><div class="color"></div>DOM content loaded event</div>
+                <div class="domContentLoadedEnd"><div class="color"></div>Page completion</div>
+                <div class="domComplete"><div class="color"></div>Page is complete</div>
+            </div>
+            <div class="tips">
+                <div>Executing Javascript and DOM queries here is a <b>bad practice</b> and slows down the DOM construction.</div>
+                <div>Some frameworks do things here, but it's not reliable and should be avoided.</div>
+                <div>Also known as "document ready". This is where you should execute <b>top-priority</b> scripts, like binding action buttons or launch a video player.</div>
+                <div>Here you can execute <b>mid-priority</b> tasks. Loading a script with createElement('script') is one way to do so.</div>
+                <div>The page is considered loaded, it's time for low <b>priority things</b> : trackers, social plugins, easter egg...</div>
+            </div>
+        </div>
+    </div>
+
+    <h2>Javascript Profiler</h2>
+    <p>
+        The table below shows the interactions between Javascript and the DOM. It is useful to understand what happens while the page loads.
+    </p>
+    <div class="filters">
+        <div>
+            <input type="checkbox" ng-model="warningsFilterOn" id="warningsFilterOn" />
+            <label for="warningsFilterOn">Show warnings only</label>
+        </div>
+        <div>
+            <input type="checkbox" ng-model="textFilterOn" />
+            Filter by
+            <input type="text" ng-model="textFilter" placeholder="search..." class="textFilter" ng-change="textFilterOn = true" />
+        </div>
+    </div>
+    <div class="table">
+
+        <toto data-info="mavariable"></toto>
+        <div class="headers">
+            <div><!-- index --></div>
+            <div>Type</div>
+            <div>Params</div>
+            <div><!-- details --></div>
+            <div>Timestamp</div>
+        </div>
+        <div ng-if="(!warningsFilterOn || node.warning || node.error) && (!textFilterOn || !textFilter.length || node.searchIndex.indexOf(textFilter) >= 0)"
+             ng-repeat="node in profilerData" ng-class="{
+                showingDetails: node.showDetails,
+                jsError: node.error,
+                windowPerformance: node.windowPerformance
+            }">
+
+            <div class="index">{{$index + 1}}</div>
+            <div class="type">{{node.data.type}}</div>
+
+            <div class="value">
+                {{node.data.callDetails.arguments[0]}}
+                <span ng-if="node.data.callDetails.arguments.length > 1"> : {{node.data.callDetails.arguments[1]}}</span>
+                <span ng-if="node.data.callDetails.arguments.length > 2"> : {{node.data.callDetails.arguments[2]}}</span>
+                <span ng-if="node.data.callDetails.arguments.length > 3"> : {{node.data.callDetails.arguments[3]}}</span>
+            </div>
+            
+            <div class="details">
+                <div ng-class="{'icon-question': !node.warning && !node.error, 'icon-warning': node.warning || node.error}"
+                     ng-click="onNodeDetailsClick(node)"
+                     ng-if="node.data.type != 'jQuery loaded' && node.data.type != 'jQuery version change' && !node.windowPerformance"></div>
+                
+                <div class="detailsOverlay" ng-if="node.showDetails">
+                    <div class="closeBtn" ng-click="onNodeDetailsClick(node)">✖</div>
+
+                    <div ng-if="node.data.callDetails.context.domElement">
+                        <h4>Called on DOM element</h4>
+                        <div>{{node.data.callDetails.context.domElement}}</div>
+                    </div>
+
+                    <div ng-if="node.data.callDetails.context.length === 0">
+                        <h4>Called on 0 jQuery element</h4>
+                        <p class="advice">Useless function call, as the jQuery object is empty.</p>
+                    </div>
+
+                    <div ng-if="node.data.callDetails.context.length == 1 && node.data.callDetails.context.firstElementPath">
+                        <h4>Called on 1 jQuery element</h4>
+                        <div>{{node.data.callDetails.context.firstElementPath}}</div>
+                    </div>
+
+                    <div ng-if="node.data.callDetails.context.length > 1">
+                        <h4>Called on {{node.data.callDetails.context.length}} jQuery elements</h4>
+                        <p class="advice" ng-if="node.data.type == 'jQuery - bind' && node.data.callDetails.context.length > 5">
+                            The .bind() method attaches the event listener to each jQuery element one by one. Using the .on() method is preferable if available (from v1.7).
+                        </p>
+                        <p ng-if="node.data.callDetails.context.firstElementPath"><b>First one is:</b> {{node.data.callDetails.context.firstElementPath}}</p>
+                    </div>
+
+                    <p class="advice" ng-if="node.data.resultsNumber === 0">
+                        The query returned 0 results. Could it be unused or dead code?
+                    </p>
+
+                    <div ng-if="node.parsedBacktrace">
+                        <h4>Backtrace</h4>
+                        <div class="table">
+                            <div ng-repeat="trace in node.parsedBacktrace track by $index">
+                                <div>{{trace.fnName || '(anonymous)'}}</div>
+                                <div class="trace"><a href="{{trace.filePath}}" title="{{trace.filePath}}" target="_blank">{{trace.fileName || 'HTML'}}</a>:{{trace.line}}</div>
+                            </div>
+                            <div ng-if="node.parsedBacktrace.length == 0 && node.data.type != 'script loaded'">
+                                <div>can't find any backtrace :/</div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div class="startTime" ng-class="node.data.loadingStep">{{node.data.timestamp | number: 0}} ms</div>
+        </div>
+    </div>
+
+    <div class="backToDashboard"><a href="#" ng-click="backToDashboard()">Back to dashboard</a></div>
+</div>

+ 38 - 0
lib/index.js

@@ -0,0 +1,38 @@
+var Q = require('q');
+
+var Runner = require('./runner');
+
+
+var yellowLabTools = function(url, options) {
+    'use strict';
+
+    var deferred = Q.defer();
+
+    if (!url) {
+
+        deferred.reject('URL missing');
+
+    } else {
+
+        if (url.indexOf('http://') !== 0 && url.indexOf('https://') !== 0) {
+            url = 'http://' + url;
+        }
+
+        var params = {
+            url: url,
+            options: options || {}
+        };
+
+        var runner = new Runner(params)
+            .then(function(data) {
+                deferred.resolve(data);
+            })
+            .fail(function(err) {
+                deferred.reject(err);
+            });
+    }
+
+    return deferred.promise;
+};
+
+module.exports = yellowLabTools;

+ 453 - 0
lib/metadata/policies.js

@@ -0,0 +1,453 @@
+var debug = require('debug')('ylt:policies');
+
+var policies = {
+    "DOMelementsCount": {
+        "tool": "phantomas",
+        "label": "DOM elements count",
+        "message": "<p>A high number of DOM elements means a lot of work for the browser to render the page.</p><p>It also slows down JavaScript DOM queries, as there are more elements to search through.</p>",
+        "isOkThreshold": 1000,
+        "isBadThreshold": 2500,
+        "isAbnormalThreshold": 4000
+    },
+    "DOMelementMaxDepth": {
+        "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": 10,
+        "isBadThreshold": 20,
+        "isAbnormalThreshold": 28
+    },
+    "iframesCount": {
+        "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": 2,
+        "isBadThreshold": 15,
+        "isAbnormalThreshold": 30
+    },
+    "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,
+        "isAbnormalThreshold": 10
+    },
+    "DOMinserts": {
+        "tool": "phantomas",
+        "label": "DOM inserts",
+        "message": "<p>Working with the DOM in JavaScript triggers layout calculations and slows down the page.</p><p>Try, as much as possible, to have an HTML page fully generated by the server instead of making changes with JS.</p>",
+        "isOkThreshold": 10,
+        "isBadThreshold": 400,
+        "isAbnormalThreshold": 1000
+    },
+    "DOMqueries": {
+        "tool": "phantomas",
+        "label": "DOM queries",
+        "message": "<p>DOM queries are like looking in a large catalog of items. Even if the browsers made progress on the performances of queries, websites often make hundreds of them.</p><p>Try to reduce the number of queries by refactoring your JavaScript code.</p><p>Avoid also to have a read query between two write queries. To be able to reduce the number repaints and optimize performances, browsers buffer the DOM writing operations and treat them in bulk. But each time a DOM reading is asked, the browser needs to empty the buffer. This can be particularly slow inside a loop.</p>",
+        "isOkThreshold": 50,
+        "isBadThreshold": 1000,
+        "isAbnormalThreshold": 2000
+    },
+    "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": 200,
+        "isAbnormalThreshold": 500,
+        "takeOffendersFrom": "DOMqueriesDuplicated"
+    },
+    "DOMqueriesWithoutResults": {
+        "tool": "phantomas",
+        "label": "DOM queries without result",
+        "message": "<p>Number of queries that return no result.</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": 100,
+        "isAbnormalThreshold": 200
+    },
+    "eventsBound": {
+        "tool": "phantomas",
+        "label": "Events bound",
+        "message": "<p>Binding too many events has a cost.</p><p>It can be avoided by using \"event delegation\". Instead of binding events on each element one by one, events delegation binds them on the top level document element and uses the bubbling principle. It will imperceptibly slow down the event when it occurs, but the loading of the page will speed-up.</p>",
+        "isOkThreshold": 100,
+        "isBadThreshold": 800,
+        "isAbnormalThreshold": 1500
+    },
+    "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>",
+        "isOkThreshold": 0,
+        "isBadThreshold": 1,
+        "isAbnormalThreshold": 4
+    },
+    "evalCalls": {
+        "tool": "phantomas",
+        "label": "eval calls",
+        "message": "<p>The 'eval' function is slow and is a bad coding practice. Try to get rid of it.</p>",
+        "isOkThreshold": 0,
+        "isBadThreshold": 10,
+        "isAbnormalThreshold": 20
+    },
+    "documentWriteCalls": {
+        "tool": "phantomas",
+        "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": 10,
+        "isAbnormalThreshold": 20
+    },
+    "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": 0,
+        "isBadThreshold": 10,
+        "isAbnormalThreshold": 25
+    },
+    "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": 10,
+        "isBadThreshold": 50,
+        "isAbnormalThreshold": 200
+    },
+    "jQueryVersion": {
+        "label": "jQuery version",
+        "message": "<p>Current latest versions of jQuery are 1.11 (with support for old IE versions) and 2.1 (without).</p><p>Each new version of jQuery optimizes performances. Do not keep an old version of jQuery. Updating can sometimes break a few things, but it is generally quite easy to fix them up. So don't hesitate.</p>",
+        "scoreFn": function(data) {
+            var differentVersions = data.toolsResults.phantomas.metrics.jQueryDifferentVersions;
+
+            if (differentVersions === 0 || differentVersions > 1) {
+                // Not applicable
+                return null;
+            } else {
+                var value = data.toolsResults.phantomas.metrics.jQueryVersion;
+                var score;
+
+                if (value.indexOf('1.11.') === 0 ||
+                    value.indexOf('1.12.') === 0 ||
+                    value.indexOf('2.1.') === 0 ||
+                    value.indexOf('2.2.') === 0 ||
+                    value.indexOf('3.0.') === 0) {
+                    score = 100;
+                } else if (value.indexOf('1.10.') === 0 ||
+                           value.indexOf('2.0.') === 0) {
+                    score = 90;
+                } else if (value.indexOf('1.9.') === 0) {
+                    score = 70;
+                } else if (value.indexOf('1.8.') === 0) {
+                    score = 50;
+                } else if (value.indexOf('1.7.') === 0) {
+                    score = 40;
+                } else if (value.indexOf('1.6.') === 0) {
+                    score = 30;
+                } else if (value.indexOf('1.5.') === 0) {
+                    score = 20;
+                } else if (value.indexOf('1.4.') === 0) {
+                    score = 10;
+                } else if (value.indexOf('1.3.') === 0) {
+                    score = 0;
+                } else if (value.indexOf('1.2.') === 0) {
+                    score = 0;
+                } else {
+                    debug('Unknown jQuery version "%s"', value);
+                    return null;
+                }
+
+                return {
+                    value: value,
+                    score: score,
+                    bad: value < 100,
+                    abnormal: false,
+                    abnormalityScore: 0
+                };
+            }
+        }
+    },
+    "jQueryDifferentVersions": {
+        "tool": "phantomas",
+        "label": "Several versions loaded",
+        "message": "<p>jQuery is a heavy library. You should <b>never<b> load jQuery more than one on the same page.</p>",
+        "isOkThreshold": 1,
+        "isBadThreshold": 2,
+        "isAbnormalThreshold": 2
+    },
+    "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,
+        "isAbnormalThreshold": 1
+    },
+    "cssRules": {
+        "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 files per page.</p>",
+        "isOkThreshold": 500,
+        "isBadThreshold": 2500,
+        "isAbnormalThreshold": 4000
+    },
+    "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.</p>",
+        "isOkThreshold": 0,
+        "isBadThreshold": 500,
+        "isAbnormalThreshold": 2000
+    },
+    "cssComplexSelectorsByAttribute": {
+        "tool": "phantomas",
+        "label": "Complex attributes selector",
+        "message": "<p>Complex attributes selectors are one of these:<ul><li>.foo[type*=bar] (contains bar)</li><li>.foo[type^=bar] (starts with bar)</li><li>.foo[type|=bar] (starts with bar or bar-)</li><li>.foo[type$=bar] (ends with bar)</li><li>.foo[type~=bar baz] (bar or baz)</li></ul></p><p>Their matching process needs more CPU and it has a cost on performances.</p>",
+        "isOkThreshold": 0,
+        "isBadThreshold": 50,
+        "isAbnormalThreshold": 100
+    },
+    "cssImports": {
+        "tool": "phantomas",
+        "label": "Uses of @import",
+        "message": "<p>It’s bad for performance to use @import because CSS files don't get downloaded in parallel.</p><p>You should use &lt;link rel='stylesheet' href='a.css'&gt; instead.</p>",
+        "isOkThreshold": 0,
+        "isBadThreshold": 1,
+        "isAbnormalThreshold": 1
+    },
+    "cssDuplicatedSelectors": {
+        "tool": "phantomas",
+        "label": "Duplicated selectors",
+        "message": "<p>This is when two or more selectors are strictly identical and should be merged.</p>",
+        "isOkThreshold": 0,
+        "isBadThreshold": 40,
+        "isAbnormalThreshold": 80
+    },
+    "cssDuplicatedProperties": {
+        "tool": "phantomas",
+        "label": "Duplicated properties",
+        "message": "<p>This is the number of property definitions duplicated within a selector.</p>",
+        "isOkThreshold": 0,
+        "isBadThreshold": 50,
+        "isAbnormalThreshold": 100
+    },
+    "cssEmptyRules": {
+        "tool": "phantomas",
+        "label": "Empty rules",
+        "message": "<p>Very easy to fix: remove all empty rules.</p>",
+        "isOkThreshold": 0,
+        "isBadThreshold": 40,
+        "isAbnormalThreshold": 100
+    },
+    "cssExpressions": {
+        "tool": "phantomas",
+        "label": "CSS expressions",
+        "message": "<p>Such as: expression( document.body.clientWidth > 600 ? \"600px\" : \"auto\" )</p><p>This is a bad practice as it slows down browsers. There are some simpler CSS3 methods for doing this.</p>",
+        "isOkThreshold": 0,
+        "isBadThreshold": 1,
+        "isAbnormalThreshold": 20
+    },
+    "cssImportants": {
+        "tool": "phantomas",
+        "label": "Uses of !important",
+        "message": "<p>It can be useful, but only as a last resort. It is a bad practice because it overrides the normal cascading logic. The more you use !important, the more you need it again to over-override. This conducts to a poor maintainability.</p>",
+        "isOkThreshold": 0,
+        "isBadThreshold": 50,
+        "isAbnormalThreshold": 150
+    },
+    "cssOldIEFixes": {
+        "tool": "phantomas",
+        "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": 300
+    },
+    "cssOldPropertyPrefixes": {
+        "tool": "phantomas",
+        "label": "Old prefixes",
+        "message": "<p>Many property prefixes such as -moz- or -webkit- are not needed anymore, or by very few people. You can remove them or replace them with the non-prefixed version. This will help reducing your stylesheets weight.</p>",
+        "isOkThreshold": 0,
+        "isBadThreshold": 50,
+        "isAbnormalThreshold": 300
+    },
+    "cssUniversalSelectors": {
+        "tool": "phantomas",
+        "label": "Universal selectors",
+        "message": "<p>Universal selectors are the most expensive CSS selectors.</p><p>More informations <a href=\"http://perfectionkills.com/profiling-css-for-fun-and-profit-optimization-notes/\" target=\"_blank\">here</a>.</p>",
+        "isOkThreshold": 0,
+        "isBadThreshold": 40,
+        "isAbnormalThreshold": 150
+    },
+    "cssRedundantBodySelectors": {
+        "tool": "phantomas",
+        "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": 50,
+        "isAbnormalThreshold": 200
+    },
+    "cssRedundantChildNodesSelectors": {
+        "tool": "phantomas",
+        "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\" element is <b>always</b> inside a \"ul\". Same thing for \"tr td\", \"select option\", ...</p><p>Lowering compexity in CSS selectors can make the page load a little faster.</p>",
+        "isOkThreshold": 0,
+        "isBadThreshold": 50,
+        "isAbnormalThreshold": 200
+    },
+    "requests": {
+        "tool": "phantomas",
+        "label": "Total 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 or icon fonts</li><li>Base64 encode small images in HTML or stylesheets</li><li>Use lazyloading for images</li></ul></p>",
+        "isOkThreshold": 15,
+        "isBadThreshold": 100,
+        "isAbnormalThreshold": 200,
+        "takeOffendersFrom": ["htmlCount", "jsCount", "cssCount", "imageCount", "webfontCount", "videoCount", "jsonCount", "jsonCount"]
+    },
+    "htmlCount": {
+        "tool": "phantomas",
+        "label": "Document count",
+        "message": "<p>The number of HTML pages requests, HTML fragments or iframes.</p>",
+        "isOkThreshold": 10,
+        "isBadThreshold": 20,
+        "isAbnormalThreshold": 30
+    },
+    "jsCount": {
+        "tool": "phantomas",
+        "label": "Script count",
+        "message": "<p>Reduce the number of scripts by concatenating them.</p>",
+        "isOkThreshold": 5,
+        "isBadThreshold": 15,
+        "isAbnormalThreshold": 30
+    },
+    "cssCount": {
+        "tool": "phantomas",
+        "label": "CSS count",
+        "message": "<p>Reduce the number of stylesheets by concatenating them.</p>",
+        "isOkThreshold": 3,
+        "isBadThreshold": 10,
+        "isAbnormalThreshold": 22
+    },
+    "imageCount": {
+        "tool": "phantomas",
+        "label": "Image count",
+        "message": "<p>Reduce the number of images by lazyloading them, by spriting them or by creating an icons font.</p>",
+        "isOkThreshold": 15,
+        "isBadThreshold": 40,
+        "isAbnormalThreshold": 70
+    },
+    "webfontCount": {
+        "tool": "phantomas",
+        "label": "Font count",
+        "message": "<p>Fonts are loaded on the critical path of the head. Load as many as possible.</p>",
+        "isOkThreshold": 0,
+        "isBadThreshold": 3,
+        "isAbnormalThreshold": 5
+    },
+    "videoCount": {
+        "tool": "phantomas",
+        "label": "Videos count",
+        "message": "<p>The number of videos loaded.</p>",
+        "isOkThreshold": 1,
+        "isBadThreshold": 5,
+        "isAbnormalThreshold": 15
+    },
+    "jsonCount": {
+        "tool": "phantomas",
+        "label": "JSON count",
+        "message": "<p>The number of AJAX requests to JSON files or webservices.</p>",
+        "isOkThreshold": 2,
+        "isBadThreshold": 10,
+        "isAbnormalThreshold": 25
+    },
+    "otherCount": {
+        "tool": "phantomas",
+        "label": "Other types of requests",
+        "message": "<p>They can be Flash, XML, music or any unknown format.</p>",
+        "isOkThreshold": 5,
+        "isBadThreshold": 20,
+        "isAbnormalThreshold": 40
+    },
+    "smallJsFiles": {
+        "tool": "phantomas",
+        "label": "Small JS files",
+        "message": "<p>Number of JS assets smaller than 2 KB that could probably be inlined or merged.</p>",
+        "isOkThreshold": 2,
+        "isBadThreshold": 10,
+        "isAbnormalThreshold": 16
+    },
+    "smallCssFiles": {
+        "tool": "phantomas",
+        "label": "Small CSS files",
+        "message": "<p>Number of CSS assets smaller than 2 KB that could probably be inlined or merged.</p>",
+        "isOkThreshold": 0,
+        "isBadThreshold": 8,
+        "isAbnormalThreshold": 12
+    },
+    "smallImages": {
+        "tool": "phantomas",
+        "label": "Small images",
+        "message": "<p>Images smaller than 2 KB that could be base64 encoded or merged into a sprite.</p>",
+        "isOkThreshold": 2,
+        "isBadThreshold": 17,
+        "isAbnormalThreshold": 30
+    },
+    "notFound": {
+        "tool": "phantomas",
+        "label": "404 not found",
+        "message": "<p>404 errors are never cached, so each time a page ask for it, it hits se server. Even if it is behind a CDN or a reverse-proxy cache.</p>",
+        "isOkThreshold": 0,
+        "isBadThreshold": 1,
+        "isAbnormalThreshold": 1
+    },
+    "closedConnections": {
+        "tool": "phantomas",
+        "label": "Connections closed",
+        "message": "<p>This counts the number of requests not keeping the connection alive (specifying \"Connection: close\" in the response headers). It is only counting a request if it is followed by another request on the same domain.</p><p>This is slowing down the next request, because the brower needs to open a new connection to the server, which means a additional round-trip.</p><p>Correct the problem by setting a Keep-Alive header on the guilty server.</p>",
+        "isOkThreshold": 0,
+        "isBadThreshold": 8,
+        "isAbnormalThreshold": 20
+    },
+    "multipleRequests": {
+        "tool": "phantomas",
+        "label": "Duplicated requests",
+        "message": "<p>This only happens when the asset has no cache and is requested more than once on the same page. Be very careful about it.</p>",
+        "isOkThreshold": 0,
+        "isBadThreshold": 5,
+        "isAbnormalThreshold": 10
+    },
+    "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
+    },
+    "cachingNotSpecified": {
+        "tool": "phantomas",
+        "label": "Caching not specified",
+        "message": "<p>When no caching is specified, each browser will handle it differently. Most of the time, it will automatically add a cache for you, but a poor one. You'd better handle it yourself.</p>",
+        "isOkThreshold": 5,
+        "isBadThreshold": 20,
+        "isAbnormalThreshold": 40
+    },
+    "cachingTooShort": {
+        "tool": "phantomas",
+        "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
+    },
+    "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
+    }
+};
+
+module.exports = policies;

+ 114 - 0
lib/metadata/scoreProfileGeneric.json

@@ -0,0 +1,114 @@
+{
+    "categories": {
+        "domComplexity": {
+            "label": "DOM complexity",
+            "policies": {
+                "DOMelementsCount": 1,
+                "DOMelementMaxDepth": 1,
+                "iframesCount": 1,
+                "DOMidDuplicated": 1
+            }
+        },
+        "domManipulations": {
+            "label": "DOM manipulations",
+            "policies": {
+                "DOMinserts": 2,
+                "DOMqueries": 1,
+                "DOMqueriesWithoutResults": 2,
+                "DOMqueriesAvoidable": 2,
+                "eventsBound": 1
+            }
+        },
+        "badJavascript": {
+            "label": "Bad JavaScript",
+            "policies": {
+                "jsErrors": 1,
+                "documentWriteCalls": 2,
+                "consoleMessages": 0.5,
+                "globalVariables": 0.5
+            }
+        },
+        "jQueryVersion": {
+            "label": "jQuery version",
+            "policies": {
+                "jQueryVersion": 5,
+                "jQueryDifferentVersions": 0.1
+            }
+        },
+        "cssSyntaxError": {
+            "label": "CSS syntax errors",
+            "policies": {
+                "cssParsingErrors": 1
+            }
+        },
+        "cssComplexity": {
+            "label": "CSS complexity",
+            "policies": {
+                "cssRules": 2,
+                "cssComplexSelectors": 2,
+                "cssComplexSelectorsByAttribute": 1.5
+            }
+        },
+        "badCSS": {
+            "label": "Bad CSS",
+            "policies": {
+                "cssImports": 3,
+                "cssDuplicatedSelectors": 2,
+                "cssDuplicatedProperties": 1,
+                "cssEmptyRules": 2,
+                "cssExpressions": 1,
+                "cssImportants": 3,
+                "cssOldIEFixes": 1,
+                "cssOldPropertyPrefixes": 1,
+                "cssUniversalSelectors": 1,
+                "cssRedundantBodySelectors": 1,
+                "cssRedundantChildNodesSelectors": 1
+            }
+        },
+        "requests": {
+            "label": "Requests number",
+            "policies": {
+                "requests": 5,
+                "htmlCount": 0,
+                "jsCount": 1,
+                "cssCount": 1,
+                "imageCount": 0,
+                "webfontCount": 2,
+                "videoCount": 0,
+                "jsonCount": 0,
+                "otherCount": 0
+            }
+        },
+        "smallRequests": {
+            "label": "Small requests",
+            "policies": {
+                "smallJsFiles": 1,
+                "smallCssFiles": 1,
+                "smallImages": 1
+            }
+        },
+        "network": {
+            "label": "Network",
+            "policies": {
+                "notFound": 3,
+                "closedConnections": 3,
+                "multipleRequests": 3,
+                "cachingDisabled": 1,
+                "cachingTooShort": 1,
+                "domains": 1
+            }
+        }
+    },
+    "globalScore": {
+        "domComplexity": 1,
+        "domManipulations": 2,
+        "badJavascript": 1,
+        "jQueryVersion": 1,
+        "cssSyntaxError": 1,
+        "cssComplexity": 1,
+        "badCSS": 1,
+        "requests": 3,
+        "smallRequests": 1,
+        "network": 2
+    }
+}

+ 98 - 0
lib/rulesChecker.js

@@ -0,0 +1,98 @@
+var debug = require('debug')('ylt:ruleschecker');
+
+var RulesChecker = function() {
+    'use strict';
+
+    this.check = function(data, policies) {
+        /*jshint loopfunc:true */
+
+        var results = {};
+
+        debug('Starting checking rules');
+
+        for (var metricName in policies) {
+            var policy = policies[metricName];
+            var rule;
+
+            if (policy.tool &&
+                data.toolsResults[policy.tool] &&
+                data.toolsResults[policy.tool].metrics &&
+                (data.toolsResults[policy.tool].metrics[metricName] || data.toolsResults[policy.tool].metrics[metricName] === 0)) {
+
+                    rule = {
+                        value: data.toolsResults[policy.tool].metrics[metricName],
+                        policy: policy
+                    };
+
+                    // Take DOMqueriesAvoidable's offenders from DOMqueriesDuplicated, for example.
+                    if (policy.takeOffendersFrom) {
+                        var fromList = policy.takeOffendersFrom;
+                        var offenders = [];
+                        
+                        // takeOffendersFrom option can be a string or an array of strings.
+                        if (typeof fromList === 'string') {
+                            fromList = [fromList];
+                        }
+                        
+                        fromList.forEach(function(from) {
+                            offenders = offenders.concat(data.toolsResults[policy.tool].offenders[from]);
+                        });
+
+                        data.toolsResults[policy.tool].offenders[metricName] = offenders;
+                    }
+
+                    if (data.toolsResults[policy.tool].offenders &&
+                        data.toolsResults[policy.tool].offenders[metricName] &&
+                        data.toolsResults[policy.tool].offenders[metricName].length > 0) {
+                            rule.offenders = data.toolsResults[policy.tool].offenders[metricName];
+                    }
+
+                    rule.bad = rule.value > policy.isOkThreshold;
+                    rule.abnormal = policy.isAbnormalThreshold && rule.value >= policy.isAbnormalThreshold;
+
+                    // A value between 0 (bad) and 100 (very good).
+                    var score = (policy.isBadThreshold - rule.value) * 100 / (policy.isBadThreshold - policy.isOkThreshold);
+                    rule.score = Math.min(Math.max(Math.round(score), 0), 100);
+
+                    // A value between 0 (abnormal) and negative-infinity (your website is a blackhole)
+                    var abnormalityScore = (policy.isAbnormalThreshold - rule.value) * 100 / (policy.isAbnormalThreshold - policy.isOkThreshold);
+                    rule.abnormalityScore = Math.min(Math.round(abnormalityScore), 0);
+
+                    results[metricName] = rule;
+                    debug('Metric %s calculated. Score: %d', metricName, rule.score);
+
+            
+            } else if (policy.scoreFn) {
+
+                debug('Custom score function for %s', metricName);
+                
+                // Custom score function
+                rule = policy.scoreFn(data);
+
+                // Check returned values (if the result is null, just don't save)
+                if (rule) {
+                    rule.policy = {
+                        label: policy.label,
+                        message: policy.message
+                    };
+
+                    results[metricName] = rule;
+                    debug('Metric %s calculated. Score: %d', metricName, rule.score);
+                } else {
+                    debug('Metric %s is null. Ignored.', metricName);
+                }
+
+            } else {
+
+                debug('Metric %s not found for tool %s', metricName, policy.tool);
+
+            }
+        }
+
+        debug('Rules checking finished');
+
+        return results;
+    };
+};
+
+module.exports = new RulesChecker();

+ 60 - 0
lib/runner.js

@@ -0,0 +1,60 @@
+var Q                       = require('q');
+var debug                   = require('debug')('ylt:runner');
+
+var phantomasWrapper        = require('./tools/phantomas/phantomasWrapper');
+var jsExecutionTransformer  = require('./tools/jsExecutionTransformer');
+var rulesChecker            = require('./rulesChecker');
+var scoreCalculator         = require('./scoreCalculator');
+
+
+var Runner = function(params) {
+    'use strict';
+
+    var deferred = Q.defer();
+
+    // The pivot format
+    var data = {
+        params: params,
+        toolsResults: {}
+    };
+
+    // Execute Phantomas first
+    phantomasWrapper.execute(data).then(function(phantomasResults) {
+        data.toolsResults.phantomas = phantomasResults;
+
+        // Treat the JS Execution Tree from offenders
+        data.javascriptExecutionTree = jsExecutionTransformer.transform(data);
+
+        // Other tools go here
+
+
+        // Rules checker
+        var policies = require('./metadata/policies');
+        data.rules = rulesChecker.check(data, policies);
+
+
+        // Scores calculator
+        var scoreProfileGeneric = require('./metadata/scoreProfileGeneric.json');
+        data.scoreProfiles = {
+            generic : scoreCalculator.calculate(data, scoreProfileGeneric)
+        };
+
+        
+        delete data.toolsResults.phantomas.metrics.javascriptExecutionTree;
+        delete data.toolsResults.phantomas.offenders.javascriptExecutionTree;
+
+        //Finished!
+        deferred.resolve(data);
+
+    }).fail(function(err) {
+        debug('Run failed');
+        debug(err);
+
+        deferred.reject(err);
+    });
+
+
+    return deferred.promise;
+};
+
+module.exports = Runner;

+ 87 - 0
lib/scoreCalculator.js

@@ -0,0 +1,87 @@
+var Q = require('q');
+var debug = require('debug')('ylt:scoreCalculator');
+
+var ScoreCalculator = function() {
+    'use strict';
+
+    this.calculate = function(data, profile) {
+
+        var results = {
+            categories: {}
+        };
+        var categoryScore;
+        var categoryName;
+        var weight;
+
+        debug('Starting calculating scores');
+
+        // Calculate categories
+        for (categoryName in profile.categories) {
+            var categoryResult = {
+                label: profile.categories[categoryName].label
+            };
+
+            categoryScore = new ScoreMerger();
+            var rules = [];
+            var policyScore;
+
+            for (var policyName in profile.categories[categoryName].policies) {
+                weight = profile.categories[categoryName].policies[policyName];
+                
+                if (data.rules[policyName]) {
+                    policyScore = data.rules[policyName].score + (data.rules[policyName].abnormalityScore * 2);
+                    categoryScore.push(policyScore, weight);
+                } else {
+                    debug('Warning: could not find rule %s', policyName);
+                }
+
+                rules.push(policyName);
+            }
+
+            categoryResult.categoryScore = categoryScore.getScore();
+
+            categoryResult.rules = rules;
+            results.categories[categoryName] = categoryResult;
+        }
+
+
+        // Calculate general score
+        var globalScore = new ScoreMerger();
+
+        for (categoryName in profile.globalScore) {
+            weight = profile.globalScore[categoryName];
+
+            if (results.categories[categoryName]) {
+                globalScore.push(results.categories[categoryName].categoryScore, weight);
+            }
+        }
+
+        results.globalScore = Math.round(globalScore.getScore());
+
+
+        debug('Score calculation finished:');
+        debug(results);
+
+        return results;
+    };
+
+
+    var ScoreMerger = function() {
+        var sum = 0;
+        var totalWeight = 0;
+
+        this.push = function(score, weight) {
+            sum += (100 - score) * weight;
+            totalWeight += weight;
+        };
+
+        this.getScore = function() {
+            if (totalWeight === 0) {
+                return 100;
+            }
+            return Math.round(100 - (sum / totalWeight));
+        };
+    };
+};
+
+module.exports = new ScoreCalculator();

+ 226 - 0
lib/server/controllers/apiController.js

@@ -0,0 +1,226 @@
+var debug               = require('debug')('ylt:server');
+
+var ylt                 = require('../../index');
+var RunsQueue           = require('../datastores/runsQueue');
+var RunsDatastore       = require('../datastores/runsDatastore');
+var ResultsDatastore    = require('../datastores/resultsDatastore');
+
+
+var ApiController = function(app) {
+    'use strict';
+
+    var queue = new RunsQueue();
+    var runsDatastore = new RunsDatastore();
+    var resultsDatastore = new ResultsDatastore();
+
+
+
+    // Create a new run
+    app.post('/api/runs', function(req, res) {
+
+        // Grab the test parameters and generate a random run ID
+        var run = {
+            runId: (Date.now()*1000 + Math.round(Math.random()*1000)).toString(36),
+            params: {
+                url: req.body.url,
+                waitForResponse: req.body.waitForResponse !== false && req.body.waitForResponse !== 'false' && req.body.waitForResponse !== 0,
+                partialResult: req.body.partialResult || null
+            }
+        };
+
+        // Add test to the testQueue
+        debug('Adding test %s to the queue', run.runId);
+        var queuePromise = queue.push(run.runId);
+
+
+        // Save the run to the datastore
+        runsDatastore.add(run, queuePromise.startingPosition);
+
+
+        // Listening for position updates
+        queuePromise.progress(function(position) {
+            runsDatastore.updatePosition(run.runId, position);
+        });
+
+        // Let's start the run
+        queuePromise.then(function() {
+
+            runsDatastore.updatePosition(run.runId, 0);
+
+            debug('Launching test %s on %s', run.runId, run.params.url);
+
+            ylt(run.params.url)
+
+                .then(function(data) {
+
+                    debug('Success');
+                    
+
+                    // Save result in datastore
+                    data.runId = run.runId;
+                    resultsDatastore.saveResult(data)
+                        .then(function() {
+
+                            runsDatastore.markAsComplete(run.runId);
+                            
+                            // Send result if the user was waiting
+                            if (run.params.waitForResponse) {
+
+                                // If the user only wants a portion of the result (partialResult option)
+                                switch(run.params.partialResult) {
+                                    case 'generalScores': 
+                                        res.redirect(302, '/api/results/' + run.runId + '/generalScores');
+                                        break;
+                                    case 'rules': 
+                                        res.redirect(302, '/api/results/' + run.runId + '/rules');
+                                        break;
+                                    case 'javascriptExecutionTree':
+                                        res.redirect(302, '/api/results/' + run.runId + '/javascriptExecutionTree');
+                                        break;
+                                    case 'phantomas':
+                                        res.redirect(302, '/api/results/' + run.runId + '/toolsResults/phantomas');
+                                        break;
+                                    default:
+                                        res.redirect(302, '/api/results/' + run.runId);
+                                }
+                            }
+                            
+                        })
+                        .fail(function(err) {
+                            debug('Saving results to resultsDatastore failed:');
+                            debug(err);
+
+                            res.status(500).send('Saving results failed');
+                        });
+
+                })
+
+                .fail(function(err) {
+                    
+                    console.error('Test failed for URL: %s', run.params.url);
+                    console.error(err.toString());
+
+                    runsDatastore.markAsFailed(run.runId, err.toString());
+
+                    res.status(400).send('Bad request');
+                    
+                })
+
+                .finally(function() {
+                    queue.remove(run.runId);
+                });
+
+        }).fail(function(err) {
+            console.error('Error or YLT\'s core instanciation');
+            console.error(err);
+            console.error(err.stack);
+        });
+
+        // The user doesn't not want to wait for the response, sending the run ID only
+        if (!run.params.waitForResponse) {
+            console.log('Sending response without waiting.');
+            res.setHeader('Content-Type', 'application/json');
+            res.send(JSON.stringify({runId: run.runId}));
+        }
+    });
+
+    // Retrive one run by id
+    app.get('/api/runs/:id', function(req, res) {
+        var runId = req.params.id;
+
+        var run = runsDatastore.get(runId);
+
+        if (run) {
+            res.setHeader('Content-Type', 'application/json');
+            res.send(JSON.stringify(run, null, 2));
+        } else {
+            res.status(404).send('Not found');
+        }
+    });
+
+    // Retrieve the list of all runs
+    /*app.get('/api/runs', function(req, res) {
+        // NOT YET
+    });*/
+
+    // Delete one run by id
+    /*app.delete('/api/runs/:id', function(req, res) {
+        deleteRun()
+    });*/
+
+    // Delete all
+    /*app.delete('/api/runs', function(req, res) {
+        purgeRuns()
+    });
+
+    // List all
+    app.get('/api/runs', function(req, res) {
+        listRuns()
+    });
+
+    // Exists
+    app.head('/api/runs/:id', function(req, res) {
+        existsX();
+        // Returns 200 if the result exists or 404 if not
+    });
+    */
+
+    // Retrive one result by id
+    app.get('/api/results/:id', function(req, res) {
+        getPartialResults(req.params.id, res, function(data) {
+            return data;
+        });
+    });
+
+    // Retrieve one result and return only the generalScores part of the response
+    app.get('/api/results/:id/generalScores', function(req, res) {
+        getPartialResults(req.params.id, res, function(data) {
+            return data.scoreProfiles.generic;
+        });
+    });
+
+    app.get('/api/results/:id/generalScores/:scoreProfile', function(req, res) {
+        getPartialResults(req.params.id, res, function(data) {
+            return data.scoreProfiles[req.params.scoreProfile];
+        });
+    });
+
+    app.get('/api/results/:id/rules', function(req, res) {
+        getPartialResults(req.params.id, res, function(data) {
+            return data.rules;
+        });
+    });
+
+    app.get('/api/results/:id/javascriptExecutionTree', function(req, res) {
+        getPartialResults(req.params.id, res, function(data) {
+            return data.javascriptExecutionTree;
+        });
+    });
+
+    app.get('/api/results/:id/toolsResults/phantomas', function(req, res) {
+        getPartialResults(req.params.id, res, function(data) {
+            return data.toolsResults.phantomas;
+        });
+    });
+
+    function getPartialResults(runId, res, partialGetterFn) {
+        resultsDatastore.getResult(runId)
+            .then(function(data) {
+                var results = partialGetterFn(data);
+                
+                if (typeof results === 'undefined') {
+                    res.status(404).send('Not found');
+                    return;
+                }
+
+                res.setHeader('Content-Type', 'application/json');
+                res.send(JSON.stringify(results, null, 2));
+
+            }).fail(function() {
+                res.status(404).send('Not found');
+            });
+    }
+
+};
+
+module.exports = ApiController;

+ 26 - 0
lib/server/controllers/frontController.js

@@ -0,0 +1,26 @@
+var path        = require('path');
+var express     = require('express');
+
+var FrontController = function(app) {
+    'use strict';
+
+    var cacheDuration = 365 * 24 * 60 * 60 * 1000; // One year
+    var assetsPath = (app.get('env') === 'development') ? '../../../front/src' : '../../../front/build';
+    
+    var routes = ['/', '/about', '/result/:runId', '/result/:runId/timeline', '/result/:runId/rule/:policy', '/queue/:runId'];
+    routes.forEach(function(route) {
+        app.get(route, function(req, res) {
+            res.setHeader('Cache-Control', 'public, max-age=20');
+            res.sendFile(path.join(__dirname, assetsPath, 'main.html'));
+        });
+    });
+    
+    app.use('/css', express.static(path.join(__dirname, assetsPath, 'css'), { maxAge: cacheDuration }));
+    app.use('/fonts', express.static(path.join(__dirname, assetsPath, 'fonts'), { maxAge: cacheDuration }));
+    app.use('/img', express.static(path.join(__dirname, assetsPath, 'img'), { maxAge: cacheDuration }));
+    app.use('/js', express.static(path.join(__dirname, assetsPath, 'js'), { maxAge: cacheDuration }));
+    app.use('/views', express.static(path.join(__dirname, assetsPath, 'views'), { maxAge: cacheDuration }));
+    app.use('/bower_components', express.static(path.join(__dirname, '../../../bower_components'), { maxAge: cacheDuration }));
+};
+
+module.exports = FrontController;

+ 89 - 0
lib/server/datastores/resultsDatastore.js

@@ -0,0 +1,89 @@
+var fs          = require('fs');
+var rimraf      = require('rimraf');
+var path        = require('path');
+var Q           = require('q');
+var debug       = require('debug')('ylt:resultsDatastore');
+
+
+function ResultsDatastore() {
+    'use strict';
+
+    var resultFileName = 'results.json';
+    var resultsFolderName = 'results';
+    var resultsDir = path.join(__dirname, '..', '..', '..', resultsFolderName);
+
+
+    this.saveResult = function(testResults) {
+        var promise = createResultFolder(testResults.runId);
+
+        debug('Saving results to disk...');
+
+        promise.then(function() {
+
+            var resultFilePath = path.join(resultsDir, testResults.runId, resultFileName);
+            debug('Destination file is %s', resultFilePath);
+            
+            return Q.nfcall(fs.writeFile, resultFilePath, JSON.stringify(testResults, null, 2));
+        });
+
+        return promise;
+    };
+
+
+    this.getResult = function(runId) {
+
+        var resultFilePath = path.join(resultsDir, runId, resultFileName);
+
+        debug('Reading results (runID = %s) from disk...', runId);
+        
+        return Q.nfcall(fs.readFile, resultFilePath, {encoding: 'utf8'}).then(function(data) {
+            return JSON.parse(data);
+        });
+    };
+
+
+    this.deleteResult = function(runId) {
+        var folder = path.join(resultsDir, runId);
+
+        debug('Deleting results (runID = %s) from disk...', runId);
+
+        return Q.nfcall(rimraf, folder);
+    };
+
+
+    // The folder /results/folderName/
+    function createResultFolder(folderName) {
+        var folder = path.join(resultsDir, folderName);
+
+        debug('Creating the folder %s', folderName);
+
+        return createGlobalFolder().then(function() {
+            return Q.nfcall(fs.mkdir, folder);
+        });
+    }
+
+    // The folder /results/
+    function createGlobalFolder() {
+        var deferred = Q.defer();
+
+        // Create the results folder if it doesn't exist
+        fs.exists(resultsDir, function(exists) {
+            if (exists) {
+                deferred.resolve();
+            } else {
+                debug('Creating the global results folder', resultsDir);
+                fs.mkdir(resultsDir, function(err) {
+                    if (err) {
+                        deferred.reject(err);
+                    } else {
+                        deferred.resolve();
+                    }
+                });
+            }
+        });
+
+        return deferred.promise;
+    }
+}
+
+module.exports = ResultsDatastore;

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

@@ -0,0 +1,106 @@
+
+
+function RunsDatastore() {
+    'use strict';
+
+    // NOT PERSISTING RUNS
+    // For the moment, maybe one day
+    var runs = {};
+
+    var STATUS_AWAITING     = 'awaiting';
+    var STATUS_RUNNING      = 'running';
+    var STATUS_COMPLETE     = 'complete';
+    var STATUS_FAILED       = 'failed';
+
+
+    this.add = function(run, position) {
+        runs[run.runId] = run;
+        this.updatePosition(run.runId, position);
+    };
+
+
+    this.get = function(runId) {
+        return runs[runId];
+    };
+
+    
+    this.updatePosition = function(runId, position) {
+        var run = runs[runId];
+        
+        if (position > 0) {
+            run.status = {
+                statusCode: STATUS_AWAITING,
+                position: position
+            };
+        } else {
+            run.status = {
+                statusCode: STATUS_RUNNING
+            };
+        }
+
+        runs[runId] = run;
+    };
+
+
+    this.markAsComplete = function(runId) {
+        var run = runs[runId];
+
+        run.status = {
+            statusCode: STATUS_COMPLETE
+        };
+
+        runs[runId] = run;
+    };
+
+
+    this.markAsFailed = function(runId, err) {
+        var run = runs[runId];
+
+        var errorMessage;
+        switch(err) {
+            case '252':
+                errorMessage = "Error 252: page timeout in Phantomas";
+                break;
+            case '253':
+                errorMessage = "Error 253: Phantomas config error";
+                break;
+            case '254':
+                errorMessage = "Error 254: page loading failed in Phantomas";
+                break;
+            case '255':
+                errorMessage = "Error 255: Phantomas error";
+                break;
+            case '1001':
+                errorMessage = "Error 1001: JavaScript profiling failed";
+                break;
+            case '1002':
+                errorMessage = "Error 1002: missing Phantomas metrics";
+                break;
+            default:
+                errorMessage = err;
+        }
+
+        run.status = {
+            statusCode: STATUS_FAILED,
+            error: errorMessage
+        };
+
+        runs[runId] = run;
+    };
+
+
+    this.delete = function(runId) {
+        delete runs[runId];
+    };
+
+
+    this.list = function() {
+        var runsArray = [];
+        Object.keys(runs).forEach(function(key) {
+            runsArray.push(runs[key]);
+        });
+        return runsArray;
+    };
+}
+
+module.exports = RunsDatastore;

+ 80 - 0
lib/server/datastores/runsQueue.js

@@ -0,0 +1,80 @@
+var Q = require('q');
+
+
+function RunsQueue() {
+    'use strict';
+
+    var queue = [];
+
+
+    this.push = function(runId) {
+        var deferred = Q.defer();
+        var startingPosition = queue.length;
+
+        if (startingPosition === 0) {
+            
+            // The queue is empty, let's run immediatly
+            queue.push({
+                runId: runId
+            });
+            
+            deferred.resolve();
+
+        } else {
+            
+            queue.push({
+                runId: runId,
+                positionChangedCallback: function(position) {
+                    deferred.notify(position);
+                },
+                itIsTimeCallback: function() {
+                    deferred.resolve();
+                }
+            });
+        }
+
+        var promise = deferred.promise;
+        promise.startingPosition = startingPosition;
+        return promise;
+    };
+
+
+    this.getPosition = function(runId) {
+        // Position 0 means it's a work in progress (a run is removed AFTER it is finished, not before)
+        var position = -1;
+
+        queue.some(function(run, index) {
+            if (run.runId === runId) {
+                position = index;
+                return true;
+            }
+            return false;
+        });
+
+        return position;
+    };
+
+
+    this.remove = function(runId) {
+        var position = this.getPosition(runId);
+        if (position >= 0) {
+            queue.splice(position, 1);
+        }
+
+        // Update other runs' positions
+        queue.forEach(function(run, index) {
+            if (index === 0 && run.itIsTimeCallback) {
+                run.itIsTimeCallback();
+            } else if (index > 0 && run.positionChangedCallback) {
+                run.positionChangedCallback(index);
+            }
+        });
+
+    };
+
+    this.length = function() {
+        return queue.length;
+    };
+}
+
+module.exports = RunsQueue;

+ 85 - 0
lib/server/middlewares/apiLimitsMiddleware.js

@@ -0,0 +1,85 @@
+var config      = require('../../../server_config/settings.json');
+
+var debug       = require('debug')('apiLimitsMiddleware');
+
+
+var apiLimitsMiddleware = function(req, res, next) {
+    'use strict';
+
+    debug('Entering API Limits Middleware with IP address %s', req.connection.remoteAddress);
+
+    if (req.path.indexOf('/api/') === 0 && !res.locals.hasApiKey) {
+        
+        
+        if (req.path === '/api/runs') {
+            
+            if (!runsTable.accepts(req.connection.remoteAddress)) {
+                // Sorry :/
+                debug('Too many tests launched from IP address %s', req.connection.remoteAddress);
+                res.status(429).send('Too many requests');
+                return;
+            }
+
+        }
+
+        if (!callsTable.accepts(req.connection.remoteAddress)) {
+            // Sorry :/
+            debug('Too many API requests from IP address %s', req.connection.remoteAddress);
+            res.status(429).send('Too many requests');
+            return;
+        }
+
+        debug('Not blocked by the API limits');
+        // It's ok for the moment
+    }
+
+    next();
+};
+
+
+var RecordTable = function(maxPerDay) {
+    var table = {};
+
+    // Check if the user overpassed the limit and save its visit
+    this.accepts = function(ipAddress) {
+        if (table[ipAddress]) {
+            
+            this.cleanEntry(ipAddress);
+
+            debug('%d visits in the last 24 hours', table[ipAddress].length);
+
+            if (table[ipAddress].length >= maxPerDay) {
+                return false;
+            } else {
+                table[ipAddress].push(Date.now());
+            }
+
+        } else {
+            table[ipAddress] = [];
+            table[ipAddress].push(Date.now());
+        }
+
+        return true;
+    };
+
+    // Clean the table for this guy
+    this.cleanEntry = function(ipAddress) {
+        table[ipAddress] = table[ipAddress].filter(function(date) {
+            return date > Date.now() - 1000*60*60*24;
+        });
+    };
+
+    // Clean the entire table once in a while
+    this.removeOld = function() {
+        for (var ipAddress in table) {
+            this.cleanEntry(ipAddress);
+        }
+    };
+
+};
+
+// Init the records tables
+var runsTable = new RecordTable(config.maxAnonymousRunsPerDay);
+var callsTable = new RecordTable(config.maxAnonymousCallsPerDay);
+
+module.exports = apiLimitsMiddleware;

+ 42 - 0
lib/server/middlewares/authMiddleware.js

@@ -0,0 +1,42 @@
+var config      = require('../../../server_config/settings.json');
+
+var debug       = require('debug')('authMiddleware');
+
+
+var authMiddleware = function(req, res, next) {
+    'use strict';
+
+    if (req.path.indexOf('/api/') === 0) {
+        
+        
+        if (req.headers && req.headers['x-api-key']) {
+
+            // Test if it's an authorized key
+            if (isApiKeyValid(req.headers['x-api-key'])) {
+                
+                // Come in!
+                debug('Authorized key: %s', req.headers['x-api-key']);
+                res.locals.hasApiKey = true;
+            
+            } else {
+                
+                // Sorry :/
+                debug('Unauthorized key %s', req.headers['x-api-key']);
+                res.status(401).send('Unauthorized');
+                return;
+            }
+        } else {
+            debug('No authorization key');
+            // It's ok for the moment but you might be blocked by the apiLimitsMiddleware, dude
+        }
+    }
+
+    next();
+};
+
+
+function isApiKeyValid(apiKey) {
+    return (config.authorizedKeys[apiKey]) ? true : false;
+}
+
+module.exports = authMiddleware;

+ 62 - 0
lib/tools/jsExecutionTransformer.js

@@ -0,0 +1,62 @@
+var debug = require('debug')('ylt:jsExecutionTransformer');
+
+var jsExecutionTransformer = function() {
+
+    this.transform = function(data) {
+        var javascriptExecutionTree = {};
+
+        debug('Starting JS execution transformation');
+
+        try {
+            javascriptExecutionTree = JSON.parse(data.toolsResults.phantomas.offenders.javascriptExecutionTree[0]);
+        
+            if (javascriptExecutionTree.children) {
+                javascriptExecutionTree.children.forEach(function(node) {
+                    
+                    // Mark abnormal things with a warning flag
+                    var contextLenght = (node.data.callDetails && node.data.callDetails.context) ? node.data.callDetails.context.length : null;
+                    if ((node.data.type === 'jQuery - bind' && contextLenght > 5) ||
+                            node.data.resultsNumber === 0 ||
+                            contextLenght === 0) {
+                        node.warning = true;
+                    }
+
+                    // Mark errors with an error flag
+                    if (node.data.type === 'error' || node.data.type === 'jQuery version change') {
+                        node.error = true;
+                    }
+
+                    // Mark a performance flag
+                    if (['domInteractive', 'domContentLoaded', 'domContentLoadedEnd', 'domComplete'].indexOf(node.data.type) >= 0) {
+                        node.windowPerformance = true;
+                    }
+
+                    // Read the execution tree and adjust the navigation timings (cause their not very well synchronised)
+                    switch(node.data.type) {
+                        case 'domInteractive':
+                            data.toolsResults.phantomas.metrics.domInteractive = node.data.timestamp;
+                            break;
+                        case 'domContentLoaded':
+                            data.toolsResults.phantomas.metrics.domContentLoaded = node.data.timestamp;
+                            break;
+                        case 'domContentLoadedEnd':
+                            data.toolsResults.phantomas.metrics.domContentLoadedEnd = node.data.timestamp;
+                            break;
+                        case 'domComplete':
+                            data.toolsResults.phantomas.metrics.domComplete = node.data.timestamp;
+                            break;
+                    }
+                });
+            }
+
+            debug('JS execution transformation complete');
+
+        } catch(err) {
+            throw err;
+        }
+
+        return javascriptExecutionTree;
+    };
+};
+
+module.exports = new jsExecutionTransformer();

+ 17 - 3
phantomas_custom/core/scopeYLT/scopeYLT.js → lib/tools/phantomas/custom_modules/core/scopeYLT/scopeYLT.js

@@ -7,7 +7,7 @@
  */
 /* global document: true, window: true */
 
-exports.version = '0.1';
+exports.version = '0.2';
 
 exports.module = function(phantomas) {
     'use strict';
@@ -42,10 +42,11 @@ exports.module = function(phantomas) {
                             return false;
                         }
 
-                        phantomas.log('Attaching a spy to "' + fn + '" function...');
+                        phantomas.log('Attaching a YLT spy to "' + fn + '" function...');
 
                         obj[fn] = function() {
                             var result;
+                            var err;
                             
                             // Before
                             if (enabled) {
@@ -54,15 +55,28 @@ exports.module = function(phantomas) {
 
                             // Execute
                             try {
+
                                 result = origFn.apply(this, arguments);
+
                             } catch(e) {
+
+                                // Catching the err for the moment, because we need to make sure the callbackAfter function is called.
+
                                 phantomas.log('Error catched on spyed function "' + fn + '": ' + e);
                                 phantomas.log(arguments);
+
+                                err = e;
+
                             } finally {
 
                                 // After
                                 if (enabled && callbackAfter) {
-                                    callbackAfter.call(this, result);
+                                    callbackAfter.call(this, result, arguments);
+                                }
+
+                                if (err) {
+                                    phantomas.log('Re-throwing the error');
+                                    throw err;
                                 }
                             }
 

+ 57 - 14
phantomas_custom/modules/domQYLT/domQYLT.js → lib/tools/phantomas/custom_modules/modules/domQYLT/domQYLT.js

@@ -3,12 +3,13 @@
  */
 /* global Element: true, Document: true, Node: true, window: true */
 
-exports.version = '0.9.a';
+exports.version = '0.10.a';
 
 exports.module = function(phantomas) {
     'use strict';
 
     phantomas.setMetric('DOMqueries'); // @desc number of all DOM queries @offenders
+    phantomas.setMetric('DOMqueriesWithoutResults'); // @desc number of DOM queries that retutned nothing @offenders
     phantomas.setMetric('DOMqueriesById'); // @desc number of document.getElementById calls
     phantomas.setMetric('DOMqueriesByClassName'); // @desc number of document.getElementsByClassName calls
     phantomas.setMetric('DOMqueriesByTagName'); // @desc number of document.getElementsByTagName calls
@@ -21,14 +22,13 @@ exports.module = function(phantomas) {
     phantomas.once('init', function() {
         phantomas.evaluate(function() {
             (function(phantomas) {
-                function querySpy(type, query, fnName, context) {
-                    phantomas.emit('domQuery', type, query, fnName, context); // @desc DOM query has been made
+                function querySpy(type, query, fnName, context, hasNoResults) {
+                    phantomas.emit('domQuery', type, query, fnName, context, hasNoResults); // @desc DOM query has been made
                 }
 
                 phantomas.spy(Document.prototype, 'getElementById', function(id) {
                     phantomas.incrMetric('DOMqueriesById');
                     phantomas.addOffender('DOMqueriesById', '#%s (in %s)', id, '#document');
-                    querySpy('id', '#' + id, 'getElementById', '#document');
 
                     phantomas.enterContext({
                         type: 'getElementById',
@@ -41,9 +41,13 @@ exports.module = function(phantomas) {
                         backtrace: phantomas.getBacktrace()
                     });
 
-                }, function(result) {
+                }, function(result, args) {
+                    var id = args[0];
+
+                    querySpy('id', '#' + id, 'getElementById', '#document', (result === null));
+
                     var moreData = {
-                        resultsNumber : result ? 1 : 0
+                        resultsNumber : (result === null) ? 0 : 1
                     };
                     phantomas.leaveContext(moreData);
                 });
@@ -56,7 +60,6 @@ exports.module = function(phantomas) {
 
                     phantomas.incrMetric('DOMqueriesByClassName');
                     phantomas.addOffender('DOMqueriesByClassName', '.%s (in %s)', className, context);
-                    querySpy('class', '.' + className, 'getElementsByClassName', context);
 
                     phantomas.enterContext({
                         type: 'getElementsByClassName',
@@ -70,7 +73,14 @@ exports.module = function(phantomas) {
                     });
                 }
 
-                function selectorClassNameAfter(result) {
+                function selectorClassNameAfter(result, args) {
+                    /*jshint validthis: true */
+
+                    var className = args[0];
+                    var context = phantomas.getDOMPath(this);
+
+                    querySpy('class', '.' + className, 'getElementsByClassName', context, (result.length === 0));
+                    
                     var moreData = {
                         resultsNumber : (result && result.length > 0) ? result.length : 0
                     };
@@ -88,7 +98,6 @@ exports.module = function(phantomas) {
 
                     phantomas.incrMetric('DOMqueriesByTagName');
                     phantomas.addOffender('DOMqueriesByTagName', '%s (in %s)', tagName, context);
-                    querySpy('tag name', tagName.toLowerCase(), 'getElementsByTagName', context);
 
                     phantomas.enterContext({
                         type: 'getElementsByTagName',
@@ -102,7 +111,14 @@ exports.module = function(phantomas) {
                     });
                 }
 
-                function selectorTagNameSpyAfter(result) {
+                function selectorTagNameSpyAfter(result, args) {
+                    /*jshint validthis: true */
+                    
+                    var tagName = args[0];
+                    var context = phantomas.getDOMPath(this);
+
+                    querySpy('tag name', tagName.toLowerCase(), 'getElementsByTagName', context, (result.length === 0));
+                    
                     var moreData = {
                         resultsNumber : (result && result.length > 0) ? result.length : 0
                     };
@@ -112,11 +128,11 @@ exports.module = function(phantomas) {
                 phantomas.spy(Document.prototype, 'getElementsByTagName', selectorTagNameSpyBefore, selectorTagNameSpyAfter);
                 phantomas.spy(Element.prototype, 'getElementsByTagName', selectorTagNameSpyBefore, selectorTagNameSpyAfter);
 
+
                 // selector queries
                 function selectorQuerySpy(selector, context) {
                     phantomas.incrMetric('DOMqueriesByQuerySelectorAll');
                     phantomas.addOffender('DOMqueriesByQuerySelectorAll', '%s (in %s)', selector, context);
-                    querySpy('selector', selector, 'querySelectorAll', context);
                 }
 
                 function selectorQuerySpyBefore(selector) {
@@ -137,7 +153,14 @@ exports.module = function(phantomas) {
                     });
                 }
 
-                function selectorQuerySpyAfter(result) {
+                function selectorQuerySpyAfter(result, args) {
+                    /*jshint validthis: true */
+
+                    var selector = args[0];
+                    var context = phantomas.getDOMPath(this);
+
+                    querySpy('selector', selector, 'querySelectorAll', context, (!result || result.length === 0));
+                    
                     var moreData = {
                         resultsNumber : result ? 1 : 0
                     };
@@ -162,7 +185,14 @@ exports.module = function(phantomas) {
                     });
                 }
 
-                function selectorAllQuerySpryAfter(result) {
+                function selectorAllQuerySpryAfter(result, args) {
+                    /*jshint validthis: true */
+
+                    var selector = args[0];
+                    var context = phantomas.getDOMPath(this);
+
+                    querySpy('selector', selector, 'querySelectorAll', context, (!result || result.length === 0));
+
                     var moreData = {
                         resultsNumber : (result && result.length > 0) ? result.length : 0
                     };
@@ -244,9 +274,22 @@ exports.module = function(phantomas) {
         });
     });
 
+    // report DOM queries that return no results (issue #420)
+    phantomas.on('domQuery', function(type, query, fnName, context, hasNoResults) {
+        // ignore DOM queries within DOM fragments (used internally by jQuery)
+        if (context.indexOf('body') !== 0 && context.indexOf('#document') !== 0) {
+            return;
+        }
+
+        if (hasNoResults === true) {
+            phantomas.incrMetric('DOMqueriesWithoutResults');
+            phantomas.addOffender('DOMqueriesWithoutResults', '%s (in %s) using %s', query, context, fnName);
+        }
+    });
+
     // count DOM queries by either ID, tag name, class name and selector query
     // @see https://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-document-doctype
-    var Collection = require('../../../node_modules/phantomas/lib/collection'),
+    var Collection = require('../../../../../../node_modules/phantomas/lib/collection'),
         DOMqueries = new Collection();
 
     phantomas.on('domQuery', function(type, query, fnName, context) {

+ 0 - 0
phantomas_custom/modules/eventListYLT/eventListYLT.js → lib/tools/phantomas/custom_modules/modules/eventListYLT/eventListYLT.js


+ 1 - 1
phantomas_custom/modules/jQYLT/jQYLT.js → lib/tools/phantomas/custom_modules/modules/jQYLT/jQYLT.js

@@ -276,7 +276,7 @@ exports.module = function(phantomas) {
 
 
     // count Sizzle calls to detect duplicated queries
-    var Collection = require('../../../node_modules/phantomas/lib/collection'),
+    var Collection = require('../../../../../../node_modules/phantomas/lib/collection'),
         sizzleCalls = new Collection(),
         jQueryLoading = new Collection();
 

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

@@ -0,0 +1,57 @@
+/**
+ * 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/
+ */
+/* global document: true, window: true */
+
+exports.version = '0.1.a';
+
+exports.module = function(phantomas) {
+    'use strict';
+
+    phantomas.setMetric('documentWriteCalls'); //@desc number of calls to either document.write or document.writeln
+    phantomas.setMetric('evalCalls'); // @desc number of calls to eval (either direct or via setTimeout / setInterval)
+
+    phantomas.once('init', function() {
+        phantomas.evaluate(function() {
+            (function(phantomas) {
+                function report(msg, caller, backtrace, metric) {
+                    phantomas.log(msg + ': from ' + caller + '!');
+                    phantomas.log('Backtrace: ' + backtrace);
+                    phantomas.incrMetric(metric);
+                }
+
+                // spy calls to eval()
+                /*phantomas.spy(window, 'eval', function(code) {
+                    report('eval() called directly', phantomas.getCaller(), phantomas.getBacktrace(), 'evalCalls');
+                    phantomas.log('eval\'ed code: ' + (code || '').substring(0, 150) + '(...)');
+                });*/
+
+                // 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);
+        });
+    });
+};

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


+ 0 - 0
phantomas_custom/modules/jsFileLoadYLT/jsFileLoadYLT.js → lib/tools/phantomas/custom_modules/modules/jsFileLoadYLT/jsFileLoadYLT.js


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


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


+ 133 - 0
lib/tools/phantomas/phantomasWrapper.js

@@ -0,0 +1,133 @@
+var async           = require('async');
+var Q               = require('q');
+var ps              = require('ps-node');
+var debug           = require('debug')('ylt:phantomaswrapper');
+var phantomas       = require('phantomas');
+
+
+var PhantomasWrapper = function() {
+    'use strict';
+
+    /**
+     * This is the phantomas launcher. It merges user chosen options into the default options
+     * Available options :
+     *
+     * - timeout : in seconds (default 60)
+     * - jsDeepAnalysis : should we inspect subrequests in the javascript execution tree?
+     *
+     */
+    this.execute = function(data) {
+
+        var deferred = Q.defer();
+
+        var task = data.params;
+
+        var options = {
+            // Cusomizable options
+            timeout: task.options.timeout || 60,
+            'js-deep-analysis': task.options.jsDeepAnalysis || false,
+
+            // Mandatory
+            reporter: 'json:pretty',
+            'analyze-css': true,
+            'skip-modules': [
+                'blockDomains', // not needed
+                'domMutations', // not compatible with webkit
+                'domQueries', // overriden
+                'eventListeners', // overridden
+                'filmStrip', // not needed
+                'har', // not needed for the moment
+                'javaScriptBottlenecks', // needs to be launched after custom module scopeYLT,
+                'jserrors', // overridden
+                'pageSource', // not needed
+                'screenshot', // not needed for the moment
+                'waitForSelector', // not needed
+                'windowPerformance' // overriden
+            ].join(','),
+            'include-dirs': [
+                'lib/tools/phantomas/custom_modules/core',
+                'lib/tools/phantomas/custom_modules/modules'
+            ].join(',')
+        };
+
+        // Output the command line for debugging purpose
+        debug('If you want to reproduce the phantomas task only, copy the following command line:');
+        var optionsString = '';
+        for (var opt in options) {
+            optionsString += ' ' + '--' + opt + '=' + options[opt];
+        }
+        debug('node node_modules/phantomas/bin/phantomas.js --url=' + task.url + optionsString + ' --verbose');
+
+        // Kill the application if nothing happens
+        var phantomasPid;
+        var killer = setTimeout(function() {
+            debug('Killing the app because the test on %s was launched %d seconds ago', task.url, 5*options.timeout);
+            // If in server mode, forever will restart the server
+            
+            // Kill the Phantomas process first
+            if (phantomasPid) {
+                ps.kill(phantomasPid, function(err) {
+                    if (err) {
+                        debug('Could not kill Phantomas process %s', phantomasPid);
+                    }
+                    else {
+                        debug('Phantomas process %s was correctly killed', phantomasPid);
+                    }
+
+                    // Then suicide.
+                    process.exit(1);
+                });
+            }
+
+        }, 5*options.timeout*1000);
+
+        // It's time to launch the test!!!
+        var triesNumber = 2;
+
+        async.retry(triesNumber, function(cb) {
+            var process = phantomas(task.url, options, function(err, json, results) {
+                debug('Returning from Phantomas');
+
+                // Adding some YellowLabTools errors here
+                if (json && json.metrics && !json.metrics.javascriptExecutionTree) {
+                    err = 1001;
+                }
+
+                if (!err && (!json || !json.metrics)) {
+                    err = 1002;
+                }
+
+                // Don't cancel test if it is a timeout and we've got some results
+                if (err === 252 && json) {
+                    debug('Timeout after ' + options.timeout + ' seconds. But it\'s not a problem, the test is valid.');
+                    err = null;
+                }
+
+                if (err) {
+                    debug('Attempt failed. Error code ' + err);
+                }
+
+                cb(err, json);
+            });
+            
+            phantomasPid = process.pid;
+
+        }, function(err, json) {
+
+            clearTimeout(killer);
+
+            if (err) {
+                debug('All ' + triesNumber + ' attemps failed for the test');
+                deferred.reject(err);
+            } else {
+
+                // Success!!!
+                deferred.resolve(json);
+            }
+        });
+
+        return deferred.promise;
+    };
+};
+
+module.exports = new PhantomasWrapper();

+ 36 - 14
package.json

@@ -1,31 +1,53 @@
 {
   "name": "yellowlabtools",
-  "version": "1.0.1",
+  "version": "1.1.0",
   "repository": {
     "type": "git",
     "url": "git://github.com/gmetais/YellowLabTools.git"
   },
+  "bin": {
+    "yellowlabtools": "./bin/cli.js"
+  },
+  "main": "./lib/index.js",
   "dependencies": {
-    "phantomas": "1.7.0",
-    "express": "~4.10.1",
     "async": "~0.9.0",
-    "socket.io": "~1.2.0",
-    "body-parser": "~1.9.2",
-    "compression": "~1.2.0"
+    "body-parser": "~1.10.0",
+    "compression": "~1.2.2",
+    "cors": "^2.5.2",
+    "debug": "~2.1.0",
+    "express": "~4.10.6",
+    "phantomas": "1.8.0",
+    "ps-node": "0.0.3",
+    "q": "~1.1.2",
+    "rimraf": "~2.2.8"
   },
   "devDependencies": {
+    "chai": "^1.10.0",
     "grunt": "^0.4.5",
-    "grunt-contrib-jshint": "^0.10.0",
-    "matchdep": "^0.3.0",
-    "grunt-mocha-test": "^0.12.2",
+    "grunt-blanket": "^0.0.8",
     "grunt-contrib-clean": "^0.6.0",
+    "grunt-contrib-concat": "^0.5.0",
     "grunt-contrib-copy": "^0.7.0",
-    "grunt-blanket": "^0.0.8",
-    "chai": "^1.9.2",
-    "mocha": "^2.0.1",
-    "phantomjs": "^1.9.10",
+    "grunt-contrib-cssmin": "^0.11.0",
+    "grunt-contrib-htmlmin": "^0.3.0",
+    "grunt-contrib-jshint": "^0.10.0",
+    "grunt-contrib-less": "^0.12.0",
+    "grunt-contrib-uglify": "^0.7.0",
+    "grunt-env": "^0.4.2",
+    "grunt-express": "^1.4.1",
+    "grunt-filerev": "^2.1.2",
     "grunt-fontsmith": "^0.9.1",
-    "grunt-contrib-less": "^0.12.0"
+    "grunt-inline-angular-templates": "^0.1.5",
+    "grunt-line-remover": "^0.0.2",
+    "grunt-mocha-test": "^0.12.4",
+    "grunt-replace": "^0.8.0",
+    "grunt-usemin": "^3.0.0",
+    "matchdep": "^0.3.0",
+    "mocha": "^2.1.0",
+    "phantomjs": "^1.9.13",
+    "request": "^2.51.0",
+    "sinon": "^1.12.1",
+    "sinon-chai": "^2.6.0"
   },
   "scripts": {
     "test": "grunt test"

+ 0 - 138
phantomas_custom/modules/analyzeStyleYLT/analyzeStyleYLT.js

@@ -1,138 +0,0 @@
-/**
- * Adds CSS related metrics using analyze-css NPM module
- *
- * @see https://github.com/macbre/analyze-css
- *
- * Run phantomas with --analyze-css option to use this module
- *
- * setMetric('cssBase64Length') @desc total length of base64-encoded data in CSS source (will warn about base64-encoded data bigger than 4 kB) @optional @offenders
- * setMetric('cssRedundantBodySelectors') @desc number of redundant body selectors (e.g. body .foo, section body h2, but not body > h1) @optional @offenders
- * setMetric('redundantChildNodesSelectors') @desc number of redundant child nodes selectors @optional @offenders
- * setMetric('cssComments') @desc number of comments in CSS source @optional @offenders
- * setMetric('cssCommentsLength') @desc length of comments content in CSS source @optional
- * setMetric('cssComplexSelectors') @desc number of complex selectors (consisting of more than three expressions, e.g. header ul li .foo) @optional @offenders
- * setMetric('cssComplexSelectorsByAttribute') @desc  [number] number of selectors with complex matching by attribute (e.g. [class$="foo"]) @optional @offenders
- * setMetric('cssDuplicatedSelectors') @desc number of CSS selectors defined more than once in CSS source @optional @offenders
- * setMetric('cssDuplicatedProperties') @desc number of CSS property definitions duplicated within a selector @optional @offenders
- * setMetric('cssEmptyRules') @desc number of rules with no properties (e.g. .foo { }) @optional @offenders
- * setMetric('cssExpressions') @desc number of rules with CSS expressions (e.g. expression( document.body.clientWidth > 600 ? "600px" : "auto" )) @optional @offenders
- * setMetric('cssOldIEFixes') @desc number of fixes for old versions of Internet Explorer (e.g. * html .foo {} and .foo { *zoom: 1 }) @optional @offenders
- * setMetric('cssImports') @desc number of @import rules @optional @offenders
- * setMetric('cssImportants') @desc number of properties with value forced by !important @optional @offenders
- * setMetric('cssMediaQueries') @desc number of media queries (e.g. @media screen and (min-width: 1370px)) @optional @offenders
- * setMetric('cssOldPropertyPrefixes') @desc number of properties with no longer needed vendor prefix, powered by data provided by autoprefixer (e.g. --moz-border-radius) @optional @offenders
- * setMetric('cssQualifiedSelectors') @desc number of qualified selectors (e.g. header#nav, .foo#bar, h1.title) @optional @offenders
- * setMetric('cssSpecificityIdAvg') @desc average specificity for ID @optional
- * setMetric('cssSpecificityIdTotal') @desc total specificity for ID @optional
- * setMetric('cssSpecificityClassAvg') @desc average specificity for class, pseudo-class or attribute @optional
- * setMetric('cssSpecificityClassTotal') @desc total specificity for class, pseudo-class or attribute @optional
- * setMetric('cssSpecificityTagAvg') @desc average specificity for element @optional
- * setMetric('cssSpecificityTagTotal') @desc total specificity for element @optional
- * setMetric('cssSelectorsByAttribute') @desc number of selectors by attribute (e.g. .foo[value=bar]) @optional
- * setMetric('cssSelectorsByClass') @desc number of selectors by class @optional
- * setMetric('cssSelectorsById') @desc number of selectors by ID @optional
- * setMetric('cssSelectorsByPseudo') @desc number of pseudo-selectors (e,g. :hover) @optional
- * setMetric('cssSelectorsByTag') @desc number of selectors by tag name @optional
- * setMetric('cssUniversalSelectors') @desc number of selectors trying to match every element (e.g. .foo > *) @optional @offenders
- * setMetric('cssLength') @desc length of CSS source (in bytes) @optional
- * setMetric('cssRules') @desc number of rules (e.g. .foo, .bar { color: red } is counted as one rule) @optional
- * setMetric('cssSelectors') @desc number of selectors (e.g. .foo, .bar { color: red } is counted as two selectors - .foo and .bar) @optional
- * setMetric('cssDeclarations') @desc number of declarations (e.g. .foo, .bar { color: red } is counted as one declaration - color: red) @optional
- */
-
-exports.version = '0.3.a';
-
-exports.module = function(phantomas) {
-    'use strict';
-
-    if (!phantomas.getParam('analyze-css')) {
-        phantomas.log('To enable CSS in-depth metrics please run phantomas with --analyze-css option');
-        return;
-    }
-
-    function ucfirst(str) {
-        // http://kevin.vanzonneveld.net
-        // +   original by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
-        // +   bugfixed by: Onno Marsman
-        // +   improved by: Brett Zamir (http://brett-zamir.me)
-        // *     example 1: ucfirst('kevin van zonneveld');
-        // *     returns 1: 'Kevin van zonneveld'
-        str += '';
-        var f = str.charAt(0).toUpperCase();
-        return f + str.substr(1);
-    }
-
-    var isWindows = (require('system').os.name === 'windows'),
-        binary = isWindows ? 'analyze-css.cmd' : 'analyze-css';
-
-    phantomas.setMetric('cssParsingErrors'); // @desc number of CSS files (or embeded CSS) that failed to be parse by analyze-css @optional
-
-    var runningScripts = 0;
-
-    phantomas.on('recv', function(entry, res) {
-        if (entry.isCSS) {
-            phantomas.log('CSS: analyzing <%s>...', entry.url);
-
-            // run analyze-css "binary" installed by npm
-            phantomas.runScript('node_modules/.bin/' + binary, ['--url', entry.url, '--json'], function(err, results) {
-                runningScripts --;
-
-                if (err !== null) {
-                    phantomas.log('analyzeCss: sub-process failed!');
-
-                    // report failed CSS parsing (issue #494(
-                    var offender = entry.url;
-                    if (err.indexOf('CSS parsing failed') > 0) {
-                        offender += ' (' + err.trim() + ')';
-                    }
-
-                    phantomas.incrMetric('cssParsingErrors');
-                    phantomas.addOffender('cssParsingErrors', offender);
-                    return;
-                }
-
-                phantomas.log('analyzeCss: using ' + results.generator);
-
-                var metrics = results.metrics || {},
-                    offenders = results.offenders || {};
-
-                Object.keys(metrics).forEach(function(metric) {
-                    var metricPrefixed = 'css' + ucfirst(metric);
-
-                    // increase metrics
-                    phantomas.incrMetric(metricPrefixed, metrics[metric]);
-
-                    // and add offenders
-                    if (typeof offenders[metric] !== 'undefined') {
-                        offenders[metric].forEach(function(msg) {
-                            phantomas.addOffender(metricPrefixed, msg);
-                        });
-                    }
-                });
-            });
-
-            runningScripts ++;
-        }
-    });
-
-    phantomas.reportQueuePush(function(done) {
-        phantomas.on('loadFinished', function() {
-            var intervalId,
-                pollFn;
-
-            phantomas.log('analyze-css: making sure all processes are finished');
-
-            pollFn = function() {
-                if (runningScripts === 0) {
-                    clearInterval(intervalId);
-                    done();
-                } else {
-                    phantomas.log('analyze-css: waiting for %d processes to finish', runningScripts);
-                }
-            };
-
-            intervalId = setInterval(pollFn, 200);
-            pollFn();
-        });
-    });
-};

+ 0 - 135
phantomas_custom/modules/domComplexYLT/domComplexYLT.js

@@ -1,135 +0,0 @@
-/**
- * Analyzes DOM complexity
- */
-/* global document: true, Node: true, window: true */
-
-exports.version = '1.0.a';
-
-exports.module = function(phantomas) {
-    'use strict';
-
-    // total length of HTML comments (including <!-- --> brackets)
-    phantomas.setMetric('commentsSize'); // @desc the size of HTML comments on the page @offenders
-
-    // total length of text nodes with whitespaces only (i.e. pretty formatting of HTML)
-    phantomas.setMetric('whiteSpacesSize'); // @desc the size of text nodes with whitespaces only
-
-    // count all tags
-    phantomas.setMetric('DOMelementsCount'); // @desc total number of HTML element nodes
-    phantomas.setMetric('DOMelementMaxDepth'); // @desc maximum level on nesting of HTML element node
-
-    // nodes with inlines CSS (style attribute)
-    phantomas.setMetric('nodesWithInlineCSS'); // @desc number of nodes with inline CSS styling (with style attribute) @offenders
-
-    // images
-    phantomas.setMetric('imagesScaledDown'); // @desc number of <img> nodes that have images scaled down in HTML @offenders
-    phantomas.setMetric('imagesWithoutDimensions'); // @desc number of <img> nodes without both width and height attribute @offenders
-
-    // duplicated ID (issue #392)
-    phantomas.setMetric('DOMidDuplicated'); // @desc number of duplicated IDs found in DOM
-
-    var Collection = require('../../../node_modules/phantomas/lib/collection'),
-        DOMids = new Collection();
-
-    phantomas.on('domId', function(id) {
-        DOMids.push(id);
-    });
-
-    // HTML size
-    phantomas.on('report', function() {
-        phantomas.setMetricEvaluate('bodyHTMLSize', function() { // @desc the size of body tag content (document.body.innerHTML.length)
-            return document.body && document.body.innerHTML.length || 0;
-        });
-
-        phantomas.evaluate(function() {
-            (function(phantomas) {
-                var runner = new phantomas.nodeRunner(),
-                    whitespacesRegExp = /^\s+$/,
-                    DOMelementMaxDepth = 0,
-                    DOMelementMaxDepthElts = [],
-                    size = 0;
-
-                runner.walk(document.body, function(node, depth) {
-                    switch (node.nodeType) {
-                        case Node.COMMENT_NODE:
-                            size = node.textContent.length + 7; // '<!--' + '-->'.length
-                            phantomas.incrMetric('commentsSize', size);
-
-                            // log HTML comments bigger than 64 characters
-                            if (size > 64) {
-                                phantomas.addOffender('commentsSize', phantomas.getDOMPath(node) + ' (' + size + ' characters)');
-                            }
-                            break;
-
-                        case Node.ELEMENT_NODE:
-                            phantomas.incrMetric('DOMelementsCount');
-                            
-                            if (depth > DOMelementMaxDepth) {
-                                DOMelementMaxDepth = depth;
-                                DOMelementMaxDepthElts = [phantomas.getDOMPath(node)];
-                            } else if (depth === DOMelementMaxDepth) {
-                                DOMelementMaxDepthElts.push(phantomas.getDOMPath(node));
-                            }
-
-                            // report duplicated ID (issue #392)
-                            if (node.id) {
-                                phantomas.emit('domId', node.id);
-                            }
-
-                            // ignore inline <script> tags
-                            if (node.nodeName === 'SCRIPT') {
-                                return false;
-                            }
-
-                            // images
-                            if (node.nodeName === 'IMG') {
-                                if (!node.hasAttribute('width') || !node.hasAttribute('height')) {
-                                    phantomas.incrMetric('imagesWithoutDimensions');
-                                    phantomas.addOffender('imagesWithoutDimensions', '%s <%s>', phantomas.getDOMPath(node), node.src);
-                                }
-                                if (node.naturalHeight && node.naturalWidth && node.height && node.width) {
-                                    if (node.naturalHeight > node.height || node.naturalWidth > node.width) {
-                                        phantomas.incrMetric('imagesScaledDown');
-                                        phantomas.addOffender('imagesScaledDown', '%s (%dx%d -> %dx%d)', node.src, node.naturalWidth, node.naturalHeight, node.width, node.height);
-                                    }
-                                }
-                            }
-
-                            // count nodes with inline CSS
-                            if (node.hasAttribute('style')) {
-                                phantomas.incrMetric('nodesWithInlineCSS');
-                                phantomas.addOffender('nodesWithInlineCSS', phantomas.getDOMPath(node) + ' (' + node.getAttribute('style')  + ')');
-                            }
-
-                            break;
-
-                        case Node.TEXT_NODE:
-                            if (whitespacesRegExp.test(node.textContent)) {
-                                phantomas.incrMetric('whiteSpacesSize', node.textContent.length);
-                            }
-                            break;
-                    }
-                });
-
-                phantomas.setMetric('DOMelementMaxDepth', DOMelementMaxDepth);
-                DOMelementMaxDepthElts.forEach(function(path) {
-                    phantomas.addOffender('DOMelementMaxDepth', path);
-                });
-
-                phantomas.spyEnabled(false, 'counting iframes and images');
-
-                // count <iframe> tags
-                phantomas.setMetric('iframesCount', document.querySelectorAll('iframe').length); // @desc number of iframe nodes
-
-                phantomas.spyEnabled(true);
-            }(window.__phantomas));
-        });
-
-        DOMids.sort().forEach(function(id, cnt) {
-            if (cnt > 1) {
-                phantomas.incrMetric('DOMidDuplicated');
-                phantomas.addOffender('DOMidDuplicated', '%s: %d occurrences', id, cnt);
-            }
-        });
-    });
-};

+ 0 - 37
phantomas_custom/modules/keepAlive/keepAlive.js

@@ -1,37 +0,0 @@
-/**
- * Analyzes if HTTP responses keep the connections alive.
- */
-
-exports.version = '0.1';
-
-exports.module = function(phantomas) {
-    'use strict';
-
-    phantomas.setMetric('closedConnections'); // @desc requests not keeping the connection alive and slowing down the next request @offenders
-
-    var closedConnectionHosts = {};
-
-    phantomas.on('recv', function(entry, res) {
-        var connectionHeader = (entry.headers.Connection || '').toLowerCase();
-        // Taking the protocol in account, in case the same domain is called with two different protocols.
-        var host = entry.protocol + '://' + entry.domain;
-
-        if (connectionHeader.indexOf('close') >= 0) {
-            // Don't blame it immediatly, wait to see if the connection is needed a second time.
-            closedConnectionHosts[host] = entry.url;
-        }
-    });
-
-    phantomas.on('send', function(entry, res) {
-        var host = entry.protocol + '://' + entry.domain;
-        var previousClosedConnection = closedConnectionHosts[host];
-        
-        if (previousClosedConnection) {
-            // There was a closed connection. We can blame it safely now!
-            phantomas.incrMetric('closedConnections');
-            phantomas.addOffender('closedConnections', previousClosedConnection);
-
-            closedConnectionHosts[host] = null;
-        }
-    });
-};

BIN
screenshot.png


+ 0 - 60
server.js

@@ -1,60 +0,0 @@
-// Config file
-var settings                = require('./server_config/settings.json');
-var pkg                     = require('./package.json');
-
-// Libraries
-var fs                      = require('fs');
-var async                   = require('async');
-var express                 = require('express');
-var app                     = express();
-var server                  = require('http').createServer(app);
-var io                      = require('socket.io').listen(server);
-var bodyParser              = require('body-parser');
-var compress                = require('compression');
-
-// Internals
-var indexController         = require('./app/node_controllers/indexController');
-var launchTestController    = require('./app/node_controllers/launchTestController');
-var resultsController       = require('./app/node_controllers/resultsController');
-var waitingQueueSocket      = require('./app/node_controllers/waitingQueueSocket');
-var testQueue               = require('./app/lib/testQueue');
-
-app.use(compress());
-app.use(bodyParser.urlencoded({ extended: false }));
-
-
-// Redirect www.yellowlab.tools to yellowlab.tools (for SEO)
-app.all('*', function(req, res, next) {
-    if (req.hostname && req.hostname.match(/^www\.yellowlab\.tools/) !== null) {
-        res.redirect('http://' + req.hostname.replace(/^www\.yellowlab\.tools/, 'yellowlab.tools') + req.url);
-    } else {
-        next();
-    }
-});
-
-// Routes definition
-app.get('/',                    function(req, res) { indexController(req, res, settings.googleAnalyticsId, pkg.version); });
-app.post('/launchTest',         function(req, res) { launchTestController(req, res, testQueue, settings.googleAnalyticsId); });
-app.get('/results/:testId',     function(req, res) { resultsController(req, res, settings.googleAnalyticsId); });
-
-
-// Static files
-app.use('/public',              express.static(__dirname + '/app/public'));
-app.use('/bower_components',    express.static(__dirname + '/bower_components'));
-
-
-// Socket.io
-io.on('connection', function(socket){
-    waitingQueueSocket(socket, testQueue);
-});
-
-// Create the results folder if it doesn't exist
-var resultsPath = 'results';
-if (!fs.existsSync(resultsPath)) {
-    fs.mkdirSync(resultsPath);
-}
-
-// Launch the server
-server.listen(settings.serverPort, function() {
-    console.log('Listening on port %d', server.address().port);
-});

+ 7 - 4
server_config/server_install.sh

@@ -12,8 +12,8 @@ sudo apt-get install -y nodejs
 source ~/.profile
 
 # Installation of some packages globally
-npm install bower -g
-npm install forever -g
+npm install bower forever grunt-cli phantomjs -g
+source ~/.profile
 
 # Installation of YellowLabTools
 sudo mkdir /space
@@ -21,10 +21,13 @@ sudo chown $USER /space
 cd /space
 git clone https://github.com/gmetais/YellowLabTools.git --branch master
 cd YellowLabTools
-npm install --production
+npm install
 bower install --config.interactive=false --allow-root
 
+# Front-end compilation
+grunt build
+
 # Start the server
 rm server_config/settings.json
 cp server_config/settings-prod.json server_config/settings.json
-forever start -c "node --stack-size=65500" server.js
+NODE_ENV=production forever start -c "node --stack-size=65500" bin/server.js

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است