Prechádzať zdrojové kódy

Merge pull request #344 from YellowLabTools/develop

Split YLT into two repositories
Gaël Métais 4 rokov pred
rodič
commit
e795b3369d
69 zmenil súbory, kde vykonal 11 pridanie a 6382 odobranie
  1. 0 3
      .gitignore
  2. 0 18
      .travis.yml
  3. 0 252
      Gruntfile.js
  4. 1 1
      README.md
  5. 0 49
      bin/cli.js
  6. 0 18
      front/src/css/about.css
  7. 0 212
      front/src/css/dashboard.css
  8. 0 196
      front/src/css/index.css
  9. 0 280
      front/src/css/main.css
  10. 0 21
      front/src/css/queue.css
  11. 0 277
      front/src/css/rule.css
  12. 0 17
      front/src/css/screenshot.css
  13. BIN
      front/src/img/favicon-fail.png
  14. BIN
      front/src/img/favicon-success.png
  15. BIN
      front/src/img/favicon.png
  16. BIN
      front/src/img/logo-large.png
  17. 0 85
      front/src/js/app.js
  18. 0 37
      front/src/js/controllers/dashboardCtrl.js
  19. 0 23
      front/src/js/controllers/indexCtrl.js
  20. 0 100
      front/src/js/controllers/queueCtrl.js
  21. 0 78
      front/src/js/controllers/ruleCtrl.js
  22. 0 30
      front/src/js/controllers/screenshotCtrl.js
  23. 0 33
      front/src/js/directives/gradeDirective.js
  24. 0 307
      front/src/js/directives/offendersDirectives.js
  25. 0 7
      front/src/js/models/resultsFactory.js
  26. 0 7
      front/src/js/models/runsFactory.js
  27. 0 64
      front/src/js/services/apiService.js
  28. 0 31
      front/src/js/services/menuService.js
  29. 0 24
      front/src/js/services/settingsService.js
  30. 0 20
      front/src/less/about.less
  31. 0 199
      front/src/less/dashboard.less
  32. 0 223
      front/src/less/index.less
  33. 0 298
      front/src/less/main.less
  34. 0 24
      front/src/less/queue.less
  35. 0 307
      front/src/less/rule.less
  36. 0 16
      front/src/less/screenshot.less
  37. 0 66
      front/src/main.html
  38. 0 13
      front/src/views/about.html
  39. 0 72
      front/src/views/dashboard.html
  40. 0 117
      front/src/views/index.html
  41. 0 46
      front/src/views/queue.html
  42. 0 7
      front/src/views/resultSubHeader.html
  43. 0 514
      front/src/views/rule.html
  44. 0 13
      front/src/views/screenshot.html
  45. 4 1
      lib/index.js
  46. 0 141
      lib/screenshotHandler.js
  47. 0 352
      lib/server/controllers/apiController.js
  48. 0 45
      lib/server/controllers/frontController.js
  49. 0 136
      lib/server/datastores/resultsDatastore.js
  50. 0 122
      lib/server/datastores/runsDatastore.js
  51. 0 90
      lib/server/datastores/runsQueue.js
  52. 0 95
      lib/server/middlewares/apiLimitsMiddleware.js
  53. 0 42
      lib/server/middlewares/authMiddleware.js
  54. 0 12
      lib/server/middlewares/wwwRedirectMiddleware.js
  55. 0 1
      lib/tools/phantomas/phantomasWrapper.js
  56. 6 41
      package.json
  57. 0 8
      server_config/error50x.html
  58. 0 13
      server_config/maintenance.js
  59. 0 28
      server_config/server_install.sh
  60. 0 24
      server_config/server_update.sh
  61. 0 23
      server_config/settings-prod.json
  62. 0 23
      server_config/settings.json
  63. 0 681
      test/api/apiTest.js
  64. 0 121
      test/api/resultsDatastoreTest.js
  65. 0 82
      test/api/runsDatastoreTest.js
  66. 0 68
      test/api/runsQueueTest.js
  67. 0 78
      test/api/screenshotHandlerTest.js
  68. 0 8
      test/gatling/README.md
  69. 0 42
      test/gatling/YLTWebInterfaceSimulation.scala

+ 0 - 3
.gitignore

@@ -2,9 +2,6 @@ node_modules
 package-lock.json
 package-lock.json
 .tmp
 .tmp
 tmp
 tmp
-.vagrant
-results/*
 coverage
 coverage
-front/build
 package-lock.json
 package-lock.json
 har.json
 har.json

+ 0 - 18
.travis.yml

@@ -1,18 +0,0 @@
-language: node_js
-sudo: false
-node_js:
-  - "12.18"
-  - "14.7"
-env:
-  - CXX=g++-4.8
-addons:
-  apt:
-    sources:
-      - ubuntu-toolchain-r-test
-    packages:
-      - g++-4.8
-before_install:
-  - "npm install -g npm"
-  - "npm install -g grunt-cli"
-install:
-  - "npm install"

+ 0 - 252
Gruntfile.js

@@ -1,252 +0,0 @@
-module.exports = function(grunt) {
-
-    // Load all grunt modules
-    require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks);
-
-    // Tell our Express server that Grunt launched it
-    process.env.GRUNTED = true;
-
-    // Project configuration.
-    grunt.initConfig({
-        pkg: grunt.file.readJSON('package.json'),
-        settings: grunt.file.readJSON('./server_config/settings.json'),
-        
-        less: {
-            all: {
-                files: [
-                    {
-                        expand: true,
-                        cwd: 'front/src/less/',
-                        src: ['**/*.less'],
-                        dest: 'front/src/css/',
-                        ext: '.css'
-                    }
-                ]
-            }
-        },
-        jshint: {
-            all: [
-                '*.js',
-                'app/lib/*.js',
-                'bin/*.js',
-                'lib/**/*.js',
-                'app/nodeControllers/*.js',
-                'app/public/scripts/*.js',
-                'phantomas_custom/**/*.js',
-                'test/api/*.js',
-                'test/core/*.js',
-                'test/fixtures/*.js',
-                'front/src/js/**/*.js'
-            ],
-            options: {
-                esversion: 6
-            }
-        },
-        clean: {
-            tmp: {
-                src: ['.tmp']
-            },
-            dev: {
-                src: ['front/src/css']
-            },
-            build: {
-                src: ['front/build']
-            }
-        },
-        copy: {
-            build: {
-                files: [
-                    {src: ['./front/src/main.html'], dest: './front/build/main.html'},
-                    {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'},
-                ]
-            },
-            favicons: {
-                files: [
-                    {src: ['./front/src/img/favicon.png'], dest: './front/build/img/favicon.png'},
-                    {src: ['./front/src/img/favicon-fail.png'], dest: './front/build/img/favicon-fail.png'},
-                    {src: ['./front/src/img/favicon-success.png'], dest: './front/build/img/favicon-success.png'},
-                ]
-            }
-        },
-        mochaTest: {
-            test: {
-                options: {
-                    reporter: 'spec',
-                },
-                src: ['test/core/*.js', 'test/api/*.js']
-            },
-            'test-current-work': {
-                options: {
-                    reporter: 'spec',
-                },
-                src: ['test/core/mediaQueriesCheckerTest.js']
-            }
-        },
-        env: {
-            dev: {
-                NODE_ENV: 'development'
-            },
-            built: {
-                NODE_ENV: 'production'
-            }
-        },
-        express: {
-            dev: {
-                options: {
-                    port: 8383,
-                    server: './bin/server.js',
-                    serverreload: true,
-                    showStack: true
-                }
-            },
-            built: {
-                options: {
-                    port: 8383,
-                    server: './bin/server.js',
-                    serverreload: true,
-                    showStack: true
-                }
-            },
-            test: {
-                options: {
-                    port: 8387,
-                    server: './bin/server.js',
-                    showStack: true
-                }
-            },
-            'test-current-work': {
-                options: {
-                    port: 8387,
-                    server: './bin/server.js',
-                    showStack: true
-                }
-            },
-            testSuite: {
-                options: {
-                    port: 8388,
-                    bases: 'test/www'
-                }
-            }
-        },
-        useminPrepare: {
-            html: './front/src/main.html',
-            options: {
-                dest: './front/build',
-                root: ['./', './front/src']
-            }
-        },
-        usemin: {
-            html: './front/build/main.html',
-            css: './front/build/css/*.css',
-            options: {
-                assetsDirs: ['front/build']
-            }
-        },
-        htmlmin: {
-            options: {
-                removeComments: true,
-                collapseWhitespace: true,
-                conservativeCollapse: true
-            },
-            main: {
-                files: [{
-                    expand: true,
-                    cwd: './front/build/',
-                    src: 'main.html',
-                    flatten: true,
-                    dest: './front/build'
-                }]
-            },
-            views: {
-                files: [{
-                    expand: true,
-                    cwd: './front/src/views',
-                    src: '*.html',
-                    flatten: true,
-                    dest: '.tmp/views/'
-                }]
-            }
-        },
-        inline_angular_templates: {
-            build: {
-                options: {
-                    base: '.tmp',
-                    method: 'append',
-                    unescape: {
-                        '&lt;': '<',
-                        '&gt;': '>'
-                    }
-                },
-                files: {
-                    './front/build/main.html': ['.tmp/views/*.html']
-                }
-            }
-        },
-        filerev: {
-            options: {
-                algorithm: 'md5',
-                length: 8
-            },
-            assets: {
-                src: './front/build/*/*.*'
-            }
-        }
-    });
-
-    // Custom task that sets a variable for tests
-    grunt.registerTask('test-settings', function() {
-        process.env.IS_TEST = true;
-    });
-
-    grunt.registerTask('build', [
-        'jshint',
-        'clean:build',
-        'copy:build',
-        'less',
-        'useminPrepare',
-        'concat',
-        'uglify',
-        'cssmin',
-        'htmlmin:views',
-        'inline_angular_templates',
-        'filerev',
-        'usemin',
-        'htmlmin:main',
-        'clean:tmp',
-        'copy:favicons'
-    ]);
-
-    grunt.registerTask('hint', [
-        'jshint'
-    ]);
-
-    grunt.registerTask('dev', [
-        'env:dev',
-        'express:dev'
-    ]);
-
-    grunt.registerTask('built', [
-        'env:built',
-        'express:built'
-    ]);
-
-    grunt.registerTask('test', [
-        'test-settings',
-        'build',
-        'express:testSuite',
-        'express:test',
-        'mochaTest:test',
-        'clean:tmp'
-    ]);
-
-    grunt.registerTask('test-current-work', [
-        'test-settings',
-        'jshint',
-        'express:testSuite',
-        'express:test-current-work',
-        'mochaTest:test-current-work',
-        'clean:tmp'
-    ]);
-
-};

+ 1 - 1
README.md

@@ -70,7 +70,7 @@ By the way, it's free because I am a geek, not businessmen. In return, you can a
 
 
 If your project is not accessible from outside or if you want to test your localhost, you might want to run your own instance of Yellow Lab Tools.
 If your project is not accessible from outside or if you want to test your localhost, you might want to run your own instance of Yellow Lab Tools.
 
 
