Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c17a709bf | ||
|
|
dd912ecc8f | ||
|
|
c9b12c4892 | ||
|
|
414aea2637 | ||
|
|
b7bc4dbadc | ||
|
|
8a8b871908 | ||
|
|
1225b27510 | ||
|
|
f86bd86043 | ||
|
|
8133164f44 | ||
|
|
2f447da34b | ||
|
|
41cdc8783e | ||
|
|
ccbb324270 | ||
|
|
8d4a0c34e7 | ||
|
|
2620f75955 | ||
|
|
4ca525f701 | ||
|
|
53a9aa377a | ||
|
|
d166003dfe | ||
|
|
31d80791a9 | ||
|
|
02245aa5f1 | ||
|
|
583b6b9a55 | ||
|
|
d4815e855e | ||
|
|
113665139a | ||
|
|
37e35b0ed6 | ||
|
|
eef8f00d8b | ||
|
|
bdb15a7780 | ||
|
|
2b59912549 | ||
|
|
8623085c2a | ||
|
|
612627d66f | ||
|
|
04700beab3 | ||
|
|
0d3d6c88e4 | ||
|
|
db57f67e98 | ||
|
|
baa145e15c | ||
|
|
c1bae20f01 | ||
|
|
41bc676b12 | ||
|
|
cea74dd63d | ||
|
|
9ccca3b33b | ||
|
|
1606cbab18 | ||
|
|
c428c8ae2d | ||
|
|
b52e84d1e7 | ||
|
|
3fbcbbe8e0 | ||
|
|
74eabd700c | ||
|
|
d7ddae38e5 | ||
|
|
fe7de2db99 | ||
|
|
9b9de42139 | ||
|
|
7b64288765 | ||
|
|
c6c967f89c | ||
|
|
040dba961a | ||
|
|
0146794cda | ||
|
|
b00eba77f9 | ||
|
|
3f27ae249a | ||
|
|
4384cffabe | ||
|
|
fde3234fc6 | ||
|
|
d998c7cd46 | ||
|
|
a8b62fc184 | ||
|
|
65de8e08d3 | ||
|
|
73cc1cd2e5 | ||
|
|
26926071b0 | ||
|
|
5c433129ec | ||
|
|
68bc1a6608 | ||
|
|
e67a0e0bc0 | ||
|
|
f553d45396 | ||
|
|
090a53b95a | ||
|
|
22ab7515fb | ||
|
|
a97dcc5efb | ||
|
|
4b098aef69 | ||
|
|
cb46274288 | ||
|
|
1e60a43075 | ||
|
|
1aa9cd032f | ||
|
|
5e3748315c | ||
|
|
7f67c032d1 | ||
|
|
33621af3ad | ||
|
|
2fb4a7bd6a | ||
|
|
1ff983a03b | ||
|
|
701530ef04 | ||
|
|
719e624ddf | ||
|
|
03a25df367 | ||
|
|
c93ce684a8 | ||
|
|
090708f38b | ||
|
|
49f93e660e | ||
|
|
867680f4e9 | ||
|
|
36ac529cc8 | ||
|
|
0ae388f0f6 | ||
|
|
b53ace765c | ||
|
|
f256299a0c | ||
|
|
4fb05e406a | ||
|
|
035639c68a | ||
|
|
09f53cac92 | ||
|
|
05a27d9003 | ||
|
|
5d4d1807ba | ||
|
|
836e465414 | ||
|
|
94457b24cd | ||
|
|
728aaec24d | ||
|
|
4ccd2d7087 | ||
|
|
9bc6d2928b | ||
|
|
d7d4fb36ad | ||
|
|
4e6a88abec | ||
|
|
a7051ace5d | ||
|
|
cee47f2846 | ||
|
|
f9e60acdf6 | ||
|
|
5f6ba048e6 | ||
|
|
aba91de480 | ||
|
|
118ec757a3 | ||
|
|
eaf712937b | ||
|
|
6c47c406cb | ||
|
|
eaeff3bf6a | ||
|
|
5b5fb3e32d | ||
|
|
07a8dd4912 | ||
|
|
a2f5127222 | ||
|
|
eab48ea202 | ||
|
|
00e2717653 | ||
|
|
d83f150810 | ||
|
|
2a7e6c2dd2 | ||
|
|
f8f792645b | ||
|
|
8c79f39389 | ||
|
|
f7da653e87 | ||
|
|
1ed753eff1 | ||
|
|
385de967a4 | ||
|
|
ac84019777 | ||
|
|
b0c6fe9474 | ||
|
|
94e2b0233a | ||
|
|
c202e3426b | ||
|
|
7e86e83173 | ||
|
|
604857648b | ||
|
|
76fb50473f | ||
|
|
ffee3b225f | ||
|
|
5f1dd42559 | ||
|
|
57a7c78ce2 | ||
|
|
33c6643388 | ||
|
|
74deba61e6 | ||
|
|
bb2f0e6c55 | ||
|
|
b2f6f68d1a | ||
|
|
fdde065a94 | ||
|
|
9231b0914b | ||
|
|
78f49ae7c9 | ||
|
|
95ff81d81f | ||
|
|
92b73f62ad | ||
|
|
1057836788 | ||
|
|
93f8702962 | ||
|
|
3d71454666 | ||
|
|
c0cc2744c6 | ||
|
|
65b1c34d06 | ||
|
|
67615719ae | ||
|
|
491c8ebe77 | ||
|
|
cae23d61e2 | ||
|
|
9cad9ad2f9 | ||
|
|
ba977d6b49 | ||
|
|
88bb2f91a5 | ||
|
|
dda11627f8 | ||
|
|
2df713ca0c | ||
|
|
486ede9396 | ||
|
|
72afcc3e5a | ||
|
|
ef2eaceb88 | ||
|
|
c2db1c70e3 |
@@ -1,7 +1,8 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "0.10.33"
|
||||
- "0.12.4"
|
||||
before_install:
|
||||
- "npm install -g npm"
|
||||
- "npm install -g grunt-cli"
|
||||
- "npm install -g phantomjs"
|
||||
install: npm install
|
||||
install:
|
||||
- "npm install"
|
||||
@@ -11,17 +11,18 @@ module.exports = function(grunt) {
|
||||
pkg: grunt.file.readJSON('package.json'),
|
||||
settings: grunt.file.readJSON('./server_config/settings.json'),
|
||||
|
||||
font: {
|
||||
webfont: {
|
||||
icons: {
|
||||
src: ['front/src/fonts/svg-icons/*.svg'],
|
||||
destCss: 'front/src/less/icons.less',
|
||||
destFonts: 'front/src/fonts/icons.woff',
|
||||
|
||||
// Optional: Custom routing of font filepaths for CSS
|
||||
cssRouter: function (fontpath) {
|
||||
var pathArray = fontpath.split('/');
|
||||
var fileName = pathArray[pathArray.length - 1];
|
||||
return '/fonts/' + fileName;
|
||||
src: 'front/src/fonts/svg-icons/*.svg',
|
||||
dest: 'tmp',
|
||||
destCss: 'front/src/less',
|
||||
options: {
|
||||
engine: 'node',
|
||||
types: 'woff',
|
||||
stylesheet: 'less',
|
||||
embed: true,
|
||||
htmlDemo: false,
|
||||
syntax: 'bootstrap'
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -102,7 +103,6 @@ module.exports = function(grunt) {
|
||||
},
|
||||
build: {
|
||||
files: [
|
||||
{src: ['./front/src/fonts/icons.woff'], dest: './front/build/fonts/icons.woff'},
|
||||
{src: ['./front/src/img/favicon.png'], dest: './front/build/img/favicon.png'},
|
||||
{src: ['./front/src/img/logo-large.png'], dest: './front/build/img/logo-large.png'},
|
||||
]
|
||||
@@ -143,7 +143,7 @@ module.exports = function(grunt) {
|
||||
options: {
|
||||
reporter: 'spec',
|
||||
},
|
||||
src: ['test/core/offendersHelpersTest.js']
|
||||
src: ['test/core/imageOptimizerTest.js']
|
||||
},
|
||||
coverage: {
|
||||
options: {
|
||||
@@ -211,10 +211,7 @@ module.exports = function(grunt) {
|
||||
html: './front/build/main.html',
|
||||
css: './front/build/css/*.css',
|
||||
options: {
|
||||
assetsDirs: ['front/build'],
|
||||
patterns: {
|
||||
css: [[/(\/fonts\/icons\.woff)/gm, 'Replacing reference to icons.woff']]
|
||||
}
|
||||
assetsDirs: ['front/build']
|
||||
}
|
||||
},
|
||||
htmlmin: {
|
||||
@@ -294,7 +291,7 @@ module.exports = function(grunt) {
|
||||
|
||||
|
||||
grunt.registerTask('icons', [
|
||||
'font:icons',
|
||||
'webfont:icons',
|
||||
'less',
|
||||
'clean:tmp'
|
||||
]);
|
||||
|
||||
@@ -1,36 +1,73 @@
|
||||
# Yellow Lab Tools
|
||||
<img src="./doc/img/logo.png" alt="Yellow Lab Tools"></img>
|
||||
|
||||
Online tool that lets you test a webpage and detects **performance** and **front-end code quality** issues.
|
||||
|
||||
[](https://gemnasium.com/gmetais/YellowLabTools) [](https://travis-ci.org/gmetais/YellowLabTools)
|
||||
Analyzes a webpage and detects **performance** or **front-end code quality** issues. Easy to launch, easy to understand, in-depth details.
|
||||
|
||||
|
||||
### Access the tool here: [http://yellowlab.tools](http://yellowlab.tools)
|
||||
#
|
||||
|
||||
<img src="./doc/img/commandment.png" alt="No website should go live without being tested with Yellow Lab Tools"></img>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="70%">
|
||||
The <b>online tool</b> (recommended):
|
||||
<a href="http://yellowlab.tools" target="_blank">http://yellowlab.tools</a>
|
||||
</td>
|
||||
<td width="30%">
|
||||
<img src="./doc/img/YLT-animated.gif"></img>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="70%">
|
||||
The <b>CLI</b> (Command Line Interface) - <a href="https://github.com/gmetais/YellowLabTools/wiki/Command-Line-Interface" target="_blank">Doc here</a>
|
||||
</td>
|
||||
<td width="30%">
|
||||
<img src="./doc/img/YLT-cli-animated.gif"></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
|
||||
</td>
|
||||
<td width="30%">
|
||||
<img src="./doc/img/grunt-logo.png"></img>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="70%">
|
||||
The <b>NodeJS</b> package - <a href="https://github.com/gmetais/YellowLabTools/wiki/NodeJS-module" target="_blank">Doc here</a>
|
||||
</td>
|
||||
<td width="30%">
|
||||
<img src="./doc/img/npm-logo.png"></img>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="70%">
|
||||
The <b>public API</b> - <a href="https://github.com/gmetais/YellowLabTools/wiki/Public-API" target="_blank">Doc here</a>
|
||||
<br>
|
||||
Allows you to launch runs from any language
|
||||
</td>
|
||||
<td width="30%">
|
||||
<img src="./doc/img/api-logo.png"></img>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||

|
||||
|
||||
|
||||
## How it works
|
||||
|
||||
The tool loads the given URL via [PhantomJS](http://phantomjs.org/) (a headless browser) and collects various metrics and statistics with the help of [Phantomas](https://github.com/macbre/phantomas). These metrics are categorized and transformed into scores. It also gives in-depth details so developpers can correct the detected issues.
|
||||
The tool loads the given URL via [PhantomJS](http://phantomjs.org/) (a headless browser) and collects various metrics and statistics with the help of [Phantomas](https://github.com/macbre/phantomas). These metrics are then categorized and transformed into scores. It also gives in-depth details so developpers can fix the detected issues.
|
||||
|
||||
By the way, it's free because we are geeks, not businessmen. All we want is a ★ on GitHub, it will boost our motivation to add more awesome features!!!
|
||||
|
||||

|
||||
|
||||
## Different ways to use YLT
|
||||
|
||||
#### The web interface:
|
||||
This is the best way to discover the tool: [http://yellowlab.tools](http://yellowlab.tools).
|
||||
Please note the server is hosted in Baltimore, USA.
|
||||
## Test your localhost
|
||||
|
||||
#### The Command Line Client:
|
||||
Might be useful if you want to build an automation tool. The documentation is [here](https://github.com/gmetais/YellowLabTools/wiki/Command-Line-Interface).
|
||||
|
||||
#### The NPM module:
|
||||
Can be used to build automation tools in NodeJS. The documentation is [here](https://github.com/gmetais/YellowLabTools/wiki/NodeJS-module).
|
||||
|
||||
#### The Public API:
|
||||
Hosted on our http://yellowlab.tools server, it is a RESTful API that allows you to launch distant tests. The documentation is [here](https://github.com/gmetais/YellowLabTools/wiki/Public-API).
|
||||
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
|
||||
@@ -38,18 +75,16 @@ Hosted on our http://yellowlab.tools server, it is a RESTful API that allows you
|
||||
If your project is not accessible from outside, or if you want to fork and improve the tool, you can build your own instance. The documentation is [here](https://github.com/gmetais/YellowLabTools/wiki/Install-your-private-server).
|
||||
|
||||
|
||||
## Help needed
|
||||
|
||||
## Help needed!
|
||||
There are so many things left to do, **your help would be greatly appreciated**! Please report bugs, ask for evolutions and come code with me.
|
||||
|
||||
|
||||
## License
|
||||
Please read the [license](LICENSE).
|
||||
Icons are the property of [IcoMoon.io](https://icomoon.io/)
|
||||
Please read the [license](LICENSE). Icons are the property of [IcoMoon.io](https://icomoon.io/)
|
||||
|
||||
|
||||
## Author
|
||||
Gaël Métais. I'm a webperf freelance based in Paris. If you understand french, you can visit [my website](http://www.gaelmetais.com).
|
||||
Gaël Métais. I'm a webperf freelance based in Paris. If you understand French, you can visit [my website](http://www.gaelmetais.com).
|
||||
|
||||
|
||||
## Contributors
|
||||
|
||||
@@ -12,8 +12,13 @@ var cli = meow({
|
||||
' yellowlabtools <url> <options>',
|
||||
'',
|
||||
'Options:',
|
||||
' --device Use "phone" or "tablet" to simulate a mobile device (by user-agent and viewport size).',
|
||||
' --screenshot Will take a screenshot and use this value as the output path. It needs to end with ".png".',
|
||||
' --js-deep-analysis When activated, the javascriptExecutionTree will contain sub-requests.',
|
||||
' --wait-for-selector Once the page is loaded, Phantomas will wait until the given CSS selector matches some elements.',
|
||||
' --cookie Adds a cookie on the main domain.',
|
||||
' --auth-user Basic HTTP authentication username.',
|
||||
' --auth-pass Basic HTTP authentication password.',
|
||||
''
|
||||
].join('\n'),
|
||||
pkg: '../package.json'
|
||||
@@ -23,8 +28,7 @@ var cli = meow({
|
||||
|
||||
// Check parameters
|
||||
if (cli.input.length < 1) {
|
||||
console.error('Incorrect parameters: url not provided');
|
||||
process.exit(1);
|
||||
cli.showHelp();
|
||||
}
|
||||
|
||||
var url = cli.input[0];
|
||||
@@ -50,6 +54,19 @@ if (cli.flags.jsDeepAnalysis === true || cli.flags.jsDeepAnalysis === 'true') {
|
||||
options.jsDeepAnalysis = true;
|
||||
}
|
||||
|
||||
// Device simulation
|
||||
options.device = cli.flags.device || 'desktop';
|
||||
|
||||
// Wait for CSS selector
|
||||
options.waitForSelector = cli.flags.waitForSelector || null;
|
||||
|
||||
// Cookie
|
||||
options.cookie = cli.flags.cookie || null;
|
||||
|
||||
// HTTP basic auth
|
||||
options.authUser = cli.flags.authUser || null;
|
||||
options.authPass = cli.flags.authPass || null;
|
||||
|
||||
|
||||
(function execute(url, options) {
|
||||
'use strict';
|
||||
|
||||
@@ -7,12 +7,14 @@ 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);
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "yellowlabtools",
|
||||
"dependencies": {
|
||||
"angular": "~1.3.14",
|
||||
"angular-route": "~1.3.14",
|
||||
"angular-resource": "~1.3.14",
|
||||
"angular-sanitize": "~1.3.14",
|
||||
"angular-animate": "~1.3.14"
|
||||
},
|
||||
"resolutions": {
|
||||
"angular": "~1.3.8"
|
||||
"angular": "1.3.15",
|
||||
"angular-route": "1.3.15",
|
||||
"angular-resource": "1.3.15",
|
||||
"angular-sanitize": "1.3.15",
|
||||
"angular-animate": "1.3.15",
|
||||
"angular-local-storage": "0.2.2",
|
||||
"angular-chart.js": "0.7.1",
|
||||
"Chart.js": "~1.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 169 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 343 B |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
@@ -46,6 +46,14 @@
|
||||
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;
|
||||
}
|
||||
.summary .notations {
|
||||
display: table;
|
||||
width: 80%;
|
||||
|
||||
@@ -1,22 +1,150 @@
|
||||
.promess {
|
||||
padding: 0em 2em 3em;
|
||||
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: #e74c3c;
|
||||
background: #ffa319;
|
||||
color: #fff;
|
||||
}
|
||||
.launchBtn:focus {
|
||||
background: #e74c3c;
|
||||
}
|
||||
.launchBtn.disabled {
|
||||
background: #deaca6;
|
||||
}
|
||||
.launchBtn.disabled:focus {
|
||||
color: #ddd;
|
||||
}
|
||||
.settings {
|
||||
width: 50%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.settings input,
|
||||
.settings select {
|
||||
font-size: 1em;
|
||||
}
|
||||
.settings input[type=text] {
|
||||
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.active {
|
||||
color: #ffa319;
|
||||
border: 2px solid #ffa319;
|
||||
padding: 0;
|
||||
}
|
||||
.device .item:hover {
|
||||
color: #ffa319;
|
||||
}
|
||||
.device .item > div {
|
||||
margin: 0.2em 0 0.1em;
|
||||
font-size: 3em;
|
||||
}
|
||||
.settingsTooltip {
|
||||
position: relative;
|
||||
}
|
||||
.settingsTooltip span {
|
||||
font-size: 0.8em;
|
||||
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;
|
||||
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;
|
||||
}
|
||||
.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: 8em auto 0;
|
||||
margin: 6em auto 0;
|
||||
font-size: 0.9em;
|
||||
color: #413;
|
||||
}
|
||||
|
||||
@@ -82,7 +82,11 @@
|
||||
font-size: 3em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.offenders .offenderButton {
|
||||
.rule .startTime {
|
||||
display: none;
|
||||
}
|
||||
.offendersTable .offenderButton,
|
||||
.value .offenderButton {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
background: #efe;
|
||||
@@ -91,16 +95,19 @@
|
||||
border-radius: 0.4em;
|
||||
z-index: 1;
|
||||
}
|
||||
.offenders .offenderButton.opens {
|
||||
.offendersTable .offenderButton.opens,
|
||||
.value .offenderButton.opens {
|
||||
padding-right: 0.75em;
|
||||
}
|
||||
.offenders .offenderButton.opens:after {
|
||||
.offendersTable .offenderButton.opens:after,
|
||||
.value .offenderButton.opens:after {
|
||||
position: relative;
|
||||
left: 0.5em;
|
||||
content: '\25BC';
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.offenders .offenderButton > div {
|
||||
.offendersTable .offenderButton > div,
|
||||
.value .offenderButton > div {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
@@ -111,29 +118,37 @@
|
||||
border-top: 1px solid #999;
|
||||
z-index: 2;
|
||||
}
|
||||
.offenders .offenderButton .domTree {
|
||||
.offendersTable .offenderButton .domTree,
|
||||
.value .offenderButton .domTree {
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.offenders .offenderButton .domTree > div {
|
||||
.offendersTable .offenderButton .domTree > div,
|
||||
.value .offenderButton .domTree > div {
|
||||
margin: 0.5em;
|
||||
}
|
||||
.offenders .offenderButton .domTree > div div {
|
||||
.offendersTable .offenderButton .domTree > div div,
|
||||
.value .offenderButton .domTree > div div {
|
||||
margin-left: 1em;
|
||||
}
|
||||
.offenders .offenderButton .backtrace,
|
||||
.offenders .offenderButton .cssFileAndLine {
|
||||
.offendersTable .offenderButton .backtrace,
|
||||
.value .offenderButton .backtrace,
|
||||
.offendersTable .offenderButton .cssFileAndLine,
|
||||
.value .offenderButton .cssFileAndLine {
|
||||
white-space: nowrap;
|
||||
padding: 0.5em;
|
||||
}
|
||||
.offenders .offenderButton.opens:hover {
|
||||
.offendersTable .offenderButton.opens:hover,
|
||||
.value .offenderButton.opens:hover {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
background: #ffe0cc;
|
||||
z-index: 2;
|
||||
}
|
||||
.offenders .offenderButton.opens:hover > div {
|
||||
.offendersTable .offenderButton.opens:hover > div,
|
||||
.value .offenderButton.opens:hover > div {
|
||||
display: block;
|
||||
background: #ffe0cc;
|
||||
}
|
||||
.offendersHtml {
|
||||
display: inline-block;
|
||||
@@ -149,16 +164,18 @@
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
}
|
||||
.colorPalette {
|
||||
width: 30em;
|
||||
border: 2px solid #000;
|
||||
text-align: left;
|
||||
.checker {
|
||||
/* Checkerboard background */
|
||||
background-color: #ddd;
|
||||
background-image: linear-gradient(45deg, #aaaaaa 25%, transparent 25%, transparent 75%, #aaaaaa 75%, #aaaaaa), linear-gradient(45deg, #aaaaaa 25%, transparent 25%, transparent 75%, #aaaaaa 75%, #aaaaaa);
|
||||
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;
|
||||
@@ -183,7 +200,6 @@
|
||||
.colorPalette > div:hover:after {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
background-color: inherit;
|
||||
left: -0.2em;
|
||||
top: -0.2em;
|
||||
width: 100%;
|
||||
@@ -191,3 +207,38 @@
|
||||
z-index: 2;
|
||||
border: 0.2em solid #f1c40f;
|
||||
}
|
||||
.totalWeightPie {
|
||||
max-width: 39em;
|
||||
margin: 2em auto 4em;
|
||||
}
|
||||
.totalWeightPie canvas {
|
||||
max-width: inherit;
|
||||
}
|
||||
.hugeFile {
|
||||
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 {
|
||||
max-height: 1.6em;
|
||||
max-width: 4em;
|
||||
border: 1px solid #000;
|
||||
margin-top: 0.2em;
|
||||
}
|
||||
|
||||
@@ -143,6 +143,10 @@
|
||||
border: 1px dotted #aaa;
|
||||
text-align: left;
|
||||
}
|
||||
.subFilters {
|
||||
margin-left: 3em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.table {
|
||||
display: table;
|
||||
width: 100%;
|
||||
@@ -306,9 +310,27 @@
|
||||
color: #e74c3c;
|
||||
cursor: pointer;
|
||||
}
|
||||
.warningsFilterOn > div {
|
||||
.queryWithoutResultsFilterOn > div {
|
||||
display: none;
|
||||
}
|
||||
.warningsFilterOn > div.warning {
|
||||
.queryWithoutResultsFilterOn > div.queryWithoutResults {
|
||||
display: table-row;
|
||||
}
|
||||
.jQueryCallOnEmptyObjectFilterOn > div {
|
||||
display: none;
|
||||
}
|
||||
.jQueryCallOnEmptyObjectFilterOn > div.jQueryCallOnEmptyObject {
|
||||
display: table-row;
|
||||
}
|
||||
.eventNotDelegatedFilterOn > div {
|
||||
display: none;
|
||||
}
|
||||
.eventNotDelegatedFilterOn > div.eventNotDelegated {
|
||||
display: table-row;
|
||||
}
|
||||
.jsErrorFilterOn > div {
|
||||
display: none;
|
||||
}
|
||||
.jsErrorFilterOn > div.jsError {
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M 256.00,0.00C 397.385,0.00, 512.00,114.615, 512.00,256.00s -114.615,256.00, -256.00,256.00s -256.00-114.615, -256.00-256.00S 114.615,0.00, 256.00,0.00z M 256.00,464.00 c 114.875,0.00 208.00-93.125 208.00-208.00S 370.875,48.00, 256.00,48.00s -208.00,93.125, -208.00,208.00S 141.125,464.00, 256.00,464.00zM 105.372,233.373l 128.001-128.00c 12.496-12.497 32.757-12.497 45.254,0.00c 12.497,12.497 12.497,32.758,0.00,45.255L 205.255,224.00 L 384.00,224.00 c 17.673,0.00 32.00,14.327 32.00,32.00c0.00,17.673, -14.327,32.00, -32.00,32.00l-178.745,0.00 l 73.373,73.372c 12.497,12.497 12.497,32.759,0.00,45.256 C 272.379,412.876, 264.189,416.00, 256.00,416.00s -16.379-3.124, -22.627-9.372l -128.001-128.00C 92.876,266.131, 92.876,245.869, 105.372,233.373z" ></path></svg>
|
||||
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M 256.00,0.00C 397.385,0.00, 512.00,114.615, 512.00,256.00s -114.615,256.00, -256.00,256.00s -256.00-114.615, -256.00-256.00S 114.615,0.00, 256.00,0.00z M 256.00,464.00 c 114.875,0.00 208.00-93.125 208.00-208.00S 370.875,48.00, 256.00,48.00s -208.00,93.125, -208.00,208.00S 141.125,464.00, 256.00,464.00zM 105.372,233.373l 128.001-128.00c 12.496-12.497 32.757-12.497 45.254,0.00c 12.497,12.497 12.497,32.758,0.00,45.255L 205.255,224.00 L 384.00,224.00 c 17.673,0.00 32.00,14.327 32.00,32.00c0.00,17.673, -14.327,32.00, -32.00,32.00l-178.745,0.00 l 73.373,73.372c 12.497,12.497 12.497,32.759,0.00,45.256 C 272.379,412.876, 264.189,416.00, 256.00,416.00s -16.379-3.124, -22.627-9.372l -128.001-128.00C 92.876,266.131, 92.876,245.869, 105.372,233.373z" ></path></svg>
|
||||
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -1 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M0.00,416.00L 512.00,416.00L 512.00,480.00L0.00,480.00zM 64.00,288.00L 128.00,288.00L 128.00,384.00L 64.00,384.00zM 160.00,160.00L 224.00,160.00L 224.00,384.00L 160.00,384.00zM 256.00,256.00L 320.00,256.00L 320.00,384.00L 256.00,384.00zM 352.00,64.00L 416.00,64.00L 416.00,384.00L 352.00,384.00z" ></path></svg>
|
||||
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M0.00,416.00L 512.00,416.00L 512.00,480.00L0.00,480.00zM 64.00,288.00L 128.00,288.00L 128.00,384.00L 64.00,384.00zM 160.00,160.00L 224.00,160.00L 224.00,384.00L 160.00,384.00zM 256.00,256.00L 320.00,256.00L 320.00,384.00L 256.00,384.00zM 352.00,64.00L 416.00,64.00L 416.00,384.00L 352.00,384.00z" ></path></svg>
|
||||
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 634 B |
@@ -1 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M 478.145,402.241L 320.00,138.729L 320.00,32.00 l 16.00,0.00 c 8.80,0.00, 16.00-7.20, 16.00-16.00s-7.20-16.00-16.00-16.00L 176.00,0.00 c-8.80,0.00-16.00,7.20-16.00,16.00s 7.20,16.00, 16.00,16.00l 16.00,0.00 l0.00,106.729 L 33.856,402.241C-2.365,462.608, 25.60,512.00, 96.00,512.00l 320.00,0.00 C 486.40,512.00, 514.365,462.608, 478.145,402.241z M 120.519,320.00L 224.00,147.531L 224.00,32.00 l 64.00,0.00 l0.00,115.531 L 391.482,320.00L 120.519,320.00 z" ></path></svg>
|
||||
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M 478.145,402.241L 320.00,138.729L 320.00,32.00 l 16.00,0.00 c 8.80,0.00, 16.00-7.20, 16.00-16.00s-7.20-16.00-16.00-16.00L 176.00,0.00 c-8.80,0.00-16.00,7.20-16.00,16.00s 7.20,16.00, 16.00,16.00l 16.00,0.00 l0.00,106.729 L 33.856,402.241C-2.365,462.608, 25.60,512.00, 96.00,512.00l 320.00,0.00 C 486.40,512.00, 514.365,462.608, 478.145,402.241z M 120.519,320.00L 224.00,147.531L 224.00,32.00 l 64.00,0.00 l0.00,115.531 L 391.482,320.00L 120.519,320.00 z" ></path></svg>
|
||||
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 792 B |
@@ -1 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M0.00,0.00L 128.00,0.00L 128.00,128.00L0.00,128.00zM 192.00,32.00L 512.00,32.00L 512.00,96.00L 192.00,96.00zM0.00,192.00L 128.00,192.00L 128.00,320.00L0.00,320.00zM 192.00,224.00L 512.00,224.00L 512.00,288.00L 192.00,288.00zM0.00,384.00L 128.00,384.00L 128.00,512.00L0.00,512.00zM 192.00,416.00L 512.00,416.00L 512.00,480.00L 192.00,480.00z" ></path></svg>
|
||||
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M0.00,0.00L 128.00,0.00L 128.00,128.00L0.00,128.00zM 192.00,32.00L 512.00,32.00L 512.00,96.00L 192.00,96.00zM0.00,192.00L 128.00,192.00L 128.00,320.00L0.00,320.00zM 192.00,224.00L 512.00,224.00L 512.00,288.00L 192.00,288.00zM0.00,384.00L 128.00,384.00L 128.00,512.00L0.00,512.00zM 192.00,416.00L 512.00,416.00L 512.00,480.00L 192.00,480.00z" ></path></svg>
|
||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 679 B |
@@ -1 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M 437.011,74.99C 390.685,28.662, 326.693,0.00, 256.00,0.00C 146.256,0.00, 52.655,69.064, 16.251,166.094l 59.938,22.477 C 103.491,115.798, 173.692,64.00, 256.00,64.00c 53.02,0.00, 101.01,21.50, 135.753,56.247L 320.00,192.00l 192.00,0.00 L 512.00,0.00 L 437.011,74.99zM 256.00,448.00c-53.02,0.00-101.013-21.496-135.756-56.244L 192.00,320.00L0.00,320.00 l0.00,192.00 l 74.997-74.997C 121.32,483.334, 185.306,512.00, 256.00,512.00 c 109.745,0.00, 203.346-69.064, 239.75-166.094l-59.938-22.477C 408.51,396.202, 338.309,448.00, 256.00,448.00z" ></path></svg>
|
||||
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M 437.011,74.99C 390.685,28.662, 326.693,0.00, 256.00,0.00C 146.256,0.00, 52.655,69.064, 16.251,166.094l 59.938,22.477 C 103.491,115.798, 173.692,64.00, 256.00,64.00c 53.02,0.00, 101.01,21.50, 135.753,56.247L 320.00,192.00l 192.00,0.00 L 512.00,0.00 L 437.011,74.99zM 256.00,448.00c-53.02,0.00-101.013-21.496-135.756-56.244L 192.00,320.00L0.00,320.00 l0.00,192.00 l 74.997-74.997C 121.32,483.334, 185.306,512.00, 256.00,512.00 c 109.745,0.00, 203.346-69.064, 239.75-166.094l-59.938-22.477C 408.51,396.202, 338.309,448.00, 256.00,448.00z" ></path></svg>
|
||||
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 875 B |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M 368.00,0.00L 144.00,0.00 c-26.40,0.00-48.00,21.60-48.00,48.00l0.00,416.00 c0.00,26.40, 21.60,48.00, 48.00,48.00l 224.00,0.00 c 26.40,0.00, 48.00-21.60, 48.00-48.00L 416.00,48.00 C 416.00,21.60, 394.40,0.00, 368.00,0.00z M 192.00,24.00 l 128.00,0.00 l0.00,16.00 L 192.00,40.00 L 192.00,24.00 z M 256.00,480.00c-17.673,0.00-32.00-14.327-32.00-32.00s 14.327-32.00, 32.00-32.00s 32.00,14.327, 32.00,32.00S 273.673,480.00, 256.00,480.00z M 384.00,384.00L 128.00,384.00 L 128.00,64.00 l 256.00,0.00 L 384.00,384.00 z" ></path></svg>
|
||||
|
After Width: | Height: | Size: 851 B |
@@ -1 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M 224.00,352.00L 288.00,352.00L 288.00,416.00L 224.00,416.00zM 352.00,128.00 C 369.673,128.00 384.00,142.327 384.00,160.00 L 384.00,256.00 L 288.00,320.00 L 224.00,320.00 L 224.00,288.00 L 320.00,224.00 L 320.00,192.00 L 160.00,192.00 L 160.00,128.00 L 352.00,128.00 ZM 256.00,48.00 C 200.441,48.00 148.208,69.636 108.922,108.922 C 69.636,148.208 48.00,200.441 48.00,256.00 C 48.00,311.559 69.636,363.792 108.922,403.078 C 148.208,442.364 200.441,464.00 256.00,464.00 C 311.559,464.00 363.792,442.364 403.078,403.078 C 442.364,363.792 464.00,311.559 464.00,256.00 C 464.00,200.441 442.364,148.208 403.078,108.922 C 363.792,69.636 311.559,48.00 256.00,48.00 Z M 256.00,0.00 L 256.00,0.00 C 397.385,0.00 512.00,114.615 512.00,256.00 C 512.00,397.385 397.385,512.00 256.00,512.00 C 114.615,512.00 0.00,397.385 0.00,256.00 C 0.00,114.615 114.615,0.00 256.00,0.00 Z" ></path></svg>
|
||||
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M 224.00,352.00L 288.00,352.00L 288.00,416.00L 224.00,416.00zM 352.00,128.00 C 369.673,128.00 384.00,142.327 384.00,160.00 L 384.00,256.00 L 288.00,320.00 L 224.00,320.00 L 224.00,288.00 L 320.00,224.00 L 320.00,192.00 L 160.00,192.00 L 160.00,128.00 L 352.00,128.00 ZM 256.00,48.00 C 200.441,48.00 148.208,69.636 108.922,108.922 C 69.636,148.208 48.00,200.441 48.00,256.00 C 48.00,311.559 69.636,363.792 108.922,403.078 C 148.208,442.364 200.441,464.00 256.00,464.00 C 311.559,464.00 363.792,442.364 403.078,403.078 C 442.364,363.792 464.00,311.559 464.00,256.00 C 464.00,200.441 442.364,148.208 403.078,108.922 C 363.792,69.636 311.559,48.00 256.00,48.00 Z M 256.00,0.00 L 256.00,0.00 C 397.385,0.00 512.00,114.615 512.00,256.00 C 512.00,397.385 397.385,512.00 256.00,512.00 C 114.615,512.00 0.00,397.385 0.00,256.00 C 0.00,114.615 114.615,0.00 256.00,0.00 Z" ></path></svg>
|
||||
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M 512.00,416.00L 512.00,32.00 L0.00,32.00 l0.00,384.00 l 224.00,0.00 l0.00,32.00 l-96.00,0.00 l0.00,32.00 l 256.00,0.00 l0.00-32.00 l-96.00,0.00 l0.00-32.00 L 512.00,416.00 z M 64.00,96.00l 384.00,0.00 l0.00,256.00 L 64.00,352.00 L 64.00,96.00 z" ></path></svg>
|
||||
|
After Width: | Height: | Size: 584 B |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M 400.00,0.00L 80.00,0.00 C 53.60,0.00, 32.00,21.60, 32.00,48.00l0.00,416.00 c0.00,26.40, 21.60,48.00, 48.00,48.00l 320.00,0.00 c 26.40,0.00, 48.00-21.60, 48.00-48.00L 448.00,48.00 C 448.00,21.60, 426.40,0.00, 400.00,0.00z M 240.00,496.00 c-8.836,0.00-16.00-7.163-16.00-16.00s 7.164-16.00, 16.00-16.00s 16.00,7.163, 16.00,16.00S 248.836,496.00, 240.00,496.00z M 384.00,448.00L 96.00,448.00 L 96.00,64.00 l 288.00,0.00 L 384.00,448.00 z" ></path></svg>
|
||||
|
After Width: | Height: | Size: 774 B |
@@ -1 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><g class="svgGrid" id="svgGrid"><line stroke-width="1" stroke="#B35047" x1="32" x2="32" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="64" x2="64" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="96" x2="96" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="128" x2="128" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="160" x2="160" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="192" x2="192" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="224" x2="224" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="256" x2="256" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="288" x2="288" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="320" x2="320" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="352" x2="352" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="384" x2="384" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="416" x2="416" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="448" x2="448" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="480" x2="480" y1="0" y2="512"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="32" y2="32"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="64" y2="64"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="96" y2="96"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="128" y2="128"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="160" y2="160"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="192" y2="192"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="224" y2="224"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="256" y2="256"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="288" y2="288"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="320" y2="320"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="352" y2="352"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="384" y2="384"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="416" y2="416"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="448" y2="448"></line><line stroke-width="1" stroke="#B35047" x1="0" x2="512" y1="480" y2="480"></line><rect x="0" y="0" width="512" height="512" fill="none" stroke-width="1" stroke="#B35047"></rect></g><path d="M 256.00,79.362 L 83.583,448.00 L 428.417,448.00 L 256.00,79.362 Z M 256.00,0.00 L 256.00,0.00 C 267.035,0.00 278.07,7.441 286.442,22.324 L 504.978,457.88 C 521.723,487.646 507.478,512.00 473.327,512.00 L 38.673,512.00 C 4.521,512.00 -9.722,487.646 7.021,457.88 L 225.558,22.324 C 233.93,7.441 244.965,0.00 256.00,0.00 ZM 224.00,384.00A32.00,32.00 2520.00 1,0 288.00,384A32.00,32.00 2520.00 1,0 224.00,384zM 256.00,192.00 C 273.673,192.00 288.00,206.327 288.00,224.00 L 278.00,320.00 L 234.00,320.00 L 224.00,224.00 C 224.00,206.327 238.327,192.00 256.00,192.00 Z" ></path></svg>
|
||||
<?xml version="1.0" encoding="utf-8"?> <!-- Generator: IcoMoon.io --> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#000000"><path d="M 256.00,79.362 L 83.583,448.00 L 428.417,448.00 L 256.00,79.362 Z M 256.00,0.00 L 256.00,0.00 C 267.035,0.00 278.07,7.441 286.442,22.324 L 504.978,457.88 C 521.723,487.646 507.478,512.00 473.327,512.00 L 38.673,512.00 C 4.521,512.00 -9.722,487.646 7.021,457.88 L 225.558,22.324 C 233.93,7.441 244.965,0.00 256.00,0.00 ZM 256.00,192.00 C 273.673,192.00 288.00,206.327 288.00,224.00 L 278.00,320.00 L 234.00,320.00 L 224.00,224.00 C 224.00,206.327 238.327,192.00 256.00,192.00 Z" ></path><circle cx="256" cy="384" r="31" style="stroke:#000000; fill:#000000"/></svg>
|
||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 887 B |
@@ -12,13 +12,27 @@ var yltApp = angular.module('YellowLabTools', [
|
||||
'resultsFactory',
|
||||
'apiService',
|
||||
'menuService',
|
||||
'settingsService',
|
||||
'gradeDirective',
|
||||
'offendersDirectives'
|
||||
'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(){
|
||||
ga('send', 'pageview', {'page': $location.path()});
|
||||
|
||||
@@ -8,7 +8,7 @@ dashboardCtrl.controller('DashboardCtrl', ['$scope', '$rootScope', '$routeParams
|
||||
function loadResults() {
|
||||
// Load result if needed
|
||||
if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
|
||||
Results.get({runId: $routeParams.runId}, function(result) {
|
||||
Results.get({runId: $routeParams.runId, exclude: 'toolsResults'}, function(result) {
|
||||
$rootScope.loadedResult = result;
|
||||
$scope.result = result;
|
||||
init();
|
||||
@@ -26,7 +26,8 @@ dashboardCtrl.controller('DashboardCtrl', ['$scope', '$rootScope', '$routeParams
|
||||
$scope.categoriesOrder = Object.keys($scope.result.scoreProfiles.generic.categories);
|
||||
|
||||
$scope.globalScore = Math.max($scope.result.scoreProfiles.generic.globalScore, 0);
|
||||
$scope.tweetText = 'My website\'s score is ' + $scope.globalScore + '/100 on #YellowLabTools!';
|
||||
|
||||
$scope.tweetText = 'I\'ve discovered this cool open-source tool that audits the front-end quality of a web page: ';
|
||||
}
|
||||
|
||||
$scope.showRulePage = function(ruleName) {
|
||||
@@ -34,21 +35,21 @@ dashboardCtrl.controller('DashboardCtrl', ['$scope', '$rootScope', '$routeParams
|
||||
};
|
||||
|
||||
$scope.testAgain = function() {
|
||||
API.launchTest($scope.result.params.url);
|
||||
API.relaunchTest($scope.result);
|
||||
};
|
||||
|
||||
/// When comming from a social shared link, the user needs to click on "See full report" button to display the full dashboard.
|
||||
// When comming from a social shared link, the user needs to click on "See full report" button to display the full dashboard.
|
||||
$scope.seeFullReport = function() {
|
||||
$scope.fromSocialShare = false;
|
||||
$location.search({});
|
||||
};
|
||||
|
||||
$scope.shareOnTwitter = function(message) {
|
||||
openSocialPopup('https://twitter.com/intent/tweet?url=' + document.URL + '%3Fshare&text=' + encodeURIComponent(message));
|
||||
openSocialPopup('https://twitter.com/intent/tweet?text=' + encodeURIComponent(message + 'http://yellowlab.tools'));
|
||||
};
|
||||
|
||||
$scope.shareOnLinkedin = function(message) {
|
||||
openSocialPopup('https://www.linkedin.com/shareArticle?mini=true&url=' + document.URL + '%3Fshare&title=' + encodeURIComponent(message) + '&summary=' + encodeURIComponent('YellowLabTools is a free online tool that analyzes performance and front-end quality of a webpage.'));
|
||||
openSocialPopup('https://www.linkedin.com/shareArticle?mini=true&url=http://yellowlab.tools&title=' + encodeURIComponent(message) + '&summary=' + encodeURIComponent('YellowLabTools is a free online tool that analyzes performance and front-end quality of a webpage.'));
|
||||
};
|
||||
|
||||
function openSocialPopup(url) {
|
||||
@@ -59,10 +60,5 @@ dashboardCtrl.controller('DashboardCtrl', ['$scope', '$rootScope', '$routeParams
|
||||
window.open(url, 'sharer', 'top=' + winTop + ',left=' + winLeft + ',toolbar=0,status=0,width=' + winWidth + ',height=' + winHeight);
|
||||
}
|
||||
|
||||
// Returns the URL of the JSON result
|
||||
$scope.getAPIUrl = function() {
|
||||
return '/api/results/' + $scope.runId;
|
||||
};
|
||||
|
||||
loadResults();
|
||||
}]);
|
||||
@@ -1,9 +1,13 @@
|
||||
var indexCtrl = angular.module('indexCtrl', []);
|
||||
|
||||
indexCtrl.controller('IndexCtrl', ['$scope', '$location', 'API', function($scope, $location, API) {
|
||||
indexCtrl.controller('IndexCtrl', ['$scope', 'Settings', 'API', function($scope, Settings, API) {
|
||||
|
||||
$scope.settings = Settings.getMergedSettings();
|
||||
|
||||
$scope.launchTest = function() {
|
||||
if ($scope.url) {
|
||||
API.launchTest($scope.url);
|
||||
Settings.saveSettings($scope.settings);
|
||||
API.launchTest($scope.url, $scope.settings);
|
||||
}
|
||||
};
|
||||
}]);
|
||||
@@ -1,4 +1,13 @@
|
||||
var ruleCtrl = angular.module('ruleCtrl', []);
|
||||
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;
|
||||
@@ -9,7 +18,7 @@ ruleCtrl.controller('RuleCtrl', ['$scope', '$rootScope', '$routeParams', '$locat
|
||||
function loadResults() {
|
||||
// Load result if needed
|
||||
if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
|
||||
Results.get({runId: $routeParams.runId}, function(result) {
|
||||
Results.get({runId: $routeParams.runId, exclude: 'toolsResults'}, function(result) {
|
||||
$rootScope.loadedResult = result;
|
||||
$scope.result = result;
|
||||
init();
|
||||
@@ -22,6 +31,23 @@ ruleCtrl.controller('RuleCtrl', ['$scope', '$rootScope', '$routeParams', '$locat
|
||||
|
||||
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 = {
|
||||
tooltipTemplate: '<%=label%>: <%=value%> KB'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
$scope.backToDashboard = function() {
|
||||
@@ -29,7 +55,7 @@ ruleCtrl.controller('RuleCtrl', ['$scope', '$rootScope', '$routeParams', '$locat
|
||||
};
|
||||
|
||||
$scope.testAgain = function() {
|
||||
API.launchTest($scope.result.params.url);
|
||||
API.relaunchTest($scope.result);
|
||||
};
|
||||
|
||||
loadResults();
|
||||
|
||||
@@ -7,10 +7,9 @@ screenshotCtrl.controller('ScreenshotCtrl', ['$scope', '$rootScope', '$routePara
|
||||
function loadResults() {
|
||||
// Load result if needed
|
||||
if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
|
||||
Results.get({runId: $routeParams.runId}, function(result) {
|
||||
Results.get({runId: $routeParams.runId, exclude: 'toolsResults'}, function(result) {
|
||||
$rootScope.loadedResult = result;
|
||||
$scope.result = result;
|
||||
init();
|
||||
}, function(err) {
|
||||
$scope.error = true;
|
||||
});
|
||||
@@ -24,7 +23,7 @@ screenshotCtrl.controller('ScreenshotCtrl', ['$scope', '$rootScope', '$routePara
|
||||
};
|
||||
|
||||
$scope.testAgain = function() {
|
||||
API.launchTest($scope.result.params.url);
|
||||
API.relaunchTest($scope.result);
|
||||
};
|
||||
|
||||
loadResults();
|
||||
|
||||
@@ -7,7 +7,7 @@ timelineCtrl.controller('TimelineCtrl', ['$scope', '$rootScope', '$routeParams',
|
||||
function loadResults() {
|
||||
// Load result if needed
|
||||
if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
|
||||
Results.get({runId: $routeParams.runId}, function(result) {
|
||||
Results.get({runId: $routeParams.runId, exclude: 'toolsResults'}, function(result) {
|
||||
$rootScope.loadedResult = result;
|
||||
$scope.result = result;
|
||||
render();
|
||||
@@ -19,26 +19,44 @@ timelineCtrl.controller('TimelineCtrl', ['$scope', '$rootScope', '$routeParams',
|
||||
}
|
||||
|
||||
function render() {
|
||||
initFilters();
|
||||
initScriptFiltering();
|
||||
initExecutionTree();
|
||||
initTimeline();
|
||||
$timeout(initProfiler, 100);
|
||||
}
|
||||
|
||||
function initFilters() {
|
||||
var hash = $location.hash();
|
||||
var filter = null;
|
||||
|
||||
if (hash.indexOf('filter=') === 0) {
|
||||
filter = hash.substr(7);
|
||||
}
|
||||
|
||||
$scope.warningsFilterOn = (filter !== null);
|
||||
$scope.warningsFilters = {
|
||||
queryWithoutResults: (filter === null || filter === 'queryWithoutResults'),
|
||||
jQueryCallOnEmptyObject: (filter === null || filter === 'jQueryCallOnEmptyObject'),
|
||||
eventNotDelegated: (filter === null || filter === 'eventNotDelegated'),
|
||||
jsError: (filter === null || filter === 'jsError')
|
||||
};
|
||||
}
|
||||
|
||||
function initScriptFiltering() {
|
||||
var offenders = $scope.result.rules.jsCount.offendersObj.list;
|
||||
var offenders = $scope.result.rules.totalRequests.offendersObj.list.byType.js;
|
||||
$scope.scripts = [];
|
||||
|
||||
offenders.forEach(function(script) {
|
||||
var filePath = script.file;
|
||||
offenders.forEach(function(filePath) {
|
||||
var shortPath = filePath;
|
||||
|
||||
if (filePath.length > 100) {
|
||||
filePath = filePath.substr(0, 98) + '...';
|
||||
shortPath = filePath.substr(0, 98) + '...';
|
||||
}
|
||||
|
||||
var scriptObj = {
|
||||
fullPath: script.file,
|
||||
shortPath: filePath
|
||||
fullPath: filePath,
|
||||
shortPath: shortPath
|
||||
};
|
||||
|
||||
$scope.scripts.push(scriptObj);
|
||||
@@ -141,7 +159,7 @@ timelineCtrl.controller('TimelineCtrl', ['$scope', '$rootScope', '$routeParams',
|
||||
};
|
||||
|
||||
$scope.testAgain = function() {
|
||||
API.launchTest($scope.result.params.url);
|
||||
API.relaunchTest($scope.result);
|
||||
};
|
||||
|
||||
loadResults();
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
} else if (context.length > 3) {
|
||||
html += ' and ' + (context.length - 2) + ' more...';
|
||||
}
|
||||
return html + '}';
|
||||
return html + ')';
|
||||
}
|
||||
|
||||
function isJQuery(node) {
|
||||
@@ -119,6 +119,22 @@
|
||||
function getNonJQueryHTML(node, onASingleLine) {
|
||||
var type = node.data.type;
|
||||
|
||||
if (node.windowPerformance) {
|
||||
switch (type) {
|
||||
case 'documentScroll':
|
||||
return '(triggering the scroll event on <b>document</b>)';
|
||||
|
||||
case 'windowScroll':
|
||||
return '(triggering the scroll event on <b>window</b>)';
|
||||
|
||||
case 'window.onscroll':
|
||||
return '(calling the <b>window.onscroll</b> function)';
|
||||
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
if (!node.data.callDetails) {
|
||||
return '';
|
||||
}
|
||||
@@ -153,6 +169,18 @@
|
||||
case 'error':
|
||||
return args[0];
|
||||
|
||||
case 'jQuery - onDOMReady':
|
||||
return '(function)';
|
||||
|
||||
case 'documentScroll':
|
||||
return 'The scroll event just triggered on document';
|
||||
|
||||
case 'windowScroll':
|
||||
return 'The scroll event just triggered on window';
|
||||
|
||||
case 'window.onscroll':
|
||||
return 'The window.onscroll function just got called';
|
||||
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
@@ -166,7 +194,7 @@
|
||||
|
||||
// escape HTML in args
|
||||
for (var i = 0 ; i < 4 ; i ++) {
|
||||
if (unescapedArgs[i]) {
|
||||
if (unescapedArgs[i] !== undefined) {
|
||||
args[i] = escapeHTML(unescapedArgs[i]);
|
||||
}
|
||||
}
|
||||
@@ -254,7 +282,7 @@
|
||||
return 'replace <b>' + args[0] + '</b> with ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
|
||||
case 'jQuery - text':
|
||||
if (args[0]) {
|
||||
if (args[0] !== undefined) {
|
||||
return 'set text "<b>' + args[0] + '</b>" to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
} else {
|
||||
return 'get text from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
@@ -318,7 +346,9 @@
|
||||
|
||||
case 'jQuery - on':
|
||||
case 'jQuery - one':
|
||||
if (args[1]) {
|
||||
if (isStringOfObject(args[0])) {
|
||||
return '<b>' + args[0].replace(/"\(function\)"/g, '(function)') + '</b>';
|
||||
} else if (args[1] && isPureString(args[1])) {
|
||||
return 'bind <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[1] + '</b>';
|
||||
} else {
|
||||
return 'bind <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
|
||||
@@ -518,9 +548,6 @@
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jQuery - onDOMReady':
|
||||
return '(function)';
|
||||
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
@@ -643,8 +670,8 @@
|
||||
|
||||
if (node.data.callDetails.context && node.data.callDetails.context.length === 0) {
|
||||
html += '<h4>Called on 0 jQuery element</h4><p class="advice">Useless function call, as the jQuery object is empty.</p>';
|
||||
} else if (node.data.type === 'jQuery - bind' && node.data.callDetails.context.length > 5) {
|
||||
html += '<p class="advice">The .bind() method attaches the event listener to each jQuery element one by one. Using the .on() method is preferable if available (from v1.7).</p>';
|
||||
} else if (node.eventNotDelegated) {
|
||||
html += '<p class="advice">This binding should use Event Delegation instead of binding each element one by one.</p>';
|
||||
}
|
||||
|
||||
if (node.data.resultsNumber === 0) {
|
||||
@@ -674,7 +701,7 @@
|
||||
function getProfilerLineHTML(index, node) {
|
||||
return '<div class="index">' + (index + 1) + '</div>' +
|
||||
'<div class="type">' + node.data.type + (node.children ? '<div class="children">' + recursiveChildrenHTML(node) + '</div>' : '') + '</div>' +
|
||||
'<div class="value offenders">' + getTimelineParamsHTML(node, false) + '</div>' +
|
||||
'<div class="value">' + getTimelineParamsHTML(node, false) + '</div>' +
|
||||
'<div class="details">' + getTimelineDetailsHTML(node) + '</div>' +
|
||||
'<div class="startTime ' + node.data.loadingStep + '">' + numberWithCommas(node.data.timestamp, 0) + ' ms</div>';
|
||||
}
|
||||
@@ -739,10 +766,23 @@
|
||||
element.append(getProfilerLineHTML(scope.index, scope.node));
|
||||
element[0].id = 'line_' + scope.index;
|
||||
|
||||
if (scope.node.warning || scope.node.error) {
|
||||
if (scope.node.warning) {
|
||||
element[0].classList.add('warning');
|
||||
|
||||
if (scope.node.queryWithoutResults) {
|
||||
element[0].classList.add('queryWithoutResults');
|
||||
}
|
||||
|
||||
if (scope.node.jQueryCallOnEmptyObject) {
|
||||
element[0].classList.add('jQueryCallOnEmptyObject');
|
||||
}
|
||||
|
||||
if (scope.node.eventNotDelegated) {
|
||||
element[0].classList.add('eventNotDelegated');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Bind click on the details icon
|
||||
var detailsIcon = element[0].querySelector('.details div');
|
||||
if (detailsIcon) {
|
||||
@@ -816,4 +856,34 @@
|
||||
};
|
||||
});
|
||||
|
||||
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';
|
||||
};
|
||||
});
|
||||
|
||||
})();
|
||||
@@ -4,13 +4,33 @@ apiService.factory('API', ['$location', 'Runs', 'Results', function($location, R
|
||||
|
||||
return {
|
||||
|
||||
launchTest: function(url) {
|
||||
Runs.save({
|
||||
launchTest: function(url, settings) {
|
||||
var runObject = {
|
||||
url: url,
|
||||
waitForResponse: false,
|
||||
screenshot: true,
|
||||
jsTimeline: true
|
||||
}, function(data) {
|
||||
jsTimeline: true,
|
||||
device: settings.device,
|
||||
waitForSelector: settings.waitForSelector,
|
||||
cookie: settings.cookie,
|
||||
authUser: settings.authUser,
|
||||
authPass: settings.authPass,
|
||||
};
|
||||
|
||||
if (settings.waitForSelector && settings.waitForSelector !== '') {
|
||||
runObject.waitForSelector = settings.waitForSelector;
|
||||
}
|
||||
|
||||
if (settings.cookie && settings.cookie !== '') {
|
||||
runObject.cookie = settings.cookie;
|
||||
}
|
||||
|
||||
if (settings.authUser && settings.authUser !== '' && settings.authPass && settings.authPass !== '') {
|
||||
runObject.authUser = settings.authUser;
|
||||
runObject.authPass = settings.authPass;
|
||||
}
|
||||
|
||||
Runs.save(runObject, function(data) {
|
||||
$location.path('/queue/' + data.runId);
|
||||
}, function(response) {
|
||||
if (response.status === 429) {
|
||||
@@ -19,9 +39,11 @@ apiService.factory('API', ['$location', 'Runs', 'Results', function($location, R
|
||||
alert('An error occured...');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
relaunchTest: function(result) {
|
||||
this.launchTest(result.params.url, result.params.options);
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
}]);
|
||||
@@ -0,0 +1,24 @@
|
||||
var settingsService = angular.module('settingsService', []);
|
||||
|
||||
settingsService.factory('Settings', ['localStorageService', function(localStorageService) {
|
||||
|
||||
return {
|
||||
|
||||
getMergedSettings: function() {
|
||||
var defaultSettings = {
|
||||
device: 'desktop',
|
||||
showAdvanced: false
|
||||
};
|
||||
|
||||
var savedValues = localStorageService.get('settings');
|
||||
|
||||
return angular.extend(defaultSettings, savedValues);
|
||||
},
|
||||
|
||||
saveSettings: function(settings) {
|
||||
localStorageService.set('settings', settings);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}]);
|
||||
@@ -51,6 +51,16 @@
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
|
||||
.screenshotWrapper.phone:hover:after {
|
||||
top: 1.7em;
|
||||
left: 0.64em;
|
||||
}
|
||||
|
||||
.screenshotWrapper.tablet:hover:after {
|
||||
top: 1.5em;
|
||||
left: 0.9em;
|
||||
}
|
||||
}
|
||||
|
||||
.summary .notations {
|
||||
|
||||
@@ -1,25 +1,174 @@
|
||||
.promess {
|
||||
padding: 0em 2em 3em;
|
||||
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: #e74c3c;
|
||||
background: #ffa319;
|
||||
color: #fff;
|
||||
&:focus {
|
||||
background: #e74c3c;
|
||||
}
|
||||
&.disabled {
|
||||
background: #deaca6;
|
||||
&:focus {
|
||||
color: #ddd;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.settings {
|
||||
width: 50%;
|
||||
margin: 0 auto;
|
||||
|
||||
input, select {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
input[type=text] {
|
||||
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;
|
||||
|
||||
&.active {
|
||||
color: #ffa319;
|
||||
border: 2px solid #ffa319;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #ffa319;
|
||||
}
|
||||
|
||||
> div {
|
||||
margin: 0.2em 0 0.1em;
|
||||
font-size: 3em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settingsTooltip {
|
||||
position: relative;
|
||||
span {
|
||||
font-size: 0.8em;
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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: 8em auto 0;
|
||||
margin: 6em auto 0;
|
||||
font-size: 0.9em;
|
||||
color: #413;
|
||||
|
||||
|
||||
@@ -126,11 +126,27 @@ a.linkButton {
|
||||
}
|
||||
|
||||
|
||||
.screenshotWrapper.desktop {
|
||||
.screenshotWrapper {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
border: 0.2em solid #AAA;
|
||||
background: #000;
|
||||
|
||||
> div {
|
||||
overflow: scroll;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.screenshotImage {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.screenshotError {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.screenshotWrapper.desktop {
|
||||
border: 0.2em solid #AAA;
|
||||
padding: 0.5em;
|
||||
border-top-left-radius: 0.4em;
|
||||
border-top-right-radius: 0.4em;
|
||||
@@ -160,12 +176,61 @@ a.linkButton {
|
||||
> div {
|
||||
width: 12em;
|
||||
height: 6.75em;
|
||||
overflow: scroll;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.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: " ";
|
||||
}
|
||||
|
||||
.screenshotImage {
|
||||
width: 100%;
|
||||
&: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: 8.25em;
|
||||
height: 11em;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,25 +253,6 @@ a.linkButton {
|
||||
}
|
||||
}
|
||||
|
||||
/* Icons */
|
||||
.icon-lab {
|
||||
.icon(@lab);
|
||||
}
|
||||
.icon-question {
|
||||
.icon(@question);
|
||||
}
|
||||
.icon-warning {
|
||||
.icon(@warning);
|
||||
}
|
||||
.icon-back {
|
||||
.icon(@arrow-left3);
|
||||
}
|
||||
.icon-summary {
|
||||
.icon(@list);
|
||||
}
|
||||
.icon-spaghetti {
|
||||
.icon(@bars);
|
||||
}
|
||||
.icon-loop {
|
||||
.icon(@loop);
|
||||
[class^="icon-"]:before, [class*=" icon-"]:before {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
@@ -90,7 +90,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.offenders {
|
||||
.rule .startTime {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.offendersTable, .value {
|
||||
.offenderButton {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
@@ -149,6 +153,7 @@
|
||||
|
||||
> div {
|
||||
display: block;
|
||||
background: #ffe0cc;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -171,16 +176,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
.colorPalette {
|
||||
width: 30em;
|
||||
border: 2px solid #000;
|
||||
text-align: left;
|
||||
|
||||
.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;
|
||||
@@ -208,7 +215,6 @@
|
||||
&:hover:after {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
background-color: inherit;
|
||||
left: -0.2em;
|
||||
top: -0.2em;
|
||||
width: 100%;
|
||||
@@ -217,4 +223,47 @@
|
||||
border: 0.2em solid #f1c40f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.totalWeightPie {
|
||||
max-width: 39em;
|
||||
margin: 2em auto 4em;
|
||||
|
||||
canvas {
|
||||
max-width: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.hugeFile {
|
||||
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 {
|
||||
max-height: 1.6em;
|
||||
max-width: 4em;
|
||||
border: 1px solid #000;
|
||||
margin-top: 0.2em;
|
||||
}
|
||||
@@ -162,6 +162,11 @@
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.subFilters {
|
||||
margin-left: 3em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.table {
|
||||
display: table;
|
||||
width: 100%;
|
||||
@@ -346,10 +351,34 @@
|
||||
color: #e74c3c;
|
||||
cursor: pointer;
|
||||
}
|
||||
.warningsFilterOn {
|
||||
.queryWithoutResultsFilterOn {
|
||||
> div {
|
||||
display: none;
|
||||
&.warning {
|
||||
&.queryWithoutResults {
|
||||
display: table-row;
|
||||
}
|
||||
}
|
||||
}
|
||||
.jQueryCallOnEmptyObjectFilterOn {
|
||||
> div {
|
||||
display: none;
|
||||
&.jQueryCallOnEmptyObject {
|
||||
display: table-row;
|
||||
}
|
||||
}
|
||||
}
|
||||
.eventNotDelegatedFilterOn {
|
||||
> div {
|
||||
display: none;
|
||||
&.eventNotDelegated {
|
||||
display: table-row;
|
||||
}
|
||||
}
|
||||
}
|
||||
.jsErrorFilterOn {
|
||||
> div {
|
||||
display: none;
|
||||
&.jsError {
|
||||
display: table-row;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,14 +16,18 @@
|
||||
<link rel="stylesheet" type="text/css" href="/css/screenshot.css">
|
||||
<link rel="stylesheet" type="text/css" href="/css/timeline.css">
|
||||
<link rel="stylesheet" type="text/css" href="/css/about.css">
|
||||
<link rel="stylesheet" type="text/css" href="/bower_components/angular-chart.js/dist/angular-chart.css">
|
||||
<!-- endbuild -->
|
||||
|
||||
<!-- build:js /js/all.js -->
|
||||
<script src="/bower_components/angular/angular.min.js"></script>
|
||||
<script src="/bower_components/Chart.js/Chart.min.js"></script>
|
||||
<script src="/bower_components/angular-route/angular-route.min.js"></script>
|
||||
<script src="/bower_components/angular-resource/angular-resource.min.js"></script>
|
||||
<script src="/bower_components/angular-sanitize/angular-sanitize.min.js"></script>
|
||||
<script src="/bower_components/angular-animate/angular-animate.min.js"></script>
|
||||
<script src="/bower_components/angular-local-storage/dist/angular-local-storage.min.js"></script>
|
||||
<script src="/bower_components/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>
|
||||
@@ -35,6 +39,8 @@
|
||||
<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 -->
|
||||
|
||||
@@ -11,9 +11,10 @@
|
||||
</div>
|
||||
<div>
|
||||
<a href="/result/{{result.runId}}/screenshot">
|
||||
<div class="screenshotWrapper desktop">
|
||||
<div class="screenshotWrapper" ng-class="result.params.options.device || 'desktop'">
|
||||
<div>
|
||||
<img class="screenshotImage" ng-src="{{result.screenshotUrl}}"/>
|
||||
<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>
|
||||
@@ -34,7 +35,8 @@
|
||||
</div>
|
||||
<div class="label">{{rule.policy.label}}</div>
|
||||
<div class="result">
|
||||
{{rule.value}}
|
||||
<span ng-if="rule.policy.unit == 'bytes'">{{rule.value | bytes}}</span>
|
||||
<span ng-if="rule.policy.unit != 'bytes'">{{rule.value}}</span>
|
||||
<span ng-if="rule.abnormal" class="icon-warning"></span>
|
||||
<span ng-if="rule.abnormalityScore <= -100" class="icon-warning"></span>
|
||||
<span ng-if="rule.abnormalityScore <= -300" class="icon-warning"></span>
|
||||
@@ -47,7 +49,7 @@
|
||||
</div>
|
||||
|
||||
<div ng-if="!error && !fromSocialShare" class="apiTip">
|
||||
<b>Did you know? Yellow Lab Tools now has an API</b>! <a href="{{getAPIUrl()}}" target="_blank">Here</a> is the JSON output for this run. Checkout <a href="https://github.com/gmetais/YellowLabTools/wiki/Public-API" target="_blank">the API doc</a>.
|
||||
<b>Did you know? Yellow Lab Tools has a brand new <a href="https://github.com/gmetais/grunt-yellowlabtools" target="_blank">Grunt plugin</a>!
|
||||
</div>
|
||||
|
||||
<div class="tweet" ng-if="!error && !fromSocialShare">
|
||||
|
||||
@@ -1,8 +1,77 @@
|
||||
<h2 class="promess">Free online test to help speeding up <b>heavy</b> web pages</h2>
|
||||
<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="http://www.mysite.com" class="url" />
|
||||
<input type="{{$root.isTouchDevice? 'url' : 'text'}}" name="url" ng-model="url" placeholder="http://www.mysite.com" class="url" />
|
||||
<input type="submit" value="Launch test" class="launchBtn" ng-class="{disabled: !url}" />
|
||||
<div class="settings">
|
||||
<div class="device">
|
||||
<div>Choose the simulated device:</div>
|
||||
<a href="" class="item" ng-class="{active: settings.device == 'desktop'}" ng-click="settings.device = 'desktop'"><div class="icon-screen"></div>Desktop</a>
|
||||
<a href="" class="item" ng-class="{active: settings.device == 'tablet'}" ng-click="settings.device = 'tablet'"><div class="icon-tablet"></div>Tablet</a>
|
||||
<a href="" class="item" ng-class="{active: settings.device == 'phone'}" ng-click="settings.device = 'phone'"><div class="icon-mobile"></div>Phone</a>
|
||||
</div>
|
||||
[ <a href="" class="showAdvanced" ng-click="settings.showAdvanced = !settings.showAdvanced">
|
||||
<span ng-if="!settings.showAdvanced">Advanced settings ✚</span>
|
||||
<span ng-if="settings.showAdvanced">Hide advanced settings ✖</span>
|
||||
</a> ]
|
||||
<span class="currentSettings" ng-if="!settings.showAdvanced && (settings.waitForSelector || settings.cookie || settings.authUser || settings.authPass)">
|
||||
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>
|
||||
<div class="advanced" ng-show="settings.showAdvanced">
|
||||
<!--<div>
|
||||
<div class="label">
|
||||
Wait selector
|
||||
<span class="settingsTooltip">
|
||||
<span class="icon-question"></span>
|
||||
<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">
|
||||
<span class="icon-question"></span>
|
||||
<div><b>Cookie</b><br><br>Adds a cookie on the main domain.<br><br>Example: "bar=foo;domain=url"</div>
|
||||
</span>
|
||||
</div>
|
||||
<div><input type="text" name="cookie" ng-model="settings.cookie" /></div>
|
||||
</div>
|
||||
<!--<div>
|
||||
<div class="label">
|
||||
Authent
|
||||
<span class="settingsTooltip">
|
||||
<span class="icon-question"></span>
|
||||
<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="text" class="authField" name="authPass" ng-model="settings.authPass" /></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>-->
|
||||
<!--<div>
|
||||
<div class="label">Blocked domains</div>
|
||||
<div>
|
||||
<div>
|
||||
Blacklist / Whitelist
|
||||
</div>
|
||||
<textarea name=""></textarea>
|
||||
</div>
|
||||
</div>-->
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
@@ -21,4 +90,4 @@
|
||||
<h3>JS Profiling</h3>
|
||||
<p>Untangles the JavaScript spaghetti code</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<div>Tested url: <a href="{{result.params.url}}" target="_blank" class="testedUrl">{{result.params.url}}</a></div>
|
||||
|
||||
<div class="resultsMenu">
|
||||
<a class="menuItem back" href="/"><div class="icon-back"></div><span>New test<span></a>
|
||||
<a class="menuItem back" href="/"><div class="icon-arrow-left3"></div><span>New test<span></a>
|
||||
<a class="menuItem restart" href="" ng-click="testAgain()"><div class="icon-loop"></div><span>Test again<span></a>
|
||||
<div class="menuItem" ng-class="{active: Menu.getCurrentPage() == 'dashboard'}" ng-click="Menu.changePage('dashboard')"><div class="icon-summary"></div><span>Dashboard</span></div>
|
||||
<div class="menuItem" ng-class="{active: Menu.getCurrentPage() == 'timeline'}" ng-click="Menu.changePage('timeline')"><div class="icon-spaghetti"></div><span>JS Timeline</span></div>
|
||||
<div class="menuItem" ng-class="{active: Menu.getCurrentPage() == 'dashboard'}" ng-click="Menu.changePage('dashboard')"><div class="icon-list"></div><span>Dashboard</span></div>
|
||||
<div class="menuItem" ng-class="{active: Menu.getCurrentPage() == 'timeline'}" ng-click="Menu.changePage('timeline')"><div class="icon-bars"></div><span>JS Timeline</span></div>
|
||||
</div>
|
||||
@@ -9,7 +9,11 @@
|
||||
<div>{{rule.score}}/100</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<h3>Value: {{rule.value}}</h3>
|
||||
<h3>
|
||||
Value:
|
||||
<span ng-if="rule.policy.unit == 'bytes'">{{rule.value | bytes}}</span>
|
||||
<span ng-if="rule.policy.unit != 'bytes'">{{rule.value}}</span>
|
||||
</h3>
|
||||
<div ng-bind-html="rule.policy.message" class="message"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -31,20 +35,25 @@
|
||||
<b>{{offender.id}}</b>: {{offender.occurrences}} occurrences
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'DOMinserts'">
|
||||
<dom-element-button obj="offender.insertedElement"></dom-element-button> appended to <dom-element-button obj="offender.receiverElement"></dom-element-button>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'DOMqueriesWithoutResults'">
|
||||
<b>{{offender.query}}</b> (in <dom-element-button obj="offender.context"></dom-element-button>) using {{offender.fn}}
|
||||
</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 === 'eventsBound'">
|
||||
<b>{{offender.eventName}}</b> bound to <dom-element-button obj="offender.element"></dom-element-button>
|
||||
<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'">
|
||||
@@ -62,6 +71,10 @@
|
||||
</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">
|
||||
@@ -124,9 +137,9 @@
|
||||
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'requests' || policyName === 'htmlCount' || policyName === 'jsCount' || policyName === 'cssCount' || policyName === 'imageCount' || policyName === 'webfontCount' || policyName === 'videoCount' || policyName === 'jsonCount' || policyName === 'otherCount' || policyName === 'smallJsFiles' || policyName === 'smallCssFiles' || policyName === 'smallImages'">
|
||||
<url-link url="offender.file" max-length="100"></url-link>
|
||||
<span ng-if="offender.size || offender.size === 0">({{offender.size}} kB)</span>
|
||||
<div ng-if="policyName === 'lazyLoadableImagesBelowTheFold'">
|
||||
<url-link url="offender" max-length="100"></url-link>
|
||||
<img ng-src="{{offender}}" class="smallPreview checker"></img>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'notFound' || policyName === 'closedConnections' || policyName === 'multipleRequests' || policyName === 'cachingDisabled' || policyName === 'cachingNotSpecified'">
|
||||
@@ -151,23 +164,170 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div ng-if="!rule.offendersObj.list" 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">
|
||||
<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 ng-if="policyName === 'DOMaccessesOnScroll' && rule.offendersObj.children.length > 0">
|
||||
<p>The table below shows the interactions between the JavaScript and the DOM on a scroll event.</p>
|
||||
<div class="table" ng-class="{warningsFilterOn: warningsFilterOn}">
|
||||
<div class="headers">
|
||||
<div><!-- index --></div>
|
||||
<div>Type</div>
|
||||
<div>Params</div>
|
||||
<div><!-- details --></div>
|
||||
</div>
|
||||
<profiler-line ng-repeat="node in rule.offendersObj.children" data-index="$index" node="node"></profiler-line>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'cssColors' && rule.offendersObj.count > 0">
|
||||
<p>This is the colors palette, sized by total occurrences:</p>
|
||||
<div class="colorPalette checker">
|
||||
<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" data="weightData" labels="weightLabels" options="weightOptions" colours="weightColours" legend="true"></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="{hugeFile: request.weight > 102400}">{{request.weight | bytes}}</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}}" class="checker" /></a></div>
|
||||
</div>
|
||||
<div>
|
||||
<p ng-if="!image.afterCompression">Current weight: {{image.original | bytes}}</p>
|
||||
<p ng-if="image.afterCompression">Current weight: {{image.original | bytes}} ({{image.afterCompression | bytes}} gzipped)</p>
|
||||
<p ng-if="image.lossless && image.afterOptimizationAndCompression">With a lossless optimization:<br/>{{image.afterOptimizationAndCompression | bytes}} gzipped (<b>-{{image.gain | bytes}}</b> gzipped)</p>
|
||||
<p ng-if="image.lossless && !image.afterOptimizationAndCompression">With a lossless optimization:<br/>{{image.lossless | bytes}} <span ng-if="!image.lossy">(<b>-{{image.gain | bytes}}</b>)</span></p>
|
||||
<p ng-if="image.lossy && image.afterOptimizationAndCompression">With a lossy optimization:<br/>{{image.afterOptimizationAndCompression | bytes}} gzipped (<b>-{{image.gain | bytes}} gzipped</b>)</p>
|
||||
<p ng-if="image.lossy && !image.afterOptimizationAndCompression">With a lossy optimization:<br/>{{image.lossy | bytes}} (<b>-{{image.gain | bytes}}</b>)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'gzipCompression'">
|
||||
<h3 ng-if="rule.value > 0">{{rule.value | bytes}} could be saved on <ng-pluralize count="rule.offendersObj.list.images.length" when="{'one': '1 file', 'other': '{} files'}"></ng-pluralize></h3>
|
||||
<div class="table">
|
||||
<div class="headers">
|
||||
<div>File</div>
|
||||
<div>Current weight</div>
|
||||
<div>Gzipped</div>
|
||||
<div>Gain</div>
|
||||
</div>
|
||||
<div ng-repeat="file in rule.offendersObj.list.files | orderBy:'-gain'">
|
||||
<div>
|
||||
<url-link url="file.url" max-length="70"></url-link>
|
||||
</div>
|
||||
<div>{{file.original | bytes}}</div>
|
||||
<div>{{file.gzipped | 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="70"></url-link>
|
||||
</div>
|
||||
<div ng-if="!file.afterCompression">{{file.original | bytes}} (gzipped)</div>
|
||||
<div ng-if="file.afterCompression">{{file.original | bytes}} ({{file.afterCompression | bytes}} gzipped)</div>
|
||||
<div ng-if="!file.afterCompression">{{file.afterOptimizationAndCompression | bytes}} (gzipped)</div>
|
||||
<div ng-if="file.afterCompression">{{file.optimized | bytes}} ({{file.afterOptimizationAndCompression | bytes}} gzipped)</div>
|
||||
<div><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 === 'smallRequests'">
|
||||
<div ng-repeat="(type, requests) in rule.offendersObj.list.byType">
|
||||
<h3><ng-pluralize count="requests.length" when="{'0': 'small ' + type + ' file', 'one': '1 small ' + type + ' file', 'other': '{} small ' + type + ' files'}"></ng-pluralize></h3>
|
||||
<p ng-if="type == 'css' && requests.length > 0">Try to inline these styles in the head of the HTML or to merge them with other files.</p>
|
||||
<p ng-if="type == 'js' && requests.length > 0">Try to inline these scripts in the HTML or merge them with other files.</p>
|
||||
<p ng-if="type == 'image' && requests.length > 0">Try to inline these images (with base64 encoding for most image types except SVG that don't need base64 encoding). You can also create sprites.</p>
|
||||
<div class="table">
|
||||
<div class="headers">
|
||||
<div ng-if="type == 'image'">Preview</div>
|
||||
<div>File</div>
|
||||
<div>Weight (bytes)</div>
|
||||
</div>
|
||||
<div ng-repeat="request in requests track by $index">
|
||||
<div ng-if="type == 'image'"><img ng-src="{{request.url}}" class="smallPreview checker" /></div>
|
||||
<div><url-link url="request.url" max-length="100"></url-link></div>
|
||||
<div>{{request.size}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'DOMaccesses'">
|
||||
<h3>{{rule.value}} offenders</h3>
|
||||
Please open the <a href="/result/{{runId}}/timeline">JS timeline</a>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'queriesWithoutResults'">
|
||||
<h3>{{rule.value}} offenders</h3>
|
||||
Please open the <a href="/result/{{runId}}/timeline#filter=queryWithoutResults">JS timeline, filtered by "Queries without results"</a>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'jQueryCallsOnEmptyObject'">
|
||||
<h3>{{rule.value}} offenders</h3>
|
||||
Please open the <a href="/result/{{runId}}/timeline#filter=jQueryCallOnEmptyObject">JS timeline, filtered by "jQuery calls on empty object"</a>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'jQueryNotDelegatedEvents'">
|
||||
<h3>{{rule.value}} offenders</h3>
|
||||
Please open the <a href="/result/{{runId}}/timeline#filter=eventNotDelegated">JS timeline, filtered by "Events not delegated"</a>
|
||||
</div>
|
||||
|
||||
<div ng-if="!rule && rule !== null" class="notFound">
|
||||
<h2>404</h2>
|
||||
Rule "{{policyName}}"" not found
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
<div class="screenshot board">
|
||||
<h2>Screenshot</h2>
|
||||
|
||||
<div class="screenshotWrapper desktop">
|
||||
<div class="screenshotWrapper" ng-class="result.params.options.device || 'desktop'">
|
||||
<div>
|
||||
<img class="screenshotImage" ng-src="{{result.screenshotUrl}}"/>
|
||||
<img ng-if="result.screenshotUrl" class="screenshotImage" ng-src="{{result.screenshotUrl}}"/>
|
||||
<span ng-if="!result.screenshotUrl" class="screenshotError">Screenshot not available</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<h2>Javascript Timeline</h2>
|
||||
<p>This graph gives a quick view of when the Javascript interactions with the DOM occur during the loading of the page.</p>
|
||||
<h2>JavaScript Timeline</h2>
|
||||
<p>This graph gives a quick view of when the JavaScript interactions with the DOM occur during the loading of the page.</p>
|
||||
|
||||
<div class="timeline">
|
||||
<div class="chart">
|
||||
@@ -16,14 +16,14 @@
|
||||
<div ng-repeat="duration in timeline track by $index"
|
||||
class="interval"
|
||||
ng-class="{
|
||||
domCreation: $index * timelineIntervalDuration < result.toolsResults.phantomas.metrics.domInteractive,
|
||||
domInteractive: $index * timelineIntervalDuration >= result.toolsResults.phantomas.metrics.domInteractive
|
||||
&& $index * timelineIntervalDuration < result.toolsResults.phantomas.metrics.domContentLoaded,
|
||||
domContentLoaded: $index * timelineIntervalDuration >= result.toolsResults.phantomas.metrics.domContentLoaded
|
||||
&& $index * timelineIntervalDuration < result.toolsResults.phantomas.metrics.domContentLoadedEnd,
|
||||
domContentLoadedEnd: $index * timelineIntervalDuration >= result.toolsResults.phantomas.metrics.domContentLoadedEnd
|
||||
&& $index * timelineIntervalDuration < result.toolsResults.phantomas.metrics.domComplete,
|
||||
domComplete: $index * timelineIntervalDuration >= result.toolsResults.phantomas.metrics.domComplete
|
||||
domCreation: $index * timelineIntervalDuration < result.javascriptExecutionTree.data.domInteractive,
|
||||
domInteractive: $index * timelineIntervalDuration >= result.javascriptExecutionTree.data.domInteractive
|
||||
&& $index * timelineIntervalDuration < result.javascriptExecutionTree.data.domContentLoaded,
|
||||
domContentLoaded: $index * timelineIntervalDuration >= result.javascriptExecutionTree.data.domContentLoaded
|
||||
&& $index * timelineIntervalDuration < result.javascriptExecutionTree.data.domContentLoadedEnd,
|
||||
domContentLoadedEnd: $index * timelineIntervalDuration >= result.javascriptExecutionTree.data.domContentLoadedEnd
|
||||
&& $index * timelineIntervalDuration < result.javascriptExecutionTree.data.domComplete,
|
||||
domComplete: $index * timelineIntervalDuration >= result.javascriptExecutionTree.data.domComplete
|
||||
}">
|
||||
<div style="height: {{100 * duration / timelineMax | number: 0}}px" class="color" ng-class="{clickable: duration > 0}" scroll-on-click="{{$index * timelineIntervalDuration}}"></div>
|
||||
<div class="tooltip detailsOverlay">
|
||||
@@ -45,7 +45,7 @@
|
||||
<div class="domComplete"><div class="color"></div>Page is complete</div>
|
||||
</div>
|
||||
<div class="tips">
|
||||
<div>Executing Javascript and DOM queries here is a <b>bad practice</b> and slows down the DOM construction.</div>
|
||||
<div>Executing JavaScript and DOM queries here is a <b>bad practice</b> and slows down the DOM construction.</div>
|
||||
<div>Some frameworks do things here, but it's not reliable and should be avoided.</div>
|
||||
<div>Also known as "document ready". This is where you should execute <b>top-priority</b> scripts, like binding action buttons or launch a video player.</div>
|
||||
<div>Here you can execute <b>mid-priority</b> tasks. Loading a script with createElement('script') is one way to do so.</div>
|
||||
@@ -54,17 +54,36 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Javascript Profiler</h2>
|
||||
<h2>JavaScript Profiler</h2>
|
||||
<p>
|
||||
The table below shows the interactions between Javascript and the DOM. It is useful to understand what happens while the page loads.
|
||||
The table below shows the interactions between the JavaScript and the DOM. It is useful to understand what happens while the page loads.
|
||||
</p>
|
||||
<div class="filters">
|
||||
<div>
|
||||
<input type="checkbox" ng-model="warningsFilterOn" id="warningsFilterOn" />
|
||||
<label for="warningsFilterOn">Show warnings only</label>
|
||||
<input type="checkbox" name="warningsFilter" ng-model="warningsFilterOn" id="warningsFilterOn" />
|
||||
<label for="warningsFilterOn">Filter on warnings and errors</label>
|
||||
<div class="subFilters" ng-if="warningsFilterOn">
|
||||
<div>
|
||||
<input type="checkbox" name="filters" ng-model="warningsFilters.queryWithoutResults" id="queryWithoutResultsFilterOn"/>
|
||||
<label for="queryWithoutResultsFilterOn">Queries without results</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" name="filters" ng-model="warningsFilters.jQueryCallOnEmptyObject" id="jQueryCallOnEmptyObjectFilterOn" />
|
||||
<label for="jQueryCallOnEmptyObjectFilterOn">jQuery calls on empty object</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" name="filters" ng-model="warningsFilters.eventNotDelegated" id="eventNotDelegatedFilterOn" />
|
||||
<label for="eventNotDelegatedFilterOn">Events not delegated</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" name="filters" ng-model="warningsFilters.jsError" id="jsErrorFilterOn" />
|
||||
<label for="jsErrorFilterOn">Errors</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{queryWithoutResultsFilterOn}}
|
||||
</div>
|
||||
<div class="table" ng-class="{warningsFilterOn: warningsFilterOn}">
|
||||
<div class="table" ng-class="{queryWithoutResultsFilterOn: warningsFilterOn && warningsFilters.queryWithoutResults, jQueryCallOnEmptyObjectFilterOn: warningsFilterOn && warningsFilters.jQueryCallOnEmptyObject, eventNotDelegatedFilterOn: warningsFilterOn && warningsFilters.eventNotDelegated, jsErrorFilterOn: warningsFilterOn && warningsFilters.jsError}">
|
||||
<div class="headers">
|
||||
<div><!-- index --></div>
|
||||
<div>Type</div>
|
||||
|
||||
@@ -43,7 +43,7 @@ var policies = {
|
||||
"message": "<p>IDs of HTML elements must be document-wide unique. This can cause problems with getElementById returning the wrong element.</p>",
|
||||
"isOkThreshold": 0,
|
||||
"isBadThreshold": 5,
|
||||
"isAbnormalThreshold": 10,
|
||||
"isAbnormalThreshold": 50,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
return {
|
||||
@@ -66,73 +66,23 @@ var policies = {
|
||||
};
|
||||
}
|
||||
},
|
||||
"DOMinserts": {
|
||||
"tool": "phantomas",
|
||||
"label": "DOM inserts",
|
||||
"message": "<p>Working with the DOM in JavaScript triggers layout calculations and slows down the page.</p><p>Try, as much as possible, to have an HTML page fully generated by the server instead of making changes with JS.</p>",
|
||||
"isOkThreshold": 10,
|
||||
"isBadThreshold": 400,
|
||||
"isAbnormalThreshold": 1000,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
return {
|
||||
count: offenders.length,
|
||||
list: offenders.map(function(offender) {
|
||||
var parts = /^"(.*)" ?appended ?to ?"(.*)"$/.exec(offender);
|
||||
|
||||
if (!parts) {
|
||||
debug('DOMinserts offenders transform function error with "%s"', offender);
|
||||
return {
|
||||
parseError: offender
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
insertedElement: offendersHelpers.domPathToDomElementObj(parts[1]),
|
||||
receiverElement: offendersHelpers.domPathToDomElementObj(parts[2])
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
},
|
||||
"DOMqueries": {
|
||||
"tool": "phantomas",
|
||||
"label": "DOM queries",
|
||||
"message": "<p>DOM queries are like looking in a large catalog of items. Even if the browsers made progress on the performances of queries, websites often make hundreds of them.</p><p>Try to reduce the number of queries by refactoring your JavaScript code.</p><p>Avoid also to have a read query between two write queries. To be able to reduce the number repaints and optimize performances, browsers buffer the DOM writing operations and treat them in bulk. But each time a DOM reading is asked, the browser needs to empty the buffer. This can be particularly slow inside a loop.</p>",
|
||||
"DOMaccesses": {
|
||||
"tool": "jsExecutionTransformer",
|
||||
"label": "DOM access",
|
||||
"message": "<p>This metric counts the number of calls to DOM related functions (both native DOM functions and jQuery functions) on page load.</p><p>The more your JavaScript code accesses the DOM, the slower the page will load.</p><p>Try, as much as possible, to have an HTML page fully generated by the server instead of making changes with JS.</p><p>Try to reduce the number of queries by refactoring your JavaScript code.</p><p>Binding too many events also has a cost. Try to use <a href=\"https://learn.jquery.com/events/event-delegation/\" target=\"_blank\">event delegation</a> as much as possible.</p>",
|
||||
"isOkThreshold": 50,
|
||||
"isBadThreshold": 1000,
|
||||
"isAbnormalThreshold": 2000,
|
||||
"isBadThreshold": 1500,
|
||||
"isAbnormalThreshold": 3000,
|
||||
"hasOffenders": false
|
||||
},
|
||||
"DOMqueriesWithoutResults": {
|
||||
"tool": "phantomas",
|
||||
"label": "DOM queries without result",
|
||||
"message": "<p>Number of queries that return no result.</p><p>It suggests the query is not used on the page, probably because it is some dead code.</p><p>Or maybe the code is trying to find an HTML block that is not always here. Look at the JS Timeline to see if the scripts correctly figures out the HTML block is not here and immediatly stops interacting further with the DOM.</p>",
|
||||
"queriesWithoutResults": {
|
||||
"tool": "jsExecutionTransformer",
|
||||
"label": "Queries without result",
|
||||
"message": "<p>Number of queries that return no result. Both native and jQuery DOM requests are counted.</p><p>It suggests the query is not used on the page, probably because it is some dead code.</p><p>Or maybe the code is trying to find an HTML block that is not always here. Look at the JS Timeline to see if the scripts correctly figures out the HTML block is not here and immediatly stops interacting further with the DOM.</p>",
|
||||
"isOkThreshold": 0,
|
||||
"isBadThreshold": 100,
|
||||
"isAbnormalThreshold": 200,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
return {
|
||||
count: offenders.length,
|
||||
list: offenders.map(function(offender) {
|
||||
var parts = /^(.*) ?\(in ?(.*)\) ?using ?(.*)$/.exec(offender);
|
||||
|
||||
if (!parts) {
|
||||
debug('DOMqueriesWithoutResults offenders transform function error with "%s"', offender);
|
||||
return {
|
||||
parseError: offender
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
query: parts[1],
|
||||
context: offendersHelpers.domPathToDomElementObj(parts[2]),
|
||||
fn: parts[3]
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
"hasOffenders": false
|
||||
},
|
||||
"DOMqueriesAvoidable": {
|
||||
"tool": "phantomas",
|
||||
@@ -166,35 +116,49 @@ var policies = {
|
||||
};
|
||||
}
|
||||
},
|
||||
"eventsBound": {
|
||||
"eventsScrollBound": {
|
||||
"tool": "phantomas",
|
||||
"label": "Events bound",
|
||||
"message": "<p>Binding too many events has a cost.</p><p>It can be avoided by using \"event delegation\". Instead of binding events on each element one by one, events delegation binds them on the top level document element and uses the bubbling principle. It will imperceptibly slow down the event when it occurs, but the loading of the page will speed-up.</p>",
|
||||
"isOkThreshold": 100,
|
||||
"isBadThreshold": 800,
|
||||
"isAbnormalThreshold": 1500,
|
||||
"label": "Scroll events bound",
|
||||
"message": "<p>Number of 'scroll' event listeners binded to 'window' or 'document'.</p><p>Asking too much work to the browser on scroll hurts the smoothness of the scroll. Merging all your event listeners into an unique listener can help you factorize their code and reduce their footprint on scroll.</p>",
|
||||
"isOkThreshold": 1,
|
||||
"isBadThreshold": 7,
|
||||
"isAbnormalThreshold": 15,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
return {
|
||||
count: offenders.length,
|
||||
list: offenders.map(function(offender) {
|
||||
var parts = /^"(.*)" ?bound ?to ?"(.*)"$/.exec(offender);
|
||||
var parts = /^bound by (.*) on ([^ ]+)$/.exec(offender);
|
||||
|
||||
if (!parts) {
|
||||
debug('eventsBound offenders transform function error with "%s"', offender);
|
||||
debug('eventsScrollBound offenders transform function error with "%s"', offender);
|
||||
return {
|
||||
parseError: offender
|
||||
};
|
||||
}
|
||||
|
||||
var backtraceArray = offendersHelpers.backtraceToArray(parts[1]);
|
||||
|
||||
return {
|
||||
eventName: parts[1],
|
||||
element: offendersHelpers.domPathToDomElementObj(parts[2])
|
||||
backtrace: backtraceArray || [],
|
||||
target: parts[2]
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
},
|
||||
"DOMaccessesOnScroll": {
|
||||
"tool": "jsExecutionTransformer",
|
||||
"label": "DOM access on scroll",
|
||||
"message": "<p>This rule counts the number of DOM-accessing functions calls on a scroll event, such as queries, readings, writings, bindings and jQuery functions.</p><p>Two scroll events are triggered quickly, one after the other, and only the second one is analyzed so throttled functions are ignored.</p><p>One of the main reasons of a poor scrolling experience is when too much JS is executed on each scroll event. Note that some devices such as smartphones and MacBooks send more scroll events than others.</p><p>Reduce the number of DOM accesses inside scroll listeners. Put DOM queries outside them when possible. Use <a href=\"http://blogorama.nerdworks.in/javascriptfunctionthrottlingan/\" target=\"_blank\">throttling or deboucing</a>.</p>",
|
||||
"isOkThreshold": 1,
|
||||
"isBadThreshold": 12,
|
||||
"isAbnormalThreshold": 25,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
return offenders;
|
||||
}
|
||||
},
|
||||
"jsErrors": {
|
||||
"tool": "phantomas",
|
||||
"label": "JavaScript errors",
|
||||
@@ -226,15 +190,6 @@ var policies = {
|
||||
};
|
||||
}
|
||||
},
|
||||
"evalCalls": {
|
||||
"tool": "phantomas",
|
||||
"label": "eval calls",
|
||||
"message": "<p>The 'eval' function is slow and is a bad coding practice. Try to get rid of it.</p>",
|
||||
"isOkThreshold": 0,
|
||||
"isBadThreshold": 10,
|
||||
"isAbnormalThreshold": 20,
|
||||
"hasOffenders": false
|
||||
},
|
||||
"documentWriteCalls": {
|
||||
"tool": "phantomas",
|
||||
"label": "document.write calls",
|
||||
@@ -292,16 +247,16 @@ var policies = {
|
||||
"message": "<p>Try to keep your console clean when in production. Debugging is good for development only.</p><p>Writing in the console has a cost, especially when dumping large object variables.</p><p>There is also a problem with Internet Explorer 8, not knowing the console object.</p>",
|
||||
"isOkThreshold": 0,
|
||||
"isBadThreshold": 10,
|
||||
"isAbnormalThreshold": 25,
|
||||
"isAbnormalThreshold": 50,
|
||||
"hasOffenders": false
|
||||
},
|
||||
"globalVariables": {
|
||||
"tool": "phantomas",
|
||||
"label": "Global variables",
|
||||
"message": "<p>It is a bad practice because they clutter up the global namespace. If two scripts use the same variable name in the global scope, it can cause conflicts and it is generally hard to debug.</p><p>Global variables also take a (very) little bit longer to be accessed than variables in the local scope of a function.</p>",
|
||||
"isOkThreshold": 10,
|
||||
"isBadThreshold": 50,
|
||||
"isAbnormalThreshold": 200,
|
||||
"isOkThreshold": 30,
|
||||
"isBadThreshold": 150,
|
||||
"isAbnormalThreshold": 700,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
return {
|
||||
@@ -337,17 +292,17 @@ var policies = {
|
||||
score = 70;
|
||||
} else if (value.indexOf('1.8.') === 0) {
|
||||
score = 50;
|
||||
} else if (value.indexOf('1.7.') === 0) {
|
||||
} else if (value.indexOf('1.7') === 0) {
|
||||
score = 40;
|
||||
} else if (value.indexOf('1.6.') === 0) {
|
||||
} else if (value.indexOf('1.6') === 0) {
|
||||
score = 30;
|
||||
} else if (value.indexOf('1.5.') === 0) {
|
||||
} else if (value.indexOf('1.5') === 0) {
|
||||
score = 20;
|
||||
} else if (value.indexOf('1.4.') === 0) {
|
||||
} else if (value.indexOf('1.4') === 0) {
|
||||
score = 10;
|
||||
} else if (value.indexOf('1.3.') === 0) {
|
||||
} else if (value.indexOf('1.3') === 0) {
|
||||
score = 0;
|
||||
} else if (value.indexOf('1.2.') === 0) {
|
||||
} else if (value.indexOf('1.2') === 0) {
|
||||
score = 0;
|
||||
} else {
|
||||
debug('Unknown jQuery version "%s"', value);
|
||||
@@ -371,39 +326,88 @@ var policies = {
|
||||
},
|
||||
"jQueryVersionsLoaded": {
|
||||
"tool": "phantomas",
|
||||
"label": "Several versions loaded",
|
||||
"label": "Several jQuery loaded",
|
||||
"message": "<p>jQuery is a heavy library. You should <b>never</b> load jQuery more than once on the same page.</p>",
|
||||
"isOkThreshold": 1,
|
||||
"isBadThreshold": 2,
|
||||
"isAbnormalThreshold": 2,
|
||||
"hasOffenders": true
|
||||
},
|
||||
"jQueryFunctionsUsed": {
|
||||
"tool": "jsExecutionTransformer",
|
||||
"label": "jQuery usage",
|
||||
"message": "<p>This is the number of different core jQuery functions called on load. This rule is not trying to blame you for using jQuery too much, but the opposite.</p><p>If only a few functions are used, why not trying to get rid of jQuery? Have a look at <a href=\"http://youmightnotneedjquery.com/\" target=\"_blank\">http://youmightnotneedjquery.com</a>.</p>",
|
||||
"isOkThreshold": 15,
|
||||
"isBadThreshold": 6,
|
||||
"isAbnormalThreshold": 0,
|
||||
"hasOffenders": true
|
||||
},
|
||||
"jQueryCallsOnEmptyObject": {
|
||||
"tool": "jsExecutionTransformer",
|
||||
"label": "Calls on empty objects",
|
||||
"message": "<p>This metric counts the number of jQuery functions called on an empty jQuery object. The call was useless.</p><p>This can be helpful to detect dead or unused code.</p>",
|
||||
"isOkThreshold": 1,
|
||||
"isBadThreshold": 100,
|
||||
"isAbnormalThreshold": 180,
|
||||
"hasOffenders": false
|
||||
},
|
||||
"jQueryNotDelegatedEvents": {
|
||||
"tool": "jsExecutionTransformer",
|
||||
"label": "Events not delegated",
|
||||
"message": "<p>This is the number of events that are bound with the .bind() or the .on() function without using <a href=\"https://learn.jquery.com/events/event-delegation/\" target=\"_blank\">event delegation</a>.</p><p>This means jQuery binds each element contained in the object one by one. This is bad for performance.</p>",
|
||||
"isOkThreshold": 1,
|
||||
"isBadThreshold": 100,
|
||||
"isAbnormalThreshold": 180,
|
||||
"hasOffenders": false
|
||||
},
|
||||
"cssParsingErrors": {
|
||||
"tool": "phantomas",
|
||||
"label": "CSS syntax error",
|
||||
"message": "<p>Yellow Lab Tools failed to parse a CSS file. I doubt the problem comes from the css parser.</p><p>Maybe a <a href=\"http://jigsaw.w3.org/css-validator\" target=\"_blank\">CSS validator</a> can help you.</p>",
|
||||
"isOkThreshold": 0,
|
||||
"isBadThreshold": 1,
|
||||
"isAbnormalThreshold": 1,
|
||||
"isAbnormalThreshold": 20,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
return {
|
||||
count: offenders.length,
|
||||
list: offenders.map(function(offender) {
|
||||
var parts = /^(?:(?:<([^ \(]*)>|\[inline CSS\]) ?)?(?:\((((?! @ ).)*)(?: @ (\d+):(\d+))?\))?$/.exec(offender);
|
||||
|
||||
if (!parts) {
|
||||
debug('cssParsingErrors offenders transform function error with "%s"', offender);
|
||||
if (offender === '[inline CSS] (Empty CSS was provided)') {
|
||||
return {
|
||||
parseError: offender
|
||||
error: 'Empty style tag',
|
||||
file: null,
|
||||
line: null,
|
||||
column: null
|
||||
};
|
||||
}
|
||||
|
||||
var parts = /^(?:(?:<([^ \(]*)>|\[inline CSS\]) ?)?(?:\((((?! @ ).)*)(?: @ (\d+):(\d+))?\))?$/.exec(offender);
|
||||
|
||||
if (parts) {
|
||||
return {
|
||||
error: parts[2] || 'Unknown parsing error' + (parts[1] ? '. The entire file was ignored. As a result, the other CSS metrics and scores are miscalculated.' : ''),
|
||||
file: parts[1] || null,
|
||||
line: (parts[4] && parts[5]) ? parseInt(parts[4], 10) : null,
|
||||
column: (parts[4] && parts[5]) ? parseInt(parts[5], 10) : null
|
||||
};
|
||||
}
|
||||
|
||||
// Try another syntax
|
||||
parts = /^(.*) <(.*)> @ (\d+):(\d+)$/.exec(offender);
|
||||
|
||||
if (parts) {
|
||||
return {
|
||||
error: parts[1] || 'Unknown parsing error',
|
||||
file: parts[2] || null,
|
||||
line: parseInt(parts[3], 10),
|
||||
column: parseInt(parts[4], 10)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
debug('cssParsingErrors offenders transform function error with "%s"', offender);
|
||||
return {
|
||||
error: parts[2],
|
||||
file: parts[1] || null,
|
||||
line: (parts[4] && parts[5]) ? parseInt(parts[4], 10) : null,
|
||||
column: (parts[4] && parts[5]) ? parseInt(parts[5], 10) : null
|
||||
parseError: offender
|
||||
};
|
||||
})
|
||||
};
|
||||
@@ -413,9 +417,9 @@ var policies = {
|
||||
"tool": "phantomas",
|
||||
"label": "Rules count",
|
||||
"message": "<p>Having a huge number of CSS rules hurts performances. If the number of CSS rules is higher than the number of DOM elements, there is clearly a problem.</p><p>Huge stylesheets generally occur when the different pages of a website load all the CSS, concatenated in a single stylesheet, even if a large part of the rules are page-specific. Solution is to create one main CSS file with global rules and one custom file per page.</p>",
|
||||
"isOkThreshold": 500,
|
||||
"isBadThreshold": 2500,
|
||||
"isAbnormalThreshold": 4000,
|
||||
"isOkThreshold": 750,
|
||||
"isBadThreshold": 3000,
|
||||
"isAbnormalThreshold": 4500,
|
||||
"hasOffenders": false
|
||||
},
|
||||
"cssComplexSelectors": {
|
||||
@@ -423,7 +427,7 @@ var policies = {
|
||||
"label": "Complex selectors",
|
||||
"message": "<p>Complex selectors are CSS selectors with 4 or more expressions, like \"#header ul li .foo\".</p><p>They are adding more work for the browser, and this could be avoided by simplifying selectors.</p>",
|
||||
"isOkThreshold": 0,
|
||||
"isBadThreshold": 500,
|
||||
"isBadThreshold": 600,
|
||||
"isAbnormalThreshold": 2000,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
@@ -441,8 +445,8 @@ var policies = {
|
||||
"label": "Complex attributes selector",
|
||||
"message": "<p>Complex attributes selectors are one of these:<ul><li>.foo[type*=bar] (contains bar)</li><li>.foo[type^=bar] (starts with bar)</li><li>.foo[type|=bar] (starts with bar or bar-)</li><li>.foo[type$=bar] (ends with bar)</li><li>.foo[type~=bar baz] (bar or baz)</li></ul></p><p>Their matching process needs more CPU and it has a cost on performances.</p>",
|
||||
"isOkThreshold": 0,
|
||||
"isBadThreshold": 50,
|
||||
"isAbnormalThreshold": 100,
|
||||
"isBadThreshold": 75,
|
||||
"isAbnormalThreshold": 150,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
return {
|
||||
@@ -526,8 +530,8 @@ var policies = {
|
||||
"label": "Duplicated selectors",
|
||||
"message": "<p>This is when two or more selectors are strictly identical and should be merged.</p>",
|
||||
"isOkThreshold": 0,
|
||||
"isBadThreshold": 40,
|
||||
"isAbnormalThreshold": 80,
|
||||
"isBadThreshold": 50,
|
||||
"isAbnormalThreshold": 100,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
return {
|
||||
@@ -555,8 +559,8 @@ var policies = {
|
||||
"label": "Duplicated properties",
|
||||
"message": "<p>This is the number of property definitions duplicated within a selector.</p>",
|
||||
"isOkThreshold": 0,
|
||||
"isBadThreshold": 50,
|
||||
"isAbnormalThreshold": 100,
|
||||
"isBadThreshold": 60,
|
||||
"isAbnormalThreshold": 120,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
return {
|
||||
@@ -589,7 +593,7 @@ var policies = {
|
||||
"label": "Empty rules",
|
||||
"message": "<p>Very easy to fix: remove all empty rules.</p>",
|
||||
"isOkThreshold": 0,
|
||||
"isBadThreshold": 40,
|
||||
"isBadThreshold": 50,
|
||||
"isAbnormalThreshold": 100,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
@@ -643,8 +647,8 @@ var policies = {
|
||||
"label": "Uses of !important",
|
||||
"message": "<p>It can be useful, but only as a last resort. It is a bad practice because it overrides the normal cascading logic. The more you use !important, the more you need it again to over-override. This conducts to a poor maintainability.</p>",
|
||||
"isOkThreshold": 0,
|
||||
"isBadThreshold": 50,
|
||||
"isAbnormalThreshold": 150,
|
||||
"isBadThreshold": 75,
|
||||
"isAbnormalThreshold": 200,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
return {
|
||||
@@ -678,7 +682,7 @@ var policies = {
|
||||
"label": "Old IE fixes",
|
||||
"message": "<p>What browser do you need to support? Once you've got the answer, take a look at these old rules that pollute your CSS code and remove them.</p><p>IE6:<ul><li>* html</li><li>html > body (everything but IE6)</li></ul><p><p>IE7:<ul><li><b>*</b>height: 123px;</li><li>height: 123px <b>!ie</b>;</li></ul><p><p>IE9:<ul><li>-ms-filter</li><li>progid:DXImageTransform.Microsoft</li></ul></p>",
|
||||
"isOkThreshold": 0,
|
||||
"isBadThreshold": 50,
|
||||
"isBadThreshold": 75,
|
||||
"isAbnormalThreshold": 300,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
@@ -735,7 +739,7 @@ var policies = {
|
||||
"label": "Old prefixes",
|
||||
"message": "<p>Many property prefixes such as -moz- or -webkit- are not needed anymore, or by very few people. You can remove them or replace them with the non-prefixed version. This will help reducing your stylesheets weight.</p>",
|
||||
"isOkThreshold": 0,
|
||||
"isBadThreshold": 50,
|
||||
"isBadThreshold": 75,
|
||||
"isAbnormalThreshold": 300,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
@@ -771,7 +775,7 @@ var policies = {
|
||||
"label": "Universal selectors",
|
||||
"message": "<p>Universal selectors are the most expensive CSS selectors.</p><p>More informations <a href=\"http://perfectionkills.com/profiling-css-for-fun-and-profit-optimization-notes/\" target=\"_blank\">here</a>.</p>",
|
||||
"isOkThreshold": 0,
|
||||
"isBadThreshold": 40,
|
||||
"isBadThreshold": 50,
|
||||
"isAbnormalThreshold": 150,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
@@ -789,7 +793,7 @@ var policies = {
|
||||
"label": "Redundant body selectors",
|
||||
"message": "<p>This is one way to remove complexity from a CSS rule. Generally, when \"body\" is specified in a rule it can be removed, because an element is necessarily inside the body.</p>",
|
||||
"isOkThreshold": 0,
|
||||
"isBadThreshold": 50,
|
||||
"isBadThreshold": 60,
|
||||
"isAbnormalThreshold": 200,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
@@ -810,7 +814,7 @@ var policies = {
|
||||
"label": "Redundant tags selectors",
|
||||
"message": "<p>Some tags included inside other tags are obvious. For example, when \"ul li\" is specified in a rule, \"ul\" can be removed because the \"li\" element is <b>always</b> inside a \"ul\". Same thing for \"tr td\", \"select option\", ...</p><p>Lowering compexity in CSS selectors can make the page load a little faster.</p>",
|
||||
"isOkThreshold": 0,
|
||||
"isBadThreshold": 50,
|
||||
"isBadThreshold": 60,
|
||||
"isAbnormalThreshold": 200,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
@@ -839,211 +843,81 @@ var policies = {
|
||||
};
|
||||
}
|
||||
},
|
||||
"requests": {
|
||||
"tool": "phantomas",
|
||||
"label": "Total requests number",
|
||||
"totalWeight": {
|
||||
"tool": "weightChecker",
|
||||
"label": "Total weight",
|
||||
"message": "<p>The weight is of course very important if you want the page to load fast. Try to stay under 1MB, which is alreay very long to download over a slow connection.</p>",
|
||||
"isOkThreshold": 716800,
|
||||
"isBadThreshold": 2097152,
|
||||
"isAbnormalThreshold": 3145728,
|
||||
"hasOffenders": true,
|
||||
"unit": 'bytes'
|
||||
},
|
||||
"imageOptimization": {
|
||||
"tool": "weightChecker",
|
||||
"label": "Image optimization",
|
||||
"message": "<p>This metric mesures 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\">ImageOption</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": 10240,
|
||||
"isBadThreshold": 122880,
|
||||
"isAbnormalThreshold": 307200,
|
||||
"hasOffenders": true,
|
||||
"unit": 'bytes'
|
||||
},
|
||||
"gzipCompression": {
|
||||
"tool": "weightChecker",
|
||||
"label": "Gzip compression",
|
||||
"message": "<p>Mesures the number of bytes that could be saved by compressing file transfers.</p><p>Gzip is a powerfull weight reducer and should be enabled on text-based assets in your server's configuration. Note that gzipping small files (< 1 KB) is arguable, and that some assets such as images should not be gzipped as they are already compressed. <a href=\"https://gist.github.com/gmetais/971ce13a1fbeebd88445\" target=\"_blank\">Here</a> is a list of Content-Types that should be gzipped.</p>",
|
||||
"isOkThreshold": 5125,
|
||||
"isBadThreshold": 81920,
|
||||
"isAbnormalThreshold": 153600,
|
||||
"hasOffenders": true,
|
||||
"unit": 'bytes'
|
||||
},
|
||||
"fileMinification": {
|
||||
"tool": "weightChecker",
|
||||
"label": "File minification",
|
||||
"message": "<p>This is the weight that could be saved if all text resources were correctly minified.</p><p>The tools in use here are <b>UglifyJS</b>, <b>clean-css</b> and <b>HTMLMinifier</b>. These tools are so good that some of your minified files can be marked as unminified. Change your tool it this happens :)</p><p>The gains of minification are generally small, but the impact can be high when these text files are loaded on the critical path.</p>",
|
||||
"isOkThreshold": 5125,
|
||||
"isBadThreshold": 61440,
|
||||
"isAbnormalThreshold": 122880,
|
||||
"hasOffenders": true,
|
||||
"unit": 'bytes'
|
||||
},
|
||||
"totalRequests": {
|
||||
"tool": "weightChecker",
|
||||
"label": "Requests number",
|
||||
"message": "<p>This is one of the most important performance rule. Every request is slowing down the page loading.</p><p>There are several technics to reduce their number:<ul><li>Concatenate JS files</li><li>Concatenate CSS files</li><li>Embed or inline small JS or CSS files in the HTML</li><li>Create sprites or icon fonts</li><li>Base64 encode small images in HTML or stylesheets</li><li>Use lazyloading for images</li></ul></p>",
|
||||
"isOkThreshold": 15,
|
||||
"isBadThreshold": 100,
|
||||
"isAbnormalThreshold": 200,
|
||||
"hasOffenders": true,
|
||||
"takeOffendersFrom": ["htmlCount", "jsCount", "cssCount", "imageCount", "webfontCount", "videoCount", "jsonCount", "jsonCount"],
|
||||
"offendersTransformFn": function(offenders) {
|
||||
return {
|
||||
count: offenders.length,
|
||||
list: offenders
|
||||
.map(function(offender) {
|
||||
return offendersHelpers.fileWithSizePattern(offender);
|
||||
}).sort(function(a, b) {
|
||||
return b.size - a.size;
|
||||
})
|
||||
};
|
||||
}
|
||||
"isAbnormalThreshold": 180,
|
||||
"hasOffenders": true
|
||||
},
|
||||
"htmlCount": {
|
||||
"domains": {
|
||||
"tool": "phantomas",
|
||||
"label": "Document count",
|
||||
"message": "<p>The number of HTML pages requests, HTML fragments or iframes.</p>",
|
||||
"label": "Different domains",
|
||||
"message": "<p>For each domain met, the browser needs to make a DNS look-up, which is slow. Avoid having to many different domains and the page should render faster.</p><p>By the way, domain sharding is not a good practice anymore.</p>",
|
||||
"isOkThreshold": 10,
|
||||
"isBadThreshold": 20,
|
||||
"isAbnormalThreshold": 30,
|
||||
"isBadThreshold": 25,
|
||||
"isAbnormalThreshold": 50,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
return {
|
||||
count: offenders.length,
|
||||
list: offenders.map(function(offender) {
|
||||
return offendersHelpers.fileWithSizePattern(offender);
|
||||
})
|
||||
};
|
||||
}
|
||||
},
|
||||
"jsCount": {
|
||||
"tool": "phantomas",
|
||||
"label": "Script count",
|
||||
"message": "<p>Reduce the number of scripts by concatenating them.</p>",
|
||||
"isOkThreshold": 5,
|
||||
"isBadThreshold": 15,
|
||||
"isAbnormalThreshold": 30,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
return {
|
||||
count: offenders.length,
|
||||
list: offenders.map(function(offender) {
|
||||
return offendersHelpers.fileWithSizePattern(offender);
|
||||
})
|
||||
};
|
||||
}
|
||||
},
|
||||
"cssCount": {
|
||||
"tool": "phantomas",
|
||||
"label": "CSS count",
|
||||
"message": "<p>Reduce the number of stylesheets by concatenating them.</p>",
|
||||
"isOkThreshold": 3,
|
||||
"isBadThreshold": 10,
|
||||
"isAbnormalThreshold": 22,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
return {
|
||||
count: offenders.length,
|
||||
list: offenders.map(function(offender) {
|
||||
return offendersHelpers.fileWithSizePattern(offender);
|
||||
})
|
||||
};
|
||||
}
|
||||
},
|
||||
"imageCount": {
|
||||
"tool": "phantomas",
|
||||
"label": "Image count",
|
||||
"message": "<p>Reduce the number of images by lazyloading them, by spriting them or by creating an icons font.</p>",
|
||||
"isOkThreshold": 15,
|
||||
"isBadThreshold": 40,
|
||||
"isAbnormalThreshold": 70,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
return {
|
||||
count: offenders.length,
|
||||
list: offenders.map(function(offender) {
|
||||
return offendersHelpers.fileWithSizePattern(offender);
|
||||
})
|
||||
};
|
||||
}
|
||||
},
|
||||
"webfontCount": {
|
||||
"tool": "phantomas",
|
||||
"label": "Font count",
|
||||
"message": "<p>Fonts are loaded on the critical path of the head. Load as few as possible.</p>",
|
||||
"isOkThreshold": 0,
|
||||
"isBadThreshold": 3,
|
||||
"isAbnormalThreshold": 5,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
return {
|
||||
count: offenders.length,
|
||||
list: offenders.map(function(offender) {
|
||||
return offendersHelpers.fileWithSizePattern(offender);
|
||||
})
|
||||
};
|
||||
}
|
||||
},
|
||||
"videoCount": {
|
||||
"tool": "phantomas",
|
||||
"label": "Video count",
|
||||
"message": "<p>The number of videos loaded.</p>",
|
||||
"isOkThreshold": 1,
|
||||
"isBadThreshold": 5,
|
||||
"isAbnormalThreshold": 15,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
return {
|
||||
count: offenders.length,
|
||||
list: offenders.map(function(offender) {
|
||||
return offendersHelpers.fileWithSizePattern(offender);
|
||||
})
|
||||
};
|
||||
}
|
||||
},
|
||||
"jsonCount": {
|
||||
"tool": "phantomas",
|
||||
"label": "JSON count",
|
||||
"message": "<p>The number of AJAX requests to JSON files or webservices.</p>",
|
||||
"isOkThreshold": 2,
|
||||
"isBadThreshold": 10,
|
||||
"isAbnormalThreshold": 25,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
return {
|
||||
count: offenders.length,
|
||||
list: offenders.map(function(offender) {
|
||||
return offendersHelpers.fileWithSizePattern(offender);
|
||||
})
|
||||
};
|
||||
}
|
||||
},
|
||||
"otherCount": {
|
||||
"tool": "phantomas",
|
||||
"label": "Other types of requests",
|
||||
"message": "<p>They can be Flash, XML, music or any unknown format.</p>",
|
||||
"isOkThreshold": 5,
|
||||
"isBadThreshold": 20,
|
||||
"isAbnormalThreshold": 40,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
return {
|
||||
count: offenders.length,
|
||||
list: offenders.map(function(offender) {
|
||||
return offendersHelpers.fileWithSizePattern(offender);
|
||||
})
|
||||
};
|
||||
}
|
||||
},
|
||||
"smallJsFiles": {
|
||||
"tool": "phantomas",
|
||||
"label": "Small JS files",
|
||||
"message": "<p>Number of JS assets smaller than 2 KB that could probably be inlined or merged.</p>",
|
||||
"isOkThreshold": 2,
|
||||
"isBadThreshold": 10,
|
||||
"isAbnormalThreshold": 16,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
return {
|
||||
count: offenders.length,
|
||||
list: offenders.map(function(offender) {
|
||||
return offendersHelpers.fileWithSizePattern(offender);
|
||||
})
|
||||
};
|
||||
}
|
||||
},
|
||||
"smallCssFiles": {
|
||||
"tool": "phantomas",
|
||||
"label": "Small CSS files",
|
||||
"message": "<p>Number of CSS assets smaller than 2 KB that could probably be inlined or merged.</p>",
|
||||
"isOkThreshold": 0,
|
||||
"isBadThreshold": 8,
|
||||
"isAbnormalThreshold": 12,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
return {
|
||||
count: offenders.length,
|
||||
list: offenders.map(function(offender) {
|
||||
return offendersHelpers.fileWithSizePattern(offender);
|
||||
})
|
||||
};
|
||||
}
|
||||
},
|
||||
"smallImages": {
|
||||
"tool": "phantomas",
|
||||
"label": "Small images",
|
||||
"message": "<p>Images smaller than 2 KB that could be base64 encoded or merged into a sprite.</p>",
|
||||
"isOkThreshold": 2,
|
||||
"isBadThreshold": 17,
|
||||
"isAbnormalThreshold": 30,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
return {
|
||||
count: offenders.length,
|
||||
list: offenders.map(function(offender) {
|
||||
return offendersHelpers.fileWithSizePattern(offender);
|
||||
})
|
||||
var parts = /^([^ ]*): (\d+) request\(s\)$/.exec(offender);
|
||||
|
||||
if (!parts) {
|
||||
debug('domains offenders transform function error with "%s"', offender);
|
||||
return {
|
||||
file: offender
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
domain: parts[1],
|
||||
requests: parseInt(parts[2])
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
},
|
||||
@@ -1061,7 +935,7 @@ var policies = {
|
||||
"label": "Connections closed",
|
||||
"message": "<p>This counts the number of requests not keeping the connection alive (specifying \"Connection: close\" in the response headers). It is only counting a request if it is followed by another request on the same domain.</p><p>This is slowing down the next request, because the brower needs to open a new connection to the server, which means an additional round-trip.</p><p>Correct the problem by setting a Keep-Alive header on the guilty server.</p>",
|
||||
"isOkThreshold": 0,
|
||||
"isBadThreshold": 8,
|
||||
"isBadThreshold": 7,
|
||||
"isAbnormalThreshold": 20,
|
||||
"hasOffenders": true
|
||||
},
|
||||
@@ -1074,6 +948,24 @@ var policies = {
|
||||
"isAbnormalThreshold": 10,
|
||||
"hasOffenders": true
|
||||
},
|
||||
"smallRequests": {
|
||||
"tool": "weightChecker",
|
||||
"label": "Small requests",
|
||||
"message": "<p>List of all requests that are less than 2 KB. Try to merge them with other files.</p>",
|
||||
"isOkThreshold": 4,
|
||||
"isBadThreshold": 30,
|
||||
"isAbnormalThreshold": 50,
|
||||
"hasOffenders": true
|
||||
},
|
||||
"lazyLoadableImagesBelowTheFold": {
|
||||
"tool": "phantomas",
|
||||
"label": "Not lazyloaded images",
|
||||
"message": "<p>This is the number of images displayed below the fold that could be lazy-loaded. This is an excellent way to accelerate the loading time of an heavy page.</p><p>I recommend using <a href=\"https://github.com/vvo/lazyload\" target=\"_blank\">this lazyloader</a>.</p>",
|
||||
"isOkThreshold": 1,
|
||||
"isBadThreshold": 12,
|
||||
"isAbnormalThreshold": 30,
|
||||
"hasOffenders": true
|
||||
},
|
||||
"cachingDisabled": {
|
||||
"tool": "phantomas",
|
||||
"label": "Caching disabled",
|
||||
@@ -1150,36 +1042,6 @@ var policies = {
|
||||
})
|
||||
};
|
||||
}
|
||||
},
|
||||
"domains": {
|
||||
"tool": "phantomas",
|
||||
"label": "Different domains",
|
||||
"message": "<p>For each domain met, the browser needs to make a DNS look-up, which is slow. Avoid having to many different domains and the page should render faster.</p><p>By the way, domain sharding is not a good practice anymore.</p>",
|
||||
"isOkThreshold": 10,
|
||||
"isBadThreshold": 25,
|
||||
"isAbnormalThreshold": 50,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
return {
|
||||
count: offenders.length,
|
||||
list: offenders.map(function(offender) {
|
||||
var parts = /^([^ ]*): (\d+) request\(s\)$/.exec(offender);
|
||||
|
||||
if (!parts) {
|
||||
debug('domains offenders transform function error with "%s"', offender);
|
||||
return {
|
||||
file: offender
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
domain: parts[1],
|
||||
requests: parseInt(parts[2])
|
||||
};
|
||||
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,29 @@
|
||||
{
|
||||
"categories": {
|
||||
"pageWeight": {
|
||||
"label": "Page weight",
|
||||
"policies": {
|
||||
"totalWeight": 5,
|
||||
"imageOptimization": 2,
|
||||
"gzipCompression": 2,
|
||||
"fileMinification": 1
|
||||
}
|
||||
},
|
||||
"requests": {
|
||||
"label": "Requests",
|
||||
"policies": {
|
||||
"totalRequests": 5,
|
||||
"domains": 3,
|
||||
"notFound": 3,
|
||||
"multipleRequests": 2,
|
||||
"smallRequests": 1,
|
||||
"lazyLoadableImagesBelowTheFold": 2
|
||||
}
|
||||
},
|
||||
"domComplexity": {
|
||||
"label": "DOM complexity",
|
||||
"policies": {
|
||||
"DOMelementsCount": 1,
|
||||
"DOMelementsCount": 3,
|
||||
"DOMelementMaxDepth": 1,
|
||||
"iframesCount": 1,
|
||||
"DOMidDuplicated": 1
|
||||
@@ -12,11 +32,16 @@
|
||||
"domManipulations": {
|
||||
"label": "DOM manipulations",
|
||||
"policies": {
|
||||
"DOMinserts": 2,
|
||||
"DOMqueries": 1,
|
||||
"DOMqueriesWithoutResults": 2,
|
||||
"DOMqueriesAvoidable": 2,
|
||||
"eventsBound": 1
|
||||
"DOMaccesses": 3,
|
||||
"queriesWithoutResults": 1,
|
||||
"DOMqueriesAvoidable": 1
|
||||
}
|
||||
},
|
||||
"scroll": {
|
||||
"label": "Scroll bottlenecks",
|
||||
"policies": {
|
||||
"eventsScrollBound": 1,
|
||||
"DOMaccessesOnScroll": 4
|
||||
}
|
||||
},
|
||||
"badJavascript": {
|
||||
@@ -28,17 +53,14 @@
|
||||
"globalVariables": 0.5
|
||||
}
|
||||
},
|
||||
"jQueryVersion": {
|
||||
"label": "jQuery version",
|
||||
"jQuery": {
|
||||
"label": "jQuery",
|
||||
"policies": {
|
||||
"jQueryVersion": 5,
|
||||
"jQueryVersionsLoaded": 0.1
|
||||
}
|
||||
},
|
||||
"cssSyntaxError": {
|
||||
"label": "CSS syntax errors",
|
||||
"policies": {
|
||||
"cssParsingErrors": 1
|
||||
"jQueryVersion": 2,
|
||||
"jQueryVersionsLoaded": 2,
|
||||
"jQueryFunctionsUsed": 1,
|
||||
"jQueryCallsOnEmptyObject": 1,
|
||||
"jQueryNotDelegatedEvents": 1
|
||||
}
|
||||
},
|
||||
"cssComplexity": {
|
||||
@@ -53,6 +75,7 @@
|
||||
"badCSS": {
|
||||
"label": "Bad CSS",
|
||||
"policies": {
|
||||
"cssParsingErrors": 4,
|
||||
"cssImports": 3,
|
||||
"cssDuplicatedSelectors": 2,
|
||||
"cssDuplicatedProperties": 1,
|
||||
@@ -66,51 +89,27 @@
|
||||
"cssRedundantChildNodesSelectors": 1
|
||||
}
|
||||
},
|
||||
"requests": {
|
||||
"label": "Requests number",
|
||||
"serverConfig": {
|
||||
"label": "Server config",
|
||||
"policies": {
|
||||
"requests": 5,
|
||||
"htmlCount": 0,
|
||||
"jsCount": 1,
|
||||
"cssCount": 1,
|
||||
"imageCount": 0,
|
||||
"webfontCount": 2,
|
||||
"videoCount": 0,
|
||||
"jsonCount": 0,
|
||||
"otherCount": 0
|
||||
}
|
||||
},
|
||||
"smallRequests": {
|
||||
"label": "Small requests",
|
||||
"policies": {
|
||||
"smallJsFiles": 1,
|
||||
"smallCssFiles": 1,
|
||||
"smallImages": 1
|
||||
}
|
||||
},
|
||||
"network": {
|
||||
"label": "Network",
|
||||
"policies": {
|
||||
"notFound": 2,
|
||||
"closedConnections": 2,
|
||||
"multipleRequests": 2,
|
||||
"cachingNotSpecified": 1,
|
||||
"cachingDisabled": 1,
|
||||
"cachingTooShort": 1,
|
||||
"domains": 1
|
||||
"cachingTooShort": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"globalScore": {
|
||||
"domComplexity": 1,
|
||||
"pageWeight": 3,
|
||||
"requests": 3,
|
||||
"domComplexity": 2,
|
||||
"domManipulations": 2,
|
||||
"scroll": 1,
|
||||
"badJavascript": 1,
|
||||
"jQueryVersion": 1,
|
||||
"jQuery": 1,
|
||||
"cssSyntaxError": 1,
|
||||
"cssComplexity": 1,
|
||||
"badCSS": 1,
|
||||
"requests": 3,
|
||||
"smallRequests": 1,
|
||||
"network": 2
|
||||
"serverConfig": 1
|
||||
}
|
||||
}
|
||||
@@ -115,7 +115,7 @@ var OffendersHelpers = function() {
|
||||
var parts = null;
|
||||
|
||||
for (var i=0 ; i<traceArray.length ; i++) {
|
||||
parts = /^(([\w$]+) )?([^ ]+):(\d+)$/.exec(traceArray[i]);
|
||||
parts = /^(([\w$]+) )?\(?([^ ]+):(\d+)\)?$/.exec(traceArray[i]);
|
||||
|
||||
if (parts) {
|
||||
var obj = {
|
||||
@@ -168,7 +168,7 @@ var OffendersHelpers = function() {
|
||||
};
|
||||
|
||||
this.fileWithSizePattern = function(fileWithSize) {
|
||||
var parts = /^([^ ]*) \((\d+\.\d{2}) kB\)$/.exec(fileWithSize);
|
||||
var parts = /^([^ ]*) \((\d+\.\d{2}|NaN) kB\)$/.exec(fileWithSize);
|
||||
|
||||
if (!parts) {
|
||||
return {
|
||||
|
||||
@@ -3,6 +3,7 @@ var debug = require('debug')('ylt:runner');
|
||||
|
||||
var phantomasWrapper = require('./tools/phantomas/phantomasWrapper');
|
||||
var jsExecutionTransformer = require('./tools/jsExecutionTransformer');
|
||||
var weightChecker = require('./tools/weightChecker/weightChecker');
|
||||
var rulesChecker = require('./rulesChecker');
|
||||
var scoreCalculator = require('./scoreCalculator');
|
||||
|
||||
@@ -19,14 +20,20 @@ var Runner = function(params) {
|
||||
};
|
||||
|
||||
// Execute Phantomas first
|
||||
phantomasWrapper.execute(data).then(function(phantomasResults) {
|
||||
phantomasWrapper.execute(data)
|
||||
|
||||
.then(function(phantomasResults) {
|
||||
data.toolsResults.phantomas = phantomasResults;
|
||||
|
||||
// Treat the JS Execution Tree from offenders
|
||||
data = jsExecutionTransformer.transform(data);
|
||||
|
||||
// Other tools go here
|
||||
// Redownload every file
|
||||
return weightChecker.recheckAllFiles(data);
|
||||
|
||||
})
|
||||
|
||||
.then(function(data) {
|
||||
|
||||
// Rules checker
|
||||
var policies = require('./metadata/policies');
|
||||
@@ -42,15 +49,21 @@ var Runner = function(params) {
|
||||
|
||||
delete data.toolsResults.phantomas.metrics.javascriptExecutionTree;
|
||||
delete data.toolsResults.phantomas.offenders.javascriptExecutionTree;
|
||||
delete data.toolsResults.phantomas.metrics.scrollExecutionTree;
|
||||
delete data.toolsResults.phantomas.offenders.scrollExecutionTree;
|
||||
|
||||
return data;
|
||||
|
||||
}).then(function(data) {
|
||||
})
|
||||
|
||||
.then(function(data) {
|
||||
|
||||
// Finished!
|
||||
deferred.resolve(data);
|
||||
|
||||
}).fail(function(err) {
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
debug('Run failed');
|
||||
debug(err);
|
||||
|
||||
|
||||
@@ -48,6 +48,8 @@ var screenshotHandler = function() {
|
||||
|
||||
})
|
||||
|
||||
.then(this.addWhiteBackground)
|
||||
|
||||
.then(this.toBuffer);
|
||||
};
|
||||
|
||||
@@ -92,6 +94,39 @@ var screenshotHandler = function() {
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
// If the page doesn't set a bg color, the default PhantomJS one is transparent
|
||||
// When transforming PNG to JPG, transparent pixels become black.
|
||||
// This is why we need to add a transparent background.
|
||||
this.addWhiteBackground = function(image) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
// Create a canvas with the same dimensions as your image:
|
||||
lwip.create(image.width(), image.height(), 'white', function(err, canvas){
|
||||
if (err) {
|
||||
debug('Could not create a white canvas');
|
||||
debug(err);
|
||||
|
||||
deferred.reject(err);
|
||||
} else {
|
||||
// Paste original image on top of the canvas
|
||||
canvas.paste(0, 0, image, function(err, image){
|
||||
if (err) {
|
||||
debug('Could not paste image on the white canvas');
|
||||
debug(err);
|
||||
|
||||
deferred.reject(err);
|
||||
} else {
|
||||
// Now image has a white background...
|
||||
debug('White background correctly added');
|
||||
deferred.resolve(image);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
|
||||
this.toBuffer = function(image) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
@@ -33,7 +33,12 @@ var ApiController = function(app) {
|
||||
waitForResponse: req.body.waitForResponse !== false && req.body.waitForResponse !== 'false' && req.body.waitForResponse !== 0,
|
||||
partialResult: req.body.partialResult || null,
|
||||
screenshot: req.body.screenshot || false,
|
||||
jsTimeline: req.body.jsTimeline || false
|
||||
jsTimeline: req.body.jsTimeline || false,
|
||||
device: req.body.device || 'desktop',
|
||||
waitForSelector: req.body.waitForSelector || null,
|
||||
cookie: req.body.cookie || null,
|
||||
authUser: req.body.authUser || null,
|
||||
authPass: req.body.authPass || null
|
||||
}
|
||||
};
|
||||
|
||||
@@ -66,7 +71,12 @@ var ApiController = function(app) {
|
||||
|
||||
var runOptions = {
|
||||
screenshot: run.params.screenshot ? screenshot.getTmpFilePath() : false,
|
||||
jsDeepAnalysis: run.params.jsTimeline
|
||||
jsDeepAnalysis: run.params.jsTimeline,
|
||||
device: run.params.device,
|
||||
waitForSelector: run.params.waitForSelector,
|
||||
cookie: run.params.cookie,
|
||||
authUser: run.params.authUser,
|
||||
authPass: run.params.authPass
|
||||
};
|
||||
|
||||
return ylt(run.params.url, runOptions);
|
||||
@@ -99,8 +109,14 @@ var ApiController = function(app) {
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
// Delete screenshot temporary file
|
||||
.then(screenshot.deleteTmpFile);
|
||||
.then(screenshot.deleteTmpFile)
|
||||
|
||||
// Don't worry if there's an error
|
||||
.fail(function(err) {
|
||||
debug('An error occured while creating the screenshot\'s thumbnail. Ignoring and continuing...');
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@@ -115,6 +131,7 @@ var ApiController = function(app) {
|
||||
// Empty javascriptExecutionTree if not needed
|
||||
if (!run.params.jsTimeline) {
|
||||
data.javascriptExecutionTree = {};
|
||||
data.scrollExecutionTree = {};
|
||||
}
|
||||
|
||||
// Remove tools results if not needed
|
||||
@@ -232,6 +249,18 @@ var ApiController = function(app) {
|
||||
// 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;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -58,6 +58,9 @@ function RunsDatastore() {
|
||||
|
||||
var errorMessage;
|
||||
switch(err) {
|
||||
case '1':
|
||||
errorMessage = "Error 1: unknown error";
|
||||
break;
|
||||
case '252':
|
||||
errorMessage = "Error 252: page timeout in Phantomas";
|
||||
break;
|
||||
@@ -76,6 +79,9 @@ function RunsDatastore() {
|
||||
case '1002':
|
||||
errorMessage = "Error 1002: missing Phantomas metrics";
|
||||
break;
|
||||
case '1003':
|
||||
errorMessage = "Error 1003: Phantomas not returning";
|
||||
break;
|
||||
default:
|
||||
errorMessage = err;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
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;
|
||||
@@ -1,34 +1,68 @@
|
||||
var debug = require('debug')('ylt:jsExecutionTransformer');
|
||||
|
||||
var offendersHelpers = require('../offendersHelpers');
|
||||
var offendersHelpers = require('../offendersHelpers');
|
||||
var Collection = require('./phantomas/custom_modules/util/collection');
|
||||
|
||||
var jsExecutionTransformer = function() {
|
||||
|
||||
this.transform = function(data) {
|
||||
var javascriptExecutionTree = {};
|
||||
var jQueryFunctionsCollection = new Collection();
|
||||
|
||||
var metrics = {
|
||||
domManipulations: 0,
|
||||
queriesWithoutResults: 0,
|
||||
jQueryCalls: 0,
|
||||
jQueryCallsOnEmptyObject: 0
|
||||
|
||||
domInteractive: 0,
|
||||
domContentLoaded: 0,
|
||||
domContentLoadedEnd: 0,
|
||||
domComplete: 0,
|
||||
|
||||
DOMaccesses: 0,
|
||||
DOMaccessesOnScroll: 0,
|
||||
queriesWithoutResults: 0
|
||||
};
|
||||
|
||||
debug('Starting JS execution transformation');
|
||||
var offenders = {};
|
||||
|
||||
var hasjQuery = (data.toolsResults.phantomas.metrics.jQueryVersionsLoaded > 0);
|
||||
if (hasjQuery) {
|
||||
metrics.jQueryCalls = 0;
|
||||
metrics.jQueryFunctionsUsed = 0;
|
||||
metrics.jQueryCallsOnEmptyObject = 0;
|
||||
metrics.jQueryNotDelegatedEvents = 0;
|
||||
|
||||
offenders.jQueryFunctionsUsed = [];
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
debug('Starting JS execution transformation');
|
||||
javascriptExecutionTree = JSON.parse(data.toolsResults.phantomas.offenders.javascriptExecutionTree[0]);
|
||||
|
||||
if (javascriptExecutionTree.children) {
|
||||
javascriptExecutionTree.children.forEach(function(node) {
|
||||
javascriptExecutionTree.children.forEach(function(node, index) {
|
||||
|
||||
// Mark abnormal things with a warning flag
|
||||
var contextLenght = (node.data.callDetails && node.data.callDetails.context) ? node.data.callDetails.context.length : null;
|
||||
if ((node.data.type === 'jQuery - bind' && contextLenght > 5) ||
|
||||
node.data.resultsNumber === 0 ||
|
||||
contextLenght === 0) {
|
||||
var contextLength = (node.data.callDetails && node.data.callDetails.context) ? node.data.callDetails.context.length : null;
|
||||
|
||||
if (isABindWithoutEventDelegation(node, contextLength)) {
|
||||
metrics.jQueryNotDelegatedEvents += contextLength;
|
||||
node.warning = true;
|
||||
node.eventNotDelegated = true;
|
||||
}
|
||||
|
||||
if (node.data.resultsNumber === 0) {
|
||||
metrics.queriesWithoutResults ++;
|
||||
node.queryWithoutResults = true;
|
||||
node.warning = true;
|
||||
}
|
||||
|
||||
if (contextLength === 0) {
|
||||
metrics.jQueryCallsOnEmptyObject ++;
|
||||
node.jQueryCallOnEmptyObject = true;
|
||||
node.warning = true;
|
||||
}
|
||||
|
||||
if (node.data.type.indexOf('jQuery - ') === 0) {
|
||||
metrics.jQueryCalls ++;
|
||||
jQueryFunctionsCollection.push(node.data.type);
|
||||
}
|
||||
|
||||
// Mark errors with an error flag
|
||||
@@ -39,51 +73,82 @@ var jsExecutionTransformer = function() {
|
||||
// Mark a performance flag
|
||||
if (['domInteractive', 'domContentLoaded', 'domContentLoadedEnd', 'domComplete'].indexOf(node.data.type) >= 0) {
|
||||
node.windowPerformance = true;
|
||||
|
||||
// Adjust the navigation timings (cause their not very well synchronised)
|
||||
switch(node.data.type) {
|
||||
case 'domInteractive':
|
||||
javascriptExecutionTree.data.domInteractive = node.data.timestamp;
|
||||
break;
|
||||
case 'domContentLoaded':
|
||||
javascriptExecutionTree.data.domContentLoaded = node.data.timestamp;
|
||||
break;
|
||||
case 'domContentLoadedEnd':
|
||||
javascriptExecutionTree.data.domContentLoadedEnd = node.data.timestamp;
|
||||
break;
|
||||
case 'domComplete':
|
||||
javascriptExecutionTree.data.domComplete = node.data.timestamp;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Fix rare bug when domComplete was never triggered
|
||||
if (index === javascriptExecutionTree.children.length - 1 && !javascriptExecutionTree.data.domComplete) {
|
||||
javascriptExecutionTree.data.domComplete = node.data.timestamp + 1000;
|
||||
}
|
||||
|
||||
// Read the execution tree and adjust the navigation timings (cause their not very well synchronised)
|
||||
switch(node.data.type) {
|
||||
case 'domInteractive':
|
||||
data.toolsResults.phantomas.metrics.domInteractive = node.data.timestamp;
|
||||
break;
|
||||
case 'domContentLoaded':
|
||||
data.toolsResults.phantomas.metrics.domContentLoaded = node.data.timestamp;
|
||||
break;
|
||||
case 'domContentLoadedEnd':
|
||||
data.toolsResults.phantomas.metrics.domContentLoadedEnd = node.data.timestamp;
|
||||
break;
|
||||
case 'domComplete':
|
||||
data.toolsResults.phantomas.metrics.domComplete = node.data.timestamp;
|
||||
break;
|
||||
}
|
||||
// Transform domPaths into objects
|
||||
changeListOfDomPaths(node);
|
||||
|
||||
// Change the list of dom paths into a tree
|
||||
treeRecursiveParser(node, function(node) {
|
||||
|
||||
if (node.data.callDetails && node.data.callDetails.context && node.data.callDetails.context.length > 0) {
|
||||
node.data.callDetails.context.elements = node.data.callDetails.context.elements.map(offendersHelpers.domPathToDomElementObj, offendersHelpers);
|
||||
}
|
||||
// Count the number of DOM accesses, by counting the tree leafs
|
||||
metrics.DOMaccesses += countTreeLeafs(node);
|
||||
});
|
||||
|
||||
if (node.data.type === 'appendChild' || node.data.type === 'insertBefore' || node.data.type === 'getComputedStyle') {
|
||||
node.data.callDetails.arguments[0] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[0]);
|
||||
}
|
||||
// Count the number of different jQuery functions called
|
||||
if (hasjQuery) {
|
||||
|
||||
if (node.data.type === 'insertBefore') {
|
||||
node.data.callDetails.arguments[1] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[1]);
|
||||
jQueryFunctionsCollection.sort().forEach(function(fnName, cnt) {
|
||||
if (fnName === 'jQuery - find') {
|
||||
fnName = 'jQuery - $';
|
||||
}
|
||||
metrics.jQueryFunctionsUsed ++;
|
||||
offenders.jQueryFunctionsUsed.push({
|
||||
functionName: fnName.substring(9),
|
||||
count: cnt
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
debug('JS execution transformation complete');
|
||||
|
||||
|
||||
debug('Starting scroll execution transformation');
|
||||
offenders.DOMaccessesOnScroll = JSON.parse(data.toolsResults.phantomas.offenders.scrollExecutionTree[0]);
|
||||
if (offenders.DOMaccessesOnScroll.children) {
|
||||
offenders.DOMaccessesOnScroll.children.forEach(function(node) {
|
||||
|
||||
// Mark a event flag
|
||||
if (['documentScroll', 'windowScroll', 'window.onscroll'].indexOf(node.data.type) >= 0) {
|
||||
node.windowPerformance = true;
|
||||
}
|
||||
|
||||
// Transform domPaths into objects
|
||||
changeListOfDomPaths(node);
|
||||
|
||||
// Count the number of DOM accesses, by counting the tree leafs
|
||||
metrics.DOMaccessesOnScroll += countTreeLeafs(node);
|
||||
});
|
||||
}
|
||||
|
||||
debug('JS execution transformation complete');
|
||||
debug('Scroll execution transformation complete');
|
||||
|
||||
} catch(err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
data.javascriptExecutionTree = javascriptExecutionTree;
|
||||
|
||||
data.toolsResults.jsExecutionTransformer = {
|
||||
metrics: metrics
|
||||
metrics: metrics,
|
||||
offenders: offenders
|
||||
};
|
||||
|
||||
return data;
|
||||
@@ -97,6 +162,63 @@ var jsExecutionTransformer = function() {
|
||||
}
|
||||
fn(node);
|
||||
}
|
||||
|
||||
function changeListOfDomPaths(rootNode) {
|
||||
treeRecursiveParser(rootNode, function(node) {
|
||||
|
||||
if (node.data.callDetails && node.data.callDetails.context && node.data.callDetails.context.length > 0) {
|
||||
node.data.callDetails.context.elements = node.data.callDetails.context.elements.map(offendersHelpers.domPathToDomElementObj, offendersHelpers);
|
||||
}
|
||||
|
||||
if (node.data.type === 'appendChild' || node.data.type === 'insertBefore' || node.data.type === 'getComputedStyle') {
|
||||
node.data.callDetails.arguments[0] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[0]);
|
||||
}
|
||||
|
||||
if (node.data.type === 'insertBefore') {
|
||||
node.data.callDetails.arguments[1] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[1]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Returns the number of leafs (nodes without children)
|
||||
function countTreeLeafs(rootNode) {
|
||||
var count = 0;
|
||||
|
||||
treeRecursiveParser(rootNode, function(node) {
|
||||
if (!node.children &&
|
||||
!node.error &&
|
||||
!node.windowPerformance &&
|
||||
node.data.type !== 'jQuery loaded') {
|
||||
count ++;
|
||||
}
|
||||
});
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
function isPureString(str) {
|
||||
return typeof str === 'string' && str[0] !== '{' && str !== '(function)' && str !== '[Object]' && str !== '[Array]' && str !== 'true' && str !== 'false' && str !== 'undefined' && str !== 'unknown' && str !== 'null';
|
||||
}
|
||||
|
||||
function isABindWithoutEventDelegation(node, contextLength) {
|
||||
// Count only on larger bindings
|
||||
if (contextLength <= 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (node.data.type === 'jQuery - on' && node.data.callDetails.arguments[1] && !isPureString(node.data.callDetails.arguments[1])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (node.data.type.indexOf('jQuery - ') === 0 && node.children && node.children.length === 1) {
|
||||
var child = node.children[0];
|
||||
if (child.data.type === 'jQuery - on' && child.data.callDetails.arguments[1] && !isPureString(child.data.callDetails.arguments[1])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = new jsExecutionTransformer();
|
||||
@@ -104,10 +104,11 @@ exports.module = function(phantomas) {
|
||||
|
||||
var root = new ContextTreeNode(null, {type: 'main'});
|
||||
var currentContext = root;
|
||||
var depth = 0;
|
||||
|
||||
if (deepAnalysis) {
|
||||
phantomas.log('Entering deep Javascript analysis mode');
|
||||
}
|
||||
var depth = 0;
|
||||
|
||||
// Add a child but don't enter his context
|
||||
function pushContext(data) {
|
||||
@@ -176,6 +177,13 @@ exports.module = function(phantomas) {
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
// Empty the tree
|
||||
function resetTree() {
|
||||
root = new ContextTreeNode(null, {type: 'main'});
|
||||
currentContext = root;
|
||||
depth = 0;
|
||||
}
|
||||
|
||||
|
||||
function ContextTreeNode(parent, data) {
|
||||
@@ -196,6 +204,7 @@ exports.module = function(phantomas) {
|
||||
phantomas.leaveContext = leaveContext;
|
||||
phantomas.getContextData = getContextData;
|
||||
phantomas.readFullTree = readFullTree;
|
||||
phantomas.resetTree = resetTree;
|
||||
|
||||
})();
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Analyzes DOM hidden content
|
||||
*/
|
||||
/* global document: true, Node: true, window: true */
|
||||
|
||||
exports.version = '0.1.a';
|
||||
|
||||
exports.module = function(phantomas) {
|
||||
'use strict';
|
||||
|
||||
// total length of HTML of hidden elements (i.e. display: none)
|
||||
phantomas.setMetric('hiddenContentSize'); // @desc the size of content of hidden elements on the page (with CSS display: none) @offenders
|
||||
|
||||
// HTML size
|
||||
phantomas.on('report', function() {
|
||||
phantomas.evaluate(function() {
|
||||
(function(phantomas) {
|
||||
phantomas.spyEnabled(false, 'checking the hiddenContentSize');
|
||||
|
||||
var runner = new phantomas.nodeRunner();
|
||||
|
||||
runner.walk(document.body, function(node, depth) {
|
||||
switch (node.nodeType) {
|
||||
case Node.ELEMENT_NODE:
|
||||
// @see https://developer.mozilla.org/en/DOM%3awindow.getComputedStyle
|
||||
var styles = window.getComputedStyle(node);
|
||||
|
||||
if (styles && styles.getPropertyValue('display') === 'none') {
|
||||
if (typeof node.innerHTML === 'string') {
|
||||
var size = node.innerHTML.length;
|
||||
phantomas.incrMetric('hiddenContentSize', size);
|
||||
|
||||
// log hidden containers bigger than 1 kB
|
||||
if (size > 1024) {
|
||||
phantomas.addOffender('hiddenContentSize', phantomas.getDOMPath(node) + ' (' + size + ' characters)');
|
||||
}
|
||||
}
|
||||
|
||||
// don't run for child nodes as they're hidden as well
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
phantomas.spyEnabled(true);
|
||||
}(window.__phantomas));
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -365,7 +365,12 @@ exports.module = function(phantomas) {
|
||||
phantomas.log('DOM query: by %s - "%s" (using %s) in %s', type, query, fnName, context);
|
||||
phantomas.incrMetric('DOMqueries');
|
||||
|
||||
if (context && context.indexOf('DocumentFragment') === -1) {
|
||||
if (context && (
|
||||
context.indexOf('html') === 0 ||
|
||||
context.indexOf('body') === 0 ||
|
||||
context.indexOf('head') === 0 ||
|
||||
context.indexOf('#document') === 0
|
||||
)) {
|
||||
DOMqueries.push(type + ' "' + query + '" with ' + fnName + ' (in context ' + context + ')');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
/**
|
||||
* Analyzes events bound to DOM elements
|
||||
*/
|
||||
/* global Document: true, Element: true, window: true */
|
||||
|
||||
exports.version = '0.2.a';
|
||||
|
||||
exports.module = function(phantomas) {
|
||||
'use strict';
|
||||
|
||||
phantomas.setMetric('eventsBound'); // @desc number of EventTarget.addEventListener calls
|
||||
|
||||
// spy calls to EventTarget.addEventListener
|
||||
// @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget.addEventListener
|
||||
phantomas.once('init', function() {
|
||||
phantomas.evaluate(function() {
|
||||
(function(phantomas) {
|
||||
function eventSpyBefore(eventType) {
|
||||
/* jshint validthis: true */
|
||||
var path = phantomas.getDOMPath(this);
|
||||
//phantomas.log('DOM event: "' + eventType + '" bound to "' + path + '"');
|
||||
|
||||
phantomas.incrMetric('eventsBound');
|
||||
phantomas.addOffender('eventsBound', '"%s" bound to "%s"', eventType, path);
|
||||
|
||||
phantomas.enterContext({
|
||||
type: 'addEventListener',
|
||||
callDetails: {
|
||||
context: {
|
||||
length: 1,
|
||||
elements: [path]
|
||||
},
|
||||
arguments: [eventType]
|
||||
},
|
||||
backtrace: phantomas.getBacktrace()
|
||||
});
|
||||
}
|
||||
|
||||
function eventSpyAfter(result) {
|
||||
phantomas.leaveContext();
|
||||
}
|
||||
|
||||
phantomas.spy(Element.prototype, 'addEventListener', eventSpyBefore, eventSpyAfter);
|
||||
phantomas.spy(Document.prototype, 'addEventListener', eventSpyBefore, eventSpyAfter);
|
||||
phantomas.spy(window, 'addEventListener', eventSpyBefore, eventSpyAfter);
|
||||
})(window.__phantomas);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Analyzes events bound to DOM elements
|
||||
*/
|
||||
/* global Document: true, Element: true, window: true */
|
||||
|
||||
exports.version = '0.4.a';
|
||||
|
||||
exports.module = function(phantomas) {
|
||||
'use strict';
|
||||
|
||||
phantomas.setMetric('eventsBound'); // @desc number of EventTarget.addEventListener calls
|
||||
phantomas.setMetric('eventsDispatched'); // @desc number of EventTarget.dispatchEvent calls
|
||||
phantomas.setMetric('eventsScrollBound'); // @desc number of scroll event bounds
|
||||
|
||||
phantomas.once('init', function() {
|
||||
phantomas.evaluate(function() {
|
||||
(function(phantomas) {
|
||||
// spy calls to EventTarget.addEventListener
|
||||
// @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget.addEventListener
|
||||
function eventSpyBefore(eventType) {
|
||||
/* jshint validthis: true */
|
||||
var path = phantomas.getDOMPath(this);
|
||||
//phantomas.log('DOM event: "' + eventType + '" bound to "' + path + '"');
|
||||
|
||||
phantomas.incrMetric('eventsBound');
|
||||
phantomas.addOffender('eventsBound', '"%s" bound to "%s"', eventType, path);
|
||||
phantomas.log('event ' + eventType + ' bound');
|
||||
|
||||
phantomas.enterContext({
|
||||
type: 'addEventListener',
|
||||
callDetails: {
|
||||
context: {
|
||||
length: 1,
|
||||
elements: [path]
|
||||
},
|
||||
arguments: [eventType]
|
||||
},
|
||||
backtrace: phantomas.getBacktrace()
|
||||
});
|
||||
|
||||
// count window.addEventListener('scroll', ...) - issue #508
|
||||
if (eventType === 'scroll' && (path === 'window' || path === '#document')) {
|
||||
phantomas.incrMetric('eventsScrollBound');
|
||||
phantomas.addOffender('eventsScrollBound', 'bound by %s on %s', phantomas.getBacktrace(), path);
|
||||
}
|
||||
}
|
||||
|
||||
function eventSpyAfter(result) {
|
||||
phantomas.leaveContext();
|
||||
}
|
||||
|
||||
phantomas.spy(Element.prototype, 'addEventListener', eventSpyBefore, eventSpyAfter);
|
||||
phantomas.spy(Document.prototype, 'addEventListener', eventSpyBefore, eventSpyAfter);
|
||||
phantomas.spy(window, 'addEventListener', eventSpyBefore, eventSpyAfter);
|
||||
|
||||
// spy calls to EventTarget.dispatchEvent
|
||||
// @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget.dispatchEvent
|
||||
phantomas.spy(Element.prototype, 'dispatchEvent', function(ev) {
|
||||
/* jshint validthis: true */
|
||||
var path = phantomas.getDOMPath(this);
|
||||
|
||||
phantomas.log('Core JS event: triggered "%s" on "%s"', ev.type, path);
|
||||
|
||||
phantomas.incrMetric('eventsDispatched');
|
||||
phantomas.addOffender('eventsDispatched', '"%s" on "%s"', ev.type, path);
|
||||
});
|
||||
})(window.__phantomas);
|
||||
});
|
||||
});
|
||||
|
||||
phantomas.on('report', function() {
|
||||
phantomas.evaluate(function() {
|
||||
(function(phantomas) {
|
||||
// Check if a window.onscroll function is defined
|
||||
if (typeof(window.onscroll) === "function") {
|
||||
phantomas.incrMetric('eventsScrollBound');
|
||||
phantomas.addOffender('eventsScrollBound', 'bound by %s on %s', '', 'window.onscroll');
|
||||
}
|
||||
}(window.__phantomas));
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -274,7 +274,6 @@ exports.module = function(phantomas) {
|
||||
args = [].slice.call(arguments);
|
||||
args.forEach(function(arg, index) {
|
||||
|
||||
|
||||
if (arg instanceof Array) {
|
||||
arg = '[Array]';
|
||||
}
|
||||
@@ -296,6 +295,13 @@ exports.module = function(phantomas) {
|
||||
} else {
|
||||
|
||||
try {
|
||||
|
||||
for (var key in arg) {
|
||||
if (typeof arg[key] === 'function') {
|
||||
arg[key] = '(function)';
|
||||
}
|
||||
}
|
||||
|
||||
arg = JSON.stringify(arg);
|
||||
} catch(e) {
|
||||
arg = '[Object]';
|
||||
|
||||
@@ -9,11 +9,11 @@ exports.version = '0.1';
|
||||
exports.module = function(phantomas) {
|
||||
'use strict';
|
||||
|
||||
phantomas.setMetric('javascriptExecutionTree'); // @desc number of duplicated DOM queries
|
||||
phantomas.setMetric('javascriptExecutionTree');
|
||||
|
||||
// save data
|
||||
phantomas.on('report', function() {
|
||||
phantomas.log('Reading execution tree JSON');
|
||||
phantomas.log('JS execution tree: Reading execution tree JSON');
|
||||
|
||||
phantomas.evaluate(function() {(function(phantomas) {
|
||||
var fullTree = phantomas.readFullTree();
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Retries download on every request to get the real file size
|
||||
*
|
||||
*/
|
||||
|
||||
exports.version = '0.1';
|
||||
|
||||
exports.module = function(phantomas) {
|
||||
'use strict';
|
||||
|
||||
phantomas.setMetric('requestsList');
|
||||
|
||||
var requests = [];
|
||||
|
||||
phantomas.on('recv', function(entry, res) {
|
||||
requests.push(entry);
|
||||
});
|
||||
|
||||
phantomas.on('report', function() {
|
||||
phantomas.setMetric('requestsList', true, true);
|
||||
phantomas.addOffender('requestsList', JSON.stringify(requests));
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
exports.version = '0.1';
|
||||
|
||||
exports.module = function(phantomas) {
|
||||
'use strict';
|
||||
|
||||
phantomas.setMetric('scrollExecutionTree');
|
||||
|
||||
phantomas.on('report', function() {
|
||||
|
||||
phantomas.evaluate(function() {
|
||||
(function(phantomas) {
|
||||
|
||||
var evt = document.createEvent('CustomEvent');
|
||||
evt.initCustomEvent('scroll', false, false, null);
|
||||
|
||||
function triggerScrollEvent() {
|
||||
phantomas.resetTree();
|
||||
|
||||
try {
|
||||
|
||||
// Chrome triggers them in this order:
|
||||
|
||||
// 1. document
|
||||
phantomas.pushContext({
|
||||
type: 'documentScroll'
|
||||
});
|
||||
document.dispatchEvent(evt);
|
||||
|
||||
// 2. window
|
||||
phantomas.pushContext({
|
||||
type: 'windowScroll'
|
||||
});
|
||||
window.dispatchEvent(evt);
|
||||
|
||||
// No need to call window.onscroll(), it's called by the scroll event on window
|
||||
|
||||
} catch(e) {
|
||||
phantomas.log('ScrollListener error: %s', e);
|
||||
}
|
||||
}
|
||||
|
||||
var firstScrollTime = Date.now();
|
||||
phantomas.log('ScrollListener: triggering a first scroll event...');
|
||||
triggerScrollEvent();
|
||||
|
||||
|
||||
// Ignore the first scroll event and only save the second one,
|
||||
// because we want to detect un-throttled things, throttled ones are ok.
|
||||
var secondScrollTime = Date.now();
|
||||
phantomas.log('ScrollListener: triggering a second scroll event (%dms after the first)...', secondScrollTime - firstScrollTime);
|
||||
triggerScrollEvent();
|
||||
|
||||
|
||||
var fullTree = phantomas.readFullTree();
|
||||
if (fullTree !== null) {
|
||||
phantomas.setMetric('scrollExecutionTree', true, true);
|
||||
phantomas.addOffender('scrollExecutionTree', JSON.stringify(fullTree));
|
||||
phantomas.log('ScrollListener: scrollExecutionTree correctly extracted');
|
||||
} else {
|
||||
phantomas.log('Error: scrollExecutionTree could not be extracted');
|
||||
}
|
||||
|
||||
|
||||
phantomas.log('ScrollListener: end of scroll triggering');
|
||||
|
||||
})(window.__phantomas);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -11,11 +11,6 @@ var PhantomasWrapper = function() {
|
||||
|
||||
/**
|
||||
* This is the phantomas launcher. It merges user chosen options into the default options
|
||||
* Available options :
|
||||
*
|
||||
* - timeout : in seconds (default 60)
|
||||
* - jsDeepAnalysis : should we inspect subrequests in the javascript execution tree?
|
||||
*
|
||||
*/
|
||||
this.execute = function(data) {
|
||||
|
||||
@@ -24,27 +19,34 @@ var PhantomasWrapper = function() {
|
||||
|
||||
|
||||
var options = {
|
||||
|
||||
// Cusomizable options
|
||||
'timeout': task.options.timeout || 60,
|
||||
'timeout': task.options.timeout || 45,
|
||||
'js-deep-analysis': task.options.jsDeepAnalysis || false,
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.110 Safari/537.36',
|
||||
'user-agent': (task.options.device === 'desktop') ? 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.110 Safari/537.36' : null,
|
||||
'tablet': (task.options.device === 'tablet'),
|
||||
'phone': (task.options.device === 'phone'),
|
||||
'screenshot': task.options.screenshot || false,
|
||||
'wait-for-selector': task.options.waitForSelector,
|
||||
'cookie': task.options.cookie,
|
||||
'auth-user': task.options.authUser,
|
||||
'auth-pass': task.options.authPass,
|
||||
|
||||
// Mandatory
|
||||
'reporter': 'json:pretty',
|
||||
'analyze-css': true,
|
||||
'skip-modules': [
|
||||
'blockDomains', // not needed
|
||||
'domHiddenContent', // overriden
|
||||
'domMutations', // not compatible with webkit
|
||||
'domQueries', // overriden
|
||||
'eventListeners', // overridden
|
||||
'events', // overridden
|
||||
'filmStrip', // not needed
|
||||
'har', // not needed for the moment
|
||||
'javaScriptBottlenecks', // needs to be launched after custom module scopeYLT
|
||||
'jQuery', // overridden
|
||||
'jserrors', // overridden
|
||||
'pageSource', // not needed
|
||||
'waitForSelector', // not needed
|
||||
'windowPerformance' // overriden
|
||||
].join(','),
|
||||
'include-dirs': [
|
||||
@@ -63,40 +65,64 @@ var PhantomasWrapper = function() {
|
||||
value = '"' + value + '"';
|
||||
}
|
||||
|
||||
optionsString += ' ' + '--' + opt + '=' + value;
|
||||
if (value === true) {
|
||||
optionsString += ' ' + '--' + opt;
|
||||
} else if (value === false || value === null || value === undefined) {
|
||||
// Nothing
|
||||
} else {
|
||||
optionsString += ' ' + '--' + opt + '=' + value;
|
||||
}
|
||||
|
||||
}
|
||||
debug('node node_modules/phantomas/bin/phantomas.js --url=' + task.url + optionsString + ' --verbose');
|
||||
|
||||
// Kill the application if nothing happens
|
||||
|
||||
var phantomasPid;
|
||||
var isKilled = false;
|
||||
|
||||
// Kill phantomas if nothing happens
|
||||
var killer = setTimeout(function() {
|
||||
debug('Killing the app because the test on %s was launched %d seconds ago', task.url, 5*options.timeout);
|
||||
// If in server mode, forever will restart the server
|
||||
console.log('Killing phantomas because the test on ' + task.url + ' was launched ' + 5*options.timeout + ' seconds ago');
|
||||
|
||||
// Kill the Phantomas process first
|
||||
if (phantomasPid) {
|
||||
ps.kill(phantomasPid, function(err) {
|
||||
|
||||
if (err) {
|
||||
debug('Could not kill Phantomas process %s', phantomasPid);
|
||||
}
|
||||
else {
|
||||
debug('Phantomas process %s was correctly killed', phantomasPid);
|
||||
|
||||
// Suicide
|
||||
process.exit(1);
|
||||
// If in server mode, forever will restart the server
|
||||
}
|
||||
|
||||
// Then suicide.
|
||||
process.exit(1);
|
||||
debug('Phantomas process %s was correctly killed', phantomasPid);
|
||||
|
||||
// Then mark the test as failed
|
||||
// Error 1003 = Phantomas not answering
|
||||
deferred.reject(1003);
|
||||
isKilled = true;
|
||||
});
|
||||
} else {
|
||||
// Suicide
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
}, 5*options.timeout*1000);
|
||||
|
||||
|
||||
// It's time to launch the test!!!
|
||||
var triesNumber = 2;
|
||||
|
||||
async.retry(triesNumber, function(cb) {
|
||||
var process = phantomas(task.url, options, function(err, json, results) {
|
||||
debug('Returning from Phantomas');
|
||||
|
||||
if (isKilled) {
|
||||
debug('Process was killed, too late Phantomas, sorry...');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
debug('Returning from Phantomas with error %s', err);
|
||||
|
||||
// Adding some YellowLabTools errors here
|
||||
if (json && json.metrics && (!json.metrics.javascriptExecutionTree || !json.offenders.javascriptExecutionTree)) {
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
var debug = require('debug')('ylt:fileMinifier');
|
||||
|
||||
var Q = require('q');
|
||||
var UglifyJS = require('uglify-js');
|
||||
var CleanCSS = require('clean-css');
|
||||
var Minimize = require('minimize');
|
||||
|
||||
|
||||
var FileMinifier = function() {
|
||||
|
||||
function minifyFile(entry) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
if (!entry.weightCheck || !entry.weightCheck.body) {
|
||||
// No valid file available
|
||||
deferred.resolve(entry);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
var fileSize = entry.weightCheck.uncompressedSize;
|
||||
debug('Let\'s try to optimize %s', entry.url);
|
||||
debug('Current file size is %d', fileSize);
|
||||
var startTime = Date.now();
|
||||
|
||||
if (entry.isJS && !isKnownAsMinified(entry.url) && !looksAlreadyMinified(entry.weightCheck.body)) {
|
||||
|
||||
debug('File is a JS');
|
||||
|
||||
return minifyJs(entry.weightCheck.body)
|
||||
|
||||
.then(function(newFile) {
|
||||
if (!newFile) {
|
||||
debug('Optimization didn\'t work');
|
||||
return entry;
|
||||
}
|
||||
|
||||
var endTime = Date.now();
|
||||
|
||||
var newFileSize = newFile.length;
|
||||
|
||||
debug('JS minification complete for %s', entry.url);
|
||||
|
||||
if (gainIsEnough(fileSize, newFileSize)) {
|
||||
entry.weightCheck.bodyAfterOptimization = newFile;
|
||||
entry.weightCheck.optimized = newFileSize;
|
||||
entry.weightCheck.isOptimized = false;
|
||||
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
|
||||
}
|
||||
|
||||
return entry;
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
return entry;
|
||||
});
|
||||
|
||||
} else if (entry.isCSS) {
|
||||
|
||||
debug('File is a CSS');
|
||||
|
||||
return minifyCss(entry.weightCheck.body)
|
||||
|
||||
.then(function(newFile) {
|
||||
if (!newFile) {
|
||||
debug('Optimization didn\'t work');
|
||||
return entry;
|
||||
}
|
||||
|
||||
var endTime = Date.now();
|
||||
debug('CSS minification took %dms', endTime - startTime);
|
||||
|
||||
var newFileSize = newFile.length;
|
||||
|
||||
debug('CSS minification complete for %s', entry.url);
|
||||
|
||||
if (gainIsEnough(fileSize, newFileSize)) {
|
||||
entry.weightCheck.bodyAfterOptimization = newFile;
|
||||
entry.weightCheck.optimized = newFileSize;
|
||||
entry.weightCheck.isOptimized = false;
|
||||
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
|
||||
}
|
||||
|
||||
return entry;
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
return entry;
|
||||
});
|
||||
|
||||
} else if (entry.isHTML) {
|
||||
|
||||
debug('File is an HTML');
|
||||
|
||||
return minifyHtml(entry.weightCheck.body)
|
||||
|
||||
.then(function(newFile) {
|
||||
if (!newFile) {
|
||||
debug('Optimization didn\'t work');
|
||||
return entry;
|
||||
}
|
||||
|
||||
var endTime = Date.now();
|
||||
debug('HTML minification took %dms', endTime - startTime);
|
||||
|
||||
var newFileSize = newFile.length;
|
||||
|
||||
debug('HTML minification complete for %s', entry.url);
|
||||
|
||||
if (gainIsEnough(fileSize, newFileSize)) {
|
||||
entry.weightCheck.bodyAfterOptimization = newFile;
|
||||
entry.weightCheck.optimized = newFileSize;
|
||||
entry.weightCheck.isOptimized = false;
|
||||
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
|
||||
}
|
||||
|
||||
return entry;
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
return entry;
|
||||
});
|
||||
|
||||
} else {
|
||||
debug('Not minfiable type or already minified');
|
||||
deferred.resolve(entry);
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
// The gain is estimated of enough value if it's over 2KB or over 20%,
|
||||
// but it's ignored if is below 400 bytes
|
||||
function gainIsEnough(oldWeight, newWeight) {
|
||||
var gain = oldWeight - newWeight;
|
||||
var ratio = gain / oldWeight;
|
||||
return (gain > 2096 || (ratio > 0.2 && gain > 400));
|
||||
}
|
||||
|
||||
// Uglify
|
||||
function minifyJs(body) {
|
||||
|
||||
// Splitting the Uglify function because it sometime takes too long (more than 10 seconds)
|
||||
// I hope that, by splitting, it can be a little more asynchronous, so the application doesn't freeze.
|
||||
|
||||
return splittedUglifyStep1(body)
|
||||
.delay(1)
|
||||
.then(splittedUglifyStep2)
|
||||
.delay(1)
|
||||
.then(splittedUglifyStep3)
|
||||
.delay(1)
|
||||
.then(splittedUglifyStep4)
|
||||
.delay(1)
|
||||
.then(splittedUglifyStep5)
|
||||
.delay(1)
|
||||
.then(splittedUglifyStep6)
|
||||
.delay(1)
|
||||
.then(splittedUglifyStep7);
|
||||
|
||||
}
|
||||
|
||||
function splittedUglifyStep1(code) {
|
||||
var deferred = Q.defer();
|
||||
var startTime = Date.now();
|
||||
|
||||
try {
|
||||
var toplevel_ast = UglifyJS.parse(code);
|
||||
|
||||
var endTime = Date.now();
|
||||
debug('Uglify step 1 took %dms', endTime - startTime);
|
||||
deferred.resolve(toplevel_ast);
|
||||
|
||||
} catch(err) {
|
||||
debug('JS syntax error, Uglify\'s parser failed (step 1)');
|
||||
deferred.reject(err);
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function splittedUglifyStep2(toplevel) {
|
||||
var deferred = Q.defer();
|
||||
var startTime = Date.now();
|
||||
|
||||
toplevel.figure_out_scope();
|
||||
|
||||
var endTime = Date.now();
|
||||
debug('Uglify step 2 took %dms', endTime - startTime);
|
||||
deferred.resolve(toplevel);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function splittedUglifyStep3(toplevel) {
|
||||
var deferred = Q.defer();
|
||||
var startTime = Date.now();
|
||||
|
||||
var compressor = UglifyJS.Compressor({warnings: false});
|
||||
var compressed_ast = toplevel.transform(compressor);
|
||||
|
||||
var endTime = Date.now();
|
||||
debug('Uglify step 3 took %dms', endTime - startTime);
|
||||
deferred.resolve(compressed_ast);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function splittedUglifyStep4(compressed_ast) {
|
||||
var deferred = Q.defer();
|
||||
var startTime = Date.now();
|
||||
|
||||
compressed_ast.figure_out_scope();
|
||||
|
||||
var endTime = Date.now();
|
||||
debug('Uglify step 4 took %dms', endTime - startTime);
|
||||
deferred.resolve(compressed_ast);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function splittedUglifyStep5(compressed_ast) {
|
||||
var deferred = Q.defer();
|
||||
var startTime = Date.now();
|
||||
|
||||
compressed_ast.compute_char_frequency();
|
||||
|
||||
var endTime = Date.now();
|
||||
debug('Uglify step 5 took %dms', endTime - startTime);
|
||||
deferred.resolve(compressed_ast);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function splittedUglifyStep6(compressed_ast) {
|
||||
var deferred = Q.defer();
|
||||
var startTime = Date.now();
|
||||
|
||||
compressed_ast.mangle_names();
|
||||
|
||||
var endTime = Date.now();
|
||||
debug('Uglify step 6 took %dms', endTime - startTime);
|
||||
deferred.resolve(compressed_ast);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function splittedUglifyStep7(compressed_ast) {
|
||||
var deferred = Q.defer();
|
||||
var startTime = Date.now();
|
||||
|
||||
var code = compressed_ast.print_to_string();
|
||||
|
||||
var endTime = Date.now();
|
||||
debug('Uglify step 7 took %dms', endTime - startTime);
|
||||
deferred.resolve(code);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
// Clear-css
|
||||
function minifyCss(body) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
try {
|
||||
var result = new CleanCSS({compatibility: 'ie8'}).minify(body);
|
||||
deferred.resolve(result.styles);
|
||||
} catch(err) {
|
||||
deferred.reject(err);
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
// HTMLMinifier
|
||||
function minifyHtml(body) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
var minimize = new Minimize({
|
||||
empty: true, // KEEP empty attributes
|
||||
conditionals: true, // KEEP conditional internet explorer comments
|
||||
spare: true // KEEP redundant attributes
|
||||
});
|
||||
|
||||
minimize.parse(body, function (error, data) {
|
||||
if (error) {
|
||||
deferred.reject(error);
|
||||
} else {
|
||||
deferred.resolve(data);
|
||||
}
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
// Avoid loosing time trying to compress some JS libraries known as already compressed
|
||||
function isKnownAsMinified(url) {
|
||||
var result = false;
|
||||
|
||||
// Twitter
|
||||
result = result || /^https?:\/\/platform\.twitter\.com\/widgets\.js/.test(url);
|
||||
|
||||
// Facebook
|
||||
result = result || /^https:\/\/connect\.facebook\.net\/[^\/]*\/(sdk|all)\.js/.test(url);
|
||||
|
||||
// Google +1
|
||||
result = result || /^https:\/\/apis\.google\.com\/js\/plusone\.js/.test(url);
|
||||
|
||||
// jQuery CDN
|
||||
result = result || /^https?:\/\/code\.jquery\.com\/.*\.min.js/.test(url);
|
||||
|
||||
// Google Analytics
|
||||
result = result || /^https?:\/\/(www|ssl)\.google-analytics\.com\/(.*)\.js/.test(url);
|
||||
|
||||
if (result === true) {
|
||||
debug('This file is known as already minified. Skipping minification: %s', url);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Avoid loosing tome trying to compress JS files if they alreay look minified
|
||||
// by counting the number of lines compared to the total size.
|
||||
// Less than 1000kb per line is suspicious
|
||||
function looksAlreadyMinified(code) {
|
||||
var linesCount = code.split(/\r\n|\r|\n/).length;
|
||||
var linesRatio = code.length / linesCount;
|
||||
var looksMinified = linesRatio > 1024;
|
||||
|
||||
debug('Lines ratio is %d bytes per line', Math.round(linesRatio));
|
||||
debug(looksMinified ? 'It looks already minified' : 'It doesn\'t look minified');
|
||||
|
||||
return looksMinified;
|
||||
}
|
||||
|
||||
function entryTypeCanBeMinified(entry) {
|
||||
return entry.isJS || entry.isCSS || entry.isHTML;
|
||||
}
|
||||
|
||||
return {
|
||||
minifyFile: minifyFile,
|
||||
minifyJs: minifyJs,
|
||||
minifyCss: minifyCss,
|
||||
minifyHtml: minifyHtml,
|
||||
gainIsEnough: gainIsEnough,
|
||||
entryTypeCanBeMinified: entryTypeCanBeMinified,
|
||||
isKnownAsMinified: isKnownAsMinified
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = new FileMinifier();
|
||||
@@ -0,0 +1,100 @@
|
||||
var debug = require('debug')('ylt:gzipCompressor');
|
||||
|
||||
var Q = require('q');
|
||||
var zlib = require('zlib');
|
||||
|
||||
var GzipCompressor = function() {
|
||||
|
||||
function compressFile(entry) {
|
||||
return gzipUncompressedFile(entry)
|
||||
|
||||
.then(gzipOptimizedFile);
|
||||
}
|
||||
|
||||
// Gzip a file if it was not already gziped
|
||||
function gzipUncompressedFile(entry) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
if (entryTypeCanBeGzipped(entry) && entry.weightCheck && !entry.weightCheck.isCompressed && entry.weightCheck.body) {
|
||||
debug('Compression missing, trying to gzip file %s', entry.url);
|
||||
|
||||
var uncompressedSize = entry.weightCheck.uncompressedSize;
|
||||
|
||||
zlib.gzip(new Buffer(entry.weightCheck.body, 'utf8'), function(err, buffer) {
|
||||
if (err) {
|
||||
debug('Could not compress uncompressed file with gzip');
|
||||
debug(err);
|
||||
|
||||
deferred.reject(err);
|
||||
} else {
|
||||
var compressedSize = buffer.length;
|
||||
|
||||
if (gainIsEnough(uncompressedSize, compressedSize)) {
|
||||
debug('File correctly gziped, was %d and is now %d bytes', uncompressedSize, compressedSize);
|
||||
|
||||
entry.weightCheck.afterCompression = compressedSize;
|
||||
} else {
|
||||
debug('Gzip gain is not enough, was %d and is now %d bytes', uncompressedSize, compressedSize);
|
||||
}
|
||||
|
||||
deferred.resolve(entry);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
deferred.resolve(entry);
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
// Gzip a file after minification or optimization if this step was successful
|
||||
function gzipOptimizedFile(entry) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
if (entryTypeCanBeGzipped(entry) && entry.weightCheck && entry.weightCheck.isOptimized === false) {
|
||||
debug('Trying to gzip file after minification: %s', entry.url);
|
||||
|
||||
var uncompressedSize = entry.weightCheck.optimized;
|
||||
|
||||
zlib.gzip(new Buffer(entry.weightCheck.bodyAfterOptimization, 'utf8'), function(err, buffer) {
|
||||
if (err) {
|
||||
debug('Could not compress minified file with gzip');
|
||||
debug(err);
|
||||
|
||||
deferred.reject(err);
|
||||
} else {
|
||||
var compressedSize = buffer.length;
|
||||
|
||||
debug('Correctly gziped the minified file, was %d and is now %d bytes', uncompressedSize, compressedSize);
|
||||
entry.weightCheck.afterOptimizationAndCompression = compressedSize;
|
||||
|
||||
deferred.resolve(entry);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
deferred.resolve(entry);
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
// The gain is estimated of enough value if it's over 1KB or over 20%,
|
||||
// but it's ignored if is below 100 bytes
|
||||
function gainIsEnough(oldWeight, newWeight) {
|
||||
var gain = oldWeight - newWeight;
|
||||
var ratio = gain / oldWeight;
|
||||
return (gain > 2048 || (ratio > 0.2 && gain > 100));
|
||||
}
|
||||
|
||||
function entryTypeCanBeGzipped(entry) {
|
||||
return entry.isJS || entry.isCSS || entry.isHTML || entry.isJSON || entry.isSVG || entry.isTTF || entry.isXML || entry.isFavicon;
|
||||
}
|
||||
|
||||
return {
|
||||
compressFile: compressFile,
|
||||
entryTypeCanBeGzipped: entryTypeCanBeGzipped
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = new GzipCompressor();
|
||||
@@ -0,0 +1,245 @@
|
||||
var debug = require('debug')('ylt:imageOptimizer');
|
||||
|
||||
var Q = require('q');
|
||||
var Imagemin = require('imagemin');
|
||||
var jpegoptim = require('imagemin-jpegoptim');
|
||||
|
||||
var ImageOptimizer = function() {
|
||||
|
||||
var MAX_JPEG_QUALITY = 85;
|
||||
var OPTIPNG_COMPRESSION_LEVEL = 1;
|
||||
|
||||
function optimizeImage(entry) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
if (!entry.weightCheck || !entry.weightCheck.body) {
|
||||
// No valid file available
|
||||
deferred.resolve(entry);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
var fileSize = entry.weightCheck.uncompressedSize;
|
||||
debug('Let\'s try to optimize %s', entry.url);
|
||||
debug('Current file size is %d', fileSize);
|
||||
|
||||
if (isJPEG(entry)) {
|
||||
debug('File is a JPEG');
|
||||
|
||||
// Starting softly with a lossless compression
|
||||
return compressJpegLosslessly(new Buffer(entry.weightCheck.body, 'binary'))
|
||||
|
||||
.then(function(newFile) {
|
||||
if (!newFile) {
|
||||
debug('Optimization didn\'t work');
|
||||
return entry;
|
||||
}
|
||||
|
||||
var newFileSize = newFile.contents.length;
|
||||
|
||||
debug('JPEG lossless compression complete for %s', entry.url);
|
||||
|
||||
if (gainIsEnough(fileSize, newFileSize)) {
|
||||
entry.weightCheck.lossless = entry.weightCheck.optimized = newFileSize;
|
||||
entry.weightCheck.isOptimized = false;
|
||||
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
|
||||
}
|
||||
|
||||
|
||||
// Now let's compress lossy to MAX_JPEG_QUALITY
|
||||
return compressJpegLossly(new Buffer(entry.weightCheck.body, 'binary'));
|
||||
})
|
||||
|
||||
.then(function(newFile) {
|
||||
if (!newFile) {
|
||||
debug('Optimization didn\'t work');
|
||||
return entry;
|
||||
}
|
||||
|
||||
var newFileSize = newFile.contents.length;
|
||||
|
||||
debug('JPEG lossy compression complete for %s', entry.url);
|
||||
|
||||
if (gainIsEnough(fileSize, newFileSize)) {
|
||||
|
||||
if (entry.weightCheck.isOptimized !== false || newFileSize < entry.weightCheck.lossless) {
|
||||
entry.weightCheck.optimized = newFileSize;
|
||||
}
|
||||
|
||||
entry.weightCheck.lossy = newFileSize;
|
||||
entry.weightCheck.isOptimized = false;
|
||||
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
|
||||
}
|
||||
|
||||
return entry;
|
||||
})
|
||||
|
||||
.fail(function() {
|
||||
return entry;
|
||||
});
|
||||
|
||||
} else if (isPNG(entry)) {
|
||||
|
||||
debug('File is a PNG');
|
||||
|
||||
// Starting softly with a lossless compression
|
||||
return compressPngLosslessly(new Buffer(entry.weightCheck.body, 'binary'))
|
||||
|
||||
.then(function(newFile) {
|
||||
if (!newFile) {
|
||||
debug('Optimization didn\'t work');
|
||||
return entry;
|
||||
}
|
||||
|
||||
var newFileSize = newFile.contents.length;
|
||||
|
||||
debug('PNG lossless compression complete for %s', entry.url);
|
||||
|
||||
debug('Old file size: %d', fileSize);
|
||||
debug('New file size: %d', newFileSize);
|
||||
debug('newgainIsEnough: %s', gainIsEnough(fileSize, newFileSize) ? 'true':'false');
|
||||
|
||||
if (gainIsEnough(fileSize, newFileSize)) {
|
||||
entry.weightCheck.lossless = entry.weightCheck.optimized = newFileSize;
|
||||
entry.weightCheck.isOptimized = false;
|
||||
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
|
||||
}
|
||||
|
||||
return entry;
|
||||
})
|
||||
|
||||
.fail(function() {
|
||||
return entry;
|
||||
});
|
||||
|
||||
} else if (isSVG(entry)) {
|
||||
|
||||
debug('File is an SVG');
|
||||
|
||||
// Starting softly with a lossless compression
|
||||
return compressSvgLosslessly(new Buffer(entry.weightCheck.body, 'utf8'))
|
||||
|
||||
.then(function(newFile) {
|
||||
if (!newFile) {
|
||||
debug('Optimization didn\'t work');
|
||||
return entry;
|
||||
}
|
||||
|
||||
var newFileSize = newFile.contents.length;
|
||||
|
||||
debug('SVG lossless compression complete for %s', entry.url);
|
||||
|
||||
if (gainIsEnough(fileSize, newFileSize)) {
|
||||
entry.weightCheck.bodyAfterOptimization = newFile.contents.toString();
|
||||
entry.weightCheck.lossless = entry.weightCheck.optimized = newFileSize;
|
||||
entry.weightCheck.isOptimized = false;
|
||||
debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize));
|
||||
}
|
||||
|
||||
return entry;
|
||||
})
|
||||
|
||||
.fail(function() {
|
||||
return entry;
|
||||
});
|
||||
|
||||
} else {
|
||||
debug('File type %s is not an optimizable image', entry.contentType);
|
||||
deferred.resolve(entry);
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
// The gain is estimated of enough value if it's over 2KB or over 20%,
|
||||
// but it's ignored if is below 100 bytes
|
||||
function gainIsEnough(oldWeight, newWeight) {
|
||||
var gain = oldWeight - newWeight;
|
||||
var ratio = gain / oldWeight;
|
||||
return (gain > 2048 || (ratio > 0.2 && gain > 100));
|
||||
}
|
||||
|
||||
function isJPEG(entry) {
|
||||
return entry.isImage && entry.contentType === 'image/jpeg';
|
||||
}
|
||||
|
||||
function isPNG(entry) {
|
||||
return entry.isImage && entry.contentType === 'image/png';
|
||||
}
|
||||
|
||||
function isSVG(entry) {
|
||||
return entry.isImage && entry.isSVG;
|
||||
}
|
||||
|
||||
function compressJpegLosslessly(imageBody) {
|
||||
return imageminLauncher(imageBody, 'jpeg', false);
|
||||
}
|
||||
|
||||
function compressJpegLossly(imageBody) {
|
||||
return imageminLauncher(imageBody, 'jpeg', true);
|
||||
}
|
||||
|
||||
function compressPngLosslessly(imageBody) {
|
||||
return imageminLauncher(imageBody, 'png', false);
|
||||
}
|
||||
|
||||
function compressSvgLosslessly(imageBody) {
|
||||
return imageminLauncher(imageBody, 'svg', false);
|
||||
}
|
||||
|
||||
function imageminLauncher(imageBody, type, lossy) {
|
||||
var deferred = Q.defer();
|
||||
var startTime = Date.now();
|
||||
|
||||
debug('Starting %s %s optimization', type, lossy ? 'lossy' : 'lossless');
|
||||
|
||||
var engine;
|
||||
if (type === 'jpeg' && !lossy) {
|
||||
engine = Imagemin.jpegtran();
|
||||
} else if (type === 'jpeg' && lossy) {
|
||||
engine = jpegoptim({max: MAX_JPEG_QUALITY});
|
||||
} else if (type === 'png' && !lossy) {
|
||||
engine = Imagemin.optipng({optimizationLevel: OPTIPNG_COMPRESSION_LEVEL});
|
||||
} else if (type === 'svg' && !lossy) {
|
||||
engine = Imagemin.svgo();
|
||||
} else {
|
||||
deferred.reject('No optimization engine found for imagemin');
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
new Imagemin()
|
||||
.src(imageBody)
|
||||
.use(engine)
|
||||
.run(function (err, files) {
|
||||
if (err) {
|
||||
deferred.reject(err);
|
||||
} else {
|
||||
deferred.resolve(files[0]);
|
||||
var endTime = Date.now();
|
||||
debug('Optimization for %s took %d ms', type, endTime - startTime);
|
||||
}
|
||||
});
|
||||
|
||||
} catch(err) {
|
||||
deferred.reject(err);
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function entryTypeCanBeOptimized(entry) {
|
||||
return isJPEG(entry) || isPNG(entry) || isSVG(entry);
|
||||
}
|
||||
|
||||
return {
|
||||
optimizeImage: optimizeImage,
|
||||
compressJpegLosslessly: compressJpegLosslessly,
|
||||
compressJpegLossly: compressJpegLossly,
|
||||
compressPngLosslessly: compressPngLosslessly,
|
||||
compressSvgLosslessly: compressSvgLosslessly,
|
||||
gainIsEnough: gainIsEnough,
|
||||
entryTypeCanBeOptimized: entryTypeCanBeOptimized
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = new ImageOptimizer();
|
||||
@@ -0,0 +1,501 @@
|
||||
/*
|
||||
* Redownloading every files after Phantomas has finished
|
||||
* Checks weight and every kind of compression
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
var debug = require('debug')('ylt:weightChecker');
|
||||
var Q = require('q');
|
||||
var http = require('http');
|
||||
var zlib = require('zlib');
|
||||
var async = require('async');
|
||||
var request = require('request');
|
||||
|
||||
var imageOptimizer = require('./imageOptimizer');
|
||||
var fileMinifier = require('./fileMinifier');
|
||||
var gzipCompressor = require('./gzipCompressor');
|
||||
|
||||
|
||||
var WeightChecker = function() {
|
||||
|
||||
var MAX_PARALLEL_DOWNLOADS = 10;
|
||||
var REQUEST_TIMEOUT = 15000; // 15 seconds
|
||||
|
||||
|
||||
// This function will re-download every asset and check if it could be optimized
|
||||
function recheckAllFiles(data) {
|
||||
var startTime = Date.now();
|
||||
debug('Redownload started');
|
||||
var deferred = Q.defer();
|
||||
|
||||
var requestsList = JSON.parse(data.toolsResults.phantomas.offenders.requestsList);
|
||||
delete data.toolsResults.phantomas.metrics.requestsList;
|
||||
delete data.toolsResults.phantomas.offenders.requestsList;
|
||||
|
||||
// Transform every request into a download function with a callback when done
|
||||
var redownloadList = requestsList.map(function(entry) {
|
||||
return function(callback) {
|
||||
|
||||
redownloadEntry(entry)
|
||||
|
||||
.then(imageOptimizer.optimizeImage)
|
||||
|
||||
.then(fileMinifier.minifyFile)
|
||||
|
||||
.then(gzipCompressor.compressFile)
|
||||
|
||||
.then(function(newEntry) {
|
||||
callback(null, newEntry);
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
callback(err);
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
// Lanch all redownload functions and wait for completion
|
||||
async.parallelLimit(redownloadList, MAX_PARALLEL_DOWNLOADS, function(err, results) {
|
||||
|
||||
if (err) {
|
||||
debug(err);
|
||||
deferred.reject(err);
|
||||
} else {
|
||||
|
||||
debug('All files checked');
|
||||
endTime = Date.now();
|
||||
debug('Redownload took %d ms', endTime - startTime);
|
||||
|
||||
var metrics = {};
|
||||
var offenders = {};
|
||||
|
||||
// Count requests
|
||||
offenders.totalRequests = listRequestsByType(results);
|
||||
metrics.totalRequests = offenders.totalRequests.total;
|
||||
|
||||
// Remove unwanted requests (redirections, about:blank)
|
||||
results = results.filter(function(result) {
|
||||
return (result !== null && result.weightCheck && result.weightCheck.bodySize > 0);
|
||||
});
|
||||
|
||||
|
||||
// Total weight
|
||||
offenders.totalWeight = listRequestWeight(results);
|
||||
metrics.totalWeight = offenders.totalWeight.totalWeight;
|
||||
|
||||
// Image compression
|
||||
offenders.imageOptimization = listImageNotOptimized(results);
|
||||
metrics.imageOptimization = offenders.imageOptimization.totalGain;
|
||||
|
||||
// File minification
|
||||
offenders.fileMinification = listFilesNotMinified(results);
|
||||
metrics.fileMinification = offenders.fileMinification.totalGain;
|
||||
|
||||
// Gzip compression
|
||||
offenders.gzipCompression = listFilesNotGzipped(results);
|
||||
metrics.gzipCompression = offenders.gzipCompression.totalGain;
|
||||
|
||||
// Small requests
|
||||
offenders.smallRequests = listSmallRequests(results);
|
||||
metrics.smallRequests = offenders.smallRequests.total;
|
||||
|
||||
data.toolsResults.weightChecker = {
|
||||
metrics: metrics,
|
||||
offenders: offenders
|
||||
};
|
||||
|
||||
deferred.resolve(data);
|
||||
}
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
|
||||
function listRequestWeight(requests) {
|
||||
var results = {
|
||||
totalWeight: 0,
|
||||
byType: {
|
||||
html: {
|
||||
totalWeight: 0,
|
||||
requests: []
|
||||
},
|
||||
css: {
|
||||
totalWeight: 0,
|
||||
requests: []
|
||||
},
|
||||
js: {
|
||||
totalWeight: 0,
|
||||
requests: []
|
||||
},
|
||||
json: {
|
||||
totalWeight: 0,
|
||||
requests: []
|
||||
},
|
||||
image: {
|
||||
totalWeight: 0,
|
||||
requests: []
|
||||
},
|
||||
video: {
|
||||
totalWeight: 0,
|
||||
requests: []
|
||||
},
|
||||
webfont: {
|
||||
totalWeight: 0,
|
||||
requests: []
|
||||
},
|
||||
other: {
|
||||
totalWeight: 0,
|
||||
requests: []
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
requests.forEach(function(req) {
|
||||
var weight = ((typeof req.weightCheck.bodySize === 'number') ? req.weightCheck.bodySize + req.weightCheck.headersSize : req.contentLength) || 0;
|
||||
var type = req.type || 'other';
|
||||
|
||||
results.totalWeight += weight;
|
||||
results.byType[type].totalWeight += weight;
|
||||
|
||||
results.byType[type].requests.push({
|
||||
url: req.url,
|
||||
weight: weight
|
||||
});
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
function listImageNotOptimized(requests) {
|
||||
var results = {
|
||||
totalGain: 0,
|
||||
images: []
|
||||
};
|
||||
|
||||
requests.forEach(function(req) {
|
||||
if (req.weightCheck.bodySize > 0 && imageOptimizer.entryTypeCanBeOptimized(req) && req.weightCheck.isOptimized === false) {
|
||||
var before = req.weightCheck.afterCompression || req.weightCheck.bodySize;
|
||||
var after = req.weightCheck.afterOptimizationAndCompression || req.weightCheck.optimized;
|
||||
var gain = before - after;
|
||||
|
||||
if (gain > 200) {
|
||||
results.totalGain += gain;
|
||||
|
||||
results.images.push({
|
||||
url: req.url,
|
||||
original: req.weightCheck.bodySize,
|
||||
isCompressed: req.weightCheck.isCompressed,
|
||||
afterCompression: req.weightCheck.afterCompression,
|
||||
afterOptimizationAndCompression: req.weightCheck.afterOptimizationAndCompression,
|
||||
lossless: req.weightCheck.lossless,
|
||||
lossy: req.weightCheck.lossy,
|
||||
gain: gain
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
function listFilesNotMinified(requests) {
|
||||
var results = {
|
||||
totalGain: 0,
|
||||
files: []
|
||||
};
|
||||
|
||||
requests.forEach(function(req) {
|
||||
if (req.weightCheck.bodySize > 0 && fileMinifier.entryTypeCanBeMinified(req) && req.weightCheck.isOptimized === false) {
|
||||
var before = req.weightCheck.afterCompression || req.weightCheck.bodySize;
|
||||
var after = req.weightCheck.afterOptimizationAndCompression || req.weightCheck.optimized;
|
||||
var gain = before - after;
|
||||
|
||||
if (gain > 200) {
|
||||
results.totalGain += gain;
|
||||
|
||||
results.files.push({
|
||||
url: req.url,
|
||||
original: req.weightCheck.bodySize,
|
||||
isCompressed: req.weightCheck.isCompressed,
|
||||
afterCompression: req.weightCheck.afterCompression,
|
||||
afterOptimizationAndCompression: req.weightCheck.afterOptimizationAndCompression,
|
||||
optimized: req.weightCheck.optimized,
|
||||
gain: gain
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function listFilesNotGzipped(requests) {
|
||||
var results = {
|
||||
totalGain: 0,
|
||||
files: []
|
||||
};
|
||||
|
||||
requests.forEach(function(req) {
|
||||
if (req.weightCheck.uncompressedSize && req.weightCheck.isCompressed === false && req.weightCheck.afterCompression) {
|
||||
var gain = req.weightCheck.uncompressedSize - req.weightCheck.afterCompression;
|
||||
|
||||
results.totalGain += gain;
|
||||
|
||||
results.files.push({
|
||||
url: req.url,
|
||||
original: req.weightCheck.uncompressedSize,
|
||||
gzipped: req.weightCheck.afterCompression,
|
||||
gain: gain
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function listRequestsByType(requests) {
|
||||
var results = {
|
||||
total: 0,
|
||||
byType: {
|
||||
html: [],
|
||||
css: [],
|
||||
js: [],
|
||||
json: [],
|
||||
image: [],
|
||||
video: [],
|
||||
webfont: [],
|
||||
other: []
|
||||
}
|
||||
};
|
||||
|
||||
requests.forEach(function(req) {
|
||||
if (req.url !== 'about:blank') {
|
||||
var type = req.type || 'other';
|
||||
results.byType[type].push(req.url);
|
||||
results.total ++;
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function listSmallRequests(requests) {
|
||||
var results = {
|
||||
total: 0,
|
||||
byType: {
|
||||
css: [],
|
||||
js: [],
|
||||
image: []
|
||||
}
|
||||
};
|
||||
|
||||
requests.forEach(function(req) {
|
||||
if (req.weightCheck.bodySize > 0 && req.weightCheck.bodySize < 2048) {
|
||||
if (req.isCSS || req.isJS || req.isImage) {
|
||||
results.byType[req.type].push({
|
||||
url: req.url,
|
||||
size: req.weightCheck.bodySize
|
||||
});
|
||||
results.total ++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
function redownloadEntry(entry) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
function downloadError(message) {
|
||||
debug('Could not download %s Error: %s', entry.url, message);
|
||||
entry.weightCheck = {
|
||||
message: message
|
||||
};
|
||||
deferred.resolve(entry);
|
||||
}
|
||||
|
||||
// Not downloaded again but will be counted in totalWeight
|
||||
function notDownloadableFile(message) {
|
||||
entry.weightCheck = {
|
||||
message: message
|
||||
};
|
||||
deferred.resolve(entry);
|
||||
}
|
||||
|
||||
// Not counted in totalWeight
|
||||
function unwantedFile(message) {
|
||||
debug(message);
|
||||
deferred.resolve(entry);
|
||||
}
|
||||
|
||||
if (entry.method !== 'GET') {
|
||||
notDownloadableFile('only downloading GET');
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
if (entry.status !== 200) {
|
||||
unwantedFile('only downloading requests with status code 200');
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
if (entry.url === 'about:blank') {
|
||||
unwantedFile('not downloading about:blank');
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
|
||||
debug('Downloading %s', entry.url);
|
||||
|
||||
// Always add a gzip header before sending, in case the server listens to it
|
||||
var reqHeaders = entry.requestHeaders;
|
||||
reqHeaders['Accept-Encoding'] = 'gzip, deflate';
|
||||
|
||||
var requestOptions = {
|
||||
method: entry.method,
|
||||
url: entry.url,
|
||||
headers: reqHeaders,
|
||||
timeout: REQUEST_TIMEOUT
|
||||
};
|
||||
|
||||
download(requestOptions, entry.contentType, function(error, result) {
|
||||
if (error) {
|
||||
if (error.code === 'ETIMEDOUT') {
|
||||
downloadError('timeout after ' + REQUEST_TIMEOUT + 'ms');
|
||||
} else {
|
||||
downloadError('error while downloading: ' + error.code);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
debug('%s downloaded correctly', entry.url);
|
||||
|
||||
entry.weightCheck = result;
|
||||
deferred.resolve(entry);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
// Inspired by https://github.com/cvan/fastHAR-api/blob/10cec585/app.js
|
||||
function download(requestOptions, contentType, callback) {
|
||||
|
||||
var statusCode;
|
||||
|
||||
request(requestOptions)
|
||||
|
||||
.on('response', function(res) {
|
||||
|
||||
// Raw headers were added in NodeJS v0.12
|
||||
// (https://github.com/joyent/node/issues/4844), but let's
|
||||
// reconstruct them for backwards compatibility.
|
||||
var rawHeaders = ('HTTP/' + res.httpVersion + ' ' + res.statusCode +
|
||||
' ' + http.STATUS_CODES[res.statusCode] + '\r\n');
|
||||
Object.keys(res.headers).forEach(function(headerKey) {
|
||||
rawHeaders += headerKey + ': ' + res.headers[headerKey] + '\r\n';
|
||||
});
|
||||
rawHeaders += '\r\n';
|
||||
|
||||
var uncompressedSize = 0; // size after uncompression
|
||||
var bodySize = 0; // bytes size over the wire
|
||||
var body = ''; // plain text body (after uncompressing gzip/deflate)
|
||||
var isCompressed = false;
|
||||
|
||||
function tally() {
|
||||
|
||||
if (statusCode !== 200) {
|
||||
callback({code: statusCode});
|
||||
return;
|
||||
}
|
||||
|
||||
var result = {
|
||||
body: body,
|
||||
headersSize: Buffer.byteLength(rawHeaders, 'utf8'),
|
||||
bodySize: bodySize,
|
||||
isCompressed: isCompressed,
|
||||
uncompressedSize: uncompressedSize
|
||||
};
|
||||
|
||||
callback(null, result);
|
||||
}
|
||||
|
||||
switch (res.headers['content-encoding']) {
|
||||
case 'gzip':
|
||||
|
||||
var gzip = zlib.createGunzip();
|
||||
|
||||
gzip.on('data', function (data) {
|
||||
body += data;
|
||||
uncompressedSize += data.length;
|
||||
}).on('end', function () {
|
||||
isCompressed = true;
|
||||
tally();
|
||||
}).on('error', function(err) {
|
||||
debug(err);
|
||||
});
|
||||
|
||||
res.on('data', function (data) {
|
||||
bodySize += data.length;
|
||||
}).pipe(gzip);
|
||||
|
||||
break;
|
||||
case 'deflate':
|
||||
res.setEncoding('utf8');
|
||||
|
||||
var deflate = zlib.createInflate();
|
||||
|
||||
deflate.on('data', function (data) {
|
||||
body += data;
|
||||
uncompressedSize += data.length;
|
||||
}).on('end', function () {
|
||||
isCompressed = true;
|
||||
tally();
|
||||
}).on('error', function(err) {
|
||||
debug(err);
|
||||
});
|
||||
|
||||
res.on('data', function (data) {
|
||||
bodySize += data.length;
|
||||
}).pipe(deflate);
|
||||
|
||||
break;
|
||||
default:
|
||||
if (contentType === 'image/jpeg' || contentType === 'image/png') {
|
||||
res.setEncoding('binary');
|
||||
}
|
||||
|
||||
res.on('data', function (data) {
|
||||
body += data;
|
||||
uncompressedSize += data.length;
|
||||
bodySize += data.length;
|
||||
}).on('end', function () {
|
||||
tally();
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
})
|
||||
|
||||
.on('response', function(response) {
|
||||
statusCode = response.statusCode;
|
||||
})
|
||||
|
||||
.on('error', function(err) {
|
||||
callback(err);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
recheckAllFiles: recheckAllFiles,
|
||||
listRequestWeight: listRequestWeight,
|
||||
redownloadEntry: redownloadEntry,
|
||||
download: download
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = new WeightChecker();
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "yellowlabtools",
|
||||
"version": "1.3.2",
|
||||
"version": "1.7.0",
|
||||
"description": "Online tool to audit a webpage for performance and front-end quality issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -11,46 +11,51 @@
|
||||
},
|
||||
"main": "./lib/index.js",
|
||||
"dependencies": {
|
||||
"async": "~0.9.0",
|
||||
"body-parser": "~1.12.1",
|
||||
"compression": "~1.4.3",
|
||||
"cors": "^2.5.3",
|
||||
"debug": "~2.1.3",
|
||||
"express": "~4.12.2",
|
||||
"lwip": "0.0.6",
|
||||
"meow": "^3.0.0",
|
||||
"phantomas": "1.10.0",
|
||||
"ps-node": "0.0.3",
|
||||
"q": "~1.1.2",
|
||||
"rimraf": "~2.3.2",
|
||||
"temporary": "0.0.8"
|
||||
"async": "1.2.1",
|
||||
"body-parser": "1.13.1",
|
||||
"clean-css": "3.3.0",
|
||||
"compression": "1.5.0",
|
||||
"cors": "2.7.1",
|
||||
"debug": "2.2.0",
|
||||
"express": "4.12.4",
|
||||
"imagemin": "3.2.0",
|
||||
"imagemin-jpegoptim": "4.0.0",
|
||||
"lwip": "0.0.7",
|
||||
"meow": "3.3.0",
|
||||
"minimize": "1.4.1",
|
||||
"phantomas": "1.11.0",
|
||||
"ps-node": "0.0.4",
|
||||
"q": "1.4.1",
|
||||
"rimraf": "2.4.0",
|
||||
"temporary": "0.0.8",
|
||||
"uglify-js": "2.4.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^2.1.2",
|
||||
"chai": "^3.0.0",
|
||||
"grunt": "^0.4.5",
|
||||
"grunt-blanket": "^0.0.8",
|
||||
"grunt-contrib-clean": "^0.6.0",
|
||||
"grunt-contrib-concat": "^0.5.1",
|
||||
"grunt-contrib-copy": "^0.8.0",
|
||||
"grunt-contrib-cssmin": "^0.12.2",
|
||||
"grunt-contrib-cssmin": "^0.12.3",
|
||||
"grunt-contrib-htmlmin": "^0.4.0",
|
||||
"grunt-contrib-jshint": "^0.11.0",
|
||||
"grunt-contrib-less": "^1.0.0",
|
||||
"grunt-contrib-uglify": "^0.8.0",
|
||||
"grunt-contrib-jshint": "^0.11.2",
|
||||
"grunt-contrib-less": "^1.0.1",
|
||||
"grunt-contrib-uglify": "^0.9.1",
|
||||
"grunt-env": "^0.4.4",
|
||||
"grunt-express": "^1.4.1",
|
||||
"grunt-filerev": "^2.1.2",
|
||||
"grunt-fontsmith": "^0.9.1",
|
||||
"grunt-filerev": "^2.3.1",
|
||||
"grunt-inline-angular-templates": "^0.1.5",
|
||||
"grunt-line-remover": "^0.0.2",
|
||||
"grunt-mocha-test": "^0.12.7",
|
||||
"grunt-replace": "^0.8.0",
|
||||
"grunt-replace": "^0.9.2",
|
||||
"grunt-usemin": "^3.0.0",
|
||||
"grunt-webfont": "^0.5.3",
|
||||
"matchdep": "^0.3.0",
|
||||
"mocha": "^2.2.1",
|
||||
"request": "^2.53.0",
|
||||
"sinon": "^1.14.0",
|
||||
"sinon-chai": "^2.7.0"
|
||||
"mocha": "^2.2.5",
|
||||
"request": "^2.58.0",
|
||||
"sinon": "^1.15.3",
|
||||
"sinon-chai": "^2.8.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "grunt test"
|
||||
|
||||
@@ -12,7 +12,7 @@ sudo apt-get install -y nodejs
|
||||
source ~/.profile
|
||||
|
||||
# Installation of some packages globally
|
||||
npm install bower forever grunt-cli phantomjs -g
|
||||
npm install bower forever grunt-cli -g
|
||||
source ~/.profile
|
||||
|
||||
# Installation of YellowLabTools
|
||||
|
||||
@@ -18,8 +18,6 @@ bower install --config.interactive=false --allow-root
|
||||
|
||||
# Front-end compilation
|
||||
rm -rf front/build
|
||||
npm install grunt-cli -g
|
||||
npm install phantomjs -g
|
||||
grunt build
|
||||
|
||||
# Restart the server
|
||||
|
||||
@@ -96,7 +96,12 @@ describe('api', function() {
|
||||
body: {
|
||||
url: wwwUrl + '/simple-page.html',
|
||||
waitForResponse: true,
|
||||
screenshot: true
|
||||
screenshot: true,
|
||||
device: 'tablet',
|
||||
//waitForSelector: '*',
|
||||
cookie: 'foo=bar',
|
||||
authUser: 'joe',
|
||||
authPass: 'secret'
|
||||
},
|
||||
json: true,
|
||||
headers: {
|
||||
@@ -164,6 +169,13 @@ describe('api', function() {
|
||||
body.should.have.a.property('javascriptExecutionTree').that.is.an('object');
|
||||
body.javascriptExecutionTree.should.deep.equal({});
|
||||
|
||||
// Check if settings are correctly sent and retrieved
|
||||
body.params.options.should.have.a.property('device').that.equals('tablet');
|
||||
//body.params.options.should.have.a.property('waitForSelector').that.equals('*');
|
||||
body.params.options.should.have.a.property('cookie').that.equals('foo=bar');
|
||||
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
|
||||
@@ -532,6 +544,110 @@ describe('api', function() {
|
||||
});
|
||||
|
||||
|
||||
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.have.a.property('javascriptExecutionTree').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.have.a.property('javascriptExecutionTree').that.is.an('object');
|
||||
|
||||
body.should.not.have.a.property('params').that.is.an('object');
|
||||
body.should.not.have.a.property('toolsResults').that.is.an('object');
|
||||
|
||||
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('javascriptExecutionTree').that.is.an('object');
|
||||
body.should.have.a.property('params').that.is.an('object');
|
||||
body.should.have.a.property('toolsResults').that.is.an('object');
|
||||
|
||||
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('javascriptExecutionTree').that.is.an('object');
|
||||
body.should.have.a.property('params').that.is.an('object');
|
||||
body.should.have.a.property('toolsResults').that.is.an('object');
|
||||
|
||||
done();
|
||||
|
||||
} else {
|
||||
done(error || response.statusCode);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should retrieve the screenshot', function(done) {
|
||||
this.timeout(5000);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
var should = require('chai').should();
|
||||
var rulesChecker = require('../../lib/rulesChecker');
|
||||
|
||||
describe('rulesChecker', function() {
|
||||
describe('customPolicies', function() {
|
||||
|
||||
var policies = require('../../lib/metadata/policies.js');
|
||||
var results;
|
||||
@@ -70,131 +70,6 @@ describe('rulesChecker', function() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should transform DOMinserts offenders', function() {
|
||||
results = rulesChecker.check({
|
||||
"toolsResults": {
|
||||
"phantomas": {
|
||||
"metrics": {
|
||||
"DOMinserts": 4
|
||||
},
|
||||
"offenders": {
|
||||
"DOMinserts": [
|
||||
"\"div\" appended to \"html\"",
|
||||
"\"DocumentFragment > link[0]\" appended to \"head\"",
|
||||
"\"div#Netaff-yh1XbS0vK3NaRGu\" appended to \"body > div#Global\"",
|
||||
"\"img\" appended to \"body\""
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}, policies);
|
||||
|
||||
results.should.have.a.property('DOMinserts');
|
||||
results.DOMinserts.should.have.a.property('offendersObj').that.deep.equals({
|
||||
"count": 4,
|
||||
"list": [
|
||||
{
|
||||
"insertedElement": {
|
||||
"type": "createdElement",
|
||||
"element": "div"
|
||||
},
|
||||
"receiverElement": {
|
||||
"type": "html"
|
||||
}
|
||||
},
|
||||
{
|
||||
"insertedElement": {
|
||||
"type": "fragmentElement",
|
||||
"element": "link[0]",
|
||||
"tree": {
|
||||
"DocumentFragment": {
|
||||
"link[0]": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"receiverElement": {
|
||||
"type": "head"
|
||||
}
|
||||
},
|
||||
{
|
||||
"insertedElement": {
|
||||
"type": "createdElement",
|
||||
"element": "div#Netaff-yh1XbS0vK3NaRGu"
|
||||
},
|
||||
"receiverElement": {
|
||||
"type": "domElement",
|
||||
"element": "div#Global",
|
||||
"tree": {
|
||||
"body": {
|
||||
"div#Global": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"insertedElement": {
|
||||
"type": "createdElement",
|
||||
"element": "img"
|
||||
},
|
||||
"receiverElement": {
|
||||
"type": "body"
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should transform DOMqueriesWithoutResults offenders', function() {
|
||||
results = rulesChecker.check({
|
||||
"toolsResults": {
|
||||
"phantomas": {
|
||||
"metrics": {
|
||||
"DOMqueriesWithoutResults": 2
|
||||
},
|
||||
"offenders": {
|
||||
"DOMqueriesWithoutResults": [
|
||||
"#SearchMenu (in #document) using getElementById",
|
||||
".partnership-link (in body > div#Global > div#Header > ul#MainMenu) using getElementsByClassName"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}, policies);
|
||||
|
||||
results.should.have.a.property('DOMqueriesWithoutResults');
|
||||
results.DOMqueriesWithoutResults.should.have.a.property('offendersObj').that.deep.equals({
|
||||
"count": 2,
|
||||
"list": [
|
||||
{
|
||||
"context": {
|
||||
"type": "document"
|
||||
},
|
||||
"fn": "getElementById",
|
||||
"query": "#SearchMenu "
|
||||
},
|
||||
{
|
||||
"context": {
|
||||
"element": "ul#MainMenu",
|
||||
"tree": {
|
||||
"body": {
|
||||
"div#Global": {
|
||||
"div#Header": {
|
||||
"ul#MainMenu": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "domElement"
|
||||
},
|
||||
"fn": "getElementsByClassName",
|
||||
"query": ".partnership-link "
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should transform DOMqueriesAvoidable offenders', function() {
|
||||
results = rulesChecker.check({
|
||||
@@ -246,61 +121,6 @@ describe('rulesChecker', function() {
|
||||
});
|
||||
|
||||
|
||||
it('should transform eventsBound offenders', function() {
|
||||
results = rulesChecker.check({
|
||||
"toolsResults": {
|
||||
"phantomas": {
|
||||
"metrics": {
|
||||
"eventsBound": 2
|
||||
},
|
||||
"offenders": {
|
||||
"eventsBound": [
|
||||
"\"DOMContentLoaded\" bound to \"#document\"",
|
||||
"\"unload\" bound to \"window\"",
|
||||
"\"submit\" bound to \"body > div#Global > div#Header > form#search_mini_form\""
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}, policies);
|
||||
|
||||
results.should.have.a.property('eventsBound');
|
||||
results.eventsBound.should.have.a.property('offendersObj').that.deep.equals({
|
||||
"count": 3,
|
||||
"list": [
|
||||
{
|
||||
"element": {
|
||||
"type": "document"
|
||||
},
|
||||
"eventName": "DOMContentLoaded"
|
||||
},
|
||||
{
|
||||
"element": {
|
||||
"type": "window"
|
||||
},
|
||||
"eventName": "unload"
|
||||
},
|
||||
{
|
||||
"element": {
|
||||
"element": "form#search_mini_form",
|
||||
"tree": {
|
||||
"body": {
|
||||
"div#Global": {
|
||||
"div#Header": {
|
||||
"form#search_mini_form": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "domElement"
|
||||
},
|
||||
"eventName": "submit"
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should transform jsErrors offenders', function() {
|
||||
results = rulesChecker.check({
|
||||
"toolsResults": {
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
var should = require('chai').should();
|
||||
var fileMinifier = require('../../lib/tools/weightChecker/fileMinifier');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
||||
describe('fileMinifier', function() {
|
||||
|
||||
it('should minify a JS file with minifyJs', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/unminified-script.js'));
|
||||
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
fileMinifier.minifyJs(fileContent.toString()).then(function(newFile) {
|
||||
var newFileSize = newFile.length;
|
||||
newFileSize.should.be.below(fileSize);
|
||||
done();
|
||||
}).fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should minify a JS file with minifyFile', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/unminified-script.js'));
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/unminified-script.js',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*',
|
||||
'Accept-Encoding': 'gzip, deflate'
|
||||
},
|
||||
status: 200,
|
||||
isJS: true,
|
||||
type: 'js',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
body: fileContent.toString('utf8'),
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize
|
||||
}
|
||||
};
|
||||
|
||||
fileMinifier.minifyFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.have.a.property('isOptimized').that.equals(false);
|
||||
newEntry.weightCheck.should.have.a.property('optimized').that.is.below(fileSize);
|
||||
newEntry.weightCheck.should.have.a.property('bodyAfterOptimization').that.is.a('string');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail minifying an already minified JS', function(done) {
|
||||
this.timeout(5000);
|
||||
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jquery1.8.3.js'));
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/jquery1.8.3.js',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*',
|
||||
'Accept-Encoding': 'gzip, deflate'
|
||||
},
|
||||
status: 200,
|
||||
isJS: true,
|
||||
type: 'js',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
body: fileContent.toString('utf8'),
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize
|
||||
}
|
||||
};
|
||||
|
||||
fileMinifier.minifyFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.not.have.a.property('isOptimized');
|
||||
newEntry.weightCheck.should.not.have.a.property('optimized');
|
||||
newEntry.weightCheck.should.not.have.a.property('bodyAfterOptimization');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail minifying a JS with syntax errors', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/svg-image.svg'));
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/svg-image.svg',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*',
|
||||
'Accept-Encoding': 'gzip, deflate'
|
||||
},
|
||||
status: 200,
|
||||
isJS: true,
|
||||
type: 'js',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
body: fileContent.toString('utf8'),
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize
|
||||
}
|
||||
};
|
||||
|
||||
fileMinifier.minifyFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.not.have.a.property('isOptimized');
|
||||
newEntry.weightCheck.should.not.have.a.property('optimized');
|
||||
newEntry.weightCheck.should.not.have.a.property('bodyAfterOptimization');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should minify a CSS file with clean-css', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/unminified-stylesheet.css'));
|
||||
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
fileMinifier.minifyCss(fileContent.toString()).then(function(newFile) {
|
||||
var newFileSize = newFile.length;
|
||||
newFileSize.should.be.below(fileSize);
|
||||
done();
|
||||
}).fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should minify a CSS file with minifyFile', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/unminified-stylesheet.css'));
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/unminified-stylesheet.css',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*',
|
||||
'Accept-Encoding': 'gzip, deflate'
|
||||
},
|
||||
status: 200,
|
||||
isCSS: true,
|
||||
type: 'css',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
body: fileContent.toString('utf8'),
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize
|
||||
}
|
||||
};
|
||||
|
||||
fileMinifier.minifyFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.have.a.property('isOptimized').that.equals(false);
|
||||
newEntry.weightCheck.should.have.a.property('optimized').that.is.below(fileSize);
|
||||
newEntry.weightCheck.should.have.a.property('bodyAfterOptimization').that.is.a('string');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should minify an HTML file with ', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jquery-page.html'));
|
||||
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
fileMinifier.minifyHtml(fileContent.toString()).then(function(newFile) {
|
||||
var newFileSize = newFile.length;
|
||||
newFileSize.should.be.below(fileSize);
|
||||
done();
|
||||
}).fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should avoid minifying some JS files known as minified', function() {
|
||||
fileMinifier.isKnownAsMinified('https://platform.twitter.com/widgets.js').should.equal(true);
|
||||
fileMinifier.isKnownAsMinified('http://platform.twitter.com/widgets.js').should.equal(true);
|
||||
|
||||
fileMinifier.isKnownAsMinified('https://connect.facebook.net/fr_FR/sdk.js').should.equal(true);
|
||||
fileMinifier.isKnownAsMinified('https://connect.facebook.net/en_EN/sdk.js').should.equal(true);
|
||||
fileMinifier.isKnownAsMinified('https://connect.facebook.net/fr_FR/all.js').should.equal(true);
|
||||
fileMinifier.isKnownAsMinified('https://connect.facebook.net/en_EN/all.js').should.equal(true);
|
||||
|
||||
fileMinifier.isKnownAsMinified('https://apis.google.com/js/plusone.js').should.equal(true);
|
||||
|
||||
fileMinifier.isKnownAsMinified('https://code.jquery.com/jquery-2.1.4.min.js').should.equal(true);
|
||||
fileMinifier.isKnownAsMinified('http://code.jquery.com/jquery-2.1.4.min.js').should.equal(true);
|
||||
fileMinifier.isKnownAsMinified('https://code.jquery.com/jquery-2.1.4.js').should.equal(false);
|
||||
fileMinifier.isKnownAsMinified('http://code.jquery.com/jquery-2.1.4.js').should.equal(false);
|
||||
|
||||
fileMinifier.isKnownAsMinified('https://ssl.google-analytics.com/ga.js').should.equal(true);
|
||||
fileMinifier.isKnownAsMinified('http://www.google-analytics.com/ga.js').should.equal(true);
|
||||
fileMinifier.isKnownAsMinified('https://www.google-analytics.com/analytics.js').should.equal(true);
|
||||
fileMinifier.isKnownAsMinified('http://www.google-analytics.com/analytics.js').should.equal(true);
|
||||
|
||||
fileMinifier.isKnownAsMinified('http://anydomain.com/anyurl').should.equal(false);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,583 @@
|
||||
var should = require('chai').should();
|
||||
var gzipCompressor = require('../../lib/tools/weightChecker/gzipCompressor');
|
||||
var fileMinifier = require('../../lib/tools/weightChecker/fileMinifier');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
||||
describe('gzipCompressor', function() {
|
||||
|
||||
var minifiedJSContent = fs.readFileSync(path.resolve(__dirname, '../www/minified-script.js'));
|
||||
var notMinifiedJSContent = fs.readFileSync(path.resolve(__dirname, '../www/unminified-script.js'));
|
||||
var someTextFileContent = fs.readFileSync(path.resolve(__dirname, '../www/svg-image.svg'));
|
||||
|
||||
|
||||
it('should gzip a JS file that was not gziped but was minified', function(done) {
|
||||
var fileContent = minifiedJSContent;
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/minified-script.js',
|
||||
status: 200,
|
||||
isJS: true,
|
||||
type: 'js',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
body: fileContent.toString('utf8'),
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize,
|
||||
isOptimized: true
|
||||
}
|
||||
};
|
||||
|
||||
gzipCompressor.compressFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize);
|
||||
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should gzip a JS file that was not gziped and not minified', function(done) {
|
||||
/*jshint expr: true*/
|
||||
|
||||
var fileContent = notMinifiedJSContent;
|
||||
var minifiedContent = minifiedJSContent;
|
||||
|
||||
var fileSize = fileContent.length;
|
||||
var minifiedSize = minifiedContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/unminified-script.js',
|
||||
status: 200,
|
||||
isJS: true,
|
||||
type: 'js',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
body: fileContent.toString('utf8'),
|
||||
bodyAfterOptimization: minifiedContent.toString('utf8'),
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize,
|
||||
isOptimized: false,
|
||||
optimized: minifiedSize
|
||||
}
|
||||
};
|
||||
|
||||
gzipCompressor.compressFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize);
|
||||
newEntry.weightCheck.should.have.a.property('afterOptimizationAndCompression').that.is.not.undefined;
|
||||
newEntry.weightCheck.should.have.a.property('afterOptimizationAndCompression').that.is.below(newEntry.weightCheck.afterCompression);
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should gzip a JS file that is gziped but not minified', function(done) {
|
||||
/*jshint expr: true*/
|
||||
|
||||
var fileContent = notMinifiedJSContent;
|
||||
var minifiedContent = minifiedJSContent;
|
||||
var fileSize = 6436;
|
||||
var gzipedSize = 2646;
|
||||
var minifiedSize = 1954;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/unminified-script.js',
|
||||
status: 200,
|
||||
isJS: true,
|
||||
type: 'js',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
body: fileContent.toString('utf8'),
|
||||
bodyAfterOptimization: minifiedContent.toString('utf8'),
|
||||
totalWeight: gzipedSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: gzipedSize,
|
||||
isCompressed: true,
|
||||
uncompressedSize: fileSize,
|
||||
isOptimized: false,
|
||||
optimized: minifiedSize
|
||||
}
|
||||
};
|
||||
|
||||
gzipCompressor.compressFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.have.a.property('afterOptimizationAndCompression').that.is.not.undefined;
|
||||
newEntry.weightCheck.should.have.a.property('afterOptimizationAndCompression').that.is.below(gzipedSize);
|
||||
newEntry.weightCheck.should.have.a.property('afterOptimizationAndCompression').that.is.below(minifiedSize);
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not gzip a JS file that was gziped and minified', function(done) {
|
||||
/*jshint expr: true*/
|
||||
|
||||
var fileContent = notMinifiedJSContent;
|
||||
var fileSize = 6436;
|
||||
var gzipedSize = 2646;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/unminified-script.js',
|
||||
status: 200,
|
||||
isJS: true,
|
||||
type: 'js',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
body: fileContent.toString('utf8'),
|
||||
totalWeight: gzipedSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: gzipedSize,
|
||||
isCompressed: true,
|
||||
uncompressedSize: fileSize,
|
||||
isOptimized: true
|
||||
}
|
||||
};
|
||||
|
||||
gzipCompressor.compressFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.not.have.a.property('minified');
|
||||
newEntry.weightCheck.should.not.have.a.property('bodyAfterOptimization');
|
||||
newEntry.weightCheck.should.not.have.a.property('afterCompression');
|
||||
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should gzip a CSS file', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/unminified-stylesheet.css'));
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/unminified-stylesheet.css',
|
||||
status: 200,
|
||||
isCSS: true,
|
||||
type: 'css',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
body: fileContent.toString('utf8'),
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize,
|
||||
isOptimized: true
|
||||
}
|
||||
};
|
||||
|
||||
gzipCompressor.compressFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize);
|
||||
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should gzip an HTML file', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jquery-page.html'));
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/jquery-page.html',
|
||||
status: 200,
|
||||
isHTML: true,
|
||||
type: 'html',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
body: fileContent.toString('utf8'),
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize,
|
||||
isOptimized: true
|
||||
}
|
||||
};
|
||||
|
||||
gzipCompressor.compressFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize);
|
||||
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should gzip an SVG file', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/svg-image.svg'));
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/svg-image.svg',
|
||||
status: 200,
|
||||
isImage: true,
|
||||
isSVG: true,
|
||||
type: 'image',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
body: fileContent.toString('utf8'),
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize,
|
||||
isOptimized: true
|
||||
}
|
||||
};
|
||||
|
||||
gzipCompressor.compressFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize);
|
||||
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should gzip an XML file', function(done) {
|
||||
var fileContent = someTextFileContent; // it dosn't matter if it's not the correct file type
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/someTextFile.xml',
|
||||
status: 200,
|
||||
isXML: true,
|
||||
type: 'xml',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
body: fileContent.toString('utf8'),
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize,
|
||||
isOptimized: true
|
||||
}
|
||||
};
|
||||
|
||||
gzipCompressor.compressFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize);
|
||||
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should gzip a JSON file', function(done) {
|
||||
var fileContent = someTextFileContent; // it dosn't matter if it's not the correct file type
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/someTextFile.json',
|
||||
status: 200,
|
||||
isJSON: true,
|
||||
type: 'json',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
body: fileContent.toString('utf8'),
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize,
|
||||
isOptimized: true
|
||||
}
|
||||
};
|
||||
|
||||
gzipCompressor.compressFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize);
|
||||
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should gzip a TTF file', function(done) {
|
||||
var fileContent = someTextFileContent; // it dosn't matter if it's not the correct file type
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/someTextFile.ttf',
|
||||
status: 200,
|
||||
isWebFont: true,
|
||||
isTTF: true,
|
||||
type: 'webfont',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
body: fileContent.toString('utf8'),
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize,
|
||||
isOptimized: true
|
||||
}
|
||||
};
|
||||
|
||||
gzipCompressor.compressFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize);
|
||||
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should gzip a favicon file', function(done) {
|
||||
var fileContent = someTextFileContent; // it dosn't matter if it's not the correct file type
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/someTextFile.ico',
|
||||
status: 200,
|
||||
isFavicon: true,
|
||||
type: 'favicon',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
body: fileContent.toString('utf8'),
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize,
|
||||
isOptimized: true
|
||||
}
|
||||
};
|
||||
|
||||
gzipCompressor.compressFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize);
|
||||
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not gzip a JPEG file', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jpeg-image.jpg'));
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/jpeg-image.jpg',
|
||||
status: 200,
|
||||
isImage: true,
|
||||
type: 'image',
|
||||
contentType: 'image/jpeg',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
body: fileContent.toString('utf8'),
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize,
|
||||
isOptimized: true
|
||||
}
|
||||
};
|
||||
|
||||
gzipCompressor.compressFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.not.have.a.property('afterCompression');
|
||||
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should not gzip a PNG file', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/png-image.png'));
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/png-image.png',
|
||||
status: 200,
|
||||
isImage: true,
|
||||
type: 'image',
|
||||
contentType: 'image/png',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
body: fileContent.toString('utf8'),
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize,
|
||||
isOptimized: true
|
||||
}
|
||||
};
|
||||
|
||||
gzipCompressor.compressFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.not.have.a.property('afterCompression');
|
||||
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not gzip a GIF file', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/png-image.png')); // Fake gif, don't tell anyone...
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/gif-image.gif',
|
||||
status: 200,
|
||||
isImage: true,
|
||||
type: 'image',
|
||||
contentType: 'image/gif',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
body: fileContent.toString('utf8'),
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize,
|
||||
isOptimized: true
|
||||
}
|
||||
};
|
||||
|
||||
gzipCompressor.compressFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.not.have.a.property('afterCompression');
|
||||
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not gzip a WEBP file', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/png-image.png')); // Fake webp, don't tell anyone...
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/webp-image.webp',
|
||||
status: 200,
|
||||
isImage: true,
|
||||
type: 'image',
|
||||
contentType: 'image/webp',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
body: fileContent.toString('utf8'),
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize,
|
||||
isOptimized: true
|
||||
}
|
||||
};
|
||||
|
||||
gzipCompressor.compressFile(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.not.have.a.property('afterCompression');
|
||||
newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,321 @@
|
||||
var should = require('chai').should();
|
||||
var imageOptimizer = require('../../lib/tools/weightChecker/imageOptimizer');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
||||
describe('imageOptimizer', function() {
|
||||
|
||||
it('should optimize a JPEG image losslessly', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jpeg-image.jpg'));
|
||||
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
imageOptimizer.compressJpegLosslessly(fileContent).then(function(newFile) {
|
||||
var newFileSize = newFile.contents.length;
|
||||
newFileSize.should.be.below(fileSize);
|
||||
done();
|
||||
}).fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should optimize a JPEG image lossly', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jpeg-image.jpg'));
|
||||
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
imageOptimizer.compressJpegLossly(fileContent).then(function(newFile) {
|
||||
var newFileSize = newFile.contents.length;
|
||||
newFileSize.should.be.below(fileSize);
|
||||
done();
|
||||
}).fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should find the best optimization for a jpeg', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jpeg-image.jpg'));
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/an-image.jpg',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*',
|
||||
'Accept-Encoding': 'gzip, deflate'
|
||||
},
|
||||
status: 200,
|
||||
isImage: true,
|
||||
type: 'image',
|
||||
contentType: 'image/jpeg',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
body: fileContent,
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize
|
||||
}
|
||||
};
|
||||
|
||||
imageOptimizer.optimizeImage(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.have.a.property('isOptimized').that.equals(false);
|
||||
newEntry.weightCheck.should.have.a.property('lossless').that.is.below(fileSize);
|
||||
newEntry.weightCheck.should.have.a.property('lossy').that.is.below(newEntry.weightCheck.lossless);
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should optimize a PNG image losslessly', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/png-image.png'));
|
||||
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
imageOptimizer.compressPngLosslessly(fileContent).then(function(newFile) {
|
||||
var newFileSize = newFile.contents.length;
|
||||
newFileSize.should.be.below(fileSize);
|
||||
done();
|
||||
}).fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to optimize an already optimized PNG', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/logo-large.png'));
|
||||
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
imageOptimizer.compressPngLosslessly(fileContent).then(function(newFile) {
|
||||
var newFileSize = newFile.contents.length;
|
||||
newFileSize.should.equal(fileSize);
|
||||
done();
|
||||
}).fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to optimize a non-PNG', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/svg-image.svg'));
|
||||
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
imageOptimizer.compressPngLosslessly(fileContent).then(function(newFile) {
|
||||
var newFileSize = newFile.contents.length;
|
||||
newFileSize.should.equal(fileSize);
|
||||
done();
|
||||
}).fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should optimize a png', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/png-image.png'));
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/an-image.png',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*',
|
||||
'Accept-Encoding': 'gzip, deflate'
|
||||
},
|
||||
status: 200,
|
||||
isImage: true,
|
||||
type: 'image',
|
||||
contentType: 'image/png',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
body: fileContent,
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize
|
||||
}
|
||||
};
|
||||
|
||||
imageOptimizer.optimizeImage(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.have.a.property('isOptimized').that.equals(false);
|
||||
newEntry.weightCheck.should.have.a.property('lossless').that.is.below(fileSize);
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should optimize an SVG image losslessly', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/svg-image.svg'));
|
||||
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
imageOptimizer.compressSvgLosslessly(fileContent).then(function(newFile) {
|
||||
var newFileSize = newFile.contents.length;
|
||||
newFileSize.should.be.below(fileSize);
|
||||
done();
|
||||
}).fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should optimize an SVG', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/svg-image.svg'));
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/an-image.svg',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*',
|
||||
'Accept-Encoding': 'gzip, deflate'
|
||||
},
|
||||
status: 200,
|
||||
isImage: true,
|
||||
isSVG: true,
|
||||
type: 'image',
|
||||
contentType: 'image/svg+xml',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
body: fileContent,
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize
|
||||
}
|
||||
};
|
||||
|
||||
imageOptimizer.optimizeImage(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.have.a.property('isOptimized').that.equals(false);
|
||||
newEntry.weightCheck.should.have.a.property('lossless').that.is.below(fileSize);
|
||||
newEntry.weightCheck.should.have.a.property('optimized').that.equals(newEntry.weightCheck.lossless);
|
||||
newEntry.weightCheck.should.have.a.property('bodyAfterOptimization').that.is.a('string');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('shouldn\'t fail optimizing a corrupted jpeg', function(done) {
|
||||
|
||||
// In this test, we try to optimize a PNG but with a falsy "image/jpeg" content type
|
||||
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/png-image.png'));
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/an-image.png',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*',
|
||||
'Accept-Encoding': 'gzip, deflate'
|
||||
},
|
||||
status: 200,
|
||||
isImage: true,
|
||||
type: 'image',
|
||||
contentType: 'image/jpeg',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
body: fileContent,
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize
|
||||
}
|
||||
};
|
||||
|
||||
imageOptimizer.optimizeImage(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.not.have.a.property('isOptimized');
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('shouldn\'t fail optimizing a corrupted png', function(done) {
|
||||
|
||||
// In this test, we try to optimize a JPEG but with a falsy "image/png" content type
|
||||
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/jpeg-image.jpg'));
|
||||
var fileSize = fileContent.length;
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/an-image.jpg',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*',
|
||||
'Accept-Encoding': 'gzip, deflate'
|
||||
},
|
||||
status: 200,
|
||||
isImage: true,
|
||||
type: 'image',
|
||||
contentType: 'image/png',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
body: fileContent,
|
||||
totalWeight: fileSize + 200,
|
||||
headersSize: 200,
|
||||
bodySize: fileSize,
|
||||
isCompressed: false,
|
||||
uncompressedSize: fileSize
|
||||
}
|
||||
};
|
||||
|
||||
imageOptimizer.optimizeImage(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.not.have.a.property('isOptimized');
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should determine if gain is enough', function() {
|
||||
imageOptimizer.gainIsEnough(20000, 10000).should.equal(true);
|
||||
imageOptimizer.gainIsEnough(2000, 1000).should.equal(true);
|
||||
imageOptimizer.gainIsEnough(20000, 21000).should.equal(false);
|
||||
imageOptimizer.gainIsEnough(20000, 40000).should.equal(false);
|
||||
imageOptimizer.gainIsEnough(20000, 19500).should.equal(false);
|
||||
imageOptimizer.gainIsEnough(250, 120).should.equal(true);
|
||||
imageOptimizer.gainIsEnough(200, 120).should.equal(false);
|
||||
imageOptimizer.gainIsEnough(2000, 1900).should.equal(false);
|
||||
imageOptimizer.gainIsEnough(200000, 197000).should.equal(true);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -53,8 +53,9 @@ describe('index.js', function() {
|
||||
data.toolsResults.phantomas.metrics.should.have.a.property('requests').that.equals(1);
|
||||
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(1);
|
||||
data.toolsResults.phantomas.offenders.DOMelementMaxDepth.should.have.length(2);
|
||||
data.toolsResults.phantomas.offenders.DOMelementMaxDepth[0].should.equal('body > h1[0]');
|
||||
data.toolsResults.phantomas.offenders.DOMelementMaxDepth[1].should.equal('body > script[1]');
|
||||
|
||||
// Test rules
|
||||
data.should.have.a.property('rules').that.is.an('object');
|
||||
@@ -76,10 +77,11 @@ describe('index.js', function() {
|
||||
"score": 100,
|
||||
"abnormalityScore": 0,
|
||||
"offendersObj": {
|
||||
"count": 1,
|
||||
"count": 2,
|
||||
"tree": {
|
||||
"body": {
|
||||
"h1[0]": 1
|
||||
"h1[0]": 1,
|
||||
"script[1]": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,12 +93,18 @@ describe('index.js', function() {
|
||||
data.should.have.a.property('javascriptExecutionTree').that.is.an('object');
|
||||
data.javascriptExecutionTree.should.have.a.property('data');
|
||||
data.javascriptExecutionTree.data.should.have.a.property('type').that.equals('main');
|
||||
data.javascriptExecutionTree.data.should.have.a.property('domInteractive').that.is.a('number');
|
||||
data.javascriptExecutionTree.data.should.have.a.property('domContentLoaded').that.is.a('number');
|
||||
data.javascriptExecutionTree.data.should.have.a.property('domContentLoadedEnd').that.is.a('number');
|
||||
data.javascriptExecutionTree.data.should.have.a.property('domComplete').that.is.a('number');
|
||||
|
||||
/*jshint expr: true*/
|
||||
console.log.should.not.have.been.called;
|
||||
|
||||
console.log.restore();
|
||||
done();
|
||||
}).fail(function(err) {
|
||||
console.log.restore();
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -166,6 +166,40 @@ describe('offendersHelpers', function() {
|
||||
]);
|
||||
});
|
||||
|
||||
it('should transform another backtrace syntax into an array', function() {
|
||||
var result = offendersHelpers.backtraceToArray('phantomjs://webpage.evaluate():38 / e (http://s7.addthis.com/js/300/addthis_widget.js:1) / a (http://s7.addthis.com/js/300/addthis_widget.js:1) / http://s7.addthis.com/js/300/addthis_widget.js:3 / e (http://s7.addthis.com/js/300/addthis_widget.js:1) / http://s7.addthis.com/js/300/addthis_widget.js:8');
|
||||
|
||||
result.should.deep.equal([
|
||||
{
|
||||
file: 'phantomjs://webpage.evaluate()',
|
||||
line: 38
|
||||
},
|
||||
{
|
||||
functionName: 'e',
|
||||
file: 'http://s7.addthis.com/js/300/addthis_widget.js',
|
||||
line: 1
|
||||
},
|
||||
{
|
||||
functionName: 'a',
|
||||
file: 'http://s7.addthis.com/js/300/addthis_widget.js',
|
||||
line: 1
|
||||
},
|
||||
{
|
||||
file: 'http://s7.addthis.com/js/300/addthis_widget.js',
|
||||
line: 3
|
||||
},
|
||||
{
|
||||
functionName: 'e',
|
||||
file: 'http://s7.addthis.com/js/300/addthis_widget.js',
|
||||
line: 1
|
||||
},
|
||||
{
|
||||
file: 'http://s7.addthis.com/js/300/addthis_widget.js',
|
||||
line: 8
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return null if it fails', function() {
|
||||
var result = offendersHelpers.backtraceToArray('http://pouet.com/js/jquery.footer-transverse-min-v1.0.20.js:1 /http://pouet.com/js/main.js:1');
|
||||
|
||||
|
||||
@@ -42,7 +42,9 @@ describe('phantomasWrapper', function() {
|
||||
phantomasWrapper.execute({
|
||||
params: {
|
||||
url: url,
|
||||
options: {}
|
||||
options: {
|
||||
device: 'desktop'
|
||||
}
|
||||
}
|
||||
}).then(function(data) {
|
||||
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
var should = require('chai').should();
|
||||
var weightChecker = require('../../lib/tools/weightChecker/weightChecker');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
||||
describe('weightChecker', function() {
|
||||
|
||||
it('should download a list of files', function(done) {
|
||||
this.timeout(10000);
|
||||
|
||||
var requestsList = [
|
||||
{
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/simple-page.html',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*'
|
||||
},
|
||||
status: 200,
|
||||
isHTML: true,
|
||||
type: 'html'
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/jquery1.8.3.js',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*'
|
||||
},
|
||||
status: 200,
|
||||
isJS: true,
|
||||
type: 'js'
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/jpeg-image.jpg',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*'
|
||||
},
|
||||
status: 200,
|
||||
isImage: true,
|
||||
type: 'image',
|
||||
contentType: 'image/jpeg'
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/svg-image.svg',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*'
|
||||
},
|
||||
status: 200,
|
||||
isImage: true,
|
||||
isSVG: true,
|
||||
type: 'image',
|
||||
contentType: 'image/svg+xml'
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/unminified-script.js',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*'
|
||||
},
|
||||
status: 200,
|
||||
isJS: true,
|
||||
type: 'js'
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/unminified-stylesheet.css',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*'
|
||||
},
|
||||
status: 200,
|
||||
isCSS: true,
|
||||
type: 'css'
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
url: 'about:blank',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*'
|
||||
},
|
||||
status: 200
|
||||
}
|
||||
];
|
||||
|
||||
var data = {
|
||||
toolsResults: {
|
||||
phantomas: {
|
||||
metrics: {
|
||||
requestsList: true
|
||||
},
|
||||
offenders: {
|
||||
requestsList: JSON.stringify(requestsList)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
weightChecker.recheckAllFiles(data)
|
||||
|
||||
.then(function(data) {
|
||||
data.toolsResults.should.have.a.property('weightChecker');
|
||||
data.toolsResults.weightChecker.should.have.a.property('metrics');
|
||||
data.toolsResults.weightChecker.should.have.a.property('offenders');
|
||||
|
||||
data.toolsResults.weightChecker.offenders.should.have.a.property('totalWeight');
|
||||
data.toolsResults.weightChecker.offenders.totalWeight.totalWeight.should.be.above(0);
|
||||
data.toolsResults.weightChecker.offenders.totalWeight.byType.html.requests.length.should.equal(1);
|
||||
data.toolsResults.weightChecker.offenders.totalWeight.byType.js.requests.length.should.equal(2);
|
||||
data.toolsResults.weightChecker.offenders.totalWeight.byType.css.requests.length.should.equal(1);
|
||||
data.toolsResults.weightChecker.offenders.totalWeight.byType.image.requests.length.should.equal(2);
|
||||
data.toolsResults.weightChecker.offenders.totalWeight.byType.other.requests.length.should.equal(0);
|
||||
|
||||
data.toolsResults.weightChecker.offenders.should.have.a.property('imageOptimization');
|
||||
data.toolsResults.weightChecker.offenders.imageOptimization.totalGain.should.be.above(0);
|
||||
data.toolsResults.weightChecker.offenders.imageOptimization.images.length.should.equal(2);
|
||||
|
||||
data.toolsResults.weightChecker.offenders.should.have.a.property('gzipCompression');
|
||||
data.toolsResults.weightChecker.offenders.gzipCompression.totalGain.should.be.above(0);
|
||||
data.toolsResults.weightChecker.offenders.gzipCompression.files.length.should.equal(4);
|
||||
|
||||
data.toolsResults.weightChecker.offenders.should.have.a.property('fileMinification');
|
||||
data.toolsResults.weightChecker.offenders.fileMinification.totalGain.should.be.above(0);
|
||||
data.toolsResults.weightChecker.offenders.fileMinification.files.length.should.equal(2);
|
||||
|
||||
data.toolsResults.weightChecker.metrics.should.have.a.property('totalRequests').that.equals(6);
|
||||
data.toolsResults.weightChecker.offenders.should.have.a.property('totalRequests');
|
||||
data.toolsResults.weightChecker.offenders.totalRequests.byType.html.length.should.equal(1);
|
||||
data.toolsResults.weightChecker.offenders.totalRequests.byType.js.length.should.equal(2);
|
||||
data.toolsResults.weightChecker.offenders.totalRequests.byType.css.length.should.equal(1);
|
||||
data.toolsResults.weightChecker.offenders.totalRequests.byType.image.length.should.equal(2);
|
||||
data.toolsResults.weightChecker.offenders.totalRequests.byType.json.length.should.equal(0);
|
||||
data.toolsResults.weightChecker.offenders.totalRequests.byType.webfont.length.should.equal(0);
|
||||
data.toolsResults.weightChecker.offenders.totalRequests.byType.video.length.should.equal(0);
|
||||
data.toolsResults.weightChecker.offenders.totalRequests.byType.other.length.should.equal(0);
|
||||
|
||||
data.toolsResults.weightChecker.metrics.should.have.a.property('smallRequests').that.equals(0);
|
||||
data.toolsResults.weightChecker.offenders.should.have.a.property('smallRequests');
|
||||
data.toolsResults.weightChecker.offenders.smallRequests.byType.js.length.should.equal(0);
|
||||
data.toolsResults.weightChecker.offenders.smallRequests.byType.css.length.should.equal(0);
|
||||
data.toolsResults.weightChecker.offenders.smallRequests.byType.image.length.should.equal(0);
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should download a file and measure its size', function(done) {
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/jquery1.8.3.js',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*'
|
||||
},
|
||||
status: 200,
|
||||
isJS: true,
|
||||
type: 'js'
|
||||
};
|
||||
|
||||
weightChecker.redownloadEntry(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
|
||||
newEntry.weightCheck.bodySize.should.equal(93636);
|
||||
newEntry.weightCheck.uncompressedSize.should.equal(newEntry.weightCheck.bodySize);
|
||||
newEntry.weightCheck.isCompressed.should.equal(false);
|
||||
newEntry.weightCheck.headersSize.should.be.above(200).and.below(400);
|
||||
newEntry.weightCheck.body.toString().should.have.string('1.8.3');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should download a PNG image and find the same body as fs.readFile', function(done) {
|
||||
var fileContent = fs.readFileSync(path.resolve(__dirname, '../www/logo-large.png'));
|
||||
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/logo-large.png',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*'
|
||||
},
|
||||
status: 200,
|
||||
isImage: true,
|
||||
contentType: 'image/png'
|
||||
};
|
||||
|
||||
weightChecker.redownloadEntry(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
|
||||
newEntry.weightCheck.bodySize.should.equal(4193);
|
||||
newEntry.weightCheck.body.should.equal(fileContent.toString('binary'));
|
||||
|
||||
// Opening the image in lwip to check if the format is good
|
||||
var lwip = require('lwip');
|
||||
var buffer = new Buffer(newEntry.weightCheck.body, 'binary');
|
||||
lwip.open(buffer, 'png', function(err, image) {
|
||||
image.width().should.equal(620);
|
||||
image.height().should.equal(104);
|
||||
done(err);
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail downloading a file in error', function(done) {
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/no-file',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*'
|
||||
},
|
||||
status: 200,
|
||||
isHTML: true,
|
||||
contentLength: 999
|
||||
};
|
||||
|
||||
weightChecker.redownloadEntry(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.weightCheck.should.have.a.property('message').that.equals('error while downloading: 404');
|
||||
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not download a non 200 request', function(done) {
|
||||
var entry = {
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/no-file',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*'
|
||||
},
|
||||
status: 302,
|
||||
isHTML: true,
|
||||
contentLength: 999
|
||||
};
|
||||
|
||||
weightChecker.redownloadEntry(entry)
|
||||
|
||||
.then(function(newEntry) {
|
||||
newEntry.should.not.have.a.property('weightCheck');
|
||||
done();
|
||||
})
|
||||
|
||||
.fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('should listRequestWeight', function() {
|
||||
var totalWeightObj = weightChecker.listRequestWeight([{
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/jquery1.8.3.js',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*',
|
||||
'Accept-Encoding': 'gzip, deflate'
|
||||
},
|
||||
status: 200,
|
||||
isHTML: true,
|
||||
type: 'html',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
body: 'blabla',
|
||||
headersSize: 200,
|
||||
bodySize: 500,
|
||||
isCompressed: false,
|
||||
uncompressedSize: 500
|
||||
}
|
||||
}]);
|
||||
|
||||
totalWeightObj.totalWeight.should.equal(700);
|
||||
totalWeightObj.byType.html.totalWeight.should.equal(700);
|
||||
totalWeightObj.byType.html.requests.should.have.length(1);
|
||||
totalWeightObj.byType.html.requests[0].should.have.a.property('url').that.equals('http://localhost:8388/jquery1.8.3.js');
|
||||
totalWeightObj.byType.html.requests[0].should.have.a.property('weight').that.equals(700);
|
||||
});
|
||||
|
||||
it('should listRequestWeight even if download failed', function() {
|
||||
var totalWeightObj = weightChecker.listRequestWeight([{
|
||||
method: 'GET',
|
||||
url: 'http://localhost:8388/jquery1.8.3.js',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*',
|
||||
'Accept-Encoding': 'gzip, deflate'
|
||||
},
|
||||
status: 200,
|
||||
isHTML: true,
|
||||
type: 'html',
|
||||
contentLength: 999,
|
||||
weightCheck: {
|
||||
message: 'error while downloading: 404'
|
||||
}
|
||||
}]);
|
||||
|
||||
totalWeightObj.totalWeight.should.equal(999);
|
||||
});
|
||||
|
||||
});
|
||||
|
After Width: | Height: | Size: 115 KiB |
@@ -20,6 +20,7 @@
|
||||
|
||||
// Let's start the spaghetti code!!!
|
||||
|
||||
$('li').bind('mouseover', function() {});
|
||||
$('ul').find('li');
|
||||
$('li', $('#foo'));
|
||||
var foo = document.getElementById('foo');
|
||||
@@ -102,6 +103,7 @@
|
||||
$tbodytr.on('click', handler);
|
||||
$tbody.on('click', 'tr', handler);
|
||||
$tbody.on('click', {someData: 'Joe'}, handler);
|
||||
$tbody.on({event1: handler, event2: handler});
|
||||
$tbodytr.off('click', handler);
|
||||
$tbodytr.off('click', 'tr');
|
||||
$tbodytr.off('click');
|
||||
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
@@ -0,0 +1,14 @@
|
||||
var timelineCtrl=angular.module("timelineCtrl",[])
|
||||
timelineCtrl.controller("TimelineCtrl",["$scope","$rootScope","$routeParams","$location","$timeout","Menu","Results","API",function(e,t,n,r,i,a,l,u){function o(){t.loadedResult&&t.loadedResult.runId===n.runId?(e.result=t.loadedResult,c()):l.get({runId:n.runId,exclude:"toolsResults"},function(n){t.loadedResult=n,e.result=n,c()})}function c(){s(),d(),f(),m(),i(p,100)}function s(){var t=r.hash(),n=null
|
||||
0===t.indexOf("filter=")&&(n=t.substr(7)),e.warningsFilterOn=null!==n,e.warningsFilters={queryWithoutResults:null===n||"queryWithoutResults"===n,jQueryCallOnEmptyObject:null===n||"jQueryCallOnEmptyObject"===n,eventNotDelegated:null===n||"eventNotDelegated"===n,jsError:null===n||"jsError"===n}}function d(){var t=e.result.rules.jsCount.offendersObj.list
|
||||
e.scripts=[],t.forEach(function(t){var n=t.file
|
||||
n.length>100&&(n=n.substr(0,98)+"...")
|
||||
var r={fullPath:t.file,shortPath:n}
|
||||
e.scripts.push(r)})}function f(){var t=e.result.javascriptExecutionTree.children||[],n=t[t.length-1]
|
||||
e.endTime=n.data.timestamp+(n.data.time||0),e.executionTree=[],t.forEach(function(t){if(e.selectedScript){if(t.data.backtrace&&-1===t.data.backtrace.indexOf(e.selectedScript.fullPath+":"))return
|
||||
if("jQuery loaded"===t.data.type||"jQuery version change"===t.data.type)return}e.executionTree.push(t)})}function m(){var t=199
|
||||
e.timelineIntervalDuration=e.endTime/t
|
||||
var n=Array.apply(null,Array(e.endTime+1)).map(Number.prototype.valueOf,0)
|
||||
e.executionTree.forEach(function(e){if(void 0!==e.data.time)for(var t=Math.min(e.data.time,100)||1,r=e.data.timestamp,i=e.data.timestamp+t;i>r;r++)n[r]|=1}),e.timeline=Array.apply(null,Array(t+1)).map(Number.prototype.valueOf,0),n.forEach(function(t,n){1===t&&(e.timeline[Math.floor(n/e.timelineIntervalDuration)]+=1)}),e.timelineMax=Math.max.apply(Math,e.timeline)}function p(){e.profilerData=e.executionTree}e.runId=n.runId,e.Menu=a.setCurrentPage("timeline",e.runId),e.changeScript=function(){f(),m(),p()},e.findLineIndexByTimestamp=function(t){for(var n=0,r=0;r<e.executionTree.length;r++){var i=e.executionTree[r].data.timestamp-t
|
||||
if(i<e.timelineIntervalDuration&&(n=r),i>0)break}return n},e.backToDashboard=function(){r.path("/result/"+e.runId)},e.testAgain=function(){u.relaunchTest(e.result)},o()}]),timelineCtrl.directive("scrollOnClick",["$animate","$timeout",function(e,t){return{restrict:"A",link:function(n,r,i){r.on("click",function(){var r=n.findLineIndexByTimestamp(i.scrollOnClick),a=angular.element(document.getElementById("line_"+r))
|
||||
a.addClass("highlight"),t(function(){e.removeClass(a,"highlight"),n.$digest()},50),window.scrollTo(0,a[0].offsetTop)})}}}])
|
||||
|
After Width: | Height: | Size: 56 KiB |
@@ -0,0 +1,18 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Scroll page</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Simple page</h1>
|
||||
|
||||
<script>
|
||||
window.onscroll = function() {
|
||||
document.getElementsByTagName('window.onscroll');
|
||||
};
|
||||
|
||||
window.addEventListener('scroll', function() {
|
||||
document.getElementsByTagName('scroll on window');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -4,5 +4,9 @@
|
||||
</head>
|
||||
<body>
|
||||
<h1>Simple page</h1>
|
||||
|
||||
<script>
|
||||
document.getElementById('foo');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 84 KiB |
@@ -0,0 +1,190 @@
|
||||
var timelineCtrl = angular.module('timelineCtrl', []);
|
||||
|
||||
timelineCtrl.controller('TimelineCtrl', ['$scope', '$rootScope', '$routeParams', '$location', '$timeout', 'Menu', 'Results', 'API', function($scope, $rootScope, $routeParams, $location, $timeout, Menu, Results, API) {
|
||||
$scope.runId = $routeParams.runId;
|
||||
$scope.Menu = Menu.setCurrentPage('timeline', $scope.runId);
|
||||
|
||||
function loadResults() {
|
||||
// Load result if needed
|
||||
if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
|
||||
Results.get({runId: $routeParams.runId, exclude: 'toolsResults'}, function(result) {
|
||||
$rootScope.loadedResult = result;
|
||||
$scope.result = result;
|
||||
render();
|
||||
});
|
||||
} else {
|
||||
$scope.result = $rootScope.loadedResult;
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
function render() {
|
||||
initFilters();
|
||||
initScriptFiltering();
|
||||
initExecutionTree();
|
||||
initTimeline();
|
||||
$timeout(initProfiler, 100);
|
||||
}
|
||||
|
||||
function initFilters() {
|
||||
var hash = $location.hash();
|
||||
var filter = null;
|
||||
|
||||
if (hash.indexOf('filter=') === 0) {
|
||||
filter = hash.substr(7);
|
||||
}
|
||||
|
||||
$scope.warningsFilterOn = (filter !== null);
|
||||
$scope.warningsFilters = {
|
||||
queryWithoutResults: (filter === null || filter === 'queryWithoutResults'),
|
||||
jQueryCallOnEmptyObject: (filter === null || filter === 'jQueryCallOnEmptyObject'),
|
||||
eventNotDelegated: (filter === null || filter === 'eventNotDelegated'),
|
||||
jsError: (filter === null || filter === 'jsError')
|
||||
};
|
||||
}
|
||||
|
||||
function initScriptFiltering() {
|
||||
var offenders = $scope.result.rules.jsCount.offendersObj.list;
|
||||
$scope.scripts = [];
|
||||
|
||||
offenders.forEach(function(script) {
|
||||
var filePath = script.file;
|
||||
|
||||
if (filePath.length > 100) {
|
||||
filePath = filePath.substr(0, 98) + '...';
|
||||
}
|
||||
|
||||
var scriptObj = {
|
||||
fullPath: script.file,
|
||||
shortPath: filePath
|
||||
};
|
||||
|
||||
$scope.scripts.push(scriptObj);
|
||||
});
|
||||
}
|
||||
|
||||
function initExecutionTree() {
|
||||
var originalExecutions = $scope.result.javascriptExecutionTree.children || [];
|
||||
|
||||
// Detect the last event of all (before filtering) and read time
|
||||
var lastEvent = originalExecutions[originalExecutions.length - 1];
|
||||
$scope.endTime = lastEvent.data.timestamp + (lastEvent.data.time || 0);
|
||||
|
||||
// Filter
|
||||
$scope.executionTree = [];
|
||||
originalExecutions.forEach(function(node) {
|
||||
|
||||
// Filter by script (if enabled)
|
||||
if ($scope.selectedScript) {
|
||||
if (node.data.backtrace && node.data.backtrace.indexOf($scope.selectedScript.fullPath + ':') === -1) {
|
||||
return;
|
||||
}
|
||||
if (node.data.type === "jQuery loaded" || node.data.type === "jQuery version change") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.executionTree.push(node);
|
||||
});
|
||||
}
|
||||
|
||||
function initTimeline() {
|
||||
|
||||
// Split the timeline into 200 intervals
|
||||
var numberOfIntervals = 199;
|
||||
$scope.timelineIntervalDuration = $scope.endTime / numberOfIntervals;
|
||||
|
||||
// Pre-fill array of as many elements as there are milleseconds
|
||||
var millisecondsArray = Array.apply(null, new Array($scope.endTime + 1)).map(Number.prototype.valueOf,0);
|
||||
|
||||
// Create the milliseconds array from the execution tree
|
||||
$scope.executionTree.forEach(function(node) {
|
||||
if (node.data.time !== undefined) {
|
||||
|
||||
// Ignore artefacts (durations > 100ms)
|
||||
var time = Math.min(node.data.time, 100) || 1;
|
||||
|
||||
for (var i=node.data.timestamp, max=node.data.timestamp + time ; i<max ; i++) {
|
||||
millisecondsArray[i] |= 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Pre-fill array of 200 elements
|
||||
$scope.timeline = Array.apply(null, new Array(numberOfIntervals + 1)).map(Number.prototype.valueOf,0);
|
||||
|
||||
// Create the timeline from the milliseconds array
|
||||
millisecondsArray.forEach(function(value, timestamp) {
|
||||
if (value === 1) {
|
||||
$scope.timeline[Math.floor(timestamp / $scope.timelineIntervalDuration)] += 1;
|
||||
}
|
||||
});
|
||||
|
||||
// Get the maximum value of the array (needed for display)
|
||||
$scope.timelineMax = Math.max.apply(Math, $scope.timeline);
|
||||
}
|
||||
|
||||
|
||||
function initProfiler() {
|
||||
$scope.profilerData = $scope.executionTree;
|
||||
}
|
||||
|
||||
$scope.changeScript = function() {
|
||||
initExecutionTree();
|
||||
initTimeline();
|
||||
initProfiler();
|
||||
};
|
||||
|
||||
$scope.findLineIndexByTimestamp = function(timestamp) {
|
||||
var lineIndex = 0;
|
||||
|
||||
for (var i = 0; i < $scope.executionTree.length; i ++) {
|
||||
var delta = $scope.executionTree[i].data.timestamp - timestamp;
|
||||
|
||||
if (delta < $scope.timelineIntervalDuration) {
|
||||
lineIndex = i;
|
||||
}
|
||||
|
||||
if (delta > 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return lineIndex;
|
||||
};
|
||||
|
||||
|
||||
$scope.backToDashboard = function() {
|
||||
$location.path('/result/' + $scope.runId);
|
||||
};
|
||||
|
||||
$scope.testAgain = function() {
|
||||
API.relaunchTest($scope.result);
|
||||
};
|
||||
|
||||
loadResults();
|
||||
|
||||
}]);
|
||||
|
||||
timelineCtrl.directive('scrollOnClick', ['$animate', '$timeout', function($animate, $timeout) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function (scope, element, attributes) {
|
||||
// When the user clicks on the timeline, find the right profiler line and scroll to it
|
||||
element.on('click', function() {
|
||||
var lineIndex = scope.findLineIndexByTimestamp(attributes.scrollOnClick);
|
||||
var lineElement = angular.element(document.getElementById('line_' + lineIndex));
|
||||
|
||||
// Animate the background color to "flash" the row
|
||||
lineElement.addClass('highlight');
|
||||
$timeout(function() {
|
||||
$animate.removeClass(lineElement, 'highlight');
|
||||
scope.$digest();
|
||||
}, 50);
|
||||
|
||||
|
||||
window.scrollTo(0, lineElement[0].offsetTop);
|
||||
});
|
||||
}
|
||||
};
|
||||
}]);
|
||||
@@ -0,0 +1,772 @@
|
||||
/* Timeline colors, related to Window Performances */
|
||||
.execution {
|
||||
text-align: center;
|
||||
}
|
||||
.selectScript {
|
||||
padding-bottom: 2em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.selectScript select {
|
||||
max-width: 30em;
|
||||
}
|
||||
.selectScript.empty {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.selectScript.empty select {
|
||||
width: 10em;
|
||||
}
|
||||
.timeline {
|
||||
margin: 2em 0 5em;
|
||||
}
|
||||
.timeline .chart {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
.timeline .startTime,
|
||||
.timeline .endTime {
|
||||
position: absolute;
|
||||
bottom: 0.5em;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
.timeline .startTime {
|
||||
left: 0em;
|
||||
}
|
||||
.timeline .endTime {
|
||||
right: 0em;
|
||||
}
|
||||
.timeline .chartPoints {
|
||||
display: table;
|
||||
height: 100px;
|
||||
width: 99%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.timeline .interval {
|
||||
display: table-cell;
|
||||
position: relative;
|
||||
height: 100px;
|
||||
width: 0.5%;
|
||||
}
|
||||
.timeline .interval .color {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.timeline .interval .color.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
.timeline div.interval:hover {
|
||||
background: #9C4274;
|
||||
}
|
||||
.timeline .interval:hover .color {
|
||||
background: #F04DA7;
|
||||
}
|
||||
.timeline .domComplete.interval {
|
||||
background: #ede3ff;
|
||||
}
|
||||
.timeline .domComplete .color {
|
||||
background: #c2a3ff;
|
||||
}
|
||||
.timeline .domContentLoadedEnd.interval {
|
||||
background: #d8f0f0;
|
||||
}
|
||||
.timeline .domContentLoadedEnd .color {
|
||||
background: #7ecccc;
|
||||
}
|
||||
.timeline .domContentLoaded.interval {
|
||||
background: #e0ffd1;
|
||||
}
|
||||
.timeline .domContentLoaded .color {
|
||||
background: #a7e846;
|
||||
}
|
||||
.timeline .domInteractive.interval {
|
||||
background: #fffccc;
|
||||
}
|
||||
.timeline .domInteractive .color {
|
||||
background: #ffe433;
|
||||
}
|
||||
.timeline .domCreation.interval {
|
||||
background: #ffe0cc;
|
||||
}
|
||||
.timeline .domCreation .color {
|
||||
background: #ff6600;
|
||||
}
|
||||
.timeline .tooltip.detailsOverlay {
|
||||
position: absolute;
|
||||
display: none;
|
||||
width: auto;
|
||||
padding: 0.5em 1em;
|
||||
top: -1.5em;
|
||||
right: 1em;
|
||||
}
|
||||
.timeline .interval:hover .tooltip {
|
||||
display: block;
|
||||
}
|
||||
.timeline .legend {
|
||||
display: table;
|
||||
width: 100%;
|
||||
margin-top: 1em;
|
||||
}
|
||||
.timeline .legend > div {
|
||||
display: table-row;
|
||||
}
|
||||
.timeline .legend > div > div {
|
||||
position: relative;
|
||||
display: table-cell;
|
||||
margin-top: 1em;
|
||||
}
|
||||
.timeline .titles {
|
||||
font-weight: bold;
|
||||
}
|
||||
.timeline .titles > div {
|
||||
padding: 0 1em 0 2em;
|
||||
}
|
||||
.timeline .tips {
|
||||
font-size: 0.7em;
|
||||
}
|
||||
.timeline .tips > div {
|
||||
padding: 1em 1em 0 0;
|
||||
}
|
||||
.timeline .legend .color {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
height: 1.5em;
|
||||
width: 1.5em;
|
||||
border-radius: 0.2em;
|
||||
}
|
||||
.filters {
|
||||
margin: 1em auto;
|
||||
padding: 0.5em;
|
||||
min-width: 30em;
|
||||
width: 30%;
|
||||
border: 1px dotted #aaa;
|
||||
text-align: left;
|
||||
}
|
||||
.subFilters {
|
||||
margin-left: 3em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.table {
|
||||
display: table;
|
||||
width: 100%;
|
||||
border-spacing: 0.25em;
|
||||
}
|
||||
.table > div {
|
||||
display: table-row;
|
||||
}
|
||||
.table > .headers > div {
|
||||
font-weight: bold;
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
.table > div > div {
|
||||
padding: 0.1em 1em;
|
||||
background: #f2f2f2;
|
||||
display: table-cell;
|
||||
text-align: left;
|
||||
}
|
||||
.table > div.jsError > .type,
|
||||
.table > div.jsError > .value {
|
||||
color: #e74c3c;
|
||||
font-weight: bold;
|
||||
}
|
||||
.table > div.windowPerformance > div,
|
||||
.table > div.windowPerformance > div.startTime {
|
||||
background: #EBD8E2;
|
||||
}
|
||||
.table > div.showingDetails > div {
|
||||
background: #f1c40f;
|
||||
}
|
||||
.table > div.highlight > div.startTime {
|
||||
background-color: #C0F090;
|
||||
}
|
||||
.table > div.highlight-remove {
|
||||
transition: 3s;
|
||||
}
|
||||
.table > div.highlight-remove > div.startTime {
|
||||
transition: background-color 3s ease-in;
|
||||
}
|
||||
.table > div > .index {
|
||||
color: #bbb;
|
||||
word-break: normal;
|
||||
}
|
||||
.table > div > .type {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.table .children {
|
||||
margin-top: 0.2em;
|
||||
font-size: 0.8em;
|
||||
line-height: 1.6em;
|
||||
}
|
||||
.table .child {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
.table .child > .child {
|
||||
margin-left: 1em;
|
||||
}
|
||||
.table .child:before {
|
||||
content: "↳";
|
||||
}
|
||||
.table .child .childArgs {
|
||||
display: none;
|
||||
}
|
||||
.table .child span {
|
||||
position: relative;
|
||||
}
|
||||
.table .child span:hover {
|
||||
background: #EBD8E2;
|
||||
}
|
||||
.table .child span:hover div {
|
||||
display: inline-block;
|
||||
}
|
||||
.table .child span:hover .childArgs {
|
||||
display: block;
|
||||
position: absolute;
|
||||
padding: 0 1em 0 2em;
|
||||
left: 100%;
|
||||
top: 0;
|
||||
background: #EBD8E2;
|
||||
line-height: 1.3em;
|
||||
height: 1.3em;
|
||||
z-index: 2;
|
||||
}
|
||||
.table .showingDetails .child span:hover {
|
||||
background: inherit;
|
||||
}
|
||||
.table .showingDetails .child span:hover .childArgs {
|
||||
display: none;
|
||||
}
|
||||
.table > div > .value {
|
||||
width: 70%;
|
||||
word-break: break-all;
|
||||
}
|
||||
.table > div > .details {
|
||||
position: relative;
|
||||
}
|
||||
.table .details .icon-question {
|
||||
color: #f1c40f;
|
||||
cursor: pointer;
|
||||
}
|
||||
.table .icon-warning {
|
||||
display: inline-block;
|
||||
width: 0.8em;
|
||||
}
|
||||
.detailsOverlay {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 3em;
|
||||
top: -3em;
|
||||
width: 45em;
|
||||
min-height: 1em;
|
||||
padding: 0 1em 1em;
|
||||
background: #fff;
|
||||
border: 2px solid #f1c40f;
|
||||
border-radius: 0.5em;
|
||||
z-index: 2;
|
||||
}
|
||||
@media screen and (max-width: 1024px) {
|
||||
.detailsOverlay {
|
||||
width: 25em;
|
||||
}
|
||||
}
|
||||
.showDetails .detailsOverlay {
|
||||
display: block;
|
||||
}
|
||||
.detailsOverlay .closeBtn {
|
||||
position: absolute;
|
||||
top: 0.5em;
|
||||
right: 0.5em;
|
||||
color: #f1c40f;
|
||||
cursor: pointer;
|
||||
}
|
||||
.detailsOverlay .advice {
|
||||
color: #e74c3c;
|
||||
font-weight: bold;
|
||||
}
|
||||
.detailsOverlay .trace {
|
||||
word-break: break-all;
|
||||
}
|
||||
.table > div > .duration,
|
||||
.table > div > .startTime {
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.table > div > .startTime.domComplete {
|
||||
background: #ede3ff;
|
||||
}
|
||||
.table > div > .startTime.domContentLoadedEnd {
|
||||
background: #d8f0f0;
|
||||
}
|
||||
.table > div > .startTime.domContentLoaded {
|
||||
background: #e0ffd1;
|
||||
}
|
||||
.table > div > .startTime.domInteractive {
|
||||
background: #fffccc;
|
||||
}
|
||||
.table > div > .startTime.domCreation {
|
||||
background: #ffe0cc;
|
||||
}
|
||||
.execution .icon-warning {
|
||||
color: #e74c3c;
|
||||
cursor: pointer;
|
||||
}
|
||||
.queryWithoutResultsFilterOn > div {
|
||||
display: none;
|
||||
}
|
||||
.queryWithoutResultsFilterOn > div.queryWithoutResults {
|
||||
display: table-row;
|
||||
}
|
||||
.jQueryCallOnEmptyObjectFilterOn > div {
|
||||
display: none;
|
||||
}
|
||||
.jQueryCallOnEmptyObjectFilterOn > div.jQueryCallOnEmptyObject {
|
||||
display: table-row;
|
||||
}
|
||||
.eventNotDelegatedFilterOn > div {
|
||||
display: none;
|
||||
}
|
||||
.eventNotDelegatedFilterOn > div.eventNotDelegated {
|
||||
display: table-row;
|
||||
}
|
||||
.jsErrorFilterOn > div {
|
||||
display: none;
|
||||
}
|
||||
.jsErrorFilterOn > div.jsError {
|
||||
display: table-row;
|
||||
}
|
||||
.testedUrl {
|
||||
color: inherit;
|
||||
}
|
||||
.summary {
|
||||
text-align: center;
|
||||
}
|
||||
.summary .globalScore {
|
||||
display: table;
|
||||
width: 60%;
|
||||
margin: 3em auto;
|
||||
}
|
||||
.summary .globalScore > div {
|
||||
display: table-cell;
|
||||
width: 50%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.summary .globalScore .globalGrade {
|
||||
margin: 0.5 auto;
|
||||
width: 2.5em;
|
||||
height: 2.5em;
|
||||
line-height: 2.5em;
|
||||
border-radius: 0.5em;
|
||||
font-size: 3em;
|
||||
font-weight: bold;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.summary .globalScore .on100 {
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
margin: 0.5em 0 1em;
|
||||
}
|
||||
.summary .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;
|
||||
}
|
||||
.summary .notations {
|
||||
display: table;
|
||||
width: 80%;
|
||||
margin: 0 10% 1.5em;
|
||||
border-spacing: 1em;
|
||||
}
|
||||
.summary .notations > div {
|
||||
display: table-row;
|
||||
}
|
||||
.summary .notations > div > div {
|
||||
display: table-cell;
|
||||
height: 2.5em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.summary .notations .category {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
width: 20%;
|
||||
}
|
||||
.summary .notations .criteria {
|
||||
font-weight: normal;
|
||||
width: 75%;
|
||||
}
|
||||
.summary .notations .A.categoryScore,
|
||||
.summary .notations .B.categoryScore,
|
||||
.summary .notations .C.categoryScore,
|
||||
.summary .notations .D.categoryScore,
|
||||
.summary .notations .E.categoryScore,
|
||||
.summary .notations .F.categoryScore,
|
||||
.summary .notations .NA.categoryScore {
|
||||
width: 2.5em;
|
||||
max-width: 2.5em;
|
||||
min-width: 2.5em;
|
||||
font-size: 2em;
|
||||
text-align: center;
|
||||
border-radius: 0.5em;
|
||||
font-weight: bold;
|
||||
}
|
||||
.summary .notations .grade .A,
|
||||
.summary .notations .grade .B,
|
||||
.summary .notations .grade .C,
|
||||
.summary .notations .grade .D,
|
||||
.summary .notations .grade .E,
|
||||
.summary .notations .grade .F,
|
||||
.summary .notations .grade .NA {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
font-size: 1em;
|
||||
color: transparent;
|
||||
margin: 0 auto;
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
.summary .notations .criteria .table {
|
||||
width: 100%;
|
||||
}
|
||||
.summary .notations .criteria .table > div:hover > div {
|
||||
background: #EBD8E2;
|
||||
cursor: pointer;
|
||||
}
|
||||
.summary .notations .criteria .table > div:hover > div.info {
|
||||
background: #FFF;
|
||||
}
|
||||
.summary .notations .criteria .table > div:hover > div.info .icon-question {
|
||||
color: #EBD8E2;
|
||||
}
|
||||
.summary .notations .criteria .grade {
|
||||
width: 10%;
|
||||
padding-left: 0.5em;
|
||||
padding-right: 0.5em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.summary .notations .criteria .label {
|
||||
width: 70%;
|
||||
}
|
||||
.summary .notations .criteria .result {
|
||||
width: 18%;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.summary .notations .warning .label,
|
||||
.summary .notations .warning .result,
|
||||
.summary .notations .icon-warning {
|
||||
color: #FF1919;
|
||||
}
|
||||
.summary .notations .criteria .info {
|
||||
width: 2%;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
background: #FFF;
|
||||
padding-left: 0.1em;
|
||||
padding-right: 0.1em;
|
||||
}
|
||||
.summary .notations .criteria .icon-question {
|
||||
color: transparent;
|
||||
}
|
||||
.summary .fromShare {
|
||||
margin-bottom: 3em;
|
||||
}
|
||||
.summary .apiTip {
|
||||
font-size: 0.8em;
|
||||
margin-bottom: 4em;
|
||||
color: #413;
|
||||
}
|
||||
.summary .apiTip a {
|
||||
color: inherit;
|
||||
}
|
||||
.summary .tweet .tweetText {
|
||||
color: #413;
|
||||
background: #F2F2F2;
|
||||
border: none;
|
||||
width: 25em;
|
||||
padding: 0.4em;
|
||||
border-radius: 0.5em;
|
||||
box-shadow: 0.05em 0.1em 0 0 #999;
|
||||
}
|
||||
.summary .tweet .tweetButton,
|
||||
.summary .tweet .linkedinButton {
|
||||
color: #413;
|
||||
background: #F2F2F2;
|
||||
margin-right: 0;
|
||||
}
|
||||
.summary .tweet .tweetButton:hover,
|
||||
.summary .tweet .linkedinButton:hover {
|
||||
color: #F2F2F2;
|
||||
background: #e74c3c;
|
||||
}
|
||||
.summary .tweet input {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
|
||||
.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: #deaca6;
|
||||
}
|
||||
.launchBtn.disabled:focus {
|
||||
color: #ddd;
|
||||
}
|
||||
.settings {
|
||||
width: 50%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.settings input,
|
||||
.settings select {
|
||||
font-size: 1em;
|
||||
}
|
||||
.settings input[type=text] {
|
||||
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.active {
|
||||
color: #ffa319;
|
||||
border: 2px solid #ffa319;
|
||||
padding: 0;
|
||||
}
|
||||
.device .item:hover {
|
||||
color: #ffa319;
|
||||
}
|
||||
.device .item > div {
|
||||
margin: 0.2em 0 0.1em;
|
||||
font-size: 3em;
|
||||
}
|
||||
.settingsTooltip {
|
||||
position: relative;
|
||||
}
|
||||
.settingsTooltip span {
|
||||
font-size: 0.8em;
|
||||
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;
|
||||
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;
|
||||
}
|
||||
.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: #413;
|
||||
}
|
||||
.features > div {
|
||||
width: 33.3%;
|
||||
display: table-cell;
|
||||
padding: 0 1.5em;
|
||||
}
|
||||
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;
|
||||
box-shadow: 0.1em 0.2em 0 0 #5e2846;
|
||||
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;
|
||||
}
|
||||
/*
|
||||
* Core Owl Carousel CSS File
|
||||
* v1.3.2
|
||||
*/
|
||||
|
||||
/* clearfix */
|
||||
.owl-carousel .owl-wrapper:after {
|
||||
content: ".";
|
||||
display: block;
|
||||
clear: both;
|
||||
visibility: hidden;
|
||||
line-height: 0;
|
||||
height: 0;
|
||||
}
|
||||
/* display none until init */
|
||||
.owl-carousel{
|
||||
display: none;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
-ms-touch-action: pan-y;
|
||||
}
|
||||
.owl-carousel .owl-wrapper{
|
||||
display: none;
|
||||
position: relative;
|
||||
-webkit-transform: translate3d(0px, 0px, 0px);
|
||||
}
|
||||
.owl-carousel .owl-wrapper-outer{
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
.owl-carousel .owl-wrapper-outer.autoHeight{
|
||||
-webkit-transition: height 500ms ease-in-out;
|
||||
-moz-transition: height 500ms ease-in-out;
|
||||
-ms-transition: height 500ms ease-in-out;
|
||||
-o-transition: height 500ms ease-in-out;
|
||||
transition: height 500ms ease-in-out;
|
||||
}
|
||||
|
||||
.owl-carousel .owl-item{
|
||||
float: left;
|
||||
}
|
||||
.owl-controls .owl-page,
|
||||
.owl-controls .owl-buttons div{
|
||||
cursor: pointer;
|
||||
}
|
||||
.owl-controls {
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* mouse grab icon */
|
||||
.grabbing {
|
||||
cursor:url(grabbing.png) 8 8, move;
|
||||
}
|
||||
|
||||
/* fix */
|
||||
.owl-carousel .owl-wrapper,
|
||||
.owl-carousel .owl-item{
|
||||
-webkit-backface-visibility: hidden;
|
||||
-moz-backface-visibility: hidden;
|
||||
-ms-backface-visibility: hidden;
|
||||
-webkit-transform: translate3d(0,0,0);
|
||||
-moz-transform: translate3d(0,0,0);
|
||||
-ms-transform: translate3d(0,0,0);
|
||||
}
|
||||