Преглед изворни кода

Merge split into aws branch

Gaël Métais пре 4 година
родитељ
комит
90ded3e1cb
76 измењених фајлова са 21 додато и 6843 уклоњено
  1. 0 5
      .dockerignore
  2. 0 3
      .gitignore
  3. 0 18
      .travis.yml
  4. 0 7
      Dockerfile
  5. 0 252
      Gruntfile.js
  6. 11 11
      README.md
  7. 0 24
      Vagrantfile
  8. 0 48
      bin/cli.js
  9. BIN
      doc/img/docker-logo.png
  10. BIN
      doc/img/grunt-logo.png
  11. 0 18
      front/src/css/about.css
  12. 0 212
      front/src/css/dashboard.css
  13. 0 196
      front/src/css/index.css
  14. 0 280
      front/src/css/main.css
  15. 0 21
      front/src/css/queue.css
  16. 0 277
      front/src/css/rule.css
  17. 0 17
      front/src/css/screenshot.css
  18. BIN
      front/src/img/favicon-fail.png
  19. BIN
      front/src/img/favicon-success.png
  20. BIN
      front/src/img/favicon.png
  21. BIN
      front/src/img/logo-large.png
  22. 0 85
      front/src/js/app.js
  23. 0 37
      front/src/js/controllers/dashboardCtrl.js
  24. 0 23
      front/src/js/controllers/indexCtrl.js
  25. 0 100
      front/src/js/controllers/queueCtrl.js
  26. 0 78
      front/src/js/controllers/ruleCtrl.js
  27. 0 30
      front/src/js/controllers/screenshotCtrl.js
  28. 0 33
      front/src/js/directives/gradeDirective.js
  29. 0 307
      front/src/js/directives/offendersDirectives.js
  30. 0 7
      front/src/js/models/resultsFactory.js
  31. 0 7
      front/src/js/models/runsFactory.js
  32. 0 64
      front/src/js/services/apiService.js
  33. 0 31
      front/src/js/services/menuService.js
  34. 0 24
      front/src/js/services/settingsService.js
  35. 0 20
      front/src/less/about.less
  36. 0 199
      front/src/less/dashboard.less
  37. 0 223
      front/src/less/index.less
  38. 0 298
      front/src/less/main.less
  39. 0 24
      front/src/less/queue.less
  40. 0 307
      front/src/less/rule.less
  41. 0 16
      front/src/less/screenshot.less
  42. 0 66
      front/src/main.html
  43. 0 13
      front/src/views/about.html
  44. 0 72
      front/src/views/dashboard.html
  45. 0 117
      front/src/views/index.html
  46. 0 47
      front/src/views/queue.html
  47. 0 7
      front/src/views/resultSubHeader.html
  48. 0 514
      front/src/views/rule.html
  49. 0 13
      front/src/views/screenshot.html
  50. 4 1
      lib/index.js
  51. 0 123
      lib/screenshotHandler.js
  52. 0 352
      lib/server/controllers/apiController.js
  53. 0 312
      lib/server/controllers/awsApiController.js
  54. 0 45
      lib/server/controllers/frontController.js
  55. 0 119
      lib/server/datastores/awsResultsDatastore.js
  56. 0 132
      lib/server/datastores/resultsDatastore.js
  57. 0 122
      lib/server/datastores/runsDatastore.js
  58. 0 91
      lib/server/datastores/runsQueue.js
  59. 0 95
      lib/server/middlewares/apiLimitsMiddleware.js
  60. 0 42
      lib/server/middlewares/authMiddleware.js
  61. 0 12
      lib/server/middlewares/wwwRedirectMiddleware.js
  62. 0 1
      lib/tools/phantomas/phantomasWrapper.js
  63. 6 42
      package.json
  64. 0 8
      server_config/error50x.html
  65. 0 13
      server_config/maintenance.js
  66. 0 28
      server_config/server_install.sh
  67. 0 24
      server_config/server_update.sh
  68. 0 25
      server_config/settings-prod.json
  69. 0 25
      server_config/settings.json
  70. 0 681
      test/api/apiTest.js
  71. 0 121
      test/api/resultsDatastoreTest.js
  72. 0 82
      test/api/runsDatastoreTest.js
  73. 0 68
      test/api/runsQueueTest.js
  74. 0 78
      test/api/screenshotHandlerTest.js
  75. 0 8
      test/gatling/README.md
  76. 0 42
      test/gatling/YLTWebInterfaceSimulation.scala

+ 0 - 5
.dockerignore

