Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51e4265109 | ||
|
|
eaebb3adfa | ||
|
|
76b4ca6ea5 | ||
|
|
b8560c6e15 | ||
|
|
8eec2549a4 | ||
|
|
aaa1ad7eaa | ||
|
|
74c57fc11c | ||
|
|
b7e25c4e1c | ||
|
|
3f97333423 | ||
|
|
71152f64a5 | ||
|
|
531d125dcc | ||
|
|
299e79e890 | ||
|
|
14f60d8dd3 | ||
|
|
a00b1a96c7 | ||
|
|
2847e02c5d | ||
|
|
57b30a1838 | ||
|
|
f2a637ada7 | ||
|
|
b3804586dc | ||
|
|
113c1f0565 | ||
|
|
3948eeaad4 | ||
|
|
f843184970 | ||
|
|
2aa9edeb8f | ||
|
|
5e37cf8224 | ||
|
|
8bd3a1edde | ||
|
|
c68962e2f2 | ||
|
|
6a9a247a87 | ||
|
|
e238370bc8 | ||
|
|
178f2fd135 | ||
|
|
55cd1160db | ||
|
|
06bdbd929c | ||
|
|
100eb01f27 | ||
|
|
00ade4ad29 | ||
|
|
f3a5779102 | ||
|
|
6f0b97e760 | ||
|
|
28f9ed64ff | ||
|
|
b6924f70d4 | ||
|
|
79ecae34c8 | ||
|
|
f4843baa0e | ||
|
|
cd97a275fe | ||
|
|
f2e77a5c3a | ||
|
|
23f4f9bd0b | ||
|
|
e8eb6964db | ||
|
|
9643c5bfcd | ||
|
|
56ebe6bb8b | ||
|
|
5f7c2f07b8 | ||
|
|
bbe965e7a3 | ||
|
|
ebd03c7b1a | ||
|
|
0d02a57600 | ||
|
|
394d3a5b25 | ||
|
|
4f9589fb6b | ||
|
|
048e3396de | ||
|
|
e3d378a4ba | ||
|
|
91f8d9c2b0 | ||
|
|
2285c896d2 | ||
|
|
9ff24918e2 | ||
|
|
e78e794240 | ||
|
|
0eb359542b | ||
|
|
c54cf5a2ba | ||
|
|
6f7f05b456 | ||
|
|
da6a8834dd | ||
|
|
7ed16d63f3 | ||
|
|
d8730cc67b | ||
|
|
b67efe0cac | ||
|
|
835cb2f90f | ||
|
|
f75e99f703 | ||
|
|
7e1bf0fc92 | ||
|
|
82693a8587 | ||
|
|
99eace0336 | ||
|
|
d0257fea8f | ||
|
|
453a5cda2e | ||
|
|
bdbd1a0f08 | ||
|
|
d32b9f78a4 | ||
|
|
46c2d8c49e | ||
|
|
88b5567131 | ||
|
|
0e81d16feb | ||
|
|
a900f0ff65 | ||
|
|
d4faa75b79 | ||
|
|
47b0de1b49 | ||
|
|
fe6a10592b | ||
|
|
8f02349ca5 | ||
|
|
1fb32de428 | ||
|
|
31b66ee6fa | ||
|
|
d22f9b81b2 | ||
|
|
3051d61365 | ||
|
|
53fd2e48e5 | ||
|
|
ef516c123d | ||
|
|
add8962390 | ||
|
|
1a444a481e | ||
|
|
96ca4a7933 | ||
|
|
f07bc615b0 | ||
|
|
2d0c88390a | ||
|
|
52574623fe | ||
|
|
b1d95ddf5f | ||
|
|
092bb285ed | ||
|
|
52ce26796a | ||
|
|
6bd55e4807 | ||
|
|
8325a9297e | ||
|
|
9cb3c78def | ||
|
|
143436ecd3 | ||
|
|
83cf87c8cf | ||
|
|
2f1003e8bc | ||
|
|
e0649eb9bd | ||
|
|
d5c607d370 | ||
|
|
2c2c8bf4cb | ||
|
|
08459ca289 | ||
|
|
162f1fa1d1 | ||
|
|
d9113edbbe | ||
|
|
eb1c5c07e9 | ||
|
|
0574429a93 | ||
|
|
8ff565404b | ||
|
|
b08d4b4cae | ||
|
|
f331662263 | ||
|
|
10693283c5 | ||
|
|
f2a933c51b | ||
|
|
172b8cfb9b | ||
|
|
47738ffa26 | ||
|
|
c8a29b4651 | ||
|
|
c305cc2f7d | ||
|
|
01676e1f99 | ||
|
|
de1e15fb25 | ||
|
|
0aa77c33ad | ||
|
|
22eeb1a47a | ||
|
|
15b3c6617b | ||
|
|
e342a88f7d | ||
|
|
7779efedf1 | ||
|
|
137b4d25b9 | ||
|
|
9be40adabf | ||
|
|
b6d4b7b40d | ||
|
|
3256789082 | ||
|
|
87adc575ac | ||
|
|
30ae0f93f3 | ||
|
|
c339654112 | ||
|
|
25c9770fba | ||
|
|
0bf7326604 | ||
|
|
7fc5a9ba8f | ||
|
|
230f805cae | ||
|
|
bc9c828584 | ||
|
|
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 |
@@ -1,5 +1,4 @@
|
||||
node_modules
|
||||
bower_components
|
||||
.tmp
|
||||
.vagrant
|
||||
results/*
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
language: node_js
|
||||
sudo: false
|
||||
node_js:
|
||||
- "0.10.33"
|
||||
- "5.8"
|
||||
- "0.12"
|
||||
env:
|
||||
- CXX=g++-4.8
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
- g++-4.8
|
||||
before_install:
|
||||
- "npm install -g grunt-cli"
|
||||
install: npm install
|
||||
- "npm install -g npm"
|
||||
- "npm install -g grunt-cli"
|
||||
install:
|
||||
- "npm install"
|
||||
@@ -143,7 +143,7 @@ module.exports = function(grunt) {
|
||||
options: {
|
||||
reporter: 'spec',
|
||||
},
|
||||
src: ['test/core/offendersHelpersTest.js']
|
||||
src: ['test/core/imageOptimizerTest.js']
|
||||
},
|
||||
coverage: {
|
||||
options: {
|
||||
|
||||
@@ -1,39 +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. Free, 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).
|
||||
|
||||
#### The Grunt task:
|
||||
If you have a Continuous Integration platform, may I suggest you to use [grunt-yellowlabtools](https://github.com/gmetais/grunt-yellowlabtools)? You set it up once, then the task fails if the front-end quality conditions you defined are not met.
|
||||
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
|
||||
@@ -41,18 +75,10 @@ If you have a Continuous Integration platform, may I suggest you to use [grunt-y
|
||||
If your project is not accessible from outside, or if you want to fork and improve the tool, you can build your own instance. The documentation is [here](https://github.com/gmetais/YellowLabTools/wiki/Install-your-private-server).
|
||||
|
||||
|
||||
## Help needed
|
||||
|
||||
There are so many things left to do, **your help would be greatly appreciated**! Please report bugs, ask for evolutions and come code with me.
|
||||
|
||||
|
||||
## License
|
||||
Please read the [license](LICENSE).
|
||||
Icons are the property of [IcoMoon.io](https://icomoon.io/)
|
||||
|
||||
|
||||
## Author
|
||||
Gaël Métais. I'm a webperf freelance based in Paris. If you understand french, you can visit [my website](http://www.gaelmetais.com).
|
||||
Gaël Métais. I'm a webperf freelance. Follow me on Twitter [@gaelmetais](https://twitter.com/gaelmetais), I tweet about Web Performances, Front-end and new versions of YellowLabTools!
|
||||
|
||||
If you understand French, you can visit [my website](http://www.gaelmetais.com) (will be soon in English too).
|
||||
|
||||
|
||||
## Contributors
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
var debug = require('debug')('ylt:cli');
|
||||
var meow = require('meow');
|
||||
var path = require('path');
|
||||
var debug = require('debug')('ylt:cli');
|
||||
var meow = require('meow');
|
||||
var path = require('path');
|
||||
var EasyXml = require('easyxml');
|
||||
|
||||
var ylt = require('../lib/index');
|
||||
var ylt = require('../lib/index');
|
||||
|
||||
var cli = meow({
|
||||
help: [
|
||||
@@ -14,11 +15,11 @@ var cli = meow({
|
||||
'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.',
|
||||
' --reporter The output format: "json" or "xml". Default is "json".',
|
||||
''
|
||||
].join('\n'),
|
||||
pkg: '../package.json'
|
||||
@@ -49,11 +50,6 @@ if (screenshot) {
|
||||
options.screenshot = cli.flags.screenshot;
|
||||
}
|
||||
|
||||
// Deep JS analysis option
|
||||
if (cli.flags.jsDeepAnalysis === true || cli.flags.jsDeepAnalysis === 'true') {
|
||||
options.jsDeepAnalysis = true;
|
||||
}
|
||||
|
||||
// Device simulation
|
||||
options.device = cli.flags.device || 'desktop';
|
||||
|
||||
@@ -67,6 +63,12 @@ options.cookie = cli.flags.cookie || null;
|
||||
options.authUser = cli.flags.authUser || null;
|
||||
options.authPass = cli.flags.authPass || null;
|
||||
|
||||
// Output format
|
||||
if (cli.flags.reporter && cli.flags.reporter !== 'json' && cli.flags.reporter !== 'xml') {
|
||||
console.error('Incorrect parameters: reporter has to be "json" or "xml"');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
(function execute(url, options) {
|
||||
'use strict';
|
||||
@@ -76,7 +78,14 @@ options.authPass = cli.flags.authPass || null;
|
||||
then(function(data) {
|
||||
|
||||
debug('Success');
|
||||
console.log(JSON.stringify(data, null, 2));
|
||||
switch(cli.flags.reporter) {
|
||||
case 'xml':
|
||||
var serializer = new EasyXml();
|
||||
console.log(serializer.render(data));
|
||||
break;
|
||||
default:
|
||||
console.log(JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
}).fail(function(err) {
|
||||
|
||||
|
||||
@@ -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,14 +0,0 @@
|
||||
{
|
||||
"name": "yellowlabtools",
|
||||
"dependencies": {
|
||||
"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.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"angular": "~1.3.8"
|
||||
}
|
||||
}
|
||||
|
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 |
@@ -109,14 +109,18 @@
|
||||
.summary .notations .criteria .table {
|
||||
width: 100%;
|
||||
}
|
||||
.summary .notations .criteria .table > div:hover > div {
|
||||
.summary .notations .criteria .table > a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
.summary .notations .criteria .table > a:hover > div {
|
||||
background: #EBD8E2;
|
||||
cursor: pointer;
|
||||
}
|
||||
.summary .notations .criteria .table > div:hover > div.info {
|
||||
.summary .notations .criteria .table > a:hover > div.info {
|
||||
background: #FFF;
|
||||
}
|
||||
.summary .notations .criteria .table > div:hover > div.info .icon-question {
|
||||
.summary .notations .criteria .table > a:hover > div.info .icon-question {
|
||||
color: #EBD8E2;
|
||||
}
|
||||
.summary .notations .criteria .grade {
|
||||
|
||||
@@ -33,7 +33,9 @@
|
||||
.settings select {
|
||||
font-size: 1em;
|
||||
}
|
||||
.settings input[type=text] {
|
||||
.settings input[type=text],
|
||||
.settings input[type=password],
|
||||
.settings textarea {
|
||||
width: 100%;
|
||||
min-width: 4em;
|
||||
}
|
||||
@@ -128,6 +130,7 @@
|
||||
.advanced > div > div.label {
|
||||
width: 25%;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.advanced .subTable {
|
||||
display: table;
|
||||
|
||||
@@ -29,9 +29,16 @@
|
||||
border-radius: 0.5em;
|
||||
margin: 0 auto 0.5em;
|
||||
}
|
||||
.rule h3 {
|
||||
margin-bottom: 0em;
|
||||
}
|
||||
.rule .okThreshold {
|
||||
font-style: italic;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.rule .message {
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
margin: 1.5em auto;
|
||||
}
|
||||
.rule .message p {
|
||||
margin: 0.5em;
|
||||
@@ -94,6 +101,7 @@
|
||||
margin: 0.2em 0;
|
||||
border-radius: 0.4em;
|
||||
z-index: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
.offendersTable .offenderButton.opens,
|
||||
.value .offenderButton.opens {
|
||||
@@ -150,6 +158,10 @@
|
||||
display: block;
|
||||
background: #ffe0cc;
|
||||
}
|
||||
.offendersTable .smallerOffenders,
|
||||
.value .smallerOffenders {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.offendersHtml {
|
||||
display: inline-block;
|
||||
}
|
||||
@@ -164,15 +176,17 @@
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
}
|
||||
.checker {
|
||||
/* Checkerboard background */
|
||||
background-color: #ddd;
|
||||
background-image: linear-gradient(45deg, #AAA 25%, transparent 25%, transparent 75%, #AAA 75%, #AAA), linear-gradient(45deg, #AAA 25%, transparent 25%, transparent 75%, #AAA 75%, #AAA);
|
||||
background-size: 1em 1em;
|
||||
background-position: 0 0, 0.5em 0.5em;
|
||||
}
|
||||
.colorPalette {
|
||||
width: 30em;
|
||||
border: 2px solid #000;
|
||||
text-align: left;
|
||||
/* 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 > div {
|
||||
display: inline-block;
|
||||
@@ -205,3 +219,50 @@
|
||||
z-index: 2;
|
||||
border: 0.2em solid #f1c40f;
|
||||
}
|
||||
.similarColors {
|
||||
margin: 1em;
|
||||
width: 20em;
|
||||
height: 6em;
|
||||
}
|
||||
.similarColors > div {
|
||||
display: inline-block;
|
||||
width: 10em;
|
||||
height: 3.5em;
|
||||
padding-top: 2.5em;
|
||||
}
|
||||
.totalWeightPie {
|
||||
max-width: 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 {
|
||||
display: block;
|
||||
max-height: 4em;
|
||||
max-width: 8em;
|
||||
border: 1px solid #000;
|
||||
margin: 1em auto 0.2em;
|
||||
}
|
||||
|
||||
@@ -62,34 +62,34 @@
|
||||
background: #F04DA7;
|
||||
}
|
||||
.timeline .domComplete.interval {
|
||||
background: #ede3ff;
|
||||
background: #EDE3FF;
|
||||
}
|
||||
.timeline .domComplete .color {
|
||||
background: #c2a3ff;
|
||||
background: #C2A3FF;
|
||||
}
|
||||
.timeline .domContentLoadedEnd.interval {
|
||||
background: #d8f0f0;
|
||||
background: #D8F0F0;
|
||||
}
|
||||
.timeline .domContentLoadedEnd .color {
|
||||
background: #7ecccc;
|
||||
background: #7ECCCC;
|
||||
}
|
||||
.timeline .domContentLoaded.interval {
|
||||
background: #e0ffd1;
|
||||
background: #E0FFD1;
|
||||
}
|
||||
.timeline .domContentLoaded .color {
|
||||
background: #a7e846;
|
||||
background: #A7E846;
|
||||
}
|
||||
.timeline .domInteractive.interval {
|
||||
background: #fffccc;
|
||||
background: #FFFCCC;
|
||||
}
|
||||
.timeline .domInteractive .color {
|
||||
background: #ffe433;
|
||||
background: #FFE433;
|
||||
}
|
||||
.timeline .domCreation.interval {
|
||||
background: #ffe0cc;
|
||||
background: #FFE0CC;
|
||||
}
|
||||
.timeline .domCreation .color {
|
||||
background: #ff6600;
|
||||
background: #FF6600;
|
||||
}
|
||||
.timeline .tooltip.detailsOverlay {
|
||||
position: absolute;
|
||||
@@ -143,19 +143,25 @@
|
||||
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 {
|
||||
.table > div,
|
||||
.table > a {
|
||||
display: table-row;
|
||||
}
|
||||
.table > .headers > div {
|
||||
font-weight: bold;
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
.table > div > div {
|
||||
.table > div > div,
|
||||
.table > a > div {
|
||||
padding: 0.1em 1em;
|
||||
background: #f2f2f2;
|
||||
display: table-cell;
|
||||
@@ -288,27 +294,45 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
.table > div > .startTime.domComplete {
|
||||
background: #ede3ff;
|
||||
background: #EDE3FF;
|
||||
}
|
||||
.table > div > .startTime.domContentLoadedEnd {
|
||||
background: #d8f0f0;
|
||||
background: #D8F0F0;
|
||||
}
|
||||
.table > div > .startTime.domContentLoaded {
|
||||
background: #e0ffd1;
|
||||
background: #E0FFD1;
|
||||
}
|
||||
.table > div > .startTime.domInteractive {
|
||||
background: #fffccc;
|
||||
background: #FFFCCC;
|
||||
}
|
||||
.table > div > .startTime.domCreation {
|
||||
background: #ffe0cc;
|
||||
background: #FFE0CC;
|
||||
}
|
||||
.execution .icon-warning {
|
||||
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></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.5 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></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.0 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></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.2 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></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.1 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></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 |
@@ -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></g><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>
|
||||
<?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>
|
||||
|
Before Width: | Height: | Size: 3.2 KiB 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></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.6 KiB After Width: | Height: | Size: 1.2 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></g><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>
|
||||
<?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>
|
||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 584 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></g><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>
|
||||
<?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>
|
||||
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 774 B |
@@ -1,3 +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></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 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>
|
||||
<?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.3 KiB After Width: | Height: | Size: 887 B |
@@ -19,12 +19,29 @@ var yltApp = angular.module('YellowLabTools', [
|
||||
]);
|
||||
|
||||
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()});
|
||||
});
|
||||
|
||||
// GitHub star button (asynchronously loaded iframe)
|
||||
window.addEventListener('load', function() {
|
||||
window.document.getElementById('ghbtn').src = 'http://ghbtns.com/github-btn.html?user=gmetais&repo=YellowLabTools&type=star&count=true&size=large';
|
||||
});
|
||||
}]);
|
||||
|
||||
yltApp.config(['$routeProvider', '$locationProvider',
|
||||
|
||||
@@ -26,29 +26,26 @@ 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.showRulePage = function(ruleName) {
|
||||
$location.path('/result/' + $scope.runId + '/rule/' + ruleName);
|
||||
};
|
||||
$scope.tweetText = 'I\'ve discovered this cool open-source tool that audits the front-end quality of a web page: ';
|
||||
}
|
||||
|
||||
$scope.testAgain = function() {
|
||||
API.relaunchTest($scope.result);
|
||||
};
|
||||
|
||||
/// 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) {
|
||||
|
||||
@@ -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;
|
||||
@@ -22,6 +31,72 @@ 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'
|
||||
};
|
||||
}
|
||||
|
||||
// Init "Breakpoints" chart
|
||||
if ($scope.policyName === 'cssBreakpoints' && $scope.rule.value > 0) {
|
||||
|
||||
// Seek for the biggest breakpoint
|
||||
var max = 0;
|
||||
$scope.rule.offendersObj.forEach(function(offender) {
|
||||
if (offender.pixels > max) {
|
||||
max = offender.pixels;
|
||||
}
|
||||
});
|
||||
max = Math.max(max + 100, 1400);
|
||||
|
||||
// We group offenders 10px by 10px
|
||||
var GROUP_SIZE = 20;
|
||||
|
||||
// Generate an empty array of values
|
||||
$scope.breakpointsLabels = [];
|
||||
$scope.breakpointsData = [[]];
|
||||
for (var i = 0; i <= max / GROUP_SIZE; i++) {
|
||||
$scope.breakpointsLabels[i] = '';
|
||||
$scope.breakpointsData[0][i] = 0;
|
||||
}
|
||||
|
||||
// Fill it with results
|
||||
$scope.rule.offendersObj.forEach(function(offender) {
|
||||
var group = Math.floor((offender.pixels + 1) / GROUP_SIZE);
|
||||
|
||||
if ($scope.breakpointsLabels[group] !== '') {
|
||||
$scope.breakpointsLabels[group] += '/';
|
||||
}
|
||||
$scope.breakpointsLabels[group] += offender.breakpoint;
|
||||
|
||||
$scope.breakpointsData[0][group] += offender.count;
|
||||
});
|
||||
|
||||
$scope.breakpointsSeries = ['Number of CSS rules per breakpoint'];
|
||||
$scope.breakpointsColours = ['#9c4274'];
|
||||
$scope.breakpointsOptions = {
|
||||
scaleShowGridLines: false,
|
||||
barShowStroke: false,
|
||||
showTooltips: false,
|
||||
pointDot: false,
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
strokeColor: 'rgba(20, 200, 20, 1)',
|
||||
scaleFontSize: 9
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
$scope.backToDashboard = function() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -608,7 +608,12 @@
|
||||
for (var i = 0 ; i < parsedBacktrace.length ; i++) {
|
||||
html += '<div>';
|
||||
html += '<div>' + (parsedBacktrace[i].fnName || '(anonymous function)') + '</div>';
|
||||
html += '<div class="trace">' + getUrlLink(parsedBacktrace[i].filePath, 40) + ':' + parsedBacktrace[i].line + '</div>';
|
||||
html += '<div class="trace">' + getUrlLink(parsedBacktrace[i].filePath, 40) + '</div>';
|
||||
if (parsedBacktrace[i].column) {
|
||||
html += '<div>' + parsedBacktrace[i].line + ':' + parsedBacktrace[i].column + '</div>';
|
||||
} else {
|
||||
html += '<div>line ' + parsedBacktrace[i].line + '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
}
|
||||
@@ -630,6 +635,17 @@
|
||||
var fnName = null, fileAndLine;
|
||||
|
||||
var withFnResult = /^([^\s\(]+) \((.+:\d+)\)$/.exec(trace);
|
||||
|
||||
if (withFnResult === null) {
|
||||
// Try the PhantomJS 2 format
|
||||
withFnResult = /^([^\s\(]+) \((.+:\d+:\d+)\)$/.exec(trace);
|
||||
}
|
||||
|
||||
if (withFnResult === null) {
|
||||
// Try the PhantomJS 2 ERROR format
|
||||
withFnResult = /^([^\s\(]+) (http.+:\d+)$/.exec(trace);
|
||||
}
|
||||
|
||||
if (withFnResult === null) {
|
||||
fileAndLine = trace;
|
||||
} else {
|
||||
@@ -637,14 +653,22 @@
|
||||
fileAndLine = withFnResult[2];
|
||||
}
|
||||
|
||||
var fileAndLineSplit = /^(.*):(\d+)$/.exec(fileAndLine);
|
||||
// And now the second part
|
||||
var fileAndLineSplit = /^(.*):(\d+):(\d+)$/.exec(fileAndLine);
|
||||
|
||||
if (fileAndLineSplit === null) {
|
||||
fileAndLineSplit = /^(.*):(\d+)$/.exec(fileAndLine);
|
||||
}
|
||||
|
||||
var filePath = fileAndLineSplit[1];
|
||||
var line = fileAndLineSplit[2];
|
||||
var column = fileAndLineSplit[3];
|
||||
|
||||
out.push({
|
||||
fnName: fnName,
|
||||
filePath: filePath,
|
||||
line: line
|
||||
line: line,
|
||||
column: column
|
||||
});
|
||||
});
|
||||
|
||||
@@ -766,10 +790,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) {
|
||||
@@ -843,4 +880,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';
|
||||
};
|
||||
});
|
||||
|
||||
})();
|
||||
@@ -9,25 +9,26 @@ apiService.factory('API', ['$location', 'Runs', 'Results', function($location, R
|
||||
url: url,
|
||||
waitForResponse: false,
|
||||
screenshot: true,
|
||||
jsTimeline: true,
|
||||
device: settings.device,
|
||||
waitForSelector: settings.waitForSelector,
|
||||
cookie: settings.cookie,
|
||||
authUser: settings.authUser,
|
||||
authPass: settings.authPass,
|
||||
blockDomain: settings.blockDomain,
|
||||
allowedDomains: settings.allowedDomains,
|
||||
noExternals: settings.noExternals
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
if (settings.domainsBlackOrWhite === 'black') {
|
||||
runObject.blockDomain = this.parseDomains(settings.domains);
|
||||
} else if (settings.domainsBlackOrWhite === 'white') {
|
||||
var allowedDomains = this.parseDomains(settings.domains);
|
||||
if (allowedDomains.length > 0) {
|
||||
runObject.allowDomain = allowedDomains;
|
||||
} else {
|
||||
runObject.noExternals = true;
|
||||
}
|
||||
}
|
||||
|
||||
Runs.save(runObject, function(data) {
|
||||
@@ -43,6 +44,17 @@ apiService.factory('API', ['$location', 'Runs', 'Results', function($location, R
|
||||
|
||||
relaunchTest: function(result) {
|
||||
this.launchTest(result.params.url, result.params.options);
|
||||
},
|
||||
|
||||
parseDomains: function(textareaContent) {
|
||||
var lines = textareaContent.split('\n');
|
||||
|
||||
function removeEmptyLines (line) {
|
||||
return line.trim() !== '';
|
||||
}
|
||||
|
||||
// Remove empty lines
|
||||
return lines.filter(removeEmptyLines).join(',');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -108,7 +108,11 @@
|
||||
|
||||
.summary .notations .criteria .table {
|
||||
width: 100%;
|
||||
> div:hover > div {
|
||||
> a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
> a:hover > div {
|
||||
background: #EBD8E2;
|
||||
cursor: pointer;
|
||||
&.info {
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
input[type=text] {
|
||||
input[type=text], input[type=password], textarea {
|
||||
width: 100%;
|
||||
min-width: 4em;
|
||||
}
|
||||
@@ -147,6 +147,7 @@
|
||||
&.label {
|
||||
width: 25%;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,8 +132,12 @@ a.linkButton {
|
||||
background: #000;
|
||||
|
||||
> div {
|
||||
overflow: scroll;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.screenshotImage {
|
||||
@@ -234,14 +238,6 @@ a.linkButton {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.star {
|
||||
font-weight: bold;
|
||||
span {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 3em;
|
||||
color: #413;
|
||||
@@ -251,6 +247,9 @@ a.linkButton {
|
||||
.version {
|
||||
font-size: 0.7em;
|
||||
}
|
||||
.github {
|
||||
margin: 1em 0 0 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
[class^="icon-"]:before, [class*=" icon-"]:before {
|
||||
|
||||
@@ -32,9 +32,18 @@
|
||||
margin: 0 auto 0.5em;
|
||||
}
|
||||
|
||||
.rule h3 {
|
||||
margin-bottom: 0em;
|
||||
}
|
||||
|
||||
.rule .okThreshold {
|
||||
font-style: italic;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.rule .message {
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
margin: 1.5em auto;
|
||||
p {
|
||||
margin: 0.5em;
|
||||
}
|
||||
@@ -103,6 +112,7 @@
|
||||
margin: 0.2em 0;
|
||||
border-radius: 0.4em;
|
||||
z-index: 1;
|
||||
cursor: pointer;
|
||||
|
||||
&.opens {
|
||||
padding-right: 0.75em;
|
||||
@@ -157,6 +167,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.smallerOffenders {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
|
||||
.offendersHtml {
|
||||
@@ -176,16 +190,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;
|
||||
@@ -221,4 +237,61 @@
|
||||
border: 0.2em solid #f1c40f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.similarColors {
|
||||
margin: 1em;
|
||||
width: 20em;
|
||||
height: 6em;
|
||||
|
||||
> div {
|
||||
display: inline-block;
|
||||
width: 10em;
|
||||
height: 3.5em;
|
||||
padding-top: 2.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.totalWeightPie {
|
||||
max-width: 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 {
|
||||
display: block;
|
||||
max-height: 4em;
|
||||
max-width: 8em;
|
||||
border: 1px solid #000;
|
||||
margin: 1em auto 0.2em;
|
||||
}
|
||||
@@ -162,13 +162,19 @@
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.subFilters {
|
||||
margin-left: 3em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.table {
|
||||
display: table;
|
||||
width: 100%;
|
||||
border-spacing: 0.25em;
|
||||
}
|
||||
|
||||
.table > div {
|
||||
.table > div,
|
||||
.table > a {
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
@@ -177,7 +183,8 @@
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
|
||||
.table > div > div {
|
||||
.table > div > div,
|
||||
.table > a > div {
|
||||
padding: 0.1em 1em;
|
||||
background: #f2f2f2;
|
||||
display: table-cell;
|
||||
@@ -346,10 +353,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,15 +16,36 @@
|
||||
<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="/node_modules/angular-chart.js/dist/angular-chart.css">
|
||||
<!-- endbuild -->
|
||||
|
||||
<link rel="preconnect" href="//www.google-analytics.com">
|
||||
<link rel="dns-prefetch" href="//www.google-analytics.com">
|
||||
<link rel="preconnect" href="//ghbtns.com">
|
||||
<link rel="dns-prefetch" href="//ghbtns.com">
|
||||
<link rel="preconnect" href="https://api.github.com">
|
||||
<link rel="dns-prefetch" href="https://api.github.com">
|
||||
|
||||
<head>
|
||||
|
||||
<body ng-app="YellowLabTools">
|
||||
<div id="header"><h1>Yellow Lab <span class="icon-lab"></span> Tools</h1></div>
|
||||
<div id="body" ng-view autoscroll="true"></div>
|
||||
<div class="footer">
|
||||
<span class="version">@@version</span>
|
||||
<br><a href="/about">More about Yellow Lab Tools</a><br>
|
||||
<div class="github"><iframe id="ghbtn" frameborder="0" scrolling="0" width="145px" height="30px"></iframe></div>
|
||||
</div>
|
||||
|
||||
<!-- build:js /js/all.js -->
|
||||
<script src="/bower_components/angular/angular.min.js"></script>
|
||||
<script src="/bower_components/angular-route/angular-route.min.js"></script>
|
||||
<script src="/bower_components/angular-resource/angular-resource.min.js"></script>
|
||||
<script src="/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="/node_modules/angular/angular.min.js"></script>
|
||||
<script src="/node_modules/chart.js/Chart.min.js"></script>
|
||||
<script src="/node_modules/angular-route/angular-route.min.js"></script>
|
||||
<script src="/node_modules/angular-resource/angular-resource.min.js"></script>
|
||||
<script src="/node_modules/angular-sanitize/angular-sanitize.min.js"></script>
|
||||
<script src="/node_modules/angular-animate/angular-animate.min.js"></script>
|
||||
<script src="/node_modules/angular-local-storage/dist/angular-local-storage.min.js"></script>
|
||||
<script src="/node_modules/angular-chart.js/dist/angular-chart.min.js"></script>
|
||||
<script src="/js/app.js"></script>
|
||||
<script src="/js/controllers/indexCtrl.js"></script>
|
||||
<script src="/js/controllers/dashboardCtrl.js"></script>
|
||||
@@ -41,15 +62,6 @@
|
||||
<script src="/js/directives/gradeDirective.js"></script>
|
||||
<script src="/js/directives/offendersDirectives.js"></script>
|
||||
<!-- endbuild -->
|
||||
<head>
|
||||
|
||||
<body ng-app="YellowLabTools">
|
||||
<div id="header"><h1>Yellow Lab <span class="icon-lab"></span> Tools</h1></div>
|
||||
<div id="body" ng-view autoscroll="true"></div>
|
||||
<div class="footer">
|
||||
<a href="/about">Learn more</a><br>If you like <b>Yellow Lab Tools</b>, <a href="https://github.com/gmetais/YellowLabTools" target="_blank" class="star">give it a <span>★</span> on GitHub</a>!<br>
|
||||
<span class="version">@@version</span>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','//www.google-analytics.com/analytics.js','ga');if('@@googleAnalyticsId'.indexOf('UA-')===0){ga('create','@@googleAnalyticsId','auto');}
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
<div ng-include="'views/resultSubHeader.html'"></div>
|
||||
<div class="summary board">
|
||||
|
||||
<div ng-if="result.blockedRequests">
|
||||
<b><ng-pluralize count="result.blockedRequests.length" when="{'0': 'No blocked request', 'one': '1 blocked request', 'other': '{} blocked requests'}"></ng-pluralize>:</b>
|
||||
<div ng-repeat="request in result.blockedRequests">
|
||||
{{request}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="globalScore" ng-if="globalScore === 0 || globalScore > 0">
|
||||
<div>
|
||||
<h2>Global score</h2>
|
||||
@@ -28,27 +35,28 @@
|
||||
<div class="category">{{category.label}}</div>
|
||||
<div class="criteria">
|
||||
<div class="table" title="Click to see details">
|
||||
<div ng-repeat="ruleName in category.rules" ng-if="result.rules[ruleName]" ng-init="rule = result.rules[ruleName]"
|
||||
ng-class="{'warning': rule.abnormal}" ng-click="showRulePage(ruleName)">
|
||||
<a ng-repeat="ruleName in category.rules" ng-if="result.rules[ruleName]" ng-init="rule = result.rules[ruleName]"
|
||||
ng-class="{'warning': rule.abnormal}" href="/result/{{runId}}/rule/{{ruleName}}">
|
||||
<div class="grade">
|
||||
<grade score="rule.score"></grade>
|
||||
</div>
|
||||
<div class="label">{{rule.policy.label}}</div>
|
||||
<div class="result">
|
||||
{{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>
|
||||
</div>
|
||||
<div class="info"><span class="icon-question"></span></div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="!error && !fromSocialShare" class="apiTip">
|
||||
<b>Did you know? Yellow Lab Tools has a brand new <a href="https://github.com/gmetais/grunt-yellowlabtools" target="_blank">Grunt plugin</a>!
|
||||
<b>Did you know?</b> You can <b>bookmark</b> this result page URL or <b>share it</b>, Yellow Lab Tools keeps test results for years!
|
||||
</div>
|
||||
|
||||
<div class="tweet" ng-if="!error && !fromSocialShare">
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
</div>
|
||||
<div><input type="text" name="cookie" ng-model="settings.cookie" /></div>
|
||||
</div>
|
||||
<!--<div>
|
||||
<div>
|
||||
<div class="label">
|
||||
Authent
|
||||
<span class="settingsTooltip">
|
||||
@@ -57,19 +57,26 @@
|
||||
</div>
|
||||
<div>
|
||||
<div><span>password</div>
|
||||
<div><input type="text" class="authField" name="authPass" ng-model="settings.authPass" /></div>
|
||||
<div><input type="password" class="authField" name="authPass" ng-model="settings.authPass" /></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>-->
|
||||
<!--<div>
|
||||
<div class="label">Blocked domains</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="label">
|
||||
Block domains
|
||||
<span class="settingsTooltip">
|
||||
<span class="icon-question"></span>
|
||||
<div><b>Block some domains</b><br><br>One line per domain or subdomain.<br><br><i><b>Example:</b><br>google-analytics.com<br>ads.yahoo.com<br>ajax.googleapis.com</i><br><br>An empty whitelist will block all domains except the main domain.</div>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
Blacklist / Whitelist
|
||||
<input type="radio" name="blackOrWhite" ng-model="settings.domainsBlackOrWhite" value="black" />blacklist
|
||||
<input type="radio" name="blackOrWhite" ng-model="settings.domainsBlackOrWhite" value="white" />whitelist
|
||||
</div>
|
||||
<textarea name=""></textarea>
|
||||
<textarea name="domains" ng-model="settings.domains" rows="5"></textarea>
|
||||
</div>
|
||||
</div>-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -90,4 +97,4 @@
|
||||
<h3>JS Profiling</h3>
|
||||
<p>Untangles the JavaScript spaghetti code</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,19 @@
|
||||
<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 class="okThreshold" ng-if="rule.score < 100 && rule.policy.isOkThreshold !== undefined">
|
||||
Have
|
||||
<span ng-if="rule.policy.unit == 'bytes'">{{rule.policy.isOkThreshold | bytes}}</span>
|
||||
<span ng-if="rule.policy.unit != 'bytes'">{{rule.policy.isOkThreshold}}</span>
|
||||
<span ng-if="rule.policy.isOkThreshold > 0 && rule.policy.isOkThreshold < rule.policy.isBadThreshold">or less</span>
|
||||
<span ng-if="rule.policy.isOkThreshold > rule.policy.isBadThreshold">or more</span>
|
||||
to get the 100/100 score.
|
||||
</div>
|
||||
<div ng-bind-html="rule.policy.message" class="message"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -31,22 +43,10 @@
|
||||
<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>
|
||||
|
||||
<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>
|
||||
@@ -80,22 +80,7 @@
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'jQueryFunctionsUsed'">
|
||||
Function <b>{{offender.functionName}}</b> used {{offender.count}} times
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'jQueryNotDelegatedEvents'">
|
||||
function <b>{{offender.functionName}}</b> used on {{offender.contextLength}} DOM elements without event delegation
|
||||
<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>
|
||||
function <b>{{offender.functionName}}</b> used {{offender.count}} times
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'documentWriteCalls'">
|
||||
@@ -111,6 +96,21 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'cssRules'">
|
||||
<span ng-if="offender.file === 'inline CSS'">inline CSS</span>
|
||||
<span ng-if="offender.file !== 'inline CSS'"><url-link url="offender.file" max-length="80"></url-link></span>
|
||||
: <ng-pluralize count="offender.rules" when="{'0': '0 rule', 'one':'1 rule','other':'{} rules'}"></ng-pluralize>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'similarColors'">
|
||||
<div class="similarColors checker"><div ng-style="{'background-color': offender.color1, 'color': offender.isDark ? '#FFF' : '#000'}">{{offender.color1}}</div><div ng-style="{'background-color': offender.color2, 'color': offender.isDark ? '#FFF' : '#000'}">{{offender.color2}}</div></div>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'cssMobileFirst'">
|
||||
<b>{{offender.query}}</b> for <ng-pluralize count="offender.rules" when="{'one':'1 rule','other':'{} rules'}"></ng-pluralize>
|
||||
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'cssParsingErrors'">
|
||||
<b>{{offender.error}}</b>
|
||||
<file-and-line file="offender.file" line="offender.line" column="offender.column"></file-and-line>
|
||||
@@ -154,26 +154,29 @@
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'cssOldPropertyPrefixes'">
|
||||
{{offender.rule}} {<b>{{offender.property}}</b>: {{offender.value + '}' }}
|
||||
<br>
|
||||
<b>{{offender.message}}</b>
|
||||
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
|
||||
<b>{{offender.property}} {{offender.message}}</b>
|
||||
<div ng-if="offender.rules.length" ng-click="offender.showMore = !offender.showMore" class="offenderButton">
|
||||
<span ng-if="!offender.showMore">show</span>
|
||||
<span ng-if="offender.showMore">hide</span>
|
||||
<ng-pluralize count="offender.rules.length" when="{'one':'1 offender','other':'{} offenders'}"></ng-pluralize>
|
||||
</div>
|
||||
<div ng-if="offender.showMore" class="smallerOffenders">
|
||||
<div ng-repeat="cssRule in offender.rules">
|
||||
{{cssRule.rule}} {{'{' + offender.property}}: {{cssRule.value + '}' }}
|
||||
<file-and-line-button file="cssRule.file" line="cssRule.line" column="cssRule.column"></file-and-line-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === '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' || policyName === 'hiddenImages'">
|
||||
<img ng-src="{{offender}}" class="smallPreview checker"></img>
|
||||
<url-link url="offender" max-length="100"></url-link>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'notFound' || policyName === 'closedConnections' || policyName === 'multipleRequests' || policyName === 'cachingDisabled' || policyName === 'cachingNotSpecified'">
|
||||
<url-link url="offender" max-length="100"></url-link>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'assetsNotGzipped'">
|
||||
<url-link url="offender.file" max-length="100"></url-link>
|
||||
({{offender.type}})
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'cachingTooShort'">
|
||||
<url-link url="offender.file" max-length="100"></url-link>
|
||||
cached for <b>{{offender.ttlWithUnit}} {{offender.unit}}</b>
|
||||
@@ -192,7 +195,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div ng-if="!rule.offendersObj.list" class="offendersHtml">
|
||||
|
||||
<div ng-if="policyName === 'DOMelementMaxDepth'">
|
||||
@@ -214,14 +216,177 @@
|
||||
|
||||
<div ng-if="policyName === 'cssColors' && rule.offendersObj.count > 0">
|
||||
<p>This is the colors palette, sized by total occurrences:</p>
|
||||
<div class="colorPalette">
|
||||
<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="policyName === 'cssBreakpoints'">
|
||||
<div ng-if="rule.value > 0" class="cssBreakpointsGraph">
|
||||
<h3>Breakpoints distribution graph</h3>
|
||||
<canvas class="chart chart-line" chart-data="breakpointsData" chart-labels="breakpointsLabels" chart-options="breakpointsOptions" chart-legend="true" chart-colours="breakpointsColours" chart-series="breakpointsSeries" width="600" height="250"></canvas>
|
||||
<h3>Breakpoints list</h3>
|
||||
<div class="offendersTable">
|
||||
<div ng-repeat="offender in rule.offendersObj | orderBy:'pixels'">
|
||||
<div>Breakpoint <b>{{offender.breakpoint}}</b> involves <ng-pluralize count="offender.count" when="{'one': '1 rule', 'other': '{} rules'}"></ng-pluralize></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="rule.value === 0">
|
||||
No breakpoint
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="policyName === 'http2'">
|
||||
<h3>Protocols advertised by the server</h3>
|
||||
<div class="offendersTable">
|
||||
<div ng-repeat="protocol in rule.offendersObj.list">
|
||||
<div>{{protocol}}</div>
|
||||
</div>
|
||||
<div ng-if="!rule.offendersObj || rule.offendersObj.count == 0">
|
||||
<div>none</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="!rule && rule !== null" class="notFound">
|
||||
<h2>404</h2>
|
||||
Rule "{{policyName}}"" not found
|
||||
|
||||
@@ -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">
|
||||
@@ -60,11 +60,30 @@
|
||||
</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,82 +66,23 @@ var policies = {
|
||||
};
|
||||
}
|
||||
},
|
||||
/*"DOMaccesses": {
|
||||
"DOMaccesses": {
|
||||
"tool": "jsExecutionTransformer",
|
||||
"label": "DOM access",
|
||||
"message": "<p>TODO</p><p>TODO</p>",
|
||||
"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": 2000,
|
||||
"isBadThreshold": 1500,
|
||||
"isAbnormalThreshold": 3000,
|
||||
"hasOffenders": false
|
||||
},*/
|
||||
"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>",
|
||||
"isOkThreshold": 50,
|
||||
"isBadThreshold": 1000,
|
||||
"isAbnormalThreshold": 2000,
|
||||
"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",
|
||||
@@ -175,42 +116,13 @@ var policies = {
|
||||
};
|
||||
}
|
||||
},
|
||||
"eventsBound": {
|
||||
"tool": "phantomas",
|
||||
"label": "Events bound",
|
||||
"message": "<p>Binding too many events has a cost.</p><p>It can be avoided by using \"event delegation\". Instead of binding events on each element one by one, events delegation binds them on the top level document element and uses the bubbling principle. It will imperceptibly slow down the event when it occurs, but the loading of the page will speed-up.</p>",
|
||||
"isOkThreshold": 100,
|
||||
"isBadThreshold": 800,
|
||||
"isAbnormalThreshold": 1500,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
return {
|
||||
count: offenders.length,
|
||||
list: offenders.map(function(offender) {
|
||||
var parts = /^"(.*)" ?bound ?to ?"(.*)"$/.exec(offender);
|
||||
|
||||
if (!parts) {
|
||||
debug('eventsBound offenders transform function error with "%s"', offender);
|
||||
return {
|
||||
parseError: offender
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
eventName: parts[1],
|
||||
element: offendersHelpers.domPathToDomElementObj(parts[2])
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
},
|
||||
"eventsScrollBound": {
|
||||
"tool": "phantomas",
|
||||
"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": 12,
|
||||
"isAbnormalThreshold": 15,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
return {
|
||||
@@ -238,10 +150,10 @@ var policies = {
|
||||
"DOMaccessesOnScroll": {
|
||||
"tool": "jsExecutionTransformer",
|
||||
"label": "DOM access on scroll",
|
||||
"message": "<p>This rule counts the number of DOM-accessing functions calls, 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>",
|
||||
"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 debouncing</a>.</p>",
|
||||
"isOkThreshold": 1,
|
||||
"isBadThreshold": 12,
|
||||
"isAbnormalThreshold": 25,
|
||||
"isBadThreshold": 20,
|
||||
"isAbnormalThreshold": 35,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
return offenders;
|
||||
@@ -278,22 +190,13 @@ 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",
|
||||
"message": "<p>They slow down the page construction, especially if they are used to insert scripts in the page. Remove them ASAP.</p><p>If you cannot remove them because they come from a third-party script (such as ads), have a look at <a href=\"https://github.com/krux/postscribe\" target=\"_blank\">PostScribe</a>.</p>",
|
||||
"isOkThreshold": 0,
|
||||
"isBadThreshold": 10,
|
||||
"isAbnormalThreshold": 20,
|
||||
"isBadThreshold": 5,
|
||||
"isAbnormalThreshold": 10,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
return {
|
||||
@@ -344,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": 40,
|
||||
"isBadThreshold": 200,
|
||||
"isAbnormalThreshold": 700,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
return {
|
||||
@@ -364,7 +267,7 @@ var policies = {
|
||||
},
|
||||
"jQueryVersion": {
|
||||
"label": "jQuery version",
|
||||
"message": "<p>Current latest versions of jQuery are 1.11 (with support for old IE versions) and 2.1 (without).</p><p>Each new version of jQuery optimizes performances. Do not keep an old version of jQuery. Updating can sometimes break a few things, but it is generally quite easy to fix them up. So don't hesitate.</p>",
|
||||
"message": "<p>Current latest versions of jQuery are 1.12 (with support for old IE versions) and 2.2 (without).</p><p>Each new version of jQuery optimizes performances. Do not keep an old version of jQuery. Updating can sometimes break a few things, but it is generally quite easy to fix them up. So don't hesitate.</p>",
|
||||
"hasOffenders": false,
|
||||
"scoreFn": function(data) {
|
||||
var differentVersions = data.toolsResults.phantomas.metrics.jQueryVersionsLoaded;
|
||||
@@ -376,30 +279,34 @@ var policies = {
|
||||
var value = data.toolsResults.phantomas.metrics.jQueryVersion;
|
||||
var score;
|
||||
|
||||
if (value.indexOf('1.11.') === 0 ||
|
||||
value.indexOf('1.12.') === 0 ||
|
||||
value.indexOf('2.1.') === 0 ||
|
||||
if (value.indexOf('1.12.') === 0 ||
|
||||
value.indexOf('2.2.') === 0 ||
|
||||
value.indexOf('3.0.') === 0) {
|
||||
value.indexOf('1.13.') === 0 ||
|
||||
value.indexOf('2.3.') === 0 ||
|
||||
value.indexOf('3.0.') === 0 ||
|
||||
value.indexOf('3.1.') === 0) {
|
||||
score = 100;
|
||||
} else if (value.indexOf('1.11.') === 0 ||
|
||||
value.indexOf('2.1.') === 0) {
|
||||
score = 90;
|
||||
} else if (value.indexOf('1.10.') === 0 ||
|
||||
value.indexOf('2.0.') === 0) {
|
||||
score = 90;
|
||||
} else if (value.indexOf('1.9.') === 0) {
|
||||
score = 70;
|
||||
} else if (value.indexOf('1.8.') === 0) {
|
||||
} else if (value.indexOf('1.9.') === 0) {
|
||||
score = 50;
|
||||
} else if (value.indexOf('1.7.') === 0) {
|
||||
} else if (value.indexOf('1.8.') === 0) {
|
||||
score = 40;
|
||||
} else if (value.indexOf('1.6.') === 0) {
|
||||
} else if (value.indexOf('1.7') === 0) {
|
||||
score = 30;
|
||||
} else if (value.indexOf('1.5.') === 0) {
|
||||
} else if (value.indexOf('1.6') === 0) {
|
||||
score = 20;
|
||||
} else if (value.indexOf('1.4.') === 0) {
|
||||
} else if (value.indexOf('1.5') === 0) {
|
||||
score = 10;
|
||||
} else if (value.indexOf('1.3.') === 0) {
|
||||
} else if (value.indexOf('1.4') === 0) {
|
||||
score = 0;
|
||||
} else if (value.indexOf('1.2.') === 0) {
|
||||
} else if (value.indexOf('1.3') === 0) {
|
||||
score = 0;
|
||||
} else if (value.indexOf('1.2') === 0) {
|
||||
score = 0;
|
||||
} else {
|
||||
debug('Unknown jQuery version "%s"', value);
|
||||
@@ -433,12 +340,21 @@ var policies = {
|
||||
"jQueryFunctionsUsed": {
|
||||
"tool": "jsExecutionTransformer",
|
||||
"label": "jQuery usage",
|
||||
"message": "<p>This is the number of different 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>",
|
||||
"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",
|
||||
@@ -446,7 +362,7 @@ var policies = {
|
||||
"isOkThreshold": 1,
|
||||
"isBadThreshold": 100,
|
||||
"isAbnormalThreshold": 180,
|
||||
"hasOffenders": true
|
||||
"hasOffenders": false
|
||||
},
|
||||
"cssParsingErrors": {
|
||||
"tool": "phantomas",
|
||||
@@ -454,7 +370,7 @@ var policies = {
|
||||
"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": 8,
|
||||
"isAbnormalThreshold": 20,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
return {
|
||||
@@ -508,7 +424,40 @@ var policies = {
|
||||
"isOkThreshold": 750,
|
||||
"isBadThreshold": 3000,
|
||||
"isAbnormalThreshold": 4500,
|
||||
"hasOffenders": false
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
var hasInline = false;
|
||||
var inlineCount = 0;
|
||||
var files = [];
|
||||
|
||||
offenders.forEach(function(line) {
|
||||
if (line.indexOf('[inline CSS]: ') === 0) {
|
||||
hasInline = true;
|
||||
inlineCount += parseInt(line.substr(14));
|
||||
} else {
|
||||
var parts = /^<(.*)>: (\d+)$/.exec(line);
|
||||
|
||||
if (parts) {
|
||||
files.push({
|
||||
file: parts[1],
|
||||
rules: parseInt(parts[2], 10)
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (hasInline) {
|
||||
files.push({
|
||||
file: 'inline CSS',
|
||||
rules: inlineCount
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
count: files.length,
|
||||
list: files
|
||||
};
|
||||
}
|
||||
},
|
||||
"cssComplexSelectors": {
|
||||
"tool": "phantomas",
|
||||
@@ -551,8 +500,8 @@ var policies = {
|
||||
},
|
||||
"cssColors": {
|
||||
"tool": "phantomas",
|
||||
"label": "Different colors",
|
||||
"message": "<p>This is the number of different colors defined in CSS.</p><p>Your CSS project will be easier to maintain if you keep a small color set.</p>",
|
||||
"label": "Colors count",
|
||||
"message": "<p>This is the number of different colors defined in CSS.</p><p>Your CSS will be easier to maintain if you keep a small color set.</p>",
|
||||
"isOkThreshold": 30,
|
||||
"isBadThreshold": 150,
|
||||
"isAbnormalThreshold": 400,
|
||||
@@ -595,6 +544,58 @@ var policies = {
|
||||
};
|
||||
}
|
||||
},
|
||||
"similarColors": {
|
||||
"tool": "colorDiff",
|
||||
"label": "Similar colors",
|
||||
"message": "<p>This is the list of colors found in the stylesheets, that are very close to each other. The eye can barely see the difference.</p><p>Use this list to reduce the number of colors in your palette, it will be easier to maintain.</p>",
|
||||
"isOkThreshold": 0,
|
||||
"isBadThreshold": 40,
|
||||
"isAbnormalThreshold": 80,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
return {
|
||||
count: offenders.length,
|
||||
list: offenders
|
||||
};
|
||||
}
|
||||
},
|
||||
"cssBreakpoints": {
|
||||
"tool": "mediaQueriesChecker",
|
||||
"label": "Breakpoints count",
|
||||
"message": "<p>This is the number of different breakpoints found in the stylesheets' media queries.</p><p>Please note this rule is based on <i>min-width</i>, <i>max-width</i>, <i>min-device-width</i> and <i>max-device-width</i> media queries only.</p><p>Your CSS will be easier to maintain if you keep a reasonable number of breakpoints. Try to make a fluid design - using percents - to avoid the creation of numerous breakpoints.</p>",
|
||||
"isOkThreshold": 6,
|
||||
"isBadThreshold": 40,
|
||||
"isAbnormalThreshold": 60,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
var offendersTable = [];
|
||||
|
||||
for (var offender in offenders) {
|
||||
offendersTable.push({
|
||||
breakpoint: offender,
|
||||
count: offenders[offender].count,
|
||||
pixels: offenders[offender].pixels
|
||||
});
|
||||
}
|
||||
|
||||
return offendersTable;
|
||||
}
|
||||
},
|
||||
"cssMobileFirst": {
|
||||
"tool": "mediaQueriesChecker",
|
||||
"label": "Not mobile-first media queries",
|
||||
"message": "<p>This is the number of CSS rules inside media queries that address small screens.</p><p>The common good practice, when creating a responsive website, is to write it \"mobile-first\". More explanation in <a href=\"http://www.sitepoint.com/introduction-mobile-first-media-queries\" target=\"_blank\">this great article</a>.</p>",
|
||||
"isOkThreshold": 25,
|
||||
"isBadThreshold": 200,
|
||||
"isAbnormalThreshold": 1000,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
return {
|
||||
count: offenders.length,
|
||||
list: offenders
|
||||
};
|
||||
}
|
||||
},
|
||||
"cssImports": {
|
||||
"tool": "phantomas",
|
||||
"label": "Uses of @import",
|
||||
@@ -825,36 +826,53 @@ var policies = {
|
||||
"cssOldPropertyPrefixes": {
|
||||
"tool": "phantomas",
|
||||
"label": "Old prefixes",
|
||||
"message": "<p>Many property prefixes such as -moz- or -webkit- are not needed anymore, or by very few people. You can remove them or replace them with the non-prefixed version. This will help reducing your stylesheets weight.</p>",
|
||||
"message": "<p>Many property prefixes such as -moz- or -webkit- are not needed anymore, or by very few people. Sometimes, they have never even existed. You can remove them or replace them with the non-prefixed version. This will help reducing your stylesheets weight.</p><p>The prefixes database comes from <a href=\"http://caniuse.com/\" target=\"_blank\">Can I Use</a>.</p>",
|
||||
"isOkThreshold": 0,
|
||||
"isBadThreshold": 75,
|
||||
"isAbnormalThreshold": 300,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
var properties = {};
|
||||
offenders.forEach(function(offender) {
|
||||
var splittedOffender = offendersHelpers.cssOffenderPattern(offender);
|
||||
|
||||
var parts = /^([^{]*)(?: ?{ ?([^ ]+): (.*) ?}) \/\/ (.*)$/.exec(splittedOffender.css);
|
||||
|
||||
if (!parts) {
|
||||
debug('cssOldPropertyPrefixes offenders transform function error with "%s"', offender);
|
||||
return {
|
||||
parseError: offender
|
||||
};
|
||||
}
|
||||
|
||||
var propertyName = parts[2];
|
||||
|
||||
if (!properties[propertyName]) {
|
||||
properties[propertyName] = {
|
||||
property: propertyName,
|
||||
message: parts[4],
|
||||
rules: []
|
||||
};
|
||||
}
|
||||
|
||||
properties[propertyName].rules.push({
|
||||
rule: parts[1],
|
||||
value: parts[3],
|
||||
file: splittedOffender.file,
|
||||
line: splittedOffender.line,
|
||||
column: splittedOffender.column
|
||||
});
|
||||
});
|
||||
|
||||
// Object to array
|
||||
var list = [];
|
||||
for (var propertyName in properties) {
|
||||
list.push(properties[propertyName]);
|
||||
}
|
||||
|
||||
return {
|
||||
count: offenders.length,
|
||||
list: offenders.map(function(offender) {
|
||||
var splittedOffender = offendersHelpers.cssOffenderPattern(offender);
|
||||
|
||||
var parts = /^([^{]*)(?: ?{ ?([^ ]+): (.*) ?}) \/\/ (.*)$/.exec(splittedOffender.css);
|
||||
|
||||
if (!parts) {
|
||||
debug('cssOldPropertyPrefixes offenders transform function error with "%s"', offender);
|
||||
return {
|
||||
parseError: offender
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
rule: parts[1],
|
||||
property: parts[2],
|
||||
value: parts[3],
|
||||
message: parts[4],
|
||||
file: splittedOffender.file,
|
||||
line: splittedOffender.line,
|
||||
column: splittedOffender.column
|
||||
};
|
||||
})
|
||||
list: list
|
||||
};
|
||||
}
|
||||
},
|
||||
@@ -931,211 +949,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><p>Please note that Yellow Lab Tools' engine (PhantomJS) is not compatible with image srcset (unless you use a polyfill). This can lead to incorrect page weight.</p>",
|
||||
"isOkThreshold": 716800,
|
||||
"isBadThreshold": 2097152,
|
||||
"isAbnormalThreshold": 3145728,
|
||||
"hasOffenders": true,
|
||||
"unit": 'bytes'
|
||||
},
|
||||
"imageOptimization": {
|
||||
"tool": "weightChecker",
|
||||
"label": "Image optimization",
|
||||
"message": "<p>This metric measures the number of bytes that could be saved by optimizing images.</p><p>Image optimization is generally one of the easiest way to reduce a page weight, and as a result, the page load time. Don't use Photoshop or other image editing tools, they're not very good for optimization. Use specialized tools such as <a href=\"https://kraken.io/\" target=\"_blank\">Kraken.io</a> or the excellent <a href=\"https://imageoptim.com/\" target=\"_blank\">ImageOptim</a> on Mac. For SVG images, you can use <a href=\"https://jakearchibald.github.io/svgomg/\" target=\"_blank\">SVGOMG</a></p><p>The tools in use in YellowLabTools are not set to their maximum optimization power (JPEG quality 85), so you might be able to compress even more!</p><p>Please note that Yellow Lab Tools' engine (PhantomJS) is not compatible with image srcset (unless you use a polyfill). This can lead to incorrect page weight.</p>",
|
||||
"isOkThreshold": 10240,
|
||||
"isBadThreshold": 122880,
|
||||
"isAbnormalThreshold": 307200,
|
||||
"hasOffenders": true,
|
||||
"unit": 'bytes'
|
||||
},
|
||||
"gzipCompression": {
|
||||
"tool": "weightChecker",
|
||||
"label": "Gzip compression",
|
||||
"message": "<p>Measures 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])
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
},
|
||||
@@ -1148,41 +1036,12 @@ var policies = {
|
||||
"isAbnormalThreshold": 1,
|
||||
"hasOffenders": true
|
||||
},
|
||||
"assetsNotGzipped": {
|
||||
"tool": "phantomas",
|
||||
"label": "Not gzipped",
|
||||
"message": "<p>This is the number of requests that should be compressed with gzip but aren't.</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": 0,
|
||||
"isBadThreshold": 12,
|
||||
"isAbnormalThreshold": 20,
|
||||
"hasOffenders": true,
|
||||
"offendersTransformFn": function(offenders) {
|
||||
return {
|
||||
count: offenders.length,
|
||||
list: offenders.map(function(offender) {
|
||||
var parts = /^([^ ]*) \((.+)\)$/.exec(offender);
|
||||
|
||||
if (!parts) {
|
||||
debug('assetsNotGzipped offenders transform function error with "%s"', offender);
|
||||
return {
|
||||
parseError: offender
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
file: parts[1],
|
||||
type: parts[2]
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
},
|
||||
"closedConnections": {
|
||||
"tool": "phantomas",
|
||||
"label": "Connections closed",
|
||||
"message": "<p>This counts the number of requests not keeping the connection alive (specifying \"Connection: close\" in the response headers). It is only counting a request if it is followed by another request on the same domain.</p><p>This is slowing down the next request, because the brower needs to open a new connection to the server, which means 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
|
||||
},
|
||||
@@ -1195,6 +1054,62 @@ 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": "Below the fold 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
|
||||
},
|
||||
"hiddenImages": {
|
||||
"tool": "phantomas",
|
||||
"label": "Hidden images",
|
||||
"message": "<p>List of all images that have a display:none property, or one of their parents. These images are loaded by the browser even if they're not visible. You might be able to find a way to lazy-load them, only when they get visible.</p><p>Trackers are an exception, you'd better hide them.</p>",
|
||||
"isOkThreshold": 1,
|
||||
"isBadThreshold": 12,
|
||||
"isAbnormalThreshold": 30,
|
||||
"hasOffenders": true
|
||||
},
|
||||
"http2": {
|
||||
"label": "HTTP/2 or SPDY",
|
||||
"message": "<p>HTTP/2 is the latest version of the HTTP protocol and is designed to optimize load speed. SPDY is deprecated but still very well supported.</p><p>The latest versions of all major browsers are now compatible. The difficulty is on the server side, where technologies are not quite ready yet.</p>",
|
||||
"hasOffenders": true,
|
||||
"scoreFn": function(data) {
|
||||
if (!data.toolsResults.http2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var isHttp2 = data.toolsResults.http2.metrics.http2;
|
||||
|
||||
var result = {
|
||||
value: isHttp2 ? 'Yes' : 'No',
|
||||
score: isHttp2 ? 100 : 0,
|
||||
bad: !isHttp2,
|
||||
abnormal: false,
|
||||
abnormalityScore: 0
|
||||
};
|
||||
|
||||
if (data.toolsResults.http2.offenders) {
|
||||
result.offendersObj = {
|
||||
count: data.toolsResults.http2.offenders.http2.length,
|
||||
list: data.toolsResults.http2.offenders.http2
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
},
|
||||
"cachingDisabled": {
|
||||
"tool": "phantomas",
|
||||
"label": "Caching disabled",
|
||||
@@ -1271,35 +1186,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,30 @@
|
||||
{
|
||||
"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,
|
||||
"hiddenImages": 1
|
||||
}
|
||||
},
|
||||
"domComplexity": {
|
||||
"label": "DOM complexity",
|
||||
"policies": {
|
||||
"DOMelementsCount": 1,
|
||||
"DOMelementsCount": 3,
|
||||
"DOMelementMaxDepth": 1,
|
||||
"iframesCount": 1,
|
||||
"DOMidDuplicated": 1
|
||||
@@ -12,11 +33,9 @@
|
||||
"domManipulations": {
|
||||
"label": "DOM manipulations",
|
||||
"policies": {
|
||||
"DOMinserts": 2,
|
||||
"DOMqueries": 1,
|
||||
"DOMqueriesWithoutResults": 2,
|
||||
"DOMqueriesAvoidable": 2,
|
||||
"eventsBound": 1
|
||||
"DOMaccesses": 3,
|
||||
"queriesWithoutResults": 1,
|
||||
"DOMqueriesAvoidable": 1
|
||||
}
|
||||
},
|
||||
"scroll": {
|
||||
@@ -38,9 +57,10 @@
|
||||
"jQuery": {
|
||||
"label": "jQuery",
|
||||
"policies": {
|
||||
"jQueryVersion": 1,
|
||||
"jQueryVersionsLoaded": 1,
|
||||
"jQueryVersion": 2,
|
||||
"jQueryVersionsLoaded": 2,
|
||||
"jQueryFunctionsUsed": 1,
|
||||
"jQueryCallsOnEmptyObject": 1,
|
||||
"jQueryNotDelegatedEvents": 1
|
||||
}
|
||||
},
|
||||
@@ -50,7 +70,10 @@
|
||||
"cssRules": 2,
|
||||
"cssComplexSelectors": 2,
|
||||
"cssComplexSelectorsByAttribute": 1.5,
|
||||
"cssColors": 0.5
|
||||
"cssColors": 0.5,
|
||||
"similarColors": 0.5,
|
||||
"cssBreakpoints": 1,
|
||||
"cssMobileFirst": 1
|
||||
}
|
||||
},
|
||||
"badCSS": {
|
||||
@@ -70,44 +93,21 @@
|
||||
"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,
|
||||
"assetsNotGzipped": 1.5,
|
||||
"http2": 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,
|
||||
@@ -115,8 +115,6 @@
|
||||
"cssSyntaxError": 1,
|
||||
"cssComplexity": 1,
|
||||
"badCSS": 1,
|
||||
"requests": 3,
|
||||
"smallRequests": 1,
|
||||
"network": 2
|
||||
"serverConfig": 1
|
||||
}
|
||||
}
|
||||
@@ -151,6 +151,9 @@ var OffendersHelpers = function() {
|
||||
};
|
||||
|
||||
this.cssOffenderPattern = function(offender) {
|
||||
// Remove any line breaks
|
||||
offender = offender.replace(/(\r\n|\n|\r)/gm, '');
|
||||
|
||||
var parts = /^(.*) (?:<([^ \(]*)>|\[inline CSS\]) @ (\d+):(\d+)$/.exec(offender);
|
||||
|
||||
if (!parts) {
|
||||
@@ -168,7 +171,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,10 @@ var debug = require('debug')('ylt:runner');
|
||||
|
||||
var phantomasWrapper = require('./tools/phantomas/phantomasWrapper');
|
||||
var jsExecutionTransformer = require('./tools/jsExecutionTransformer');
|
||||
var colorDiff = require('./tools/colorDiff');
|
||||
var mediaQueriesChecker = require('./tools/mediaQueriesChecker');
|
||||
var isHttp2 = require('./tools/isHttp2');
|
||||
var weightChecker = require('./tools/weightChecker/weightChecker');
|
||||
var rulesChecker = require('./rulesChecker');
|
||||
var scoreCalculator = require('./scoreCalculator');
|
||||
|
||||
@@ -19,14 +23,31 @@ 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
|
||||
// Compare colors
|
||||
data = colorDiff.compareAllColors(data);
|
||||
|
||||
// Check media queries
|
||||
data = mediaQueriesChecker.analyzeMediaQueries(data);
|
||||
|
||||
// Redownload every file
|
||||
return weightChecker.recheckAllFiles(data);
|
||||
|
||||
})
|
||||
|
||||
.then(function(data) {
|
||||
// Check if HTTP2
|
||||
return isHttp2.check(data);
|
||||
})
|
||||
|
||||
.then(function(data) {
|
||||
|
||||
// Rules checker
|
||||
var policies = require('./metadata/policies');
|
||||
@@ -45,14 +66,17 @@ var Runner = function(params) {
|
||||
delete data.toolsResults.phantomas.metrics.scrollExecutionTree;
|
||||
delete data.toolsResults.phantomas.offenders.scrollExecutionTree;
|
||||
|
||||
return data;
|
||||
|
||||
}).then(function(data) {
|
||||
if (data.toolsResults.phantomas.offenders.blockedRequests) {
|
||||
data.blockedRequests = data.toolsResults.phantomas.offenders.blockedRequests;
|
||||
}
|
||||
|
||||
|
||||
// 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();
|
||||
|
||||
@@ -6,6 +6,7 @@ var ScreenshotHandler = require('../../screenshotHandler');
|
||||
var RunsQueue = require('../datastores/runsQueue');
|
||||
var RunsDatastore = require('../datastores/runsDatastore');
|
||||
var ResultsDatastore = require('../datastores/resultsDatastore');
|
||||
var serverSettings = require('../../../server_config/settings.json');
|
||||
|
||||
|
||||
var ApiController = function(app) {
|
||||
@@ -33,12 +34,14 @@ 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,
|
||||
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
|
||||
authPass: req.body.authPass || null,
|
||||
blockDomain: req.body.blockDomain || null,
|
||||
allowDomain: req.body.allowDomain || null,
|
||||
noExternals: req.body.noExternals || false
|
||||
}
|
||||
};
|
||||
|
||||
@@ -52,7 +55,6 @@ var ApiController = function(app) {
|
||||
debug('Adding test %s to the queue', run.runId);
|
||||
var queuePromise = queue.push(run.runId);
|
||||
|
||||
|
||||
// Save the run to the datastore
|
||||
runsDatastore.add(run, queuePromise.startingPosition);
|
||||
|
||||
@@ -71,12 +73,15 @@ var ApiController = function(app) {
|
||||
|
||||
var runOptions = {
|
||||
screenshot: run.params.screenshot ? screenshot.getTmpFilePath() : false,
|
||||
jsDeepAnalysis: run.params.jsTimeline,
|
||||
device: run.params.device,
|
||||
waitForSelector: run.params.waitForSelector,
|
||||
cookie: run.params.cookie,
|
||||
authUser: run.params.authUser,
|
||||
authPass: run.params.authPass
|
||||
authPass: run.params.authPass,
|
||||
blockDomain: run.params.blockDomain,
|
||||
allowDomain: run.params.allowDomain,
|
||||
noExternals: run.params.noExternals,
|
||||
phantomasEngine: serverSettings.phantomasEngine
|
||||
};
|
||||
|
||||
return ylt(run.params.url, runOptions);
|
||||
@@ -95,7 +100,7 @@ var ApiController = function(app) {
|
||||
if (run.params.screenshot) {
|
||||
|
||||
// Replace the empty promise created earlier with Q.resolve()
|
||||
screenshotPromise = screenshot.toThumbnail(400)
|
||||
screenshotPromise = screenshot.toThumbnail(serverSettings.screenshotWidth || 400)
|
||||
|
||||
// Read screenshot
|
||||
.then(function(screenshotBuffer) {
|
||||
@@ -128,13 +133,7 @@ var ApiController = function(app) {
|
||||
// Remove uneeded temp screenshot path
|
||||
delete data.params.options.screenshot;
|
||||
|
||||
// Empty javascriptExecutionTree if not needed
|
||||
if (!run.params.jsTimeline) {
|
||||
data.javascriptExecutionTree = {};
|
||||
data.scrollExecutionTree = {};
|
||||
}
|
||||
|
||||
// Remove tools results if not needed
|
||||
// Here we can remove tools results if not needed
|
||||
|
||||
return resultsDatastore.saveResult(data);
|
||||
})
|
||||
|
||||
@@ -20,7 +20,7 @@ var FrontController = function(app) {
|
||||
app.use('/img', express.static(path.join(__dirname, assetsPath, 'img'), { maxAge: cacheDuration }));
|
||||
app.use('/js', express.static(path.join(__dirname, assetsPath, 'js'), { maxAge: cacheDuration }));
|
||||
app.use('/views', express.static(path.join(__dirname, assetsPath, 'views'), { maxAge: cacheDuration }));
|
||||
app.use('/bower_components', express.static(path.join(__dirname, '../../../bower_components'), { maxAge: cacheDuration }));
|
||||
app.use('/node_modules', express.static(path.join(__dirname, '../../../node_modules'), { maxAge: cacheDuration }));
|
||||
};
|
||||
|
||||
module.exports = FrontController;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,92 @@
|
||||
var debug = require('debug')('ylt:colorDiff');
|
||||
var diff = require('color-diff');
|
||||
var parse = require('parse-color');
|
||||
|
||||
|
||||
var colorDiff = function() {
|
||||
|
||||
var COLOR_DIFF_THRESHOLD = 0.9;
|
||||
|
||||
this.compareAllColors = function(data) {
|
||||
debug('Starting to compare all colors...');
|
||||
|
||||
var offenders = data.toolsResults.phantomas.offenders.cssColors;
|
||||
var colors = (offenders) ? this.parseAllColors(offenders) : [];
|
||||
var result = [];
|
||||
|
||||
// Compare colors to each other
|
||||
for (var i = 0; i < colors.length - 1 ; i++) {
|
||||
for (var j = i + 1; j < colors.length; j++) {
|
||||
debug('Comparing color %s with color %s', colors[i].original, colors[j].original);
|
||||
if (this.compareTwoColors(colors[i], colors[j])) {
|
||||
debug('Colors are similar!');
|
||||
result.push({
|
||||
color1: colors[i].original,
|
||||
color2: colors[j].original,
|
||||
isDark: (colors[i].Lab.L < 60)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data.toolsResults.colorDiff = {
|
||||
metrics: {
|
||||
similarColors: result.length
|
||||
},
|
||||
offenders: {
|
||||
similarColors: result
|
||||
}
|
||||
};
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
this.parseAllColors = function(offenders) {
|
||||
var parsedOffenders = offenders.map(this.parseOffender);
|
||||
var deduplicatedColors = {};
|
||||
|
||||
parsedOffenders.forEach(function(color) {
|
||||
if (color !== null) {
|
||||
deduplicatedColors[color] = color;
|
||||
}
|
||||
});
|
||||
|
||||
return Object.keys(deduplicatedColors).map(this.parseColor);
|
||||
};
|
||||
|
||||
this.parseOffender = function(offender) {
|
||||
var regexResult = /^(.*) \(\d+ times\)$/.exec(offender);
|
||||
return regexResult ? regexResult[1] : null;
|
||||
};
|
||||
|
||||
this.parseColor = function(color) {
|
||||
var colorArray = parse(color).rgba;
|
||||
|
||||
var obj = {
|
||||
R: colorArray[0],
|
||||
G: colorArray[1],
|
||||
B: colorArray[2],
|
||||
A: colorArray[3]
|
||||
};
|
||||
|
||||
obj.Lab = diff.rgb_to_lab(obj);
|
||||
obj.original = color;
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
||||
this.compareTwoColors = function(color1, color2) {
|
||||
if (color1.A !== color2.A) {
|
||||
debug('Comparison not possible, because the alpha channel is different');
|
||||
return false;
|
||||
}
|
||||
|
||||
var delta = diff.diff(color1.Lab, color2.Lab);
|
||||
debug('Delta is %d', delta);
|
||||
|
||||
return delta <= COLOR_DIFF_THRESHOLD;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
module.exports = new colorDiff();
|
||||
@@ -0,0 +1,105 @@
|
||||
var debug = require('debug')('ylt:isHttp2');
|
||||
var url = require('url');
|
||||
var Q = require('q');
|
||||
var http2 = require('is-http2');
|
||||
|
||||
var isHttp2 = function() {
|
||||
'use strict';
|
||||
|
||||
this.check = function(data) {
|
||||
debug('Starting to check for HTTP2 support...');
|
||||
|
||||
return this.checkHttp2(data)
|
||||
|
||||
.then(function(result) {
|
||||
|
||||
if (result.isHttp) {
|
||||
debug('The website is not even in HTTPS');
|
||||
|
||||
data.toolsResults.http2 = {
|
||||
metrics: {
|
||||
http2: false
|
||||
}
|
||||
};
|
||||
|
||||
} else if (result.isHttp2) {
|
||||
debug('HTTP/2 (or SPDY) is supported');
|
||||
|
||||
data.toolsResults.http2 = {
|
||||
metrics: {
|
||||
http2: true
|
||||
}
|
||||
};
|
||||
|
||||
} else {
|
||||
debug('HTTP/2 is not supported');
|
||||
|
||||
data.toolsResults.http2 = {
|
||||
metrics: {
|
||||
http2: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Add the supported protocols as offenders
|
||||
if (result.supportedProtocols) {
|
||||
debug('Supported protocols: ' + result.supportedProtocols.join(' '));
|
||||
data.toolsResults.http2.offenders = {
|
||||
http2: result.supportedProtocols
|
||||
};
|
||||
}
|
||||
|
||||
debug('End of HTTP2 support check');
|
||||
|
||||
return data;
|
||||
})
|
||||
|
||||
.fail(function() {
|
||||
return data;
|
||||
});
|
||||
};
|
||||
|
||||
this.getParsedUrl = function(data) {
|
||||
return url.parse(data.toolsResults.phantomas.url);
|
||||
};
|
||||
|
||||
this.getProtocol = function(data) {
|
||||
return this.getParsedUrl(data).protocol;
|
||||
};
|
||||
|
||||
this.getDomain = function(data) {
|
||||
return this.getParsedUrl(data).hostname;
|
||||
};
|
||||
|
||||
this.checkHttp2 = function(data) {
|
||||
var deferred = Q.defer();
|
||||
|
||||
// Check if it's HTTPS first
|
||||
if (this.getProtocol(data) === 'http:') {
|
||||
|
||||
deferred.resolve({
|
||||
isHttp: true
|
||||
});
|
||||
|
||||
} else {
|
||||
|
||||
// To make is-http2 work, you need to have openssl in a version greater than 1.0.0 installed and available in your $path.
|
||||
http2(this.getDomain(data), {includeSpdy: true})
|
||||
|
||||
.then(function(result) {
|
||||
deferred.resolve(result);
|
||||
})
|
||||
|
||||
.catch(function(error) {
|
||||
debug('Error while checking HTTP2 support:');
|
||||
debug(error);
|
||||
deferred.reject('Error while checking for HTTP2 support');
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = new isHttp2();
|
||||
@@ -7,18 +7,20 @@ var jsExecutionTransformer = function() {
|
||||
|
||||
this.transform = function(data) {
|
||||
var javascriptExecutionTree = {};
|
||||
var scrollExecutionTree = {};
|
||||
var jQueryFunctionsCollection = new Collection();
|
||||
|
||||
var metrics = {
|
||||
domInteractive: 0,
|
||||
domContentLoaded: 0,
|
||||
domContentLoadedEnd: 0,
|
||||
domComplete: 0,
|
||||
|
||||
DOMaccesses: 0,
|
||||
DOMaccessesOnScroll: 0,
|
||||
queriesWithoutResults: 0
|
||||
};
|
||||
|
||||
var offenders = {
|
||||
|
||||
};
|
||||
var offenders = {};
|
||||
|
||||
var hasjQuery = (data.toolsResults.phantomas.metrics.jQueryVersionsLoaded > 0);
|
||||
if (hasjQuery) {
|
||||
@@ -28,7 +30,6 @@ var jsExecutionTransformer = function() {
|
||||
metrics.jQueryNotDelegatedEvents = 0;
|
||||
|
||||
offenders.jQueryFunctionsUsed = [];
|
||||
offenders.jQueryNotDelegatedEvents = [];
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -37,28 +38,25 @@ var jsExecutionTransformer = function() {
|
||||
javascriptExecutionTree = JSON.parse(data.toolsResults.phantomas.offenders.javascriptExecutionTree[0]);
|
||||
|
||||
if (javascriptExecutionTree.children) {
|
||||
javascriptExecutionTree.children.forEach(function(node) {
|
||||
javascriptExecutionTree.children.forEach(function(node, index) {
|
||||
|
||||
var contextLength = (node.data.callDetails && node.data.callDetails.context) ? node.data.callDetails.context.length : null;
|
||||
|
||||
if (isABindWithoutEventDelegation(node, contextLength)) {
|
||||
metrics.jQueryNotDelegatedEvents += contextLength;
|
||||
offenders.jQueryNotDelegatedEvents.push({
|
||||
functionName: node.data.type.substring(9),
|
||||
contextLength: contextLength,
|
||||
backtrace: offendersHelpers.backtraceToArray(node.data.backtrace)
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -75,22 +73,26 @@ var jsExecutionTransformer = function() {
|
||||
// Mark a performance flag
|
||||
if (['domInteractive', 'domContentLoaded', 'domContentLoadedEnd', 'domComplete'].indexOf(node.data.type) >= 0) {
|
||||
node.windowPerformance = true;
|
||||
}
|
||||
|
||||
// Read the execution tree and adjust the navigation timings (cause their not very well synchronised)
|
||||
switch(node.data.type) {
|
||||
case 'domInteractive':
|
||||
data.toolsResults.phantomas.metrics.domInteractive = node.data.timestamp;
|
||||
break;
|
||||
case 'domContentLoaded':
|
||||
data.toolsResults.phantomas.metrics.domContentLoaded = node.data.timestamp;
|
||||
break;
|
||||
case 'domContentLoadedEnd':
|
||||
data.toolsResults.phantomas.metrics.domContentLoadedEnd = node.data.timestamp;
|
||||
break;
|
||||
case 'domComplete':
|
||||
data.toolsResults.phantomas.metrics.domComplete = node.data.timestamp;
|
||||
break;
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Transform domPaths into objects
|
||||
@@ -119,24 +121,28 @@ var jsExecutionTransformer = function() {
|
||||
debug('JS execution transformation complete');
|
||||
|
||||
|
||||
debug('Starting scroll execution transformation');
|
||||
offenders.scrollExecutionTree = JSON.parse(data.toolsResults.phantomas.offenders.scrollExecutionTree[0]);
|
||||
if (offenders.scrollExecutionTree.children) {
|
||||
offenders.scrollExecutionTree.children.forEach(function(node) {
|
||||
|
||||
// Mark a event flag
|
||||
if (['documentScroll', 'windowScroll', 'window.onscroll'].indexOf(node.data.type) >= 0) {
|
||||
node.windowPerformance = true;
|
||||
}
|
||||
if (data.toolsResults.phantomas.offenders.scrollExecutionTree) {
|
||||
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);
|
||||
});
|
||||
// Transform domPaths into objects
|
||||
changeListOfDomPaths(node);
|
||||
|
||||
// Count the number of DOM accesses, by counting the tree leafs
|
||||
metrics.DOMaccessesOnScroll += countTreeLeafs(node);
|
||||
});
|
||||
}
|
||||
debug('Scroll execution transformation complete');
|
||||
} else {
|
||||
debug('Could not parse scrollExecutionTree');
|
||||
}
|
||||
debug('Scroll execution transformation complete');
|
||||
|
||||
} catch(err) {
|
||||
throw err;
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
var debug = require('debug')('ylt:mediaQueriesChecker');
|
||||
var parseMediaQuery = require('css-mq-parser');
|
||||
var offendersHelpers = require('../offendersHelpers');
|
||||
|
||||
|
||||
var mediaQueriesChecker = function() {
|
||||
'use strict';
|
||||
|
||||
var MOBILE_MIN_BREAKPOINT = 200;
|
||||
var MOBILE_MAX_BREAKPOINT = 300;
|
||||
|
||||
this.analyzeMediaQueries = function(data) {
|
||||
debug('Starting to check all media queries...');
|
||||
|
||||
var offenders = data.toolsResults.phantomas.offenders.cssMediaQueries;
|
||||
var mediaQueries = (offenders) ? this.parseAllMediaQueries(offenders) : [];
|
||||
|
||||
var notMobileFirstCount = 0;
|
||||
var notMobileFirstOffenders = [];
|
||||
|
||||
var breakpointsOffenders = {};
|
||||
|
||||
for (var i = 0; i < mediaQueries.length; i++) {
|
||||
var item = mediaQueries[i];
|
||||
|
||||
if (!item) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.isForMobile) {
|
||||
notMobileFirstCount += item.mediaQuery.rules;
|
||||
notMobileFirstOffenders.push(item.mediaQuery);
|
||||
}
|
||||
|
||||
for (var j = 0; j < item.breakpoints.length; j++) {
|
||||
var breakpointString = item.breakpoints[j].string;
|
||||
if (!breakpointsOffenders[breakpointString]) {
|
||||
breakpointsOffenders[breakpointString] = {
|
||||
count: 1,
|
||||
pixels: item.breakpoints[j].pixels
|
||||
};
|
||||
} else {
|
||||
breakpointsOffenders[breakpointString].count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data.toolsResults.mediaQueriesChecker = {
|
||||
metrics: {
|
||||
cssMobileFirst: notMobileFirstCount,
|
||||
cssBreakpoints: Object.keys(breakpointsOffenders).length
|
||||
},
|
||||
offenders: {
|
||||
cssMobileFirst: notMobileFirstOffenders,
|
||||
cssBreakpoints: breakpointsOffenders
|
||||
}
|
||||
};
|
||||
|
||||
debug('End of media queries check');
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
this.parseAllMediaQueries = function(offenders) {
|
||||
return offenders.map(this.parseOneMediaQuery);
|
||||
};
|
||||
|
||||
this.parseOneMediaQuery = function(offender) {
|
||||
var splittedOffender = offendersHelpers.cssOffenderPattern(offender);
|
||||
var parts = /^@media (.*) \((\d+ rules)\)$/.exec(splittedOffender.css);
|
||||
|
||||
if (!parts) {
|
||||
debug('Failed to parse media query ' + offender);
|
||||
return false;
|
||||
}
|
||||
|
||||
var rulesCount = parseInt(parts[2], 10);
|
||||
var query = parts[1];
|
||||
|
||||
var isForMobile = false;
|
||||
var breakpoints = [];
|
||||
|
||||
try {
|
||||
|
||||
var ast = parseMediaQuery(query);
|
||||
|
||||
var min = 0;
|
||||
var max = Infinity;
|
||||
var pixels;
|
||||
|
||||
ast.forEach(function(astItem) {
|
||||
astItem.expressions.forEach(function(expression) {
|
||||
if (expression.feature === 'width' || expression.feature === 'device-width') {
|
||||
if (astItem.inverse === false) {
|
||||
if (expression.modifier === 'max') {
|
||||
pixels = toPixels(expression.value);
|
||||
max = Math.min(max, pixels);
|
||||
breakpoints.push({
|
||||
string: expression.value,
|
||||
pixels: pixels
|
||||
});
|
||||
} else if (expression.modifier === 'min') {
|
||||
pixels = toPixels(expression.value);
|
||||
min = Math.max(min, pixels);
|
||||
breakpoints.push({
|
||||
string: expression.value,
|
||||
pixels: pixels
|
||||
});
|
||||
}
|
||||
} else if (astItem.inverse === true) {
|
||||
if (expression.modifier === 'max') {
|
||||
pixels = toPixels(expression.value);
|
||||
min = Math.max(min, pixels);
|
||||
breakpoints.push({
|
||||
string: expression.value,
|
||||
pixels: pixels
|
||||
});
|
||||
} else if (expression.modifier === 'min') {
|
||||
pixels = toPixels(expression.value);
|
||||
max = Math.min(max, pixels);
|
||||
breakpoints.push({
|
||||
string: expression.value,
|
||||
pixels: pixels
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
isForMobile = (min <= MOBILE_MIN_BREAKPOINT && max >= MOBILE_MAX_BREAKPOINT && max !== Infinity);
|
||||
|
||||
} catch(error) {
|
||||
debug('Failed to parse media query ' + offender);
|
||||
}
|
||||
|
||||
return {
|
||||
mediaQuery: {
|
||||
query: query,
|
||||
rules: rulesCount,
|
||||
file: splittedOffender.file,
|
||||
line: splittedOffender.line,
|
||||
column: splittedOffender.column
|
||||
},
|
||||
isForMobile: isForMobile,
|
||||
breakpoints: breakpoints
|
||||
};
|
||||
};
|
||||
|
||||
// Parses a size in em, pt (or px) and returns it in px
|
||||
function toPixels(size) {
|
||||
var splittedSize = /^([\d\.]+)(.*)/.exec(size);
|
||||
var value = parseFloat(splittedSize[1]);
|
||||
var unit = splittedSize[2];
|
||||
|
||||
if (unit === 'em') {
|
||||
return value * 16;
|
||||
}
|
||||
|
||||
if (unit === 'pt') {
|
||||
return value / 12 * 16;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = new mediaQueriesChecker();
|
||||
@@ -18,8 +18,8 @@ exports.module = function(phantomas) {
|
||||
responseEndTime = Date.now();
|
||||
});
|
||||
|
||||
phantomas.once('init', function() {
|
||||
phantomas.evaluate(function(responseEndTime, deepAnalysis) {
|
||||
phantomas.on('init', function() {
|
||||
phantomas.evaluate(function(responseEndTime) {
|
||||
(function(phantomas) {
|
||||
|
||||
// Overwritting phantomas spy function
|
||||
@@ -106,48 +106,85 @@ exports.module = function(phantomas) {
|
||||
var currentContext = root;
|
||||
var depth = 0;
|
||||
|
||||
if (deepAnalysis) {
|
||||
phantomas.log('Entering deep Javascript analysis mode');
|
||||
}
|
||||
|
||||
// Add a child but don't enter his context
|
||||
// Add a child but don't enter its context
|
||||
function pushContext(data) {
|
||||
if (depth === 0 || deepAnalysis) {
|
||||
|
||||
// Some data is not needed on subchildren
|
||||
if (depth === 0) {
|
||||
data.timestamp = Date.now() - responseEndTime;
|
||||
data.loadingStep = phantomas.currentStep || '';
|
||||
currentContext.addChild(data);
|
||||
}
|
||||
|
||||
// Some data is not needed on subchildren
|
||||
if (depth > 0) {
|
||||
if (data.backtrace) {
|
||||
delete data.backtrace;
|
||||
}
|
||||
if (data.resultsNumber) {
|
||||
delete data.resultsNumber;
|
||||
}
|
||||
}
|
||||
|
||||
currentContext.addChild(data);
|
||||
}
|
||||
|
||||
// Add a child to the current context and enter his context
|
||||
// Add a child to the current context and enter its context
|
||||
function enterContext(data) {
|
||||
if (depth === 0 || deepAnalysis) {
|
||||
|
||||
// Some data is not needed on subchildren
|
||||
if (depth === 0) {
|
||||
data.timestamp = Date.now() - responseEndTime;
|
||||
data.loadingStep = phantomas.currentStep || '';
|
||||
currentContext = currentContext.addChild(data);
|
||||
}
|
||||
|
||||
// Some data is not needed on subchildren
|
||||
if (depth > 0) {
|
||||
if (data.backtrace) {
|
||||
delete data.backtrace;
|
||||
}
|
||||
if (data.resultsNumber) {
|
||||
delete data.resultsNumber;
|
||||
}
|
||||
}
|
||||
|
||||
currentContext = currentContext.addChild(data);
|
||||
|
||||
depth ++;
|
||||
}
|
||||
|
||||
// Save given data in the current context and jump change current context to its parent
|
||||
function leaveContext(moreData) {
|
||||
if (depth === 1 || deepAnalysis) {
|
||||
|
||||
// Some data is not needed on subchildren
|
||||
if (depth === 1) {
|
||||
currentContext.data.time = Date.now() - currentContext.data.timestamp - responseEndTime;
|
||||
}
|
||||
|
||||
// Merge previous data with moreData (ovewrites if exists)
|
||||
if (moreData) {
|
||||
for (var key in moreData) {
|
||||
currentContext.data[key] = moreData[key];
|
||||
}
|
||||
// Some data is not needed on subchildren
|
||||
if (depth > 1) {
|
||||
if (moreData && moreData.backtrace) {
|
||||
delete moreData.backtrace;
|
||||
}
|
||||
|
||||
var parent = currentContext.parent;
|
||||
if (parent === null) {
|
||||
console.error('Error: trying to close root context in ContextTree');
|
||||
} else {
|
||||
currentContext = parent;
|
||||
if (moreData && moreData.resultsNumber) {
|
||||
delete moreData.resultsNumber;
|
||||
}
|
||||
}
|
||||
|
||||
// Merge previous data with moreData (ovewrites if exists)
|
||||
if (moreData) {
|
||||
for (var key in moreData) {
|
||||
currentContext.data[key] = moreData[key];
|
||||
}
|
||||
}
|
||||
|
||||
var parent = currentContext.parent;
|
||||
if (parent === null) {
|
||||
console.error('Error: trying to close root context in ContextTree');
|
||||
} else {
|
||||
currentContext = parent;
|
||||
}
|
||||
|
||||
depth --;
|
||||
}
|
||||
|
||||
@@ -161,7 +198,6 @@ exports.module = function(phantomas) {
|
||||
if (root !== currentContext) {
|
||||
return null;
|
||||
}
|
||||
var current = currentContext;
|
||||
|
||||
function recusiveRead(node) {
|
||||
if (node.children.length === 0) {
|
||||
@@ -209,6 +245,6 @@ exports.module = function(phantomas) {
|
||||
})();
|
||||
|
||||
})(window.__phantomas);
|
||||
}, responseEndTime, phantomas.getParam('js-deep-analysis'));
|
||||
}, responseEndTime);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3,21 +3,23 @@
|
||||
*/
|
||||
/* global document: true, Node: true, window: true */
|
||||
|
||||
exports.version = '0.1.a';
|
||||
exports.version = '1.0.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
|
||||
phantomas.setMetric('hiddenImages'); // @desc number of hidden images that can be lazy-loaded @offenders
|
||||
|
||||
// HTML size
|
||||
phantomas.on('report', function() {
|
||||
phantomas.evaluate(function() {
|
||||
(function(phantomas) {
|
||||
phantomas.spyEnabled(false, 'checking the hiddenContentSize');
|
||||
var runner = new phantomas.nodeRunner(),
|
||||
lazyLoadableImages = {};
|
||||
|
||||
var runner = new phantomas.nodeRunner();
|
||||
phantomas.spyEnabled(false, 'analyzing hidden content');
|
||||
|
||||
runner.walk(document.body, function(node, depth) {
|
||||
switch (node.nodeType) {
|
||||
@@ -36,6 +38,29 @@ exports.module = function(phantomas) {
|
||||
}
|
||||
}
|
||||
|
||||
// count hidden images that can be lazy loaded (issue #524)
|
||||
var images = [];
|
||||
if (node.tagName === 'IMG') {
|
||||
images = [node];
|
||||
} else if (typeof node.querySelectorAll === 'function') {
|
||||
images = node.querySelectorAll('img') || [];
|
||||
}
|
||||
|
||||
for (var i = 0, len = images.length; i < len; i++) {
|
||||
var src = images[i].src,
|
||||
path;
|
||||
|
||||
if (src === '' || src.indexOf('data:image') === 0) continue;
|
||||
|
||||
if (!lazyLoadableImages[src]) {
|
||||
path = phantomas.getDOMPath(images[i]);
|
||||
|
||||
lazyLoadableImages[src] = {
|
||||
path: path
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// don't run for child nodes as they're hidden as well
|
||||
return false;
|
||||
}
|
||||
@@ -43,6 +68,15 @@ exports.module = function(phantomas) {
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(lazyLoadableImages).forEach(function(img) {
|
||||
var entry = lazyLoadableImages[img];
|
||||
|
||||
phantomas.incrMetric('hiddenImages');
|
||||
phantomas.addOffender('hiddenImages', img);
|
||||
|
||||
phantomas.log('hiddenImages: <%s> image (%s) is hidden and can be lazy-loaded', img, entry.path);
|
||||
});
|
||||
|
||||
phantomas.spyEnabled(true);
|
||||
}(window.__phantomas));
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ exports.module = function(phantomas) {
|
||||
phantomas.setMetric('DOMqueriesAvoidable'); // @desc number of repeated uses of a duplicated query
|
||||
|
||||
// fake native DOM functions
|
||||
phantomas.once('init', function() {
|
||||
phantomas.on('init', function() {
|
||||
phantomas.evaluate(function() {
|
||||
(function(phantomas) {
|
||||
function querySpy(type, query, fnName, context, hasNoResults) {
|
||||
@@ -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 + ')');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ exports.module = function(phantomas) {
|
||||
phantomas.setMetric('eventsDispatched'); // @desc number of EventTarget.dispatchEvent calls
|
||||
phantomas.setMetric('eventsScrollBound'); // @desc number of scroll event bounds
|
||||
|
||||
phantomas.once('init', function() {
|
||||
phantomas.on('init', function() {
|
||||
phantomas.evaluate(function() {
|
||||
(function(phantomas) {
|
||||
// spy calls to EventTarget.addEventListener
|
||||
|
||||
@@ -138,10 +138,9 @@ exports.module = function(phantomas) {
|
||||
jQueryFunctions = jQueryFunctions.concat(jQueryTraversalFunctions);
|
||||
|
||||
// spy calls to jQuery functions
|
||||
phantomas.once('init', function() {
|
||||
phantomas.on('init', function() {
|
||||
phantomas.evaluate(function(jQueryFunctions, jQueryTraversalFunctions) {
|
||||
(function(phantomas) {
|
||||
var jQuery;
|
||||
var oldJQuery;
|
||||
|
||||
phantomas.spyGlobalVar('jQuery', function(jQuery) {
|
||||
@@ -371,14 +370,6 @@ exports.module = function(phantomas) {
|
||||
});
|
||||
|
||||
|
||||
// store the last resource that was received
|
||||
// try to report where given jQuery version was loaded from
|
||||
phantomas.on('recv', function(entry) {
|
||||
if (entry.isJS) {
|
||||
lastUrl = entry.url;
|
||||
}
|
||||
});
|
||||
|
||||
phantomas.on('jQueryLoaded', function(version) {
|
||||
phantomas.log('jQuery: loaded v' + version);
|
||||
phantomas.setMetric('jQueryVersion', version);
|
||||
@@ -386,7 +377,5 @@ exports.module = function(phantomas) {
|
||||
// report multiple jQuery "instances" (issue #435)
|
||||
phantomas.incrMetric('jQueryVersionsLoaded');
|
||||
phantomas.addOffender('jQueryVersionsLoaded', 'v%s', version);
|
||||
|
||||
phantomas.log('jQuery: v%s (probably loaded from <%s>)', version, lastUrl);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -23,7 +23,7 @@ exports.module = function(phantomas) {
|
||||
phantomas.log('javaScriptBottlenecks: to spy calls to eval() run phantomas with --spy-eval option');
|
||||
}
|
||||
|
||||
phantomas.once('init', function() {
|
||||
phantomas.on('init', function() {
|
||||
phantomas.evaluate(function(spyEval) {
|
||||
(function(phantomas) {
|
||||
function report(msg, caller, backtrace, metric) {
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Analyzes images and detects which one can be lazy-loaded (are below the fold)
|
||||
*
|
||||
* @see https://github.com/macbre/phantomas/issues/494
|
||||
*/
|
||||
/* global document: true, window: true */
|
||||
|
||||
exports.version = '1.0.a';
|
||||
|
||||
exports.module = function(phantomas) {
|
||||
'use strict';
|
||||
|
||||
phantomas.setMetric('lazyLoadableImagesBelowTheFold'); // @desc number of images displayed below the fold that can be lazy-loaded
|
||||
|
||||
phantomas.on('report', function() {
|
||||
phantomas.log('lazyLoadableImages: analyzing which images can be lazy-loaded...');
|
||||
|
||||
phantomas.evaluate(function() {
|
||||
(function(phantomas) {
|
||||
phantomas.spyEnabled(false, 'analyzing which images can be lazy-loaded');
|
||||
|
||||
var images = document.body.getElementsByTagName('img'),
|
||||
i,
|
||||
len = images.length,
|
||||
offset,
|
||||
path,
|
||||
processedImages = {},
|
||||
src,
|
||||
viewportHeight = window.innerHeight;
|
||||
|
||||
phantomas.log('lazyLoadableImages: %d image(s) found, assuming %dpx offset to be the fold', len, viewportHeight);
|
||||
|
||||
for (i = 0; i < len; i++) {
|
||||
// @see https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect
|
||||
offset = images[i].getBoundingClientRect().top;
|
||||
src = images[i].src;
|
||||
|
||||
// ignore base64-encoded images
|
||||
if (src === '' || /^data:/.test(src)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
path = phantomas.getDOMPath(images[i]);
|
||||
|
||||
// get the most top position for a given image (deduplicate by src)
|
||||
if (typeof processedImages[src] === 'undefined') {
|
||||
processedImages[src] = {
|
||||
offset: offset,
|
||||
path: path
|
||||
};
|
||||
}
|
||||
|
||||
// maybe there's the same image loaded above the fold?
|
||||
if (offset < processedImages[src].offset) {
|
||||
processedImages[src] = {
|
||||
offset: offset,
|
||||
path: path
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
phantomas.log('lazyLoadableImages: checking %d unique image(s)', Object.keys(processedImages).length);
|
||||
|
||||
Object.keys(processedImages).forEach(function(src) {
|
||||
var img = processedImages[src];
|
||||
|
||||
if (img.offset > viewportHeight) {
|
||||
phantomas.log('lazyLoadableImages: <%s> image (%s) is below the fold (at %dpx)', src, img.path, img.offset);
|
||||
|
||||
phantomas.incrMetric('lazyLoadableImagesBelowTheFold');
|
||||
phantomas.addOffender('lazyLoadableImagesBelowTheFold', src);
|
||||
}
|
||||
});
|
||||
|
||||
phantomas.spyEnabled(true);
|
||||
})(window.__phantomas);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -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));
|
||||
});
|
||||
};
|
||||
@@ -17,12 +17,12 @@ var PhantomasWrapper = function() {
|
||||
var deferred = Q.defer();
|
||||
var task = data.params;
|
||||
|
||||
|
||||
var options = {
|
||||
|
||||
// Cusomizable options
|
||||
'timeout': task.options.timeout || 45,
|
||||
'js-deep-analysis': task.options.jsDeepAnalysis || false,
|
||||
'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,
|
||||
'engine': task.options.phantomasEngine || 'webkit',
|
||||
'timeout': task.options.timeout || 30,
|
||||
'user-agent': (task.options.device === 'desktop') ? 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) YLT Chrome/27.0.1453.110 Safari/537.36' : null,
|
||||
'tablet': (task.options.device === 'tablet'),
|
||||
'phone': (task.options.device === 'phone'),
|
||||
'screenshot': task.options.screenshot || false,
|
||||
@@ -30,12 +30,14 @@ var PhantomasWrapper = function() {
|
||||
'cookie': task.options.cookie,
|
||||
'auth-user': task.options.authUser,
|
||||
'auth-pass': task.options.authPass,
|
||||
'block-domain': task.options.blockDomain,
|
||||
'allow-domain': task.options.allowDomain,
|
||||
'no-externals': task.options.noExternals,
|
||||
|
||||
// Mandatory
|
||||
'reporter': 'json:pretty',
|
||||
'analyze-css': true,
|
||||
'skip-modules': [
|
||||
'blockDomains', // not needed
|
||||
'domHiddenContent', // overriden
|
||||
'domMutations', // not compatible with webkit
|
||||
'domQueries', // overriden
|
||||
@@ -45,6 +47,7 @@ var PhantomasWrapper = function() {
|
||||
'javaScriptBottlenecks', // needs to be launched after custom module scopeYLT
|
||||
'jQuery', // overridden
|
||||
'jserrors', // overridden
|
||||
'lazyLoadableImages', //overriden
|
||||
'pageSource', // not needed
|
||||
'windowPerformance' // overriden
|
||||
].join(','),
|
||||
@@ -66,7 +69,7 @@ var PhantomasWrapper = function() {
|
||||
|
||||
if (value === true) {
|
||||
optionsString += ' ' + '--' + opt;
|
||||
} else if (value === false || value === null) {
|
||||
} else if (value === false || value === null || value === undefined) {
|
||||
// Nothing
|
||||
} else {
|
||||
optionsString += ' ' + '--' + opt + '=' + value;
|
||||
@@ -75,56 +78,84 @@ var PhantomasWrapper = function() {
|
||||
}
|
||||
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;
|
||||
var currentTry = 0;
|
||||
|
||||
async.retry(triesNumber, function(cb) {
|
||||
|
||||
currentTry ++;
|
||||
|
||||
var process = phantomas(task.url, options, function(err, json, results) {
|
||||
debug('Returning from Phantomas');
|
||||
var errorCode = err ? parseInt(err.message, 10) : null;
|
||||
|
||||
if (isKilled) {
|
||||
debug('Process was killed, too late Phantomas, sorry...');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
debug('Returning from Phantomas with error %s', errorCode);
|
||||
|
||||
// Adding some YellowLabTools errors here
|
||||
if (json && json.metrics && (!json.metrics.javascriptExecutionTree || !json.offenders.javascriptExecutionTree)) {
|
||||
err = 1001;
|
||||
errorCode = 1001;
|
||||
}
|
||||
|
||||
if (!err && (!json || !json.metrics)) {
|
||||
err = 1002;
|
||||
if (!errorCode && (!json || !json.metrics)) {
|
||||
errorCode = 1002;
|
||||
}
|
||||
|
||||
// Don't cancel test if it is a timeout and we've got some results
|
||||
if (err === 252 && json) {
|
||||
if (errorCode === 252 && json) {
|
||||
debug('Timeout after ' + options.timeout + ' seconds. But it\'s not a problem, the test is valid.');
|
||||
err = null;
|
||||
errorCode = null;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
debug('Attempt failed. Error code ' + err);
|
||||
if (errorCode) {
|
||||
debug('Attempt failed. Error code ' + errorCode);
|
||||
}
|
||||
|
||||
cb(err, json);
|
||||
cb(errorCode, json);
|
||||
|
||||
}).fail(function() {
|
||||
// This function is useless, but the failing promise needs to be handled,
|
||||
// otherwise the module meow writes in the console in case of a timeout (error code 252).
|
||||
debug('Failing promise handled');
|
||||
});
|
||||
|
||||
phantomasPid = process.pid;
|
||||
|
||||
@@ -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 minifiable 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,102 @@
|
||||
var debug = require('debug')('ylt:gzipCompressor');
|
||||
|
||||
var Q = require('q');
|
||||
var zlib = require('zlib');
|
||||
|
||||
var GzipCompressor = function() {
|
||||
|
||||
function compressFile(entry) {
|
||||
debug('Entering compressFile');
|
||||
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 {
|
||||
debug('Compression not needed');
|
||||
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 {
|
||||
debug('Compressing optimized file not needed');
|
||||
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({ plugins: [ { removeUselessDefs: false } ] });
|
||||
} 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,536 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
var httpAuth = null;
|
||||
if (data.params && data.params.options && data.params.options.authUser && data.params.options.authPass) {
|
||||
httpAuth = {
|
||||
username: data.params.options.authUser,
|
||||
password: data.params.options.authPass
|
||||
};
|
||||
}
|
||||
|
||||
// Transform every request into a download function with a callback when done
|
||||
var redownloadList = requestsList.map(function(entry) {
|
||||
return function(callback) {
|
||||
|
||||
redownloadEntry(entry, httpAuth)
|
||||
|
||||
.then(imageOptimizer.optimizeImage)
|
||||
|
||||
.then(fileMinifier.minifyFile)
|
||||
|
||||
.then(gzipCompressor.compressFile)
|
||||
|
||||
.then(function(newEntry) {
|
||||
debug('File %s - Redownloaded, optimized, minified, compressed: done', entry.url);
|
||||
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';
|
||||
type = (results.byType[type]) ? 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';
|
||||
type = (results.byType[type]) ? 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, httpAuth) {
|
||||
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
|
||||
};
|
||||
|
||||
// Basic auth
|
||||
if (httpAuth) {
|
||||
requestOptions.auth = {
|
||||
user: httpAuth.username,
|
||||
pass: httpAuth.password,
|
||||
sendImmediately: false // Tries a first time without auth, wait for a 401 error before resending
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
try {
|
||||
|
||||
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('Error while decoding %s', requestOptions.url);
|
||||
debug(err);
|
||||
callback(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('Error while decoding %s', requestOptions.url);
|
||||
debug(err);
|
||||
callback(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) {
|
||||
debug('Error while downloading %s', requestOptions.url);
|
||||
debug(err);
|
||||
callback(err);
|
||||
});
|
||||
|
||||
} catch(err) {
|
||||
debug('Error while downloading %s', requestOptions.url);
|
||||
debug(err);
|
||||
callback(err);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
recheckAllFiles: recheckAllFiles,
|
||||
listRequestWeight: listRequestWeight,
|
||||
redownloadEntry: redownloadEntry,
|
||||
download: download
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = new WeightChecker();
|
||||
@@ -1,7 +1,13 @@
|
||||
{
|
||||
"name": "yellowlabtools",
|
||||
"version": "1.5.0",
|
||||
"version": "1.10.0",
|
||||
"description": "Online tool to audit a webpage for performance and front-end quality issues",
|
||||
"license": "GPL-2.0",
|
||||
"author": {
|
||||
"name": "Gaël Métais",
|
||||
"email": "gael@gaelmetais.com",
|
||||
"url": "http://www.gaelmetais.com"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/gmetais/YellowLabTools.git"
|
||||
@@ -9,50 +15,80 @@
|
||||
"bin": {
|
||||
"yellowlabtools": "./bin/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.12.0"
|
||||
},
|
||||
"main": "./lib/index.js",
|
||||
"dependencies": {
|
||||
"async": "~0.9.0",
|
||||
"body-parser": "~1.12.4",
|
||||
"compression": "~1.4.3",
|
||||
"cors": "^2.6.0",
|
||||
"debug": "~2.2.0",
|
||||
"express": "~4.12.3",
|
||||
"lwip": "0.0.6",
|
||||
"meow": "^3.1.0",
|
||||
"phantomas": "1.10.2",
|
||||
"ps-node": "0.0.4",
|
||||
"q": "~1.4.0",
|
||||
"rimraf": "~2.3.3",
|
||||
"temporary": "0.0.8"
|
||||
"angular": "1.5.2",
|
||||
"angular-animate": "1.5.2",
|
||||
"angular-chart.js": "0.9.0",
|
||||
"angular-local-storage": "0.2.7",
|
||||
"angular-resource": "1.5.2",
|
||||
"angular-route": "1.5.2",
|
||||
"angular-sanitize": "1.5.2",
|
||||
"async": "1.5.2",
|
||||
"body-parser": "1.15.0",
|
||||
"chart.js": "1.0.2",
|
||||
"clean-css": "3.4.10",
|
||||
"color-diff": "1.0.0",
|
||||
"compression": "1.6.1",
|
||||
"cors": "2.7.1",
|
||||
"css-mq-parser": "0.0.3",
|
||||
"debug": "2.2.0",
|
||||
"easyxml": "2.0.1",
|
||||
"express": "4.13.4",
|
||||
"imagemin": "4.0.0",
|
||||
"imagemin-jpegoptim": "4.1.0",
|
||||
"is-http2": "1.0.4",
|
||||
"lwip": "0.0.8",
|
||||
"meow": "3.7.0",
|
||||
"minimize": "1.8.1",
|
||||
"parse-color": "1.0.0",
|
||||
"phantomas": "1.15.1",
|
||||
"ps-node": "0.0.5",
|
||||
"q": "1.4.1",
|
||||
"request": "2.69.0",
|
||||
"rimraf": "2.5.2",
|
||||
"temporary": "0.0.8",
|
||||
"try-thread-sleep": "1.0.0",
|
||||
"uglify-js": "2.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^2.3.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.3",
|
||||
"grunt-contrib-htmlmin": "^0.4.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.3.1",
|
||||
"grunt-inline-angular-templates": "^0.1.5",
|
||||
"grunt-line-remover": "^0.0.2",
|
||||
"grunt-mocha-test": "^0.12.7",
|
||||
"grunt-replace": "^0.9.2",
|
||||
"grunt-usemin": "^3.0.0",
|
||||
"grunt-webfont": "^0.5.3",
|
||||
"matchdep": "^0.3.0",
|
||||
"mocha": "^2.2.4",
|
||||
"request": "^2.55.0",
|
||||
"sinon": "^1.14.1",
|
||||
"sinon-chai": "^2.7.0"
|
||||
"chai": "~3.5.0",
|
||||
"grunt": "~0.4.5",
|
||||
"grunt-blanket": "~0.0.10",
|
||||
"grunt-contrib-clean": "~1.0.0",
|
||||
"grunt-contrib-concat": "~1.0.0",
|
||||
"grunt-contrib-copy": "~1.0.0",
|
||||
"grunt-contrib-cssmin": "~1.0.1",
|
||||
"grunt-contrib-htmlmin": "~1.1.0",
|
||||
"grunt-contrib-jshint": "~1.0.0",
|
||||
"grunt-contrib-less": "~1.2.0",
|
||||
"grunt-contrib-uglify": "~1.0.1",
|
||||
"grunt-env": "~0.4.4",
|
||||
"grunt-express": "~1.4.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.11.0",
|
||||
"grunt-usemin": "~3.1.1",
|
||||
"grunt-webfont": "~1.2.0",
|
||||
"matchdep": "~1.0.1",
|
||||
"mocha": "~2.4.5",
|
||||
"sinon": "~1.17.3",
|
||||
"sinon-chai": "~2.8.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "grunt test"
|
||||
}
|
||||
},
|
||||
"keywords": [
|
||||
"performance",
|
||||
"metrics",
|
||||
"webperf",
|
||||
"pagespeed",
|
||||
"budget",
|
||||
"phantomas"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
var express = require('express');
|
||||
var app = express();
|
||||
var server = require('http').createServer(app);
|
||||
|
||||
var settings = require('./settings.json');
|
||||
|
||||
app.all('*', function(req, res) {
|
||||
res.status(500).send('YellowLabTools is in maintenance. It should come back soon with a new version!');
|
||||
});
|
||||
|
||||
server.listen(settings.serverPort, function() {
|
||||
console.log('Maintenance mode started on port %d', server.address().port);
|
||||
});
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
# APT-GET
|
||||
sudo apt-get update
|
||||
sudo apt-get install lsb-release libfontconfig1 libfreetype6 -y --force-yes
|
||||
sudo apt-get install lsb-release libfontconfig1 libfreetype6 libjpeg-dev -y --force-yes
|
||||
sudo apt-get install curl git python-software-properties build-essential make g++ -y --force-yes
|
||||
|
||||
# Installation of NodeJS
|
||||
@@ -12,7 +12,7 @@ sudo apt-get install -y nodejs
|
||||
source ~/.profile
|
||||
|
||||
# Installation of some packages globally
|
||||
npm install bower forever grunt-cli -g
|
||||
npm install forever grunt-cli -g
|
||||
source ~/.profile
|
||||
|
||||
# Installation of YellowLabTools
|
||||
@@ -21,8 +21,7 @@ sudo chown $USER /space
|
||||
cd /space
|
||||
git clone https://github.com/gmetais/YellowLabTools.git --branch master
|
||||
cd YellowLabTools
|
||||
npm install
|
||||
bower install --config.interactive=false --allow-root
|
||||
npm install || exit 1
|
||||
|
||||
# Front-end compilation
|
||||
grunt build
|
||||
|
||||
@@ -2,23 +2,23 @@
|
||||
|
||||
cd /space/YellowLabTools
|
||||
|
||||
# Stop the server
|
||||
# Stop the server and start the maintenance mode
|
||||
forever stopall
|
||||
forever start server_config/maintenance.js
|
||||
|
||||
# Keep the settings.json file
|
||||
git stash
|
||||
git pull
|
||||
git stash pop
|
||||
|
||||
# In case something was added in package.json or bower.json
|
||||
# In case something was added in package.json
|
||||
rm -rf node_modules
|
||||
npm install
|
||||
rm -rf bower_components
|
||||
bower install --config.interactive=false --allow-root
|
||||
npm install || exit 1
|
||||
|
||||
# Front-end compilation
|
||||
rm -rf front/build
|
||||
grunt build
|
||||
|
||||
# Restart the server
|
||||
# Stop the maintenance mode and restart the server
|
||||
forever stopall
|
||||
NODE_ENV=production forever start -c "node --stack-size=262000" bin/server.js
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"serverPort": 80,
|
||||
"phantomasEngine": "webkit",
|
||||
"googleAnalyticsId": "",
|
||||
"screenshotWidth": 400,
|
||||
|
||||
"authorizedKeys": {
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"serverPort": 8383,
|
||||
"phantomasEngine": "webkit",
|
||||
"googleAnalyticsId": "",
|
||||
"screenshotWidth": 400,
|
||||
|
||||
"authorizedKeys": {
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ describe('api', function() {
|
||||
},
|
||||
json: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Api-Key': 'invalid'
|
||||
}
|
||||
}, function(error, response, body) {
|
||||
@@ -53,6 +54,7 @@ describe('api', function() {
|
||||
},
|
||||
json: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Api-Key': Object.keys(config.authorizedKeys)[0]
|
||||
}
|
||||
}, function(error, response, body) {
|
||||
@@ -76,6 +78,7 @@ describe('api', function() {
|
||||
},
|
||||
json: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Api-Key': Object.keys(config.authorizedKeys)[0]
|
||||
}
|
||||
}, function(error, response, body) {
|
||||
@@ -99,12 +102,13 @@ describe('api', function() {
|
||||
screenshot: true,
|
||||
device: 'tablet',
|
||||
//waitForSelector: '*',
|
||||
cookie: 'foo=bar',
|
||||
cookie: 'foo=bar;domain=google.com',
|
||||
authUser: 'joe',
|
||||
authPass: 'secret'
|
||||
},
|
||||
json: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Api-Key': Object.keys(config.authorizedKeys)[0]
|
||||
}
|
||||
}, function(error, response, body) {
|
||||
@@ -133,6 +137,7 @@ describe('api', function() {
|
||||
},
|
||||
json: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Api-Key': Object.keys(config.authorizedKeys)[0]
|
||||
}
|
||||
}, function(error, response, body) {
|
||||
@@ -165,14 +170,13 @@ describe('api', function() {
|
||||
body.should.have.a.property('rules').that.is.an('object');
|
||||
body.should.have.a.property('toolsResults').that.is.an('object');
|
||||
|
||||
// javascriptExecutionTree should only be filled if option jsTimeline is true
|
||||
body.should.have.a.property('javascriptExecutionTree').that.is.an('object');
|
||||
body.javascriptExecutionTree.should.deep.equal({});
|
||||
body.javascriptExecutionTree.should.not.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('cookie').that.equals('foo=bar;domain=google.com');
|
||||
body.params.options.should.have.a.property('authUser').that.equals('joe');
|
||||
body.params.options.should.have.a.property('authPass').that.equals('secret');
|
||||
|
||||
@@ -208,6 +212,7 @@ describe('api', function() {
|
||||
},
|
||||
json: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Api-Key': Object.keys(config.authorizedKeys)[0]
|
||||
}
|
||||
}, function(error, response, body) {
|
||||
@@ -232,6 +237,7 @@ describe('api', function() {
|
||||
url: serverUrl + '/api/runs/' + asyncRunId,
|
||||
json: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Api-Key': Object.keys(config.authorizedKeys)[0]
|
||||
}
|
||||
}, function(error, response, body) {
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
var should = require('chai').should();
|
||||
var colorDiff = require('../../lib/tools/colorDiff');
|
||||
|
||||
describe('colorDiff', function() {
|
||||
|
||||
it('should parse offenders correctly', function() {
|
||||
colorDiff.parseOffender('#000 (1 times)').should.equal('#000');
|
||||
colorDiff.parseOffender('#5bc0de (2 times)').should.equal('#5bc0de');
|
||||
colorDiff.parseOffender('rgba(0,0,0,0.075) (100 times)').should.equal('rgba(0,0,0,0.075)');
|
||||
colorDiff.parseOffender('rgb(91,192,222) (1000 times)').should.equal('rgb(91,192,222)');
|
||||
});
|
||||
|
||||
it('should parse colors correctly', function() {
|
||||
colorDiff.parseColor('#000').should.deep.equal({R: 0, G: 0, B: 0, A: 1, Lab: {L: 0, a: 0, b: 0}, original: '#000'});
|
||||
colorDiff.parseColor('rgba(255,255,255,0.5)').should.deep.equal({R: 255, G: 255, B: 255, A: 0.5, Lab: {L: 100, a: 0.00526049995830391, b: -0.010408184525267927}, original: 'rgba(255,255,255,0.5)'});
|
||||
});
|
||||
|
||||
it('should not compare colors with different alpha', function() {
|
||||
var color1 = colorDiff.parseColor("rgba(0, 0, 0, 1)");
|
||||
var color2 = colorDiff.parseColor("rgba(0, 0, 0, 0.5)");
|
||||
|
||||
colorDiff.compareTwoColors(color1, color2).should.equal(false);
|
||||
});
|
||||
|
||||
it('should find that two colors are similar', function() {
|
||||
var color1 = colorDiff.parseColor("rgba(0, 0, 0, 1)");
|
||||
var color2 = colorDiff.parseColor("rgba(0, 0, 1, 1)");
|
||||
|
||||
colorDiff.compareTwoColors(color1, color2).should.equal(true);
|
||||
});
|
||||
|
||||
it('should find that two colors are similar even with alpha', function() {
|
||||
var color1 = colorDiff.parseColor("rgba(0, 0, 0, 0.3)");
|
||||
var color2 = colorDiff.parseColor("rgba(0, 0, 2, 0.3)");
|
||||
|
||||
colorDiff.compareTwoColors(color1, color2).should.equal(true);
|
||||
});
|
||||
|
||||
it('should find that two colors are different', function() {
|
||||
var color1 = colorDiff.parseColor("rgba(0, 0, 0, 1)");
|
||||
var color2 = colorDiff.parseColor("rgba(99, 99, 99, 1)");
|
||||
|
||||
colorDiff.compareTwoColors(color1, color2).should.equal(false);
|
||||
});
|
||||
|
||||
it('should compare all colors to each other', function() {
|
||||
var colors = [
|
||||
'#000 (1 times)',
|
||||
'#5bc0de (2 times)',
|
||||
'rgba(0,0,0,0.075) (100 times)',
|
||||
'rgb(91,192,222) (1000 times)',
|
||||
'rgba(0,0,2,1) (1 times)',
|
||||
'rgba(99,99,99,1) (1 times)',
|
||||
'rgba(100,100,100,1) (1 times)'
|
||||
];
|
||||
|
||||
var data = {
|
||||
toolsResults: {
|
||||
phantomas: {
|
||||
offenders: {
|
||||
cssColors: colors
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
newData = colorDiff.compareAllColors(data);
|
||||
|
||||
newData.toolsResults.should.have.a.property('colorDiff');
|
||||
newData.toolsResults.colorDiff.should.have.a.property('metrics');
|
||||
newData.toolsResults.colorDiff.metrics.should.have.a.property('similarColors').that.equals(3);
|
||||
newData.toolsResults.colorDiff.should.have.a.property('offenders');
|
||||
newData.toolsResults.colorDiff.offenders.should.have.a.property('similarColors').that.deep.equals([
|
||||
{
|
||||
color1: '#000',
|
||||
color2: 'rgba(0,0,2,1)',
|
||||
isDark: true
|
||||
},
|
||||
{
|
||||
color1: '#5bc0de',
|
||||
color2: 'rgb(91,192,222)',
|
||||
isDark: false
|
||||
},
|
||||
{
|
||||
color1: 'rgba(99,99,99,1)',
|
||||
color2: 'rgba(100,100,100,1)',
|
||||
isDark: true
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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": {
|
||||
@@ -363,18 +183,19 @@ describe('rulesChecker', function() {
|
||||
|
||||
var versions = {
|
||||
'1.2.9': 0,
|
||||
'1.3.9': 0,
|
||||
'1.4.4': 10,
|
||||
'1.5.0': 20,
|
||||
'1.6.3': 30,
|
||||
'1.7.0': 40,
|
||||
'1.8.3a': 50,
|
||||
'1.9.2': 70,
|
||||
'1.10.1': 90,
|
||||
'2.0.0-rc1': 90,
|
||||
'1.11.1': 100,
|
||||
'2.1.1-beta1': 100,
|
||||
'3.0.0': 100
|
||||
'1.4.4': 0,
|
||||
'1.5.0': 10,
|
||||
'1.6.3': 20,
|
||||
'1.7.0': 30,
|
||||
'1.8.3a': 40,
|
||||
'1.9.2': 50,
|
||||
'1.10.1': 70,
|
||||
'2.0.0-rc1': 70,
|
||||
'1.11.1': 90,
|
||||
'2.1.1-beta1': 90,
|
||||
'1.12.1': 100,
|
||||
'2.3.1': 100,
|
||||
'3.1.0': 100
|
||||
};
|
||||
|
||||
for (var version in versions) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
var should = require('chai').should();
|
||||
var isHttp2 = require('../../lib/tools/isHttp2');
|
||||
|
||||
describe('isHttp2', function() {
|
||||
|
||||
it('should parse the protocol correctly', function() {
|
||||
isHttp2.getProtocol({
|
||||
toolsResults: {
|
||||
phantomas: {
|
||||
url: 'http://www.yahoo.com'
|
||||
}
|
||||
}
|
||||
}).should.equal('http:');
|
||||
|
||||
|
||||
isHttp2.getProtocol({
|
||||
toolsResults: {
|
||||
phantomas: {
|
||||
url: 'https://www.yahoo.com'
|
||||
}
|
||||
}
|
||||
}).should.equal('https:');
|
||||
});
|
||||
|
||||
it('should parse the domain correctly', function() {
|
||||
isHttp2.getDomain({
|
||||
toolsResults: {
|
||||
phantomas: {
|
||||
url: 'http://www.yahoo.com'
|
||||
}
|
||||
}
|
||||
}).should.equal('www.yahoo.com');
|
||||
|
||||
|
||||
isHttp2.getDomain({
|
||||
toolsResults: {
|
||||
phantomas: {
|
||||
url: 'https://www.yahoo.com'
|
||||
}
|
||||
}
|
||||
}).should.equal('www.yahoo.com');
|
||||
});
|
||||
|
||||
it('should have a function checkHttp2', function() {
|
||||
isHttp2.should.have.a.property('checkHttp2').that.is.a('function');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
var should = require('chai').should();
|
||||
var mediaQueriesChecker = require('../../lib/tools/mediaQueriesChecker');
|
||||
|
||||
describe('mediaQueriesChecker', function() {
|
||||
|
||||
it('should parse mediaQueryes correctly', function() {
|
||||
mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 1024px) (1 rules) <http://domain.com/css/stylesheet.css> @ 269:1').should.deep.equal({
|
||||
mediaQuery: {
|
||||
query: 'screen and (max-width: 1024px)',
|
||||
rules: 1,
|
||||
file: 'http://domain.com/css/stylesheet.css',
|
||||
line: 269,
|
||||
column: 1
|
||||
},
|
||||
isForMobile: true,
|
||||
breakpoints: [{string: '1024px', pixels: 1024}]
|
||||
});
|
||||
});
|
||||
|
||||
it('should determine if it\'s for mobile correctly', function() {
|
||||
mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 1024px) (1 rules) <file> @ 1:1').isForMobile.should.equal(true);
|
||||
mediaQueriesChecker.parseOneMediaQuery('@media (max-width:1024px) (1 rules) <file> @ 1:1').isForMobile.should.equal(true);
|
||||
mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 320px) (1 rules) <file> @ 1:1').isForMobile.should.equal(true);
|
||||
mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 290px) (1 rules) <file> @ 1:1').isForMobile.should.equal(false);
|
||||
mediaQueriesChecker.parseOneMediaQuery('@media screen and (min-width: 320px) (1 rules) <file> @ 1:1').isForMobile.should.equal(false);
|
||||
mediaQueriesChecker.parseOneMediaQuery('@media screen and (min-width: 600px) (1 rules) <file> @ 1:1').isForMobile.should.equal(false);
|
||||
mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 20em) (1 rules) <file> @ 1:1').isForMobile.should.equal(true);
|
||||
mediaQueriesChecker.parseOneMediaQuery('@media screen and (min-width: 40em) (1 rules) <file> @ 1:1').isForMobile.should.equal(false);
|
||||
mediaQueriesChecker.parseOneMediaQuery('@media (min-width: 600px) and (max-width: 1000px) (1 rules) <file> @ 1:1').isForMobile.should.equal(false);
|
||||
mediaQueriesChecker.parseOneMediaQuery('@media (min-width: 180px) and (max-width: 400px) (1 rules) <file> @ 1:1').isForMobile.should.equal(true);
|
||||
mediaQueriesChecker.parseOneMediaQuery('@media (min-width: 180px) and (max-width: 290px) (1 rules) <file> @ 1:1').isForMobile.should.equal(false);
|
||||
mediaQueriesChecker.parseOneMediaQuery('@media not all and (min-width: 600px) (1 rules) <file> @ 1:1').isForMobile.should.equal(true);
|
||||
mediaQueriesChecker.parseOneMediaQuery('@media not all and (min-width: 180px) (1 rules) <file> @ 1:1').isForMobile.should.equal(false);
|
||||
mediaQueriesChecker.parseOneMediaQuery('@media not all and (min-width: 1000px) and (max-width: 600px) (1 rules) <file> @ 1:1').isForMobile.should.equal(false);
|
||||
mediaQueriesChecker.parseOneMediaQuery('@media not all and (min-width: 400px) and (max-width: 180px) (1 rules) <file> @ 1:1').isForMobile.should.equal(true);
|
||||
});
|
||||
|
||||
it('should count breakpoints correctly', function() {
|
||||
mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 1024px) (1 rules) <file> @ 1:1').breakpoints.should.deep.equal([{string: '1024px', pixels: 1024}]);
|
||||
mediaQueriesChecker.parseOneMediaQuery('@media (max-width:1024px) (1 rules) <file> @ 1:1').breakpoints.should.deep.equal([{string: '1024px', pixels: 1024}]);
|
||||
mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 320px) (1 rules) <file> @ 1:1').breakpoints.should.deep.equal([{string: '320px', pixels: 320}]);
|
||||
mediaQueriesChecker.parseOneMediaQuery('@media (min-width: 600px) and (max-width: 1000px) (1 rules) <file> @ 1:1').breakpoints.should.deep.equal([{string: '600px', pixels: 600}, {string: '1000px', pixels: 1000}]);
|
||||
mediaQueriesChecker.parseOneMediaQuery('@media not all and (min-width: 180px) (1 rules) <file> @ 1:1').breakpoints.should.deep.equal([{string: '180px', pixels: 180}]);
|
||||
mediaQueriesChecker.parseOneMediaQuery('@media not all and (min-width: 1000px) and (max-width: 600px) (1 rules) <file> @ 1:1').breakpoints.should.deep.equal([{string: '1000px', pixels: 1000}, {string: '600px', pixels: 600}]);
|
||||
mediaQueriesChecker.parseOneMediaQuery('@media (max-height:500px) (1 rules) <file> @ 1:1').breakpoints.should.deep.equal([]);
|
||||
mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 100em) (1 rules) <file> @ 1:1').breakpoints.should.deep.equal([{string: '100em', pixels: 1600}]);
|
||||
mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 120pt) (1 rules) <file> @ 1:1').breakpoints.should.deep.equal([{string: '120pt', pixels: 160}]);
|
||||
mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 40.2em) (1 rules) <file> @ 1:1').breakpoints.should.deep.equal([{string: '40.2em', pixels: 643.2}]);
|
||||
});
|
||||
|
||||
it('should fail silently', function() {
|
||||
mediaQueriesChecker.parseOneMediaQuery('@media bad syntax (1 rules) <file> @ 1:1').isForMobile.should.equal(false);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -299,6 +299,17 @@ describe('offendersHelpers', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle line breaks inside the string', function() {
|
||||
var result = offendersHelpers.cssOffenderPattern('.card-mask-wrap { -moz-transform: translate3d(0, \n-288px\n, 0) } // was required by firefox 15 and earlier [inline CSS] @ 29:3');
|
||||
|
||||
result.should.deep.equal({
|
||||
css: '.card-mask-wrap { -moz-transform: translate3d(0, -288px, 0) } // was required by firefox 15 and earlier',
|
||||
file: null,
|
||||
line: 29,
|
||||
column: 3
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('fileWithSizePattern', function() {
|
||||
|
||||
@@ -42,18 +42,23 @@ describe('phantomasWrapper', function() {
|
||||
phantomasWrapper.execute({
|
||||
params: {
|
||||
url: url,
|
||||
options: {}
|
||||
options: {
|
||||
device: 'desktop'
|
||||
}
|
||||
}
|
||||
}).then(function(data) {
|
||||
|
||||
done('Error: unwanted success');
|
||||
|
||||
}).fail(function(err) {
|
||||
try {
|
||||
should.exist(err);
|
||||
err.should.equal(254);
|
||||
|
||||
should.exist(err);
|
||||
err.should.equal(254);
|
||||
|
||||
done();
|
||||
done();
|
||||
} catch(error) {
|
||||
done(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -70,16 +75,20 @@ describe('phantomasWrapper', function() {
|
||||
}
|
||||
}).then(function(data) {
|
||||
/*jshint -W030 */
|
||||
|
||||
data.should.be.an('object');
|
||||
data.should.have.a.property('generator');
|
||||
data.generator.should.contain('phantomas');
|
||||
data.should.have.a.property('url').that.equals(url);
|
||||
data.should.have.a.property('metrics').that.is.an('object').not.empty;
|
||||
data.should.have.a.property('offenders').that.is.an('object').not.empty;
|
||||
data.offenders.should.have.a.property('javascriptExecutionTree').that.is.a('array').not.empty;
|
||||
|
||||
done();
|
||||
try {
|
||||
data.should.be.an('object');
|
||||
data.should.have.a.property('generator');
|
||||
data.generator.should.contain('phantomas');
|
||||
data.should.have.a.property('url').that.equals(url);
|
||||
data.should.have.a.property('metrics').that.is.an('object').not.empty;
|
||||
data.should.have.a.property('offenders').that.is.an('object').not.empty;
|
||||
data.offenders.should.have.a.property('javascriptExecutionTree').that.is.a('array').not.empty;
|
||||
|
||||
done();
|
||||
} catch(error) {
|
||||
done(error);
|
||||
}
|
||||
}).fail(function(err) {
|
||||
done(err);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
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: 'http://localhost:8388/xml.xml',
|
||||
requestHeaders: {
|
||||
'User-Agent': 'something',
|
||||
Referer: 'http://www.google.fr/',
|
||||
Accept: '*/*'
|
||||
},
|
||||
status: 200,
|
||||
isXML: true,
|
||||
type: 'xml'
|
||||
},
|
||||
{
|
||||
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(1);
|
||||
|
||||
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(5);
|
||||
|
||||
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(7);
|
||||
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(1);
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"serverPort": "8387",
|
||||
"phantomasEngine": "webkit",
|
||||
"googleAnalyticsId": "",
|
||||
"screenshotWidth": 400,
|
||||
|
||||
"authorizedKeys": {
|
||||
"1234567890": "contact@gaelmetais.com"
|
||||
|
||||
|
After Width: | Height: | Size: 115 KiB |
|
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 |
@@ -4,5 +4,9 @@
|
||||
</head>
|
||||
<body>
|
||||
<h1>Simple page</h1>
|
||||
|
||||
<script>
|
||||
document.getElementById('foo');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 84 KiB |