Compare commits

...
163 Commits
Author SHA1 Message Date
Gaël Métais 7e1bf0fc92 Merge pull request #112 from gmetais/develop
v1.8.0
2015-09-20 01:34:58 +02:00
Gaël Métais 82693a8587 v1.8 + update dependencies 2015-09-20 01:29:45 +02:00
Gaël Métais 99eace0336 Merge pull request #111 from gmetais/color-diff
New rule: similar colors
2015-09-20 01:19:13 +02:00
Gaël Métais d0257fea8f New rule: similar colors 2015-09-20 01:06:42 +02:00
Gaël Métais 453a5cda2e Stricter hiddenImages rule 2015-09-19 11:55:38 +02:00
Gaël Métais bdbd1a0f08 Merge pull request #108 from gmetais/hiddenImages
New metric: hiddenImages
2015-09-18 18:59:47 +02:00
Gaël Métais d32b9f78a4 New metric: hiddenImages 2015-09-18 18:51:41 +02:00
Gaël Métais 46c2d8c49e Fix backtraces in the profiler when using PhantomJS 2 2015-09-18 13:49:24 +02:00
Gaël Métais 88b5567131 Merge pull request #106 from gmetais/develop
v1.7.8
2015-09-15 07:53:41 +02:00
Gaël Métais 0e81d16feb Reduce the timeout from 45 to 30s 2015-09-15 07:37:33 +02:00
Gaël Métais a900f0ff65 Cooler DOMaccessesOnScroll rule 2015-09-14 22:54:17 +02:00
Gaël Métais d4faa75b79 Update dependencies 2015-09-14 22:52:50 +02:00
Gaël Métais 47b0de1b49 v1.7.8 2015-09-14 22:50:43 +02:00
Gaël Métais fe6a10592b Merge pull request #105 from gmetais/basic-auth
Basic auth
2015-09-14 22:49:29 +02:00
Gaël Métais 8f02349ca5 Fix tests 2015-09-14 22:36:43 +02:00
Gaël Métais 1fb32de428 Change auth pwd field to a password type 2015-09-14 17:25:23 +02:00
Gaël Métais 31b66ee6fa Send auth headers with requests 2015-09-14 17:25:23 +02:00
Gaël Métais d22f9b81b2 Merge pull request #104 from gmetais/develop
v1.7.7
2015-09-11 10:47:02 +02:00
Gaël Métais 3051d61365 Show the goal for 100/100 on the rule page 2015-09-11 10:24:20 +02:00
Gaël Métais 53fd2e48e5 Cooler on global variables, not on document.write 2015-09-11 10:23:13 +02:00
Gaël Métais ef516c123d Update dependencies 2015-09-11 08:45:01 +02:00
Gaël Métais add8962390 Merge pull request #103 from gmetais/xml
Add xml output option to the CLI
2015-09-10 22:58:57 +02:00
Gaël Métais 1a444a481e Add xml output option to the CLI 2015-09-10 18:42:19 +02:00
Gaël Métais 96ca4a7933 Remove input type url for mobiles, causing a bug on iOS 2015-09-10 18:08:05 +02:00
Gaël Métais f07bc615b0 Merge pull request #101 from gmetais/develop
v1.7.6
2015-08-28 11:18:43 +02:00
Gaël Métais 2d0c88390a Fix tests 2015-08-28 10:57:46 +02:00
Gaël Métais 52574623fe Merge branch 'phantomjs2-quick' into develop 2015-08-28 10:49:19 +02:00
Gaël Métais b1d95ddf5f v1.7.6 2015-08-28 10:48:11 +02:00
Gaël Métais 092bb285ed Add a phantomasEngine setting for the server 2015-08-28 10:36:49 +02:00
Gaël Métais 52ce26796a Merge pull request #98 from gmetais/develop
v1.7.5
2015-08-16 14:38:35 +02:00
Gaël Métais 6bd55e4807 Avoid destroying SVG fonts when optimizing images 2015-08-16 14:23:37 +02:00
Gaël Métais 8325a9297e v1.7.5 2015-08-16 14:04:29 +02:00
Gaël Métais 9cb3c78def Hide scrollbars on screenshot (show then on hover) 2015-08-16 14:03:55 +02:00
Gaël Métais 143436ecd3 Change tooltip message 2015-08-16 13:47:41 +02:00
Gaël Métais 83cf87c8cf Merge pull request #97 from gmetais/browserify
Remove Bower and update dependencies
2015-08-16 13:36:43 +02:00
Gaël Métais 2f1003e8bc Remove bower from shell scripts 2015-08-16 13:30:57 +02:00
Gaël Métais e0649eb9bd Replace bower by npm (browserify) 2015-08-16 13:22:12 +02:00
Gaël Métais d5c607d370 Update dependencies 2015-08-16 13:06:18 +02:00
Gaël Métais 2c2c8bf4cb Add author to package.json 2015-08-11 21:24:43 +02:00
Gaël Métais 08459ca289 Add keywords to package.json 2015-08-11 21:23:46 +02:00
Gaël Métais 162f1fa1d1 Migrate Travis-CI to container-base infra 2015-08-01 19:42:33 +02:00
Gaël Métais d9113edbbe Merge pull request #95 from gmetais/develop
v1.7.4
2015-08-01 19:41:26 +02:00
Gaël Métais eb1c5c07e9 v1.7.4 2015-08-01 19:30:56 +02:00
Gaël Métais 0574429a93 Warn about srcset not being used 2015-08-01 19:27:48 +02:00
Gaël Métais 8ff565404b Warn about srcset not being used 2015-08-01 19:22:06 +02:00
Gaël Métais b08d4b4cae Merge pull request #94 from gmetais/links
Change ng-click to real <a> on dashboard rows
2015-08-01 18:53:02 +02:00
Gaël Métais f331662263 Merge branch 'develop' of https://github.com/gmetais/YellowLabTools into develop 2015-08-01 18:52:06 +02:00
Gaël Métais 10693283c5 Add license to package.json 2015-08-01 18:51:51 +02:00
Gaël Métais f2a933c51b Remove uneeded function 2015-08-01 18:44:49 +02:00
Gaël Métais 172b8cfb9b Change ng-click to real links on rule rows 2015-07-31 19:41:47 +02:00
Gaël Métais 47738ffa26 Merge pull request #93 from gmetais/fix-update
Exit server update script if npm install fails
2015-07-31 16:39:17 +02:00
Gaël Métais c8a29b4651 Fail the shell scripts if npm or bower fail 2015-07-31 16:16:26 +02:00
Gaël Métais c305cc2f7d Merge branch 'develop' of https://github.com/gmetais/YellowLabTools into develop 2015-07-31 16:14:59 +02:00
Gaël Métais 01676e1f99 Fix typo 2015-07-30 11:09:34 +02:00
Gaël Métais de1e15fb25 Merge pull request #90 from RaphArbuz/fix-typo-deboucing
fix typo: deboucing -> debouncing
2015-07-22 16:41:23 +02:00
RaphArbuz 0aa77c33ad fix typo: deboucing -> debouncing 2015-07-22 16:37:05 +02:00
Gaël Métais 22eeb1a47a Merge pull request #89 from gmetais/develop
v1.7.3
2015-07-22 16:02:03 +02:00
Gaël Métais 15b3c6617b v1.7.3 2015-07-22 16:01:08 +02:00
Gaël Métais e342a88f7d Fix YLT freezing the queue when an incorrect gzip header is sent on a file 2015-07-22 15:48:45 +02:00
Gaël Métais 7779efedf1 Add a GitHub star button in the footer 2015-07-20 11:48:35 +02:00
Gaël Métais 137b4d25b9 Fix css offenders parsing when containing line breaks 2015-07-08 23:25:11 +02:00
Gaël Métais 9be40adabf v1.7.2 2015-06-24 08:42:00 +02:00
Gaël Métais b6d4b7b40d v1.7.2 2015-06-24 08:41:43 +02:00
Gaël Métais 3256789082 Merge pull request #87 from gmetais/fix-xml
Fix server freeze issue
2015-06-24 08:32:30 +02:00
Gaël Métais 87adc575ac Fix type xml issue 2015-06-24 08:01:58 +02:00
Gaël Métais 30ae0f93f3 Remove unneeded promise step 2015-06-24 08:00:56 +02:00
Gaël Métais c339654112 Failing test case with xml file 2015-06-24 07:49:44 +02:00
Gaël Métais 25c9770fba v1.7.1 2015-06-22 15:29:45 +02:00
Gaël Métais 0bf7326604 v1.7.1 2015-06-22 15:29:36 +02:00
Gaël Métais 7fc5a9ba8f v1.7.1 2015-06-22 15:25:47 +02:00
Gaël Métais 230f805cae Add libjpeg to installation script 2015-06-22 15:19:00 +02:00
Gaël Métais bc9c828584 Fix dependency problem 2015-06-22 15:18:19 +02:00
Gaël Métais 6c17a709bf Merge pull request #85 from gmetais/develop
Releasing v1.7.0 very soon
2015-06-20 18:00:04 +02:00
Gaël Métais dd912ecc8f Merge pull request #84 from gmetais/reorder
New rules and rules reordering
2015-06-20 16:24:08 +02:00
Gaël Métais c9b12c4892 Remove bugged lossless image optim gain when there's a lossy one 2015-06-20 16:11:10 +02:00
Gaël Métais 414aea2637 Update dependencies and set version v1.7.0 2015-06-20 15:51:46 +02:00
Gaël Métais b7bc4dbadc Made Weight and Requests the first categories because they are the most important 2015-06-20 15:22:42 +02:00
Gaël Métais 8a8b871908 Changed some thresholds, mainly cooler 2015-06-20 15:19:50 +02:00
Gaël Métais 1225b27510 Fix timeline script filter 2015-06-20 14:34:13 +02:00
Gaël Métais f86bd86043 Add google analytics to already minified fies 2015-06-20 14:20:31 +02:00
Gaël Métais 8133164f44 Rename total requests number 2015-06-20 13:51:34 +02:00
Gaël Métais 2f447da34b Add Images not lazyloaded rule 2015-06-20 00:12:47 +02:00
Gaël Métais 41cdc8783e Remove uneeded policies 2015-06-19 23:47:25 +02:00
Gaël Métais ccbb324270 Replace TotalRequests and SmallRequests rules 2015-06-19 23:46:39 +02:00
Gaël Métais 8d4a0c34e7 Fix requests offenders 2015-06-19 18:45:34 +02:00
Gaël Métais 2620f75955 Fix SVG test 2015-06-19 18:01:02 +02:00
Gaël Métais 4ca525f701 Compare gzip size for svg optimizations 2015-06-19 17:16:30 +02:00
Gaël Métais 53a9aa377a Add Gzip offenders 2015-06-19 16:16:00 +02:00
Gaël Métais d166003dfe Update Phantomas, needed for svg optimization 2015-06-19 16:15:19 +02:00
Gaël Métais 31d80791a9 Compare minified files on gzipped size 2015-06-19 15:24:19 +02:00
Gaël Métais 02245aa5f1 Fix test 2015-06-19 14:55:13 +02:00
Gaël Métais 583b6b9a55 Optimizing JS minification step that takes too long 2015-06-19 14:05:21 +02:00
Gaël Métais d4815e855e Fix bug with SVG minification+compression 2015-06-18 23:06:57 +02:00
Gaël Métais 113665139a Rename isMinified into isOptimized 2015-06-18 22:51:02 +02:00
Gaël Métais 37e35b0ed6 Debugging test failing on travis 2015-06-18 16:42:24 +02:00
Gaël Métais eef8f00d8b Debugging test failing on travis 2015-06-18 15:51:00 +02:00
Gaël Métais bdb15a7780 Debugging test failing on travis 2015-06-18 15:09:10 +02:00
Gaël Métais 2b59912549 Debugging test failing on travis 2015-06-18 14:52:26 +02:00
Gaël Métais 8623085c2a Debugging test failing on travis 2015-06-18 13:50:34 +02:00
Gaël Métais 612627d66f Restore console.log correctly 2015-06-18 12:11:37 +02:00
Gaël Métais 04700beab3 Fixing tests on Travis 2015-06-18 11:55:36 +02:00
Gaël Métais 0d3d6c88e4 Trying to fix build on Travis 2015-06-18 11:10:47 +02:00
Gaël Métais db57f67e98 Change policy text 2015-06-18 11:08:16 +02:00
Gaël Métais baa145e15c Add a gzip compression task 2015-06-18 11:07:19 +02:00
Gaël Métais c1bae20f01 File minification 2015-06-16 20:33:19 +02:00
Gaël Métais 41bc676b12 Fix bug with about:blank 2015-06-01 17:44:34 +02:00
Gaël Métais cea74dd63d Cooler Image Optimization score 2015-06-01 17:44:18 +02:00
Gaël Métais 9ccca3b33b Add offenders to imageOptimization 2015-06-01 16:52:50 +02:00
Gaël Métais 1606cbab18 Add Image Optimization rule to the dashbard 2015-06-01 14:57:05 +02:00
Gaël Métais c428c8ae2d Optimize SVG files 2015-05-31 19:36:54 +02:00
Gaël Métais b52e84d1e7 Optimize PNG files 2015-05-31 19:25:34 +02:00
Gaël Métais 3fbcbbe8e0 Merge branch 'develop' into reorder 2015-05-31 19:02:40 +02:00
Gaël Métais 74eabd700c Add a JPEG optimizer 2015-05-31 18:39:52 +02:00
Gaël Métais d7ddae38e5 Total Weight offenders 2015-05-30 12:52:50 +02:00
Gaël Métais fe7de2db99 Fix rare bug with domComplete never triggered 2015-05-29 22:56:07 +02:00
Gaël Métais 9b9de42139 Fix test on headersSize 2015-05-29 17:01:51 +02:00
Gaël Métais 7b64288765 Fix typo in module name 2015-05-29 16:41:12 +02:00
Gaël Métais c6c967f89c Adding totalWeight metric 2015-05-29 16:13:32 +02:00
Gaël Métais 040dba961a v1.6.4 2015-05-28 11:26:39 +02:00
Gaël Métais 0146794cda Merge pull request #83 from gmetais/black
Fix black backgrounds in screenshots #82.
2015-05-28 11:21:00 +02:00
Gaël Métais b00eba77f9 Fix typo in comment 2015-05-28 11:12:02 +02:00
Gaël Métais 3f27ae249a Add a white background to screenshots 2015-05-28 11:09:45 +02:00
Gaël Métais 4384cffabe Merge branch 'develop' 2015-05-26 15:25:22 +02:00
Gaël Métais fde3234fc6 v1.6.3 2015-05-26 15:24:43 +02:00
Gaël Métais d998c7cd46 Merge branch 'develop' 2015-05-26 15:23:30 +02:00
Gaël Métais a8b62fc184 Fix score display on dashboard 2015-05-26 15:23:09 +02:00
Gaël Métais 65de8e08d3 Merge branch 'develop' 2015-05-26 11:19:54 +02:00
Gaël Métais 73cc1cd2e5 v1.6.2 2015-05-26 11:19:39 +02:00
Gaël Métais 26926071b0 Merge branch 'master' 2015-05-26 11:16:56 +02:00
Gaël Métais 5c433129ec Fix score display on dashboard 2015-05-26 11:16:24 +02:00
Gaël Métais 68bc1a6608 Wording 2015-05-25 19:06:45 +02:00
Gaël Métais e67a0e0bc0 Merge pull request #81 from gmetais/develop
v1.6.0
2015-05-25 16:38:14 +02:00
Gaël Métais f553d45396 v1.6.0 2015-05-25 16:01:24 +02:00
Gaël Métais 090a53b95a Killing a Phantomas process will not kill YLT anymore 2015-05-25 15:57:01 +02:00
Gaël Métais 22ab7515fb Redirect www. to non-www (for SEO) 2015-05-25 15:01:21 +02:00
Gaël Métais a97dcc5efb Change the tweet-this message 2015-05-25 14:35:39 +02:00
Gaël Métais 4b098aef69 Fix timeline bug, not being displayed 2015-05-25 14:04:27 +02:00
Gaël Métais cb46274288 New rule: jQuery calls on empty objects 2015-05-25 13:33:26 +02:00
Gaël Métais 1e60a43075 Don't count duplicated queries on elements not attached to DOM 2015-05-25 11:42:47 +02:00
Gaël Métais 1aa9cd032f Update dependencies 2015-05-25 09:22:45 +02:00
Gaël Métais 5e3748315c Remove jQueryNotDelegatedEvents offenders and send user to filtered timeline 2015-05-25 09:22:25 +02:00
Gaël Métais 7f67c032d1 fix tests 2015-05-25 08:30:02 +02:00
Gaël Métais 33621af3ad Replaced DOMqueriesWithoutResults by queriesWithoutResults to include jQuery queries 2015-05-22 18:54:32 +02:00
Gaël Métais 2fb4a7bd6a Replace rules DOMinserts, DOMqueries & eventsBound by DOMaccesses 2015-05-22 17:47:13 +02:00
Gaël Métais 1ff983a03b Add specific sub-filters on warnings in the timeline 2015-05-22 17:44:54 +02:00
Gaël Métais 701530ef04 Be cooler on global variables 2015-05-22 11:16:18 +02:00
Gaël Métais 719e624ddf Fix icons showing a background grid on IE 2015-05-22 11:09:29 +02:00
Gaël Métais 03a25df367 One webfont is ok 2015-05-22 10:57:39 +02:00
Gaël Métais c93ce684a8 Old jQuery major versions didn't have the trailing .0 in number 2015-05-22 10:52:12 +02:00
Gaël Métais 090708f38b Merge branch 'HipsterBrown-url-input' into develop 2015-05-22 10:06:27 +02:00
Gaël Métais 49f93e660e User agent sniffing 2015-05-22 10:05:41 +02:00
Nick Hehr 867680f4e9 Updates LaunchTest() form text input to be of type "url".
Making this change not only adds automatic url validation from modern
browsers but enabled special keyboards on mobile devices to make
inputing urls much easier.
2015-05-20 10:31:34 -04:00
Gaël Métais 36ac529cc8 Improve doc 2015-05-13 19:29:26 +02:00
Gaël Métais 0ae388f0f6 Improve doc 2015-05-13 19:23:12 +02:00
Gaël Métais b53ace765c Improve doc 2015-05-13 19:13:38 +02:00
Gaël Métais f256299a0c Improve doc 2015-05-13 18:48:33 +02:00
Gaël Métais 4fb05e406a New doc test 2015-05-13 18:21:31 +02:00
Gaël Métais 035639c68a Improve doc 2015-05-13 09:12:21 +02:00
Gaël Métais 09f53cac92 Add ngrok in README 2015-05-12 13:57:45 +02:00
Gaël Métais 05a27d9003 v1.5.1 2015-05-12 11:43:09 +02:00
Gaël Métais 5d4d1807ba v1.5.1 2015-05-12 11:42:50 +02:00
Gaël Métais 836e465414 Fix bug with DOMaccessesOnScroll offenders 2015-05-12 11:38:05 +02:00
Gaël Métais 94457b24cd Small wording adjustment 2015-05-12 10:30:28 +02:00
92 changed files with 6134 additions and 907 deletions
-3
View File
@@ -1,3 +0,0 @@
{
"directory" : "bower_components"
}
-1
View File
@@ -1,5 +1,4 @@
node_modules
bower_components
.tmp
.vagrant
results/*
+5 -2
View File
@@ -1,6 +1,9 @@
language: node_js
sudo: false
node_js:
- "0.10.33"
- "0.12.4"
before_install:
- "npm install -g npm"
- "npm install -g grunt-cli"
install: npm install
install:
- "npm install"
+1 -1
View File
@@ -143,7 +143,7 @@ module.exports = function(grunt) {
options: {
reporter: 'spec',
},
src: ['test/core/offendersHelpersTest.js']
src: ['test/core/imageOptimizerTest.js']
},
coverage: {
options: {
+59 -27
View File
@@ -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.
[![Dependency Status](https://gemnasium.com/gmetais/YellowLabTools.svg)](https://gemnasium.com/gmetais/YellowLabTools) [![Build Status](https://travis-ci.org/gmetais/YellowLabTools.svg?branch=master)](https://travis-ci.org/gmetais/YellowLabTools)
Analyzes a webpage and detects **performance** or **front-end code quality** issues. Easy to launch, easy to understand, in-depth details.
### Access the tool here: [http://yellowlab.tools](http://yellowlab.tools)
#
<img src="./doc/img/commandment.png" alt="No website should go live without being tested with Yellow Lab Tools"></img>
<table>
<tr>
<td width="70%">
The <b>online tool</b> (recommended):
<a href="http://yellowlab.tools" target="_blank">http://yellowlab.tools</a>
</td>
<td width="30%">
<img src="./doc/img/YLT-animated.gif"></img>
</td>
</tr>
<tr>
<td width="70%">
The <b>CLI</b> (Command Line Interface) - <a href="https://github.com/gmetais/YellowLabTools/wiki/Command-Line-Interface" target="_blank">Doc here</a>
</td>
<td width="30%">
<img src="./doc/img/YLT-cli-animated.gif"></img>
</td>
</tr>
<tr>
<td width="70%">
The <b>Grunt task</b>: <a href="https://github.com/gmetais/grunt-yellowlabtools" traget="_blank">gmetais/grunt-yellowlabtools</a>
<br>For developers or Continuous Integration
</td>
<td width="30%">
<img src="./doc/img/grunt-logo.png"></img>
</td>
</tr>
<tr>
<td width="70%">
The <b>NodeJS</b> package - <a href="https://github.com/gmetais/YellowLabTools/wiki/NodeJS-module" target="_blank">Doc here</a>
</td>
<td width="30%">
<img src="./doc/img/npm-logo.png"></img>
</td>
</tr>
<tr>
<td width="70%">
The <b>public API</b> - <a href="https://github.com/gmetais/YellowLabTools/wiki/Public-API" target="_blank">Doc here</a>
<br>
Allows you to launch runs from any language
</td>
<td width="30%">
<img src="./doc/img/api-logo.png"></img>
</td>
</tr>
</table>
![example dashboard screenshot](screenshot.png)
## 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!!!
![example dashboard screenshot](./doc/img/screenshot.png)
## 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,16 @@ 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
## Help needed!
There are so many things left to do, **your help would be greatly appreciated**! Please report bugs, ask for evolutions and come code with me.
## License
Please read the [license](LICENSE).
Icons are the property of [IcoMoon.io](https://icomoon.io/)
Please read the [license](LICENSE). Icons are the property of [IcoMoon.io](https://icomoon.io/)
## Author
Gaël Métais. I'm a webperf freelance based in Paris. If you understand french, you can visit [my website](http://www.gaelmetais.com).
Gaël Métais. I'm a webperf freelance based in Paris. If you understand French, you can visit [my website](http://www.gaelmetais.com).
## Contributors
+19 -5
View File
@@ -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 jstoxml = require('jstoxml');
var ylt = require('../lib/index');
var ylt = require('../lib/index');
var cli = meow({
help: [
@@ -19,6 +20,7 @@ var cli = meow({
' --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'
@@ -67,6 +69,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 +84,13 @@ options.authPass = cli.flags.authPass || null;
then(function(data) {
debug('Success');
console.log(JSON.stringify(data, null, 2));
switch(cli.flags.reporter) {
case 'xml':
console.log(jstoxml.toXML(data, {indent: ' '}));
break;
default:
console.log(JSON.stringify(data, null, 2));
}
}).fail(function(err) {
+2
View File
@@ -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);
-14
View File
@@ -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"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 B

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 96 KiB

+7 -3
View File
@@ -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 {
File diff suppressed because one or more lines are too long
+3 -1
View File
@@ -33,7 +33,8 @@
.settings select {
font-size: 1em;
}
.settings input[type=text] {
.settings input[type=text],
.settings input[type=password] {
width: 100%;
min-width: 4em;
}
@@ -128,6 +129,7 @@
.advanced > div > div.label {
width: 25%;
white-space: nowrap;
vertical-align: middle;
}
.advanced .subTable {
display: table;
File diff suppressed because one or more lines are too long
+61 -5
View File
@@ -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;
@@ -164,16 +171,18 @@
font-style: italic;
font-weight: normal;
}
.colorPalette {
width: 30em;
border: 2px solid #000;
text-align: left;
.checker {
/* Checkerboard background */
background-color: #ddd;
background-image: linear-gradient(45deg, #aaaaaa 25%, transparent 25%, transparent 75%, #aaaaaa 75%, #aaaaaa), linear-gradient(45deg, #aaaaaa 25%, transparent 25%, transparent 75%, #aaaaaa 75%, #aaaaaa);
background-size: 1em 1em;
background-position: 0 0, 0.5em 0.5em;
}
.colorPalette {
width: 30em;
border: 2px solid #000;
text-align: left;
}
.colorPalette > div {
display: inline-block;
height: 2em;
@@ -205,3 +214,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;
}
+28 -4
View File
@@ -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;
@@ -306,9 +312,27 @@
color: #e74c3c;
cursor: pointer;
}
.warningsFilterOn > div {
.queryWithoutResultsFilterOn > div {
display: none;
}
.warningsFilterOn > div.warning {
.queryWithoutResultsFilterOn > div.queryWithoutResults {
display: table-row;
}
.jQueryCallOnEmptyObjectFilterOn > div {
display: none;
}
.jQueryCallOnEmptyObjectFilterOn > div.jQueryCallOnEmptyObject {
display: table-row;
}
.eventNotDelegatedFilterOn > div {
display: none;
}
.eventNotDelegatedFilterOn > div.eventNotDelegated {
display: table-row;
}
.jsErrorFilterOn > div {
display: none;
}
.jsErrorFilterOn > div.jsError {
display: table-row;
}
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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

+17
View File
@@ -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',
+5 -8
View File
@@ -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) {
+27 -1
View File
@@ -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,23 @@ ruleCtrl.controller('RuleCtrl', ['$scope', '$rootScope', '$routeParams', '$locat
function init() {
$scope.rule = $scope.result.rules[$scope.policyName];
// Init "Total Weight" chart
if ($scope.policyName === 'totalWeight') {
$scope.weightLabels = [];
$scope.weightColours = ['#7ECCCC', '#A7E846', '#FF944D', '#FFE74A', '#C2A3FF', '#5A9AED', '#FF6452', '#C1C1C1'];
$scope.weightData = [];
var types = ['html', 'css', 'js', 'json', 'image', 'video', 'webfont', 'other'];
types.forEach(function(type) {
$scope.weightLabels.push(type);
$scope.weightData.push(Math.round($scope.rule.offendersObj.list.byType[type].totalWeight / 1024));
});
$scope.weightOptions = {
tooltipTemplate: '<%=label%>: <%=value%> KB'
};
}
}
$scope.backToDashboard = function() {
+24 -6
View File
@@ -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);
+71 -4
View File
@@ -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';
};
});
})();
+1 -1
View File
@@ -14,7 +14,7 @@ apiService.factory('API', ['$location', 'Runs', 'Results', function($location, R
waitForSelector: settings.waitForSelector,
cookie: settings.cookie,
authUser: settings.authUser,
authPass: settings.authPass,
authPass: settings.authPass
};
if (settings.waitForSelector && settings.waitForSelector !== '') {
+5 -1
View File
@@ -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 {
File diff suppressed because one or more lines are too long
+2 -1
View File
@@ -38,7 +38,7 @@
font-size: 1em;
}
input[type=text] {
input[type=text], input[type=password] {
width: 100%;
min-width: 4em;
}
@@ -147,6 +147,7 @@
&.label {
width: 25%;
white-space: nowrap;
vertical-align: middle;
}
}
}
+8 -9
View File
@@ -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 {
+74 -6
View File
@@ -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;
}
@@ -176,16 +185,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 +232,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;
}
+35 -4
View File
@@ -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;
}
}
+11 -7
View File
@@ -16,15 +16,18 @@
<link rel="stylesheet" type="text/css" href="/css/screenshot.css">
<link rel="stylesheet" type="text/css" href="/css/timeline.css">
<link rel="stylesheet" type="text/css" href="/css/about.css">
<link rel="stylesheet" type="text/css" href="/node_modules/angular-chart.js/dist/angular-chart.css">
<!-- endbuild -->
<!-- build:js /js/all.js -->
<script src="/bower_components/angular/angular.min.js"></script>
<script src="/bower_components/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>
@@ -47,8 +50,9 @@
<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>&#9733;</span> on GitHub</a>!<br>
<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>
<script>
+6 -5
View File
@@ -28,27 +28,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">
+4 -4
View File
@@ -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,10 +57,10 @@
</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>
<div class="label">Blocked domains</div>
<div>
@@ -90,4 +90,4 @@
<h3>JS Profiling</h3>
<p>Untangles the JavaScript spaghetti code</p>
</div>
</div>
</div>
+159 -41
View File
@@ -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,10 @@
</span>
</div>
<div ng-if="policyName === 'similarColors'">
<div class="similarColors checker"><div ng-style="{'background-color': offender.color1, 'color': offender.isDark ? '#FFF' : '#000'}">{{offender.color1}}</div><div ng-style="{'background-color': offender.color2, 'color': offender.isDark ? '#FFF' : '#000'}">{{offender.color2}}</div></div>
</div>
<div ng-if="policyName === 'cssParsingErrors'">
<b>{{offender.error}}</b>
<file-and-line file="offender.file" line="offender.line" column="offender.column"></file-and-line>
@@ -160,20 +149,15 @@
<file-and-line-button file="offender.file" line="offender.line" column="offender.column"></file-and-line-button>
</div>
<div ng-if="policyName === 'requests' || policyName === 'htmlCount' || policyName === 'jsCount' || policyName === 'cssCount' || policyName === 'imageCount' || policyName === 'webfontCount' || policyName === 'videoCount' || policyName === 'jsonCount' || policyName === 'otherCount' || policyName === 'smallJsFiles' || policyName === 'smallCssFiles' || policyName === 'smallImages'">
<url-link url="offender.file" max-length="100"></url-link>
<span ng-if="offender.size || offender.size === 0">({{offender.size}} kB)</span>
<div ng-if="policyName === 'lazyLoadableImagesBelowTheFold' || 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 +176,6 @@
</div>
</div>
<div ng-if="!rule.offendersObj.list" class="offendersHtml">
<div ng-if="policyName === 'DOMelementMaxDepth'">
@@ -214,14 +197,149 @@
<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="!rule && rule !== null" class="notFound">
<h2>404</h2>
Rule "{{policyName}}"" not found
+30 -11
View File
@@ -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>
+145 -379
View File
@@ -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 {
@@ -389,17 +292,17 @@ var policies = {
score = 70;
} else if (value.indexOf('1.8.') === 0) {
score = 50;
} else if (value.indexOf('1.7.') === 0) {
} else if (value.indexOf('1.7') === 0) {
score = 40;
} else if (value.indexOf('1.6.') === 0) {
} else if (value.indexOf('1.6') === 0) {
score = 30;
} else if (value.indexOf('1.5.') === 0) {
} else if (value.indexOf('1.5') === 0) {
score = 20;
} else if (value.indexOf('1.4.') === 0) {
} else if (value.indexOf('1.4') === 0) {
score = 10;
} else if (value.indexOf('1.3.') === 0) {
} else if (value.indexOf('1.3') === 0) {
score = 0;
} else if (value.indexOf('1.2.') === 0) {
} else if (value.indexOf('1.2') === 0) {
score = 0;
} else {
debug('Unknown jQuery version "%s"', value);
@@ -433,12 +336,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 +358,7 @@ var policies = {
"isOkThreshold": 1,
"isBadThreshold": 100,
"isAbnormalThreshold": 180,
"hasOffenders": true
"hasOffenders": false
},
"cssParsingErrors": {
"tool": "phantomas",
@@ -454,7 +366,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 {
@@ -551,7 +463,7 @@ var policies = {
},
"cssColors": {
"tool": "phantomas",
"label": "Different colors",
"label": "Colors count",
"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>",
"isOkThreshold": 30,
"isBadThreshold": 150,
@@ -595,6 +507,21 @@ 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
};
}
},
"cssImports": {
"tool": "phantomas",
"label": "Uses of @import",
@@ -931,211 +858,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 mesures the number of bytes that could be saved by optimizing images.</p><p>Image optimization is generally one of the easiest way to reduce a page weight, and as a result, the page load time. Don't use Photoshop or other image editing tools, they're not very good for optimization. Use specialized tools such as <a href=\"https://kraken.io/\" target=\"_blank\">Kraken.io</a> or the excellent <a href=\"https://imageoptim.com/\" target=\"_blank\">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>Mesures the number of bytes that could be saved by compressing file transfers.</p><p>Gzip is a powerfull weight reducer and should be enabled on text-based assets in your server's configuration. Note that gzipping small files (< 1 KB) is arguable, and that some assets such as images should not be gzipped as they are already compressed. <a href=\"https://gist.github.com/gmetais/971ce13a1fbeebd88445\" target=\"_blank\">Here</a> is a list of Content-Types that should be gzipped.</p>",
"isOkThreshold": 5125,
"isBadThreshold": 81920,
"isAbnormalThreshold": 153600,
"hasOffenders": true,
"unit": 'bytes'
},
"fileMinification": {
"tool": "weightChecker",
"label": "File minification",
"message": "<p>This is the weight that could be saved if all text resources were correctly minified.</p><p>The tools in use here are <b>UglifyJS</b>, <b>clean-css</b> and <b>HTMLMinifier</b>. These tools are so good that some of your minified files can be marked as unminified. Change your tool it this happens :)</p><p>The gains of minification are generally small, but the impact can be high when these text files are loaded on the critical path.</p>",
"isOkThreshold": 5125,
"isBadThreshold": 61440,
"isAbnormalThreshold": 122880,
"hasOffenders": true,
"unit": 'bytes'
},
"totalRequests": {
"tool": "weightChecker",
"label": "Requests number",
"message": "<p>This is one of the most important performance rule. Every request is slowing down the page loading.</p><p>There are several technics to reduce their number:<ul><li>Concatenate JS files</li><li>Concatenate CSS files</li><li>Embed or inline small JS or CSS files in the HTML</li><li>Create sprites or icon fonts</li><li>Base64 encode small images in HTML or stylesheets</li><li>Use lazyloading for images</li></ul></p>",
"isOkThreshold": 15,
"isBadThreshold": 100,
"isAbnormalThreshold": 200,
"hasOffenders": true,
"takeOffendersFrom": ["htmlCount", "jsCount", "cssCount", "imageCount", "webfontCount", "videoCount", "jsonCount", "jsonCount"],
"offendersTransformFn": function(offenders) {
return {
count: offenders.length,
list: offenders
.map(function(offender) {
return offendersHelpers.fileWithSizePattern(offender);
}).sort(function(a, b) {
return b.size - a.size;
})
};
}
"isAbnormalThreshold": 180,
"hasOffenders": true
},
"htmlCount": {
"domains": {
"tool": "phantomas",
"label": "Document count",
"message": "<p>The number of HTML pages requests, HTML fragments or iframes.</p>",
"label": "Different domains",
"message": "<p>For each domain met, the browser needs to make a DNS look-up, which is slow. Avoid having to many different domains and the page should render faster.</p><p>By the way, domain sharding is not a good practice anymore.</p>",
"isOkThreshold": 10,
"isBadThreshold": 20,
"isAbnormalThreshold": 30,
"isBadThreshold": 25,
"isAbnormalThreshold": 50,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return {
count: offenders.length,
list: offenders.map(function(offender) {
return offendersHelpers.fileWithSizePattern(offender);
})
};
}
},
"jsCount": {
"tool": "phantomas",
"label": "Script count",
"message": "<p>Reduce the number of scripts by concatenating them.</p>",
"isOkThreshold": 5,
"isBadThreshold": 15,
"isAbnormalThreshold": 30,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return {
count: offenders.length,
list: offenders.map(function(offender) {
return offendersHelpers.fileWithSizePattern(offender);
})
};
}
},
"cssCount": {
"tool": "phantomas",
"label": "CSS count",
"message": "<p>Reduce the number of stylesheets by concatenating them.</p>",
"isOkThreshold": 3,
"isBadThreshold": 10,
"isAbnormalThreshold": 22,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return {
count: offenders.length,
list: offenders.map(function(offender) {
return offendersHelpers.fileWithSizePattern(offender);
})
};
}
},
"imageCount": {
"tool": "phantomas",
"label": "Image count",
"message": "<p>Reduce the number of images by lazyloading them, by spriting them or by creating an icons font.</p>",
"isOkThreshold": 15,
"isBadThreshold": 40,
"isAbnormalThreshold": 70,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return {
count: offenders.length,
list: offenders.map(function(offender) {
return offendersHelpers.fileWithSizePattern(offender);
})
};
}
},
"webfontCount": {
"tool": "phantomas",
"label": "Font count",
"message": "<p>Fonts are loaded on the critical path of the head. Load as few as possible.</p>",
"isOkThreshold": 0,
"isBadThreshold": 3,
"isAbnormalThreshold": 5,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return {
count: offenders.length,
list: offenders.map(function(offender) {
return offendersHelpers.fileWithSizePattern(offender);
})
};
}
},
"videoCount": {
"tool": "phantomas",
"label": "Video count",
"message": "<p>The number of videos loaded.</p>",
"isOkThreshold": 1,
"isBadThreshold": 5,
"isAbnormalThreshold": 15,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return {
count: offenders.length,
list: offenders.map(function(offender) {
return offendersHelpers.fileWithSizePattern(offender);
})
};
}
},
"jsonCount": {
"tool": "phantomas",
"label": "JSON count",
"message": "<p>The number of AJAX requests to JSON files or webservices.</p>",
"isOkThreshold": 2,
"isBadThreshold": 10,
"isAbnormalThreshold": 25,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return {
count: offenders.length,
list: offenders.map(function(offender) {
return offendersHelpers.fileWithSizePattern(offender);
})
};
}
},
"otherCount": {
"tool": "phantomas",
"label": "Other types of requests",
"message": "<p>They can be Flash, XML, music or any unknown format.</p>",
"isOkThreshold": 5,
"isBadThreshold": 20,
"isAbnormalThreshold": 40,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return {
count: offenders.length,
list: offenders.map(function(offender) {
return offendersHelpers.fileWithSizePattern(offender);
})
};
}
},
"smallJsFiles": {
"tool": "phantomas",
"label": "Small JS files",
"message": "<p>Number of JS assets smaller than 2 KB that could probably be inlined or merged.</p>",
"isOkThreshold": 2,
"isBadThreshold": 10,
"isAbnormalThreshold": 16,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return {
count: offenders.length,
list: offenders.map(function(offender) {
return offendersHelpers.fileWithSizePattern(offender);
})
};
}
},
"smallCssFiles": {
"tool": "phantomas",
"label": "Small CSS files",
"message": "<p>Number of CSS assets smaller than 2 KB that could probably be inlined or merged.</p>",
"isOkThreshold": 0,
"isBadThreshold": 8,
"isAbnormalThreshold": 12,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return {
count: offenders.length,
list: offenders.map(function(offender) {
return offendersHelpers.fileWithSizePattern(offender);
})
};
}
},
"smallImages": {
"tool": "phantomas",
"label": "Small images",
"message": "<p>Images smaller than 2 KB that could be base64 encoded or merged into a sprite.</p>",
"isOkThreshold": 2,
"isBadThreshold": 17,
"isAbnormalThreshold": 30,
"hasOffenders": true,
"offendersTransformFn": function(offenders) {
return {
count: offenders.length,
list: offenders.map(function(offender) {
return offendersHelpers.fileWithSizePattern(offender);
})
var parts = /^([^ ]*): (\d+) request\(s\)$/.exec(offender);
if (!parts) {
debug('domains offenders transform function error with "%s"', offender);
return {
file: offender
};
}
return {
domain: parts[1],
requests: parseInt(parts[2])
};
})
};
}
},
@@ -1148,41 +945,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 +963,33 @@ 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
},
"cachingDisabled": {
"tool": "phantomas",
"label": "Caching disabled",
@@ -1271,35 +1066,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])
};
})
};
}
}
};
+37 -42
View File
@@ -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,8 @@
"cssRules": 2,
"cssComplexSelectors": 2,
"cssComplexSelectorsByAttribute": 1.5,
"cssColors": 0.5
"cssColors": 0.5,
"similarColors": 0.5
}
},
"badCSS": {
@@ -70,44 +91,20 @@
"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,
"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 +112,6 @@
"cssSyntaxError": 1,
"cssComplexity": 1,
"badCSS": 1,
"requests": 3,
"smallRequests": 1,
"network": 2
"serverConfig": 1
}
}
+4 -1
View File
@@ -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 {
+16 -7
View File
@@ -3,6 +3,8 @@ var debug = require('debug')('ylt:runner');
var phantomasWrapper = require('./tools/phantomas/phantomasWrapper');
var jsExecutionTransformer = require('./tools/jsExecutionTransformer');
var colorDiff = require('./tools/colorDiff');
var weightChecker = require('./tools/weightChecker/weightChecker');
var rulesChecker = require('./rulesChecker');
var scoreCalculator = require('./scoreCalculator');
@@ -19,14 +21,23 @@ 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);
// Redownload every file
return weightChecker.recheckAllFiles(data);
})
.then(function(data) {
// Rules checker
var policies = require('./metadata/policies');
@@ -45,14 +56,12 @@ var Runner = function(params) {
delete data.toolsResults.phantomas.metrics.scrollExecutionTree;
delete data.toolsResults.phantomas.offenders.scrollExecutionTree;
return data;
}).then(function(data) {
// Finished!
deferred.resolve(data);
}).fail(function(err) {
})
.fail(function(err) {
debug('Run failed');
debug(err);
+35
View File
@@ -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();
+3 -2
View File
@@ -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) {
@@ -52,7 +53,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);
@@ -76,7 +76,8 @@ var ApiController = function(app) {
waitForSelector: run.params.waitForSelector,
cookie: run.params.cookie,
authUser: run.params.authUser,
authPass: run.params.authPass
authPass: run.params.authPass,
phantomasEngine: serverSettings.phantomasEngine
};
return ylt(run.params.url, runOptions);
+1 -1
View File
@@ -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;
+6
View File
@@ -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;
+90
View File
@@ -0,0 +1,90 @@
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) {
deduplicatedColors[color] = color;
});
return Object.keys(deduplicatedColors).map(this.parseColor);
};
this.parseOffender = function(offender) {
var regexResult = /^(.*) \(\d+ times\)$/.exec(offender);
return regexResult[1];
};
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();
+31 -29
View File
@@ -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
@@ -120,9 +122,9 @@ var jsExecutionTransformer = function() {
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) {
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) {
@@ -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));
});
@@ -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 + ')');
}
});
@@ -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));
});
};
+32 -13
View File
@@ -17,10 +17,11 @@ var PhantomasWrapper = function() {
var deferred = Q.defer();
var task = data.params;
var options = {
// Cusomizable options
'timeout': task.options.timeout || 45,
'engine': task.options.phantomasEngine || 'webkit',
'timeout': task.options.timeout || 30,
'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,
'tablet': (task.options.device === 'tablet'),
@@ -66,7 +67,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,35 +76,53 @@ 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;
async.retry(triesNumber, function(cb) {
var process = phantomas(task.url, options, function(err, json, results) {
debug('Returning from Phantomas');
if (isKilled) {
debug('Process was killed, too late Phantomas, sorry...');
return;
}
debug('Returning from Phantomas with error %s', err);
// Adding some YellowLabTools errors here
if (json && json.metrics && (!json.metrics.javascriptExecutionTree || !json.offenders.javascriptExecutionTree)) {
+343
View File
@@ -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();
+102
View File
@@ -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();
+245
View File
@@ -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();
+528
View File
@@ -0,0 +1,528 @@
/*
* 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;
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);
});
}
return {
recheckAllFiles: recheckAllFiles,
listRequestWeight: listRequestWeight,
redownloadEntry: redownloadEntry,
download: download
};
};
module.exports = new WeightChecker();
+69 -39
View File
@@ -1,7 +1,13 @@
{
"name": "yellowlabtools",
"version": "1.5.0",
"version": "1.8.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"
@@ -11,48 +17,72 @@
},
"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",
"angular": "1.4.5",
"angular-animate": "1.4.5",
"angular-chart.js": "0.8.4",
"angular-local-storage": "0.2.2",
"angular-resource": "1.4.5",
"angular-route": "1.4.5",
"angular-sanitize": "1.4.5",
"async": "1.4.2",
"body-parser": "1.14.0",
"chart.js": "1.0.2",
"clean-css": "3.4.3",
"color-diff": "0.1.7",
"compression": "1.5.2",
"cors": "2.7.1",
"debug": "2.2.0",
"express": "4.13.3",
"imagemin": "3.2.0",
"imagemin-jpegoptim": "4.0.0",
"jstoxml": "0.2.3",
"lwip": "0.0.7",
"meow": "3.3.0",
"minimize": "1.7.1",
"parse-color": "1.0.0",
"phantomas": "1.12.0",
"ps-node": "0.0.4",
"q": "~1.4.0",
"rimraf": "~2.3.3",
"temporary": "0.0.8"
"q": "1.4.1",
"request": "2.62.0",
"rimraf": "2.4.3",
"temporary": "0.0.8",
"uglify-js": "2.4.24"
},
"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.2.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.1",
"grunt-contrib-cssmin": "~0.14.0",
"grunt-contrib-htmlmin": "~0.4.0",
"grunt-contrib-jshint": "~0.11.3",
"grunt-contrib-less": "~1.0.1",
"grunt-contrib-uglify": "~0.9.2",
"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": "~0.5.4",
"matchdep": "~0.3.0",
"mocha": "~2.3.2",
"sinon": "~1.16.1",
"sinon-chai": "~2.8.0"
},
"scripts": {
"test": "grunt test"
}
},
"keywords": [
"performance",
"metrics",
"webperf",
"pagespeed",
"budget",
"phantomas"
]
}
+3 -4
View File
@@ -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 -4
View File
@@ -10,11 +10,9 @@ 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
+1
View File
@@ -1,5 +1,6 @@
{
"serverPort": 80,
"phantomasEngine": "webkit",
"googleAnalyticsId": "",
"authorizedKeys": {
+1
View File
@@ -1,5 +1,6 @@
{
"serverPort": 8383,
"phantomasEngine": "webkit",
"googleAnalyticsId": "",
"authorizedKeys": {
+92
View File
@@ -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 -181
View File
@@ -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": {
+244
View File
@@ -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);
});
});
+583
View File
@@ -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);
});
});
});
+321
View File
@@ -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);
});
});
+11 -3
View File
@@ -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);
});
});
+11
View File
@@ -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() {
+3 -1
View File
@@ -42,7 +42,9 @@ describe('phantomasWrapper', function() {
phantomasWrapper.execute({
params: {
url: url,
options: {}
options: {
device: 'desktop'
}
}
}).then(function(data) {
+352
View File
@@ -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
View File
@@ -1,5 +1,6 @@
{
"serverPort": "8387",
"phantomasEngine": "webkit",
"googleAnalyticsId": "",
"authorizedKeys": {
Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

+14
View File
@@ -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)})}}}])
Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

+4
View File
@@ -4,5 +4,9 @@
</head>
<body>
<h1>Simple page</h1>
<script>
document.getElementById('foo');
</script>
</body>
</html>
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 84 KiB

+190
View File
@@ -0,0 +1,190 @@
var timelineCtrl = angular.module('timelineCtrl', []);
timelineCtrl.controller('TimelineCtrl', ['$scope', '$rootScope', '$routeParams', '$location', '$timeout', 'Menu', 'Results', 'API', function($scope, $rootScope, $routeParams, $location, $timeout, Menu, Results, API) {
$scope.runId = $routeParams.runId;
$scope.Menu = Menu.setCurrentPage('timeline', $scope.runId);
function loadResults() {
// Load result if needed
if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) {
Results.get({runId: $routeParams.runId, exclude: 'toolsResults'}, function(result) {
$rootScope.loadedResult = result;
$scope.result = result;
render();
});
} else {
$scope.result = $rootScope.loadedResult;
render();
}
}
function render() {
initFilters();
initScriptFiltering();
initExecutionTree();
initTimeline();
$timeout(initProfiler, 100);
}
function initFilters() {
var hash = $location.hash();
var filter = null;
if (hash.indexOf('filter=') === 0) {
filter = hash.substr(7);
}
$scope.warningsFilterOn = (filter !== null);
$scope.warningsFilters = {
queryWithoutResults: (filter === null || filter === 'queryWithoutResults'),
jQueryCallOnEmptyObject: (filter === null || filter === 'jQueryCallOnEmptyObject'),
eventNotDelegated: (filter === null || filter === 'eventNotDelegated'),
jsError: (filter === null || filter === 'jsError')
};
}
function initScriptFiltering() {
var offenders = $scope.result.rules.jsCount.offendersObj.list;
$scope.scripts = [];
offenders.forEach(function(script) {
var filePath = script.file;
if (filePath.length > 100) {
filePath = filePath.substr(0, 98) + '...';
}
var scriptObj = {
fullPath: script.file,
shortPath: filePath
};
$scope.scripts.push(scriptObj);
});
}
function initExecutionTree() {
var originalExecutions = $scope.result.javascriptExecutionTree.children || [];
// Detect the last event of all (before filtering) and read time
var lastEvent = originalExecutions[originalExecutions.length - 1];
$scope.endTime = lastEvent.data.timestamp + (lastEvent.data.time || 0);
// Filter
$scope.executionTree = [];
originalExecutions.forEach(function(node) {
// Filter by script (if enabled)
if ($scope.selectedScript) {
if (node.data.backtrace && node.data.backtrace.indexOf($scope.selectedScript.fullPath + ':') === -1) {
return;
}
if (node.data.type === "jQuery loaded" || node.data.type === "jQuery version change") {
return;
}
}
$scope.executionTree.push(node);
});
}
function initTimeline() {
// Split the timeline into 200 intervals
var numberOfIntervals = 199;
$scope.timelineIntervalDuration = $scope.endTime / numberOfIntervals;
// Pre-fill array of as many elements as there are milleseconds
var millisecondsArray = Array.apply(null, new Array($scope.endTime + 1)).map(Number.prototype.valueOf,0);
// Create the milliseconds array from the execution tree
$scope.executionTree.forEach(function(node) {
if (node.data.time !== undefined) {
// Ignore artefacts (durations > 100ms)
var time = Math.min(node.data.time, 100) || 1;
for (var i=node.data.timestamp, max=node.data.timestamp + time ; i<max ; i++) {
millisecondsArray[i] |= 1;
}
}
});
// Pre-fill array of 200 elements
$scope.timeline = Array.apply(null, new Array(numberOfIntervals + 1)).map(Number.prototype.valueOf,0);
// Create the timeline from the milliseconds array
millisecondsArray.forEach(function(value, timestamp) {
if (value === 1) {
$scope.timeline[Math.floor(timestamp / $scope.timelineIntervalDuration)] += 1;
}
});
// Get the maximum value of the array (needed for display)
$scope.timelineMax = Math.max.apply(Math, $scope.timeline);
}
function initProfiler() {
$scope.profilerData = $scope.executionTree;
}
$scope.changeScript = function() {
initExecutionTree();
initTimeline();
initProfiler();
};
$scope.findLineIndexByTimestamp = function(timestamp) {
var lineIndex = 0;
for (var i = 0; i < $scope.executionTree.length; i ++) {
var delta = $scope.executionTree[i].data.timestamp - timestamp;
if (delta < $scope.timelineIntervalDuration) {
lineIndex = i;
}
if (delta > 0) {
break;
}
}
return lineIndex;
};
$scope.backToDashboard = function() {
$location.path('/result/' + $scope.runId);
};
$scope.testAgain = function() {
API.relaunchTest($scope.result);
};
loadResults();
}]);
timelineCtrl.directive('scrollOnClick', ['$animate', '$timeout', function($animate, $timeout) {
return {
restrict: 'A',
link: function (scope, element, attributes) {
// When the user clicks on the timeline, find the right profiler line and scroll to it
element.on('click', function() {
var lineIndex = scope.findLineIndexByTimestamp(attributes.scrollOnClick);
var lineElement = angular.element(document.getElementById('line_' + lineIndex));
// Animate the background color to "flash" the row
lineElement.addClass('highlight');
$timeout(function() {
$animate.removeClass(lineElement, 'highlight');
scope.$digest();
}, 50);
window.scrollTo(0, lineElement[0].offsetTop);
});
}
};
}]);
+772
View File
@@ -0,0 +1,772 @@
/* Timeline colors, related to Window Performances */
.execution {
text-align: center;
}
.selectScript {
padding-bottom: 2em;
font-size: 0.9em;
}
.selectScript select {
max-width: 30em;
}
.selectScript.empty {
font-size: 0.8em;
}
.selectScript.empty select {
width: 10em;
}
.timeline {
margin: 2em 0 5em;
}
.timeline .chart {
position: relative;
width: 100%;
border-bottom: 1px solid #000;
}
.timeline .startTime,
.timeline .endTime {
position: absolute;
bottom: 0.5em;
font-size: 0.8em;
}
.timeline .startTime {
left: 0em;
}
.timeline .endTime {
right: 0em;
}
.timeline .chartPoints {
display: table;
height: 100px;
width: 99%;
margin: 0 auto;
}
.timeline .interval {
display: table-cell;
position: relative;
height: 100px;
width: 0.5%;
}
.timeline .interval .color {
position: absolute;
bottom: 0;
width: 100%;
}
.timeline .interval .color.clickable {
cursor: pointer;
}
.timeline div.interval:hover {
background: #9C4274;
}
.timeline .interval:hover .color {
background: #F04DA7;
}
.timeline .domComplete.interval {
background: #ede3ff;
}
.timeline .domComplete .color {
background: #c2a3ff;
}
.timeline .domContentLoadedEnd.interval {
background: #d8f0f0;
}
.timeline .domContentLoadedEnd .color {
background: #7ecccc;
}
.timeline .domContentLoaded.interval {
background: #e0ffd1;
}
.timeline .domContentLoaded .color {
background: #a7e846;
}
.timeline .domInteractive.interval {
background: #fffccc;
}
.timeline .domInteractive .color {
background: #ffe433;
}
.timeline .domCreation.interval {
background: #ffe0cc;
}
.timeline .domCreation .color {
background: #ff6600;
}
.timeline .tooltip.detailsOverlay {
position: absolute;
display: none;
width: auto;
padding: 0.5em 1em;
top: -1.5em;
right: 1em;
}
.timeline .interval:hover .tooltip {
display: block;
}
.timeline .legend {
display: table;
width: 100%;
margin-top: 1em;
}
.timeline .legend > div {
display: table-row;
}
.timeline .legend > div > div {
position: relative;
display: table-cell;
margin-top: 1em;
}
.timeline .titles {
font-weight: bold;
}
.timeline .titles > div {
padding: 0 1em 0 2em;
}
.timeline .tips {
font-size: 0.7em;
}
.timeline .tips > div {
padding: 1em 1em 0 0;
}
.timeline .legend .color {
display: block;
position: absolute;
left: 0;
height: 1.5em;
width: 1.5em;
border-radius: 0.2em;
}
.filters {
margin: 1em auto;
padding: 0.5em;
min-width: 30em;
width: 30%;
border: 1px dotted #aaa;
text-align: left;
}
.subFilters {
margin-left: 3em;
font-size: 0.9em;
}
.table {
display: table;
width: 100%;
border-spacing: 0.25em;
}
.table > div {
display: table-row;
}
.table > .headers > div {
font-weight: bold;
padding: 0.5em 1em;
}
.table > div > div {
padding: 0.1em 1em;
background: #f2f2f2;
display: table-cell;
text-align: left;
}
.table > div.jsError > .type,
.table > div.jsError > .value {
color: #e74c3c;
font-weight: bold;
}
.table > div.windowPerformance > div,
.table > div.windowPerformance > div.startTime {
background: #EBD8E2;
}
.table > div.showingDetails > div {
background: #f1c40f;
}
.table > div.highlight > div.startTime {
background-color: #C0F090;
}
.table > div.highlight-remove {
transition: 3s;
}
.table > div.highlight-remove > div.startTime {
transition: background-color 3s ease-in;
}
.table > div > .index {
color: #bbb;
word-break: normal;
}
.table > div > .type {
white-space: nowrap;
}
.table .children {
margin-top: 0.2em;
font-size: 0.8em;
line-height: 1.6em;
}
.table .child {
margin-left: 0.5em;
}
.table .child > .child {
margin-left: 1em;
}
.table .child:before {
content: "↳";
}
.table .child .childArgs {
display: none;
}
.table .child span {
position: relative;
}
.table .child span:hover {
background: #EBD8E2;
}
.table .child span:hover div {
display: inline-block;
}
.table .child span:hover .childArgs {
display: block;
position: absolute;
padding: 0 1em 0 2em;
left: 100%;
top: 0;
background: #EBD8E2;
line-height: 1.3em;
height: 1.3em;
z-index: 2;
}
.table .showingDetails .child span:hover {
background: inherit;
}
.table .showingDetails .child span:hover .childArgs {
display: none;
}
.table > div > .value {
width: 70%;
word-break: break-all;
}
.table > div > .details {
position: relative;
}
.table .details .icon-question {
color: #f1c40f;
cursor: pointer;
}
.table .icon-warning {
display: inline-block;
width: 0.8em;
}
.detailsOverlay {
display: none;
position: absolute;
right: 3em;
top: -3em;
width: 45em;
min-height: 1em;
padding: 0 1em 1em;
background: #fff;
border: 2px solid #f1c40f;
border-radius: 0.5em;
z-index: 2;
}
@media screen and (max-width: 1024px) {
.detailsOverlay {
width: 25em;
}
}
.showDetails .detailsOverlay {
display: block;
}
.detailsOverlay .closeBtn {
position: absolute;
top: 0.5em;
right: 0.5em;
color: #f1c40f;
cursor: pointer;
}
.detailsOverlay .advice {
color: #e74c3c;
font-weight: bold;
}
.detailsOverlay .trace {
word-break: break-all;
}
.table > div > .duration,
.table > div > .startTime {
text-align: center;
white-space: nowrap;
}
.table > div > .startTime.domComplete {
background: #ede3ff;
}
.table > div > .startTime.domContentLoadedEnd {
background: #d8f0f0;
}
.table > div > .startTime.domContentLoaded {
background: #e0ffd1;
}
.table > div > .startTime.domInteractive {
background: #fffccc;
}
.table > div > .startTime.domCreation {
background: #ffe0cc;
}
.execution .icon-warning {
color: #e74c3c;
cursor: pointer;
}
.queryWithoutResultsFilterOn > div {
display: none;
}
.queryWithoutResultsFilterOn > div.queryWithoutResults {
display: table-row;
}
.jQueryCallOnEmptyObjectFilterOn > div {
display: none;
}
.jQueryCallOnEmptyObjectFilterOn > div.jQueryCallOnEmptyObject {
display: table-row;
}
.eventNotDelegatedFilterOn > div {
display: none;
}
.eventNotDelegatedFilterOn > div.eventNotDelegated {
display: table-row;
}
.jsErrorFilterOn > div {
display: none;
}
.jsErrorFilterOn > div.jsError {
display: table-row;
}
.testedUrl {
color: inherit;
}
.summary {
text-align: center;
}
.summary .globalScore {
display: table;
width: 60%;
margin: 3em auto;
}
.summary .globalScore > div {
display: table-cell;
width: 50%;
vertical-align: middle;
}
.summary .globalScore .globalGrade {
margin: 0.5 auto;
width: 2.5em;
height: 2.5em;
line-height: 2.5em;
border-radius: 0.5em;
font-size: 3em;
font-weight: bold;
vertical-align: middle;
}
.summary .globalScore .on100 {
font-size: 1.2em;
font-weight: bold;
margin: 0.5em 0 1em;
}
.summary .globalScore .screenshotWrapper:hover {
opacity: 0.75;
}
.summary .globalScore .screenshotWrapper:hover:after {
position: absolute;
width: 1.25em;
height: 1.25em;
top: 0.7em;
left: 1.55em;
font-size: 3em;
color: #FFF;
background: #000;
border-radius: 0.2em;
text-align: center;
content: "+";
opacity: 0.85;
}
.summary .globalScore .screenshotWrapper.phone:hover:after {
top: 1.7em;
left: 0.64em;
}
.summary .globalScore .screenshotWrapper.tablet:hover:after {
top: 1.5em;
left: 0.9em;
}
.summary .notations {
display: table;
width: 80%;
margin: 0 10% 1.5em;
border-spacing: 1em;
}
.summary .notations > div {
display: table-row;
}
.summary .notations > div > div {
display: table-cell;
height: 2.5em;
vertical-align: middle;
}
.summary .notations .category {
font-weight: bold;
text-align: center;
width: 20%;
}
.summary .notations .criteria {
font-weight: normal;
width: 75%;
}
.summary .notations .A.categoryScore,
.summary .notations .B.categoryScore,
.summary .notations .C.categoryScore,
.summary .notations .D.categoryScore,
.summary .notations .E.categoryScore,
.summary .notations .F.categoryScore,
.summary .notations .NA.categoryScore {
width: 2.5em;
max-width: 2.5em;
min-width: 2.5em;
font-size: 2em;
text-align: center;
border-radius: 0.5em;
font-weight: bold;
}
.summary .notations .grade .A,
.summary .notations .grade .B,
.summary .notations .grade .C,
.summary .notations .grade .D,
.summary .notations .grade .E,
.summary .notations .grade .F,
.summary .notations .grade .NA {
width: 1em;
height: 1em;
font-size: 1em;
color: transparent;
margin: 0 auto;
border-radius: 0.5em;
}
.summary .notations .criteria .table {
width: 100%;
}
.summary .notations .criteria .table > div:hover > div {
background: #EBD8E2;
cursor: pointer;
}
.summary .notations .criteria .table > div:hover > div.info {
background: #FFF;
}
.summary .notations .criteria .table > div:hover > div.info .icon-question {
color: #EBD8E2;
}
.summary .notations .criteria .grade {
width: 10%;
padding-left: 0.5em;
padding-right: 0.5em;
vertical-align: middle;
}
.summary .notations .criteria .label {
width: 70%;
}
.summary .notations .criteria .result {
width: 18%;
font-weight: bold;
white-space: nowrap;
text-align: center;
vertical-align: middle;
}
.summary .notations .warning .label,
.summary .notations .warning .result,
.summary .notations .icon-warning {
color: #FF1919;
}
.summary .notations .criteria .info {
width: 2%;
text-align: center;
vertical-align: middle;
background: #FFF;
padding-left: 0.1em;
padding-right: 0.1em;
}
.summary .notations .criteria .icon-question {
color: transparent;
}
.summary .fromShare {
margin-bottom: 3em;
}
.summary .apiTip {
font-size: 0.8em;
margin-bottom: 4em;
color: #413;
}
.summary .apiTip a {
color: inherit;
}
.summary .tweet .tweetText {
color: #413;
background: #F2F2F2;
border: none;
width: 25em;
padding: 0.4em;
border-radius: 0.5em;
box-shadow: 0.05em 0.1em 0 0 #999;
}
.summary .tweet .tweetButton,
.summary .tweet .linkedinButton {
color: #413;
background: #F2F2F2;
margin-right: 0;
}
.summary .tweet .tweetButton:hover,
.summary .tweet .linkedinButton:hover {
color: #F2F2F2;
background: #e74c3c;
}
.summary .tweet input {
font-size: 0.9em;
}
.promess {
padding: 0em 2em;
margin-bottom: 0.5em;
font-weight: normal;
font-size: 1.2em;
}
.price {
padding: 0em 2em 3em;
margin-top: 0em;
font-size: 0.9em;
}
.url {
width: 50%;
}
.launchBtn {
background: #ffa319;
color: #fff;
}
.launchBtn:focus {
background: #e74c3c;
}
.launchBtn.disabled {
background: #deaca6;
}
.launchBtn.disabled:focus {
color: #ddd;
}
.settings {
width: 50%;
margin: 0 auto;
}
.settings input,
.settings select {
font-size: 1em;
}
.settings input[type=text] {
width: 100%;
min-width: 4em;
}
.device {
margin-top: 3em;
}
.device .item {
display: inline-block;
margin: 1em 0.75em;
width: 5.5em;
height: 5.5em;
color: #FFF;
border: 1px solid #FFF;
padding: 1px;
border-radius: 0.5em;
cursor: pointer;
text-decoration: none;
font-size: 0.8em;
}
.device .item.active {
color: #ffa319;
border: 2px solid #ffa319;
padding: 0;
}
.device .item:hover {
color: #ffa319;
}
.device .item > div {
margin: 0.2em 0 0.1em;
font-size: 3em;
}
.settingsTooltip {
position: relative;
}
.settingsTooltip span {
font-size: 0.8em;
vertical-align: text-top;
}
.settingsTooltip div {
display: none;
position: absolute;
padding: 0.5em;
width: 25em;
background: #FFF;
color: #000;
font-size: 0.8em;
border-radius: 1em;
border: 2px solid #ffa319;
white-space: normal;
z-index: 2;
}
.settingsTooltip:hover div {
display: block;
}
.showAdvanced {
display: inline-block;
margin-top: 2em;
color: #FFF;
text-decoration: none;
font-size: 0.9em;
}
.showAdvanced:hover {
color: #ffa319;
}
.currentSettings {
font-size: 0.9em;
}
.currentSettings span {
color: #ffa319;
}
.currentSettings span:after {
color: #FFF;
content: ",";
}
.currentSettings span:last-child:after {
content: "";
}
.advanced {
margin: 1em 0 0;
display: table;
width: 100%;
text-align: left;
border-spacing: 0.75em;
}
.advanced > div {
display: table-row;
}
.advanced > div > div {
display: table-cell;
width: 75%;
}
.advanced > div > div.label {
width: 25%;
white-space: nowrap;
}
.advanced .subTable {
display: table;
border-spacing: 0;
width: 100%;
}
.advanced .subTable > div {
display: table-row;
}
.advanced .subTable > div > div {
display: table-cell;
padding: 0 0 0.75em;
}
.features {
display: table;
width: 50%;
margin: 6em auto 0;
font-size: 0.9em;
color: #413;
}
.features > div {
width: 33.3%;
display: table-cell;
padding: 0 1.5em;
}
input[type=submit],
input.url {
padding: 0 0.5em;
margin: 0.5em;
font-size: 1.2em;
height: 2em;
border: 0 solid;
border-radius: 0.5em;
box-shadow: 0.1em 0.2em 0 0 #5e2846;
outline: none;
}
input[type=submit]:hover {
color: #ddd;
}
input[type=submit].clicked {
color: #ddd;
position: relative;
left: 0.1em;
top: 0.2em;
box-shadow: none;
}
/*
* Core Owl Carousel CSS File
* v1.3.2
*/
/* clearfix */
.owl-carousel .owl-wrapper:after {
content: ".";
display: block;
clear: both;
visibility: hidden;
line-height: 0;
height: 0;
}
/* display none until init */
.owl-carousel{
display: none;
position: relative;
width: 100%;
-ms-touch-action: pan-y;
}
.owl-carousel .owl-wrapper{
display: none;
position: relative;
-webkit-transform: translate3d(0px, 0px, 0px);
}
.owl-carousel .owl-wrapper-outer{
overflow: hidden;
position: relative;
width: 100%;
}
.owl-carousel .owl-wrapper-outer.autoHeight{
-webkit-transition: height 500ms ease-in-out;
-moz-transition: height 500ms ease-in-out;
-ms-transition: height 500ms ease-in-out;
-o-transition: height 500ms ease-in-out;
transition: height 500ms ease-in-out;
}
.owl-carousel .owl-item{
float: left;
}
.owl-controls .owl-page,
.owl-controls .owl-buttons div{
cursor: pointer;
}
.owl-controls {
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
/* mouse grab icon */
.grabbing {
cursor:url(grabbing.png) 8 8, move;
}
/* fix */
.owl-carousel .owl-wrapper,
.owl-carousel .owl-item{
-webkit-backface-visibility: hidden;
-moz-backface-visibility: hidden;
-ms-backface-visibility: hidden;
-webkit-transform: translate3d(0,0,0);
-moz-transform: translate3d(0,0,0);
-ms-transform: translate3d(0,0,0);
}
+80
View File
@@ -0,0 +1,80 @@
<?xml version="1.0"?>
<catalog>
<book id="bk101">
<author>Gambardella, Matthew</author>
<title>XML Developer's Guide</title>
<genre>Computer</genre>
<price>44.95</price>
<publish_date>2000-10-01</publish_date>
<description>An in-depth look at creating applications
with XML.</description>
</book>
<book id="bk102">
<author>Ralls, Kim</author>
<title>Midnight Rain</title>
<genre>Fantasy</genre>
<price>5.95</price>
<publish_date>2000-12-16</publish_date>
<description>A former architect battles corporate zombies,
an evil sorceress, and her own childhood to become queen
of the world.</description>
</book>
<book id="bk103">
<author>Corets, Eva</author>
<title>Maeve Ascendant</title>
<genre>Fantasy</genre>
<price>5.95</price>
<publish_date>2000-11-17</publish_date>
<description>After the collapse of a nanotechnology
society in England, the young survivors lay the
foundation for a new society.</description>
</book>
<book id="bk104">
<author>Corets, Eva</author>
<title>Oberon's Legacy</title>
<genre>Fantasy</genre>
<price>5.95</price>
<publish_date>2001-03-10</publish_date>
<description>In post-apocalypse England, the mysterious
agent known only as Oberon helps to create a new life
for the inhabitants of London. Sequel to Maeve
Ascendant.</description>
</book>
<book id="bk105">
<author>Corets, Eva</author>
<title>The Sundered Grail</title>
<genre>Fantasy</genre>
<price>5.95</price>
<publish_date>2001-09-10</publish_date>
<description>The two daughters of Maeve, half-sisters,
battle one another for control of England. Sequel to
Oberon's Legacy.</description>
</book>
<book id="bk106">
<author>Randall, Cynthia</author>
<title>Lover Birds</title>
<genre>Romance</genre>
<price>4.95</price>
<publish_date>2000-09-02</publish_date>
<description>When Carla meets Paul at an ornithology
conference, tempers fly as feathers get ruffled.</description>
</book>
<book id="bk107">
<author>Thurman, Paula</author>
<title>Splish Splash</title>
<genre>Romance</genre>
<price>4.95</price>
<publish_date>2000-11-02</publish_date>
<description>A deep sea diver finds true love twenty
thousand leagues beneath the sea.</description>
</book>
<book id="bk108">
<author>Knorr, Stefan</author>
<title>Creepy Crawlies</title>
<genre>Horror</genre>
<price>4.95</price>
<publish_date>2000-12-06</publish_date>
<description>An anthology of horror stories about roaches,
centipedes, scorpions and other insects.</description>
</book>
</catalog>