@@ -1,5 +0,0 @@
-node_modules/
-results/
-test/
-doc/
-front/

+ 0 - 3
.gitignore

@@ -2,10 +2,7 @@ node_modules
 package-lock.json
 .tmp
 tmp
-.vagrant
-results/*
 coverage
-front/build
 package-lock.json
 yarn.lock
 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 - 7
Dockerfile

@@ -1,7 +0,0 @@
-FROM    node:10
-WORKDIR /app
-ENV     VERSION=master
-EXPOSE  8383
-RUN     git clone --branch ${VERSION} https://github.com/LumberjackOtters/YellowLabTools ylt && cd ylt && yarn install && yarn build
-ENV     NODE_ENV=production
-CMD     ["node", "/app/ylt/bin/server.js"]

+ 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'
-    ]);
-
-};

+ 11 - 11
README.md

@@ -19,19 +19,20 @@ Analyzes a webpage and detects **performance** or **front-end code quality** iss
     </tr>
     <tr>
         <td width="70%">
-            The <b>CLI</b> (Command Line Interface) - <a href="https://github.com/YellowLabTools/YellowLabTools/wiki/Command-Line-Interface" target="_blank">Doc here</a>
+            The <b>Docker image</b> - <a href="https://github.com/ousamabenyounes/docker-yellowlabtools" target="_blank">ousamabenyounes/docker-yellowlabtools</a>
+            <br>
+            Your own private instance of Yellow Lab Tools, on your computer.
         </td>
         <td width="30%">
-            <img src="./doc/img/YLT-cli-animated.gif"></img>
+            <img src="./doc/img/docker-logo.png"></img>
         </td>
     </tr>
     <tr>
         <td width="70%">
-            The <b>Grunt task</b>: <a href="https://github.com/gmetais/grunt-yellowlabtools" traget="_blank">gmetais/grunt-yellowlabtools</a>
-            <br>For developers or Continuous Integration
+            The <b>CLI</b> (Command Line Interface) - <a href="https://github.com/YellowLabTools/YellowLabTools/wiki/Command-Line-Interface" target="_blank">Doc here</a>
         </td>
         <td width="30%">
-            <img src="./doc/img/grunt-logo.png"></img>
+            <img src="./doc/img/YLT-cli-animated.gif"></img>
         </td>
     </tr>
     <tr>
@@ -65,18 +66,17 @@ By the way, it's free because I am a geek, not businessmen. In return, you can a
 ![example dashboard screenshot](./doc/img/screenshot.png)
 
 
-## Test your localhost
-
-You can use [ngrok](https://ngrok.com/), a tool that creates a secure tunnel between your localhost and the online tool (or the public API). You can also use the CLI or the Grunt tasks as they run on your machine.
+## Install your own private instance
 
+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.
 
-## Install your own private instance
+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).
 
-If your project is not accessible from outside, or if you want to fork and improve the tool, you can build your own instance. The documentation is [here](https://github.com/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)).
 
 
 ## Author
-Gaël Métais. I'm a webperf freelance. Follow me on Twitter [@gaelmetais](https://twitter.com/gaelmetais), I tweet about Web Performances, Front-end and new versions of YellowLabTools!
+Gaël Métais. I'm a webperf freelance. Follow me on Twitter [@gaelmetais](https://twitter.com/gaelmetais), I tweet about Web Performances, Front-end and new versions of Yellow Lab Tools!
 
 <a href='https://ko-fi.com/gaelmetais' target='_blank'><img height='35' style='border:0px;height:46px;' src='https://az743702.vo.msecnd.net/cdn/kofi3.png?v=0' border='0' alt='Buy me a coffee' /><a>
 

+ 0 - 24
Vagrantfile

@@ -1,24 +0,0 @@
-Vagrant.configure("2") do |config|
-
-  config.vm.box = "ubuntu/trusty64"
-
-  config.vm.network :private_network, ip: "10.10.10.10"
-  config.ssh.forward_agent = true
-
-  # http://foo-o-rama.com/vagrant--stdin-is-not-a-tty--fix.html
-  config.vm.provision "fix-no-tty", type: "shell" do |s|
-    s.privileged = false
-    s.inline = "sudo sed -i '/tty/!s/mesg n/tty -s \\&\\& mesg n/' /root/.profile"
-  end
-
-  config.vm.provider :virtualbox do |vb|
-    vb.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
-    vb.customize ["modifyvm", :id, "--memory", 1024]
-    vb.customize ["modifyvm", :id, "--cpus", 2]
-  end
-
-  config.vm.synced_folder "./", "/space/YellowLabTools"
-
-  config.vm.provision :shell, :path => "server_config/server_install.sh"
-
-end

+ 0 - 48
bin/cli.js

@@ -1,48 +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');
-
-
-// Let's start the server!
-if (!process.env.GRUNTED) {
-    var settings = require('../server_config/settings.json');
-
-    // Initialize the controllers
-    var apiController = settings.awsHosting ? require('../lib/server/controllers/awsApiController')(app) : require('../lib/server/controllers/apiController')(app);
-    var frontController = require('../lib/server/controllers/frontController')(app);
-
-    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;

BIN
doc/img/docker-logo.png


BIN
doc/img/grunt-logo.png


+ 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 - 47
front/src/views/queue.html

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

@@ -4,6 +4,8 @@ var Q                   = require('q');
 var Runner              = require('./runner');
 var ScreenshotHandler   = require('./screenshotHandler');
 
+var packageJson = require('../package.json');
+
 
 var yellowLabTools = function(url, options) {
     var deferred = Q.defer();
@@ -76,4 +78,5 @@ var yellowLabTools = function(url, options) {
     return deferred.promise;
 };
 
-module.exports = yellowLabTools;
+module.exports = yellowLabTools;
+module.exports.version = packageJson.version;

+ 0 - 123
lib/screenshotHandler.js

@@ -1,123 +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 serverSettings = require('../server_config/settings.json');
-
-
-var screenshotHandler = function() {
-
-
-    this.findAndOptimizeScreenshot = function(width) {
-        var that = this;
-
-        debug('Starting screenshot transformation');
-
-        return this.openImage(this.getTmpFileRelativePath())
-
-            .then(function(image) {
-                that.deleteTmpFile(that.getTmpFileRelativePath());
-                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(this.getTmpFileRelativePath(), 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;
-    };
-
-
-    this.getTmpFileRelativePath = function() {
-        
-        // Chrome saves a temporary file on the disk, which is then removed.
-        // Its default folder is /tmp, but it can be changed in server_config/settings.json
-        var tmpFolderPath = serverSettings.screenshotTempPath  || '/tmp';
-        var tmpFileName = 'temp-chrome-screenshot.png';
-        var tmpFileFullPath = path.join(tmpFolderPath, tmpFileName);
-
-        return tmpFileFullPath;
-    };
-};
-
-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 https 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 = 'https://' + 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 - 312
lib/server/controllers/awsApiController.js

@@ -1,312 +0,0 @@
-var debug               = require('debug')('ylt:server');
-var Q                   = require('q');
-var AWS                 = require('aws-sdk');
-
-var ylt                 = require('../../index');
-var ScreenshotHandler   = require('../../screenshotHandler');
-var RunsQueue           = require('../datastores/runsQueue');
-var RunsDatastore       = require('../datastores/runsDatastore');
-
-var serverSettings      = (process.env.IS_TEST) ? require('../../../test/fixtures/settings.json') : require('../../../server_config/settings.json');
-
-var ResultsDatastore = (serverSettings.awsHosting) ? require('../datastores/awsResultsDatastore') : require('../datastores/resultsDatastore');
-
-var ApiController = function(app) {
-    'use strict';
-
-    var queue = new RunsQueue();
-    var runsDatastore = new RunsDatastore();
-    var resultsDatastore = new ResultsDatastore();
-
-    // Increase AWS Lambda timeout
-    AWS.config.update({httpOptions: {timeout: 300000}});
-
-    // 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 = 'https://' + 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
-            }
-        };
-
-        // 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);
-        runsDatastore.add(run, 0);
-
-        // 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
-            };
-
-            const {region, arn} = chooseLambdaRegionByGeoIP(req.headers);
-            const lambda = new AWS.Lambda({region: region});
-            
-            return lambda.invoke({
-                FunctionName: arn,
-                InvocationType: 'RequestResponse',
-                Payload: JSON.stringify({url: run.params.url, id: run.runId, options: runOptions})
-            }).promise();
-
-        })
-
-        .then(function(response) {
-            debug('We\'ve got a response from AWS Lambda');
-            debug('StatusCode = %d', response.StatusCode);
-            debug('Payload = %s', response.Payload);
-
-            if (response.StatusCode === 200 && response.Payload && response.Payload !== 'null') {
-                const payload = JSON.parse(response.Payload);
-                if (payload.status === 'failed') {
-                    debug('Failed with error %s', payload.errorMessage);
-                    runsDatastore.markAsFailed(run.runId, payload.errorMessage);
-                } else {
-                    debug('Success!');
-                    runsDatastore.markAsComplete(run.runId);
-                }
-            } else {
-                debug('Empty response from the lambda agent');
-                runsDatastore.markAsFailed(run.runId, "Empty response from the agent");
-            }
-        })
-
-        .catch(function(err) {
-            debug('Error from AWS Lambda:');
-            debug(err);
-
-            runsDatastore.markAsFailed(run.runId, err.toString());
-        });
-
-        // The user doesn't want to wait for the response, sending the run ID only
-        debug('Sending response without waiting.');
-        res.setHeader('Content-Type', 'application/json');
-        res.send(JSON.stringify({runId: run.runId}));
-
-    });
-
-
-    // Reads the Geoip_Continent_Code header and chooses the right region from the settings
-    function chooseLambdaRegionByGeoIP(headers) {
-
-        // The settings can be configured like this in server_config/settings.json:
-        //
-        // "awsHosting": {
-        //     "lambda": {
-        //         "regionByContinent": {
-        //             "AF": "eu-west-3",
-        //             "AS": "ap-southeast-1",
-        //             "EU": "eu-west-3",
-        //             "NA": "us-east-1",
-        //             "OC": "ap-southeast-1",
-        //             "SA": "us-east-1",
-        //             "default": "eu-west-3"
-        //         },
-        //         "arnByRegion": {
-        //             "us-east-1": "arn:aws:lambda:us-east-1:xxx:function:xxx",
-        //             "eu-west-3": "arn:aws:lambda:eu-west-3:xxx:function:xxx",
-        //             "ap-southeast-1": "arn:aws:lambda:ap-southeast-1:xxx:function:xxx"
-        //         }
-        //     }
-        // },
-
-        const header = headers.geoip_continent_code;
-        debug('Value of the Geoip_Continent_Code header: %s', header);
-
-        const continent = header || 'default';
-        const region = serverSettings.awsHosting.lambda.regionByContinent[continent];
-        const arn = serverSettings.awsHosting.lambda.arnByRegion[region];
-        debug('The chosen AWS Lambda is: %s', arn);
-
-        return {region, arn};
-    }
-
-
-    // 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;
-                }
-
-                // Quickfix (TODO remove)
-                results.runId = runId;
-                results.screenshotUrl = '/api/results/' + runId + '/screenshot.jpg';
-
-                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 - 119
lib/server/datastores/awsResultsDatastore.js

@@ -1,119 +0,0 @@
-const Q           = require('q');
-const debug       = require('debug')('ylt:resultsDatastore');
-const path        = require('path');
-const AWS         = require('aws-sdk');
-
-
-function ResultsDatastore() {
-    'use strict';
-
-    const serverSettings = require('../../../server_config/settings.json');
-    
-    const s3 = new AWS.S3();
-
-    const resultFileName = 'results.json';
-    const resultScreenshotName = 'screenshot.jpg';
-    const resultsFolderName = 'results';
-
-
-    this.saveResult = function(testResults) {
-        const resultFilePath = path.join(resultsFolderName, testResults.runId, resultFileName);
-        const screenshotFilePath = path.join(resultsFolderName, testResults.runId, resultScreenshotName);
-
-        debug('Starting to save screenshot then results.json file on s3...');
-
-        return saveScreenshotIfExists(testResults, screenshotFilePath)
-
-        .then(function() {
-            debug('Saving results file to s3, destination is %s', resultFilePath);
-            return s3PutObject(resultFilePath, JSON.stringify(testResults, null, 2));
-        });
-    };
-
-
-    this.getResult = function(runId) {
-        const resultFilePath = path.join(resultsFolderName, runId, resultFileName);
-        debug('Reading results (runID = %s) from AWS s3...', runId);
-        return s3GetObject(resultFilePath).then(function(bodyBuffer) {
-            return JSON.parse(bodyBuffer.toString('utf-8'));
-        });
-    };
-
-
-    // If there is a screenshot, save it as screenshot.jpg in the same folder as the results
-    function saveScreenshotIfExists(testResults, imagePath) {
-        var deferred = Q.defer();
-
-        if (testResults.screenshotBuffer) {
-            s3PutObject(imagePath, testResults.screenshotBuffer)
-            
-            .fail(function() {
-                debug('Image %s could not be saved on s3. Ignoring.', imagePath);
-            })
-            
-            .finally(function() {
-                delete testResults.screenshotBuffer;
-                deferred.resolve();
-            });
-
-        } else {
-            debug('Screenshot not found');
-            deferred.resolve();
-        }
-
-        return deferred.promise;
-    }
-
-
-    this.getScreenshot = function(runId) {
-        const screenshotFilePath = path.join(resultsFolderName, runId, resultScreenshotName);
-        debug('Retrieving screenshot (runID = %s) from s3...', runId);
-        return s3GetObject(screenshotFilePath);
-    };
-
-
-    function s3PutObject(path, body, ignoreError) {
-        var deferred = Q.defer();
-
-        s3.putObject({
-            Bucket: serverSettings.awsHosting.s3.bucket,
-            Key: path,
-            Body: body
-        }, function(err, data) {
-            if (err) {
-                debug('Could not save file %s on s3', path);
-                debug(err);
-                deferred.reject('File saving failed on s3');
-            } else {
-                debug('File %s saved on s3', path);
-                deferred.resolve();
-            }
-        });
-
-        return deferred.promise;
-    }
-
-
-    function s3GetObject(path) {
-        var deferred = Q.defer();
-
-        s3.getObject({
-            Bucket: serverSettings.awsHosting.s3.bucket,
-            Key: path
-        }, function(err, data) {
-            if (err) {
-                debug('Failed retrieving object %s from s3', path);
-                debug(err);
-                deferred.reject(err);
-            } else {
-                debug('Response for %s received from s3...', path);
-                deferred.resolve(data.Body);
-            }
-        });
-
-        return deferred.promise;
-    }
-
-}
-
-module.exports = ResultsDatastore;

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

@@ -1,132 +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;
-    }
-
-    // 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 - 91
lib/server/datastores/runsQueue.js

@@ -1,91 +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;
-        var startingPosition = 0;
-
-        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 Q                       = require('q');
-var ps                      = require('ps-node');
 var path                    = require('path');
 var debug                   = require('debug')('ylt:phantomaswrapper');
 var phantomas               = require('phantomas');

+ 6 - 42
package.json

@@ -1,7 +1,7 @@
 {
   "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",
   "author": {
     "name": "Gaël Métais",
@@ -20,26 +20,12 @@
   },
   "main": "./lib/index.js",
   "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",
-    "aws-sdk": "2.862.0",
-    "body-parser": "1.18.3",
-    "chart.js": "2.7.3",
     "clean-css": "4.2.1",
     "color-diff": "1.1.0",
-    "compression": "1.7.3",
-    "cors": "2.8.5",
     "css-mq-parser": "0.0.3",
     "debug": "4.1.1",
     "easyxml": "2.0.1",
-    "ejs": "2.6.1",
-    "express": "4.16.4",
     "fontkit": "1.7.8",
     "html-minifier": "4.0.0",
     "image-size": "0.7.1",
@@ -59,47 +45,24 @@
     "is-webp": "1.0.1",
     "is-woff": "1.0.3",
     "is-woff2": "1.0.0",
-    "jimp": "0.6.0",
     "md5": "2.2.1",
     "meow": "5.0.0",
     "parse-color": "1.0.0",
-    "phantomas": "github:gmetais/phantomas#charactersCount",
-    "ps-node": "0.1.6",
+    "phantomas": "2.2.0",
     "q": "1.5.1",
     "request": "2.88.0",
-    "rimraf": "2.6.3",
-    "temporary": "0.0.8",
     "ttf2woff2": "4.0.1",
     "uglify-js": "3.4.9",
     "woff-tools": "0.1.0"
   },
   "devDependencies": {
     "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",
     "sinon": "~7.2.3",
     "sinon-chai": "~3.3.0"
   },
   "scripts": {
-    "test": "grunt test",
-    "build": "grunt build"
+    "test": "todo"
   },
   "keywords": [
     "performance",
@@ -107,6 +70,7 @@
     "webperf",
     "pagespeed",
     "budget",
-    "phantomas"
+    "phantomas",
+    "puppeteer"
   ]
 }

Разлика између датотеке није приказан због своје велике величине
+ 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 - 25
server_config/settings-prod.json

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

+ 0 - 25
server_config/settings.json

@@ -1,25 +0,0 @@
-{
-    "serverPort": 8383,
-    "baseUrl": "/",
-    
-    "googleAnalyticsId": "",
-    
-    "screenshotWidth": {
-        "phone": 360,
-        "tablet": 420,
-        "desktop": 600,
-        "desktop-hd": 600
-    },
-    "screenshotTempPath": "/tmp/",
-    
-    "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)
-}

Неке датотеке нису приказане због велике количине промена