Compare commits

...
Author SHA1 Message Date
dependabot[bot]andGitHub c0bde3b3e6 Bump async from 2.6.1 to 2.6.4
Bumps [async](https://github.com/caolan/async) from 2.6.1 to 2.6.4.
- [Release notes](https://github.com/caolan/async/releases)
- [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md)
- [Commits](https://github.com/caolan/async/compare/v2.6.1...v2.6.4)

---
updated-dependencies:
- dependency-name: async
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-11 22:32:48 +00:00
Gaël MétaisandGitHub 73c5b3383e Merge pull request #391 for YLT v3.0
YLT v3.0 is coming soon!
2023-10-12 00:30:16 +02:00
Gaël MétaisandGitHub 5f09fb3bde Merge branch 'develop' into webp-avif 2023-10-06 16:43:32 +02:00
Gaël Métais 0610fd6fbb Update Phantomas and increase min NodeJS version 2023-10-06 16:18:23 +02:00
Gaël Métais 85c685a5ca Tag a phantomas version 2023-08-16 09:37:34 +02:00
Gaël Métais 2977506cb8 Adjustments to excessive image density 2023-08-16 07:57:01 +02:00
Gaël Métais 538d734a5b Switch imageExcessiveDensity to KB instead of simple count 2023-08-16 01:07:17 +02:00
Gaël Métais e680f429eb Re-introduce the Requests Number metric 2023-08-16 01:05:13 +02:00
Gaël Métais c308850eb3 WIP new image analizis module in Phantomas 2023-08-14 01:54:17 +02:00
Gaël Métais 8e941233a0 Revert the image reformater (moved to Phantomas) 2023-08-12 04:29:43 +02:00
Gaël MétaisandGitHub 22a8a206ab Merge pull request #388 from YellowLabTools/revert-387-webp-avif
Revert "Introduce a new "Old Image Formats" rule"
2023-08-12 04:08:02 +02:00
Gaël MétaisandGitHub 773249ca4f Revert "Introduce a new "Old Image Formats" rule" 2023-08-12 04:07:03 +02:00
Gaël MétaisandGitHub 599d02ea0e Merge pull request #387 from YellowLabTools/webp-avif
Introduce a new "Old Image Formats" rule
2023-07-30 22:47:10 +02:00
Gaël Métais 675316c9b7 Wording 2023-07-26 18:02:21 +02:00
Gaël Métais c22fd8b132 Add more information to the Old Image Formats offenders 2023-07-26 17:28:53 +02:00
Gaël Métais c4b008f910 New rule that converts images to WebP and AVIF and checks the gain 2023-07-26 16:04:20 +02:00
Gaël Métais 0cde5a3607 Add Avif to content type detection 2023-07-21 10:17:18 +02:00
Gaël Métais 6180b9f86c Replace Jimp by Sharp for image processing (faster + supports webp/avif) 2023-07-20 17:38:14 +02:00
Gaël MétaisandGitHub daae7d8523 Merge pull request #386 from YellowLabTools/aws
Starting to prepare version 3.0.0
2023-07-20 16:33:05 +02:00
Gaël Métais 540e5b5ac0 Version 3.0.0 2023-07-20 16:10:46 +02:00
Gaël MétaisandGitHub 4b671001e0 Merge branch 'develop' into aws 2023-07-20 15:39:06 +02:00
Gaël Métais 1ae4bed339 Update more dependencies 2023-07-19 22:01:29 +02:00
Gaël Métais 04e734bd5a Updating some dependancies 2023-07-19 00:14:36 +02:00
Gaël Métais a0e71ef444 No support for IE10 when minifying css files 2023-07-19 00:13:44 +02:00
Gaël Métais 05d8da36d8 Fix tests 2023-07-19 00:12:51 +02:00
Gaël Métais 5cae61b896 Making ylt server work on both normal and AWS machines 2023-07-16 10:47:58 +02:00
Gaël Métais a319e84831 v2.2.0 2022-02-24 14:27:05 +01:00
Gaël MétaisandGitHub d38781400e Merge pull request #359 from EXXETA/add-storage-handling
Storage handling feature from phantomas 2.4.0
2022-02-24 14:21:32 +01:00
Kevin Kammleiter cbcc6647f2 feat(storage-handling): passthrough session and local storage 2022-01-17 13:13:07 +01:00
Gaël Métais d2a217319d Bring back the screenshot handler code 2021-05-11 00:12:25 +01:00
Gaël Métais 280b564ab3 Fix bug when the node module is called without any option 2021-05-11 00:11:47 +01:00
Gaël Métais 0e14b5bd04 Remove Lambda specific files, they will be commited into a new repository YellowLabTools/YellowLabTools-lambda-agent 2021-05-10 22:34:38 +01:00
Gaël Métais 90ded3e1cb Merge split into aws branch 2021-05-10 21:53:08 +01:00
Gaël MétaisandGitHub e795b3369d Merge pull request #344 from YellowLabTools/develop
Split YLT into two repositories
2021-05-07 16:10:53 +01:00
Gaël Métais 2f26c7db96 Fix documentation after split 2021-05-07 16:03:39 +01:00
Gaël Métais ed588ba43c Export version number 2021-04-25 00:03:31 +01:00
Gaël Métais c9f63138a0 Split YLT into 2 projects. The server is now in the project YellowLabTools/YellowLabTools-server 2021-04-24 02:40:30 +01:00
Gaël Métais 97f779dc83 Remove uneeded module temporary 2021-04-24 01:18:39 +01:00
Gaël Métais 2753446a57 Remove unneeded package ps-node 2021-04-24 01:15:45 +01:00
Gaël Métais 163eef58c2 Business errors differenciated from technical errors in the Lambda runner 2021-04-11 13:14:35 +01:00
Gaël Métais 201bf75bf7 Fix unused unicode ranges metric 2021-04-04 23:43:55 +01:00
Gaël MétaisandGitHub dd4d0d4607 Merge pull request #341 from YellowLabTools/docker
Removing unused .dockerignore file
2021-03-22 02:14:59 +00:00
Ben Younes Ousama 88bbaea4c9 Removing unused .dockerignore file 2021-03-22 02:41:31 +01:00
Gaël Métais ac37ebb4e2 Fix crash 2021-03-22 00:10:31 +00:00
Gaël Métais 16c3abf3f2 Remove JSHint from build steps 2021-03-22 00:06:18 +00:00
Gaël Métais d1681c3d88 Temporarily remove queue messages 2021-03-21 23:53:04 +00:00
Gaël Métais 97099e9f65 Add runId quickfix 2021-03-21 23:15:32 +00:00
Gaël Métais 4e8cb19f55 Add runId in the results 2021-03-21 23:13:56 +00:00
Gaël MétaisandGitHub f7280c1a90 Merge pull request #340 from YellowLabTools/docker
Promote the Docker image
2021-03-21 20:28:44 +00:00
Gaël Métais bfd6c5d2bd Promote the Docker image as a simple way to install YLT 2021-03-21 20:23:16 +00:00
Gaël Métais 47eab96a67 Two version of the API: normal/lambda. Will be merged later 2021-03-20 18:24:11 +00:00
Gaël Métais ec7cba8f49 Disable browser cache in Docker 2021-03-20 18:21:28 +00:00
Gaël Métais e0d497796a Save screenshot when called with a saveFile option 2021-03-16 07:45:49 +00:00
Gaël Métais c5bc02d45d Move settings invocation to init phase 2021-03-16 07:45:10 +00:00
Gaël Métais e2b713282c Fix error Service should be called with new 2021-03-16 07:44:18 +00:00
Gaël Métais 05c883fa3e Temporarily use a fork of Phantomas for Characters Count 2021-03-15 23:22:22 +00:00
Gaël Métais e5d9680b23 Merge branch 'aws' of https://github.com/YellowLabTools/YellowLabTools into aws 2021-03-15 23:20:54 +00:00
Gaël MétaisandGitHub 4a40483348 Merge pull request #337 from ohoareau/master
feat: add Dockerfile, Makefile and lambda handler to create image for AWS Lambda
2021-03-15 23:20:35 +00:00
Gaël MétaisandGitHub 5f12d1ce0e Merge branch 'aws' into master 2021-03-15 23:19:26 +00:00
Gaël Métais 050b2303f0 Merge branch 'master' into aws 2021-03-15 23:17:37 +00:00
Gaël MétaisandGitHub a579f5736b Merge pull request #338 from YellowLabTools/develop
Merge develop into master
2021-03-15 23:08:02 +00:00
Olivier Hoareau 82294983b0 feat: add Dockerfile to create image for Lambda 2021-03-15 11:25:08 +01:00
Gaël Métais 8558ea29df Run YLT on AWS Lambda. TODO: should work both ways 2021-03-15 01:49:28 +00:00
Gaël Métais 9211074b32 It is now possible to save the results to an AWS S3 bucket 2021-03-14 01:49:23 +00:00
Gaël Métais 2da4371847 The screenshot is now saved in the system /tmp folder 2021-03-14 01:35:26 +00:00
Gaël Métais 791bf9fc8d Remove Yarn.lock 2021-03-13 23:58:56 +00:00
Gaël Métais e4b324d209 Clean cli.js 2021-03-13 23:57:06 +00:00
Gaël Métais 3b0bd172ba Clean results from redownload details 2021-03-13 16:08:31 +00:00
Gaël Métais 91e1724540 Get ready for Phantomas 2.2 2021-03-13 11:17:58 +00:00
Gaël Métais fcdf3c9240 Fix monitoring being blocked 2021-03-11 09:56:41 +00:00
Gaël Métais 92d92b8021 Remove API limits for /api/runs in GET mode 2021-03-10 07:11:13 +00:00
Gaël Métais dc5bd2e71e Add last test launch time in the new /api/runs route 2021-03-10 00:54:50 +00:00
Gaël MétaisandGitHub 942dd89ea3 Merge pull request #327 from YellowLabTools/develop
The v2.0 beta version is almost finished
2021-02-03 23:21:53 +00:00
89 changed files with 295 additions and 14647 deletions
-1
View File
@@ -1 +0,0 @@
node_modules/
+3 -4
View File
@@ -2,9 +2,8 @@ node_modules
package-lock.json
.tmp
tmp
.vagrant
results/*
coverage
front/build
package-lock.json
har.json
yarn.lock
har.json
.idea/
-18
View File
@@ -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"
-7
View File
@@ -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"]
-252
View File
@@ -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'
]);
};
+12 -12
View File
@@ -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 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).
If your project is not accessible from outside or if you want to test your localhost, you might want to run your own instance of Yellow Lab Tools.
The classical way is to clone the YLT server's GitHub repository and run it on Linux or MacOS. The documentation is [here](https://github.com/YellowLabTools/YellowLabTools/wiki/Install-your-private-server).
The new recommended solution is to run Yellow Lab Tools inside a Docker virtual machine. My friend Ousama Ben Younes maintains [this ready-to-use Docker image based on Alpine](https://github.com/ousamabenyounes/docker-yellowlabtools)).
## 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>
Vendored
-24
View File
@@ -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
Regular → Executable
+6 -2
View File
@@ -15,7 +15,6 @@ var cli = meow({
'Options:',
' --device Simulates a device. Choose between phone (default), tablet, desktop and desktop-hd.',
' --screenshot Will take a screenshot and use this value as the output path. It needs to end with ".png".',
//' --wait-for-selector Once the page is loaded, Phantomas will wait until the given CSS selector matches some elements.',
' --proxy Sets an HTTP proxy to pass through. Syntax is "host:port".',
' --cookie Adds a cookie on the main domain.',
' --auth-user Basic HTTP authentication username.',
@@ -24,6 +23,8 @@ var cli = meow({
' --allow-domain Only allow requests to given (comma-separated) domains.',
' --no-externals Block all domains except the main one.',
' --reporter The output format: "json" or "xml". Default is "json".',
' --local-storage Ability to set a local storage, key-value pairs (e.g. "bar=foo;domain=url")',
' --session-storage Ability to set a session storage, key-value pairs (e.g. "bar=foo;domain=url")',
''
].join('\n'),
pkg: require('../package.json')
@@ -75,6 +76,10 @@ options.blockDomain = cli.flags.blockDomain || null;
options.allowDomain = cli.flags.allowDomain || null;
options.noExternals = cli.flags.noExternals || null;
// Storage injection
options.localStorage = cli.flags.localStorage;
options.sessionStorage = cli.flags.sessionStorage;
// Output format
if (cli.flags.reporter && cli.flags.reporter !== 'json' && cli.flags.reporter !== 'xml') {
console.error('Incorrect parameters: reporter has to be "json" or "xml"');
@@ -98,7 +103,6 @@ if (cli.flags.reporter && cli.flags.reporter !== 'json' && cli.flags.reporter !=
// Remove some heavy parts of the results object
delete data.toolsResults;
delete data.javascriptExecutionTree;
var xmlOutput = serializer.render(data);
-49
View File
@@ -1,49 +0,0 @@
var express = require('express');
var app = express();
var server = require('http').createServer(app);
var bodyParser = require('body-parser');
var compress = require('compression');
var cors = require('cors');
var authMiddleware = require('../lib/server/middlewares/authMiddleware');
var apiLimitsMiddleware = require('../lib/server/middlewares/apiLimitsMiddleware');
var wwwRedirectMiddleware = require('../lib/server/middlewares/wwwRedirectMiddleware');
// Middlewares
app.use(compress());
app.use(bodyParser.json());
app.use(cors());
app.use(wwwRedirectMiddleware);
app.use(authMiddleware);
app.use(apiLimitsMiddleware);
app.use(bodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded
// EJS HTML engine
app.engine('.html', require('ejs').__express);
app.set('view engine', 'ejs');
// Initialize the controllers
var apiController = require('../lib/server/controllers/apiController')(app);
var frontController = require('../lib/server/controllers/frontController')(app);
// Let's start the server!
if (!process.env.GRUNTED) {
var settings = require('../server_config/settings.json');
app.locals.baseUrl = settings.baseUrl;
server.listen(settings.serverPort, function() {
console.log('Listening on port %d', server.address().port);
// For the tests
if (server.startTests) {
server.startTests();
}
});
}
module.exports = app;
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

-18
View File
@@ -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;
}
-212
View File
@@ -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;
}
-196
View File
@@ -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;
}
-280
View File
@@ -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;
}
-21
View File
@@ -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;
}
-277
View File
@@ -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;
}
-17
View File
@@ -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;
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 706 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 614 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

-85
View File
@@ -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);
}]);
-37
View File
@@ -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();
}]);
-23
View File
@@ -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();
}
}
}]);
-100
View File
@@ -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();
}]);
-78
View File
@@ -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();
}]);
@@ -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();
}]);
-33
View File
@@ -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';
};
}]
};
});
@@ -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);
};
});
})();
-7
View File
@@ -1,7 +0,0 @@
var resultsFactory = angular.module('resultsFactory', ['ngResource']);
resultsFactory.factory('Results', ['$resource', function($resource) {
return $resource('api/results/:runId', {
});
}]);
-7
View File
@@ -1,7 +0,0 @@
var runsFactory = angular.module('runsFactory', ['ngResource']);
runsFactory.factory('Runs', ['$resource', function($resource) {
return $resource('api/runs/:runId', {
});
}]);
-64
View File
@@ -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(',');
}
};
}]);
-31
View File
@@ -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');
}
}
};
}]);
-24
View File
@@ -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);
}
};
}]);
-20
View File
@@ -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;
}
-199
View File
@@ -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;
}
}
-223
View File
@@ -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;
}
-298
View File
@@ -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;
}
-24
View File
@@ -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;
}
-307
View File
@@ -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;
}
-16
View File
@@ -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;
}
}
-66
View File
@@ -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>
-13
View File
@@ -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>
-72
View File
@@ -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>
-117
View File
@@ -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>
<% } %>
-46
View File
@@ -1,46 +0,0 @@
<p ng-if="url">Tested url: &nbsp; <a href="{{url}}" target="_blank" class="testedUrl">{{url}}</a></p>
<div ng-if="!notFound && !connectionLost">
<div ng-if="status.statusCode == 'failed'">
<div class="status">Test failed</div>
<p class="statusSubMessage">{{status.error}}</p>
<a class="linkButton" href="https://github.com/YellowLabTools/YellowLabTools/issues" target="_blank">Report the issue on GitHub</a>
<a class="linkButton" href="<%= baseUrl %>">New test</a>
</div>
<div ng-if="status.statusCode == 'awaiting'">
<div class="status">
<ng-pluralize count="status.position" when="{'one': 'Waiting behind 1 other test', 'other': 'Waiting behind {} other tests'}">
</ng-pluralize>
</div>
<p class="statusSubMessage">(auto-refresh activated)</p>
</div>
<div ng-if="status.statusCode == 'running'">
<div class="status">Test is running...</div>
<div class="progress">
<div class="progressBarEmpty">
<div class="progressBarFilled" ng-style="{'width': (progress.estimatedProgress*100) + '%'}"></div>
</div>
</div>
<p class="statusSubMessage" ng-if="!progress">(Phantomas launched)</p>
<p class="statusSubMessage" ng-if="progress.milestone == 'domReady'">(DOM Ready fired)</p>
<p class="statusSubMessage" ng-if="progress.milestone == 'domComplete'">(page loaded, waiting for late requests)</p>
<p class="statusSubMessage" ng-if="progress.milestone == 'phantomas'">(now simulating compression, optimization and minification)</p>
<p class="statusSubMessage" ng-if="progress.milestone == 'redownload'">(calculating score and retrieving screenshot)</p>
</div>
<div ng-if="status.statusCode == 'complete'">
<div class="status">Test complete</div>
<p class="statusSubMessage">Opening results...</p>
</div>
</div>
<div ng-if="notFound == true">
<div class="status">Error 404 (test not found)</div>
<p class="statusSubMessage">The server probably just rebooted. We are very sorry about that, please try to launch the test again.</p>
<a class="linkButton" href="<%= baseUrl %>">New test</a>
</div>
<div ng-if="connectionLost == true">
<div class="status">Connection lost with server</div>
<p class="statusSubMessage">Check your wifi cable, or maybe YellowLab.tools is rebooting.</p>
</div>
<canvas id="faviconRotator" hidden width=32 height=32></canvas>
-7
View File
@@ -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>
-514
View File
@@ -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>
-13
View File
@@ -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>
+48 -11
View File
@@ -1,11 +1,13 @@
var Q = require('q');
var debug = require('debug')('ylt:index');
var Q = require('q');
var Runner = require('./runner');
var Runner = require('./runner');
var ScreenshotHandler = require('./screenshotHandler');
var packageJson = require('../package.json');
var yellowLabTools = function(url, options) {
'use strict';
var deferred = Q.defer();
if (!url) {
@@ -25,18 +27,53 @@ var yellowLabTools = function(url, options) {
var runner = new Runner(params)
.progress(deferred.notify)
.progress(deferred.notify)
.then(function(data) {
.then(function(data) {
// If a screenshot saveFunction was provided in the options
if (options && typeof options.saveScreenshotFn === 'function') {
debug('Now optimizing screenshot...');
// TODO: temporarily set all screenshot sizes to 600px, until we find a solution
ScreenshotHandler.findAndOptimizeScreenshot(data.params.options.screenshot, 600)
.then(function(screenshotBuffer) {
debug('Screenshot optimized, now saving...');
return options.saveScreenshotFn('screenshot.jpg', screenshotBuffer);
})
.then(function(response) {
debug('Screenshot saved');
debug(response);
// Remove uneeded temp screenshot path
delete data.params.options.screenshot;
})
.catch(function(err) {
// It's ok if we can't save the screenshot
debug('Screenshot could not be saved');
debug(err);
})
.finally(function() {
deferred.resolve(data);
});
} else {
deferred.resolve(data);
})
}
})
.fail(function(err) {
deferred.reject(err);
});
.catch(function(err) {
deferred.reject(err);
});
}
return deferred.promise;
};
module.exports = yellowLabTools;
module.exports = yellowLabTools;
module.exports.version = packageJson.version;
+90 -16
View File
@@ -54,7 +54,7 @@ var policies = {
"isBadThreshold": 2000,
"isAbnormalThreshold": 4000,
"hasOffenders": false,
"unit": 'ms'
"unit": "ms"
},
"DOMaccesses": {
"tool": "domAccessAgregator",
@@ -538,7 +538,7 @@ var policies = {
"message": "<p>It can be useful, but only as a last resort. It is a bad practice because it overrides the normal cascading logic. The more you use !important, the more you need it again to over-override. This conducts to a poor maintainability.</p>",
"isOkThreshold": 0,
"isBadThreshold": 200,
"isAbnormalThreshold": 500,
"isAbnormalThreshold": 1000,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
var parsedOffenders = offenders.map(function(offender) {
@@ -734,22 +734,96 @@ var policies = {
"isBadThreshold": 3145728,
"isAbnormalThreshold": 5242880,
"hasOffenders": true,
"unit": 'bytes'
"unit": "bytes"
},
"imageOptimization": {
"tool": "redownload",
"imagesNotOptimized": {
"tool": "phantomas",
"label": "Image optimization",
"message": "<p>This metric measures the number of bytes that could be saved by optimizing images.</p><p>Image optimization is generally one of the easiest way to reduce a page weight, and as a result, the page load time. Don't use Photoshop or other image editing tools, they're not very good for optimization. Use specialized tools such as <a href=\"https://kraken.io/\" target=\"_blank\">Kraken.io</a> or the excellent <a href=\"https://imageoptim.com/\" target=\"_blank\">ImageOptim</a> on Mac. For SVG images, you can use <a href=\"https://jakearchibald.github.io/svgomg/\" target=\"_blank\">SVGOMG</a>.</p><p>The tools in use in YellowLabTools are not set to their maximum optimization power (JPEG quality 85), so you might be able to compress even more!</p>",
"isOkThreshold": 20480,
"message": "<p>This metric measures the number of bytes that could be saved by optimizing images.</p><p>Image optimization is generally one of the easiest way to reduce a page weight, and as a result, the page load time. Don't use Photoshop or other image editing tools, they're not very good for optimization. Use specialized tools such as <a href=\"https://kraken.io/\" target=\"_blank\">Kraken.io</a> or the excellent <a href=\"https://imageoptim.com/\" target=\"_blank\">ImageOptim</a> on Mac. For SVG images, you can use <a href=\"https://jakearchibald.github.io/svgomg/\" target=\"_blank\">SVGOMG</a>.</p><p>The tools in use in YellowLabTools are not set to their maximum optimization power, so you might be able to compress even more!</p>",
"isOkThreshold": 2048,
"isBadThreshold": 204800,
"isAbnormalThreshold": 307200,
"hasOffenders": true,
"unit": 'bytes'
"unit": "bytes",
"valueTransformFn": function(offenders) {
let totalGain = 0;
offenders.forEach((offender) => {
offender.gain = offender.fileSize - offender.newFileSize;
totalGain += offender.gain;
});
return totalGain;
},
"offendersTransformFn": function(offenders) {
return offenders;
}
},
"imagesTooLarge": {
"tool": "redownload",
"imagesOldFormat": {
"tool": "phantomas",
"label": "Old image formats",
"message": "<p>This metric goes further than \"Image optimization\". Measures the number of bytes that could be saved by converting images to newer and more efficient formats. The best image format is generally AVIF and the second best is WebP.</p><p>Be careful, you need to provide fallback images for old browsers and search engine bots.</p>",
"isOkThreshold": 2048,
"isBadThreshold": 512000,
"isAbnormalThreshold": 819200,
"hasOffenders": true,
"unit": "bytes",
"valueTransformFn": function(offenders) {
let totalGain = 0;
offenders.forEach((offender) => {
offender.gain = offender.fileSize - offender.newFileSize;
totalGain += offender.gain;
});
return totalGain;
},
"offendersTransformFn": function(offenders) {
return offenders;
}
},
"imagesScaledDown": {
"tool": "phantomas",
"label": "Oversized images",
"message": "<p>This is the number of images with a width >1200px on mobile, >1800px on tablet, >2400 on desktop, >3200px on HD desktop. Try reducing their size.</p><p>Please ignore if the file is used as a sprite.</p>",
"message": "<p>This rule compares the number of pixels in a loaded images to the number of physical pixels it is displayed on. Then it estimates the number of KB that could be saved by serving it with the correct dimensions.</p><p>Of course, it is hard to serve perfect images for all screens. For this reason, this rule is quite permissive.</p>",
"isOkThreshold": 2048,
"isBadThreshold": 307200,
"isAbnormalThreshold": 512000,
"hasOffenders": true,
"unit": "bytes",
"valueTransformFn": function(offenders) {
let totalGain = 0;
offenders.forEach((offender) => {
offender.gain = offender.fileSize - offender.newFileSize;
totalGain += offender.gain;
});
return totalGain;
},
"offendersTransformFn": function(offenders) {
return offenders;
}
},
"imagesExcessiveDensity": {
"tool": "phantomas",
"label": "Excessive image density",
"message": "<p>This metric measures the number of bytes that could be saved by going further than just resizing images.<p>Devices with very high pixel density screen (such as 3x or 4x) are programmed to load high density images. This is the normal behavior, however the human eye barely sees the difference over 2x. This metric alerts you if an image density is > 2.2x.</p><p>There is currently no browser functionnality to prevent the issue (for this reason its impact on global score is low). But you can build your own clever solution!</p>",
"isOkThreshold": 102400,
"isBadThreshold": 307200,
"isAbnormalThreshold": 512000,
"hasOffenders": true,
"unit": "bytes",
"valueTransformFn": function(offenders) {
let totalGain = 0;
offenders.forEach((offender) => {
offender.gain = offender.fileSize - offender.newFileSize;
totalGain += offender.gain;
});
return totalGain;
},
"offendersTransformFn": function(offenders) {
return offenders;
}
},
"imagesWithIncorrectSizesParam": {
"tool": "phantomas",
"label": "Incorrect sizes parameter",
"message": "<p>When using an adaptative image with a <i>srcset</i> attribute and <i>w</i> values, it is important to correctly set the <i>sizes</i> attribute. Otherwise, the browser might pick the wrong image in the <i>srcset</i>.</p><p>The <a href=\”https://chrome.google.com/webstore/detail/responsive-image-linter/mnddginionlghpblkimpdalcecpnbjln\" target=\"_blank\">Responsive Image Linter</a> extension for Chrome can help you further.</p>",
"isOkThreshold": 0,
"isBadThreshold": 5,
"isAbnormalThreshold": 10,
@@ -763,7 +837,7 @@ var policies = {
"isBadThreshold": 204800,
"isAbnormalThreshold": 409600,
"hasOffenders": true,
"unit": 'bytes'
"unit": "bytes"
},
"fileMinification": {
"tool": "redownload",
@@ -773,12 +847,12 @@ var policies = {
"isBadThreshold": 61440,
"isAbnormalThreshold": 122880,
"hasOffenders": true,
"unit": 'bytes'
"unit": "bytes"
},
"totalRequests": {
"tool": "redownload",
"label": "Requests number",
"message": "<p>Each request slows down the page loading, especially on the protocol HTTP/1, but also a little on HTTP/2.</p><p>There are several technics to reduce their number:<ul><li>Concatenate JS files</li><li>Concatenate CSS files</li><li>Embed or inline small JS or CSS files in the HTML</li><li>Create sprites</li><li>Base64 encode small images in HTML or stylesheets</li><li>Use lazyloading for images</li></ul></p>",
"message": "<p>Each request slows down the page loading, especially on the protocol HTTP/1, but also a little on HTTP/2 or 3.</p><p>There are several technics to reduce their number:<ul><li>Concatenate JS files</li><li>Concatenate CSS files</li><li>Embed or inline small JS or CSS files in the HTML</li><li>Create sprites</li><li>Base64 encode small images in HTML or stylesheets</li><li>Use lazyloading for images</li></ul></p>",
"isOkThreshold": 80,
"isBadThreshold": 240,
"isAbnormalThreshold": 320,
@@ -869,7 +943,7 @@ var policies = {
"isOkThreshold": 0,
"isBadThreshold": 102400,
"isAbnormalThreshold": 204800,
"unit": 'bytes',
"unit": "bytes",
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return offenders;
@@ -895,7 +969,7 @@ var policies = {
"isBadThreshold": 51200,
"isAbnormalThreshold": 122880,
"hasOffenders": true,
"unit": 'bytes'
"unit": "bytes"
},
"oldHttpProtocol": {
"label": "HTTP protocols",
+26 -23
View File
@@ -1,23 +1,38 @@
{
"globalScore": {
"pageWeight": 2,
"images": 2,
"domComplexity": 1,
"javascriptComplexity": 2,
"badJavascript": 2,
"jQuery": 0.5,
"cssComplexity": 0.5,
"badCSS": 1,
"fonts": 1,
"serverConfig": 1
},
"categories": {
"pageWeight": {
"label": "Page weight",
"label": "Network",
"policies": {
"totalWeight": 5,
"imageOptimization": 2,
"imagesTooLarge": 1,
"compression": 2,
"fileMinification": 2
}
},
"requests": {
"label": "Requests",
"policies": {
"totalRequests": 2,
"domains": 3,
"notFound": 2,
"compression": 2,
"fileMinification": 2,
"identicalFiles": 2,
"emptyRequests": 3,
"notFound": 2
}
},
"images": {
"label": "Images",
"policies": {
"imagesNotOptimized": 2,
"imagesOldFormat": 1,
"imagesScaledDown": 2,
"imagesExcessiveDensity": 0.25,
"imagesWithIncorrectSizesParam": 1,
"lazyLoadableImagesBelowTheFold": 2,
"hiddenImages": 1
}
@@ -101,17 +116,5 @@
"cachingTooShort": 1
}
}
},
"globalScore": {
"pageWeight": 3,
"requests": 2,
"domComplexity": 2,
"javascriptComplexity": 2,
"badJavascript": 2,
"jQuery": 0.5,
"cssComplexity": 0.5,
"badCSS": 1,
"fonts": 1,
"serverConfig": 1
}
}
+8 -1
View File
@@ -57,9 +57,16 @@ var RulesChecker = function() {
data.toolsResults[policy.tool].offenders[metricName]) {
offenders = data.toolsResults[policy.tool].offenders[metricName];
}
// It is possible to declare a transformation function for the main metric value.
// The function should
if (policy.valueTransformFn) {
rule.value = policy.valueTransformFn(offenders);
}
var offendersObj = {};
// It is possible to declare a transformation function for the offenders.
// The function should take an array of strings as single parameter and return a string.
if (policy.offendersTransformFn) {
+1 -1
View File
@@ -79,7 +79,7 @@ var Runner = function(params) {
});
// Fix: don't display Unicode ranges if the module is not present in Phantomas
if (!data.toolsResults.phantomas.metrics.charactersCount) {
if (!data.toolsResults.phantomas.metrics.differentCharacters) {
delete data.toolsResults.redownload.metrics.unusedUnicodeRanges;
delete data.toolsResults.redownload.offenders.unusedUnicodeRanges;
}
+4 -37
View File
@@ -1,4 +1,4 @@
var debug = require('debug')('ylt:screenshotHandler');
var debug = require('debug')('ylt:screenshotHandlerAgent');
var Jimp = require('jimp');
var Q = require('q');
var fs = require('fs');
@@ -7,21 +7,15 @@ var path = require('path');
var screenshotHandler = function() {
var tmpFolderPath = 'tmp';
var tmpFolderFullPath = path.join(__dirname, '..', tmpFolderPath);
var tmpFileName = 'temp-screenshot.png';
var tmpFileFullPath = path.join(tmpFolderFullPath, tmpFileName);
this.findAndOptimizeScreenshot = function(width) {
this.findAndOptimizeScreenshot = function(tmpScreenshotPath, width) {
var that = this;
debug('Starting screenshot transformation');
return this.openImage(tmpFileFullPath)
return this.openImage(tmpScreenshotPath)
.then(function(image) {
that.deleteTmpFile(tmpFileFullPath);
that.deleteTmpFile(tmpScreenshotPath);
return that.resizeImage(image, width);
})
@@ -109,33 +103,6 @@ var screenshotHandler = function() {
return deferred.promise;
};
// Create a /tmp folder on the project's root directory
this.createTmpScreenshotFolder = function() {
var deferred = Q.defer();
// Create the folder if it doesn't exist
fs.exists(tmpFolderFullPath, function(exists) {
if (exists) {
deferred.resolve();
} else {
debug('Creating the tmp image folder', tmpFolderFullPath);
fs.mkdir(tmpFolderFullPath, function(err) {
if (err) {
deferred.reject(err);
} else {
deferred.resolve();
}
});
}
});
return deferred.promise;
};
this.getTmpFileRelativePath = function() {
return tmpFolderPath + '/' + tmpFileName;
};
};
module.exports = new screenshotHandler();
-349
View File
@@ -1,349 +0,0 @@
var debug = require('debug')('ylt:server');
var Q = require('q');
var ylt = require('../../index');
var ScreenshotHandler = require('../../screenshotHandler');
var RunsQueue = require('../datastores/runsQueue');
var RunsDatastore = require('../datastores/runsDatastore');
var ResultsDatastore = require('../datastores/resultsDatastore');
var serverSettings = (process.env.IS_TEST) ? require('../../../test/fixtures/settings.json') : require('../../../server_config/settings.json');
var ApiController = function(app) {
'use strict';
var queue = new RunsQueue();
var runsDatastore = new RunsDatastore();
var resultsDatastore = new ResultsDatastore();
// Create a new run
app.post('/api/runs', function(req, res) {
// Add http to the test URL
if (req.body.url && req.body.url.toLowerCase().indexOf('http://') !== 0 && req.body.url.toLowerCase().indexOf('https://') !== 0) {
req.body.url = 'http://' + req.body.url;
}
// Block requests to unwanted websites (=spam)
if (req.body.url && isBlocked(req.body.url)) {
console.error('Test blocked for URL: %s', req.body.url);
res.status(403).send('Forbidden');
return;
}
// Grab the test parameters and generate a random run ID
var run = {
runId: (Date.now()*1000 + Math.round(Math.random()*1000)).toString(36),
params: {
url: req.body.url,
waitForResponse: req.body.waitForResponse === true || req.body.waitForResponse === 'true' || req.body.waitForResponse === 1,
partialResult: req.body.partialResult || null,
screenshot: req.body.screenshot || false,
device: req.body.device || 'desktop',
proxy: req.body.proxy || null,
waitForSelector: req.body.waitForSelector || null,
cookie: req.body.cookie || null,
authUser: req.body.authUser || null,
authPass: req.body.authPass || null,
blockDomain: req.body.blockDomain || null,
allowDomain: req.body.allowDomain || null,
noExternals: req.body.noExternals || false
}
};
// Create the tmp folder if it doesn't exist
ScreenshotHandler.createTmpScreenshotFolder(run.runId);
// Add test to the testQueue
debug('Adding test %s to the queue', run.runId);
var queuePromise = queue.push(run.runId);
// Save the run to the datastore
runsDatastore.add(run, queuePromise.startingPosition);
// Listening for position updates
queuePromise.progress(function(position) {
runsDatastore.updatePosition(run.runId, position);
});
// Let's start the run
queuePromise.then(function() {
runsDatastore.updatePosition(run.runId, 0);
console.log('Launching test ' + run.runId + ' on ' + run.params.url);
var runOptions = {
screenshot: run.params.screenshot ? ScreenshotHandler.getTmpFileRelativePath() : false,
device: run.params.device,
proxy: run.params.proxy,
waitForSelector: run.params.waitForSelector,
cookie: run.params.cookie,
authUser: run.params.authUser,
authPass: run.params.authPass,
blockDomain: run.params.blockDomain,
allowDomain: run.params.allowDomain,
noExternals: run.params.noExternals
};
return ylt(run.params.url, runOptions)
// Update the progress bar on each progress
.progress(function(progress) {
runsDatastore.updateRunProgress(run.runId, progress);
});
})
// Phantomas completed
.then(function(data) {
debug('Success');
data.runId = run.runId;
// Some conditional steps exist if there is a screenshot
var screenshotPromise = Q.resolve();
if (run.params.screenshot) {
var screenshotSize = serverSettings.screenshotWidth ? serverSettings.screenshotWidth[run.params.device] : 400;
// Replace the empty promise created earlier with Q.resolve()
screenshotPromise = ScreenshotHandler.findAndOptimizeScreenshot(screenshotSize)
// Read screenshot
.then(function(screenshotBuffer) {
if (screenshotBuffer) {
debug('Image optimized');
data.screenshotBuffer = screenshotBuffer;
data.screenshotUrl = '/api/results/' + data.runId + '/screenshot.jpg';
}
})
// Don't worry if there's an error
.fail(function(err) {
debug('An error occured while creating the screenshot\'s thumbnail. Ignoring and continuing...');
debug(err);
});
}
// Let's continue
return screenshotPromise
// Save results
.then(function() {
// Remove uneeded temp screenshot path
delete data.params.options.screenshot;
// Here we can remove tools results if not needed
delete data.toolsResults.phantomas.offenders.requests;
return resultsDatastore.saveResult(data);
})
// Mark as the run as complete and send the response if the request is still waiting
.then(function() {
debug('Result saved in datastore');
runsDatastore.markAsComplete(run.runId);
if (run.params.waitForResponse) {
// If the user only wants a portion of the result (partialResult option)
switch(run.params.partialResult) {
case 'generalScores':
res.redirect(302, '/api/results/' + run.runId + '/generalScores');
break;
case 'rules':
res.redirect(302, '/api/results/' + run.runId + '/rules');
break;
case 'javascriptExecutionTree':
res.redirect(302, '/api/results/' + run.runId + '/javascriptExecutionTree');
break;
case 'phantomas':
res.redirect(302, '/api/results/' + run.runId + '/toolsResults/phantomas');
break;
default:
res.redirect(302, '/api/results/' + run.runId);
}
}
})
.fail(function(err) {
console.error('Test failed for URL: %s', run.params.url);
console.error(err.toString());
runsDatastore.markAsFailed(run.runId, err.toString());
res.status(500).send('An error occured');
});
})
.fail(function(err) {
console.error('Test failed for URL: %s', run.params.url);
console.error(err.toString());
runsDatastore.markAsFailed(run.runId, err.toString());
res.status(400).send('Bad request');
})
.finally(function() {
queue.remove(run.runId);
});
// The user doesn't want to wait for the response, sending the run ID only
if (!run.params.waitForResponse) {
debug('Sending response without waiting.');
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify({runId: run.runId}));
}
});
// Retrive one run by id
app.get('/api/runs/:id', function(req, res) {
var runId = req.params.id;
var run = runsDatastore.get(runId);
if (run) {
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify(run, null, 2));
} else {
res.status(404).send('Not found');
}
});
// Counts all pending runs
app.get('/api/runs', function(req, res) {
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify({pendingRuns: queue.length()}, 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;
-45
View File
@@ -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;
-136
View File
@@ -1,136 +0,0 @@
var fs = require('fs');
var rimraf = require('rimraf');
var path = require('path');
var Q = require('q');
var debug = require('debug')('ylt:resultsDatastore');
function ResultsDatastore() {
'use strict';
var resultFileName = 'results.json';
var resultScreenshotName = 'screenshot.jpg';
var resultsFolderName = 'results';
var resultsDir = path.join(__dirname, '..', '..', '..', resultsFolderName);
this.saveResult = function(testResults) {
var screenshotFilePath = path.join(resultsDir, testResults.runId, resultScreenshotName);
var screenshotAPIPath = '/';
return createResultFolder(testResults.runId)
.then(function() {
return saveScreenshotIfExists(testResults, screenshotFilePath);
})
.then(function() {
debug('Saving results to disk...');
var resultFilePath = path.join(resultsDir, testResults.runId, resultFileName);
debug('Destination file is %s', resultFilePath);
return Q.nfcall(fs.writeFile, resultFilePath, JSON.stringify(testResults, null, 2));
});
};
this.getResult = function(runId) {
var resultFilePath = path.join(resultsDir, runId, resultFileName);
debug('Reading results (runID = %s) from disk...', runId);
return Q.nfcall(fs.readFile, resultFilePath, {encoding: 'utf8'}).then(function(data) {
return JSON.parse(data);
});
};
this.deleteResult = function(runId) {
var folder = path.join(resultsDir, runId);
debug('Deleting results (runID = %s) from disk...', runId);
return Q.nfcall(rimraf, folder);
};
// The folder /results/folderName/
function createResultFolder(runId) {
var folder = path.join(resultsDir, runId);
debug('Creating the folder %s', runId);
return createGlobalFolder().then(function() {
return Q.nfcall(fs.mkdir, folder);
});
}
// The folder /results/
function createGlobalFolder() {
var deferred = Q.defer();
// Create the results folder if it doesn't exist
fs.exists(resultsDir, function(exists) {
if (exists) {
deferred.resolve();
} else {
debug('Creating the global results folder', resultsDir);
fs.mkdir(resultsDir, function(err) {
if (err) {
deferred.reject(err);
} else {
deferred.resolve();
}
});
}
});
return deferred.promise;
}
this.getResultFolder = function(runId) {
return path.join(resultsDir, runId);
};
// If there is a screenshot, save it as screenshot.jpg in the same folder as the results
function saveScreenshotIfExists(testResults, path) {
var deferred = Q.defer();
if (testResults.screenshotBuffer) {
fs.writeFile(path, testResults.screenshotBuffer, function(err) {
if (err) {
debug('Could not save final screenshot');
debug(err);
// But it is OK, we don't need to fail the run
deferred.resolve();
} else {
debug('Final screenshot saved: ' + path);
deferred.resolve();
}
});
delete testResults.screenshotBuffer;
} else {
debug('Screenshot not found');
deferred.resolve();
}
return deferred.promise;
}
this.getScreenshot = function(runId) {
var screenshotFilePath = path.join(resultsDir, runId, resultScreenshotName);
debug('Getting screenshot (runID = %s) from disk...', runId);
return Q.nfcall(fs.readFile, screenshotFilePath);
};
}
module.exports = ResultsDatastore;
-122
View File
@@ -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;
-83
View File
@@ -1,83 +0,0 @@
var Q = require('q');
var debug = require('debug')('ylt:runsQueue');
function RunsQueue() {
'use strict';
var queue = [];
this.push = function(runId) {
var deferred = Q.defer();
var startingPosition = queue.length;
debug('Adding run %s to the queue, position is %d', runId, startingPosition);
if (startingPosition === 0) {
// The queue is empty, let's run immediatly
queue.push({
runId: runId
});
deferred.resolve();
} else {
queue.push({
runId: runId,
positionChangedCallback: function(position) {
deferred.notify(position);
},
itIsTimeCallback: function() {
deferred.resolve();
}
});
}
var promise = deferred.promise;
promise.startingPosition = startingPosition;
return promise;
};
this.getPosition = function(runId) {
// Position 0 means it's a work in progress (a run is removed AFTER it is finished, not before)
var position = -1;
queue.some(function(run, index) {
if (run.runId === runId) {
position = index;
return true;
}
return false;
});
return position;
};
this.remove = function(runId) {
var position = this.getPosition(runId);
if (position >= 0) {
queue.splice(position, 1);
}
// Update other runs' positions
queue.forEach(function(run, index) {
if (index === 0 && run.itIsTimeCallback) {
run.itIsTimeCallback();
} else if (index > 0 && run.positionChangedCallback) {
run.positionChangedCallback(index);
}
});
};
this.length = function() {
return queue.length;
};
}
module.exports = RunsQueue;
@@ -1,87 +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) {
if (req.path === '/api/runs') {
if (!runsTable.accepts(ipAddress)) {
// Sorry :/
debug('Too many tests launched from IP address %s', ipAddress);
res.status(429).send('Too many requests');
return;
}
}
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;
-42
View File
@@ -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;
@@ -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;
+11 -3
View File
@@ -1,6 +1,4 @@
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');
@@ -44,10 +42,14 @@ var PhantomasWrapper = function() {
'block-domain': task.options.blockDomain,
'allow-domain': task.options.allowDomain,
'no-externals': task.options.noExternals,
'local-storage': task.options.localStorage,
'session-storage': task.options.sessionStorage,
// Mandatory
'analyze-css': true,
'ignoreSslErrors': true
'analyze-images': true,
'ignoreSslErrors': true, // until Phantomas 2.1
'ignore-ssl-errors': true // for Phantomas >= 2.2
};
@@ -79,6 +81,12 @@ var PhantomasWrapper = function() {
offenders: results.getAllOffenders()
};
// Special rules here
if (task.options.device !== 'phone') {
delete json.metrics.imagesExcessiveDensity;
delete json.offenders.imagesExcessiveDensity;
}
deferred.resolve(json);
}).
catch(res => {
+1 -1
View File
@@ -162,7 +162,7 @@ var FileMinifier = function() {
var deferred = Q.defer();
try {
var result = new CleanCSS({compatibility: 'ie8'}).minify(body);
var result = new CleanCSS().minify(body);
deferred.resolve(result.styles);
} catch(err) {
deferred.reject(err);
+4 -1
View File
@@ -204,7 +204,10 @@ var ImageOptimizer = function() {
} else if (type === 'png' && !lossy) {
engine = imageminOptipng({optimizationLevel: OPTIPNG_COMPRESSION_LEVEL});
} else if (type === 'svg' && !lossy) {
engine = imageminSvgo({ plugins: [ { removeUselessDefs: false } ] });
engine = imageminSvgo({plugins: [{
name: 'preset-default',
params: {overrides: {removeUselessDefs: false}}
}]});
} else {
deferred.reject('No optimization engine found for imagemin');
}
+23 -15
View File
@@ -14,13 +14,13 @@ var async = require('async');
var request = require('request');
var md5 = require('md5');
var imageOptimizer = require('./imageOptimizer');
//var imageOptimizer = require('./imageOptimizer');
var fileMinifier = require('./fileMinifier');
var gzipCompressor = require('./gzipCompressor');
var brotliCompressor = require('./brotliCompressor');
var contentTypeChecker = require('./contentTypeChecker');
var fontAnalyzer = require('./fontAnalyzer');
var imageDimensions = require('./imageDimensions');
//var imageDimensions = require('./imageDimensions');
var Redownload = function() {
@@ -63,10 +63,9 @@ var Redownload = function() {
}
}
// Prevent a bug with the font analyzer on empty pages
var differentCharacters = '';
if (data.toolsResults.phantomas.offenders.charactersCount && data.toolsResults.phantomas.offenders.charactersCount.length > 0) {
differentCharacters = data.toolsResults.phantomas.offenders.charactersCount[0];
if (data.toolsResults.phantomas.offenders.differentCharacters && data.toolsResults.phantomas.offenders.differentCharacters.length > 0) {
differentCharacters = data.toolsResults.phantomas.offenders.differentCharacters[0];
}
// Transform every request into a download function with a callback when done
@@ -77,9 +76,9 @@ var Redownload = function() {
.then(contentTypeChecker.checkContentType)
.then(imageOptimizer.optimizeImage)
//.then(imageOptimizer.optimizeImage)
.then(imageDimensions.getDimensions)
//.then(imageDimensions.getDimensions)
.then(fileMinifier.minifyFile)
@@ -152,12 +151,12 @@ var Redownload = function() {
});
// Image compression
offenders.imageOptimization = listImagesNotOptimized(results);
metrics.imageOptimization = offenders.imageOptimization.totalGain;
//offenders.imageOptimization = listImagesNotOptimized(results);
//metrics.imageOptimization = offenders.imageOptimization.totalGain;
// Image width
offenders.imagesTooLarge = listImagesTooLarge(results, data.params.options.device);
metrics.imagesTooLarge = offenders.imagesTooLarge.length;
//offenders.imagesTooLarge = listImagesTooLarge(results, data.params.options.device);
//metrics.imagesTooLarge = offenders.imagesTooLarge.length;
// File minification
offenders.fileMinification = listFilesNotMinified(results);
@@ -195,6 +194,8 @@ var Redownload = function() {
offenders: offenders
};
cleanResults(results);
deferred.resolve(data);
}
});
@@ -340,7 +341,7 @@ var Redownload = function() {
}
function listImagesNotOptimized(requests) {
/*function listImagesNotOptimized(requests) {
var results = {
totalGain: 0,
images: []
@@ -395,9 +396,9 @@ var Redownload = function() {
}
});
return results;
}
}*/
function listImagesTooLarge(requests, device) {
/*function listImagesTooLarge(requests, device) {
var results = [];
requests.forEach(function(req) {
@@ -422,7 +423,7 @@ var Redownload = function() {
});
return results;
}
}*/
function listFilesNotMinified(requests) {
@@ -1010,6 +1011,13 @@ var Redownload = function() {
}
}
// Clean all the pollution this module added to the results
function cleanResults(requests) {
requests.forEach(function(req) {
delete req.weightCheck;
});
}
return {
recheckAllFiles: recheckAllFiles,
listRequestWeight: listRequestWeight,
+23 -56
View File
@@ -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": "3.0.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",
@@ -16,37 +16,24 @@
"yellowlabtools": "./bin/cli.js"
},
"engines": {
"node": ">= 12.0"
"node": ">= 18.0"
},
"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",
"body-parser": "1.18.3",
"chart.js": "2.7.3",
"async": "2.6.4",
"clean-css": "4.2.1",
"color-diff": "1.1.0",
"compression": "1.7.3",
"cors": "2.8.5",
"color-diff": "1.4.0",
"css-mq-parser": "0.0.3",
"debug": "4.1.1",
"debug": "4.3.4",
"easyxml": "2.0.1",
"ejs": "2.6.1",
"express": "4.16.4",
"fontkit": "1.7.8",
"fontkit": "2.0.2",
"html-minifier": "4.0.0",
"image-size": "0.7.1",
"image-size": "1.0.2",
"imagemin": "7.0.1",
"imagemin-jpegoptim": "7.0.0",
"imagemin-jpegtran": "7.0.0",
"imagemin-optipng": "8.0.0",
"imagemin-svgo": "8.0.0",
"imagemin-svgo": "9.0.0",
"is-eot": "1.0.0",
"is-gif": "3.0.0",
"is-jpg": "2.0.0",
@@ -58,47 +45,26 @@
"is-webp": "1.0.1",
"is-woff": "1.0.3",
"is-woff2": "1.0.0",
"jimp": "0.6.0",
"md5": "2.2.1",
"jimp": "0.22.8",
"md5": "2.3.0",
"meow": "5.0.0",
"parse-color": "1.0.0",
"phantomas": "github:macbre/phantomas#devel",
"ps-node": "0.1.6",
"phantomas": "2.10.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",
"request": "2.88.2",
"sharp": "0.32.6",
"ttf2woff2": "5.0.0",
"uglify-js": "3.17.4",
"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"
"chai": "~4.3.7",
"mocha": "~10.2.0",
"sinon": "~15.2.0",
"sinon-chai": "3.7.0"
},
"scripts": {
"test": "grunt test",
"build": "grunt build"
"test": "mocha './test/core/**.js'"
},
"keywords": [
"performance",
@@ -106,6 +72,7 @@
"webperf",
"pagespeed",
"budget",
"phantomas"
"phantomas",
"puppeteer"
]
}
File diff suppressed because one or more lines are too long
-13
View File
@@ -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);
});
-28
View File
@@ -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
-24
View File
@@ -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
-23
View File
@@ -1,23 +0,0 @@
{
"serverPort": 80,
"googleAnalyticsId": "",
"screenshotWidth": {
"phone": 360,
"tablet": 420,
"desktop": 600,
"desktop-hd": 600
},
"baseUrl": "/",
"authorizedKeys": {
},
"maxAnonymousRunsPerDay": 1000,
"maxAnonymousCallsPerDay": 100000,
"blockedUrls": [],
"sponsoring" : {
"home": "(this is a private instance)",
"dashboard": null,
"about": "(this is a private instance)"
}
}
-23
View File
@@ -1,23 +0,0 @@
{
"serverPort": 8383,
"googleAnalyticsId": "",
"screenshotWidth": {
"phone": 360,
"tablet": 420,
"desktop": 600,
"desktop-hd": 600
},
"baseUrl": "/",
"authorizedKeys": {
},
"maxAnonymousRunsPerDay": 99999999,
"maxAnonymousCallsPerDay": 99999999,
"blockedUrls": [],
"sponsoring" : {
"home": "(this is a private instance)",
"dashboard": null,
"about": "(this is a private instance)"
}
}
-681
View File
@@ -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);
}
});
});
});
-121
View File
@@ -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);
});
});
});
-82
View File
@@ -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);
});
});
-68
View File
@@ -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);
});
});
-78
View File
@@ -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');
});
});
+2 -2
View File
@@ -49,7 +49,7 @@ describe('colorDiff', function() {
{url:'file.css', value: {message: '#5bc0de (2 times)'}},
{url:'file.css', value: {message: 'rgba(0,0,0,0.075) (100 times)'}},
{url:'file.css', value: {message: 'rgb(91,192,222) (1000 times)'}},
{url:'file.css', value: {message: 'rgba(0,0,2,1) (1 times)'}},
{url:'file.css', value: {message: 'rgba(0,0,1,1) (1 times)'}},
{url:'file.css', value: {message: 'rgba(99,99,99,1) (1 times)'}},
{url:'file.css', value: {message: 'rgba(100,100,100,1) (1 times)'}}
];
@@ -73,7 +73,7 @@ describe('colorDiff', function() {
newData.toolsResults.colorDiff.offenders.should.have.a.property('similarColors').that.deep.equals([
{
color1: '#000',
color2: 'rgba(0,0,2,1)',
color2: 'rgba(0,0,1,1)',
isDark: true
},
{
+10 -114
View File
@@ -76,114 +76,6 @@ describe('customPolicies', function() {
});
it('should transform DOMqueriesAvoidable offenders', function() {
results = rulesChecker.check({
"toolsResults": {
"phantomas": {
"metrics": {
"DOMqueriesAvoidable": 2
},
"offenders": {
"DOMqueriesDuplicated": [
"id \"#j2t-top-cart\" with getElementById (in context #document): 4 queries",
"class \".listingResult\" with getElementsByClassName (in context body > div#Global > div#Listing): 4 queries"
]
}
}
}
}, policies);
results.should.have.a.property('DOMqueriesAvoidable');
results.DOMqueriesAvoidable.should.have.a.property('offendersObj').that.deep.equals({
"count": 2,
"list": [
{
"query": "#j2t-top-cart",
"context": {
"type": "document"
},
"fn": "getElementById ",
"count": 4
},
{
"query": ".listingResult",
"context": {
"type": "domElement",
"element": "div#Listing",
"tree": {
"body": {
"div#Global": {
"div#Listing": 1
}
}
}
},
"fn": "getElementsByClassName ",
"count": 4
}
]
});
});
it('should transform jsErrors offenders', function() {
results = rulesChecker.check({
"toolsResults": {
"phantomas": {
"metrics": {
"jsErrors": 2
},
"offenders": {
"jsErrors": [
"TypeError: 'undefined' is not a function (evaluating 'this.successfullyCollected.bind(this)') - http://asset.easydmp.net/js/collect.js:1160 / callCollecte http://asset.easydmp.net/js/collect.js:1203 / callbackUpdateParams http://asset.easydmp.net/js/collect.js:1135 / http://asset.easydmp.net/js/collect.js:1191",
"TypeError: 'undefined' is not an object (evaluating 'd.readyState') - http://me.hunkal.com/p/:3"
]
}
}
}
}, policies);
results.should.have.a.property('jsErrors');
results.jsErrors.should.have.a.property('offendersObj').that.deep.equals({
"count": 2,
"list": [
{
"error": "TypeError: 'undefined' is not a function (evaluating 'this.successfullyCollected.bind(this)')",
"backtrace": [
{
"file": "http://asset.easydmp.net/js/collect.js",
"line": 1160
},
{
"file": "http://asset.easydmp.net/js/collect.js",
"line": 1203,
"functionName": "callCollecte"
},
{
"file": "http://asset.easydmp.net/js/collect.js",
"line": 1135,
"functionName": "callbackUpdateParams"
},
{
"file": "http://asset.easydmp.net/js/collect.js",
"line": 1191
}
]
},
{
"error": "TypeError: 'undefined' is not an object (evaluating 'd.readyState')",
"backtrace": [
{
"file": "http://me.hunkal.com/p/",
"line": 3
}
]
}
]
});
});
it('should grade correctly jQuery versions', function() {
var versions = {
@@ -208,7 +100,8 @@ describe('customPolicies', function() {
"phantomas": {
"metrics": {
"jQueryVersion": version
}
},
"offenders": {}
}
}
}, policies);
@@ -222,11 +115,12 @@ describe('customPolicies', function() {
"phantomas": {
"metrics": {
"jQueryVersion": "wooot"
}
},
"offenders": {}
}
}
}, policies);
results.should.deep.equals({});
results.should.not.have.a.property('jQueryVersion');
// If jQueryVersionsLoaded is 0
@@ -236,7 +130,8 @@ describe('customPolicies', function() {
"metrics": {
"jQueryVersion": "1.6.0",
"jQueryVersionsLoaded": 0
}
},
"offenders": {}
}
}
}, policies);
@@ -252,14 +147,15 @@ describe('customPolicies', function() {
"metrics": {
"jQueryVersion": "1.6.0",
"jQueryVersionsLoaded": 2
}
},
"offenders": {}
}
}
}, policies);
results.should.not.have.a.property('jQueryVersion');
results.should.have.a.property('jQueryVersionsLoaded');
results.jQueryVersionsLoaded.should.have.a.property('score').that.equals(0);
results.jQueryVersionsLoaded.should.have.a.property('abnormal').that.equals(true);
results.jQueryVersionsLoaded.should.have.a.property('abnormal').that.equals(false);
});
+17 -9
View File
@@ -12,13 +12,13 @@ chai.use(sinonChai);
describe('index.js', function() {
it('should return a promise', function() {
var promise = ylt();
/*var promise = ylt();
promise.should.have.property('then').that.is.a('function');
promise.should.have.property('fail').that.is.a('function');
promise.should.have.property('fail').that.is.a('function');*/
});
it('should fail an undefined url', function(done) {
it('should fail with an undefined url', function(done) {
ylt().fail(function(err) {
err.should.be.a('string').that.equals('URL missing');
done();
@@ -50,7 +50,7 @@ describe('index.js', function() {
data.toolsResults.phantomas.should.be.an('object');
data.toolsResults.phantomas.should.have.a.property('url').that.equals(url);
data.toolsResults.phantomas.should.have.a.property('metrics').that.is.an('object');
data.toolsResults.phantomas.metrics.should.have.a.property('requests').that.equals(1);
data.toolsResults.phantomas.metrics.should.have.a.property('requests').that.equals(2);
data.toolsResults.phantomas.should.have.a.property('offenders').that.is.an('object');
data.toolsResults.phantomas.offenders.should.have.a.property('DOMelementMaxDepth');
data.toolsResults.phantomas.offenders.DOMelementMaxDepth.should.have.length(2);
@@ -66,13 +66,14 @@ describe('index.js', function() {
"tool": "phantomas",
"label": "DOM max depth",
"message": "<p>A deep DOM makes the CSS matching with DOM elements difficult.</p><p>It also slows down JavaScript modifications to the DOM because changing the dimensions of an element makes the browser re-calculate the dimensions of it's parents. Same thing for JavaScript events, that bubble up to the document root.</p>",
"isOkThreshold": 12,
"isBadThreshold": 22,
"isAbnormalThreshold": 30,
"isOkThreshold": 15,
"isBadThreshold": 25,
"isAbnormalThreshold": 32,
"hasOffenders": true
},
"value": 1,
"bad": false,
"globalScoreIfFixed": 98,
"abnormal": false,
"score": 100,
"abnormalityScore": 0,
@@ -90,10 +91,10 @@ describe('index.js', function() {
/*jshint expr: true*/
console.log.should.not.have.been.called;
console.log.restore();
//console.log.restore();
done();
}).fail(function(err) {
console.log.restore();
//console.log.restore();
done(err);
});
});
@@ -105,6 +106,7 @@ describe('index.js', function() {
ylt(url)
.then(function(data) {
console.log(data.toolsResults.phantomas.offenders.jsErrors);
data.toolsResults.phantomas.metrics.should.have.a.property('jsErrors').that.equals(0);
done();
}).fail(function(err) {
@@ -119,6 +121,10 @@ describe('index.js', function() {
var url = 'http://localhost:8388/simple-page.html';
var screenshotPath = path.join(__dirname, '../../.tmp/indexTestScreenshot.png');
if (!fs.existsSync(path.join(__dirname, '../../.tmp'))){
fs.mkdirSync(path.join(__dirname, '../../.tmp'));
}
ylt(url, {screenshot: screenshotPath})
.then(function(data) {
@@ -130,6 +136,8 @@ describe('index.js', function() {
done();
}).fail(function(err) {
done(err);
}).finally(function() {
});
});
-48
View File
@@ -1,48 +0,0 @@
var should = require('chai').should();
var isHttp2 = require('../../lib/tools/isHttp2');
describe('isHttp2', function() {
it('should parse the protocol correctly', function() {
isHttp2.getProtocol({
toolsResults: {
phantomas: {
url: 'http://www.yahoo.com'
}
}
}).should.equal('http:');
isHttp2.getProtocol({
toolsResults: {
phantomas: {
url: 'https://www.yahoo.com'
}
}
}).should.equal('https:');
});
it('should parse the domain correctly', function() {
isHttp2.getDomain({
toolsResults: {
phantomas: {
url: 'http://www.yahoo.com'
}
}
}).should.equal('www.yahoo.com');
isHttp2.getDomain({
toolsResults: {
phantomas: {
url: 'https://www.yahoo.com'
}
}
}).should.equal('www.yahoo.com');
});
it('should have a function checkHttp2', function() {
isHttp2.should.have.a.property('checkHttp2').that.is.a('function');
});
});
+5 -11
View File
@@ -82,16 +82,9 @@ describe('redownload', function() {
data.toolsResults.redownload.offenders.totalWeight.byType.image.requests.length.should.equal(2);
data.toolsResults.redownload.offenders.totalWeight.byType.other.requests.length.should.equal(1);
data.toolsResults.redownload.offenders.should.have.a.property('imageOptimization');
data.toolsResults.redownload.offenders.imageOptimization.totalGain.should.be.above(0);
data.toolsResults.redownload.offenders.imageOptimization.images.length.should.equal(2);
data.toolsResults.redownload.offenders.should.have.a.property('imagesTooLarge');
data.toolsResults.redownload.offenders.imagesTooLarge.length.should.equal(0);
data.toolsResults.redownload.offenders.should.have.a.property('gzipCompression');
data.toolsResults.redownload.offenders.gzipCompression.totalGain.should.be.above(0);
data.toolsResults.redownload.offenders.gzipCompression.files.length.should.equal(5);
data.toolsResults.redownload.offenders.should.have.a.property('compression');
data.toolsResults.redownload.offenders.compression.totalGain.should.be.above(0);
data.toolsResults.redownload.offenders.compression.files.length.should.equal(5);
data.toolsResults.redownload.offenders.should.have.a.property('fileMinification');
data.toolsResults.redownload.offenders.fileMinification.totalGain.should.be.above(0);
@@ -224,7 +217,8 @@ describe('redownload', function() {
},
status: 302,
isHTML: true,
contentLength: 999
contentLength: 999,
notFound: true
};
redownload.redownloadEntry(entry)
-8
View File
@@ -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.
@@ -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)
}
+1
View File
@@ -9,6 +9,7 @@
document.getElementById(undefined);
document.getElementsByClassName(undefined);
document.getElementsByTagName(undefined);
document.querySelector(undefined);
} catch(err) {
console.log('Error found: ' + err);
-8050
View File
File diff suppressed because it is too large Load Diff