-The classical way is to clone the project's GitHub repository and run it on Linux of MacOS. The documentation is [here](https://github.com/YellowLabTools/YellowLabTools/wiki/Install-your-private-server).
+The classical way is to clone the YLT server's GitHub repository and run it on Linux or MacOS. The documentation is [here](https://github.com/YellowLabTools/YellowLabTools/wiki/Install-your-private-server).
 
 
 The new recommended solution is to run Yellow Lab Tools inside a Docker virtual machine. My friend Ousama Ben Younes maintains [this ready-to-use Docker image based on Alpine](https://github.com/ousamabenyounes/docker-yellowlabtools)).
 The new recommended solution is to run Yellow Lab Tools inside a Docker virtual machine. My friend Ousama Ben Younes maintains [this ready-to-use Docker image based on Alpine](https://github.com/ousamabenyounes/docker-yellowlabtools)).
 
 

+ 0 - 49
bin/cli.js

@@ -1,49 +0,0 @@
-var express                 = require('express');
-var app                     = express();
-var server                  = require('http').createServer(app);
-var bodyParser              = require('body-parser');
-var compress                = require('compression');
-var cors                    = require('cors');
-
-var authMiddleware          = require('../lib/server/middlewares/authMiddleware');
-var apiLimitsMiddleware     = require('../lib/server/middlewares/apiLimitsMiddleware');
-var wwwRedirectMiddleware   = require('../lib/server/middlewares/wwwRedirectMiddleware');
-
-
-// Middlewares
-app.use(compress());
-app.use(bodyParser.json());
-app.use(cors());
-app.use(wwwRedirectMiddleware);
-app.use(authMiddleware);
-app.use(apiLimitsMiddleware);
-app.use(bodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded
-
-
-// EJS HTML engine
-app.engine('.html', require('ejs').__express);
-app.set('view engine', 'ejs');
-
-
-// Initialize the controllers
-var apiController           = require('../lib/server/controllers/apiController')(app);
-var frontController         = require('../lib/server/controllers/frontController')(app);
-
-
-// Let's start the server!
-if (!process.env.GRUNTED) {
-    var settings = require('../server_config/settings.json');
-
-    app.locals.baseUrl = settings.baseUrl;
-
-    server.listen(settings.serverPort, function() {
-        console.log('Listening on port %d', server.address().port);
-
-        // For the tests
-        if (server.startTests) {
-            server.startTests();
-        }
-    });
-}
-
-module.exports = app;

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

@@ -1,18 +0,0 @@
-.about {
-  margin: 3em auto;
-  width: 80%;
-}
-@media (min-width: 640px) {
-  .about {
-    width: 50%;
-  }
-}
-.about p {
-  margin: 2em;
-}
-.about a {
-  color: #fff;
-}
-.sponsor {
-  color: #ffa319;
-}

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

@@ -1,212 +0,0 @@
-.testedUrl {
-  color: inherit;
-}
-.summary {
-  text-align: center;
-}
-.summary .globalScore {
-  margin: 3em auto;
-}
-.summary .globalScore .globalGrade {
-  margin: 0.5 auto;
-  width: 2.5em;
-  height: 2.5em;
-  line-height: 2.5em;
-  border-radius: 0.5em;
-  font-size: 3em;
-  vertical-align: middle;
-}
-.summary .globalScore .on100 {
-  font-size: 1.2em;
-  margin: 0.5em 0 1em;
-}
-.summary .globalScore .screenshotWrapper:hover {
-  opacity: 0.75;
-}
-.summary .globalScore .screenshotWrapper:hover:after {
-  position: absolute;
-  width: 1.25em;
-  height: 1.25em;
-  top: 0.7em;
-  left: 1.55em;
-  font-size: 3em;
-  color: #FFF;
-  background: #000;
-  border-radius: 0.2em;
-  text-align: center;
-  content: "+";
-  opacity: 0.85;
-}
-.summary .globalScore .screenshotWrapper.phone:hover:after {
-  top: 1.7em;
-  left: 0.64em;
-}
-.summary .globalScore .screenshotWrapper.tablet:hover:after {
-  top: 1.5em;
-  left: 0.9em;
-}
-@media (min-width: 820px) {
-  .summary .globalScore {
-    width: 65%;
-    display: table;
-  }
-  .summary .globalScore > div {
-    display: table-cell;
-    width: 50%;
-    vertical-align: middle;
-  }
-}
-.summary .notations {
-  width: 100%;
-  display: table;
-  margin: 0 0 1.5em;
-  border-spacing: 0 1em;
-}
-@media (min-width: 820px) {
-  .summary .notations {
-    width: 80%;
-    margin: 0 10% 1.5em;
-    border-spacing: 1em;
-  }
-}
-.summary .notations > div {
-  display: table-row;
-}
-.summary .notations > div > div {
-  vertical-align: middle;
-}
-@media (min-width: 820px) {
-  .summary .notations > div > div {
-    display: table-cell;
-    height: 2.5em;
-  }
-}
-.summary .notations .category {
-  font-size: 1.2em;
-  width: 50%;
-  float: left;
-  text-align: left;
-  margin: 0.5em 0.25em;
-}
-@media (min-width: 820px) {
-  .summary .notations .category {
-    width: 20%;
-    text-align: center;
-    float: none;
-  }
-}
-.summary .notations .criteria {
-  font-weight: normal;
-}
-@media (min-width: 820px) {
-  .summary .notations .criteria {
-    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;
-  margin: 0.2em;
-  font-size: 1.5em;
-  text-align: center;
-  border-radius: 0.5em;
-  float: right;
-}
-@media (min-width: 820px) {
-  .summary .notations .A.categoryScore,
-  .summary .notations .B.categoryScore,
-  .summary .notations .C.categoryScore,
-  .summary .notations .D.categoryScore,
-  .summary .notations .E.categoryScore,
-  .summary .notations .F.categoryScore,
-  .summary .notations .NA.categoryScore {
-    float: none;
-    font-size: 2em;
-  }
-}
-.summary .notations .grade .A,
-.summary .notations .grade .B,
-.summary .notations .grade .C,
-.summary .notations .grade .D,
-.summary .notations .grade .E,
-.summary .notations .grade .F,
-.summary .notations .grade .NA {
-  width: 1em;
-  height: 1em;
-  font-size: 1em;
-  color: transparent;
-  margin: 0 auto;
-  border-radius: 0.5em;
-}
-.summary .notations .criteria .table {
-  width: 100%;
-}
-.summary .notations .criteria .table > a {
-  text-decoration: none;
-  color: inherit;
-}
-.summary .notations .criteria .table > a:hover > div {
-  background: #d8ebe0;
-  cursor: pointer;
-}
-.summary .notations .criteria .table > a:hover > div.info {
-  background: #FFF;
-}
-.summary .notations .criteria .table > a:hover > div.info svg {
-  fill: #d8ebe0;
-}
-.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%;
-  white-space: nowrap;
-  text-align: center;
-  vertical-align: middle;
-}
-.summary .notations .warning .label,
-.summary .notations .warning .result {
-  color: #FF1919;
-}
-.summary .notations .icon-warning svg {
-  fill: #FF1919;
-  margin: -2px 0;
-}
-.summary .notations .criteria .info {
-  display: none;
-}
-@media (min-width: 820px) {
-  .summary .notations .criteria .info {
-    display: table-cell;
-    width: 2%;
-    text-align: center;
-    vertical-align: middle;
-    background: #FFF;
-    padding-left: 0.1em;
-    padding-right: 0.1em;
-  }
-}
-.summary .notations .criteria .info svg {
-  fill: transparent;
-}
-.summary .sponsor {
-  font-size: 0.9em;
-  margin-bottom: 4em;
-  color: #ffa319;
-}
-.summary .sponsor a {
-  color: inherit;
-}

+ 0 - 196
front/src/css/index.css

@@ -1,196 +0,0 @@
-.promess {
-  padding: 0em 2em;
-  margin-bottom: 0.5em;
-  font-weight: normal;
-  font-size: 1.2em;
-}
-.price {
-  padding: 0em 2em 3em;
-  margin-top: 0em;
-  font-size: 0.9em;
-}
-.url {
-  width: 50%;
-}
-.launchBtn {
-  background: #ffa319;
-  color: #fff;
-}
-.launchBtn:focus {
-  background: #e74c3c;
-}
-.launchBtn.disabled {
-  background: #f1bd70;
-}
-.launchBtn.disabled:focus {
-  color: #ddd;
-}
-.settings {
-  width: 50%;
-  margin: 0 auto;
-}
-.settings input,
-.settings select {
-  font-size: 1em;
-}
-.settings input[type=text],
-.settings input[type=password],
-.settings textarea {
-  width: 100%;
-  min-width: 4em;
-}
-.device {
-  margin-top: 3em;
-}
-.device .item {
-  display: inline-block;
-  margin: 1em 0.75em;
-  width: 5.5em;
-  height: 5.5em;
-  color: #FFF;
-  border: 1px solid #FFF;
-  padding: 1px;
-  border-radius: 0.5em;
-  cursor: pointer;
-  text-decoration: none;
-  font-size: 0.8em;
-}
-.device .item > svg {
-  display: block;
-  margin: 0.6em auto 0.3em;
-  fill: #fff;
-}
-.device .item.active {
-  color: #ffa319;
-  border: 2px solid #ffa319;
-  padding: 0;
-}
-.device .item.active > svg {
-  fill: #ffa319;
-}
-.device .item:hover {
-  color: #ffa319;
-}
-.device .item:hover > svg {
-  fill: #ffa319;
-}
-.settingsTooltip {
-  position: relative;
-}
-.settingsTooltip svg {
-  vertical-align: text-top;
-}
-.settingsTooltip div {
-  display: none;
-  position: absolute;
-  padding: 0.5em;
-  width: 25em;
-  background: #FFF;
-  color: #000;
-  font-size: 0.8em;
-  border-radius: 1em;
-  border: 2px solid #ffa319;
-  white-space: normal;
-  word-break: break-all;
-  word-break: break-word;
-  z-index: 2;
-}
-.settingsTooltip:hover div {
-  display: block;
-}
-.showAdvanced {
-  display: inline-block;
-  margin-top: 2em;
-  color: #FFF;
-  text-decoration: none;
-  font-size: 0.9em;
-}
-.showAdvanced:hover {
-  color: #ffa319;
-}
-.currentSettings {
-  font-size: 0.9em;
-}
-.currentSettings span {
-  color: #ffa319;
-}
-.currentSettings span:after {
-  color: #FFF;
-  content: ",";
-}
-.currentSettings span:last-child:after {
-  content: "";
-}
-.advanced {
-  margin: 1em 0 0;
-  display: table;
-  width: 100%;
-  text-align: left;
-  border-spacing: 0.75em;
-}
-.advanced > div {
-  display: table-row;
-}
-.advanced > div > div {
-  display: table-cell;
-  width: 75%;
-}
-.advanced > div > div.label {
-  width: 25%;
-  white-space: nowrap;
-  vertical-align: middle;
-}
-.advanced .subTable {
-  display: table;
-  border-spacing: 0;
-  width: 100%;
-}
-.advanced .subTable > div {
-  display: table-row;
-}
-.advanced .subTable > div > div {
-  display: table-cell;
-  padding: 0 0 0.75em;
-}
-.features {
-  display: table;
-  width: 50%;
-  margin: 6em auto 0;
-  font-size: 0.9em;
-  color: #8abfaf;
-}
-@media (min-width: 640px) {
-  .features > div {
-    width: 33.3%;
-    display: table-cell;
-    padding: 0 1.5em;
-  }
-}
-.features h3 {
-  font-size: 1.5em;
-  font-weight: normal;
-  color: #fff;
-}
-input[type=submit],
-input.url {
-  padding: 0 0.5em;
-  margin: 0.5em;
-  font-size: 1.2em;
-  height: 2em;
-  border: 0 solid;
-  border-radius: 0.5em;
-  outline: none;
-}
-input[type=submit]:hover {
-  color: #ddd;
-}
-input[type=submit].clicked {
-  color: #ddd;
-  position: relative;
-  left: 0.1em;
-  top: 0.2em;
-  box-shadow: none;
-}
-.homeSponsor {
-  margin-top: 3em;
-}

+ 0 - 280
front/src/css/main.css

@@ -1,280 +0,0 @@
-html {
-  margin: 35px 5px;
-}
-@media (min-width: 640px) {
-  html {
-    margin: 100px 50px;
-  }
-}
-body {
-  margin: 0 auto;
-  max-width: 1280px;
-  background: #212240;
-  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 {
-  font-weight: 200;
-}
-.resultsMenu {
-  margin-top: 2em;
-}
-.resultsMenu .menuItem {
-  font-size: 0.8em;
-  display: inline-block;
-  width: 7em;
-  height: 7em;
-  color: #fff;
-  cursor: pointer;
-  text-decoration: none;
-}
-@media (min-width: 640px) {
-  .resultsMenu .menuItem {
-    font-size: 1em;
-    margin: 1em;
-    width: 8em;
-    border: 2px solid #fff;
-    border-radius: 0.5em;
-  }
-}
-.resultsMenu .menuItem svg {
-  fill: #fff;
-}
-.resultsMenu .menuItem.back,
-.resultsMenu .menuItem.restart {
-  color: #fff;
-  border-color: #fff;
-}
-.resultsMenu .menuItem div {
-  padding-top: 0.5em;
-  font-size: 3em;
-}
-.resultsMenu svg {
-  display: block;
-  margin: 1.2em auto 0.2em;
-}
-.resultsMenu .active,
-.resultsMenu .menuItem.active:hover {
-  color: #ffa319;
-  border-color: #ffa319;
-}
-.resultsMenu .active svg,
-.resultsMenu .menuItem.active:hover svg {
-  fill: #ffa319;
-}
-.resultsMenu .menuItem:hover {
-  color: #ffa319;
-}
-.resultsMenu .menuItem:hover svg {
-  fill: #ffa319;
-}
-.resultsMenu span {
-  position: relative;
-  top: 0.5em;
-}
-/* Grade colors */
-.A {
-  /* green */
-  background: #0C4;
-}
-.B {
-  /* green */
-  background: #CD0;
-}
-.C {
-  /* yellow */
-  background: #FD2;
-}
-.D {
-  /* orange */
-  background: #FA2;
-}
-.E {
-  /* red */
-  background: #F60;
-}
-.F {
-  /* red */
-  background: #F22;
-}
-.NA {
-  /* Non applicable */
-  background: #CCC;
-}
-.board {
-  margin-top: 2em;
-  padding: 1em;
-  background: #fff;
-  color: #000;
-  border-radius: 0.5em;
-  text-align: left;
-}
-.backToDashboard {
-  text-align: center;
-}
-.backToDashboard a {
-  font-size: 0.9em;
-  display: block;
-  margin-top: 4em;
-  color: black;
-}
-a.linkButton {
-  font-size: 1em;
-  padding: 0.3em 0.5em;
-  margin: 0.5em;
-  line-height: 2em;
-  border: 0 solid;
-  border-radius: 0.5em;
-  box-shadow: 0.1em 0.2em 0 0 #5e2846;
-  background: #e74c3c;
-  color: #fff;
-  text-decoration: none;
-}
-.screenshotWrapper {
-  display: inline-block;
-  position: relative;
-  background: #000;
-}
-.screenshotWrapper > div {
-  position: relative;
-  overflow: hidden;
-}
-.screenshotWrapper .screenshotImage {
-  width: 100%;
-}
-.screenshotWrapper .screenshotError {
-  color: #fff;
-}
-.screenshotWrapper.desktop,
-.screenshotWrapper.desktop-hd {
-  border: 0.2em solid #AAA;
-  padding: 0.5em;
-  border-top-left-radius: 0.4em;
-  border-top-right-radius: 0.4em;
-}
-.screenshotWrapper.desktop:before,
-.screenshotWrapper.desktop-hd:before {
-  position: absolute;
-  width: 15em;
-  height: 0.6em;
-  bottom: -0.75em;
-  left: -1em;
-  background: #CCC;
-  border-bottom-left-radius: 0.2em;
-  border-bottom-right-radius: 0.2em;
-  content: " ";
-}
-.screenshotWrapper.desktop:after,
-.screenshotWrapper.desktop-hd:after {
-  position: absolute;
-  width: 0.4em;
-  height: 0.2em;
-  bottom: -0.55em;
-  left: 12.5em;
-  background: lime;
-  content: " ";
-}
-.screenshotWrapper.desktop > div,
-.screenshotWrapper.desktop-hd > div {
-  width: 12em;
-  height: 7.5em;
-}
-.screenshotWrapper.phone {
-  border: 0.07em solid #CCC;
-  padding: 1em 0.3em 1.5em;
-  border-radius: 0.6em;
-}
-.screenshotWrapper.phone:before {
-  position: absolute;
-  width: 0.8em;
-  height: 0.8em;
-  bottom: 0.3em;
-  left: 3.3em;
-  border: 0.07em solid #CCC;
-  border-radius: 0.5em;
-  content: " ";
-}
-.screenshotWrapper.phone:after {
-  position: absolute;
-  width: 1em;
-  height: 0.1em;
-  bottom: 13.9em;
-  left: 3.2em;
-  background: #555;
-  border-radius: 0.05em;
-  content: " ";
-}
-.screenshotWrapper.phone > div {
-  width: 6.75em;
-  height: 12em;
-}
-.screenshotWrapper.tablet {
-  border: 0.07em solid #CCC;
-  padding: 0.8em 0.5em 0.9em;
-  border-radius: 0.6em;
-}
-.screenshotWrapper.tablet:before {
-  position: absolute;
-  width: 0.5em;
-  height: 0.5em;
-  bottom: 0.15em;
-  left: 4.35em;
-  border: 0.07em solid #CCC;
-  border-radius: 0.4em;
-  content: " ";
-}
-.screenshotWrapper.tablet > div {
-  width: 8em;
-  height: 12.8em;
-}
-.table {
-  display: table;
-  width: 100%;
-  border-spacing: 0.25em;
-}
-.table > div,
-.table > a {
-  display: table-row;
-}
-.table > .headers > div {
-  font-weight: bold;
-  padding: 0.5em 1em;
-}
-.table > div > div,
-.table > a > div {
-  padding: 0.1em 1em;
-  background: #f2f2f2;
-  display: table-cell;
-  text-align: left;
-}
-.footer {
-  padding: 3em;
-  color: #fff;
-}
-.footer a {
-  color: inherit;
-}
-.footer .version {
-  font-size: 0.7em;
-}
-.footer .github {
-  margin: 1em 0 0 0.5em;
-}
-.footer .sponsor {
-  font-size: 0.9em;
-}
-.homeSponsor {
-  color: #ffa319;
-}

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

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

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

@@ -1,277 +0,0 @@
-.rule.board {
-  text-align: center;
-}
-.rule .ruleTable {
-  border-spacing: 1em;
-  width: 90%;
-  margin: 2em auto;
-  background: #f2f2f2;
-  border: 1px dashed #666;
-  border-radius: 0.5em;
-}
-@media (min-width: 820px) {
-  .rule .ruleTable {
-    display: table;
-  }
-  .rule .ruleTable > div {
-    display: table-cell;
-    vertical-align: middle;
-  }
-  .rule .ruleTable .left {
-    width: 33%;
-  }
-  .rule .ruleTable .right {
-    width: 67%;
-  }
-}
-.rule .score {
-  font-size: 2.5em;
-  line-height: 2em;
-  height: 2em;
-  width: 2em;
-  border-radius: 0.5em;
-  margin: 0 auto 0.25em;
-}
-.rule h3 {
-  margin-bottom: 0em;
-}
-.rule .okThreshold {
-  font-style: italic;
-  font-size: 0.9em;
-}
-.rule .message {
-  width: 80%;
-  margin: 1.5em auto;
-}
-.rule .message p {
-  margin: 0.5em;
-}
-.rule .message ul {
-  list-style-type: none;
-  padding-left: 0;
-}
-.rule .message li:before {
-  content: '\25e6';
-  margin-right: 0.3em;
-  font-size: 1.2em;
-  position: relative;
-  top: 0.1em;
-}
-.rule .warning {
-  width: 90%;
-  margin: -1em auto 2em;
-  background: #FEE;
-  border: 1px dashed #e74c3c;
-  color: #e74c3c;
-  border-radius: 0.5em;
-}
-.rule .offendersTable {
-  display: table;
-  border-spacing: 0 0.25em;
-  margin: 0 auto;
-  min-width: 10%;
-  font-size: 0.875em;
-}
-@media (min-width: 820px) {
-  .rule .offendersTable {
-    max-width: 90%;
-    font-size: 1em;
-  }
-}
-.rule .offendersTable > div {
-  display: table-row;
-}
-.rule .offendersTable > div > div {
-  display: table-cell;
-  background: #f2f2f2;
-  padding: 0 0.25em;
-  word-wrap: break-word;
-  word-break: break-all;
-}
-.rule .offendersTable > div > div:hover {
-  background: #d8ebe0;
-}
-.rule .notFound {
-  font-size: 1em;
-}
-.rule .notFound h2 {
-  font-size: 3em;
-  margin-bottom: 1em;
-}
-.rule .startTime {
-  display: none;
-}
-.offendersTable .offenderButton,
-.value .offenderButton {
-  display: inline-block;
-  position: relative;
-  background: #efe;
-  padding: 0 0.5em;
-  margin: 0.2em 0;
-  border-radius: 0.4em;
-  z-index: 1;
-  cursor: pointer;
-}
-.offendersTable .offenderButton.opens,
-.value .offenderButton.opens {
-  padding-right: 0.75em;
-}
-.offendersTable .offenderButton.opens:after,
-.value .offenderButton.opens:after {
-  position: relative;
-  left: 0.5em;
-  content: '\25BC';
-  font-size: 0.8em;
-}
-.offendersTable .offenderButton > div,
-.value .offenderButton > div {
-  display: none;
-  position: absolute;
-  right: 0;
-  min-width: 100%;
-  background: inherit;
-  border-bottom-left-radius: 0.4em;
-  border-bottom-right-radius: 0.4em;
-  border-top: 1px solid #999;
-  z-index: 2;
-}
-.offendersTable .offenderButton .domTree,
-.value .offenderButton .domTree {
-  text-align: left;
-  white-space: nowrap;
-}
-.offendersTable .offenderButton .domTree > div,
-.value .offenderButton .domTree > div {
-  margin: 0.5em;
-}
-.offendersTable .offenderButton .domTree > div div,
-.value .offenderButton .domTree > div div {
-  margin-left: 1em;
-}
-.offendersTable .offenderButton .backtrace,
-.value .offenderButton .backtrace,
-.offendersTable .offenderButton .cssFileAndLine,
-.value .offenderButton .cssFileAndLine {
-  white-space: nowrap;
-  padding: 0.5em;
-}
-.offendersTable .offenderButton.opens:hover,
-.value .offenderButton.opens:hover {
-  border-bottom-left-radius: 0;
-  border-bottom-right-radius: 0;
-  background: #ffe0cc;
-  z-index: 2;
-}
-.offendersTable .offenderButton.opens:hover > div,
-.value .offenderButton.opens:hover > div {
-  display: block;
-  background: #ffe0cc;
-}
-.offendersTable .smallerOffenders,
-.value .smallerOffenders {
-  font-size: 0.9em;
-}
-.offendersHtml {
-  display: inline-block;
-}
-.domTree div {
-  text-align: left;
-  margin-left: 1em;
-}
-.domTree div span:only-child {
-  font-weight: bold;
-}
-.domTree div span:only-child span {
-  font-style: italic;
-  font-weight: normal;
-}
-.checker {
-  /* Checkerboard background */
-  background-color: #ddd;
-  background-image: linear-gradient(45deg, #AAA 25%, transparent 25%, transparent 75%, #AAA 75%, #AAA), linear-gradient(45deg, #AAA 25%, transparent 25%, transparent 75%, #AAA 75%, #AAA);
-  background-size: 1em 1em;
-  background-position: 0 0, 0.5em 0.5em;
-}
-.colorPalette {
-  width: 30em;
-  border: 2px solid #000;
-  text-align: left;
-}
-.colorPalette > div {
-  display: inline-block;
-  height: 2em;
-  position: relative;
-}
-.colorPalette > div div {
-  display: none;
-  position: absolute;
-  left: 100%;
-  top: 100%;
-  background: #FFF;
-  padding: 0.5em;
-  border: 2px solid #f1c40f;
-  border-radius: 0.5em;
-  white-space: nowrap;
-  z-index: 3;
-  font-weight: bold;
-}
-.colorPalette > div:hover div {
-  display: block;
-}
-.colorPalette > div:hover:after {
-  content: " ";
-  position: absolute;
-  left: -0.2em;
-  top: -0.2em;
-  width: 100%;
-  height: 100%;
-  z-index: 2;
-  border: 0.2em solid #f1c40f;
-}
-.similarColors {
-  margin: 1em;
-  width: 20em;
-  height: 6em;
-}
-.similarColors > div {
-  display: inline-block;
-  width: 10em;
-  height: 3.5em;
-  padding-top: 2.5em;
-}
-.totalWeightPie {
-  max-width: 20em;
-  margin: 2em auto 4em;
-}
-.totalWeightPie canvas {
-  max-width: inherit;
-}
-.offenderProblem {
-  font-weight: bold;
-  color: #e74c3c;
-}
-.imageOffenders {
-  display: table;
-  border-spacing: 3em;
-  width: 90%;
-}
-.imageOffenders > div {
-  display: table-row;
-}
-.imageOffenders > div > div {
-  display: table-cell;
-  vertical-align: middle;
-}
-.imageOffenders img {
-  max-height: 10em;
-  max-width: 40em;
-  border: 1px solid #000;
-  margin-top: 0.5em;
-}
-.smallPreview {
-  display: block;
-  max-height: 6em;
-  max-width: 16em;
-  border: 1px solid #000;
-  margin: 1em auto 0.2em;
-}

+ 0 - 17
front/src/css/screenshot.css

@@ -1,17 +0,0 @@
-.screenshot.board {
-  text-align: center;
-}
-.screenshot .screenshotWrapper {
-  font-size: 1.2em;
-  margin-bottom: 0.5em;
-}
-@media (min-width: 420px) {
-  .screenshot .screenshotWrapper {
-    font-size: 1.6em;
-  }
-}
-@media (min-width: 640px) {
-  .screenshot .screenshotWrapper {
-    font-size: 2.08333333em;
-  }
-}

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


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


BIN
front/src/img/favicon.png


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


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

@@ -1,85 +0,0 @@
-var yltApp = angular.module('YellowLabTools', [
-    'ngRoute',
-    'ngSanitize',
-    'ngAnimate',
-    'indexCtrl',
-    'dashboardCtrl',
-    'queueCtrl',
-    'ruleCtrl',
-    'screenshotCtrl',
-    'runsFactory',
-    'resultsFactory',
-    'apiService',
-    'menuService',
-    'settingsService',
-    'gradeDirective',
-    'offendersDirectives',
-    'LocalStorageModule'
-]);
-
-yltApp.run(['$rootScope', '$location', function($rootScope, $location) {
-    $rootScope.isTouchDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(window.navigator.userAgent);
-    $rootScope.loadedRunId = null;
-
-    var oldHash;
-
-    // We don't want the hash to be kept between two pages
-    $rootScope.$on('$locationChangeStart', function(param1, param2, param3, param4){
-        var newHash = $location.hash();
-        if (newHash === oldHash) {
-            $location.hash(null);
-        }
-        oldHash = newHash;
-    });
-
-    // Google Analytics
-    $rootScope.$on('$routeChangeSuccess', function(){
-        if (typeof ga !== "undefined") {
-            ga('send', 'pageview', {'page': $location.path()});
-        }
-    });
-
-    // GitHub star button (asynchronously loaded iframe)
-    window.addEventListener('load', function() {
-        window.document.getElementById('ghbtn').src = 'https://ghbtns.com/github-btn.html?user=YellowLabTools&repo=YellowLabTools&type=star&count=true&size=large';
-    });
-}]);
-
-yltApp.config(['$routeProvider', '$locationProvider',
-    function($routeProvider, $locationProvider) {
-        $routeProvider.
-            when('/', {
-                templateUrl: 'views/index.html',
-                controller: 'IndexCtrl'
-            }).
-            when('/queue/:runId', {
-                templateUrl: 'views/queue.html',
-                controller: 'QueueCtrl'
-            }).
-            when('/about', {
-                templateUrl: 'views/about.html'
-            }).
-            when('/result/:runId', {
-                templateUrl: 'views/dashboard.html',
-                controller: 'DashboardCtrl'
-            }).
-            when('/result/:runId/screenshot', {
-                templateUrl: 'views/screenshot.html',
-                controller: 'ScreenshotCtrl'
-            }).
-            when('/result/:runId/rule/:policy', {
-                templateUrl: 'views/rule.html',
-                controller: 'RuleCtrl'
-            }).
-            otherwise({
-                redirectTo: '/'
-            });
-            
-            $locationProvider.html5Mode(true);
-    }
-]);
-
-// Disable debugging https://docs.angularjs.org/guide/production
-yltApp.config(['$compileProvider', function ($compileProvider) {
-    $compileProvider.debugInfoEnabled(false);
-}]);

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

@@ -1,37 +0,0 @@
-var dashboardCtrl = angular.module('dashboardCtrl', ['resultsFactory', 'menuService']);
-
-dashboardCtrl.controller('DashboardCtrl', ['$scope', '$rootScope', '$routeParams', '$location', 'Results', 'API', 'Menu', function($scope, $rootScope, $routeParams, $location, Results, API, Menu) {
-    $scope.runId = $routeParams.runId;
-    $scope.Menu = Menu.setCurrentPage('dashboard', $scope.runId);
-    
-    function loadResults() {
-        // Load result if needed
-        if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
-            Results.get({runId: $routeParams.runId, exclude: 'toolsResults'}, function(result) {
-                $rootScope.loadedResult = result;
-                $scope.result = result;
-                init();
-            }, function(err) {
-                $scope.error = true;
-            });
-        } else {
-            $scope.result = $rootScope.loadedResult;
-            init();
-        }
-    }
-
-    function init() {
-        // By default, Angular sorts object's attributes alphabetically. Countering this problem by retrieving the keys order here.
-        $scope.categoriesOrder = Object.keys($scope.result.scoreProfiles.generic.categories);
-        
-        $scope.globalScore = Math.max($scope.result.scoreProfiles.generic.globalScore, 0);
-
-        $scope.tweetText = 'I\'ve discovered this cool open-source tool that audits the front-end quality of a web page: ';
-    }
-
-    $scope.testAgain = function() {
-        API.relaunchTest($scope.result);
-    };
-
-    loadResults();
-}]);

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

@@ -1,23 +0,0 @@
-var indexCtrl = angular.module('indexCtrl', []);
-
-indexCtrl.controller('IndexCtrl', ['$scope', '$routeParams', '$location', 'Settings', 'API', function($scope, $routeParams, $location, Settings, API) {
-    
-    $scope.settings = Settings.getMergedSettings();
-
-    $scope.launchTest = function() {
-        if ($scope.url) {
-            $location.search('url', null);
-            $location.search('run', null);
-            Settings.saveSettings($scope.settings);
-            API.launchTest($scope.url, $scope.settings);
-        }
-    };
-
-    // Auto fill URL field and auto launch test when the good params are set in the URL
-    if ($routeParams.url) {
-        $scope.url = $routeParams.url;
-        if ($routeParams.run === 'true' || $routeParams.run === 1 || $routeParams.run === '1') {
-            $scope.launchTest();
-        }
-    }
-}]);

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

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

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

@@ -1,78 +0,0 @@
-var ruleCtrl = angular.module('ruleCtrl', ['chart.js']);
-
-ruleCtrl.config(['ChartJsProvider', function (ChartJsProvider) {
-    // Configure all charts
-    ChartJsProvider.setOptions({
-        animation: false,
-        colours: ['#FF5252', '#FF8A80'],
-        responsive: true
-    });
-}]);
-
-ruleCtrl.controller('RuleCtrl', ['$scope', '$rootScope', '$routeParams', '$location', '$sce', 'Menu', 'Results', 'API', function($scope, $rootScope, $routeParams, $location, $sce, Menu, Results, API) {
-    $scope.runId = $routeParams.runId;
-    $scope.policyName = $routeParams.policy;
-    $scope.Menu = Menu.setCurrentPage(null, $scope.runId);
-    $scope.rule = null;
-
-    function loadResults() {
-        // Load result if needed
-        if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
-            Results.get({runId: $routeParams.runId, exclude: 'toolsResults'}, function(result) {
-                $rootScope.loadedResult = result;
-                $scope.result = result;
-                init();
-            });
-        } else {
-            $scope.result = $rootScope.loadedResult;
-            init();
-        }
-    }
-
-    function init() {
-        $scope.rule = $scope.result.rules[$scope.policyName];
-
-        // Init "Total Weight" chart
-        if ($scope.policyName === 'totalWeight') {
-            $scope.weightLabels = [];
-            $scope.weightColours = ['#7ECCCC', '#A7E846', '#FF944D', '#FFE74A', '#C2A3FF', '#5A9AED', '#FF6452', '#C1C1C1'];
-            $scope.weightData = [];
-
-            var types = ['html', 'css', 'js', 'json', 'image', 'video', 'webfont', 'other'];
-            types.forEach(function(type) {
-                $scope.weightLabels.push(type);
-                $scope.weightData.push(Math.round($scope.rule.offendersObj.list.byType[type].totalWeight / 1024));
-            });
-
-            $scope.weightOptions = {
-                tooltips: {
-                    callbacks: {
-                        label: function(tooltipItem, data) {
-                            var label = data.labels[tooltipItem.index];
-                            var value = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index];
-                            return label + ': ' + value + ' KB';
-                        }
-                    }
-                },
-                legend: {
-                    display: true,
-                    position: 'bottom',
-                    labels: {
-                        boxWidth: 12,
-                        fontSize: 14
-                    }
-                }
-            };
-        }
-    }
-
-    $scope.backToDashboard = function() {
-        $location.path('/result/' + $scope.runId);
-    };
-
-    $scope.testAgain = function() {
-        API.relaunchTest($scope.result);
-    };
-
-    loadResults();
-}]);

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

@@ -1,30 +0,0 @@
-var screenshotCtrl = angular.module('screenshotCtrl', ['resultsFactory', 'menuService']);
-
-screenshotCtrl.controller('ScreenshotCtrl', ['$scope', '$rootScope', '$routeParams', '$location', 'Results', 'API', 'Menu', function($scope, $rootScope, $routeParams, $location, Results, API, Menu) {
-    $scope.runId = $routeParams.runId;
-    $scope.Menu = Menu.setCurrentPage(null, $scope.runId);
-    
-    function loadResults() {
-        // Load result if needed
-        if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
-            Results.get({runId: $routeParams.runId, exclude: 'toolsResults'}, function(result) {
-                $rootScope.loadedResult = result;
-                $scope.result = result;
-            }, function(err) {
-                $scope.error = true;
-            });
-        } else {
-            $scope.result = $rootScope.loadedResult;
-        }
-    }
-
-    $scope.backToDashboard = function() {
-        $location.path('/result/' + $scope.runId);
-    };
-
-    $scope.testAgain = function() {
-        API.relaunchTest($scope.result);
-    };
-
-    loadResults();
-}]);

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

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

+ 0 - 307
front/src/js/directives/offendersDirectives.js

@@ -1,307 +0,0 @@
-(function() {
-    "use strict";
-    var offendersDirectives = angular.module('offendersDirectives', []);
-
-    function getdomTreeHTML(tree) {
-        return '<div class="domTree">' + getdomTreeInnerHTML(tree) + '</div>';
-    }
-
-    function getdomTreeInnerHTML(tree) {
-        return recursiveHtmlBuilder(tree);
-    }
-
-    function recursiveHtmlBuilder(tree) {
-        var html = '';
-        var keys = Object.keys(tree);
-        
-        keys.forEach(function(key) {
-            if (isNaN(tree[key])) {
-                html += '<div><span>' + key + '</span>' + recursiveHtmlBuilder(tree[key]) + '</div>';
-            } else if (tree[key] > 1) {
-                html += '<div><span>' + key + ' <span>(x' + tree[key] + ')</span></span></div>';
-            } else {
-                html += '<div><span>' + key + '</span></div>';
-            }
-        });
-
-        return html;
-    }
-
-    offendersDirectives.directive('domTree', function() {
-        return {
-            restrict: 'E',
-            scope: {
-                tree: '='
-            },
-            template: '<div class="domTree"></div>',
-            replace: true,
-            link: function(scope, element) {
-                element.append(getdomTreeInnerHTML(scope.tree));
-            }
-        };
-    });
-
-    function getDomElementButtonHTML(obj, onASingleLine) {
-        if (obj.tree && !onASingleLine) {
-            return '<div class="offenderButton opens">' + getDomElementButtonInnerHTML(obj, onASingleLine) + '</div>';
-        } else {
-            return '<div class="offenderButton">' + getDomElementButtonInnerHTML(obj, onASingleLine) + '</div>';
-        }
-    }
-
-    function getDomElementButtonInnerHTML(obj, onASingleLine) {
-        if (obj.type === 'html' ||
-            obj.type === 'body' ||
-            obj.type === 'head' ||
-            obj.type === 'window' ||
-            obj.type === 'document' ||
-            obj.type === 'fragment') {
-                return obj.type;
-        }
-
-        if (obj.type === 'notAnElement') {
-            return 'Incorrect element';
-        }
-
-        var html = '';
-        if (obj.type === 'domElement') {
-            html = 'DOM element <b>' + obj.element + '</b>';
-        } else if (obj.type === 'fragmentElement') {
-            html = 'Fragment element <b>' + obj.element + '</b>';
-        } else if (obj.type === 'createdElement') {
-            html = 'Created element <b>' + obj.element + '</b>';
-        }
-
-        if (obj.tree && !onASingleLine) {
-            html += getdomTreeHTML(obj.tree);
-        }
-
-        return html;
-    }
-
-    offendersDirectives.directive('domElementButton', function() {
-        return {
-            restrict: 'E',
-            scope: {
-                obj: '='
-            },
-            template: '<div class="offenderButton" ng-class="{opens: obj.tree}"></div>',
-            replace: true,
-            link: function(scope, element) {                
-                element.append(getDomElementButtonInnerHTML(scope.obj));
-            }
-        };
-    });
-
-    offendersDirectives.filter('lastDOMNode', function() {
-        return function(str) {
-            var splited = str.split(' > ');
-            return splited[splited.length - 1];
-        };
-    });
-
-    function getBacktraceHTML(backtrace) {
-        var html = '';
-        var parsedBacktrace = parseBacktrace(backtrace);
-
-        if (!parsedBacktrace || parsedBacktrace.length === 0) {
-            html += '<div><div>can\'t find any backtrace :/</div></div>';
-        } else {
-            for (var i = 0 ; i < parsedBacktrace.length ; i++) {
-                html += '<div>';
-                html += '<div>' + (parsedBacktrace[i].fnName || '(anonymous function)') + '</div>';
-                html += '<div class="trace">' + getUrlLink(parsedBacktrace[i].filePath, 40) + '</div>';
-                if (parsedBacktrace[i].column) {
-                    html += '<div>' + parsedBacktrace[i].line + ':' + parsedBacktrace[i].column + '</div>';    
-                } else {
-                    html += '<div>line ' + parsedBacktrace[i].line + '</div>';
-                }
-                html += '</div>';
-            }
-        }
-
-        return html;
-    }
-
-    function parseBacktrace(str) {
-        if (!str) {
-            return null;
-        }
-
-        var out = [];
-        var splited = str.split(' / ');
-        
-        try {
-
-            splited.forEach(function(trace) {
-                var fnName = null, fileAndLine;
-
-                var withFnResult = /^([^\s\(]+) \((.+:\d+)\)$/.exec(trace);
-                
-                if (withFnResult === null) {
-                    // Try the PhantomJS 2 format
-                    withFnResult = /^([^\s\(]+) \((.+:\d+:\d+)\)$/.exec(trace);
-                }
-
-                if (withFnResult === null) {
-                    // Yet another PhantomJS 2 format?
-                    withFnResult = /^([^\s\(]+|global code)@(.+:\d+:\d+)$/.exec(trace);
-                }
-
-                if (withFnResult === null) {
-                    // Try the PhantomJS 2 ERROR format
-                    withFnResult = /^([^\s\(]+) (http.+:\d+)$/.exec(trace);
-                }
-
-                if (withFnResult === null) {
-                    fileAndLine = trace;
-                } else {
-                    fnName = withFnResult[1];
-                    fileAndLine = withFnResult[2];
-                }
-
-                // And now the second part
-                var fileAndLineSplit = /^(.*):(\d+):(\d+)$/.exec(fileAndLine);
-
-                if (fileAndLineSplit === null) {
-                    fileAndLineSplit = /^(.*):(\d+)$/.exec(fileAndLine);
-                }
-
-                var filePath = fileAndLineSplit[1];
-                var line = fileAndLineSplit[2];
-                var column = fileAndLineSplit[3];
-
-                // Filter phantomas code
-                if (filePath.indexOf('phantomjs://') === -1) {
-                    out.push({
-                        fnName: fnName,
-                        filePath: filePath,
-                        line: line,
-                        column: column
-                    });
-                }
-            });
-
-        } catch(e) {
-            return null;
-        }
-
-        return out;
-    }
-
-    function shortenUrl(url, maxLength) {
-        if (!maxLength) {
-            maxLength = 110;
-        }
-
-        // Why dividing by 2.1? Because it adds a 5% margin.
-        var leftLength = Math.floor((maxLength - 5) / 2.1);
-        var rightLength = Math.ceil((maxLength - 5) / 2.1);
-
-        return (url.length > maxLength) ? url.substr(0, leftLength) + ' ... ' + url.substr(-rightLength) : url;
-    }
-
-    offendersDirectives.filter('shortenUrl', function() {
-        return shortenUrl;
-    });
-
-    function getUrlLink(url, maxLength) {
-        return '<a href="' + url + '" target="_blank" title="' + url + '">' + shortenUrl(url, maxLength) + '</a>';
-    }
-
-    offendersDirectives.directive('urlLink', function() {
-        return {
-            restrict: 'E',
-            scope: {
-                url: '=',
-                maxLength: '='
-            },
-            template: '<a href="{{url}}" target="_blank" title="{{url}}">{{url | shortenUrl:maxLength}}</a>',
-            replace: true
-        };
-    });
-
-    offendersDirectives.filter('encodeURIComponent', function() {
-        return window.encodeURIComponent;
-    });
-
-    offendersDirectives.directive('fileAndLine', function() {
-        return {
-            restrict: 'E',
-            scope: {
-                file: '=',
-                line: '=',
-                column: '='
-            },
-            template: '<span><span ng-if="file"><url-link url="file" max-length="60"></url-link></span><span ng-if="!file">&lt;inline CSS&gt;</span><span ng-if="line !== null && column !== null"> @ {{line}}:{{column}}</span></span>',
-            replace: true
-        };
-    });
-
-    offendersDirectives.directive('fileAndLineButton', function() {
-        return {
-            restrict: 'E',
-            scope: {
-                file: '=',
-                line: '=',
-                column: '='
-            },
-            template: '<div class="offenderButton opens">css file<div class="cssFileAndLine"><file-and-line file="file" line="line" column="column" button="true"></file-and-line></div></div>',
-            replace: true
-        };
-    });
-
-    offendersDirectives.filter('bytes', function() {
-        return function(bytes) {
-            if (isNaN(parseFloat(bytes)) || !isFinite(bytes)) {
-                return '-';
-            }
-            
-            var kilo = bytes / 1024;
-
-            if (kilo < 1) {
-                return bytes + ' bytes';
-            }
-
-            if (kilo < 100) {
-                return kilo.toFixed(1) + ' KB';
-            }
-
-            if (kilo < 1024) {
-                return kilo.toFixed(0) + ' KB';
-            }
-
-            var mega = kilo / 1024;
-
-            if (mega < 10) {
-                return mega.toFixed(2) + ' MB';
-            }
-
-            return mega.toFixed(1) + ' MB';
-        };
-    });
-
-    offendersDirectives.filter('addSpaces', function() {
-        return function(str) {
-            return str.split('').join(' ');
-        };
-    });
-
-    // Proxify an HTTP image to HTTPS if hosted on HTTPS
-    // Uses a great free open-source external service: https://images.weserv.nl
-    offendersDirectives.filter('https', function() {
-        return function(url) {
-            if (url && url.indexOf('http://') === 0 && window.location.protocol === 'https:') {
-                return 'https://images.weserv.nl/?url=' + encodeURIComponent(url.substr(7));
-            }
-            return url;
-        };
-    });
-
-    offendersDirectives.filter('roundNbr', function() {
-        return function(nbr) {
-            return Math.round(nbr);
-        };
-    });
-
-})();

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

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

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

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

+ 0 - 64
front/src/js/services/apiService.js

@@ -1,64 +0,0 @@
-var apiService = angular.module('apiService', []);
-
-apiService.factory('API', ['$location', 'Runs', 'Results', function($location, Runs, Results) {
-
-    return {
-
-        launchTest: function(url, settings) {
-            var runObject = {
-                url: url,
-                waitForResponse: false,
-                screenshot: true,
-                device: settings.device,
-                waitForSelector: settings.waitForSelector,
-                proxy: settings.proxy,
-                cookie: settings.cookie,
-                authUser: settings.authUser,
-                authPass: settings.authPass,
-                blockDomain: settings.blockDomain,
-                allowedDomains: settings.allowedDomains,
-                noExternals: settings.noExternals
-            };
-
-            
-            if (settings.domainsBlockOrAllow === 'block') {
-                runObject.blockDomain = this.parseDomains(settings.domains);
-            } else if (settings.domainsBlockOrAllow === 'allow') {
-                var allowedDomains = this.parseDomains(settings.domains);
-                if (allowedDomains.length > 0) {
-                    runObject.allowDomain = allowedDomains;
-                } else {
-                    runObject.noExternals = true;
-                }
-            }
-
-            Runs.save(runObject, function(data) {
-                $location.path('/queue/' + data.runId);
-            }, function(response) {
-                if (response.status === 429) {
-                    alert('Too many requests, you reached the max number of requests allowed in 24h');
-                } else if (response.status === 403) {
-                    alert('This particular query was blocked due to spamming. If you think it\'s an error, please open an issue on GitHub.');
-                } else {
-                    alert('An error occured...');
-                }
-            });
-        },
-
-        relaunchTest: function(result) {
-            this.launchTest(result.params.url, result.params.options);
-        },
-
-        parseDomains: function(textareaContent) {
-            var lines = textareaContent.split('\n');
-            
-            function removeEmptyLines (line) {
-                return line.trim() !== '';
-            }
-
-            // Remove empty lines
-            return lines.filter(removeEmptyLines).join(',');
-        }
-    };
-
-}]);

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

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

+ 0 - 24
front/src/js/services/settingsService.js

@@ -1,24 +0,0 @@
-var settingsService = angular.module('settingsService', []);
-
-settingsService.factory('Settings', ['localStorageService', function(localStorageService) {
-
-    return {
-
-        getMergedSettings: function() {
-            var defaultSettings = {
-                device: 'phone',
-                showAdvanced: false
-            };
-            
-            var savedValues = localStorageService.get('settings');
-
-            return angular.extend(defaultSettings, savedValues);
-        },
-
-        saveSettings: function(settings) {
-            localStorageService.set('settings', settings);
-        }
-
-    };
-
-}]);

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

@@ -1,20 +0,0 @@
-.about {
-    margin: 3em auto;
-    width: 80%;
-
-    @media (min-width: 640px) {
-        width: 50%;
-    }
-}
-
-.about p {
-    margin: 2em;
-}
-
-.about a {
-    color: #fff;
-}
-
-.sponsor {
-    color: #ffa319;
-}

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

@@ -1,199 +0,0 @@
-.testedUrl {
-    color: inherit;
-}
-
-.summary {
-    text-align: center;
-}
-
-.summary .globalScore {
-    margin: 3em auto;
-
-    .globalGrade {
-        margin: 0.5 auto;
-        width: 2.5em;
-        height: 2.5em;
-        line-height: 2.5em;
-        border-radius: 0.5em;
-        font-size: 3em;
-        vertical-align: middle;
-    }
-    .on100 {
-        font-size: 1.2em;
-        margin: 0.5em 0 1em;
-    }
-
-    .screenshotWrapper:hover {
-        opacity: 0.75;
-
-        &:after {
-            position: absolute;
-            width: 1.25em;
-            height: 1.25em;
-            top: 0.7em;
-            left: 1.55em;
-            font-size: 3em;
-            color: #FFF;
-            background: #000;
-            border-radius: 0.2em;
-            text-align: center;
-            content: "+";
-            opacity: 0.85;
-        }
-    }
-
-    .screenshotWrapper.phone:hover:after {
-        top: 1.7em;
-        left: 0.64em;
-    }
-
-    .screenshotWrapper.tablet:hover:after {
-        top: 1.5em;
-        left: 0.9em;
-    }
-
-    @media (min-width: 820px) {
-        width: 65%;
-        display: table;
-
-        > div {
-            display: table-cell;
-            width: 50%;
-            vertical-align: middle;
-        }
-    }
-}
-
-.summary .notations {
-    width: 100%;
-    display: table;
-    margin: 0 0 1.5em;
-    border-spacing: 0 1em;
-
-    @media (min-width: 820px) {
-        width: 80%;
-        margin: 0 10% 1.5em;
-        border-spacing: 1em;
-    }
-}
-.summary .notations > div {
-    display: table-row;
-}
-.summary .notations > div > div {
-    vertical-align: middle;
-
-    @media (min-width: 820px) {
-        display: table-cell;
-        height: 2.5em;
-    }
-}
-.summary .notations .category {
-    font-size: 1.2em;
-    width: 50%;
-    float: left;
-    text-align: left;
-    margin: 0.5em 0.25em;
-
-    @media (min-width: 820px) {
-        width: 20%;
-        text-align: center;
-        float: none;
-    }
-}
-.summary .notations .criteria {
-    font-weight: normal;
-
-    @media (min-width: 820px) {
-        width: 75%;
-    }
-}
-.A, .B, .C, .D, .E, .F, .NA {
-    .summary .notations &.categoryScore {
-        width: 2.5em;
-        max-width: 2.5em;
-        min-width: 2.5em;
-        margin: 0.2em;
-        font-size: 1.5em;
-        text-align: center;
-        border-radius: 0.5em;
-        float: right;
-
-        @media (min-width: 820px) {
-            float: none;
-            font-size: 2em;
-        }
-    }
-    .summary .notations .grade & {
-        width: 1em;
-        height: 1em;
-        font-size: 1em;
-        color: transparent;
-        margin: 0 auto;
-        border-radius: 0.5em;
-    }
-}
-
-.summary .notations .criteria .table {
-    width: 100%;
-    > a {
-        text-decoration: none;
-        color: inherit;
-    }
-    > a:hover > div {
-        background: #d8ebe0;
-        cursor: pointer;
-        &.info {
-            background: #FFF;
-            svg {
-                fill: #d8ebe0;
-            }
-        }
-    }
-}
-.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%;
-    white-space: nowrap;
-    text-align: center;
-    vertical-align: middle;
-}
-.summary .notations .warning .label, .summary .notations .warning .result {
-    color: #FF1919;
-}
-.summary .notations .icon-warning svg {
-    fill: #FF1919;
-    margin: -2px 0;
-}
-.summary .notations .criteria .info {
-    display: none;
-
-    @media (min-width: 820px) {
-        display: table-cell;
-        width: 2%;
-        text-align: center;
-        vertical-align: middle;
-        background: #FFF;
-        padding-left: 0.1em;
-        padding-right: 0.1em;
-    }
-}
-.summary .notations .criteria .info svg {
-    fill: transparent;
-}
-
-.summary .sponsor {
-    font-size: 0.9em;
-    margin-bottom: 4em;
-    color: #ffa319;
-    a {
-        color: inherit;
-    }
-}

+ 0 - 223
front/src/less/index.less

@@ -1,223 +0,0 @@
-.promess {
-    padding: 0em 2em;
-    margin-bottom: 0.5em;
-    font-weight: normal;
-    font-size: 1.2em;
-}
-
-.price {
-    padding: 0em 2em 3em;
-    margin-top: 0em;
-    font-size: 0.9em;
-}
-
-.url {
-    width: 50%;
-}
-
-.launchBtn {
-    background: #ffa319;
-    color: #fff;
-    &:focus {
-        background: #e74c3c;
-    }
-    &.disabled {
-        background: #f1bd70;
-        &:focus {
-            color: #ddd;
-        }
-    }
-    
-}
-
-.settings {
-    width: 50%;
-    margin: 0 auto;
-
-    input, select {
-        font-size: 1em;
-    }
-
-    input[type=text], input[type=password], textarea {
-        width: 100%;
-        min-width: 4em;
-    }
-}
-
-.device {
-    margin-top: 3em;
-    .item {
-        display: inline-block;
-        margin: 1em 0.75em;
-        width: 5.5em;
-        height: 5.5em;
-        color: #FFF;
-        border: 1px solid #FFF;
-        padding: 1px;
-        border-radius: 0.5em;
-        cursor: pointer;
-        text-decoration: none;
-        font-size: 0.8em;
-
-        > svg {
-            display: block;
-            margin: 0.6em auto 0.3em;
-            fill: #fff;
-        }
-
-        &.active {
-            color: #ffa319;
-            border: 2px solid #ffa319;
-            padding: 0;
-
-            > svg {
-                fill: #ffa319;
-            }
-        }
-
-        &:hover {
-            color: #ffa319;
-
-            > svg {
-                fill: #ffa319;
-            }
-        }
-    }
-}
-
-.settingsTooltip {
-    position: relative;
-    svg {
-        vertical-align: text-top;
-    }
-    
-    div {
-        display: none;
-        position: absolute;
-        padding: 0.5em;
-        width: 25em;
-        background: #FFF;
-        color: #000;
-        font-size: 0.8em;
-        border-radius: 1em;
-        border: 2px solid #ffa319;
-        white-space: normal;
-        word-break: break-all;
-        word-break: break-word;
-        z-index: 2;
-    }
-
-    &:hover div {
-        display: block;
-    }
-}
-
-.showAdvanced {
-    display: inline-block;
-    margin-top: 2em;
-    color: #FFF;
-    text-decoration: none;
-    font-size: 0.9em;
-    
-    &:hover {
-        color: #ffa319;
-    }
-}
-
-.currentSettings {
-    font-size: 0.9em;
-
-    span {
-        color: #ffa319;
-        &:after {
-            color: #FFF;
-            content: ",";
-        }
-
-        &:last-child:after {
-            content: "";
-        }
-    }
-}
-
-.advanced {
-    margin: 1em 0 0;
-    display: table;
-    width: 100%;
-    text-align: left;
-    border-spacing: 0.75em;
-
-    > div {
-        display: table-row;
-
-        > div {
-            display: table-cell;
-            width: 75%;
-
-            &.label {
-                width: 25%;
-                white-space: nowrap;
-                vertical-align: middle;
-            }
-        }
-    }
-
-    .subTable {
-        display: table;
-        border-spacing: 0;
-        width: 100%;
-        > div {
-            display: table-row;
-            > div {
-                display: table-cell;
-                padding: 0 0 0.75em;
-            }
-        }
-    }
-}
-
-.features {
-    display: table;
-    width: 50%;
-    margin: 6em auto 0;
-    font-size: 0.9em;
-    color: #8abfaf;
-
-    > div {
-        @media (min-width: 640px) {
-            width: 33.3%;
-            display: table-cell;
-            padding: 0 1.5em;
-        }
-    }
-
-    h3 {
-        font-size: 1.5em;
-        font-weight: normal;
-        color: #fff;
-    }
-}
-
-input[type=submit], input.url {
-    padding: 0 0.5em;
-    margin: 0.5em;
-    font-size: 1.2em;
-    height: 2em;
-    border: 0 solid;
-    border-radius: 0.5em;
-    outline: none;
-}
-input[type=submit]:hover {
-    color: #ddd;
-}
-input[type=submit].clicked {
-    color: #ddd;
-    position: relative;
-    left: 0.1em;
-    top: 0.2em;
-    box-shadow: none;
-}
-
-.homeSponsor {
-    margin-top: 3em;
-}

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

@@ -1,298 +0,0 @@
-html {
-    margin: 35px 5px;
-    @media (min-width: 640px) {
-        margin: 100px 50px;
-    }
-}
-
-body {
-    margin: 0 auto;
-    max-width: 1280px;
-    background: #212240;
-    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 {
-    font-weight: 200;
-}
-
-.resultsMenu {
-    margin-top: 2em;
-}
-.resultsMenu .menuItem {
-    font-size: 0.8em;
-    display: inline-block;
-    width: 7em;
-    height: 7em;
-    color: #fff;
-    cursor: pointer;
-    text-decoration: none;
-
-    @media (min-width: 640px) {
-        font-size: 1em;
-        margin: 1em;
-        width: 8em;
-        border: 2px solid #fff;
-        border-radius: 0.5em;
-    }
-
-    svg {
-        fill: #fff;
-    }
-
-    &.back, &.restart {
-        color: #fff;
-        border-color: #fff;
-    }
-}
-.resultsMenu .menuItem div {
-    padding-top: 0.5em;
-    font-size: 3em;
-}
-.resultsMenu svg {
-    display: block;
-    margin: 1.2em auto 0.2em;
-}
-.resultsMenu .active, .resultsMenu .menuItem.active:hover {
-    color: #ffa319;
-    border-color: #ffa319;
-
-    svg {
-        fill: #ffa319;
-    }
-}
-.resultsMenu .menuItem:hover {
-    color: #ffa319;
-
-    svg {
-        fill: #ffa319;
-    }
-}
-.resultsMenu span {
-    position: relative;
-    top: 0.5em;
-}
-
-/* Grade colors */
-.A {
-    /* green */
-    background: #0C4;
-}
-.B {
-    /* green */
-    background: #CD0;
-}
-.C {
-    /* yellow */
-    background: #FD2;
-}
-.D {
-    /* orange */
-    background: #FA2;
-}
-.E {
-    /* red */
-    background: #F60;
-}
-.F {
-    /* red */
-    background: #F22;
-}
-.NA {
-    /* Non applicable */
-    background: #CCC;
-}
-
-.board {
-    margin-top: 2em;
-    padding: 1em;
-    background: #fff;
-    color: #000;
-    border-radius: 0.5em;
-    text-align: left;
-}
-
-.backToDashboard {
-    text-align: center;
-
-    a {
-        font-size: 0.9em;
-        display: block;
-        margin-top: 4em;
-        color: black;
-    }
-}
-
-a.linkButton {
-    font-size: 1em;
-    padding: 0.3em 0.5em;
-    margin: 0.5em;
-    line-height: 2em;
-    border: 0 solid;
-    border-radius: 0.5em;
-    box-shadow: 0.1em 0.2em 0 0 #5e2846;
-    background: #e74c3c;
-    color: #fff;
-    text-decoration: none;
-}
-
-
-.screenshotWrapper {
-    display: inline-block;
-    position: relative;
-    background: #000;
-
-    > div {
-        position: relative;
-        overflow: hidden;
-    }
-
-    .screenshotImage {
-        width: 100%;
-    }
-
-    .screenshotError {
-        color: #fff;
-    }
-}
-
-.screenshotWrapper.desktop, .screenshotWrapper.desktop-hd {
-    border: 0.2em solid #AAA;
-    padding: 0.5em;
-    border-top-left-radius: 0.4em;
-    border-top-right-radius: 0.4em;
-
-    &:before {
-        position: absolute;
-        width: 15em;
-        height: 0.6em;
-        bottom: -0.75em;
-        left: -1em;
-        background: #CCC;
-        border-bottom-left-radius: 0.2em;
-        border-bottom-right-radius: 0.2em;
-        content: " ";
-    }
-
-    &:after {
-        position: absolute;
-        width: 0.4em;
-        height: 0.2em;
-        bottom: -0.55em;
-        left: 12.5em;
-        background: lime;
-        content: " ";
-    }
-
-    > div {
-        width: 12em;
-        height: 7.5em;
-    }
-}
-
-.screenshotWrapper.phone {
-    border: 0.07em solid #CCC;
-    padding: 1em 0.3em 1.5em;
-    border-radius: 0.6em;
-
-    &:before {
-        position: absolute;
-        width: 0.8em;
-        height: 0.8em;
-        bottom: 0.3em;
-        left: 3.3em;
-        border: 0.07em solid #CCC;
-        border-radius: 0.5em;
-        content: " ";
-    }
-
-    &:after {
-        position: absolute;
-        width: 1em;
-        height: 0.1em;
-        bottom: 13.9em;
-        left: 3.2em;
-        background: #555;
-        border-radius: 0.05em;
-        content: " ";
-    }
-
-    > div {
-        width: 6.75em;
-        height: 12em;
-    }
-}
-
-.screenshotWrapper.tablet {
-    border: 0.07em solid #CCC;
-    padding: 0.8em 0.5em 0.9em;
-    border-radius: 0.6em;
-
-    &:before {
-        position: absolute;
-        width: 0.5em;
-        height: 0.5em;
-        bottom: 0.15em;
-        left: 4.35em;
-        border: 0.07em solid #CCC;
-        border-radius: 0.4em;
-        content: " ";
-    }
-
-    > div {
-        width: 8em;
-        height: 12.8em;
-    }
-}
-
-.table {
-    display: table;
-    width: 100%;
-    border-spacing: 0.25em;
-}
-.table > div,
-.table > a {
-    display: table-row;
-}
-.table > .headers > div {
-    font-weight: bold;
-    padding: 0.5em 1em;
-}
-.table > div > div,
-.table > a > div {
-    padding: 0.1em 1em;
-    background: #f2f2f2;
-    display: table-cell;
-    text-align: left;
-}
-
-.footer {
-    padding: 3em;
-    color: #fff;
-    a {
-        color: inherit;
-    }
-    .version {
-        font-size: 0.7em;
-    }
-    .github {
-        margin: 1em 0 0 0.5em;
-    }
-    .sponsor {
-        font-size: 0.9em;
-    }
-}
-
-.homeSponsor {
-    color: #ffa319;
-}

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

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

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

@@ -1,307 +0,0 @@
-.rule.board {
-    text-align: center;
-}
-
-.rule .ruleTable {
-    border-spacing: 1em;
-    width: 90%;
-    margin: 2em auto;
-    background: #f2f2f2;
-    border: 1px dashed #666;
-    border-radius: 0.5em;
-
-    @media (min-width: 820px) {
-        display: table;
-
-        > div {
-            display: table-cell;
-            vertical-align: middle;
-        }
-
-        .left {
-            width: 33%;
-        }
-        .right {
-            width: 67%;
-        }
-    }
-}
-
-.rule .score {
-    font-size: 2.5em;
-    line-height: 2em;
-    height: 2em;
-    width: 2em;
-    border-radius: 0.5em;
-    margin: 0 auto 0.25em;
-}
-
-.rule h3 {
-    margin-bottom: 0em;
-}
-
-.rule .okThreshold {
-    font-style: italic;
-    font-size: 0.9em;
-}
-
-.rule .message {
-    width: 80%;
-    margin: 1.5em auto;
-    p {
-        margin: 0.5em;
-    }
-}
-
-.rule .message ul {
-    list-style-type: none;
-    padding-left: 0;
-}
-.rule .message li:before {
-    content:'\25e6';
-    margin-right: 0.3em;
-    font-size: 1.2em;
-    position: relative;
-    top: 0.1em;
-}
-
-.rule .warning {
-    width: 90%;
-    margin: -1em auto 2em;
-    background: #FEE;
-    border: 1px dashed #e74c3c;
-    color: #e74c3c;
-    border-radius: 0.5em;
-}
-
-.rule .offendersTable {
-    display: table;
-    border-spacing: 0 0.25em;
-    margin: 0 auto;
-    min-width: 10%;
-    font-size: 0.875em;
-
-    @media (min-width: 820px) {
-        max-width: 90%;
-        font-size: 1em;
-    }
-
-    > div {
-        display: table-row;
-        > div {
-            display: table-cell;
-            background: #f2f2f2;
-            padding: 0 0.25em;
-            word-wrap: break-word;
-            word-break: break-all;
-            &:hover {
-                background: #d8ebe0;
-            }
-        }
-    }
-}
-
-.rule .notFound {
-    font-size: 1em;
-    h2 {
-        font-size: 3em;
-        margin-bottom: 1em;
-    }
-}
-
-.rule .startTime {
-    display: none;
-}
-
-.offendersTable, .value {
-    .offenderButton {
-        display: inline-block;
-        position: relative;
-        background: #efe;
-        padding: 0 0.5em;
-        margin: 0.2em 0;
-        border-radius: 0.4em;
-        z-index: 1;
-        cursor: pointer;
-
-        &.opens {
-            padding-right: 0.75em;
-
-            &:after {
-                position: relative;
-                left: 0.5em;
-                content: '\25BC';
-                font-size: 0.8em;
-            }
-        }
-
-        > div {
-            display: none;
-            position: absolute;
-            right: 0;
-            min-width: 100%;
-            background: inherit;
-            border-bottom-left-radius: 0.4em;
-            border-bottom-right-radius: 0.4em;
-            border-top: 1px solid #999;
-            z-index: 2;
-        }
-
-        .domTree {
-            text-align: left;
-            white-space: nowrap;
-
-            > div {
-                margin: 0.5em;
-
-                div {
-                    margin-left: 1em;
-                }
-            }
-        }
-
-        .backtrace, .cssFileAndLine {
-            white-space: nowrap;
-            padding: 0.5em;
-        }
-
-        &.opens:hover {
-            border-bottom-left-radius: 0;
-            border-bottom-right-radius: 0;
-            background: #ffe0cc;
-            z-index: 2;
-
-            > div {
-                display: block;
-                background: #ffe0cc;
-            }
-        }
-    }
-
-    .smallerOffenders {
-        font-size: 0.9em;
-    }
-}
-
-.offendersHtml {
-    display: inline-block;
-}
-
-.domTree div {
-    text-align: left;
-    margin-left: 1em;
-
-    span:only-child {
-        font-weight: bold;
-        span {
-            font-style: italic;
-            font-weight: normal;
-        }
-    }
-}
-
-.checker {
-    /* Checkerboard background */
-    background-color: #ddd;
-    background-image: linear-gradient(45deg, #AAA 25%, transparent 25%, transparent 75%, #AAA 75%, #AAA), linear-gradient(45deg, #AAA 25%, transparent 25%, transparent 75%, #AAA 75%, #AAA);
-    background-size:1em 1em;
-    background-position:0 0, 0.5em 0.5em;
-}
-
-.colorPalette {
-    width: 30em;
-    border: 2px solid #000;
-    text-align: left;
-
-    > div {
-        display: inline-block;
-        height: 2em;
-        position: relative;
-
-        div {
-            display: none;
-            position: absolute;
-            left: 100%;
-            top: 100%;
-            background: #FFF;
-            padding: 0.5em;
-            border: 2px solid #f1c40f;
-            border-radius: 0.5em;
-            white-space: nowrap;
-            z-index: 3;
-            font-weight: bold;
-        }
-
-        &:hover div {
-            display: block;
-        }
-
-        &:hover:after {
-            content: " ";
-            position: absolute;
-            left: -0.2em;
-            top: -0.2em;
-            width: 100%;
-            height: 100%;
-            z-index: 2;
-            border: 0.2em solid #f1c40f;
-        }
-    }
-}
-
-.similarColors {
-    margin: 1em;
-    width: 20em;
-    height: 6em;
-
-    > div {
-        display: inline-block;
-        width: 10em;
-        height: 3.5em;
-        padding-top: 2.5em;
-    }
-}
-
-.totalWeightPie {
-    max-width: 20em;
-    margin: 2em auto 4em;
-
-    canvas {
-        max-width: inherit;
-    }
-}
-
-.offenderProblem {
-    font-weight: bold;
-    color: #e74c3c;
-}
-
-.imageOffenders {
-    display: table;
-    border-spacing: 3em;
-    width: 90%;
-
-    > div {
-        display: table-row;
-
-        > div {
-            display: table-cell;
-            vertical-align: middle;
-        }
-    }
-
-    img {
-        max-height: 10em;
-        max-width: 40em;
-        border: 1px solid #000;
-        margin-top: 0.5em;
-    }
-}
-
-.smallPreview {
-    display: block;
-    max-height: 6em;
-    max-width: 16em;
-    border: 1px solid #000;
-    margin: 1em auto 0.2em;
-}

+ 0 - 16
front/src/less/screenshot.less

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

+ 0 - 66
front/src/main.html

@@ -1,66 +0,0 @@
-<html>
-<head>
-	<meta charset="utf-8">
-    <title>Yellow Lab Tools - Page Speed audit</title>
-    <base href="<%= baseUrl %>">
-    <link rel="icon" type="image/png" href="img/favicon.png">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
-    <meta property="og:image" content="img/logo-large.png" />
-    <meta name="description" content="Yellow Lab Tools is a free online web performance analyzer. It audits a webpage for performance and front-end quality issues. And it's open-source!" />
-
-    <!-- build:css css/styles.css-->
-    <link rel="stylesheet" type="text/css" href="css/main.css">
-    <link rel="stylesheet" type="text/css" href="css/index.css">
-    <link rel="stylesheet" type="text/css" href="css/dashboard.css">
-    <link rel="stylesheet" type="text/css" href="css/queue.css">
-    <link rel="stylesheet" type="text/css" href="css/rule.css">
-    <link rel="stylesheet" type="text/css" href="css/screenshot.css">
-    <link rel="stylesheet" type="text/css" href="css/about.css">
-    <!-- endbuild -->
-
-    <link rel="preconnect" href="https://www.google-analytics.com">
-    <link rel="preconnect" href="https://ghbtns.com">
-    <link rel="preconnect" href="https://api.github.com">
-
-</head>
-
-<body ng-app="YellowLabTools">
-    <div id="header"><h1>Yellow Lab <svg width="32" height="32" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" fill="#ffa319"><path d="M478 402L320 139V32h16c9 0 16-7 16-16s-7-16-16-16H176c-9 0-16 7-16 16s7 16 16 16h16v107L34 402c-36 61-8 110 62 110h320c70 0 98-49 62-110zm-357-82l103-172V32h64v116l103 172H121z"/></svg> Tools</h1></div>
-    <div id="body" ng-view autoscroll="true"></div>
-    <div class="footer">
-        <span class="version" id="version">@@version</span>
-        <br><a href="<%= baseUrl %>about">More about Yellow Lab Tools</a><br>
-        <div class="github"><iframe id="ghbtn" frameborder="0" scrolling="0" width="160px" height="30px"></iframe></div>
-    </div>
-
-    <!-- build:js js/all.js -->
-    <script src="node_modules/angular/angular.min.js"></script>
-    <script src="node_modules/chart.js/dist/Chart.min.js"></script>
-    <script src="node_modules/angular-route/angular-route.min.js"></script>
-    <script src="node_modules/angular-resource/angular-resource.min.js"></script>
-    <script src="node_modules/angular-sanitize/angular-sanitize.min.js"></script>
-    <script src="node_modules/angular-animate/angular-animate.min.js"></script>
-    <script src="node_modules/angular-local-storage/dist/angular-local-storage.min.js"></script>
-    <script src="node_modules/angular-chart.js/dist/angular-chart.min.js"></script>
-    <script src="js/app.js"></script>
-    <script src="js/controllers/indexCtrl.js"></script>
-    <script src="js/controllers/dashboardCtrl.js"></script>
-    <script src="js/controllers/queueCtrl.js"></script>
-    <script src="js/controllers/ruleCtrl.js"></script>
-    <script src="js/controllers/screenshotCtrl.js"></script>
-    <script src="js/models/resultsFactory.js"></script>
-    <script src="js/models/runsFactory.js"></script>
-    <script src="js/services/apiService.js"></script>
-    <script src="js/services/menuService.js"></script>
-    <script src="js/services/settingsService.js"></script>
-
-    <script src="js/directives/gradeDirective.js"></script>
-    <script src="js/directives/offendersDirectives.js"></script>
-    <!-- endbuild -->
-
-    <script>
-        document.getElementById('version').innerHTML = "<%= version %>";
-        if('<%= googleAnalyticsId %>'.indexOf('UA-')===0){(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');ga('create','<%= googleAnalyticsId %>','auto');}
-    </script>
-</body>
-</html>

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

@@ -1,13 +0,0 @@
-<div class="about">
-    <p><b>Yellow Lab Tools</b> is an open source project by <a href="https://letstalkaboutwebperf.com/en/" target="_blank">Gaël Métais</a>. It allows you to test a webpage (via an URL) and detects <b>performance</b> and <b>front-end code quality</b> issues.</p>
-
-    <p>It is based on <a href="https://github.com/macbre/phantomas" target="_blank">Phantomas</a>, a tool that instruments Chrome Headless to collect dozens of metrics. These metrics are then categorized and transformed into scores. It also provides in-depth details so that developers can fix the detected issues.</p>
-
-    <p>By the way, <b>it's entirely free</b>. In return, you can add <a href="https://github.com/YellowLabTools/YellowLabTools" target="_blank">a <span>&#9733;</span> on GitHub</a> or <a href="https://ko-fi.com/gaelmetais" target="_blank">buy me a coffee</a>. It will boost my motivation to add more awesome features!</p>
-
-    <%if (sponsoring.about) { %>
-        <div class="sponsor"><%- sponsoring.about %></div>
-    <% } %>
-
-    <p><br><a href="<%= baseUrl %>">Back to index</a></p>
-</div>

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

@@ -1,72 +0,0 @@
-<div ng-include="'views/resultSubHeader.html'"></div>
-<div class="summary board">
-    
-    <div ng-if="result.blockedRequests">
-        <b><ng-pluralize count="result.blockedRequests.length" when="{'0': 'No blocked request', 'one': '1 blocked request', 'other': '{} blocked requests'}"></ng-pluralize>:</b>
-        <div ng-repeat="request in result.blockedRequests track by $index">
-            {{request}}
-        </div>
-    </div>
-
-    <div class="globalScore" ng-if="globalScore === 0 || globalScore > 0">
-        <div>
-            <h2>Global score</h2>
-            <div class="globalScoreDisplay">
-                <grade score="result.scoreProfiles.generic.globalScore" class="globalGrade"></grade>
-                <div class="on100">{{globalScore}}/100</div>
-            </div>
-        </div>
-        <div>
-            <a href="result/{{result.runId}}/screenshot">
-                <div class="screenshotWrapper" ng-class="result.params.options.device || 'phone'">
-                    <div>
-                        <img ng-if="result.screenshotUrl" class="screenshotImage" ng-src="{{result.screenshotUrl}}"/>
-                        <span ng-if="!result.screenshotUrl" class="screenshotError">Screenshot not available</span>
-                    </div>
-                </div>
-            </a>
-        </div>
-    </div>
-
-    <h2 ng-if="!error && !fromSocialShare">Score details</h2>
-    <div ng-if="!error && !fromSocialShare" class="notations">
-        <div ng-repeat="categoryKey in categoriesOrder" ng-init="category = result.scoreProfiles.generic.categories[categoryKey]">
-            <grade score="category.categoryScore" class="categoryScore"></grade>
-            <div class="category">{{category.label}}</div>
-            <div class="criteria">
-                <div class="table" title="Click to see details">
-                    <a ng-repeat="ruleName in category.rules" ng-if="result.rules[ruleName]" ng-init="rule = result.rules[ruleName]"
-                         ng-class="{'warning': rule.abnormal}" href="result/{{runId}}/rule/{{ruleName}}">
-                        <div class="grade">
-                            <grade score="rule.score"></grade>
-                        </div>
-                        <div class="label">{{rule.policy.label}}</div>
-                        <div class="result">
-                            <span ng-if="rule.policy.unit == 'bytes'">{{rule.value | bytes}}</span>
-                            <span ng-if="rule.policy.unit != 'bytes'">{{rule.value}} <span ng-if="rule.policy.unit"> {{rule.policy.unit}}</span></span>
-                            <span ng-if="rule.abnormal" class="icon-warning"><svg width="16" height="16" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M256 79L84 448h344L256 79zm0-79c11 0 22 7 30 22l219 436c17 30 2 54-32 54H39c-34 0-49-24-32-54L226 22c8-15 19-22 30-22zm0 192c18 0 32 14 32 32l-10 96h-44l-10-96c0-18 14-32 32-32z"/><circle cx="256" cy="384" r="31" stroke="#000"/></svg></span>
-                            <span ng-if="rule.abnormalityScore <= -100" class="icon-warning"><svg width="16" height="16" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M256 79L84 448h344L256 79zm0-79c11 0 22 7 30 22l219 436c17 30 2 54-32 54H39c-34 0-49-24-32-54L226 22c8-15 19-22 30-22zm0 192c18 0 32 14 32 32l-10 96h-44l-10-96c0-18 14-32 32-32z"/><circle cx="256" cy="384" r="31" stroke="#000"/></svg></span>
-                            <span ng-if="rule.abnormalityScore <= -300" class="icon-warning"><svg width="16" height="16" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M256 79L84 448h344L256 79zm0-79c11 0 22 7 30 22l219 436c17 30 2 54-32 54H39c-34 0-49-24-32-54L226 22c8-15 19-22 30-22zm0 192c18 0 32 14 32 32l-10 96h-44l-10-96c0-18 14-32 32-32z"/><circle cx="256" cy="384" r="31" stroke="#000"/></svg></span>
-                        </div>
-                        <div class="info"><svg width="16" height="16" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M224 352h64v64h-64zm128-224c18 0 32 14 32 32v96l-96 64h-64v-32l96-64v-32H160v-64h192zm-96-80A207 207 0 0048 256a207 207 0 00208 208 207 207 0 00208-208A207 207 0 00256 48zm0-48a256 256 0 110 512 256 256 0 010-512z"/></svg></div>
-                    </a>
-                </div>
-            </div>
-        </div>
-    </div>
-
-    <%if (sponsoring.wordpress) { %>
-        <div ng-if="result.frameworks.isWordPress && !error" class="sponsor"><%- sponsoring.wordpress %></div>
-    <% } %>
-
-    <%if (sponsoring.dashboard) { %>
-        <div ng-if="!error" class="sponsor"><%- sponsoring.dashboard %></div>
-    <% } %>
-
-    
-    
-
-    <div ng-if="error">
-        <h2>Run failed / Run not found</h2>
-    </div>
-</div>

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

@@ -1,117 +0,0 @@
-<h2 class="promess">Online test to help speeding up <b>heavy</b> web pages</h2>
-<p class="price">Free and open source!</p>
-
-<form ng-submit="launchTest()" >
-    <input type="text" name="url" ng-model="url" placeholder="https://www.mysite.com" class="url" />
-    <input type="submit" value="Launch test" class="launchBtn" ng-class="{disabled: !url}" />
-    <div class="settings">
-        <div class="device">
-            <div>Choose the simulated device:</div>
-            <a href="" class="item" ng-class="{active: settings.device == 'phone'}" ng-click="settings.device = 'phone'"><svg width="38" height="38" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M368 0H144c-26 0-48 22-48 48v416c0 26 22 48 48 48h224c26 0 48-22 48-48V48c0-26-22-48-48-48zM192 24h128v16H192V24zm64 456a32 32 0 110-64 32 32 0 010 64zm128-96H128V64h256v320z"/></svg>Phone</a>
-            <a href="" class="item" ng-class="{active: settings.device == 'tablet'}" ng-click="settings.device = 'tablet'"><svg width="38" height="38" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M400 0H80C54 0 32 22 32 48v416c0 26 22 48 48 48h320c26 0 48-22 48-48V48c0-26-22-48-48-48zM240 496a16 16 0 110-32 16 16 0 010 32zm144-48H96V64h288v384z"/></svg>Tablet</a>
-            <a href="" class="item" ng-class="{active: settings.device == 'desktop'}" ng-click="settings.device = 'desktop'"><svg width="38" height="38" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M512 416V32H0v384h224v32h-96v32h256v-32h-96v-32h224zM64 96h384v256H64V96z"/></svg>Desktop</a>
-            <a href="" class="item" ng-class="{active: settings.device == 'desktop-hd'}" ng-click="settings.device = 'desktop-hd'"><svg width="38" height="38" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M512 416V32H0v384h224v32h-96v32h256v-32h-96v-32zM64 96h384v256H64z"/><path d="M270 297V161h28c14 0 25 2 33 4 8 3 16 7 23 14 14 13 21 29 21 50s-7 38-22 51c-7 6-15 11-23 13-8 3-18 4-32 4zm20-19h10c9 0 16-1 23-3a47 47 0 0031-46c0-15-5-27-15-36-9-8-22-12-39-12h-10zm-123-64h59v-53h20v136h-20v-63h-59v63h-20V161h20z"/></svg>Desktop</a>
-        </div>
-        [ <a href="" class="showAdvanced" ng-click="settings.showAdvanced = !settings.showAdvanced">
-            <span ng-if="!settings.showAdvanced">Advanced settings &nbsp;✚</span>
-            <span ng-if="settings.showAdvanced">Hide advanced settings &nbsp;✖</span>
-        </a> ]
-        <span class="currentSettings" ng-if="!settings.showAdvanced && (settings.waitForSelector || settings.cookie || settings.authUser || settings.authPass || settings.proxy || settings.domains)">
-            Currently set:
-            <span ng-if="settings.waitForSelector">wait for selector</span>
-            <span ng-if="settings.cookie">cookie</span>
-            <span ng-if="settings.authUser || settings.authPass">authentication</span>
-            <span ng-if="settings.proxy">proxy</span>
-            <span ng-if="settings.domains">domain blocking</span>
-        </span>
-        <div class="advanced" ng-show="settings.showAdvanced">
-            <!--<div>
-                <div class="label">
-                    Wait selector
-                    <span class="settingsTooltip">
-                        <svg width="14" height="14" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#FFF" d="M224 352h64v64h-64zm128-224c18 0 32 14 32 32v96l-96 64h-64v-32l96-64v-32H160v-64h192zm-96-80A207 207 0 0048 256a207 207 0 00208 208 207 207 0 00208-208A207 207 0 00256 48zm0-48a256 256 0 110 512 256 256 0 010-512z"/></svg>
-                        <div><b>Wait for a CSS selector</b><br><br>Once the page is considered loaded, PhantomJS will repeatedly try to match the given CSS selector until it is found in the page. A 60 seconds timeout still applies anyway.<br><br>Example: "body.loaded"</div>
-                    </span>
-                </div>
-                <div><input type="text" name="waitForSelector" ng-model="settings.waitForSelector" /></div>
-            </div>-->
-            <div>
-                <div class="label">
-                    Cookie
-                    <span class="settingsTooltip">
-                        <svg width="14" height="14" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#FFF" d="M224 352h64v64h-64zm128-224c18 0 32 14 32 32v96l-96 64h-64v-32l96-64v-32H160v-64h192zm-96-80A207 207 0 0048 256a207 207 0 00208 208 207 207 0 00208-208A207 207 0 00256 48zm0-48a256 256 0 110 512 256 256 0 010-512z"/></svg>
-                        <div><b>Cookie</b><br><br>Adds cookies, separated by a pipe character.<br><br>Example: "bar1=foo1;domain=.domain1.com|bar2=foo2;domain=www.domain2.com"</div>
-                    </span>
-                </div>
-                <div><input type="text" name="cookie" ng-model="settings.cookie" /></div>
-            </div>
-            <div>
-                <div class="label">
-                    Authent
-                    <span class="settingsTooltip">
-                        <svg width="14" height="14" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#FFF" d="M224 352h64v64h-64zm128-224c18 0 32 14 32 32v96l-96 64h-64v-32l96-64v-32H160v-64h192zm-96-80A207 207 0 0048 256a207 207 0 00208 208 207 207 0 00208-208A207 207 0 00256 48zm0-48a256 256 0 110 512 256 256 0 010-512z"/></svg>
-                        <div><b>Basic HTTP authentication</b><br><br>Enter your credentials here if you need to bypass a basic authentication.<br><br><i>PS: if your authentication is not basic, you might be able to copy the session cookie from your browser, paste it in the "Cookie" setting and launch a run before your cookie expires.</i></div>
-                    </span>
-                </div>
-                <div class="subTable">
-                    <div>
-                        <div>username</div>
-                        <div><input type="text" class="authField" name="authUser" ng-model="settings.authUser" /></div>
-                    </div>
-                    <div>
-                        <div><span>password</div>
-                        <div><input type="password" class="authField" name="authPass" ng-model="settings.authPass" /></div>
-                    </div>
-                </div>
-            </div>
-            <div>
-                <div class="label">
-                    HTTP proxy
-                    <span class="settingsTooltip">
-                        <svg width="14" height="14" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#FFF" d="M224 352h64v64h-64zm128-224c18 0 32 14 32 32v96l-96 64h-64v-32l96-64v-32H160v-64h192zm-96-80A207 207 0 0048 256a207 207 0 00208 208 207 207 0 00208-208A207 207 0 00256 48zm0-48a256 256 0 110 512 256 256 0 010-512z"/></svg>
-                        <div><b>HTTP proxy</b><br><br>Insert here your proxy settings with the format "host:port".<br><br>Example: "192.168.10.0:3333"</div>
-                    </span>
-                </div>
-                <div><input type="text" name="proxy" ng-model="settings.proxy" /></div>
-            </div>
-            <div>
-                <div class="label">
-                    Block domains
-                    <span class="settingsTooltip">
-                        <svg width="14" height="14" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#FFF" d="M224 352h64v64h-64zm128-224c18 0 32 14 32 32v96l-96 64h-64v-32l96-64v-32H160v-64h192zm-96-80A207 207 0 0048 256a207 207 0 00208 208 207 207 0 00208-208A207 207 0 00256 48zm0-48a256 256 0 110 512 256 256 0 010-512z"/></svg>
-                        <div><b>Block some domains</b><br><br>One line per domain or subdomain.<br><br><i><b>Example:</b><br>google-analytics.com<br>ads.yahoo.com<br>ajax.googleapis.com</i><br><br>An empty allow list will block all domains except the main domain.</div>
-                    </span>
-                </div>
-                <div>
-                    <div>
-                        <input type="radio" name="blockOrAllow" ng-model="settings.domainsBlockOrAllow" value="block" />block list
-                        <input type="radio" name="blockOrAllow" ng-model="settings.domainsBlockOrAllow" value="allow" />allow list
-                    </div>
-                    <textarea name="domains" ng-model="settings.domains" rows="5"></textarea>
-                </div>
-            </div>
-        </div>
-    </div>
-</form>
-
-
-<div class="features">
-    <div>
-        <h3>Page speed audit</h3>
-        <p>Checks if performance good practices are respected</p>
-    </div>
-
-    <div>
-        <h3>Front-end analyzis</h3>
-        <p>Detects problems on HTML, CSS, JS, images, fonts and more</p>
-    </div>
-
-    <div>
-        <h3>In-depth details</h3>
-        <p>Provides precise information to fix the detected performance issues</p>
-    </div>
-</div>
-
-<%if (sponsoring.home) { %>
-    <div class="homeSponsor"><%- sponsoring.home %></div>
-<% } %>

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

@@ -1,46 +0,0 @@
-<p ng-if="url">Tested url: &nbsp; <a href="{{url}}" target="_blank" class="testedUrl">{{url}}</a></p>
-
-<div ng-if="!notFound && !connectionLost">
-    <div ng-if="status.statusCode == 'failed'">
-        <div class="status">Test failed</div>
-        <p class="statusSubMessage">{{status.error}}</p>
-        
-        <a class="linkButton" href="https://github.com/YellowLabTools/YellowLabTools/issues" target="_blank">Report the issue on GitHub</a>
-        <a class="linkButton" href="<%= baseUrl %>">New test</a>
-    </div>
-    <div ng-if="status.statusCode == 'awaiting'">
-        <div class="status">
-            <ng-pluralize count="status.position" when="{'one': 'Waiting behind 1 other test', 'other': 'Waiting behind {} other tests'}">
-            </ng-pluralize>
-        </div>
-        <p class="statusSubMessage">(auto-refresh activated)</p>
-    </div>
-    <div ng-if="status.statusCode == 'running'">
-        <div class="status">Test is running...</div>
-        <div class="progress">
-            <div class="progressBarEmpty">
-                <div class="progressBarFilled" ng-style="{'width': (progress.estimatedProgress*100) + '%'}"></div>
-            </div>
-        </div>
-        <p class="statusSubMessage" ng-if="!progress">(Phantomas launched)</p>
-        <p class="statusSubMessage" ng-if="progress.milestone == 'domReady'">(DOM Ready fired)</p>
-        <p class="statusSubMessage" ng-if="progress.milestone == 'domComplete'">(page loaded, waiting for late requests)</p>
-        <p class="statusSubMessage" ng-if="progress.milestone == 'phantomas'">(now simulating compression, optimization and minification)</p>
-        <p class="statusSubMessage" ng-if="progress.milestone == 'redownload'">(calculating score and retrieving screenshot)</p>
-    </div>
-    <div ng-if="status.statusCode == 'complete'">
-        <div class="status">Test complete</div>
-        <p class="statusSubMessage">Opening results...</p>
-    </div>
-</div>
-<div ng-if="notFound == true">
-    <div class="status">Error 404 (test not found)</div>
-    <p class="statusSubMessage">The server probably just rebooted. We are very sorry about that, please try to launch the test again.</p>
-    
-    <a class="linkButton" href="<%= baseUrl %>">New test</a>
-</div>
-<div ng-if="connectionLost == true">
-    <div class="status">Connection lost with server</div>
-    <p class="statusSubMessage">Check your wifi cable, or maybe YellowLab.tools is rebooting.</p>
-</div>
-<canvas id="faviconRotator" hidden width=32 height=32></canvas>

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

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

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

@@ -1,514 +0,0 @@
-<div ng-include="'views/resultSubHeader.html'"></div>
-<div class="rule board">
-    <div class="backToDashboard"><a href="#" ng-click="backToDashboard()">Back to dashboard</a></div>
-
-    <div ng-if="rule" class="ruleTable">
-        <div class="left">
-            <h2>{{rule.policy.label}}</h2>
-            <grade score="rule.score" class="score"></grade>
-            <div>{{rule.score}}/100</div>
-        </div>
-        <div class="right">
-            <h3>
-                Value:
-                <span ng-if="rule.policy.unit == 'bytes'">{{rule.value | bytes}}</span>
-                <span ng-if="rule.policy.unit != 'bytes'">{{rule.value}}<span ng-if="rule.policy.unit"> {{rule.policy.unit}}</span></span>
-            </h3>
-            <div class="okThreshold" ng-if="rule.score < 100 && rule.policy.isOkThreshold !== undefined">
-                Have
-                <span ng-if="rule.policy.unit == 'bytes'">{{rule.policy.isOkThreshold | bytes}}</span>
-                <span ng-if="rule.policy.unit != 'bytes'">{{rule.policy.isOkThreshold}}<span ng-if="rule.policy.unit"> {{rule.policy.unit}}</span></span>
-                <span ng-if="rule.policy.isOkThreshold > 0 && rule.policy.isOkThreshold < rule.policy.isBadThreshold">or less</span>
-                <span ng-if="rule.policy.isOkThreshold > rule.policy.isBadThreshold">or more</span>
-                to get the 100/100 score on this issue.
-            </div>
-            <div class="okThreshold" ng-if="rule.globalScoreIfFixed > result.scoreProfiles.generic.globalScore && rule.globalScoreIfFixed > 0 && result.scoreProfiles.generic.globalScore >= 0">
-                Your new global score would increase by {{rule.globalScoreIfFixed - result.scoreProfiles.generic.globalScore}} points ({{rule.globalScoreIfFixed}}/100).
-            </div>
-            <div class="okThreshold" ng-if="rule.globalScoreIfFixed > result.scoreProfiles.generic.globalScore && rule.globalScoreIfFixed > 0 && result.scoreProfiles.generic.globalScore < 0">
-                Your new global score would increase by {{rule.globalScoreIfFixed}} points ({{rule.globalScoreIfFixed}}/100).
-            </div>
-            <div class="okThreshold" ng-if="rule.globalScoreIfFixed > result.scoreProfiles.generic.globalScore && rule.globalScoreIfFixed <= 0">
-                Your new global score would increase, but still not enough to reach 0/100. That's embarassing...
-            </div>
-            <div class="okThreshold" ng-if="rule.globalScoreIfFixed == result.scoreProfiles.generic.globalScore && rule.score < 100">
-                Your new global score would slightly increase, but not enough to gain a single point.
-            </div>
-            <div ng-bind-html="rule.policy.message" class="message"></div>
-        </div>
-    </div>
-    <div ng-if="rule.abnormal" class="warning">
-        <h3>Warning</h3>
-        <p>This rule reached the abnormality threshold, which means there is a real problem you should care about.</p>
-    </div>
-    <div class="offenders" ng-if="rule.policy.hasOffenders">
-        <h3 ng-if="rule.offendersObj.count >= 0"><ng-pluralize count="rule.offendersObj.count" when="{'0': 'No offender', 'one': '1 offender', 'other': '{} offenders'}"></ng-pluralize></h3>
-
-        <div ng-if="rule.offendersObj.list" class="offendersTable">
-            <div ng-repeat="offender in rule.offendersObj.list track by $index">
-                <div ng-if="offender.parseError">
-                    {{offender.parseError}}
-                </div>
-                <div ng-if="!offender.parseError">
-
-                    <div ng-if="policyName === 'iframesCount'">
-                        <span ng-if="offender.url">{{offender.url}}</span>
-                        <span ng-if="!offender.url">an iframe without URL</span>
-                    </div>
-
-                    <div ng-if="policyName === 'DOMidDuplicated'">
-                        <b>{{offender.id}}</b>: {{offender.count}} occurrences
-                    </div>
-
-                    <div ng-if="policyName === 'DOMqueriesAvoidable'">
-                        <b>{{offender.query}}</b> (in <dom-element-button obj="offender.context"></dom-element-button>) using {{offender.fn}}: <b>{{offender.count}} queries</b>
-                    </div>
-
-                    <div ng-if="policyName === 'eventsScrollBound'">
-                        <span ng-if="offender.target == 'window'">Scroll event bound on <b>window</b></span>
-                        <span ng-if="offender.target == '#document'">Scroll event bound on <b>document</b></span>
-                        <span ng-if="offender.target == 'window.onscroll'"><b>window.onscroll</b> function declared</span>
-                        <div class="offenderButton" ng-if="offender.backtrace.length == 0">no backtrace</div>
-                        <div class="offenderButton opens" ng-if="offender.backtrace.length > 0">
-                            backtrace
-                            <div class="backtrace">
-                                <div ng-repeat="obj in offender.backtrace track by $index">
-                                    <span ng-if="obj.functionName">{{obj.functionName}}()</span>
-                                    <url-link url="obj.file" max-length="60"></url-link>
-                                    line {{obj.line}}
-                                </div>
-                            </div>
-                        </div>
-                    </div>
-
-                    <div ng-if="policyName === 'jsErrors'">
-                        <b>{{offender.error}}</b>
-                        <div class="offenderButton" ng-if="offender.backtrace.length == 0">no backtrace</div>
-                        <div class="offenderButton opens" ng-if="offender.backtrace.length > 0">
-                            backtrace
-                            <div class="backtrace">
-                                <div ng-repeat="obj in offender.backtrace track by $index">
-                                    <span ng-if="obj.functionName">{{obj.functionName}}()</span>
-                                    <url-link url="obj.file" max-length="60"></url-link>
-                                    line {{obj.line}}
-                                </div>
-                            </div>
-                        </div>
-                    </div>
-
-                    <div ng-if="policyName === 'jQueryFunctionsUsed'">
-                        function <b>{{offender.functionName}}</b> used {{offender.count}} times
-                    </div>
-
-                    <div ng-if="policyName === 'documentWriteCalls'">
-                        <b>{{offender.writeFn}}</b>
-                        <span ng-if="offender.from">
-                            called from
-                            <span ng-if="offender.from.functionName">{{offender.from.functionName}}()</span>
-                            <url-link url="offender.from.file" max-length="50"></url-link>
-                            line {{offender.from.line}}
-                        </span>
-                        <span ng-if="!offender.from">
-                            called from (no backtrace available)
-                        </span>
-                    </div>
-
-                    <div ng-if="policyName === 'cssRules'">
-                        <span ng-if="offender.url === '[inline CSS]'">inline CSS</span>
-                        <span ng-if="offender.url !== '[inline CSS]'"><url-link url="offender.url" max-length="80"></url-link></span>
-                        : <ng-pluralize count="offender.value" when="{'0': '0 rule', 'one':'1 rule','other':'{} rules'}"></ng-pluralize>
-                    </div>
-
-                    <div ng-if="policyName === 'similarColors'">
-                        <div class="similarColors checker"><div ng-style="{'background-color': offender.color1, 'color': offender.isDark ? '#FFF' : '#000'}">{{offender.color1}}</div><div ng-style="{'background-color': offender.color2, 'color': offender.isDark ? '#FFF' : '#000'}">{{offender.color2}}</div></div>
-                    </div>
-
-                    <div ng-if="policyName === 'cssParsingErrors'">
-                        <b>{{offender.error}}</b>
-                        <file-and-line file="offender.file" line="offender.line" column="offender.column"></file-and-line>
-                        <span ng-if="offender.file">(<a href="http://jigsaw.w3.org/css-validator/validator?profile=css3&usermedium=all&warning=no&uri={{offender.file | encodeURIComponent}}" target="_blank">Check on the W3C validator</a>)</span>
-                    </div>
-
-                    <div ng-if="policyName === 'cssImports'">
-                        {{offender.css}}
-                        <file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
-                    </div>
-
-                    <div ng-if="policyName === 'cssOldPropertyPrefixes'">
-                        <b>{{offender.property}}</b> {{offender.message}}
-                        <div ng-if="offender.rules.length" ng-click="offender.showMore = !offender.showMore" class="offenderButton">
-                            <span ng-if="!offender.showMore">show</span>
-                            <span ng-if="offender.showMore">hide</span>
-                            <ng-pluralize count="offender.rules.length" when="{'one':'1 offender','other':'{} offenders'}"></ng-pluralize>
-                        </div>
-                        <div ng-if="offender.showMore" class="smallerOffenders">
-                            <div ng-repeat="cssRule in offender.rules">
-                                {{cssRule.rule}} {{'{' + offender.property}}: {{cssRule.value + '}' }}
-                                <file-and-line-button file="cssRule.file" line="cssRule.line" column="cssRule.column"></file-and-line-button>
-                            </div>
-                        </div>
-                    </div>
-
-                    <div ng-if="policyName === 'lazyLoadableImagesBelowTheFold'">
-                        <img ng-src="{{offender.url | https}}" class="smallPreview checker"></img>
-                        <url-link url="offender.url" max-length="70"></url-link> (offset: {{offender.offset | roundNbr}}px)
-                    </div>
-
-                    <div ng-if="policyName === 'hiddenImages'">
-                        <img ng-src="{{offender | https}}" class="smallPreview checker"></img>
-                        <url-link url="offender" max-length="100"></url-link>
-                    </div>
-
-                    <div ng-if="policyName === 'imagesTooLarge'">
-                        <img ng-src="{{offender.url | https}}" class="smallPreview checker"></img>
-                        <div>{{offender.width}}x{{offender.height}}</div>
-                        <url-link url="offender.url" max-length="100"></url-link>
-                    </div>
-
-                    <div ng-if="policyName === 'notFound' || policyName === 'emptyRequests' || policyName === 'closedConnections' || policyName === 'multipleRequests' || policyName === 'cachingDisabled' || policyName === 'cachingNotSpecified'">
-                        <url-link url="offender" max-length="100"></url-link>
-                    </div>
-
-                    <div ng-if="policyName === 'cachingTooShort'">
-                        <url-link url="offender.url" max-length="100"></url-link>
-                        cached for <b>{{offender.ttlWithUnit}} {{offender.unit}}</b>
-                    </div>
-
-                    <div ng-if="policyName === 'domains'">
-                        <b>{{offender.domain}}</b>
-                        (<ng-pluralize count="offender.requests" when="{'one':'1 request','other':'{} requests'}"></ng-pluralize>)
-                    </div>
-
-                    <div ng-if="policyName === 'globalVariables'">
-                        {{offender}}
-                    </div>
-
-                    <div ng-if="policyName === 'jQueryVersionsLoaded'">
-                        {{offender.version}}
-                    </div>
-
-                    <div ng-if="policyName === 'synchronousXHR'">
-                        {{offender.url}}
-                    </div>
-
-                    <div ng-if="policyName === 'fontsCount'">
-                        <url-link url="offender.url" max-length="70"></url-link>
-                        ({{offender.size | bytes}})
-                    </div>
-
-                    <div ng-if="policyName === 'oldHttpProtocol'">
-                        <b>{{offender.domain}}</b> sends <span ng-class="offender.requests > 4 ? 'offenderProblem' : ''"><b><ng-pluralize count="offender.requests" when="{'one':'1 request','other':'{} requests'}"></ng-pluralize></b></span> over {{offender.httpVersion}}
-                    </div>
-
-                    <div ng-if="policyName === 'oldTlsProtocol'">
-                        <b>{{offender.domain}}</b> uses {{offender.tlsVersion}} <span ng-if="offender.beforeDomReady === true" class="offenderProblem">and seems to be on the critical path</span>
-                    </div>
-                </div>
-            </div>
-        </div>
-
-        <div ng-repeat="fileDetails in rule.offendersObj.byFile track by $index">
-            <h3>
-                <ng-pluralize count="fileDetails.count" when="{'one': '1 offender', 'other': '{} offenders'}"></ng-pluralize>
-                in
-                <url-link ng-if="fileDetails.url !== 'Inline CSS' && fileDetails.url !== '[inline CSS]'" url="fileDetails.url" max-length="80"></url-link>
-                <span ng-if="fileDetails.url === 'Inline CSS' || fileDetails.url === '[inline CSS]'">inline CSS</span>
-            </h3>
-
-            <div class="offendersTable">
-                <div ng-repeat="offender in fileDetails.offenders track by $index">
-                    <div ng-if="policyName === 'cssComplexSelectors' || policyName === 'cssComplexSelectorsByAttribute' || policyName === 'cssUniversalSelectors' || policyName === 'cssRedundantBodySelectors' || policyName === 'cssRedundantChildNodesSelectors'">
-                        <span ng-if="offender.bolded" ng-bind-html="offender.bolded"></span>
-                        <b ng-if="!offender.bolded">{{offender.css}}</b>
-                        <span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
-                    </div>
-
-                    <div ng-if="policyName === 'cssMobileFirst'">
-                        <b>{{offender.query}}</b> for <ng-pluralize count="offender.rules" when="{'one':'1 rule','other':'{} rules'}"></ng-pluralize>
-                        <span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
-                    </div>
-
-                    <div ng-if="policyName === 'cssDuplicatedSelectors'">
-                        {{offender.rule}} (<b>x{{offender.occurrences}}</b>)
-                    </div>
-
-                    <div ng-if="policyName === 'cssDuplicatedProperties'">
-                        Property <b>{{offender.property}}</b> duplicated in <b>{{offender.rule}} { }</b>
-                        <span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
-                    </div>
-
-                    <div ng-if="policyName === 'cssEmptyRules'">
-                        <b>{{offender.css}} { }</b>
-                        <span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
-                    </div>
-
-                    <div ng-if="policyName === 'cssImportants'">
-                        {{offender.rule}} {{ '{' + offender.property}}: {{offender.value}} <b>!important</b>}
-                        <span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
-                    </div>
-
-                    <div ng-if="policyName === 'cssOldIEFixes'">
-                        <span ng-if="offender.browser"><b>{{offender.browser}} fix:</b></span>
-                        <span ng-bind-html="offender.bolded"></span>
-                        <span ng-if="offender.line !== null && offender.column !== null"> @ {{offender.line}}:{{offender.column}}</span>
-                    </div>
-                </div>
-            </div>
-        </div>
-
-        <div ng-if="!rule.offendersObj.list && !rule.offendersObj.byFile" class="offendersHtml">
-            
-            <div ng-if="policyName === 'DOMelementMaxDepth'">
-                <dom-tree tree="rule.offendersObj.tree"></dom-tree>
-            </div>
-
-            <div ng-if="policyName === 'cssColors' && rule.offendersObj.count > 0">
-                <p>This is the colors palette, sized by total occurrences:</p>
-                <div class="colorPalette checker">
-                    <div ng-repeat="offender in rule.offendersObj.palette" style="background-color: {{offender.color}}; width: {{offender.occurrences * 100 / rule.offendersObj.palette[0].occurrences}}%"><div>{{offender.color}} ({{offender.occurrences}} times)</div></div>
-                </div>
-            </div>
-        </div>
-    </div>
-
-    <div ng-if="policyName === 'totalWeight'">
-        <h3>Weight by MIME type</h3>
-        <div class="totalWeightPie">
-            <canvas class="chart chart-doughnut" chart-data="weightData" chart-labels="weightLabels" chart-options="weightOptions" chart-colors="weightColours"></canvas>
-        </div>
-        <div ng-repeat="type in weightLabels">
-            <h3>{{rule.offendersObj.list.byType[type].totalWeight | bytes}} of {{type}}</h3>
-            <div class="offendersTable">
-                <div ng-repeat="request in rule.offendersObj.list.byType[type].requests | orderBy:'-weight'" ng-if="request.weight > 0">
-                    <div><url-link url="request.url" max-length="60"></url-link></div>
-                    <div ng-class="{offenderProblem: request.weight > 102400}">{{request.weight | bytes}}</div>
-                </div>
-            </div>
-        </div>
-    </div>
-
-    <div ng-if="policyName === 'DOMaccesses'">
-        <div ng-repeat="(type, list) in rule.offendersObj.list.byType">
-            <h3>
-                <ng-pluralize count="list.length" when="{'0': 'No offender', 'one': '1 offender', 'other': '{} offenders'}"></ng-pluralize> from
-                <span ng-if="type === 'DOMqueriesById'">getElementById()</span>
-                <span ng-if="type === 'DOMqueriesByTagName'">getElementsByTagName()</span>
-                <span ng-if="type === 'DOMqueriesByClassName'">getElementsByClassName()</span>
-                <span ng-if="type === 'DOMqueriesByQuerySelectorAll'">querySelector() or querySelectorAll()</span>
-                <span ng-if="type === 'DOMinserts'">appendChild() or insertBefore()</span>
-                <span ng-if="type === 'DOMmutationsInserts'">added nodes</span>
-                <span ng-if="type === 'DOMmutationsRemoves'">removed nodes</span>
-                <span ng-if="type === 'DOMmutationsAttributes'">attribute changes</span>
-                <span ng-if="type === 'eventsBound'">addEventListener()</span>
-            </h3>
-            <div class="offendersTable">
-                <div ng-repeat="access in list">
-                    <div ng-if="type === 'DOMqueriesById'">#{{access.id}}</div>
-                    <div ng-if="type === 'DOMqueriesByTagName'">{{access.tag}} <b>on</b> <span title="{{access.node}}">{{access.node | lastDOMNode}}</span></div>
-                    <div ng-if="type === 'DOMqueriesByClassName'">.{{access.class}} <b>on</b> <span title="{{access.node}}">{{access.node | lastDOMNode}}</span></div>
-                    <div ng-if="type === 'DOMqueriesByQuerySelectorAll'">{{access.selector}} <b>on</b> <span title="{{access.node}}">{{access.node | lastDOMNode}}</span></div>
-                    <div ng-if="type === 'DOMinserts'"><span title="{{access.append}}">{{access.append | lastDOMNode}}</span> <b>added to</b> <span title="{{access.node}}">{{access.node | lastDOMNode}}</span></div>
-                    <div ng-if="type === 'DOMmutationsInserts'">{{access.node}} <b>added to</b> {{access.target}}</div>
-                    <div ng-if="type === 'DOMmutationsRemoves'">{{access.node}} <b>removed from</b> {{access.target}}</div>
-                    <div ng-if="type === 'DOMmutationsAttributes'">{{access.attribute}} <b>changed on</b> {{access.node}}</div>
-                    <div ng-if="type === 'eventsBound'">{{access.eventType}} <b>on</b> <span title="{{access.path}}">{{access.path | lastDOMNode}}</span></div>
-                </div>
-            </div>
-        </div>
-    </div>
-
-    <div ng-if="policyName === 'imageOptimization'">
-        <h3 ng-if="rule.value > 0">{{rule.value | bytes}} could be saved on <ng-pluralize count="rule.offendersObj.list.images.length" when="{'one': '1 image', 'other': '{} images'}"></ng-pluralize></h3>
-        <div class="imageOffenders">
-            <div ng-repeat="image in rule.offendersObj.list.images | orderBy:'-gain'">
-                <div>
-                    Current file: <url-link url="image.url" max-length="50"></url-link>
-                    <div><a href="{{image.url}}" target="_blank"><img ng-src="{{image.url | https}}" class="checker" /></a></div>
-                </div>
-                <div>
-                    <p ng-if="!image.isCompressible || image.isCompressed">Current weight: {{image.originalWeigth | bytes}}</p>
-                    <p ng-if="image.isCompressible && !image.isCompressed">Current weight: {{image.originalWeigth | bytes}} ({{image.originalCompressedWeight | bytes}} compressed)</p>
-
-                    <p ng-if="image.lossless && image.isCompressible">With a lossless optimization:<br/>{{image.afterOptimizationAndCompression | bytes}} compressed (<b>-{{image.gain | bytes}}</b> compressed)</p>
-                    <p ng-if="image.lossless && !image.isCompressible">With a lossless optimization:<br/>{{image.lossless | bytes}} <span ng-if="!image.lossy">(<b>-{{image.gain | bytes}}</b>)</span></p>
-
-                    <p ng-if="image.lossy && image.isCompressible">With a lossy optimization:<br/>{{image.afterOptimizationAndCompression | bytes}} compressed (<b>-{{image.gain | bytes}} compressed</b>)</p>
-                    <p ng-if="image.lossy && !image.isCompressible">With a lossy optimization:<br/>{{image.lossy | bytes}} (<b>-{{image.gain | bytes}}</b>)</p>
-                </div>
-            </div>
-        </div>
-    </div>
-
-    <div ng-if="policyName === 'compression'">
-        <h3 ng-if="rule.value > 0">{{rule.value | bytes}} could be saved on <ng-pluralize count="rule.offendersObj.list.files.length" when="{'one': '1 file', 'other': '{} files'}"></ng-pluralize></h3>
-        <div class="table">
-            <div class="headers">
-                <div>File</div>
-                <div>Current weight</div>
-                <div>Gzip weight</div>
-                <div>Brotli</div>
-                <div>Gain</div>
-            </div>
-            <div ng-repeat="file in rule.offendersObj.list.files | orderBy:'-gain'">
-                <div>
-                    <url-link url="file.url" max-length="60"></url-link>
-                </div>
-                <div>{{file.originalSize | bytes}}</div>
-                
-                <div ng-if="file.wasCompressed"><i>already gzipped</i></div>
-                <div ng-if="!file.wasCompressed">{{file.gzipped | bytes}}</div>
-
-                <div>{{file.brotlified | bytes}}</div>
-                
-                <div><b>-{{file.gain | bytes}}</b></div>
-            </div>
-        </div>
-    </div>
-
-    <div ng-if="policyName === 'fileMinification'">
-        <h3 ng-if="rule.value > 0">{{rule.value | bytes}} could be saved on <ng-pluralize count="rule.offendersObj.list.files.length" when="{'one': '1 file', 'other': '{} files'}"></ng-pluralize></h3>
-        <div class="table">
-            <div class="headers">
-                <div>File</div>
-                <div>Current weight</div>
-                <div>Minified</div>
-                <div>Gain</div>
-            </div>
-            <div ng-repeat="file in rule.offendersObj.list.files | orderBy:'-gain'">
-                <div>
-                    <url-link url="file.url" max-length="60"></url-link>
-                </div>
-                <div ng-if="file.isCompressed">{{file.originalWeigth | bytes}} (compressed)</div>
-                <div ng-if="!file.isCompressed">{{file.originalWeigth | bytes}} ({{file.originalCompressedWeight | bytes}} compressed)</div>
-                <div ng-if="file.isCompressed">{{file.afterOptimizationAndCompression | bytes}} (compressed)</div>
-                <div ng-if="!file.isCompressed">{{file.optimized | bytes}} ({{file.afterOptimizationAndCompression | bytes}} compressed)</div>
-                <div><b>-{{file.gain | bytes}}</b></div>
-            </div>
-        </div>
-    </div>
-
-    <div ng-if="policyName === 'totalRequests'">
-        <h3>Requests by MIME type</h3>
-        <div ng-repeat="(type, requests) in rule.offendersObj.list.byType">
-            <h3><ng-pluralize count="requests.length" when="{'0': 'No ' + type + ' request', 'one': '1 ' + type + ' request', 'other': '{} ' + type + ' requests'}"></ng-pluralize></h3>
-            <p ng-if="type == 'css' && requests.length > 2">Reduce the number of stylesheets by concatenating them.</p>
-            <p ng-if="type == 'js' && requests.length > 3">Reduce the number of scripts by concatenating them.</p>
-            <p ng-if="type == 'image' && requests.length > 5">Reduce the number of images by lazyloading them or by spriting them.</p>
-            <p ng-if="type == 'webfont' && requests.length > 1">Fonts are generally loaded on the critical path of the head. Load as few as possible.</p>
-            <p ng-if="type == 'other' && requests.length > 0">They can be Flash, XML, music or any undetected format.</p>
-            <div class="offendersTable">
-                <div ng-repeat="request in requests track by $index">
-                    <div><url-link url="request" max-length="100"></url-link></div>
-                </div>
-            </div>
-        </div>
-    </div>
-
-    <div ng-if="policyName === 'identicalFiles'">
-        <div ng-repeat="offender in rule.offendersObj.list track by $index">
-            <h4>A file of {{offender.weight | bytes}} is loaded {{offender.urls.length}} times:</h4>
-            <div class="offendersTable">
-                <div ng-repeat="url in offender.urls">
-                    <div><url-link url="url" max-length="100"></url-link></div>
-                </div>
-            </div>
-        </div>
-    </div>
-
-    <div ng-if="policyName === 'cssBreakpoints'">
-        <div ng-if="rule.value > 0">
-            <h3>Breakpoints list</h3>
-            <div class="offendersTable">
-                <div ng-repeat="offender in rule.offendersObj | orderBy:'pixels'">
-                    <div>Breakpoint <b>{{offender.breakpoint}}</b> involves <ng-pluralize count="offender.count" when="{'one': '1 rule', 'other': '{} rules'}"></ng-pluralize></div>
-                </div>
-            </div>
-        </div>
-        <div ng-if="rule.value === 0">
-        No breakpoint
-        </div>
-    </div>
-
-    <div ng-if="policyName === 'heavyFonts'">
-        <div ng-repeat="font in rule.offendersObj.fonts | orderBy:'-weight' track by $index">
-            <h3><url-link url="font.url" max-length="80"></url-link></h3>
-            <div class="offendersTable">
-                <div>
-                    <div>Weight</div>
-                    <div ng-if="font.weight <= 40960">{{font.weight | bytes}}</div>
-                    <div ng-if="font.weight > 40960" class="offenderProblem">{{font.weight | bytes}}</div>
-                </div>
-                <div>
-                    <div>Number of glyphs</div>
-                    <div ng-if="font.numGlyphs <= 500">{{font.numGlyphs}}</div>
-                    <div ng-if="font.numGlyphs > 500" class="offenderProblem">{{font.numGlyphs}} (better &lt; 500)</div>
-                </div>
-                <div>
-                    <div>Average glyph complexity</div>
-                    <div ng-if="font.averageGlyphComplexity <= 35">{{font.averageGlyphComplexity}}</div>
-                    <div ng-if="font.averageGlyphComplexity > 35" class="offenderProblem">{{font.averageGlyphComplexity}} (better &lt; 35)</div>
-                </div>
-            </div>
-        </div>
-    </div>
-
-    <div ng-if="policyName === 'unusedUnicodeRanges'">
-        <div ng-repeat="font in rule.offendersObj.fonts | orderBy:'-compressedWeigth' track by $index">
-            <h3><url-link url="font.url" max-length="60"></url-link> ({{font.weight | bytes}})</h3>
-            <div ng-if="font.isIconFont" class="offendersTable">
-                <div>
-                    <div>
-                        This font seems to be an icon font
-                        <span ng-if="font.numGlyphsInCommonWithPageContent / font.glyphs <= 0.05" class="offenderProblem">but only {{font.numGlyphsInCommonWithPageContent}} of its {{font.glyphs}} glyphs <ng-pluralize count="font.numGlyphsInCommonWithPageContent" when="{'one': 'is', 'other': 'are'}"></ng-pluralize> possibly used!</span>
-                        <span ng-if="font.numGlyphsInCommonWithPageContent / font.glyphs > 0.05">and {{font.numGlyphsInCommonWithPageContent}} of its {{font.glyphs}} glyphs <ng-pluralize count="font.numGlyphsInCommonWithPageContent" when="{'one': 'is', 'other': 'are'}"></ng-pluralize> possibly used.</span>
-                    </div>
-                </div>
-            </div>
-            <div ng-if="!font.isIconFont" class="offendersTable">
-                <div ng-repeat="range in font.unicodeRanges track by $index">
-                    <div><b>{{range.name}}</b></div>
-                    <div ng-if="!range.underused">{{range.numGlyphsInCommonWithPageContent}} of its {{range.charset.length}} glyphs <ng-pluralize count="range.numGlyphsInCommonWithPageContent" when="{'one': 'is', 'other': 'are'}"></ng-pluralize> possibly used</div>
-                    <div ng-if="range.underused" class="offenderProblem">{{range.numGlyphsInCommonWithPageContent}} of its {{range.charset.length}} glyphs are used</div>
-                    <div>
-                        <div class="offenderButton opens">
-                            glyphes list
-                            <div>{{range.charset | addSpaces}}</div>
-                        </div>
-                    </div>
-                </div>
-                <div ng-if="font.ligaturesOrHiddenChars > 0">
-                    <div><b>Ligatures or hidden chars</b></div>
-                    <div ng-class="{offenderProblem: (font.ligaturesOrHiddenChars > 25)}">{{font.ligaturesOrHiddenChars}} glyphs</div>
-                    <div></div>
-                </div>
-            </div>
-        </div>
-    </div>
-
-    <div ng-if="policyName === 'nonWoff2Fonts'">
-        <h3 ng-if="rule.value > 0">{{rule.value | bytes}} could be saved on <ng-pluralize count="rule.offendersObj.list.fonts.length" when="{'one': '1 file', 'other': '{} files'}"></ng-pluralize></h3>
-        <div class="table">
-            <div class="headers">
-                <div>File</div>
-                <div>Current weight</div>
-                <div>WOFF 2 weight</div>
-                <div>Gain</div>
-            </div>
-            <div ng-repeat="file in rule.offendersObj.list.fonts | orderBy:'-gain'">
-                <div>
-                    <url-link url="file.url" max-length="70"></url-link>
-                </div>
-                <div>{{file.originalSize | bytes}}</div>
-                <div>{{file.woff2Size | bytes}}</div>
-                <div><b>-{{file.gain | bytes}}</b></div>
-            </div>
-        </div>
-    </div>
-
-    <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>

+ 0 - 13
front/src/views/screenshot.html

@@ -1,13 +0,0 @@
-<div ng-include="'views/resultSubHeader.html'"></div>
-<div class="screenshot board">
-    <h2>Screenshot</h2>
-
-    <div class="screenshotWrapper" ng-class="result.params.options.device || 'phone'">
-        <div>
-            <img ng-if="result.screenshotUrl" class="screenshotImage" ng-src="{{result.screenshotUrl}}"/>
-            <span ng-if="!result.screenshotUrl" class="screenshotError">Screenshot not available</span>
-        </div>
-    </div>
-
-    <div class="backToDashboard"><a href="#" ng-click="backToDashboard()">Back to dashboard</a></div>
-</div>

+ 4 - 1
lib/index.js

@@ -2,6 +2,8 @@ var Q = require('q');
 
 
 var Runner = require('./runner');
 var Runner = require('./runner');
 
 
+var packageJson = require('../package.json');
+
 
 
 var yellowLabTools = function(url, options) {
 var yellowLabTools = function(url, options) {
     'use strict';
     'use strict';
@@ -39,4 +41,5 @@ var yellowLabTools = function(url, options) {
     return deferred.promise;
     return deferred.promise;
 };
 };
 
 
-module.exports = yellowLabTools;
+module.exports = yellowLabTools;
+module.exports.version = packageJson.version;

+ 0 - 141
lib/screenshotHandler.js

@@ -1,141 +0,0 @@
-var debug       = require('debug')('ylt:screenshotHandler');
-var Jimp        = require('jimp');
-var Q           = require('q');
-var fs          = require('fs');
-var path        = require('path');
-
-
-var screenshotHandler = function() {
-
-    var tmpFolderPath = 'tmp';
-    var tmpFolderFullPath = path.join(__dirname, '..', tmpFolderPath);
-    var tmpFileName = 'temp-screenshot.png';
-    var tmpFileFullPath = path.join(tmpFolderFullPath, tmpFileName);
-
-
-    this.findAndOptimizeScreenshot = function(width) {
-        var that = this;
-
-        debug('Starting screenshot transformation');
-
-        return this.openImage(tmpFileFullPath)
-
-            .then(function(image) {
-                that.deleteTmpFile(tmpFileFullPath);
-                return that.resizeImage(image, width);
-            })
-
-            .then(this.toBuffer);
-    };
-
-
-    this.openImage = function(imagePath) {
-        var deferred = Q.defer();
-
-        Jimp.read(imagePath, function(err, image){
-            if (err) {
-                debug('Could not open imagePath %s', imagePath);
-                debug(err);
-
-                deferred.reject(err);
-            } else {
-                debug('Image correctly open');
-                deferred.resolve(image);
-            }
-        });
-
-        return deferred.promise;
-    };
-
-
-    this.resizeImage = function(image, newWidth) {
-        var deferred = Q.defer();
-
-        var currentWidth = image.bitmap.width;
-
-        if (currentWidth > 0) {
-            var ratio = newWidth / currentWidth;
-
-            image.scale(ratio, function(err, image){
-                if (err) {
-                    debug('Could not resize image');
-                    debug(err);
-
-                    deferred.reject(err);
-                } else {
-                    debug('Image correctly resized');
-                    deferred.resolve(image);
-                }
-            });
-        } else {
-            deferred.reject('Could not resize an empty image');
-        }
-
-        return deferred.promise;        
-    };
-
-
-    this.toBuffer = function(image) {
-        var deferred = Q.defer();
-
-        image.quality(85).getBuffer(Jimp.MIME_JPEG, function(err, buffer){
-            if (err) {
-                debug('Could not save image to buffer');
-                debug(err);
-
-                deferred.reject(err);
-            } else {
-                debug('Image correctly transformed to buffer');
-                deferred.resolve(buffer);
-            }
-        });
-
-        return deferred.promise;
-    };
-
-
-    this.deleteTmpFile = function(tmpFilePath) {
-        var deferred = Q.defer();
-
-        fs.unlink(tmpFilePath, function (err) {
-            if (err) {
-                debug('Screenshot temporary file not found, could not be deleted. But it is not a problem.');
-            } else {
-                debug('Screenshot temporary file deleted.');
-            }
-
-            deferred.resolve();
-        });
-
-        return deferred.promise;
-    };
-
-    // Create a /tmp folder on the project's root directory
-    this.createTmpScreenshotFolder = function() {
-        var deferred = Q.defer();
-
-        // Create the folder if it doesn't exist
-        fs.exists(tmpFolderFullPath, function(exists) {
-            if (exists) {
-                deferred.resolve();
-            } else {
-                debug('Creating the tmp image folder', tmpFolderFullPath);
-                fs.mkdir(tmpFolderFullPath, function(err) {
-                    if (err) {
-                        deferred.reject(err);
-                    } else {
-                        deferred.resolve();
-                    }
-                });
-            }
-        });
-
-        return deferred.promise;
-    };
-
-    this.getTmpFileRelativePath = function() {
-        return tmpFolderPath + '/' + tmpFileName;
-    };
-};
-
-module.exports = new screenshotHandler();

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

@@ -1,352 +0,0 @@
-var debug               = require('debug')('ylt:server');
-var Q                   = require('q');
-
-var ylt                 = require('../../index');
-var ScreenshotHandler   = require('../../screenshotHandler');
-var RunsQueue           = require('../datastores/runsQueue');
-var RunsDatastore       = require('../datastores/runsDatastore');
-var ResultsDatastore    = require('../datastores/resultsDatastore');
-
-var serverSettings      = (process.env.IS_TEST) ? require('../../../test/fixtures/settings.json') : require('../../../server_config/settings.json');
-
-var ApiController = function(app) {
-    'use strict';
-
-    var queue = new RunsQueue();
-    var runsDatastore = new RunsDatastore();
-    var resultsDatastore = new ResultsDatastore();
-
-    // Create a new run
-    app.post('/api/runs', function(req, res) {
-
-        // Add http to the test URL
-        if (req.body.url && req.body.url.toLowerCase().indexOf('http://') !== 0 && req.body.url.toLowerCase().indexOf('https://') !== 0) {
-            req.body.url = 'http://' + req.body.url;
-        }
-
-        // Block requests to unwanted websites (=spam)
-        if (req.body.url && isBlocked(req.body.url)) {
-            console.error('Test blocked for URL: %s', req.body.url);
-            res.status(403).send('Forbidden');
-            return;
-        }
-
-        // Grab the test parameters and generate a random run ID
-        var run = {
-            runId: (Date.now()*1000 + Math.round(Math.random()*1000)).toString(36),
-            params: {
-                url: req.body.url,
-                waitForResponse: req.body.waitForResponse === true || req.body.waitForResponse === 'true' || req.body.waitForResponse === 1,
-                partialResult: req.body.partialResult || null,
-                screenshot: req.body.screenshot || false,
-                device: req.body.device || 'desktop',
-                proxy: req.body.proxy || null,
-                waitForSelector: req.body.waitForSelector || null,
-                cookie: req.body.cookie || null,
-                authUser: req.body.authUser || null,
-                authPass: req.body.authPass || null,
-                blockDomain: req.body.blockDomain || null,
-                allowDomain: req.body.allowDomain || null,
-                noExternals: req.body.noExternals || false
-            }
-        };
-
-        // Create the tmp folder if it doesn't exist
-        ScreenshotHandler.createTmpScreenshotFolder(run.runId);
-
-        // Add test to the testQueue
-        debug('Adding test %s to the queue', run.runId);
-        var queuePromise = queue.push(run.runId);
-
-        // Save the run to the datastore
-        runsDatastore.add(run, queuePromise.startingPosition);
-
-
-        // Listening for position updates
-        queuePromise.progress(function(position) {
-            runsDatastore.updatePosition(run.runId, position);
-        });
-
-        // Let's start the run
-        queuePromise.then(function() {
-
-            runsDatastore.updatePosition(run.runId, 0);
-
-            console.log('Launching test ' + run.runId + ' on ' + run.params.url);
-
-            var runOptions = {
-                screenshot: run.params.screenshot ? ScreenshotHandler.getTmpFileRelativePath() : false,
-                device: run.params.device,
-                proxy: run.params.proxy,
-                waitForSelector: run.params.waitForSelector,
-                cookie: run.params.cookie,
-                authUser: run.params.authUser,
-                authPass: run.params.authPass,
-                blockDomain: run.params.blockDomain,
-                allowDomain: run.params.allowDomain,
-                noExternals: run.params.noExternals
-            };
-
-            return ylt(run.params.url, runOptions)
-
-            // Update the progress bar on each progress
-            .progress(function(progress) {
-                runsDatastore.updateRunProgress(run.runId, progress);
-            });
-
-        })
-
-        // Phantomas completed
-        .then(function(data) {
-
-            debug('Success');
-            data.runId = run.runId;
-
-            
-            // Some conditional steps exist if there is a screenshot
-            var screenshotPromise = Q.resolve();
-
-            if (run.params.screenshot) {
-
-                var screenshotSize = serverSettings.screenshotWidth ? serverSettings.screenshotWidth[run.params.device] : 400;
-
-                // Replace the empty promise created earlier with Q.resolve()
-                screenshotPromise = ScreenshotHandler.findAndOptimizeScreenshot(screenshotSize)
-                
-                    // Read screenshot
-                    .then(function(screenshotBuffer) {
-                        if (screenshotBuffer) {
-                            debug('Image optimized');
-                            data.screenshotBuffer = screenshotBuffer;
-                            data.screenshotUrl = '/api/results/' + data.runId + '/screenshot.jpg';
-                        }
-                    })
-
-                    // Don't worry if there's an error
-                    .fail(function(err) {
-                        debug('An error occured while creating the screenshot\'s thumbnail. Ignoring and continuing...');
-                        debug(err);
-                    });
-
-            }
-
-            // Let's continue
-            return screenshotPromise
-
-                // Save results
-                .then(function() {
-                    // Remove uneeded temp screenshot path
-                    delete data.params.options.screenshot;
-
-                    // Here we can remove tools results if not needed
-                    delete data.toolsResults.phantomas.offenders.requests;
-                    
-                    return resultsDatastore.saveResult(data);
-                })
-
-                // Mark as the run as complete and send the response if the request is still waiting
-                .then(function() {
-
-                    debug('Result saved in datastore');
-
-                    runsDatastore.markAsComplete(run.runId);
-
-                    if (run.params.waitForResponse) {
-
-                        // If the user only wants a portion of the result (partialResult option)
-                        switch(run.params.partialResult) {
-                            case 'generalScores': 
-                                res.redirect(302, '/api/results/' + run.runId + '/generalScores');
-                                break;
-                            case 'rules': 
-                                res.redirect(302, '/api/results/' + run.runId + '/rules');
-                                break;
-                            case 'javascriptExecutionTree':
-                                res.redirect(302, '/api/results/' + run.runId + '/javascriptExecutionTree');
-                                break;
-                            case 'phantomas':
-                                res.redirect(302, '/api/results/' + run.runId + '/toolsResults/phantomas');
-                                break;
-                            default:
-                                res.redirect(302, '/api/results/' + run.runId);
-                        }
-                    }
-                                    
-                })
-                .fail(function(err) {
-                    console.error('Test failed for URL: %s', run.params.url);
-                    console.error(err.toString());
-
-                    runsDatastore.markAsFailed(run.runId, err.toString());
-
-                    res.status(500).send('An error occured');
-                });
-
-        })
-
-        .fail(function(err) {
-                    
-            console.error('Test failed for URL: %s', run.params.url);
-            console.error(err.toString());
-
-            runsDatastore.markAsFailed(run.runId, err.toString());
-
-            res.status(400).send('Bad request');
-            
-        })
-
-        .finally(function() {
-            queue.remove(run.runId);
-        });
-
-
-        // The user doesn't want to wait for the response, sending the run ID only
-        if (!run.params.waitForResponse) {
-            debug('Sending response without waiting.');
-            res.setHeader('Content-Type', 'application/json');
-            res.send(JSON.stringify({runId: run.runId}));
-        }
-
-    });
-
-
-    // Retrive one run by id
-    app.get('/api/runs/:id', function(req, res) {
-        var runId = req.params.id;
-
-        var run = runsDatastore.get(runId);
-
-        if (run) {
-            res.setHeader('Content-Type', 'application/json');
-            res.send(JSON.stringify(run, null, 2));
-        } else {
-            res.status(404).send('Not found');
-        }
-    });
-
-    // Counts all pending runs
-    app.get('/api/runs', function(req, res) {
-        res.setHeader('Content-Type', 'application/json');
-        res.send(JSON.stringify({
-            pendingRuns: queue.length(),
-            timeSinceLastTestStarted: queue.timeSinceLastTestStarted()
-        }, null, 2));
-    });
-
-    // Delete one run by id
-    /*app.delete('/api/runs/:id', function(req, res) {
-        deleteRun()
-    });*/
-
-    // Delete all
-    /*app.delete('/api/runs', function(req, res) {
-        purgeRuns()
-    });
-
-    // List all
-    app.get('/api/runs', function(req, res) {
-        listRuns()
-    });
-
-    // Exists
-    app.head('/api/runs/:id', function(req, res) {
-        existsX();
-        // Returns 200 if the result exists or 404 if not
-    });
-    */
-
-    // Retrive one result by id
-    app.get('/api/results/:id', function(req, res) {
-        getPartialResults(req.params.id, res, function(data) {
-            
-            // Some fields can be excluded from the response, this way:
-            // /api/results/:id?exclude=field1,field2
-            if (req.query.exclude && typeof req.query.exclude === 'string') {
-                var excludedFields = req.query.exclude.split(',');
-                excludedFields.forEach(function(fieldName) {
-                    if (data[fieldName]) {
-                        delete data[fieldName];
-                    }
-                });
-            }
-
-            return data;
-        });
-    });
-
-    // Retrieve one result and return only the generalScores part of the response
-    app.get('/api/results/:id/generalScores', function(req, res) {
-        getPartialResults(req.params.id, res, function(data) {
-            return data.scoreProfiles.generic;
-        });
-    });
-
-    app.get('/api/results/:id/generalScores/:scoreProfile', function(req, res) {
-        getPartialResults(req.params.id, res, function(data) {
-            return data.scoreProfiles[req.params.scoreProfile];
-        });
-    });
-
-    app.get('/api/results/:id/rules', function(req, res) {
-        getPartialResults(req.params.id, res, function(data) {
-            return data.rules;
-        });
-    });
-
-    app.get('/api/results/:id/javascriptExecutionTree', function(req, res) {
-        getPartialResults(req.params.id, res, function(data) {
-            return data.javascriptExecutionTree;
-        });
-    });
-
-    app.get('/api/results/:id/toolsResults/phantomas', function(req, res) {
-        getPartialResults(req.params.id, res, function(data) {
-            return data.toolsResults.phantomas;
-        });
-    });
-
-    function getPartialResults(runId, res, partialGetterFn) {
-        resultsDatastore.getResult(runId)
-            .then(function(data) {
-                var results = partialGetterFn(data);
-                
-                if (typeof results === 'undefined') {
-                    res.status(404).send('Not found');
-                    return;
-                }
-
-                res.setHeader('Content-Type', 'application/json');
-                res.send(JSON.stringify(results, null, 2));
-
-            }).fail(function() {
-                res.status(404).send('Not found');
-            });
-    }
-
-    // Retrive one result by id
-    app.get('/api/results/:id/screenshot.jpg', function(req, res) {
-        var runId = req.params.id;
-
-        resultsDatastore.getScreenshot(runId)
-            .then(function(screenshotBuffer) {
-                
-                res.setHeader('Content-Type', 'image/jpeg');
-                res.send(screenshotBuffer);
-
-            }).fail(function() {
-                res.status(404).send('Not found');
-            });
-    });
-
-    function isBlocked(url) {
-        if (!serverSettings.blockedUrls) {
-            return false;
-        }
-
-        return serverSettings.blockedUrls.some(function(blockedUrl) {
-            return (url.indexOf(blockedUrl) === 0);
-        });
-    }
-};
-
-module.exports = ApiController;

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

@@ -1,45 +0,0 @@
-var path        = require('path');
-var express     = require('express');
-
-var serverSettings  = (process.env.IS_TEST) ? require('../../../test/fixtures/settings.json') : require('../../../server_config/settings.json');
-var packageJson     = require('../../../package.json');
-
-var FrontController = function(app) {
-    'use strict';
-
-    var cacheDuration = 365 * 24 * 60 * 60 * 1000; // One year
-    var assetsPath = (app.get('env') === 'development') ? '../../../front/src' : '../../../front/build';
-
-    // Routes templating    
-    var routes = ['/', '/about', '/result/:runId', '/result/:runId/screenshot', '/result/:runId/rule/:policy', '/queue/:runId'];
-
-    routes.forEach(function(route) {
-        app.get(route, function(req, res) {
-            res.setHeader('Cache-Control', 'public, max-age=20');
-            res.render(path.join(__dirname, assetsPath, 'main.html'), {
-                version: 'v' + packageJson.version,
-                baseUrl: app.locals.baseUrl || '/',
-                googleAnalyticsId: serverSettings.googleAnalyticsId,
-                sponsoring: serverSettings.sponsoring || {}
-            });
-        });
-    });
-
-    // Views templating
-    app.get('/views/:viewName', function(req, res) {
-        res.setHeader('Cache-Control', 'public, max-age=' + cacheDuration);
-        res.render(path.join(__dirname, assetsPath, 'views/' + req.params.viewName), {
-            baseUrl: app.locals.baseUrl || '/',
-            sponsoring: serverSettings.sponsoring || {}
-        });
-    });
-    
-    // Static assets
-    app.use('/css', express.static(path.join(__dirname, assetsPath, 'css'), { maxAge: cacheDuration }));
-    app.use('/fonts', express.static(path.join(__dirname, assetsPath, 'fonts'), { maxAge: cacheDuration }));
-    app.use('/img', express.static(path.join(__dirname, assetsPath, 'img'), { maxAge: cacheDuration }));
-    app.use('/js', express.static(path.join(__dirname, assetsPath, 'js'), { maxAge: cacheDuration }));
-    app.use('/node_modules', express.static(path.join(__dirname, '../../../node_modules'), { maxAge: cacheDuration }));
-};
-
-module.exports = FrontController;

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

@@ -1,136 +0,0 @@
-var fs          = require('fs');
-var rimraf      = require('rimraf');
-var path        = require('path');
-var Q           = require('q');
-var debug       = require('debug')('ylt:resultsDatastore');
-
-
-function ResultsDatastore() {
-    'use strict';
-
-    var resultFileName = 'results.json';
-    var resultScreenshotName = 'screenshot.jpg';
-    var resultsFolderName = 'results';
-    var resultsDir = path.join(__dirname, '..', '..', '..', resultsFolderName);
-
-
-    this.saveResult = function(testResults) {
-        
-        var screenshotFilePath = path.join(resultsDir, testResults.runId, resultScreenshotName);
-        var screenshotAPIPath = '/';
-
-        return createResultFolder(testResults.runId)
-
-            .then(function() {
-                return saveScreenshotIfExists(testResults, screenshotFilePath);
-            })
-
-            .then(function() {
-
-                debug('Saving results to disk...');
-
-                var resultFilePath = path.join(resultsDir, testResults.runId, resultFileName);
-                debug('Destination file is %s', resultFilePath);
-                
-                return Q.nfcall(fs.writeFile, resultFilePath, JSON.stringify(testResults, null, 2));
-            });
-    };
-
-
-    this.getResult = function(runId) {
-
-        var resultFilePath = path.join(resultsDir, runId, resultFileName);
-
-        debug('Reading results (runID = %s) from disk...', runId);
-        
-        return Q.nfcall(fs.readFile, resultFilePath, {encoding: 'utf8'}).then(function(data) {
-            return JSON.parse(data);
-        });
-    };
-
-
-    this.deleteResult = function(runId) {
-        var folder = path.join(resultsDir, runId);
-
-        debug('Deleting results (runID = %s) from disk...', runId);
-
-        return Q.nfcall(rimraf, folder);
-    };
-
-
-    // The folder /results/folderName/
-    function createResultFolder(runId) {
-        var folder = path.join(resultsDir, runId);
-
-        debug('Creating the folder %s', runId);
-
-        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;
-    }
-
-    this.getResultFolder = function(runId) {
-        return path.join(resultsDir, runId);
-    };
-
-    // If there is a screenshot, save it as screenshot.jpg in the same folder as the results
-    function saveScreenshotIfExists(testResults, path) {
-        var deferred = Q.defer();
-
-        if (testResults.screenshotBuffer) {
-
-            fs.writeFile(path, testResults.screenshotBuffer, function(err) {
-                if (err) {
-                    debug('Could not save final screenshot');
-                    debug(err);
-                    // But it is OK, we don't need to fail the run
-                    deferred.resolve();
-                } else {
-                    debug('Final screenshot saved: ' + path);
-                    deferred.resolve();
-                }
-            });
-            delete testResults.screenshotBuffer;
-
-        } else {
-            debug('Screenshot not found');
-            deferred.resolve();
-        }
-
-        return deferred.promise;
-    }
-
-    this.getScreenshot = function(runId) {
-
-        var screenshotFilePath = path.join(resultsDir, runId, resultScreenshotName);
-
-        debug('Getting screenshot (runID = %s) from disk...', runId);
-        
-        return Q.nfcall(fs.readFile, screenshotFilePath);
-    };
-}
-
-module.exports = ResultsDatastore;

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

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

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

@@ -1,90 +0,0 @@
-var Q = require('q');
-var debug = require('debug')('ylt:runsQueue');
-
-
-function RunsQueue() {
-    'use strict';
-
-    var queue = [];
-    var lastTestTimestamp = 0;
-
-    this.push = function(runId) {
-        var deferred = Q.defer();
-        var startingPosition = queue.length;
-
-        debug('Adding run %s to the queue, position is %d', runId, startingPosition);
-
-        if (startingPosition === 0) {
-            
-            // The queue is empty, let's run immediatly
-            queue.push({
-                runId: runId
-            });
-            
-            lastTestTimestamp = Date.now();
-            deferred.resolve();
-
-        } else {
-            
-            queue.push({
-                runId: runId,
-                positionChangedCallback: function(position) {
-                    deferred.notify(position);
-                },
-                itIsTimeCallback: function() {
-                    lastTestTimestamp = Date.now();
-                    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;
-    };
-
-    // Returns the number of seconds since the last test was launched
-    this.timeSinceLastTestStarted = function() {
-        return Math.round((Date.now() - lastTestTimestamp) / 1000);
-    };
-}
-
-module.exports = RunsQueue;

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

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

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

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

+ 0 - 12
lib/server/middlewares/wwwRedirectMiddleware.js

@@ -1,12 +0,0 @@
-var wwwRedirectMiddleware = function(req, res, next) {
-    'use strict';
-
-    // Redirect www.yellowlab.tools to yellowlab.tools without "www" (for SEO purpose)
-    if(/^www\.yellowlab\.tools/.test(req.headers.host)) {
-        res.redirect(301, req.protocol + '://' + req.headers.host.replace(/^www\./, '') + req.url);
-    } else {
-        next();
-    }
-};
-
-module.exports = wwwRedirectMiddleware;

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

@@ -1,6 +1,5 @@
 var async                   = require('async');
 var async                   = require('async');
 var Q                       = require('q');
 var Q                       = require('q');
-var ps                      = require('ps-node');
 var path                    = require('path');
 var path                    = require('path');
 var debug                   = require('debug')('ylt:phantomaswrapper');
 var debug                   = require('debug')('ylt:phantomaswrapper');
 var phantomas               = require('phantomas');
 var phantomas               = require('phantomas');

+ 6 - 41
package.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "yellowlabtools",
   "name": "yellowlabtools",
-  "version": "2.0.0",
-  "description": "Online tool to audit a webpage for performance and front-end quality issues",
+  "version": "2.1.0",
+  "description": "A tool that audits a webpage for performance and front-end quality issues",
   "license": "GPL-2.0",
   "license": "GPL-2.0",
   "author": {
   "author": {
     "name": "Gaël Métais",
     "name": "Gaël Métais",
@@ -20,25 +20,12 @@
   },
   },
   "main": "./lib/index.js",
   "main": "./lib/index.js",
   "dependencies": {
   "dependencies": {
-    "angular": "1.7.7",
-    "angular-animate": "1.7.7",
-    "angular-chart.js": "1.1.1",
-    "angular-local-storage": "0.7.1",
-    "angular-resource": "1.7.7",
-    "angular-route": "1.7.7",
-    "angular-sanitize": "1.7.7",
     "async": "2.6.1",
     "async": "2.6.1",
-    "body-parser": "1.18.3",
-    "chart.js": "2.7.3",
     "clean-css": "4.2.1",
     "clean-css": "4.2.1",
     "color-diff": "1.1.0",
     "color-diff": "1.1.0",
-    "compression": "1.7.3",
-    "cors": "2.8.5",
     "css-mq-parser": "0.0.3",
     "css-mq-parser": "0.0.3",
     "debug": "4.1.1",
     "debug": "4.1.1",
     "easyxml": "2.0.1",
     "easyxml": "2.0.1",
-    "ejs": "2.6.1",
-    "express": "4.16.4",
     "fontkit": "1.7.8",
     "fontkit": "1.7.8",
     "html-minifier": "4.0.0",
     "html-minifier": "4.0.0",
     "image-size": "0.7.1",
     "image-size": "0.7.1",
@@ -58,47 +45,24 @@
     "is-webp": "1.0.1",
     "is-webp": "1.0.1",
     "is-woff": "1.0.3",
     "is-woff": "1.0.3",
     "is-woff2": "1.0.0",
     "is-woff2": "1.0.0",
-    "jimp": "0.6.0",
     "md5": "2.2.1",
     "md5": "2.2.1",
     "meow": "5.0.0",
     "meow": "5.0.0",
     "parse-color": "1.0.0",
     "parse-color": "1.0.0",
-    "phantomas": "github:macbre/phantomas#devel",
-    "ps-node": "0.1.6",
+    "phantomas": "2.2.0",
     "q": "1.5.1",
     "q": "1.5.1",
     "request": "2.88.0",
     "request": "2.88.0",
-    "rimraf": "2.6.3",
-    "temporary": "0.0.8",
     "ttf2woff2": "4.0.1",
     "ttf2woff2": "4.0.1",
     "uglify-js": "3.4.9",
     "uglify-js": "3.4.9",
     "woff-tools": "0.1.0"
     "woff-tools": "0.1.0"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "chai": "~4.2.0",
     "chai": "~4.2.0",
-    "grunt": "~1.0.3",
-    "grunt-contrib-clean": "~2.0.0",
-    "grunt-contrib-concat": "~1.0.1",
-    "grunt-contrib-copy": "~1.0.0",
-    "grunt-contrib-cssmin": "~3.0.0",
-    "grunt-contrib-htmlmin": "~3.0.0",
-    "grunt-contrib-jshint": "~2.0.0",
-    "grunt-contrib-less": "~2.0.0",
-    "grunt-contrib-uglify": "~4.0.0",
-    "grunt-contrib-watch": "~1.1.0",
-    "grunt-env": "~0.4.4",
-    "grunt-express": "~1.4.1",
-    "grunt-filerev": "~2.3.1",
-    "grunt-inline-angular-templates": "~0.1.5",
-    "grunt-mocha-test": "~0.13.3",
-    "grunt-parallel": "~0.5.1",
-    "grunt-usemin": "~3.1.1",
-    "matchdep": "~2.0.0",
     "mocha": "~5.2.0",
     "mocha": "~5.2.0",
     "sinon": "~7.2.3",
     "sinon": "~7.2.3",
     "sinon-chai": "~3.3.0"
     "sinon-chai": "~3.3.0"
   },
   },
   "scripts": {
   "scripts": {
-    "test": "grunt test",
-    "build": "grunt build"
+    "test": "todo"
   },
   },
   "keywords": [
   "keywords": [
     "performance",
     "performance",
@@ -106,6 +70,7 @@
     "webperf",
     "webperf",
     "pagespeed",
     "pagespeed",
     "budget",
     "budget",
-    "phantomas"
+    "phantomas",
+    "puppeteer"
   ]
   ]
 }
 }

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 8
server_config/error50x.html


+ 0 - 13
server_config/maintenance.js

@@ -1,13 +0,0 @@
-var express                 = require('express');
-var app                     = express();
-var server                  = require('http').createServer(app);
-
-var settings                = require('./settings.json');
-
-app.all('*', function(req, res) {
-    res.status(500).send('YellowLabTools is in maintenance. It should come back soon with a new version!');
-});
-
-server.listen(settings.serverPort, function() {
-    console.log('Maintenance mode started on port %d', server.address().port);
-});

+ 0 - 28
server_config/server_install.sh

@@ -1,28 +0,0 @@
-#!/usr/bin/env bash
-
-# APT-GET
-sudo apt-get update
-sudo apt-get install lsb-release libfontconfig1 libfreetype6 libjpeg-dev libnss3 libatk1.0-0 libatk-bridge2.0-0 gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release libgbm1 xdg-utils wget -y --force-yes > /dev/null 2>&1
-sudo apt-get install curl git software-properties-common build-essential make g++ -y --force-yes > /dev/null 2>&1
-
-# Installation of NodeJS
-curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
-sudo apt-get install -y nodejs > /dev/null 2>&1
-source ~/.profile
-
-# Installation of some packages globally
-npm install forever grunt-cli -g
-source ~/.profile
-
-# Installation of YellowLabTools
-sudo chown -R $USER /space
-cd /space/YellowLabTools
-npm install || exit 1
-
-# Front-end compilation
-grunt build
-
-# Start the server
-rm server_config/settings.json
-cp server_config/settings-prod.json server_config/settings.json
-NODE_ENV=production forever start -c "node --stack-size=262000" bin/server.js

+ 0 - 24
server_config/server_update.sh

@@ -1,24 +0,0 @@
-#!/usr/bin/env bash
-
-cd /space/YellowLabTools
-
-# Stop the server and start the maintenance mode
-forever stopall
-forever start server_config/maintenance.js
-
-# Keep the settings.json file
-git stash
-git pull
-git stash pop
-
-# In case something was added in package.json
-rm -rf node_modules
-npm install || exit 1
-
-# Front-end compilation
-rm -rf front/build
-grunt build
-
-# Stop the maintenance mode and restart the server
-forever stopall
-NODE_ENV=production forever start -c "node --stack-size=262000" bin/server.js

+ 0 - 23
server_config/settings-prod.json

@@ -1,23 +0,0 @@
-{
-    "serverPort": 80,
-    "googleAnalyticsId": "",
-    "screenshotWidth": {
-        "phone": 360,
-        "tablet": 420,
-        "desktop": 600,
-        "desktop-hd": 600
-    },
-    "baseUrl": "/",
-    "authorizedKeys": {
-        
-    },
-    "maxAnonymousRunsPerDay": 1000,
-    "maxAnonymousCallsPerDay": 100000,
-    "blockedUrls": [],
-
-    "sponsoring" : {
-        "home": "(this is a private instance)",
-        "dashboard": null,
-        "about": "(this is a private instance)"
-    }
-}

+ 0 - 23
server_config/settings.json

@@ -1,23 +0,0 @@
-{
-    "serverPort": 8383,
-    "googleAnalyticsId": "",
-    "screenshotWidth": {
-        "phone": 360,
-        "tablet": 420,
-        "desktop": 600,
-        "desktop-hd": 600
-    },
-    "baseUrl": "/",
-    "authorizedKeys": {
-        
-    },
-    "maxAnonymousRunsPerDay": 99999999,
-    "maxAnonymousCallsPerDay": 99999999,
-    "blockedUrls": [],
-
-    "sponsoring" : {
-        "home": "(this is a private instance)",
-        "dashboard": null,
-        "about": "(this is a private instance)"
-    }
-}

+ 0 - 681
test/api/apiTest.js

@@ -1,681 +0,0 @@
-var should      = require('chai').should();
-var request     = require('request');
-var Q           = require('q');
-
-var config = {
-    "authorizedKeys": {
-        "1234567890": "contact@gaelmetais.com"
-    }
-};
-
-var serverUrl = 'http://localhost:8387';
-var wwwUrl = 'http://localhost:8388';
-
-describe('api', function() {
-
-
-    var syncRunResultUrl;
-    var asyncRunId;
-    var screenshotUrl;
-
-
-    it('should refuse a query with an invalid key', function(done) {
-        this.timeout(5000);
-
-        request({
-            method: 'POST',
-            url: serverUrl + '/api/runs',
-            body: {
-                url: wwwUrl + '/simple-page.html',
-                waitForResponse: false
-            },
-            json: true,
-            headers: {
-                'Content-Type': 'application/json',
-                'X-Api-Key': 'invalid'
-            }
-        }, function(error, response, body) {
-            if (!error && response.statusCode === 401) {
-                done();
-            } else {
-                done(error || response.statusCode);
-            }
-        });
-    });
-
-    it('should fail without an URL when asynchronous', function(done) {
-        this.timeout(15000);
-
-        request({
-            method: 'POST',
-            url: serverUrl + '/api/runs',
-            body: {
-                url: '',
-                waitForResponse: true
-            },
-            json: true,
-            headers: {
-                'Content-Type': 'application/json',
-                'X-Api-Key': Object.keys(config.authorizedKeys)[0]
-            }
-        }, function(error, response, body) {
-            if (!error && response.statusCode === 400) {
-                done();
-            } else {
-                done(error || response.statusCode);
-            }
-        });
-    });
-
-    it('should fail without an URL when synchronous', function(done) {
-        this.timeout(15000);
-
-        request({
-            method: 'POST',
-            url: serverUrl + '/api/runs',
-            body: {
-                url: '',
-                waitForResponse: true
-            },
-            json: true,
-            headers: {
-                'Content-Type': 'application/json',
-                'X-Api-Key': Object.keys(config.authorizedKeys)[0]
-            }
-        }, function(error, response, body) {
-            if (!error && response.statusCode === 400) {
-                done();
-            } else {
-                done(error || response.statusCode);
-            }
-        });
-    });
-
-    it('should launch a synchronous run', function(done) {
-        this.timeout(15000);
-
-        request({
-            method: 'POST',
-            url: serverUrl + '/api/runs',
-            body: {
-                url: wwwUrl + '/simple-page.html',
-                waitForResponse: true,
-                screenshot: true,
-                device: 'tablet',
-                //waitForSelector: '*',
-                cookie: 'foo=bar;domain=google.com',
-                authUser: 'joe',
-                authPass: 'secret'
-            },
-            json: true,
-            headers: {
-                'Content-Type': 'application/json',
-                'X-Api-Key': Object.keys(config.authorizedKeys)[0]
-            }
-        }, function(error, response, body) {
-            if (!error && response.statusCode === 302) {
-
-                response.headers.should.have.a.property('location').that.is.a('string');
-                syncRunResultUrl = response.headers.location;
-
-                done();
-            } else {
-                done(error || response.statusCode);
-            }
-        });
-    });
-
-    it('should return the rules only', function(done) {
-        this.timeout(15000);
-
-        request({
-            method: 'POST',
-            url: serverUrl + '/api/runs',
-            body: {
-                url: wwwUrl + '/simple-page.html',
-                waitForResponse: true,
-                partialResult: 'rules'
-            },
-            json: true,
-            headers: {
-                'Content-Type': 'application/json',
-                'X-Api-Key': Object.keys(config.authorizedKeys)[0]
-            }
-        }, function(error, response, body) {
-            if (!error && response.statusCode === 302) {
-
-                response.headers.should.have.a.property('location').that.is.a('string');
-                response.headers.location.should.contain('/rules');
-
-                done();
-            } else {
-                done(error || response.statusCode);
-            }
-        });
-    });
-
-
-    it('should retrieve the results for the synchronous run', function(done) {
-        this.timeout(15000);
-
-        request({
-            method: 'GET',
-            url: serverUrl + syncRunResultUrl,
-            json: true,
-        }, function(error, response, body) {
-            if (!error && response.statusCode === 200) {
-
-                body.should.have.a.property('runId').that.is.a('string');
-                body.should.have.a.property('params').that.is.an('object');
-                body.should.have.a.property('scoreProfiles').that.is.an('object');
-                body.should.have.a.property('rules').that.is.an('object');
-                body.should.have.a.property('toolsResults').that.is.an('object');
-
-                // Check if settings are correctly sent and retrieved
-                body.params.options.should.have.a.property('device').that.equals('tablet');
-                //body.params.options.should.have.a.property('waitForSelector').that.equals('*');
-                body.params.options.should.have.a.property('cookie').that.equals('foo=bar;domain=google.com');
-                body.params.options.should.have.a.property('authUser').that.equals('joe');
-                body.params.options.should.have.a.property('authPass').that.equals('secret');
-
-                // Check if the screenshot temporary file was correctly removed
-                body.params.options.should.not.have.a.property('screenshot');
-                // Check if the screenshot buffer was correctly removed
-                body.should.not.have.a.property('screenshotBuffer');
-                // Check if the screenshot url is here
-                body.should.have.a.property('screenshotUrl');
-                body.screenshotUrl.should.equal('api/results/' + body.runId + '/screenshot.jpg');
-
-                screenshotUrl = body.screenshotUrl;
-
-                done();
-
-            } else {
-                done(error || response.statusCode);
-            }
-        });
-    });
-
-
-    it('should launch a run without waiting for the response', function(done) {
-        this.timeout(5000);
-
-        request({
-            method: 'POST',
-            url: serverUrl + '/api/runs',
-            body: {
-                url: wwwUrl + '/simple-page.html',
-                waitForResponse: false,
-                jsTimeline: true
-            },
-            json: true,
-            headers: {
-                'Content-Type': 'application/json',
-                'X-Api-Key': Object.keys(config.authorizedKeys)[0]
-            }
-        }, function(error, response, body) {
-            if (!error && response.statusCode === 200) {
-
-                asyncRunId = body.runId;
-                asyncRunId.should.be.a('string');
-                done();
-
-            } else {
-                done(error || response.statusCode);
-            }
-        });
-    });
-
-
-    it('should respond run status: running', function(done) {
-        this.timeout(5000);
-
-        request({
-            method: 'GET',
-            url: serverUrl + '/api/runs/' + asyncRunId,
-            json: true,
-            headers: {
-                'Content-Type': 'application/json',
-                'X-Api-Key': Object.keys(config.authorizedKeys)[0]
-            }
-        }, function(error, response, body) {
-            if (!error && response.statusCode === 200) {
-
-                body.runId.should.equal(asyncRunId);
-                body.status.should.deep.equal({
-                    statusCode: 'running'
-                });
-
-                done();
-
-            } else {
-                done(error || response.statusCode);
-            }
-        });
-    });
-
-    it('should accept up to 10 anonymous runs to the API', function(done) {
-        this.timeout(5000);
-
-        function launchRun() {
-            var deferred = Q.defer();
-
-            request({
-                method: 'POST',
-                url: serverUrl + '/api/runs',
-                body: {
-                    url: wwwUrl + '/simple-page.html',
-                    waitForResponse: false
-                },
-                json: true
-            }, function(error, response, body) {
-
-                lastRunId = body.runId;
-
-                if (error) {
-                    deferred.reject(error);
-                } else {
-                    deferred.resolve(response, body);
-                }
-            });
-
-            return deferred.promise;
-        }
-
-        launchRun()
-        .then(launchRun)
-        .then(launchRun)
-        .then(launchRun)
-        .then(launchRun)
-
-        .then(function(response, body) {
-            
-            // Here should still be ok
-            response.statusCode.should.equal(200);
-
-            launchRun()
-            .then(launchRun)
-            .then(launchRun)
-            .then(launchRun)
-            .then(launchRun)
-            .then(launchRun)
-
-            .then(function(response, body) {
-
-                // It should fail now
-                response.statusCode.should.equal(429);
-                done();
-
-            })
-            .fail(function(error) {
-                done(error);
-            });
-
-        }).fail(function(error) {
-            done(error);
-        });
-        
-    });
-
-
-    it('should respond 404 to unknown runId', function(done) {
-        this.timeout(5000);
-
-        request({
-            method: 'GET',
-            url: serverUrl + '/api/runs/unknown',
-            json: true
-        }, function(error, response, body) {
-            if (!error && response.statusCode === 404) {
-                done();
-            } else {
-                done(error || response.statusCode);
-            }
-        });
-    });
-
-
-    it('should respond 404 to unknown result', function(done) {
-        this.timeout(5000);
-
-        request({
-            method: 'GET',
-            url: serverUrl + '/api/results/unknown',
-            json: true
-        }, function(error, response, body) {
-            if (!error && response.statusCode === 404) {
-                done();
-            } else {
-                done(error || response.statusCode);
-            }
-        });
-    });
-
-    
-    it('should respond status complete to the first run', function(done) {
-        this.timeout(12000);
-
-        function checkStatus() {
-            request({
-                method: 'GET',
-                url: serverUrl + '/api/runs/' + asyncRunId,
-                json: true
-            }, function(error, response, body) {
-                if (!error && response.statusCode === 200) {
-
-                    body.runId.should.equal(asyncRunId);
-                    
-                    if (body.status.statusCode === 'running') {
-                        setTimeout(checkStatus, 250);
-                    } else if (body.status.statusCode === 'complete') {
-                        done();
-                    } else {
-                        done(body.status.statusCode);
-                    }
-
-                } else {
-                    done(error || response.statusCode);
-                }
-            });
-        }
-
-        checkStatus();
-    });
-
-
-    it('should find the result of the async run', function(done) {
-        this.timeout(5000);
-
-        request({
-            method: 'GET',
-            url: serverUrl + '/api/results/' + asyncRunId,
-            json: true,
-        }, function(error, response, body) {
-            if (!error && response.statusCode === 200) {
-
-                body.should.have.a.property('runId').that.equals(asyncRunId);
-                body.should.have.a.property('params').that.is.an('object');
-                body.params.url.should.equal(wwwUrl + '/simple-page.html');
-
-                body.should.have.a.property('scoreProfiles').that.is.an('object');
-                body.scoreProfiles.should.have.a.property('generic').that.is.an('object');
-                body.scoreProfiles.generic.should.have.a.property('globalScore').that.is.a('number');
-                body.scoreProfiles.generic.should.have.a.property('categories').that.is.an('object');
-
-                body.should.have.a.property('rules').that.is.an('object');
-
-                body.should.have.a.property('toolsResults').that.is.an('object');
-                body.toolsResults.should.have.a.property('phantomas').that.is.an('object');
-
-                done();
-
-            } else {
-                done(error || response.statusCode);
-            }
-        });
-    });
-
-
-    it('should return the generic score object', function(done) {
-        this.timeout(5000);
-
-        request({
-            method: 'GET',
-            url: serverUrl + '/api/results/' + asyncRunId + '/generalScores',
-            json: true,
-        }, function(error, response, body) {
-            if (!error && response.statusCode === 200) {
-                body.should.have.a.property('globalScore').that.is.a('number');
-                body.should.have.a.property('categories').that.is.an('object');
-                done();
-
-            } else {
-                done(error || response.statusCode);
-            }
-        });
-    });
-
-
-    it('should return the generic score object also', function(done) {
-        this.timeout(5000);
-
-        request({
-            method: 'GET',
-            url: serverUrl + '/api/results/' + asyncRunId + '/generalScores/generic',
-            json: true,
-        }, function(error, response, body) {
-            if (!error && response.statusCode === 200) {
-                body.should.have.a.property('globalScore').that.is.a('number');
-                body.should.have.a.property('categories').that.is.an('object');
-                done();
-
-            } else {
-                done(error || response.statusCode);
-            }
-        });
-    });
-
-
-    it('should not find an unknown score object', function(done) {
-        this.timeout(5000);
-
-        request({
-            method: 'GET',
-            url: serverUrl + '/api/results/' + asyncRunId + '/generalScores/unknown',
-            json: true,
-        }, function(error, response, body) {
-            if (!error && response.statusCode === 404) {
-                done();
-            } else {
-                done(error || response.statusCode);
-            }
-        });
-    });
-
-
-    it('should return the rules', function(done) {
-        this.timeout(5000);
-
-        request({
-            method: 'GET',
-            url: serverUrl + '/api/results/' + asyncRunId + '/rules',
-            json: true,
-        }, function(error, response, body) {
-            if (!error && response.statusCode === 200) {
-                
-                var firstRule = body[Object.keys(body)[0]];
-                firstRule.should.have.a.property('policy').that.is.an('object');
-                firstRule.should.have.a.property('value').that.is.a('number');
-                firstRule.should.have.a.property('bad').that.is.a('boolean');
-                firstRule.should.have.a.property('abnormal').that.is.a('boolean');
-                firstRule.should.have.a.property('score').that.is.a('number');
-                firstRule.should.have.a.property('abnormalityScore').that.is.a('number');
-                
-                done();
-
-            } else {
-                done(error || response.statusCode);
-            }
-        });
-    });
-
-
-    it('should return the phantomas results', function(done) {
-        this.timeout(5000);
-
-        request({
-            method: 'GET',
-            url: serverUrl + '/api/results/' + asyncRunId + '/toolsResults/phantomas',
-            json: true,
-        }, function(error, response, body) {
-            if (!error && response.statusCode === 200) {
-                
-                body.should.have.a.property('metrics').that.is.an('object');
-                body.should.have.a.property('offenders').that.is.an('object');
-                
-                done();
-
-            } else {
-                done(error || response.statusCode);
-            }
-        });
-    });
-
-
-    it('should return the entire object and exclude toolsResults', function(done) {
-        this.timeout(5000);
-
-        request({
-            method: 'GET',
-            url: serverUrl + '/api/results/' + asyncRunId + '?exclude=toolsResults',
-            json: true,
-        }, function(error, response, body) {
-            if (!error && response.statusCode === 200) {
-                
-                body.should.have.a.property('runId').that.equals(asyncRunId);
-                body.should.have.a.property('params').that.is.an('object');
-                body.should.have.a.property('scoreProfiles').that.is.an('object');
-                body.should.have.a.property('rules').that.is.an('object');
-                
-                body.should.not.have.a.property('toolsResults').that.is.an('object');
-
-                done();
-
-            } else {
-                done(error || response.statusCode);
-            }
-        });
-    });
-
-
-    it('should return the entire object and exclude params and toolsResults', function(done) {
-        this.timeout(5000);
-
-        request({
-            method: 'GET',
-            url: serverUrl + '/api/results/' + asyncRunId + '?exclude=toolsResults,params',
-            json: true,
-        }, function(error, response, body) {
-            if (!error && response.statusCode === 200) {
-                
-                body.should.have.a.property('runId').that.equals(asyncRunId);
-                body.should.have.a.property('scoreProfiles').that.is.an('object');
-                body.should.have.a.property('rules').that.is.an('object');
-                
-                body.should.not.have.a.property('params').that.is.an('object');
-                body.should.not.have.a.property('toolsResults').that.is.an('object');
-
-                done();
-
-            } else {
-                done(error || response.statusCode);
-            }
-        });
-    });
-
-    it('should return the entire object and don\'t exclude anything', function(done) {
-        this.timeout(5000);
-
-        request({
-            method: 'GET',
-            url: serverUrl + '/api/results/' + asyncRunId + '?exclude=',
-            json: true,
-        }, function(error, response, body) {
-            if (!error && response.statusCode === 200) {
-                
-                body.should.have.a.property('runId').that.equals(asyncRunId);
-                body.should.have.a.property('scoreProfiles').that.is.an('object');
-                body.should.have.a.property('rules').that.is.an('object');
-                body.should.have.a.property('params').that.is.an('object');
-                body.should.have.a.property('toolsResults').that.is.an('object');
-
-                done();
-
-            } else {
-                done(error || response.statusCode);
-            }
-        });
-    });
-
-    it('should return the entire object and don\'t exclude anything', function(done) {
-        this.timeout(5000);
-
-        request({
-            method: 'GET',
-            url: serverUrl + '/api/results/' + asyncRunId + '?exclude=null',
-            json: true,
-        }, function(error, response, body) {
-            if (!error && response.statusCode === 200) {
-                
-                body.should.have.a.property('runId').that.equals(asyncRunId);
-                body.should.have.a.property('scoreProfiles').that.is.an('object');
-                body.should.have.a.property('rules').that.is.an('object');
-                body.should.have.a.property('params').that.is.an('object');
-                body.should.have.a.property('toolsResults').that.is.an('object');
-
-                done();
-
-            } else {
-                done(error || response.statusCode);
-            }
-        });
-    });
-
-
-    it('should retrieve the screenshot', function(done) {
-        this.timeout(5000);
-
-        request({
-            method: 'GET',
-            url: serverUrl + '/' + screenshotUrl
-        }, function(error, response, body) {
-            if (!error && response.statusCode === 200) {
-                response.headers['content-type'].should.equal('image/jpeg');
-                done();
-            } else {
-                done(error || response.statusCode);
-            }
-        });
-    });
-
-
-    it('should fail on a unexistant screenshot', function(done) {
-        this.timeout(5000);
-
-        request({
-            method: 'GET',
-            url: serverUrl + '/api/results/000000/screenshot.jpg'
-        }, function(error, response, body) {
-            if (!error && response.statusCode === 404) {
-                done();
-            } else {
-                done(error || response.statusCode);
-            }
-        });
-    });
-
-    it('should refuse a query on a blocked Url', function(done) {
-        this.timeout(5000);
-
-        request({
-            method: 'POST',
-            url: serverUrl + '/api/runs',
-            body: {
-                url: 'http://www.test.com/something.html',
-                waitForResponse: false
-            },
-            json: true,
-            headers: {
-                'Content-Type': 'application/json',
-                'X-Api-Key': Object.keys(config.authorizedKeys)[0]
-            }
-        }, function(error, response, body) {
-            if (!error && response.statusCode === 403) {
-                done();
-            } else {
-                done(error || response.statusCode);
-            }
-        });
-    });
-
-});

+ 0 - 121
test/api/resultsDatastoreTest.js

@@ -1,121 +0,0 @@
-var should = require('chai').should();
-var resultsDatastore = require('../../lib/server/datastores/resultsDatastore');
-
-var fs = require('fs');
-var path = require('path');
-
-describe('resultsDatastore', function() {
-    
-    var datastore = new resultsDatastore();
-    
-    var testId1 = '123456789';
-    var testData1 = {
-        runId: testId1,
-        other: {
-            foo: 'foo',
-            bar: 1
-        }
-    };
-
-
-    it('should store a result', function(done) {
-        datastore.should.have.a.property('saveResult').that.is.a('function');
-
-        datastore.saveResult(testData1).then(function() {
-            done();
-        }).fail(function(err) {
-            done(err);
-        });
-    });
-
-    it('should store another result', function(done) {
-        var testData2 = {
-            runId: '987654321',
-            other: {
-                foo: 'foo',
-                bar: 2
-            }
-        };
-
-        datastore.saveResult(testData2).then(function() {
-            done();
-        }).fail(function(err) {
-            done(err);
-        });
-    });
-
-    it('should retrieve a result', function(done) {
-        datastore.getResult(testId1)
-            .then(function(results) {
-
-                // Compare results with testData
-                results.should.deep.equal(testData1);
-
-                done();
-            }).fail(function(err) {
-                done(err);
-            });
-    });
-
-    it('should delete a result', function(done) {
-        datastore.deleteResult(testId1)
-            .then(function() {
-                done();
-            }).fail(function(err) {
-                done(err);
-            });
-    });
-
-    it('should not find the result anymore', function(done) {
-        datastore.getResult(testId1)
-            .then(function(results) {
-                done('Error, the result is still in the datastore');
-            }).fail(function(err) {
-                done();
-            });
-    });
-
-
-    var testId3 = '555555';
-    var testData3 = {
-        runId: testId3,
-        other: {
-            foo: 'foo',
-            bar: 2
-        },
-        screenshotBuffer: fs.readFileSync(path.join(__dirname, '../fixtures/logo-large.png'))
-    };
-
-    it('should store a test with a screenshot', function(done) {
-
-        datastore.saveResult(testData3).then(function() {
-            done();
-        }).fail(function(err) {
-            done(err);
-        });
-    });
-
-    it('should have a normal result', function(done) {
-        datastore.getResult(testId3)
-            .then(function(results) {
-
-                results.should.not.have.a.property('screenshot');
-
-                done();
-            })
-            .fail(function(err) {
-                done(err);
-            });
-    });
-
-    it('should retrieve the saved image', function() {
-        datastore.getScreenshot(testId3)
-            .then(function(imageBuffer) {
-                imageBuffer.should.be.an.instanceof(Buffer);
-                done();
-            })
-            .fail(function(err) {
-                done(err);
-            });
-    });
-});

+ 0 - 82
test/api/runsDatastoreTest.js

@@ -1,82 +0,0 @@
-var should = require('chai').should();
-var runsDatastore = require('../../lib/server/datastores/runsDatastore');
-
-describe('runsDatastore', function() {
-    
-    var datastore = new runsDatastore();
-
-    var firstRunId = 333;
-    var secondRunId = 999;
-
-    it('should accept new runs', function() {
-        datastore.should.have.a.property('add').that.is.a('function');
-
-        datastore.add({
-            runId: firstRunId,
-            otherData: 123456789
-        }, 0);
-
-        datastore.add({
-            runId: secondRunId,
-            otherData: 'whatever'
-        }, 1);
-    });
-
-    it('should have stored the runs with a status "runnung"', function() {
-        datastore.should.have.a.property('get').that.is.a('function');
-
-        var firstRun = datastore.get(firstRunId);
-        firstRun.should.have.a.property('runId').that.equals(firstRunId);
-        firstRun.should.have.a.property('status').that.deep.equals({
-            statusCode: 'running'
-        });
-
-        var secondRun = datastore.get(secondRunId);
-        secondRun.should.have.a.property('runId').that.equals(secondRunId);
-        secondRun.should.have.a.property('status').that.deep.equals({
-            statusCode: 'awaiting',
-            position: 1
-        });
-
-    });
-
-    it('should have exactly 2 runs in the store', function() {
-        var runs = datastore.list();
-        runs.should.be.a('array');
-        runs.should.have.length(2);
-        runs[0].should.have.a.property('runId').that.equals(firstRunId);
-    });
-
-    it('shoud update statuses correctly', function() {
-        
-        datastore.markAsComplete(firstRunId);
-        var firstRun = datastore.get(firstRunId);
-        firstRun.should.have.a.property('status').that.deep.equals({
-            statusCode: 'complete'
-        });
-
-        datastore.updatePosition(secondRunId, 0);
-        var secondRun = datastore.get(secondRunId);
-        secondRun.should.have.a.property('status').that.deep.equals({
-            statusCode: 'running'
-        });
-
-        datastore.markAsFailed(secondRunId, 'Error message');
-        secondRun = datastore.get(secondRunId);
-        secondRun.should.have.a.property('status').that.deep.equals({
-            statusCode: 'failed',
-            error: 'Error message'
-        });
-
-    });
-
-    it('should delete a run', function() {
-        datastore.delete(firstRunId);
-
-        var runs = datastore.list();
-        runs.should.be.a('array');
-        runs.should.have.length(1);
-
-        runs[0].should.have.a.property('runId').that.equals(secondRunId);
-    });
-});

+ 0 - 68
test/api/runsQueueTest.js

@@ -1,68 +0,0 @@
-var should = require('chai').should();
-var runsQueue = require('../../lib/server/datastores/runsQueue');
-
-describe('runsQueue', function() {
-
-    var queue = new runsQueue();
-    var aaaRun = null;
-    var bbbRun = null;
-    var cccRun = null;
-
-    it('should accept a new runId', function(done) {
-        queue.should.have.a.property('push').that.is.a('function');
-
-        aaaRun = queue.push('aaa');
-        bbbRun = queue.push('bbb');
-
-        aaaRun.then(function() {
-            done();
-        });
-    });
-
-    it('should return the right positions', function() {
-        var aaaPosition = queue.getPosition('aaa');
-        aaaPosition.should.equal(0);
-        aaaRun.startingPosition.should.equal(0);
-
-        var bbbPosition = queue.getPosition('bbb');
-        bbbPosition.should.equal(1);
-        bbbRun.startingPosition.should.equal(1);
-
-        var cccPosition = queue.getPosition('ccc');
-        cccPosition.should.equal(-1);
-    });
-
-    it('should refresh runs\' positions', function(done) {
-        cccRun = queue.push('ccc');
-
-        cccRun.progress(function(position) {
-            position.should.equal(1);
-
-            var positionDoubleCheck = queue.getPosition('ccc');
-            positionDoubleCheck.should.equal(1);
-
-            done();
-        });
-
-        queue.remove('aaa');
-    });
-
-    it('should fulfill the promise when first in the line', function(done) {
-        cccRun.then(function() {
-            done();
-        });
-
-        queue.remove('bbb');
-    });
-
-    it('should not keep removed runs', function() {
-        var aaaPosition = queue.getPosition('aaa');
-        aaaPosition.should.equal(-1);
-
-        var bbbPosition = queue.getPosition('bbb');
-        bbbPosition.should.equal(-1);
-
-        var cccPosition = queue.getPosition('ccc');
-        cccPosition.should.equal(0);
-    });
-});

+ 0 - 78
test/api/screenshotHandlerTest.js

@@ -1,78 +0,0 @@
-var should = require('chai').should();
-var ScreenshotHandler = require('../../lib/screenshotHandler');
-
-var fs = require('fs');
-var path = require('path');
-var rimraf = require('rimraf');
-
-describe('screenshotHandler', function() {
-
-    var imagePath = path.join(__dirname, '../fixtures/logo-large.png');
-    var screenshot, jimpImage;
-
-
-    it('should open an image and return an jimp object', function(done) {
-        ScreenshotHandler.openImage(imagePath)
-            .then(function(image) {
-                jimpImage = image;
-
-                jimpImage.should.be.an('object');
-                jimpImage.bitmap.width.should.equal(620);
-                jimpImage.bitmap.height.should.equal(104);
-
-                done();
-            })
-            .fail(function(err) {
-                done(err);
-            });
-    });
-
-
-    it('should resize an jimp image', function(done) {
-        ScreenshotHandler.resizeImage(jimpImage, 310)
-            .then(function(image) {
-                jimpImage = image;
-
-                jimpImage.bitmap.width.should.equal(310);
-                jimpImage.bitmap.height.should.equal(52);
-
-                done();
-            })
-            .fail(function(err) {
-                done(err);
-            });
-    });
-
-
-    it('should transform a jimp image into a buffer', function(done) {
-        ScreenshotHandler.toBuffer(jimpImage)
-            .then(function(buffer) {
-                buffer.should.be.an.instanceof(Buffer);
-                done();
-            })
-            .fail(function(err) {
-                done(err);
-            });
-    });
-
-
-    it('should create the tmp folder if it doesn\'t exist', function(done) {
-        // Delete tmp folder if it exists
-        rimraf.sync("/some/directory");
-        
-        // The function we want to test
-        ScreenshotHandler.createTmpScreenshotFolder()
-            .then(function(buffer) {
-                fs.existsSync(path.join(__dirname, '../../tmp')).should.equal(true);
-                done();
-            })
-            .fail(function(err) {
-                done(err);
-            });
-    });
-
-    it('should return the tmp folder path', function() {
-        ScreenshotHandler.getTmpFileRelativePath().should.equal('tmp/temp-screenshot.png');
-    });
-
-});

+ 0 - 8
test/gatling/README.md

@@ -1,8 +0,0 @@
-# Stress tests with Gatling
-
-[Gatling](http://gatling.io/) is an open source stress test tool 
-
-Objective: 500 simultaneous users on the website
-
-The YLTWebInterfaceSimulation.scala file is a scenario simulating a user coming on the web page and launching a run.
-

+ 0 - 42
test/gatling/YLTWebInterfaceSimulation.scala

@@ -1,42 +0,0 @@
-package computerdatabase // 1
-
-import io.gatling.core.Predef._ // 2
-import io.gatling.http.Predef._
-import scala.concurrent.duration._
-
-class YLTWebInterfaceSimulation extends Simulation {
-
-  val httpConf = http
-    .baseURL("http://localhost:8383")
-    .acceptHeader("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
-    .doNotTrackHeader("1")
-    .acceptLanguageHeader("en-US,en;q=0.5")
-    .acceptEncodingHeader("gzip, deflate")
-    .userAgentHeader("Mozilla/5.0 (Windows NT 5.1; rv:31.0) Gecko/20100101 Firefox/31.0")
-
-  val scn = scenario("YLTWebInterfaceSimulation")
-    .exec(http("home page")
-      .get("/")
-    )
-    .exec(http("static asset")
-      .get("/front/fonts/icons.woff")  
-    )
-    .pause(100 milliseconds)
-    .exec(http("launch run")
-      .post("/api/runs")
-      .body(StringBody("""{ "url": "http://www.google.com", "waitForResponse":false }""")).asJSON
-    )
-    .repeat(10, "loop") {
-      exec(http("get status")
-        .get("/api/runs/dzlqsahu8d")
-      )
-      .pause(2000 milliseconds)
-    }
-    .exec(http("get result")
-      .get("/api/results/dzlqsahu8d")
-    )
-
-  setUp(
-    scn.inject(rampUsers(1000) over(60 seconds))
-  ).protocols(httpConf)
-}

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov